Unify CEK callable dispatch, add named-let transpiler, full stdlib

Three changes that together enable the full 46-function stdlib migration:

1. CEK callable unification (spec/evaluator.sx):
   cek-call now routes both native callables and SX lambdas through
   continue-with-call, so replacing a native function with an SX lambda
   doesn't change shift/reset behavior.

2. Named-let transpiler support (hosts/javascript/transpiler.sx):
   (let loop ((i 0)) body...) now transpiles to a named IIFE:
   (function loop(i) { body })(0)
   This was the cause of the 3 test regressions (produced [object Object]).

3. Full stdlib via runtime eval (hosts/javascript/bootstrap.py):
   stdlib.sx is eval'd at runtime (not transpiled) so its defines go
   into PRIMITIVES without shadowing module-scope variables that the
   transpiled evaluator uses directly.

stdlib.sx now contains all 46 library functions:
  Logic: not
  Comparison: != <= >= eq? eqv? equal?
  Predicates: boolean? number? string? list? dict? continuation?
    zero? odd? even? empty?
  Arithmetic: inc dec abs ceil round min max clamp
  Collections: first last rest nth cons append reverse flatten
    range chunk-every zip-pairs
  Dict: vals has-key? assoc dissoc into
  Strings: upcase downcase string-length substring string-contains?
    starts-with? ends-with? split join replace contains?
  Text: pluralize escape parse-datetime assert

All hosts: JS 957+1080, Python 744, OCaml 952 — zero regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-17 17:11:09 +00:00
parent 4c54843542
commit f9e65e1d17
6 changed files with 324 additions and 92 deletions

View File

@@ -131,7 +131,8 @@ def compile_ref_to_js(
# evaluator.sx = merged frames + eval utilities + CEK machine
sx_files = [
("evaluator.sx", "evaluator (frames + eval + CEK)"),
("stdlib.sx", "stdlib (library functions from former primitives)"),
# stdlib.sx is loaded at runtime via eval, not transpiled —
# transpiling it would shadow native PRIMITIVES in module scope.
("freeze.sx", "freeze (serializable state boundaries)"),
("content.sx", "content (content-addressed computation)"),
("render.sx", "render (core)"),
@@ -233,6 +234,28 @@ def compile_ref_to_js(
if has_cek:
parts.append(CEK_FIXUPS_JS)
# Load stdlib.sx via eval (NOT transpiled) so defines go into the eval
# env, not the module scope. This prevents stdlib functions from
# shadowing native PRIMITIVES aliases used by transpiled evaluator code.
stdlib_path = _find_sx("stdlib.sx")
if stdlib_path:
with open(stdlib_path) as f:
stdlib_src = f.read()
# Escape for JS string literal
stdlib_escaped = stdlib_src.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
parts.append(f'\n // === stdlib.sx (eval\'d at runtime, not transpiled) ===')
parts.append(f' (function() {{')
parts.append(f' var src = "{stdlib_escaped}";')
parts.append(f' var forms = sxParse(src);')
parts.append(f' var tmpEnv = merge({{}}, PRIMITIVES);')
parts.append(f' for (var i = 0; i < forms.length; i++) {{')
parts.append(f' trampoline(evalExpr(forms[i], tmpEnv));')
parts.append(f' }}')
parts.append(f' for (var k in tmpEnv) {{')
parts.append(f' if (!PRIMITIVES[k]) PRIMITIVES[k] = tmpEnv[k];')
parts.append(f' }}')
parts.append(f' }})();\n')
for name in ("dom", "engine", "orchestration", "boot"):
if name in adapter_set and name in adapter_platform:
parts.append(adapter_platform[name])