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 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 isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||
@@ -218,17 +218,6 @@
|
||||
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
|
||||
function jsonSerialize(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)
|
||||
var wrapped = isLambda(handler)
|
||||
? (lambdaParams(handler).length === 0
|
||||
? function(e) { try { invoke(handler); } 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, NIL); } 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;
|
||||
if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
|
||||
el.addEventListener(name, wrapped);
|
||||
@@ -6393,7 +6382,6 @@ return (function() {
|
||||
PRIMITIVES["stop-propagation"] = stopPropagation_;
|
||||
PRIMITIVES["error-message"] = errorMessage;
|
||||
PRIMITIVES["schedule-idle"] = scheduleIdle;
|
||||
PRIMITIVES["invoke"] = invoke;
|
||||
PRIMITIVES["error"] = function(msg) { throw new Error(msg); };
|
||||
PRIMITIVES["filter"] = filter;
|
||||
// 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));
|
||||
}
|
||||
|
||||
// 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
|
||||
function jsonSerialize(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)
|
||||
var wrapped = isLambda(handler)
|
||||
? (lambdaParams(handler).length === 0
|
||||
? function(e) { try { invoke(handler); } 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, NIL); } 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;
|
||||
if (name === "click") logInfo("domListen: click on <" + (el.tagName||"?").toLowerCase() + "> text=" + (el.textContent||"").substring(0,20) + " isLambda=" + isLambda(handler));
|
||||
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["error-message"] = errorMessage;
|
||||
PRIMITIVES["schedule-idle"] = scheduleIdle;
|
||||
PRIMITIVES["invoke"] = invoke;
|
||||
PRIMITIVES["error"] = function(msg) { throw new Error(msg); };
|
||||
PRIMITIVES["filter"] = filter;
|
||||
// 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):
|
||||
import json
|
||||
|
||||
@@ -6,6 +6,7 @@ import os, sys
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
|
||||
sys.path.insert(0, _PROJECT)
|
||||
sys.setrecursionlimit(20000)
|
||||
|
||||
from shared.sx.parser import parse_all
|
||||
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["keyword-name"] = lambda k: k.name if isinstance(k, Keyword) else str(k)
|
||||
env["type-of"] = sx_ref.type_of
|
||||
env["primitive?"] = lambda n: n in sx_ref.PRIMITIVES
|
||||
env["get-primitive"] = lambda n: sx_ref.PRIMITIVES.get(n)
|
||||
env["primitive?"] = sx_ref.is_primitive
|
||||
env["get-primitive"] = sx_ref.get_primitive
|
||||
env["strip-prefix"] = lambda s, p: s[len(p):] if s.startswith(p) else s
|
||||
env["inspect"] = repr
|
||||
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["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
|
||||
for name in ["sf-defhandler", "sf-defpage", "sf-defquery", "sf-defaction"]:
|
||||
pyname = name.replace("-", "_")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""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
|
||||
that need evaluator dispatch).
|
||||
"""
|
||||
@@ -25,14 +25,9 @@ sx_ref.trampoline = trampoline
|
||||
# Build env with primitives
|
||||
env = make_env()
|
||||
|
||||
# --- Patch invoke and apply BEFORE anything else ---
|
||||
# Test expressions create SX Lambdas that bootstrapped code calls via invoke/apply.
|
||||
# Patch the module-level functions so all bootstrapped functions see them.
|
||||
def _invoke(f, *args):
|
||||
if isinstance(f, Lambda):
|
||||
return trampoline(eval_expr([f] + list(args), env))
|
||||
return f(*args)
|
||||
sx_ref.__dict__["invoke"] = _invoke
|
||||
# --- Patch apply BEFORE anything else ---
|
||||
# Test expressions create SX Lambdas that bootstrapped code calls via apply.
|
||||
# Patch the module-level function so all bootstrapped functions see it.
|
||||
|
||||
# apply is used by swap! and other forms to call functions with arg lists
|
||||
def _apply(f, args):
|
||||
@@ -124,7 +119,7 @@ env["effect"] = sx_ref.effect
|
||||
# Wrap it to work correctly in the test context.
|
||||
def _batch(thunk):
|
||||
sx_ref._batch_depth = getattr(sx_ref, '_batch_depth', 0) + 1
|
||||
_invoke(thunk)
|
||||
sx_ref.cek_call(thunk, None)
|
||||
sx_ref._batch_depth -= 1
|
||||
if sx_ref._batch_depth == 0:
|
||||
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["with-island-scope"] = sx_ref.with_island_scope
|
||||
env["register-in-scope"] = sx_ref.register_in_scope
|
||||
env["invoke"] = _invoke
|
||||
env["callable?"] = sx_ref.is_callable
|
||||
|
||||
# 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):
|
||||
import json
|
||||
|
||||
@@ -152,3 +152,60 @@
|
||||
;; Change signal — no update should fire
|
||||
(reset! s 999)
|
||||
(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