From 6a4269d32747da640fc3bbab361c430f6aa0fa62 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 24 Apr 2026 12:21:22 +0000 Subject: [PATCH] =?UTF-8?q?plan:=20Blocker=20=E2=80=94=20SX=20number=20pro?= =?UTF-8?q?motion=20narrows=20floats=20to=20ints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plans/js-on-sx.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plans/js-on-sx.md b/plans/js-on-sx.md index 93d0b0b4..7e8c53a0 100644 --- a/plans/js-on-sx.md +++ b/plans/js-on-sx.md @@ -279,6 +279,13 @@ Anything that would require a change outside `lib/js/` goes here with a minimal - 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. +- **SX number promotion loses floats on exact-int results.** Minimal repro: `(type-of (* 1.5 2))` is `"number"` (fine) but the value is `3` — an int. In OCaml terms, multiplying a float by something that produces an integral float representable in a `Pervasives.int` triggers a narrowing. Consequence: any iterative float routine like `(let loop ((x 1.5) (n 100)) (if (<= n 0) x (loop (* x 2.0) (- n 1))))` overflows to 0 by n=60 because it's walking ints at that point. In JS terms this blocks: + - `Number.MAX_VALUE` — our `js-max-value-approx` loops 1.0 × 2 and overflows to 0; we can't compute a correct 1.7976931348623157e308 from inside the runtime + - `Number.MIN_VALUE` — same shape (loop 1.0 / 2 → 0 before reaching denormal 5e-324) + - Any literal `1e308` — the SX tokenizer parses `e308` but clips too + - `Math.pow(2, 100)` — same loop + Proper fix is spec-level: keep `Sx_types.Number` boxed as OCaml `float` until an explicit int cast happens, or introduce a separate `Sx_types.Int` path and a promotion rule. For js-on-sx, `Number.MAX_VALUE` tests are blocked until then. Works around ~6 Number failures and nontrivial surface in Math. + - **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.