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            if needs_parens_as_array_element(element) {
1735                out.push('(');
1736                render_type(out, element);
1737                out.push(')');
1738            } else {
1739                render_type(out, element);
1740            }
1741            out.push_str("[]");
1742        }
1743        TsType::Object(members) => {
1744            // Review of #1358, finding 1: a `Method` member's own `doc` has
1745            // no single-line form — this inline shape has nowhere to put a
1746            // JSDoc block, unlike `TsDecl::Interface`'s own render arm
1747            // (which calls `render_doc_comment` before a documented
1748            // member's own line, since it has `depth` and a real newline
1749            // budget to work with). The identical case #1338's own review
1750            // already ruled on for `TsObjectEntry::Method.doc` in
1751            // `render_object_entry_inline` — a loud check, not a silent
1752            // drop, since silently dropping a JSDoc block is a strictly
1753            // worse failure mode than a loud one. Not reachable today
1754            // (`workers_entry.rs`'s own one real `TsType::Object` method
1755            // member goes through `TsTypeMember::method`, so `doc` is
1756            // always `None`) — the #1337 case was equally unreachable,
1757            // which is exactly why it got the assert.
1758            debug_assert!(
1759                !members
1760                    .iter()
1761                    .any(|m| matches!(m, TsTypeMember::Method { doc: Some(_), .. })),
1762                "render_type: a Method member's own doc comment has no single-line form"
1763            );
1764            if members.is_empty() {
1765                out.push_str("{}");
1766            } else {
1767                out.push_str("{ ");
1768                for (i, m) in members.iter().enumerate() {
1769                    if i > 0 {
1770                        out.push_str("; ");
1771                    }
1772                    render_type_member(out, m);
1773                }
1774                out.push_str(" }");
1775            }
1776        }
1777        TsType::Fn { params, ret } => {
1778            out.push('(');
1779            for (i, p) in params.iter().enumerate() {
1780                if i > 0 {
1781                    out.push_str(", ");
1782                }
1783                out.push('a');
1784                out.push_str(&i.to_string());
1785                out.push_str(": ");
1786                render_type(out, p);
1787            }
1788            out.push_str(") => ");
1789            render_type(out, ret);
1790        }
1791        TsType::Union {
1792            members,
1793            multiline: false,
1794        } => {
1795            for (i, m) in members.iter().enumerate() {
1796                if i > 0 {
1797                    out.push_str(" | ");
1798                }
1799                render_type(out, m);
1800            }
1801        }
1802        // #1339: `emit_sum_type`'s own real shape — one variant per line, a
1803        // leading `|` on every line except the first (which gets equivalent
1804        // spacing instead, matching the pre-conversion `writeln!` code's own
1805        // `let pipe = if i == 0 { " " } else { "|" };` exactly), the closing
1806        // `;` appended by `TsDecl::TypeAlias`'s own caller directly after
1807        // the last member's own line — see `TsType::Union`'s own doc for
1808        // why this needs no depth parameter despite the multi-line shape.
1809        TsType::Union {
1810            members,
1811            multiline: true,
1812        } => {
1813            for (i, m) in members.iter().enumerate() {
1814                if i > 0 {
1815                    out.push('\n');
1816                }
1817                out.push_str("  ");
1818                out.push_str(if i == 0 { " " } else { "|" });
1819                out.push(' ');
1820                render_type(out, m);
1821            }
1822        }
1823        TsType::Intersection(members) => {
1824            for (i, m) in members.iter().enumerate() {
1825                if i > 0 {
1826                    out.push_str(" & ");
1827                }
1828                render_type(out, m);
1829            }
1830        }
1831    }
1832}
1833
1834/// Whether `element` must be parenthesised before a postfix `[]`. `[]` binds
1835/// tighter than every prefix or infix type operator, so a readonly array
1836/// (`readonly T[][]` reads as `readonly (T[][])`, which tsc rejects with
1837/// TS1354), a function type (`() => T[]` returns the array) and a union or
1838/// intersection (`A | B[]`) would each be misread bare. A mutable array, a
1839/// named type and an object literal are postfix-safe as they are.
1840fn needs_parens_as_array_element(element: &TsType) -> bool {
1841    matches!(
1842        element,
1843        TsType::Array { readonly: true, .. }
1844            | TsType::Fn { .. }
1845            | TsType::Union { .. }
1846            | TsType::Intersection(_)
1847    )
1848}
1849
1850/// One [`TsTypeMember`] rendered inline (no leading indent, no trailing
1851/// separator) — [`render_type`]'s own `TsType::Object` arm's per-member
1852/// renderer.
1853fn render_type_member(out: &mut String, member: &TsTypeMember) {
1854    match member {
1855        TsTypeMember::Prop {
1856            name,
1857            ty,
1858            optional,
1859            readonly,
1860        } => {
1861            if *readonly {
1862                out.push_str("readonly ");
1863            }
1864            out.push_str(name);
1865            if *optional {
1866                out.push('?');
1867            }
1868            out.push_str(": ");
1869            render_type(out, ty);
1870        }
1871        TsTypeMember::Method {
1872            name,
1873            generics,
1874            params,
1875            ret,
1876            doc: _,
1877        } => {
1878            out.push_str(name);
1879            render_bare_generics(out, generics);
1880            out.push('(');
1881            render_params(out, params);
1882            out.push_str("): ");
1883            render_type(out, ret);
1884        }
1885        TsTypeMember::Index {
1886            key_name,
1887            key_ty,
1888            value_ty,
1889        } => {
1890            out.push('[');
1891            out.push_str(key_name);
1892            out.push_str(": ");
1893            render_type(out, key_ty);
1894            out.push_str("]: ");
1895            render_type(out, value_ty);
1896        }
1897    }
1898}
1899
1900/// Print a single [`TsType`] on its own — the real callers this closes R7.2
1901/// for (`bynk-emit`'s `ts_type_ref*`/`ts_ty` families, P7.9, #1315) each
1902/// want one type fragment to interpolate into a larger, still-hand-built
1903/// line (a field's own type annotation, a parameter list, …), not a whole
1904/// [`TsProgram`]. No source-map/buffer machinery — [`print()`] owns that
1905/// for a whole document; this is the printer's other, narrower entry
1906/// point, sharing the same internal recursion rather than a second copy.
1907pub fn print_type(ty: &TsType) -> String {
1908    let mut out = String::new();
1909    render_type(&mut out, ty);
1910    out
1911}
1912
1913/// Print a single [`TsExpr`] on its own — the expression-level sibling of
1914/// [`print_type`]'s own "one fragment, not a whole document" entry point.
1915/// Arc C, step (11) (#1388) need: `emit_icu_placeholder`'s own `Select` arm
1916/// stays one opaque, hand-built block-bodied IIFE (at the time, `TsExpr::
1917/// Arrow` had no block-body variant at all, and extending it for that one
1918/// site was rejected as disproportionate) — its own arm VALUES,
1919/// `emit_sub_message`'s now-real `TsExpr` results, still need stringifying
1920/// back into that opaque host text. #1435 (Arc E slice 1) later added
1921/// [`TsArrowBody::Block`] for a genuinely different real site
1922/// (`serialisation.rs`'s `Float` guard); `emit_icu_placeholder`'s own
1923/// `Select` arm was not reconverted along with it — out of that slice's own
1924/// scope — so this call site's opaque text stays exactly as it was. No
1925/// source-map/buffer machinery, matching [`print_type`]/[`print_stmt`]'s
1926/// own scope exactly.
1927pub fn print_expr(expr: &TsExpr) -> String {
1928    let mut out = String::new();
1929    render_expr(&mut out, expr);
1930    out
1931}
1932
1933/// Print a single [`TsStmt`] on its own, at `depth` — the statement-level
1934/// sibling of [`print_type`]'s own "one fragment, not a whole document"
1935/// entry point. `bynk-emit`'s own #1333 need (`emit_doc_block`, a shared
1936/// helper spliced into ~14 still-unconverted callers' own buffers) wants
1937/// one statement's own printed text at a caller-supplied depth, not a
1938/// whole [`TsProgram`] — no source-map/buffer machinery, matching
1939/// [`print_type`]'s own scope exactly, and reusing `render_stmt`'s own
1940/// exhaustive per-kind dispatch (this module's own private renderer)
1941/// rather than a second copy.
1942/// Review of #1402: a `TsStmtKind::Raw` nested inside a `Switch` case's own
1943/// body (`emit_stub_rhs`'s own `ReturnsEach` dispatch, Arc C slice 33,
1944/// `tests_emit.rs` slice C) carries the identical "no indent of its own,
1945/// pre-indented at a fixed absolute depth" hazard `render_class_method`'s
1946/// and `render_multiline_object_entry`'s own `debug_assert!`s already guard
1947/// — `stmt_contains_raw` already recurses into `Switch` cases, so the same
1948/// check applies here, this fragment entry point's own first `Raw`-bearing
1949/// `Switch` caller. A bare `Raw` passed directly as `stmt` itself is exempt
1950/// (not a false negative — `render_stmt`'s own `Raw` arm never reads
1951/// `depth` at all, so calling `print_stmt` on a bare `Raw` is safe at any
1952/// depth, the established `print_stmt_renders_raw_text_verbatim_with_no_
1953/// added_indent_or_punctuation` contract below): only a Raw *nested inside*
1954/// a depth-using wrapper (like this `Switch` case) is the real hazard.
1955pub fn print_stmt(stmt: &TsStmt, depth: usize) -> String {
1956    debug_assert!(
1957        depth == 0 || matches!(stmt.kind, TsStmtKind::Raw(_)) || !stmt_contains_raw(stmt),
1958        "print_stmt: a Raw-bearing statement's own baked-in indent only matches depth 0"
1959    );
1960    let mut out = String::new();
1961    render_stmt(&mut out, stmt, depth, None);
1962    out
1963}
1964
1965/// [`print_stmt`]'s own sibling (#1477), the `TsDecl::Function`-body
1966/// counterpart to [`print_class_method_and_merge`]: identical rendering,
1967/// but merges any `nested_map` `stmt` carries — on `stmt` itself, or on
1968/// anything nested inside it (a function's body, a class's constructor/
1969/// methods, …) — into `map` as it prints, at the real print-time offset —
1970/// the same offset `bynk-emit`'s own `emit_free_fn` (`emitter/emit.rs`) used
1971/// to recover by exact arithmetic over the returned text, guarded by a
1972/// `debug_assert!`, before #1480 converted it to set `nested_map` directly
1973/// instead. Kept separate from `print_stmt` itself so every existing
1974/// caller's own call sites are untouched. `source_id` is `map`'s
1975/// own registered source this statement's content belongs to (see
1976/// this crate's own private `MergeTarget`'s own doc for why this can't just be hardcoded).
1977///
1978/// **Appends to the caller's own `out`, unlike `print_stmt`'s own
1979/// return-a-fresh-`String` shape** — the merge computes each checkpoint's
1980/// absolute offset from `out.len()` *as this statement's own text is
1981/// written*, so it must be the caller's real, already-populated buffer, not
1982/// a throwaway local one starting at 0. Returning a fresh string here (the
1983/// first shape this function took, caught before merge) would silently
1984/// record every checkpoint at the wrong offset the moment the caller
1985/// spliced the returned text anywhere but the very start of its own buffer.
1986pub fn print_stmt_and_merge(
1987    out: &mut String,
1988    stmt: &TsStmt,
1989    depth: usize,
1990    map: &mut SourceMapBuilder,
1991    source_id: usize,
1992) {
1993    debug_assert!(
1994        depth == 0 || matches!(stmt.kind, TsStmtKind::Raw(_)) || !stmt_contains_raw(stmt),
1995        "print_stmt_and_merge: a Raw-bearing statement's own baked-in indent only matches depth 0"
1996    );
1997    render_stmt(out, stmt, depth, Some(MergeTarget { map, source_id }));
1998}
1999
2000/// Print a single [`TsObjectEntry`] on its own, at `depth` — the
2001/// object-entry sibling of [`print_stmt`]'s own "one fragment, not a whole
2002/// document" entry point, `depth` meaning the SAME thing it does for this
2003/// crate's own internal multi-line-object renderer: the *object's* own
2004/// depth, so the entry itself lands one level deeper, matching an object built by that
2005/// renderer exactly. #1337's own real need: `emit_attached_methods` (a
2006/// shared helper spliced into `emit_refined_type`/`emit_record_type`/
2007/// `emit_sum_type`'s own still-unconverted `&mut String` buffers) now
2008/// returns `Vec<TsObjectEntry>` instead of writing text directly — each
2009/// caller renders the returned entries one at a time through this, the
2010/// same P7.9/#1333 "keep the caller's own signature, print just the
2011/// fragment" pattern applied to an object-entry-shaped fragment instead of
2012/// a whole statement or type.
2013///
2014/// Review of #1338, finding 3: a `TsStmtKind::Raw` body statement (e.g.
2015/// `emit_method`'s own opaque `lower.rs`-sourced body, or `emit_refined_
2016/// type`'s own opaque `emit_refined_checks`-sourced guard body, #1339's
2017/// second real use) carries NO indent of its own — its text is captured
2018/// pre-indented at a fixed absolute depth by its own caller — so it only
2019/// renders correctly when `depth` is `0`. The guard for this now lives in
2020/// `render_multiline_object_entry` itself (a private renderer, so named
2021/// here in text rather than linked — moved there by review of #1340,
2022/// finding 1: this function's own copy missed the `TsExpr::
2023/// multiline_object_entries`/`render_expr` call path #1339 added, which
2024/// reaches that renderer directly, never through this one), so it fires
2025/// for every caller, not just this entry point.
2026pub fn print_object_entry(entry: &TsObjectEntry, depth: usize) -> String {
2027    let mut out = String::new();
2028    render_multiline_object_entry(&mut out, entry, depth, None);
2029    out
2030}
2031
2032/// [`print_object_entry`]'s own sibling (#1477), the `TsObjectEntry::Method`
2033/// counterpart to [`print_class_method_and_merge`]/[`print_stmt_and_merge`]:
2034/// identical rendering, but merges a `Method` entry's own body `nested_map`
2035/// into `map` as it prints, at the real print-time offset. Kept separate
2036/// from `print_object_entry` itself so every existing caller's own call
2037/// sites are untouched.
2038///
2039/// Appends to the caller's own `out`, the same "must be the real buffer,
2040/// not a throwaway local one" reasoning [`print_stmt_and_merge`]'s own doc
2041/// explains in full. `source_id`: see this crate's own private `MergeTarget`'s own doc.
2042pub fn print_object_entry_and_merge(
2043    out: &mut String,
2044    entry: &TsObjectEntry,
2045    depth: usize,
2046    map: &mut SourceMapBuilder,
2047    source_id: usize,
2048) {
2049    render_multiline_object_entry(out, entry, depth, Some(MergeTarget { map, source_id }));
2050}
2051
2052/// Whether `stmts`, or any block/branch/case nested inside them at any
2053/// depth, contains a [`TsStmtKind::Raw`] — the recursive check
2054/// [`render_class_method`]'s and [`render_multiline_object_entry`]'s own
2055/// `debug_assert!`s need. Review of #1374: a top-level-only scan
2056/// (`body.iter().any(...)`) is blind to a `Raw` buried inside an `If`'s own
2057/// `Block` — `emit_agent`'s own `commitState` (#1373) is exactly this shape,
2058/// a transition's own hoisted-statement `Raw`s sitting inside
2059/// `if (__prior !== undefined) { ... }` — so the two guards this exists for
2060/// would have silently missed exactly the case they exist to catch. Every
2061/// `TsStmtKind` variant is named explicitly, not a wildcard, matching this
2062/// crate's own exhaustiveness discipline.
2063fn contains_raw(stmts: &[TsStmt]) -> bool {
2064    stmts.iter().any(stmt_contains_raw)
2065}
2066
2067fn stmt_contains_raw(stmt: &TsStmt) -> bool {
2068    match &stmt.kind {
2069        TsStmtKind::Raw(_) => true,
2070        TsStmtKind::Verbatim { .. }
2071        | TsStmtKind::Decl(_)
2072        | TsStmtKind::Const { .. }
2073        | TsStmtKind::Let { .. }
2074        | TsStmtKind::ExprStmt(_)
2075        | TsStmtKind::Return(_)
2076        | TsStmtKind::Throw(_)
2077        | TsStmtKind::Continue
2078        | TsStmtKind::Assign { .. }
2079        | TsStmtKind::Comment(_)
2080        | TsStmtKind::DocComment(_)
2081        | TsStmtKind::Blank
2082        | TsStmtKind::Increment(_) => false,
2083        TsStmtKind::If {
2084            then_branch,
2085            else_branch,
2086            ..
2087        } => {
2088            stmt_contains_raw(then_branch) || else_branch.as_deref().is_some_and(stmt_contains_raw)
2089        }
2090        TsStmtKind::ForOf { body, .. } | TsStmtKind::For { body, .. } => stmt_contains_raw(body),
2091        TsStmtKind::TryCatch {
2092            try_block,
2093            catch_block,
2094            ..
2095        } => stmt_contains_raw(try_block) || stmt_contains_raw(catch_block),
2096        TsStmtKind::Block(stmts) | TsStmtKind::InlineBlock(stmts) => contains_raw(stmts),
2097        TsStmtKind::Switch { cases, .. } => cases.iter().any(|c| contains_raw(&c.body)),
2098    }
2099}
2100
2101/// Print a single [`TsClassMethod`] on its own, at `depth` — the
2102/// class-method sibling of [`print_object_entry`]'s own "one fragment, not
2103/// a whole document" entry point. `depth` means the *class's* own depth
2104/// (matching `print_object_entry`'s own convention exactly), so the method
2105/// itself lands at `depth + 1`, its own body at `depth + 2`. #1359's own
2106/// real need: `emit_provider`'s own class wrapper stays hand-written text
2107/// (each method's own body needs a per-method source-map sub-builder/
2108/// `merge`, and the wrapper's own real spacing — no blank line between
2109/// methods — genuinely differs from [`TsDecl::Class`]'s own "one blank
2110/// line before each method" policy, `events_fanout.rs`'s real convention,
2111/// #1317), so each real method prints through here directly into that
2112/// still-hand-written wrapper. Deliberately no automatic blank-line
2113/// insertion of its own — the same "caller controls spacing" contract
2114/// `print_object_entry` already established.
2115pub fn print_class_method(method: &TsClassMethod, depth: usize) -> String {
2116    let mut out = String::new();
2117    render_class_method(&mut out, method, depth, None);
2118    out
2119}
2120
2121/// [`print_class_method`]'s own sibling (#1477): identical rendering, but
2122/// merges any of `method.body`'s own direct-child `nested_map`s into `map`
2123/// as they print, at the real print-time offset — no reverse-engineering
2124/// the offset from the returned text afterward, the way `bynk-emit`'s own
2125/// `emit_class_method_and_merge_source_map` (`emitter/emit.rs`) has to
2126/// today. Kept as a separate function, not an added parameter on
2127/// `print_class_method` itself, so every existing caller's own call sites
2128/// are untouched — this is a strict addition.
2129///
2130/// Appends to the caller's own `out`, the same "must be the real buffer,
2131/// not a throwaway local one" reasoning [`print_stmt_and_merge`]'s own doc
2132/// explains in full. `source_id`: see this crate's own private `MergeTarget`'s own doc.
2133pub fn print_class_method_and_merge(
2134    out: &mut String,
2135    method: &TsClassMethod,
2136    depth: usize,
2137    map: &mut SourceMapBuilder,
2138    source_id: usize,
2139) {
2140    render_class_method(out, method, depth, Some(MergeTarget { map, source_id }));
2141}
2142
2143/// The one real per-method render, shared by [`print_class_method`] and
2144/// [`TsDecl::Class`]'s own methods-loop arm — review of #1360, finding 4:
2145/// keeping two independent copies of this shape was exactly the drift risk
2146/// this track exists to design against (two printers disagreeing about a
2147/// formatting convention). `depth` means the *class's* own depth, matching
2148/// both callers' own convention.
2149///
2150/// Review of #1360, finding 1: a `TsStmtKind::Raw` method body has no
2151/// indent of its own — its text is captured pre-indented at a fixed
2152/// absolute depth by its own caller — so it only renders correctly when
2153/// `depth` is `0`, the same hazard `render_multiline_object_entry`'s own
2154/// identical `debug_assert!` guards (review of #1338 finding 3, relocated
2155/// by review of #1340 finding 1). `print_class_method`'s own doc frames it
2156/// as a general fragment entry point, so a future caller at a non-zero
2157/// depth is the expected case this guards against, not a hypothetical.
2158///
2159/// `map`: see [`render_block_stmts`]'s own doc (#1477) — forwarded
2160/// unchanged to `method.body`'s own rendering.
2161fn render_class_method(
2162    out: &mut String,
2163    method: &TsClassMethod,
2164    depth: usize,
2165    map: Option<MergeTarget<'_>>,
2166) {
2167    debug_assert!(
2168        depth == 0 || !contains_raw(&method.body),
2169        "render_class_method: a Raw method body's own baked-in indent only matches depth 0"
2170    );
2171    if let Some(text) = &method.doc {
2172        render_doc_comment(out, text, depth + 1);
2173    }
2174    out.push_str(&indent(depth + 1));
2175    if method.private {
2176        out.push_str("private ");
2177    }
2178    if method.is_async {
2179        out.push_str("async ");
2180    }
2181    out.push_str(&method.name);
2182    out.push('(');
2183    render_params(out, &method.params);
2184    out.push(')');
2185    if let Some(rt) = &method.return_type {
2186        out.push_str(": ");
2187        render_type(out, rt);
2188    }
2189    render_block_stmts(out, &method.body, depth + 1, map);
2190    out.push('\n');
2191}
2192
2193fn render_params(out: &mut String, params: &[TsParam]) {
2194    for (i, p) in params.iter().enumerate() {
2195        if i > 0 {
2196            out.push_str(", ");
2197        }
2198        out.push_str(&p.name);
2199        if p.optional {
2200            out.push('?');
2201        }
2202        if let Some(ty) = &p.ty {
2203            out.push_str(": ");
2204            render_type(out, ty);
2205        }
2206    }
2207}
2208
2209fn render_decl(out: &mut String, decl: &TsDecl, depth: usize, map: Option<MergeTarget<'_>>) {
2210    // `render_decl_body`'s own `TsDecl::Export` arm already writes
2211    // `"export "` before recursing into the inner declaration — the
2212    // `if let Export` special case this replaced produced byte-identical
2213    // output by duplicating that same logic one layer up (review of
2214    // #1314, smaller note: the two branches were provably the same for
2215    // every input, so only one needs to exist).
2216    out.push_str(&indent(depth));
2217    render_decl_body(out, decl, depth, map);
2218}
2219
2220/// The declaration's own text, with no leading indent — [`render_decl`]
2221/// writes the indent (and, for `Export`, the `export ` keyword) once, then
2222/// hands off here so `Export(inner)` doesn't duplicate `inner`'s own
2223/// leading whitespace.
2224///
2225/// `map`: see [`render_block_stmts`]'s own doc (#1477) — reaches a
2226/// `TsDecl::Function`'s own body, and a `TsDecl::Class`'s own constructor/
2227/// methods, unchanged; every other arm ignores it (no body of its own to
2228/// carry a `nested_map`).
2229fn render_decl_body(
2230    out: &mut String,
2231    decl: &TsDecl,
2232    depth: usize,
2233    mut map: Option<MergeTarget<'_>>,
2234) {
2235    match decl {
2236        TsDecl::Import {
2237            type_only,
2238            names,
2239            from,
2240        } => {
2241            out.push_str(if *type_only {
2242                "import type { "
2243            } else {
2244                "import { "
2245            });
2246            out.push_str(&names.join(", "));
2247            out.push_str(" } from \"");
2248            out.push_str(from);
2249            out.push_str("\";\n");
2250        }
2251        TsDecl::ImportNamespace {
2252            type_only,
2253            alias,
2254            from,
2255        } => {
2256            out.push_str(if *type_only {
2257                "import type * as "
2258            } else {
2259                "import * as "
2260            });
2261            out.push_str(alias);
2262            out.push_str(" from \"");
2263            out.push_str(from);
2264            out.push_str("\";\n");
2265        }
2266        TsDecl::ImportDefault { alias, from } => {
2267            out.push_str("import ");
2268            out.push_str(alias);
2269            out.push_str(" from \"");
2270            out.push_str(from);
2271            out.push_str("\";\n");
2272        }
2273        TsDecl::ReExport { names, from } => {
2274            out.push_str("export { ");
2275            out.push_str(&names.join(", "));
2276            out.push_str(" } from \"");
2277            out.push_str(from);
2278            out.push_str("\";\n");
2279        }
2280        TsDecl::ExportNames { names } => {
2281            out.push_str("export { ");
2282            out.push_str(&names.join(", "));
2283            out.push_str(" };\n");
2284        }
2285        TsDecl::ReExportAll { from } => {
2286            out.push_str("export * from \"");
2287            out.push_str(from);
2288            out.push_str("\";\n");
2289        }
2290        TsDecl::Export(inner) => {
2291            out.push_str("export ");
2292            render_decl_body(out, inner, depth, map);
2293        }
2294        TsDecl::Interface {
2295            name,
2296            type_params,
2297            members,
2298        } => {
2299            out.push_str("interface ");
2300            out.push_str(name);
2301            render_bare_generics(out, type_params);
2302            out.push_str(" {\n");
2303            for member in members {
2304                // #1357: a `Method` member's own `doc` renders here, not in
2305                // `render_type_member` itself — that function has no
2306                // `depth` to give `render_doc_comment`, the same "doc lives
2307                // at the call site that carries depth" split
2308                // `render_multiline_object_entry`'s own `Method` arm
2309                // already uses for the identical field.
2310                if let TsTypeMember::Method {
2311                    doc: Some(text), ..
2312                } = member
2313                {
2314                    render_doc_comment(out, text, depth + 1);
2315                }
2316                out.push_str(&indent(depth + 1));
2317                render_type_member(out, member);
2318                out.push_str(";\n");
2319            }
2320            out.push_str(&indent(depth));
2321            out.push_str("}\n");
2322        }
2323        TsDecl::ConstDecl { name, ty, init } => {
2324            out.push_str("const ");
2325            out.push_str(name);
2326            if let Some(ty) = ty {
2327                out.push_str(": ");
2328                render_type(out, ty);
2329            }
2330            out.push_str(" = ");
2331            render_stmt_level_expr(out, init, depth);
2332            out.push_str(";\n");
2333        }
2334        TsDecl::Class {
2335            name,
2336            fields,
2337            constructor,
2338            methods,
2339        } => {
2340            out.push_str("class ");
2341            out.push_str(name);
2342            out.push_str(" {\n");
2343            for f in fields {
2344                out.push_str(&indent(depth + 1));
2345                if f.private {
2346                    out.push_str("private ");
2347                }
2348                out.push_str(&f.name);
2349                out.push_str(": ");
2350                render_type(out, &f.ty);
2351                out.push_str(";\n");
2352            }
2353            // Readability policy (this module's own doc): no blank line
2354            // between fields and the constructor; one blank line before
2355            // each method — `events_fanout.rs`'s own real class spacing.
2356            let mut wrote_member = !fields.is_empty();
2357            if let Some(ctor) = constructor {
2358                out.push_str(&indent(depth + 1));
2359                out.push_str("constructor(");
2360                render_params(out, &ctor.params);
2361                out.push(')');
2362                render_block_stmts(
2363                    out,
2364                    &ctor.body,
2365                    depth + 1,
2366                    map.as_mut().map(|t| t.reborrow()),
2367                );
2368                out.push('\n');
2369                wrote_member = true;
2370            }
2371            for m in methods {
2372                if wrote_member {
2373                    out.push('\n');
2374                }
2375                render_class_method(out, m, depth, map.as_mut().map(|t| t.reborrow()));
2376                wrote_member = true;
2377            }
2378            out.push_str(&indent(depth));
2379            out.push_str("}\n");
2380        }
2381        TsDecl::Function {
2382            name,
2383            generics,
2384            params,
2385            return_type,
2386            body,
2387            is_async,
2388            inline,
2389        } => {
2390            if *is_async {
2391                out.push_str("async ");
2392            }
2393            out.push_str("function ");
2394            out.push_str(name);
2395            render_bare_generics(out, generics);
2396            out.push('(');
2397            render_params(out, params);
2398            out.push(')');
2399            if let Some(rt) = return_type {
2400                out.push_str(": ");
2401                render_type(out, rt);
2402            }
2403            if *inline {
2404                out.push(' ');
2405                render_inline_block(out, body);
2406            } else {
2407                render_block_stmts(out, body, depth, map);
2408                out.push('\n');
2409            }
2410        }
2411        TsDecl::TypeAlias {
2412            name,
2413            type_params,
2414            ty,
2415        } => {
2416            out.push_str("type ");
2417            out.push_str(name);
2418            render_bare_generics(out, type_params);
2419            // #1339: a multiline Union's own first rendered character is
2420            // its first member's leading indent, not a newline — the `=`
2421            // itself must be followed directly by `\n` here (no space
2422            // before it), matching the pre-conversion `writeln!(out,
2423            // "export type {name}{params} =")` line's own exact bytes;
2424            // every other `ty` keeps the ordinary `" = "` (space both
2425            // sides) ordinary single-line form.
2426            if matches!(
2427                ty,
2428                TsType::Union {
2429                    multiline: true,
2430                    ..
2431                }
2432            ) {
2433                out.push_str(" =\n");
2434            } else {
2435                out.push_str(" = ");
2436            }
2437            render_type(out, ty);
2438            out.push_str(";\n");
2439        }
2440        TsDecl::ExportDefault(expr) => {
2441            out.push_str("export default ");
2442            render_stmt_level_expr(out, expr, depth);
2443            out.push_str(";\n");
2444        }
2445        TsDecl::DeclareConst { name, ty } => {
2446            out.push_str("declare const ");
2447            out.push_str(name);
2448            out.push_str(": ");
2449            render_type(out, ty);
2450            out.push_str(";\n");
2451        }
2452    }
2453}
2454
2455/// `{ <stmts> }` for a constructor/method body — same shape as
2456/// [`render_block_body`] but over a plain `&[TsStmt]` (a `TsClassCtor`/
2457/// `TsClassMethod`'s own `body` field, not a boxed `TsStmt`), and always
2458/// followed by the caller's own `\n` (constructor/method declarations, not
2459/// a same-line `try`/`catch` continuation).
2460///
2461/// `map: Some(_)` when the caller wants a `nested_map`-bearing statement
2462/// reachable from `stmts` merged into a module source map as it prints
2463/// (#1477) — see [`crate::program::TsStmt::nested_map`]'s own doc for the
2464/// problem this solves. Reborrowed once per child and forwarded to
2465/// [`render_stmt`], which does the actual check — this function itself
2466/// holds none of that logic.
2467fn render_block_stmts(
2468    out: &mut String,
2469    stmts: &[TsStmt],
2470    depth: usize,
2471    mut map: Option<MergeTarget<'_>>,
2472) {
2473    out.push_str(" {\n");
2474    for s in stmts {
2475        render_stmt(out, s, depth + 1, map.as_mut().map(|t| t.reborrow()));
2476    }
2477    out.push_str(&indent(depth));
2478    out.push('}');
2479}
2480
2481#[cfg(test)]
2482mod tests {
2483    use super::*;
2484    use crate::program::{TsClassCtor, TsClassField, TsClassMethod, TsSwitchCase, VerbatimOrigin};
2485    use bynk_syntax::span::Span;
2486
2487    /// Pins the readability policy's statement-separation guarantee (R7.5,
2488    /// this module's own doc): every statement starts on its own generated
2489    /// line.
2490    #[test]
2491    fn prints_every_statement_in_order() {
2492        let mut program = TsProgram::new();
2493        program.push(TsStmt::verbatim(
2494            VerbatimOrigin::Contracts,
2495            "const a = 1;\n",
2496            None,
2497        ));
2498        program.push(TsStmt::verbatim(
2499            VerbatimOrigin::Secrets,
2500            "const b = 2;\n",
2501            None,
2502        ));
2503        let printed = print(&program, "x.bynk", "", "x.ts");
2504        assert_eq!(printed.text, "const a = 1;\nconst b = 2;\n");
2505    }
2506
2507    /// Pins the readability policy's statement-separation guarantee (R7.5,
2508    /// this module's own doc) in its sharpest form: nothing requires
2509    /// `Verbatim` text to be newline-terminated — the printer owns line
2510    /// structure (R7.3), so it's the one that guarantees two statements
2511    /// never share a generated line, not a `TsStmt::verbatim` caller
2512    /// obligation nobody enforces (review of #1308, finding 2).
2513    #[test]
2514    fn a_statement_missing_its_own_trailing_newline_still_gets_its_own_line() {
2515        let mut program = TsProgram::new();
2516        program.push(TsStmt::verbatim(
2517            VerbatimOrigin::Contracts,
2518            "const a = 1;",
2519            None,
2520        ));
2521        program.push(TsStmt::verbatim(
2522            VerbatimOrigin::Secrets,
2523            "const b = 2;",
2524            None,
2525        ));
2526        let printed = print(&program, "x.bynk", "", "x.ts");
2527        assert_eq!(printed.text, "const a = 1;\nconst b = 2;\n");
2528    }
2529
2530    /// Production `Verbatim` content is rarely one line — `VerbatimOrigin::
2531    /// NotYetConverted` (P7.6) wraps a whole generated document per
2532    /// statement, so one statement routinely spans dozens of generated
2533    /// lines. Neither of the two tests above exercised that shape (review
2534    /// of #1312, finding 1) — this one confirms the readability policy's
2535    /// real invariant (the *next* statement always starts on a fresh line)
2536    /// holds for multi-line content too, not just the single-line text
2537    /// those tests happened to use.
2538    #[test]
2539    fn a_multi_line_statement_still_starts_the_next_statement_on_a_fresh_line() {
2540        let mut program = TsProgram::new();
2541        program.push(TsStmt::verbatim(
2542            VerbatimOrigin::Contracts,
2543            "function a() {\n  return 1;\n}\n",
2544            None,
2545        ));
2546        program.push(TsStmt::verbatim(
2547            VerbatimOrigin::Secrets,
2548            "const b = 2;\n",
2549            None,
2550        ));
2551        let printed = print(&program, "x.bynk", "", "x.ts");
2552        assert_eq!(
2553            printed.text,
2554            "function a() {\n  return 1;\n}\nconst b = 2;\n"
2555        );
2556    }
2557
2558    /// `print` decides whether to append a trailing newline by checking the
2559    /// whole *buffer*, not the statement's own text (review of #1312,
2560    /// finding 2) — an empty statement's text changes nothing, so whether
2561    /// it gets a line depends entirely on what's already in the buffer.
2562    /// First in a program, the buffer is still empty, so the check fires
2563    /// and the output opens with a blank line. Pinned here so this edge
2564    /// stays a known, deliberate reading of the code rather than an
2565    /// untested accident.
2566    #[test]
2567    fn an_empty_statement_first_in_the_program_yields_a_leading_blank_line() {
2568        let mut program = TsProgram::new();
2569        program.push(TsStmt::verbatim(VerbatimOrigin::Contracts, "", None));
2570        program.push(TsStmt::verbatim(
2571            VerbatimOrigin::Secrets,
2572            "const b = 2;\n",
2573            None,
2574        ));
2575        let printed = print(&program, "x.bynk", "", "x.ts");
2576        assert_eq!(printed.text, "\nconst b = 2;\n");
2577    }
2578
2579    /// The other half of the buffer-vs-statement-text distinction (review
2580    /// of #1312, finding 2): an empty statement following one that already
2581    /// ended its own text in `\n` contributes no line at all — the buffer
2582    /// already ends in `\n`, so the check doesn't fire, and the empty
2583    /// statement's own (non-)content and the next statement's text end up
2584    /// on what reads as one generated line for the next statement alone.
2585    #[test]
2586    fn an_empty_statement_after_a_newline_terminated_one_contributes_no_line_of_its_own() {
2587        let mut program = TsProgram::new();
2588        program.push(TsStmt::verbatim(
2589            VerbatimOrigin::Contracts,
2590            "const a = 1;\n",
2591            None,
2592        ));
2593        program.push(TsStmt::verbatim(VerbatimOrigin::Secrets, "", None));
2594        program.push(TsStmt::verbatim(
2595            VerbatimOrigin::RuntimeUse,
2596            "const c = 3;\n",
2597            None,
2598        ));
2599        let printed = print(&program, "x.bynk", "", "x.ts");
2600        assert_eq!(printed.text, "const a = 1;\nconst c = 3;\n");
2601    }
2602
2603    #[test]
2604    fn no_spans_at_all_means_no_source_map() {
2605        let mut program = TsProgram::new();
2606        program.push(TsStmt::verbatim(VerbatimOrigin::Contracts, "x;\n", None));
2607        let printed = print(&program, "x.bynk", "let x = 1\n", "x.ts");
2608        assert_eq!(printed.source_map, None);
2609    }
2610
2611    /// The property that actually matters (R7.4): the printer's own
2612    /// buffer-position bookkeeping is correct — a checkpoint recorded from a
2613    /// later statement's span still resolves to the *generated* line that
2614    /// statement's own text landed on, not (as the old splice-based
2615    /// mechanism could get wrong, #4) some other buffer's offset.
2616    ///
2617    /// Review of #1308, finding 3: the original version of this test
2618    /// asserted only that the source map's `sources` array was present —
2619    /// true even with the second statement's checkpoint silently dropped,
2620    /// exactly the failure this doc comment claims is ruled out. Asserting
2621    /// full equality against a map built independently (replicating the
2622    /// printer's own record-then-write order by hand, not calling `print`)
2623    /// actually pins both statements' own offsets.
2624    #[test]
2625    fn each_statements_span_resolves_to_its_own_generated_line() {
2626        let source = "let a = 1\nlet b = 2\n";
2627        let off_a = source.find("let a").unwrap();
2628        let off_b = source.find("let b").unwrap();
2629        let span_a = Span::new(off_a, off_a + 5);
2630        let span_b = Span::new(off_b, off_b + 5);
2631        let text_a = "const a = 1;\n";
2632        let text_b = "const b = 2;\n";
2633        let mut program = TsProgram::new();
2634        program.push(TsStmt::verbatim(
2635            VerbatimOrigin::Contracts,
2636            text_a,
2637            Some(span_a),
2638        ));
2639        program.push(TsStmt::verbatim(
2640            VerbatimOrigin::Secrets,
2641            text_b,
2642            Some(span_b),
2643        ));
2644
2645        let printed = print(&program, "x.bynk", source, "x.ts");
2646
2647        let mut expected_map = SourceMapBuilder::new();
2648        expected_map.add_source("x.bynk", source);
2649        expected_map.record(0, span_a);
2650        expected_map.record(text_a.len(), span_b);
2651        let expected = expected_map.to_v3(&printed.text, "x.ts");
2652
2653        assert_eq!(printed.source_map, expected);
2654    }
2655
2656    // -- #1477: nested source-map checkpoints on an opaque body blob. --
2657
2658    /// `print_stmt_and_merge` — the `TsDecl::Function` shape (`emit_free_fn`'s
2659    /// own real site): a `Raw` body statement's own `nested_map` merges into
2660    /// the caller's map at the real print-time offset, appended into a
2661    /// buffer that already has *other* content first (`base != 0`) — proving
2662    /// this reads the caller's own real buffer position, not a fresh
2663    /// throwaway one starting at 0 (the bug this function's own first draft
2664    /// had, caught before merge: returning a `String` computes the offset
2665    /// against that empty local buffer, silently wrong the moment the
2666    /// caller splices the result anywhere but the very start of its own
2667    /// `out`).
2668    #[test]
2669    fn print_stmt_and_merge_merges_a_function_bodys_own_nested_checkpoint() {
2670        let source = "fn f() { return 1 }\n";
2671        let ret_off = source.find("return 1").unwrap();
2672        let ret_span = Span::new(ret_off, ret_off + 8);
2673
2674        let body_text = "  return 1;\n";
2675        let mut nested = SourceMapBuilder::new();
2676        nested.record(0, ret_span);
2677
2678        let mut raw = TsStmt::raw(body_text, None);
2679        raw.nested_map = Some(nested);
2680
2681        let func = TsStmt::decl(
2682            TsDecl::Function {
2683                name: "f".to_string(),
2684                generics: Vec::new(),
2685                params: Vec::new(),
2686                return_type: None,
2687                body: vec![raw],
2688                is_async: false,
2689                inline: false,
2690            },
2691            None,
2692        );
2693
2694        let mut out = String::from("// header\n");
2695        let mut map = SourceMapBuilder::new();
2696        map.add_source("x.bynk", source);
2697
2698        print_stmt_and_merge(&mut out, &func, 0, &mut map, 0);
2699
2700        assert_eq!(out, "// header\nfunction f() {\n  return 1;\n}\n");
2701        let base = out.find(body_text).unwrap();
2702
2703        let mut expected = SourceMapBuilder::new();
2704        expected.add_source("x.bynk", source);
2705        expected.record(base, ret_span);
2706        assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
2707    }
2708
2709    /// `print_class_method_and_merge` — the `TsClassMethod` shape
2710    /// (`emit_provider`/`emit_agent`'s own real site, today reverse-engineered
2711    /// by `emit_class_method_and_merge_source_map`'s own string-matching).
2712    /// Same non-zero-`base` proof as the `TsDecl::Function` test above.
2713    #[test]
2714    fn print_class_method_and_merge_merges_a_methods_own_nested_checkpoint() {
2715        let source = "on call m() { commit }\n";
2716        let commit_off = source.find("commit").unwrap();
2717        let commit_span = Span::new(commit_off, commit_off + 6);
2718
2719        let body_text = "    this.commit();\n";
2720        let mut nested = SourceMapBuilder::new();
2721        nested.record(0, commit_span);
2722
2723        let mut raw = TsStmt::raw(body_text, None);
2724        raw.nested_map = Some(nested);
2725
2726        let method = TsClassMethod {
2727            name: "m".to_string(),
2728            private: false,
2729            is_async: false,
2730            params: Vec::new(),
2731            return_type: None,
2732            doc: None,
2733            body: vec![raw],
2734        };
2735
2736        let mut out = String::from("class C {\n");
2737        let mut map = SourceMapBuilder::new();
2738        map.add_source("x.bynk", source);
2739
2740        print_class_method_and_merge(&mut out, &method, 0, &mut map, 0);
2741
2742        let base = out.find(body_text).unwrap();
2743        let mut expected = SourceMapBuilder::new();
2744        expected.add_source("x.bynk", source);
2745        expected.record(base, commit_span);
2746        assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
2747    }
2748
2749    /// `print_object_entry_and_merge` — the `TsObjectEntry::Method` shape
2750    /// (`emit_service`'s own real per-handler site): identical mechanism,
2751    /// exercised through its own, third public entry point.
2752    #[test]
2753    fn print_object_entry_and_merge_merges_a_methods_own_nested_checkpoint() {
2754        let source = "on call h() { reply }\n";
2755        let reply_off = source.find("reply").unwrap();
2756        let reply_span = Span::new(reply_off, reply_off + 5);
2757
2758        let body_text = "    return reply();\n";
2759        let mut nested = SourceMapBuilder::new();
2760        nested.record(0, reply_span);
2761
2762        let mut raw = TsStmt::raw(body_text, None);
2763        raw.nested_map = Some(nested);
2764
2765        let entry = TsObjectEntry::Method {
2766            name: "h".to_string(),
2767            is_async: false,
2768            generics: Vec::new(),
2769            params: Vec::new(),
2770            return_type: None,
2771            doc: None,
2772            inline: false,
2773            body: vec![raw],
2774        };
2775
2776        let mut out = String::from("export const svc = {\n");
2777        let mut map = SourceMapBuilder::new();
2778        map.add_source("x.bynk", source);
2779
2780        print_object_entry_and_merge(&mut out, &entry, 0, &mut map, 0);
2781
2782        let base = out.find(body_text).unwrap();
2783        let mut expected = SourceMapBuilder::new();
2784        expected.add_source("x.bynk", source);
2785        expected.record(base, reply_span);
2786        assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
2787    }
2788
2789    /// `print()`'s own top-level loop (#1477): a real `TsProgram` containing
2790    /// a `Decl`-kinded top-level statement whose own body carries a
2791    /// `nested_map` merges it automatically — no `bynk-emit` change needed
2792    /// to benefit once a real construction site sets the field, since
2793    /// `print()` already threads its own module map through `render_stmt`
2794    /// for every top-level statement.
2795    #[test]
2796    fn print_merges_a_top_level_functions_own_nested_checkpoint_automatically() {
2797        let source = "fn f() { return 1 }\n";
2798        let ret_off = source.find("return 1").unwrap();
2799        let ret_span = Span::new(ret_off, ret_off + 8);
2800
2801        let body_text = "  return 1;\n";
2802        let mut nested = SourceMapBuilder::new();
2803        nested.record(0, ret_span);
2804
2805        let mut raw = TsStmt::raw(body_text, None);
2806        raw.nested_map = Some(nested);
2807
2808        let mut program = TsProgram::new();
2809        program.push(TsStmt::decl(
2810            TsDecl::Function {
2811                name: "f".to_string(),
2812                generics: Vec::new(),
2813                params: Vec::new(),
2814                return_type: None,
2815                body: vec![raw],
2816                is_async: false,
2817                inline: false,
2818            },
2819            None,
2820        ));
2821
2822        let printed = print(&program, "x.bynk", source, "x.ts");
2823        let base = printed.text.find(body_text).unwrap();
2824
2825        let mut expected = SourceMapBuilder::new();
2826        expected.add_source("x.bynk", source);
2827        expected.record(base, ret_span);
2828        assert_eq!(printed.source_map, expected.to_v3(&printed.text, "x.ts"));
2829    }
2830
2831    /// Review of #1488, finding 1: `source_id` must reach the merge, not be
2832    /// hardcoded — a multi-source module map (a test module aggregating
2833    /// several `.bynk` files, `tests_emit.rs:537`/`:1936`'s own real shape)
2834    /// tags a nested checkpoint to the *wrong* source silently if the merge
2835    /// always assumes index 0.
2836    ///
2837    /// Decisive by construction, not just an assertion on the map's own
2838    /// `sources` list (which would pass either way — every registered
2839    /// source name appears there regardless of which one a checkpoint
2840    /// actually resolves against, `to_v3`'s own doc/body confirms `sources`/
2841    /// `sourcesContent` iterate the full registered list unconditionally):
2842    /// source 0's own text is a single short line, too short to contain the
2843    /// real checkpoint's span. If the merge resolves the checkpoint against
2844    /// source 0 (the bug this test exists to catch), `to_v3`'s own
2845    /// out-of-range guard (`span.start > text.len()`) silently drops it and
2846    /// the whole map comes back `None` (its only checkpoint). Only
2847    /// resolving against the real source (id 1, correctly threaded through)
2848    /// keeps the span in range and produces a real map.
2849    #[test]
2850    fn print_stmt_and_merge_tags_a_nested_checkpoint_to_the_given_source_id() {
2851        let primary = "x\n"; // deliberately shorter than `ret_span`'s own offset
2852        let secondary = "fn f() { return 1 }\n";
2853        let ret_off = secondary.find("return 1").unwrap();
2854        let ret_span = Span::new(ret_off, ret_off + 8);
2855        assert!(
2856            ret_span.start > primary.len(),
2857            "sanity: the span must fall outside source 0's own short text"
2858        );
2859
2860        let body_text = "  return 1;\n";
2861        let mut nested = SourceMapBuilder::new();
2862        nested.record(0, ret_span);
2863
2864        let mut raw = TsStmt::raw(body_text, None);
2865        raw.nested_map = Some(nested);
2866
2867        let func = TsStmt::decl(
2868            TsDecl::Function {
2869                name: "f".to_string(),
2870                generics: Vec::new(),
2871                params: Vec::new(),
2872                return_type: None,
2873                body: vec![raw],
2874                is_async: false,
2875                inline: false,
2876            },
2877            None,
2878        );
2879
2880        let mut out = String::new();
2881        let mut map = SourceMapBuilder::new();
2882        map.add_source("primary.bynk", primary); // id 0
2883        let secondary_id = map.add_source("secondary.bynk", secondary); // id 1
2884        assert_eq!(secondary_id, 1);
2885
2886        print_stmt_and_merge(&mut out, &func, 0, &mut map, secondary_id);
2887
2888        assert!(
2889            map.to_v3(&out, "x.ts").is_some(),
2890            "the checkpoint resolved against the wrong source (id 0) and was silently dropped"
2891        );
2892    }
2893
2894    /// #1486's own sibling gap to the one above, but in [`print_multi_
2895    /// source`]'s own automatic top-level loop rather than the manual
2896    /// `print_stmt_and_merge` entry point: a real `TsProgram` built for a
2897    /// multi-source module (a test/integration module aggregating several
2898    /// `.bynk` fragment files, one per case) needs each case's own
2899    /// top-level statement merged against *its own* registered source, not
2900    /// the module's primary one (id 0) — `nested_map_source_id`'s own doc
2901    /// comment. Decisive the same way the sibling test above is: source 0's
2902    /// text is too short to contain the real span, so a wrong-source merge
2903    /// silently drops the checkpoint (`to_v3`'s own out-of-range guard) and
2904    /// the map comes back missing it.
2905    #[test]
2906    fn print_multi_source_honours_a_top_level_statements_own_nested_map_source_id() {
2907        let primary = "x\n";
2908        let secondary = "fn f() { return 1 }\n";
2909        let ret_off = secondary.find("return 1").unwrap();
2910        let ret_span = Span::new(ret_off, ret_off + 8);
2911        assert!(
2912            ret_span.start > primary.len(),
2913            "sanity: the span must fall outside source 0's own short text"
2914        );
2915
2916        let body_text = "  return 1;\n";
2917        let mut nested = SourceMapBuilder::new();
2918        nested.record(0, ret_span);
2919
2920        let mut raw = TsStmt::raw(body_text, None);
2921        raw.nested_map = Some(nested);
2922
2923        let mut func = TsStmt::decl(
2924            TsDecl::Function {
2925                name: "f".to_string(),
2926                generics: Vec::new(),
2927                params: Vec::new(),
2928                return_type: None,
2929                body: vec![raw],
2930                is_async: false,
2931                inline: false,
2932            },
2933            None,
2934        );
2935        func.nested_map_source_id = 1;
2936
2937        let mut program = TsProgram::new();
2938        program.push(func);
2939
2940        let mut map = SourceMapBuilder::new();
2941        map.add_source("primary.bynk", primary);
2942        map.add_source("secondary.bynk", secondary);
2943        let printed = print_multi_source(&program, map, "x.ts");
2944        assert!(
2945            printed.source_map.is_some(),
2946            "the checkpoint resolved against the wrong source (id 0) and was silently dropped"
2947        );
2948    }
2949
2950    /// Review of #1488, finding 2: a `nested_map` set on the statement handed
2951    /// directly to `print`/`print_stmt_and_merge` itself — not nested inside
2952    /// a `Decl` body — must still merge, not silently vanish. The first
2953    /// version of this mechanism only checked a body's own *direct
2954    /// children* (inside `render_block_stmts`), missing this exact case.
2955    #[test]
2956    fn print_stmt_and_merge_merges_a_bare_top_level_raw_statements_own_nested_checkpoint() {
2957        let source = "contract seal() {}\n";
2958        let off = source.find("contract").unwrap();
2959        let span = Span::new(off, off + 8);
2960
2961        let text = "seal();\n";
2962        let mut nested = SourceMapBuilder::new();
2963        nested.record(0, span);
2964
2965        let mut raw = TsStmt::raw(text, None);
2966        raw.nested_map = Some(nested);
2967
2968        let mut out = String::from("// header\n");
2969        let mut map = SourceMapBuilder::new();
2970        map.add_source("x.bynk", source);
2971
2972        print_stmt_and_merge(&mut out, &raw, 0, &mut map, 0);
2973
2974        let base = out.find(text).unwrap();
2975        let mut expected = SourceMapBuilder::new();
2976        expected.add_source("x.bynk", source);
2977        expected.record(base, span);
2978        assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
2979    }
2980
2981    /// Review of #1488, finding 3 (part 1): the merge's own line-rebasing
2982    /// arithmetic (`out_line = base_line + body_line`) is exercised for
2983    /// real, not just the degenerate `body_line == 0` case every other test
2984    /// here uses — a two-statement body with the checkpoint on the
2985    /// *second* line.
2986    #[test]
2987    fn print_stmt_and_merge_rebases_a_checkpoint_on_a_bodys_second_line() {
2988        let source = "fn f() { let a = 1\n  return a }\n";
2989        let ret_off = source.find("return a").unwrap();
2990        let ret_span = Span::new(ret_off, ret_off + 8);
2991
2992        let body_text = "  const a = 1;\n  return a;\n";
2993        let second_line_off = body_text.find("return a;").unwrap();
2994        let mut nested = SourceMapBuilder::new();
2995        nested.record(second_line_off, ret_span);
2996
2997        let mut raw = TsStmt::raw(body_text, None);
2998        raw.nested_map = Some(nested);
2999
3000        let func = TsStmt::decl(
3001            TsDecl::Function {
3002                name: "f".to_string(),
3003                generics: Vec::new(),
3004                params: Vec::new(),
3005                return_type: None,
3006                body: vec![raw],
3007                is_async: false,
3008                inline: false,
3009            },
3010            None,
3011        );
3012
3013        let mut out = String::from("// header\n");
3014        let mut map = SourceMapBuilder::new();
3015        map.add_source("x.bynk", source);
3016
3017        print_stmt_and_merge(&mut out, &func, 0, &mut map, 0);
3018
3019        let base_of_body = out.find(body_text).unwrap();
3020        let base_of_second_line = out.find("  return a;\n").unwrap();
3021        assert!(
3022            base_of_second_line > base_of_body,
3023            "sanity: second line is after the first"
3024        );
3025
3026        let mut expected = SourceMapBuilder::new();
3027        expected.add_source("x.bynk", source);
3028        expected.record(base_of_second_line, ret_span);
3029        assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
3030    }
3031
3032    /// Review of #1488, finding 3 (part 2): `TsDecl::Class`'s own
3033    /// constructor-then-methods sequence is the one real place `map` is
3034    /// reborrowed across more than one consumer — a class with a
3035    /// `nested_map`-bearing constructor body *and* a `nested_map`-bearing
3036    /// method body proves both reborrows land correctly, not just the
3037    /// first one. `emit_provider`/`emit_agent` (both class-shaped) are
3038    /// exactly the real callers this slice unblocks.
3039    #[test]
3040    fn a_classs_constructor_and_method_both_merge_their_own_nested_checkpoints() {
3041        let source = "on new() { init }\non call m() { commit }\n";
3042        let init_off = source.find("init").unwrap();
3043        let init_span = Span::new(init_off, init_off + 4);
3044        let commit_off = source.find("commit").unwrap();
3045        let commit_span = Span::new(commit_off, commit_off + 6);
3046
3047        let ctor_body_text = "    this.init();\n";
3048        let mut ctor_nested = SourceMapBuilder::new();
3049        ctor_nested.record(0, init_span);
3050        let mut ctor_raw = TsStmt::raw(ctor_body_text, None);
3051        ctor_raw.nested_map = Some(ctor_nested);
3052
3053        let method_body_text = "    this.commit();\n";
3054        let mut method_nested = SourceMapBuilder::new();
3055        method_nested.record(0, commit_span);
3056        let mut method_raw = TsStmt::raw(method_body_text, None);
3057        method_raw.nested_map = Some(method_nested);
3058
3059        let class = TsDecl::Class {
3060            name: "C".to_string(),
3061            fields: Vec::new(),
3062            constructor: Some(TsClassCtor {
3063                params: Vec::new(),
3064                body: vec![ctor_raw],
3065            }),
3066            methods: vec![TsClassMethod {
3067                name: "m".to_string(),
3068                private: false,
3069                is_async: false,
3070                params: Vec::new(),
3071                return_type: None,
3072                doc: None,
3073                body: vec![method_raw],
3074            }],
3075        };
3076
3077        let mut out = String::new();
3078        let mut map = SourceMapBuilder::new();
3079        map.add_source("x.bynk", source);
3080
3081        print_stmt_and_merge(&mut out, &TsStmt::decl(class, None), 0, &mut map, 0);
3082
3083        let ctor_base = out.find(ctor_body_text).unwrap();
3084        let method_base = out.find(method_body_text).unwrap();
3085
3086        let mut expected = SourceMapBuilder::new();
3087        expected.add_source("x.bynk", source);
3088        expected.record(ctor_base, init_span);
3089        expected.record(method_base, commit_span);
3090        assert_eq!(map.to_v3(&out, "x.ts"), expected.to_v3(&out, "x.ts"));
3091    }
3092
3093    // -- P7.8 (#1313): real node rendering. --
3094
3095    #[test]
3096    fn prints_a_const_statement_with_a_destructured_binding_and_a_type_cast() {
3097        let mut program = TsProgram::new();
3098        program.push(TsStmt::const_stmt(
3099            TsBindingName::ObjectPattern(vec!["events".to_string()]),
3100            None,
3101            TsExpr::As {
3102                // #1323: `Await` no longer auto-parenthesizes under `As` —
3103                // this file's own real content wraps it explicitly via
3104                // `Paren` (see `render_expr`'s own `As` arm doc).
3105                expr: Box::new(TsExpr::Paren(Box::new(TsExpr::Await(Box::new(
3106                    TsExpr::Call {
3107                        callee: Box::new(TsExpr::Member {
3108                            object: Box::new(TsExpr::Ident("request".to_string())),
3109                            property: "json".to_string(),
3110                        }),
3111                        args: vec![],
3112                    },
3113                ))))),
3114                ty: TsType::Object(vec![TsTypeMember::prop(
3115                    "events",
3116                    TsType::array(TsType::named("FanoutEvent")),
3117                )]),
3118            },
3119            None,
3120        ));
3121        let printed = print(&program, "x.bynk", "", "x.ts");
3122        assert_eq!(
3123            printed.text,
3124            "const { events } = (await request.json()) as { events: FanoutEvent[] };\n"
3125        );
3126    }
3127
3128    #[test]
3129    fn prints_if_without_braces_when_the_body_is_not_a_block() {
3130        let mut program = TsProgram::new();
3131        program.push(TsStmt::if_stmt(
3132            TsExpr::Unary {
3133                op: TsUnaryOp::Not,
3134                expr: Box::new(TsExpr::Call {
3135                    callee: Box::new(TsExpr::Member {
3136                        object: Box::new(TsExpr::Ident("Array".to_string())),
3137                        property: "isArray".to_string(),
3138                    }),
3139                    args: vec![TsExpr::Ident("subs".to_string())],
3140                }),
3141            },
3142            TsStmt::continue_stmt(None),
3143            None,
3144        ));
3145        let printed = print(&program, "x.bynk", "", "x.ts");
3146        assert_eq!(printed.text, "if (!Array.isArray(subs)) continue;\n");
3147    }
3148
3149    /// Review of #1317/#1318, finding 2: a bare `Comment` as an `if`'s
3150    /// brace-free body must not fall through to the top-level `//`-line
3151    /// form — `if (cond) // text` comments out the rest of the physical
3152    /// line, leaving `if` with no body at all (a parse error). Renders as
3153    /// a block comment instead, which cannot swallow anything after it.
3154    #[test]
3155    fn a_comment_as_an_ifs_brace_free_body_renders_as_a_block_comment() {
3156        let mut program = TsProgram::new();
3157        program.push(TsStmt::if_stmt(
3158            TsExpr::Ident("cond".to_string()),
3159            TsStmt::comment("annotation", None),
3160            None,
3161        ));
3162        let printed = print(&program, "x.bynk", "", "x.ts");
3163        assert_eq!(printed.text, "if (cond) /* annotation */\n");
3164    }
3165
3166    #[test]
3167    fn prints_a_for_of_loop_with_a_braced_body() {
3168        let mut program = TsProgram::new();
3169        program.push(TsStmt::for_of(
3170            "ev",
3171            TsExpr::Ident("events".to_string()),
3172            TsStmt::block(
3173                vec![TsStmt::return_stmt(
3174                    Some(TsExpr::Ident("ev".to_string())),
3175                    None,
3176                )],
3177                None,
3178            ),
3179            None,
3180        ));
3181        let printed = print(&program, "x.bynk", "", "x.ts");
3182        assert_eq!(
3183            printed.text,
3184            "for (const ev of events) {\n  return ev;\n}\n"
3185        );
3186    }
3187
3188    /// Arc E slice 7 (#1447): `TsStmtKind::For` — pins the exact C-style
3189    /// header shape `ListInst`/`MapInst`'s own real deserialise-side element
3190    /// loops need (`for (let i = 0; i < json.length; i++) { ... }`),
3191    /// including the postfix `++` the printer appends over `name` itself
3192    /// (see `TsStmtKind::For`'s own doc for why there is no separate
3193    /// `update` field to pass).
3194    #[test]
3195    fn prints_a_c_style_for_loop_with_a_braced_body() {
3196        let mut program = TsProgram::new();
3197        program.push(TsStmt::for_stmt(
3198            "i",
3199            TsExpr::Lit(TsLit::Num("0".to_string())),
3200            TsExpr::Binary {
3201                op: TsBinaryOp::LessThan,
3202                left: Box::new(TsExpr::Ident("i".to_string())),
3203                right: Box::new(TsExpr::Member {
3204                    object: Box::new(TsExpr::Ident("json".to_string())),
3205                    property: "length".to_string(),
3206                }),
3207            },
3208            TsStmt::block(
3209                vec![TsStmt::expr_stmt(TsExpr::Ident("item".to_string()), None)],
3210                None,
3211            ),
3212            None,
3213        ));
3214        let printed = print(&program, "x.bynk", "", "x.ts");
3215        assert_eq!(
3216            printed.text,
3217            "for (let i = 0; i < json.length; i++) {\n  item;\n}\n"
3218        );
3219    }
3220
3221    /// A `For` loop's own brace-free body falls back to the same inline
3222    /// rendering `ForOf`'s own brace-free body already uses — exercised as
3223    /// the *outer* statement, over a `Continue` body (not reachable from any
3224    /// real `bynk-emit` content today, but a real, direct shape).
3225    #[test]
3226    fn prints_a_c_style_for_loops_brace_free_body_inline() {
3227        let mut program = TsProgram::new();
3228        program.push(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        let printed = print(&program, "x.bynk", "", "x.ts");
3240        assert_eq!(printed.text, "for (let i = 0; i < n; i++) continue;\n");
3241    }
3242
3243    /// Review of #1448, finding 1: the previous test above pins a brace-free
3244    /// `For` as the *outer* statement, which renders through `render_stmt`'s
3245    /// own dedicated `For` arm, not the `render_inline_stmt` fallback this
3246    /// slice's own `| TsStmtKind::For { .. }` line actually added. That
3247    /// fallback only fires when a `For` is itself the brace-free body of an
3248    /// ENCLOSING `if`/`for...of`/`For` — exercised directly here, nesting a
3249    /// brace-free `For` as an `if`'s own brace-free branch.
3250    #[test]
3251    fn prints_a_brace_free_for_loop_nested_as_an_ifs_own_brace_free_body() {
3252        let mut program = TsProgram::new();
3253        program.push(TsStmt::if_stmt(
3254            TsExpr::Ident("cond".to_string()),
3255            TsStmt::for_stmt(
3256                "i",
3257                TsExpr::Lit(TsLit::Num("0".to_string())),
3258                TsExpr::Binary {
3259                    op: TsBinaryOp::LessThan,
3260                    left: Box::new(TsExpr::Ident("i".to_string())),
3261                    right: Box::new(TsExpr::Ident("n".to_string())),
3262                },
3263                TsStmt::continue_stmt(None),
3264                None,
3265            ),
3266            None,
3267        ));
3268        let printed = print(&program, "x.bynk", "", "x.ts");
3269        assert_eq!(
3270            printed.text,
3271            "if (cond) for (let i = 0; i < n; i++) continue;\n"
3272        );
3273    }
3274
3275    #[test]
3276    fn prints_a_try_catch_on_the_shared_closing_brace_line() {
3277        let mut program = TsProgram::new();
3278        program.push(TsStmt::try_catch(
3279            TsStmt::block(
3280                vec![TsStmt::expr_stmt(
3281                    TsExpr::Await(Box::new(TsExpr::Call {
3282                        callee: Box::new(TsExpr::Ident("deliverEvent".to_string())),
3283                        args: vec![TsExpr::Ident("binding".to_string())],
3284                    })),
3285                    None,
3286                )],
3287                None,
3288            ),
3289            Some("e"),
3290            TsStmt::block(
3291                vec![TsStmt::expr_stmt(
3292                    TsExpr::Call {
3293                        callee: Box::new(TsExpr::Member {
3294                            object: Box::new(TsExpr::Ident("console".to_string())),
3295                            property: "error".to_string(),
3296                        }),
3297                        args: vec![TsExpr::Lit(TsLit::Str("failed".to_string()))],
3298                    },
3299                    None,
3300                )],
3301                None,
3302            ),
3303            None,
3304        ));
3305        let printed = print(&program, "x.bynk", "", "x.ts");
3306        assert_eq!(
3307            printed.text,
3308            "try {\n  await deliverEvent(binding);\n} catch (e) {\n  console.error(\"failed\");\n}\n"
3309        );
3310    }
3311
3312    #[test]
3313    fn prints_an_interface_with_an_inline_nested_object_type() {
3314        let mut program = TsProgram::new();
3315        program.push(TsStmt::decl(
3316            TsDecl::Interface {
3317                name: "FanoutEvent".to_string(),
3318                type_params: Vec::new(),
3319                members: vec![
3320                    TsTypeMember::prop("type", TsType::named("string")),
3321                    TsTypeMember::prop("payload", TsType::named("unknown")),
3322                    TsTypeMember::prop(
3323                        "envelope",
3324                        TsType::Object(vec![
3325                            TsTypeMember::prop("eventId", TsType::named("string")),
3326                            TsTypeMember::prop("publisherId", TsType::named("string")),
3327                        ]),
3328                    ),
3329                ],
3330            },
3331            None,
3332        ));
3333        let printed = print(&program, "x.bynk", "", "x.ts");
3334        assert_eq!(
3335            printed.text,
3336            "interface FanoutEvent {\n  type: string;\n  payload: unknown;\n  envelope: { eventId: string; publisherId: string };\n}\n"
3337        );
3338    }
3339
3340    #[test]
3341    fn prints_two_adjacent_imports_with_no_blank_line_between_them() {
3342        let mut program = TsProgram::new();
3343        program.push(TsStmt::decl(
3344            TsDecl::Import {
3345                type_only: true,
3346                names: vec!["A".to_string()],
3347                from: "../x.js".to_string(),
3348            },
3349            None,
3350        ));
3351        program.push(TsStmt::decl(
3352            TsDecl::Import {
3353                type_only: false,
3354                names: vec!["b".to_string()],
3355                from: "../x.js".to_string(),
3356            },
3357            None,
3358        ));
3359        program.push(TsStmt::decl(
3360            TsDecl::ConstDecl {
3361                name: "c".to_string(),
3362                ty: None,
3363                init: TsExpr::Lit(TsLit::Null),
3364            },
3365            None,
3366        ));
3367        let printed = print(&program, "x.bynk", "", "x.ts");
3368        assert_eq!(
3369            printed.text,
3370            "import type { A } from \"../x.js\";\nimport { b } from \"../x.js\";\n\nconst c = null;\n"
3371        );
3372    }
3373
3374    /// #1486: `no_blank_before` suppresses the automatic blank line before
3375    /// exactly the statement that carries it — a `const`/`function` pair
3376    /// neither an import, a comment, a `ReExportAll`, nor `Verbatim`, so
3377    /// none of the existing exemptions apply; without this field the two
3378    /// would print with an unwanted blank between them (confirmed by
3379    /// temporarily reverting the field during this same change — the
3380    /// printer's own default policy really does insert one here). A third,
3381    /// ordinary statement after it confirms the field is scoped to the one
3382    /// carrying it, not sticky for the rest of the program.
3383    #[test]
3384    fn no_blank_before_suppresses_exactly_the_next_statements_own_blank() {
3385        let mut program = TsProgram::new();
3386        program.push(TsStmt::decl(
3387            TsDecl::ConstDecl {
3388                name: "__FooRegistry".to_string(),
3389                ty: None,
3390                init: TsExpr::Lit(TsLit::Null),
3391            },
3392            None,
3393        ));
3394        let mut zero_fn = TsStmt::decl(
3395            TsDecl::Function {
3396                name: "__zeroOfFooState".to_string(),
3397                generics: Vec::new(),
3398                params: Vec::new(),
3399                return_type: None,
3400                body: Vec::new(),
3401                is_async: false,
3402                inline: true,
3403            },
3404            None,
3405        );
3406        zero_fn.no_blank_before = true;
3407        program.push(zero_fn);
3408        program.push(TsStmt::decl(
3409            TsDecl::ConstDecl {
3410                name: "c".to_string(),
3411                ty: None,
3412                init: TsExpr::Lit(TsLit::Null),
3413            },
3414            None,
3415        ));
3416        let printed = print(&program, "x.bynk", "", "x.ts");
3417        assert_eq!(
3418            printed.text,
3419            "const __FooRegistry = null;\nfunction __zeroOfFooState() {  }\n\nconst c = null;\n"
3420        );
3421    }
3422
3423    /// The required "does the algebra hold against real content" test
3424    /// (P7.8's own accepted proposal, #1313): builds
3425    /// `bynk-emit/src/emitter/events_fanout.rs`'s own `EventsFanoutDO`
3426    /// class as real nodes (its field, constructor, and `fetch` method,
3427    /// with the exact control flow the real function emits — `for...of`,
3428    /// nested `if`-without-braces, `try`/`catch`) and asserts the printed
3429    /// text is byte-identical to what `emit_events_fanout_do` produces
3430    /// today for that class (transcribed directly from
3431    /// `bynk-emit/src/emitter/events_fanout.rs`'s own `write!` calls, not
3432    /// re-derived — including the constructor body, `this.env = (env ??
3433    /// {}) as Record<string, ServiceBinding>;`, via `TsStmtKind::Assign`,
3434    /// added in review of #1313: the accepted proposal's own grounding
3435    /// catalogue detailed the `fetch` method's body but missed the
3436    /// constructor's one real statement, which turned out to need a
3437    /// variant Decision B's list didn't name). The leading header comment
3438    /// and the `__eventRoutes` table's own per-project literal entries are
3439    /// deliberately not included here — a representative fragment covering
3440    /// the class is exactly what the accepted proposal's own "Done when"
3441    /// allows, and this is the shape that actually exercises every new
3442    /// node kind this slice adds.
3443    #[test]
3444    fn prints_events_fanout_dos_own_class_byte_identical_to_the_real_emitter() {
3445        let real_ctor = TsClassCtor {
3446            params: vec![
3447                TsParam {
3448                    name: "_state".to_string(),
3449                    ty: Some(TsType::named("DurableObjectState")),
3450                    optional: false,
3451                },
3452                TsParam {
3453                    name: "env".to_string(),
3454                    ty: Some(TsType::named("unknown")),
3455                    optional: true,
3456                },
3457            ],
3458            body: vec![TsStmt::assign(
3459                TsExpr::Member {
3460                    object: Box::new(TsExpr::Ident("this".to_string())),
3461                    property: "env".to_string(),
3462                },
3463                TsExpr::As {
3464                    expr: Box::new(TsExpr::Binary {
3465                        op: TsBinaryOp::NullishCoalescing,
3466                        left: Box::new(TsExpr::Ident("env".to_string())),
3467                        right: Box::new(TsExpr::object(vec![])),
3468                    }),
3469                    ty: TsType::named_with_args(
3470                        "Record",
3471                        vec![TsType::named("string"), TsType::named("ServiceBinding")],
3472                    ),
3473                },
3474                None,
3475            )],
3476        };
3477
3478        let fetch = TsClassMethod {
3479            name: "fetch".to_string(),
3480            private: false,
3481            is_async: true,
3482            params: vec![TsParam {
3483                name: "request".to_string(),
3484                ty: Some(TsType::named("Request")),
3485                optional: false,
3486            }],
3487            return_type: Some(TsType::named_with_args(
3488                "Promise",
3489                vec![TsType::named("Response")],
3490            )),
3491            doc: None,
3492            body: vec![
3493                TsStmt::const_stmt(
3494                    TsBindingName::ObjectPattern(vec!["events".to_string()]),
3495                    None,
3496                    TsExpr::As {
3497                        expr: Box::new(TsExpr::Paren(Box::new(TsExpr::Await(Box::new(
3498                            TsExpr::Call {
3499                                callee: Box::new(TsExpr::Member {
3500                                    object: Box::new(TsExpr::Ident("request".to_string())),
3501                                    property: "json".to_string(),
3502                                }),
3503                                args: vec![],
3504                            },
3505                        ))))),
3506                        ty: TsType::Object(vec![TsTypeMember::prop(
3507                            "events",
3508                            TsType::array(TsType::named("FanoutEvent")),
3509                        )]),
3510                    },
3511                    None,
3512                ),
3513                TsStmt::for_of(
3514                    "ev",
3515                    TsExpr::Ident("events".to_string()),
3516                    TsStmt::block(
3517                        vec![
3518                            TsStmt::const_stmt(
3519                                TsBindingName::Ident("subs".to_string()),
3520                                None,
3521                                TsExpr::Index {
3522                                    object: Box::new(TsExpr::Ident("__eventRoutes".to_string())),
3523                                    index: Box::new(TsExpr::Member {
3524                                        object: Box::new(TsExpr::Ident("ev".to_string())),
3525                                        property: "type".to_string(),
3526                                    }),
3527                                },
3528                                None,
3529                            ),
3530                            TsStmt::if_stmt(
3531                                TsExpr::Unary {
3532                                    op: TsUnaryOp::Not,
3533                                    expr: Box::new(TsExpr::Call {
3534                                        callee: Box::new(TsExpr::Member {
3535                                            object: Box::new(TsExpr::Ident("Array".to_string())),
3536                                            property: "isArray".to_string(),
3537                                        }),
3538                                        args: vec![TsExpr::Ident("subs".to_string())],
3539                                    }),
3540                                },
3541                                TsStmt::continue_stmt(None),
3542                                None,
3543                            ),
3544                            TsStmt::for_of(
3545                                "sub",
3546                                TsExpr::Ident("subs".to_string()),
3547                                TsStmt::block(
3548                                    vec![
3549                                        TsStmt::const_stmt(
3550                                            TsBindingName::Ident("binding".to_string()),
3551                                            None,
3552                                            TsExpr::Index {
3553                                                object: Box::new(TsExpr::Member {
3554                                                    object: Box::new(TsExpr::Ident(
3555                                                        "this".to_string(),
3556                                                    )),
3557                                                    property: "env".to_string(),
3558                                                }),
3559                                                index: Box::new(TsExpr::Member {
3560                                                    object: Box::new(TsExpr::Ident(
3561                                                        "sub".to_string(),
3562                                                    )),
3563                                                    property: "binding".to_string(),
3564                                                }),
3565                                            },
3566                                            None,
3567                                        ),
3568                                        TsStmt::if_stmt(
3569                                            TsExpr::Unary {
3570                                                op: TsUnaryOp::Not,
3571                                                expr: Box::new(TsExpr::Ident(
3572                                                    "binding".to_string(),
3573                                                )),
3574                                            },
3575                                            TsStmt::continue_stmt(None),
3576                                            None,
3577                                        ),
3578                                        TsStmt::try_catch(
3579                                            TsStmt::block(
3580                                                vec![TsStmt::expr_stmt(
3581                                                    TsExpr::Await(Box::new(TsExpr::Call {
3582                                                        callee: Box::new(TsExpr::Ident(
3583                                                            "deliverEvent".to_string(),
3584                                                        )),
3585                                                        args: vec![
3586                                                            TsExpr::Ident("binding".to_string()),
3587                                                            TsExpr::Member {
3588                                                                object: Box::new(TsExpr::Ident(
3589                                                                    "sub".to_string(),
3590                                                                )),
3591                                                                property: "service".to_string(),
3592                                                            },
3593                                                            TsExpr::Member {
3594                                                                object: Box::new(TsExpr::Ident(
3595                                                                    "ev".to_string(),
3596                                                                )),
3597                                                                property: "payload".to_string(),
3598                                                            },
3599                                                            TsExpr::Member {
3600                                                                object: Box::new(TsExpr::Ident(
3601                                                                    "ev".to_string(),
3602                                                                )),
3603                                                                property: "envelope".to_string(),
3604                                                            },
3605                                                        ],
3606                                                    })),
3607                                                    None,
3608                                                )],
3609                                                None,
3610                                            ),
3611                                            Some("e"),
3612                                            TsStmt::block(
3613                                                vec![TsStmt::expr_stmt(
3614                                                    TsExpr::Call {
3615                                                        callee: Box::new(TsExpr::Member {
3616                                                            object: Box::new(TsExpr::Ident(
3617                                                                "console".to_string(),
3618                                                            )),
3619                                                            property: "error".to_string(),
3620                                                        }),
3621                                                        args: vec![
3622                                                            TsExpr::Lit(TsLit::Str(
3623                                                                "EventsFanout delivery failed"
3624                                                                    .to_string(),
3625                                                            )),
3626                                                            TsExpr::object(vec![
3627                                                                (
3628                                                                    "event".to_string(),
3629                                                                    TsExpr::Member {
3630                                                                        object: Box::new(
3631                                                                            TsExpr::Ident(
3632                                                                                "ev".to_string(),
3633                                                                            ),
3634                                                                        ),
3635                                                                        property: "type"
3636                                                                            .to_string(),
3637                                                                    },
3638                                                                ),
3639                                                                (
3640                                                                    "service".to_string(),
3641                                                                    TsExpr::Member {
3642                                                                        object: Box::new(
3643                                                                            TsExpr::Ident(
3644                                                                                "sub".to_string(),
3645                                                                            ),
3646                                                                        ),
3647                                                                        property: "service"
3648                                                                            .to_string(),
3649                                                                    },
3650                                                                ),
3651                                                                (
3652                                                                    "error".to_string(),
3653                                                                    TsExpr::Call {
3654                                                                        callee: Box::new(
3655                                                                            TsExpr::Ident(
3656                                                                                "String"
3657                                                                                    .to_string(),
3658                                                                            ),
3659                                                                        ),
3660                                                                        args: vec![TsExpr::Ident(
3661                                                                            "e".to_string(),
3662                                                                        )],
3663                                                                    },
3664                                                                ),
3665                                                            ]),
3666                                                        ],
3667                                                    },
3668                                                    None,
3669                                                )],
3670                                                None,
3671                                            ),
3672                                            None,
3673                                        ),
3674                                    ],
3675                                    None,
3676                                ),
3677                                None,
3678                            ),
3679                        ],
3680                        None,
3681                    ),
3682                    None,
3683                ),
3684                TsStmt::return_stmt(
3685                    Some(TsExpr::New {
3686                        callee: Box::new(TsExpr::Ident("Response".to_string())),
3687                        args: vec![
3688                            TsExpr::Lit(TsLit::Null),
3689                            TsExpr::object(vec![(
3690                                "status".to_string(),
3691                                TsExpr::Lit(TsLit::Num("204".to_string())),
3692                            )]),
3693                        ],
3694                    }),
3695                    None,
3696                ),
3697            ],
3698        };
3699
3700        let class = TsDecl::Export(Box::new(TsDecl::Class {
3701            name: "EventsFanoutDO".to_string(),
3702            fields: vec![TsClassField {
3703                name: "env".to_string(),
3704                ty: TsType::named_with_args(
3705                    "Record",
3706                    vec![TsType::named("string"), TsType::named("ServiceBinding")],
3707                ),
3708                private: true,
3709            }],
3710            constructor: Some(real_ctor),
3711            methods: vec![fetch],
3712        }));
3713
3714        let mut program = TsProgram::new();
3715        program.push(TsStmt::decl(class, None));
3716        let printed = print(&program, "x.bynk", "", "x.ts");
3717
3718        // Transcribed directly from `bynk-emit/src/emitter/events_fanout.rs`'s
3719        // own `write!` calls, byte-for-byte including the constructor body.
3720        let expected_lines = [
3721            "export class EventsFanoutDO {",
3722            "  private env: Record<string, ServiceBinding>;",
3723            "  constructor(_state: DurableObjectState, env?: unknown) {",
3724            "    this.env = (env ?? {}) as Record<string, ServiceBinding>;",
3725            "  }",
3726            "",
3727            "  async fetch(request: Request): Promise<Response> {",
3728            "    const { events } = (await request.json()) as { events: FanoutEvent[] };",
3729            "    for (const ev of events) {",
3730            "      const subs = __eventRoutes[ev.type];",
3731            "      if (!Array.isArray(subs)) continue;",
3732            "      for (const sub of subs) {",
3733            "        const binding = this.env[sub.binding];",
3734            "        if (!binding) continue;",
3735            "        try {",
3736            "          await deliverEvent(binding, sub.service, ev.payload, ev.envelope);",
3737            "        } catch (e) {",
3738            "          console.error(\"EventsFanout delivery failed\", { event: ev.type, service: sub.service, error: String(e) });",
3739            "        }",
3740            "      }",
3741            "    }",
3742            "    return new Response(null, { status: 204 });",
3743            "  }",
3744            "}",
3745        ];
3746        let expected = format!("{}\n", expected_lines.join("\n"));
3747        assert_eq!(printed.text, expected);
3748    }
3749
3750    /// Coverage gap named in review of #1314, finding 4: `Let` had no test
3751    /// anywhere, including its distinct `init: None` branch (`Const` always
3752    /// has an initialiser; `Let` is the one new statement kind that can
3753    /// omit one).
3754    #[test]
3755    fn prints_a_let_statement_with_no_initialiser() {
3756        let mut program = TsProgram::new();
3757        program.push(TsStmt::let_stmt(
3758            TsBindingName::Ident("subs".to_string()),
3759            None,
3760            None,
3761            None,
3762        ));
3763        let printed = print(&program, "x.bynk", "", "x.ts");
3764        assert_eq!(printed.text, "let subs;\n");
3765    }
3766
3767    /// Coverage gap named in review of #1314, finding 4: `TsExpr::Array`
3768    /// was never constructed in any test.
3769    #[test]
3770    fn prints_an_array_literal() {
3771        let mut program = TsProgram::new();
3772        program.push(TsStmt::expr_stmt(
3773            TsExpr::array(vec![
3774                TsExpr::Lit(TsLit::Num("1".to_string())),
3775                TsExpr::Ident("x".to_string()),
3776            ]),
3777            None,
3778        ));
3779        let printed = print(&program, "x.bynk", "", "x.ts");
3780        assert_eq!(printed.text, "[1, x];\n");
3781    }
3782
3783    /// P7.9 (#1315): `TsType::Array`'s new `readonly` modifier — the shape
3784    /// every `List`/`Query` element type `bynk-emit`'s `ts_type_ref*`/
3785    /// `ts_ty` families build (`readonly T[]`), which `TsType::array`'s own
3786    /// `readonly: false` default cannot represent.
3787    #[test]
3788    fn print_type_renders_a_readonly_array() {
3789        assert_eq!(
3790            print_type(&TsType::readonly_array(TsType::named("Order"))),
3791            "readonly Order[]"
3792        );
3793        assert_eq!(
3794            print_type(&TsType::array(TsType::named("Order"))),
3795            "Order[]"
3796        );
3797    }
3798
3799    /// A readonly array, function, union or intersection element is
3800    /// parenthesised before `[]`; a bare `readonly readonly T[][]` is TS1354.
3801    #[test]
3802    fn print_type_parenthesises_a_non_postfix_safe_array_element() {
3803        let order = || TsType::named("Order");
3804        assert_eq!(
3805            print_type(&TsType::readonly_array(TsType::readonly_array(order()))),
3806            "readonly (readonly Order[])[]"
3807        );
3808        assert_eq!(
3809            print_type(&TsType::readonly_array(TsType::array(order()))),
3810            "readonly Order[][]"
3811        );
3812        assert_eq!(
3813            print_type(&TsType::readonly_array(TsType::Fn {
3814                params: vec![order()],
3815                ret: Box::new(order()),
3816            })),
3817            "readonly ((a0: Order) => Order)[]"
3818        );
3819        assert_eq!(
3820            print_type(&TsType::array(TsType::Union {
3821                members: vec![order(), TsType::named("null")],
3822                multiline: false,
3823            })),
3824            "(Order | null)[]"
3825        );
3826        assert_eq!(
3827            print_type(&TsType::array(TsType::Intersection(vec![
3828                order(),
3829                TsType::named("Tagged"),
3830            ]))),
3831            "(Order & Tagged)[]"
3832        );
3833    }
3834
3835    /// P7.9 (#1315): `TsType::Fn`'s zero-`params` form — the query-thunk
3836    /// wrapper shape, `bynk-emit`'s own `(() => readonly T[])`.
3837    #[test]
3838    fn print_type_renders_a_zero_param_function_type() {
3839        let ty = TsType::Fn {
3840            params: vec![],
3841            ret: Box::new(TsType::readonly_array(TsType::named("Order"))),
3842        };
3843        assert_eq!(print_type(&ty), "() => readonly Order[]");
3844    }
3845
3846    /// P7.9 (#1315): `TsType::Fn`'s real parametered form — positional
3847    /// `a0`/`a1`/… names, matching `bynk-emit`'s own pre-P7.9 convention
3848    /// exactly (TypeScript requires *some* name in function-type syntax).
3849    #[test]
3850    fn print_type_renders_a_parametered_function_type_with_positional_names() {
3851        let ty = TsType::Fn {
3852            params: vec![TsType::named("string"), TsType::named("number")],
3853            ret: Box::new(TsType::named("void")),
3854        };
3855        assert_eq!(print_type(&ty), "(a0: string, a1: number) => void");
3856    }
3857
3858    /// `TsType::Union`, added during review of #1315's own implementation —
3859    /// a real gap `bynk-emit`'s `ts_ty` needed for a resolved multi-actor
3860    /// sum (`Ty::ActorSum`), beyond the accepted proposal's own `readonly`/
3861    /// `Fn` gap list. Members print `" | "`-joined, in order.
3862    #[test]
3863    fn print_type_renders_a_union_of_named_types() {
3864        let ty = TsType::union(vec![
3865            TsType::named("string"),
3866            TsType::named("number"),
3867            TsType::Object(vec![TsTypeMember::prop(
3868                "tag",
3869                TsType::named("\"literal\""),
3870            )]),
3871        ]);
3872        assert_eq!(print_type(&ty), "string | number | { tag: \"literal\" }");
3873    }
3874
3875    /// `TsStmtKind::Comment`, added for Arc C's own first real conversion
3876    /// slice (#1317). A multi-line comment (embedded `\n`) prints one
3877    /// `// `-prefixed line per real line.
3878    #[test]
3879    fn prints_a_comment_one_prefixed_line_per_embedded_newline() {
3880        let mut program = TsProgram::new();
3881        program.push(TsStmt::comment(
3882            "Generated by bynkc — do not edit by hand.\nSecond line.",
3883            None,
3884        ));
3885        let printed = print(&program, "x.bynk", "", "x.ts");
3886        assert_eq!(
3887            printed.text,
3888            "// Generated by bynkc — do not edit by hand.\n// Second line.\n"
3889        );
3890    }
3891
3892    /// Two adjacent top-level `Comment` statements get no blank line
3893    /// between them — the same exception already established for two
3894    /// adjacent `import`s, matching `events_fanout.rs`'s own real two-line
3895    /// header banner (built as two separate `Comment` statements, not one
3896    /// with an embedded newline).
3897    #[test]
3898    fn no_blank_line_between_two_adjacent_comment_statements() {
3899        let mut program = TsProgram::new();
3900        program.push(TsStmt::comment(
3901            "Generated by bynkc — do not edit by hand.",
3902            None,
3903        ));
3904        program.push(TsStmt::comment("Second line.", None));
3905        program.push(TsStmt::decl(
3906            TsDecl::Interface {
3907                name: "A".to_string(),
3908                type_params: Vec::new(),
3909                members: vec![],
3910            },
3911            None,
3912        ));
3913        let printed = print(&program, "x.bynk", "", "x.ts");
3914        assert_eq!(
3915            printed.text,
3916            "// Generated by bynkc — do not edit by hand.\n// Second line.\n\ninterface A {\n}\n"
3917        );
3918    }
3919
3920    /// `TsExpr::multiline_object`, added for Arc C's own first real
3921    /// conversion slice (#1317): `events_fanout.rs`'s own `__eventRoutes`
3922    /// table is a top-level `const` initialiser with one entry per line,
3923    /// each with its own trailing comma (including the last), closing
3924    /// brace back at the statement's own indent — TypeScript's ordinary
3925    /// multi-line object-literal convention, which `TsExpr::object`'s
3926    /// single-line form cannot represent. Only reachable through a
3927    /// statement/declaration-level renderer that carries `depth`
3928    /// (`render_stmt_level_expr`) — this exercises it via a top-level
3929    /// `ConstDecl`, the real shape `events_fanout.rs` itself uses.
3930    #[test]
3931    fn prints_a_multiline_object_as_a_top_level_const_initialiser() {
3932        let mut program = TsProgram::new();
3933        program.push(TsStmt::decl(
3934            TsDecl::ConstDecl {
3935                name: "table".to_string(),
3936                ty: None,
3937                init: TsExpr::multiline_object(vec![
3938                    ("a".to_string(), TsExpr::Lit(TsLit::Num("1".to_string()))),
3939                    ("b".to_string(), TsExpr::Lit(TsLit::Num("2".to_string()))),
3940                ]),
3941            },
3942            None,
3943        ));
3944        let printed = print(&program, "x.bynk", "", "x.ts");
3945        assert_eq!(printed.text, "const table = {\n  a: 1,\n  b: 2,\n};\n");
3946    }
3947
3948    /// Review of #1317/#1318, finding 1: an empty `multiline_object` still
3949    /// prints the open/close-brace-on-separate-lines shape, not the tight
3950    /// `{}` a single-line empty object uses — matching the pre-conversion
3951    /// `writeln!` code's own real behaviour (its `for` loop simply not
3952    /// iterating, while the open-brace and closing `};` lines were written
3953    /// unconditionally either way). This is a real, reachable shape:
3954    /// `events_fanout.rs`'s own `__eventRoutes` table can be empty for a
3955    /// context that publishes only events nobody subscribes to.
3956    #[test]
3957    fn an_empty_multiline_object_still_prints_the_open_and_close_brace_on_separate_lines() {
3958        let mut program = TsProgram::new();
3959        program.push(TsStmt::decl(
3960            TsDecl::ConstDecl {
3961                name: "table".to_string(),
3962                ty: None,
3963                init: TsExpr::multiline_object(vec![]),
3964            },
3965            None,
3966        ));
3967        let printed = print(&program, "x.bynk", "", "x.ts");
3968        assert_eq!(printed.text, "const table = {\n};\n");
3969    }
3970
3971    /// A `multiline_object` reached through the ordinary, depth-unaware
3972    /// `render_expr` recursion (nested inside another expression, not a
3973    /// statement's own top-level initialiser) falls back to single-line
3974    /// rendering — documented on `TsExpr::Object` itself as a real,
3975    /// deliberate boundary, not silently wrong. Pinned here so a future
3976    /// change to that boundary is a visible, intentional decision.
3977    #[test]
3978    fn a_nested_multiline_object_falls_back_to_single_line() {
3979        let mut program = TsProgram::new();
3980        program.push(TsStmt::expr_stmt(
3981            TsExpr::array(vec![TsExpr::multiline_object(vec![(
3982                "a".to_string(),
3983                TsExpr::Lit(TsLit::Num("1".to_string())),
3984            )])]),
3985            None,
3986        ));
3987        let printed = print(&program, "x.bynk", "", "x.ts");
3988        assert_eq!(printed.text, "[{ a: 1 }];\n");
3989    }
3990
3991    /// Coverage gap named in review of #1314, finding 4: the blank-line-
3992    /// between-top-level-declarations policy was never tested for the
3993    /// mixed `Verbatim` + real-`Decl` case — the case that actually keeps
3994    /// every P7.6 fixture's zero-diff claim true today, since every P7.6
3995    /// construction site is all-`Verbatim`. Pins both halves of the rule's
3996    /// asymmetry: no blank line is added *after* a `Verbatim` statement
3997    /// (P7.7's own boundary — its content's own trailing spacing isn't the
3998    /// printer's decision), but a blank line *is* added after a real
3999    /// `Decl` even when `Verbatim` follows it.
4000    #[test]
4001    fn no_blank_line_after_verbatim_but_one_before_it() {
4002        let mut program = TsProgram::new();
4003        program.push(TsStmt::verbatim(
4004            VerbatimOrigin::Contracts,
4005            "const legacy = 1;\n",
4006            None,
4007        ));
4008        program.push(TsStmt::decl(
4009            TsDecl::Interface {
4010                name: "A".to_string(),
4011                type_params: Vec::new(),
4012                members: vec![],
4013            },
4014            None,
4015        ));
4016        program.push(TsStmt::verbatim(
4017            VerbatimOrigin::Secrets,
4018            "const trailing = 2;\n",
4019            None,
4020        ));
4021        let printed = print(&program, "x.bynk", "", "x.ts");
4022        assert_eq!(
4023            printed.text,
4024            "const legacy = 1;\ninterface A {\n}\n\nconst trailing = 2;\n"
4025        );
4026    }
4027
4028    /// Coverage gap named in review of #1314, finding 4: nothing exercised
4029    /// `render_block_body`'s own graceful-degradation path — its doc
4030    /// comment promises a single non-`Block` statement still renders
4031    /// sensibly as a one-statement body rather than panicking, but no test
4032    /// ever passed one. `TryCatch` is the real caller that could hit this
4033    /// (its `try_block`/`catch_block` are typed as a bare `TsStmt`, not
4034    /// required to be a `Block`).
4035    #[test]
4036    fn a_try_block_that_is_not_a_block_statement_still_prints_as_a_braced_body() {
4037        let mut program = TsProgram::new();
4038        program.push(TsStmt::try_catch(
4039            TsStmt::expr_stmt(
4040                TsExpr::Call {
4041                    callee: Box::new(TsExpr::Ident("risky".to_string())),
4042                    args: vec![],
4043                },
4044                None,
4045            ),
4046            Some("e"),
4047            TsStmt::expr_stmt(
4048                TsExpr::Call {
4049                    callee: Box::new(TsExpr::Ident("handle".to_string())),
4050                    args: vec![TsExpr::Ident("e".to_string())],
4051                },
4052                None,
4053            ),
4054            None,
4055        ));
4056        let printed = print(&program, "x.bynk", "", "x.ts");
4057        assert_eq!(
4058            printed.text,
4059            "try {\n  risky();\n} catch (e) {\n  handle(e);\n}\n"
4060        );
4061    }
4062
4063    /// Coverage gap named in review of #1314, finding 4: `render_inline_stmt`'s
4064    /// shared fallback arm (finding 3's fix — explicit variants, not a
4065    /// wildcard) was never actually exercised by a test; every existing
4066    /// brace-free `if`/`for...of` body used `Continue`/`Return`/`ExprStmt`,
4067    /// which each have their own dedicated inline arm.
4068    #[test]
4069    fn a_brace_free_if_body_that_is_not_one_of_the_dedicated_inline_kinds_still_prints() {
4070        let mut program = TsProgram::new();
4071        program.push(TsStmt::if_stmt(
4072            TsExpr::Ident("cond".to_string()),
4073            TsStmt::const_stmt(
4074                TsBindingName::Ident("x".to_string()),
4075                None,
4076                TsExpr::Lit(TsLit::Num("1".to_string())),
4077                None,
4078            ),
4079            None,
4080        ));
4081        let printed = print(&program, "x.bynk", "", "x.ts");
4082        assert_eq!(printed.text, "if (cond) const x = 1;\n");
4083    }
4084
4085    /// Review of #1314, finding 2: nothing checked operator-precedence
4086    /// parenthesisation outside `As` — a `Binary` nested as the operand of
4087    /// `Unary`/`Member`/`Await` printed with no parens at all, silently
4088    /// changing what the text parses back to. Three of the review's own
4089    /// examples, pinned directly.
4090    #[test]
4091    fn parenthesises_a_binary_operand_of_unary_member_and_await() {
4092        let bin = || TsExpr::Binary {
4093            op: TsBinaryOp::NullishCoalescing,
4094            left: Box::new(TsExpr::Ident("a".to_string())),
4095            right: Box::new(TsExpr::Ident("b".to_string())),
4096        };
4097
4098        let mut not_program = TsProgram::new();
4099        not_program.push(TsStmt::expr_stmt(
4100            TsExpr::Unary {
4101                op: TsUnaryOp::Not,
4102                expr: Box::new(bin()),
4103            },
4104            None,
4105        ));
4106        assert_eq!(
4107            print(&not_program, "x.bynk", "", "x.ts").text,
4108            "!(a ?? b);\n"
4109        );
4110
4111        let mut member_program = TsProgram::new();
4112        member_program.push(TsStmt::expr_stmt(
4113            TsExpr::Member {
4114                object: Box::new(bin()),
4115                property: "c".to_string(),
4116            },
4117            None,
4118        ));
4119        assert_eq!(
4120            print(&member_program, "x.bynk", "", "x.ts").text,
4121            "(a ?? b).c;\n"
4122        );
4123
4124        let mut await_program = TsProgram::new();
4125        await_program.push(TsStmt::expr_stmt(TsExpr::Await(Box::new(bin())), None));
4126        assert_eq!(
4127            print(&await_program, "x.bynk", "", "x.ts").text,
4128            "await (a ?? b);\n"
4129        );
4130    }
4131
4132    /// Review of #1314, finding 2's fourth example: a right-nested `Binary`
4133    /// (`a ?? (b ?? c)`, as built) must not print as the bare `a ?? b ?? c`
4134    /// that a naive left-to-right walk would produce — that text parses
4135    /// back as `(a ?? b) ?? c`, a different tree than what was built.
4136    #[test]
4137    fn parenthesises_a_nested_binary_operand_of_another_binary() {
4138        let mut program = TsProgram::new();
4139        program.push(TsStmt::expr_stmt(
4140            TsExpr::Binary {
4141                op: TsBinaryOp::NullishCoalescing,
4142                left: Box::new(TsExpr::Ident("a".to_string())),
4143                right: Box::new(TsExpr::Binary {
4144                    op: TsBinaryOp::NullishCoalescing,
4145                    left: Box::new(TsExpr::Ident("b".to_string())),
4146                    right: Box::new(TsExpr::Ident("c".to_string())),
4147                }),
4148            },
4149            None,
4150        ));
4151        let printed = print(&program, "x.bynk", "", "x.ts");
4152        assert_eq!(printed.text, "a ?? (b ?? c);\n");
4153    }
4154
4155    /// Review of #1314, finding 1: `TsLit::Str` escaped only `"`/`\`, so a
4156    /// literal containing a newline, tab, or carriage return printed raw —
4157    /// unterminated-string-literal TypeScript `tsc` can't parse. Pins the
4158    /// fix against every character `bynk_check::wire_default::
4159    /// escape_ts_literal` escapes.
4160    #[test]
4161    fn escapes_newline_tab_and_carriage_return_in_a_string_literal() {
4162        let mut program = TsProgram::new();
4163        program.push(TsStmt::expr_stmt(
4164            TsExpr::Lit(TsLit::Str("line one\nline two\ttabbed\r".to_string())),
4165            None,
4166        ));
4167        let printed = print(&program, "x.bynk", "", "x.ts");
4168        assert_eq!(printed.text, "\"line one\\nline two\\ttabbed\\r\";\n");
4169    }
4170
4171    // -- Arc C slice 3 (#1321, `workers.rs`): the five new shapes Decision A
4172    // names, plus the real gaps found beyond it (`TsDecl::Function`/
4173    // `TsDecl::TypeAlias`/`TsDecl::ImportNamespace`, the parameterless
4174    // `catch {}` form, and the two new `TsUnaryOp`/four new `TsBinaryOp`
4175    // operators). --
4176
4177    #[test]
4178    fn prints_a_shorthand_async_method_entry_in_a_multiline_object() {
4179        let mut program = TsProgram::new();
4180        program.push(TsStmt::return_stmt(
4181            Some(TsExpr::multiline_object_entries(vec![
4182                TsObjectEntry::Method {
4183                    name: "foo".to_string(),
4184                    is_async: true,
4185                    generics: Vec::new(),
4186                    params: vec![TsParam {
4187                        name: "a".to_string(),
4188                        ty: Some(TsType::named("string")),
4189                        optional: false,
4190                    }],
4191                    return_type: None,
4192                    doc: None,
4193                    inline: false,
4194                    body: vec![TsStmt::return_stmt(
4195                        Some(TsExpr::Ident("a".to_string())),
4196                        None,
4197                    )],
4198                },
4199            ])),
4200            None,
4201        ));
4202        let printed = print(&program, "x.bynk", "", "x.ts");
4203        assert_eq!(
4204            printed.text,
4205            "return {\n  async foo(a: string) {\n    return a;\n  },\n};\n"
4206        );
4207    }
4208
4209    #[test]
4210    fn prints_shorthand_and_spread_entries_inline() {
4211        let mut program = TsProgram::new();
4212        program.push(TsStmt::expr_stmt(
4213            TsExpr::object_entries(vec![
4214                TsObjectEntry::Spread(TsExpr::Ident("deps".to_string())),
4215                TsObjectEntry::Shorthand("cap1".to_string()),
4216                TsObjectEntry::Prop(
4217                    "identity".to_string(),
4218                    TsExpr::Ident("__caller".to_string()),
4219                ),
4220            ]),
4221            None,
4222        ));
4223        let printed = print(&program, "x.bynk", "", "x.ts");
4224        assert_eq!(printed.text, "{ ...deps, cap1, identity: __caller };\n");
4225    }
4226
4227    #[test]
4228    fn prints_an_expression_bodied_arrow() {
4229        let mut program = TsProgram::new();
4230        program.push(TsStmt::expr_stmt(
4231            TsExpr::Arrow {
4232                params: vec![TsParam {
4233                    name: "events".to_string(),
4234                    ty: Some(TsType::named_with_args(
4235                        "Array",
4236                        vec![TsType::named("Wire")],
4237                    )),
4238                    optional: false,
4239                }],
4240                is_async: false,
4241                generics: Vec::new(),
4242                return_type: None,
4243                body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Call {
4244                    callee: Box::new(TsExpr::Ident("dispatch".to_string())),
4245                    args: vec![TsExpr::Ident("events".to_string())],
4246                }))),
4247            },
4248            None,
4249        ));
4250        let printed = print(&program, "x.bynk", "", "x.ts");
4251        assert_eq!(printed.text, "(events: Array<Wire>) => dispatch(events);\n");
4252    }
4253
4254    /// #1327's own real gap: `emit_composition_root`'s `__eventsDispatch`
4255    /// closure is the first real `Arrow` site that's async.
4256    #[test]
4257    fn prints_an_async_arrow() {
4258        let mut program = TsProgram::new();
4259        program.push(TsStmt::expr_stmt(
4260            TsExpr::Arrow {
4261                params: vec![TsParam {
4262                    name: "events".to_string(),
4263                    ty: Some(TsType::named_with_args(
4264                        "Array",
4265                        vec![TsType::named("Wire")],
4266                    )),
4267                    optional: false,
4268                }],
4269                is_async: true,
4270                generics: Vec::new(),
4271                return_type: None,
4272                body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Ident(
4273                    "{ dispatch(events); }".to_string(),
4274                )))),
4275            },
4276            None,
4277        ));
4278        let printed = print(&program, "x.bynk", "", "x.ts");
4279        assert_eq!(
4280            printed.text,
4281            "async (events: Array<Wire>) => { dispatch(events); };\n"
4282        );
4283    }
4284
4285    /// #1435 (Arc E slice 1): [`TsArrowBody::Block`], the first real
4286    /// statement-bodied arrow — pins the exact `serialisation.rs` `Float`
4287    /// non-finite guard shape (`serialise_field_expr_wire`'s own
4288    /// `WireRef::Base { base: Float, .. }` arm) directly against a hand-built
4289    /// tree, the same way #1322's own three parenthesisation tests below pin
4290    /// their own gap. Proves `render_arrow_body`'s `Block` arm reuses
4291    /// `render_compact_stmts` (an `If` with a brace-free `Throw` then-branch,
4292    /// followed by a `Return`) correctly: both statements land on the SAME
4293    /// physical line as the arrow's own `{ ... }`, with no trailing newline
4294    /// leaking out of the block into the enclosing call's own `;`.
4295    #[test]
4296    fn prints_a_block_bodied_arrow_iife() {
4297        let mut program = TsProgram::new();
4298        let guard = TsExpr::Call {
4299            callee: Box::new(TsExpr::Arrow {
4300                params: vec![TsParam {
4301                    name: "v".to_string(),
4302                    ty: Some(TsType::named("number")),
4303                    optional: false,
4304                }],
4305                is_async: false,
4306                generics: Vec::new(),
4307                return_type: None,
4308                body: Box::new(TsArrowBody::Block(vec![
4309                    TsStmt::if_stmt(
4310                        TsExpr::Unary {
4311                            op: TsUnaryOp::Not,
4312                            expr: Box::new(TsExpr::Call {
4313                                callee: Box::new(TsExpr::Member {
4314                                    object: Box::new(TsExpr::Ident("Number".to_string())),
4315                                    property: "isFinite".to_string(),
4316                                }),
4317                                args: vec![TsExpr::Ident("v".to_string())],
4318                            }),
4319                        },
4320                        TsStmt::throw_stmt(
4321                            TsExpr::New {
4322                                callee: Box::new(TsExpr::Ident("Error".to_string())),
4323                                args: vec![TsExpr::Lit(TsLit::Str(
4324                                    "non-finite Float at boundary".to_string(),
4325                                ))],
4326                            },
4327                            None,
4328                        ),
4329                        None,
4330                    ),
4331                    TsStmt::return_stmt(
4332                        Some(TsExpr::As {
4333                            expr: Box::new(TsExpr::Ident("v".to_string())),
4334                            ty: TsType::named("JsonValue"),
4335                        }),
4336                        None,
4337                    ),
4338                ])),
4339            }),
4340            args: vec![TsExpr::Ident("value".to_string())],
4341        };
4342        program.push(TsStmt::expr_stmt(guard, None));
4343        let printed = print(&program, "x.bynk", "", "x.ts");
4344        assert_eq!(
4345            printed.text,
4346            "((v: number) => { if (!Number.isFinite(v)) throw new Error(\"non-finite Float at boundary\"); return v as JsonValue; })(value);\n"
4347        );
4348    }
4349
4350    /// Review of #1322, finding 1: `Arrow` was missing from every
4351    /// parenthesisation rule. Not reachable through any real `bynk-emit`
4352    /// content today (the one grounded `Arrow` site is an object-property
4353    /// value, rendered through the depth-unaware `render_expr` with no
4354    /// operand context) — these three tests pin the fix directly against
4355    /// hand-built trees, the same way #1314's own precedence tests did
4356    /// before any real content exercised them.
4357    #[test]
4358    fn parenthesises_an_arrow_used_as_a_call_callee() {
4359        let mut program = TsProgram::new();
4360        program.push(TsStmt::expr_stmt(
4361            TsExpr::Call {
4362                callee: Box::new(TsExpr::Arrow {
4363                    params: vec![TsParam {
4364                        name: "x".to_string(),
4365                        ty: None,
4366                        optional: false,
4367                    }],
4368                    is_async: false,
4369                    generics: Vec::new(),
4370                    return_type: None,
4371                    body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Ident("x".to_string())))),
4372                }),
4373                args: vec![TsExpr::Lit(TsLit::Num("1".to_string()))],
4374            },
4375            None,
4376        ));
4377        let printed = print(&program, "x.bynk", "", "x.ts");
4378        assert_eq!(printed.text, "((x) => x)(1);\n");
4379    }
4380
4381    #[test]
4382    fn parenthesises_an_arrow_used_as_a_binary_operand() {
4383        let mut program = TsProgram::new();
4384        program.push(TsStmt::expr_stmt(
4385            TsExpr::Binary {
4386                op: TsBinaryOp::NullishCoalescing,
4387                left: Box::new(TsExpr::Ident("a".to_string())),
4388                right: Box::new(TsExpr::Arrow {
4389                    params: vec![TsParam {
4390                        name: "x".to_string(),
4391                        ty: None,
4392                        optional: false,
4393                    }],
4394                    is_async: false,
4395                    generics: Vec::new(),
4396                    return_type: None,
4397                    body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Ident("y".to_string())))),
4398                }),
4399            },
4400            None,
4401        ));
4402        let printed = print(&program, "x.bynk", "", "x.ts");
4403        assert_eq!(printed.text, "a ?? ((x) => y);\n");
4404    }
4405
4406    #[test]
4407    fn parenthesises_an_arrow_used_as_an_as_operand() {
4408        let mut program = TsProgram::new();
4409        program.push(TsStmt::expr_stmt(
4410            TsExpr::As {
4411                expr: Box::new(TsExpr::Arrow {
4412                    params: vec![TsParam {
4413                        name: "x".to_string(),
4414                        ty: None,
4415                        optional: false,
4416                    }],
4417                    is_async: false,
4418                    generics: Vec::new(),
4419                    return_type: None,
4420                    body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Ident("y".to_string())))),
4421                }),
4422                ty: TsType::named("Handler"),
4423            },
4424            None,
4425        ));
4426        let printed = print(&program, "x.bynk", "", "x.ts");
4427        assert_eq!(printed.text, "((x) => y) as Handler;\n");
4428    }
4429
4430    #[test]
4431    fn prints_optional_member_and_optional_index_chained() {
4432        let mut program = TsProgram::new();
4433        program.push(TsStmt::expr_stmt(
4434            TsExpr::OptionalIndex {
4435                object: Box::new(TsExpr::OptionalMember {
4436                    object: Box::new(TsExpr::Ident("x".to_string())),
4437                    property: "process".to_string(),
4438                }),
4439                index: Box::new(TsExpr::Lit(TsLit::Str("secret".to_string()))),
4440            },
4441            None,
4442        ));
4443        let printed = print(&program, "x.bynk", "", "x.ts");
4444        assert_eq!(printed.text, "x?.process?.[\"secret\"];\n");
4445    }
4446
4447    #[test]
4448    fn print_type_renders_an_optional_object_field() {
4449        let ty = TsType::Object(vec![
4450            TsTypeMember::optional_prop("env", TsType::named("unknown")),
4451            TsTypeMember::prop("name", TsType::named("string")),
4452        ]);
4453        assert_eq!(print_type(&ty), "{ env?: unknown; name: string }");
4454    }
4455
4456    #[test]
4457    fn prints_a_top_level_function_declaration() {
4458        let mut program = TsProgram::new();
4459        program.push(TsStmt::decl(
4460            TsDecl::Export(Box::new(TsDecl::Function {
4461                name: "compose".to_string(),
4462                generics: Vec::new(),
4463                params: vec![TsParam {
4464                    name: "env".to_string(),
4465                    ty: Some(TsType::named("Env")),
4466                    optional: false,
4467                }],
4468                return_type: None,
4469                body: vec![TsStmt::return_stmt(Some(TsExpr::Lit(TsLit::Null)), None)],
4470                is_async: false,
4471                inline: false,
4472            })),
4473            None,
4474        ));
4475        let printed = print(&program, "x.bynk", "", "x.ts");
4476        assert_eq!(
4477            printed.text,
4478            "export function compose(env: Env) {\n  return null;\n}\n"
4479        );
4480    }
4481
4482    /// #1351's own real gap: `TsDecl::Function` had no `generics` field —
4483    /// `emit_free_fn`'s own v0.20a erased generics (`export function
4484    /// foo<A, B>(...)`) needed one. Review of #1352, finding 3: every other
4485    /// existing `TsDecl::Function` test only threads `generics: Vec::new()`
4486    /// through; this pins the non-empty case directly, matching #1339's own
4487    /// precedent of a dedicated test per new field.
4488    #[test]
4489    fn prints_a_generic_top_level_function_declaration() {
4490        let mut program = TsProgram::new();
4491        program.push(TsStmt::decl(
4492            TsDecl::Export(Box::new(TsDecl::Function {
4493                name: "identity".to_string(),
4494                generics: vec!["A".to_string(), "B".to_string()],
4495                params: vec![TsParam {
4496                    name: "x".to_string(),
4497                    ty: Some(TsType::named("A")),
4498                    optional: false,
4499                }],
4500                return_type: Some(TsType::named("A")),
4501                body: vec![TsStmt::return_stmt(
4502                    Some(TsExpr::Ident("x".to_string())),
4503                    None,
4504                )],
4505                is_async: false,
4506                inline: false,
4507            })),
4508            None,
4509        ));
4510        let printed = print(&program, "x.bynk", "", "x.ts");
4511        assert_eq!(
4512            printed.text,
4513            "export function identity<A, B>(x: A): A {\n  return x;\n}\n"
4514        );
4515    }
4516
4517    #[test]
4518    fn prints_a_top_level_type_alias() {
4519        let mut program = TsProgram::new();
4520        program.push(TsStmt::decl(
4521            TsDecl::TypeAlias {
4522                name: "Foo".to_string(),
4523                type_params: Vec::new(),
4524                ty: TsType::named("{ get(id: any): any }"),
4525            },
4526            None,
4527        ));
4528        let printed = print(&program, "x.bynk", "", "x.ts");
4529        assert_eq!(printed.text, "type Foo = { get(id: any): any };\n");
4530    }
4531
4532    #[test]
4533    fn prints_a_namespace_import_adjacent_to_a_named_import_with_no_blank_line() {
4534        let mut program = TsProgram::new();
4535        program.push(TsStmt::decl(
4536            TsDecl::Import {
4537                type_only: false,
4538                names: vec!["a".to_string()],
4539                from: "./x.js".to_string(),
4540            },
4541            None,
4542        ));
4543        program.push(TsStmt::decl(
4544            TsDecl::ImportNamespace {
4545                type_only: false,
4546                alias: "handlers".to_string(),
4547                from: "./handlers.js".to_string(),
4548            },
4549            None,
4550        ));
4551        let printed = print(&program, "x.bynk", "", "x.ts");
4552        assert_eq!(
4553            printed.text,
4554            "import { a } from \"./x.js\";\nimport * as handlers from \"./handlers.js\";\n"
4555        );
4556    }
4557
4558    /// Arc C, step (10) (#1392): `ImportNamespace.type_only`, a parallel gap
4559    /// by omission — `TsDecl::Import` already had this field, but nothing
4560    /// before `emit_cross_context_namespace_imports`'s own conversion built
4561    /// a type-only namespace import (`import type * as ns from "...";`, a
4562    /// Workers-mode consumed-context import reaching the callee's types
4563    /// only, #661).
4564    #[test]
4565    fn prints_a_type_only_namespace_import() {
4566        let mut program = TsProgram::new();
4567        program.push(TsStmt::decl(
4568            TsDecl::ImportNamespace {
4569                type_only: true,
4570                alias: "commerce_payment".to_string(),
4571                from: "../commerce-payment/handlers.js".to_string(),
4572            },
4573            None,
4574        ));
4575        let printed = print(&program, "x.bynk", "", "x.ts");
4576        assert_eq!(
4577            printed.text,
4578            "import type * as commerce_payment from \"../commerce-payment/handlers.js\";\n"
4579        );
4580    }
4581
4582    /// Arc C, slice 37 (#1409, `tests_emit.rs`'s own slice G): `emit_integration_
4583    /// module`'s own per-participant `import worker_{ns} from "../workers/{dir}/
4584    /// index.js";` (the participant's Worker entry module's default export) —
4585    /// the first real default import anywhere in `bynk-emit`'s own converted
4586    /// content.
4587    #[test]
4588    fn prints_a_default_import() {
4589        let mut program = TsProgram::new();
4590        program.push(TsStmt::decl(
4591            TsDecl::ImportDefault {
4592                alias: "worker_shop_api".to_string(),
4593                from: "../workers/shop-api/index.js".to_string(),
4594            },
4595            None,
4596        ));
4597        let printed = print(&program, "x.bynk", "", "x.ts");
4598        assert_eq!(
4599            printed.text,
4600            "import worker_shop_api from \"../workers/shop-api/index.js\";\n"
4601        );
4602    }
4603
4604    /// #1321's own real gap beyond the accepted proposal's five: ES2019's
4605    /// optional catch binding (`emit_http_sum_wrapper`'s own `} catch {`,
4606    /// no `(e)` at all).
4607    #[test]
4608    fn prints_a_parameterless_catch() {
4609        let mut program = TsProgram::new();
4610        program.push(TsStmt::try_catch(
4611            TsStmt::block(vec![], None),
4612            None::<String>,
4613            TsStmt::block(vec![], None),
4614            None,
4615        ));
4616        let printed = print(&program, "x.bynk", "", "x.ts");
4617        assert_eq!(printed.text, "try {\n} catch {\n}\n");
4618    }
4619
4620    #[test]
4621    fn prints_typeof_and_the_new_comparison_and_logical_operators() {
4622        let mut program = TsProgram::new();
4623        program.push(TsStmt::expr_stmt(
4624            TsExpr::Unary {
4625                op: TsUnaryOp::Typeof,
4626                expr: Box::new(TsExpr::Ident("x".to_string())),
4627            },
4628            None,
4629        ));
4630        program.push(TsStmt::expr_stmt(
4631            TsExpr::Binary {
4632                op: TsBinaryOp::StrictEq,
4633                left: Box::new(TsExpr::Ident("a".to_string())),
4634                right: Box::new(TsExpr::Ident("b".to_string())),
4635            },
4636            None,
4637        ));
4638        program.push(TsStmt::expr_stmt(
4639            TsExpr::Binary {
4640                op: TsBinaryOp::StrictNotEq,
4641                left: Box::new(TsExpr::Ident("a".to_string())),
4642                right: Box::new(TsExpr::Ident("b".to_string())),
4643            },
4644            None,
4645        ));
4646        program.push(TsStmt::expr_stmt(
4647            TsExpr::Binary {
4648                op: TsBinaryOp::And,
4649                left: Box::new(TsExpr::Ident("a".to_string())),
4650                right: Box::new(TsExpr::Ident("b".to_string())),
4651            },
4652            None,
4653        ));
4654        program.push(TsStmt::expr_stmt(
4655            TsExpr::Binary {
4656                op: TsBinaryOp::Or,
4657                left: Box::new(TsExpr::Ident("a".to_string())),
4658                right: Box::new(TsExpr::Ident("b".to_string())),
4659            },
4660            None,
4661        ));
4662        let printed = print(&program, "x.bynk", "", "x.ts");
4663        assert_eq!(
4664            printed.text,
4665            "typeof x;\n\na === b;\n\na !== b;\n\na && b;\n\na || b;\n"
4666        );
4667    }
4668
4669    /// #1321's own real gap: with only `??` in the algebra, a nested
4670    /// `Binary` operand of another `Binary` was always parenthesized,
4671    /// regardless of operator (see `parenthesises_a_nested_binary_operand_
4672    /// of_another_binary`, unchanged). `workers.rs`'s own real content
4673    /// nests a *strictly higher precedence* comparison inside `||`/`&&`
4674    /// (`__authz === null || !__authz.startsWith(...)`) and the byte-golden
4675    /// fixtures have no parens around it — this pins that the printer
4676    /// omits them precisely in that case, not more broadly.
4677    #[test]
4678    fn a_strictly_higher_precedence_comparison_needs_no_parens_inside_or_or_and() {
4679        let mut program = TsProgram::new();
4680        program.push(TsStmt::expr_stmt(
4681            TsExpr::Binary {
4682                op: TsBinaryOp::Or,
4683                left: Box::new(TsExpr::Binary {
4684                    op: TsBinaryOp::StrictEq,
4685                    left: Box::new(TsExpr::Ident("a".to_string())),
4686                    right: Box::new(TsExpr::Lit(TsLit::Null)),
4687                }),
4688                right: Box::new(TsExpr::Unary {
4689                    op: TsUnaryOp::Not,
4690                    expr: Box::new(TsExpr::Ident("b".to_string())),
4691                }),
4692            },
4693            None,
4694        ));
4695        let printed = print(&program, "x.bynk", "", "x.ts");
4696        assert_eq!(printed.text, "a === null || !b;\n");
4697    }
4698
4699    // -- #1323 (workers_entry.rs): Switch, ExportDefault, Conditional,
4700    // TsType::Object readonly/method members, and the smaller mechanical
4701    // gaps found during implementation (ReExport, Blank, TsLit::Bool,
4702    // TsExpr::Paren, same-operator-chain flattening). --
4703
4704    /// #1323's own largest gap: `switch (<discriminant>) { <cases> }`. A
4705    /// non-`default` `case` is `{ }`-blocked; `default` is not — pinned
4706    /// against `workers_entry.rs`'s own real internal-dispatch shape.
4707    #[test]
4708    fn prints_a_switch_with_a_braced_case_and_an_unbraced_default() {
4709        let mut program = TsProgram::new();
4710        program.push(TsStmt::switch_stmt(
4711            TsExpr::Ident("servicePath".to_string()),
4712            vec![
4713                TsSwitchCase {
4714                    test: Some(TsExpr::Lit(TsLit::Str("orders".to_string()))),
4715                    body: vec![TsStmt::return_stmt(None, None)],
4716                    default_braced: false,
4717                    case_braced: true,
4718                },
4719                TsSwitchCase {
4720                    test: None,
4721                    body: vec![TsStmt::expr_stmt(
4722                        TsExpr::Call {
4723                            callee: Box::new(TsExpr::Member {
4724                                object: Box::new(TsExpr::Ident("console".to_string())),
4725                                property: "log".to_string(),
4726                            }),
4727                            args: vec![],
4728                        },
4729                        None,
4730                    )],
4731                    default_braced: false,
4732                    case_braced: true,
4733                },
4734            ],
4735            None,
4736        ));
4737        let printed = print(&program, "x.bynk", "", "x.ts");
4738        assert_eq!(
4739            printed.text,
4740            "switch (servicePath) {\n  case \"orders\": {\n    return;\n  }\n  default:\n    console.log();\n}\n"
4741        );
4742    }
4743
4744    /// Arc C, slice 33 (`tests_emit.rs` slice C, #1401): `TsSwitchCase.
4745    /// default_braced` — `emit_stub_class`'s own `ReturnsEach` dispatch
4746    /// braces its `default` case, unlike `workers_entry.rs`'s own unbraced
4747    /// one pinned just above.
4748    #[test]
4749    fn prints_a_switch_with_a_braced_default() {
4750        let mut program = TsProgram::new();
4751        program.push(TsStmt::switch_stmt(
4752            TsExpr::Ident("__k".to_string()),
4753            vec![TsSwitchCase {
4754                test: None,
4755                body: vec![TsStmt::expr_stmt(TsExpr::Ident("x".to_string()), None)],
4756                default_braced: true,
4757                case_braced: true,
4758            }],
4759            None,
4760        ));
4761        let printed = print(&program, "x.bynk", "", "x.ts");
4762        assert_eq!(
4763            printed.text,
4764            "switch (__k) {\n  default: {\n    x;\n  }\n}\n"
4765        );
4766    }
4767
4768    /// Arc E slice 6 (#1445): `TsSwitchCase.case_braced` — the mirror-image
4769    /// gap `default_braced` left open. `emit_sum_codec`'s own payload-free
4770    /// variant case is unbraced right beside a braced payload-carrying
4771    /// sibling in the *same* switch, pinned against `212_json_codec`'s own
4772    /// mixed `Status` fixture (`case "Pending": return { kind: "Pending"
4773    /// };` then `case "Shipped": { ... }`).
4774    #[test]
4775    fn prints_a_switch_with_an_unbraced_case_beside_a_braced_one() {
4776        let mut program = TsProgram::new();
4777        program.push(TsStmt::switch_stmt(
4778            TsExpr::Ident("kind".to_string()),
4779            vec![
4780                TsSwitchCase {
4781                    test: Some(TsExpr::Lit(TsLit::Str("Pending".to_string()))),
4782                    body: vec![TsStmt::return_stmt(
4783                        Some(TsExpr::Ident("a".to_string())),
4784                        None,
4785                    )],
4786                    default_braced: false,
4787                    case_braced: false,
4788                },
4789                TsSwitchCase {
4790                    test: Some(TsExpr::Lit(TsLit::Str("Shipped".to_string()))),
4791                    body: vec![TsStmt::return_stmt(
4792                        Some(TsExpr::Ident("b".to_string())),
4793                        None,
4794                    )],
4795                    default_braced: false,
4796                    case_braced: true,
4797                },
4798            ],
4799            None,
4800        ));
4801        let printed = print(&program, "x.bynk", "", "x.ts");
4802        assert_eq!(
4803            printed.text,
4804            "switch (kind) {\n  case \"Pending\":\n    return a;\n  case \"Shipped\": {\n    return b;\n  }\n}\n"
4805        );
4806    }
4807
4808    /// #1323's own second gap: `export default <expr>;` — a default export
4809    /// of a bare expression, not a declaration. Uses the same depth-aware
4810    /// multiline-object handling `const`/`let`/`return` already get.
4811    #[test]
4812    fn prints_an_export_default_object_literal_multiline() {
4813        let mut program = TsProgram::new();
4814        program.push(TsStmt::decl(
4815            TsDecl::ExportDefault(TsExpr::multiline_object(vec![(
4816                "fetch".to_string(),
4817                TsExpr::Ident("handler".to_string()),
4818            )])),
4819            None,
4820        ));
4821        let printed = print(&program, "x.bynk", "", "x.ts");
4822        assert_eq!(printed.text, "export default {\n  fetch: handler,\n};\n");
4823    }
4824
4825    /// #1355's own real gap: a `multiline: true` `TsExpr::Object` nested as
4826    /// one `Prop`'s own value inside ANOTHER multiline object —
4827    /// `emit_messages_bundle`'s own real doubly-nested `{ locale: { code:
4828    /// expr, ... }, ... }` table. Before this slice, `render_multiline_
4829    /// object_entry`'s own `Prop` arm rendered its value through the plain,
4830    /// depth-unaware `render_expr`, which ignores `multiline` entirely —
4831    /// this would have silently collapsed the inner object to one line.
4832    #[test]
4833    fn prints_a_nested_multiline_object_as_a_props_own_value() {
4834        let mut program = TsProgram::new();
4835        program.push(TsStmt::const_stmt(
4836            TsBindingName::Ident("byLocale".to_string()),
4837            None,
4838            TsExpr::multiline_object_entries(vec![TsObjectEntry::Prop(
4839                "\"en\"".to_string(),
4840                TsExpr::multiline_object_entries(vec![
4841                    TsObjectEntry::Prop(
4842                        "\"greeting\"".to_string(),
4843                        TsExpr::Ident("renderGreeting".to_string()),
4844                    ),
4845                    TsObjectEntry::Prop(
4846                        "\"farewell\"".to_string(),
4847                        TsExpr::Ident("renderFarewell".to_string()),
4848                    ),
4849                ]),
4850            )]),
4851            None,
4852        ));
4853        let printed = print(&program, "x.bynk", "", "x.ts");
4854        assert_eq!(
4855            printed.text,
4856            "const byLocale = {\n  \"en\": {\n    \"greeting\": renderGreeting,\n    \"farewell\": renderFarewell,\n  },\n};\n"
4857        );
4858    }
4859
4860    /// Review of #1356, finding 2: `render_multiline_object_entry`'s own
4861    /// `Prop` arm routes through `render_stmt_level_expr` for BOTH
4862    /// `TsExpr::Object`/`Array` (see `TsExpr::Array`'s own doc, updated by
4863    /// #1355) — the object case is pinned above, this is the array sibling,
4864    /// making that doc claim self-verifying rather than merely asserted.
4865    #[test]
4866    fn prints_a_nested_multiline_array_as_a_props_own_value() {
4867        let mut program = TsProgram::new();
4868        program.push(TsStmt::const_stmt(
4869            TsBindingName::Ident("byGroup".to_string()),
4870            None,
4871            TsExpr::multiline_object_entries(vec![TsObjectEntry::Prop(
4872                "\"a\"".to_string(),
4873                TsExpr::multiline_array(vec![
4874                    TsExpr::Ident("one".to_string()),
4875                    TsExpr::Ident("two".to_string()),
4876                ]),
4877            )]),
4878            None,
4879        ));
4880        let printed = print(&program, "x.bynk", "", "x.ts");
4881        assert_eq!(
4882            printed.text,
4883            "const byGroup = {\n  \"a\": [\n    one,\n    two,\n  ],\n};\n"
4884        );
4885    }
4886
4887    /// #1323's own third gap: `test ? consequent : alternate`.
4888    #[test]
4889    fn prints_a_conditional_expression() {
4890        let mut program = TsProgram::new();
4891        program.push(TsStmt::expr_stmt(
4892            TsExpr::Conditional {
4893                test: Box::new(TsExpr::Binary {
4894                    op: TsBinaryOp::StrictEq,
4895                    left: Box::new(TsExpr::Ident("method".to_string())),
4896                    right: Box::new(TsExpr::Lit(TsLit::Str("HEAD".to_string()))),
4897                }),
4898                consequent: Box::new(TsExpr::Lit(TsLit::Num("1".to_string()))),
4899                alternate: Box::new(TsExpr::Lit(TsLit::Num("2".to_string()))),
4900            },
4901            None,
4902        ));
4903        let printed = print(&program, "x.bynk", "", "x.ts");
4904        assert_eq!(printed.text, "method === \"HEAD\" ? 1 : 2;\n");
4905    }
4906
4907    /// Review-anticipated gap (matching #1322 finding 1's own class): a
4908    /// `Conditional` used as a binary operand, an `as`-operand, or a call
4909    /// callee all need parens — closed proactively in this same slice
4910    /// rather than left for a review round to re-find.
4911    #[test]
4912    fn parenthesises_a_conditional_used_as_a_binary_operand_and_an_as_operand() {
4913        let cond = || TsExpr::Conditional {
4914            test: Box::new(TsExpr::Ident("a".to_string())),
4915            consequent: Box::new(TsExpr::Ident("b".to_string())),
4916            alternate: Box::new(TsExpr::Ident("c".to_string())),
4917        };
4918        let mut program = TsProgram::new();
4919        program.push(TsStmt::expr_stmt(
4920            TsExpr::Binary {
4921                op: TsBinaryOp::NullishCoalescing,
4922                left: Box::new(TsExpr::Ident("x".to_string())),
4923                right: Box::new(cond()),
4924            },
4925            None,
4926        ));
4927        program.push(TsStmt::expr_stmt(
4928            TsExpr::As {
4929                expr: Box::new(cond()),
4930                ty: TsType::named("string"),
4931            },
4932            None,
4933        ));
4934        let printed = print(&program, "x.bynk", "", "x.ts");
4935        assert_eq!(
4936            printed.text,
4937            "x ?? (a ? b : c);\n\n(a ? b : c) as string;\n"
4938        );
4939    }
4940
4941    /// #1323's own real gap: a `Method` member of a type-position object
4942    /// literal (no body), alongside a `readonly` property.
4943    #[test]
4944    fn print_type_renders_a_readonly_field_and_a_method_signature() {
4945        let ty = TsType::Object(vec![
4946            TsTypeMember::readonly_prop("cron", TsType::named("string")),
4947            TsTypeMember::method("ack", vec![], TsType::named("void")),
4948            TsTypeMember::method(
4949                "waitUntil",
4950                vec![TsParam {
4951                    name: "promise".to_string(),
4952                    ty: Some(TsType::named_with_args(
4953                        "Promise",
4954                        vec![TsType::named("unknown")],
4955                    )),
4956                    optional: false,
4957                }],
4958                TsType::named("void"),
4959            ),
4960        ]);
4961        assert_eq!(
4962            print_type(&ty),
4963            "{ readonly cron: string; ack(): void; waitUntil(promise: Promise<unknown>): void }"
4964        );
4965    }
4966
4967    /// #1323's own real gap: `TsObjectEntry::Method` gained a
4968    /// `return_type` field (mirroring `TsClassMethod`'s own existing one) —
4969    /// `workers_entry.rs`'s own `export default { fetch, scheduled?,
4970    /// queue? }` entries all carry an explicit return-type annotation,
4971    /// unlike `workers.rs`'s own `compose`-returned wrapper methods
4972    /// (`#1321`), none of which do.
4973    #[test]
4974    fn prints_a_multiline_object_methods_own_return_type_annotation() {
4975        let mut program = TsProgram::new();
4976        program.push(TsStmt::decl(
4977            TsDecl::ExportDefault(TsExpr::multiline_object_entries(vec![
4978                TsObjectEntry::Method {
4979                    name: "fetch".to_string(),
4980                    is_async: true,
4981                    generics: Vec::new(),
4982                    params: vec![],
4983                    return_type: Some(TsType::named_with_args(
4984                        "Promise",
4985                        vec![TsType::named("Response")],
4986                    )),
4987                    doc: None,
4988                    inline: false,
4989                    body: vec![],
4990                },
4991            ])),
4992            None,
4993        ));
4994        let printed = print(&program, "x.bynk", "", "x.ts");
4995        assert_eq!(
4996            printed.text,
4997            "export default {\n  async fetch(): Promise<Response> {\n  },\n};\n"
4998        );
4999    }
5000
5001    /// #1323's own real correction: `Await` no longer auto-parenthesizes
5002    /// under `As` — `workers_entry.rs`'s own real `await request.json() as
5003    /// JsonValue` has no parens (`as` binds looser than `await`, so none
5004    /// are grammatically needed; P7.8's original "always parenthesize"
5005    /// reasoning conflated one file's own real text with a grammar
5006    /// requirement).
5007    #[test]
5008    fn an_await_used_as_an_as_operand_no_longer_needs_parens() {
5009        let mut program = TsProgram::new();
5010        program.push(TsStmt::const_stmt(
5011            TsBindingName::Ident("args".to_string()),
5012            None,
5013            TsExpr::As {
5014                expr: Box::new(TsExpr::Await(Box::new(TsExpr::Call {
5015                    callee: Box::new(TsExpr::Member {
5016                        object: Box::new(TsExpr::Ident("request".to_string())),
5017                        property: "json".to_string(),
5018                    }),
5019                    args: vec![],
5020                }))),
5021                ty: TsType::named("JsonValue"),
5022            },
5023            None,
5024        ));
5025        let printed = print(&program, "x.bynk", "", "x.ts");
5026        assert_eq!(
5027            printed.text,
5028            "const args = await request.json() as JsonValue;\n"
5029        );
5030    }
5031
5032    /// #1353's own real gap: `bynk_ts::TsStmtKind::Throw`, added for
5033    /// `emit_contract_guarded_body`'s own real `throw __e;` sites. Review of
5034    /// #1354, finding 1: every real call site today reaches this only
5035    /// through an `InlineBlock` (`render_stmt(out, stmt, 0)`), so the
5036    /// top-level, non-inline `render_stmt` arm's own `indent(depth)` at a
5037    /// nonzero depth was untested — pinned directly here, mirroring
5038    /// `Return`'s own shape exactly.
5039    #[test]
5040    fn prints_a_throw_statement_at_a_nested_depth() {
5041        let stmt = TsStmt::throw_stmt(TsExpr::Ident("__e".to_string()), None);
5042        assert_eq!(print_stmt(&stmt, 2), "    throw __e;\n");
5043    }
5044
5045    /// #1353's own real gap, byte-for-byte: the whole contract-guard shape
5046    /// `emit_contract_guarded_body` builds (`if (!(pred)) { const __e = ...;
5047    /// __e.name = ...; throw __e; }`) — an `If` over an `InlineBlock` of a
5048    /// `Const`/`Assign`/`Throw` trio — asserted as one exact string. Review
5049    /// of #1354, finding 1: no existing test (fixture, `contract_behaviour.
5050    /// rs`, `source_map.rs`) asserts this shape's own bytes; they only
5051    /// `contains`-check a runtime message or a generated line number, none
5052    /// would catch a missing space, a reordered statement, or a lost
5053    /// indent.
5054    #[test]
5055    fn prints_a_contract_guard_if_stmt() {
5056        let cond = TsExpr::Unary {
5057            op: TsUnaryOp::Not,
5058            expr: Box::new(TsExpr::Paren(Box::new(TsExpr::Ident("n > 0".to_string())))),
5059        };
5060        let new_error = TsExpr::New {
5061            callee: Box::new(TsExpr::Ident("Error".to_string())),
5062            args: vec![TsExpr::template_lit(
5063                vec![
5064                    "contract violated: precondition \\`positive\\` of scale (n=${n})".to_string(),
5065                ],
5066                Vec::new(),
5067            )],
5068        };
5069        let const_e = TsStmt::const_stmt(
5070            TsBindingName::Ident("__e".to_string()),
5071            None,
5072            new_error,
5073            None,
5074        );
5075        let assign_name = TsStmt::assign(
5076            TsExpr::Member {
5077                object: Box::new(TsExpr::Ident("__e".to_string())),
5078                property: "name".to_string(),
5079            },
5080            TsExpr::Lit(TsLit::Str("BynkContractError".to_string())),
5081            None,
5082        );
5083        let throw_e = TsStmt::throw_stmt(TsExpr::Ident("__e".to_string()), None);
5084        let then_branch = TsStmt::inline_block(vec![const_e, assign_name, throw_e], None);
5085        let if_stmt = TsStmt::if_stmt(cond, then_branch, None);
5086        assert_eq!(
5087            print_stmt(&if_stmt, 1),
5088            "  if (!(n > 0)) { const __e = new Error(`contract violated: precondition \\`positive\\` of scale (n=${n})`); __e.name = \"BynkContractError\"; throw __e; }\n"
5089        );
5090    }
5091
5092    /// #1323's own real gap: `if`'s own `else` branch, printed on a fresh
5093    /// line at the `if`'s own indent, then following the same block-vs-
5094    /// inline rule the `if` itself does — `workers_entry.rs`'s own real
5095    /// queue-consumer ack/retry dispatch.
5096    #[test]
5097    fn prints_an_if_else_with_an_inline_then_and_an_inline_block_else() {
5098        let mut program = TsProgram::new();
5099        program.push(TsStmt::if_else_stmt(
5100            TsExpr::Binary {
5101                op: TsBinaryOp::StrictEq,
5102                left: Box::new(TsExpr::Member {
5103                    object: Box::new(TsExpr::Ident("result".to_string())),
5104                    property: "tag".to_string(),
5105                }),
5106                right: Box::new(TsExpr::Lit(TsLit::Str("Ack".to_string()))),
5107            },
5108            TsStmt::expr_stmt(
5109                TsExpr::Call {
5110                    callee: Box::new(TsExpr::Member {
5111                        object: Box::new(TsExpr::Ident("msg".to_string())),
5112                        property: "ack".to_string(),
5113                    }),
5114                    args: vec![],
5115                },
5116                None,
5117            ),
5118            TsStmt::inline_block(
5119                vec![TsStmt::expr_stmt(
5120                    TsExpr::Call {
5121                        callee: Box::new(TsExpr::Member {
5122                            object: Box::new(TsExpr::Ident("msg".to_string())),
5123                            property: "retry".to_string(),
5124                        }),
5125                        args: vec![],
5126                    },
5127                    None,
5128                )],
5129                None,
5130            ),
5131            None,
5132        ));
5133        let printed = print(&program, "x.bynk", "", "x.ts");
5134        assert_eq!(
5135            printed.text,
5136            "if (result.tag === \"Ack\") msg.ack();\nelse { msg.retry(); }\n"
5137        );
5138    }
5139
5140    /// #1323's own real gap: a `TryCatch`'s own `catch` body can be an
5141    /// `InlineBlock` too — the braces stay on their own lines (this shape's
5142    /// usual convention), but the content packs onto one compact line.
5143    /// `workers_entry.rs`'s own real queue-consumer catch clause.
5144    #[test]
5145    fn a_try_catchs_own_catch_block_can_be_an_inline_block() {
5146        let mut program = TsProgram::new();
5147        program.push(TsStmt::try_catch(
5148            TsStmt::block(
5149                vec![TsStmt::expr_stmt(TsExpr::Ident("work".to_string()), None)],
5150                None,
5151            ),
5152            Some("e"),
5153            TsStmt::inline_block(
5154                vec![
5155                    TsStmt::expr_stmt(
5156                        TsExpr::Call {
5157                            callee: Box::new(TsExpr::Member {
5158                                object: Box::new(TsExpr::Ident("console".to_string())),
5159                                property: "error".to_string(),
5160                            }),
5161                            args: vec![TsExpr::Ident("e".to_string())],
5162                        },
5163                        None,
5164                    ),
5165                    TsStmt::expr_stmt(
5166                        TsExpr::Call {
5167                            callee: Box::new(TsExpr::Member {
5168                                object: Box::new(TsExpr::Ident("msg".to_string())),
5169                                property: "retry".to_string(),
5170                            }),
5171                            args: vec![],
5172                        },
5173                        None,
5174                    ),
5175                ],
5176                None,
5177            ),
5178            None,
5179        ));
5180        let printed = print(&program, "x.bynk", "", "x.ts");
5181        assert_eq!(
5182            printed.text,
5183            "try {\n  work;\n} catch (e) {\n  console.error(e); msg.retry();\n}\n"
5184        );
5185    }
5186
5187    /// #1323's own real gap: `TsBinaryOp::GreaterThan` — the request-body
5188    /// ceiling guard's own `Number(__contentLength) > <cap>`, the one real
5189    /// site needing a relational (not equality) comparison.
5190    #[test]
5191    fn prints_a_greater_than_comparison() {
5192        let mut program = TsProgram::new();
5193        program.push(TsStmt::expr_stmt(
5194            TsExpr::Binary {
5195                op: TsBinaryOp::GreaterThan,
5196                left: Box::new(TsExpr::Call {
5197                    callee: Box::new(TsExpr::Ident("Number".to_string())),
5198                    args: vec![TsExpr::Ident("__contentLength".to_string())],
5199                }),
5200                right: Box::new(TsExpr::Lit(TsLit::Num("100".to_string()))),
5201            },
5202            None,
5203        ));
5204        let printed = print(&program, "x.bynk", "", "x.ts");
5205        assert_eq!(printed.text, "Number(__contentLength) > 100;\n");
5206    }
5207
5208    /// #1323's own real gap: `TsTypeMember::Index` — `workers_entry.rs`'s
5209    /// own `args as { [k: string]: JsonValue }`, textually distinct from
5210    /// the semantically-equivalent `Record<string, JsonValue>`.
5211    #[test]
5212    fn print_type_renders_an_index_signature() {
5213        let ty = TsType::Object(vec![TsTypeMember::index(
5214            "k",
5215            TsType::named("string"),
5216            TsType::named("JsonValue"),
5217        )]);
5218        assert_eq!(print_type(&ty), "{ [k: string]: JsonValue }");
5219    }
5220
5221    /// #1323's own real gap, found implementing `workers_entry.rs`:
5222    /// `export { a, b } from "spec";` — a re-export, distinct from both
5223    /// `Import` and `Export`. Not classified alongside imports for the
5224    /// "no blank line between adjacent imports" exception.
5225    #[test]
5226    fn prints_a_re_export_with_the_ordinary_blank_line_rule() {
5227        let mut program = TsProgram::new();
5228        program.push(TsStmt::decl(
5229            TsDecl::ImportNamespace {
5230                type_only: false,
5231                alias: "handlers".to_string(),
5232                from: "./handlers.js".to_string(),
5233            },
5234            None,
5235        ));
5236        program.push(TsStmt::decl(
5237            TsDecl::ReExport {
5238                names: vec!["Orders".to_string()],
5239                from: "./handlers.js".to_string(),
5240            },
5241            None,
5242        ));
5243        let printed = print(&program, "x.bynk", "", "x.ts");
5244        assert_eq!(
5245            printed.text,
5246            "import * as handlers from \"./handlers.js\";\n\nexport { Orders } from \"./handlers.js\";\n"
5247        );
5248    }
5249
5250    /// #1323's own real gap, found implementing `workers_entry.rs`: a bare
5251    /// blank line usable at any nesting depth, distinct from `print()`'s
5252    /// own top-level-only policy.
5253    #[test]
5254    fn a_blank_statement_prints_one_empty_line_inside_a_nested_block() {
5255        let mut program = TsProgram::new();
5256        program.push(TsStmt::if_stmt(
5257            TsExpr::Ident("cond".to_string()),
5258            TsStmt::block(
5259                vec![
5260                    TsStmt::expr_stmt(TsExpr::Ident("a".to_string()), None),
5261                    TsStmt::blank(None),
5262                    TsStmt::expr_stmt(TsExpr::Ident("b".to_string()), None),
5263                ],
5264                None,
5265            ),
5266            None,
5267        ));
5268        let printed = print(&program, "x.bynk", "", "x.ts");
5269        assert_eq!(printed.text, "if (cond) {\n  a;\n\n  b;\n}\n");
5270    }
5271
5272    /// #1323's own real gap, found implementing `workers_entry.rs`:
5273    /// `CorsPolicy.credentials`/`SecurityPolicy.nosniff` are real booleans.
5274    #[test]
5275    fn prints_a_boolean_literal() {
5276        let mut program = TsProgram::new();
5277        program.push(TsStmt::expr_stmt(
5278            TsExpr::object(vec![
5279                ("credentials".to_string(), TsExpr::Lit(TsLit::Bool(true))),
5280                ("nosniff".to_string(), TsExpr::Lit(TsLit::Bool(false))),
5281            ]),
5282            None,
5283        ));
5284        let printed = print(&program, "x.bynk", "", "x.ts");
5285        assert_eq!(printed.text, "{ credentials: true, nosniff: false };\n");
5286    }
5287
5288    /// #1323's own real gap: an explicit `Paren` always prints its own
5289    /// literal parens, even when the wrapped expression's own precedence
5290    /// would not otherwise need any — `workers_entry.rs`'s CORS-preflight
5291    /// guard wraps its path-match condition in `(...)` unconditionally,
5292    /// even for a single equality check with nothing lower-precedence than
5293    /// the outer `&&` inside it (which the ordinary precedence-derived
5294    /// rules correctly do NOT parenthesize on their own).
5295    #[test]
5296    fn an_explicit_paren_always_prints_regardless_of_the_inner_expressions_own_precedence() {
5297        let mut program = TsProgram::new();
5298        program.push(TsStmt::expr_stmt(
5299            TsExpr::Binary {
5300                op: TsBinaryOp::And,
5301                left: Box::new(TsExpr::Ident("method".to_string())),
5302                right: Box::new(TsExpr::Paren(Box::new(TsExpr::Binary {
5303                    op: TsBinaryOp::StrictEq,
5304                    left: Box::new(TsExpr::Ident("path".to_string())),
5305                    right: Box::new(TsExpr::Lit(TsLit::Str("/foo".to_string()))),
5306                }))),
5307            },
5308            None,
5309        ));
5310        let printed = print(&program, "x.bynk", "", "x.ts");
5311        assert_eq!(printed.text, "method && (path === \"/foo\");\n");
5312    }
5313
5314    /// #1323's own real gap: a 3-term `||` chain of the *same* operator
5315    /// prints flat, matching `emit_call_handler_dispatch`'s own real
5316    /// `typeof args !== "object" || args === null || Array.isArray(args)`
5317    /// — the pre-#1323 "always parenthesize equal precedence" rule would
5318    /// have wrongly added parens around the first two terms.
5319    #[test]
5320    fn a_three_term_or_chain_of_the_same_operator_prints_flat() {
5321        let mut program = TsProgram::new();
5322        program.push(TsStmt::expr_stmt(
5323            TsExpr::Binary {
5324                op: TsBinaryOp::Or,
5325                left: Box::new(TsExpr::Binary {
5326                    op: TsBinaryOp::Or,
5327                    left: Box::new(TsExpr::Binary {
5328                        op: TsBinaryOp::StrictNotEq,
5329                        left: Box::new(TsExpr::Unary {
5330                            op: TsUnaryOp::Typeof,
5331                            expr: Box::new(TsExpr::Ident("args".to_string())),
5332                        }),
5333                        right: Box::new(TsExpr::Lit(TsLit::Str("object".to_string()))),
5334                    }),
5335                    right: Box::new(TsExpr::Binary {
5336                        op: TsBinaryOp::StrictEq,
5337                        left: Box::new(TsExpr::Ident("args".to_string())),
5338                        right: Box::new(TsExpr::Lit(TsLit::Null)),
5339                    }),
5340                }),
5341                right: Box::new(TsExpr::Call {
5342                    callee: Box::new(TsExpr::Member {
5343                        object: Box::new(TsExpr::Ident("Array".to_string())),
5344                        property: "isArray".to_string(),
5345                    }),
5346                    args: vec![TsExpr::Ident("args".to_string())],
5347                }),
5348            },
5349            None,
5350        ));
5351        let printed = print(&program, "x.bynk", "", "x.ts");
5352        assert_eq!(
5353            printed.text,
5354            "typeof args !== \"object\" || args === null || Array.isArray(args);\n"
5355        );
5356    }
5357
5358    /// Arc C, step (11) (#1388): `TsBinaryOp::Add`'s own real, dominant real
5359    /// site — a message template's own literal/placeholder segments,
5360    /// left-folded via `.join(" + ")` — needs the identical flat-chain
5361    /// treatment `||`/`&&` already established, joining the same
5362    /// associativity exemption in `render_binary_operand`, not a
5363    /// bespoke third rule.
5364    #[test]
5365    fn a_three_term_add_chain_of_the_same_operator_prints_flat() {
5366        let mut program = TsProgram::new();
5367        program.push(TsStmt::expr_stmt(
5368            TsExpr::Binary {
5369                op: TsBinaryOp::Add,
5370                left: Box::new(TsExpr::Binary {
5371                    op: TsBinaryOp::Add,
5372                    left: Box::new(TsExpr::Lit(TsLit::Str("a".to_string()))),
5373                    right: Box::new(TsExpr::Lit(TsLit::Str("b".to_string()))),
5374                }),
5375                right: Box::new(TsExpr::Lit(TsLit::Str("c".to_string()))),
5376            },
5377            None,
5378        ));
5379        let printed = print(&program, "x.bynk", "", "x.ts");
5380        assert_eq!(printed.text, "\"a\" + \"b\" + \"c\";\n");
5381    }
5382
5383    /// Review of #1389, finding 1: unlike `||`/`&&` (semantically
5384    /// associative — flattening either side is safe), `+` is only
5385    /// grammatically left-associative — `1 + (2 + "3")` and `(1 + 2) + "3"`
5386    /// disagree once a number joins the chain. A right-nested `Add` must
5387    /// keep its parens, matching every other non-`Or`/`And` same-operator
5388    /// chain's own treatment (`a_right_nested_strict_eq_chain_keeps_its_
5389    /// parens`/`a_right_nested_greater_than_chain_keeps_its_parens`).
5390    #[test]
5391    fn a_right_nested_add_chain_keeps_its_parens() {
5392        let mut program = TsProgram::new();
5393        program.push(TsStmt::expr_stmt(
5394            TsExpr::Binary {
5395                op: TsBinaryOp::Add,
5396                left: Box::new(TsExpr::Lit(TsLit::Num("1".to_string()))),
5397                right: Box::new(TsExpr::Binary {
5398                    op: TsBinaryOp::Add,
5399                    left: Box::new(TsExpr::Lit(TsLit::Num("2".to_string()))),
5400                    right: Box::new(TsExpr::Lit(TsLit::Str("3".to_string()))),
5401                }),
5402            },
5403            None,
5404        ));
5405        let printed = print(&program, "x.bynk", "", "x.ts");
5406        assert_eq!(printed.text, "1 + (2 + \"3\");\n");
5407    }
5408
5409    /// `Add`'s own new `binary_precedence` entry (higher than
5410    /// `GreaterThan`, matching real JS/TS) — a nested `Add` under a
5411    /// `GreaterThan` needs no parens (`+` binds tighter), the reverse
5412    /// direction does.
5413    #[test]
5414    fn add_binds_tighter_than_greater_than_on_both_sides() {
5415        let mut program = TsProgram::new();
5416        program.push(TsStmt::expr_stmt(
5417            TsExpr::Binary {
5418                op: TsBinaryOp::GreaterThan,
5419                left: Box::new(TsExpr::Binary {
5420                    op: TsBinaryOp::Add,
5421                    left: Box::new(TsExpr::Ident("a".to_string())),
5422                    right: Box::new(TsExpr::Ident("b".to_string())),
5423                }),
5424                right: Box::new(TsExpr::Ident("c".to_string())),
5425            },
5426            None,
5427        ));
5428        let printed = print(&program, "x.bynk", "", "x.ts");
5429        assert_eq!(printed.text, "a + b > c;\n");
5430    }
5431
5432    /// The reverse nesting: a `GreaterThan` operand under an outer `Add`
5433    /// needs its parens (`+` binds tighter, so `Add`'s own precedence check
5434    /// against a lower-precedence `GreaterThan` operand must still fire).
5435    #[test]
5436    fn greater_than_nested_under_add_keeps_its_parens() {
5437        let mut program = TsProgram::new();
5438        program.push(TsStmt::expr_stmt(
5439            TsExpr::Binary {
5440                op: TsBinaryOp::Add,
5441                left: Box::new(TsExpr::Ident("a".to_string())),
5442                right: Box::new(TsExpr::Binary {
5443                    op: TsBinaryOp::GreaterThan,
5444                    left: Box::new(TsExpr::Ident("b".to_string())),
5445                    right: Box::new(TsExpr::Ident("c".to_string())),
5446                }),
5447            },
5448            None,
5449        ));
5450        let printed = print(&program, "x.bynk", "", "x.ts");
5451        assert_eq!(printed.text, "a + (b > c);\n");
5452    }
5453
5454    /// Review of #1324, finding 1: the same-operator flattening #1323 added
5455    /// for `||`/`&&` was wrongly applied to every operator, including
5456    /// non-associative ones — a right-nested `StrictEq` inside `StrictEq`
5457    /// must keep its parens (`a === b === c` parses as `(a === b) === c`,
5458    /// not the tree's real `a === (b === c)`).
5459    #[test]
5460    fn a_right_nested_strict_eq_chain_keeps_its_parens() {
5461        let mut program = TsProgram::new();
5462        program.push(TsStmt::expr_stmt(
5463            TsExpr::Binary {
5464                op: TsBinaryOp::StrictEq,
5465                left: Box::new(TsExpr::Ident("a".to_string())),
5466                right: Box::new(TsExpr::Binary {
5467                    op: TsBinaryOp::StrictEq,
5468                    left: Box::new(TsExpr::Ident("b".to_string())),
5469                    right: Box::new(TsExpr::Ident("c".to_string())),
5470                }),
5471            },
5472            None,
5473        ));
5474        let printed = print(&program, "x.bynk", "", "x.ts");
5475        assert_eq!(printed.text, "a === (b === c);\n");
5476    }
5477
5478    /// Same finding, for the new `GreaterThan` operator: `a > b > c` parses
5479    /// as `(a > b) > c` (a boolean compared against `c`), not the tree's
5480    /// real `a > (b > c)` — a right-nested chain must keep its parens.
5481    #[test]
5482    fn a_right_nested_greater_than_chain_keeps_its_parens() {
5483        let mut program = TsProgram::new();
5484        program.push(TsStmt::expr_stmt(
5485            TsExpr::Binary {
5486                op: TsBinaryOp::GreaterThan,
5487                left: Box::new(TsExpr::Ident("a".to_string())),
5488                right: Box::new(TsExpr::Binary {
5489                    op: TsBinaryOp::GreaterThan,
5490                    left: Box::new(TsExpr::Ident("b".to_string())),
5491                    right: Box::new(TsExpr::Ident("c".to_string())),
5492                }),
5493            },
5494            None,
5495        ));
5496        let printed = print(&program, "x.bynk", "", "x.ts");
5497        assert_eq!(printed.text, "a > (b > c);\n");
5498    }
5499
5500    /// Review of #1324, finding 2: a `Blank` used as an `if`'s brace-free
5501    /// body used to render as a bare newline, silently letting the very
5502    /// next statement in the enclosing block become the `if`'s own body —
5503    /// now it prints an honest empty statement (`;`) that can't swallow
5504    /// anything after it.
5505    #[test]
5506    fn a_blank_if_body_prints_an_empty_statement_not_a_swallowing_newline() {
5507        // Nested inside a `Block`, not two top-level program statements —
5508        // `print`'s own blank-line-between-statements policy only applies
5509        // between top-level statements (see its own doc comment), so a
5510        // top-level pair would mask the swallowing bug this test exists to
5511        // catch: the real hazard is two statements sharing one physical
5512        // line with nothing separating them, exactly what `render_stmt`'s
5513        // `Block` arm produces with no blank-line insertion of its own.
5514        let mut program = TsProgram::new();
5515        program.push(TsStmt::block(
5516            vec![
5517                TsStmt::if_stmt(TsExpr::Ident("cond".to_string()), TsStmt::blank(None), None),
5518                TsStmt::expr_stmt(
5519                    TsExpr::Call {
5520                        callee: Box::new(TsExpr::Ident("nextStatement".to_string())),
5521                        args: vec![],
5522                    },
5523                    None,
5524                ),
5525            ],
5526            None,
5527        ));
5528        let printed = print(&program, "x.bynk", "", "x.ts");
5529        assert_eq!(printed.text, "{\n  if (cond) ;\n  nextStatement();\n}\n");
5530    }
5531
5532    /// A left-nested same-operator chain also keeps its parens for a
5533    /// non-associative operator (unlike the `||`/`&&` case above) —
5534    /// `(a === b) === c` reads the same as `a === b === c` would parse, so
5535    /// this is really about `render_binary_operand` not silently dropping
5536    /// parens it should keep, not about a left/right asymmetry.
5537    #[test]
5538    fn a_left_nested_strict_eq_chain_keeps_its_parens() {
5539        let mut program = TsProgram::new();
5540        program.push(TsStmt::expr_stmt(
5541            TsExpr::Binary {
5542                op: TsBinaryOp::StrictEq,
5543                left: Box::new(TsExpr::Binary {
5544                    op: TsBinaryOp::StrictEq,
5545                    left: Box::new(TsExpr::Ident("a".to_string())),
5546                    right: Box::new(TsExpr::Ident("b".to_string())),
5547                }),
5548                right: Box::new(TsExpr::Ident("c".to_string())),
5549            },
5550            None,
5551        ));
5552        let printed = print(&program, "x.bynk", "", "x.ts");
5553        assert_eq!(printed.text, "(a === b) === c;\n");
5554    }
5555
5556    // -- #1325: emit_test_main's own new shapes ------------------------------
5557
5558    #[test]
5559    fn prints_a_template_literal_with_substitutions() {
5560        let mut program = TsProgram::new();
5561        program.push(TsStmt::expr_stmt(
5562            TsExpr::template_lit(
5563                vec![
5564                    String::new(),
5565                    " passed, ".to_string(),
5566                    " failed.".to_string(),
5567                ],
5568                vec![
5569                    TsExpr::Ident("passed".to_string()),
5570                    TsExpr::Ident("failed".to_string()),
5571                ],
5572            ),
5573            None,
5574        ));
5575        let printed = print(&program, "x.bynk", "", "x.ts");
5576        assert_eq!(printed.text, "`${passed} passed, ${failed} failed.`;\n");
5577    }
5578
5579    /// A template literal's own static parts print with no escaping applied
5580    /// by the printer — see `TsExpr::TemplateLit`'s own doc for why: a
5581    /// generic escaper would double the literal backslash of an
5582    /// already-pre-formed JS unicode escape (`✓`), corrupting it. This
5583    /// pins the real, grounded shape directly: the six ASCII characters
5584    /// `✓` pass through unchanged, not doubled into `\\u2713`.
5585    #[test]
5586    fn a_template_literal_part_carrying_a_preformed_unicode_escape_is_not_reescaped() {
5587        let mut program = TsProgram::new();
5588        program.push(TsStmt::expr_stmt(
5589            TsExpr::template_lit(
5590                vec!["  \\u2713 ".to_string(), String::new()],
5591                vec![TsExpr::Ident("r".to_string())],
5592            ),
5593            None,
5594        ));
5595        let printed = print(&program, "x.bynk", "", "x.ts");
5596        assert_eq!(printed.text, "`  \\u2713 ${r}`;\n");
5597    }
5598
5599    #[test]
5600    fn a_raw_literal_prints_exactly_as_given_with_no_escaping() {
5601        let mut program = TsProgram::new();
5602        program.push(TsStmt::expr_stmt(
5603            TsExpr::Lit(TsLit::Raw("\"integration \\u00b7 \"".to_string())),
5604            None,
5605        ));
5606        let printed = print(&program, "x.bynk", "", "x.ts");
5607        assert_eq!(printed.text, "\"integration \\u00b7 \";\n");
5608    }
5609
5610    #[test]
5611    fn prints_a_multiline_array_literal() {
5612        let mut program = TsProgram::new();
5613        program.push(TsStmt::const_stmt(
5614            TsBindingName::Ident("modules".to_string()),
5615            None,
5616            TsExpr::multiline_array(vec![
5617                TsExpr::object(vec![(
5618                    "name".to_string(),
5619                    TsExpr::Lit(TsLit::Str("a".to_string())),
5620                )]),
5621                TsExpr::object(vec![(
5622                    "name".to_string(),
5623                    TsExpr::Lit(TsLit::Str("b".to_string())),
5624                )]),
5625            ]),
5626            None,
5627        ));
5628        let printed = print(&program, "x.bynk", "", "x.ts");
5629        assert_eq!(
5630            printed.text,
5631            "const modules = [\n  { name: \"a\" },\n  { name: \"b\" },\n];\n"
5632        );
5633    }
5634
5635    /// Same reachability boundary as `TsExpr::Object`'s own `multiline`
5636    /// field (see its doc) — a `multiline: true` array nested inside another
5637    /// expression falls back to single-line via the depth-unaware
5638    /// `render_expr` recursion.
5639    #[test]
5640    fn a_nested_multiline_array_falls_back_to_single_line() {
5641        let mut program = TsProgram::new();
5642        program.push(TsStmt::expr_stmt(
5643            TsExpr::array(vec![TsExpr::multiline_array(vec![TsExpr::Lit(
5644                TsLit::Num("1".to_string()),
5645            )])]),
5646            None,
5647        ));
5648        let printed = print(&program, "x.bynk", "", "x.ts");
5649        assert_eq!(printed.text, "[[1]];\n");
5650    }
5651
5652    #[test]
5653    fn prints_a_declare_const_ambient_binding() {
5654        let mut program = TsProgram::new();
5655        program.push(TsStmt::decl(
5656            TsDecl::DeclareConst {
5657                name: "process".to_string(),
5658                ty: TsType::Object(vec![TsTypeMember::prop("env", TsType::named("unknown"))]),
5659            },
5660            None,
5661        ));
5662        let printed = print(&program, "x.bynk", "", "x.ts");
5663        assert_eq!(printed.text, "declare const process: { env: unknown };\n");
5664    }
5665
5666    #[test]
5667    fn prints_an_async_top_level_function() {
5668        let mut program = TsProgram::new();
5669        program.push(TsStmt::decl(
5670            TsDecl::Function {
5671                name: "main".to_string(),
5672                generics: Vec::new(),
5673                params: vec![],
5674                return_type: None,
5675                body: vec![TsStmt::return_stmt(None, None)],
5676                is_async: true,
5677                inline: false,
5678            },
5679            None,
5680        ));
5681        let printed = print(&program, "x.bynk", "", "x.ts");
5682        assert_eq!(printed.text, "async function main() {\n  return;\n}\n");
5683    }
5684
5685    /// #1369 (Arc C, slice 20): `TsDecl::Function.inline` — the zero-factory
5686    /// shape (`function name(): Ret { return <expr>; }` all on one line)
5687    /// `emit_agent` needs, mirroring `TsObjectEntry::Method.inline`'s own
5688    /// single-line-vs-multi-line precedent (#1337) at a different node kind.
5689    #[test]
5690    fn prints_an_inline_top_level_function() {
5691        let mut program = TsProgram::new();
5692        program.push(TsStmt::decl(
5693            TsDecl::Function {
5694                name: "zero".to_string(),
5695                generics: Vec::new(),
5696                params: vec![],
5697                return_type: Some(TsType::named("Foo")),
5698                body: vec![TsStmt::return_stmt(
5699                    Some(TsExpr::object(vec![(
5700                        "n".to_string(),
5701                        TsExpr::Lit(TsLit::Num("0".to_string())),
5702                    )])),
5703                    None,
5704                )],
5705                is_async: false,
5706                inline: true,
5707            },
5708            None,
5709        ));
5710        let printed = print(&program, "x.bynk", "", "x.ts");
5711        assert_eq!(printed.text, "function zero(): Foo { return { n: 0 }; }\n");
5712    }
5713
5714    #[test]
5715    fn prints_a_postfix_increment_statement() {
5716        let mut program = TsProgram::new();
5717        program.push(TsStmt::increment(TsExpr::Ident("passed".to_string()), None));
5718        let printed = print(&program, "x.bynk", "", "x.ts");
5719        assert_eq!(printed.text, "passed++;\n");
5720    }
5721
5722    /// An `Increment` inside an `InlineBlock` renders correctly through
5723    /// `render_inline_stmt`'s fallback — `emit_test_main`'s own real
5724    /// `{ passed++; console.log(...); }` shape.
5725    #[test]
5726    fn a_postfix_increment_renders_correctly_inside_an_inline_block() {
5727        let mut program = TsProgram::new();
5728        program.push(TsStmt::if_stmt(
5729            TsExpr::Ident("cond".to_string()),
5730            TsStmt::inline_block(
5731                vec![
5732                    TsStmt::increment(TsExpr::Ident("passed".to_string()), None),
5733                    TsStmt::expr_stmt(TsExpr::Ident("next".to_string()), None),
5734                ],
5735                None,
5736            ),
5737            None,
5738        ));
5739        let printed = print(&program, "x.bynk", "", "x.ts");
5740        assert_eq!(printed.text, "if (cond) { passed++; next; }\n");
5741    }
5742
5743    /// `same_line_else` with a `Block` then-branch — `} else {` on one
5744    /// physical line, distinct from the fresh-line default pinned elsewhere
5745    /// in this module.
5746    #[test]
5747    fn same_line_else_puts_the_else_keyword_after_the_closing_brace() {
5748        let mut program = TsProgram::new();
5749        program.push(TsStmt::if_else_same_line_stmt(
5750            TsExpr::Ident("cond".to_string()),
5751            TsStmt::block(
5752                vec![TsStmt::expr_stmt(TsExpr::Ident("a".to_string()), None)],
5753                None,
5754            ),
5755            TsStmt::block(
5756                vec![TsStmt::expr_stmt(TsExpr::Ident("b".to_string()), None)],
5757                None,
5758            ),
5759            None,
5760        ));
5761        let printed = print(&program, "x.bynk", "", "x.ts");
5762        assert_eq!(printed.text, "if (cond) {\n  a;\n} else {\n  b;\n}\n");
5763    }
5764
5765    /// `same_line_else` with `InlineBlock` branches on both sides —
5766    /// `emit_test_main`'s own real `if (r.pass) { ... } else { ... }` shape,
5767    /// entirely on one generated line.
5768    #[test]
5769    fn same_line_else_with_inline_block_branches_stays_on_one_line() {
5770        let mut program = TsProgram::new();
5771        program.push(TsStmt::if_else_same_line_stmt(
5772            TsExpr::Ident("cond".to_string()),
5773            TsStmt::inline_block(
5774                vec![TsStmt::expr_stmt(TsExpr::Ident("a".to_string()), None)],
5775                None,
5776            ),
5777            TsStmt::inline_block(
5778                vec![TsStmt::expr_stmt(TsExpr::Ident("b".to_string()), None)],
5779                None,
5780            ),
5781            None,
5782        ));
5783        let printed = print(&program, "x.bynk", "", "x.ts");
5784        assert_eq!(printed.text, "if (cond) { a; } else { b; }\n");
5785    }
5786
5787    /// `same_line_else` with a brace-free `then_branch` falls back to the
5788    /// ordinary fresh-line rendering — nothing real needs `<inline-stmt>
5789    /// else {`, and the printer doesn't claim to support it (see
5790    /// `TsStmtKind::If`'s own doc).
5791    #[test]
5792    fn same_line_else_with_a_brace_free_then_branch_falls_back_to_fresh_line() {
5793        let mut program = TsProgram::new();
5794        program.push(TsStmt::if_else_same_line_stmt(
5795            TsExpr::Ident("cond".to_string()),
5796            TsStmt::continue_stmt(None),
5797            TsStmt::block(
5798                vec![TsStmt::expr_stmt(TsExpr::Ident("b".to_string()), None)],
5799                None,
5800            ),
5801            None,
5802        ));
5803        let printed = print(&program, "x.bynk", "", "x.ts");
5804        assert_eq!(printed.text, "if (cond) continue;\nelse {\n  b;\n}\n");
5805    }
5806
5807    /// `TsDecl::ReExportAll` prints `export * from "spec";` — no braces, no
5808    /// name list, matching `emit_commons_barrel`'s own real per-file line.
5809    #[test]
5810    fn re_export_all_prints_a_wildcard_re_export() {
5811        let mut program = TsProgram::new();
5812        program.push(TsStmt::decl(
5813            TsDecl::ReExportAll {
5814                from: "./thing/make.js".to_string(),
5815            },
5816            None,
5817        ));
5818        let printed = print(&program, "x.bynk", "", "x.ts");
5819        assert_eq!(printed.text, "export * from \"./thing/make.js\";\n");
5820    }
5821
5822    /// Review of #1329's own grounding: `emit_commons_barrel`'s real barrel
5823    /// module is one header `Comment` immediately followed by one `export
5824    /// *` line per constituent source file, every one of those lines
5825    /// adjacent with no blank line anywhere — the exact byte shape pinned
5826    /// here, matching `251_multi_file_commons_test`'s own real
5827    /// `expected/thing.ts`.
5828    #[test]
5829    fn a_header_comment_and_consecutive_re_export_alls_have_no_blank_lines() {
5830        let mut program = TsProgram::new();
5831        program.push(TsStmt::comment(
5832            "Generated by bynkc — do not edit by hand.",
5833            None,
5834        ));
5835        program.push(TsStmt::decl(
5836            TsDecl::ReExportAll {
5837                from: "./thing/make.js".to_string(),
5838            },
5839            None,
5840        ));
5841        program.push(TsStmt::decl(
5842            TsDecl::ReExportAll {
5843                from: "./thing/widget.js".to_string(),
5844            },
5845            None,
5846        ));
5847        let printed = print(&program, "x.bynk", "", "x.ts");
5848        assert_eq!(
5849            printed.text,
5850            "// Generated by bynkc — do not edit by hand.\n\
5851             export * from \"./thing/make.js\";\n\
5852             export * from \"./thing/widget.js\";\n"
5853        );
5854    }
5855
5856    /// Review of #1330: the grouping rule added for #1329 is scoped to
5857    /// `ReExportAll`-adjacent-to-`ReExportAll` (and a `Comment` immediately
5858    /// before one) — a `ReExportAll` next to anything else still gets the
5859    /// ordinary blank line. `emit_commons_barrel` is `ReExportAll`'s only
5860    /// producer today, so this is the invariant keeping the rule scoped if
5861    /// a future slice ever gives it a second one.
5862    #[test]
5863    fn a_re_export_all_next_to_a_non_re_export_all_gets_the_ordinary_blank_line() {
5864        let mut program = TsProgram::new();
5865        program.push(TsStmt::decl(
5866            TsDecl::ImportNamespace {
5867                type_only: false,
5868                alias: "handlers".to_string(),
5869                from: "./handlers.js".to_string(),
5870            },
5871            None,
5872        ));
5873        program.push(TsStmt::decl(
5874            TsDecl::ReExportAll {
5875                from: "./thing/make.js".to_string(),
5876            },
5877            None,
5878        ));
5879        program.push(TsStmt::comment("trailing note", None));
5880        let printed = print(&program, "x.bynk", "", "x.ts");
5881        assert_eq!(
5882            printed.text,
5883            "import * as handlers from \"./handlers.js\";\n\n\
5884             export * from \"./thing/make.js\";\n\n\
5885             // trailing note\n"
5886        );
5887    }
5888
5889    /// #1333: `print_stmt` prints a `TsStmtKind::DocComment` as a real JSDoc
5890    /// block, matching `emit_doc_block`'s own real multi-line shape
5891    /// (`137_agent_instantiation_workers/expected/workers/demo-counter/
5892    /// handlers.ts`'s own real header comment).
5893    #[test]
5894    fn print_stmt_renders_a_multi_line_doc_comment() {
5895        let stmt = TsStmt::doc_comment(
5896            "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.",
5897            None,
5898        );
5899        assert_eq!(
5900            print_stmt(&stmt, 0),
5901            "/**\n \
5902             * A minimal stateful agent in the bundle target: instantiation lowers through the\n \
5903             * generated factory, the method call is a direct call, and state persists per key\n \
5904             * across calls within a session.\n \
5905             */\n"
5906        );
5907    }
5908
5909    /// #1333: a literal `*/` inside doc text escapes to `*\/`, matching
5910    /// `emit_doc_block`'s own pre-conversion behaviour exactly (issue
5911    /// #720 — an unescaped `*/` would otherwise close the comment early
5912    /// and let trailing text land as executable top-level TypeScript).
5913    #[test]
5914    fn print_stmt_escapes_a_literal_comment_terminator_inside_doc_text() {
5915        let stmt = TsStmt::doc_comment("docs */ ; (globalThis as any).PWNED = true; /*", None);
5916        let printed = print_stmt(&stmt, 0);
5917        assert!(!printed.contains("*/ ;"), "unescaped terminator: {printed}");
5918        assert_eq!(
5919            printed,
5920            "/**\n * docs *\\/ ; (globalThis as any).PWNED = true; /*\n */\n"
5921        );
5922    }
5923
5924    /// #1333: a blank line inside doc text prints as a bare ` *`, no
5925    /// trailing space — distinct from a non-blank line's own ` * <line>`.
5926    #[test]
5927    fn print_stmt_renders_a_blank_doc_line_as_a_bare_star() {
5928        let stmt = TsStmt::doc_comment("first paragraph\n\nsecond paragraph", None);
5929        assert_eq!(
5930            print_stmt(&stmt, 0),
5931            "/**\n * first paragraph\n *\n * second paragraph\n */\n"
5932        );
5933    }
5934
5935    /// #1333: `print_stmt`'s own `depth` parameter indents every line of
5936    /// the JSDoc block, matching `render_stmt`'s own 2-space-per-level
5937    /// convention — `emit_doc_block`'s real callers pass `INDENT_STEP`
5938    /// (2 raw spaces = depth 1) for a nested doc comment.
5939    #[test]
5940    fn print_stmt_indents_a_doc_comment_at_depth() {
5941        let stmt = TsStmt::doc_comment("a method", None);
5942        assert_eq!(print_stmt(&stmt, 1), "  /**\n   * a method\n   */\n");
5943    }
5944
5945    /// #1337: `TsStmtKind::Raw` prints its own text exactly as given — no
5946    /// leading indent (unlike every other statement kind, whose own
5947    /// `render_stmt` arm prefixes `indent(depth)`), no added semicolon or
5948    /// braces. `emit_method`'s own opaque `lower.rs`-sourced body is
5949    /// already fully, absolutely indented by the time it's captured into
5950    /// one `Raw` node, so the printer must contribute nothing further —
5951    /// the same reasoning `Verbatim` already established for pre-rendered
5952    /// content, `Raw`'s own doc explains why it's a distinct kind.
5953    #[test]
5954    fn print_stmt_renders_raw_text_verbatim_with_no_added_indent_or_punctuation() {
5955        let stmt = TsStmt::raw("    return x + 1;\n", None);
5956        // depth is passed but deliberately has no effect on Raw's own output.
5957        assert_eq!(print_stmt(&stmt, 3), "    return x + 1;\n");
5958    }
5959
5960    /// #1337: multi-line `Raw` text (the real shape — a whole function
5961    /// body, not one line) passes through with every embedded line intact,
5962    /// confirming the printer doesn't split, re-indent, or otherwise
5963    /// interpret it.
5964    #[test]
5965    fn print_stmt_renders_multi_line_raw_text_unchanged() {
5966        let stmt = TsStmt::raw(
5967            "    const r = Uuid.of(crypto.randomUUID());\n    return r.value;\n",
5968            None,
5969        );
5970        assert_eq!(
5971            print_stmt(&stmt, 0),
5972            "    const r = Uuid.of(crypto.randomUUID());\n    return r.value;\n"
5973        );
5974    }
5975
5976    /// #1337: `TsObjectEntry::Method` with a `Raw` body — `emit_method`'s
5977    /// own real shape (`{method}{generics}({params}): {ret} { <raw body>
5978    /// },`), pinned at the actual depth its own object literal renders at
5979    /// (a top-level `export const {...}`, depth 0 → entries at depth 1,
5980    /// two-space indent, matching `emit_method`'s own pre-conversion
5981    /// hand-written `"  {method}..."` line).
5982    #[test]
5983    fn multiline_object_renders_a_method_entry_with_a_raw_body() {
5984        let mut out = String::new();
5985        render_multiline_object(
5986            &mut out,
5987            &[TsObjectEntry::Method {
5988                name: "of".to_string(),
5989                is_async: false,
5990                generics: Vec::new(),
5991                params: vec![TsParam {
5992                    name: "value".to_string(),
5993                    ty: Some(TsType::named("string")),
5994                    optional: false,
5995                }],
5996                return_type: Some(TsType::named("Uuid")),
5997                doc: None,
5998                inline: false,
5999                body: vec![TsStmt::raw("    return value as Uuid;\n", None)],
6000            }],
6001            0,
6002        );
6003        assert_eq!(
6004            out,
6005            "{\n  of(value: string): Uuid {\n    return value as Uuid;\n  },\n}"
6006        );
6007    }
6008
6009    /// #1337: `TsObjectEntry::Method`'s own `generics` field prints
6010    /// `<A, U>` between the method name and its parameter list —
6011    /// `Box.map`'s own real shape (`402_generic_instance_method`, a
6012    /// single-file-form fixture the accepted proposal's own project-form
6013    /// search missed).
6014    #[test]
6015    fn multiline_object_renders_a_generic_method_entry() {
6016        let mut out = String::new();
6017        render_multiline_object(
6018            &mut out,
6019            &[TsObjectEntry::Method {
6020                name: "map".to_string(),
6021                is_async: false,
6022                generics: vec!["A".to_string(), "U".to_string()],
6023                params: vec![
6024                    TsParam {
6025                        name: "self".to_string(),
6026                        ty: Some(TsType::named("Box<A>")),
6027                        optional: false,
6028                    },
6029                    TsParam {
6030                        name: "f".to_string(),
6031                        ty: Some(TsType::named("(a0: A) => U")),
6032                        optional: false,
6033                    },
6034                ],
6035                return_type: Some(TsType::named("Box<U>")),
6036                doc: None,
6037                inline: false,
6038                body: vec![TsStmt::return_stmt(
6039                    Some(TsExpr::Ident("{ value: f(self.value) }".to_string())),
6040                    None,
6041                )],
6042            }],
6043            0,
6044        );
6045        assert_eq!(
6046            out,
6047            "{\n  map<A, U>(self: Box<A>, f: (a0: A) => U): Box<U> {\n    return { value: f(self.value) };\n  },\n}"
6048        );
6049    }
6050
6051    /// #1337: a method with no generics prints no `<>` at all — the
6052    /// ordinary, dominant case (every prior slice's own real method
6053    /// entries), confirming `generics: Vec::new()` is a true no-op, not
6054    /// an empty `<>`.
6055    #[test]
6056    fn multiline_object_renders_a_non_generic_method_entry_with_no_angle_brackets() {
6057        let mut out = String::new();
6058        render_multiline_object(
6059            &mut out,
6060            &[TsObjectEntry::Method {
6061                name: "get".to_string(),
6062                is_async: false,
6063                generics: Vec::new(),
6064                params: vec![],
6065                return_type: Some(TsType::named("void")),
6066                doc: None,
6067                inline: false,
6068                body: vec![],
6069            }],
6070            0,
6071        );
6072        assert_eq!(out, "{\n  get(): void {\n  },\n}");
6073    }
6074
6075    /// #1337: `TsObjectEntry::Method`'s own `doc` field prints a JSDoc
6076    /// block immediately before the method entry, same indent, no blank
6077    /// line between — `Timestamp.diff`'s own real shape
6078    /// (`65_money_uses_time`), reusing `render_doc_comment` (the same
6079    /// renderer `TsStmtKind::DocComment` already uses) rather than a
6080    /// second copy.
6081    #[test]
6082    fn multiline_object_renders_a_method_entry_with_a_preceding_doc_comment() {
6083        let mut out = String::new();
6084        render_multiline_object(
6085            &mut out,
6086            &[TsObjectEntry::Method {
6087                name: "diff".to_string(),
6088                is_async: false,
6089                generics: Vec::new(),
6090                params: vec![TsParam {
6091                    name: "self".to_string(),
6092                    ty: Some(TsType::named("Timestamp")),
6093                    optional: false,
6094                }],
6095                return_type: Some(TsType::named("Span")),
6096                doc: Some("Compute the duration between two timestamps.".to_string()),
6097                inline: false,
6098                body: vec![TsStmt::raw("    return 0;\n", None)],
6099            }],
6100            0,
6101        );
6102        assert_eq!(
6103            out,
6104            "{\n  /**\n   * Compute the duration between two timestamps.\n   */\n  diff(self: Timestamp): Span {\n    return 0;\n  },\n}"
6105        );
6106    }
6107
6108    /// #1337: `TsObjectEntry::Method`'s own `inline: true` renders the
6109    /// whole entry — signature and one-statement body alike — on ONE
6110    /// physical line, `emit_forwarded_methods`'s own real shape
6111    /// (`255_context_uses_commons_static_method`'s own real `equals`
6112    /// entry: `equals(self: Cents, other: Cents): boolean { return
6113    /// __CommonsCents.equals(self, other) as unknown as boolean; },`),
6114    /// distinct from every other real `Method` entry in this tree
6115    /// (always multi-line, `inline: false`).
6116    #[test]
6117    fn multiline_object_renders_an_inline_method_entry_on_one_line() {
6118        let mut out = String::new();
6119        render_multiline_object(
6120            &mut out,
6121            &[TsObjectEntry::Method {
6122                name: "equals".to_string(),
6123                is_async: false,
6124                generics: Vec::new(),
6125                params: vec![
6126                    TsParam {
6127                        name: "self".to_string(),
6128                        ty: Some(TsType::named("Cents")),
6129                        optional: false,
6130                    },
6131                    TsParam {
6132                        name: "other".to_string(),
6133                        ty: Some(TsType::named("Cents")),
6134                        optional: false,
6135                    },
6136                ],
6137                return_type: Some(TsType::named("boolean")),
6138                doc: None,
6139                inline: true,
6140                body: vec![TsStmt::return_stmt(
6141                    Some(TsExpr::As {
6142                        expr: Box::new(TsExpr::As {
6143                            expr: Box::new(TsExpr::Call {
6144                                callee: Box::new(TsExpr::Member {
6145                                    object: Box::new(TsExpr::Ident("__CommonsCents".to_string())),
6146                                    property: "equals".to_string(),
6147                                }),
6148                                args: vec![
6149                                    TsExpr::Ident("self".to_string()),
6150                                    TsExpr::Ident("other".to_string()),
6151                                ],
6152                            }),
6153                            ty: TsType::named("unknown"),
6154                        }),
6155                        ty: TsType::named("boolean"),
6156                    }),
6157                    None,
6158                )],
6159            }],
6160            0,
6161        );
6162        assert_eq!(
6163            out,
6164            "{\n  equals(self: Cents, other: Cents): boolean { return __CommonsCents.equals(self, other) as unknown as boolean; },\n}"
6165        );
6166    }
6167
6168    /// Review of #1338, finding 4: `print_object_entry` — the exact public
6169    /// API `emit_refined_type`/`emit_record_type`/`emit_sum_type`'s own
6170    /// real call sites depend on — had no direct test; every #1337 test
6171    /// above drives `render_multiline_object` instead. Pins the contract
6172    /// those call sites rely on: `print_object_entry(&entry, 0)` produces
6173    /// the same text as the entry-slice portion of what
6174    /// `render_multiline_object`'s own depth-0 output produces for the
6175    /// same entry (its own depth convention — the entry lands one level
6176    /// deeper than the object, i.e. `depth + 1` — matches exactly, not
6177    /// coincidentally, since both paths route through the same
6178    /// `render_multiline_object_entry`).
6179    #[test]
6180    fn print_object_entry_matches_the_multiline_objects_own_entry_slice() {
6181        // A real `TsStmt::return_stmt` body, not `Raw` — `Raw`'s own baked-in
6182        // indent only matches depth 0 (finding 3's own new guard), so an
6183        // ordinary real-node body is what this depth-convention contract
6184        // needs to prove at a non-zero depth.
6185        let entry = TsObjectEntry::Method {
6186            name: "of".to_string(),
6187            is_async: false,
6188            generics: Vec::new(),
6189            params: vec![TsParam {
6190                name: "value".to_string(),
6191                ty: Some(TsType::named("string")),
6192                optional: false,
6193            }],
6194            return_type: Some(TsType::named("Uuid")),
6195            doc: None,
6196            inline: false,
6197            body: vec![TsStmt::return_stmt(
6198                Some(TsExpr::As {
6199                    expr: Box::new(TsExpr::Ident("value".to_string())),
6200                    ty: TsType::named("Uuid"),
6201                }),
6202                None,
6203            )],
6204        };
6205
6206        let mut whole_object = String::new();
6207        render_multiline_object(&mut whole_object, std::slice::from_ref(&entry), 0);
6208        // `render_multiline_object`'s own entry line is followed by `\n`
6209        // then the closing `}` (no blank line between them for a single
6210        // entry) — `print_object_entry`'s own output keeps that same
6211        // trailing `\n` (it has no closing brace of its own to attach to),
6212        // so the real equivalence is entry-slice-plus-newline, not a bare
6213        // slice.
6214        let entry_slice_with_trailing_newline = whole_object
6215            .strip_prefix("{\n")
6216            .and_then(|s| s.strip_suffix('}'))
6217            .expect("render_multiline_object's own single-entry output has a { }-wrapper");
6218
6219        assert_eq!(
6220            print_object_entry(&entry, 0),
6221            entry_slice_with_trailing_newline
6222        );
6223    }
6224
6225    /// #1339's own real gap: `emit_refined_type`'s own branded-type alias,
6226    /// `{base} & { readonly __brand: "..." }`, has no representation among
6227    /// `Named`/`Array`/`Object`/`Fn`/`Union` — mirrors `Union`'s own single-
6228    /// line, ` & `-joined shape exactly.
6229    #[test]
6230    fn print_type_renders_an_intersection() {
6231        let ty = TsType::intersection(vec![
6232            TsType::named("string"),
6233            TsType::Object(vec![TsTypeMember::readonly_prop(
6234                "__brand",
6235                TsType::named("\"Order\""),
6236            )]),
6237        ]);
6238        assert_eq!(print_type(&ty), "string & { readonly __brand: \"Order\" }");
6239    }
6240
6241    /// #1339's own real gap: `emit_sum_type`'s own multi-line discriminated
6242    /// union — a leading `|` on every line except the first (which gets
6243    /// equivalent spacing instead, matching the pre-conversion `writeln!`
6244    /// code's own `let pipe = if i == 0 { " " } else { "|" };` exactly), no
6245    /// trailing newline or `;` of its own (the caller, `TsDecl::TypeAlias`'s
6246    /// own render arm, owns both).
6247    #[test]
6248    fn print_type_renders_a_multiline_union() {
6249        let ty = TsType::multiline_union(vec![
6250            TsType::Object(vec![TsTypeMember::readonly_prop(
6251                "tag",
6252                TsType::named("\"a\""),
6253            )]),
6254            TsType::Object(vec![
6255                TsTypeMember::readonly_prop("tag", TsType::named("\"b\"")),
6256                TsTypeMember::readonly_prop("value", TsType::named("number")),
6257            ]),
6258        ]);
6259        assert_eq!(
6260            print_type(&ty),
6261            "    { readonly tag: \"a\" }\n  | { readonly tag: \"b\"; readonly value: number }"
6262        );
6263    }
6264
6265    /// #1339: `TsDecl::TypeAlias`'s own multiline-union special case — the
6266    /// `=` is followed directly by `\n` (no trailing space, matching the
6267    /// pre-conversion `writeln!(out, "export type {name}{params} =")`
6268    /// line's own exact bytes), each variant on its own line, the closing
6269    /// `;` appended directly to the last variant's own line. Also pins
6270    /// `type_params`' own bare-generics rendering on the alias header.
6271    #[test]
6272    fn prints_a_generic_sum_types_own_multiline_type_alias() {
6273        let mut program = TsProgram::new();
6274        program.push(TsStmt::decl(
6275            TsDecl::Export(Box::new(TsDecl::TypeAlias {
6276                name: "Opt".to_string(),
6277                type_params: vec!["T".to_string()],
6278                ty: TsType::multiline_union(vec![
6279                    TsType::Object(vec![TsTypeMember::readonly_prop(
6280                        "tag",
6281                        TsType::named("\"none\""),
6282                    )]),
6283                    TsType::Object(vec![
6284                        TsTypeMember::readonly_prop("tag", TsType::named("\"some\"")),
6285                        TsTypeMember::readonly_prop("value", TsType::named("T")),
6286                    ]),
6287                ]),
6288            })),
6289            None,
6290        ));
6291        let printed = print(&program, "x.bynk", "", "x.ts");
6292        assert_eq!(
6293            printed.text,
6294            "export type Opt<T> =\n    \
6295             { readonly tag: \"none\" }\n  \
6296             | { readonly tag: \"some\"; readonly value: T };\n"
6297        );
6298    }
6299
6300    /// #1339's own real gap: `emit_record_type`'s own `export interface
6301    /// {name}{params} { readonly {field}: {ty}; ... }` — bare generic names
6302    /// on the interface header, `readonly` on every real member here.
6303    #[test]
6304    fn prints_a_generic_interface_with_readonly_members() {
6305        let mut program = TsProgram::new();
6306        program.push(TsStmt::decl(
6307            TsDecl::Export(Box::new(TsDecl::Interface {
6308                name: "Box".to_string(),
6309                type_params: vec!["T".to_string()],
6310                members: vec![TsTypeMember::readonly_prop("value", TsType::named("T"))],
6311            })),
6312            None,
6313        ));
6314        let printed = print(&program, "x.bynk", "", "x.ts");
6315        assert_eq!(
6316            printed.text,
6317            "export interface Box<T> {\n  readonly value: T;\n}\n"
6318        );
6319    }
6320
6321    /// #1357's own real gap: `TsTypeMember::Method` had no `generics`/`doc`
6322    /// fields — `emit_capability`'s own interface methods are genuinely
6323    /// generic (no monomorphisation) and doc-commented per op. `doc` renders
6324    /// at `TsDecl::Interface`'s own render arm (not `render_type_member`
6325    /// itself, which has no `depth` to give `render_doc_comment`), mirroring
6326    /// `TsObjectEntry::Method.doc`'s own identical split (#1337).
6327    #[test]
6328    fn prints_a_generic_documented_interface_method() {
6329        let mut program = TsProgram::new();
6330        program.push(TsStmt::decl(
6331            TsDecl::Export(Box::new(TsDecl::Interface {
6332                name: "Clock".to_string(),
6333                type_params: Vec::new(),
6334                members: vec![TsTypeMember::Method {
6335                    name: "now".to_string(),
6336                    generics: vec!["T".to_string()],
6337                    params: vec![TsParam {
6338                        name: "unit".to_string(),
6339                        ty: Some(TsType::named("T")),
6340                        optional: false,
6341                    }],
6342                    ret: TsType::named("number"),
6343                    doc: Some("Returns the current time.".to_string()),
6344                }],
6345            })),
6346            None,
6347        ));
6348        let printed = print(&program, "x.bynk", "", "x.ts");
6349        assert_eq!(
6350            printed.text,
6351            "export interface Clock {\n  /**\n   * Returns the current time.\n   */\n  now<T>(unit: T): number;\n}\n"
6352        );
6353    }
6354
6355    /// #1359's own real need: `print_class_method` — a class-method sibling
6356    /// of `print_object_entry`'s own fragment-printing entry point,
6357    /// `emit_provider`'s own hand-written class wrapper prints each real
6358    /// method through this directly. `depth` means the *class's* own
6359    /// depth, so at `depth == 0` the method itself lands at `indent(1)`,
6360    /// its body at `indent(2)`, matching `TsDecl::Class`'s own equivalent
6361    /// per-method indent exactly — but with no automatic blank-line
6362    /// insertion of its own (unlike `TsDecl::Class`'s own render arm),
6363    /// since `emit_provider`'s own real spacing has none between methods.
6364    #[test]
6365    fn prints_a_single_class_method_fragment() {
6366        let method = TsClassMethod {
6367            name: "double".to_string(),
6368            private: false,
6369            is_async: true,
6370            params: vec![TsParam {
6371                name: "n".to_string(),
6372                ty: Some(TsType::named("number")),
6373                optional: false,
6374            }],
6375            return_type: Some(TsType::named("number")),
6376            doc: None,
6377            body: vec![TsStmt::return_stmt(
6378                Some(TsExpr::Binary {
6379                    op: TsBinaryOp::NullishCoalescing,
6380                    left: Box::new(TsExpr::Ident("n".to_string())),
6381                    right: Box::new(TsExpr::Lit(TsLit::Num("0".to_string()))),
6382                }),
6383                None,
6384            )],
6385        };
6386        assert_eq!(
6387            print_class_method(&method, 0),
6388            "  async double(n: number): number {\n    return n ?? 0;\n  }\n"
6389        );
6390    }
6391
6392    /// Arc C, slice 21 (#1371): `TsClassMethod.private` — pins both the
6393    /// keyword itself and its render-*before*-`async` ordering directly,
6394    /// the same precedent `TsClassField.private` already has (its own
6395    /// direct test, not left to transitive `bynkc` fixture coverage alone)
6396    /// — review of #1372 caught this one still missing for the sibling
6397    /// field.
6398    #[test]
6399    fn prints_a_private_class_method_fragment() {
6400        let method = TsClassMethod {
6401            name: "loadState".to_string(),
6402            private: true,
6403            is_async: true,
6404            params: Vec::new(),
6405            return_type: Some(TsType::named_with_args(
6406                "Promise",
6407                vec![TsType::named("OrderState")],
6408            )),
6409            doc: None,
6410            body: vec![TsStmt::return_stmt(
6411                Some(TsExpr::Ident("state".to_string())),
6412                None,
6413            )],
6414        };
6415        assert_eq!(
6416            print_class_method(&method, 0),
6417            "  private async loadState(): Promise<OrderState> {\n    return state;\n  }\n"
6418        );
6419    }
6420
6421    /// Arc C, slice 23 (#1375): `TsClassMethod.doc` — the grounding pass's
6422    /// own second predicted gap (#1366), the same need
6423    /// `TsObjectEntry::Method.doc` (#1337) and `TsTypeMember::Method.doc`
6424    /// (#1357) already solved for their own node kinds, closed here for the
6425    /// third and last real method-shaped node in this crate.
6426    #[test]
6427    fn prints_a_documented_class_method_fragment() {
6428        let method = TsClassMethod {
6429            name: "spend".to_string(),
6430            private: false,
6431            is_async: false,
6432            params: Vec::new(),
6433            return_type: Some(TsType::named("void")),
6434            doc: Some("Debits the account.".to_string()),
6435            body: vec![],
6436        };
6437        assert_eq!(
6438            print_class_method(&method, 0),
6439            "  /**\n   * Debits the account.\n   */\n  spend(): void {\n  }\n"
6440        );
6441    }
6442
6443    /// #1339's own real gap: `TsExpr::Arrow` had no `generics`/`return_type`
6444    /// field — `emit_sum_type`'s own generic payload-constructor arrows
6445    /// (`<T>(name: T): Sum<T> => (...)`) need both. The object-literal body
6446    /// is wrapped in an explicit `Paren` — `Arrow`'s own renderer does not
6447    /// auto-parenthesise an object body the way real JS/TS syntax requires
6448    /// to disambiguate it from a block.
6449    #[test]
6450    fn prints_a_generic_arrow_with_a_parenthesised_object_body() {
6451        let mut program = TsProgram::new();
6452        program.push(TsStmt::expr_stmt(
6453            TsExpr::Arrow {
6454                params: vec![TsParam {
6455                    name: "value".to_string(),
6456                    ty: Some(TsType::named("T")),
6457                    optional: false,
6458                }],
6459                is_async: false,
6460                generics: vec!["T".to_string()],
6461                return_type: Some(TsType::named_with_args("Sum", vec![TsType::named("T")])),
6462                body: Box::new(TsArrowBody::Expr(Box::new(TsExpr::Paren(Box::new(
6463                    TsExpr::object_entries(vec![
6464                        TsObjectEntry::Prop(
6465                            "tag".to_string(),
6466                            TsExpr::Lit(TsLit::Str("some".to_string())),
6467                        ),
6468                        TsObjectEntry::Shorthand("value".to_string()),
6469                    ]),
6470                ))))),
6471            },
6472            None,
6473        ));
6474        let printed = print(&program, "x.bynk", "", "x.ts");
6475        assert_eq!(
6476            printed.text,
6477            "<T>(value: T): Sum<T> => ({ tag: \"some\", value });\n"
6478        );
6479    }
6480
6481    /// Arc C, slice 33 (#1401): `TsBinaryOp::LessThan` — pins the operator
6482    /// text itself.
6483    #[test]
6484    fn prints_a_less_than_comparison() {
6485        let mut program = TsProgram::new();
6486        program.push(TsStmt::expr_stmt(
6487            TsExpr::Binary {
6488                op: TsBinaryOp::LessThan,
6489                left: Box::new(TsExpr::Ident("a".to_string())),
6490                right: Box::new(TsExpr::Ident("b".to_string())),
6491            },
6492            None,
6493        ));
6494        let printed = print(&program, "x.bynk", "", "x.ts");
6495        assert_eq!(printed.text, "a < b;\n");
6496    }
6497
6498    /// `LessThan` shares `GreaterThan`'s own precedence tier — a nested
6499    /// `Add` under it needs no parens (`+` binds tighter), mirroring
6500    /// `add_binds_tighter_than_greater_than_on_both_sides` for the other
6501    /// relational operator.
6502    #[test]
6503    fn add_binds_tighter_than_less_than() {
6504        let mut program = TsProgram::new();
6505        program.push(TsStmt::expr_stmt(
6506            TsExpr::Binary {
6507                op: TsBinaryOp::LessThan,
6508                left: Box::new(TsExpr::Binary {
6509                    op: TsBinaryOp::Add,
6510                    left: Box::new(TsExpr::Ident("a".to_string())),
6511                    right: Box::new(TsExpr::Ident("b".to_string())),
6512                }),
6513                right: Box::new(TsExpr::Ident("c".to_string())),
6514            },
6515            None,
6516        ));
6517        let printed = print(&program, "x.bynk", "", "x.ts");
6518        assert_eq!(printed.text, "a + b < c;\n");
6519    }
6520
6521    /// Arc C, slice 34 (`tests_emit.rs` slice D, #1403): `TsBinaryOp::
6522    /// InstanceOf` — pins the keyword operator text itself, sharing
6523    /// `LessThan`/`GreaterThan`'s own precedence tier.
6524    #[test]
6525    fn prints_an_instanceof_check() {
6526        let mut program = TsProgram::new();
6527        program.push(TsStmt::expr_stmt(
6528            TsExpr::Binary {
6529                op: TsBinaryOp::InstanceOf,
6530                left: Box::new(TsExpr::Ident("e".to_string())),
6531                right: Box::new(TsExpr::Ident("ExpectationError".to_string())),
6532            },
6533            None,
6534        ));
6535        let printed = print(&program, "x.bynk", "", "x.ts");
6536        assert_eq!(printed.text, "e instanceof ExpectationError;\n");
6537    }
6538
6539    /// Review of #1404, finding 2: `prints_an_instanceof_check` alone pins
6540    /// the operator text but not the precedence tier the same diff added to
6541    /// `binary_precedence` — it passes identically whether `InstanceOf` sits
6542    /// at tier 5, tier 1, or is missing from that arm entirely. `Add` (tier
6543    /// 6) binds tighter, mirroring `add_binds_tighter_than_less_than`
6544    /// for the other relational operator sharing this tier.
6545    #[test]
6546    fn add_binds_tighter_than_instanceof() {
6547        let mut program = TsProgram::new();
6548        program.push(TsStmt::expr_stmt(
6549            TsExpr::Binary {
6550                op: TsBinaryOp::InstanceOf,
6551                left: Box::new(TsExpr::Binary {
6552                    op: TsBinaryOp::Add,
6553                    left: Box::new(TsExpr::Ident("a".to_string())),
6554                    right: Box::new(TsExpr::Ident("b".to_string())),
6555                }),
6556                right: Box::new(TsExpr::Ident("C".to_string())),
6557            },
6558            None,
6559        ));
6560        let printed = print(&program, "x.bynk", "", "x.ts");
6561        assert_eq!(printed.text, "a + b instanceof C;\n");
6562    }
6563
6564    /// Review of #1404, finding 2 (the more valuable half): a catch clause
6565    /// naturally grows into `e instanceof A || e instanceof B` as a second
6566    /// error type is added. `InstanceOf` (tier 5) binds tighter than `Or`
6567    /// (tier 2), so this must print flat — a wrong tier would silently
6568    /// over-parenthesize it into `(e instanceof A) || (e instanceof B)`.
6569    #[test]
6570    fn instanceof_binds_tighter_than_or_on_both_sides() {
6571        let mut program = TsProgram::new();
6572        program.push(TsStmt::expr_stmt(
6573            TsExpr::Binary {
6574                op: TsBinaryOp::Or,
6575                left: Box::new(TsExpr::Binary {
6576                    op: TsBinaryOp::InstanceOf,
6577                    left: Box::new(TsExpr::Ident("e".to_string())),
6578                    right: Box::new(TsExpr::Ident("A".to_string())),
6579                }),
6580                right: Box::new(TsExpr::Binary {
6581                    op: TsBinaryOp::InstanceOf,
6582                    left: Box::new(TsExpr::Ident("e".to_string())),
6583                    right: Box::new(TsExpr::Ident("B".to_string())),
6584                }),
6585            },
6586            None,
6587        ));
6588        let printed = print(&program, "x.bynk", "", "x.ts");
6589        assert_eq!(printed.text, "e instanceof A || e instanceof B;\n");
6590    }
6591
6592    /// Arc E slice 5 (`serialisation.rs`, #1443): `TsBinaryOp::In` — pins
6593    /// the keyword operator text itself, sharing `LessThan`/`GreaterThan`/
6594    /// `InstanceOf`'s own precedence tier.
6595    #[test]
6596    fn prints_an_in_check() {
6597        let mut program = TsProgram::new();
6598        program.push(TsStmt::expr_stmt(
6599            TsExpr::Binary {
6600                op: TsBinaryOp::In,
6601                left: Box::new(TsExpr::Lit(TsLit::Str("name".to_string()))),
6602                right: Box::new(TsExpr::Ident("obj".to_string())),
6603            },
6604            None,
6605        ));
6606        let printed = print(&program, "x.bynk", "", "x.ts");
6607        assert_eq!(printed.text, "\"name\" in obj;\n");
6608    }
6609
6610    /// Mirrors `add_binds_tighter_than_instanceof`/`add_binds_tighter_than_
6611    /// less_than` for the newest member of this shared precedence tier:
6612    /// `Add` (tier 6) binds tighter than `In` (tier 5).
6613    #[test]
6614    fn add_binds_tighter_than_in() {
6615        let mut program = TsProgram::new();
6616        program.push(TsStmt::expr_stmt(
6617            TsExpr::Binary {
6618                op: TsBinaryOp::In,
6619                left: Box::new(TsExpr::Binary {
6620                    op: TsBinaryOp::Add,
6621                    left: Box::new(TsExpr::Ident("a".to_string())),
6622                    right: Box::new(TsExpr::Ident("b".to_string())),
6623                }),
6624                right: Box::new(TsExpr::Ident("obj".to_string())),
6625            },
6626            None,
6627        ));
6628        let printed = print(&program, "x.bynk", "", "x.ts");
6629        assert_eq!(printed.text, "a + b in obj;\n");
6630    }
6631
6632    /// Mirrors `instanceof_binds_tighter_than_or_on_both_sides`: `In` (tier
6633    /// 5) binds tighter than `Or` (tier 2), so a real
6634    /// `"a" in obj || "b" in obj` reads flat with no parens — a wrong tier
6635    /// would silently over-parenthesize it.
6636    #[test]
6637    fn in_binds_tighter_than_or_on_both_sides() {
6638        let mut program = TsProgram::new();
6639        program.push(TsStmt::expr_stmt(
6640            TsExpr::Binary {
6641                op: TsBinaryOp::Or,
6642                left: Box::new(TsExpr::Binary {
6643                    op: TsBinaryOp::In,
6644                    left: Box::new(TsExpr::Lit(TsLit::Str("a".to_string()))),
6645                    right: Box::new(TsExpr::Ident("obj".to_string())),
6646                }),
6647                right: Box::new(TsExpr::Binary {
6648                    op: TsBinaryOp::In,
6649                    left: Box::new(TsExpr::Lit(TsLit::Str("b".to_string()))),
6650                    right: Box::new(TsExpr::Ident("obj".to_string())),
6651                }),
6652            },
6653            None,
6654        ));
6655        let printed = print(&program, "x.bynk", "", "x.ts");
6656        assert_eq!(printed.text, "\"a\" in obj || \"b\" in obj;\n");
6657    }
6658
6659    /// Review of #1471, finding 1: `TsBinaryOp::GreaterThanEq`/`LessThanEq`
6660    /// landed with no direct `bynk-ts` test — only pinned indirectly through
6661    /// `bynk-emit`'s own fixture goldens. This test and the four below close
6662    /// that locality gap, mirroring the coverage every other operator on
6663    /// this precedence tier already has (`prints_an_in_check`/`add_binds_
6664    /// tighter_than_in`/`in_binds_tighter_than_or_on_both_sides`).
6665    #[test]
6666    fn prints_a_greater_than_or_equal_comparison() {
6667        let mut program = TsProgram::new();
6668        program.push(TsStmt::expr_stmt(
6669            TsExpr::Binary {
6670                op: TsBinaryOp::GreaterThanEq,
6671                left: Box::new(TsExpr::Ident("a".to_string())),
6672                right: Box::new(TsExpr::Ident("b".to_string())),
6673            },
6674            None,
6675        ));
6676        let printed = print(&program, "x.bynk", "", "x.ts");
6677        assert_eq!(printed.text, "a >= b;\n");
6678    }
6679
6680    #[test]
6681    fn prints_a_less_than_or_equal_comparison() {
6682        let mut program = TsProgram::new();
6683        program.push(TsStmt::expr_stmt(
6684            TsExpr::Binary {
6685                op: TsBinaryOp::LessThanEq,
6686                left: Box::new(TsExpr::Ident("a".to_string())),
6687                right: Box::new(TsExpr::Ident("b".to_string())),
6688            },
6689            None,
6690        ));
6691        let printed = print(&program, "x.bynk", "", "x.ts");
6692        assert_eq!(printed.text, "a <= b;\n");
6693    }
6694
6695    /// Pins `GreaterThanEq`/`LessThanEq`'s own tier-5 placement — the real
6696    /// `pred_condition_and_message` `InRange` shape (#1471) nests both under
6697    /// `And` (tier 3) and must print flat, with no parens on either side. A
6698    /// regression that demoted either operator to `And`'s own tier or below
6699    /// would silently wrap this in parens instead.
6700    #[test]
6701    fn greater_than_eq_and_less_than_eq_nest_flat_under_and() {
6702        let mut program = TsProgram::new();
6703        program.push(TsStmt::expr_stmt(
6704            TsExpr::Binary {
6705                op: TsBinaryOp::And,
6706                left: Box::new(TsExpr::Binary {
6707                    op: TsBinaryOp::GreaterThanEq,
6708                    left: Box::new(TsExpr::Ident("value".to_string())),
6709                    right: Box::new(TsExpr::Lit(TsLit::Num("0".to_string()))),
6710                }),
6711                right: Box::new(TsExpr::Binary {
6712                    op: TsBinaryOp::LessThanEq,
6713                    left: Box::new(TsExpr::Ident("value".to_string())),
6714                    right: Box::new(TsExpr::Lit(TsLit::Num("100".to_string()))),
6715                }),
6716            },
6717            None,
6718        ));
6719        let printed = print(&program, "x.bynk", "", "x.ts");
6720        assert_eq!(printed.text, "value >= 0 && value <= 100;\n");
6721    }
6722
6723    /// Same tier, *different* operator: `render_binary_operand`'s own doc
6724    /// says equal precedence still parenthesises unless the two sides are
6725    /// the exact same associative operator — `GreaterThan`/`GreaterThanEq`
6726    /// are equal-tier but distinct, so nesting one under the other must
6727    /// still keep its parens (the `_ => true` fallback of the equal-
6728    /// precedence match, newly reachable now this tier has more than one
6729    /// non-keyword operator).
6730    #[test]
6731    fn a_greater_than_eq_operand_of_a_different_relational_op_still_parenthesises() {
6732        let mut program = TsProgram::new();
6733        program.push(TsStmt::expr_stmt(
6734            TsExpr::Binary {
6735                op: TsBinaryOp::GreaterThan,
6736                left: Box::new(TsExpr::Ident("a".to_string())),
6737                right: Box::new(TsExpr::Binary {
6738                    op: TsBinaryOp::GreaterThanEq,
6739                    left: Box::new(TsExpr::Ident("b".to_string())),
6740                    right: Box::new(TsExpr::Ident("c".to_string())),
6741                }),
6742            },
6743            None,
6744        ));
6745        let printed = print(&program, "x.bynk", "", "x.ts");
6746        assert_eq!(printed.text, "a > (b >= c);\n");
6747    }
6748
6749    /// `GreaterThanEq`/`LessThanEq` nested in themselves are NOT associative
6750    /// (`a >= b >= c` does not parse as `a >= (b >= c)`), unlike `||`/`&&` —
6751    /// so, unlike `in_binds_tighter_than_or_on_both_sides`'s own flat `In`
6752    /// chain, a same-operator nesting here must still parenthesise.
6753    #[test]
6754    fn a_right_nested_greater_than_eq_chain_keeps_its_parens() {
6755        let mut program = TsProgram::new();
6756        program.push(TsStmt::expr_stmt(
6757            TsExpr::Binary {
6758                op: TsBinaryOp::GreaterThanEq,
6759                left: Box::new(TsExpr::Ident("a".to_string())),
6760                right: Box::new(TsExpr::Binary {
6761                    op: TsBinaryOp::GreaterThanEq,
6762                    left: Box::new(TsExpr::Ident("b".to_string())),
6763                    right: Box::new(TsExpr::Ident("c".to_string())),
6764                }),
6765            },
6766            None,
6767        ));
6768        let printed = print(&program, "x.bynk", "", "x.ts");
6769        assert_eq!(printed.text, "a >= (b >= c);\n");
6770    }
6771}