Remove invoke from platform interfaces, add cek-call integration tests
- platform_js.py: remove invoke function definition and PRIMITIVES registration, switch domListen handler wrapping to cek-call - platform_py.py: remove invoke function definition - run_signal_tests.py: remove invoke patch, use cek_call in batch wrapper - run_cek_reactive_tests.py: remove invoke, fix primitive lookup to use two-level is_primitive/get_primitive, increase recursion limit for interpreted CEK evaluation - test-cek-reactive.sx: add 7 new tests covering cek-call dispatch with computed, effect, cleanup, batch coalescing All 79 tests pass (20 signal + 43 CEK + 16 CEK reactive). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||||
var SX_VERSION = "2026-03-14T10:16:03Z";
|
var SX_VERSION = "2026-03-14T10:27:39Z";
|
||||||
|
|
||||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||||
@@ -218,17 +218,6 @@
|
|||||||
return new Island(name, params, hasChildren, body, merge(env));
|
return new Island(name, params, hasChildren, body, merge(env));
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoke — call any callable (native fn or SX lambda) with args.
|
|
||||||
// Transpiled code emits direct calls f(args) which fail on SX lambdas
|
|
||||||
// from runtime-evaluated island bodies. invoke dispatches correctly.
|
|
||||||
function invoke() {
|
|
||||||
var f = arguments[0];
|
|
||||||
var args = Array.prototype.slice.call(arguments, 1);
|
|
||||||
if (isLambda(f)) return trampoline(callLambda(f, args, lambdaClosure(f)));
|
|
||||||
if (typeof f === 'function') return f.apply(null, args);
|
|
||||||
return NIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSON / dict helpers for island state serialization
|
// JSON / dict helpers for island state serialization
|
||||||
function jsonSerialize(obj) {
|
function jsonSerialize(obj) {
|
||||||
return JSON.stringify(obj);
|
return JSON.stringify(obj);
|
||||||
@@ -5192,8 +5181,8 @@ return (function() {
|
|||||||
// If lambda takes 0 params, call without event arg (convenience for on-click handlers)
|
// If lambda takes 0 params, call without event arg (convenience for on-click handlers)
|
||||||
var wrapped = isLambda(handler)
|
var wrapped = isLambda(handler)
|
||||||
? (lambdaParams(handler).length === 0
|
? (lambdaParams(handler).length === 0
|
||||||
? function(e) { try { invoke(handler); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }
|
? function(e) { try { cekCall(handler, NIL); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }
|
||||||
: function(e) { try { invoke(handler, e); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } })
|
: function(e) { try { cekCall(handler, [e]); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } })
|
||||||
: handler;
|
: handler;
|
||||||
if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
|
if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
|
||||||
el.addEventListener(name, wrapped);
|
el.addEventListener(name, wrapped);
|
||||||
@@ -6393,7 +6382,6 @@ return (function() {
|
|||||||
PRIMITIVES["stop-propagation"] = stopPropagation_;
|
PRIMITIVES["stop-propagation"] = stopPropagation_;
|
||||||
PRIMITIVES["error-message"] = errorMessage;
|
PRIMITIVES["error-message"] = errorMessage;
|
||||||
PRIMITIVES["schedule-idle"] = scheduleIdle;
|
PRIMITIVES["schedule-idle"] = scheduleIdle;
|
||||||
PRIMITIVES["invoke"] = invoke;
|
|
||||||
PRIMITIVES["error"] = function(msg) { throw new Error(msg); };
|
PRIMITIVES["error"] = function(msg) { throw new Error(msg); };
|
||||||
PRIMITIVES["filter"] = filter;
|
PRIMITIVES["filter"] = filter;
|
||||||
// DOM primitives for sx-on:* handlers and data-init scripts
|
// DOM primitives for sx-on:* handlers and data-init scripts
|
||||||
|
|||||||
@@ -1250,17 +1250,6 @@ PLATFORM_JS_PRE = '''
|
|||||||
return new Island(name, params, hasChildren, body, merge(env));
|
return new Island(name, params, hasChildren, body, merge(env));
|
||||||
}
|
}
|
||||||
|
|
||||||
// invoke — call any callable (native fn or SX lambda) with args.
|
|
||||||
// Transpiled code emits direct calls f(args) which fail on SX lambdas
|
|
||||||
// from runtime-evaluated island bodies. invoke dispatches correctly.
|
|
||||||
function invoke() {
|
|
||||||
var f = arguments[0];
|
|
||||||
var args = Array.prototype.slice.call(arguments, 1);
|
|
||||||
if (isLambda(f)) return trampoline(callLambda(f, args, lambdaClosure(f)));
|
|
||||||
if (typeof f === 'function') return f.apply(null, args);
|
|
||||||
return NIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSON / dict helpers for island state serialization
|
// JSON / dict helpers for island state serialization
|
||||||
function jsonSerialize(obj) {
|
function jsonSerialize(obj) {
|
||||||
return JSON.stringify(obj);
|
return JSON.stringify(obj);
|
||||||
@@ -1809,8 +1798,8 @@ PLATFORM_DOM_JS = """
|
|||||||
// If lambda takes 0 params, call without event arg (convenience for on-click handlers)
|
// If lambda takes 0 params, call without event arg (convenience for on-click handlers)
|
||||||
var wrapped = isLambda(handler)
|
var wrapped = isLambda(handler)
|
||||||
? (lambdaParams(handler).length === 0
|
? (lambdaParams(handler).length === 0
|
||||||
? function(e) { try { invoke(handler); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }
|
? function(e) { try { cekCall(handler, NIL); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } }
|
||||||
: function(e) { try { invoke(handler, e); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } })
|
: function(e) { try { cekCall(handler, [e]); } catch(err) { console.error("[sx-ref] domListen handler error:", name, err); } })
|
||||||
: handler;
|
: handler;
|
||||||
if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
|
if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
|
||||||
el.addEventListener(name, wrapped);
|
el.addEventListener(name, wrapped);
|
||||||
@@ -3023,7 +3012,6 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False, has_deps=False, has_
|
|||||||
PRIMITIVES["stop-propagation"] = stopPropagation_;
|
PRIMITIVES["stop-propagation"] = stopPropagation_;
|
||||||
PRIMITIVES["error-message"] = errorMessage;
|
PRIMITIVES["error-message"] = errorMessage;
|
||||||
PRIMITIVES["schedule-idle"] = scheduleIdle;
|
PRIMITIVES["schedule-idle"] = scheduleIdle;
|
||||||
PRIMITIVES["invoke"] = invoke;
|
|
||||||
PRIMITIVES["error"] = function(msg) { throw new Error(msg); };
|
PRIMITIVES["error"] = function(msg) { throw new Error(msg); };
|
||||||
PRIMITIVES["filter"] = filter;
|
PRIMITIVES["filter"] = filter;
|
||||||
// DOM primitives for sx-on:* handlers and data-init scripts
|
// DOM primitives for sx-on:* handlers and data-init scripts
|
||||||
|
|||||||
@@ -467,15 +467,6 @@ def is_identical(a, b):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def invoke(f, *args):
|
|
||||||
"""Call f with args — handles both native callables and SX lambdas.
|
|
||||||
|
|
||||||
In Python, all transpiled lambdas are natively callable, so this is
|
|
||||||
just a direct call. The JS host needs dispatch logic here because
|
|
||||||
SX lambdas from runtime-evaluated code are objects, not functions.
|
|
||||||
"""
|
|
||||||
return f(*args)
|
|
||||||
|
|
||||||
|
|
||||||
def json_serialize(obj):
|
def json_serialize(obj):
|
||||||
import json
|
import json
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import os, sys
|
|||||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
|
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
|
||||||
sys.path.insert(0, _PROJECT)
|
sys.path.insert(0, _PROJECT)
|
||||||
|
sys.setrecursionlimit(20000)
|
||||||
|
|
||||||
from shared.sx.parser import parse_all
|
from shared.sx.parser import parse_all
|
||||||
from shared.sx.ref import sx_ref
|
from shared.sx.ref import sx_ref
|
||||||
@@ -138,8 +139,8 @@ env["env-merge"] = env_merge
|
|||||||
env["symbol-name"] = lambda s: s.name if isinstance(s, Symbol) else str(s)
|
env["symbol-name"] = lambda s: s.name if isinstance(s, Symbol) else str(s)
|
||||||
env["keyword-name"] = lambda k: k.name if isinstance(k, Keyword) else str(k)
|
env["keyword-name"] = lambda k: k.name if isinstance(k, Keyword) else str(k)
|
||||||
env["type-of"] = sx_ref.type_of
|
env["type-of"] = sx_ref.type_of
|
||||||
env["primitive?"] = lambda n: n in sx_ref.PRIMITIVES
|
env["primitive?"] = sx_ref.is_primitive
|
||||||
env["get-primitive"] = lambda n: sx_ref.PRIMITIVES.get(n)
|
env["get-primitive"] = sx_ref.get_primitive
|
||||||
env["strip-prefix"] = lambda s, p: s[len(p):] if s.startswith(p) else s
|
env["strip-prefix"] = lambda s, p: s[len(p):] if s.startswith(p) else s
|
||||||
env["inspect"] = repr
|
env["inspect"] = repr
|
||||||
env["debug-log"] = lambda *args: None
|
env["debug-log"] = lambda *args: None
|
||||||
@@ -201,16 +202,6 @@ env["call-thunk"] = lambda f, e: f() if callable(f) else trampoline(eval_expr([f
|
|||||||
env["dict-get"] = lambda d, k: d.get(k, NIL) if isinstance(d, dict) else NIL
|
env["dict-get"] = lambda d, k: d.get(k, NIL) if isinstance(d, dict) else NIL
|
||||||
env["identical?"] = lambda a, b: a is b
|
env["identical?"] = lambda a, b: a is b
|
||||||
|
|
||||||
def _invoke(f, *args):
|
|
||||||
"""Call f with args — handles both native callables and SX lambdas."""
|
|
||||||
if isinstance(f, Lambda):
|
|
||||||
return trampoline(sx_ref.call_lambda(f, list(args), env))
|
|
||||||
elif callable(f):
|
|
||||||
return f(*args)
|
|
||||||
return NIL
|
|
||||||
|
|
||||||
env["invoke"] = _invoke
|
|
||||||
|
|
||||||
# defhandler, defpage, defquery, defaction stubs
|
# defhandler, defpage, defquery, defaction stubs
|
||||||
for name in ["sf-defhandler", "sf-defpage", "sf-defquery", "sf-defaction"]:
|
for name in ["sf-defhandler", "sf-defpage", "sf-defquery", "sf-defaction"]:
|
||||||
pyname = name.replace("-", "_")
|
pyname = name.replace("-", "_")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Run test-signals.sx using the bootstrapped evaluator with signal primitives.
|
"""Run test-signals.sx using the bootstrapped evaluator with signal primitives.
|
||||||
|
|
||||||
Uses bootstrapped signal functions from sx_ref.py directly, patching invoke
|
Uses bootstrapped signal functions from sx_ref.py directly, patching apply
|
||||||
to handle SX lambdas from the interpreter (test expressions create lambdas
|
to handle SX lambdas from the interpreter (test expressions create lambdas
|
||||||
that need evaluator dispatch).
|
that need evaluator dispatch).
|
||||||
"""
|
"""
|
||||||
@@ -25,14 +25,9 @@ sx_ref.trampoline = trampoline
|
|||||||
# Build env with primitives
|
# Build env with primitives
|
||||||
env = make_env()
|
env = make_env()
|
||||||
|
|
||||||
# --- Patch invoke and apply BEFORE anything else ---
|
# --- Patch apply BEFORE anything else ---
|
||||||
# Test expressions create SX Lambdas that bootstrapped code calls via invoke/apply.
|
# Test expressions create SX Lambdas that bootstrapped code calls via apply.
|
||||||
# Patch the module-level functions so all bootstrapped functions see them.
|
# Patch the module-level function so all bootstrapped functions see it.
|
||||||
def _invoke(f, *args):
|
|
||||||
if isinstance(f, Lambda):
|
|
||||||
return trampoline(eval_expr([f] + list(args), env))
|
|
||||||
return f(*args)
|
|
||||||
sx_ref.__dict__["invoke"] = _invoke
|
|
||||||
|
|
||||||
# apply is used by swap! and other forms to call functions with arg lists
|
# apply is used by swap! and other forms to call functions with arg lists
|
||||||
def _apply(f, args):
|
def _apply(f, args):
|
||||||
@@ -124,7 +119,7 @@ env["effect"] = sx_ref.effect
|
|||||||
# Wrap it to work correctly in the test context.
|
# Wrap it to work correctly in the test context.
|
||||||
def _batch(thunk):
|
def _batch(thunk):
|
||||||
sx_ref._batch_depth = getattr(sx_ref, '_batch_depth', 0) + 1
|
sx_ref._batch_depth = getattr(sx_ref, '_batch_depth', 0) + 1
|
||||||
_invoke(thunk)
|
sx_ref.cek_call(thunk, None)
|
||||||
sx_ref._batch_depth -= 1
|
sx_ref._batch_depth -= 1
|
||||||
if sx_ref._batch_depth == 0:
|
if sx_ref._batch_depth == 0:
|
||||||
queue = list(sx_ref._batch_queue)
|
queue = list(sx_ref._batch_queue)
|
||||||
@@ -145,7 +140,6 @@ env["flush-subscribers"] = sx_ref.flush_subscribers
|
|||||||
env["dispose-computed"] = sx_ref.dispose_computed
|
env["dispose-computed"] = sx_ref.dispose_computed
|
||||||
env["with-island-scope"] = sx_ref.with_island_scope
|
env["with-island-scope"] = sx_ref.with_island_scope
|
||||||
env["register-in-scope"] = sx_ref.register_in_scope
|
env["register-in-scope"] = sx_ref.register_in_scope
|
||||||
env["invoke"] = _invoke
|
|
||||||
env["callable?"] = sx_ref.is_callable
|
env["callable?"] = sx_ref.is_callable
|
||||||
|
|
||||||
# Load test framework
|
# Load test framework
|
||||||
|
|||||||
@@ -426,15 +426,6 @@ def is_identical(a, b):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def invoke(f, *args):
|
|
||||||
"""Call f with args — handles both native callables and SX lambdas.
|
|
||||||
|
|
||||||
In Python, all transpiled lambdas are natively callable, so this is
|
|
||||||
just a direct call. The JS host needs dispatch logic here because
|
|
||||||
SX lambdas from runtime-evaluated code are objects, not functions.
|
|
||||||
"""
|
|
||||||
return f(*args)
|
|
||||||
|
|
||||||
|
|
||||||
def json_serialize(obj):
|
def json_serialize(obj):
|
||||||
import json
|
import json
|
||||||
|
|||||||
@@ -152,3 +152,60 @@
|
|||||||
;; Change signal — no update should fire
|
;; Change signal — no update should fire
|
||||||
(reset! s 999)
|
(reset! s 999)
|
||||||
(assert-equal 0 (len update-calls)))))
|
(assert-equal 0 (len update-calls)))))
|
||||||
|
|
||||||
|
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
;; cek-call integration — computed/effect use cek-call dispatch
|
||||||
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(defsuite "cek-call dispatch"
|
||||||
|
(deftest "cek-call invokes native function"
|
||||||
|
(let ((log (list)))
|
||||||
|
(cek-call (fn (x) (append! log x)) (list 42))
|
||||||
|
(assert-equal (list 42) log)))
|
||||||
|
|
||||||
|
(deftest "cek-call invokes zero-arg lambda"
|
||||||
|
(let ((result (cek-call (fn () (+ 1 2)) nil)))
|
||||||
|
(assert-equal 3 result)))
|
||||||
|
|
||||||
|
(deftest "cek-call with nil function returns nil"
|
||||||
|
(assert-nil (cek-call nil nil)))
|
||||||
|
|
||||||
|
(deftest "computed tracks deps via cek-call"
|
||||||
|
(let ((s (signal 10)))
|
||||||
|
(let ((c (computed (fn () (* 2 (deref s))))))
|
||||||
|
(assert-equal 20 (deref c))
|
||||||
|
(reset! s 5)
|
||||||
|
(assert-equal 10 (deref c)))))
|
||||||
|
|
||||||
|
(deftest "effect runs and re-runs via cek-call"
|
||||||
|
(let ((s (signal "a"))
|
||||||
|
(log (list)))
|
||||||
|
(effect (fn () (append! log (deref s))))
|
||||||
|
(assert-equal (list "a") log)
|
||||||
|
(reset! s "b")
|
||||||
|
(assert-equal (list "a" "b") log)))
|
||||||
|
|
||||||
|
(deftest "effect cleanup runs on re-trigger"
|
||||||
|
(let ((s (signal 0))
|
||||||
|
(log (list)))
|
||||||
|
(effect (fn ()
|
||||||
|
(let ((val (deref s)))
|
||||||
|
(append! log (str "run:" val))
|
||||||
|
;; Return cleanup function
|
||||||
|
(fn () (append! log (str "clean:" val))))))
|
||||||
|
(assert-equal (list "run:0") log)
|
||||||
|
(reset! s 1)
|
||||||
|
(assert-equal (list "run:0" "clean:0" "run:1") log)))
|
||||||
|
|
||||||
|
(deftest "batch coalesces via cek-call"
|
||||||
|
(let ((s (signal 0))
|
||||||
|
(count (signal 0)))
|
||||||
|
(effect (fn () (do (deref s) (swap! count inc))))
|
||||||
|
(assert-equal 1 (deref count))
|
||||||
|
(batch (fn ()
|
||||||
|
(reset! s 1)
|
||||||
|
(reset! s 2)
|
||||||
|
(reset! s 3)))
|
||||||
|
;; batch should coalesce — effect runs once, not three times
|
||||||
|
(assert-equal 2 (deref count)))))
|
||||||
|
|||||||
Reference in New Issue
Block a user