Add Scheme forms: named let, letrec, dynamic-wind, three-tier equality

Spec (eval.sx, primitives.sx):
- Named let: (let loop ((i 0)) body) — self-recursive lambda with TCO
- letrec: mutually recursive local bindings with closure patching
- dynamic-wind: entry/exit guards with wind stack for future continuations
- eq?/eqv?/equal?: identity, atom-value, and deep structural equality

Implementation (evaluator.py, async_eval.py, primitives.py):
- Both sync and async evaluators implement all four forms
- 33 new tests covering all forms including TCO at 10k depth

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 01:11:31 +00:00
parent 9cde15c3ce
commit f34e55aa9b
7 changed files with 785 additions and 26 deletions

View File

@@ -306,6 +306,11 @@ def _sf_or(expr: list, env: dict) -> Any:
def _sf_let(expr: list, env: dict) -> Any:
if len(expr) < 3:
raise EvalError("let requires bindings and body")
# Named let: (let name ((x 0) ...) body)
if isinstance(expr[1], Symbol):
return _sf_named_let(expr, env)
bindings = expr[1]
local = dict(env)
@@ -336,6 +341,127 @@ def _sf_let(expr: list, env: dict) -> Any:
return _Thunk(body[-1], local)
def _sf_named_let(expr: list, env: dict) -> Any:
"""``(let name ((x 0) (y 1)) body...)`` — self-recursive loop.
Desugars to a lambda bound to *name* whose closure includes itself,
called with the initial values. Tail calls to *name* produce TCO thunks.
"""
loop_name = expr[1].name
bindings = expr[2]
body = expr[3:]
params: list[str] = []
inits: list[Any] = []
if isinstance(bindings, list):
if bindings and isinstance(bindings[0], list):
for binding in bindings:
var = binding[0]
params.append(var.name if isinstance(var, Symbol) else var)
inits.append(binding[1])
elif len(bindings) % 2 == 0:
for i in range(0, len(bindings), 2):
var = bindings[i]
params.append(var.name if isinstance(var, Symbol) else var)
inits.append(bindings[i + 1])
# Build loop body (wrap in begin if multiple expressions)
loop_body = body[0] if len(body) == 1 else [Symbol("begin")] + list(body)
# Create self-recursive lambda
loop_fn = Lambda(params, loop_body, dict(env), name=loop_name)
loop_fn.closure[loop_name] = loop_fn
# Evaluate initial values in enclosing env, then call
init_vals = [_trampoline(_eval(init, env)) for init in inits]
return _call_lambda(loop_fn, init_vals, env)
def _sf_letrec(expr: list, env: dict) -> Any:
"""``(letrec ((name1 val1) ...) body)`` — mutually recursive bindings.
All names are bound to NIL first, then values are evaluated (so they
can reference each other), then lambda closures are patched.
"""
if len(expr) < 3:
raise EvalError("letrec requires bindings and body")
bindings = expr[1]
local = dict(env)
names: list[str] = []
val_exprs: list[Any] = []
if isinstance(bindings, list):
if bindings and isinstance(bindings[0], list):
for binding in bindings:
var = binding[0]
vname = var.name if isinstance(var, Symbol) else var
names.append(vname)
val_exprs.append(binding[1])
local[vname] = NIL
elif len(bindings) % 2 == 0:
for i in range(0, len(bindings), 2):
var = bindings[i]
vname = var.name if isinstance(var, Symbol) else var
names.append(vname)
val_exprs.append(bindings[i + 1])
local[vname] = NIL
# Evaluate all values — they can see each other's names (initially NIL)
values = [_trampoline(_eval(ve, local)) for ve in val_exprs]
# Bind final values
for name, val in zip(names, values):
local[name] = val
# Patch lambda closures so they see the final bindings
for val in values:
if isinstance(val, Lambda):
for name in names:
val.closure[name] = local[name]
body = expr[2:]
for body_expr in body[:-1]:
_trampoline(_eval(body_expr, local))
return _Thunk(body[-1], local)
def _sf_dynamic_wind(expr: list, env: dict) -> Any:
"""``(dynamic-wind before body after)`` — entry/exit guards.
All three arguments are thunks (zero-arg functions).
*before* is called on entry, *after* is always called on exit (even on
error). The wind stack is maintained for future continuation support.
"""
if len(expr) != 4:
raise EvalError("dynamic-wind requires 3 arguments (before, body, after)")
before = _trampoline(_eval(expr[1], env))
body_fn = _trampoline(_eval(expr[2], env))
after = _trampoline(_eval(expr[3], env))
def _call_thunk(fn: Any) -> Any:
if isinstance(fn, Lambda):
return _trampoline(_call_lambda(fn, [], env))
if callable(fn):
return fn()
raise EvalError(f"dynamic-wind: expected thunk, got {type(fn).__name__}")
# Entry
_call_thunk(before)
_WIND_STACK.append((before, after))
try:
result = _call_thunk(body_fn)
finally:
_WIND_STACK.pop()
_call_thunk(after)
return result
# Wind stack for dynamic-wind (thread-safe enough for sync evaluator)
_WIND_STACK: list[tuple] = []
def _sf_lambda(expr: list, env: dict) -> Lambda:
if len(expr) < 3:
raise EvalError("lambda requires params and body")
@@ -883,6 +1009,7 @@ _SPECIAL_FORMS: dict[str, Any] = {
"or": _sf_or,
"let": _sf_let,
"let*": _sf_let,
"letrec": _sf_letrec,
"lambda": _sf_lambda,
"fn": _sf_lambda,
"define": _sf_define,
@@ -895,6 +1022,7 @@ _SPECIAL_FORMS: dict[str, Any] = {
"quote": _sf_quote,
"->": _sf_thread_first,
"set!": _sf_set_bang,
"dynamic-wind": _sf_dynamic_wind,
"defmacro": _sf_defmacro,
"quasiquote": _sf_quasiquote,
"defhandler": _sf_defhandler,