diff --git a/lib/js/.gitignore b/lib/js/.gitignore index cb52cc17..3c4d1c7a 100644 --- a/lib/js/.gitignore +++ b/lib/js/.gitignore @@ -1 +1,2 @@ test262-upstream/ +.harness-cache/ diff --git a/lib/js/test262-runner.py b/lib/js/test262-runner.py index 0fa15c97..49dbe999 100644 --- a/lib/js/test262-runner.py +++ b/lib/js/test262-runner.py @@ -55,6 +55,10 @@ HARNESS_DIR = UPSTREAM / "harness" DEFAULT_PER_TEST_TIMEOUT_S = 5.0 DEFAULT_BATCH_TIMEOUT_S = 120 +# Cache dir for precomputed SX source of harness JS (one file per Python run). +# Written once in main(), read via (load ...) by every worker session. +HARNESS_CACHE_DIR = REPO / "lib" / "js" / ".harness-cache" + # --------------------------------------------------------------------------- # Harness stub — replaces assert.js + sta.js with something our parser handles. @@ -588,6 +592,175 @@ def load_test(path: Path): ) +# --------------------------------------------------------------------------- +# Harness cache — transpile HARNESS_STUB once, write SX to disk. +# Every worker then loads the cached .sx (a few ms) instead of re-running +# js-tokenize + js-parse + js-transpile (15+ s). +# --------------------------------------------------------------------------- + +# Remembered across the Python process. None until we've run the precompute. +_HARNESS_CACHE_PATH: "Path | None" = None +# Per-filename include cache: maps 'compareArray.js' -> Path of cached .sx. +_EXTRA_HARNESS_CACHE: dict = {} + + +def _harness_cache_rel_path() -> "str | None": + if _HARNESS_CACHE_PATH is None: + return None + try: + return _HARNESS_CACHE_PATH.relative_to(REPO).as_posix() + except ValueError: + return str(_HARNESS_CACHE_PATH) + + +def _precompute_sx(js_source: str, timeout_s: float = 120.0) -> str: + """Run one throwaway sx_server to turn a chunk of JS into the SX text that + js-eval would have evaluated. Returns the raw SX source (no outer quotes). + """ + proc = subprocess.Popen( + [str(SX_SERVER)], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + cwd=str(REPO), + bufsize=0, + ) + fd = proc.stdout.fileno() + os.set_blocking(fd, False) + + buf = [b""] + + def readline(timeout: float): + deadline = time.monotonic() + timeout + while True: + nl = buf[0].find(b"\n") + if nl >= 0: + line = buf[0][: nl + 1] + buf[0] = buf[0][nl + 1 :] + return line + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TimeoutError("precompute readline timeout") + rlist, _, _ = select.select([fd], [], [], remaining) + if not rlist: + raise TimeoutError("precompute readline timeout") + try: + chunk = os.read(fd, 65536) + except (BlockingIOError, InterruptedError): + continue + if not chunk: + return None + buf[0] += chunk + + def run(epoch: int, cmd: str, to: float = 60.0): + proc.stdin.write(f"(epoch {epoch})\n{cmd}\n".encode("utf-8")) + proc.stdin.flush() + deadline = time.monotonic() + to + while time.monotonic() < deadline: + line = readline(deadline - time.monotonic()) + if line is None: + raise RuntimeError("precompute: sx_server closed stdout") + m = RX_OK_INLINE.match(line.decode("utf-8", "replace")) + if m and int(m.group(1)) == epoch: + return "ok", m.group(2) + m = RX_OK_LEN.match(line.decode("utf-8", "replace")) + if m and int(m.group(1)) == epoch: + val = readline(deadline - time.monotonic()) + return "ok", (val or b"").decode("utf-8", "replace").rstrip("\n") + m = RX_ERR.match(line.decode("utf-8", "replace")) + if m and int(m.group(1)) == epoch: + return "error", m.group(2) + raise TimeoutError(f"precompute epoch {epoch}") + + try: + # Wait for ready + deadline = time.monotonic() + 15.0 + while time.monotonic() < deadline: + line = readline(deadline - time.monotonic()) + if line is None: + raise RuntimeError("precompute: sx_server closed before ready") + if b"(ready)" in line: + break + # Load JS kernel + run(1, '(load "lib/r7rs.sx")') + run(2, '(load "lib/js/lexer.sx")') + run(3, '(load "lib/js/parser.sx")') + run(4, '(load "lib/js/transpile.sx")') + # Transpile to SX source via inspect + inner = js_source.replace("\\", "\\\\").replace('"', '\\"') + inner = inner.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t") + outer = inner.replace("\\", "\\\\").replace('"', '\\"') + cmd = f'(eval "(inspect (js-transpile (js-parse (js-tokenize \\"{outer}\\"))))")' + kind, payload = run(5, cmd, timeout_s) + if kind != "ok": + raise RuntimeError(f"precompute error: {payload[:200]}") + # payload is an SX string-literal — peel one layer of quoting. + import json as _json + if payload.startswith('"') and payload.endswith('"'): + return _json.loads(payload) + return payload + finally: + try: + proc.stdin.close() + except Exception: + pass + try: + proc.terminate() + proc.wait(timeout=3) + except Exception: + try: + proc.kill() + except Exception: + pass + + +def _harness_fingerprint() -> str: + import hashlib + # Include the exact runtime/transpile source hash so a change to the + # transpiler invalidates the cache automatically. + h = hashlib.sha256() + h.update(HARNESS_STUB.encode("utf-8")) + for p in ("lib/js/lexer.sx", "lib/js/parser.sx", "lib/js/transpile.sx"): + try: + h.update((REPO / p).read_bytes()) + except Exception: + pass + return h.hexdigest()[:16] + + +def precompute_harness_cache() -> Path: + """Populate _HARNESS_CACHE_PATH by transpiling HARNESS_STUB once and + writing it to disk. Every worker session then does (load ) instead. + + Reuses a prior cache file from a previous `python3 test262-runner.py` + run when the fingerprint (harness text + transpiler source hash) still + matches — that covers the common case of re-running scoreboards back-to-back + without touching transpile.sx. + """ + global _HARNESS_CACHE_PATH + HARNESS_CACHE_DIR.mkdir(parents=True, exist_ok=True) + fp = _harness_fingerprint() + dst = HARNESS_CACHE_DIR / f"stub.{fp}.sx" + stable = HARNESS_CACHE_DIR / "stub.sx" + if dst.exists() and dst.stat().st_size > 0: + # Expose both the canonical and fingerprinted names — sessions load + # the canonical one. + stable.write_bytes(dst.read_bytes()) + _HARNESS_CACHE_PATH = stable + print(f"harness cache: reused {dst.name} ({dst.stat().st_size} bytes)", + file=sys.stderr) + return stable + t0 = time.monotonic() + sx = _precompute_sx(HARNESS_STUB) + dst.write_text(sx, encoding="utf-8") + stable.write_text(sx, encoding="utf-8") + _HARNESS_CACHE_PATH = stable + dt = time.monotonic() - t0 + print(f"harness cache: {len(HARNESS_STUB)} JS chars → {len(sx)} SX chars " + f"at {stable.relative_to(REPO)} (fp={fp}, {dt:.2f}s)", file=sys.stderr) + return stable + + # --------------------------------------------------------------------------- # Long-lived server session # --------------------------------------------------------------------------- @@ -625,13 +798,18 @@ class ServerSession: self._run_and_collect(3, '(load "lib/js/parser.sx")', timeout=60.0) self._run_and_collect(4, '(load "lib/js/transpile.sx")', timeout=60.0) self._run_and_collect(5, '(load "lib/js/runtime.sx")', timeout=60.0) - # Preload the stub harness as one big js-eval - stub_escaped = sx_escape_for_nested_eval(HARNESS_STUB) - self._run_and_collect( - 6, - f'(eval "(js-eval \\"{stub_escaped}\\")")', - timeout=60.0, - ) + # Preload the stub harness — use precomputed SX cache when available + # (huge win: ~15s js-eval HARNESS_STUB → ~0s load precomputed .sx). + cache_rel = _harness_cache_rel_path() + if cache_rel is not None: + self._run_and_collect(6, f'(load "{cache_rel}")', timeout=60.0) + else: + stub_escaped = sx_escape_for_nested_eval(HARNESS_STUB) + self._run_and_collect( + 6, + f'(eval "(js-eval \\"{stub_escaped}\\")")', + timeout=60.0, + ) def stop(self) -> None: if self.proc is not None: @@ -964,6 +1142,14 @@ def main(argv): all_paths = all_paths[: args.limit] print(f"Discovered {len(all_paths)} test files.", file=sys.stderr) + # Precompute harness cache once per run. Workers (forked) inherit module + # globals, so the cache path is visible to every session.start() call. + try: + precompute_harness_cache() + except Exception as e: + print(f"harness cache precompute failed ({e}); falling back to js-eval per session", + file=sys.stderr) + tests = [] results = [] per_cat_count = defaultdict(int) diff --git a/lib/js/test262-scoreboard.json b/lib/js/test262-scoreboard.json index f794ef92..fc9e0008 100644 --- a/lib/js/test262-scoreboard.json +++ b/lib/js/test262-scoreboard.json @@ -1,12 +1,12 @@ { "totals": { - "pass": 118, - "fail": 160, + "pass": 115, + "fail": 174, "skip": 1597, - "timeout": 22, + "timeout": 11, "total": 1897, "runnable": 300, - "pass_rate": 39.3 + "pass_rate": 38.3 }, "categories": [ { @@ -35,46 +35,42 @@ { "category": "built-ins/Number", "total": 340, - "pass": 48, - "fail": 45, + "pass": 44, + "fail": 52, "skip": 240, - "timeout": 7, - "pass_rate": 48.0, + "timeout": 4, + "pass_rate": 44.0, "top_failures": [ [ "Test262Error (assertion failed)", - 42 + 52 ], [ "Timeout", - 7 - ], - [ - "ReferenceError (undefined symbol)", - 3 + 4 ] ] }, { "category": "built-ins/String", "total": 1223, - "pass": 30, - "fail": 56, + "pass": 31, + "fail": 63, "skip": 1123, - "timeout": 14, - "pass_rate": 30.0, + "timeout": 6, + "pass_rate": 31.0, "top_failures": [ [ "Test262Error (assertion failed)", - 43 + 53 ], [ "Timeout", - 14 + 6 ], [ "ReferenceError (undefined symbol)", - 7 + 2 ], [ "Unhandled: Not callable: \\\\\\", @@ -100,19 +96,19 @@ "top_failure_modes": [ [ "Test262Error (assertion failed)", - 108 + 128 ], [ "TypeError: not a function", - 36 + 37 ], [ "Timeout", - 22 + 11 ], [ "ReferenceError (undefined symbol)", - 10 + 2 ], [ "Unhandled: Not callable: \\\\\\", @@ -129,9 +125,13 @@ [ "Unhandled: Not callable: {:__proto__ {}} (kont=5 frames)\\", 1 + ], + [ + "Unhandled: js-transpile-binop: unsupported op: >>>\\", + 1 ] ], "pinned_commit": "d5e73fc8d2c663554fb72e2380a8c2bc1a318a33", - "elapsed_seconds": 429.3, + "elapsed_seconds": 288.0, "workers": 1 } \ No newline at end of file diff --git a/lib/js/test262-scoreboard.md b/lib/js/test262-scoreboard.md index 3869618c..09c381d4 100644 --- a/lib/js/test262-scoreboard.md +++ b/lib/js/test262-scoreboard.md @@ -1,36 +1,37 @@ # test262 scoreboard Pinned commit: `d5e73fc8d2c663554fb72e2380a8c2bc1a318a33` -Wall time: 429.3s +Wall time: 288.0s -**Total:** 118/300 runnable passed (39.3%). Raw: pass=118 fail=160 skip=1597 timeout=22 total=1897. +**Total:** 115/300 runnable passed (38.3%). Raw: pass=115 fail=174 skip=1597 timeout=11 total=1897. ## Top failure modes -- **108x** Test262Error (assertion failed) -- **36x** TypeError: not a function -- **22x** Timeout -- **10x** ReferenceError (undefined symbol) +- **128x** Test262Error (assertion failed) +- **37x** TypeError: not a function +- **11x** Timeout +- **2x** ReferenceError (undefined symbol) - **2x** Unhandled: Not callable: \\\ - **2x** Unhandled: Not callable: {:__proto__ {}} (kont=6 frames)\ - **1x** SyntaxError (parse/unsupported syntax) - **1x** Unhandled: Not callable: {:__proto__ {}} (kont=5 frames)\ +- **1x** Unhandled: js-transpile-binop: unsupported op: >>>\ ## Categories (worst pass-rate first, min 10 runnable) | Category | Pass | Fail | Skip | Timeout | Total | Pass % | |---|---:|---:|---:|---:|---:|---:| -| built-ins/String | 30 | 56 | 1123 | 14 | 1223 | 30.0% | +| built-ins/String | 31 | 63 | 1123 | 6 | 1223 | 31.0% | | built-ins/Math | 40 | 59 | 227 | 1 | 327 | 40.0% | -| built-ins/Number | 48 | 45 | 240 | 7 | 340 | 48.0% | +| built-ins/Number | 44 | 52 | 240 | 4 | 340 | 44.0% | ## Per-category top failures (min 10 runnable, worst first) -### built-ins/String (30/100 — 30.0%) +### built-ins/String (31/100 — 31.0%) -- **43x** Test262Error (assertion failed) -- **14x** Timeout -- **7x** ReferenceError (undefined symbol) +- **53x** Test262Error (assertion failed) +- **6x** Timeout +- **2x** ReferenceError (undefined symbol) - **2x** Unhandled: Not callable: \\\ - **2x** Unhandled: Not callable: {:__proto__ {}} (kont=6 frames)\ @@ -40,8 +41,7 @@ Wall time: 429.3s - **23x** Test262Error (assertion failed) - **1x** Timeout -### built-ins/Number (48/100 — 48.0%) +### built-ins/Number (44/100 — 44.0%) -- **42x** Test262Error (assertion failed) -- **7x** Timeout -- **3x** ReferenceError (undefined symbol) +- **52x** Test262Error (assertion failed) +- **4x** Timeout diff --git a/lib/js/transpile.sx b/lib/js/transpile.sx index 42c9dd4d..fbbb452b 100644 --- a/lib/js/transpile.sx +++ b/lib/js/transpile.sx @@ -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