The DirectParser and the tape materializer produce different values from the same JSON text, and node agrees with the tape. On benchmarks/json_polyglot/bench_field_access.ts (10k records, values like i * 3.14159, checksum = sum of nested.x + stringify lengths over 50 iterations):
| parse path |
checksum |
| node v22.23.1 |
2552985550 |
tape materializer (PERRY_JSON_TAPE=1 / auto) |
2552985550 ✅ |
DirectParser (PERRY_JSON_TAPE=0) |
2552986400 ✗ |
Reproduce: PERRY_JSON_TAPE=0 vs =1 on that benchmark; the divergence is deterministic and survives PERRY_GEN_GC=0, so it is the parser, not GC.
Why it matters beyond the flag
- The DirectParser is the default for real workloads: every blob under 1 KB and over 16 MB, plus every non-top-level-array shape, parses through it (
try_parse_via_tape's auto window). Those all get the divergent values today, silently.
- It blocks the most promising
field_access optimization. force_materialize_lazy could re-parse the retained blob with the DirectParser (~2.3× faster than the per-element tape materializer — see the companion perf issue), and a prototype of exactly that produced the wrong checksum because of this divergence. Verified: swapping the materializer for the DirectParser flips the checksum from node-matching to divergent.
Where to look
Both paths parse JSON numbers from the same bytes, so one of the two float-parsing routines rounds differently (likely a hand-rolled decimal-to-f64 in one path vs a stricter algorithm in the other). Since node (V8's strtod) agrees with the tape side, the DirectParser's number parsing is the suspect. stringify lengths also feed the checksum, so a float→string difference downstream of parse is the alternative — distinguishing the two is the first step: sum nested.x alone (pure parse) vs stringify-length alone.
The DirectParser and the tape materializer produce different values from the same JSON text, and node agrees with the tape. On
benchmarks/json_polyglot/bench_field_access.ts(10k records, values likei * 3.14159, checksum = sum ofnested.x+ stringify lengths over 50 iterations):PERRY_JSON_TAPE=1/ auto)PERRY_JSON_TAPE=0)Reproduce:
PERRY_JSON_TAPE=0vs=1on that benchmark; the divergence is deterministic and survivesPERRY_GEN_GC=0, so it is the parser, not GC.Why it matters beyond the flag
try_parse_via_tape's auto window). Those all get the divergent values today, silently.field_accessoptimization.force_materialize_lazycould re-parse the retained blob with the DirectParser (~2.3× faster than the per-element tape materializer — see the companion perf issue), and a prototype of exactly that produced the wrong checksum because of this divergence. Verified: swapping the materializer for the DirectParser flips the checksum from node-matching to divergent.Where to look
Both paths parse JSON numbers from the same bytes, so one of the two float-parsing routines rounds differently (likely a hand-rolled decimal-to-f64 in one path vs a stricter algorithm in the other). Since node (V8's strtod) agrees with the tape side, the DirectParser's number parsing is the suspect.
stringifylengths also feed the checksum, so a float→string difference downstream of parse is the alternative — distinguishing the two is the first step: sumnested.xalone (pure parse) vs stringify-length alone.