Fix all 9 spec test failures: Env scope chain, IO detection, offline mutation
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m11s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m11s
- env.py: Add MergedEnv with dual-parent lookup (primary for set!, secondary for reads), add dict-compat methods to Env - platform_py.py: make_lambda stores env reference (no copy), env_merge uses MergedEnv for proper set! propagation, ancestor detection prevents unbounded chains in TCO recursion, sf_set_bang walks scope chain - types.py: Component/Island io_refs defaults to None (not computed) instead of empty set, so component-pure? falls through to scan - run.py: Test env uses Env class, mock execute-action calls SX lambdas via _call_sx instead of direct Python call Spec tests: 320/320 (was 311/320) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,7 @@ from shared.sx.types import (
|
||||
HandlerDef, QueryDef, ActionDef, PageDef, _ShiftSignal,
|
||||
)
|
||||
from shared.sx.parser import SxExpr
|
||||
from shared.sx.env import Env as _Env, MergedEnv as _MergedEnv
|
||||
'''
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -195,8 +196,15 @@ def make_keyword(n):
|
||||
return Keyword(n)
|
||||
|
||||
|
||||
def _ensure_env(env):
|
||||
"""Wrap plain dict in Env if needed."""
|
||||
if isinstance(env, _Env):
|
||||
return env
|
||||
return _Env(env if isinstance(env, dict) else {})
|
||||
|
||||
|
||||
def make_lambda(params, body, env):
|
||||
return Lambda(params=list(params), body=body, closure=dict(env))
|
||||
return Lambda(params=list(params), body=body, closure=_ensure_env(env))
|
||||
|
||||
|
||||
def make_component(name, params, has_children, body, env, affinity="auto"):
|
||||
@@ -490,13 +498,26 @@ def env_set(env, name, val):
|
||||
|
||||
|
||||
def env_extend(env):
|
||||
return dict(env)
|
||||
return _ensure_env(env).extend()
|
||||
|
||||
|
||||
def env_merge(base, overlay):
|
||||
result = dict(base)
|
||||
result.update(overlay)
|
||||
return result
|
||||
base = _ensure_env(base)
|
||||
overlay = _ensure_env(overlay)
|
||||
if base is overlay:
|
||||
# Same env — just extend with empty local scope for params
|
||||
return base.extend()
|
||||
# Check if base is an ancestor of overlay — if so, no need to merge
|
||||
# (common for self-recursive calls where closure == caller's ancestor)
|
||||
p = overlay
|
||||
depth = 0
|
||||
while p is not None and depth < 100:
|
||||
if p is base:
|
||||
return base.extend()
|
||||
p = getattr(p, '_parent', None)
|
||||
depth += 1
|
||||
# MergedEnv: reads walk base then overlay; set! walks base only
|
||||
return _MergedEnv({}, primary=base, secondary=overlay)
|
||||
|
||||
|
||||
def dict_set(d, k, v):
|
||||
@@ -1022,8 +1043,10 @@ PLATFORM_DEPS_PY = (
|
||||
' return list(classes)\n'
|
||||
'\n'
|
||||
'def component_io_refs(c):\n'
|
||||
' """Return cached IO refs list for a component (may be empty)."""\n'
|
||||
' return list(c.io_refs) if hasattr(c, "io_refs") and c.io_refs else []\n'
|
||||
' """Return cached IO refs list, or NIL if not yet computed."""\n'
|
||||
' if not hasattr(c, "io_refs") or c.io_refs is None:\n'
|
||||
' return NIL\n'
|
||||
' return list(c.io_refs)\n'
|
||||
'\n'
|
||||
'def component_set_io_refs(c, refs):\n'
|
||||
' """Cache IO refs on a component."""\n'
|
||||
@@ -1255,6 +1278,20 @@ def _wrap_aser_outputs():
|
||||
return SxExpr(result) if isinstance(result, str) else result
|
||||
aser_call = _aser_call_wrapped
|
||||
aser_fragment = _aser_fragment_wrapped
|
||||
|
||||
|
||||
# Override sf_set_bang to walk the Env scope chain so that (set! var val)
|
||||
# updates the variable in its defining scope, not just the local copy.
|
||||
def sf_set_bang(args, env):
|
||||
name = symbol_name(first(args))
|
||||
value = trampoline(eval_expr(nth(args, 1), env))
|
||||
env = _ensure_env(env)
|
||||
try:
|
||||
env.set(name, value)
|
||||
except KeyError:
|
||||
# Not found in chain — define locally (matches prior behavior)
|
||||
env[name] = value
|
||||
return value
|
||||
'''
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1349,7 +1386,9 @@ def public_api_py(has_html: bool, has_sx: bool, has_deps: bool = False) -> str:
|
||||
'def evaluate(expr, env=None):',
|
||||
' """Evaluate expr in env and return the result."""',
|
||||
' if env is None:',
|
||||
' env = {}',
|
||||
' env = _Env()',
|
||||
' elif isinstance(env, dict):',
|
||||
' env = _Env(env)',
|
||||
' result = eval_expr(expr, env)',
|
||||
' while is_thunk(result):',
|
||||
' result = eval_expr(thunk_expr(result), thunk_env(result))',
|
||||
@@ -1369,8 +1408,8 @@ def public_api_py(has_html: bool, has_sx: bool, has_deps: bool = False) -> str:
|
||||
'',
|
||||
'',
|
||||
'def make_env(**kwargs):',
|
||||
' """Create an environment dict with initial bindings."""',
|
||||
' return dict(kwargs)',
|
||||
' """Create an environment with initial bindings."""',
|
||||
' return _Env(dict(kwargs))',
|
||||
])
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user