diff --git a/Cargo.toml b/Cargo.toml index b2347107e..a373998a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,9 +68,16 @@ tokio = { version = "1" } futures-util = "0.3" phf = { version = "0.14", features = ["macros"] } serde = { version = "1", features = ["derive"] } -# float_roundtrip: exact f64 parsing so big literal values (e.g. integral -# doubles beyond u64) survive the expected.json round-trip byte-faithfully -serde_json = { version = "1", features = ["preserve_order", "float_roundtrip"] } +# preserve_order: keep object key order (wire field-order conformance). +# float_roundtrip: exact shortest f64 parsing. +# arbitrary_precision: `serde_json::Value` stores number tokens verbatim +# instead of collapsing them to f64, so the fixture pipeline (expected.json +# generation + validation, both of which round-trip through `Value` via +# `to_json_with_tabs`) compares the writer's exact number bytes against acorn's +# — a literal like `1e20`/`0.000001` no longer gets reformatted through ryu on +# both sides, which had masked `Number::toString` divergences. The production +# wire writer (`JsonWriter`) never touches `Value`, so its output is unchanged. +serde_json = { version = "1", features = ["preserve_order", "float_roundtrip", "arbitrary_precision"] } smallvec = "1.15.2" thiserror = "2" unicode-ident = "1.0" diff --git a/crates/tsv_ts/CLAUDE.md b/crates/tsv_ts/CLAUDE.md index 8d3fe5a01..579faae71 100644 --- a/crates/tsv_ts/CLAUDE.md +++ b/crates/tsv_ts/CLAUDE.md @@ -23,7 +23,7 @@ Standard `ast/` (internal + convert), `lexer/`, `parser/`, `printer/` layout — - `parse(source, arena: &'arena Bump) -> Result>` — the AST is bump-arena-allocated; the caller owns the `bumpalo::Bump` (caller-owns-arena, so the returned AST borrows it and the arena never escapes the call). See [docs/architecture.md §Nested AST](../../docs/architecture.md#nested-ast-bump-arena-not-flatindexed). `parse` is the `Goal::Module` form of `parse_with_goal(source, goal, arena)` — pass `Goal::Script` for a standalone strict script (`await` an ordinary identifier; `import`/`export`/`import.meta` errors). `Program.goal` drives the public `sourceType`. See **Strict mode only** below. - `parse_preserve_parens(source, arena) -> Result` — `parse` with grouping parens kept as `ParenthesizedExpression` nodes (acorn's `preserveParens: true`). The standalone analog of the embedding `parse_embedded_preserve_parens`; the binding audit (`tsv_debug binding_audit`) reparses formatted output this way so the paren structure a glued comment binds to is visible in the wire JSON. `is_jsdoc_type_cast_comment(content) -> bool` is exposed alongside it — the cast-content predicate the audit (and its own printer) classify a glued comment's anchor rule by. - `format(program, source) -> String` — the format entry point (output is identical for standalone `.ts` and Svelte-embedded TS). `format_in(program, source, &DocArena)` is the same but builds into a caller-provided doc arena, so multi-file drivers (CLI/FFI/NAPI) reuse one arena across files (`DocArena::reset()` between files); `format` is the fresh-arena wrapper -- `convert_ast_json_bytes(...) -> Vec` / `convert_ast_json_string(...) -> String` / `convert_ast_json(...) -> serde_json::Value` (all gated on `convert` feature). The bytes variant is the **sole emission path** — the compact-wire hot path (FFI/CLI non-pretty; the string variant is the same bytes plus one output UTF-8 validation, for `&str` boundaries — the WASM binding's `JSON.parse`, N-API strings). It is a **writer-mode conversion** (`ast/convert/write/`) that emits the wire JSON directly during a single walk of the *internal* AST — no typed public tree is ever materialized — and **fuses byte→UTF-16 offset translation into the walk**: the writer threads a `tsv_lang::LocationMapper` (tracker + `ByteToCharMap`) and emits final char-space `start`/`end`/`loc` directly, so no post-conversion translation walk runs (per-language pipeline shapes: [docs/architecture.md §Closed Scope, Open Convention](../../docs/architecture.md#closed-scope-open-convention)). The writer is a faithful emission of the acorn quirk catalog (field order, skip rules, scalar formatting; dynamic strings and non-integral floats delegate to `serde_json` so escaping matches exactly). `convert_ast_json` is a thin wrapper — `serde_json::from_slice(&convert_ast_json_bytes(...))` — for the `Value` consumers (the CLI's `--pretty`, the fixture gate), not an independent conversion. The oracle the writer is gated against is the canonical parser's `expected.json`, including the multibyte and ` diff --git a/tests/fixtures/typescript/expressions/literals/numeric/edge_cases/expected.json b/tests/fixtures/typescript/expressions/literals/numeric/edge_cases/expected.json index 25c05afb3..702c4578b 100644 --- a/tests/fixtures/typescript/expressions/literals/numeric/edge_cases/expected.json +++ b/tests/fixtures/typescript/expressions/literals/numeric/edge_cases/expected.json @@ -1268,7 +1268,7 @@ "column": 48 } }, - "value": 5.674724124163433e+20, + "value": 567472412416343300000, "raw": "0o75415030007726607105367" } } @@ -2026,7 +2026,7 @@ "column": 28 } }, - "value": 1e-6, + "value": 0.000001, "raw": "0.000001" } } diff --git a/tests/fixtures/typescript/expressions/objects/numeric_keys/expected.json b/tests/fixtures/typescript/expressions/objects/numeric_keys/expected.json index 6bb36b0f3..de8166f16 100644 --- a/tests/fixtures/typescript/expressions/objects/numeric_keys/expected.json +++ b/tests/fixtures/typescript/expressions/objects/numeric_keys/expected.json @@ -331,7 +331,7 @@ "column": 22 } }, - "value": 3.3627389621760085e+20, + "value": 336273896217600850000, "raw": "0x123abcdef456abcdef" }, "value": {