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:
2026-03-13 22:14:55 +00:00
parent 11fdd1a840
commit 1765216335
16 changed files with 2174 additions and 26 deletions

View File

@@ -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)