Implement explicit CEK machine, continuations, effect signatures, fix dynamic-wind and inspect shadowing
Three-phase foundations implementation:
Phase A — Activate dormant shift/reset continuations with 24 SX-native tests
covering basic semantics, predicates, stored continuations, nested reset,
scope interaction, and TCO.
Phase B — Bridge compile-time effect system to runtime: boundary_parser extracts
46 effect annotations, platform provides populate_effect_annotations() and
check_component_effects() for static analysis. 6 new type tests.
Phase C — Explicit CEK machine (frames.sx + cek.sx): evaluation state as data
({control, env, kont, phase, value}), 21 frame types, two-phase step function
(step-eval/step-continue), native shift/reset via frame capture. Bootstrapper
integration: --spec-modules cek transpiles to Python with iterative cek_run.
43 interpreted + 49 transpiled tests passing.
Bug fixes:
- inspect() shadowed by `import inspect` in PLATFORM_ASYNC_PY — renamed to
`import inspect as _inspect`
- dynamic-wind missing platform functions (call_thunk, push_wind!, pop_wind!) —
added with try/finally error safety via dynamic_wind_call
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1015,6 +1015,37 @@ def for_each_indexed(fn, coll):
|
||||
def map_dict(fn, d):
|
||||
return {k: fn(k, v) for k, v in d.items()}
|
||||
|
||||
# Dynamic wind support (used by sf-dynamic-wind in eval.sx)
|
||||
_wind_stack = []
|
||||
|
||||
def push_wind_b(before, after):
|
||||
_wind_stack.append((before, after))
|
||||
return NIL
|
||||
|
||||
def pop_wind_b():
|
||||
if _wind_stack:
|
||||
_wind_stack.pop()
|
||||
return NIL
|
||||
|
||||
def call_thunk(f, env):
|
||||
"""Call a zero-arg function/lambda."""
|
||||
if is_callable(f) and not is_lambda(f):
|
||||
return f()
|
||||
if is_lambda(f):
|
||||
return trampoline(call_lambda(f, [], env))
|
||||
return trampoline(eval_expr([f], env))
|
||||
|
||||
def dynamic_wind_call(before, body, after, env):
|
||||
"""Execute dynamic-wind with try/finally for error safety."""
|
||||
call_thunk(before, env)
|
||||
push_wind_b(before, after)
|
||||
try:
|
||||
result = call_thunk(body, env)
|
||||
finally:
|
||||
pop_wind_b()
|
||||
call_thunk(after, env)
|
||||
return result
|
||||
|
||||
# Aliases used directly by transpiled code
|
||||
first = PRIMITIVES["first"]
|
||||
last = PRIMITIVES["last"]
|
||||
@@ -1103,7 +1134,7 @@ def component_set_io_refs(c, refs):
|
||||
# =========================================================================
|
||||
|
||||
import contextvars
|
||||
import inspect
|
||||
import inspect as _inspect
|
||||
|
||||
from shared.sx.primitives_io import (
|
||||
IO_PRIMITIVES, RequestContext, execute_io,
|
||||
@@ -1196,7 +1227,7 @@ def sx_parse(src):
|
||||
|
||||
|
||||
def is_async_coroutine(x):
|
||||
return inspect.iscoroutine(x)
|
||||
return _inspect.iscoroutine(x)
|
||||
|
||||
|
||||
async def async_await(x):
|
||||
@@ -1890,12 +1921,7 @@ def sf_dynamic_wind(args, env):
|
||||
before = trampoline(eval_expr(first(args), env))
|
||||
body = trampoline(eval_expr(nth(args, 1), env))
|
||||
after = trampoline(eval_expr(nth(args, 2), env))
|
||||
call_thunk(before, env)
|
||||
push_wind_b(before, after)
|
||||
result = call_thunk(body, env)
|
||||
pop_wind_b()
|
||||
call_thunk(after, env)
|
||||
return result
|
||||
return dynamic_wind_call(before, body, after, env)
|
||||
|
||||
# sf-scope
|
||||
def sf_scope(args, env):
|
||||
@@ -4634,3 +4660,65 @@ def render(expr, env=None):
|
||||
def make_env(**kwargs):
|
||||
"""Create an environment with initial bindings."""
|
||||
return _Env(dict(kwargs))
|
||||
|
||||
|
||||
def populate_effect_annotations(env, effect_map=None):
|
||||
"""Populate *effect-annotations* in env from boundary declarations.
|
||||
|
||||
If effect_map is provided, use it directly (dict of name -> effects list).
|
||||
Otherwise, parse boundary.sx via boundary_parser.
|
||||
"""
|
||||
if effect_map is None:
|
||||
from shared.sx.ref.boundary_parser import parse_boundary_effects
|
||||
effect_map = parse_boundary_effects()
|
||||
anns = env.get("*effect-annotations*", {})
|
||||
if not isinstance(anns, dict):
|
||||
anns = {}
|
||||
anns.update(effect_map)
|
||||
env["*effect-annotations*"] = anns
|
||||
return anns
|
||||
|
||||
|
||||
def check_component_effects(env, comp_name=None):
|
||||
"""Check effect violations for components in env.
|
||||
|
||||
If comp_name is given, check only that component.
|
||||
Returns list of diagnostic dicts (warnings, not errors).
|
||||
"""
|
||||
anns = env.get("*effect-annotations*")
|
||||
if not anns:
|
||||
return []
|
||||
diagnostics = []
|
||||
names = [comp_name] if comp_name else [k for k in env if isinstance(k, str) and k.startswith("~")]
|
||||
for name in names:
|
||||
val = env.get(name)
|
||||
if val is not None and type_of(val) == "component":
|
||||
comp_effects = anns.get(name)
|
||||
if comp_effects is None:
|
||||
continue # unannotated — skip
|
||||
body = val.body if hasattr(val, "body") else None
|
||||
if body is None:
|
||||
continue
|
||||
_walk_effects(body, name, comp_effects, anns, diagnostics)
|
||||
return diagnostics
|
||||
|
||||
|
||||
def _walk_effects(node, comp_name, caller_effects, anns, diagnostics):
|
||||
"""Walk AST node and check effect calls."""
|
||||
if not isinstance(node, list) or not node:
|
||||
return
|
||||
head = node[0]
|
||||
if isinstance(head, Symbol):
|
||||
callee = head.name
|
||||
callee_effects = anns.get(callee)
|
||||
if callee_effects is not None and caller_effects is not None:
|
||||
for e in callee_effects:
|
||||
if e not in caller_effects:
|
||||
diagnostics.append({
|
||||
"level": "warning",
|
||||
"message": f"`{callee}` has effects {callee_effects} but `{comp_name}` only allows {caller_effects or '[pure]'}",
|
||||
"component": comp_name,
|
||||
})
|
||||
break
|
||||
for child in node[1:]:
|
||||
_walk_effects(child, comp_name, caller_effects, anns, diagnostics)
|
||||
|
||||
Reference in New Issue
Block a user