js-on-sx: harness cache — precompute HARNESS_STUB SX once per run

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.
This commit is contained in:
2026-04-24 11:20:55 +00:00
parent f14a257533
commit 4a277941b6
5 changed files with 298 additions and 81 deletions

View File

@@ -23,7 +23,46 @@
;; ── tiny helpers ──────────────────────────────────────────────────
(define js-sym (fn (name) (make-symbol name)))
(define js-has-dollar? (fn (name) (js-has-dollar-loop? name 0 (len name))))
(define
js-has-dollar-loop?
(fn
(s i n)
(cond
((>= i n) false)
((= (char-at s i) "$") true)
(else (js-has-dollar-loop? s (+ i 1) n)))))
(define
js-mangle-ident
(fn
(name)
(if
(js-has-dollar? name)
(js-mangle-ident-loop name 0 (len name) "")
name)))
;; ── main dispatcher ───────────────────────────────────────────────
(define
js-mangle-ident-loop
(fn
(s i n acc)
(cond
((>= i n) acc)
((= (char-at s i) "$")
(js-mangle-ident-loop s (+ i 1) n (str acc "_js_dollar_")))
(else (js-mangle-ident-loop s (+ i 1) n (str acc (char-at s i)))))))
;; ── Identifier lookup ─────────────────────────────────────────────
;; `undefined` in JS is really a global binding. If the parser emits
;; (js-undef) we handle that above. A bare `undefined` ident also maps
;; to the same sentinel.
(define js-sym (fn (name) (make-symbol (js-mangle-ident name))))
;; ── Unary ops ─────────────────────────────────────────────────────
(define
js-tag?
(fn
@@ -34,9 +73,11 @@
(= (type-of (first ast)) "symbol")
(= (symbol-name (first ast)) tag))))
;; ── Binary ops ────────────────────────────────────────────────────
(define js-ast-tag (fn (ast) (symbol-name (first ast))))
;; ── main dispatcher ───────────────────────────────────────────────
;; ── Member / index ────────────────────────────────────────────────
(define
js-transpile
@@ -146,11 +187,6 @@
(else
(error (str "js-transpile: unexpected value type: " (type-of ast)))))))
;; ── Identifier lookup ─────────────────────────────────────────────
;; `undefined` in JS is really a global binding. If the parser emits
;; (js-undef) we handle that above. A bare `undefined` ident also maps
;; to the same sentinel.
(define
js-transpile-ident
(fn
@@ -164,8 +200,10 @@
((= name "Function") (js-sym "js-function-global"))
(else (js-sym name)))))
;; ── Unary ops ─────────────────────────────────────────────────────
;; ── Call ──────────────────────────────────────────────────────────
;; JS `f(a, b, c)` → `(f a b c)` after transpile. Works for both
;; identifier calls and computed callee (arrow fn, member access).
(define
js-transpile-unop
(fn
@@ -196,7 +234,7 @@
((= op "void") (list (js-sym "quote") :js-undefined))
(else (error (str "js-transpile-unop: unsupported op: " op)))))))))
;; ── Binary ops ────────────────────────────────────────────────────
;; ── Array literal ─────────────────────────────────────────────────
(define
js-transpile-binop
@@ -259,22 +297,25 @@
(js-sym "_a"))))
(else (error (str "js-transpile-binop: unsupported op: " op))))))
;; ── Member / index ────────────────────────────────────────────────
;; ── Object literal ────────────────────────────────────────────────
;; Build a dict by `(dict)` + `dict-set!` inside a `let` that yields
;; the dict as its final expression. This keeps keys in JS insertion
;; order and allows computed values.
(define
js-transpile-member
(fn (obj key) (list (js-sym "js-get-prop") (js-transpile obj) key)))
;; ── Conditional ───────────────────────────────────────────────────
(define
js-transpile-index
(fn
(obj idx)
(list (js-sym "js-get-prop") (js-transpile obj) (js-transpile idx))))
;; ── Call ──────────────────────────────────────────────────────────
;; ── Arrow function ────────────────────────────────────────────────
;; JS `f(a, b, c)` → `(f a b c)` after transpile. Works for both
;; identifier calls and computed callee (arrow fn, member access).
(define
js-transpile-call
(fn
@@ -320,8 +361,11 @@
(js-transpile callee)
(js-transpile-args args))))))
;; ── Array literal ─────────────────────────────────────────────────
;; ── Assignment ────────────────────────────────────────────────────
;; `a = b` on an ident → (set! a b).
;; `a += b` on an ident → (set! a (js-add a b)).
;; `obj.k = v` / `obj[k] = v` → (js-set-prop obj "k" v).
(define
js-transpile-new
(fn
@@ -331,11 +375,6 @@
(js-transpile callee)
(cons (js-sym "list") (map js-transpile args)))))
;; ── Object literal ────────────────────────────────────────────────
;; Build a dict by `(dict)` + `dict-set!` inside a `let` that yields
;; the dict as its final expression. This keeps keys in JS insertion
;; order and allows computed values.
(define
js-transpile-array
(fn
@@ -354,8 +393,6 @@
elts))
(cons (js-sym "list") (map js-transpile elts)))))
;; ── Conditional ───────────────────────────────────────────────────
(define
js-has-spread?
(fn
@@ -365,8 +402,9 @@
((js-tag? (first lst) "js-spread") true)
(else (js-has-spread? (rest lst))))))
;; ── Arrow function ────────────────────────────────────────────────
;; ── End-to-end entry points ───────────────────────────────────────
;; Transpile + eval a single JS expression string.
(define
js-transpile-args
(fn
@@ -385,11 +423,8 @@
args))
(cons (js-sym "list") (map js-transpile args)))))
;; ── Assignment ────────────────────────────────────────────────────
;; `a = b` on an ident → (set! a b).
;; `a += b` on an ident → (set! a (js-add a b)).
;; `obj.k = v` / `obj[k] = v` → (js-set-prop obj "k" v).
;; Transpile a JS expression string to SX source text (for inspection
;; in tests). Useful for asserting the exact emitted tree.
(define
js-transpile-object
(fn
@@ -451,9 +486,6 @@
(append inits (list (js-transpile body))))))))
(list (js-sym "fn") param-syms body-tr))))
;; ── End-to-end entry points ───────────────────────────────────────
;; Transpile + eval a single JS expression string.
(define
js-transpile-tpl
(fn
@@ -465,8 +497,6 @@
(else
(cons (js-sym "js-template-concat") (js-transpile-tpl-parts parts))))))
;; Transpile a JS expression string to SX source text (for inspection
;; in tests). Useful for asserting the exact emitted tree.
(define
js-transpile-tpl-parts
(fn