Skip to content

PERRY_JSON_TAPE=0 direct parser loses a ULP on JSON floats — round-trip is not byte-stable (tape path and node are) #7266

Description

@proggeramlug

Found while verifying #7264 (fixed in #7265). Separate bug, separate path — filing so it isn't lost.

With PERRY_JSON_TAPE=0 (the direct recursive-descent parser, used as a bisection escape hatch and as the perry (mark-sweep, no lazy) row in benchmarks/json_polyglot), a JSON.parseJSON.stringify round-trip does not preserve the shortest round-trippable double. The default tape path and node both do.

Reproducer

const items: any[] = [];
for (let i = 0; i < 10000; i++) items.push({ id: i, name: "item_" + i, value: i * 3.14159, tags: ["t"], nested: { x: i, y: i * 2 } });
const blob = JSON.stringify(items);
const parsed = JSON.parse(blob);
let s = 0;
for (let i = 0; i < parsed.length; i++) s += parsed[i].nested.x;   // force materialization
const re = JSON.stringify(parsed);
console.log("len=" + re.length + " same=" + (re === blob));
perry (default, tape)      len=944711 same=true
perry PERRY_JSON_TAPE=0    len=944728 same=false
node 26.5.1                len=944711 same=true

First divergence:

blob: ..."name":"item_83","value":260.75197,...
re  : ..."name":"item_83","value":260.75197000000003,...

So the direct parser produces a double one ULP away from the one the tape parser (and node) produce for the same text 260.75197, and the re-serialization is 17 characters longer per 10k-record blob.

Impact

Likely area: the number-token → f64 conversion in crates/perry-runtime/src/json/parser.rs, versus materialize_number in crates/perry-runtime/src/json_tape.rs. One of them is not using a correctly-rounded decimal→binary conversion.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions