Skip to main content

bynk_ts/
printer.rs

1//! The printer — `TsProgram -> Artefacts` (R7.3: "printing is `TsProgram ->
2//! Artefacts`. The printer owns the buffer, the indentation and the offset
3//! arithmetic. It is the only code in the compiler that writes a
4//! character."). Owns one buffer for the whole [`TsProgram`], so unlike
5//! `bynk-emit`'s own splice-based lowering (`crate::source_map`'s own module
6//! doc), it never needs to rebase a sub-buffer's checkpoints — it records
7//! directly from each statement's own `span` as it writes, which is what
8//! makes R7.4 ("the source map is produced by the printer from
9//! `TsNode.span`. No phase before the printer records an offset") true by
10//! construction for this path, from this slice.
11//!
12//! # Readability policy (R7.5)
13//!
14//! R7.5: "Readable output is a printer policy with a name and a test, not a
15//! property of how carefully strings were typed." P7.7 named the policy's
16//! then-whole surface (statement separation); P7.8 (#1313) extends it now
17//! that the printer has real structured nodes to have an opinion about
18//! (`TsExpr`/`TsType`/`TsDecl`, plus real `TsStmt` variants):
19//!
20//! - **Every statement starts on its own generated line, so two statements
21//!   can never share one.** Unchanged from P7.7 — see [`print()`]'s own
22//!   loop. Still true for the new node kinds: every one of their own
23//!   renderers ends its own output in `\n`.
24//! - **Two-space indentation, one level per nesting depth.** The only
25//!   indentation width this printer produces — chosen because it's the one
26//!   `events_fanout.rs` (this slice's own grounding file) already uses, not
27//!   because of any earlier general readability decision; matches the
28//!   depth threaded through [`render_stmt`]/[`render_block_body`] exactly.
29//! - **One blank line separates top-level declarations, except two
30//!   consecutive `import`s or two consecutive `Comment`s.** [`print()`]'s
31//!   own loop implements this by peeking at the next statement — matching
32//!   `events_fanout.rs`'s own real spacing (its two `import` lines sit
33//!   adjacent, and so do its own two header-comment lines; every other pair
34//!   of top-level declarations has a blank line between). Added for Arc C's
35//!   own first real conversion slice (#1317): every `bynk-emit`-generated
36//!   file opens with a multi-line header banner, one [`TsStmtKind::Comment`]
37//!   per real line.
38//! - **[`TsStmt::no_blank_before`] suppresses that same blank immediately
39//!   before the statement carrying it**, composing with every rule above
40//!   rather than replacing any of them — a property of the *statement*, not
41//!   of a pair, so it stays correct however many different kinds end up
42//!   adjacent to it. Scoped to exactly [`print()`]'s own top-level loop: a
43//!   statement carrying it inside a `Block`/`InlineBlock`/function body, or
44//!   printed through [`print_stmt`] / `bynk-emit`'s own `extend_printed`
45//!   splicing helpers, is silently ignored there today — harmless only
46//!   because none of those paths inserts an automatic blank of its own in
47//!   the first place. Added for #1486: `emit_agent`'s own already-shipped
48//!   registry-`const`-then-zero-factory pairing needs zero blank lines
49//!   between two adjacent real top-level statements, a shape no existing
50//!   pair rule above can express without a `TsDecl`-pair special case.
51//! - **Inside a `class`, no blank line between fields and the constructor;
52//!   one blank line before each method.** [`render_decl_body`]'s own
53//!   `TsDecl::Class` arm — again, this is what `events_fanout.rs` itself
54//!   does, not a general rule derived some other way.
55//! - **`if`/`for...of`'s own body prints with braces when it's a
56//!   `TsStmtKind::Block`, and inline on the same line otherwise** —
57//!   `if (!Array.isArray(subs)) continue;` has no braces in the grounding
58//!   file; a `for...of` always does. See [`render_branch`].
59//! - **A statement's own interior is not the printer's concern for
60//!   `Verbatim` content — still.** `Verbatim` text renders exactly as
61//!   written (P7.7's own boundary, unchanged); everything above applies
62//!   only to the new, real node kinds this slice adds.
63//! - **A shorthand async method entry in an object literal
64//!   (`TsObjectEntry::Method`) prints exactly like a class method** —
65//!   `async name(params) { <body> },` with a trailing comma, body indented
66//!   one level deeper than the entry itself, closing `},` back at the
67//!   entry's own indent. Added for Arc C's own third slice (#1321,
68//!   `workers.rs`): the `compose`-returned surface object's dominant real
69//!   shape, one entry per wrapper.
70//! - **`?.`/`?.[...]` print with no surrounding spaces** — `object?.property`/
71//!   `object?.[index]`, matching plain `Member`/`Index`'s own spacing
72//!   exactly, just with the extra `?`. Added for #1321: `workers.rs`'s own
73//!   secret-probe idiom.
74//! - **An optional field in a type-position object literal prints `key?:
75//!   ty`** — the direct type-side counterpart to `TsParam.optional`'s
76//!   already-established `name?: ty` for parameters. Added for #1321.
77//! - **An arrow function prints `(params) => body` on one line, expression
78//!   body only** — no block-body form exists yet (nothing built here needs
79//!   one). Added for #1321.
80//! - **A nested `Binary` operand of another `Binary` parenthesizes only
81//!   when the inner operator's precedence is not strictly higher than the
82//!   outer's, or when the same operator nests in itself and that operator
83//!   is `||`/`&&`** (`render_binary_operand`) — every other operand
84//!   position (`Member`/`Index`/`Call`/`New`/`Await`/`Unary`,
85//!   `render_operand`) still always parenthesizes a nested `Binary`/`As`/
86//!   `Arrow`/`Conditional`, unchanged. Added for #1321: `workers.rs`'s own
87//!   `__authz === null || !__authz.startsWith(...)` has no parens around
88//!   the comparison in the byte-golden fixtures, and `??` mixed with
89//!   `||`/`&&` still always parenthesizes (TS forbids that combination
90//!   unparenthesized). Extended for #1323: a 3+-term `||`/`&&` chain of the
91//!   *same* operator (`typeof args !== "object" || args === null ||
92//!   Array.isArray(args)`) prints flat, with no parens around the nested
93//!   pair — `??` chained with itself keeps the pre-#1321 conservative
94//!   choice (still parenthesized), since nothing real needs it flattened.
95//! - **`switch (<discriminant>) { <cases> }` — a non-`default` `case` is
96//!   always `{ }`-blocked; a `default` case never is.** Added for #1323:
97//!   `workers_entry.rs`'s four real `switch` statements are the first real
98//!   content this tree represents with a genuinely new statement-grouping
99//!   construct, not a single expression/type variant.
100//! - **`export default <expr>;` prints the expression through the same
101//!   depth-aware multiline-object handling `const`/`let`/`return`/`Assign`
102//!   already get.** Added for #1323: `workers_entry.rs`'s own top-level
103//!   shape is a default-exported object literal.
104//! - **`export { a, b } from "spec";` — a re-export — prints with no blank
105//!   line rule of its own** (it is not classified alongside `import`/
106//!   `import * as` for the "no blank line between adjacent imports"
107//!   exception, so the ordinary "one blank line between top-level
108//!   declarations" default applies both before and after it). Added for
109//!   #1323: `workers_entry.rs`'s own agent/fan-out-DO re-exports, each
110//!   already separated from its neighbours by a blank line in the
111//!   pre-conversion output.
112//! - **`test ? consequent : alternate` prints on one line, with `test`
113//!   parenthesized only when it is itself an `Arrow` or a `Conditional`.**
114//!   Added for #1323: `workers_entry.rs`'s own two real ternaries.
115//! - **A `readonly`/method-signature/index-signature member of a
116//!   type-position object literal prints `readonly name: ty` /
117//!   `name(params): ret` / `[key: key_ty]: value_ty`.** Added for #1323:
118//!   `workers_entry.rs`'s own `scheduled`/`queue` parameter types and its
119//!   multi-param `on call` dispatch's `args as { [k: string]: JsonValue }`.
120//! - **An explicit `Paren` always prints its own literal parentheses,
121//!   independent of the wrapped expression's own precedence.** Added for
122//!   #1323: `workers_entry.rs`'s CORS-preflight guard condition, which the
123//!   pre-conversion code always wrapped in `(...)` even when nothing about
124//!   the wrapped expression's own precedence required it.
125//! - **A bare blank line (`Blank`) prints exactly one empty line**, usable
126//!   at any nesting depth — distinct from `print()`'s own top-level-only
127//!   blank-line policy above. Added for #1323: three specific points inside
128//!   `workers_entry.rs`'s `fetch` method body.
129//! - **An `if`'s own `else` prints on its own fresh line, at the `if`'s own
130//!   indent, then follows the same block-vs-inline branch rule the `if`
131//!   itself does.** Added for #1323: `workers_entry.rs`'s queue-consumer
132//!   ack/retry dispatch.
133//! - **`InlineBlock` prints `{ stmt; stmt; ... }` on one generated line**,
134//!   distinct from `Block`'s always-multi-line form — reachable only as an
135//!   `if`/`else` branch in real content today. Added for #1323:
136//!   `workers_entry.rs`'s own two real sites (a deserialise-failure guard,
137//!   and the ack/retry dispatch's `else`), both hand-written as compact
138//!   one-liners in the pre-conversion text.
139//! - **An array literal supports the same `multiline` shape an object
140//!   literal does** — one item per line, each with its own trailing comma,
141//!   closing `]` at the statement's own indent, only through
142//!   `render_stmt_level_expr`. Added for #1325: `emit_test_main`'s own
143//!   `modules` array.
144//! - **A template literal's static parts print verbatim, with no escaping of
145//!   their own.** Added for #1325 — see `TsExpr::TemplateLit`'s own doc for
146//!   why (a generic escaper would double an already-pre-formed JS unicode
147//!   escape's own literal backslash).
148//! - **`declare const name: ty;` prints with no initialiser at all** —
149//!   distinct from every other `const` form, which always has one. Added
150//!   for #1325: `emit_test_main`'s own ambient `process` declaration.
151//! - **An `async function` prints `async function name(...)`** — the
152//!   top-level sibling to `TsObjectEntry::Method`'s own `is_async` handling.
153//!   Added for #1325: `emit_test_main`'s own top-level `main`.
154//! - **`<expr>++;` prints a bare postfix increment as a whole statement.**
155//!   Added for #1325: `emit_test_main`'s own `passed++;`/`failed++;`
156//!   counters.
157//! - **An `if`'s own `else` prints `} else {` on the same physical line when
158//!   `same_line_else` is set** (only reachable when `then_branch` is a
159//!   `Block` or `InlineBlock`) — a second, real convention alongside the
160//!   fresh-line default just above, not a replacement for it. Added for
161//!   #1325: all three of `emit_test_main`'s own real `if`/`else` sites use
162//!   this spacing, none use the fresh-line form `workers_entry.rs`'s own
163//!   real content needs — two already-real files disagreeing on the same
164//!   construct, the same tension the `Await`-under-`As` correction
165//!   (#1323/#1324) found for parenthesisation.
166//! - **`export * from "spec";` — a wildcard re-export — groups with itself
167//!   AND with an immediately-preceding header `Comment`, no blank line
168//!   either way.** A different spacing shape from `ReExport`'s own (no
169//!   grouping rule, see above): `emit_commons_barrel`'s own real barrel
170//!   module is one header comment followed by one `export *` line per
171//!   constituent source file, every one of those lines adjacent with no
172//!   blank line anywhere. Added for #1329.
173//! - **A `TsStmtKind::DocComment` prints a JSDoc block comment**, one
174//!   ` * <line>` per non-blank source line, a blank source line as a bare
175//!   ` *`. Distinct from `Comment`'s own `//`-per-line form, and printed
176//!   only through [`print_stmt`] (`bynk-emit`'s own `emit_doc_block`, a
177//!   shared helper spliced into still-unconverted callers' buffers), never
178//!   through [`print()`]'s own `TsProgram` loop — so this shape has no
179//!   blank-line grouping rule of its own to name here. Added for #1333.
180//! - **A `TsStmtKind::Raw` prints its own text verbatim** — no leading
181//!   indent, no added punctuation, the same rendering `Verbatim` gets
182//!   (deliberately a distinct kind, not a reuse of it — see
183//!   [`crate::program::TsStmtKind::Raw`]'s own doc for why). Never reached
184//!   through [`print()`]'s own `TsProgram` loop — no blank-line grouping
185//!   rule of its own to name here either. Added for #1337:
186//!   `emit_method`'s own body, delegated wholesale to `emitter/lower.rs`'s
187//!   `emit_block_as_function_body_with_return` — a permanent Arc C
188//!   exclusion (ADR `arc-c-lower-rs-permanent-exclusion`), not residue.
189//!   #1339 added a second, differently-reasoned real use:
190//!   `emit_refined_type`'s own `of()` guard body, carrying `emit_refined_
191//!   checks`'s already-printed output — not a permanent exclusion, just
192//!   scope that function's own conversion didn't reach. Both uses carry
193//!   text pre-indented at a fixed absolute depth by their own caller, so
194//!   `render_multiline_object_entry`'s own `debug_assert!` guards that this
195//!   only renders correctly at `depth == 0` — see its doc and
196//!   [`print_object_entry`]'s. **Reached through more than a
197//!   `TsObjectEntry::Method`'s own `body` now** (review of #1370 caught
198//!   this doc drifting stale): `emit_free_fn` (#1352) and `emit_agent`'s
199//!   own rehydrate function (#1369) both hold `Raw` statements inside an
200//!   ordinary `TsDecl::Function`'s own `body`, rendered through
201//!   `render_block_stmts` at whatever `depth` the enclosing `print_stmt`
202//!   call used (always 0 for a top-level declaration in every real site
203//!   today, satisfying the same depth-0 assumption named above). Since
204//!   #1369 also added `TsDecl::Function.inline`, a `Raw`-bearing function
205//!   body reached with `inline: true` would route through
206//!   `render_inline_block`/`render_compact_stmts` instead — see
207//!   [`render_inline_stmt`]'s own `Raw` arm for why that combination stays
208//!   deliberately unbuilt.
209//!
210//! None of the above is claimed as *the* TypeScript style this printer will
211//! use forever — it's what this slice's own grounding file needs, named
212//! rather than left implicit, the same posture the node algebra itself
213//! takes (`program.rs`'s own module doc). A future file with a construct
214//! this policy doesn't cover yet (an `else` branch, a multi-field class with
215//! blank lines between fields, …) extends it the same way `program.rs`'s
216//! own node list grows: file by file, against real content.
217
218use crate::program::{
219    TsArrowBody, TsBinaryOp, TsBindingName, TsClassMethod, TsDecl, TsExpr, TsLit, TsObjectEntry,
220    TsParam, TsProgram, TsStmt, TsStmtKind, TsType, TsTypeMember, TsUnaryOp,
221};
222use crate::source_map::SourceMapBuilder;
223
224/// The result of printing a [`TsProgram`]: the emitted text, and its source
225/// map — `None` when no checkpoint resolved, either because no statement
226/// carried a span or every span fell outside `source_text`
227/// ([`SourceMapBuilder::to_v3`]'s own "nothing resolves" case).
228#[derive(Debug)]
229pub struct Printed {
230    pub text: String,
231    pub source_map: Option<String>,
232}
233
234/// Print `program` to TypeScript text. `source_name`/`source_text` register
235/// the `.bynk` source every statement's own span is measured against —
236/// today always exactly one, since nothing spans two files yet;
237/// `output_file` names the generated file in the source map's own `file`
238/// field. Only a *top-level* statement's own span is recorded as a
239/// checkpoint (R7.4's existing scope, unchanged by P7.8 — see
240/// [`TsStmt::span`]'s own doc for why a nested statement's span isn't
241/// recorded yet).
242pub fn print(
243    program: &TsProgram,
244    source_name: &str,
245    source_text: &str,
246    output_file: &str,
247) -> Printed {
248    let mut map = SourceMapBuilder::new();
249    map.add_source(source_name, source_text);
250    print_with_map(program, map, output_file)
251}
252
253/// #1486's own multi-source sibling of [`print()`]: a test/integration
254/// module's own aggregate map spans one source per fragment file a case's
255/// body was spliced in from (`emit_test_module`/`emit_integration_module`,
256/// `project/tests_emit.rs`), not the single `.bynk` file `print`'s own
257/// signature assumes — see [`TsStmt::nested_map_source_id`]'s own doc for
258/// why a per-statement source selector needs a caller that can register
259/// more than one source in the first place. Takes the caller's own already-
260/// built `map` directly (rather than a `(name, text)` list built fresh
261/// here) so a caller can keep registering sources incrementally as it
262/// discovers them — `emit_test_module`'s own per-case loop, one `add_source`
263/// call per case, exactly as it already did before this function existed —
264/// and get each call's own returned id back immediately, to set on that
265/// case's own top-level statement's `nested_map_source_id`, instead of
266/// having to pre-compute the full source list up front. Every statement's
267/// own `nested_map_source_id` selects which of `map`'s already-registered
268/// sources (by id) its own `nested_map` merges against; a statement with no
269/// `span` of its own (this whole module's own shape — an aggregate with no
270/// single "primary" source of its own) never records a top-level
271/// checkpoint, so `map`'s own id-`0` source (if any) needs no special
272/// "primary" meaning here the way [`print()`]'s single source does.
273pub fn print_multi_source(
274    program: &TsProgram,
275    map: SourceMapBuilder,
276    output_file: &str,
277) -> Printed {
278    print_with_map(program, map, output_file)
279}
280
281fn print_with_map(program: &TsProgram, mut map: SourceMapBuilder, output_file: &str) -> Printed {
282    let mut out = String::new();
283    for (i, stmt) in program.stmts.iter().enumerate() {
284        if let Some(span) = stmt.span {
285            map.record(out.len(), span);
286        }
287        // #1477: also merges any nested checkpoints this top-level
288        // statement (or anything nested inside it) itself carries — the
289        // same mechanism `print_stmt_and_merge`/`print_class_method_and_
290        // merge` expose to a caller printing one fragment at a time,
291        // applied here automatically for every top-level statement in a
292        // real `TsProgram`. `source_id` defaults to `stmt.
293        // nested_map_source_id`'s own default of `0`, matching `record`'s
294        // own unconditional targeting of the primary source just above for
295        // every single-source module; #1486's own multi-source case
296        // (`nested_map_source_id`'s own doc comment) sets it per statement.
297        render_stmt(
298            &mut out,
299            stmt,
300            0,
301            Some(MergeTarget {
302                map: &mut map,
303                source_id: stmt.nested_map_source_id,
304            }),
305        );
306        // The printer owns line structure (R7.3), so a statement's own text
307        // not ending in its own newline can't leave two statements sharing
308        // a generated line — review of #1308, finding 2: nothing required
309        // `Verbatim` text to be newline-terminated, and two that weren't
310        // would jam onto one line *and* silently lose the earlier
311        // statement's own checkpoint (`SourceMapBuilder::record`'s
312        // same-offset dedup, and `to_v3`'s one-checkpoint-per-line forward
313        // pass, both keep only the later one). Every real (non-`Verbatim`)
314        // renderer already ends its own output in `\n`, so this is a no-op
315        // for them; kept unconditional so `Verbatim`'s own guarantee stays
316        // exactly as it was.
317        if !out.ends_with('\n') {
318            out.push('\n');
319        }
320        // Readability policy (this module's own doc): one blank line
321        // between top-level declarations, except two consecutive
322        // `import`s, two consecutive `Comment`s, two consecutive
323        // `ReExportAll`s, a `Comment` immediately before a `ReExportAll`,
324        // or the next statement's own `no_blank_before` (#1486's own
325        // escape hatch — a real, already-shipped multi-statement sequence
326        // that genuinely needs no blank between two adjacent real
327        // statements, not expressible as a `TsStmtKind`-pair rule the way
328        // every case above is) — and never after `Verbatim` (P7.7's own
329        // boundary — `Verbatim` content's own spacing is not this
330        // printer's decision).
331        if let Some(next) = program.stmts.get(i + 1) {
332            // #1321: `workers.rs`'s own header has `import { ... } from
333            // "runtime"` (named) adjacent to `import * as handlers from
334            // "./handlers.js"` (namespace) adjacent to one `import * as
335            // {ns} from "..."` per referenced unit — all import-like, all
336            // real-content-adjacent with no blank line, matching the same
337            // "no blank line between two adjacent imports" rule
338            // `events_fanout.rs`'s own two named imports already exercised.
339            fn is_import_decl(kind: &TsStmtKind) -> bool {
340                matches!(
341                    kind,
342                    TsStmtKind::Decl(TsDecl::Import { .. })
343                        | TsStmtKind::Decl(TsDecl::ImportNamespace { .. })
344                        | TsStmtKind::Decl(TsDecl::ImportDefault { .. })
345                )
346            }
347            // #1329: `emit_commons_barrel`'s own real barrel module is one
348            // header `Comment` immediately followed by an `export *` line
349            // per constituent source file, every one of those lines
350            // adjacent to the next with no blank line anywhere — a
351            // genuinely different spacing shape from `ReExport`'s own
352            // (#1323's `workers_entry.rs` re-exports, each already
353            // separated from its neighbours by a blank line in the
354            // pre-conversion output, so `ReExport` itself still gets no
355            // grouping rule of its own). Scoped to exactly this new
356            // adjacency — a `Comment` before an ordinary `Import`/other
357            // decl still gets its blank line (see `events_fanout.rs`'s own
358            // header-comment-then-blank-then-import shape), unchanged.
359            fn is_reexport_all(kind: &TsStmtKind) -> bool {
360                matches!(kind, TsStmtKind::Decl(TsDecl::ReExportAll { .. }))
361            }
362            let both_imports = is_import_decl(&stmt.kind) && is_import_decl(&next.kind);
363            let both_comments = matches!(&stmt.kind, TsStmtKind::Comment(_))
364                && matches!(&next.kind, TsStmtKind::Comment(_));
365            let both_reexport_all = is_reexport_all(&stmt.kind) && is_reexport_all(&next.kind);
366            let comment_then_reexport_all =
367                matches!(&stmt.kind, TsStmtKind::Comment(_)) && is_reexport_all(&next.kind);
368            if !both_imports
369                && !both_comments
370                && !both_reexport_all
371                && !comment_then_reexport_all
372                && !next.no_blank_before
373                && !matches!(stmt.kind, TsStmtKind::Verbatim { .. })
374            {
375                out.push('\n');
376            }
377        }
378    }
379    let source_map = map.to_v3(&out, output_file);
380    Printed {
381        text: out,
382        source_map,
383    }
384}
385
386fn indent(depth: usize) -> String {
387    "  ".repeat(depth)
388}
389
390/// `emit_doc_block`'s own real rendering, byte-for-byte (#1333): `/**`
391/// opens; one ` * <line>` per non-blank source line, a literal `*/`
392/// escaped to `*\/` (issue #720 — otherwise it would close the comment
393/// early and let trailing text land as executable top-level TypeScript); a
394/// blank source line prints as a bare ` *`, no trailing space; `*/`
395/// closes. All at `depth`'s own 2-space-per-level indent.
396fn render_doc_comment(out: &mut String, text: &str, depth: usize) {
397    let ind = indent(depth);
398    out.push_str(&ind);
399    out.push_str("/**\n");
400    for line in text.lines() {
401        let trimmed = line.trim_end();
402        out.push_str(&ind);
403        if trimmed.is_empty() {
404            out.push_str(" *\n");
405        } else {
406            out.push_str(" * ");
407            out.push_str(&trimmed.replace("*/", "*\\/"));
408            out.push('\n');
409        }
410    }
411    out.push_str(&ind);
412    out.push_str(" */\n");
413}
414
415/// #1477's own carrier for a nested-checkpoint merge target: which map to
416/// merge a [`crate::program::TsStmt::nested_map`]-bearing statement's own
417/// checkpoints into, and which of that map's own registered sources they
418/// belong to. A bare `&mut SourceMapBuilder` isn't enough on its own
419/// (review of #1488, finding 1): hardcoding source `0` is correct for
420/// `print`'s own single-primary-source map and for every real caller today
421/// (`emit_free_fn`/`emit_class_method_and_merge_source_map` both already
422/// pass `0`), but silently wrong for a multi-source aggregate map — a test
423/// module's own map spans several `.bynk` files, and its real per-body
424/// merges already pass a non-zero id (`bynk-emit/src/project/tests_emit.rs:
425/// 537`/`:1936`) — exactly the shape #1475 found is this arc's own next
426/// conversion target. Threading `source_id` alongside `map` here, not
427/// hardcoded inside `SourceMapBuilder::merge`'s own caller here, keeps that
428/// conversion correct from the start rather than needing a second pass to
429/// discover the same gap.
430struct MergeTarget<'a> {
431    map: &'a mut SourceMapBuilder,
432    source_id: usize,
433}
434
435impl MergeTarget<'_> {
436    /// A fresh, independently-usable borrow of the same target — `Option<
437    /// MergeTarget>` isn't `Copy` (it holds a `&mut`), so a caller handing
438    /// the same target to more than one child (a `Vec<TsStmt>`'s own loop,
439    /// `TsDecl::Class`'s own constructor-then-methods sequence) reborrows
440    /// once per child via this, the same shape `Option::as_deref_mut` gives
441    /// a bare `&mut`.
442    fn reborrow(&mut self) -> MergeTarget<'_> {
443        MergeTarget {
444            map: self.map,
445            source_id: self.source_id,
446        }
447    }
448}
449
450/// Render one statement, including its own leading indent.
451///
452/// `map`: #1477's own thread-through, checked *here*, once, rather than
453/// re-checked at every one of this function's own many call sites (review
454/// of #1488, finding 2: the first version of this mechanism only checked a
455/// `Vec<TsStmt>` body's own *direct* children, inside `render_block_stmts`
456/// — silently dropping a `nested_map` set on a statement handed to `print`/
457/// `print_stmt_and_merge` directly, or on a statement nested one level
458/// deeper still, inside a `Block`/`Switch` case/`try`/`catch` body within a
459/// real function body). Checking unconditionally at this one entry point,
460/// before dispatching on `stmt.kind`, means every caller — however deep —
461/// gets the merge for free by simply forwarding `map` on, the same way
462/// `depth` already threads through unconditionally; a caller that has
463/// nothing to merge just passes `None`, as most still do.
464fn render_stmt(out: &mut String, stmt: &TsStmt, depth: usize, mut map: Option<MergeTarget<'_>>) {
465    if let Some(nested) = &stmt.nested_map
466        && let Some(target) = map.as_mut()
467    {
468        let base = out.len();
469        render_stmt_kind(out, stmt, depth, None);
470        target
471            .map
472            .merge(nested, &out[base..], out, base, target.source_id);
473        return;
474    }
475    render_stmt_kind(out, stmt, depth, map);
476}
477
478/// [`render_stmt`]'s own per-`kind` dispatch, split out so the nested-
479/// checkpoint check above runs exactly once per statement regardless of
480/// which arm below ends up recursing back into `render_stmt` for a child.
481fn render_stmt_kind(
482    out: &mut String,
483    stmt: &TsStmt,
484    depth: usize,
485    mut map: Option<MergeTarget<'_>>,
486) {
487    match &stmt.kind {
488        TsStmtKind::Verbatim { text, .. } => {
489            out.push_str(text);
490        }
491        TsStmtKind::Decl(decl) => render_decl(out, decl, depth, map),
492        TsStmtKind::Const { name, ty, init } => {
493            out.push_str(&indent(depth));
494            out.push_str("const ");
495            render_binding_name(out, name);
496            if let Some(ty) = ty {
497                out.push_str(": ");
498                render_type(out, ty);
499            }
500            out.push_str(" = ");
501            render_stmt_level_expr(out, init, depth);
502            out.push_str(";\n");
503        }
504        TsStmtKind::Let { name, ty, init } => {
505            out.push_str(&indent(depth));
506            out.push_str("let ");
507            render_binding_name(out, name);
508            if let Some(ty) = ty {
509                out.push_str(": ");
510                render_type(out, ty);
511            }
512            if let Some(init) = init {
513                out.push_str(" = ");
514                render_stmt_level_expr(out, init, depth);
515            }
516            out.push_str(";\n");
517        }
518        TsStmtKind::ExprStmt(expr) => {
519            out.push_str(&indent(depth));
520            render_expr(out, expr);
521            out.push_str(";\n");
522        }
523        TsStmtKind::Return(expr) => {
524            out.push_str(&indent(depth));
525            out.push_str("return");
526            if let Some(expr) = expr {
527                out.push(' ');
528                // #1321: `return { ... }` needs the same depth-aware
529                // multiline-object handling `Const`/`Let`/`Assign` already
530                // get — `workers.rs`'s own `emit_worker_compose` returns
531                // its whole compose surface this way (one shorthand async
532                // `Method` entry per wrapper, one per line), a shape
533                // `render_expr`'s plain recursion can't render correctly
534                // (see `TsExpr::Object`'s own doc).
535                render_stmt_level_expr(out, expr, depth);
536            }
537            out.push_str(";\n");
538        }
539        TsStmtKind::Throw(expr) => {
540            out.push_str(&indent(depth));
541            out.push_str("throw ");
542            render_stmt_level_expr(out, expr, depth);
543            out.push_str(";\n");
544        }
545        TsStmtKind::If {
546            cond,
547            then_branch,
548            else_branch,
549            same_line_else,
550        } => {
551            out.push_str(&indent(depth));
552            out.push_str("if (");
553            render_expr(out, cond);
554            out.push(')');
555            // `} else {` on one line — #1325's own real gap, all three of
556            // `emit_test_main`'s own real `if`/`else` sites (one `Block`,
557            // one `InlineBlock`). Only reachable when `then_branch` is
558            // itself braced (`Block` or `InlineBlock` — every real
559            // `same_line_else` site is one or the other); a brace-free
560            // `then_branch` has no closing `}` for `else` to sit against,
561            // so this falls back to the ordinary fresh-line rendering
562            // rather than producing `<inline-stmt> else {`, which nothing
563            // real needs. Review of #1326, finding 2: the two-variant
564            // check needs no `Option<()>`/re-match/`unreachable!()` — a
565            // plain `matches!` guard plus one `if let ... else` dispatch
566            // says the same thing with no wildcard arm.
567            let then_is_braced = matches!(
568                &then_branch.kind,
569                TsStmtKind::Block(_) | TsStmtKind::InlineBlock(_)
570            );
571            if *same_line_else
572                && then_is_braced
573                && let Some(else_branch) = else_branch
574            {
575                // `render_inline_block`'s own `{ stmts }` shape, minus its
576                // own trailing `\n` and leading space (added here instead,
577                // matching `render_branch`'s own InlineBlock arm) so
578                // `else` continues on the same physical line;
579                // `render_block_body`'s own `{ stmts }` (no trailing
580                // newline, the exact shape `TryCatch`'s own `} catch (e) {`
581                // continuation already reuses) for the `Block` case,
582                // guaranteed by `then_is_braced` above.
583                if let TsStmtKind::InlineBlock(stmts) = &then_branch.kind {
584                    out.push_str(" { ");
585                    render_compact_stmts(out, stmts);
586                    out.push_str(" }");
587                } else {
588                    render_block_body(out, then_branch, depth, None);
589                }
590                out.push_str(" else");
591                render_branch(out, else_branch, depth);
592            } else {
593                render_branch(out, then_branch, depth);
594                if let Some(else_branch) = else_branch {
595                    out.push_str(&indent(depth));
596                    out.push_str("else");
597                    render_branch(out, else_branch, depth);
598                }
599            }
600        }
601        TsStmtKind::ForOf {
602            binding,
603            iter,
604            body,
605        } => {
606            out.push_str(&indent(depth));
607            out.push_str("for (const ");
608            out.push_str(binding);
609            out.push_str(" of ");
610            render_expr(out, iter);
611            out.push(')');
612            render_branch(out, body, depth);
613        }
614        TsStmtKind::For {
615            name,
616            init,
617            test,
618            body,
619        } => {
620            out.push_str(&indent(depth));
621            out.push_str("for (let ");
622            out.push_str(name);
623            out.push_str(" = ");
624            render_expr(out, init);
625            out.push_str("; ");
626            render_expr(out, test);
627            out.push_str("; ");
628            out.push_str(name);
629            out.push_str("++)");
630            render_branch(out, body, depth);
631        }
632        TsStmtKind::TryCatch {
633            try_block,
634            catch_param,
635            catch_block,
636        } => {
637            out.push_str(&indent(depth));
638            out.push_str("try");
639            render_block_body(out, try_block, depth, map.as_mut().map(|t| t.reborrow()));
640            out.push_str(" catch");
641            if let Some(p) = catch_param {
642                out.push_str(" (");
643                out.push_str(p);
644                out.push(')');
645            }
646            render_block_body(out, catch_block, depth, map.as_mut().map(|t| t.reborrow()));
647            out.push('\n');
648        }
649        TsStmtKind::Block(stmts) => {
650            out.push_str(&indent(depth));
651            out.push_str("{\n");
652            for s in stmts {
653                render_stmt(out, s, depth + 1, map.as_mut().map(|t| t.reborrow()));
654            }
655            out.push_str(&indent(depth));
656            out.push_str("}\n");
657        }
658        TsStmtKind::Continue => {
659            out.push_str(&indent(depth));
660            out.push_str("continue;\n");
661        }
662        TsStmtKind::Assign { target, value } => {
663            out.push_str(&indent(depth));
664            render_expr(out, target);
665            out.push_str(" = ");
666            render_stmt_level_expr(out, value, depth);
667            out.push_str(";\n");
668        }
669        TsStmtKind::Comment(text) => {
670            for line in text.split('\n') {
671                out.push_str(&indent(depth));
672                out.push_str("// ");
673                out.push_str(line);
674                out.push('\n');
675            }
676        }
677        TsStmtKind::DocComment(text) => render_doc_comment(out, text, depth),
678        TsStmtKind::Blank => out.push('\n'),
679        TsStmtKind::Switch {
680            discriminant,
681            cases,
682        } => {
683            out.push_str(&indent(depth));
684            out.push_str("switch (");
685            render_expr(out, discriminant);
686            out.push_str(") {\n");
687            for case in cases {
688                debug_assert!(
689                    !(case.test.is_some() && case.default_braced),
690                    "TsSwitchCase.default_braced only means something on the default (test: None) case — review of #1402's own nit"
691                );
692                // Review of #1446: the mirror invariant for `case_braced`
693                // (Arc E slice 6, #1445) — it only means something on a
694                // non-`default` (`test: Some(..)`) case; every real
695                // `test: None` call site sets it `true` with no effect
696                // (`default_braced` owns that arm's bracing instead), which
697                // would silently swallow a future caller's `false` there.
698                debug_assert!(
699                    case.test.is_some() || case.case_braced,
700                    "TsSwitchCase.case_braced only means something on a non-default (test: Some(..)) case"
701                );
702                out.push_str(&indent(depth + 1));
703                match &case.test {
704                    Some(test) => {
705                        out.push_str("case ");
706                        render_expr(out, test);
707                        // Arc E slice 6 (#1445): `case_braced` is the
708                        // non-`default` mirror of `default_braced` just
709                        // below — `emit_sum_codec`'s own payload-free
710                        // variant case (`case "Pending":\n  return { kind:
711                        // "Pending" };`) is real, unbraced content sitting
712                        // right beside a braced payload-carrying sibling in
713                        // the same switch (`212_json_codec`'s own `Status`
714                        // fixture).
715                        if case.case_braced {
716                            out.push_str(": {\n");
717                            for s in &case.body {
718                                render_stmt(out, s, depth + 2, map.as_mut().map(|t| t.reborrow()));
719                            }
720                            out.push_str(&indent(depth + 1));
721                            out.push_str("}\n");
722                        } else {
723                            out.push_str(":\n");
724                            for s in &case.body {
725                                render_stmt(out, s, depth + 2, map.as_mut().map(|t| t.reborrow()));
726                            }
727                        }
728                    }
729                    None => {
730                        if case.default_braced {
731                            out.push_str("default: {\n");
732                            for s in &case.body {
733                                render_stmt(out, s, depth + 2, map.as_mut().map(|t| t.reborrow()));
734                            }
735                            out.push_str(&indent(depth + 1));
736                            out.push_str("}\n");
737                        } else {
738                            out.push_str("default:\n");
739                            for s in &case.body {
740                                render_stmt(out, s, depth + 2, map.as_mut().map(|t| t.reborrow()));
741                            }
742                        }
743                    }
744                }
745            }
746            out.push_str(&indent(depth));
747            out.push_str("}\n");
748        }
749        TsStmtKind::InlineBlock(stmts) => {
750            out.push_str(&indent(depth));
751            render_inline_block(out, stmts);
752        }
753        TsStmtKind::Increment(expr) => {
754            out.push_str(&indent(depth));
755            render_expr(out, expr);
756            out.push_str("++;\n");
757        }
758        // Same rendering as `Verbatim` above — the text is printed exactly
759        // as given, no `indent(depth)` prefix, no added punctuation (see
760        // `TsStmtKind::Raw`'s own doc for why this is a distinct kind, not
761        // a reuse of `Verbatim`).
762        TsStmtKind::Raw(text) => out.push_str(text),
763    }
764}
765
766/// `if`/`for...of`'s own body: braces (and a nested block) when `branch` is
767/// itself a `Block`, otherwise printed inline on the same line — matching
768/// `events_fanout.rs`'s own `if (!Array.isArray(subs)) continue;` (no
769/// braces) alongside its always-braced `for...of` bodies.
770fn render_branch(out: &mut String, branch: &TsStmt, depth: usize) {
771    match &branch.kind {
772        TsStmtKind::Block(_) => {
773            render_block_body(out, branch, depth, None);
774            out.push('\n');
775        }
776        TsStmtKind::InlineBlock(stmts) => {
777            out.push(' ');
778            render_inline_block(out, stmts);
779        }
780        _ => {
781            out.push(' ');
782            render_inline_stmt(out, branch);
783        }
784    }
785}
786
787/// `{ stmt; stmt; ... }` on one generated line — [`TsStmtKind::InlineBlock`]'s
788/// own renderer, shared by [`render_branch`] (an `if`/`else` branch) and
789/// [`render_stmt`]'s own top-level/nested-statement-list arm (not reachable
790/// from any real content at that position today, but every `TsStmtKind`
791/// variant still needs a real rendering, not a wildcard). Each statement
792/// renders through [`render_inline_stmt`] (reusing its own exhaustive
793/// per-kind handling) with its own trailing newline trimmed, then a single
794/// space — matching the real, hand-written `{ a; b; c; }` shape exactly.
795fn render_inline_block(out: &mut String, stmts: &[TsStmt]) {
796    out.push_str("{ ");
797    render_compact_stmts(out, stmts);
798    out.push_str(" }\n");
799}
800
801/// `stmt1; stmt2; ...` — every statement in `stmts` rendered on the current
802/// line, semicolon-and-space-separated, no leading/trailing space or
803/// surrounding braces of its own. The shared core both real compact shapes
804/// need: [`TsStmtKind::InlineBlock`]'s own single-line-*braced* form
805/// (`render_inline_block`, braces on the same line as the content) and
806/// [`render_block_body`]'s own special case for a `TryCatch` block whose
807/// body is an `InlineBlock` (braces on their own lines, per that shape's
808/// usual convention, but the body itself still one compact line) —
809/// `workers_entry.rs`'s own two real shapes, distinguished by whether the
810/// pre-conversion `writeln!` code put the opening brace on the same
811/// physical line as the content or not.
812fn render_compact_stmts(out: &mut String, stmts: &[TsStmt]) {
813    for (i, s) in stmts.iter().enumerate() {
814        if i > 0 {
815            out.push(' ');
816        }
817        let mut piece = String::new();
818        render_inline_stmt(&mut piece, s);
819        // Review of #1324, finding 2: only `trim_end_matches('\n')`'s own
820        // trailing newline is stripped — an *embedded* newline (a genuinely
821        // multi-line statement, e.g. `Block`/`Switch`/`TryCatch`, rendered
822        // via `render_inline_stmt`'s own `render_stmt(out, stmt, 0)`
823        // fallback) would break the one-line shape this function promises.
824        // Not reachable today — `workers_entry.rs`'s own three real
825        // `InlineBlock` sites are `ExprStmt`/`Continue` only — but worth a
826        // loud check rather than a silent multi-line break if a future
827        // slice's `InlineBlock` ever holds one.
828        debug_assert!(
829            !piece.trim_end_matches('\n').contains('\n'),
830            "render_compact_stmts: a genuinely multi-line statement can't render on one line"
831        );
832        out.push_str(piece.trim_end_matches('\n'));
833    }
834}
835
836/// A statement rendered without its own leading indent, for the "no braces"
837/// half of [`render_branch`] — only the shapes `events_fanout.rs` actually
838/// needs inline (`continue`, `return`, a bare expression) get dedicated
839/// handling; anything else falls back to a normal indented render (depth 0)
840/// rather than panicking, so an unanticipated future shape still prints
841/// something plausible instead of crashing.
842fn render_inline_stmt(out: &mut String, stmt: &TsStmt) {
843    match &stmt.kind {
844        TsStmtKind::Continue => out.push_str("continue;\n"),
845        TsStmtKind::Return(expr) => {
846            out.push_str("return");
847            if let Some(expr) = expr {
848                out.push(' ');
849                render_expr(out, expr);
850            }
851            out.push_str(";\n");
852        }
853        TsStmtKind::ExprStmt(expr) => {
854            render_expr(out, expr);
855            out.push_str(";\n");
856        }
857        // Review of #1317/#1318, finding 2: every other variant in the
858        // fallback group below is safe to render via `render_stmt`'s own
859        // top-level `//`-line-comment form — `Comment` is not. A `//`
860        // comment run through the generic fallback (`if (cond) // text`)
861        // comments out the rest of the physical line, leaving the `if`
862        // with no body at all — a TypeScript parse error, not merely an
863        // unlikely shape. Not reachable today (`events_fanout.rs` never
864        // puts a bare `Comment` in a brace-free `if`/`for...of` body), but
865        // the fallback group exists precisely so a future slice trips over
866        // a missing case at compile time, not at parse time in emitted
867        // output — so `Comment` needs its own real inline shape now, a
868        // block comment (`/* text */`), which cannot swallow anything
869        // after it. Embedded newlines flatten to spaces, keeping this
870        // strictly one generated line like every other inline shape here.
871        // Review of #1324, finding 2: `Blank`'s own top-level rendering is a
872        // bare `'\n'` (see `render_stmt`), which the generic fallback below
873        // would inherit unchanged — reaching `render_branch`'s brace-free
874        // arm (`if (cond) <inline>`), that prints `if (cond) \n`, and since
875        // JS/TS statement grammar doesn't care about the newline, the very
876        // next statement in the enclosing block silently becomes the `if`'s
877        // own body. Exactly the same swallowing-bug class the #1317/#1318
878        // review already fixed for `Comment` just above — an honest empty
879        // statement (`;`) is what a "no code here" body actually means in an
880        // inline TS position, and cannot swallow anything after it.
881        TsStmtKind::Blank => out.push_str(";\n"),
882        TsStmtKind::Comment(text) => {
883            out.push_str("/* ");
884            out.push_str(&text.replace('\n', " "));
885            out.push_str(" */\n");
886        }
887        // No inline shape of its own — `events_fanout.rs` never puts one of
888        // these in a brace-free `if`/`for...of` body — but listed by name,
889        // not a wildcard (review of #1314, finding 3): a future Arc C
890        // slice's new `TsStmtKind` variant must fail to compile here rather
891        // than silently inherit this fallback, the same exhaustiveness
892        // discipline `VerbatimOrigin`'s own doc states for `bynk-emit`.
893        TsStmtKind::Verbatim { .. }
894        | TsStmtKind::Decl(_)
895        | TsStmtKind::Const { .. }
896        | TsStmtKind::Let { .. }
897        | TsStmtKind::If { .. }
898        | TsStmtKind::ForOf { .. }
899        // Arc E slice 7 (#1447): not reachable today — `ListInst`/
900        // `MapInst`'s two real `For` sites both use a braced `Block` body,
901        // which `render_branch` dispatches to its own multi-line arm before
902        // this fallback group is ever consulted — but safe for the same
903        // reason `ForOf` already is: a brace-free `For` body renders through
904        // `render_stmt`'s own indented, newline-terminated form, which is
905        // exactly what this fallback provides.
906        | TsStmtKind::For { .. }
907        | TsStmtKind::TryCatch { .. }
908        | TsStmtKind::Block(_)
909        | TsStmtKind::Assign { .. }
910        | TsStmtKind::Switch { .. }
911        | TsStmtKind::InlineBlock(_)
912        // #1325: `passed++;`/`failed++;` inside `emit_test_main`'s own
913        // compact `{ passed++; console.log(...); }` shape — always complete,
914        // single-line, semicolon-terminated content (never a bare newline
915        // the way `Blank`'s own top-level form is), so the same fallback
916        // that's safe for `Const`/`If`/etc. above is safe here too.
917        | TsStmtKind::Increment(_)
918        // #1353: `emit_contract_guarded_body`'s own precondition/
919        // postcondition guards are exactly this fallback's own target case —
920        // a real, reachable `InlineBlock` site (`if (!(pred)) { const __e =
921        // ...; __e.name = ...; throw __e; }`), not a defensive placeholder.
922        // Safe for the same reason as `Const`/`Assign` above, with the same
923        // caveat those two already carry: `render_stmt_level_expr` renders a
924        // `multiline: true` operand with embedded newlines, which would
925        // break this fallback's one-line contract — untested today only
926        // because the one real call site (`throw __e;`, a bare `Ident`)
927        // never reaches that case, not because `Throw` structurally cannot.
928        | TsStmtKind::Throw(_)
929        // #1333: not reachable today — every real `DocComment` reaches the
930        // printer only via `print_stmt`, never through a `TsProgram`'s own
931        // tree — which is what actually makes this fallback safe: unlike
932        // `Comment`'s own `//` form, a `/** ... */` block always closes
933        // itself before any subsequent text, so it cannot swallow the next
934        // statement the way an unterminated `//` line comment could — but
935        // the fallback's own `render_stmt(out, stmt, 0)` still emits the
936        // block's embedded newlines, which would break an `InlineBlock`'s
937        // single-line contract if this arm ever became reachable through
938        // one. Listed by name per this group's own exhaustiveness
939        // discipline, not folded into a wildcard.
940        | TsStmtKind::DocComment(_)
941        // #1337: not reachable today — every real `Raw`-bearing body
942        // (`TsObjectEntry::Method`, and since #1352/#1369 also
943        // `TsDecl::Function`) renders through `render_block_stmts`, never
944        // through `render_inline_stmt`'s own call path (an `if`/`for...of`
945        // branch or an `InlineBlock`). Unsafe if it ever became reachable
946        // there for the same reason `DocComment` is: `Raw`'s own text is
947        // typically a whole multi-statement function body, never
948        // single-line, so the fallback's embedded newlines would break an
949        // `InlineBlock`'s single-line contract. Since #1369 added
950        // `TsDecl::Function.inline`, this is now one field flip away from
951        // live rather than purely hypothetical — `render_compact_stmts`'s
952        // own `debug_assert!` is the actual backstop if a future call site
953        // ever combines `inline: true` with a `Raw` body statement;
954        // `emit_agent`'s own rehydrate function (#1369) deliberately keeps
955        // `inline: false` specifically to avoid exercising this arm.
956        // Listed by name, not folded into a wildcard, for the same reason
957        // as every other arm in this group.
958        | TsStmtKind::Raw(_) => render_stmt(out, stmt, 0, None),
959    }
960}
961
962/// `{ <stmts> }`, with the closing brace at `depth`'s own indent and no
963/// trailing newline — the shared shape [`TsStmtKind::TryCatch`] needs to
964/// keep writing on the same generated line (`} catch (e) {`) and
965/// [`render_branch`]'s own `Block` case needs with a `\n` appended after.
966/// `block` is expected to be a [`TsStmtKind::Block`]; anything else is
967/// still rendered sensibly (as a single-statement body) rather than
968/// panicking.
969///
970/// `map`: forwarded into `stmts` when `block` is a real `Block` (#1477) —
971/// `TryCatch`'s own `try`/`catch` bodies are the one real site this covers
972/// (a `nested_map`-bearing statement inside either); `render_branch`'s own
973/// `If`/`ForOf`/`For` call sites always pass `None`, since nothing threads
974/// a map into `render_branch` itself (out of scope — ADR 0391's own
975/// permanently-opaque content is always a whole function/method/constructor
976/// body, never embedded directly inside an `if`/loop branch).
977fn render_block_body(
978    out: &mut String,
979    block: &TsStmt,
980    depth: usize,
981    mut map: Option<MergeTarget<'_>>,
982) {
983    out.push_str(" {\n");
984    if let TsStmtKind::Block(stmts) = &block.kind {
985        for s in stmts {
986            render_stmt(out, s, depth + 1, map.as_mut().map(|t| t.reborrow()));
987        }
988    } else if let TsStmtKind::InlineBlock(stmts) = &block.kind {
989        // The braces sit on their own lines here (this function's own usual
990        // shape, e.g. `TryCatch`'s `catch (e) { ... }`), but the body itself
991        // is one compact line — `workers_entry.rs`'s own real queue-consumer
992        // catch clause (`console.error(...); msg.retry();`).
993        out.push_str(&indent(depth + 1));
994        render_compact_stmts(out, stmts);
995        out.push('\n');
996    } else {
997        render_stmt(out, block, depth + 1, None);
998    }
999    out.push_str(&indent(depth));
1000    out.push('}');
1001}
1002
1003fn render_binding_name(out: &mut String, name: &TsBindingName) {
1004    match name {
1005        TsBindingName::Ident(s) => out.push_str(s),
1006        TsBindingName::ObjectPattern(names) => {
1007            out.push_str("{ ");
1008            for (i, n) in names.iter().enumerate() {
1009                if i > 0 {
1010                    out.push_str(", ");
1011                }
1012                out.push_str(n);
1013            }
1014            out.push_str(" }");
1015        }
1016    }
1017}
1018
1019/// Whether `expr`, printed in a position member access, indexing, a call
1020/// callee, `await`, or unary `!`/`typeof` binds tighter than, needs its own
1021/// parens to preserve the built tree's meaning — `Binary`/`As`/`Arrow`/
1022/// `Conditional` all bind looser than any of those (review of #1314, finding
1023/// 2: `!a ?? b`/`a ?? b.c`/`await a ?? b` all silently changed meaning
1024/// without this; review of #1322, finding 1: `Arrow` was missing from this
1025/// same rule — `(x) => x(1)` is a call whose *body* is `x(1)`, not the IIFE
1026/// `((x) => x)(1)` a `Call { callee: Arrow, .. }` node means; #1323 adds
1027/// `Conditional` proactively, the same class of gap, before any real content
1028/// or review round hits it). Deliberately conservative for *these* contexts
1029/// specifically: a nested `Binary`/`Arrow`/`Conditional` is always
1030/// parenthesized here regardless of which operator/shape it is — correct in
1031/// every case that class of bug can hit (none of these
1032/// six positions is ever real, grounded content in this file today), at
1033/// the cost of an occasional pair a real precedence table could omit.
1034///
1035/// **Not** used for `Binary`'s own left/right operands — see
1036/// [`render_binary_operand`]/[`binary_precedence`] for why that context
1037/// needed to become precedence-aware once #1321 added more than one binary
1038/// operator (this function's own always-parenthesize rule would print
1039/// `(__authz === null) || …` for `workers.rs`'s own real
1040/// `__authz === null || !__authz.startsWith(...)`, which the byte-golden
1041/// fixtures don't have parens around).
1042fn needs_parens_as_operand(expr: &TsExpr) -> bool {
1043    matches!(
1044        expr,
1045        TsExpr::Binary { .. }
1046            | TsExpr::As { .. }
1047            | TsExpr::Arrow { .. }
1048            | TsExpr::Conditional { .. }
1049    )
1050}
1051
1052fn render_operand(out: &mut String, expr: &TsExpr) {
1053    if needs_parens_as_operand(expr) {
1054        out.push('(');
1055        render_expr(out, expr);
1056        out.push(')');
1057    } else {
1058        render_expr(out, expr);
1059    }
1060}
1061
1062/// Standard JS/TS relative precedence for the operators [`TsBinaryOp`]
1063/// currently has — only used to decide a nested `Binary` operand's own
1064/// parenthesisation (see [`render_binary_operand`]); higher binds tighter.
1065fn binary_precedence(op: TsBinaryOp) -> u8 {
1066    match op {
1067        TsBinaryOp::NullishCoalescing => 1,
1068        TsBinaryOp::Or => 2,
1069        TsBinaryOp::And => 3,
1070        TsBinaryOp::StrictEq | TsBinaryOp::StrictNotEq => 4,
1071        TsBinaryOp::GreaterThan
1072        | TsBinaryOp::LessThan
1073        | TsBinaryOp::InstanceOf
1074        | TsBinaryOp::In
1075        | TsBinaryOp::GreaterThanEq
1076        | TsBinaryOp::LessThanEq => 5,
1077        TsBinaryOp::Add => 6,
1078    }
1079}
1080
1081/// Render `expr` as one side of a `Binary { op: outer_op, .. }` — #1321's
1082/// own real gap (`workers.rs`'s own `__authz === null ||
1083/// !__authz.startsWith(...)` / `__authz !== null &&
1084/// __authz.startsWith(...)`): with only `??` in the algebra,
1085/// [`render_operand`]'s "always parenthesize a nested `Binary`" rule never
1086/// mismatched real content; a comparison (`===`/`!==`) nested inside `||`/
1087/// `&&` needs *no* parens to preserve meaning (`===` binds tighter), and
1088/// the byte-golden fixtures have none — so this context needed to become
1089/// precedence-aware rather than reusing [`render_operand`] unchanged.
1090/// `??` mixed with `||`/`&&` is a real TS syntax error without explicit
1091/// parens (not just a precedence question), so that combination always
1092/// parenthesizes regardless of the numeric table above; nothing in
1093/// `bynk-emit` builds that combination today, but a future caller that did
1094/// still gets correct, parseable output rather than a silent `tsc` error.
1095///
1096/// Equal precedence still parenthesizes for two *different* operators
1097/// (`<=`, not `<`) — deliberately preserving [`render_operand`]'s own
1098/// pre-#1321 "always parenthesize a same/lower-precedence nested chain,
1099/// even where associativity would read the same without it" conservatism
1100/// for that case; only a *strictly higher*-precedence nested operator — the
1101/// one real, grounded case #1321 needs — omits parens there. #1323 adds one
1102/// more real case: the exact *same* operator nested in itself omits parens
1103/// too, for `||`/`&&` specifically (both associative, and real content —
1104/// `emit_call_handler_dispatch`'s 3-term `||` chain — needs the flat,
1105/// unparenthesized reading) — `??` keeps the pre-#1321 conservatism
1106/// unchanged (pinned by `parenthesises_a_nested_binary_operand_of_another_
1107/// binary`'s own test, still `a ?? (b ?? c)`), since nothing in real
1108/// content needs a flattened `??` chain and this slice's own grounding
1109/// gives no reason to widen that boundary opportunistically.
1110fn render_binary_operand(out: &mut String, outer_op: TsBinaryOp, expr: &TsExpr, is_left: bool) {
1111    let needs_parens = match expr {
1112        // `Arrow`/`Conditional` are the two lowest-precedence expression
1113        // forms in JS/TS — `(x) => y || z` as a binary operand must print
1114        // `(x) => (y || z)`, never the bare `(x) => y || z` (which reparses
1115        // as the arrow's own *body* being `y || z`, not the arrow itself
1116        // being one operand of `||`); a `Conditional` operand needs the
1117        // identical protection for the identical reason (review of #1322,
1118        // finding 1, for `Arrow`; #1323 adds `Conditional` proactively, the
1119        // same class of gap).
1120        TsExpr::As { .. } | TsExpr::Arrow { .. } | TsExpr::Conditional { .. } => true,
1121        TsExpr::Binary { op: inner_op, .. } => {
1122            let mixes_nullish = (outer_op == TsBinaryOp::NullishCoalescing)
1123                != (*inner_op == TsBinaryOp::NullishCoalescing);
1124            if mixes_nullish {
1125                true
1126            } else if *inner_op == outer_op {
1127                // Same operator chained with itself: only `||`/`&&` are
1128                // associative, so only they may print flat with no parens —
1129                // #1323's own `typeof args !== "object" || args === null ||
1130                // Array.isArray(args)` (`emit_call_handler_dispatch`), a
1131                // genuine 3-term chain the pre-#1323 "always parenthesize
1132                // equal precedence" rule would have wrongly printed as
1133                // `(typeof args !== "object" || args === null) ||
1134                // Array.isArray(args)`. Every other operator — including
1135                // `??` (kept at its pre-#1321 conservative choice; nothing in
1136                // real content needs a flattened `??` chain, and
1137                // `parenthesises_a_nested_binary_operand_of_another_binary`
1138                // pins that `a ?? (b ?? c)` still gets its parens) and, as of
1139                // this fix, the equality/relational operators (`===`/`!==`/
1140                // `>`) — is NOT associative: `a === b === c` parses as
1141                // `(a === b) === c`, not the tree's real `a === (b === c)`,
1142                // so a same-operator nesting of any of those still needs its
1143                // parens regardless of which side it's nested on (review of
1144                // #1324, finding 1 — the original fix wrongly generalized
1145                // the `||`/`&&` exemption to every operator). `+` (Arc C,
1146                // step (11), #1388) does NOT join that exemption on both
1147                // sides — review of #1389, finding 1: `||`/`&&` are
1148                // SEMANTICALLY associative (`(a || b) || c` and
1149                // `a || (b || c)` always agree), but `+` is only
1150                // GRAMMATICALLY left-associative — with a mixed number/string
1151                // chain, `1 + (2 + "3")` (`"123"`) and `(1 + 2) + "3"`
1152                // (`"33"`) disagree, so flattening a RIGHT-nested `Add` would
1153                // silently change the value. `join_plus`
1154                // (`bynk-emit/src/emitter/emit.rs`) only ever left-folds, so
1155                // this asymmetry never showed up in that caller's own
1156                // zero-diff fixture run — but `bynk-ts` is a shared
1157                // primitive, not scoped to that one caller's own usage.
1158                match outer_op {
1159                    TsBinaryOp::Or | TsBinaryOp::And => false,
1160                    TsBinaryOp::Add => !is_left,
1161                    _ => true,
1162                }
1163            } else {
1164                binary_precedence(*inner_op) <= binary_precedence(outer_op)
1165            }
1166        }
1167        _ => false,
1168    };
1169    if needs_parens {
1170        out.push('(');
1171        render_expr(out, expr);
1172        out.push(')');
1173    } else {
1174        render_expr(out, expr);
1175    }
1176}
1177
1178/// Render `expr` as a statement or declaration's own top-level expression
1179/// (a `const`/`let` initialiser, an assignment's own value, …) — one of the
1180/// two places `depth` is available to render a `multiline: true`
1181/// [`TsExpr::Object`]/[`TsExpr::Array`] correctly (see `Object`'s own doc);
1182/// the other is [`render_multiline_object_entry`]'s own `Prop` arm (#1355 —
1183/// a `Prop`'s own value can itself be a nested multiline object/array,
1184/// `emit_messages_bundle`'s own real doubly-nested table). Every other
1185/// shape defers to the ordinary, depth-unaware [`render_expr`] unchanged;
1186/// this exists only to intercept the one shape that needs `depth` before it
1187/// gets there.
1188fn render_stmt_level_expr(out: &mut String, expr: &TsExpr, depth: usize) {
1189    if let TsExpr::Object {
1190        entries,
1191        multiline: true,
1192    } = expr
1193    {
1194        render_multiline_object(out, entries, depth);
1195    } else if let TsExpr::Array {
1196        items,
1197        multiline: true,
1198    } = expr
1199    {
1200        render_multiline_array(out, items, depth);
1201    } else {
1202        render_expr(out, expr);
1203    }
1204}
1205
1206/// `{ <newline> ("  "*depth+1)<key>: <value>,<newline> ... ("  "*depth)}` —
1207/// one entry per line, each with its own trailing comma (including the
1208/// last), closing brace back at `depth`'s own indent. `events_fanout.rs`'s
1209/// own `__eventRoutes` table is the real, grounded shape this exists for
1210/// (#1317) — TypeScript's ordinary multi-line object-literal convention.
1211/// #1321 (`workers.rs`): entries may now be [`TsObjectEntry`], not just
1212/// `Prop` — the `compose`-returned surface object is one shorthand async
1213/// `Method` entry per wrapper, one per line.
1214fn render_multiline_object(out: &mut String, entries: &[TsObjectEntry], depth: usize) {
1215    // Deliberately no empty-entries shortcut (review of #1317/#1318, finding
1216    // 1): `events_fanout.rs`'s own `__eventRoutes` table is reachable with
1217    // zero entries (a context can `ctx_uses_emit` while publishing only
1218    // events nobody subscribes to — `own_event_routes` filters down to
1219    // exactly that empty case, `bynk-emit/src/project.rs`'s own
1220    // `own_event_routes` computation), and the pre-conversion `writeln!`
1221    // code always wrote the open-brace line and the closing `};` line
1222    // unconditionally, regardless of how many times its own `for` loop
1223    // iterated — an empty table printed `{\n};`, not the tight `{}` a
1224    // single-line object's own empty case uses. Matching that byte-for-byte
1225    // means never taking the single-line shortcut here, not even for zero
1226    // entries.
1227    out.push_str("{\n");
1228    for entry in entries {
1229        render_multiline_object_entry(out, entry, depth, None);
1230    }
1231    out.push_str(&indent(depth));
1232    out.push('}');
1233}
1234
1235/// One [`TsObjectEntry`], as [`render_multiline_object`]'s own per-entry
1236/// loop body — factored out so [`print_object_entry`] can render exactly
1237/// one entry without a whole object's own opening/closing braces, sharing
1238/// this one real per-kind dispatch rather than a second copy (the same
1239/// "one document-fragment entry point, no duplicated rendering" posture
1240/// [`print_stmt`]/[`print_type`] already established).
1241/// `map`: see [`render_block_stmts`]'s own doc (#1477) — reaches a `Method`
1242/// entry's own body unchanged; every other entry kind ignores it (no body
1243/// of its own).
1244fn render_multiline_object_entry(
1245    out: &mut String,
1246    entry: &TsObjectEntry,
1247    depth: usize,
1248    map: Option<MergeTarget<'_>>,
1249) {
1250    // Review of #1340, finding 1: a `Raw`-bodied `Method` entry's own text
1251    // is captured pre-indented at a fixed absolute depth by its caller (see
1252    // `print_object_entry`'s own doc) — correct only when `depth` is `0`.
1253    // Originally guarded only in `print_object_entry`, the one entry point
1254    // #1337 had in mind; #1339's `emit_refined_type` reaches this function
1255    // directly (via `TsExpr::multiline_object_entries`/`render_expr`, never
1256    // through `print_object_entry`), which the original guard's placement
1257    // silently missed. Asserted here instead, where both callers converge,
1258    // so neither path can bypass it again.
1259    if let TsObjectEntry::Method { body, .. } = entry {
1260        debug_assert!(
1261            depth == 0 || !contains_raw(body),
1262            "render_multiline_object_entry: a Raw-bodied Method entry's own baked-in indent only matches depth 0"
1263        );
1264    }
1265    match entry {
1266        TsObjectEntry::Prop(k, v) => {
1267            out.push_str(&indent(depth + 1));
1268            out.push_str(k);
1269            out.push_str(": ");
1270            // #1355: a `Prop`'s own value sits one level deeper than the
1271            // entry itself (matching this function's own `indent(depth +
1272            // 1)` above) — the same depth `render_stmt_level_expr`'s own
1273            // doc names as "the only place `depth` is available to render a
1274            // `multiline: true` `TsExpr::Object`/`Array` correctly."
1275            // `emit_messages_bundle`'s own real, doubly-nested `{ locale: {
1276            // code: expr, ... }, ... }` table is this gap's real grounding
1277            // — a nested multiline object value previously fell back to
1278            // single-line silently (the plain, depth-unaware `render_expr`
1279            // recursion ignores `multiline` entirely).
1280            render_stmt_level_expr(out, v, depth + 1);
1281            out.push_str(",\n");
1282        }
1283        TsObjectEntry::Shorthand(name) => {
1284            out.push_str(&indent(depth + 1));
1285            out.push_str(name);
1286            out.push_str(",\n");
1287        }
1288        TsObjectEntry::Spread(e) => {
1289            out.push_str(&indent(depth + 1));
1290            out.push_str("...");
1291            render_expr(out, e);
1292            out.push_str(",\n");
1293        }
1294        // `workers.rs`'s own dominant real shape (Decision A, gap 1):
1295        // `async {name}({params}) { <body> },`, body indented one
1296        // level deeper still — the same shape a class method's own
1297        // body gets (`render_decl_body`'s `TsDecl::Class` arm), here
1298        // for one object-literal entry instead.
1299        TsObjectEntry::Method {
1300            name,
1301            is_async,
1302            generics,
1303            params,
1304            return_type,
1305            doc,
1306            inline,
1307            body,
1308        } => {
1309            if let Some(text) = doc {
1310                render_doc_comment(out, text, depth + 1);
1311            }
1312            out.push_str(&indent(depth + 1));
1313            if *is_async {
1314                out.push_str("async ");
1315            }
1316            out.push_str(name);
1317            render_bare_generics(out, generics);
1318            out.push('(');
1319            render_params(out, params);
1320            out.push(')');
1321            if let Some(rt) = return_type {
1322                out.push_str(": ");
1323                render_type(out, rt);
1324            }
1325            if *inline {
1326                out.push_str(" { ");
1327                render_compact_stmts(out, body);
1328                out.push_str(" }");
1329            } else {
1330                render_block_stmts(out, body, depth + 1, map);
1331            }
1332            out.push_str(",\n");
1333        }
1334    }
1335}
1336
1337/// `<{names.join(", ")}>`, or nothing at all when `names` is empty — bare
1338/// names only, matching `bynk-emit`'s own `ts_type_params` rendering
1339/// exactly. Originally `TsObjectEntry::Method`'s own generic parameter list
1340/// (#1337); generalised by #1339 into the one shared renderer for every
1341/// bare-name generics/type-params list this crate builds
1342/// (`TsDecl::Interface.type_params`, `TsDecl::TypeAlias.type_params`,
1343/// `TsExpr::Arrow.generics`) rather than four near-identical copies.
1344fn render_bare_generics(out: &mut String, names: &[String]) {
1345    if names.is_empty() {
1346        return;
1347    }
1348    out.push('<');
1349    for (i, g) in names.iter().enumerate() {
1350        if i > 0 {
1351            out.push_str(", ");
1352        }
1353        out.push_str(g);
1354    }
1355    out.push('>');
1356}
1357
1358/// `[ <newline> ("  "*depth+1)<item>,<newline> ... ("  "*depth)]` — one
1359/// item per line, each with its own trailing comma, closing bracket back at
1360/// `depth`'s own indent. [`render_multiline_object`]'s own sibling for array
1361/// literals — #1325's own real, grounded shape: `emit_test_main`'s own
1362/// `modules` array (one `{ name, run }` entry per test). Same "no
1363/// empty-items shortcut" discipline as `render_multiline_object` — nothing
1364/// in `emit_test_main`'s own real content reaches this with zero tests, but
1365/// matching the pre-conversion `writeln!` code's own unconditional
1366/// open/close-bracket-on-separate-lines shape (rather than assuming an
1367/// untested empty case) is the same precedent review of #1317/#1318 set.
1368fn render_multiline_array(out: &mut String, items: &[TsExpr], depth: usize) {
1369    out.push_str("[\n");
1370    for item in items {
1371        out.push_str(&indent(depth + 1));
1372        render_expr(out, item);
1373        out.push_str(",\n");
1374    }
1375    out.push_str(&indent(depth));
1376    out.push(']');
1377}
1378
1379/// One [`TsObjectEntry`] rendered inline (no leading indent, no trailing
1380/// comma/newline) — [`render_expr`]'s own `TsExpr::Object` arm's per-entry
1381/// renderer, shared so the single-line and multi-line forms agree on what
1382/// each entry kind looks like. A `Method` entry has no real single-line
1383/// call site today (`workers.rs`'s own method entries are always the
1384/// multi-line `return { ... }` shape), but is still rendered plausibly
1385/// (not panicking) rather than left unreachable, matching this crate's own
1386/// "no wildcard, but still sensible on an unanticipated shape" posture
1387/// elsewhere (e.g. `render_block_body`'s own non-`Block` fallback).
1388fn render_object_entry_inline(out: &mut String, entry: &TsObjectEntry) {
1389    match entry {
1390        TsObjectEntry::Prop(k, v) => {
1391            out.push_str(k);
1392            out.push_str(": ");
1393            render_expr(out, v);
1394        }
1395        TsObjectEntry::Shorthand(name) => out.push_str(name),
1396        TsObjectEntry::Spread(e) => {
1397            out.push_str("...");
1398            render_expr(out, e);
1399        }
1400        TsObjectEntry::Method {
1401            name,
1402            is_async,
1403            generics,
1404            params,
1405            return_type,
1406            doc,
1407            inline,
1408            body,
1409        } => {
1410            // Review of #1338, finding 2: a JSDoc block has no single-line
1411            // form — not reachable through this function today: every real
1412            // `Method` entry (`inline` or not) is placed via
1413            // `render_multiline_object_entry`, never here (this function
1414            // renders one ENTRY inline for when the whole ENCLOSING OBJECT
1415            // is single-line, a different concept from a method's own body
1416            // being compact). Silently dropping a JSDoc block would be a
1417            // strictly worse failure mode than a loud check — the same
1418            // "not reachable today, but worth a loud check" posture
1419            // `render_compact_stmts`'s own `debug_assert!` (above) already
1420            // established for an analogous inline-rendering hazard.
1421            debug_assert!(
1422                doc.is_none(),
1423                "render_object_entry_inline: a Method entry's own doc comment has no single-line form"
1424            );
1425            if *is_async {
1426                out.push_str("async ");
1427            }
1428            out.push_str(name);
1429            render_bare_generics(out, generics);
1430            out.push('(');
1431            render_params(out, params);
1432            out.push(')');
1433            if let Some(rt) = return_type {
1434                out.push_str(": ");
1435                render_type(out, rt);
1436            }
1437            if *inline {
1438                out.push_str(" { ");
1439                render_compact_stmts(out, body);
1440                out.push_str(" }");
1441            } else {
1442                render_block_stmts(out, body, 0, None);
1443            }
1444        }
1445    }
1446}
1447
1448fn render_expr(out: &mut String, expr: &TsExpr) {
1449    match expr {
1450        TsExpr::Ident(name) => out.push_str(name),
1451        // #1539: printed exactly as `Ident` always was — raw text, no
1452        // escaping of its own. The origin exists for `verbatim_origins`/
1453        // `verbatim_sites` (xtask's gated probes) to line-scan, not for the
1454        // printer to branch on.
1455        TsExpr::VerbatimExpr(text, _origin) => out.push_str(text),
1456        TsExpr::Member { object, property } => {
1457            render_operand(out, object);
1458            out.push('.');
1459            out.push_str(property);
1460        }
1461        TsExpr::OptionalMember { object, property } => {
1462            render_operand(out, object);
1463            out.push_str("?.");
1464            out.push_str(property);
1465        }
1466        TsExpr::Index { object, index } => {
1467            render_operand(out, object);
1468            out.push('[');
1469            render_expr(out, index);
1470            out.push(']');
1471        }
1472        TsExpr::OptionalIndex { object, index } => {
1473            render_operand(out, object);
1474            out.push_str("?.[");
1475            render_expr(out, index);
1476            out.push(']');
1477        }
1478        TsExpr::Arrow {
1479            params,
1480            is_async,
1481            generics,
1482            return_type,
1483            body,
1484        } => {
1485            if *is_async {
1486                out.push_str("async ");
1487            }
1488            render_bare_generics(out, generics);
1489            out.push('(');
1490            render_params(out, params);
1491            out.push(')');
1492            if let Some(rt) = return_type {
1493                out.push_str(": ");
1494                render_type(out, rt);
1495            }
1496            out.push_str(" => ");
1497            render_arrow_body(out, body);
1498        }
1499        TsExpr::Call { callee, args } => {
1500            render_operand(out, callee);
1501            out.push('(');
1502            render_expr_list(out, args);
1503            out.push(')');
1504        }
1505        TsExpr::New { callee, args } => {
1506            out.push_str("new ");
1507            render_operand(out, callee);
1508            out.push('(');
1509            render_expr_list(out, args);
1510            out.push(')');
1511        }
1512        // `multiline` is ignored here deliberately: this recursion has no
1513        // `depth` to render a multi-line object correctly against (see
1514        // `TsExpr::Object`'s own doc). Every real `bynk-emit` call site
1515        // that needs `multiline: true` reaches it through
1516        // `render_stmt_level_expr` instead, which intercepts the shape
1517        // before it gets here.
1518        TsExpr::Object { entries, .. } => {
1519            if entries.is_empty() {
1520                out.push_str("{}");
1521            } else {
1522                out.push_str("{ ");
1523                for (i, entry) in entries.iter().enumerate() {
1524                    if i > 0 {
1525                        out.push_str(", ");
1526                    }
1527                    render_object_entry_inline(out, entry);
1528                }
1529                out.push_str(" }");
1530            }
1531        }
1532        // `multiline` is ignored here deliberately, for the same reason
1533        // `TsExpr::Object`'s own arm just above ignores it: this recursion
1534        // has no `depth` to render a multi-line array correctly against.
1535        // Every real `bynk-emit` call site that needs `multiline: true`
1536        // reaches it through `render_stmt_level_expr` instead.
1537        TsExpr::Array { items, .. } => {
1538            out.push('[');
1539            render_expr_list(out, items);
1540            out.push(']');
1541        }
1542        TsExpr::TemplateLit { parts, exprs } => {
1543            out.push('`');
1544            for (i, part) in parts.iter().enumerate() {
1545                out.push_str(part);
1546                if let Some(e) = exprs.get(i) {
1547                    out.push_str("${");
1548                    render_expr(out, e);
1549                    out.push('}');
1550                }
1551            }
1552            out.push('`');
1553        }
1554        TsExpr::Await(inner) => {
1555            out.push_str("await ");
1556            render_operand(out, inner);
1557        }
1558        TsExpr::As { expr, ty } => {
1559            // `(a ?? b) as T` / `((x) => y) as T` / `((a ? b : c)) as T` — a
1560            // binary expression, an arrow, or a conditional all need parens
1561            // before `as` (`(x) => y as T` binds the cast to the arrow's own
1562            // *body*, not the whole arrow — review of #1322, finding 1;
1563            // #1323 adds `Conditional` proactively, the same class of gap).
1564            // `Await` is deliberately NOT in this list (corrected by #1323):
1565            // `as` binds looser than `await`, so `await x as T` already
1566            // parses as `(await x) as T` with no parens needed — P7.8's
1567            // original reasoning conflated "this file's real text has
1568            // parens" with "parens are grammatically required"; `workers_
1569            // entry.rs`'s own real `await request.json() as JsonValue` has
1570            // none. A caller whose own real content *does* want them (like
1571            // `events_fanout.rs`'s own historical text) wraps its own
1572            // `Await` in an explicit [`TsExpr::Paren`] instead of relying on
1573            // an implicit per-shape rule here.
1574            let needs_parens = matches!(
1575                **expr,
1576                TsExpr::Binary { .. } | TsExpr::Arrow { .. } | TsExpr::Conditional { .. }
1577            );
1578            if needs_parens {
1579                out.push('(');
1580                render_expr(out, expr);
1581                out.push(')');
1582            } else {
1583                render_expr(out, expr);
1584            }
1585            out.push_str(" as ");
1586            render_type(out, ty);
1587        }
1588        TsExpr::Unary { op, expr } => {
1589            out.push_str(match op {
1590                TsUnaryOp::Not => "!",
1591                TsUnaryOp::Typeof => "typeof ",
1592            });
1593            render_operand(out, expr);
1594        }
1595        TsExpr::Binary { op, left, right } => {
1596            render_binary_operand(out, *op, left, true);
1597            out.push_str(match op {
1598                TsBinaryOp::NullishCoalescing => " ?? ",
1599                TsBinaryOp::Or => " || ",
1600                TsBinaryOp::And => " && ",
1601                TsBinaryOp::StrictEq => " === ",
1602                TsBinaryOp::StrictNotEq => " !== ",
1603                TsBinaryOp::GreaterThan => " > ",
1604                TsBinaryOp::LessThan => " < ",
1605                TsBinaryOp::InstanceOf => " instanceof ",
1606                TsBinaryOp::Add => " + ",
1607                TsBinaryOp::In => " in ",
1608                TsBinaryOp::GreaterThanEq => " >= ",
1609                TsBinaryOp::LessThanEq => " <= ",
1610            });
1611            render_binary_operand(out, *op, right, false);
1612        }
1613        TsExpr::Conditional {
1614            test,
1615            consequent,
1616            alternate,
1617        } => {
1618            // `test` prints like a `ShortCircuitExpression` (the real JS/TS
1619            // grammar position) — an `Arrow` or a nested `Conditional` there
1620            // needs parens (grammar), a `Binary`/`As`/anything else doesn't.
1621            // `consequent`/`alternate` are `AssignmentExpression` positions
1622            // and admit a nested `Arrow`/`Conditional` with no parens at all
1623            // (right-associative chaining, e.g. `a ? b : c ? d : e`, is
1624            // already the correct reading) — neither needs the operand
1625            // machinery the way `test` does.
1626            let test_needs_parens =
1627                matches!(**test, TsExpr::Arrow { .. } | TsExpr::Conditional { .. });
1628            if test_needs_parens {
1629                out.push('(');
1630                render_expr(out, test);
1631                out.push(')');
1632            } else {
1633                render_expr(out, test);
1634            }
1635            out.push_str(" ? ");
1636            render_expr(out, consequent);
1637            out.push_str(" : ");
1638            render_expr(out, alternate);
1639        }
1640        TsExpr::Paren(inner) => {
1641            out.push('(');
1642            render_expr(out, inner);
1643            out.push(')');
1644        }
1645        TsExpr::Lit(lit) => render_lit(out, lit),
1646    }
1647}
1648
1649/// [`TsExpr::Arrow`]'s own `body` renderer — see [`TsArrowBody`]'s own doc
1650/// for why there are exactly two shapes. `Expr` is the ordinary case, no
1651/// braces, the same plain `render_expr` recursion every arrow used before
1652/// #1435. `Block` reuses [`render_compact_stmts`] (the same shared core
1653/// `TsStmtKind::InlineBlock`'s own renderer already reduces to), not a
1654/// fresh copy of its "one physical line, semicolon-separated" logic — every
1655/// real `Block` site today (`serialisation.rs`'s `Float` guard) is exactly
1656/// this one-line-IIFE shape, so no trailing newline is added here (unlike
1657/// `render_inline_block`'s own statement-position form): this text is
1658/// embedded mid-expression, not standing alone as a statement.
1659fn render_arrow_body(out: &mut String, body: &TsArrowBody) {
1660    match body {
1661        TsArrowBody::Expr(expr) => render_expr(out, expr),
1662        TsArrowBody::Block(stmts) => {
1663            out.push_str("{ ");
1664            render_compact_stmts(out, stmts);
1665            out.push_str(" }");
1666        }
1667    }
1668}
1669
1670fn render_expr_list(out: &mut String, exprs: &[TsExpr]) {
1671    for (i, e) in exprs.iter().enumerate() {
1672        if i > 0 {
1673            out.push_str(", ");
1674        }
1675        render_expr(out, e);
1676    }
1677}
1678
1679fn render_lit(out: &mut String, lit: &TsLit) {
1680    match lit {
1681        TsLit::Str(s) => {
1682            out.push('"');
1683            // Matches `bynk_check::wire_default::escape_ts_literal`
1684            // byte-for-byte (review of #1314, finding 1) — the previous
1685            // version escaped only `"`/`\`, so a `\n`/`\t`/`\r` inside a
1686            // literal printed raw into the output, an unterminated string
1687            // literal `tsc` can't parse. `bynk-ts` can't depend on
1688            // `bynk-check` to share that function directly (this crate's
1689            // own module doc: `bynk-syntax` only) — inlined instead, kept
1690            // deliberately identical rather than reinvented, since both
1691            // this printer and `bynk-emit`'s `escape_ts_string` splice into
1692            // generated TypeScript and must never disagree.
1693            for c in s.chars() {
1694                match c {
1695                    '\\' => out.push_str("\\\\"),
1696                    '"' => out.push_str("\\\""),
1697                    '\n' => out.push_str("\\n"),
1698                    '\t' => out.push_str("\\t"),
1699                    '\r' => out.push_str("\\r"),
1700                    c => out.push(c),
1701                }
1702            }
1703            out.push('"');
1704        }
1705        TsLit::Num(n) => out.push_str(n),
1706        TsLit::Null => out.push_str("null"),
1707        TsLit::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
1708        // The whole literal, printed exactly as given — see `TsLit::Raw`'s
1709        // own doc for why: this is the one place in this printer that must
1710        // NOT apply `Str`'s own escaping.
1711        TsLit::Raw(text) => out.push_str(text),
1712    }
1713}
1714
1715fn render_type(out: &mut String, ty: &TsType) {
1716    match ty {
1717        TsType::Named { name, type_args } => {
1718            out.push_str(name);
1719            if !type_args.is_empty() {
1720                out.push('<');
1721                for (i, arg) in type_args.iter().enumerate() {
1722                    if i > 0 {
1723                        out.push_str(", ");
1724                    }
1725                    render_type(out, arg);
1726                }
1727                out.push('>');
1728            }
1729        }
1730        TsType::Array { element, readonly } => {
1731            if *readonly {
1732                out.push_str("readonly ");
1733            }
1734            render_type(out, element);
1735            out.push_str("[]");
1736        }
1737        TsType::Object(members) => {
1738            // Review of #1358, finding 1: a `Method` member's own `doc` has
1739            // no single-line form — this inline shape has nowhere to put a
1740            // JSDoc block, unlike `TsDecl::Interface`'s own render arm
1741            // (which calls `render_doc_comment` before a documented
1742            // member's own line, since it has `depth` and a real newline
1743            // budget to work with). The identical case #1338's own review
1744            // already ruled on for `TsObjectEntry::Method.doc` in
1745            // `render_object_entry_inline` — a loud check, not a silent
1746            // drop, since silently dropping a JSDoc block is a strictly
1747            // worse failure mode than a loud one. Not reachable today
1748            // (`workers_entry.rs`'s own one real `TsType::Object` method
1749            // member goes through `TsTypeMember::method`, so `doc` is
1750            // always `None`) — the #1337 case was equally unreachable,
1751            // which is exactly why it got the assert.
1752            debug_assert!(
1753                !members
1754                    .iter()
1755                    .any(|m| matches!(m, TsTypeMember::Method { doc: Some(_), .. })),
1756                "render_type: a Method member's own doc comment has no single-line form"
1757            );
1758            if members.is_empty() {
1759                out.push_str("{}");
1760            } else {
1761                out.push_str("{ ");
1762                for (i, m) in members.iter().enumerate() {
1763                    if i > 0 {
1764                        out.push_str("; ");
1765                    }
1766                    render_type_member(out, m);
1767                }
1768                out.push_str(" }");
1769            }
1770        }
1771        TsType::Fn { params, ret } => {
1772            out.push('(');
1773            for (i, p) in params.iter().enumerate() {
1774                if i > 0 {
1775                    out.push_str(", ");
1776                }
1777                out.push('a');
1778                out.push_str(&i.to_string());
1779                out.push_str(": ");
1780                render_type(out, p);
1781            }
1782            out.push_str(") => ");
1783            render_type(out, ret);
1784        }
1785        TsType::Union {
1786            members,
1787            multiline: false,
1788        } => {
1789            for (i, m) in members.iter().enumerate() {
1790                if i > 0 {
1791                    out.push_str(" | ");
1792                }
1793                render_type(out, m);
1794            }
1795        }
1796        // #1339: `emit_sum_type`'s own real shape — one variant per line, a
1797        // leading `|` on every line except the first (which gets equivalent
1798        // spacing instead, matching the pre-conversion `writeln!` code's own
1799        // `let pipe = if i == 0 { " " } else { "|" };` exactly), the closing
1800        // `;` appended by `TsDecl::TypeAlias`'s own caller directly after
1801        // the last member's own line — see `TsType::Union`'s own doc for
1802        // why this needs no depth parameter despite the multi-line shape.
1803        TsType::Union {
1804            members,
1805            multiline: true,
1806        } => {
1807            for (i, m) in members.iter().enumerate() {
1808                if i > 0 {
1809                    out.push('\n');
1810                }
1811                out.push_str("  ");
1812                out.push_str(if i == 0 { " " } else { "|" });
1813                out.push(' ');
1814                render_type(out, m);
1815            }
1816        }
1817        TsType::Intersection(members) => {
1818            for (i, m) in members.iter().enumerate() {
1819                if i > 0 {
1820                    out.push_str(" & ");
1821                }
1822                render_type(out, m);
1823            }
1824        }
1825    }
1826}
1827
1828/// One [`TsTypeMember`] rendered inline (no leading indent, no trailing
1829/// separator) — [`render_type`]'s own `TsType::Object` arm's per-member
1830/// renderer.
1831fn render_type_member(out: &mut String, member: &TsTypeMember) {
1832    match member {
1833        TsTypeMember::Prop {
1834            name,
1835            ty,
1836            optional,
1837            readonly,
1838        } => {
1839            if *readonly {
1840                out.push_str("readonly ");
1841            }
1842            out.push_str(name);
1843            if *optional {
1844                out.push('?');
1845            }
1846            out.push_str(": ");
1847            render_type(out, ty);
1848        }
1849        TsTypeMember::Method {
1850            name,
1851            generics,
1852            params,
1853            ret,
1854            doc: _,
1855        } => {
1856            out.push_str(name);
1857            render_bare_generics(out, generics);
1858            out.push('(');
1859            render_params(out, params);
1860            out.push_str("): ");
1861            render_type(out, ret);
1862        }
1863        TsTypeMember::Index {
1864            key_name,
1865            key_ty,
1866            value_ty,
1867        } => {
1868            out.push('[');
1869            out.push_str(key_name);
1870            out.push_str(": ");
1871            render_type(out, key_ty);
1872            out.push_str("]: ");
1873            render_type(out, value_ty);
1874        }
1875    }
1876}
1877
1878/// Print a single [`TsType`] on its own — the real callers this closes R7.2
1879/// for (`bynk-emit`'s `ts_type_ref*`/`ts_ty` families, P7.9, #1315) each
1880/// want one type fragment to interpolate into a larger, still-hand-built
1881/// line (a field's own type annotation, a parameter list, …), not a whole
1882/// [`TsProgram`]. No source-map/buffer machinery — [`print()`] owns that
1883/// for a whole document; this is the printer's other, narrower entry
1884/// point, sharing the same internal recursion rather than a second copy.
1885pub fn print_type(ty: &TsType) -> String {
1886    let mut out = String::new();
1887    render_type(&mut out, ty);
1888    out
1889}
1890
1891/// Print a single [`TsExpr`] on its own — the expression-level sibling of
1892/// [`print_type`]'s own "one fragment, not a whole document" entry point.
1893/// Arc C, step (11) (#1388) need: `emit_icu_placeholder`'s own `Select` arm
1894/// stays one opaque, hand-built block-bodied IIFE (at the time, `TsExpr::
1895/// Arrow` had no block-body variant at all, and extending it for that one
1896/// site was rejected as disproportionate) — its own arm VALUES,
1897/// `emit_sub_message`'s now-real `TsExpr` results, still need stringifying
1898/// back into that opaque host text. #1435 (Arc E slice 1) later added
1899/// [`TsArrowBody::Block`] for a genuinely different real site
1900/// (`serialisation.rs`'s `Float` guard); `emit_icu_placeholder`'s own
1901/// `Select` arm was not reconverted along with it — out of that slice's own
1902/// scope — so this call site's opaque text stays exactly as it was. No
1903/// source-map/buffer machinery, matching [`print_type`]/[`print_stmt`]'s
1904/// own scope exactly.
1905pub fn print_expr(expr: &TsExpr) -> String {
1906    let mut out = String::new();
1907    render_expr(&mut out, expr);
1908    out
1909}
1910
1911/// Print a single [`TsStmt`] on its own, at `depth` — the statement-level
1912/// sibling of [`print_type`]'s own "one fragment, not a whole document"
1913/// entry point. `bynk-emit`'s own #1333 need (`emit_doc_block`, a shared
1914/// helper spliced into ~14 still-unconverted callers' own buffers) wants
1915/// one statement's own printed text at a caller-supplied depth, not a
1916/// whole [`TsProgram`] — no source-map/buffer machinery, matching
1917/// [`print_type`]'s own scope exactly, and reusing `render_stmt`'s own
1918/// exhaustive per-kind dispatch (this module's own private renderer)
1919/// rather than a second copy.
1920/// Review of #1402: a `TsStmtKind::Raw` nested inside a `Switch` case's own
1921/// body (`emit_stub_rhs`'s own `ReturnsEach` dispatch, Arc C slice 33,
1922/// `tests_emit.rs` slice C) carries the identical "no indent of its own,
1923/// pre-indented at a fixed absolute depth" hazard `render_class_method`'s
1924/// and `render_multiline_object_entry`'s own `debug_assert!`s already guard
1925/// — `stmt_contains_raw` already recurses into `Switch` cases, so the same
1926/// check applies here, this fragment entry point's own first `Raw`-bearing
1927/// `Switch` caller. A bare `Raw` passed directly as `stmt` itself is exempt
1928/// (not a false negative — `render_stmt`'s own `Raw` arm never reads
1929/// `depth` at all, so calling `print_stmt` on a bare `Raw` is safe at any
1930/// depth, the established `print_stmt_renders_raw_text_verbatim_with_no_
1931/// added_indent_or_punctuation` contract below): only a Raw *nested inside*
1932/// a depth-using wrapper (like this `Switch` case) is the real hazard.
1933pub fn print_stmt(stmt: &TsStmt, depth: usize) -> String {
1934    debug_assert!(
1935        depth == 0 || matches!(stmt.kind, TsStmtKind::Raw(_)) || !stmt_contains_raw(stmt),
1936        "print_stmt: a Raw-bearing statement's own baked-in indent only matches depth 0"
1937    );
1938    let mut out = String::new();
1939    render_stmt(&mut out, stmt, depth, None);
1940    out
1941}
1942
1943/// [`print_stmt`]'s own sibling (#1477), the `TsDecl::Function`-body
1944/// counterpart to [`print_class_method_and_merge`]: identical rendering,
1945/// but merges any `nested_map` `stmt` carries — on `stmt` itself, or on
1946/// anything nested inside it (a function's body, a class's constructor/
1947/// methods, …) — into `map` as it prints, at the real print-time offset —
1948/// the same offset `bynk-emit`'s own `emit_free_fn` (`emitter/emit.rs`) used
1949/// to recover by exact arithmetic over the returned text, guarded by a
1950/// `debug_assert!`, before #1480 converted it to set `nested_map` directly
1951/// instead. Kept separate from `print_stmt` itself so every existing
1952/// caller's own call sites are untouched. `source_id` is `map`'s
1953/// own registered source this statement's content belongs to (see
1954/// this crate's own private `MergeTarget`'s own doc for why this can't just be hardcoded).
1955///
1956/// **Appends to the caller's own `out`, unlike `print_stmt`'s own
1957/// return-a-fresh-`String` shape** — the merge computes each checkpoint's
1958/// absolute offset from `out.len()` *as this statement's own text is
1959/// written*, so it must be the caller's real, already-populated buffer, not
1960/// a throwaway local one starting at 0. Returning a fresh string here (the
1961/// first shape this function took, caught before merge) would silently
1962/// record every checkpoint at the wrong offset the moment the caller
1963/// spliced the returned text anywhere but the very start of its own buffer.
1964pub fn print_stmt_and_merge(
1965    out: &mut String,
1966    stmt: &TsStmt,
1967    depth: usize,
1968    map: &mut SourceMapBuilder,
1969    source_id: usize,
1970) {
1971    debug_assert!(
1972        depth == 0 || matches!(stmt.kind, TsStmtKind::Raw(_)) || !stmt_contains_raw(stmt),
1973        "print_stmt_and_merge: a Raw-bearing statement's own baked-in indent only matches depth 0"
1974    );
1975    render_stmt(out, stmt, depth, Some(MergeTarget { map, source_id }));
1976}
1977
1978/// Print a single [`TsObjectEntry`] on its own, at `depth` — the
1979/// object-entry sibling of [`print_stmt`]'s own "one fragment, not a whole
1980/// document" entry point, `depth` meaning the SAME thing it does for this
1981/// crate's own internal multi-line-object renderer: the *object's* own
1982/// depth, so the entry itself lands one level deeper, matching an object built by that
1983/// renderer exactly. #1337's own real need: `emit_attached_methods` (a
1984/// shared helper spliced into `emit_refined_type`/`emit_record_type`/
1985/// `emit_sum_type`'s own still-unconverted `&mut String` buffers) now
1986/// returns `Vec<TsObjectEntry>` instead of writing text directly — each
1987/// caller renders the returned entries one at a time through this, the
1988/// same P7.9/#1333 "keep the caller's own signature, print just the
1989/// fragment" pattern applied to an object-entry-shaped fragment instead of
1990/// a whole statement or type.
1991///
1992/// Review of #1338, finding 3: a `TsStmtKind::Raw` body statement (e.g.
1993/// `emit_method`'s own opaque `lower.rs`-sourced body, or `emit_refined_
1994/// type`'s own opaque `emit_refined_checks`-sourced guard body, #1339's
1995/// second real use) carries NO indent of its own — its text is captured
1996/// pre-indented at a fixed absolute depth by its own caller — so it only
1997/// renders correctly when `depth` is `0`. The guard for this now lives in
1998/// `render_multiline_object_entry` itself (a private renderer, so named
1999/// here in text rather than linked — moved there by review of #1340,
2000/// finding 1: this function's own copy missed the `TsExpr::
2001/// multiline_object_entries`/`render_expr` call path #1339 added, which
2002/// reaches that renderer directly, never through this one), so it fires
2003/// for every caller, not just this entry point.
2004pub fn print_object_entry(entry: &TsObjectEntry, depth: usize) -> String {
2005    let mut out = String::new();
2006    render_multiline_object_entry(&mut out, entry, depth, None);
2007    out
2008}
2009
2010/// [`print_object_entry`]'s own sibling (#1477), the `TsObjectEntry::Method`
2011/// counterpart to [`print_class_method_and_merge`]/[`print_stmt_and_merge`]:
2012/// identical rendering, but merges a `Method` entry's own body `nested_map`
2013/// into `map` as it prints, at the real print-time offset. Kept separate
2014/// from `print_object_entry` itself so every existing caller's own call
2015/// sites are untouched.
2016///
2017/// Appends to the caller's own `out`, the same "must be the real buffer,
2018/// not a throwaway local one" reasoning [`print_stmt_and_merge`]'s own doc
2019/// explains in full. `source_id`: see this crate's own private `MergeTarget`'s own doc.
2020pub fn print_object_entry_and_merge(
2021    out: &mut String,
2022    entry: &TsObjectEntry,
2023    depth: usize,
2024    map: &mut SourceMapBuilder,
2025    source_id: usize,
2026) {
2027    render_multiline_object_entry(out, entry, depth, Some(MergeTarget { map, source_id }));
2028}
2029
2030/// Whether `stmts`, or any block/branch/case nested inside them at any
2031/// depth, contains a [`TsStmtKind::Raw`] — the recursive check
2032/// [`render_class_method`]'s and [`render_multiline_object_entry`]'s own
2033/// `debug_assert!`s need. Review of #1374: a top-level-only scan
2034/// (`body.iter().any(...)`) is blind to a `Raw` buried inside an `If`'s own
2035/// `Block` — `emit_agent`'s own `commitState` (#1373) is exactly this shape,
2036/// a transition's own hoisted-statement `Raw`s sitting inside
2037/// `if (__prior !== undefined) { ... }` — so the two guards this exists for
2038/// would have silently missed exactly the case they exist to catch. Every
2039/// `TsStmtKind` variant is named explicitly, not a wildcard, matching this
2040/// crate's own exhaustiveness discipline.
2041fn contains_raw(stmts: &[TsStmt]) -> bool {
2042    stmts.iter().any(stmt_contains_raw)
2043}
2044
2045fn stmt_contains_raw(stmt: &TsStmt) -> bool {
2046    match &stmt.kind {
2047        TsStmtKind::Raw(_) => true,
2048        TsStmtKind::Verbatim { .. }
2049        | TsStmtKind::Decl(_)
2050        | TsStmtKind::Const { .. }
2051        | TsStmtKind::Let { .. }
2052        | TsStmtKind::ExprStmt(_)
2053        | TsStmtKind::Return(_)
2054        | TsStmtKind::Throw(_)
2055        | TsStmtKind::Continue
2056        | TsStmtKind::Assign { .. }
2057        | TsStmtKind::Comment(_)
2058        | TsStmtKind::DocComment(_)
2059        | TsStmtKind::Blank
2060        | TsStmtKind::Increment(_) => false,
2061        TsStmtKind::If {
2062            then_branch,
2063            else_branch,
2064            ..
2065        } => {
2066            stmt_contains_raw(then_branch) || else_branch.as_deref().is_some_and(stmt_contains_raw)
2067        }
2068        TsStmtKind::ForOf { body, .. } | TsStmtKind::For { body, .. } => stmt_contains_raw(body),
2069        TsStmtKind::TryCatch {
2070            try_block,
2071            catch_block,
2072            ..
2073        } => stmt_contains_raw(try_block) || stmt_contains_raw(catch_block),
2074        TsStmtKind::Block(stmts) | TsStmtKind::InlineBlock(stmts) => contains_raw(stmts),
2075        TsStmtKind::Switch { cases, .. } => cases.iter().any(|c| contains_raw(&c.body)),
2076    }
2077}
2078
2079/// Print a single [`TsClassMethod`] on its own, at `depth` — the
2080/// class-method sibling of [`print_object_entry`]'s own "one fragment, not
2081/// a whole document" entry point. `depth` means the *class's* own depth
2082/// (matching `print_object_entry`'s own convention exactly), so the method
2083/// itself lands at `depth + 1`, its own body at `depth + 2`. #1359's own
2084/// real need: `emit_provider`'s own class wrapper stays hand-written text
2085/// (each method's own body needs a per-method source-map sub-builder/
2086/// `merge`, and the wrapper's own real spacing — no blank line between
2087/// methods — genuinely differs from [`TsDecl::Class`]'s own "one blank
2088/// line before each method" policy, `events_fanout.rs`'s real convention,
2089/// #1317), so each real method prints through here directly into that
2090/// still-hand-written wrapper. Deliberately no automatic blank-line
2091/// insertion of its own — the same "caller controls spacing" contract
2092/// `print_object_entry` already established.
2093pub fn print_class_method(method: &TsClassMethod, depth: usize) -> String {
2094    let mut out = String::new();
2095    render_class_method(&mut out, method, depth, None);
2096    out
2097}
2098
2099/// [`print_class_method`]'s own sibling (#1477): identical rendering, but
2100/// merges any of `method.body`'s own direct-child `nested_map`s into `map`
2101/// as they print, at the real print-time offset — no reverse-engineering
2102/// the offset from the returned text afterward, the way `bynk-emit`'s own
2103/// `emit_class_method_and_merge_source_map` (`emitter/emit.rs`) has to
2104/// today. Kept as a separate function, not an added parameter on
2105/// `print_class_method` itself, so every existing caller's own call sites
2106/// are untouched — this is a strict addition.
2107///
2108/// Appends to the caller's own `out`, the same "must be the real buffer,
2109/// not a throwaway local one" reasoning [`print_stmt_and_merge`]'s own doc
2110/// explains in full. `source_id`: see this crate's own private `MergeTarget`'s own doc.
2111pub fn print_class_method_and_merge(
2112    out: &mut String,
2113    method: &TsClassMethod,
2114    depth: usize,
2115    map: &mut SourceMapBuilder,
2116    source_id: usize,
2117) {
2118    render_class_method(out, method, depth, Some(MergeTarget { map, source_id }));
2119}
2120
2121/// The one real per-method render, shared by [`print_class_method`] and
2122/// [`TsDecl::Class`]'s own methods-loop arm — review of #1360, finding 4:
2123/// keeping two independent copies of this shape was exactly the drift risk
2124/// this track exists to design against (two printers disagreeing about a
2125/// formatting convention). `depth` means the *class's* own depth, matching
2126/// both callers' own convention.
2127///
2128/// Review of #1360, finding 1: a `TsStmtKind::Raw` method body has no
2129/// indent of its own — its text is captured pre-indented at a fixed
2130/// absolute depth by its own caller — so it only renders correctly when
2131/// `depth` is `0`, the same hazard `render_multiline_object_entry`'s own
2132/// identical `debug_assert!` guards (review of #1338 finding 3, relocated
2133/// by review of #1340 finding 1). `print_class_method`'s own doc frames it
2134/// as a general fragment entry point, so a future caller at a non-zero
2135/// depth is the expected case this guards against, not a hypothetical.
2136///
2137/// `map`: see [`render_block_stmts`]'s own doc (#1477) — forwarded
2138/// unchanged to `method.body`'s own rendering.
2139fn render_class_method(
2140    out: &mut String,
2141    method: &TsClassMethod,
2142    depth: usize,
2143    map: Option<MergeTarget<'_>>,
2144) {
2145    debug_assert!(
2146        depth == 0 || !contains_raw(&method.body),
2147        "render_class_method: a Raw method body's own baked-in indent only matches depth 0"
2148    );
2149    if let Some(text) = &method.doc {
2150        render_doc_comment(out, text, depth + 1);
2151    }
2152    out.push_str(&indent(depth + 1));
2153    if method.private {
2154        out.push_str("private ");
2155    }
2156    if method.is_async {
2157        out.push_str("async ");
2158    }
2159    out.push_str(&method.name);
2160    out.push('(');
2161    render_params(out, &method.params);
2162    out.push(')');
2163    if let Some(rt) = &method.return_type {
2164        out.push_str(": ");
2165        render_type(out, rt);
2166    }
2167    render_block_stmts(out, &method.body, depth + 1, map);
2168    out.push('\n');
2169}
2170
2171fn render_params(out: &mut String, params: &[TsParam]) {
2172    for (i, p) in params.iter().enumerate() {
2173        if i > 0 {
2174            out.push_str(", ");
2175        }
2176        out.push_str(&p.name);
2177        if p.optional {
2178            out.push('?');
2179        }
2180        if let Some(ty) = &p.ty {
2181            out.push_str(": ");
2182            render_type(out, ty);
2183        }
2184    }
2185}
2186
2187fn render_decl(out: &mut String, decl: &TsDecl, depth: usize, map: Option<MergeTarget<'_>>) {
2188    // `render_decl_body`'s own `TsDecl::Export` arm already writes
2189    // `"export "` before recursing into the inner declaration — the
2190    // `if let Export` special case this replaced produced byte-identical
2191    // output by duplicating that same logic one layer up (review of
2192    // #1314, smaller note: the two branches were provably the same for
2193    // every input, so only one needs to exist).
2194    out.push_str(&indent(depth));
2195    render_decl_body(out, decl, depth, map);
2196}
2197
2198/// The declaration's own text, with no leading indent — [`render_decl`]
2199/// writes the indent (and, for `Export`, the `export ` keyword) once, then
2200/// hands off here so `Export(inner)` doesn't duplicate `inner`'s own
2201/// leading whitespace.
2202///
2203/// `map`: see [`render_block_stmts`]'s own doc (#1477) — reaches a
2204/// `TsDecl::Function`'s own body, and a `TsDecl::Class`'s own constructor/
2205/// methods, unchanged; every other arm ignores it (no body of its own to
2206/// carry a `nested_map`).
2207fn render_decl_body(
2208    out: &mut String,
2209    decl: &TsDecl,
2210    depth: usize,
2211    mut map: Option<MergeTarget<'_>>,
2212) {
2213    match decl {
2214        TsDecl::Import {
2215            type_only,
2216            names,
2217            from,
2218        } => {
2219            out.push_str(if *type_only {
2220                "import type { "
2221            } else {
2222                "import { "
2223            });
2224            out.push_str(&names.join(", "));
2225            out.push_str(" } from \"");
2226            out.push_str(from);
2227            out.push_str("\";\n");
2228        }
2229        TsDecl::ImportNamespace {
2230            type_only,
2231            alias,
2232            from,
2233        } => {
2234            out.push_str(if *type_only {
2235                "import type * as "
2236            } else {
2237                "import * as "
2238            });
2239            out.push_str(alias);
2240            out.push_str(" from \"");
2241            out.push_str(from);
2242            out.push_str("\";\n");
2243        }
2244        TsDecl::ImportDefault { alias, from } => {
2245            out.push_str("import ");
2246            out.push_str(alias);
2247            out.push_str(" from \"");
2248            out.push_str(from);
2249            out.push_str("\";\n");
2250        }
2251        TsDecl::ReExport { names, from } => {
2252            out.push_str("export { ");
2253            out.push_str(&names.join(", "));
2254            out.push_str(" } from \"");
2255            out.push_str(from);
2256            out.push_str("\";\n");
2257        }
2258        TsDecl::ReExportAll { from } => {
2259            out.push_str("export * from \"");
2260            out.push_str(from);
2261            out.push_str("\";\n");
2262        }
2263        TsDecl::Export(inner) => {
2264            out.push_str("export ");
2265            render_decl_body(out, inner, depth, map);
2266        }
2267        TsDecl::Interface {
2268            name,
2269            type_params,
2270            members,
2271        } => {
2272            out.push_str("interface ");
2273            out.push_str(name);
2274            render_bare_generics(out, type_params);
2275            out.push_str(" {\n");
2276            for member in members {
2277                // #1357: a `Method` member's own `doc` renders here, not in
2278                // `render_type_member` itself — that function has no
2279                // `depth` to give `render_doc_comment`, the same "doc lives
2280                // at the call site that carries depth" split
2281                // `render_multiline_object_entry`'s own `Method` arm
2282                // already uses for the identical field.
2283                if let TsTypeMember::Method {
2284                    doc: Some(text), ..
2285                } = member
2286                {
2287                    render_doc_comment(out, text, depth + 1);
2288                }
2289                out.push_str(&indent(depth + 1));
2290                render_type_member(out, member);
2291                out.push_str(";\n");
2292            }
2293            out.push_str(&indent(depth));
2294            out.push_str("}\n");
2295        }
2296        TsDecl::ConstDecl { name, ty, init } => {
2297            out.push_str("const ");
2298            out.push_str(name);
2299            if let Some(ty) = ty {
2300                out.push_str(": ");
2301                render_type(out, ty);
2302            }
2303            out.push_str(" = ");
2304            render_stmt_level_expr(out, init, depth);
2305            out.push_str(";\n");
2306        }
2307        TsDecl::Class {
2308            name,
2309            fields,
2310            constructor,
2311            methods,
2312        } => {
2313            out.push_str("class ");
2314            out.push_str(name);
2315            out.push_str(" {\n");
2316            for f in fields {
2317                out.push_str(&indent(depth + 1));
2318                if f.private {
2319                    out.push_str("private ");
2320                }
2321                out.push_str(&f.name);
2322                out.push_str(": ");
2323                render_type(out, &f.ty);
2324                out.push_str(";\n");
2325            }
2326            // Readability policy (this module's own doc): no blank line
2327            // between fields and the constructor; one blank line before
2328            // each method — `events_fanout.rs`'s own real class spacing.
2329            let mut wrote_member = !fields.is_empty();
2330            if let Some(ctor) = constructor {
2331                out.push_str(&indent(depth + 1));
2332                out.push_str("constructor(");
2333                render_params(out, &ctor.params);
2334                out.push(')');
2335                render_block_stmts(
2336                    out,
2337                    &ctor.body,
2338                    depth + 1,
2339                    map.as_mut().map(|t| t.reborrow()),
2340                );
2341                out.push('\n');
2342                wrote_member = true;
2343            }
2344            for m in methods {
2345                if wrote_member {
2346                    out.push('\n');
2347                }
2348                render_class_method(out, m, depth, map.as_mut().map(|t| t.reborrow()));
2349                wrote_member = true;
2350            }
2351            out.push_str(&indent(depth));
2352            out.push_str("}\n");
2353        }
2354        TsDecl::Function {
2355            name,
2356            generics,
2357            params,
2358            return_type,
2359            body,
2360            is_async,
2361            inline,
2362        } => {
2363            if *is_async {
2364                out.push_str("async ");
2365            }
2366            out.push_str("function ");
2367            out.push_str(name);
2368            render_bare_generics(out, generics);
2369            out.push('(');
2370            render_params(out, params);
2371            out.push(')');
2372            if let Some(rt) = return_type {
2373                out.push_str(": ");
2374                render_type(out, rt);
2375            }
2376            if *inline {
2377                out.push(' ');
2378                render_inline_block(out, body);
2379            } else {
2380                render_block_stmts(out, body, depth, map);
2381                out.push('\n');
2382            }
2383        }
2384        TsDecl::TypeAlias {
2385            name,
2386            type_params,
2387            ty,
2388        } => {
2389            out.push_str("type ");
2390            out.push_str(name);
2391            render_bare_generics(out, type_params);
2392            // #1339: a multiline Union's own first rendered character is
2393            // its first member's leading indent, not a newline — the `=`
2394            // itself must be followed directly by `\n` here (no space
2395            // before it), matching the pre-conversion `writeln!(out,
2396            // "export type {name}{params} =")` line's own exact bytes;
2397            // every other `ty` keeps the ordinary `" = "` (space both
2398            // sides) ordinary single-line form.
2399            if matches!(
2400                ty,
2401                TsType::Union {
2402                    multiline: true,
2403                    ..
2404                }
2405            ) {
2406                out.push_str(" =\n");
2407            } else {
2408                out.push_str(" = ");
2409            }
2410            render_type(out, ty);
2411            out.push_str(";\n");
2412        }
2413        TsDecl::ExportDefault(expr) => {
2414            out.push_str("export default ");
2415            render_stmt_level_expr(out, expr, depth);
2416            out.push_str(";\n");
2417        }
2418        TsDecl::DeclareConst { name, ty } => {
2419            out.push_str("declare const ");
2420            out.push_str(name);
2421            out.push_str(": ");
2422            render_type(out, ty);
2423            out.push_str(";\n");
2424        }
2425    }
2426}
2427
2428/// `{ <stmts> }` for a constructor/method body — same shape as
2429/// [`render_block_body`] but over a plain `&[TsStmt]` (a `TsClassCtor`/
2430/// `TsClassMethod`'s own `body` field, not a boxed `TsStmt`), and always
2431/// followed by the caller's own `\n` (constructor/method declarations, not
2432/// a same-line `try`/`catch` continuation).
2433///
2434/// `map: Some(_)` when the caller wants a `nested_map`-bearing statement
2435/// reachable from `stmts` merged into a module source map as it prints
2436/// (#1477) — see [`crate::program::TsStmt::nested_map`]'s own doc for the
2437/// problem this solves. Reborrowed once per child and forwarded to
2438/// [`render_stmt`], which does the actual check — this function itself
2439/// holds none of that logic.
2440fn render_block_stmts(
2441    out: &mut String,
2442    stmts: &[TsStmt],
2443    depth: usize,
2444    mut map: Option<MergeTarget<'_>>,
2445) {
2446    out.push_str(" {\n");
2447    for s in stmts {
2448        render_stmt(out, s, depth + 1, map.as_mut().map(|t| t.reborrow()));
2449    }
2450    out.push_str(&indent(depth));
2451    out.push('}');
2452}
2453
2454#[cfg(test)]
2455mod tests {
2456    use super::*;
2457    use crate::program::{TsClassCtor, TsClassField, TsClassMethod, TsSwitchCase, VerbatimOrigin};
2458    use bynk_syntax::span::Span;
2459
2460    /// Pins the readability policy's statement-separation guarantee (R7.5,
2461    /// this module's own doc): every statement starts on its own generated
2462    /// line.
2463    #[test]
2464    fn prints_every_statement_in_order() {
2465        let mut program = TsProgram::new();
2466        program.push(TsStmt::verbatim(
2467            VerbatimOrigin::Contracts,
2468            "const a = 1;\n",
2469            None,
2470        ));
2471        program.push(TsStmt::verbatim(
2472            VerbatimOrigin::Secrets,
2473            "const b = 2;\n",
2474            None,
2475        ));
2476        let printed = print(&program, "x.bynk", "", "x.ts");
2477        assert_eq!(printed.text, "const a = 1;\nconst b = 2;\n");
2478    }
2479
2480    /// Pins the readability policy's statement-separation guarantee (R7.5,
2481    /// this module's own doc) in its sharpest form: nothing requires
2482    /// `Verbatim` text to be newline-terminated — the printer owns line
2483    /// structure (R7.3), so it's the one that guarantees two statements
2484    /// never share a generated line, not a `TsStmt::verbatim` caller
2485    /// obligation nobody enforces (review of #1308, finding 2).
2486    #[test]
2487    fn a_statement_missing_its_own_trailing_newline_still_gets_its_own_line() {
2488        let mut program = TsProgram::new();
2489        program.push(TsStmt::verbatim(
2490            VerbatimOrigin::Contracts,
2491            "const a = 1;",
2492            None,
2493        ));
2494        program.push(TsStmt::verbatim(
2495            VerbatimOrigin::Secrets,
2496            "const b = 2;",
2497            None,
2498        ));
2499        let printed = print(&program, "x.bynk", "", "x.ts");
2500        assert_eq!(printed.text, "const a = 1;\nconst b = 2;\n");
2501    }
2502
2503    /// Production `Verbatim` content is rarely one line — `VerbatimOrigin::
2504    /// NotYetConverted` (P7.6) wraps a whole generated document per
2505    /// statement, so one statement routinely spans dozens of generated
2506    /// lines. Neither of the two tests above exercised that shape (review
2507    /// of #1312, finding 1) — this one confirms the readability policy's
2508    /// real invariant (the *next* statement always starts on a fresh line)
2509    /// holds for multi-line content too, not just the single-line text
2510    /// those tests happened to use.
2511    #[test]
2512    fn a_multi_line_statement_still_starts_the_next_statement_on_a_fresh_line() {
2513        let mut program = TsProgram::new();
2514        program.push(TsStmt::verbatim(
2515            VerbatimOrigin::Contracts,
2516            "function a() {\n  return 1;\n}\n",
2517            None,
2518        ));
2519        program.push(TsStmt::verbatim(
2520            VerbatimOrigin::Secrets,
2521            "const b = 2;\n",
2522            None,
2523        ));
2524        let printed = print(&program, "x.bynk", "", "x.ts");
2525        assert_eq!(
2526            printed.text,
2527            "function a() {\n  return 1;\n}\nconst b = 2;\n"
2528        );
2529    }
2530
2531    /// `print` decides whether to append a trailing newline by checking the
2532    /// whole *buffer*, not the statement's own text (review of #1312,
2533    /// finding 2) — an empty statement's text changes nothing, so whether
2534    /// it gets a line depends entirely on what's already in the buffer.
2535    /// First in a program, the buffer is still empty, so the check fires
2536    /// and the output opens with a blank line. Pinned here so this edge
2537    /// stays a known, deliberate reading of the code rather than an
2538    /// untested accident.
2539    #[test]
2540    fn an_empty_statement_first_in_the_program_yields_a_leading_blank_line() {
2541        let mut program = TsProgram::new();
2542        program.push(TsStmt::verbatim(VerbatimOrigin::Contracts, "", None));
2543        program.push(TsStmt::verbatim(
2544            VerbatimOrigin::Secrets,
2545            "const b = 2;\n",
2546            None,
2547        ));
2548        let printed = print(&program, "x.bynk", "", "x.ts");
2549        assert_eq!(printed.text, "\nconst b = 2;\n");
2550    }
2551
2552    /// The other half of the buffer-vs-statement-text distinction (review
2553    /// of #1312, finding 2): an empty statement following one that already
2554    /// ended its own text in `\n` contributes no line at all — the buffer
2555    /// already ends in `\n`, so the check doesn't fire, and the empty
2556    /// statement's own (non-)content and the next statement's text end up
2557    /// on what reads as one generated line for the next statement alone.
2558    #[test]
2559    fn an_empty_statement_after_a_newline_terminated_one_contributes_no_line_of_its_own() {
2560        let mut program = TsProgram::new();
2561        program.push(TsStmt::verbatim(
2562            VerbatimOrigin::Contracts,
2563            "const a = 1;\n",
2564            None,
2565        ));
2566        program.push(TsStmt::verbatim(VerbatimOrigin::Secrets, "", None));
2567        program.push(TsStmt::verbatim(
2568            VerbatimOrigin::RuntimeUse,
2569            "const c = 3;\n",
2570            None,
2571        ));
2572        let printed = print(&program, "x.bynk", "", "x.ts");
2573        assert_eq!(printed.text, "const a = 1;\nconst c = 3;\n");
2574    }
2575
2576    #[test]
2577    fn no_spans_at_all_means_no_source_map() {
2578        let mut program = TsProgram::new();
2579        program.push(TsStmt::verbatim(VerbatimOrigin::Contracts, "x;\n", None));
2580        let printed = print(&program, "x.bynk", "let x = 1\n", "x.ts");
2581        assert_eq!(printed.source_map, None);
2582    }
2583
2584    /// The property that actually matters (R7.4): the printer's own
2585    /// buffer-position bookkeeping is correct — a checkpoint recorded from a
2586    /// later statement's span still resolves to the *generated* line that
2587    /// statement's own text landed on, not (as the old splice-based
2588    /// mechanism could get wrong, #4) some other buffer's offset.
2589    ///
2590    /// Review of #1308, finding 3: the original version of this test
2591    /// asserted only that the source map's `sources` array was present —
2592    /// true even with the second statement's checkpoint silently dropped,
2593    /// exactly the failure this doc comment claims is ruled out. Asserting
2594    /// full equality against a map built independently (replicating the
2595    /// printer's own record-then-write order by hand, not calling `print`)
2596    /// actually pins both statements' own offsets.
2597    #[test]
2598    fn each_statements_span_resolves_to_its_own_generated_line() {
2599        let source = "let a = 1\nlet b = 2\n";
2600        let off_a = source.find("let a").unwrap();
2601        let off_b = source.find("let b").unwrap();
2602        let span_a = Span::new(off_a, off_a + 5);
2603        let span_b = Span::new(off_b, off_b + 5);
2604        let text_a = "const a = 1;\n";
2605        let text_b = "const b = 2;\n";
2606        let mut program = TsProgram::new();
2607        program.push(TsStmt::verbatim(
2608            VerbatimOrigin::Contracts,
2609            text_a,
2610            Some(span_a),
2611        ));
2612        program.push(TsStmt::verbatim(
2613            VerbatimOrigin::Secrets,
2614            text_b,
2615            Some(span_b),
2616        ));
2617
2618        let printed = print(&program, "x.bynk", source, "x.ts");
2619
2620        let mut expected_map = SourceMapBuilder::new();
2621        expected_map.add_source("x.bynk", source);
2622        expected_map.record(0, span_a);
2623        expected_map.record(text_a.len(), span_b);
2624        let expected = expected_map.to_v3(&printed.text, "x.ts");
2625
2626        assert_eq!(printed.source_map, expected);
2627    }
2628
2629    // -- #1477: nested source-map checkpoints on an opaque body blob. --
2630
2631    /// `print_stmt_and_merge` — the `TsDecl::Function` shape (`emit_free_fn`'s
2632    /// own real site): a `Raw` body statement's own `nested_map` merges into
2633    /// the caller's map at the real print-time offset, appended into a
2634    /// buffer that already has *other* content first (`base != 0`) — proving
2635    /// this reads the caller's own real buffer position, not a fresh
2636    /// throwaway one starting at 0 (the bug this function's own first draft
2637    /// had, caught before merge: returning a `String` computes the offset
2638    /// against that empty local buffer, silently wrong the moment the
2639    /// caller splices the result anywhere but the very start of its own
2640    /// `out`).
2641    #[test]
2642    fn print_stmt_and_merge_merges_a_function_bodys_own_nested_checkpoint() {
2643        let source = "fn f() { return 1 }\n";
2644        let ret_off = source.find("return 1").unwrap();
2645        let ret_span = Span::new(ret_off, ret_off + 8);
2646
2647        let body_text = "  return 1;\n";
2648        let mut nested = SourceMapBuilder::new();
2649        nested.record(0, ret_span);
2650
2651        let mut raw = TsStmt::raw(body_text, None);
2652        raw.nested_map = Some(nested);
2653
2654        let func = TsStmt::decl(
2655            TsDecl::Function {
2656                name: "f".to_string(),
2657                generics: Vec::new(),
2658                params: Vec::new(),
2659                return_type: None,
2660                body: vec![raw],
2661                is_async: false,
2662                inline: false,
2663            },
2664            None,
2665        );
2666
2667        let mut out = String::from("// header\n");
2668        let mut map = SourceMapBuilder::new();
2669        map.add_source("x.bynk", source);
2670
2671        print_stmt_and_merge(&mut out, &func, 0, &mut map, 0);
2672
2673        assert_eq!(out, "// header\nfunction f() {\n  return 1;\n}\n");
2674        let base = out.find(body_text).unwrap();
2675
2676        let mut expected = SourceMapBuilder::new();
2677        expected.add_source("x.bynk", source);
2678        expected.record(base, ret_span);
2679        assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
2680    }
2681
2682    /// `print_class_method_and_merge` — the `TsClassMethod` shape
2683    /// (`emit_provider`/`emit_agent`'s own real site, today reverse-engineered
2684    /// by `emit_class_method_and_merge_source_map`'s own string-matching).
2685    /// Same non-zero-`base` proof as the `TsDecl::Function` test above.
2686    #[test]
2687    fn print_class_method_and_merge_merges_a_methods_own_nested_checkpoint() {
2688        let source = "on call m() { commit }\n";
2689        let commit_off = source.find("commit").unwrap();
2690        let commit_span = Span::new(commit_off, commit_off + 6);
2691
2692        let body_text = "    this.commit();\n";
2693        let mut nested = SourceMapBuilder::new();
2694        nested.record(0, commit_span);
2695
2696        let mut raw = TsStmt::raw(body_text, None);
2697        raw.nested_map = Some(nested);
2698
2699        let method = TsClassMethod {
2700            name: "m".to_string(),
2701            private: false,
2702            is_async: false,
2703            params: Vec::new(),
2704            return_type: None,
2705            doc: None,
2706            body: vec![raw],
2707        };
2708
2709        let mut out = String::from("class C {\n");
2710        let mut map = SourceMapBuilder::new();
2711        map.add_source("x.bynk", source);
2712
2713        print_class_method_and_merge(&mut out, &method, 0, &mut map, 0);
2714
2715        let base = out.find(body_text).unwrap();
2716        let mut expected = SourceMapBuilder::new();
2717        expected.add_source("x.bynk", source);
2718        expected.record(base, commit_span);
2719        assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
2720    }
2721
2722    /// `print_object_entry_and_merge` — the `TsObjectEntry::Method` shape
2723    /// (`emit_service`'s own real per-handler site): identical mechanism,
2724    /// exercised through its own, third public entry point.
2725    #[test]
2726    fn print_object_entry_and_merge_merges_a_methods_own_nested_checkpoint() {
2727        let source = "on call h() { reply }\n";
2728        let reply_off = source.find("reply").unwrap();
2729        let reply_span = Span::new(reply_off, reply_off + 5);
2730
2731        let body_text = "    return reply();\n";
2732        let mut nested = SourceMapBuilder::new();
2733        nested.record(0, reply_span);
2734
2735        let mut raw = TsStmt::raw(body_text, None);
2736        raw.nested_map = Some(nested);
2737
2738        let entry = TsObjectEntry::Method {
2739            name: "h".to_string(),
2740            is_async: false,
2741            generics: Vec::new(),
2742            params: Vec::new(),
2743            return_type: None,
2744            doc: None,
2745            inline: false,
2746            body: vec![raw],
2747        };
2748
2749        let mut out = String::from("export const svc = {\n");
2750        let mut map = SourceMapBuilder::new();
2751        map.add_source("x.bynk", source);
2752
2753        print_object_entry_and_merge(&mut out, &entry, 0, &mut map, 0);
2754
2755        let base = out.find(body_text).unwrap();
2756        let mut expected = SourceMapBuilder::new();
2757        expected.add_source("x.bynk", source);
2758        expected.record(base, reply_span);
2759        assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
2760    }
2761
2762    /// `print()`'s own top-level loop (#1477): a real `TsProgram` containing
2763    /// a `Decl`-kinded top-level statement whose own body carries a
2764    /// `nested_map` merges it automatically — no `bynk-emit` change needed
2765    /// to benefit once a real construction site sets the field, since
2766    /// `print()` already threads its own module map through `render_stmt`
2767    /// for every top-level statement.
2768    #[test]
2769    fn print_merges_a_top_level_functions_own_nested_checkpoint_automatically() {
2770        let source = "fn f() { return 1 }\n";
2771        let ret_off = source.find("return 1").unwrap();
2772        let ret_span = Span::new(ret_off, ret_off + 8);
2773
2774        let body_text = "  return 1;\n";
2775        let mut nested = SourceMapBuilder::new();
2776        nested.record(0, ret_span);
2777
2778        let mut raw = TsStmt::raw(body_text, None);
2779        raw.nested_map = Some(nested);
2780
2781        let mut program = TsProgram::new();
2782        program.push(TsStmt::decl(
2783            TsDecl::Function {
2784                name: "f".to_string(),
2785                generics: Vec::new(),
2786                params: Vec::new(),
2787                return_type: None,
2788                body: vec![raw],
2789                is_async: false,
2790                inline: false,
2791            },
2792            None,
2793        ));
2794
2795        let printed = print(&program, "x.bynk", source, "x.ts");
2796        let base = printed.text.find(body_text).unwrap();
2797
2798        let mut expected = SourceMapBuilder::new();
2799        expected.add_source("x.bynk", source);
2800        expected.record(base, ret_span);
2801        assert_eq!(printed.source_map, expected.to_v3(&printed.text, "x.ts"));
2802    }
2803
2804    /// Review of #1488, finding 1: `source_id` must reach the merge, not be
2805    /// hardcoded — a multi-source module map (a test module aggregating
2806    /// several `.bynk` files, `tests_emit.rs:537`/`:1936`'s own real shape)
2807    /// tags a nested checkpoint to the *wrong* source silently if the merge
2808    /// always assumes index 0.
2809    ///
2810    /// Decisive by construction, not just an assertion on the map's own
2811    /// `sources` list (which would pass either way — every registered
2812    /// source name appears there regardless of which one a checkpoint
2813    /// actually resolves against, `to_v3`'s own doc/body confirms `sources`/
2814    /// `sourcesContent` iterate the full registered list unconditionally):
2815    /// source 0's own text is a single short line, too short to contain the
2816    /// real checkpoint's span. If the merge resolves the checkpoint against
2817    /// source 0 (the bug this test exists to catch), `to_v3`'s own
2818    /// out-of-range guard (`span.start > text.len()`) silently drops it and
2819    /// the whole map comes back `None` (its only checkpoint). Only
2820    /// resolving against the real source (id 1, correctly threaded through)
2821    /// keeps the span in range and produces a real map.
2822    #[test]
2823    fn print_stmt_and_merge_tags_a_nested_checkpoint_to_the_given_source_id() {
2824        let primary = "x\n"; // deliberately shorter than `ret_span`'s own offset
2825        let secondary = "fn f() { return 1 }\n";
2826        let ret_off = secondary.find("return 1").unwrap();
2827        let ret_span = Span::new(ret_off, ret_off + 8);
2828        assert!(
2829            ret_span.start > primary.len(),
2830            "sanity: the span must fall outside source 0's own short text"
2831        );
2832
2833        let body_text = "  return 1;\n";
2834        let mut nested = SourceMapBuilder::new();
2835        nested.record(0, ret_span);
2836
2837        let mut raw = TsStmt::raw(body_text, None);
2838        raw.nested_map = Some(nested);
2839
2840        let func = TsStmt::decl(
2841            TsDecl::Function {
2842                name: "f".to_string(),
2843                generics: Vec::new(),
2844                params: Vec::new(),
2845                return_type: None,
2846                body: vec![raw],
2847                is_async: false,
2848                inline: false,
2849            },
2850            None,
2851        );
2852
2853        let mut out = String::new();
2854        let mut map = SourceMapBuilder::new();
2855        map.add_source("primary.bynk", primary); // id 0
2856        let secondary_id = map.add_source("secondary.bynk", secondary); // id 1
2857        assert_eq!(secondary_id, 1);
2858
2859        print_stmt_and_merge(&mut out, &func, 0, &mut map, secondary_id);
2860
2861        assert!(
2862            map.to_v3(&out, "x.ts").is_some(),
2863            "the checkpoint resolved against the wrong source (id 0) and was silently dropped"
2864        );
2865    }
2866
2867    /// #1486's own sibling gap to the one above, but in [`print_multi_
2868    /// source`]'s own automatic top-level loop rather than the manual
2869    /// `print_stmt_and_merge` entry point: a real `TsProgram` built for a
2870    /// multi-source module (a test/integration module aggregating several
2871    /// `.bynk` fragment files, one per case) needs each case's own
2872    /// top-level statement merged against *its own* registered source, not
2873    /// the module's primary one (id 0) — `nested_map_source_id`'s own doc
2874    /// comment. Decisive the same way the sibling test above is: source 0's
2875    /// text is too short to contain the real span, so a wrong-source merge
2876    /// silently drops the checkpoint (`to_v3`'s own out-of-range guard) and
2877    /// the map comes back missing it.
2878    #[test]
2879    fn print_multi_source_honours_a_top_level_statements_own_nested_map_source_id() {
2880        let primary = "x\n";
2881        let secondary = "fn f() { return 1 }\n";
2882        let ret_off = secondary.find("return 1").unwrap();
2883        let ret_span = Span::new(ret_off, ret_off + 8);
2884        assert!(
2885            ret_span.start > primary.len(),
2886            "sanity: the span must fall outside source 0's own short text"
2887        );
2888
2889        let body_text = "  return 1;\n";
2890        let mut nested = SourceMapBuilder::new();
2891        nested.record(0, ret_span);
2892
2893        let mut raw = TsStmt::raw(body_text, None);
2894        raw.nested_map = Some(nested);
2895
2896        let mut func = TsStmt::decl(
2897            TsDecl::Function {
2898                name: "f".to_string(),
2899                generics: Vec::new(),
2900                params: Vec::new(),
2901                return_type: None,
2902                body: vec![raw],
2903                is_async: false,
2904                inline: false,
2905            },
2906            None,
2907        );
2908        func.nested_map_source_id = 1;
2909
2910        let mut program = TsProgram::new();
2911        program.push(func);
2912
2913        let mut map = SourceMapBuilder::new();
2914        map.add_source("primary.bynk", primary);
2915        map.add_source("secondary.bynk", secondary);
2916        let printed = print_multi_source(&program, map, "x.ts");
2917        assert!(
2918            printed.source_map.is_some(),
2919            "the checkpoint resolved against the wrong source (id 0) and was silently dropped"
2920        );
2921    }
2922
2923    /// Review of #1488, finding 2: a `nested_map` set on the statement handed
2924    /// directly to `print`/`print_stmt_and_merge` itself — not nested inside
2925    /// a `Decl` body — must still merge, not silently vanish. The first
2926    /// version of this mechanism only checked a body's own *direct
2927    /// children* (inside `render_block_stmts`), missing this exact case.
2928    #[test]
2929    fn print_stmt_and_merge_merges_a_bare_top_level_raw_statements_own_nested_checkpoint() {
2930        let source = "contract seal() {}\n";
2931        let off = source.find("contract").unwrap();
2932        let span = Span::new(off, off + 8);
2933
2934        let text = "seal();\n";
2935        let mut nested = SourceMapBuilder::new();
2936        nested.record(0, span);
2937
2938        let mut raw = TsStmt::raw(text, None);
2939        raw.nested_map = Some(nested);
2940
2941        let mut out = String::from("// header\n");
2942        let mut map = SourceMapBuilder::new();
2943        map.add_source("x.bynk", source);
2944
2945        print_stmt_and_merge(&mut out, &raw, 0, &mut map, 0);
2946
2947        let base = out.find(text).unwrap();
2948        let mut expected = SourceMapBuilder::new();
2949        expected.add_source("x.bynk", source);
2950        expected.record(base, span);
2951        assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
2952    }
2953
2954    /// Review of #1488, finding 3 (part 1): the merge's own line-rebasing
2955    /// arithmetic (`out_line = base_line + body_line`) is exercised for
2956    /// real, not just the degenerate `body_line == 0` case every other test
2957    /// here uses — a two-statement body with the checkpoint on the
2958    /// *second* line.
2959    #[test]
2960    fn print_stmt_and_merge_rebases_a_checkpoint_on_a_bodys_second_line() {
2961        let source = "fn f() { let a = 1\n  return a }\n";
2962        let ret_off = source.find("return a").unwrap();
2963        let ret_span = Span::new(ret_off, ret_off + 8);
2964
2965        let body_text = "  const a = 1;\n  return a;\n";
2966        let second_line_off = body_text.find("return a;").unwrap();
2967        let mut nested = SourceMapBuilder::new();
2968        nested.record(second_line_off, ret_span);
2969
2970        let mut raw = TsStmt::raw(body_text, None);
2971        raw.nested_map = Some(nested);
2972
2973        let func = TsStmt::decl(
2974            TsDecl::Function {
2975                name: "f".to_string(),
2976                generics: Vec::new(),
2977                params: Vec::new(),
2978                return_type: None,
2979                body: vec![raw],
2980                is_async: false,
2981                inline: false,
2982            },
2983            None,
2984        );
2985
2986        let mut out = String::from("// header\n");
2987        let mut map = SourceMapBuilder::new();
2988        map.add_source("x.bynk", source);
2989
2990        print_stmt_and_merge(&mut out, &func, 0, &mut map, 0);
2991
2992        let base_of_body = out.find(body_text).unwrap();
2993        let base_of_second_line = out.find("  return a;\n").unwrap();
2994        assert!(
2995            base_of_second_line > base_of_body,
2996            "sanity: second line is after the first"
2997        );
2998
2999        let mut expected = SourceMapBuilder::new();
3000        expected.add_source("x.bynk", source);
3001        expected.record(base_of_second_line, ret_span);
3002        assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
3003    }
3004
3005    /// Review of #1488, finding 3 (part 2): `TsDecl::Class`'s own
3006    /// constructor-then-methods sequence is the one real place `map` is
3007    /// reborrowed across more than one consumer — a class with a
3008    /// `nested_map`-bearing constructor body *and* a `nested_map`-bearing
3009    /// method body proves both reborrows land correctly, not just the
3010    /// first one. `emit_provider`/`emit_agent` (both class-shaped) are
3011    /// exactly the real callers this slice unblocks.
3012    #[test]
3013    fn a_classs_constructor_and_method_both_merge_their_own_nested_checkpoints() {
3014        let source = "on new() { init }\non call m() { commit }\n";
3015        let init_off = source.find("init").unwrap();
3016        let init_span = Span::new(init_off, init_off + 4);
3017        let commit_off = source.find("commit").unwrap();
3018        let commit_span = Span::new(commit_off, commit_off + 6);
3019
3020        let ctor_body_text = "    this.init();\n";
3021        let mut ctor_nested = SourceMapBuilder::new();
3022        ctor_nested.record(0, init_span);
3023        let mut ctor_raw = TsStmt::raw(ctor_body_text, None);
3024        ctor_raw.nested_map = Some(ctor_nested);
3025
3026        let method_body_text = "    this.commit();\n";
3027        let mut method_nested = SourceMapBuilder::new();
3028        method_nested.record(0, commit_span);
3029        let mut method_raw = TsStmt::raw(method_body_text, None);
3030        method_raw.nested_map = Some(method_nested);
3031
3032        let class = TsDecl::Class {
3033            name: "C".to_string(),
3034            fields: Vec::new(),
3035            constructor: Some(TsClassCtor {
3036                params: Vec::new(),
3037                body: vec![ctor_raw],
3038            }),
3039            methods: vec![TsClassMethod {
3040                name: "m".to_string(),
3041                private: false,
3042                is_async: false,
3043                params: Vec::new(),
3044                return_type: None,
3045                doc: None,
3046                body: vec![method_raw],
3047            }],
3048        };
3049
3050        let mut out = String::new();
3051        let mut map = SourceMapBuilder::new();
3052        map.add_source("x.bynk", source);
3053
3054        print_stmt_and_merge(&mut out, &TsStmt::decl(class, None), 0, &mut map, 0);
3055
3056        let ctor_base = out.find(ctor_body_text).unwrap();
3057        let method_base = out.find(method_body_text).unwrap();
3058
3059        let mut expected = SourceMapBuilder::new();
3060        expected.add_source("x.bynk", source);
3061        expected.record(ctor_base, init_span);
3062        expected.record(method_base, commit_span);
3063        assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
3064    }
3065
3066    // -- P7.8 (#1313): real node rendering. --
3067
3068    #[test]
3069    fn prints_a_const_statement_with_a_destructured_binding_and_a_type_cast() {
3070        let mut program = TsProgram::new();
3071        program.push(TsStmt::const_stmt(
3072            TsBindingName::ObjectPattern(vec!["events".to_string()]),
3073            None,
3074            TsExpr::As {
3075                // #1323: `Await` no longer auto-parenthesizes under `As` —
3076                // this file's own real content wraps it explicitly via
3077                // `Paren` (see `render_expr`'s own `As` arm doc).
3078                expr: Box::new(TsExpr::Paren(Box::new(TsExpr::Await(Box::new(
3079                    TsExpr::Call {
3080                        callee: Box::new(TsExpr::Member {
3081                            object: Box::new(TsExpr::Ident("request".to_string())),
3082                            property: "json".to_string(),
3083                        }),
3084                        args: vec![],
3085                    },
3086                ))))),
3087                ty: TsType::Object(vec![TsTypeMember::prop(
3088                    "events",
3089                    TsType::array(TsType::named("FanoutEvent")),
3090                )]),
3091            },
3092            None,
3093        ));
3094        let printed = print(&program, "x.bynk", "", "x.ts");
3095        assert_eq!(
3096            printed.text,
3097            "const { events } = (await request.json()) as { events: FanoutEvent[] };\n"
3098        );
3099    }
3100
3101    #[test]
3102    fn prints_if_without_braces_when_the_body_is_not_a_block() {
3103        let mut program = TsProgram::new();
3104        program.push(TsStmt::if_stmt(
3105            TsExpr::Unary {
3106                op: TsUnaryOp::Not,
3107                expr: Box::new(TsExpr::Call {
3108                    callee: Box::new(TsExpr::Member {
3109                        object: Box::new(TsExpr::Ident("Array".to_string())),
3110                        property: "isArray".to_string(),
3111                    }),
3112                    args: vec![TsExpr::Ident("subs".to_string())],
3113                }),
3114            },
3115            TsStmt::continue_stmt(None),
3116            None,
3117        ));
3118        let printed = print(&program, "x.bynk", "", "x.ts");
3119        assert_eq!(printed.text, "if (!Array.isArray(subs)) continue;\n");
3120    }
3121
3122    /// Review of #1317/#1318, finding 2: a bare `Comment` as an `if`'s
3123    /// brace-free body must not fall through to the top-level `//`-line
3124    /// form — `if (cond) // text` comments out the rest of the physical
3125    /// line, leaving `if` with no body at all (a parse error). Renders as
3126    /// a block comment instead, which cannot swallow anything after it.
3127    #[test]
3128    fn a_comment_as_an_ifs_brace_free_body_renders_as_a_block_comment() {
3129        let mut program = TsProgram::new();
3130        program.push(TsStmt::if_stmt(
3131            TsExpr::Ident("cond".to_string()),
3132            TsStmt::comment("annotation", None),
3133            None,
3134        ));
3135        let printed = print(&program, "x.bynk", "", "x.ts");
3136        assert_eq!(printed.text, "if (cond) /* annotation */\n");
3137    }
3138
3139    #[test]
3140    fn prints_a_for_of_loop_with_a_braced_body() {
3141        let mut program = TsProgram::new();
3142        program.push(TsStmt::for_of(
3143            "ev",
3144            TsExpr::Ident("events".to_string()),
3145            TsStmt::block(
3146                vec![TsStmt::return_stmt(
3147                    Some(TsExpr::Ident("ev".to_string())),
3148                    None,
3149                )],
3150                None,
3151            ),
3152            None,
3153        ));
3154        let printed = print(&program, "x.bynk", "", "x.ts");
3155        assert_eq!(
3156            printed.text,
3157            "for (const ev of events) {\n  return ev;\n}\n"
3158        );
3159    }
3160
3161    /// Arc E slice 7 (#1447): `TsStmtKind::For` — pins the exact C-style
3162    /// header shape `ListInst`/`MapInst`'s own real deserialise-side element
3163    /// loops need (`for (let i = 0; i < json.length; i++) { ... }`),
3164    /// including the postfix `++` the printer appends over `name` itself
3165    /// (see `TsStmtKind::For`'s own doc for why there is no separate
3166    /// `update` field to pass).
3167    #[test]
3168    fn prints_a_c_style_for_loop_with_a_braced_body() {
3169        let mut program = TsProgram::new();
3170        program.push(TsStmt::for_stmt(
3171            "i",
3172            TsExpr::Lit(TsLit::Num("0".to_string())),
3173            TsExpr::Binary {
3174                op: TsBinaryOp::LessThan,
3175                left: Box::new(TsExpr::Ident("i".to_string())),
3176                right: Box::new(TsExpr::Member {
3177                    object: Box::new(TsExpr::Ident("json".to_string())),
3178                    property: "length".to_string(),
3179                }),
3180            },
3181            TsStmt::block(
3182                vec![TsStmt::expr_stmt(TsExpr::Ident("item".to_string()), None)],
3183                None,
3184            ),
3185            None,
3186        ));
3187        let printed = print(&program, "x.bynk", "", "x.ts");
3188        assert_eq!(
3189            printed.text,
3190            "for (let i = 0; i < json.length; i++) {\n  item;\n}\n"
3191        );
3192    }
3193
3194    /// A `For` loop's own brace-free body falls back to the same inline
3195    /// rendering `ForOf`'s own brace-free body already uses — exercised as
3196    /// the *outer* statement, over a `Continue` body (not reachable from any
3197    /// real `bynk-emit` content today, but a real, direct shape).
3198    #[test]
3199    fn prints_a_c_style_for_loops_brace_free_body_inline() {
3200        let mut program = TsProgram::new();
3201        program.push(TsStmt::for_stmt(
3202            "i",
3203            TsExpr::Lit(TsLit::Num("0".to_string())),
3204            TsExpr::Binary {
3205                op: TsBinaryOp::LessThan,
3206                left: Box::new(TsExpr::Ident("i".to_string())),
3207                right: Box::new(TsExpr::Ident("n".to_string())),
3208            },
3209            TsStmt::continue_stmt(None),
3210            None,
3211        ));
3212        let printed = print(&program, "x.bynk", "", "x.ts");
3213        assert_eq!(printed.text, "for (let i = 0; i < n; i++) continue;\n");
3214    }
3215
3216    /// Review of #1448, finding 1: the previous test above pins a brace-free
3217    /// `For` as the *outer* statement, which renders through `render_stmt`'s
3218    /// own dedicated `For` arm, not the `render_inline_stmt` fallback this
3219    /// slice's own `| TsStmtKind::For { .. }` line actually added. That
3220    /// fallback only fires when a `For` is itself the brace-free body of an
3221    /// ENCLOSING `if`/`for...of`/`For` — exercised directly here, nesting a
3222    /// brace-free `For` as an `if`'s own brace-free branch.
3223    #[test]
3224    fn prints_a_brace_free_for_loop_nested_as_an_ifs_own_brace_free_body() {
3225        let mut program = TsProgram::new();
3226        program.push(TsStmt::if_stmt(
3227            TsExpr::Ident("cond".to_string()),
3228            TsStmt::for_stmt(
3229                "i",
3230                TsExpr::Lit(TsLit::Num("0".to_string())),
3231                TsExpr::Binary {
3232                    op: TsBinaryOp::LessThan,
3233                    left: Box::new(TsExpr::Ident("i".to_string())),
3234                    right: Box::new(TsExpr::Ident("n".to_string())),
3235                },
3236                TsStmt::continue_stmt(None),
3237                None,
3238            ),
3239            None,
3240        ));
3241        let printed = print(&program, "x.bynk", "", "x.ts");
3242        assert_eq!(
3243            printed.text,
3244            "if (cond) for (let i = 0; i < n; i++) continue;\n"
3245        );
3246    }
3247
3248    #[test]
3249    fn prints_a_try_catch_on_the_shared_closing_brace_line() {
3250        let mut program = TsProgram::new();
3251        program.push(TsStmt::try_catch(
3252            TsStmt::block(
3253                vec![TsStmt::expr_stmt(
3254                    TsExpr::Await(Box::new(TsExpr::Call {
3255                        callee: Box::new(TsExpr::Ident("deliverEvent".to_string())),
3256                        args: vec![TsExpr::Ident("binding".to_string())],
3257                    })),
3258                    None,
3259                )],
3260                None,
3261            ),
3262            Some("e"),
3263            TsStmt::block(
3264                vec![TsStmt::expr_stmt(
3265                    TsExpr::Call {
3266                        callee: Box::new(TsExpr::Member {
3267                            object: Box::new(TsExpr::Ident("console".to_string())),
3268                            property: "error".to_string(),
3269                        }),
3270                        args: vec![TsExpr::Lit(TsLit::Str("failed".to_string()))],
3271                    },
3272                    None,
3273                )],
3274                None,
3275            ),
3276            None,
3277        ));
3278        let printed = print(&program, "x.bynk", "", "x.ts");
3279        assert_eq!(
3280            printed.text,
3281            "try {\n  await deliverEvent(binding);\n} catch (e) {\n  console.error(\"failed\");\n}\n"
3282        );
3283    }
3284
3285    #[test]
3286    fn prints_an_interface_with_an_inline_nested_object_type() {
3287        let mut program = TsProgram::new();
3288        program.push(TsStmt::decl(
3289            TsDecl::Interface {
3290                name: "FanoutEvent".to_string(),
3291                type_params: Vec::new(),
3292                members: vec![
3293                    TsTypeMember::prop("type", TsType::named("string")),
3294                    TsTypeMember::prop("payload", TsType::named("unknown")),
3295                    TsTypeMember::prop(
3296                        "envelope",
3297                        TsType::Object(vec![
3298                            TsTypeMember::prop("eventId", TsType::named("string")),
3299                            TsTypeMember::prop("publisherId", TsType::named("string")),
3300                        ]),
3301                    ),
3302                ],
3303            },
3304            None,
3305        ));
3306        let printed = print(&program, "x.bynk", "", "x.ts");
3307        assert_eq!(
3308            printed.text,
3309            "interface FanoutEvent {\n  type: string;\n  payload: unknown;\n  envelope: { eventId: string; publisherId: string };\n}\n"
3310        );
3311    }
3312
3313    #[test]
3314    fn prints_two_adjacent_imports_with_no_blank_line_between_them() {
3315        let mut program = TsProgram::new();
3316        program.push(TsStmt::decl(
3317            TsDecl::Import {
3318                type_only: true,
3319                names: vec!["A".to_string()],
3320                from: "../x.js".to_string(),
3321            },
3322            None,
3323        ));
3324        program.push(TsStmt::decl(
3325            TsDecl::Import {
3326                type_only: false,
3327                names: vec!["b".to_string()],
3328                from: "../x.js".to_string(),
3329            },
3330            None,
3331        ));
3332        program.push(TsStmt::decl(
3333            TsDecl::ConstDecl {
3334                name: "c".to_string(),
3335                ty: None,
3336                init: TsExpr::Lit(TsLit::Null),
3337            },
3338            None,
3339        ));
3340        let printed = print(&program, "x.bynk", "", "x.ts");
3341        assert_eq!(
3342            printed.text,
3343            "import type { A } from \"../x.js\";\nimport { b } from \"../x.js\";\n\nconst c = null;\n"
3344        );
3345    }
3346
3347    /// #1486: `no_blank_before` suppresses the automatic blank line before
3348    /// exactly the statement that carries it — a `const`/`function` pair
3349    /// neither an import, a comment, a `ReExportAll`, nor `Verbatim`, so
3350    /// none of the existing exemptions apply; without this field the two
3351    /// would print with an unwanted blank between them (confirmed by
3352    /// temporarily reverting the field during this same change — the
3353    /// printer's own default policy really does insert one here). A third,
3354    /// ordinary statement after it confirms the field is scoped to the one
3355    /// carrying it, not sticky for the rest of the program.
3356    #[test]
3357    fn no_blank_before_suppresses_exactly_the_next_statements_own_blank() {
3358        let mut program = TsProgram::new();
3359        program.push(TsStmt::decl(
3360            TsDecl::ConstDecl {
3361                name: "__FooRegistry".to_string(),
3362                ty: None,
3363                init: TsExpr::Lit(TsLit::Null),
3364            },
3365            None,
3366        ));
3367        let mut zero_fn = TsStmt::decl(
3368            TsDecl::Function {
3369                name: "__zeroOfFooState".to_string(),
3370                generics: Vec::new(),
3371                params: Vec::new(),
3372                return_type: None,
3373                body: Vec::new(),
3374                is_async: false,
3375                inline: true,
3376            },
3377            None,
3378        );
3379        zero_fn.no_blank_before = true;
3380        program.push(zero_fn);
3381        program.push(TsStmt::decl(
3382            TsDecl::ConstDecl {
3383                name: "c".to_string(),
3384                ty: None,
3385                init: TsExpr::Lit(TsLit::Null),
3386            },
3387            None,
3388        ));
3389        let printed = print(&program, "x.bynk", "", "x.ts");
3390        assert_eq!(
3391            printed.text,
3392            "const __FooRegistry = null;\nfunction __zeroOfFooState() {  }\n\nconst c = null;\n"
3393        );
3394    }
3395
3396    /// The required "does the algebra hold against real content" test
3397    /// (P7.8's own accepted proposal, #1313): builds
3398    /// `bynk-emit/src/emitter/events_fanout.rs`'s own `EventsFanoutDO`
3399    /// class as real nodes (its field, constructor, and `fetch` method,
3400    /// with the exact control flow the real function emits — `for...of`,
3401    /// nested `if`-without-braces, `try`/`catch`) and asserts the printed
3402    /// text is byte-identical to what `emit_events_fanout_do` produces
3403    /// today for that class (transcribed directly from
3404    /// `bynk-emit/src/emitter/events_fanout.rs`'s own `write!` calls, not
3405    /// re-derived — including the constructor body, `this.env = (env ??
3406    /// {}) as Record<string, ServiceBinding>;`, via `TsStmtKind::Assign`,
3407    /// added in review of #1313: the accepted proposal's own grounding
3408    /// catalogue detailed the `fetch` method's body but missed the
3409    /// constructor's one real statement, which turned out to need a
3410    /// variant Decision B's list didn't name). The leading header comment
3411    /// and the `__eventRoutes` table's own per-project literal entries are
3412    /// deliberately not included here — a representative fragment covering
3413    /// the class is exactly what the accepted proposal's own "Done when"
3414    /// allows, and this is the shape that actually exercises every new
3415    /// node kind this slice adds.
3416    #[test]
3417    fn prints_events_fanout_dos_own_class_byte_identical_to_the_real_emitter() {
3418        let real_ctor = TsClassCtor {
3419            params: vec![
3420                TsParam {
3421                    name: "_state".to_string(),
3422                    ty: Some(TsType::named("DurableObjectState")),
3423                    optional: false,
3424                },
3425                TsParam {
3426                    name: "env".to_string(),
3427                    ty: Some(TsType::named("unknown")),
3428                    optional: true,
3429                },
3430            ],
3431            body: vec![TsStmt::assign(
3432                TsExpr::Member {
3433                    object: Box::new(TsExpr::Ident("this".to_string())),
3434                    property: "env".to_string(),
3435                },
3436                TsExpr::As {
3437                    expr: Box::new(TsExpr::Binary {
3438                        op: TsBinaryOp::NullishCoalescing,
3439                        left: Box::new(TsExpr::Ident("env".to_string())),
3440                        right: Box::new(TsExpr::object(vec![])),
3441                    }),
3442                    ty: TsType::named_with_args(
3443                        "Record",
3444                        vec![TsType::named("string"), TsType::named("ServiceBinding")],
3445                    ),
3446                },
3447                None,
3448            )],
3449        };
3450
3451        let fetch = TsClassMethod {
3452            name: "fetch".to_string(),
3453            private: false,
3454            is_async: true,
3455            params: vec![TsParam {
3456                name: "request".to_string(),
3457                ty: Some(TsType::named("Request")),
3458                optional: false,
3459            }],
3460            return_type: Some(TsType::named_with_args(
3461                "Promise",
3462                vec![TsType::named("Response")],
3463            )),
3464            doc: None,
3465            body: vec![
3466                TsStmt::const_stmt(
3467                    TsBindingName::ObjectPattern(vec!["events".to_string()]),
3468                    None,
3469                    TsExpr::As {
3470                        expr: Box::new(TsExpr::Paren(Box::new(TsExpr::Await(Box::new(
3471                            TsExpr::Call {
3472                                callee: Box::new(TsExpr::Member {
3473                                    object: Box::new(TsExpr::Ident("request".to_string())),
3474                                    property: "json".to_string(),
3475                                }),
3476                                args: vec![],
3477                            },
3478                        ))))),
3479                        ty: TsType::Object(vec![TsTypeMember::prop(
3480                            "events",
3481                            TsType::array(TsType::named("FanoutEvent")),
3482                        )]),
3483                    },
3484                    None,
3485                ),
3486                TsStmt::for_of(
3487                    "ev",
3488                    TsExpr::Ident("events".to_string()),
3489                    TsStmt::block(
3490                        vec![
3491                            TsStmt::const_stmt(
3492                                TsBindingName::Ident("subs".to_string()),
3493                                None,
3494                                TsExpr::Index {
3495                                    object: Box::new(TsExpr::Ident("__eventRoutes".to_string())),
3496                                    index: Box::new(TsExpr::Member {
3497                                        object: Box::new(TsExpr::Ident("ev".to_string())),
3498                                        property: "type".to_string(),
3499                                    }),
3500                                },
3501                                None,
3502                            ),
3503                            TsStmt::if_stmt(
3504                                TsExpr::Unary {
3505                                    op: TsUnaryOp::Not,
3506                                    expr: Box::new(TsExpr::Call {
3507                                        callee: Box::new(TsExpr::Member {
3508                                            object: Box::new(TsExpr::Ident("Array".to_string())),
3509                                            property: "isArray".to_string(),
3510                                        }),
3511                                        args: vec![TsExpr::Ident("subs".to_string())],
3512                                    }),
3513                                },
3514                                TsStmt::continue_stmt(None),
3515                                None,
3516                            ),
3517                            TsStmt::for_of(
3518                                "sub",
3519                                TsExpr::Ident("subs".to_string()),
3520                                TsStmt::block(
3521                                    vec![
3522                                        TsStmt::const_stmt(
3523                                            TsBindingName::Ident("binding".to_string()),
3524                                            None,
3525                                            TsExpr::Index {
3526                                                object: Box::new(TsExpr::Member {
3527                                                    object: Box::new(TsExpr::Ident(
3528                                                        "this".to_string(),
3529                                                    )),
3530                                                    property: "env".to_string(),
3531                                                }),
3532                                                index: Box::new(TsExpr::Member {
3533                                                    object: Box::new(TsExpr::Ident(
3534                                                        "sub".to_string(),
3535                                                    )),
3536                                                    property: "binding".to_string(),
3537                                                }),
3538                                            },
3539                                            None,
3540                                        ),
3541                                        TsStmt::if_stmt(
3542                                            TsExpr::Unary {
3543                                                op: TsUnaryOp::Not,
3544                                                expr: Box::new(TsExpr::Ident(
3545                                                    "binding".to_string(),
3546                                                )),
3547                                            },
3548                                            TsStmt::continue_stmt(None),
3549                                            None,
3550                                        ),
3551                                        TsStmt::try_catch(
3552                                            TsStmt::block(
3553                                                vec![TsStmt::expr_stmt(
3554                                                    TsExpr::Await(Box::new(TsExpr::Call {
3555                                                        callee: Box::new(TsExpr::Ident(
3556                                                            "deliverEvent".to_string(),
3557                                                        )),
3558                                                        args: vec![
3559                                                            TsExpr::Ident("binding".to_string()),
3560                                                            TsExpr::Member {
3561                                                                object: Box::new(TsExpr::Ident(
3562                                                                    "sub".to_string(),
3563                                                                )),
3564                                                                property: "service".to_string(),
3565                                                            },
3566                                                            TsExpr::Member {
3567                                                                object: Box::new(TsExpr::Ident(
3568                                                                    "ev".to_string(),
3569                                                                )),
3570                                                                property: "payload".to_string(),
3571                                                            },
3572                                                            TsExpr::Member {
3573                                                                object: Box::new(TsExpr::Ident(
3574                                                                    "ev".to_string(),
3575                                                                )),
3576                                                                property: "envelope".to_string(),
3577                                                            },
3578                                                        ],
3579                                                    })),
3580                                                    None,
3581                                                )],
3582                                                None,
3583                                            ),
3584                                            Some("e"),
3585                                            TsStmt::block(
3586                                                vec![TsStmt::expr_stmt(
3587                                                    TsExpr::Call {
3588                                                        callee: Box::new(TsExpr::Member {
3589                                                            object: Box::new(TsExpr::Ident(
3590                                                                "console".to_string(),
3591                                                            )),
3592                                                            property: "error".to_string(),
3593                                                        }),
3594                                                        args: vec![
3595                                                            TsExpr::Lit(TsLit::Str(
3596                                                                "EventsFanout delivery failed"
3597                                                                    .to_string(),
3598                                                            )),
3599                                                            TsExpr::object(vec![
3600                                                                (
3601                                                                    "event".to_string(),
3602                                                                    TsExpr::Member {
3603                                                                        object: Box::new(
3604                                                                            TsExpr::Ident(
3605                                                                                "ev".to_string(),
3606                                                                            ),
3607                                                                        ),
3608                                                                        property: "type"
3609                                                                            .to_string(),
3610                                                                    },
3611                                                                ),
3612                                                                (
3613                                                                    "service".to_string(),
3614                                                                    TsExpr::Member {
3615                                                                        object: Box::new(
3616                                                                            TsExpr::Ident(
3617                                                                                "sub".to_string(),
3618                                                                            ),
3619                                                                        ),
3620                                                                        property: "service"
3621                                                                            .to_string(),
3622                                                                    },
3623                                                                ),
3624                                                                (
3625                                                                    "error".to_string(),
3626                                                                    TsExpr::Call {
3627                                                                        callee: Box::new(
3628                                                                            TsExpr::Ident(
3629                                                                                "String"
3630                                                                                    .to_string(),
3631                                                                            ),
3632                                                                        ),
3633                                                                        args: vec![TsExpr::Ident(
3634                                                                            "e".to_string(),
3635                                                                        )],
3636                                                                    },
3637                                                                ),
3638                                                            ]),
3639                                                        ],
3640                                                    },
3641                                                    None,
3642                                                )],
3643                                                None,
3644                                            ),
3645                                            None,
3646                                        ),
3647                                    ],
3648                                    None,
3649                                ),
3650                                None,
3651                            ),
3652                        ],
3653                        None,
3654                    ),
3655                    None,
3656                ),
3657                TsStmt::return_stmt(
3658                    Some(TsExpr::New {
3659                        callee: Box::new(TsExpr::Ident("Response".to_string())),
3660                        args: vec![
3661                            TsExpr::Lit(TsLit::Null),
3662                            TsExpr::object(vec![(
3663                                "status".to_string(),
3664                                TsExpr::Lit(TsLit::Num("204".to_string())),
3665                            )]),
3666                        ],
3667                    }),
3668                    None,
3669                ),
3670            ],
3671        };
3672
3673        let class = TsDecl::Export(Box::new(TsDecl::Class {
3674            name: "EventsFanoutDO".to_string(),
3675            fields: vec![TsClassField {
3676                name: "env".to_string(),
3677                ty: TsType::named_with_args(
3678                    "Record",
3679                    vec![TsType::named("string"), TsType::named("ServiceBinding")],
3680                ),
3681                private: true,
3682            }],
3683            constructor: Some(real_ctor),
3684            methods: vec![fetch],
3685        }));
3686
3687        let mut program = TsProgram::new();
3688        program.push(TsStmt::decl(class, None));
3689        let printed = print(&program, "x.bynk", "", "x.ts");
3690
3691        // Transcribed directly from `bynk-emit/src/emitter/events_fanout.rs`'s
3692        // own `write!` calls, byte-for-byte including the constructor body.
3693        let expected_lines = [
3694            "export class EventsFanoutDO {",
3695            "  private env: Record<string, ServiceBinding>;",
3696            "  constructor(_state: DurableObjectState, env?: unknown) {",
3697            "    this.env = (env ?? {}) as Record<string, ServiceBinding>;",
3698            "  }",
3699            "",
3700            "  async fetch(request: Request): Promise<Response> {",
3701            "    const { events } = (await request.json()) as { events: FanoutEvent[] };",
3702            "    for (const ev of events) {",
3703            "      const subs = __eventRoutes[ev.type];",
3704            "      if (!Array.isArray(subs)) continue;",
3705            "      for (const sub of subs) {",
3706            "        const binding = this.env[sub.binding];",
3707            "        if (!binding) continue;",
3708            "        try {",
3709            "          await deliverEvent(binding, sub.service, ev.payload, ev.envelope);",
3710            "        } catch (e) {",
3711            "          console.error(\"EventsFanout delivery failed\", { event: ev.type, service: sub.service, error: String(e) });",
3712            "        }",
3713            "      }",
3714            "    }",
3715            "    return new Response(null, { status: 204 });",
3716            "  }",
3717            "}",
3718        ];
3719        let expected = format!("{}\n", expected_lines.join("\n"));
3720        assert_eq!(printed.text, expected);
3721    }
3722
3723    /// Coverage gap named in review of #1314, finding 4: `Let` had no test
3724    /// anywhere, including its distinct `init: None` branch (`Const` always
3725    /// has an initialiser; `Let` is the one new statement kind that can
3726    /// omit one).
3727    #[test]
3728    fn prints_a_let_statement_with_no_initialiser() {
3729        let mut program = TsProgram::new();
3730        program.push(TsStmt::let_stmt(
3731            TsBindingName::Ident("subs".to_string()),
3732            None,
3733            None,
3734            None,
3735        ));
3736        let printed = print(&program, "x.bynk", "", "x.ts");
3737        assert_eq!(printed.text, "let subs;\n");
3738    }
3739
3740    /// Coverage gap named in review of #1314, finding 4: `TsExpr::Array`
3741    /// was never constructed in any test.
3742    #[test]
3743    fn prints_an_array_literal() {
3744        let mut program = TsProgram::new();
3745        program.push(TsStmt::expr_stmt(
3746            TsExpr::array(vec![
3747                TsExpr::Lit(TsLit::Num("1".to_string())),
3748                TsExpr::Ident("x".to_string()),
3749            ]),
3750            None,
3751        ));
3752        let printed = print(&program, "x.bynk", "", "x.ts");
3753        assert_eq!(printed.text, "[1, x];\n");
3754    }
3755
3756    /// P7.9 (#1315): `TsType::Array`'s new `readonly` modifier — the shape
3757    /// every `List`/`Query` element type `bynk-emit`'s `ts_type_ref*`/
3758    /// `ts_ty` families build (`readonly T[]`), which `TsType::array`'s own
3759    /// `readonly: false` default cannot represent.
3760    #[test]
3761    fn print_type_renders_a_readonly_array() {
3762        assert_eq!(
3763            print_type(&TsType::readonly_array(TsType::named("Order"))),
3764            "readonly Order[]"
3765        );
3766        assert_eq!(
3767            print_type(&TsType::array(TsType::named("Order"))),
3768            "Order[]"
3769        );
3770    }
3771
3772    /// P7.9 (#1315): `TsType::Fn`'s zero-`params` form — the query-thunk
3773    /// wrapper shape, `bynk-emit`'s own `(() => readonly T[])`.
3774    #[test]
3775    fn print_type_renders_a_zero_param_function_type() {
3776        let ty = TsType::Fn {
3777            params: vec![],
3778            ret: Box::new(TsType::readonly_array(TsType::named("Order"))),
3779        };
3780        assert_eq!(print_type(&ty), "() => readonly Order[]");
3781    }
3782
3783    /// P7.9 (#1315): `TsType::Fn`'s real parametered form — positional
3784    /// `a0`/`a1`/… names, matching `bynk-emit`'s own pre-P7.9 convention
3785    /// exactly (TypeScript requires *some* name in function-type syntax).
3786    #[test]
3787    fn print_type_renders_a_parametered_function_type_with_positional_names() {
3788        let ty = TsType::Fn {
3789            params: vec![TsType::named("string"), TsType::named("number")],
3790            ret: Box::new(TsType::named("void")),
3791        };
3792        assert_eq!(print_type(&ty), "(a0: string, a1: number) => void");
3793    }
3794
3795    /// `TsType::Union`, added during review of #1315's own implementation —
3796    /// a real gap `bynk-emit`'s `ts_ty` needed for a resolved multi-actor
3797    /// sum (`Ty::ActorSum`), beyond the accepted proposal's own `readonly`/
3798    /// `Fn` gap list. Members print `" | "`-joined, in order.
3799    #[test]
3800    fn print_type_renders_a_union_of_named_types() {
3801        let ty = TsType::union(vec![
3802            TsType::named("string"),
3803            TsType::named("number"),
3804            TsType::Object(vec![TsTypeMember::prop(
3805                "tag",
3806                TsType::named("\"literal\""),
3807            )]),
3808        ]);
3809        assert_eq!(print_type(&ty), "string | number | { tag: \"literal\" }");
3810    }
3811
3812    /// `TsStmtKind::Comment`, added for Arc C's own first real conversion
3813    /// slice (#1317). A multi-line comment (embedded `\n`) prints one
3814    /// `// `-prefixed line per real line.
3815    #[test]
3816    fn prints_a_comment_one_prefixed_line_per_embedded_newline() {
3817        let mut program = TsProgram::new();
3818        program.push(TsStmt::comment(
3819            "Generated by bynkc — do not edit by hand.\nSecond line.",
3820            None,
3821        ));
3822        let printed = print(&program, "x.bynk", "", "x.ts");
3823        assert_eq!(
3824            printed.text,
3825            "// Generated by bynkc — do not edit by hand.\n// Second line.\n"
3826        );
3827    }
3828
3829    /// Two adjacent top-level `Comment` statements get no blank line
3830    /// between them — the same exception already established for two
3831    /// adjacent `import`s, matching `events_fanout.rs`'s own real two-line
3832    /// header banner (built as two separate `Comment` statements, not one
3833    /// with an embedded newline).
3834    #[test]
3835    fn no_blank_line_between_two_adjacent_comment_statements() {
3836        let mut program = TsProgram::new();
3837        program.push(TsStmt::comment(
3838            "Generated by bynkc — do not edit by hand.",
3839            None,
3840        ));
3841        program.push(TsStmt::comment("Second line.", None));
3842        program.push(TsStmt::decl(
3843            TsDecl::Interface {
3844                name: "A".to_string(),
3845                type_params: Vec::new(),
3846                members: vec![],
3847            },
3848            None,
3849        ));
3850        let printed = print(&program, "x.bynk", "", "x.ts");
3851        assert_eq!(
3852            printed.text,
3853            "// Generated by bynkc — do not edit by hand.\n// Second line.\n\ninterface A {\n}\n"
3854        );
3855    }
3856
3857    /// `TsExpr::multiline_object`, added for Arc C's own first real
3858    /// conversion slice (#1317): `events_fanout.rs`'s own `__eventRoutes`
3859    /// table is a top-level `const` initialiser with one entry per line,
3860    /// each with its own trailing comma (including the last), closing
3861    /// brace back at the statement's own indent — TypeScript's ordinary
3862    /// multi-line object-literal convention, which `TsExpr::object`'s
3863    /// single-line form cannot represent. Only reachable through a
3864    /// statement/declaration-level renderer that carries `depth`
3865    /// (`render_stmt_level_expr`) — this exercises it via a top-level
3866    /// `ConstDecl`, the real shape `events_fanout.rs` itself uses.
3867    #[test]
3868    fn prints_a_multiline_object_as_a_top_level_const_initialiser() {
3869        let mut program = TsProgram::new();
3870        program.push(TsStmt::decl(
3871            TsDecl::ConstDecl {
3872                name: "table".to_string(),
3873                ty: None,
3874                init: TsExpr::multiline_object(vec![
3875                    ("a".to_string(), TsExpr::Lit(TsLit::Num("1".to_string()))),
3876                    ("b".to_string(), TsExpr::Lit(TsLit::Num("2".to_string()))),
3877                ]),
3878            },
3879            None,
3880        ));
3881        let printed = print(&program, "x.bynk", "", "x.ts");
3882        assert_eq!(printed.text, "const table = {\n  a: 1,\n  b: 2,\n};\n");
3883    }
3884
3885    /// Review of #1317/#1318, finding 1: an empty `multiline_object` still
3886    /// prints the open/close-brace-on-separate-lines shape, not the tight
3887    /// `{}` a single-line empty object uses — matching the pre-conversion
3888    /// `writeln!` code's own real behaviour (its `for` loop simply not
3889    /// iterating, while the open-brace and closing `};` lines were written
3890    /// unconditionally either way). This is a real, reachable shape:
3891    /// `events_fanout.rs`'s own `__eventRoutes` table can be empty for a
3892    /// context that publishes only events nobody subscribes to.
3893    #[test]
3894    fn an_empty_multiline_object_still_prints_the_open_and_close_brace_on_separate_lines() {
3895        let mut program = TsProgram::new();
3896        program.push(TsStmt::decl(
3897            TsDecl::ConstDecl {
3898                name: "table".to_string(),
3899                ty: None,
3900                init: TsExpr::multiline_object(vec![]),
3901            },
3902            None,
3903        ));
3904        let printed = print(&program, "x.bynk", "", "x.ts");
3905        assert_eq!(printed.text, "const table = {\n};\n");
3906    }
3907
3908    /// A `multiline_object` reached through the ordinary, depth-unaware
3909    /// `render_expr` recursion (nested inside another expression, not a
3910    /// statement's own top-level initialiser) falls back to single-line
3911    /// rendering — documented on `TsExpr::Object` itself as a real,
3912    /// deliberate boundary, not silently wrong. Pinned here so a future
3913    /// change to that boundary is a visible, intentional decision.
3914    #[test]
3915    fn a_nested_multiline_object_falls_back_to_single_line() {
3916        let mut program = TsProgram::new();
3917        program.push(TsStmt::expr_stmt(
3918            TsExpr::array(vec![TsExpr::multiline_object(vec![(
3919                "a".to_string(),
3920                TsExpr::Lit(TsLit::Num("1".to_string())),
3921            )])]),
3922            None,
3923        ));
3924        let printed = print(&program, "x.bynk", "", "x.ts");
3925        assert_eq!(printed.text, "[{ a: 1 }];\n");
3926    }
3927
3928    /// Coverage gap named in review of #1314, finding 4: the blank-line-
3929    /// between-top-level-declarations policy was never tested for the
3930    /// mixed `Verbatim` + real-`Decl` case — the case that actually keeps
3931    /// every P7.6 fixture's zero-diff claim true today, since every P7.6
3932    /// construction site is all-`Verbatim`. Pins both halves of the rule's
3933    /// asymmetry: no blank line is added *after* a `Verbatim` statement
3934    /// (P7.7's own boundary — its content's own trailing spacing isn't the
3935    /// printer's decision), but a blank line *is* added after a real
3936    /// `Decl` even when `Verbatim` follows it.
3937    #[test]
3938    fn no_blank_line_after_verbatim_but_one_before_it() {
3939        let mut program = TsProgram::new();
3940        program.push(TsStmt::verbatim(
3941            VerbatimOrigin::Contracts,
3942            "const legacy = 1;\n",
3943            None,
3944        ));
3945        program.push(TsStmt::decl(
3946            TsDecl::Interface {
3947                name: "A".to_string(),
3948                type_params: Vec::new(),
3949                members: vec![],
3950            },
3951            None,
3952        ));
3953        program.push(TsStmt::verbatim(
3954            VerbatimOrigin::Secrets,
3955            "const trailing = 2;\n",
3956            None,
3957        ));
3958        let printed = print(&program, "x.bynk", "", "x.ts");
3959        assert_eq!(
3960            printed.text,
3961            "const legacy = 1;\ninterface A {\n}\n\nconst trailing = 2;\n"
3962        );
3963    }
3964
3965    /// Coverage gap named in review of #1314, finding 4: nothing exercised
3966    /// `render_block_body`'s own graceful-degradation path — its doc
3967    /// comment promises a single non-`Block` statement still renders
3968    /// sensibly as a one-statement body rather than panicking, but no test
3969    /// ever passed one. `TryCatch` is the real caller that could hit this
3970    /// (its `try_block`/`catch_block` are typed as a bare `TsStmt`, not
3971    /// required to be a `Block`).
3972    #[test]
3973    fn a_try_block_that_is_not_a_block_statement_still_prints_as_a_braced_body() {
3974        let mut program = TsProgram::new();
3975        program.push(TsStmt::try_catch(
3976            TsStmt::expr_stmt(
3977                TsExpr::Call {
3978                    callee: Box::new(TsExpr::Ident("risky".to_string())),
3979                    args: vec![],
3980                },
3981                None,
3982            ),
3983            Some("e"),
3984            TsStmt::expr_stmt(
3985                TsExpr::Call {
3986                    callee: Box::new(TsExpr::Ident("handle".to_string())),
3987                    args: vec![TsExpr::Ident("e".to_string())],
3988                },
3989                None,
3990            ),
3991            None,
3992        ));
3993        let printed = print(&program, "x.bynk", "", "x.ts");
3994        assert_eq!(
3995            printed.text,
3996            "try {\n  risky();\n} catch (e) {\n  handle(e);\n}\n"
3997        );
3998    }
3999
4000    /// Coverage gap named in review of #1314, finding 4: `render_inline_stmt`'s
4001    /// shared fallback arm (finding 3's fix — explicit variants, not a
4002    /// wildcard) was never actually exercised by a test; every existing
4003    /// brace-free `if`/`for...of` body used `Continue`/`Return`/`ExprStmt`,
4004    /// which each have their own dedicated inline arm.
4005    #[test]
4006    fn a_brace_free_if_body_that_is_not_one_of_the_dedicated_inline_kinds_still_prints() {
4007        let mut program = TsProgram::new();
4008        program.push(TsStmt::if_stmt(
4009            TsExpr::Ident("cond".to_string()),
4010            TsStmt::const_stmt(
4011                TsBindingName::Ident("x".to_string()),
4012                None,
4013                TsExpr::Lit(TsLit::Num("1".to_string())),
4014                None,
4015            ),
4016            None,
4017        ));
4018        let printed = print(&program, "x.bynk", "", "x.ts");
4019        assert_eq!(printed.text, "if (cond) const x = 1;\n");
4020    }
4021
4022    /// Review of #1314, finding 2: nothing checked operator-precedence
4023    /// parenthesisation outside `As` — a `Binary` nested as the operand of
4024    /// `Unary`/`Member`/`Await` printed with no parens at all, silently
4025    /// changing what the text parses back to. Three of the review's own
4026    /// examples, pinned directly.
4027    #[test]
4028    fn parenthesises_a_binary_operand_of_unary_member_and_await() {
4029        let bin = || TsExpr::Binary {
4030            op: TsBinaryOp::NullishCoalescing,
4031            left: Box::new(TsExpr::Ident("a".to_string())),
4032            right: Box::new(TsExpr::Ident("b".to_string())),
4033        };
4034
4035        let mut not_program = TsProgram::new();
4036        not_program.push(TsStmt::expr_stmt(
4037            TsExpr::Unary {
4038                op: TsUnaryOp::Not,
4039                expr: Box::new(bin()),
4040            },
4041            None,
4042        ));
4043        assert_eq!(
4044            print(&not_program, "x.bynk", "", "x.ts").text,
4045            "!(a ?? b);\n"
4046        );
4047
4048        let mut member_program = TsProgram::new();
4049        member_program.push(TsStmt::expr_stmt(
4050            TsExpr::Member {
4051                object: Box::new(bin()),
4052                property: "c".to_string(),
4053            },
4054            None,
4055        ));
4056        assert_eq!(
4057            print(&member_program, "x.bynk", "", "x.ts").text,
4058            "(a ?? b).c;\n"
4059        );
4060
4061        let mut await_program = TsProgram::new();
4062        await_program.push(TsStmt::expr_stmt(TsExpr::Await(Box::new(bin())), None));
4063        assert_eq!(
4064            print(&await_program, "x.bynk", "", "x.ts").text,
4065            "await (a ?? b);\n"
4066        );
4067    }
4068
4069    /// Review of #1314, finding 2's fourth example: a right-nested `Binary`
4070    /// (`a ?? (b ?? c)`, as built) must not print as the bare `a ?? b ?? c`
4071    /// that a naive left-to-right walk would produce — that text parses
4072    /// back as `(a ?? b) ?? c`, a different tree than what was built.
4073    #[test]
4074    fn parenthesises_a_nested_binary_operand_of_another_binary() {
4075        let mut program = TsProgram::new();
4076        program.push(TsStmt::expr_stmt(
4077            TsExpr::Binary {
4078                op: TsBinaryOp::NullishCoalescing,
4079                left: Box::new(TsExpr::Ident("a".to_string())),
4080                right: Box::new(TsExpr::Binary {
4081                    op: TsBinaryOp::NullishCoalescing,
4082                    left: Box::new(TsExpr::Ident("b".to_string())),
4083                    right: Box::new(TsExpr::Ident("c".to_string())),
4084                }),
4085            },
4086            None,
4087        ));
4088        let printed = print(&program, "x.bynk", "", "x.ts");
4089        assert_eq!(printed.text, "a ?? (b ?? c);\n");
4090    }
4091
4092    /// Review of #1314, finding 1: `TsLit::Str` escaped only `"`/`\`, so a
4093    /// literal containing a newline, tab, or carriage return printed raw —
4094    /// unterminated-string-literal TypeScript `tsc` can't parse. Pins the
4095    /// fix against every character `bynk_check::wire_default::
4096    /// escape_ts_literal` escapes.
4097    #[test]
4098    fn escapes_newline_tab_and_carriage_return_in_a_string_literal() {
4099        let mut program = TsProgram::new();
4100        program.push(TsStmt::expr_stmt(
4101            TsExpr::Lit(TsLit::Str("line one\nline two\ttabbed\r".to_string())),
4102            None,
4103        ));
4104        let printed = print(&program, "x.bynk", "", "x.ts");
4105        assert_eq!(printed.text, "\"line one\\nline two\\ttabbed\\r\";\n");
4106    }
4107
4108    // -- Arc C slice 3 (#1321, `workers.rs`): the five new shapes Decision A
4109    // names, plus the real gaps found beyond it (`TsDecl::Function`/
4110    // `TsDecl::TypeAlias`/`TsDecl::ImportNamespace`, the parameterless
4111    // `catch {}` form, and the two new `TsUnaryOp`/four new `TsBinaryOp`
4112    // operators). --
4113
4114    #[test]
4115    fn prints_a_shorthand_async_method_entry_in_a_multiline_object() {
4116        let mut program = TsProgram::new();
4117        program.push(TsStmt::return_stmt(
4118            Some(TsExpr::multiline_object_entries(vec![
4119                TsObjectEntry::Method {
4120                    name: "foo".to_string(),
4121                    is_async: true,
4122                    generics: Vec::new(),
4123                    params: vec![TsParam {
4124                        name: "a".to_string(),
4125                        ty: Some(TsType::named("string")),
4126                        optional: false,
4127                    }],
4128                    return_type: None,
4129                    doc: None,
4130                    inline: false,
4131                    body: vec![TsStmt::return_stmt(
4132                        Some(TsExpr::Ident("a".to_string())),
4133                        None,
4134                    )],
4135                },
4136            ])),
4137            None,
4138        ));
4139        let printed = print(&program, "x.bynk", "", "x.ts");
4140        assert_eq!(
4141            printed.text,
4142            "return {\n  async foo(a: string) {\n    return a;\n  },\n};\n"
4143        );
4144    }
4145
4146    #[test]
4147    fn prints_shorthand_and_spread_entries_inline() {
4148        let mut program = TsProgram::new();
4149        program.push(TsStmt::expr_stmt(
4150            TsExpr::object_entries(vec![
4151                TsObjectEntry::Spread(TsExpr::Ident("deps".to_string())),
4152                TsObjectEntry::Shorthand("cap1".to_string()),
4153                TsObjectEntry::Prop(
4154                    "identity".to_string(),
4155                    TsExpr::Ident("__caller".to_string()),
4156                ),
4157            ]),
4158            None,
4159        ));
4160        let printed = print(&program, "x.bynk", "", "x.ts");
4161        assert_eq!(printed.text, "{ ...deps, cap1, identity: __caller };\n");
4162    }
4163
4164    #[test]
4165    fn prints_an_expression_bodied_arrow() {
4166        let mut program = TsProgram::new();
4167        program.push(TsStmt::expr_stmt(
4168            TsExpr::Arrow {
4169                params: vec![TsParam {
4170                    name: "events".to_string(),
4171                    ty: Some(TsType::named_with_args(
4172                        "Array",
4173                        vec![TsType::named("Wire")],
4174                    )),
4175                    optional: false,
4176                }],
4177                is_async: false,
4178                generics: Vec::new(),
4179                return_type: None,
4180                body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Call {
4181                    callee: Box::new(TsExpr::Ident("dispatch".to_string())),
4182                    args: vec![TsExpr::Ident("events".to_string())],
4183                }))),
4184            },
4185            None,
4186        ));
4187        let printed = print(&program, "x.bynk", "", "x.ts");
4188        assert_eq!(printed.text, "(events: Array<Wire>) => dispatch(events);\n");
4189    }
4190
4191    /// #1327's own real gap: `emit_composition_root`'s `__eventsDispatch`
4192    /// closure is the first real `Arrow` site that's async.
4193    #[test]
4194    fn prints_an_async_arrow() {
4195        let mut program = TsProgram::new();
4196        program.push(TsStmt::expr_stmt(
4197            TsExpr::Arrow {
4198                params: vec![TsParam {
4199                    name: "events".to_string(),
4200                    ty: Some(TsType::named_with_args(
4201                        "Array",
4202                        vec![TsType::named("Wire")],
4203                    )),
4204                    optional: false,
4205                }],
4206                is_async: true,
4207                generics: Vec::new(),
4208                return_type: None,
4209                body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Ident(
4210                    "{ dispatch(events); }".to_string(),
4211                )))),
4212            },
4213            None,
4214        ));
4215        let printed = print(&program, "x.bynk", "", "x.ts");
4216        assert_eq!(
4217            printed.text,
4218            "async (events: Array<Wire>) => { dispatch(events); };\n"
4219        );
4220    }
4221
4222    /// #1435 (Arc E slice 1): [`TsArrowBody::Block`], the first real
4223    /// statement-bodied arrow — pins the exact `serialisation.rs` `Float`
4224    /// non-finite guard shape (`serialise_field_expr_wire`'s own
4225    /// `WireRef::Base { base: Float, .. }` arm) directly against a hand-built
4226    /// tree, the same way #1322's own three parenthesisation tests below pin
4227    /// their own gap. Proves `render_arrow_body`'s `Block` arm reuses
4228    /// `render_compact_stmts` (an `If` with a brace-free `Throw` then-branch,
4229    /// followed by a `Return`) correctly: both statements land on the SAME
4230    /// physical line as the arrow's own `{ ... }`, with no trailing newline
4231    /// leaking out of the block into the enclosing call's own `;`.
4232    #[test]
4233    fn prints_a_block_bodied_arrow_iife() {
4234        let mut program = TsProgram::new();
4235        let guard = TsExpr::Call {
4236            callee: Box::new(TsExpr::Arrow {
4237                params: vec![TsParam {
4238                    name: "v".to_string(),
4239                    ty: Some(TsType::named("number")),
4240                    optional: false,
4241                }],
4242                is_async: false,
4243                generics: Vec::new(),
4244                return_type: None,
4245                body: Box::new(TsArrowBody::Block(vec![
4246                    TsStmt::if_stmt(
4247                        TsExpr::Unary {
4248                            op: TsUnaryOp::Not,
4249                            expr: Box::new(TsExpr::Call {
4250                                callee: Box::new(TsExpr::Member {
4251                                    object: Box::new(TsExpr::Ident("Number".to_string())),
4252                                    property: "isFinite".to_string(),
4253                                }),
4254                                args: vec![TsExpr::Ident("v".to_string())],
4255                            }),
4256                        },
4257                        TsStmt::throw_stmt(
4258                            TsExpr::New {
4259                                callee: Box::new(TsExpr::Ident("Error".to_string())),
4260                                args: vec![TsExpr::Lit(TsLit::Str(
4261                                    "non-finite Float at boundary".to_string(),
4262                                ))],
4263                            },
4264                            None,
4265                        ),
4266                        None,
4267                    ),
4268                    TsStmt::return_stmt(
4269                        Some(TsExpr::As {
4270                            expr: Box::new(TsExpr::Ident("v".to_string())),
4271                            ty: TsType::named("JsonValue"),
4272                        }),
4273                        None,
4274                    ),
4275                ])),
4276            }),
4277            args: vec![TsExpr::Ident("value".to_string())],
4278        };
4279        program.push(TsStmt::expr_stmt(guard, None));
4280        let printed = print(&program, "x.bynk", "", "x.ts");
4281        assert_eq!(
4282            printed.text,
4283            "((v: number) => { if (!Number.isFinite(v)) throw new Error(\"non-finite Float at boundary\"); return v as JsonValue; })(value);\n"
4284        );
4285    }
4286
4287    /// Review of #1322, finding 1: `Arrow` was missing from every
4288    /// parenthesisation rule. Not reachable through any real `bynk-emit`
4289    /// content today (the one grounded `Arrow` site is an object-property
4290    /// value, rendered through the depth-unaware `render_expr` with no
4291    /// operand context) — these three tests pin the fix directly against
4292    /// hand-built trees, the same way #1314's own precedence tests did
4293    /// before any real content exercised them.
4294    #[test]
4295    fn parenthesises_an_arrow_used_as_a_call_callee() {
4296        let mut program = TsProgram::new();
4297        program.push(TsStmt::expr_stmt(
4298            TsExpr::Call {
4299                callee: Box::new(TsExpr::Arrow {
4300                    params: vec![TsParam {
4301                        name: "x".to_string(),
4302                        ty: None,
4303                        optional: false,
4304                    }],
4305                    is_async: false,
4306                    generics: Vec::new(),
4307                    return_type: None,
4308                    body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Ident("x".to_string())))),
4309                }),
4310                args: vec![TsExpr::Lit(TsLit::Num("1".to_string()))],
4311            },
4312            None,
4313        ));
4314        let printed = print(&program, "x.bynk", "", "x.ts");
4315        assert_eq!(printed.text, "((x) => x)(1);\n");
4316    }
4317
4318    #[test]
4319    fn parenthesises_an_arrow_used_as_a_binary_operand() {
4320        let mut program = TsProgram::new();
4321        program.push(TsStmt::expr_stmt(
4322            TsExpr::Binary {
4323                op: TsBinaryOp::NullishCoalescing,
4324                left: Box::new(TsExpr::Ident("a".to_string())),
4325                right: Box::new(TsExpr::Arrow {
4326                    params: vec![TsParam {
4327                        name: "x".to_string(),
4328                        ty: None,
4329                        optional: false,
4330                    }],
4331                    is_async: false,
4332                    generics: Vec::new(),
4333                    return_type: None,
4334                    body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Ident("y".to_string())))),
4335                }),
4336            },
4337            None,
4338        ));
4339        let printed = print(&program, "x.bynk", "", "x.ts");
4340        assert_eq!(printed.text, "a ?? ((x) => y);\n");
4341    }
4342
4343    #[test]
4344    fn parenthesises_an_arrow_used_as_an_as_operand() {
4345        let mut program = TsProgram::new();
4346        program.push(TsStmt::expr_stmt(
4347            TsExpr::As {
4348                expr: Box::new(TsExpr::Arrow {
4349                    params: vec![TsParam {
4350                        name: "x".to_string(),
4351                        ty: None,
4352                        optional: false,
4353                    }],
4354                    is_async: false,
4355                    generics: Vec::new(),
4356                    return_type: None,
4357                    body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Ident("y".to_string())))),
4358                }),
4359                ty: TsType::named("Handler"),
4360            },
4361            None,
4362        ));
4363        let printed = print(&program, "x.bynk", "", "x.ts");
4364        assert_eq!(printed.text, "((x) => y) as Handler;\n");
4365    }
4366
4367    #[test]
4368    fn prints_optional_member_and_optional_index_chained() {
4369        let mut program = TsProgram::new();
4370        program.push(TsStmt::expr_stmt(
4371            TsExpr::OptionalIndex {
4372                object: Box::new(TsExpr::OptionalMember {
4373                    object: Box::new(TsExpr::Ident("x".to_string())),
4374                    property: "process".to_string(),
4375                }),
4376                index: Box::new(TsExpr::Lit(TsLit::Str("secret".to_string()))),
4377            },
4378            None,
4379        ));
4380        let printed = print(&program, "x.bynk", "", "x.ts");
4381        assert_eq!(printed.text, "x?.process?.[\"secret\"];\n");
4382    }
4383
4384    #[test]
4385    fn print_type_renders_an_optional_object_field() {
4386        let ty = TsType::Object(vec![
4387            TsTypeMember::optional_prop("env", TsType::named("unknown")),
4388            TsTypeMember::prop("name", TsType::named("string")),
4389        ]);
4390        assert_eq!(print_type(&ty), "{ env?: unknown; name: string }");
4391    }
4392
4393    #[test]
4394    fn prints_a_top_level_function_declaration() {
4395        let mut program = TsProgram::new();
4396        program.push(TsStmt::decl(
4397            TsDecl::Export(Box::new(TsDecl::Function {
4398                name: "compose".to_string(),
4399                generics: Vec::new(),
4400                params: vec![TsParam {
4401                    name: "env".to_string(),
4402                    ty: Some(TsType::named("Env")),
4403                    optional: false,
4404                }],
4405                return_type: None,
4406                body: vec![TsStmt::return_stmt(Some(TsExpr::Lit(TsLit::Null)), None)],
4407                is_async: false,
4408                inline: false,
4409            })),
4410            None,
4411        ));
4412        let printed = print(&program, "x.bynk", "", "x.ts");
4413        assert_eq!(
4414            printed.text,
4415            "export function compose(env: Env) {\n  return null;\n}\n"
4416        );
4417    }
4418
4419    /// #1351's own real gap: `TsDecl::Function` had no `generics` field —
4420    /// `emit_free_fn`'s own v0.20a erased generics (`export function
4421    /// foo<A, B>(...)`) needed one. Review of #1352, finding 3: every other
4422    /// existing `TsDecl::Function` test only threads `generics: Vec::new()`
4423    /// through; this pins the non-empty case directly, matching #1339's own
4424    /// precedent of a dedicated test per new field.
4425    #[test]
4426    fn prints_a_generic_top_level_function_declaration() {
4427        let mut program = TsProgram::new();
4428        program.push(TsStmt::decl(
4429            TsDecl::Export(Box::new(TsDecl::Function {
4430                name: "identity".to_string(),
4431                generics: vec!["A".to_string(), "B".to_string()],
4432                params: vec![TsParam {
4433                    name: "x".to_string(),
4434                    ty: Some(TsType::named("A")),
4435                    optional: false,
4436                }],
4437                return_type: Some(TsType::named("A")),
4438                body: vec![TsStmt::return_stmt(
4439                    Some(TsExpr::Ident("x".to_string())),
4440                    None,
4441                )],
4442                is_async: false,
4443                inline: false,
4444            })),
4445            None,
4446        ));
4447        let printed = print(&program, "x.bynk", "", "x.ts");
4448        assert_eq!(
4449            printed.text,
4450            "export function identity<A, B>(x: A): A {\n  return x;\n}\n"
4451        );
4452    }
4453
4454    #[test]
4455    fn prints_a_top_level_type_alias() {
4456        let mut program = TsProgram::new();
4457        program.push(TsStmt::decl(
4458            TsDecl::TypeAlias {
4459                name: "Foo".to_string(),
4460                type_params: Vec::new(),
4461                ty: TsType::named("{ get(id: any): any }"),
4462            },
4463            None,
4464        ));
4465        let printed = print(&program, "x.bynk", "", "x.ts");
4466        assert_eq!(printed.text, "type Foo = { get(id: any): any };\n");
4467    }
4468
4469    #[test]
4470    fn prints_a_namespace_import_adjacent_to_a_named_import_with_no_blank_line() {
4471        let mut program = TsProgram::new();
4472        program.push(TsStmt::decl(
4473            TsDecl::Import {
4474                type_only: false,
4475                names: vec!["a".to_string()],
4476                from: "./x.js".to_string(),
4477            },
4478            None,
4479        ));
4480        program.push(TsStmt::decl(
4481            TsDecl::ImportNamespace {
4482                type_only: false,
4483                alias: "handlers".to_string(),
4484                from: "./handlers.js".to_string(),
4485            },
4486            None,
4487        ));
4488        let printed = print(&program, "x.bynk", "", "x.ts");
4489        assert_eq!(
4490            printed.text,
4491            "import { a } from \"./x.js\";\nimport * as handlers from \"./handlers.js\";\n"
4492        );
4493    }
4494
4495    /// Arc C, step (10) (#1392): `ImportNamespace.type_only`, a parallel gap
4496    /// by omission — `TsDecl::Import` already had this field, but nothing
4497    /// before `emit_cross_context_namespace_imports`'s own conversion built
4498    /// a type-only namespace import (`import type * as ns from "...";`, a
4499    /// Workers-mode consumed-context import reaching the callee's types
4500    /// only, #661).
4501    #[test]
4502    fn prints_a_type_only_namespace_import() {
4503        let mut program = TsProgram::new();
4504        program.push(TsStmt::decl(
4505            TsDecl::ImportNamespace {
4506                type_only: true,
4507                alias: "commerce_payment".to_string(),
4508                from: "../commerce-payment/handlers.js".to_string(),
4509            },
4510            None,
4511        ));
4512        let printed = print(&program, "x.bynk", "", "x.ts");
4513        assert_eq!(
4514            printed.text,
4515            "import type * as commerce_payment from \"../commerce-payment/handlers.js\";\n"
4516        );
4517    }
4518
4519    /// Arc C, slice 37 (#1409, `tests_emit.rs`'s own slice G): `emit_integration_
4520    /// module`'s own per-participant `import worker_{ns} from "../workers/{dir}/
4521    /// index.js";` (the participant's Worker entry module's default export) —
4522    /// the first real default import anywhere in `bynk-emit`'s own converted
4523    /// content.
4524    #[test]
4525    fn prints_a_default_import() {
4526        let mut program = TsProgram::new();
4527        program.push(TsStmt::decl(
4528            TsDecl::ImportDefault {
4529                alias: "worker_shop_api".to_string(),
4530                from: "../workers/shop-api/index.js".to_string(),
4531            },
4532            None,
4533        ));
4534        let printed = print(&program, "x.bynk", "", "x.ts");
4535        assert_eq!(
4536            printed.text,
4537            "import worker_shop_api from \"../workers/shop-api/index.js\";\n"
4538        );
4539    }
4540
4541    /// #1321's own real gap beyond the accepted proposal's five: ES2019's
4542    /// optional catch binding (`emit_http_sum_wrapper`'s own `} catch {`,
4543    /// no `(e)` at all).
4544    #[test]
4545    fn prints_a_parameterless_catch() {
4546        let mut program = TsProgram::new();
4547        program.push(TsStmt::try_catch(
4548            TsStmt::block(vec![], None),
4549            None::<String>,
4550            TsStmt::block(vec![], None),
4551            None,
4552        ));
4553        let printed = print(&program, "x.bynk", "", "x.ts");
4554        assert_eq!(printed.text, "try {\n} catch {\n}\n");
4555    }
4556
4557    #[test]
4558    fn prints_typeof_and_the_new_comparison_and_logical_operators() {
4559        let mut program = TsProgram::new();
4560        program.push(TsStmt::expr_stmt(
4561            TsExpr::Unary {
4562                op: TsUnaryOp::Typeof,
4563                expr: Box::new(TsExpr::Ident("x".to_string())),
4564            },
4565            None,
4566        ));
4567        program.push(TsStmt::expr_stmt(
4568            TsExpr::Binary {
4569                op: TsBinaryOp::StrictEq,
4570                left: Box::new(TsExpr::Ident("a".to_string())),
4571                right: Box::new(TsExpr::Ident("b".to_string())),
4572            },
4573            None,
4574        ));
4575        program.push(TsStmt::expr_stmt(
4576            TsExpr::Binary {
4577                op: TsBinaryOp::StrictNotEq,
4578                left: Box::new(TsExpr::Ident("a".to_string())),
4579                right: Box::new(TsExpr::Ident("b".to_string())),
4580            },
4581            None,
4582        ));
4583        program.push(TsStmt::expr_stmt(
4584            TsExpr::Binary {
4585                op: TsBinaryOp::And,
4586                left: Box::new(TsExpr::Ident("a".to_string())),
4587                right: Box::new(TsExpr::Ident("b".to_string())),
4588            },
4589            None,
4590        ));
4591        program.push(TsStmt::expr_stmt(
4592            TsExpr::Binary {
4593                op: TsBinaryOp::Or,
4594                left: Box::new(TsExpr::Ident("a".to_string())),
4595                right: Box::new(TsExpr::Ident("b".to_string())),
4596            },
4597            None,
4598        ));
4599        let printed = print(&program, "x.bynk", "", "x.ts");
4600        assert_eq!(
4601            printed.text,
4602            "typeof x;\n\na === b;\n\na !== b;\n\na && b;\n\na || b;\n"
4603        );
4604    }
4605
4606    /// #1321's own real gap: with only `??` in the algebra, a nested
4607    /// `Binary` operand of another `Binary` was always parenthesized,
4608    /// regardless of operator (see `parenthesises_a_nested_binary_operand_
4609    /// of_another_binary`, unchanged). `workers.rs`'s own real content
4610    /// nests a *strictly higher precedence* comparison inside `||`/`&&`
4611    /// (`__authz === null || !__authz.startsWith(...)`) and the byte-golden
4612    /// fixtures have no parens around it — this pins that the printer
4613    /// omits them precisely in that case, not more broadly.
4614    #[test]
4615    fn a_strictly_higher_precedence_comparison_needs_no_parens_inside_or_or_and() {
4616        let mut program = TsProgram::new();
4617        program.push(TsStmt::expr_stmt(
4618            TsExpr::Binary {
4619                op: TsBinaryOp::Or,
4620                left: Box::new(TsExpr::Binary {
4621                    op: TsBinaryOp::StrictEq,
4622                    left: Box::new(TsExpr::Ident("a".to_string())),
4623                    right: Box::new(TsExpr::Lit(TsLit::Null)),
4624                }),
4625                right: Box::new(TsExpr::Unary {
4626                    op: TsUnaryOp::Not,
4627                    expr: Box::new(TsExpr::Ident("b".to_string())),
4628                }),
4629            },
4630            None,
4631        ));
4632        let printed = print(&program, "x.bynk", "", "x.ts");
4633        assert_eq!(printed.text, "a === null || !b;\n");
4634    }
4635
4636    // -- #1323 (workers_entry.rs): Switch, ExportDefault, Conditional,
4637    // TsType::Object readonly/method members, and the smaller mechanical
4638    // gaps found during implementation (ReExport, Blank, TsLit::Bool,
4639    // TsExpr::Paren, same-operator-chain flattening). --
4640
4641    /// #1323's own largest gap: `switch (<discriminant>) { <cases> }`. A
4642    /// non-`default` `case` is `{ }`-blocked; `default` is not — pinned
4643    /// against `workers_entry.rs`'s own real internal-dispatch shape.
4644    #[test]
4645    fn prints_a_switch_with_a_braced_case_and_an_unbraced_default() {
4646        let mut program = TsProgram::new();
4647        program.push(TsStmt::switch_stmt(
4648            TsExpr::Ident("servicePath".to_string()),
4649            vec![
4650                TsSwitchCase {
4651                    test: Some(TsExpr::Lit(TsLit::Str("orders".to_string()))),
4652                    body: vec![TsStmt::return_stmt(None, None)],
4653                    default_braced: false,
4654                    case_braced: true,
4655                },
4656                TsSwitchCase {
4657                    test: None,
4658                    body: vec![TsStmt::expr_stmt(
4659                        TsExpr::Call {
4660                            callee: Box::new(TsExpr::Member {
4661                                object: Box::new(TsExpr::Ident("console".to_string())),
4662                                property: "log".to_string(),
4663                            }),
4664                            args: vec![],
4665                        },
4666                        None,
4667                    )],
4668                    default_braced: false,
4669                    case_braced: true,
4670                },
4671            ],
4672            None,
4673        ));
4674        let printed = print(&program, "x.bynk", "", "x.ts");
4675        assert_eq!(
4676            printed.text,
4677            "switch (servicePath) {\n  case \"orders\": {\n    return;\n  }\n  default:\n    console.log();\n}\n"
4678        );
4679    }
4680
4681    /// Arc C, slice 33 (`tests_emit.rs` slice C, #1401): `TsSwitchCase.
4682    /// default_braced` — `emit_stub_class`'s own `ReturnsEach` dispatch
4683    /// braces its `default` case, unlike `workers_entry.rs`'s own unbraced
4684    /// one pinned just above.
4685    #[test]
4686    fn prints_a_switch_with_a_braced_default() {
4687        let mut program = TsProgram::new();
4688        program.push(TsStmt::switch_stmt(
4689            TsExpr::Ident("__k".to_string()),
4690            vec![TsSwitchCase {
4691                test: None,
4692                body: vec![TsStmt::expr_stmt(TsExpr::Ident("x".to_string()), None)],
4693                default_braced: true,
4694                case_braced: true,
4695            }],
4696            None,
4697        ));
4698        let printed = print(&program, "x.bynk", "", "x.ts");
4699        assert_eq!(
4700            printed.text,
4701            "switch (__k) {\n  default: {\n    x;\n  }\n}\n"
4702        );
4703    }
4704
4705    /// Arc E slice 6 (#1445): `TsSwitchCase.case_braced` — the mirror-image
4706    /// gap `default_braced` left open. `emit_sum_codec`'s own payload-free
4707    /// variant case is unbraced right beside a braced payload-carrying
4708    /// sibling in the *same* switch, pinned against `212_json_codec`'s own
4709    /// mixed `Status` fixture (`case "Pending": return { kind: "Pending"
4710    /// };` then `case "Shipped": { ... }`).
4711    #[test]
4712    fn prints_a_switch_with_an_unbraced_case_beside_a_braced_one() {
4713        let mut program = TsProgram::new();
4714        program.push(TsStmt::switch_stmt(
4715            TsExpr::Ident("kind".to_string()),
4716            vec![
4717                TsSwitchCase {
4718                    test: Some(TsExpr::Lit(TsLit::Str("Pending".to_string()))),
4719                    body: vec![TsStmt::return_stmt(
4720                        Some(TsExpr::Ident("a".to_string())),
4721                        None,
4722                    )],
4723                    default_braced: false,
4724                    case_braced: false,
4725                },
4726                TsSwitchCase {
4727                    test: Some(TsExpr::Lit(TsLit::Str("Shipped".to_string()))),
4728                    body: vec![TsStmt::return_stmt(
4729                        Some(TsExpr::Ident("b".to_string())),
4730                        None,
4731                    )],
4732                    default_braced: false,
4733                    case_braced: true,
4734                },
4735            ],
4736            None,
4737        ));
4738        let printed = print(&program, "x.bynk", "", "x.ts");
4739        assert_eq!(
4740            printed.text,
4741            "switch (kind) {\n  case \"Pending\":\n    return a;\n  case \"Shipped\": {\n    return b;\n  }\n}\n"
4742        );
4743    }
4744
4745    /// #1323's own second gap: `export default <expr>;` — a default export
4746    /// of a bare expression, not a declaration. Uses the same depth-aware
4747    /// multiline-object handling `const`/`let`/`return` already get.
4748    #[test]
4749    fn prints_an_export_default_object_literal_multiline() {
4750        let mut program = TsProgram::new();
4751        program.push(TsStmt::decl(
4752            TsDecl::ExportDefault(TsExpr::multiline_object(vec![(
4753                "fetch".to_string(),
4754                TsExpr::Ident("handler".to_string()),
4755            )])),
4756            None,
4757        ));
4758        let printed = print(&program, "x.bynk", "", "x.ts");
4759        assert_eq!(printed.text, "export default {\n  fetch: handler,\n};\n");
4760    }
4761
4762    /// #1355's own real gap: a `multiline: true` `TsExpr::Object` nested as
4763    /// one `Prop`'s own value inside ANOTHER multiline object —
4764    /// `emit_messages_bundle`'s own real doubly-nested `{ locale: { code:
4765    /// expr, ... }, ... }` table. Before this slice, `render_multiline_
4766    /// object_entry`'s own `Prop` arm rendered its value through the plain,
4767    /// depth-unaware `render_expr`, which ignores `multiline` entirely —
4768    /// this would have silently collapsed the inner object to one line.
4769    #[test]
4770    fn prints_a_nested_multiline_object_as_a_props_own_value() {
4771        let mut program = TsProgram::new();
4772        program.push(TsStmt::const_stmt(
4773            TsBindingName::Ident("byLocale".to_string()),
4774            None,
4775            TsExpr::multiline_object_entries(vec![TsObjectEntry::Prop(
4776                "\"en\"".to_string(),
4777                TsExpr::multiline_object_entries(vec![
4778                    TsObjectEntry::Prop(
4779                        "\"greeting\"".to_string(),
4780                        TsExpr::Ident("renderGreeting".to_string()),
4781                    ),
4782                    TsObjectEntry::Prop(
4783                        "\"farewell\"".to_string(),
4784                        TsExpr::Ident("renderFarewell".to_string()),
4785                    ),
4786                ]),
4787            )]),
4788            None,
4789        ));
4790        let printed = print(&program, "x.bynk", "", "x.ts");
4791        assert_eq!(
4792            printed.text,
4793            "const byLocale = {\n  \"en\": {\n    \"greeting\": renderGreeting,\n    \"farewell\": renderFarewell,\n  },\n};\n"
4794        );
4795    }
4796
4797    /// Review of #1356, finding 2: `render_multiline_object_entry`'s own
4798    /// `Prop` arm routes through `render_stmt_level_expr` for BOTH
4799    /// `TsExpr::Object`/`Array` (see `TsExpr::Array`'s own doc, updated by
4800    /// #1355) — the object case is pinned above, this is the array sibling,
4801    /// making that doc claim self-verifying rather than merely asserted.
4802    #[test]
4803    fn prints_a_nested_multiline_array_as_a_props_own_value() {
4804        let mut program = TsProgram::new();
4805        program.push(TsStmt::const_stmt(
4806            TsBindingName::Ident("byGroup".to_string()),
4807            None,
4808            TsExpr::multiline_object_entries(vec![TsObjectEntry::Prop(
4809                "\"a\"".to_string(),
4810                TsExpr::multiline_array(vec![
4811                    TsExpr::Ident("one".to_string()),
4812                    TsExpr::Ident("two".to_string()),
4813                ]),
4814            )]),
4815            None,
4816        ));
4817        let printed = print(&program, "x.bynk", "", "x.ts");
4818        assert_eq!(
4819            printed.text,
4820            "const byGroup = {\n  \"a\": [\n    one,\n    two,\n  ],\n};\n"
4821        );
4822    }
4823
4824    /// #1323's own third gap: `test ? consequent : alternate`.
4825    #[test]
4826    fn prints_a_conditional_expression() {
4827        let mut program = TsProgram::new();
4828        program.push(TsStmt::expr_stmt(
4829            TsExpr::Conditional {
4830                test: Box::new(TsExpr::Binary {
4831                    op: TsBinaryOp::StrictEq,
4832                    left: Box::new(TsExpr::Ident("method".to_string())),
4833                    right: Box::new(TsExpr::Lit(TsLit::Str("HEAD".to_string()))),
4834                }),
4835                consequent: Box::new(TsExpr::Lit(TsLit::Num("1".to_string()))),
4836                alternate: Box::new(TsExpr::Lit(TsLit::Num("2".to_string()))),
4837            },
4838            None,
4839        ));
4840        let printed = print(&program, "x.bynk", "", "x.ts");
4841        assert_eq!(printed.text, "method === \"HEAD\" ? 1 : 2;\n");
4842    }
4843
4844    /// Review-anticipated gap (matching #1322 finding 1's own class): a
4845    /// `Conditional` used as a binary operand, an `as`-operand, or a call
4846    /// callee all need parens — closed proactively in this same slice
4847    /// rather than left for a review round to re-find.
4848    #[test]
4849    fn parenthesises_a_conditional_used_as_a_binary_operand_and_an_as_operand() {
4850        let cond = || TsExpr::Conditional {
4851            test: Box::new(TsExpr::Ident("a".to_string())),
4852            consequent: Box::new(TsExpr::Ident("b".to_string())),
4853            alternate: Box::new(TsExpr::Ident("c".to_string())),
4854        };
4855        let mut program = TsProgram::new();
4856        program.push(TsStmt::expr_stmt(
4857            TsExpr::Binary {
4858                op: TsBinaryOp::NullishCoalescing,
4859                left: Box::new(TsExpr::Ident("x".to_string())),
4860                right: Box::new(cond()),
4861            },
4862            None,
4863        ));
4864        program.push(TsStmt::expr_stmt(
4865            TsExpr::As {
4866                expr: Box::new(cond()),
4867                ty: TsType::named("string"),
4868            },
4869            None,
4870        ));
4871        let printed = print(&program, "x.bynk", "", "x.ts");
4872        assert_eq!(
4873            printed.text,
4874            "x ?? (a ? b : c);\n\n(a ? b : c) as string;\n"
4875        );
4876    }
4877
4878    /// #1323's own real gap: a `Method` member of a type-position object
4879    /// literal (no body), alongside a `readonly` property.
4880    #[test]
4881    fn print_type_renders_a_readonly_field_and_a_method_signature() {
4882        let ty = TsType::Object(vec![
4883            TsTypeMember::readonly_prop("cron", TsType::named("string")),
4884            TsTypeMember::method("ack", vec![], TsType::named("void")),
4885            TsTypeMember::method(
4886                "waitUntil",
4887                vec![TsParam {
4888                    name: "promise".to_string(),
4889                    ty: Some(TsType::named_with_args(
4890                        "Promise",
4891                        vec![TsType::named("unknown")],
4892                    )),
4893                    optional: false,
4894                }],
4895                TsType::named("void"),
4896            ),
4897        ]);
4898        assert_eq!(
4899            print_type(&ty),
4900            "{ readonly cron: string; ack(): void; waitUntil(promise: Promise<unknown>): void }"
4901        );
4902    }
4903
4904    /// #1323's own real gap: `TsObjectEntry::Method` gained a
4905    /// `return_type` field (mirroring `TsClassMethod`'s own existing one) —
4906    /// `workers_entry.rs`'s own `export default { fetch, scheduled?,
4907    /// queue? }` entries all carry an explicit return-type annotation,
4908    /// unlike `workers.rs`'s own `compose`-returned wrapper methods
4909    /// (`#1321`), none of which do.
4910    #[test]
4911    fn prints_a_multiline_object_methods_own_return_type_annotation() {
4912        let mut program = TsProgram::new();
4913        program.push(TsStmt::decl(
4914            TsDecl::ExportDefault(TsExpr::multiline_object_entries(vec![
4915                TsObjectEntry::Method {
4916                    name: "fetch".to_string(),
4917                    is_async: true,
4918                    generics: Vec::new(),
4919                    params: vec![],
4920                    return_type: Some(TsType::named_with_args(
4921                        "Promise",
4922                        vec![TsType::named("Response")],
4923                    )),
4924                    doc: None,
4925                    inline: false,
4926                    body: vec![],
4927                },
4928            ])),
4929            None,
4930        ));
4931        let printed = print(&program, "x.bynk", "", "x.ts");
4932        assert_eq!(
4933            printed.text,
4934            "export default {\n  async fetch(): Promise<Response> {\n  },\n};\n"
4935        );
4936    }
4937
4938    /// #1323's own real correction: `Await` no longer auto-parenthesizes
4939    /// under `As` — `workers_entry.rs`'s own real `await request.json() as
4940    /// JsonValue` has no parens (`as` binds looser than `await`, so none
4941    /// are grammatically needed; P7.8's original "always parenthesize"
4942    /// reasoning conflated one file's own real text with a grammar
4943    /// requirement).
4944    #[test]
4945    fn an_await_used_as_an_as_operand_no_longer_needs_parens() {
4946        let mut program = TsProgram::new();
4947        program.push(TsStmt::const_stmt(
4948            TsBindingName::Ident("args".to_string()),
4949            None,
4950            TsExpr::As {
4951                expr: Box::new(TsExpr::Await(Box::new(TsExpr::Call {
4952                    callee: Box::new(TsExpr::Member {
4953                        object: Box::new(TsExpr::Ident("request".to_string())),
4954                        property: "json".to_string(),
4955                    }),
4956                    args: vec![],
4957                }))),
4958                ty: TsType::named("JsonValue"),
4959            },
4960            None,
4961        ));
4962        let printed = print(&program, "x.bynk", "", "x.ts");
4963        assert_eq!(
4964            printed.text,
4965            "const args = await request.json() as JsonValue;\n"
4966        );
4967    }
4968
4969    /// #1353's own real gap: `bynk_ts::TsStmtKind::Throw`, added for
4970    /// `emit_contract_guarded_body`'s own real `throw __e;` sites. Review of
4971    /// #1354, finding 1: every real call site today reaches this only
4972    /// through an `InlineBlock` (`render_stmt(out, stmt, 0)`), so the
4973    /// top-level, non-inline `render_stmt` arm's own `indent(depth)` at a
4974    /// nonzero depth was untested — pinned directly here, mirroring
4975    /// `Return`'s own shape exactly.
4976    #[test]
4977    fn prints_a_throw_statement_at_a_nested_depth() {
4978        let stmt = TsStmt::throw_stmt(TsExpr::Ident("__e".to_string()), None);
4979        assert_eq!(print_stmt(&stmt, 2), "    throw __e;\n");
4980    }
4981
4982    /// #1353's own real gap, byte-for-byte: the whole contract-guard shape
4983    /// `emit_contract_guarded_body` builds (`if (!(pred)) { const __e = ...;
4984    /// __e.name = ...; throw __e; }`) — an `If` over an `InlineBlock` of a
4985    /// `Const`/`Assign`/`Throw` trio — asserted as one exact string. Review
4986    /// of #1354, finding 1: no existing test (fixture, `contract_behaviour.
4987    /// rs`, `source_map.rs`) asserts this shape's own bytes; they only
4988    /// `contains`-check a runtime message or a generated line number, none
4989    /// would catch a missing space, a reordered statement, or a lost
4990    /// indent.
4991    #[test]
4992    fn prints_a_contract_guard_if_stmt() {
4993        let cond = TsExpr::Unary {
4994            op: TsUnaryOp::Not,
4995            expr: Box::new(TsExpr::Paren(Box::new(TsExpr::Ident("n > 0".to_string())))),
4996        };
4997        let new_error = TsExpr::New {
4998            callee: Box::new(TsExpr::Ident("Error".to_string())),
4999            args: vec![TsExpr::template_lit(
5000                vec![
5001                    "contract violated: precondition \\`positive\\` of scale (n=${n})".to_string(),
5002                ],
5003                Vec::new(),
5004            )],
5005        };
5006        let const_e = TsStmt::const_stmt(
5007            TsBindingName::Ident("__e".to_string()),
5008            None,
5009            new_error,
5010            None,
5011        );
5012        let assign_name = TsStmt::assign(
5013            TsExpr::Member {
5014                object: Box::new(TsExpr::Ident("__e".to_string())),
5015                property: "name".to_string(),
5016            },
5017            TsExpr::Lit(TsLit::Str("BynkContractError".to_string())),
5018            None,
5019        );
5020        let throw_e = TsStmt::throw_stmt(TsExpr::Ident("__e".to_string()), None);
5021        let then_branch = TsStmt::inline_block(vec![const_e, assign_name, throw_e], None);
5022        let if_stmt = TsStmt::if_stmt(cond, then_branch, None);
5023        assert_eq!(
5024            print_stmt(&if_stmt, 1),
5025            "  if (!(n > 0)) { const __e = new Error(`contract violated: precondition \\`positive\\` of scale (n=${n})`); __e.name = \"BynkContractError\"; throw __e; }\n"
5026        );
5027    }
5028
5029    /// #1323's own real gap: `if`'s own `else` branch, printed on a fresh
5030    /// line at the `if`'s own indent, then following the same block-vs-
5031    /// inline rule the `if` itself does — `workers_entry.rs`'s own real
5032    /// queue-consumer ack/retry dispatch.
5033    #[test]
5034    fn prints_an_if_else_with_an_inline_then_and_an_inline_block_else() {
5035        let mut program = TsProgram::new();
5036        program.push(TsStmt::if_else_stmt(
5037            TsExpr::Binary {
5038                op: TsBinaryOp::StrictEq,
5039                left: Box::new(TsExpr::Member {
5040                    object: Box::new(TsExpr::Ident("result".to_string())),
5041                    property: "tag".to_string(),
5042                }),
5043                right: Box::new(TsExpr::Lit(TsLit::Str("Ack".to_string()))),
5044            },
5045            TsStmt::expr_stmt(
5046                TsExpr::Call {
5047                    callee: Box::new(TsExpr::Member {
5048                        object: Box::new(TsExpr::Ident("msg".to_string())),
5049                        property: "ack".to_string(),
5050                    }),
5051                    args: vec![],
5052                },
5053                None,
5054            ),
5055            TsStmt::inline_block(
5056                vec![TsStmt::expr_stmt(
5057                    TsExpr::Call {
5058                        callee: Box::new(TsExpr::Member {
5059                            object: Box::new(TsExpr::Ident("msg".to_string())),
5060                            property: "retry".to_string(),
5061                        }),
5062                        args: vec![],
5063                    },
5064                    None,
5065                )],
5066                None,
5067            ),
5068            None,
5069        ));
5070        let printed = print(&program, "x.bynk", "", "x.ts");
5071        assert_eq!(
5072            printed.text,
5073            "if (result.tag === \"Ack\") msg.ack();\nelse { msg.retry(); }\n"
5074        );
5075    }
5076
5077    /// #1323's own real gap: a `TryCatch`'s own `catch` body can be an
5078    /// `InlineBlock` too — the braces stay on their own lines (this shape's
5079    /// usual convention), but the content packs onto one compact line.
5080    /// `workers_entry.rs`'s own real queue-consumer catch clause.
5081    #[test]
5082    fn a_try_catchs_own_catch_block_can_be_an_inline_block() {
5083        let mut program = TsProgram::new();
5084        program.push(TsStmt::try_catch(
5085            TsStmt::block(
5086                vec![TsStmt::expr_stmt(TsExpr::Ident("work".to_string()), None)],
5087                None,
5088            ),
5089            Some("e"),
5090            TsStmt::inline_block(
5091                vec![
5092                    TsStmt::expr_stmt(
5093                        TsExpr::Call {
5094                            callee: Box::new(TsExpr::Member {
5095                                object: Box::new(TsExpr::Ident("console".to_string())),
5096                                property: "error".to_string(),
5097                            }),
5098                            args: vec![TsExpr::Ident("e".to_string())],
5099                        },
5100                        None,
5101                    ),
5102                    TsStmt::expr_stmt(
5103                        TsExpr::Call {
5104                            callee: Box::new(TsExpr::Member {
5105                                object: Box::new(TsExpr::Ident("msg".to_string())),
5106                                property: "retry".to_string(),
5107                            }),
5108                            args: vec![],
5109                        },
5110                        None,
5111                    ),
5112                ],
5113                None,
5114            ),
5115            None,
5116        ));
5117        let printed = print(&program, "x.bynk", "", "x.ts");
5118        assert_eq!(
5119            printed.text,
5120            "try {\n  work;\n} catch (e) {\n  console.error(e); msg.retry();\n}\n"
5121        );
5122    }
5123
5124    /// #1323's own real gap: `TsBinaryOp::GreaterThan` — the request-body
5125    /// ceiling guard's own `Number(__contentLength) > <cap>`, the one real
5126    /// site needing a relational (not equality) comparison.
5127    #[test]
5128    fn prints_a_greater_than_comparison() {
5129        let mut program = TsProgram::new();
5130        program.push(TsStmt::expr_stmt(
5131            TsExpr::Binary {
5132                op: TsBinaryOp::GreaterThan,
5133                left: Box::new(TsExpr::Call {
5134                    callee: Box::new(TsExpr::Ident("Number".to_string())),
5135                    args: vec![TsExpr::Ident("__contentLength".to_string())],
5136                }),
5137                right: Box::new(TsExpr::Lit(TsLit::Num("100".to_string()))),
5138            },
5139            None,
5140        ));
5141        let printed = print(&program, "x.bynk", "", "x.ts");
5142        assert_eq!(printed.text, "Number(__contentLength) > 100;\n");
5143    }
5144
5145    /// #1323's own real gap: `TsTypeMember::Index` — `workers_entry.rs`'s
5146    /// own `args as { [k: string]: JsonValue }`, textually distinct from
5147    /// the semantically-equivalent `Record<string, JsonValue>`.
5148    #[test]
5149    fn print_type_renders_an_index_signature() {
5150        let ty = TsType::Object(vec![TsTypeMember::index(
5151            "k",
5152            TsType::named("string"),
5153            TsType::named("JsonValue"),
5154        )]);
5155        assert_eq!(print_type(&ty), "{ [k: string]: JsonValue }");
5156    }
5157
5158    /// #1323's own real gap, found implementing `workers_entry.rs`:
5159    /// `export { a, b } from "spec";` — a re-export, distinct from both
5160    /// `Import` and `Export`. Not classified alongside imports for the
5161    /// "no blank line between adjacent imports" exception.
5162    #[test]
5163    fn prints_a_re_export_with_the_ordinary_blank_line_rule() {
5164        let mut program = TsProgram::new();
5165        program.push(TsStmt::decl(
5166            TsDecl::ImportNamespace {
5167                type_only: false,
5168                alias: "handlers".to_string(),
5169                from: "./handlers.js".to_string(),
5170            },
5171            None,
5172        ));
5173        program.push(TsStmt::decl(
5174            TsDecl::ReExport {
5175                names: vec!["Orders".to_string()],
5176                from: "./handlers.js".to_string(),
5177            },
5178            None,
5179        ));
5180        let printed = print(&program, "x.bynk", "", "x.ts");
5181        assert_eq!(
5182            printed.text,
5183            "import * as handlers from \"./handlers.js\";\n\nexport { Orders } from \"./handlers.js\";\n"
5184        );
5185    }
5186
5187    /// #1323's own real gap, found implementing `workers_entry.rs`: a bare
5188    /// blank line usable at any nesting depth, distinct from `print()`'s
5189    /// own top-level-only policy.
5190    #[test]
5191    fn a_blank_statement_prints_one_empty_line_inside_a_nested_block() {
5192        let mut program = TsProgram::new();
5193        program.push(TsStmt::if_stmt(
5194            TsExpr::Ident("cond".to_string()),
5195            TsStmt::block(
5196                vec![
5197                    TsStmt::expr_stmt(TsExpr::Ident("a".to_string()), None),
5198                    TsStmt::blank(None),
5199                    TsStmt::expr_stmt(TsExpr::Ident("b".to_string()), None),
5200                ],
5201                None,
5202            ),
5203            None,
5204        ));
5205        let printed = print(&program, "x.bynk", "", "x.ts");
5206        assert_eq!(printed.text, "if (cond) {\n  a;\n\n  b;\n}\n");
5207    }
5208
5209    /// #1323's own real gap, found implementing `workers_entry.rs`:
5210    /// `CorsPolicy.credentials`/`SecurityPolicy.nosniff` are real booleans.
5211    #[test]
5212    fn prints_a_boolean_literal() {
5213        let mut program = TsProgram::new();
5214        program.push(TsStmt::expr_stmt(
5215            TsExpr::object(vec![
5216                ("credentials".to_string(), TsExpr::Lit(TsLit::Bool(true))),
5217                ("nosniff".to_string(), TsExpr::Lit(TsLit::Bool(false))),
5218            ]),
5219            None,
5220        ));
5221        let printed = print(&program, "x.bynk", "", "x.ts");
5222        assert_eq!(printed.text, "{ credentials: true, nosniff: false };\n");
5223    }
5224
5225    /// #1323's own real gap: an explicit `Paren` always prints its own
5226    /// literal parens, even when the wrapped expression's own precedence
5227    /// would not otherwise need any — `workers_entry.rs`'s CORS-preflight
5228    /// guard wraps its path-match condition in `(...)` unconditionally,
5229    /// even for a single equality check with nothing lower-precedence than
5230    /// the outer `&&` inside it (which the ordinary precedence-derived
5231    /// rules correctly do NOT parenthesize on their own).
5232    #[test]
5233    fn an_explicit_paren_always_prints_regardless_of_the_inner_expressions_own_precedence() {
5234        let mut program = TsProgram::new();
5235        program.push(TsStmt::expr_stmt(
5236            TsExpr::Binary {
5237                op: TsBinaryOp::And,
5238                left: Box::new(TsExpr::Ident("method".to_string())),
5239                right: Box::new(TsExpr::Paren(Box::new(TsExpr::Binary {
5240                    op: TsBinaryOp::StrictEq,
5241                    left: Box::new(TsExpr::Ident("path".to_string())),
5242                    right: Box::new(TsExpr::Lit(TsLit::Str("/foo".to_string()))),
5243                }))),
5244            },
5245            None,
5246        ));
5247        let printed = print(&program, "x.bynk", "", "x.ts");
5248        assert_eq!(printed.text, "method && (path === \"/foo\");\n");
5249    }
5250
5251    /// #1323's own real gap: a 3-term `||` chain of the *same* operator
5252    /// prints flat, matching `emit_call_handler_dispatch`'s own real
5253    /// `typeof args !== "object" || args === null || Array.isArray(args)`
5254    /// — the pre-#1323 "always parenthesize equal precedence" rule would
5255    /// have wrongly added parens around the first two terms.
5256    #[test]
5257    fn a_three_term_or_chain_of_the_same_operator_prints_flat() {
5258        let mut program = TsProgram::new();
5259        program.push(TsStmt::expr_stmt(
5260            TsExpr::Binary {
5261                op: TsBinaryOp::Or,
5262                left: Box::new(TsExpr::Binary {
5263                    op: TsBinaryOp::Or,
5264                    left: Box::new(TsExpr::Binary {
5265                        op: TsBinaryOp::StrictNotEq,
5266                        left: Box::new(TsExpr::Unary {
5267                            op: TsUnaryOp::Typeof,
5268                            expr: Box::new(TsExpr::Ident("args".to_string())),
5269                        }),
5270                        right: Box::new(TsExpr::Lit(TsLit::Str("object".to_string()))),
5271                    }),
5272                    right: Box::new(TsExpr::Binary {
5273                        op: TsBinaryOp::StrictEq,
5274                        left: Box::new(TsExpr::Ident("args".to_string())),
5275                        right: Box::new(TsExpr::Lit(TsLit::Null)),
5276                    }),
5277                }),
5278                right: Box::new(TsExpr::Call {
5279                    callee: Box::new(TsExpr::Member {
5280                        object: Box::new(TsExpr::Ident("Array".to_string())),
5281                        property: "isArray".to_string(),
5282                    }),
5283                    args: vec![TsExpr::Ident("args".to_string())],
5284                }),
5285            },
5286            None,
5287        ));
5288        let printed = print(&program, "x.bynk", "", "x.ts");
5289        assert_eq!(
5290            printed.text,
5291            "typeof args !== \"object\" || args === null || Array.isArray(args);\n"
5292        );
5293    }
5294
5295    /// Arc C, step (11) (#1388): `TsBinaryOp::Add`'s own real, dominant real
5296    /// site — a message template's own literal/placeholder segments,
5297    /// left-folded via `.join(" + ")` — needs the identical flat-chain
5298    /// treatment `||`/`&&` already established, joining the same
5299    /// associativity exemption in `render_binary_operand`, not a
5300    /// bespoke third rule.
5301    #[test]
5302    fn a_three_term_add_chain_of_the_same_operator_prints_flat() {
5303        let mut program = TsProgram::new();
5304        program.push(TsStmt::expr_stmt(
5305            TsExpr::Binary {
5306                op: TsBinaryOp::Add,
5307                left: Box::new(TsExpr::Binary {
5308                    op: TsBinaryOp::Add,
5309                    left: Box::new(TsExpr::Lit(TsLit::Str("a".to_string()))),
5310                    right: Box::new(TsExpr::Lit(TsLit::Str("b".to_string()))),
5311                }),
5312                right: Box::new(TsExpr::Lit(TsLit::Str("c".to_string()))),
5313            },
5314            None,
5315        ));
5316        let printed = print(&program, "x.bynk", "", "x.ts");
5317        assert_eq!(printed.text, "\"a\" + \"b\" + \"c\";\n");
5318    }
5319
5320    /// Review of #1389, finding 1: unlike `||`/`&&` (semantically
5321    /// associative — flattening either side is safe), `+` is only
5322    /// grammatically left-associative — `1 + (2 + "3")` and `(1 + 2) + "3"`
5323    /// disagree once a number joins the chain. A right-nested `Add` must
5324    /// keep its parens, matching every other non-`Or`/`And` same-operator
5325    /// chain's own treatment (`a_right_nested_strict_eq_chain_keeps_its_
5326    /// parens`/`a_right_nested_greater_than_chain_keeps_its_parens`).
5327    #[test]
5328    fn a_right_nested_add_chain_keeps_its_parens() {
5329        let mut program = TsProgram::new();
5330        program.push(TsStmt::expr_stmt(
5331            TsExpr::Binary {
5332                op: TsBinaryOp::Add,
5333                left: Box::new(TsExpr::Lit(TsLit::Num("1".to_string()))),
5334                right: Box::new(TsExpr::Binary {
5335                    op: TsBinaryOp::Add,
5336                    left: Box::new(TsExpr::Lit(TsLit::Num("2".to_string()))),
5337                    right: Box::new(TsExpr::Lit(TsLit::Str("3".to_string()))),
5338                }),
5339            },
5340            None,
5341        ));
5342        let printed = print(&program, "x.bynk", "", "x.ts");
5343        assert_eq!(printed.text, "1 + (2 + \"3\");\n");
5344    }
5345
5346    /// `Add`'s own new `binary_precedence` entry (higher than
5347    /// `GreaterThan`, matching real JS/TS) — a nested `Add` under a
5348    /// `GreaterThan` needs no parens (`+` binds tighter), the reverse
5349    /// direction does.
5350    #[test]
5351    fn add_binds_tighter_than_greater_than_on_both_sides() {
5352        let mut program = TsProgram::new();
5353        program.push(TsStmt::expr_stmt(
5354            TsExpr::Binary {
5355                op: TsBinaryOp::GreaterThan,
5356                left: Box::new(TsExpr::Binary {
5357                    op: TsBinaryOp::Add,
5358                    left: Box::new(TsExpr::Ident("a".to_string())),
5359                    right: Box::new(TsExpr::Ident("b".to_string())),
5360                }),
5361                right: Box::new(TsExpr::Ident("c".to_string())),
5362            },
5363            None,
5364        ));
5365        let printed = print(&program, "x.bynk", "", "x.ts");
5366        assert_eq!(printed.text, "a + b > c;\n");
5367    }
5368
5369    /// The reverse nesting: a `GreaterThan` operand under an outer `Add`
5370    /// needs its parens (`+` binds tighter, so `Add`'s own precedence check
5371    /// against a lower-precedence `GreaterThan` operand must still fire).
5372    #[test]
5373    fn greater_than_nested_under_add_keeps_its_parens() {
5374        let mut program = TsProgram::new();
5375        program.push(TsStmt::expr_stmt(
5376            TsExpr::Binary {
5377                op: TsBinaryOp::Add,
5378                left: Box::new(TsExpr::Ident("a".to_string())),
5379                right: Box::new(TsExpr::Binary {
5380                    op: TsBinaryOp::GreaterThan,
5381                    left: Box::new(TsExpr::Ident("b".to_string())),
5382                    right: Box::new(TsExpr::Ident("c".to_string())),
5383                }),
5384            },
5385            None,
5386        ));
5387        let printed = print(&program, "x.bynk", "", "x.ts");
5388        assert_eq!(printed.text, "a + (b > c);\n");
5389    }
5390
5391    /// Review of #1324, finding 1: the same-operator flattening #1323 added
5392    /// for `||`/`&&` was wrongly applied to every operator, including
5393    /// non-associative ones — a right-nested `StrictEq` inside `StrictEq`
5394    /// must keep its parens (`a === b === c` parses as `(a === b) === c`,
5395    /// not the tree's real `a === (b === c)`).
5396    #[test]
5397    fn a_right_nested_strict_eq_chain_keeps_its_parens() {
5398        let mut program = TsProgram::new();
5399        program.push(TsStmt::expr_stmt(
5400            TsExpr::Binary {
5401                op: TsBinaryOp::StrictEq,
5402                left: Box::new(TsExpr::Ident("a".to_string())),
5403                right: Box::new(TsExpr::Binary {
5404                    op: TsBinaryOp::StrictEq,
5405                    left: Box::new(TsExpr::Ident("b".to_string())),
5406                    right: Box::new(TsExpr::Ident("c".to_string())),
5407                }),
5408            },
5409            None,
5410        ));
5411        let printed = print(&program, "x.bynk", "", "x.ts");
5412        assert_eq!(printed.text, "a === (b === c);\n");
5413    }
5414
5415    /// Same finding, for the new `GreaterThan` operator: `a > b > c` parses
5416    /// as `(a > b) > c` (a boolean compared against `c`), not the tree's
5417    /// real `a > (b > c)` — a right-nested chain must keep its parens.
5418    #[test]
5419    fn a_right_nested_greater_than_chain_keeps_its_parens() {
5420        let mut program = TsProgram::new();
5421        program.push(TsStmt::expr_stmt(
5422            TsExpr::Binary {
5423                op: TsBinaryOp::GreaterThan,
5424                left: Box::new(TsExpr::Ident("a".to_string())),
5425                right: Box::new(TsExpr::Binary {
5426                    op: TsBinaryOp::GreaterThan,
5427                    left: Box::new(TsExpr::Ident("b".to_string())),
5428                    right: Box::new(TsExpr::Ident("c".to_string())),
5429                }),
5430            },
5431            None,
5432        ));
5433        let printed = print(&program, "x.bynk", "", "x.ts");
5434        assert_eq!(printed.text, "a > (b > c);\n");
5435    }
5436
5437    /// Review of #1324, finding 2: a `Blank` used as an `if`'s brace-free
5438    /// body used to render as a bare newline, silently letting the very
5439    /// next statement in the enclosing block become the `if`'s own body —
5440    /// now it prints an honest empty statement (`;`) that can't swallow
5441    /// anything after it.
5442    #[test]
5443    fn a_blank_if_body_prints_an_empty_statement_not_a_swallowing_newline() {
5444        // Nested inside a `Block`, not two top-level program statements —
5445        // `print`'s own blank-line-between-statements policy only applies
5446        // between top-level statements (see its own doc comment), so a
5447        // top-level pair would mask the swallowing bug this test exists to
5448        // catch: the real hazard is two statements sharing one physical
5449        // line with nothing separating them, exactly what `render_stmt`'s
5450        // `Block` arm produces with no blank-line insertion of its own.
5451        let mut program = TsProgram::new();
5452        program.push(TsStmt::block(
5453            vec![
5454                TsStmt::if_stmt(TsExpr::Ident("cond".to_string()), TsStmt::blank(None), None),
5455                TsStmt::expr_stmt(
5456                    TsExpr::Call {
5457                        callee: Box::new(TsExpr::Ident("nextStatement".to_string())),
5458                        args: vec![],
5459                    },
5460                    None,
5461                ),
5462            ],
5463            None,
5464        ));
5465        let printed = print(&program, "x.bynk", "", "x.ts");
5466        assert_eq!(printed.text, "{\n  if (cond) ;\n  nextStatement();\n}\n");
5467    }
5468
5469    /// A left-nested same-operator chain also keeps its parens for a
5470    /// non-associative operator (unlike the `||`/`&&` case above) —
5471    /// `(a === b) === c` reads the same as `a === b === c` would parse, so
5472    /// this is really about `render_binary_operand` not silently dropping
5473    /// parens it should keep, not about a left/right asymmetry.
5474    #[test]
5475    fn a_left_nested_strict_eq_chain_keeps_its_parens() {
5476        let mut program = TsProgram::new();
5477        program.push(TsStmt::expr_stmt(
5478            TsExpr::Binary {
5479                op: TsBinaryOp::StrictEq,
5480                left: Box::new(TsExpr::Binary {
5481                    op: TsBinaryOp::StrictEq,
5482                    left: Box::new(TsExpr::Ident("a".to_string())),
5483                    right: Box::new(TsExpr::Ident("b".to_string())),
5484                }),
5485                right: Box::new(TsExpr::Ident("c".to_string())),
5486            },
5487            None,
5488        ));
5489        let printed = print(&program, "x.bynk", "", "x.ts");
5490        assert_eq!(printed.text, "(a === b) === c;\n");
5491    }
5492
5493    // -- #1325: emit_test_main's own new shapes ------------------------------
5494
5495    #[test]
5496    fn prints_a_template_literal_with_substitutions() {
5497        let mut program = TsProgram::new();
5498        program.push(TsStmt::expr_stmt(
5499            TsExpr::template_lit(
5500                vec![
5501                    String::new(),
5502                    " passed, ".to_string(),
5503                    " failed.".to_string(),
5504                ],
5505                vec![
5506                    TsExpr::Ident("passed".to_string()),
5507                    TsExpr::Ident("failed".to_string()),
5508                ],
5509            ),
5510            None,
5511        ));
5512        let printed = print(&program, "x.bynk", "", "x.ts");
5513        assert_eq!(printed.text, "`${passed} passed, ${failed} failed.`;\n");
5514    }
5515
5516    /// A template literal's own static parts print with no escaping applied
5517    /// by the printer — see `TsExpr::TemplateLit`'s own doc for why: a
5518    /// generic escaper would double the literal backslash of an
5519    /// already-pre-formed JS unicode escape (`✓`), corrupting it. This
5520    /// pins the real, grounded shape directly: the six ASCII characters
5521    /// `✓` pass through unchanged, not doubled into `\\u2713`.
5522    #[test]
5523    fn a_template_literal_part_carrying_a_preformed_unicode_escape_is_not_reescaped() {
5524        let mut program = TsProgram::new();
5525        program.push(TsStmt::expr_stmt(
5526            TsExpr::template_lit(
5527                vec!["  \\u2713 ".to_string(), String::new()],
5528                vec![TsExpr::Ident("r".to_string())],
5529            ),
5530            None,
5531        ));
5532        let printed = print(&program, "x.bynk", "", "x.ts");
5533        assert_eq!(printed.text, "`  \\u2713 ${r}`;\n");
5534    }
5535
5536    #[test]
5537    fn a_raw_literal_prints_exactly_as_given_with_no_escaping() {
5538        let mut program = TsProgram::new();
5539        program.push(TsStmt::expr_stmt(
5540            TsExpr::Lit(TsLit::Raw("\"integration \\u00b7 \"".to_string())),
5541            None,
5542        ));
5543        let printed = print(&program, "x.bynk", "", "x.ts");
5544        assert_eq!(printed.text, "\"integration \\u00b7 \";\n");
5545    }
5546
5547    #[test]
5548    fn prints_a_multiline_array_literal() {
5549        let mut program = TsProgram::new();
5550        program.push(TsStmt::const_stmt(
5551            TsBindingName::Ident("modules".to_string()),
5552            None,
5553            TsExpr::multiline_array(vec![
5554                TsExpr::object(vec![(
5555                    "name".to_string(),
5556                    TsExpr::Lit(TsLit::Str("a".to_string())),
5557                )]),
5558                TsExpr::object(vec![(
5559                    "name".to_string(),
5560                    TsExpr::Lit(TsLit::Str("b".to_string())),
5561                )]),
5562            ]),
5563            None,
5564        ));
5565        let printed = print(&program, "x.bynk", "", "x.ts");
5566        assert_eq!(
5567            printed.text,
5568            "const modules = [\n  { name: \"a\" },\n  { name: \"b\" },\n];\n"
5569        );
5570    }
5571
5572    /// Same reachability boundary as `TsExpr::Object`'s own `multiline`
5573    /// field (see its doc) — a `multiline: true` array nested inside another
5574    /// expression falls back to single-line via the depth-unaware
5575    /// `render_expr` recursion.
5576    #[test]
5577    fn a_nested_multiline_array_falls_back_to_single_line() {
5578        let mut program = TsProgram::new();
5579        program.push(TsStmt::expr_stmt(
5580            TsExpr::array(vec![TsExpr::multiline_array(vec![TsExpr::Lit(
5581                TsLit::Num("1".to_string()),
5582            )])]),
5583            None,
5584        ));
5585        let printed = print(&program, "x.bynk", "", "x.ts");
5586        assert_eq!(printed.text, "[[1]];\n");
5587    }
5588
5589    #[test]
5590    fn prints_a_declare_const_ambient_binding() {
5591        let mut program = TsProgram::new();
5592        program.push(TsStmt::decl(
5593            TsDecl::DeclareConst {
5594                name: "process".to_string(),
5595                ty: TsType::Object(vec![TsTypeMember::prop("env", TsType::named("unknown"))]),
5596            },
5597            None,
5598        ));
5599        let printed = print(&program, "x.bynk", "", "x.ts");
5600        assert_eq!(printed.text, "declare const process: { env: unknown };\n");
5601    }
5602
5603    #[test]
5604    fn prints_an_async_top_level_function() {
5605        let mut program = TsProgram::new();
5606        program.push(TsStmt::decl(
5607            TsDecl::Function {
5608                name: "main".to_string(),
5609                generics: Vec::new(),
5610                params: vec![],
5611                return_type: None,
5612                body: vec![TsStmt::return_stmt(None, None)],
5613                is_async: true,
5614                inline: false,
5615            },
5616            None,
5617        ));
5618        let printed = print(&program, "x.bynk", "", "x.ts");
5619        assert_eq!(printed.text, "async function main() {\n  return;\n}\n");
5620    }
5621
5622    /// #1369 (Arc C, slice 20): `TsDecl::Function.inline` — the zero-factory
5623    /// shape (`function name(): Ret { return <expr>; }` all on one line)
5624    /// `emit_agent` needs, mirroring `TsObjectEntry::Method.inline`'s own
5625    /// single-line-vs-multi-line precedent (#1337) at a different node kind.
5626    #[test]
5627    fn prints_an_inline_top_level_function() {
5628        let mut program = TsProgram::new();
5629        program.push(TsStmt::decl(
5630            TsDecl::Function {
5631                name: "zero".to_string(),
5632                generics: Vec::new(),
5633                params: vec![],
5634                return_type: Some(TsType::named("Foo")),
5635                body: vec![TsStmt::return_stmt(
5636                    Some(TsExpr::object(vec![(
5637                        "n".to_string(),
5638                        TsExpr::Lit(TsLit::Num("0".to_string())),
5639                    )])),
5640                    None,
5641                )],
5642                is_async: false,
5643                inline: true,
5644            },
5645            None,
5646        ));
5647        let printed = print(&program, "x.bynk", "", "x.ts");
5648        assert_eq!(printed.text, "function zero(): Foo { return { n: 0 }; }\n");
5649    }
5650
5651    #[test]
5652    fn prints_a_postfix_increment_statement() {
5653        let mut program = TsProgram::new();
5654        program.push(TsStmt::increment(TsExpr::Ident("passed".to_string()), None));
5655        let printed = print(&program, "x.bynk", "", "x.ts");
5656        assert_eq!(printed.text, "passed++;\n");
5657    }
5658
5659    /// An `Increment` inside an `InlineBlock` renders correctly through
5660    /// `render_inline_stmt`'s fallback — `emit_test_main`'s own real
5661    /// `{ passed++; console.log(...); }` shape.
5662    #[test]
5663    fn a_postfix_increment_renders_correctly_inside_an_inline_block() {
5664        let mut program = TsProgram::new();
5665        program.push(TsStmt::if_stmt(
5666            TsExpr::Ident("cond".to_string()),
5667            TsStmt::inline_block(
5668                vec![
5669                    TsStmt::increment(TsExpr::Ident("passed".to_string()), None),
5670                    TsStmt::expr_stmt(TsExpr::Ident("next".to_string()), None),
5671                ],
5672                None,
5673            ),
5674            None,
5675        ));
5676        let printed = print(&program, "x.bynk", "", "x.ts");
5677        assert_eq!(printed.text, "if (cond) { passed++; next; }\n");
5678    }
5679
5680    /// `same_line_else` with a `Block` then-branch — `} else {` on one
5681    /// physical line, distinct from the fresh-line default pinned elsewhere
5682    /// in this module.
5683    #[test]
5684    fn same_line_else_puts_the_else_keyword_after_the_closing_brace() {
5685        let mut program = TsProgram::new();
5686        program.push(TsStmt::if_else_same_line_stmt(
5687            TsExpr::Ident("cond".to_string()),
5688            TsStmt::block(
5689                vec![TsStmt::expr_stmt(TsExpr::Ident("a".to_string()), None)],
5690                None,
5691            ),
5692            TsStmt::block(
5693                vec![TsStmt::expr_stmt(TsExpr::Ident("b".to_string()), None)],
5694                None,
5695            ),
5696            None,
5697        ));
5698        let printed = print(&program, "x.bynk", "", "x.ts");
5699        assert_eq!(printed.text, "if (cond) {\n  a;\n} else {\n  b;\n}\n");
5700    }
5701
5702    /// `same_line_else` with `InlineBlock` branches on both sides —
5703    /// `emit_test_main`'s own real `if (r.pass) { ... } else { ... }` shape,
5704    /// entirely on one generated line.
5705    #[test]
5706    fn same_line_else_with_inline_block_branches_stays_on_one_line() {
5707        let mut program = TsProgram::new();
5708        program.push(TsStmt::if_else_same_line_stmt(
5709            TsExpr::Ident("cond".to_string()),
5710            TsStmt::inline_block(
5711                vec![TsStmt::expr_stmt(TsExpr::Ident("a".to_string()), None)],
5712                None,
5713            ),
5714            TsStmt::inline_block(
5715                vec![TsStmt::expr_stmt(TsExpr::Ident("b".to_string()), None)],
5716                None,
5717            ),
5718            None,
5719        ));
5720        let printed = print(&program, "x.bynk", "", "x.ts");
5721        assert_eq!(printed.text, "if (cond) { a; } else { b; }\n");
5722    }
5723
5724    /// `same_line_else` with a brace-free `then_branch` falls back to the
5725    /// ordinary fresh-line rendering — nothing real needs `<inline-stmt>
5726    /// else {`, and the printer doesn't claim to support it (see
5727    /// `TsStmtKind::If`'s own doc).
5728    #[test]
5729    fn same_line_else_with_a_brace_free_then_branch_falls_back_to_fresh_line() {
5730        let mut program = TsProgram::new();
5731        program.push(TsStmt::if_else_same_line_stmt(
5732            TsExpr::Ident("cond".to_string()),
5733            TsStmt::continue_stmt(None),
5734            TsStmt::block(
5735                vec![TsStmt::expr_stmt(TsExpr::Ident("b".to_string()), None)],
5736                None,
5737            ),
5738            None,
5739        ));
5740        let printed = print(&program, "x.bynk", "", "x.ts");
5741        assert_eq!(printed.text, "if (cond) continue;\nelse {\n  b;\n}\n");
5742    }
5743
5744    /// `TsDecl::ReExportAll` prints `export * from "spec";` — no braces, no
5745    /// name list, matching `emit_commons_barrel`'s own real per-file line.
5746    #[test]
5747    fn re_export_all_prints_a_wildcard_re_export() {
5748        let mut program = TsProgram::new();
5749        program.push(TsStmt::decl(
5750            TsDecl::ReExportAll {
5751                from: "./thing/make.js".to_string(),
5752            },
5753            None,
5754        ));
5755        let printed = print(&program, "x.bynk", "", "x.ts");
5756        assert_eq!(printed.text, "export * from \"./thing/make.js\";\n");
5757    }
5758
5759    /// Review of #1329's own grounding: `emit_commons_barrel`'s real barrel
5760    /// module is one header `Comment` immediately followed by one `export
5761    /// *` line per constituent source file, every one of those lines
5762    /// adjacent with no blank line anywhere — the exact byte shape pinned
5763    /// here, matching `251_multi_file_commons_test`'s own real
5764    /// `expected/thing.ts`.
5765    #[test]
5766    fn a_header_comment_and_consecutive_re_export_alls_have_no_blank_lines() {
5767        let mut program = TsProgram::new();
5768        program.push(TsStmt::comment(
5769            "Generated by bynkc — do not edit by hand.",
5770            None,
5771        ));
5772        program.push(TsStmt::decl(
5773            TsDecl::ReExportAll {
5774                from: "./thing/make.js".to_string(),
5775            },
5776            None,
5777        ));
5778        program.push(TsStmt::decl(
5779            TsDecl::ReExportAll {
5780                from: "./thing/widget.js".to_string(),
5781            },
5782            None,
5783        ));
5784        let printed = print(&program, "x.bynk", "", "x.ts");
5785        assert_eq!(
5786            printed.text,
5787            "// Generated by bynkc — do not edit by hand.\n\
5788             export * from \"./thing/make.js\";\n\
5789             export * from \"./thing/widget.js\";\n"
5790        );
5791    }
5792
5793    /// Review of #1330: the grouping rule added for #1329 is scoped to
5794    /// `ReExportAll`-adjacent-to-`ReExportAll` (and a `Comment` immediately
5795    /// before one) — a `ReExportAll` next to anything else still gets the
5796    /// ordinary blank line. `emit_commons_barrel` is `ReExportAll`'s only
5797    /// producer today, so this is the invariant keeping the rule scoped if
5798    /// a future slice ever gives it a second one.
5799    #[test]
5800    fn a_re_export_all_next_to_a_non_re_export_all_gets_the_ordinary_blank_line() {
5801        let mut program = TsProgram::new();
5802        program.push(TsStmt::decl(
5803            TsDecl::ImportNamespace {
5804                type_only: false,
5805                alias: "handlers".to_string(),
5806                from: "./handlers.js".to_string(),
5807            },
5808            None,
5809        ));
5810        program.push(TsStmt::decl(
5811            TsDecl::ReExportAll {
5812                from: "./thing/make.js".to_string(),
5813            },
5814            None,
5815        ));
5816        program.push(TsStmt::comment("trailing note", None));
5817        let printed = print(&program, "x.bynk", "", "x.ts");
5818        assert_eq!(
5819            printed.text,
5820            "import * as handlers from \"./handlers.js\";\n\n\
5821             export * from \"./thing/make.js\";\n\n\
5822             // trailing note\n"
5823        );
5824    }
5825
5826    /// #1333: `print_stmt` prints a `TsStmtKind::DocComment` as a real JSDoc
5827    /// block, matching `emit_doc_block`'s own real multi-line shape
5828    /// (`137_agent_instantiation_workers/expected/workers/demo-counter/
5829    /// handlers.ts`'s own real header comment).
5830    #[test]
5831    fn print_stmt_renders_a_multi_line_doc_comment() {
5832        let stmt = TsStmt::doc_comment(
5833            "A minimal stateful agent in the bundle target: instantiation lowers through the\ngenerated factory, the method call is a direct call, and state persists per key\nacross calls within a session.",
5834            None,
5835        );
5836        assert_eq!(
5837            print_stmt(&stmt, 0),
5838            "/**\n \
5839             * A minimal stateful agent in the bundle target: instantiation lowers through the\n \
5840             * generated factory, the method call is a direct call, and state persists per key\n \
5841             * across calls within a session.\n \
5842             */\n"
5843        );
5844    }
5845
5846    /// #1333: a literal `*/` inside doc text escapes to `*\/`, matching
5847    /// `emit_doc_block`'s own pre-conversion behaviour exactly (issue
5848    /// #720 — an unescaped `*/` would otherwise close the comment early
5849    /// and let trailing text land as executable top-level TypeScript).
5850    #[test]
5851    fn print_stmt_escapes_a_literal_comment_terminator_inside_doc_text() {
5852        let stmt = TsStmt::doc_comment("docs */ ; (globalThis as any).PWNED = true; /*", None);
5853        let printed = print_stmt(&stmt, 0);
5854        assert!(!printed.contains("*/ ;"), "unescaped terminator: {printed}");
5855        assert_eq!(
5856            printed,
5857            "/**\n * docs *\\/ ; (globalThis as any).PWNED = true; /*\n */\n"
5858        );
5859    }
5860
5861    /// #1333: a blank line inside doc text prints as a bare ` *`, no
5862    /// trailing space — distinct from a non-blank line's own ` * <line>`.
5863    #[test]
5864    fn print_stmt_renders_a_blank_doc_line_as_a_bare_star() {
5865        let stmt = TsStmt::doc_comment("first paragraph\n\nsecond paragraph", None);
5866        assert_eq!(
5867            print_stmt(&stmt, 0),
5868            "/**\n * first paragraph\n *\n * second paragraph\n */\n"
5869        );
5870    }
5871
5872    /// #1333: `print_stmt`'s own `depth` parameter indents every line of
5873    /// the JSDoc block, matching `render_stmt`'s own 2-space-per-level
5874    /// convention — `emit_doc_block`'s real callers pass `INDENT_STEP`
5875    /// (2 raw spaces = depth 1) for a nested doc comment.
5876    #[test]
5877    fn print_stmt_indents_a_doc_comment_at_depth() {
5878        let stmt = TsStmt::doc_comment("a method", None);
5879        assert_eq!(print_stmt(&stmt, 1), "  /**\n   * a method\n   */\n");
5880    }
5881
5882    /// #1337: `TsStmtKind::Raw` prints its own text exactly as given — no
5883    /// leading indent (unlike every other statement kind, whose own
5884    /// `render_stmt` arm prefixes `indent(depth)`), no added semicolon or
5885    /// braces. `emit_method`'s own opaque `lower.rs`-sourced body is
5886    /// already fully, absolutely indented by the time it's captured into
5887    /// one `Raw` node, so the printer must contribute nothing further —
5888    /// the same reasoning `Verbatim` already established for pre-rendered
5889    /// content, `Raw`'s own doc explains why it's a distinct kind.
5890    #[test]
5891    fn print_stmt_renders_raw_text_verbatim_with_no_added_indent_or_punctuation() {
5892        let stmt = TsStmt::raw("    return x + 1;\n", None);
5893        // depth is passed but deliberately has no effect on Raw's own output.
5894        assert_eq!(print_stmt(&stmt, 3), "    return x + 1;\n");
5895    }
5896
5897    /// #1337: multi-line `Raw` text (the real shape — a whole function
5898    /// body, not one line) passes through with every embedded line intact,
5899    /// confirming the printer doesn't split, re-indent, or otherwise
5900    /// interpret it.
5901    #[test]
5902    fn print_stmt_renders_multi_line_raw_text_unchanged() {
5903        let stmt = TsStmt::raw(
5904            "    const r = Uuid.of(crypto.randomUUID());\n    return r.value;\n",
5905            None,
5906        );
5907        assert_eq!(
5908            print_stmt(&stmt, 0),
5909            "    const r = Uuid.of(crypto.randomUUID());\n    return r.value;\n"
5910        );
5911    }
5912
5913    /// #1337: `TsObjectEntry::Method` with a `Raw` body — `emit_method`'s
5914    /// own real shape (`{method}{generics}({params}): {ret} { <raw body>
5915    /// },`), pinned at the actual depth its own object literal renders at
5916    /// (a top-level `export const {...}`, depth 0 → entries at depth 1,
5917    /// two-space indent, matching `emit_method`'s own pre-conversion
5918    /// hand-written `"  {method}..."` line).
5919    #[test]
5920    fn multiline_object_renders_a_method_entry_with_a_raw_body() {
5921        let mut out = String::new();
5922        render_multiline_object(
5923            &mut out,
5924            &[TsObjectEntry::Method {
5925                name: "of".to_string(),
5926                is_async: false,
5927                generics: Vec::new(),
5928                params: vec![TsParam {
5929                    name: "value".to_string(),
5930                    ty: Some(TsType::named("string")),
5931                    optional: false,
5932                }],
5933                return_type: Some(TsType::named("Uuid")),
5934                doc: None,
5935                inline: false,
5936                body: vec![TsStmt::raw("    return value as Uuid;\n", None)],
5937            }],
5938            0,
5939        );
5940        assert_eq!(
5941            out,
5942            "{\n  of(value: string): Uuid {\n    return value as Uuid;\n  },\n}"
5943        );
5944    }
5945
5946    /// #1337: `TsObjectEntry::Method`'s own `generics` field prints
5947    /// `<A, U>` between the method name and its parameter list —
5948    /// `Box.map`'s own real shape (`402_generic_instance_method`, a
5949    /// single-file-form fixture the accepted proposal's own project-form
5950    /// search missed).
5951    #[test]
5952    fn multiline_object_renders_a_generic_method_entry() {
5953        let mut out = String::new();
5954        render_multiline_object(
5955            &mut out,
5956            &[TsObjectEntry::Method {
5957                name: "map".to_string(),
5958                is_async: false,
5959                generics: vec!["A".to_string(), "U".to_string()],
5960                params: vec![
5961                    TsParam {
5962                        name: "self".to_string(),
5963                        ty: Some(TsType::named("Box<A>")),
5964                        optional: false,
5965                    },
5966                    TsParam {
5967                        name: "f".to_string(),
5968                        ty: Some(TsType::named("(a0: A) => U")),
5969                        optional: false,
5970                    },
5971                ],
5972                return_type: Some(TsType::named("Box<U>")),
5973                doc: None,
5974                inline: false,
5975                body: vec![TsStmt::return_stmt(
5976                    Some(TsExpr::Ident("{ value: f(self.value) }".to_string())),
5977                    None,
5978                )],
5979            }],
5980            0,
5981        );
5982        assert_eq!(
5983            out,
5984            "{\n  map<A, U>(self: Box<A>, f: (a0: A) => U): Box<U> {\n    return { value: f(self.value) };\n  },\n}"
5985        );
5986    }
5987
5988    /// #1337: a method with no generics prints no `<>` at all — the
5989    /// ordinary, dominant case (every prior slice's own real method
5990    /// entries), confirming `generics: Vec::new()` is a true no-op, not
5991    /// an empty `<>`.
5992    #[test]
5993    fn multiline_object_renders_a_non_generic_method_entry_with_no_angle_brackets() {
5994        let mut out = String::new();
5995        render_multiline_object(
5996            &mut out,
5997            &[TsObjectEntry::Method {
5998                name: "get".to_string(),
5999                is_async: false,
6000                generics: Vec::new(),
6001                params: vec![],
6002                return_type: Some(TsType::named("void")),
6003                doc: None,
6004                inline: false,
6005                body: vec![],
6006            }],
6007            0,
6008        );
6009        assert_eq!(out, "{\n  get(): void {\n  },\n}");
6010    }
6011
6012    /// #1337: `TsObjectEntry::Method`'s own `doc` field prints a JSDoc
6013    /// block immediately before the method entry, same indent, no blank
6014    /// line between — `Timestamp.diff`'s own real shape
6015    /// (`65_money_uses_time`), reusing `render_doc_comment` (the same
6016    /// renderer `TsStmtKind::DocComment` already uses) rather than a
6017    /// second copy.
6018    #[test]
6019    fn multiline_object_renders_a_method_entry_with_a_preceding_doc_comment() {
6020        let mut out = String::new();
6021        render_multiline_object(
6022            &mut out,
6023            &[TsObjectEntry::Method {
6024                name: "diff".to_string(),
6025                is_async: false,
6026                generics: Vec::new(),
6027                params: vec![TsParam {
6028                    name: "self".to_string(),
6029                    ty: Some(TsType::named("Timestamp")),
6030                    optional: false,
6031                }],
6032                return_type: Some(TsType::named("Span")),
6033                doc: Some("Compute the duration between two timestamps.".to_string()),
6034                inline: false,
6035                body: vec![TsStmt::raw("    return 0;\n", None)],
6036            }],
6037            0,
6038        );
6039        assert_eq!(
6040            out,
6041            "{\n  /**\n   * Compute the duration between two timestamps.\n   */\n  diff(self: Timestamp): Span {\n    return 0;\n  },\n}"
6042        );
6043    }
6044
6045    /// #1337: `TsObjectEntry::Method`'s own `inline: true` renders the
6046    /// whole entry — signature and one-statement body alike — on ONE
6047    /// physical line, `emit_forwarded_methods`'s own real shape
6048    /// (`255_context_uses_commons_static_method`'s own real `equals`
6049    /// entry: `equals(self: Cents, other: Cents): boolean { return
6050    /// __CommonsCents.equals(self, other) as unknown as boolean; },`),
6051    /// distinct from every other real `Method` entry in this tree
6052    /// (always multi-line, `inline: false`).
6053    #[test]
6054    fn multiline_object_renders_an_inline_method_entry_on_one_line() {
6055        let mut out = String::new();
6056        render_multiline_object(
6057            &mut out,
6058            &[TsObjectEntry::Method {
6059                name: "equals".to_string(),
6060                is_async: false,
6061                generics: Vec::new(),
6062                params: vec![
6063                    TsParam {
6064                        name: "self".to_string(),
6065                        ty: Some(TsType::named("Cents")),
6066                        optional: false,
6067                    },
6068                    TsParam {
6069                        name: "other".to_string(),
6070                        ty: Some(TsType::named("Cents")),
6071                        optional: false,
6072                    },
6073                ],
6074                return_type: Some(TsType::named("boolean")),
6075                doc: None,
6076                inline: true,
6077                body: vec![TsStmt::return_stmt(
6078                    Some(TsExpr::As {
6079                        expr: Box::new(TsExpr::As {
6080                            expr: Box::new(TsExpr::Call {
6081                                callee: Box::new(TsExpr::Member {
6082                                    object: Box::new(TsExpr::Ident("__CommonsCents".to_string())),
6083                                    property: "equals".to_string(),
6084                                }),
6085                                args: vec![
6086                                    TsExpr::Ident("self".to_string()),
6087                                    TsExpr::Ident("other".to_string()),
6088                                ],
6089                            }),
6090                            ty: TsType::named("unknown"),
6091                        }),
6092                        ty: TsType::named("boolean"),
6093                    }),
6094                    None,
6095                )],
6096            }],
6097            0,
6098        );
6099        assert_eq!(
6100            out,
6101            "{\n  equals(self: Cents, other: Cents): boolean { return __CommonsCents.equals(self, other) as unknown as boolean; },\n}"
6102        );
6103    }
6104
6105    /// Review of #1338, finding 4: `print_object_entry` — the exact public
6106    /// API `emit_refined_type`/`emit_record_type`/`emit_sum_type`'s own
6107    /// real call sites depend on — had no direct test; every #1337 test
6108    /// above drives `render_multiline_object` instead. Pins the contract
6109    /// those call sites rely on: `print_object_entry(&entry, 0)` produces
6110    /// the same text as the entry-slice portion of what
6111    /// `render_multiline_object`'s own depth-0 output produces for the
6112    /// same entry (its own depth convention — the entry lands one level
6113    /// deeper than the object, i.e. `depth + 1` — matches exactly, not
6114    /// coincidentally, since both paths route through the same
6115    /// `render_multiline_object_entry`).
6116    #[test]
6117    fn print_object_entry_matches_the_multiline_objects_own_entry_slice() {
6118        // A real `TsStmt::return_stmt` body, not `Raw` — `Raw`'s own baked-in
6119        // indent only matches depth 0 (finding 3's own new guard), so an
6120        // ordinary real-node body is what this depth-convention contract
6121        // needs to prove at a non-zero depth.
6122        let entry = TsObjectEntry::Method {
6123            name: "of".to_string(),
6124            is_async: false,
6125            generics: Vec::new(),
6126            params: vec![TsParam {
6127                name: "value".to_string(),
6128                ty: Some(TsType::named("string")),
6129                optional: false,
6130            }],
6131            return_type: Some(TsType::named("Uuid")),
6132            doc: None,
6133            inline: false,
6134            body: vec![TsStmt::return_stmt(
6135                Some(TsExpr::As {
6136                    expr: Box::new(TsExpr::Ident("value".to_string())),
6137                    ty: TsType::named("Uuid"),
6138                }),
6139                None,
6140            )],
6141        };
6142
6143        let mut whole_object = String::new();
6144        render_multiline_object(&mut whole_object, std::slice::from_ref(&entry), 0);
6145        // `render_multiline_object`'s own entry line is followed by `\n`
6146        // then the closing `}` (no blank line between them for a single
6147        // entry) — `print_object_entry`'s own output keeps that same
6148        // trailing `\n` (it has no closing brace of its own to attach to),
6149        // so the real equivalence is entry-slice-plus-newline, not a bare
6150        // slice.
6151        let entry_slice_with_trailing_newline = whole_object
6152            .strip_prefix("{\n")
6153            .and_then(|s| s.strip_suffix('}'))
6154            .expect("render_multiline_object's own single-entry output has a { }-wrapper");
6155
6156        assert_eq!(
6157            print_object_entry(&entry, 0),
6158            entry_slice_with_trailing_newline
6159        );
6160    }
6161
6162    /// #1339's own real gap: `emit_refined_type`'s own branded-type alias,
6163    /// `{base} & { readonly __brand: "..." }`, has no representation among
6164    /// `Named`/`Array`/`Object`/`Fn`/`Union` — mirrors `Union`'s own single-
6165    /// line, ` & `-joined shape exactly.
6166    #[test]
6167    fn print_type_renders_an_intersection() {
6168        let ty = TsType::intersection(vec![
6169            TsType::named("string"),
6170            TsType::Object(vec![TsTypeMember::readonly_prop(
6171                "__brand",
6172                TsType::named("\"Order\""),
6173            )]),
6174        ]);
6175        assert_eq!(print_type(&ty), "string & { readonly __brand: \"Order\" }");
6176    }
6177
6178    /// #1339's own real gap: `emit_sum_type`'s own multi-line discriminated
6179    /// union — a leading `|` on every line except the first (which gets
6180    /// equivalent spacing instead, matching the pre-conversion `writeln!`
6181    /// code's own `let pipe = if i == 0 { " " } else { "|" };` exactly), no
6182    /// trailing newline or `;` of its own (the caller, `TsDecl::TypeAlias`'s
6183    /// own render arm, owns both).
6184    #[test]
6185    fn print_type_renders_a_multiline_union() {
6186        let ty = TsType::multiline_union(vec![
6187            TsType::Object(vec![TsTypeMember::readonly_prop(
6188                "tag",
6189                TsType::named("\"a\""),
6190            )]),
6191            TsType::Object(vec![
6192                TsTypeMember::readonly_prop("tag", TsType::named("\"b\"")),
6193                TsTypeMember::readonly_prop("value", TsType::named("number")),
6194            ]),
6195        ]);
6196        assert_eq!(
6197            print_type(&ty),
6198            "    { readonly tag: \"a\" }\n  | { readonly tag: \"b\"; readonly value: number }"
6199        );
6200    }
6201
6202    /// #1339: `TsDecl::TypeAlias`'s own multiline-union special case — the
6203    /// `=` is followed directly by `\n` (no trailing space, matching the
6204    /// pre-conversion `writeln!(out, "export type {name}{params} =")`
6205    /// line's own exact bytes), each variant on its own line, the closing
6206    /// `;` appended directly to the last variant's own line. Also pins
6207    /// `type_params`' own bare-generics rendering on the alias header.
6208    #[test]
6209    fn prints_a_generic_sum_types_own_multiline_type_alias() {
6210        let mut program = TsProgram::new();
6211        program.push(TsStmt::decl(
6212            TsDecl::Export(Box::new(TsDecl::TypeAlias {
6213                name: "Opt".to_string(),
6214                type_params: vec!["T".to_string()],
6215                ty: TsType::multiline_union(vec![
6216                    TsType::Object(vec![TsTypeMember::readonly_prop(
6217                        "tag",
6218                        TsType::named("\"none\""),
6219                    )]),
6220                    TsType::Object(vec![
6221                        TsTypeMember::readonly_prop("tag", TsType::named("\"some\"")),
6222                        TsTypeMember::readonly_prop("value", TsType::named("T")),
6223                    ]),
6224                ]),
6225            })),
6226            None,
6227        ));
6228        let printed = print(&program, "x.bynk", "", "x.ts");
6229        assert_eq!(
6230            printed.text,
6231            "export type Opt<T> =\n    \
6232             { readonly tag: \"none\" }\n  \
6233             | { readonly tag: \"some\"; readonly value: T };\n"
6234        );
6235    }
6236
6237    /// #1339's own real gap: `emit_record_type`'s own `export interface
6238    /// {name}{params} { readonly {field}: {ty}; ... }` — bare generic names
6239    /// on the interface header, `readonly` on every real member here.
6240    #[test]
6241    fn prints_a_generic_interface_with_readonly_members() {
6242        let mut program = TsProgram::new();
6243        program.push(TsStmt::decl(
6244            TsDecl::Export(Box::new(TsDecl::Interface {
6245                name: "Box".to_string(),
6246                type_params: vec!["T".to_string()],
6247                members: vec![TsTypeMember::readonly_prop("value", TsType::named("T"))],
6248            })),
6249            None,
6250        ));
6251        let printed = print(&program, "x.bynk", "", "x.ts");
6252        assert_eq!(
6253            printed.text,
6254            "export interface Box<T> {\n  readonly value: T;\n}\n"
6255        );
6256    }
6257
6258    /// #1357's own real gap: `TsTypeMember::Method` had no `generics`/`doc`
6259    /// fields — `emit_capability`'s own interface methods are genuinely
6260    /// generic (no monomorphisation) and doc-commented per op. `doc` renders
6261    /// at `TsDecl::Interface`'s own render arm (not `render_type_member`
6262    /// itself, which has no `depth` to give `render_doc_comment`), mirroring
6263    /// `TsObjectEntry::Method.doc`'s own identical split (#1337).
6264    #[test]
6265    fn prints_a_generic_documented_interface_method() {
6266        let mut program = TsProgram::new();
6267        program.push(TsStmt::decl(
6268            TsDecl::Export(Box::new(TsDecl::Interface {
6269                name: "Clock".to_string(),
6270                type_params: Vec::new(),
6271                members: vec![TsTypeMember::Method {
6272                    name: "now".to_string(),
6273                    generics: vec!["T".to_string()],
6274                    params: vec![TsParam {
6275                        name: "unit".to_string(),
6276                        ty: Some(TsType::named("T")),
6277                        optional: false,
6278                    }],
6279                    ret: TsType::named("number"),
6280                    doc: Some("Returns the current time.".to_string()),
6281                }],
6282            })),
6283            None,
6284        ));
6285        let printed = print(&program, "x.bynk", "", "x.ts");
6286        assert_eq!(
6287            printed.text,
6288            "export interface Clock {\n  /**\n   * Returns the current time.\n   */\n  now<T>(unit: T): number;\n}\n"
6289        );
6290    }
6291
6292    /// #1359's own real need: `print_class_method` — a class-method sibling
6293    /// of `print_object_entry`'s own fragment-printing entry point,
6294    /// `emit_provider`'s own hand-written class wrapper prints each real
6295    /// method through this directly. `depth` means the *class's* own
6296    /// depth, so at `depth == 0` the method itself lands at `indent(1)`,
6297    /// its body at `indent(2)`, matching `TsDecl::Class`'s own equivalent
6298    /// per-method indent exactly — but with no automatic blank-line
6299    /// insertion of its own (unlike `TsDecl::Class`'s own render arm),
6300    /// since `emit_provider`'s own real spacing has none between methods.
6301    #[test]
6302    fn prints_a_single_class_method_fragment() {
6303        let method = TsClassMethod {
6304            name: "double".to_string(),
6305            private: false,
6306            is_async: true,
6307            params: vec![TsParam {
6308                name: "n".to_string(),
6309                ty: Some(TsType::named("number")),
6310                optional: false,
6311            }],
6312            return_type: Some(TsType::named("number")),
6313            doc: None,
6314            body: vec![TsStmt::return_stmt(
6315                Some(TsExpr::Binary {
6316                    op: TsBinaryOp::NullishCoalescing,
6317                    left: Box::new(TsExpr::Ident("n".to_string())),
6318                    right: Box::new(TsExpr::Lit(TsLit::Num("0".to_string()))),
6319                }),
6320                None,
6321            )],
6322        };
6323        assert_eq!(
6324            print_class_method(&method, 0),
6325            "  async double(n: number): number {\n    return n ?? 0;\n  }\n"
6326        );
6327    }
6328
6329    /// Arc C, slice 21 (#1371): `TsClassMethod.private` — pins both the
6330    /// keyword itself and its render-*before*-`async` ordering directly,
6331    /// the same precedent `TsClassField.private` already has (its own
6332    /// direct test, not left to transitive `bynkc` fixture coverage alone)
6333    /// — review of #1372 caught this one still missing for the sibling
6334    /// field.
6335    #[test]
6336    fn prints_a_private_class_method_fragment() {
6337        let method = TsClassMethod {
6338            name: "loadState".to_string(),
6339            private: true,
6340            is_async: true,
6341            params: Vec::new(),
6342            return_type: Some(TsType::named_with_args(
6343                "Promise",
6344                vec![TsType::named("OrderState")],
6345            )),
6346            doc: None,
6347            body: vec![TsStmt::return_stmt(
6348                Some(TsExpr::Ident("state".to_string())),
6349                None,
6350            )],
6351        };
6352        assert_eq!(
6353            print_class_method(&method, 0),
6354            "  private async loadState(): Promise<OrderState> {\n    return state;\n  }\n"
6355        );
6356    }
6357
6358    /// Arc C, slice 23 (#1375): `TsClassMethod.doc` — the grounding pass's
6359    /// own second predicted gap (#1366), the same need
6360    /// `TsObjectEntry::Method.doc` (#1337) and `TsTypeMember::Method.doc`
6361    /// (#1357) already solved for their own node kinds, closed here for the
6362    /// third and last real method-shaped node in this crate.
6363    #[test]
6364    fn prints_a_documented_class_method_fragment() {
6365        let method = TsClassMethod {
6366            name: "spend".to_string(),
6367            private: false,
6368            is_async: false,
6369            params: Vec::new(),
6370            return_type: Some(TsType::named("void")),
6371            doc: Some("Debits the account.".to_string()),
6372            body: vec![],
6373        };
6374        assert_eq!(
6375            print_class_method(&method, 0),
6376            "  /**\n   * Debits the account.\n   */\n  spend(): void {\n  }\n"
6377        );
6378    }
6379
6380    /// #1339's own real gap: `TsExpr::Arrow` had no `generics`/`return_type`
6381    /// field — `emit_sum_type`'s own generic payload-constructor arrows
6382    /// (`<T>(name: T): Sum<T> => (...)`) need both. The object-literal body
6383    /// is wrapped in an explicit `Paren` — `Arrow`'s own renderer does not
6384    /// auto-parenthesise an object body the way real JS/TS syntax requires
6385    /// to disambiguate it from a block.
6386    #[test]
6387    fn prints_a_generic_arrow_with_a_parenthesised_object_body() {
6388        let mut program = TsProgram::new();
6389        program.push(TsStmt::expr_stmt(
6390            TsExpr::Arrow {
6391                params: vec![TsParam {
6392                    name: "value".to_string(),
6393                    ty: Some(TsType::named("T")),
6394                    optional: false,
6395                }],
6396                is_async: false,
6397                generics: vec!["T".to_string()],
6398                return_type: Some(TsType::named_with_args("Sum", vec![TsType::named("T")])),
6399                body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Paren(Box::new(
6400                    TsExpr::object_entries(vec![
6401                        TsObjectEntry::Prop(
6402                            "tag".to_string(),
6403                            TsExpr::Lit(TsLit::Str("some".to_string())),
6404                        ),
6405                        TsObjectEntry::Shorthand("value".to_string()),
6406                    ]),
6407                ))))),
6408            },
6409            None,
6410        ));
6411        let printed = print(&program, "x.bynk", "", "x.ts");
6412        assert_eq!(
6413            printed.text,
6414            "<T>(value: T): Sum<T> => ({ tag: \"some\", value });\n"
6415        );
6416    }
6417
6418    /// Arc C, slice 33 (#1401): `TsBinaryOp::LessThan` — pins the operator
6419    /// text itself.
6420    #[test]
6421    fn prints_a_less_than_comparison() {
6422        let mut program = TsProgram::new();
6423        program.push(TsStmt::expr_stmt(
6424            TsExpr::Binary {
6425                op: TsBinaryOp::LessThan,
6426                left: Box::new(TsExpr::Ident("a".to_string())),
6427                right: Box::new(TsExpr::Ident("b".to_string())),
6428            },
6429            None,
6430        ));
6431        let printed = print(&program, "x.bynk", "", "x.ts");
6432        assert_eq!(printed.text, "a < b;\n");
6433    }
6434
6435    /// `LessThan` shares `GreaterThan`'s own precedence tier — a nested
6436    /// `Add` under it needs no parens (`+` binds tighter), mirroring
6437    /// `add_binds_tighter_than_greater_than_on_both_sides` for the other
6438    /// relational operator.
6439    #[test]
6440    fn add_binds_tighter_than_less_than() {
6441        let mut program = TsProgram::new();
6442        program.push(TsStmt::expr_stmt(
6443            TsExpr::Binary {
6444                op: TsBinaryOp::LessThan,
6445                left: Box::new(TsExpr::Binary {
6446                    op: TsBinaryOp::Add,
6447                    left: Box::new(TsExpr::Ident("a".to_string())),
6448                    right: Box::new(TsExpr::Ident("b".to_string())),
6449                }),
6450                right: Box::new(TsExpr::Ident("c".to_string())),
6451            },
6452            None,
6453        ));
6454        let printed = print(&program, "x.bynk", "", "x.ts");
6455        assert_eq!(printed.text, "a + b < c;\n");
6456    }
6457
6458    /// Arc C, slice 34 (`tests_emit.rs` slice D, #1403): `TsBinaryOp::
6459    /// InstanceOf` — pins the keyword operator text itself, sharing
6460    /// `LessThan`/`GreaterThan`'s own precedence tier.
6461    #[test]
6462    fn prints_an_instanceof_check() {
6463        let mut program = TsProgram::new();
6464        program.push(TsStmt::expr_stmt(
6465            TsExpr::Binary {
6466                op: TsBinaryOp::InstanceOf,
6467                left: Box::new(TsExpr::Ident("e".to_string())),
6468                right: Box::new(TsExpr::Ident("ExpectationError".to_string())),
6469            },
6470            None,
6471        ));
6472        let printed = print(&program, "x.bynk", "", "x.ts");
6473        assert_eq!(printed.text, "e instanceof ExpectationError;\n");
6474    }
6475
6476    /// Review of #1404, finding 2: `prints_an_instanceof_check` alone pins
6477    /// the operator text but not the precedence tier the same diff added to
6478    /// `binary_precedence` — it passes identically whether `InstanceOf` sits
6479    /// at tier 5, tier 1, or is missing from that arm entirely. `Add` (tier
6480    /// 6) binds tighter, mirroring `add_binds_tighter_than_less_than`
6481    /// for the other relational operator sharing this tier.
6482    #[test]
6483    fn add_binds_tighter_than_instanceof() {
6484        let mut program = TsProgram::new();
6485        program.push(TsStmt::expr_stmt(
6486            TsExpr::Binary {
6487                op: TsBinaryOp::InstanceOf,
6488                left: Box::new(TsExpr::Binary {
6489                    op: TsBinaryOp::Add,
6490                    left: Box::new(TsExpr::Ident("a".to_string())),
6491                    right: Box::new(TsExpr::Ident("b".to_string())),
6492                }),
6493                right: Box::new(TsExpr::Ident("C".to_string())),
6494            },
6495            None,
6496        ));
6497        let printed = print(&program, "x.bynk", "", "x.ts");
6498        assert_eq!(printed.text, "a + b instanceof C;\n");
6499    }
6500
6501    /// Review of #1404, finding 2 (the more valuable half): a catch clause
6502    /// naturally grows into `e instanceof A || e instanceof B` as a second
6503    /// error type is added. `InstanceOf` (tier 5) binds tighter than `Or`
6504    /// (tier 2), so this must print flat — a wrong tier would silently
6505    /// over-parenthesize it into `(e instanceof A) || (e instanceof B)`.
6506    #[test]
6507    fn instanceof_binds_tighter_than_or_on_both_sides() {
6508        let mut program = TsProgram::new();
6509        program.push(TsStmt::expr_stmt(
6510            TsExpr::Binary {
6511                op: TsBinaryOp::Or,
6512                left: Box::new(TsExpr::Binary {
6513                    op: TsBinaryOp::InstanceOf,
6514                    left: Box::new(TsExpr::Ident("e".to_string())),
6515                    right: Box::new(TsExpr::Ident("A".to_string())),
6516                }),
6517                right: Box::new(TsExpr::Binary {
6518                    op: TsBinaryOp::InstanceOf,
6519                    left: Box::new(TsExpr::Ident("e".to_string())),
6520                    right: Box::new(TsExpr::Ident("B".to_string())),
6521                }),
6522            },
6523            None,
6524        ));
6525        let printed = print(&program, "x.bynk", "", "x.ts");
6526        assert_eq!(printed.text, "e instanceof A || e instanceof B;\n");
6527    }
6528
6529    /// Arc E slice 5 (`serialisation.rs`, #1443): `TsBinaryOp::In` — pins
6530    /// the keyword operator text itself, sharing `LessThan`/`GreaterThan`/
6531    /// `InstanceOf`'s own precedence tier.
6532    #[test]
6533    fn prints_an_in_check() {
6534        let mut program = TsProgram::new();
6535        program.push(TsStmt::expr_stmt(
6536            TsExpr::Binary {
6537                op: TsBinaryOp::In,
6538                left: Box::new(TsExpr::Lit(TsLit::Str("name".to_string()))),
6539                right: Box::new(TsExpr::Ident("obj".to_string())),
6540            },
6541            None,
6542        ));
6543        let printed = print(&program, "x.bynk", "", "x.ts");
6544        assert_eq!(printed.text, "\"name\" in obj;\n");
6545    }
6546
6547    /// Mirrors `add_binds_tighter_than_instanceof`/`add_binds_tighter_than_
6548    /// less_than` for the newest member of this shared precedence tier:
6549    /// `Add` (tier 6) binds tighter than `In` (tier 5).
6550    #[test]
6551    fn add_binds_tighter_than_in() {
6552        let mut program = TsProgram::new();
6553        program.push(TsStmt::expr_stmt(
6554            TsExpr::Binary {
6555                op: TsBinaryOp::In,
6556                left: Box::new(TsExpr::Binary {
6557                    op: TsBinaryOp::Add,
6558                    left: Box::new(TsExpr::Ident("a".to_string())),
6559                    right: Box::new(TsExpr::Ident("b".to_string())),
6560                }),
6561                right: Box::new(TsExpr::Ident("obj".to_string())),
6562            },
6563            None,
6564        ));
6565        let printed = print(&program, "x.bynk", "", "x.ts");
6566        assert_eq!(printed.text, "a + b in obj;\n");
6567    }
6568
6569    /// Mirrors `instanceof_binds_tighter_than_or_on_both_sides`: `In` (tier
6570    /// 5) binds tighter than `Or` (tier 2), so a real
6571    /// `"a" in obj || "b" in obj` reads flat with no parens — a wrong tier
6572    /// would silently over-parenthesize it.
6573    #[test]
6574    fn in_binds_tighter_than_or_on_both_sides() {
6575        let mut program = TsProgram::new();
6576        program.push(TsStmt::expr_stmt(
6577            TsExpr::Binary {
6578                op: TsBinaryOp::Or,
6579                left: Box::new(TsExpr::Binary {
6580                    op: TsBinaryOp::In,
6581                    left: Box::new(TsExpr::Lit(TsLit::Str("a".to_string()))),
6582                    right: Box::new(TsExpr::Ident("obj".to_string())),
6583                }),
6584                right: Box::new(TsExpr::Binary {
6585                    op: TsBinaryOp::In,
6586                    left: Box::new(TsExpr::Lit(TsLit::Str("b".to_string()))),
6587                    right: Box::new(TsExpr::Ident("obj".to_string())),
6588                }),
6589            },
6590            None,
6591        ));
6592        let printed = print(&program, "x.bynk", "", "x.ts");
6593        assert_eq!(printed.text, "\"a\" in obj || \"b\" in obj;\n");
6594    }
6595
6596    /// Review of #1471, finding 1: `TsBinaryOp::GreaterThanEq`/`LessThanEq`
6597    /// landed with no direct `bynk-ts` test — only pinned indirectly through
6598    /// `bynk-emit`'s own fixture goldens. This test and the four below close
6599    /// that locality gap, mirroring the coverage every other operator on
6600    /// this precedence tier already has (`prints_an_in_check`/`add_binds_
6601    /// tighter_than_in`/`in_binds_tighter_than_or_on_both_sides`).
6602    #[test]
6603    fn prints_a_greater_than_or_equal_comparison() {
6604        let mut program = TsProgram::new();
6605        program.push(TsStmt::expr_stmt(
6606            TsExpr::Binary {
6607                op: TsBinaryOp::GreaterThanEq,
6608                left: Box::new(TsExpr::Ident("a".to_string())),
6609                right: Box::new(TsExpr::Ident("b".to_string())),
6610            },
6611            None,
6612        ));
6613        let printed = print(&program, "x.bynk", "", "x.ts");
6614        assert_eq!(printed.text, "a >= b;\n");
6615    }
6616
6617    #[test]
6618    fn prints_a_less_than_or_equal_comparison() {
6619        let mut program = TsProgram::new();
6620        program.push(TsStmt::expr_stmt(
6621            TsExpr::Binary {
6622                op: TsBinaryOp::LessThanEq,
6623                left: Box::new(TsExpr::Ident("a".to_string())),
6624                right: Box::new(TsExpr::Ident("b".to_string())),
6625            },
6626            None,
6627        ));
6628        let printed = print(&program, "x.bynk", "", "x.ts");
6629        assert_eq!(printed.text, "a <= b;\n");
6630    }
6631
6632    /// Pins `GreaterThanEq`/`LessThanEq`'s own tier-5 placement — the real
6633    /// `pred_condition_and_message` `InRange` shape (#1471) nests both under
6634    /// `And` (tier 3) and must print flat, with no parens on either side. A
6635    /// regression that demoted either operator to `And`'s own tier or below
6636    /// would silently wrap this in parens instead.
6637    #[test]
6638    fn greater_than_eq_and_less_than_eq_nest_flat_under_and() {
6639        let mut program = TsProgram::new();
6640        program.push(TsStmt::expr_stmt(
6641            TsExpr::Binary {
6642                op: TsBinaryOp::And,
6643                left: Box::new(TsExpr::Binary {
6644                    op: TsBinaryOp::GreaterThanEq,
6645                    left: Box::new(TsExpr::Ident("value".to_string())),
6646                    right: Box::new(TsExpr::Lit(TsLit::Num("0".to_string()))),
6647                }),
6648                right: Box::new(TsExpr::Binary {
6649                    op: TsBinaryOp::LessThanEq,
6650                    left: Box::new(TsExpr::Ident("value".to_string())),
6651                    right: Box::new(TsExpr::Lit(TsLit::Num("100".to_string()))),
6652                }),
6653            },
6654            None,
6655        ));
6656        let printed = print(&program, "x.bynk", "", "x.ts");
6657        assert_eq!(printed.text, "value >= 0 && value <= 100;\n");
6658    }
6659
6660    /// Same tier, *different* operator: `render_binary_operand`'s own doc
6661    /// says equal precedence still parenthesises unless the two sides are
6662    /// the exact same associative operator — `GreaterThan`/`GreaterThanEq`
6663    /// are equal-tier but distinct, so nesting one under the other must
6664    /// still keep its parens (the `_ => true` fallback of the equal-
6665    /// precedence match, newly reachable now this tier has more than one
6666    /// non-keyword operator).
6667    #[test]
6668    fn a_greater_than_eq_operand_of_a_different_relational_op_still_parenthesises() {
6669        let mut program = TsProgram::new();
6670        program.push(TsStmt::expr_stmt(
6671            TsExpr::Binary {
6672                op: TsBinaryOp::GreaterThan,
6673                left: Box::new(TsExpr::Ident("a".to_string())),
6674                right: Box::new(TsExpr::Binary {
6675                    op: TsBinaryOp::GreaterThanEq,
6676                    left: Box::new(TsExpr::Ident("b".to_string())),
6677                    right: Box::new(TsExpr::Ident("c".to_string())),
6678                }),
6679            },
6680            None,
6681        ));
6682        let printed = print(&program, "x.bynk", "", "x.ts");
6683        assert_eq!(printed.text, "a > (b >= c);\n");
6684    }
6685
6686    /// `GreaterThanEq`/`LessThanEq` nested in themselves are NOT associative
6687    /// (`a >= b >= c` does not parse as `a >= (b >= c)`), unlike `||`/`&&` —
6688    /// so, unlike `in_binds_tighter_than_or_on_both_sides`'s own flat `In`
6689    /// chain, a same-operator nesting here must still parenthesise.
6690    #[test]
6691    fn a_right_nested_greater_than_eq_chain_keeps_its_parens() {
6692        let mut program = TsProgram::new();
6693        program.push(TsStmt::expr_stmt(
6694            TsExpr::Binary {
6695                op: TsBinaryOp::GreaterThanEq,
6696                left: Box::new(TsExpr::Ident("a".to_string())),
6697                right: Box::new(TsExpr::Binary {
6698                    op: TsBinaryOp::GreaterThanEq,
6699                    left: Box::new(TsExpr::Ident("b".to_string())),
6700                    right: Box::new(TsExpr::Ident("c".to_string())),
6701                }),
6702            },
6703            None,
6704        ));
6705        let printed = print(&program, "x.bynk", "", "x.ts");
6706        assert_eq!(printed.text, "a >= (b >= c);\n");
6707    }
6708}