diff --git a/CLAUDE.md b/CLAUDE.md index 99e42d91b..7a6b314a7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -764,7 +764,7 @@ See ./docs/conformance_test262.md (command interface; §Differential for the tsv **Typechecker conformance (`tsc_conformance`) — EXPERIMENTAL, may never ship.** `tsv_check` is a from-scratch TypeScript binder + checker; no shipped artifact links it -(`cargo tree -i tsv_check` → only `tsv_debug`), and the parser and formatter are never modified in +(`cargo tree -i tsv_check` → `tsv_debug`, plus the workspace root's dev-dependency on it), and the parser and formatter are never modified in service of it. `tsv_debug tsc_conformance` grades it against tsgo's committed `.errors.txt` baselines (`../typescript-go`, pin `168e7015`), surfaced as **on-demand** tasks — none in `deno task check`, `deno task conformance`, or release gating, and `../typescript-go` is not a @@ -819,30 +819,22 @@ This is one instance of a broader stance: **the parser is deliberately permissiv **`parse` never rewrites its input** — its byte offsets are a drop-in contract with acorn / Svelte / `parseCss` over the author's own bytes. **Every parse-then-format entry point folds `` / `` to `` before it parses** -(`tsv_lang::printing::normalize_carriage_returns`, from `tsv_ts` / `tsv_svelte`'s -`format_str`, the CLI's `format_source` — CSS's only parse-then-format entry point — each -binding's format export, and `canonicalize_js`), so tsv's output is LF-only -even inside the regions it copies verbatim. Ahead of the parse is the only place that -answers it once: the printers ask "where are the lines?" in several places that split on -`'\n'` alone (`Comment::multiline`, `is_indentable_block_comment`), and folding the -finished string instead leaves those disagreeing with the output — the same document -then formats two ways on two passes. -`` / `` are deliberately NOT folded. Full rationale + spec citations: -./docs/architecture.md#line-terminators-parse-takes-the-authors-bytes-format-folds-first - -**Counting them is a separate question, and the Svelte wire answers it TWO ways** — because -Svelte's parser does. Svelte's own positions (`locate-character`) open a line at `\n` alone: -the spine, `name_loc`, CSS `loc`, a `Program`'s own `loc`, and the `character`-bearing -identifiers `read_identifier` builds. Everything **acorn** parses carries acorn's, the -ECMAScript class. And it is not one table per class either: acorn seeds its counter **once per -parse**, over whatever prefix Svelte prepared for that island (blanked to LF for `"); // Test inputs are hardcoded valid sources; a parse failure should panic let arena = bumpalo::Bump::new(); - #[allow(clippy::expect_used)] + #[expect(clippy::expect_used)] let root = crate::parse(&source, &arena).expect("parse"); crate::convert_ast_json(&root, &source) } @@ -335,7 +335,7 @@ mod tests { /// Parse a whole component and return the public JSON AST. fn convert_component(source: &str) -> Value { let arena = bumpalo::Bump::new(); - #[allow(clippy::expect_used)] + #[expect(clippy::expect_used)] let root = crate::parse(source, &arena).expect("parse"); crate::convert_ast_json(&root, source) } diff --git a/crates/tsv_svelte/src/ast/convert/write.rs b/crates/tsv_svelte/src/ast/convert/write.rs index b8db0cd7f..ced5f2178 100644 --- a/crates/tsv_svelte/src/ast/convert/write.rs +++ b/crates/tsv_svelte/src/ast/convert/write.rs @@ -1642,7 +1642,7 @@ fn write_directive_value_expression( write_braced_island(w, expr, span, ctx); } else { // Shorthand: the parser builds this as a synthetic `Identifier`. - #[allow(clippy::unreachable)] + #[expect(clippy::unreachable)] let tsv_ts::ast::internal::Expression::Identifier(id) = expr else { unreachable!("shorthand directive expression is always an Identifier"); }; @@ -1812,7 +1812,7 @@ fn write_script( /// `<`/`>` positions, not of the `Program`'s content span. The final char-space /// columns are those same tag positions translated to char space, so an indented /// `\t{#snippet foo()}…{/snippet}` collides a *snippet* with a script binding, and `
{#snippet a}…{/snippet}{#snippet a}…{/snippet}
` collides two snippets in one fragment — acorn sees neither, because neither is a JS declaration. Both are ported (see -below); the caveat retains no live consequence. +below). #### The parse-time rules @@ -689,8 +682,8 @@ no `at(-2)`, so at most one of the three can fire. (`SnippetBlock::span.start`) rather than name. Only a top-level snippet's span is inserted into the hoistable set, so a nested snippet's span is absent by construction: `is_hoisted` returns false for it and it lands in its enclosing block - body regardless of its top-level twin's verdict. Both shapes this formerly refused - now compile at parity — the top-level twin **hoists** (static, no instance binding), + body regardless of its top-level twin's verdict. Both shapes compile at parity — the + top-level twin **hoists** (static, no instance binding), leaving the nested one in the body (`{#snippet a()}static{/snippet}
{#snippet a()}{v}{/snippet}
{@render a()}`, pinned by `snippets/nested_name_hoisted`), and the top-level twin does **not** hoist, @@ -698,7 +691,7 @@ no `at(-2)`, so at most one of the three can fire. `collect_hoisted_snippets`'s recursive-direct-first walk) (`
{#snippet a()}nested{/snippet}
{#snippet a()}{v}{/snippet}{@render a()}`, pinned by `snippets/nested_name_body`); the fixpoint-demotion variant compiles too. - This retired the former `NestedSnippetNameCollision` refusal. (The per-**fragment** + (The per-**fragment** `declaration_duplicate` rule above is distinct and still refuses — it fires only when two snippets share a name in ONE fragment.) - **Refused**: `{#snippet} {name} shadows the component prop of the same name (the @@ -780,9 +773,9 @@ entry's KIND, because the two halves of that sentence have different answers. A it is a write to the local and compiles. A nested `const` does not: it is `declaration_kind: 'const'` to the oracle wherever it sits — `validate_no_const_assignment` reads the SCOPE CHAIN, not a top-level set — so it carries `constant_assignment` itself and -the write must REFUSE. An earlier form of this scoped set treated every entry uniformly as -"shadowed ⇒ no rule" and so **compiled writes the oracle rejects** — an over-acceptance and -a refusal-contract violation, live-verified on +the write must REFUSE. A scoped set that treated every entry uniformly as +"shadowed ⇒ no rule" would **compile writes the oracle rejects** — an over-acceptance and +a refusal-contract violation, oracle-verified on `const a = 1; function f() { { const a = 0; a = 2; } }` and three siblings. Storing the kind is what keeps the two apart, and it must stay stored: the two nested orderings have opposite verdicts (`let a; { const a; a = 1 }` refuses; `const a; { let a; a = 1 }` @@ -790,15 +783,13 @@ compiles, both oracle-verified), so a set that merely knew "some open binding of is const" would get one of them wrong. The enumeration of declaration FORMS is a **separate** question from the kind one above. -⚠️ An earlier form of this section claimed that incompleteness "fails in the safe -direction — a form the walk does not record leaves no binding, so the write falls through -to the component-level sets and still refuses". **That claim is FALSE, and it is the same -conflation as the one above, one level out**: the fall-through refuses only when the name -is ALSO in a component-level set. When the shadowed-out name is purely LOCAL there is no -component-level entry to fall through to, no rule applies at all, and the write is -ACCEPTED — an over-acceptance whenever that local was a `const`. Two such over-acceptances -were live, both listed by the old text as safe examples, both oracle-verified -(`constant_assignment`): +⚠️ Incompleteness does **not** fail in the safe direction. A form the walk does not +record leaves no binding, and the write falls through to the component-level sets — but +the fall-through refuses only when the name is ALSO in a component-level set (the same +conflation as the one above, one level out). When the shadowed-out name is purely LOCAL +there is no component-level entry to fall through to, no rule applies at all, and the +write is ACCEPTED — an over-acceptance whenever that local was a `const`. Two shapes that +read as safe are exactly this, both oracle-verified (`constant_assignment`): ```svelte @@ -992,33 +983,33 @@ cargo run -p tsv_debug compile_corpus_compare t.svelte`. ### Mismatch classes under mutation -`compile_fuzz --seed 0 --iterations 20000` produces **16 mismatches**, classified from -the dumped mutants by diff signature. The fuzzer's operators, grading rules, and flags -are documented in [compile_tooling.md](compile_tooling.md). +The mismatch classes `compile_fuzz` reaches that the real-component corpus does not, +each with its mechanism and its pin. The fuzzer's operators, grading rules, and flags are +documented in [compile_tooling.md](compile_tooling.md). A run's mismatch *count* is a +measurement of one seed over one corpus, not a property of the compiler, and is not +recorded here: the seed corpus IS `tests/fixtures_compile`, so every fixture added changes +which mutants are generated, and a count is comparable only against another run over the +same corpus. -⚠️ The count is not comparable across corpus edits. The seed corpus IS -`tests/fixtures_compile`, so adding a fixture changes which mutants are generated; -compare a run only against another run over the same corpus. - -**C1 — `{#each}` counter numbering — is CLOSED.** tsv and the oracle disagreed on -which loop got `$$index` vs `$$index_1`/`$$index_2` because tsv allocated both -generated each-block names from one emission-order counter. The oracle allocates them -in two *different* passes, and therefore two different orders: +**C1 — `{#each}` counter numbering.** The two generated each-block names cannot share +one emission-order counter — which loop gets `$$index` vs `$$index_1`/`$$index_2` then +disagrees with the oracle, which allocates them in two *different* passes, and +therefore two different orders: | name | oracle pass | order | dropped `{:catch}` | | --- | --- | --- | --- | | `each_array` | 3-transform, server `EachBlock` visitor (`state.scope.root.unique`) | pre-order | not visited → consumes nothing | | `$$index` | scope creation, `EachBlock` visitor's trailing `node.metadata = { … }` | **post-order** (assigned *after* body + fallback) | visited → **consumes a name** | -So an `{#each}` nested inside another one's fragment, or sitting in a dropped -`{:catch}`, mis-numbered every later loop. `blocks::assign_each_index_names` now -assigns `$$index` upfront in post-order over the whole fragment tree; `each_array` -stays at emission. Fixtures: `blocks/each_fallback_nested_each`, +Under one counter, an `{#each}` nested inside another one's fragment, or sitting in a +dropped `{:catch}`, mis-numbers every later loop. `blocks::assign_each_index_names` +assigns `$$index` upfront in post-order over the whole fragment tree; `each_array` is +assigned at emission. Fixtures: `blocks/each_fallback_nested_each`, `blocks/each_index_after_dropped_catch_each`. ⚠️ Only two of those nestings are reachable today. An `{#each}` in another's **body** still refuses (`Refusal::NestedEach` — `env.in_each`, a separate gate on the -unvalidated nested *emission* path), so the numbering fix is exercised by an `{#each}` +unvalidated nested *emission* path), so the post-order numbering is exercised by an `{#each}` in a `{:else}` fallback and by one in a dropped `{:catch}`, which is what the two fixtures cover. The body case is modelled but not yet reachable; it becomes so when `NestedEach` lifts. @@ -1031,23 +1022,22 @@ refuses (`Refusal::ModuleCommentAfterInstanceScript`); see for the mechanism, the probed boundary, and why the refusal is coarser than the mismatch. Zero corpus parity cost. -The residual 16 by diff signature (a clean partition this time — each mutant carries -exactly one): +The other classes a run reaches, by diff signature: -| Signature | Count | Shape | -| --- | --- | --- | -| `$$props` | 6 | a user `const $$props = 1` where the oracle emits `const $$sanitized_props = 1` (generated-name deconfliction) | -| module-script comment (block-recovered) | 7 | the **other** half of the class — now **CLOSED** (carried, not refused); see below | -| generated-function order | 1 | a `` `failed` snippet / hoisted snippet function emitted at a different point in the body than the oracle emits it | -| wrapper | 1 | `$$renderer.component(…)` emitted where the oracle emits none | -| static fold | 1 | tsv folds a `{b}` read the oracle keeps as `$.escape(b)` | +| Signature | Shape | +| --- | --- | +| `$$props` | a user `const $$props = 1` where the oracle emits `const $$sanitized_props = 1` (generated-name deconfliction) | +| module-script comment (block-recovered) | the **other** half of the class — carried, not refused; see below | +| generated-function order | a `` `failed` snippet / hoisted snippet function emitted at a different point in the body than the oracle emits it | +| wrapper | `$$renderer.component(…)` emitted where the oracle emits none | +| static fold | tsv folds a `{b}` read the oracle keeps as `$.escape(b)` | -#### The open half: a module comment recovered by a preceding block +#### The other half: a module comment recovered by a preceding block -**Now CLOSED (carried, not refused).** The 7 residual module-comment mismatches are the -**same mechanism** as the closed half — +**Carried, not refused.** The residual module-comment mismatches are the +**same mechanism** as the refused half (C2) — esrap's single comment index being re-seeked backward — reached by a different route. -They are **CARRIED, not refused**: unlike the closed half (a module-second comment +They are **CARRIED, not refused**: unlike the refused half (a module-second comment lands in an unrelated template expression, unreproducible), a module-first comment the oracle keeps carries at its authored span, which the parity bar accepts (a byte match, or a comment-POSITION difference the oracle sometimes forces by re-attaching into the @@ -1091,16 +1081,15 @@ keep/drop/refuse matrix in `src/tests/module_script.rs`. ⚠️ Keyed to the pinned oracle's `reset_comment_index` behavior — re-probe the keep condition if that pin moves. -⚠️ **A further class exists but did not come from this run.** `` ordering — -tsv emits `$.head(…)` *before* the hoisted snippet function where the oracle emits it -after — is a real, hand-confirmed bug. No `--seed 0` mutant contains `` at -all, so it is tracked separately and must not be counted against a `compile_fuzz` run's -mismatch total. +⚠️ **`` ordering is a further open class the fuzzer does not reach**: tsv +emits `$.head(…)` *before* the hoisted snippet function where the oracle emits it after. +The seed corpus holds no ``, so no mutant contains one; it is hand-confirmed +rather than fuzz-found. #### C5 — trailing template whitespace: the source `trimEnd` class -A **sixth** class, produced by `compile_fuzz`'s `exotic_whitespace` operator and -confirmed by hand. A document whose last character is `U+FEFF` or `U+0085` mismatches, +A class produced by `compile_fuzz`'s `exotic_whitespace` operator and confirmed by +hand. A document whose last character is `U+FEFF` or `U+0085` mismatches, and the two mismatch in **opposite directions**: | Document | tsv emits | oracle emits | diff --git a/docs/checklist_typescript.md b/docs/checklist_typescript.md index c347f3ff7..b5bf74b4b 100644 --- a/docs/checklist_typescript.md +++ b/docs/checklist_typescript.md @@ -473,7 +473,7 @@ ES2015 module syntax with ES2025 additions. - Multiple `implements` - `declare class` -**Decorators** (a TC39 proposal — not in any ECMAScript edition; shipped in TS 5.0): +**Decorators** (a TC39 proposal — not in any ECMAScript edition): - Class decorators (`@decorator class C {}`) - Decorated class expressions (`x = @dec class {}`) @@ -495,8 +495,8 @@ Note: An ambient (`declare class`) member parses decorators exactly like a concr **Other Features**: -- `override` modifier - TS 4.3, including the `abstract override` pair. `abstract override` is tsc's canonical order and the only one it accepts; tsv prints that order (as prettier does) whichever way the source spells it -- `accessor` keyword - ES2022/TS 4.9 +- `override` modifier, including the `abstract override` pair. `abstract override` is tsc's canonical order and the only one it accepts; tsv prints that order (as prettier does) whichever way the source spells it +- `accessor` keyword - Parameter properties (`constructor(public x: T)`) — all modifiers: `public`/`private`/`protected`, `override`, `readonly` (canonical order `accessibility → override → readonly`) --- @@ -592,7 +592,7 @@ Note: An ambient (`declare class`) member parses decorators exactly like a concr - Angle-bracket assertion (`expr`) - `as const` assertion - Non-null assertion (`expr!`) -- `satisfies` operator - TS 4.9 +- `satisfies` operator ### Type Aliases @@ -699,13 +699,13 @@ Note: An ambient (`declare class`) member parses decorators exactly like a concr - Generic interfaces - Generic type aliases -**Advanced Generics** (TS 4.7+): +**Advanced Generics**: -- Const type parameters (``) - TS 5.0 -- Variance modifiers (`in`, `out`) - TS 4.7 +- Const type parameters (``) +- Variance modifiers (`in`, `out`) - Modifiers in any ORDER (``, ``, ``) — tsc's parser collects them order-free and leaves the ordering rules ("'const' modifier must precede 'in' modifier", TS1029 for the variance pair) to its grammar checker, so tsv defers them alongside the context one; prettier formats every spelling. A **repeat** is the exception tsv rejects (`` → `Duplicate modifier: 'out'`, matching acorn): a duplicate is invalid in every context and adjudicable from the construct alone — the unconditional-local bucket — where tsc again defers (TS1030) and prettier collapses it. The repeat check sits behind the name test, so a trailing repeat that IS the name is untouched (``) - `out` as a type parameter NAME (``, ``, ``, ``, ``) — the variance keyword is contextual, so it is a modifier only when a name can still follow it (tsc's `nextTokenCanFollowModifier`); the test is the next token's shape, which is why `` keeps `out` a modifier and then rejects for want of a name, in tsc, prettier and acorn alike -- Type instantiation expressions (`fn`) - TS 4.7 +- Type instantiation expressions (`fn`) - A numeric type argument may omit its integer part (`fn<.5>`, `fn`, `fn`) — the spec's `.`-led `DecimalLiteral` production is a literal type like any other, so acorn reads each as an instantiation. The `.` opens one only where a **digit** follows it: everywhere else it is a member tail on what precedes it, and after an operator keyword both readings are live — `p < keyof.a > (t, u)` is a comparison on the value `keyof.a` (prettier's tsc parser rejects that line outright, see [conformance_prettier_ts.md](./conformance_prettier_ts.md) §Prettier rejects valid input) ### Function TypeScript Features @@ -730,7 +730,7 @@ A **finished (Stage 4)** TC39 proposal — `../../proposals/finished-proposals.m expected publication year of 2027, so the grammar is settled but has not yet landed in a published edition. It is absent from the `../../ecma262/` draft, and that proves nothing either way: a finished proposal is one that "is (or soon will be) included in the latest draft", so -the draft's silence is not evidence of an earlier stage. Shipped in TS 5.2. Svelte's parser +the draft's silence is not evidence of an earlier stage. Svelte's parser rejects it; tsv is native — see [conformance_svelte.md](./conformance_svelte.md#typescript-corrections). diff --git a/docs/comments.md b/docs/comments.md index 9ab8a1b26..5acde903a 100644 --- a/docs/comments.md +++ b/docs/comments.md @@ -181,18 +181,18 @@ The one thing that separator asks is whether the author **glued** this comment t `Printer::split_terminator_gap_comments` is the same walk once more, for the terminators whose operand may be parenthesized (`return` / `throw`, `export default`, `export =`) — everything in that gap floats past the `;`, except the comment a grouping paren the caller PRINTS still encloses. Its three arms are the three sentences above, and it had all three wrong at once: a fixed anchor split an author-glued pair, an own-line arm keyed on the comment's KIND sent a `//` to the statement's line as if it trailed there, and a bare `hardline` instead of the blank-preserving suffix ate the author's blank *and* rendered ahead of anything already buffered. Pinned by `syntax/comments/terminator_gap_glued_run` (the glue, stated across both terminator emitters) and `syntax/comments/terminator_gap_own_line_run`. -**⚠️ A deferred run must not leave the construct it was written in.** Deferring is *end of line*, not *escape*: the run flushes wherever the enclosing line happens to end, so a construct that closes without breaking carries the comment past its own closer — re-binding it from the node it trails to whatever statement it lands on, and landing it on a line that may already hold a deferred comment, where the two weld irreversibly. So a **line** comment in one of these gaps forces its construct **open**: the closer drops to its own line and the comment flushes inside. Every bracketed type region does this — a type literal's `}`, a type-argument list's `>`, a tuple's `]`, a function type's `)`, an indexed access's `]`, and a paren shell, which is *retained* rather than stripped for exactly this reason (`tsv_ts`'s `build_parenthesized_type_unwrap_doc`). **A shell reaches that emitter only if nothing strips it first**, and one thing did: the keyword→value paren-strip hang (`Printer::keyword_value_stripped_paren_hang`) fires on a **leading** `//` in the shell, so a shell carrying both a leading and a trailing one satisfied the retain rule while already being gone — its `//` lifted out onto the enclosing construct's closing line, where a comment authored in that construct's own trailing gap was waiting to weld with it. The seam now declines exactly the shells `Printer::paren_retains_for_trailing_run` retains, at all seven hang positions and at the cast's second, own strip path; the rule is read once, where the shell is decided, rather than at each construct's trailing emitter (which would have to learn that a deferred run is pending). The one sanctioned exception is a union / intersection member a `|`/`&` separator still **follows** in source: the next separator's per-member break ends the output line exactly where the shell ends, so the strip is lossless and the comment still trails its member. The carve-out is asked structurally — `Printer::type_member_separator_follows`, scanning past trivia and the `)` closers of enclosing redundant layers — rather than as builder context, so it stops precisely where its argument stops: the **last** member has no separator, its line ends only at the statement's tail, and a stripped shell there carried the comment past the `;` while forcing a break the reparse could not reproduce (non-idempotent, and prettier's own form there still is — pinned by `type_suffix_trailing_comment_union_member`'s `audit_signature.txt`). So the last member retains like every other position. And the break the sanctioned strip *does* force is **flush-scoped** (`DocArena::flush_break`, not `break_parent`): stripping changes the reparse geometry — the comment ends up in the composite's member gap, outside the parens it was written in — so only the group the deferred run actually flushes in re-breaks on the next pass. The unscoped node forced every intermediate group too (a shell one composite deep, `B & (A // c) | C`, broke the intersection the reparse then reprinted flat — a 2-pass convergence). `FlushBreak` is invisible to `will_break` and armed in the `arena_fits` walk: while pending in flat mode, a breakable `line` or a plain `if_break` whose break arm can break does not fit — the group owning the next line opportunity breaks, and a group with no line after the suffix stays flat. A construct that is **retained** keeps plain `break_parent` (`with_stripped_paren_trailing`'s hang seams, the retained-paren union's trailing arm): its doc regenerates identically each pass, so the unscoped force is reproducible — the scoped node is for the strips. See [conformance_prettier_ts_comments.md §Comment relocation](./conformance_prettier_ts_comments.md#comment-relocation). +**⚠️ A deferred run must not leave the construct it was written in.** Deferring is *end of line*, not *escape*: the run flushes wherever the enclosing line happens to end, so a construct that closes without breaking carries the comment past its own closer — re-binding it from the node it trails to whatever statement it lands on, and landing it on a line that may already hold a deferred comment, where the two weld irreversibly. So a **line** comment in one of these gaps forces its construct **open**: the closer drops to its own line and the comment flushes inside. Every bracketed type region does this — a type literal's `}`, a type-argument list's `>`, a tuple's `]`, a function type's `)`, an indexed access's `]`, and a paren shell, which is *retained* rather than stripped for exactly this reason (`tsv_ts`'s `build_parenthesized_type_unwrap_doc`). **A shell reaches that emitter only if nothing strips it first**, and one thing did: the keyword→value paren-strip hang (`Printer::keyword_value_stripped_paren_hang`) fires on a **leading** `//` in the shell, so a shell carrying both a leading and a trailing one satisfied the retain rule while already being gone — its `//` lifted out onto the enclosing construct's closing line, where a comment authored in that construct's own trailing gap was waiting to weld with it. The seam now declines exactly the shells `Printer::paren_retains_for_trailing_run` retains, at all seven hang positions and at the cast's second, own strip path; the rule is read once, where the shell is decided, rather than at each construct's trailing emitter (which would have to learn that a deferred run is pending). The one sanctioned exception is a union / intersection member a `|`/`&` separator still **follows** in source: the next separator's per-member break ends the output line exactly where the shell ends, so the strip is lossless and the comment still trails its member. The carve-out is asked structurally — `Printer::type_member_separator_follows`, scanning past trivia and the `)` closers of enclosing redundant layers — rather than as builder context, so it stops precisely where its argument stops: the **last** member has no separator, its line ends only at the statement's tail, and a stripped shell there carried the comment past the `;` while forcing a break the reparse could not reproduce (non-idempotent, and prettier's own form there still is — pinned by `type_suffix_trailing_comment_union_member_prettier_divergence`'s `audit_signature.txt`). So the last member retains like every other position. And the break the sanctioned strip *does* force is **flush-scoped** (`DocArena::flush_break`, not `break_parent`): stripping changes the reparse geometry — the comment ends up in the composite's member gap, outside the parens it was written in — so only the group the deferred run actually flushes in re-breaks on the next pass. The unscoped node forced every intermediate group too (a shell one composite deep, `B & (A // c) | C`, broke the intersection the reparse then reprinted flat — a 2-pass convergence). `FlushBreak` is invisible to `will_break` and armed in the `arena_fits` walk: while pending in flat mode, a breakable `line` or a plain `if_break` whose break arm can break does not fit — the group owning the next line opportunity breaks, and a group with no line after the suffix stays flat. A construct that is **retained** keeps plain `break_parent` (`with_stripped_paren_trailing`'s hang seams, the retained-paren union's trailing arm): its doc regenerates identically each pass, so the unscoped force is reproducible — the scoped node is for the strips. See [conformance_prettier_ts_comments.md §Comment relocation](./conformance_prettier_ts_comments.md#comment-relocation). The two differ only in what supplies the outer breaks. The trailing emitter owns the break away from the previous item, and reads `prev_end == 0` as "there is no previous item at all" — a comments-only file, which is how the program's `}`-less end-of-body reaches the same code. The dangling emitter leaves both outer breaks to the delimiter pair, so the caller's `sep` decides whether a lone fitting block comment stays inline (`{/* c */}`) or the body always explodes; "always breaks" is that same emitter with a `hardline` separator, not a second builder. **Where a deferred run lands is the renderer's question, and the flush must END THE LINE.** The `line_suffix` buffer is drained by whichever comes first: a `line` the renderer takes in break mode, or a `lineSuffixBoundary` — the marker a construct plants where its own text must not follow a pending `//` (an assignment's `fluid_after_operator`, a template-literal `${…}` closer, a member lookup). Both flush, and both then end the line; prettier spells the boundary's half as a pushed `hardlineWithoutBreakParent` (`printer.js` `DOC_TYPE_LINE_SUFFIX_BOUNDARY`). A flush that ends inline instead lets the deferred `//` swallow the code the boundary exists to protect — `const x: T[K // c⏎] = y` printing as `const x: T[K] = // c` with the initializer *inside* the comment, output that does not even reparse. The obligation runs both ways: because the boundary supplies that break itself, a doc that plants one directly in front of its **own** forced break renders a blank line, so a boundary belongs only where nothing else ends the line — a chain gap, whose `build_chain_line_break` always breaks, takes `build_deferred_line_comments_doc` instead. `arena_fits` carries the other half of the rule: a boundary reached with a suffix pending does **not** fit (prettier's `hasLineSuffix`), so a group spanning one breaks rather than measuring flat across a line the flush is about to end. -**⚠️ A deferred `//` must be the ONLY thing reaching its line end — two deferred comments flushing at one break WELD.** Whatever flushes second lands inside the first `//` (`fn().bar; // c // c1`, the second becoming text of the first; a block trailer is reordered behind it instead, `.bar; /* c1 */ // c`), and the fixture, census and ledger are all blind to it because every comment is still printed once — only a prettier `compare` or the gap audit's SWALLOW class sees it. So the sanctioned deferral of a member chain's trailing-member gap `//` (`fn() // c⏎.bar;` → `fn().bar; // c`, `trailing_member_after_call_comment`) is licensed only while nothing else reaches that line end, and its askers state that: `trailing_member_gap_line_comment` refuses the deferral (the chain expands, each comment in place) when the `//` is own-line, when **any** comment follows it inside the same gap — asked on the **on-page** axis, since a follower glued to the property is owned by it and lands ahead of the deferred `//` all the same — or when a trailer follows the chain's end through closing punctuation (`Printer::trailer_follows_through_closers` — read through `)` `]` `}` `;` `,` because the expanded reprint puts `);` on a later line than the member); and the chain grouping (`group_chain_nodes`) never lets a member with a `//` in its gap sit *inside* a group at all — prettier's own rule, a node with a trailing comment closes its group and the factory merge is refused for one — since a member printed mid-group can only defer, and two such gaps weld (`fn().bar // c2 // c3`). A trailer behind a further token — an operator's right side (`fn()// c⏎.bar as T; // c1`) or a following sibling in a list (`foo(fn()// c⏎.bar, z); // c1`) — is out of that read's reach, and is caught by the flush's own separator below instead. Pinned by `chained/trailing_member_gap_comment_statement_trailer`, `chained/trailing_member_gap_comment_run` and `chained/group_interior_member_line_comment`. +**⚠️ A deferred `//` must be the ONLY thing reaching its line end — two deferred comments flushing at one break WELD.** Whatever flushes second lands inside the first `//` (`fn().bar; // c // c1`, the second becoming text of the first; a block trailer is reordered behind it instead, `.bar; /* c1 */ // c`), and the fixture, census and ledger are all blind to it because every comment is still printed once — only a prettier `compare` or the gap audit's SWALLOW class sees it. So the sanctioned deferral of a member chain's trailing-member gap `//` (`fn() // c⏎.bar;` → `fn().bar; // c`, `trailing_member_after_call_comment`) is licensed only while nothing else reaches that line end, and its askers state that: `trailing_member_gap_line_comment` refuses the deferral (the chain expands, each comment in place) when the `//` is own-line, when **any** comment follows it inside the same gap — asked on the **on-page** axis, since a follower glued to the property is owned by it and lands ahead of the deferred `//` all the same — or when a trailer follows the chain's end through closing punctuation (`Printer::trailer_follows_through_closers` — read through `)` `]` `}` `;` `,` because the expanded reprint puts `);` on a later line than the member); and the chain grouping (`group_chain_nodes`) never lets a member with a `//` in its gap sit *inside* a group at all — prettier's own rule, a node with a trailing comment closes its group and the factory merge is refused for one — since a member printed mid-group can only defer, and two such gaps weld (`fn().bar // c2 // c3`). A trailer behind a further token — an operator's right side (`fn()// c⏎.bar as T; // c1`) or a following sibling in a list (`foo(fn()// c⏎.bar, z); // c1`) — is out of that read's reach, and is caught by the flush's own separator below instead. Pinned by `chained/trailing_member_gap_comment_statement_trailer_prettier_divergence`, `chained/trailing_member_gap_comment_run_prettier_divergence` and `chained/group_interior_member_line_comment_prettier_divergence`. **The flush is itself a RUN, and it owes the same separator — this one at render time.** `doc::arena_render_suffix` drains the buffer onto one physical line, so everything the separator rule says about a run applies to it verbatim: a suffix landing behind a `//` takes a break first (`push_run_separator`), and one that already opens its own line does not (`build_trailing_comment_doc_own_line` carries its break inside the suffix, and a second would fabricate a blank). This is the FLOOR under the build-time askers above, not a substitute for them — they keep the comment where the author wrote it, while the flush only guarantees the two never merge — and it is the only one of the two that can see the layout, which is why it closes the shapes no source read reaches. Three properties are load-bearing: - **The line-comment test is the node's identity, never the emitted bytes.** A suffix payload is comment text, and a block comment may legitimately *contain* `//` (a URL), so the walk asks whether the LAST text node the suffix emits is a whole line comment — the same one-node spelling [`doc::swallow`](../crates/tsv_lang/src/doc/swallow.rs) keys its diagnostic on (`DocArena::line_comment_source_span` / `line_comment_text_pooled`). A printer that spelled a comment as `text("//") + ` would be invisible here exactly as it is there. -- **The break takes the FLUSH's indent, not the suffix's.** A suffix carries the indent it was *queued* at, deep inside a construct that has since closed; the indent of the break the flush is happening at is the document's own answer for what starts the next line, and is what a reformat then agrees with. The one shape where it is not is a switch's **last** case, where the next break is the switch's `}` — a level out from where a dangling comment in a case settles — and that shape never reaches the separator: the case's builder claims the last statement's `;`-line comments itself, own-line and dedented to the case's level (the clause-tail pattern, `statements/control_flow/switch.rs`), so the landing is a one-pass fixed point. Cataloged with the fixtures (`syntax/comments/deferred_comment_run_separator`, `statements/switch/last_case_terminator_comment_run`). +- **The break takes the FLUSH's indent, not the suffix's.** A suffix carries the indent it was *queued* at, deep inside a construct that has since closed; the indent of the break the flush is happening at is the document's own answer for what starts the next line, and is what a reformat then agrees with. The one shape where it is not is a switch's **last** case, where the next break is the switch's `}` — a level out from where a dangling comment in a case settles — and that shape never reaches the separator: the case's builder claims the last statement's `;`-line comments itself, own-line and dedented to the case's level (the clause-tail pattern, `statements/control_flow/switch.rs`), so the landing is a one-pass fixed point. Cataloged with the fixtures (`syntax/comments/deferred_comment_run_separator_prettier_divergence`, `statements/switch/last_case_terminator_comment_run_prettier_divergence`). - **The renderer's own newline hook is what keeps the diagnostic honest.** `render_line_break` is the single seam every break passes through, so `swallow::note_line_end` lives there rather than on a render policy: a break emitted outside a policy's reach — this separator is one — would otherwise leave the tracker's pending `//` armed and report the comment behind it as a swallow that no longer happens. **A statement's `;`-terminator gap defers its own-line run in CLAUSE position, dedented to the flushing construct's level.** The container decides (`StatementContext`, threaded through `Printer::build_statement_doc`): in a statement LIST the joiner's hardline immediately follows the statement doc, so `Printer::push_gap_comments`' own-line arm can take real breaks — the emission closes its own line before anything can queue a suffix, and the real hardline carries the correct indent at container tails too (a block's last statement; the one settled-on-pass-2 holdout is a switch's LAST case, the known bound in `doc/arena_render_suffix.rs`). A non-block CLAUSE body's last line instead stays open to the enclosing construct — an `else`, a do-while `while`, or just the construct's end — where a later gap's deferred `//` flushes onto a real-text `//` and welds, the render-time separator above blind to it because it only compares suffix against suffix, never suffix against emitted text. So the clause tail rides in `line_suffix` docs (`GapDeferral::LineSuffix`), and each own-line member's interior break is wrapped in `dedent` (`Printer::build_clause_tail_comment_doc`): a suffix's payload renders at the indent it was QUEUED at — the body's, one `indent` wrap per enclosing clause even when every one of them collapsed — while the comment settles at the flushing construct's level, where the reparse re-reads it from that construct's own gap. The dedent count mirrors the wraps exactly (`StatementContext::clause_body`: one per body-site `indent`, reset by a construct that CONTINUES on the flush line), which is what makes the deferred form its own fixed point in one pass and lands the run where prettier puts it (the hoist past a collapsed `if (a) expr;`). diff --git a/docs/conformance_prettier_css.md b/docs/conformance_prettier_css.md index 365060164..e19ac4089 100644 --- a/docs/conformance_prettier_css.md +++ b/docs/conformance_prettier_css.md @@ -30,7 +30,7 @@ in [conformance_prettier.md](./conformance_prettier.md). **Escaped whitespace ending a prelude**: an at-rule prelude can end in an escaped whitespace (`@layer a\ ;` — the layer is named `a `, since `\ ` is a valid escape whose escaped code point IS that space, css-syntax-3 §4.3.4/§4.3.7). tsv preserves it. Prettier trims it as padding, stranding the backslash onto the `;`, which it then escapes: alone, prettier's output no longer parses (tsv rejects `@layer a\;` with `Expected '{' or ';' after 'at-rule prelude'`); in context it parses but the prelude swallows everything up to the next `{`, silently **merging two at-rules into one**. Prettier gets the block form right (`@layer b\ {`), where the payload space already separates the prelude from the brace — and tsv likewise adds no second separator there. Same rule, same reason as [CSS: Values §Escaped whitespace in a value](#css-values). See [layer_escaped_whitespace](../tests/fixtures/css/at_rules/layer_escaped_whitespace_prettier_divergence/). -**Escaped control char in an at-rule name**: an escape *inside the at-rule name token* (`@m\A x`) is part of the name — Svelte's `parseCss` (and tsv, matching it) decode the whole run to the name (`"m\nx"`, a control char mid-name), with an empty prelude. Postcss/prettier instead **split** the name at the escape into a name plus a prelude (`@m \A x {`), and reject outright when the name *starts* with an escape (`@\6D edia` → "At-rule without name"). tsv can't adopt prettier's split without contradicting its own parse, so it emits the name from **source**, preserving the escape verbatim (`@m\A x {`) — the decoded control char never lands raw in the output. Emitting the *decoded* form (the prior bug) injected a raw newline, corrupting the file, and dropped the escape on reparse (name `m\nx` → `m`) — content loss and a broken fixed point. Same escape-preservation rule as escaped property names, which both formatters keep verbatim (`escaped_property_name`, no divergence). The end-of-name subcase (`@n\A {`, the escape needs a terminator space) adds no second separator before `{`, the same absorption as [Escaped whitespace ending a prelude](#css-at-rules) above. See [name_escaped_control_char](../tests/fixtures/css/at_rules/name_escaped_control_char_prettier_divergence/). +**Escaped control char in an at-rule name**: an escape *inside the at-rule name token* (`@m\A x`) is part of the name — Svelte's `parseCss` (and tsv, matching it) decode the whole run to the name (`"m\nx"`, a control char mid-name), with an empty prelude. Postcss/prettier instead **split** the name at the escape into a name plus a prelude (`@m \A x {`), and reject outright when the name *starts* with an escape (`@\6D edia` → "At-rule without name"). tsv can't adopt prettier's split without contradicting its own parse, so it emits the name from **source**, preserving the escape verbatim (`@m\A x {`) — the decoded control char never lands raw in the output. Emitting the *decoded* form would inject a raw newline, corrupting the file, and drop the escape on reparse (name `m\nx` → `m`) — content loss and a broken fixed point. Same escape-preservation rule as escaped property names, which both formatters keep verbatim (`escaped_property_name`, no divergence). The end-of-name subcase (`@n\A {`, the escape needs a terminator space) adds no second separator before `{`, the same absorption as [Escaped whitespace ending a prelude](#css-at-rules) above. See [name_escaped_control_char](../tests/fixtures/css/at_rules/name_escaped_control_char_prettier_divergence/). **`selector()` argument**: ` = selector( )` (css-conditional-4 §"Extensions to the @supports rule"), so tsv parses the argument as a selector and prints it with the same printer a rule's own selector uses — `selector(div>span)` normalizes to `selector(div > span)`, `selector(div[data-attr=value])` to `[data-attr='value']`, exactly as those selectors format in rule position, and matching prettier ([supports_selector_interior](../tests/fixtures/css/at_rules/supports_selector_interior/)). The position therefore inherits tsv's cataloged selector-printing choices (the column combinator, the An+B leading `+`, …) rather than a second set of rules. The grammar's argument is a *single* complex selector, but a comma-separated list is read as the list it lexically is — the singular production decides whether the condition is true, which is not a formatting question. Three divergences follow from the grammar rather than from taste: @@ -155,11 +155,11 @@ An unquoted `url()` has the same failure at its closing paren — `url(x\ )` → tsv declines to reproduce any of it — its format→re-parse invariant outranks matching prettier, and emitting output that does not parse is never the defensible side. Svelte's error-recovering `parseCss` still consumes prettier's output, so this is a *formatter* divergence only; tsv's parse AST matches `parseCss` on the input exactly. The selector axis (an escaped space before a combinator, `,`, or `{`, on a class/id/type leaf **or a pseudo**) is a plain bug rather than a divergence — prettier gets it right — and is pinned by [css/selectors/escaped_names](../tests/fixtures/css/selectors/escaped_names/); so is the whitespace-collapse inside a dropped forgiving-selector item ([forgiving_is_where](../tests/fixtures/css/selectors/forgiving_is_where_svelte_divergence/)). See [escaped_whitespace](../tests/fixtures/css/values/escaped_whitespace_prettier_divergence/). -**String glued to a non-ASCII space**: A comma- or space-separated value whose element is a quoted string glued to a **non-ASCII whitespace** character — a non-breaking space (U+00A0) or an em space (U+2003) — on either side, as in `grid-template-areas: 'x', 'y'` (leading) or `grid-template-columns: 'a', 'b'` (trailing). CSS whitespace is ASCII-only (CSS Syntax 3 §4.2), so a non-ASCII space is not a separator but value content — tsv treats every code point ≥ U+00A0 as an identifier code point, following Svelte's `parseCss` (broader than the CSS Syntax ident set, which excludes these look-alike whitespace chars). tsv keeps the element as **one opaque token, inline**, preserving the character. Prettier's value tokenizer splits it into a word (``) plus a string (`'y'`), which makes the group multi-token — so prettier inserts a space between them and, for a comma list, breaks it one item per line (` 'y'`). tsv does not split adjacent glued value tokens, so the run stays one token and its bytes survive verbatim — the same lossless form it emits for the pure-ASCII analog `font-family: 'x', a'y'` (tsv keeps `a'y'`; prettier splits to `a 'y'`). This pins a former **content loss**: the value parser's element-boundary trims and the printer's value-text whitespace normalizer both used the Unicode-aware `str::trim`, which drops a boundary non-ASCII space — for a string element outright (the narrowed text no longer began with a quote, so the string printer emitted nothing and the whole element vanished, `'x',;` then `'x';` on a second pass — non-idempotent). The fix trims only CSS whitespace (`trim_start_css` / `trim_end_preserving_escape`, the same escape-aware trims that keep an escaped `50px\ ` whole), matching CSS's ASCII-only definition. See [comma_string_nonascii_space](../tests/fixtures/css/values/lists/comma_string_nonascii_space_prettier_divergence/). +**String glued to a non-ASCII space**: A comma- or space-separated value whose element is a quoted string glued to a **non-ASCII whitespace** character — a non-breaking space (U+00A0) or an em space (U+2003) — on either side, as in `grid-template-areas: 'x', 'y'` (leading) or `grid-template-columns: 'a', 'b'` (trailing). CSS whitespace is ASCII-only (CSS Syntax 3 §4.2), so a non-ASCII space is not a separator but value content — tsv treats every code point ≥ U+00A0 as an identifier code point, following Svelte's `parseCss` (broader than the CSS Syntax ident set, which excludes these look-alike whitespace chars). tsv keeps the element as **one opaque token, inline**, preserving the character. Prettier's value tokenizer splits it into a word (``) plus a string (`'y'`), which makes the group multi-token — so prettier inserts a space between them and, for a comma list, breaks it one item per line (` 'y'`). tsv does not split adjacent glued value tokens, so the run stays one token and its bytes survive verbatim — the same lossless form it emits for the pure-ASCII analog `font-family: 'x', a'y'` (tsv keeps `a'y'`; prettier splits to `a 'y'`). The trims are load-bearing for **content**: a Unicode-aware `str::trim` at the value parser's element boundaries or in the printer's value-text whitespace normalizer drops a boundary non-ASCII space — for a string element outright (the narrowed text no longer begins with a quote, so the string printer emits nothing and the whole element vanishes, `'x',;` then `'x';` on a second pass — non-idempotent). Both trim only CSS whitespace (`trim_start_css` / `trim_end_preserving_escape`, the same escape-aware trims that keep an escaped `50px\ ` whole), matching CSS's ASCII-only definition. See [comma_string_nonascii_space](../tests/fixtures/css/values/lists/comma_string_nonascii_space_prettier_divergence/). -**Single value glued to a non-ASCII space**: The single-value (non-list) sibling of the above. A **single** declaration value glued to a non-ASCII whitespace character — a non-breaking space (U+00A0) or an em space (U+2003) — at either boundary, as in `content: 'z'` (leading) or `quotes: 'a'` (trailing). Same rule and same design choice: the non-ASCII space is value content — tsv treats it as an identifier code point (≥ U+00A0), following Svelte's `parseCss` — so tsv keeps the value as **one opaque token, inline** (`content: 'z'`), while prettier splits a **string** value into a word (``) plus the string and inserts a space (`content: 'z'`). For an **identifier** value (`font-family: q`) prettier keeps it glued, so tsv matches prettier there — only the string cases diverge. This pins a former **content loss** (the space was dropped, `content: 'z'` → `content: 'z'`): a **trailing** space was stripped by `locate_value`'s Unicode-aware value-boundary trim (now CSS-whitespace-only via the escape-aware `trim_start_css` / `trim_end_preserving_escape`), and a **leading** space was skipped by the lexer's `char::is_whitespace()` whitespace scan as part of the colon→value gap before the value began (the scan now treats only ASCII whitespace as CSS whitespace, per CSS Syntax 3 §4.2, so a code point ≥ U+00A0 — which `parseCss` treats as an identifier code point — is no longer skipped). See [boundary_nonascii_space](../tests/fixtures/css/values/boundary_nonascii_space_prettier_divergence/). +**Single value glued to a non-ASCII space**: The single-value (non-list) sibling of the above. A **single** declaration value glued to a non-ASCII whitespace character — a non-breaking space (U+00A0) or an em space (U+2003) — at either boundary, as in `content: 'z'` (leading) or `quotes: 'a'` (trailing). Same rule and same design choice: the non-ASCII space is value content — tsv treats it as an identifier code point (≥ U+00A0), following Svelte's `parseCss` — so tsv keeps the value as **one opaque token, inline** (`content: 'z'`), while prettier splits a **string** value into a word (``) plus the string and inserts a space (`content: 'z'`). For an **identifier** value (`font-family: q`) prettier keeps it glued, so tsv matches prettier there — only the string cases diverge. Two readers must agree for the **content** to survive (`content: 'z'` → `content: 'z'` otherwise): a **trailing** space reaches `locate_value`'s value-boundary trim, which is CSS-whitespace-only (the escape-aware `trim_start_css` / `trim_end_preserving_escape`), and a **leading** space reaches the lexer's whitespace scan in the colon→value gap, which treats only ASCII whitespace as CSS whitespace (CSS Syntax 3 §4.2), so a code point ≥ U+00A0 — which `parseCss` treats as an identifier code point — opens the value's first token rather than being skipped. See [boundary_nonascii_space](../tests/fixtures/css/values/boundary_nonascii_space_prettier_divergence/). -**Glued run of quoted strings**: The pure-ASCII sibling of the two glued cases above, with no separator at all. A single declaration value that is a **run of glued value tokens** whose first and last token are quoted strings, written adjacent: `content: 'a''b'` (two strings), `content: 'a'x'b'` (string, ident, string), `content: "a"x"b"` (double-quoted). tsv does not split adjacent glued value tokens, so it keeps the run as **one opaque token, inline**, verbatim (`content: 'a'x'b'`); prettier's tokenizer splits it into its component tokens and inserts a space, also normalizing the quote (`content: 'a' x 'b'`). Same lossless choice as `font-family: 'x', a'y'` (tsv keeps `a'y'`; prettier splits to `a 'y'`). This pins a former **content corruption**: the value re-parser's `parse_string_literal` classified the run as a single string on "first char is a quote and last char is the matching quote" — true for `'a'x'b'` even though its opening `'` closes at index 2 — then stripped the outer quotes and re-quoted the interior, turning the **delimiter** quotes into literal content (`content: 'a'x'b'` → `content: "a'x'b"`, a value whose CSS meaning changed and which round-trips to a different AST). The fix requires the opening quote's first **unescaped** matching close to fall at the end of the run (`quoted_string_spans_all`); otherwise the run is kept verbatim as an opaque identifier. An escaped interior quote (`'a\'b'`) still closes at the end, so a genuine single string is unaffected. See [glued_string_run](../tests/fixtures/css/values/glued_string_run_prettier_divergence/). +**Glued run of quoted strings**: The pure-ASCII sibling of the two glued cases above, with no separator at all. A single declaration value that is a **run of glued value tokens** whose first and last token are quoted strings, written adjacent: `content: 'a''b'` (two strings), `content: 'a'x'b'` (string, ident, string), `content: "a"x"b"` (double-quoted). tsv does not split adjacent glued value tokens, so it keeps the run as **one opaque token, inline**, verbatim (`content: 'a'x'b'`); prettier's tokenizer splits it into its component tokens and inserts a space, also normalizing the quote (`content: 'a' x 'b'`). Same lossless choice as `font-family: 'x', a'y'` (tsv keeps `a'y'`; prettier splits to `a 'y'`). The classification is what protects the **content**: a `parse_string_literal` that called the run a single string on "first char is a quote and last char is the matching quote" — true for `'a'x'b'` even though its opening `'` closes at index 2 — would strip the outer quotes and re-quote the interior, turning the **delimiter** quotes into literal content (`content: 'a'x'b'` → `content: "a'x'b"`, a value whose CSS meaning changes and which round-trips to a different AST). So the run is a single string only when the opening quote's first **unescaped** matching close falls at the end of the run (`quoted_string_spans_all`); otherwise it is kept verbatim as an opaque identifier. An escaped interior quote (`'a\'b'`) still closes at the end, so a genuine single string is unaffected. See [glued_string_run](../tests/fixtures/css/values/glued_string_run_prettier_divergence/). **Escaped-paren url() whitespace**: An unquoted `url(...)` whose content carries an escaped paren (`\(` / `\)`) and outer whitespace inside the parens. Per CSS Syntax 3 §4.3.6 the `` tokenizer consumes the leading/trailing whitespace, so its canonical value is trimmed; tsv emits that form (`url( a\(b )` → `url(a\(b)`). Prettier trims plain url whitespace the same way, but its postcss escaped-paren handling — the same bug that makes it *throw* on [url_escaped_paren](../tests/fixtures/css/values/functions/url_escaped_paren_prettier_divergence/) (`url(a\)b)` → `Unbalanced parenthesis`) — here stops normalizing and keeps the whitespace verbatim (`url( a\(b )` stays as-is). Prettier's output is valid, but tsv follows the spec's canonical serialization, so the divergence is escaped-paren-only (plain content agrees). Because tsv's trimmed form is dual-stable, this is pinned by a `prettier_variant_*` rather than an `output_prettier`. On Prettier's own corpus it surfaces in `tests/format/css/url/url.css`. See [url_escaped_paren_ws](../tests/fixtures/css/values/functions/url_escaped_paren_ws_prettier_divergence/). diff --git a/docs/conformance_prettier_svelte.md b/docs/conformance_prettier_svelte.md index d3ff39382..f34bc486d 100644 --- a/docs/conformance_prettier_svelte.md +++ b/docs/conformance_prettier_svelte.md @@ -45,7 +45,7 @@ the parser keeps the wider set deliberately. See - Wide inline child own-line — ◆print_width — [inline_component_wide_long](../tests/fixtures/svelte/elements/inline_component_wide_long_prettier_divergence/) (component), [inline_component_wide_longname_long](../tests/fixtures/svelte/elements/inline_component_wide_longname_long_prettier_divergence/) (long tag name), [inline_component_wide_multi_long](../tests/fixtures/svelte/elements/inline_component_wide_multi_long_prettier_divergence/) (two components), [inline_component_wide_multiattr_long](../tests/fixtures/svelte/elements/inline_component_wide_multiattr_long_prettier_divergence/) (multi-attr, breakable inner), [inline_svelte_element_wide_long](../tests/fixtures/svelte/elements/inline_svelte_element_wide_long_prettier_divergence/) (special element), [inline_element_wide_long](../tests/fixtures/svelte/elements/inline_element_wide_long_prettier_divergence/) (HTML element parity), [inline_element_wide_multiattr_long](../tests/fixtures/svelte/elements/inline_element_wide_multiattr_long_prettier_divergence/) (HTML element multi-attr drop coupling) - Wide inline content + trailing text — ◆print_width ◆design_choice — [inline_wide_content_trailing_long](../tests/fixtures/svelte/elements/inline_wide_content_trailing_long_prettier_divergence/) (wide content lays out **block-style** with both tags intact; the terminal tail is layout-keyed — a **space** hugs the intact ``, an authored **newline** keeps the tail's own line beside the multiline-rendering element (`variant_newline_tail`, dual-stable), and `variant_blank_line_tail` pins the rule's edge: an authored blank line is a separate signal and survives under both formatters; `` + ``), [inline_wide_content_trailing_sandwiched_long](../tests/fixtures/svelte/elements/inline_wide_content_trailing_sandwiched_long_prettier_divergence/) (same, but the wide element is **sandwiched** by a preceding text sibling — the space-spelled terminal tail still hugs), [inline_wide_content_text_sibling_long](../tests/fixtures/svelte/elements/inline_wide_content_text_sibling_long_prettier_divergence/) (non-terminal text after a **prose-content** wide element: the space spelling hugs the intact closing tag when it fits; prettier re-breaks the hug), [inline_wide_element_content_tail_long](../tests/fixtures/svelte/elements/inline_wide_element_content_tail_long_prettier_divergence/) (the **element-content** twin at the exact 100/101 boundary, where the space spelling's per-width answer is additionally forced by the regeneration invariant; prettier re-breaks the hug), [inline_nested_child_trailing_space_long](../tests/fixtures/svelte/elements/inline_nested_child_trailing_space_long_prettier_divergence/) (nested wide child + trailing text: the space-authored tail hugs ``; the newline authoring is dual-stable beside the multiline child — `variant_ownline`), [inline_short_content_fill_long](../tests/fixtures/svelte/elements/inline_short_content_fill_long_prettier_divergence/) (the **short-element** counterpart: a short inline element sandwiched in a wrapping fill packs greedily and pairwise after the preceding word rather than being isolated onto its own line; the print_width wrap is the sole divergence), [inline_short_component_fill_long](../tests/fixtures/svelte/elements/inline_short_component_fill_long_prettier_divergence/) (the **component** sibling of the above — a short inline *component* packs pairwise identically, since both route through the same element-type-agnostic after-element fold). See [§Svelte: Inline content block-style](#svelte-inline-content-block-style). - Inline closing intact — ◆print_width — [inline_closing_intact_long](../tests/fixtures/svelte/elements/inline_closing_intact_long_prettier_divergence/) -- Fill multi-expression run travels — ◆print_width — [fill_multi_expr_travel_long](../tests/fixtures/svelte/elements/fill_multi_expr_travel_long_prettier_divergence/) (the rule + its 100/101 boundary: a welded word+tag unit that fits stays flat, the first that doesn't travels whole; glued ternary+tag pairs around a spaced `=` split at the boundary after the `=`; a unit too wide for a fresh line travels first, then breaks internally), [fill_multiple_expr_long](../tests/fixtures/svelte/elements/fill_multiple_expr_long_prettier_divergence/) (the same run at an indent deep enough that prettier dangles the parent's opening bracket instead), [fill_competing_expr](../tests/fixtures/svelte/elements/fill_competing_expr_prettier_divergence/) (**two** breakable expressions — neither breaks, the overflowing unit travels; pins convergence across the padded/hugged/dangled boundary authorings, which historically oscillated) +- Fill multi-expression run travels — ◆print_width — [fill_multi_expr_travel_long](../tests/fixtures/svelte/elements/fill_multi_expr_travel_long_prettier_divergence/) (the rule + its 100/101 boundary: a welded word+tag unit that fits stays flat, the first that doesn't travels whole; glued ternary+tag pairs around a spaced `=` split at the boundary after the `=`; a unit too wide for a fresh line travels first, then breaks internally), [fill_multiple_expr_long](../tests/fixtures/svelte/elements/fill_multiple_expr_long_prettier_divergence/) (the same run at an indent deep enough that prettier dangles the parent's opening bracket instead), [fill_competing_expr](../tests/fixtures/svelte/elements/fill_competing_expr_prettier_divergence/) (**two** breakable expressions — neither breaks, the overflowing unit travels; pins convergence across the padded/hugged/dangled boundary authorings) - Inline content (text) — ◆design_choice — [inline_content_text_wrap](../tests/fixtures/svelte/elements/inline_content_text_wrap_prettier_divergence/), [text_non_breaking_whitespace](../tests/fixtures/svelte/elements/text_non_breaking_whitespace_prettier_divergence/) — an inline element's wrapping text content lays out **block-style** (both tags intact, content on its own indented line, collapsing inline when it fits); prettier pre-breaks the opening tag. See [§Svelte: Inline content block-style](#svelte-inline-content-block-style). - Inline content (expression) — ◆design_choice — [inline_content_hug_long](../tests/fixtures/svelte/elements/inline_content_hug_long_prettier_divergence/) — breakable-expression content also lays out **block-style** (uniform with text/element content), where prettier dangles. See [§Svelte: Inline content block-style](#svelte-inline-content-block-style). - Inline content (line-continuation string) — ◆design_choice — [string_line_continuation_content](../tests/fixtures/svelte/expressions/string_line_continuation_content_prettier_divergence/) — a line-continuation string literal's mandatory newline is the **third** block-style trigger (a leaf's forced newline, beside width and structure): the element goes block-style with the leaf un-broken, where prettier dangles the tag delimiters around it. See [§Svelte: Inline content block-style](#svelte-inline-content-block-style). @@ -76,7 +76,7 @@ the parser keeps the wider set deliberately. See **Wide inline child own-line**: When an inline child (component or HTML element) is too wide to share a line with the preceding text, Prettier hugs it onto the text line (101+) and breaks it internally — attributes wrap and the closing `>` dangles (the inline content hug). tsv keeps printWidth a hard limit, so the whole child drops to its own line and the preceding word stays hugged on the text line. The break sits at the collapsible space before the child, so it holds regardless of tag-name length (`inline_component_wide_longname_long`), for repeated children in one run (`inline_component_wide_multi_long`), for a multi-attribute component whose own attributes could break (`inline_component_wide_multiattr_long` — it still drops whole, attributes intact), and for special elements (`inline_svelte_element_wide_long`); HTML inline elements produce the identical shape (`inline_element_wide_long`, and `inline_element_wide_multiattr_long` for the multi-attribute counterpart). When a *short* dropped child (its content fits, so the whole tag stays intact on its own line) is followed by trailing text, that text **hugs the intact tag on either authored boundary**, exactly like the wide-content case below — the boundary is render-free, so a newline converges to the form a space produces — because the short child packs its tail like any other fill word (`inline_component_wide_multiattr_long` / `inline_element_wide_multiattr_long`). These fixtures also pin idempotence — the compact authored form and Prettier's hugged form both normalize to the flow form in one pass. -**Wide inline content + trailing text**: The mirror of the above for an element whose own _content_ (not its attributes) overflows, followed by trailing text — `…90 chars… tail`. Prettier keeps the content on a single over-width line and lets `>…content…` with no attributes and `` with a short one): a **space** boundary hugs the intact `` (` tail`), and an authored **newline** keeps the tail's own line beside the multiline-rendering element — both spellings are clean there, so the authored one is kept (`variant_newline_tail`, dual-stable; the authored-newline rule below). This mirrors how a _short_ inline element already keeps `x tail` inline, and it matches prettier's own boundary split (hug a space, keep a newline), so on either authoring the **content wrap is the divergence**. The `unformatted_ours_*` variants pin tsv's idempotence on the space-authored forms. The convergence stops at a **blank line** — an authored blank line is a Tier-2 signal independent of render, so it survives under both formatters (`variant_blank_line_tail`, dual-stable). A **non-terminal** text run (followed by another inline element) takes the same answer per spelling: the tail boundary's **space** spelling after an inline element is a fill decision measured from the closing tag's own column — the tail hugs when it fits and breaks when it does not, however the element came to be multiline (the newline spelling is layout-keyed, per the authored-newline rule below) (`inline_wide_content_text_sibling_long` for prose content, `inline_wide_element_content_tail_long` for element-child content at the exact 100/101 boundary; prettier groups the boundary with the element, so its tail always drops once the element is multiline, and it re-breaks the hugged form where tsv holds it). For the **regenerating kinds** — a component, or an element with any non-text-only content — the per-width answer for the space spelling is forced by idempotence, not taste: their width-broken form emits the very boundary newlines the next parse reads as the Tier-2 expansion signal, so the width-broken and statically-broken renderings must answer the tail boundary identically or the document converges only on pass 2 (the authored-newline join below satisfies the same invariant by construction — the joint group breaks for a width break and a static break alike) ([inline_comment_wrap_fill_tail_long](../tests/fixtures/svelte/elements/inline_comment_wrap_fill_tail_long_prettier_divergence/) pins the fill-content case inside a comment wrap, where a stale non-fill reading once left a period-2 on the formatter's own output). The one scope where the joint element+boundary measurement survives is a **text-only** element — the one content kind whose emitted newlines re-trigger nothing (text-only content's layout is width's alone) — inside a leading inline-sibling wrap whose **other side does not flow**: a spaced comment or a control-flow block, whose own-line spelling is a fixed point and therefore the only convergeable target; two boundaries then met on one element and resolved outside-in (see the two-boundaries entry below). That scope is **retired** — the fused measurement broke at the width where the element lays its own content out block-style (`inline_sibling_drop_tail_wide_long`), and every non-terminal tail now answers per width, the comment boundary keeping its authored line as one of two fixed points (`inline_sibling_drop_tail_flow_long`). A wrap coming from an element or tag sibling flows, so it takes the per-width answer like every unwrapped shape (`inline_wide_content_text_sibling_long`'s element-sibling case). The terminal-hug is scoped to *terminal* trailing text, **not** to a first-child element: a wide element **sandwiched** by a preceding text sibling (pushed to its own line by a preceding break) still lets its space-authored terminal tail hug the intact `` (`inline_wide_content_trailing_sandwiched_long`) — nothing follows the tail, so the hug stays convergent. The rule also covers the **dropped short child**: a short element pushed to its own line by the preceding text (`inline_component_wide_multiattr_long` / `inline_element_wide_multiattr_long`) packs its tail after the intact tag on either authoring, just like the wide-content case. It reaches the **nested wide child** too (`inline_nested_child_trailing_space_long`) — the space-authored tail hugs ``, and the newline authoring is dual-stable beside the multiline child (`variant_ownline`), per the authored-newline rule below. +**Wide inline content + trailing text**: The mirror of the above for an element whose own _content_ (not its attributes) overflows, followed by trailing text — `…90 chars… tail`. Prettier keeps the content on a single over-width line and lets `>…content…` with no attributes and `` with a short one): a **space** boundary hugs the intact `` (` tail`), and an authored **newline** keeps the tail's own line beside the multiline-rendering element — both spellings are clean there, so the authored one is kept (`variant_newline_tail`, dual-stable; the authored-newline rule below). This mirrors how a _short_ inline element already keeps `x tail` inline, and it matches prettier's own boundary split (hug a space, keep a newline), so on either authoring the **content wrap is the divergence**. The `unformatted_ours_*` variants pin tsv's idempotence on the space-authored forms. The convergence stops at a **blank line** — an authored blank line is a Tier-2 signal independent of render, so it survives under both formatters (`variant_blank_line_tail`, dual-stable). A **non-terminal** text run (followed by another inline element) takes the same answer per spelling: the tail boundary's **space** spelling after an inline element is a fill decision measured from the closing tag's own column — the tail hugs when it fits and breaks when it does not, however the element came to be multiline (the newline spelling is layout-keyed, per the authored-newline rule below) (`inline_wide_content_text_sibling_long` for prose content, `inline_wide_element_content_tail_long` for element-child content at the exact 100/101 boundary; prettier groups the boundary with the element, so its tail always drops once the element is multiline, and it re-breaks the hugged form where tsv holds it). For the **regenerating kinds** — a component, or an element with any non-text-only content — the per-width answer for the space spelling is forced by idempotence, not taste: their width-broken form emits the very boundary newlines the next parse reads as the Tier-2 expansion signal, so the width-broken and statically-broken renderings must answer the tail boundary identically or the document converges only on pass 2 (the authored-newline join below satisfies the same invariant by construction — the joint group breaks for a width break and a static break alike) ([inline_comment_wrap_fill_tail_long](../tests/fixtures/svelte/elements/inline_comment_wrap_fill_tail_long_prettier_divergence/) pins the fill-content case inside a comment wrap, where a non-fill reading leaves a period-2 on the formatter's own output). No scope keeps a joint element+boundary measurement — not even a **text-only** element (the one content kind whose emitted newlines re-trigger nothing; text-only content's layout is width's alone) inside a leading inline-sibling wrap whose **other side does not flow** (a spaced comment or a control-flow block, whose own-line spelling is a fixed point): the fused measurement breaks at the width where the element lays its own content out block-style (`inline_sibling_drop_tail_wide_long`), so every non-terminal tail answers per width, the comment boundary keeping its authored line as one of two fixed points (`inline_sibling_drop_tail_flow_long`; the two-boundaries entry below). A wrap coming from an element or tag sibling flows, so it takes the per-width answer like every unwrapped shape (`inline_wide_content_text_sibling_long`'s element-sibling case). The terminal-hug is scoped to *terminal* trailing text, **not** to a first-child element: a wide element **sandwiched** by a preceding text sibling (pushed to its own line by a preceding break) still lets its space-authored terminal tail hug the intact `` (`inline_wide_content_trailing_sandwiched_long`) — nothing follows the tail, so the hug stays convergent. The rule also covers the **dropped short child**: a short element pushed to its own line by the preceding text (`inline_component_wide_multiattr_long` / `inline_element_wide_multiattr_long`) packs its tail after the intact tag on either authoring, just like the wide-content case. It reaches the **nested wide child** too (`inline_nested_child_trailing_space_long`) — the space-authored tail hugs ``, and the newline authoring is dual-stable beside the multiline child (`variant_ownline`), per the authored-newline rule below. **An authored newline after the closing tag follows the element's rendered layout (◆design_choice).** A single authored newline between an inline unit's closing tag — an element's or component's ``, a `svelte:*` element's, or a void/self-closing unit's `/>` — and following sibling text is **preserved when the element renders multiline** and **reflowed when it renders inline** — `⏎text` beside a multiline-rendering `` keeps the text on its own line (the author separated the prose from the tag pile; both forms are clean, so the authored one is kept), while the same authoring beside a fitting element reflows with the fill and converges with the space spelling. The **space** spelling never takes this rule: it stays the per-width hug above, so the boundary is dual-stable beside a multiline unit and single-form beside a fitting one. The mechanism is a render-time probe, not a second measurement: the predecessor's doc carries `flow_break_probe` (the renderer records whether its subtree actually emitted a newline) and the tail's fill carries `hold_line_after_broken_flow` (its leading line renders as a forced break exactly when the probe answered yes) — so the decision is layout-keyed at render with **no measurement change at all**, which is what keeps it out of the §129 class: an outer fits walk sees an ordinary fill whose leading line is an ordinary break opportunity, the element's own lead boundary is decided exactly as before (the comment-hug fixed point of `inline_sibling_drop_tail_flow_long` stands), and the regeneration invariant holds by construction. (A `group([element, line])` join is not equivalent: it gets measured *through* by the preceding boundary's fit walk — the tail's leading break point vanishes from the measurement space — re-breaking that boundary on the re-parse, a 2-cycle only the width sweep can see.) Two shapes keep the per-width answer instead: a **tag** predecessor (`{expr}⏎text` — its break lands inside its own expression, so the tag-pile reading does not arise) and a **glued-headed** unit (wrapping its doc in a probe context would bury the welded-run marker from the flow walk). Prettier preserves the authored spelling at every such boundary regardless of the element's layout, so the fitting-side reflow is the divergence and the multiline-side preserve is agreement; prettier also normalizes one shape itself (a non-terminal space-hugged tail after a multiline element, ` text2⏎a` and `ab` converge alike — **when that separator's inline run holds a content text**, and not otherwise. A run of pure elements or tags (``) keeps its authored lines: with no prose its newlines are the only structure the author has, collapsing them packs independent siblings onto one line, and on a short run the collapse cascades into the parent element's own hug decision — an outright idempotency break (`` re-hugs on the second pass once its children fold). A run is bounded by whatever owns its own line — a block element, a control-flow block, a comment — and by an authored blank line, so a fragment's prose does not license flow in a neighbouring run that has none. Bounding is not sterilizing, though: a run that *does* hold prose still flows right up against the bounding sibling, and the two spellings of that ("its own boundaries are held" vs "every line near it is held") are only separated by a control that re-authors the neighbouring run — see the comment-boundary note above. Two mechanics keep this sound and are load-bearing. First, a flowing single newline takes the **space arm verbatim** rather than a parallel one: emitting a different collapsible form for the newline than for the space it claims to equal makes the formatter write a newline that its next pass re-reads as flowable and collapses. Second, the boundary must defer to the next sibling for a **tag** as well as an element — a bare `line` in the child list resolves all-or-nothing with the parent group, and a multiline fragment's parent group is always broken, so without that the one boundary owned by a content text's `fill` would flow while the rest of the same run hard-broke. That deferral is gated on the flow predicate and not on "next is a tag", because a plain authored space before a tag may have a **comment** on its other side, whose line is authorship. Note `authoring_audit` is blind to the tag half: both spellings agree there whichever form the rule picks — on the flowed form now, on the hard-broken one under a narrower gate — so it scores them as converging either way. — [inline_sibling_newline_flow](../tests/fixtures/svelte/elements/inline_sibling_newline_flow_prettier_divergence/) (an inline element, a component and all three tag kinds — expression, render and `{@html}`, one predicate arm each — plus all five controls) and [expressions/angle_escaped](../tests/fixtures/svelte/expressions/angle_escaped_prettier_divergence/) (the tag-separator case, where prettier's plain `line` holds each tag on its authored line), and the shapes the rule reaches across the suite: [fill_text_inline_collapse](../tests/fixtures/svelte/elements/fill_text_inline_collapse_prettier_divergence/), [root_text_newline_inline](../tests/fixtures/svelte/elements/root_text_newline_inline_prettier_divergence/), [block_text_multiline_collapse](../tests/fixtures/svelte/elements/block_text_multiline_collapse_prettier_divergence/), [inline_attrs_multiline_content](../tests/fixtures/svelte/elements/inline_attrs_multiline_content_prettier_divergence/), [components/root_with_text](../tests/fixtures/svelte/components/root_with_text_prettier_divergence/). **The CONTAINER holding the run is not an axis either**, and no fixture above varies it to say so — nearly all put the flowing run in a block container, and the one inline container that carries one ([inline_attrs_multiline_content](../tests/fixtures/svelte/elements/inline_attrs_multiline_content_prettier_divergence/)) reaches multiline past a 99-char attribute line, confounding the kind with the attribute wrap that fixture exists to test. An inline element reaches the multiline form by a *different boundary rule* — both-or-neither, rather than the block's leading boundary alone — so it is the kind where a container-keyed rule would show, and the run lays out identically once there: [container_kind_newline_flow](../tests/fixtures/svelte/elements/container_kind_newline_flow_prettier_divergence/) holds one run fixed and varies the container across all four kinds — `