js-on-sx: Blockers — Math trig primitives + evaluator CPU bound

Two shared-file entries based on scoreboard patterns:

- Math trig/transcendental primitives missing. 34× "TypeError: not a
  function" across Math category — sin/cos/tan/asin/acos/atan/atan2,
  sinh/cosh/tanh/asinh/acosh/atanh, log/log2/log10/log1p/expm1,
  clz32/imul/fround, variadic hypot/max/min. All need OCaml/JS platform
  primitives; can't polyfill from pure SX and keep precision. Once
  present in the runtime, `js-global.Math` gets one extension and all 34
  failures flip together.

- Evaluator CPU bound at ~1 test/s on 2-core box. Runner already
  auto-disables parallel workers on ≤2 cores. Optimization surface for
  the shared evaluator: lexical addresses (vs name walk), inline caches
  on js-get-prop (vs __proto__ walk), force-JIT transpiled JS bodies
  (vs lazy), OCaml 5 domains (vs separate processes).

Progress-log entry for P0 harness cache added alongside.
This commit is contained in:
2026-04-24 11:21:58 +00:00
parent 4a277941b6
commit dc97c17304

View File

@@ -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.<fp>.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.33s 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`