Phase 1: CEK is now the sole evaluator on JavaScript

- Override evalExpr/trampoline in CEK_FIXUPS_JS to route through
  cekRun (matching what Python already does)
- Always include frames+cek in JS builds (not just when DOM present)
- Remove CONTINUATIONS_JS extension (CEK handles shift/reset natively)
- Remove Continuation constructor guard (always define it)
- Add strict-mode type checking to CEK call path via head-name
  propagation through ArgFrame

Standard build: 746/747 passing (1 dotimes macro edge case)
Full build: 858/870 passing (6 continuation edge cases, 5 deftype
issues, 1 dotimes — all pre-existing CEK behavioral differences)

The tree-walk eval-expr, eval-list, eval-call, and all sf-*/ho-*
forms in eval.sx are now dead code — never reached at runtime.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 12:49:08 +00:00
parent ebb3445667
commit 293af75821
7 changed files with 94 additions and 56 deletions

View File

@@ -112,10 +112,9 @@ def compile_ref_to_js(
spec_mod_set.add("deps")
if "page-helpers" in SPEC_MODULES:
spec_mod_set.add("page-helpers")
# CEK needed for reactive rendering (deref-as-shift)
if "dom" in adapter_set:
spec_mod_set.add("cek")
spec_mod_set.add("frames")
# CEK is the canonical evaluator — always included
spec_mod_set.add("cek")
spec_mod_set.add("frames")
# cek module requires frames
if "cek" in spec_mod_set:
spec_mod_set.add("frames")
@@ -238,8 +237,11 @@ def compile_ref_to_js(
for name in ("dom", "engine", "orchestration", "boot"):
if name in adapter_set and name in adapter_platform:
parts.append(adapter_platform[name])
if has_continuations:
parts.append(CONTINUATIONS_JS)
# CONTINUATIONS_JS is the tree-walk shift/reset extension.
# With CEK as sole evaluator, continuations are handled natively by
# cek.sx (step-sf-reset, step-sf-shift). Skip the tree-walk extension.
# if has_continuations:
# parts.append(CONTINUATIONS_JS)
if has_dom:
parts.append(ASYNC_IO_JS)
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps, has_router, has_signals, has_page_helpers, has_cek))

View File

@@ -1504,18 +1504,15 @@ PLATFORM_CEK_JS = '''
// Platform: CEK module — explicit CEK machine
// =========================================================================
// Continuation type (needed by CEK even without the tree-walk shift/reset extension)
// Continuations must be callable as JS functions so isCallable/apply work.
if (typeof Continuation === "undefined") {
function Continuation(fn) {
var c = function(value) { return fn(value !== undefined ? value : NIL); };
c.fn = fn;
c._continuation = true;
c.call = function(value) { return fn(value !== undefined ? value : NIL); };
return c;
}
// Continuation type — callable as JS function so isCallable/apply work.
// CEK is the canonical evaluator; continuations are always available.
function Continuation(fn) {
var c = function(value) { return fn(value !== undefined ? value : NIL); };
c.fn = fn;
c._continuation = true;
c.call = function(value) { return fn(value !== undefined ? value : NIL); };
return c;
}
// Always register the predicate (may be overridden by continuations extension)
PRIMITIVES["continuation?"] = function(x) { return x != null && x._continuation === true; };
// Standalone aliases for primitives used by cek.sx / frames.sx
@@ -1543,6 +1540,20 @@ CEK_FIXUPS_JS = '''
return cekValue(state);
};
// CEK is the canonical evaluator — override evalExpr to use it.
// The tree-walk evaluator (evalExpr from eval.sx) is superseded.
var _treeWalkEvalExpr = evalExpr;
evalExpr = function(expr, env) {
return cekRun(makeCekState(expr, env, []));
};
// CEK never produces thunks — trampoline resolves any legacy thunks
var _treeWalkTrampoline = trampoline;
trampoline = function(val) {
if (isThunk(val)) return evalExpr(thunkExpr(val), thunkEnv(val));
return val;
};
// Platform functions — defined in platform_js.py, not in .sx spec files.
// Spec defines self-register via js-emit-define; these are the platform interface.
PRIMITIVES["type-of"] = typeOf;

View File

@@ -14,7 +14,7 @@
// =========================================================================
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
var SX_VERSION = "2026-03-15T12:23:29Z";
var SX_VERSION = "2026-03-15T12:48:42Z";
function isNil(x) { return x === NIL || x === null || x === undefined; }
function isSxTruthy(x) { return x !== false && !isNil(x); }
@@ -775,18 +775,15 @@
// Platform: CEK module — explicit CEK machine
// =========================================================================
// Continuation type (needed by CEK even without the tree-walk shift/reset extension)
// Continuations must be callable as JS functions so isCallable/apply work.
if (typeof Continuation === "undefined") {
function Continuation(fn) {
var c = function(value) { return fn(value !== undefined ? value : NIL); };
c.fn = fn;
c._continuation = true;
c.call = function(value) { return fn(value !== undefined ? value : NIL); };
return c;
}
// Continuation type — callable as JS function so isCallable/apply work.
// CEK is the canonical evaluator; continuations are always available.
function Continuation(fn) {
var c = function(value) { return fn(value !== undefined ? value : NIL); };
c.fn = fn;
c._continuation = true;
c.call = function(value) { return fn(value !== undefined ? value : NIL); };
return c;
}
// Always register the predicate (may be overridden by continuations extension)
PRIMITIVES["continuation?"] = function(x) { return x != null && x._continuation === true; };
// Standalone aliases for primitives used by cek.sx / frames.sx
@@ -4275,7 +4272,7 @@ PRIMITIVES["make-define-frame"] = makeDefineFrame;
PRIMITIVES["make-set-frame"] = makeSetFrame;
// make-arg-frame
var makeArgFrame = function(f, evaled, remaining, env, rawArgs) { return {"type": "arg", "f": f, "evaled": evaled, "remaining": remaining, "env": env, "raw-args": rawArgs}; };
var makeArgFrame = function(f, evaled, remaining, env, rawArgs, headName) { return {"type": "arg", "f": f, "evaled": evaled, "remaining": remaining, "env": env, "raw-args": rawArgs, "head-name": sxOr(headName, NIL)}; };
PRIMITIVES["make-arg-frame"] = makeArgFrame;
// make-call-frame
@@ -5057,7 +5054,10 @@ return forEach(function(d) { return cekCall(d, NIL); }, subDisposers); });
PRIMITIVES["reactive-shift-deref"] = reactiveShiftDeref;
// step-eval-call
var stepEvalCall = function(head, args, env, kont) { return makeCekState(head, env, kontPush(makeArgFrame(NIL, [], args, env, args), kont)); };
var stepEvalCall = function(head, args, env, kont) { return (function() {
var hname = (isSxTruthy((typeOf(head) == "symbol")) ? symbolName(head) : NIL);
return makeCekState(head, env, kontPush(makeArgFrame(NIL, [], args, env, args, hname), kont));
})(); };
PRIMITIVES["step-eval-call"] = stepEvalCall;
// step-ho-map
@@ -5222,9 +5222,10 @@ PRIMITIVES["step-ho-for-each"] = stepHoForEach;
var remaining = get(frame, "remaining");
var fenv = get(frame, "env");
var rawArgs = get(frame, "raw-args");
return (isSxTruthy(isNil(f)) ? (isSxTruthy(isEmpty(remaining)) ? continueWithCall(value, [], fenv, rawArgs, restK) : makeCekState(first(remaining), fenv, kontPush(makeArgFrame(value, [], rest(remaining), fenv, rawArgs), restK))) : (function() {
var hname = get(frame, "head-name");
return (isSxTruthy(isNil(f)) ? ((isSxTruthy((isSxTruthy(_strict_) && hname)) ? strictCheckArgs(hname, []) : NIL), (isSxTruthy(isEmpty(remaining)) ? continueWithCall(value, [], fenv, rawArgs, restK) : makeCekState(first(remaining), fenv, kontPush(makeArgFrame(value, [], rest(remaining), fenv, rawArgs, hname), restK)))) : (function() {
var newEvaled = append(evaled, [value]);
return (isSxTruthy(isEmpty(remaining)) ? continueWithCall(f, newEvaled, fenv, rawArgs, restK) : makeCekState(first(remaining), fenv, kontPush(makeArgFrame(f, newEvaled, rest(remaining), fenv, rawArgs), restK)));
return (isSxTruthy(isEmpty(remaining)) ? ((isSxTruthy((isSxTruthy(_strict_) && hname)) ? strictCheckArgs(hname, newEvaled) : NIL), continueWithCall(f, newEvaled, fenv, rawArgs, restK)) : makeCekState(first(remaining), fenv, kontPush(makeArgFrame(f, newEvaled, rest(remaining), fenv, rawArgs, hname), restK)));
})());
})() : (isSxTruthy((ft == "dict")) ? (function() {
var remaining = get(frame, "remaining");
@@ -5848,6 +5849,20 @@ PRIMITIVES["resource"] = resource;
return cekValue(state);
};
// CEK is the canonical evaluator — override evalExpr to use it.
// The tree-walk evaluator (evalExpr from eval.sx) is superseded.
var _treeWalkEvalExpr = evalExpr;
evalExpr = function(expr, env) {
return cekRun(makeCekState(expr, env, []));
};
// CEK never produces thunks — trampoline resolves any legacy thunks
var _treeWalkTrampoline = trampoline;
trampoline = function(val) {
if (isThunk(val)) return evalExpr(thunkExpr(val), thunkEnv(val));
return val;
};
// Platform functions — defined in platform_js.py, not in .sx spec files.
// Spec defines self-register via js-emit-define; these are the platform interface.
PRIMITIVES["type-of"] = typeOf;

File diff suppressed because one or more lines are too long

View File

@@ -20,7 +20,7 @@
:class "hidden md:flex md:flex-col max-w-xs md:h-full md:min-h-0 mr-3"
(when aside aside))
(section :id "main-panel"
:class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"
:class "flex-1 md:h-full md:min-h-0 md:overflow-y-auto md:overscroll-contain js-grid-viewport"
(when content content)
(div :class "pb-8")))))))
@@ -35,7 +35,7 @@
(div :id "root-menu" :sx-swap-oob "outerHTML" :class "md:hidden"
(when menu menu))
(section :id "main-panel"
:class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"
:class "flex-1 md:h-full md:min-h-0 md:overflow-y-auto md:overscroll-contain js-grid-viewport"
(when content content))))
(defcomp ~shared:layout/hamburger ()

View File

@@ -466,11 +466,13 @@
(define step-eval-call
(fn (head args env kont)
;; First evaluate the head, then evaluate args left-to-right
(make-cek-state
head env
(kont-push
(make-arg-frame nil (list) args env args)
kont))))
;; Preserve head name for strict mode type checking
(let ((hname (if (= (type-of head) "symbol") (symbol-name head) nil)))
(make-cek-state
head env
(kont-push
(make-arg-frame nil (list) args env args hname)
kont)))))
;; --------------------------------------------------------------------------
@@ -760,28 +762,36 @@
(evaled (get frame "evaled"))
(remaining (get frame "remaining"))
(fenv (get frame "env"))
(raw-args (get frame "raw-args")))
(raw-args (get frame "raw-args"))
(hname (get frame "head-name")))
(if (nil? f)
;; Head just evaluated — value is the function
(if (empty? remaining)
;; No args — call immediately
(continue-with-call value (list) fenv raw-args rest-k)
;; Start evaluating args
(make-cek-state
(first remaining) fenv
(kont-push
(make-arg-frame value (list) (rest remaining) fenv raw-args)
rest-k)))
(do
;; Strict mode: check arg types for named primitives
(when (and *strict* hname)
(strict-check-args hname (list)))
(if (empty? remaining)
;; No args — call immediately
(continue-with-call value (list) fenv raw-args rest-k)
;; Start evaluating args
(make-cek-state
(first remaining) fenv
(kont-push
(make-arg-frame value (list) (rest remaining) fenv raw-args hname)
rest-k))))
;; An arg was evaluated — accumulate
(let ((new-evaled (append evaled (list value))))
(if (empty? remaining)
;; All args evaluated — call
(continue-with-call f new-evaled fenv raw-args rest-k)
;; All args evaluated — strict check then call
(do
(when (and *strict* hname)
(strict-check-args hname new-evaled))
(continue-with-call f new-evaled fenv raw-args rest-k))
;; Next arg
(make-cek-state
(first remaining) fenv
(kont-push
(make-arg-frame f new-evaled (rest remaining) fenv raw-args)
(make-arg-frame f new-evaled (rest remaining) fenv raw-args hname)
rest-k))))))
;; --- DictFrame: value evaluated ---

View File

@@ -88,9 +88,9 @@
;; f = function value (already evaluated), evaled = already evaluated args
;; remaining = remaining arg expressions
(define make-arg-frame
(fn (f evaled remaining env raw-args)
(fn (f evaled remaining env raw-args head-name)
{:type "arg" :f f :evaled evaled :remaining remaining :env env
:raw-args raw-args}))
:raw-args raw-args :head-name (or head-name nil)}))
;; CallFrame: about to call with fully evaluated args
(define make-call-frame