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