Repository navigation
Description
Test I ran
A script that parses decimal strings with JSON.parse and compares each result with Number() of the same string, then checks a few inputs that ECMA-262 requires JSON.parse to accept.
node json_parse.ts
slice run json_parse.tsjson_parse.ts
// JSON.parse conformance: compare each result with the engine's own Number() / expected ECMA-262 behaviour.
const row = (k: string, v: unknown) => console.log(k.padEnd(44) + v);
for (const s of ["90.33333333333333", "14.185190207411157", "0.0075350713764946314", "95319.50955061219"]) {
const p = JSON.parse(s); row(`JSON.parse("${s}")`, `${p} ${p === Number(s) ? "ok" : "WRONG (Number() gives " + Number(s) + ")"}`);
}
let bad = 0;
for (let i = 1; i <= 20000; i++) { const x = i / 3; if (JSON.parse(String(x)) !== x) bad++; }
row("i/3 for i=1..20000, parse(String(x)) !== x", `${bad} / 20000`);
bad = 0; let seed = 12345; const rnd = () => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed / 0x7fffffff; };
for (let i = 0; i < 20000; i++) { const x = rnd() * Math.pow(10, Math.floor(rnd() * 12) - 4); if (JSON.parse(String(x)) !== x) bad++; }
row("20000 pseudo-random doubles, same check", `${bad} / 20000`);
const data = Array.from({ length: 3000 }, (_, i) => ({ id: i, x: i / 3 }));
const text = JSON.stringify(data);
row("JSON.stringify(JSON.parse(text)) === text", JSON.stringify(JSON.parse(text)) === text);
const attempt = (label: string, src: string, show: (v: any) => unknown) => { try { row(label, show(JSON.parse(src))); } catch (e: any) { row(label, "THROWS " + e.name + ": " + e.message); } };
attempt('JSON.parse("1e400")', "1e400", (v) => v);
attempt('JSON.parse("-1e400")', "-1e400", (v) => v);
attempt('JSON.parse(\'"\\\\ud800"\') (lone surrogate escape)', '"\\ud800"', (v) => "length " + v.length + ", charCode 0x" + v.charCodeAt(0).toString(16));
attempt("JSON.parse of 127 nested arrays", "[".repeat(127) + "]".repeat(127), () => "ok");
attempt("JSON.parse of 129 nested arrays", "[".repeat(129) + "]".repeat(129), () => "ok");
attempt("JSON.parse of 2000 nested arrays", "[".repeat(2000) + "]".repeat(2000), () => "ok");Result
Node v24.22.2:
JSON.parse("90.33333333333333") 90.33333333333333 ok
JSON.parse("14.185190207411157") 14.185190207411157 ok
JSON.parse("0.0075350713764946314") 0.0075350713764946314 ok
JSON.parse("95319.50955061219") 95319.50955061219 ok
i/3 for i=1..20000, parse(String(x)) !== x 0 / 20000
20000 pseudo-random doubles, same check 0 / 20000
JSON.stringify(JSON.parse(text)) === text true
JSON.parse("1e400") Infinity
JSON.parse("-1e400") -Infinity
JSON.parse('"\\ud800"') (lone surrogate escape)length 1, charCode 0xd800
JSON.parse of 127 nested arrays ok
JSON.parse of 129 nested arrays ok
JSON.parse of 2000 nested arrays ok
Slice v0.10.7:
JSON.parse("90.33333333333333") 90.33333333333331 WRONG (Number() gives 90.33333333333333)
JSON.parse("14.185190207411157") 14.185190207411155 WRONG (Number() gives 14.185190207411157)
JSON.parse("0.0075350713764946314") 0.007535071376494632 WRONG (Number() gives 0.0075350713764946314)
JSON.parse("95319.50955061219") 95319.5095506122 WRONG (Number() gives 95319.50955061219)
i/3 for i=1..20000, parse(String(x)) !== x 228 / 20000
20000 pseudo-random doubles, same check 1913 / 20000
JSON.stringify(JSON.parse(text)) === text false
JSON.parse("1e400") THROWS SyntaxError: number out of range at line 1 column 5
JSON.parse("-1e400") THROWS SyntaxError: number out of range at line 1 column 6
JSON.parse('"\\ud800"') (lone surrogate escape)THROWS SyntaxError: unexpected end of hex escape at line 1 column 8
JSON.parse of 127 nested arrays ok
JSON.parse of 129 nested arrays THROWS SyntaxError: recursion limit exceeded at line 1 column 128
JSON.parse of 2000 nested arrays THROWS SyntaxError: recursion limit exceeded at line 1 column 128
What is wrong
- Numbers are not correctly rounded. 1,913 of 20,000 pseudo-random doubles (9.6%) and 228 of the first 20,000 thirds come back 1–2 ULP away from the value
Number()produces for the same text, soJSON.parse(JSON.stringify(x))no longer round-trips. Any number with ~16–17 significant digits is affected, in a bare value, an array or an object.Response#json()andRequest#json()return the same wrong values (await new Response("90.33333333333333").json()gives90.33333333333331). 1e400/-1e400throwSyntaxError: number out of rangeinstead of returning±Infinity.- A lone surrogate escape (
"\ud800") throws instead of producing a one-code-unit string. - Nesting deeper than 128 throws
SyntaxError: recursion limit exceeded. If this is kept as a deliberate guard it should be documented; Node, Bun and Deno parse 2,000 levels.
Likely cause
All four behaviours are serde_json defaults. vendor/boa_engine/src/builtins/json/mod.rs (SLICE PATCH P01, JsonSeed) materialises values straight from serde_json::Deserializer, and serde_json is built without the float_roundtrip feature (cargo tree -e features -i serde_json shows only default). Without that feature serde_json documents its float parsing as best-effort rather than correctly rounded. Upstream Boa only uses serde_json to validate and then evaluates the text with the JS parser, which is why upstream does not have the precision problem.
Environment
- Slice
v0.10.7, commitea7c70c, built withcargo +1.96.0 build --release --locked --bin slice - Reference: Node.js
v24.22.2(same script, same machine) - Ubuntu 26.04, Linux 7.0, x86_64
- The same output is produced by the
v0.10.5tag, so this is not a regression in 0.10.6/0.10.7.
Activity
siktec-lab commentedon Nov 11, 2026
Fixed in PR #65 (
v0.10.9). Replaced theserde_jsonandboa_parserpaths inJSON.parsewith a direct iterative ECMA-404 parser (parse_ecma404) overLatin1(&[u8])/Utf16(&[u16]):fast-float2for exact IEEE-754 round-to-nearest-evenf64conversion (9007199254740993.0,1e23) and±Infinityon exponent overflow (1e400,-1e400).\uD800–\uDFFF) in bothJSON.parseandJSON.stringify.2048nesting levels via an explicit heap-allocated container stack (O(1)C/Rust call stack) and directPropertyMap::insert_data_propertyslot insertion.