diff --git a/plans/js-on-sx.md b/plans/js-on-sx.md index f5aec07b..68cc1296 100644 --- a/plans/js-on-sx.md +++ b/plans/js-on-sx.md @@ -201,6 +201,7 @@ Append-only record of completed iterations. Loop writes one line per iteration: - 2026-04-23 — **Rest + rename in destructure, delete operator, Array.prototype / String.prototype / Number.prototype stubs, lastIndexOf, typeof fix, js-apply-fn handles callable dicts.** Array destructure `[a, ...rest]` transpiles rest-tail to `(js-list-slice tmp i (len tmp))`. Obj pattern `{key: local}` renames to `local`. `delete obj.k` → `js-delete-prop` which sets value to undefined. `Array.prototype.push` etc. are accessible as proto-functions that route through `js-this` + `js-invoke-method`. `Number.prototype` stub with `toString/valueOf/toFixed`. Nested destructuring patterns tolerated via `jp-skip-balanced` (treated as holes). Destructuring params in fn decls accepted (holes). **Final Math scoreboard: 66/288 (22.9%)** vs 56/288 (19.4%) baseline, +10 passes (+3.5 percentage points). 446/448 unit (438→+8). 148/148 slice unchanged. Top remaining Math failures: 94× ReferenceError (`Math.log`/`Math.sin`/`Math.cos` etc. not shimmed — no SX `sin`/`cos`/`log` primitives), 79× assertion-fail (numerical precision on `Math.floor` / `ceil` / `trunc` edge cases), 31× TypeError, 16× Timeout. +- 2026-04-23 (session 4) — **P0 harness cache: 2.06× speedup.** Root cause: every worker sx_server session was running `js-eval` on the 3.6 KB `HARNESS_STUB` from scratch (~15 s per session — tokenize + parse + transpile). Fix: transpile once per Python process via a throwaway sx_server, call `(inspect (js-transpile (js-parse (js-tokenize HARNESS_STUB))))`, write the SX text to `lib/js/.harness-cache/stub..sx` (fingerprint = sha256 of stub + lexer + parser + transpile), and have every session `(load …)` the cached file instead. Disk cache survives across Python invocations; edits to `transpile.sx` invalidate automatically. Had to add a `$`-to-`_js_dollar_` name-mangler in `js-sym`: SX's tokenizer rejects `$` in identifiers, which broke round-tripping through `inspect` (e.g. `$DONOTEVALUATE` → parse-error on reload). No internal JS-on-SX names contain `$`, so the mangler is a pure add. Measured on 300-test wide (Math+Number+String @ 100/cat, 5 s per-test timeout): **593.7 s → 288.0 s**, 2.06× speedup. Scoreboard 114→115/300 (38.3%, noise band). Committed: `4a277941`. Also filed two Blockers entries — **Math trig/transcendental primitives missing** (sin/cos/tan/log/atan2/… × 22 platform-level primitives — accounts for all 34 Math "not a function" failures) and **Evaluator CPU bound at ~1 test/s** (lexical addresses, inline caches, force JIT on transpiled bodies, OCaml 5 domains). - 2026-04-23 (session 3, continued) — **Scoreboard scoped 100/cat**: Math 40% / Number 43% / String 31% = 114/300 overall. Additional commits (85a329e8..c3b0aef1): - `fn.length` reflects arity via `lambda-params` + `js-count-real-params`. Math.abs.length = 1 etc. - `Object` global now callable (`new Object()`, `Object(5)`). Object.prototype has `hasOwnProperty`/`isPrototypeOf`/`propertyIsEnumerable`/`toString`/`valueOf`. @@ -261,6 +262,21 @@ Anything that would require a change outside `lib/js/` goes here with a minimal - `regex-flags compiled` → string Ideally a single `(js-regex-platform-install-all! platform)` entry point the host calls once at boot. OCaml would wrap `Str` / `Re` or a dedicated regex lib; JS host can just delegate to the native `RegExp`. +- **Math trig + transcendental primitives missing.** The scoreboard shows 34× "TypeError: not a function" across the Math category — every one a test calling `Math.sin/cos/tan/log/…` on our runtime. We shim `Math` via `js-global`; the SX runtime supplies `sqrt`, `pow`, `abs`, `floor`, `ceil`, `round` and a hand-rolled `trunc`/`sign`/`cbrt`/`hypot`. Nothing else. Missing platform primitives (each is a one-line OCaml/JS binding, but a primitive all the same — we can't land approximation polynomials from inside the JS shim, they'd blow `Math.sin(1e308)` precision): + - Trig: `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2` + - Hyperbolic: `sinh`, `cosh`, `tanh`, `asinh`, `acosh`, `atanh` + - Log/exp: `log` (natural), `log2`, `log10`, `log1p`, `expm1` + - Bit-width: `clz32`, `imul`, `fround` + - Variadic lifts: `hypot(…)` (n-args), `max(…)`/`min(…)` (we have 2-arg; JS allows 0 through N) + Minimal repro: `Math.sin(0)` currently raises `TypeError: Math.sin is not a function` because `js-global.Math` has no `sin` key. Once the primitives exist in the runtime, `js-global.Math` can be extended in one drop — all 34 Math `not a function` failures flip together. + +- **Evaluator CPU bound at ~1 test/s on a 2-core box.** Runner auto-defaults to 1 worker on <=2-cores because two CEK workers starve each other — both are pure-Python/OCaml heavy loops with no IO concurrency to exploit. Each test that parses JS, transpiles to SX, then evaluates the SX under CEK runs in the 0.3–3s range; the long tail is mostly per-test `js-parse` scaling superlinearly with test length plus CEK env lookup walking a list of frames per free variable. Optimization surface someone else could pick up (all shared-file): + - **Lexical addresses** in the evaluator: swap env-walk-by-name for `(depth, slot)` tuples resolved at transpile time. Would slash the per-CEK-step cost by an order of magnitude on tight loops. + - **Inline caches on `js-get-prop`.** Every `a.b` walks the `__proto__` chain; 99% of call sites hit the same shape. A monomorphic cache keyed on the receiver's structural shape would collapse that to one lookup. + - **Force JIT on transpiled JS bodies.** Currently JS → SX → tree-walk CEK. The VM JIT exists but lambda compilation triggers on lazy call — and many test bodies are top-level straight-lines that never hit the JIT threshold. An explicit `(jit-compile!)` after transpile, or an AOT pass when loading the harness cache, would be a one-shot win. + - **OCaml 5 domains.** The runner has `--workers N` but the box is 2-core. OCaml 5 multicore would let one process host N CEK domains with work-stealing instead of N Python subprocesses each booting their own kernel. + Minimal repro: `time python3 lib/js/test262-runner.py --filter built-ins/Number --max-per-category 50 --workers 1` — currently ~50s wall, ~1 test/s after the ~4s warm-start. Target for any of the above fixes: ≥3× uplift. `built-ins/String` timeouts will thin out naturally at that rate because many of them are pure 2000-iter `for` loops hitting the per-test 5s cap. + ## First-iteration checklist (scaffolding) — DONE - [x] `lib/js/lexer.sx` — stub `js-tokenize`