Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,12 @@ bracketed type region does this (`{}`, `<>`, tuple `[]`, function-type `()`, ind
sanctioned exception is a union / intersection member a `|`/`&` separator still **follows**,
whose per-member break ends the line where the shell ends
(`Printer::type_member_separator_follows`); the last member has no separator and retains.
The break that sanctioned strip forces is **flush-scoped** (`DocArena::flush_break`, not
`break_parent`): only the group the deferred run actually flushes in breaks — an
intermediate composite with no line after the suffix stays flat, since forcing it was a
break the reparse could not reproduce. Unscoped `break_parent` after a deferred suffix
stays correct only where the comment's construct is *retained* (its doc regenerates
identically each pass); a strip changes the reparse geometry and needs the scoped node.

⚠️ **A deferred run's FLUSH must end the line, so a `lineSuffixBoundary` belongs only where
nothing else does.** The renderer drains the buffer at a break-mode `line` or at a boundary,
Expand Down
2 changes: 2 additions & 0 deletions crates/tsv_debug/src/cli/commands/arena_stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ const NODE_KINDS: &[&str] = &[
"LineSuffix",
"LineSuffixBoundary",
"BreakParent",
"FlushBreak",
"Align",
];
const TEXT_KINDS: &[&str] = &["Static", "Pooled", "SourceSpan"];
Expand Down Expand Up @@ -363,6 +364,7 @@ fn classify_node(n: &DocNode) -> &'static str {
DocNode::LineSuffix(_) => "LineSuffix",
DocNode::LineSuffixBoundary => "LineSuffixBoundary",
DocNode::BreakParent => "BreakParent",
DocNode::FlushBreak => "FlushBreak",
}
}

Expand Down
7 changes: 6 additions & 1 deletion crates/tsv_debug/src/cli/commands/gap_audit_known.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# the gate rather than being pinned.
#
# Format: KIND<TAB>SHAPE<TAB>PAYLOADS
# shapes: 507
# shapes: 512
DROPPED !)⟨⟩!. annotation,block,jsdoc_cast
DROPPED &&⟨⟩␣ annotation,block,jsdoc_cast,multiline
DROPPED &⟨⟩␣ annotation,block,jsdoc_cast,line,multiline
Expand Down Expand Up @@ -400,6 +400,8 @@ SWALLOW (⟨⟩IDENT line
SWALLOW (⟨⟩NUM line
SWALLOW (⟨⟩{ line
SWALLOW (⟨⟩␣ line
SWALLOW )))⟨⟩␣ line
SWALLOW ))⟨⟩) line
SWALLOW ))⟨⟩␣ line
SWALLOW ).⟨⟩IDENT line
SWALLOW );}⟨⟩, line
Expand All @@ -408,6 +410,8 @@ SWALLOW )=>⟨⟩( line
SWALLOW )=>⟨⟩(( line
SWALLOW )=>⟨⟩({ line
SWALLOW )=>⟨⟩/* line
SWALLOW )⟨⟩) line
SWALLOW )⟨⟩)) line
SWALLOW )⟨⟩. line
SWALLOW )⟨⟩; line
SWALLOW )⟨⟩␣ line
Expand Down Expand Up @@ -449,6 +453,7 @@ SWALLOW >⟨⟩; line
SWALLOW ?(⟨⟩// line
SWALLOW IDENT⟨⟩&// line
SWALLOW IDENT⟨⟩) line
SWALLOW IDENT⟨⟩)) line
SWALLOW IDENT⟨⟩). line
SWALLOW IDENT⟨⟩); line
SWALLOW IDENT⟨⟩, line
Expand Down
4 changes: 2 additions & 2 deletions crates/tsv_lang/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ The doc builder is the core of the formatting architecture. Language printers bu

### Key Types

- **`DocArena`** — Contiguous storage for all doc nodes, plus the text pool (the `String` backing `Pooled`/`MultilineText` bodies) and an inline direct-mapped static cache whose slots carry two halves: the amortized-eager widths behind `text()` statics, and the per-document **interned node** — repeated `text(",")` calls within one format return one shared `DocId` instead of allocating per call (`empty()` interns through a dedicated cell; sound because statics are position-free at render, nodes are append-only, and no consumer compares `DocId` identity). The stateless singleton nodes intern the same way through dedicated generation-gated cells with no hash probe: the four `Line` kinds (direct-indexed by `LineKind` discriminant), `LineSuffixBoundary`, and `BreakParent` — a `Line` node carries no mode or indent (both supplied per visit by the enclosing render command), so every `line()`/`softline()`/`hardline()`/`literalline()` within one document returns one shared node. The arena also parks a per-render output scratch buffer (`take_render_scratch()`/`park_render_scratch()` — the render analog of `pool_writer()`'s parked scratch): the hot per-piece render-and-write seams (TS whole-program/per-expression, CSS per declaration, Svelte per root node) render through the `*_into` entry points into it, one warm buffer per file instead of an alloc/free per call, with a fresh-fallback empty default so nested renders stay correct. The render loop's work buffers pool the same way — each top-level render borrows the arena's command stack + line-suffix buffer (`RefCell`-backed, cleared at borrow; sub-renders keep their own inline `SmallVec` locals) — and the per-file line-break table parks via `take_line_breaks_scratch()`/`park_line_breaks_scratch()` (filled by `printing::build_line_breaks_into` in each `format_in`), and the multi-line block-comment builders borrow a parked line-offset scratch (`borrow_line_spans_scratch()` — one `split('\n')` pass per comment fills each body line's `(start, end)` range, so the classifier and builders iterate slice-cheap with no per-comment line buffer). The doc-build side pools too: the wide-list builders assemble their parts into a `DocBuf` drawn from a recursion-safe free-list (`acquire_docbuf`/`release_docbuf`, or the `PooledDocBuf` RAII guard from `pooled_docbuf()`) — a builder pops a cleared buffer (retaining a prior spill's heap capacity) and returns it on scope exit, so the many transient `SmallVec` spills across a document collapse into a handful of long-lived reused buffers; the free-list keeps **only spilled buffers** (a release drops a never-spilled one — nothing to retain, free to re-construct), so every pooled entry carries real heap capacity and a big-need builder can't pop a virgin buffer while capacity sits deeper in the LIFO; retained across `reset()`; byte-identical — allocation only, never output. A parked node-keyed doc-share map (`share_map_scratch()`, an AST-node pointer → built `DocId` table) backs the TS printer's member-chain argument sharing the same way — the consumer clears it at share-scope entry/exit, so only its table capacity persists instead of a fresh `HashMap` resize chain per printer/file. Heuristic capacity: ~2 nodes per source byte (kept above the post-interning ~0.26/byte density because `estimated_children = nodes/2` must still clear the un-shrunk children demand); the text pool pre-sizes at source/8 (measured per-file demand p50 ≈ 0.17× source). `reset()` clears the node/child/text-pool/memo stores while retaining capacity — O(1) on the node store, since `DocNode` carries no drop glue — so a multi-file driver reuses one arena across files (the doc-IR analogue of the binding crates' `Bump::reset()` reuse); the static cache's width halves deliberately survive `reset()` (they key on `'static` string addresses — warming once per arena lifetime) while the interned node halves are invalidated in O(1) by the reset's `format_gen` bump; the printers borrow `&DocArena` and the caller owns the reusable one (`format_in` on each language crate is the borrowed-arena entry point).
- **`DocArena`** — Contiguous storage for all doc nodes, plus the text pool (the `String` backing `Pooled`/`MultilineText` bodies) and an inline direct-mapped static cache whose slots carry two halves: the amortized-eager widths behind `text()` statics, and the per-document **interned node** — repeated `text(",")` calls within one format return one shared `DocId` instead of allocating per call (`empty()` interns through a dedicated cell; sound because statics are position-free at render, nodes are append-only, and no consumer compares `DocId` identity). The stateless singleton nodes intern the same way through dedicated generation-gated cells with no hash probe: the four `Line` kinds (direct-indexed by `LineKind` discriminant), `LineSuffixBoundary`, `BreakParent`, and `FlushBreak` — a `Line` node carries no mode or indent (both supplied per visit by the enclosing render command), so every `line()`/`softline()`/`hardline()`/`literalline()` within one document returns one shared node. The arena also parks a per-render output scratch buffer (`take_render_scratch()`/`park_render_scratch()` — the render analog of `pool_writer()`'s parked scratch): the hot per-piece render-and-write seams (TS whole-program/per-expression, CSS per declaration, Svelte per root node) render through the `*_into` entry points into it, one warm buffer per file instead of an alloc/free per call, with a fresh-fallback empty default so nested renders stay correct. The render loop's work buffers pool the same way — each top-level render borrows the arena's command stack + line-suffix buffer (`RefCell`-backed, cleared at borrow; sub-renders keep their own inline `SmallVec` locals) — and the per-file line-break table parks via `take_line_breaks_scratch()`/`park_line_breaks_scratch()` (filled by `printing::build_line_breaks_into` in each `format_in`), and the multi-line block-comment builders borrow a parked line-offset scratch (`borrow_line_spans_scratch()` — one `split('\n')` pass per comment fills each body line's `(start, end)` range, so the classifier and builders iterate slice-cheap with no per-comment line buffer). The doc-build side pools too: the wide-list builders assemble their parts into a `DocBuf` drawn from a recursion-safe free-list (`acquire_docbuf`/`release_docbuf`, or the `PooledDocBuf` RAII guard from `pooled_docbuf()`) — a builder pops a cleared buffer (retaining a prior spill's heap capacity) and returns it on scope exit, so the many transient `SmallVec` spills across a document collapse into a handful of long-lived reused buffers; the free-list keeps **only spilled buffers** (a release drops a never-spilled one — nothing to retain, free to re-construct), so every pooled entry carries real heap capacity and a big-need builder can't pop a virgin buffer while capacity sits deeper in the LIFO; retained across `reset()`; byte-identical — allocation only, never output. A parked node-keyed doc-share map (`share_map_scratch()`, an AST-node pointer → built `DocId` table) backs the TS printer's member-chain argument sharing the same way — the consumer clears it at share-scope entry/exit, so only its table capacity persists instead of a fresh `HashMap` resize chain per printer/file. Heuristic capacity: ~2 nodes per source byte (kept above the post-interning ~0.26/byte density because `estimated_children = nodes/2` must still clear the un-shrunk children demand); the text pool pre-sizes at source/8 (measured per-file demand p50 ≈ 0.17× source). `reset()` clears the node/child/text-pool/memo stores while retaining capacity — O(1) on the node store, since `DocNode` carries no drop glue — so a multi-file driver reuses one arena across files (the doc-IR analogue of the binding crates' `Bump::reset()` reuse); the static cache's width halves deliberately survive `reset()` (they key on `'static` string addresses — warming once per arena lifetime) while the interned node halves are invalidated in O(1) by the reset's `format_gen` bump; the printers borrow `&DocArena` and the caller owns the reusable one (`format_in` on each language crate is the borrowed-arena entry point).
- **`DocId`** (`u32`) — Lightweight, `Copy` handle into the arena. No cloning, no recursive Drop.
- **`DocBuf`** (`SmallVec<[DocId; 8]>`) — Shared stack buffer for assembling a node's doc parts before `concat()` / `fill()`. Most nodes have only a handful of parts, so the common case stays off the heap; larger nodes spill. Used by all language printers (the TS chain / binary-operator printers, the Svelte template printer) as the single canonical doc-parts buffer type. Wide-list builders (statement / object / array / parameter / specifier lists) draw a reusable buffer from the arena's `DocBuf` free-list (`pooled_docbuf()`) rather than allocating a fresh `SmallVec` per call, amortizing the per-spill malloc/free churn (see `DocArena` below).
- **`DocNode`** — Node variants: `Text`, `MultilineText` (a `\n`-separated body rendered with per-line context indent — one pool-stored body for an indentable multi-line block comment), `Line`, `Indent`, `Dedent`, `Group`, `IfBreak`, `Concat`, `Fill`, etc. `DocNode` carries no drop glue (`const`-asserted via `needs_drop`): dynamic text lives in the arena text pool, so `reset()`/drop never walk the node store running destructors. Its size is also pinned by a companion `const` assert — **32 B on 64-bit** (the native flagship), **16 B on wasm32** (the shipped WASM bundles); the size is pointer-width dependent (`AlignRoot`'s `usize`, `DocText::Static`'s fat pointer), so the pin is `cfg`-gated per target. The node store is walked linearly at render, so the AoS layout's cache locality is the point (shrinking the node has been refuted repeatedly on this traversal-bound engine); a variant that bloats it is a deliberate decision, not an accident.
Expand All @@ -58,7 +58,7 @@ All methods take `&self` (interior mutability via `RefCell`):
- Sequences — `concat()`, `fill()`, `join()`, `join_doc()`
- Buffer pooling — `pooled_docbuf()` (RAII `PooledDocBuf`, releases on drop) / `acquire_docbuf()` / `release_docbuf()` — reusable `DocBuf` assembly buffers for wide-list builders
- Context — `with_context()`
- Line suffix — `line_suffix()`, `line_suffix_boundary()`, `break_parent()`
- Line suffix — `line_suffix()`, `line_suffix_boundary()`, `break_parent()`, `flush_break()` (flush-scoped: forces only the group the deferred run flushes in)
- Convenience — `wrap()`, `parens()`, `brackets()`, `braces()`
- Inspection — `will_break()`, `has_forced_break()`
- Transforms — `remove_lines()` / `atomize()` — rebuild a subtree with its lines statically flattened (old nodes stay in the arena, unused). **Two operations, not one function with a strength dial**, so pick by which prettier behavior you want: `remove_lines` is prettier's `removeLines` (breakable lines only; hard lines and `MultilineText` survive — it cannot promise one line), while `atomize` emulates a re-render at `printWidth: Infinity` (hard lines deleted, `conditional_group` collapsed to its least-expanded state). Atomizing is only sound where the caller has proved no newline is required — deleting a hard line fuses the content around it. The atomize contract is asserted directly by a width-invariance test: its result must render identically at every width
Expand Down
Loading