diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 04172ec9..b760b601 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -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 diff --git a/shared/sx/ref/platform_js.py b/shared/sx/ref/platform_js.py index fc90b118..218418b8 100644 --- a/shared/sx/ref/platform_js.py +++ b/shared/sx/ref/platform_js.py @@ -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 diff --git a/shared/sx/ref/platform_py.py b/shared/sx/ref/platform_py.py index c0f1be1f..a84cf761 100644 --- a/shared/sx/ref/platform_py.py +++ b/shared/sx/ref/platform_py.py @@ -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 diff --git a/shared/sx/ref/run_cek_reactive_tests.py b/shared/sx/ref/run_cek_reactive_tests.py index 299e0229..73ef7251 100644 --- a/shared/sx/ref/run_cek_reactive_tests.py +++ b/shared/sx/ref/run_cek_reactive_tests.py @@ -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("-", "_") diff --git a/shared/sx/ref/run_signal_tests.py b/shared/sx/ref/run_signal_tests.py index 3e7e21bd..80f0ce74 100644 --- a/shared/sx/ref/run_signal_tests.py +++ b/shared/sx/ref/run_signal_tests.py @@ -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 diff --git a/shared/sx/ref/sx_ref.py b/shared/sx/ref/sx_ref.py index 2a4a2b98..aad3ff52 100644 --- a/shared/sx/ref/sx_ref.py +++ b/shared/sx/ref/sx_ref.py @@ -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 diff --git a/shared/sx/ref/test-cek-reactive.sx b/shared/sx/ref/test-cek-reactive.sx index 456501c3..0669dcdb 100644 --- a/shared/sx/ref/test-cek-reactive.sx +++ b/shared/sx/ref/test-cek-reactive.sx @@ -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)))))