Fix: local bindings now shadow HTML tag special forms in browser evaluator

Root cause: sx_browser.ml registered all HTML tags (a, b, i, p, s, u, g, etc.)
as custom special forms. The evaluator's step_eval_list checked custom special
forms BEFORE checking local env bindings. So (let ((a (fn () 42))) (a))
matched the HTML tag <a> instead of calling the local function a.

Fix: skip custom special forms AND render-check when the symbol is bound in
the local env. Added (not (env-has? env name)) guard to both checks in
step-eval-list (spec/evaluator.sx and transpiled sx_ref.ml).

This was the root cause of "[sx] resume: Not callable: nil" — after hs-wait
resumed, calling letrec-bound functions like wait-boot (which is not an HTML
tag) worked, but any function whose name collided with an HTML tag failed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 12:53:09 +00:00
parent 0f9bb68ba2
commit c641b445f8
16 changed files with 1541 additions and 1376 deletions

View File

@@ -299,6 +299,48 @@ node -e '
K.eval("(let ((d (list))) (let ((el (with-island-scope (fn (x) (append! d x)) (fn () (render-to-dom (quote (div :class (deref test-reactive-sig) \"content\")) (global-env) nil))))) (reset! test-reactive-sig \"after\") (dom-get-attr el \"class\")))"),
"after");
// =====================================================================
// Section 4: Letrec + perform resume (async _driveAsync)
// =====================================================================
// Define the letrec+perform pattern — this matches the test-runner island
K.eval("(define __letrec-test-fn (letrec ((other (fn () \"from-other\")) (go (fn () (do (perform {:op \"io-sleep\" :args (list 50)}) (other))))) go))");
// Get the function as a JS-callable value
var letrecFn = K.eval("__letrec-test-fn");
if (typeof letrecFn !== "function") {
fail++; console.error("FAIL: letrec-fn not callable, got: " + typeof letrecFn);
} else {
// Call via callFn — same path as island click handlers
var letrecResult = K.callFn(letrecFn, []);
// Resume through all suspensions — tests that resume() preserves letrec env
try {
while (letrecResult && letrecResult.suspended) { letrecResult = letrecResult.resume(null); }
assert("letrec sibling after perform resume", letrecResult, "from-other");
} catch(e) {
fail++; console.error("FAIL: letrec perform resume: " + (e.message || e));
}
}
// Recursive letrec after perform — the wait-boot pattern
K.eval("(define __wb-counter 0)");
K.eval("(define __recur-test-fn (letrec ((recur (fn () (set! __wb-counter (+ __wb-counter 1)) (if (>= __wb-counter 3) \"done\" (do (perform {:op \"io-sleep\" :args (list 10)}) (recur)))))) (fn () (set! __wb-counter 0) (recur))))");
var recurFn = K.eval("__recur-test-fn");
if (typeof recurFn !== "function") {
fail++; console.error("FAIL: recur-fn not callable, got: " + typeof recurFn);
} else {
var recurResult = K.callFn(recurFn, []);
try {
// Resume through all suspensions synchronously
while (recurResult && recurResult.suspended) { recurResult = recurResult.resume(null); }
assert("recursive letrec after perform", recurResult, "done");
assert("recursive letrec counter", K.eval("__wb-counter"), 3);
} catch(e) {
fail++; console.error("FAIL: recursive letrec perform: " + (e.message || e));
}
}
// =====================================================================
// Summary
// =====================================================================