Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 45s
570 lines
143 KiB
Markdown
570 lines
143 KiB
Markdown
# JS-on-SX: cherry-picked test262 conformance
|
||
|
||
Transpile a restricted JS subset to SX AST, run on the existing CEK/VM. Goal is runtime-hardening via conformance tests, plus a second hyperscript substrate (hyperscript.js running on SX).
|
||
|
||
## Ground rules for the loop
|
||
|
||
- **Scope:** only touch `lib/js/**` and `plans/js-on-sx.md`. Do **not** edit `spec/evaluator.sx`, `spec/primitives.sx`, `shared/sx/**`, or `lib/hyperscript/**` — the user is working on those elsewhere.
|
||
- **Shared-file issues** go under "Blockers" below with a minimal repro; do not fix them from this loop.
|
||
- **SX files:** use `sx-tree` MCP tools only (never `Edit`/`Read`/`Write` on `.sx` files). Use `sx_write_file` for new files, path-based or pattern-based edits for changes.
|
||
- **Architecture:** JS source → JS AST → SX AST → existing CEK. No standalone JS evaluator — reuse everything.
|
||
- **Tests:** mirror `lib/hyperscript/test.sh` pattern. Start with a tiny in-repo smoke suite; graduate to a cherry-picked test262 slice once the lexer+expression-parser cycle is green.
|
||
|
||
## The loop
|
||
|
||
A single long-running background agent works `plans/js-on-sx.md` forever, one feature per commit. It runs a prioritized queue driven by the real test262 scoreboard. The briefing lives at `plans/agent-briefings/loop.md`; the recovery helper is `plans/restore-loop.sh`.
|
||
|
||
The queue (condensed — full version in the briefing):
|
||
|
||
1. Baseline commit (stage what's on disk now).
|
||
2. Fix `lib/js/test262-runner.py` so it produces a real scoreboard.
|
||
3. Full scoreboard run across the whole `test/` tree.
|
||
4. Regex lexer/parser/runtime stub + Blockers entry listing platform primitives needed.
|
||
5. Scoreboard-driven: pick the worst-passing category each iteration; fix; re-score.
|
||
6. When the scoreboard plateaus, tackle deferred items (ASI, CEK-suspend await).
|
||
|
||
### Crash recovery
|
||
|
||
The agent process dies with the machine. **Work survives if it was committed** — that's why every iteration ends in a commit. After a crash:
|
||
|
||
```bash
|
||
bash plans/restore-loop.sh # show state: tests, commits, scoreboard, regex hook
|
||
bash plans/restore-loop.sh --print # also cat the briefing for easy copy-paste
|
||
```
|
||
|
||
Then respawn the agent by pasting `plans/agent-briefings/loop.md` into Claude Code via the `Agent` tool with `run_in_background=true`. The agent re-reads the plan, picks up wherever the queue has got to, and carries on.
|
||
|
||
## Architecture sketch
|
||
|
||
```
|
||
JS source text
|
||
│
|
||
▼
|
||
lib/js/lexer.sx — tokens: {:type :value :pos}
|
||
│
|
||
▼
|
||
lib/js/parser.sx — JS AST as SX trees, e.g. (js-binop "+" left right)
|
||
│
|
||
▼
|
||
lib/js/transpile.sx — JS AST → SX AST (reusable by CEK)
|
||
│ e.g. (js-binop "+" l r) → (+ (transpile l) (transpile r))
|
||
▼
|
||
existing CEK / VM — evaluation, async/IO, exceptions already handled
|
||
```
|
||
|
||
Runtime shims in `lib/js/runtime.sx`: `js-global`, `js-console-log`, `js-typeof`, coercion helpers (`to-number`, `to-string`, `to-boolean`, abstract-equality), eventually the prototype chain.
|
||
|
||
## Roadmap
|
||
|
||
Each item: implement → tests → update progress. Mark `[x]` when tests green.
|
||
|
||
### Phase 1 — Lexer
|
||
- [x] Numeric literals (int, float, hex, exponent)
|
||
- [x] String literals (double, single, escape sequences, template strings later)
|
||
- [x] Identifiers + reserved words
|
||
- [x] Punctuation: `( ) { } [ ] , ; : . ...`
|
||
- [x] Operators: `+ - * / % ** = == === != !== < > <= >= && || ! ?? ?: & | ^ ~ << >> >>> += -= ...`
|
||
- [x] Comments (`//`, `/* */`)
|
||
- [x] Automatic Semicolon Insertion (defer — initially require semicolons)
|
||
|
||
### Phase 2 — Expression parser (Pratt-style)
|
||
- [x] Literals → AST nodes
|
||
- [x] Binary operators with precedence
|
||
- [x] Unary operators (`- + ! ~ typeof void`)
|
||
- [x] Member access (`.`, `[]`)
|
||
- [x] Function calls
|
||
- [x] Array literals
|
||
- [x] Object literals (string/ident keys, shorthand later)
|
||
- [x] Conditional `a ? b : c`
|
||
- [x] Arrow functions (expression body only)
|
||
|
||
### Phase 3 — Transpile to SX AST
|
||
- [x] Numeric/string/bool/null literals → SX literals
|
||
- [x] Arithmetic: `+ - * / % **` with numeric coercion (`js-add` does string-concat dispatch)
|
||
- [x] Comparisons: `=== !== == != < > <= >=`
|
||
- [x] Logical: `&& ||` (short-circuit, value-returning via thunk trick); `??` nullish coalesce
|
||
- [x] Unary: `- + ! ~ typeof void`
|
||
- [x] Member access → `js-get-prop` (handles dicts, lists `.length`/index, strings)
|
||
- [x] Array literal → `(list ...)`
|
||
- [x] Object literal → `(dict)` + `dict-set!` sequence
|
||
- [x] Function call → SX call (callee can be ident, member, arrow)
|
||
- [x] Arrow fn → `(fn ...)` (params become SX symbols; closures inherited)
|
||
- [x] Assignment (`=` and compound `+= -= ...`) on ident/member/index targets
|
||
- [x] `js-eval` end-to-end: source → tokens → AST → SX → `eval-expr`
|
||
|
||
### Phase 4 — Runtime shims (`lib/js/runtime.sx`)
|
||
- [x] `js-typeof`, `js-to-number`, `js-to-string`, `js-to-boolean`
|
||
- [x] Abstract equality (`js-loose-eq`) vs strict (`js-strict-eq`)
|
||
- [x] Arithmetic (`js-add` `js-sub` `js-mul` `js-div` `js-mod` `js-pow` `js-neg` `js-pos`)
|
||
- [x] Logical (`js-and` `js-or` via thunks for lazy rhs) and `js-not` / `js-bitnot`
|
||
- [x] Relational (`js-lt` `js-gt` `js-le` `js-ge`) incl. lexicographic strings
|
||
- [x] `js-get-prop` / `js-set-prop` (dict/list/string; `.length`; numeric index)
|
||
- [x] `console.log` → `log-info` bridge (`console` dict wired)
|
||
- [x] `Math` object shim (`abs` `floor` `ceil` `round` `max` `min` `random` `PI` `E`)
|
||
- [x] `js-undefined` sentinel (keyword) distinct from `nil` (JS `null`)
|
||
- [x] Number parser `js-num-from-string` (handles int/float/±sign, no NaN yet)
|
||
|
||
### Phase 5 — Conformance harness
|
||
- [x] Cherry-picked slice vendored at `lib/js/test262-slice/` (69 fixtures across 7 categories)
|
||
- [x] Harness `lib/js/conformance.sh` — batch-load kernel, one epoch per fixture, substring-compare
|
||
- [x] Initial target: ≥50% pass. **Actual: 69/69 (100%).**
|
||
|
||
### Phase 6 — Statements
|
||
- [x] `var`/`let`/`const` declarations (all behave as `define` — block scope via SX lexical semantics)
|
||
- [x] `if`/`else`
|
||
- [x] `while`, `do..while`
|
||
- [x] `for (init; cond; step)`
|
||
- [x] `return`, `break`, `continue` (via `call/cc` continuation bindings `__return__` / `__break__` / `__continue__`)
|
||
- [x] Block scoping (via `begin` — lexical scope inherited, no TDZ)
|
||
|
||
### Phase 7 — Functions & scoping
|
||
- [x] `function` declarations (with call/cc-wrapped bodies for `return`)
|
||
- [x] Function expressions (named + anonymous)
|
||
- [x] Hoisting — function decls hoisted to enclosing scope (scan body first, emit defines ahead of statements)
|
||
- [x] Closures — work via SX `fn` env capture
|
||
- [x] Rest params (`...rest` → `&rest`)
|
||
- [x] Default parameters (desugar to `if (param === undefined) param = default`)
|
||
- [x] `var` hoisting (shallow — collects direct `var` decls, emits `(define name :js-undefined)` before funcdecls)
|
||
- [ ] `let`/`const` TDZ (deferred)
|
||
|
||
### Phase 8 — Objects, prototypes, `this`
|
||
- [x] Property descriptors (simplified — plain-dict `__proto__` chain, `js-set-prop` mutates)
|
||
- [x] Prototype chain lookup (`js-dict-get-walk` walks `__proto__`)
|
||
- [x] `this` binding rules: method call, function call (undefined), arrow (lexical)
|
||
- [x] `new` + constructor semantics (fresh dict, proto linked, ctor called with `this`)
|
||
- [x] ES6 classes (sugar over prototypes, incl. `extends`)
|
||
- [x] Array mutation: `a[i] = v`, `a.push(...)` — via `set-nth!` / `append!`
|
||
- [x] Array builtins: push, pop, shift, slice, indexOf, join, concat, map, filter, forEach, reduce
|
||
- [x] String builtins: charAt, charCodeAt, indexOf, slice, substring, toUpperCase, toLowerCase, split, concat
|
||
- [x] `instanceof` and `in` operators
|
||
|
||
### Phase 9 — Async & Promises
|
||
- [x] Promise constructor + `.then` / `.catch` / `.finally`
|
||
- [x] `Promise.resolve` / `Promise.reject` / `Promise.all` / `Promise.race`
|
||
- [x] `async` functions (decl, expr, arrow) return Promises
|
||
- [x] `await` — synchronous-ish: drains microtasks, unwraps settled Promise
|
||
- [x] Microtask queue with FIFO drain (`__drain()` exposed to JS)
|
||
- [ ] True CEK suspension on `await` for pending Promises (deferred — needs cek-step-loop plumbing)
|
||
|
||
### Phase 10 — Error handling
|
||
- [x] `throw` statement → `(raise v)`
|
||
- [x] `try`/`catch`/`finally` (desugars to `guard` + optional finally wrapper)
|
||
- [x] Error hierarchy (`Error`, `TypeError`, `RangeError`, `SyntaxError`, `ReferenceError` as constructor shims)
|
||
|
||
### Phase 11 — Stretch / deferred
|
||
- ASI, regex literals, generators, iterators, destructuring, template strings with `${}`, tagged templates, Symbol, Proxy, typed arrays, ESM modules.
|
||
|
||
## Progress log
|
||
|
||
Append-only record of completed iterations. Loop writes one line per iteration: date, what was done, test count delta.
|
||
|
||
- 2026-05-10 — **test262-runner inlines small upstream harness includes (`nans.js`, `sta.js`, `byteConversionValues.js`, `compareArray.js`) per-test.** The runner parsed `includes:` frontmatter but never used it, so tests like `built-ins/isNaN/return-true-nan.js` (which depends on `var NaNs = [...]`) failed with "ReferenceError: undefined symbol". Added `_load_harness_include` (cached) and `assemble_source` now prepends each allowlisted include's source to the test. Allowlist excludes large helpers like `propertyHelper.js` because per-test js-eval+JIT cost on a 371-line harness pushes tests over the 15s per-test timeout (regressed Math/abs 7/7 → 4/7 in a first-pass attempt before allowlisting). Result: built-ins/isNaN 2/7 → 3/7. conformance.sh: 148/148.
|
||
|
||
- 2026-05-10 — **Real `Date.prototype.setFullYear/setMonth/setDate/setHours/setMinutes/setSeconds/setMilliseconds` (+ UTC variants) and a corrected `setTime`.** All Date setters were missing — only `setTime` existed and didn't validate. Added a unified `js-date-setter(d, field, args)` that decomposes the current ms into `(y mo da hh mm ss msv)` via `js-date-decompose`, splices in the `args` per the field's optional-arg contract (e.g. `setHours(h, m?, s?, ms?)`), recomposes via `js-date-civil-to-days`, and TimeClips at ±8.64e15. NaN args anywhere → ms set to NaN. Wired all 14 setters to the helper. Hit a parser gotcha: SX `cond` clause body is single-form only — multi-expression bodies like `(else (dict-set! ...) new-ms)` silently treat the second form as `(<first-result> new-ms)` ("Not callable: false"). Wrapped these in `(begin ...)`. Result: setFullYear 5/18 → 13/18 (+8). setHours 5/21 → 15/21 (+10). setMonth 3/15 → 9/15 (+6). setMinutes 4/16 → 10/16 (+6). setSeconds 3/15 → 9/15 (+6). setDate 2/12 → 6/12 (+4). setMilliseconds 2/12 → 6/12 (+4). setTime 4/9 → 6/9 (+2). conformance.sh: 148/148.
|
||
|
||
- 2026-05-10 — **`Object.assign` keys now visible to `Object.keys` / `JSON.stringify`.** `Object.assign({}, {a:1})` was mutating the target via `dict-set!` which bypasses our `__js_order__` insertion-order side table; `Object.keys(t)` (which iterates `__js_order__` when present) returned `[]`, and `JSON.stringify` saw nothing. Switched `js-object-assign` to use `js-set-prop` (which calls `js-obj-order-add!` on new keys) for both dict and string sources. Result: built-ins/Object/assign 13/25 → 14/25. conformance.sh: 148/148.
|
||
|
||
- 2026-05-10 — **User functions' `prototype` chain through Object.prototype + auto-set `constructor`.** Per ES spec, every function's `prototype` slot defaults to `{ constructor: F, __proto__: Object.prototype }`. Our `js-get-ctor-proto` lazily created a fresh empty `(dict)` for user functions on first access — so `(new F) instanceof Object` was `false`, `F.prototype.constructor` was undefined, and `x.constructor === F` failed. Now the lazy-init seeds the proto with `__proto__ → Object.prototype` and `constructor → F` before caching in `__js_proto_table__`. Result: language/expressions/instanceof 25/30 → 26/30. conformance.sh: 148/148.
|
||
|
||
- 2026-05-10 — **Postfix `++`/`--` reject a preceding LineTerminator (ASI).** Per ES spec, `x\n++;` is a syntax error: no LineTerminator allowed between LHS and postfix `++`/`--`. Our `jp-parse-postfix` was matching `++`/`--` regardless of whether the preceding token had `:nl true`. Added `(not (jp-token-nl? st))` guard so newline-before-`++` makes the postfix arm fall through, the `++` then becomes a prefix-expr starting a new statement, which fails to parse and the runner classifies as SyntaxError. Result: language/expressions/postfix-increment 16/30 → 18/30 (+2). postfix-decrement 16/30 → 18/30 (+2). conformance.sh: 148/148.
|
||
|
||
- 2026-05-10 — **Parse-time SyntaxError when `let`/`const`/`function`/`class` appear as a single-statement body of `if`/`while`/`do`/`for`/labeled.** Per ES grammar, those positions accept a Statement, not a Declaration — only block bodies (`{ ... }`) may contain Declarations. Added `jp-disallow-decl-stmt!` helper that, when the next token is a Declaration keyword in single-statement context, raises SyntaxError. The `let` arm checks for `let <ident>`, `let [`, or `let {` to avoid mis-rejecting `let;` (where `let` is just an identifier expression). Hook calls in `jp-parse-if-stmt` (then + else branches), `jp-parse-while-stmt`, `jp-parse-do-while-stmt`, both for-of/in and C-for body sites, and the labeled-statement entry. Result: language/statements/while 16/30 → 20/30. statements/labeled 4/15 → 7/15. statements/if 20/30 → 21/30. conformance.sh: 148/148.
|
||
|
||
- 2026-05-10 — **Parse-time SyntaxError for `break`/`continue` outside loops/switches and `return` outside functions; `void <expr>` evaluates `<expr>` for side effects.** Parser tracks `:loop-depth`, `:switch-depth`, and `:fn-depth` on the state dict (initialized to 0). `jp-parse-while-stmt`, `jp-parse-do-while-stmt`, `jp-parse-for-stmt` (both for-of/in and C-for) bump `:loop-depth` around body parsing; `jp-parse-switch-stmt` bumps `:switch-depth`; new `jp-parse-fn-body` and `jp-parse-arrow-body` save+reset loop/switch depth and bump `:fn-depth` (so `break` inside an outer loop's nested function is rejected). Bare `break` requires `loop-depth > 0 OR switch-depth > 0`; bare `continue` requires `loop-depth > 0`; `return` requires `fn-depth > 0`. Separately, `void <expr>` was compiling to just `:js-undefined` (dropping the expression entirely); now `(begin <expr> :js-undefined)` so side effects fire. Result: language/statements/return 4/15 → 14/15 (+10). statements/break 9/20 → 12/20. statements/continue 12/24 → 15/24. expressions/void 7/9 → 8/9. conformance.sh: 148/148.
|
||
|
||
- 2026-05-10 — **`Math.hypot` and `Math.cbrt` honour spec edges for NaN, ±Infinity, and ±0.** `Math.hypot(NaN, Infinity)` was returning NaN instead of +Infinity (spec: any ±Infinity arg dominates NaN). Rewrote `js-math-hypot` to scan args once tracking inf/nan flags, return +Infinity if any arg is ±Infinity, else NaN if any was NaN, else `sqrt(sum of squares)`. `Math.cbrt(NaN)` was 0 (because `pow(NaN, 1/3)` produced 0 in our path); also `Math.cbrt(-0)` returned +0 instead of -0. Added explicit short-circuits: NaN→NaN, ±Infinity→arg, ±0→arg, plus changed `(/ 1 3)` (rational) to `(/ 1.0 3.0)` (inexact) to avoid rational fractional-power oddities. Result: built-ins/Math/hypot 9/11 → 10/11. Math/cbrt 3/4 → 4/4. conformance.sh: 148/148.
|
||
|
||
- 2026-05-10 — **`globalThis.globalThis === globalThis`; `Number.prototype.toFixed` honours digit-range and ≥1e21 fallback.** (1) `globalThis` was bound to `nil` in the global object literal (originally to dodge an inspect-cycle hang) — added `(dict-set! js-global "globalThis" js-global)` after the literal so `globalThis.globalThis === globalThis` per spec. (2) `Number.prototype.toFixed` rewrites: RangeError when fractionDigits is NaN or outside `[0,100]` (was silently producing garbage), and for `|x| >= 1e21` returns `js-number-to-string` (the value's own ToString) per spec step 9. conformance.sh: 148/148.
|
||
|
||
- 2026-05-10 — **`delete <ident>` returns `false` instead of `true` per non-strict spec.** ES non-strict semantics: `delete x` where `x` is a declared binding (variable / function / parameter) returns `false` and does not unbind. Our transpiler was emitting `true` for any `delete <expr>` whose argument wasn't a member or index access. Now `delete <js-ident>` → `false`, and `delete <js-paren expr>` recurses on the inner expression so `delete (1+2)` still works. Result: language/expressions/delete 14/30 → 18/30 (+4). conformance.sh: 148/148.
|
||
|
||
- 2026-05-10 — **Parser rejects unary-op directly before `**` (e.g. `-1 ** 2`, `delete o.p ** 2`, `!x ** 2`, `~x ** 2`) per ES spec.** ES disallows `UnaryExpression ** ExponentiationExpression`; only `UpdateExpression ** ExponentiationExpression` and `(<UnaryExpr>) ** ...` are legal. Added a guard in `jp-binary-loop`: when op is `**` and the LHS is a `(js-unop ...)` node, raise SyntaxError. Parens are made transparent for everything except this check via a new `jp-paren-wrap` helper that emits `(js-paren <unop>)` only when wrapping an explicit unary op (so `(-1) ** 2` parses fine), and a new `js-paren` AST tag in `js-transpile` that just unwraps. Result: language/expressions/exponentiation 25/30 → 28/30 (+3). conformance.sh: 148/148.
|
||
|
||
- 2026-05-10 — **`Math.round` / `Math.max` / `Math.min` honour spec edge cases for NaN, ±Infinity, and ±0.** `Math.round(NaN)` was returning 0 because `floor(NaN+0.5)` doesn't propagate NaN; ditto `±Infinity` paths. `Math.max({})` silently returned `-Infinity` (initial accumulator) because the first arg wasn't ToNumber'd. `Math.max(0, -0)` returned `-0` because `>` doesn't distinguish them. Rewrites: round NaN/±Infinity/±0 short-circuits; max/min ToNumber the first arg, propagate NaN immediately, and use a `js-is-positive-zero?` (rational-safe) tiebreaker so `Math.max(0, -0) === 0` per spec. Result: built-ins/Math/round 5/10 → 8/10 (+3). Math/max 6/9 → 8/9 (+2). Math/min 6/9 → 8/9 (+2). conformance.sh: 148/148.
|
||
|
||
- 2026-05-10 — **`Map.prototype.*` and `Set.prototype.*` raise TypeError when called on non-Map / non-Set `this`.** All five `js-map-do-*` and four `js-set-do-*` helpers were assuming `this` had `__map_keys__` / `__set_items__`, so `Map.prototype.clear.call({})` silently returned undefined (after creating dangling state) instead of throwing. Added `js-map-check!` / `js-set-check!` guards run as the first step of each method; raise spec-correct `TypeError` instances. Result: built-ins/Map 18/30 → 22/30 (+4). built-ins/Set 15/30 → 28/30 (+13). conformance.sh: 148/148.
|
||
|
||
- 2026-05-10 — **`Date.UTC` / `new Date(...)` propagate NaN/±Infinity arguments and return NaN.** `Date.UTC()` (no args) returned 0 instead of NaN; `Date.UTC(NaN, ...)` did the math and produced bogus ms; `new Date(year, NaN)` constructed a normal Date instead of an invalid one. Added `js-date-args-have-nan?` (also detects ±Infinity and propagates from rationals) used by both `Date.UTC` and the multi-arg constructor branch; UTC now returns NaN on no-arg / any-NaN-arg / out-of-range result, and `new Date(args)` stores NaN in `__date_value__` when any arg is NaN. Also fixed `js-date-from-one(undefined)` to return NaN. Result: built-ins/Date/UTC 6/16 → 10/16 (+4). Date 17/30 → 26/30 (timeouts dropped from 12 → 4 because invalid Dates now short-circuit). conformance.sh: 148/148.
|
||
|
||
- 2026-05-10 — **Real `Date` construction + getters via Howard-Hinnant civil-day arithmetic.** `js-date-from-parts` now computes a true ms-since-epoch from `(year, month, day, hour, min, sec, ms)` via `js-date-civil-to-days` (the inverse of last iteration's `days-to-ymd`), with the legacy 2-digit-year coercion (0..99 → 1900+y). `getFullYear/Month/Date/Day/Hours/Minutes/Seconds/Milliseconds` (UTC + non-UTC) all share a new `js-date-getter`: TypeErrors on non-Date this, returns NaN on invalid time, otherwise decomposes ms into y/m/d/h/m/s/ms/dow. Plus added `Date.prototype.constructor = Date` (was missing). Result: each of the 8 Date getter categories went 2/6 → 5/6 (+3 each, +24 total). Date toISOString 11/16 → 13/16. Some Date construction-loop tests now exceed the 15s per-test timeout — the new civil math is heavier than the old (year-1970)*ms-per-year approximation, but correctness wins. conformance.sh: 148/148.
|
||
|
||
- 2026-05-10 — **`Date.prototype.toISOString` produces real `YYYY-MM-DDTHH:mm:ss.sssZ` and validates input.** Old `js-date-iso` only computed the year and hardcoded the rest as `01-01T00:00:00.000Z`. Added: (1) TypeError when this isn't a Date (no `__js_is_date__` slot); (2) RangeError when ms is NaN, undefined, or |ms| > 8.64e15; (3) full date breakdown via Howard-Hinnant `days_to_civil` algorithm (`js-date-days-to-ymd`) → year/month/day, plus modular hours/min/sec/ms; (4) extended-year format `±YYYYYY` for years outside 0..9999. Result: built-ins/Date/prototype/toISOString 7/16 → 11/16 (+4). Date 21/30. conformance.sh: 148/148.
|
||
|
||
- 2026-05-10 — **`JSON.stringify` honours `replacer` (function + array forms), `space`, and `toJSON`.** Previous impl ignored the second/third arguments entirely and never called `toJSON`. Rewrote around a `js-json-serialize-property(key, holder, rep-fn, rep-keys, gap, indent)` core: walks `toJSON` first, then replacer-fn (with `holder` as `this`); arrays-as-replacer become a property-name allowlist; numeric `space` clamped to 0..10 spaces, string `space` truncated to 10 chars, non-empty gap activates indented output with `:` → `: ` separator. Number wrapper / String wrapper / Boolean wrapper unwrap before serialization; non-finite numbers serialize as `"null"`; functions serialize as `undefined`. Result: built-ins/JSON/stringify 6/30 → 14/30 (+8). conformance.sh: 148/148.
|
||
|
||
- 2026-05-10 — **`JSON.parse` raises spec-correct `SyntaxError` instances and rejects malformed input.** Previously `JSON.parse("12 34")` silently returned `12` (no trailing-content check), `JSON.parse('" |