Previously a JS function body with no return fell through the call/cc
begin as nil, making `(function(){}())` return null and typeof → object.
Spec: falls-off-end gives undefined.
Wrap the call/cc in (let ((__r__ ...)) (if (= __r__ nil) :js-undefined __r__)).
Downside: explicit `return null` also returns nil, but so does (pick
your last expression evaluating to null). For 99% of cases it's
fall-off-end and the fix is correct. Code that genuinely needs
distinguishable null would need separate nil/undef handling in the
evaluator.
Unit 521/522, slice 148/148 unchanged.
Number 73/100 → 74/100 (+1), String 33/100 → 34/100 (+1).
Fixes S15.5.1.1_A1_T1 family (String(function(){}()) should be "undefined").
Root cause: every sx_server worker session used js-eval on the 3.6KB
HARNESS_STUB, paying ~15s for tokenize+parse+transpile even though every
session does the same thing. Over a full scoreboard with periodic worker
restarts that's minutes of wasted work.
Fix: transpile once per Python process. Spin up a throwaway sx_server,
run (inspect (js-transpile (js-parse (js-tokenize HARNESS_STUB)))), write
the resulting SX source to lib/js/.harness-cache/stub.<fingerprint>.sx and
a stable-name symlink-ish copy stub.sx. Every worker session then does a
single (load .harness-cache/stub.sx) instead of re-running js-eval.
Fingerprint: sha256(HARNESS_STUB + lexer.sx + parser.sx + transpile.sx).
Transpiler edits invalidate the cache automatically. Runs back-to-back
reuse the cache — only the first run after a transpiler change pays the
~15s precompute.
Transpile had to gain a $-to-_js_dollar_ name-mangler: the SX tokenizer
rejects $ in identifiers, which broke round-tripping via inspect. JS
$DONOTEVALUATE → SX _js_dollar_DONOTEVALUATE. Internal JS-on-SX names are
unaffected (none contain $).
Measured: 300-test wide (Math+Number+String @ 100/cat, --per-test-timeout 5):
593.7s → 288.0s, 2.06x speedup. Scoreboard 114→115/300 (38.3%, noise band).
Math 40%, Number 44%, String 30% — same shape as prior.
Baselines: 520/522 unit, 148/148 slice — unchanged.
Several tests check 'new Function("return 1")' — we can't actually
implement that (would need runtime JS eval). Now Function is a dict with
__callable__ that throws TypeError, and a prototype containing call/apply/
bind/toString/length/name stubs so code that probes Function.prototype
doesn't crash with 'Undefined symbol: Function'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JS 'globalThis' now rewrites to SX (js-global) — the global object dict.
JS 'eval' rewrites to js-global-eval, a no-op stub that echoes its first
arg. Many test262 tests probe eval's existence or pass simple literals
through it; a no-op is better than 'Undefined symbol: eval'.
A full eval would require plumbing js-eval into the runtime with access
to the enclosing lexical scope — non-trivial. The stub unblocks tests
that just need eval to be callable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously NaN / Infinity were SX symbols that couldn't be (define)'d
because the SX tokenizer parses 'NaN' and 'Infinity' as numeric literals.
js-transpile-ident now rewrites NaN -> (js-nan-value), Infinity ->
(js-infinity-value), each a zero-arg function returning the appropriate
IEEE value ((/ 0.0 0.0) and (/ 1.0 0.0)).
Also fixes js-number-is-nan: in this SX, (= nan nan) returns true, so the
classic 'v !== v' trick doesn't work. Now checks (inspect v) against
'nan'/'-nan' strings.
Extends js-strict-eq: NaN === NaN returns false per ES spec.
8 new unit tests, 497/499 total.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
js-transpile-unop intercepts 'delete' before transpiling the
operand. Maps to (js-delete-prop obj key) for members and indexed
access. Runtime js-delete-prop sets the dict value to js-undefined
and returns true.
444/446 unit (+2), 148/148 slice unchanged.
Array destructure now supports [a, ...rest]. Rest entry is
transpiled to (define name (js-list-slice tmp i (len tmp))).
Nested patterns like [[a,b], c] now parse (as holes) instead of
erroring. jp-skip-balanced skips nested groups.
440/442 unit (+2), 148/148 slice unchanged.
Parser: jp-parse-postfix handles op "?." followed by ident / [ / (
emitting (js-optchain-member obj name), (js-optchain-index obj k),
or (js-optchain-call callee args).
Transpile: each emits (js-optchain-get obj key) or (js-optchain-call
fn args).
Runtime: js-optchain-get and js-optchain-call short-circuit to
js-undefined when receiver is null/undefined.
423/425 unit (+5), 148/148 slice unchanged.
Parser: jp-array-loop and jp-call-args-loop detect punct "..."
and emit (js-spread inner).
Transpile: when any element is spread, build array/args via
js-array-spread-build with (list "js-value" v) and (list
"js-spread" xs) tags.
Runtime: js-array-spread-build walks items, appending values or
splicing spreads via js-iterable-to-list (handles list/string/dict).
Works in arrays, call args, variadic fns (Math.max(...arr)),
and string spread ([...'abc']).
414/416 unit (+5), 148/148 slice unchanged.
Parser: jp-parse-postfix emits (js-postfix op target) on trailing
++/--; jp-parse-primary emits (js-prefix op target) before the
unary -/+/!/~ branch.
Transpile: js-transpile-prefix → (set! name (+ (js-to-number name)
±1)) for idents, (js-set-prop obj key ...) for members/indices.
js-transpile-postfix caches old value in a let binding, updates,
returns the saved value.
340/342 unit (+11), 148/148 slice unchanged.
Initial commit of the lib/js/ tree and plans/ directory. A previous
session left template-string work in progress — 278/280 unit tests pass
(2 failing: tpl part-count off-by-one, escaped-backtick ident lookup).
test262-runner.py and scoreboard are placeholders (0/8 with 7 timeouts);
fixing the runner is the next queue item.