Skip to main content

bynk_ts/
program.rs

1//! [`TsProgram`]/[`TsStmt`] — the tree. P7.5 built it wide enough only for
2//! the `Verbatim` escape hatch (Q2, `design/tracks/the-typescript-tree.md`
3//! §3.2). P7.8 (#1313) adds the real node algebra ([`TsExpr`]/[`TsType`]/
4//! [`TsDecl`], plus real [`TsStmt`] variants) — not the full §7.1 reference
5//! sketch as literally written (a variant-name list with almost no
6//! field-level design), but the subset `bynk-emit/src/emitter/
7//! events_fanout.rs` (Arc C's real next file — P7.8's own proposal
8//! corrected the track doc's stale schedule, see `design/tracks/
9//! the-typescript-tree.md` §6/§9) concretely needs, grounded against that
10//! file's own real shape rather than guessed. Building the rest of the
11//! sketch's unvalidated variants now would repeat the exact "guessing, not
12//! designing" risk `bynk-ts`'s own module doc (`lib.rs`) already named for
13//! this layer — Arc C's later slices add more variants file by file, the
14//! same precedent [`VerbatimOrigin`] already set.
15
16use crate::source_map::SourceMapBuilder;
17use bynk_syntax::span::Span;
18
19/// A whole generated TypeScript module, as an ordered sequence of top-level
20/// statements. `Vec<TsStmt>`, plain — no richer container yet (P7.6's own
21/// `Artefacts { docs: BTreeMap<PathBuf, Document> }` is where a *project's*
22/// documents get keyed; this is one document's own tree).
23#[derive(Debug, Default)]
24pub struct TsProgram {
25    pub stmts: Vec<TsStmt>,
26}
27
28impl TsProgram {
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    pub fn push(&mut self, stmt: TsStmt) {
34        self.stmts.push(stmt);
35    }
36
37    /// Every `TsStmtKind::Verbatim`/[`TsExpr::VerbatimExpr`] leaf's own
38    /// text, tagged by its [`VerbatimOrigin`] — found by walking every
39    /// statement/expression container in the tree, not just the top level.
40    /// #1538's own real gap: nothing before this walked a whole `TsProgram`
41    /// to find every opaque leaf nested inside real structured content (a
42    /// `Verbatim` inside an `if` branch, a `VerbatimExpr` inside a `Call`'s
43    /// arguments, …) — the only prior way to inspect a `Verbatim`'s content
44    /// was scanning the *whole printed document* text
45    /// ([`crate::verbatim_violations`] doesn't distinguish opaque text from
46    /// a real node's own printed output), which also flags real, structured
47    /// nodes that happen to print the substring `any` (`TsType::named("any")`
48    /// residue, tracked separately by `xtask`'s own `ts_any` probe) — a
49    /// false positive for *this* check, whose whole point is the opaque
50    /// escape hatch specifically. `TsStmtKind` is `pub(crate)`, so only code
51    /// inside this crate can walk it directly; this method is the public
52    /// surface a caller outside `bynk-ts` (`bynkc`'s own
53    /// `tests/tsc_verify.rs`) uses instead.
54    ///
55    /// Every container is walked exhaustively (`match`, no wildcard arm) so
56    /// a future `TsStmt`/`TsExpr` variant fails to compile here until this
57    /// walker accounts for it — the same "compile-time construct, not a
58    /// grep" discipline [`VerbatimOrigin`]'s own doc already states for
59    /// itself. `TsStmtKind::Raw` is deliberately not walked into any
60    /// deeper than skipped: it carries no `VerbatimOrigin` at all (ADR
61    /// `arc-c-lower-rs-permanent-exclusion`'s own permanent exclusion, a
62    /// different bucket from `Verbatim`'s temporary conversion residue), so
63    /// it has nothing this method could report. [`TsType`] positions are
64    /// never walked — no `TsType` variant can hold a `Verbatim`/
65    /// `VerbatimExpr` leaf, so there is nothing to find there.
66    ///
67    /// **A real gap this walker exposed (#1538):** no `VerbatimOrigin::Emit`
68    /// leaf can currently reach a `TsProgram` returned from `bynk-emit` at
69    /// all. Every real `bynk-emit/src/emitter/emit.rs` construction site
70    /// builds a [`TsExpr::VerbatimExpr`], immediately prints it
71    /// (`crate::print_expr`/`crate::print_stmt`), and splices the resulting
72    /// text into a plain `String` a caller further up wraps in
73    /// `TsStmtKind::Raw` — not tagged with a `VerbatimOrigin` at all, and
74    /// so invisible to this walker by design (see `TsStmtKind::Raw`'s own
75    /// doc). `VerbatimExpr`'s own doc promises this content is "visible to
76    /// `crate::verbatim_violations`"; today that's only true at the instant
77    /// of construction, not once the surrounding document is returned —
78    /// closing it needs a real Arc C conversion slice, not a change here.
79    pub fn verbatim_content(&self) -> Vec<(VerbatimOrigin, &str)> {
80        let mut out = Vec::new();
81        for stmt in &self.stmts {
82            walk_stmt(stmt, &mut out);
83        }
84        out
85    }
86
87    /// The module specifier of every top-level import declaration (named,
88    /// namespace or default), in statement order.
89    pub fn import_specifiers(&self) -> Vec<&str> {
90        self.stmts
91            .iter()
92            .filter_map(|s| match &s.kind {
93                TsStmtKind::Decl(
94                    TsDecl::Import { from, .. }
95                    | TsDecl::ImportNamespace { from, .. }
96                    | TsDecl::ImportDefault { from, .. },
97                ) => Some(from.as_str()),
98                _ => None,
99            })
100            .collect()
101    }
102}
103
104fn walk_stmt<'a>(stmt: &'a TsStmt, out: &mut Vec<(VerbatimOrigin, &'a str)>) {
105    match &stmt.kind {
106        TsStmtKind::Verbatim { origin, text } => out.push((*origin, text.as_str())),
107        TsStmtKind::Decl(decl) => walk_decl(decl, out),
108        TsStmtKind::Const { init, .. } => walk_expr(init, out),
109        TsStmtKind::Let { init, .. } => {
110            if let Some(e) = init {
111                walk_expr(e, out);
112            }
113        }
114        TsStmtKind::ExprStmt(e) => walk_expr(e, out),
115        TsStmtKind::Return(e) => {
116            if let Some(e) = e {
117                walk_expr(e, out);
118            }
119        }
120        TsStmtKind::Throw(e) => walk_expr(e, out),
121        TsStmtKind::If {
122            cond,
123            then_branch,
124            else_branch,
125            same_line_else: _,
126        } => {
127            walk_expr(cond, out);
128            walk_stmt(then_branch, out);
129            if let Some(e) = else_branch {
130                walk_stmt(e, out);
131            }
132        }
133        TsStmtKind::ForOf {
134            binding: _,
135            iter,
136            body,
137        } => {
138            walk_expr(iter, out);
139            walk_stmt(body, out);
140        }
141        TsStmtKind::For {
142            name: _,
143            init,
144            test,
145            body,
146        } => {
147            walk_expr(init, out);
148            walk_expr(test, out);
149            walk_stmt(body, out);
150        }
151        TsStmtKind::TryCatch {
152            try_block,
153            catch_param: _,
154            catch_block,
155        } => {
156            walk_stmt(try_block, out);
157            walk_stmt(catch_block, out);
158        }
159        TsStmtKind::Block(stmts) | TsStmtKind::InlineBlock(stmts) => {
160            for s in stmts {
161                walk_stmt(s, out);
162            }
163        }
164        TsStmtKind::Continue => {}
165        TsStmtKind::Assign { target, value } => {
166            walk_expr(target, out);
167            walk_expr(value, out);
168        }
169        TsStmtKind::Comment(_) | TsStmtKind::DocComment(_) | TsStmtKind::Blank => {}
170        TsStmtKind::Switch {
171            discriminant,
172            cases,
173        } => {
174            walk_expr(discriminant, out);
175            for case in cases {
176                if let Some(t) = &case.test {
177                    walk_expr(t, out);
178                }
179                for s in &case.body {
180                    walk_stmt(s, out);
181                }
182            }
183        }
184        TsStmtKind::Increment(e) => walk_expr(e, out),
185        TsStmtKind::Raw(_) => {}
186    }
187}
188
189fn walk_decl<'a>(decl: &'a TsDecl, out: &mut Vec<(VerbatimOrigin, &'a str)>) {
190    match decl {
191        TsDecl::Import { .. }
192        | TsDecl::ImportNamespace { .. }
193        | TsDecl::ImportDefault { .. }
194        | TsDecl::ReExport { .. }
195        | TsDecl::ExportNames { .. }
196        | TsDecl::ReExportAll { .. }
197        | TsDecl::Interface { .. }
198        | TsDecl::TypeAlias { .. }
199        | TsDecl::DeclareConst { .. } => {}
200        TsDecl::Export(inner) => walk_decl(inner, out),
201        TsDecl::ConstDecl { init, .. } => walk_expr(init, out),
202        TsDecl::Class {
203            name: _,
204            fields: _,
205            constructor,
206            methods,
207        } => {
208            if let Some(ctor) = constructor {
209                for s in &ctor.body {
210                    walk_stmt(s, out);
211                }
212            }
213            for m in methods {
214                for s in &m.body {
215                    walk_stmt(s, out);
216                }
217            }
218        }
219        TsDecl::Function { body, .. } => {
220            for s in body {
221                walk_stmt(s, out);
222            }
223        }
224        TsDecl::ExportDefault(e) => walk_expr(e, out),
225    }
226}
227
228fn walk_arrow_body<'a>(body: &'a TsArrowBody, out: &mut Vec<(VerbatimOrigin, &'a str)>) {
229    match body {
230        TsArrowBody::Expr(e) => walk_expr(e, out),
231        TsArrowBody::Block(stmts) => {
232            for s in stmts {
233                walk_stmt(s, out);
234            }
235        }
236    }
237}
238
239fn walk_object_entry<'a>(entry: &'a TsObjectEntry, out: &mut Vec<(VerbatimOrigin, &'a str)>) {
240    match entry {
241        TsObjectEntry::Prop(_, v) => walk_expr(v, out),
242        TsObjectEntry::Shorthand(_) => {}
243        TsObjectEntry::Method { body, .. } => {
244            for s in body {
245                walk_stmt(s, out);
246            }
247        }
248        TsObjectEntry::Spread(e) => walk_expr(e, out),
249    }
250}
251
252fn walk_expr<'a>(expr: &'a TsExpr, out: &mut Vec<(VerbatimOrigin, &'a str)>) {
253    match expr {
254        TsExpr::Ident(_) => {}
255        TsExpr::VerbatimExpr(text, origin) => out.push((*origin, text.as_str())),
256        TsExpr::Member { object, .. } | TsExpr::OptionalMember { object, .. } => {
257            walk_expr(object, out);
258        }
259        TsExpr::Index { object, index } | TsExpr::OptionalIndex { object, index } => {
260            walk_expr(object, out);
261            walk_expr(index, out);
262        }
263        TsExpr::Arrow { body, .. } => walk_arrow_body(body, out),
264        TsExpr::Call { callee, args } | TsExpr::New { callee, args } => {
265            walk_expr(callee, out);
266            for a in args {
267                walk_expr(a, out);
268            }
269        }
270        TsExpr::Object { entries, .. } => {
271            for e in entries {
272                walk_object_entry(e, out);
273            }
274        }
275        TsExpr::Array { items, .. } => {
276            for i in items {
277                walk_expr(i, out);
278            }
279        }
280        TsExpr::TemplateLit { parts: _, exprs } => {
281            for e in exprs {
282                walk_expr(e, out);
283            }
284        }
285        TsExpr::Await(e) => walk_expr(e, out),
286        TsExpr::As { expr, ty: _ } => walk_expr(expr, out),
287        TsExpr::Unary { op: _, expr } => walk_expr(expr, out),
288        TsExpr::Binary { op: _, left, right } => {
289            walk_expr(left, out);
290            walk_expr(right, out);
291        }
292        TsExpr::Conditional {
293            test,
294            consequent,
295            alternate,
296        } => {
297            walk_expr(test, out);
298            walk_expr(consequent, out);
299            walk_expr(alternate, out);
300        }
301        TsExpr::Paren(e) => walk_expr(e, out),
302        TsExpr::Lit(_) => {}
303    }
304}
305
306/// One name a statement exports ([`TsStmt::exported_names`]).
307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
308pub struct ExportedName<'a> {
309    /// The exported name (an `X as Y` entry's `Y`).
310    pub name: &'a str,
311    /// Whether the statement exports a value under the name, as a `const`,
312    /// `function` or `class` does, or only a type, as an `interface`, a
313    /// `type` alias or a `type X` list entry does.
314    pub has_value: bool,
315}
316
317/// One statement — a `Verbatim`-tagged escape hatch (still constructible
318/// only via [`TsStmt::verbatim`], per #1307's Decision D — the
319/// `verbatim_sites` probe needs exactly one string to line-scan for), or,
320/// from this slice, a real structured kind. The real kinds have no such
321/// sealing: they're normal typed constructors, not a "wrap opaque text"
322/// escape hatch, so the `verbatim_sites` concern that motivates `verbatim`'s
323/// own single-constructor discipline doesn't apply to them.
324#[derive(Debug, Clone)]
325pub struct TsStmt {
326    pub(crate) kind: TsStmtKind,
327    /// Where this statement's content originated in the `.bynk` source, if
328    /// known. Only a *top-level* statement's own span is currently recorded
329    /// as a source-map checkpoint ([`crate::printer::print`], unchanged
330    /// from P7.5/R7.4's own scope) — a nested statement (inside a `Block`,
331    /// `If`, `ForOf`, `TryCatch`) still carries this field structurally, for
332    /// whichever future slice gives sub-statement source maps real value,
333    /// but the printer does not yet record a checkpoint from it. Named here
334    /// explicitly (P7.8's own accepted proposal: "an implementation-time
335    /// call within this same shape") rather than left ambiguous.
336    pub span: Option<Span>,
337    /// #1477's own real gap: a body-bearing statement — in practice always
338    /// a `TsStmtKind::Raw`/`TsStmtKind::Verbatim` opaque blob standing in
339    /// for a lowered function/method body (ADR 0391's own permanent
340    /// exclusion) — carries its *own* per-statement source-map checkpoints,
341    /// collected by the caller's own body-local `SourceMapBuilder` before
342    /// this node existed. Before this field, every real `bynk-emit` caller
343    /// that needed to merge those checkpoints into its own module map had to
344    /// reverse-engineer this node's own print-time byte offset from the
345    /// *outside* — `bynk-emit`'s own `emit_class_method_and_merge_source_map`
346    /// (`emitter/emit.rs`) recovers it by subtracting known lengths and
347    /// string-matching the printed text's own tail, degrading to a silent
348    /// skip if that search fails; still live for `emit_service`/`emit_agent`'s
349    /// own not-yet-converted call sites (#1481/#1482). `emit_free_fn` used to
350    /// recover it by separate, independent exact arithmetic, guarded by a
351    /// `debug_assert!` — #1480 converted it to set this field directly
352    /// instead, the first real `bynk-emit` caller to do so. Both existed
353    /// only because nothing reported *this* node's own real print-time
354    /// offset directly. Setting this field lets the printer itself do the
355    /// merge, at the exact offset it is about to write this node's text to
356    /// — no reverse-engineering, no silent-skip fallback (see this crate's
357    /// own private `printer::render_block_stmts` for the handling). `None`
358    /// for every real site that predates this field and every node whose own
359    /// text carries no nested checkpoints of its own — the overwhelmingly
360    /// common case, and the reason this is an `Option`, not a required
361    /// field.
362    pub nested_map: Option<SourceMapBuilder>,
363    /// #1486's own second real gap, found converting `emit_test_module`/
364    /// `emit_integration_module`: [`crate::printer::print`]'s own top-level
365    /// loop always merges a statement's [`nested_map`](Self::nested_map)
366    /// against parent source id `0` (`crate::printer::MergeTarget`'s own
367    /// review-of-#1488 fix threads a `source_id` through every *manual*
368    /// merge entry point, but `print`'s own automatic top-level case never
369    /// had a per-statement way to say "not 0") — correct for every
370    /// single-source module (`emit_project`, every other real caller so
371    /// far), but wrong for a test/integration module's own aggregate map,
372    /// which registers one source per fragment file a case's body came from
373    /// (`SourceMapBuilder::add_source`) and needs each such case's own
374    /// top-level statement merged against *that* source's id, not the
375    /// module's primary one. `0` (every site predating this field, and
376    /// every single-source module after it) keeps the automatic policy
377    /// exactly as it already was; a case/property/attack wrapper statement
378    /// sets this to its own registered source id before `print` sees it.
379    pub nested_map_source_id: usize,
380    /// #1486's own real gap, found converting `emit_project`/`emit_test_
381    /// module`/`emit_integration_module` off a single opaque top-level
382    /// `Verbatim` wrap: [`crate::printer::print`]'s own top-level loop
383    /// inserts exactly one blank line between every pair of adjacent
384    /// top-level statements (its own "readability policy" doc, right below
385    /// this field's own use site) — correct for content that was always
386    /// designed against that policy (`emit_composition_root`/`emit_test_
387    /// main`/`workers.rs`/`workers_entry.rs`, every one of them zero-diff
388    /// from day one), but `emit_agent`'s own already-shipped `Vec<TsStmt>`
389    /// (#1482, predating this field) was built against the *previous*
390    /// regime — where every real caller printed via `bynk-emit`'s own
391    /// `extend_printed`/`extend_printed_and_merged` (a flat per-stmt loop
392    /// with no automatic spacing of its own) — and genuinely needs a
393    /// same-registry-then-zero-factory pairing with **no** blank line
394    /// between them, matching the pre-conversion hand-written text this
395    /// crate's own zero-diff discipline requires preserving exactly.
396    /// `true` on a statement suppresses the automatic blank line
397    /// [`crate::printer::print`] would otherwise insert immediately before
398    /// it — the narrow, general escape hatch this real, already-shipped
399    /// content needs, not a special case hard-coded into the printer for
400    /// one specific pair of `TsDecl` variants. `false` (every site
401    /// predating this field, and the overwhelming majority after it) keeps
402    /// the automatic policy exactly as it already is.
403    pub no_blank_before: bool,
404}
405
406#[derive(Debug, Clone)]
407pub(crate) enum TsStmtKind {
408    Verbatim {
409        // Read by `TsProgram::verbatim_content` (#1538) to attribute each
410        // opaque leaf's text to its origin file.
411        origin: VerbatimOrigin,
412        text: String,
413    },
414    /// A top-level declaration ([`TsDecl`]) printed as a statement — the
415    /// bridge between `TsProgram`'s flat `Vec<TsStmt>` and the reference
416    /// sketch's separate `TsDecl` enum (an `import`/`interface`/top-level
417    /// `const`/`class` *is* one kind of top-level statement in this tree,
418    /// not a different container).
419    Decl(TsDecl),
420    /// A local `const` binding, e.g. `const { events } = ...;` or
421    /// `const subs = ...;` — distinct from [`TsDecl::ConstDecl`], which is
422    /// the top-level form. Carries a real destructuring [`TsBindingName`]
423    /// because `events_fanout.rs`'s own `const { events } = ...` needs one
424    /// (a gap beyond the accepted proposal's own variant list — a bare
425    /// `String` name cannot represent it; named explicitly as a deviation,
426    /// not invented silently).
427    Const {
428        name: TsBindingName,
429        ty: Option<TsType>,
430        init: TsExpr,
431    },
432    /// `let`'s sibling to `Const` — unused by `events_fanout.rs` itself, but
433    /// the `const`/`let` distinction is real TypeScript semantics the
434    /// printer must preserve once one of the pair exists (the accepted
435    /// proposal's own reasoning for keeping it).
436    Let {
437        name: TsBindingName,
438        ty: Option<TsType>,
439        init: Option<TsExpr>,
440    },
441    /// An expression used as a whole statement (a bare call, e.g.).
442    ExprStmt(TsExpr),
443    Return(Option<TsExpr>),
444    /// `throw <expr>;` — #1353's own real gap: `emit_contract_guarded_body`'s
445    /// own precondition/postcondition guards (`bynk-emit/src/emitter/
446    /// emit.rs`) each throw a constructed `Error` on violation — no prior
447    /// slice needed a bare `throw` (every other error-signalling site in
448    /// this tree so far has been a `Return`). Mirrors `Return`'s own shape
449    /// exactly, minus the `Option` (a `throw` always carries a value; there
450    /// is no bare `throw;` in JS/TS).
451    Throw(TsExpr),
452    /// `if (cond) <then_branch>` (optionally `else <else_branch>`).
453    /// `then_branch`/`else_branch` may each be a [`TsStmtKind::Block`]
454    /// (printed with braces) or any other single statement (printed inline
455    /// on the same line, matching `if (!Array.isArray(subs)) continue;`'s
456    /// own real, brace-free shape). `else_branch` is #1323's own real gap:
457    /// `workers_entry.rs`'s queue-consumer ack/retry dispatch (`if
458    /// (result.tag === "Ack") msg.ack(); else { ...; msg.retry(); }`) —
459    /// `events_fanout.rs`'s own grounding never used one, so P7.8 named
460    /// omitting it a deliberate choice, not an oversight; this slice's own
461    /// real content needs it for real.
462    ///
463    /// `same_line_else` is #1325's own real gap: `workers_entry.rs`'s own
464    /// real `else` sits on its own fresh line (`}\nelse {`), but
465    /// `emit_test_main`'s own real content wants the conventional `} else
466    /// {` — two already-real files disagreeing on the same construct, the
467    /// same class of tension the `Await`-under-`As` correction (#1323/
468    /// #1324) found for parenthesisation. `false` (fresh line) is the
469    /// existing, already-tested default via [`TsStmt::if_else_stmt`]; `true`
470    /// (same line) is reached only through
471    /// [`TsStmt::if_else_same_line_stmt`].
472    If {
473        cond: TsExpr,
474        then_branch: Box<TsStmt>,
475        else_branch: Option<Box<TsStmt>>,
476        same_line_else: bool,
477    },
478    /// `for (const <binding> of <iter>) <body>`.
479    ForOf {
480        binding: String,
481        iter: TsExpr,
482        body: Box<TsStmt>,
483    },
484    /// `for (let <name> = <init>; <test>; <name>++) <body>` — a C-style
485    /// indexed loop, `ForOf`'s counterpart for index-based iteration (as
486    /// opposed to `for...of`'s element-based iteration over an existing
487    /// collection). Arc E slice 7 (#1447)'s own real, narrow gap:
488    /// `bynk-emit`'s `serialisation.rs` has two real sites — `ListInst`'s
489    /// and `MapInst`'s own deserialise-side element loops, both `for (let i
490    /// = 0; i < json.length; i++) { ... }` — that walk a JSON array by
491    /// index, a shape `ForOf` cannot represent (its own `binding` names a
492    /// per-element destructuring target bound fresh each iteration, not a
493    /// counter carrying its own init/test/update clauses across
494    /// iterations). Deliberately grounded in only what those two real call
495    /// sites need, not the general C-style-for grammar — the same "extend
496    /// narrowly" posture this file's own `TsBinaryOp::In`/`LessThan`
497    /// additions already took:
498    ///   - `name`/`init` are always a plain `let <name> = <init>;`
499    ///     declaration — no destructuring, no multi-declarator list, no
500    ///     explicit type annotation (neither real site needs one).
501    ///   - The update clause always renders as the postfix increment
502    ///     `<name>++`, over `name` itself rather than a separate field —
503    ///     review of #1448 found an independent `update: TsExpr` field
504    ///     structurally redundant with `name` (both real construction sites
505    ///     could only ever pass `ident(name)`) with a genuinely dangerous
506    ///     failure mode if the two ever disagreed: unlike a wrong `test`
507    ///     (which `tsc --strict` or a non-running loop tends to surface), a
508    ///     mismatched update clause (`for (let i = 0; i < json.length;
509    ///     j++)`) compiles cleanly whenever `j` is merely in scope and hangs
510    ///     the generated worker at runtime, with nothing in the type, the
511    ///     printer, or a test to catch it. Removing the field removes the
512    ///     failure mode entirely rather than merely asserting against it —
513    ///     the same "postfix increment has no expression-position
514    ///     representation, only a dedicated statement/clause shape"
515    ///     restriction `Increment`'s own doc above already states for a
516    ///     bare `<expr>++;` statement (#1325), applied here too.
517    ///   - `body` is the loop body; both real sites pass a `Block`, though
518    ///     the same brace-vs-inline rendering `If`/`ForOf` already share
519    ///     applies to any other shape too.
520    For {
521        name: String,
522        init: TsExpr,
523        test: TsExpr,
524        body: Box<TsStmt>,
525    },
526    /// `try <try_block> catch (<catch_param>) <catch_block>` — a real gap
527    /// beyond the reference sketch (`design/bynk-greenfield-compiler.md`'s
528    /// §7.1 has no `TryCatch` at all), found and named by P7.8's own
529    /// accepted proposal: `events_fanout.rs`'s subscriber-failure-isolation
530    /// `try`/`catch` (ADR 0284) is load-bearing control flow, not
531    /// decorative. `catch_param: None` prints the bare `catch { ... }`
532    /// form (ES2019's optional catch binding) — #1321's own real gap:
533    /// `workers.rs`'s `emit_http_sum_wrapper` reads the raw request body in
534    /// a `try`/`catch` that never uses the caught error (`} catch {`, no
535    /// `(e)` at all).
536    TryCatch {
537        try_block: Box<TsStmt>,
538        catch_param: Option<String>,
539        catch_block: Box<TsStmt>,
540    },
541    /// `{ <stmts> }` — the body container for `If`/`ForOf`/`TryCatch`/
542    /// constructor and method bodies.
543    Block(Vec<TsStmt>),
544    /// A bare `continue;` — a second real gap beyond the accepted
545    /// proposal's own variant list: `events_fanout.rs` uses it twice
546    /// (`if (!Array.isArray(subs)) continue;` / `if (!binding) continue;`),
547    /// load-bearing loop control the accepted proposal's `TsStmt` list
548    /// doesn't name. No label — nothing in the grounding file needs one.
549    Continue,
550    /// `target = value;` — a third real gap, found in review of the
551    /// implementing PR (#1313): `events_fanout.rs`'s own constructor body is
552    /// exactly one statement, `this.env = (env ?? {}) as
553    /// Record<string, ServiceBinding>;` (a field assignment, not a `const`/
554    /// `let` binding), which the accepted proposal's own grounding
555    /// catalogue missed — it catalogued the `fetch` method's body in detail
556    /// but not the constructor's. `target` is deliberately a full `TsExpr`
557    /// (not a narrower "assignable" type) so it can hold `this.env` (a
558    /// `Member` expression) without a second binding-target type; nothing
559    /// about `Assign` validates that `target` is actually assignable
560    /// (`bynk-check` already does that on the `.bynk` side before emission
561    /// ever runs).
562    Assign {
563        target: TsExpr,
564        value: TsExpr,
565    },
566    /// `// <text>` — a bare line comment. Added for Arc C's own first real
567    /// conversion slice (#1317): every `bynk-emit`-generated `.ts` file
568    /// opens with the same two-line header banner
569    /// (`// Generated by bynkc — do not edit by hand.` / a file-specific
570    /// second line), and nothing before this slice could represent one.
571    /// Not speculative — a universal need every later Arc C slice hits on
572    /// its own first line, closed once here rather than left as a residual
573    /// `Verbatim` wrap that would make `verbatim_sites` show zero
574    /// improvement for real, substantial conversion work.
575    ///
576    /// Carries no semantic content the printer or checker ever reads back —
577    /// pure, inert text. Printed one `// `-prefixed line per `\n` in `text`
578    /// (`bynk-ts/src/printer.rs`'s own `render_stmt`), so a multi-line
579    /// comment is representable as one statement, though
580    /// `events_fanout.rs`'s own two-line header is built as two separate
581    /// `Comment` statements instead — matching how its two adjacent
582    /// `import` lines are two separate `TsDecl::Import` statements, not one
583    /// with an embedded line break, and exercising the printer's own new
584    /// "no blank line between adjacent `Comment`s" rule (the same
585    /// exception already established for adjacent `import`s).
586    Comment(String),
587    /// `/** ... */` — a JSDoc block comment, distinct from
588    /// [`TsStmtKind::Comment`]'s own `//`-per-line form. #1333's own real
589    /// need: `emit_doc_block` (`bynk-emit/src/emitter/emit.rs`) renders a
590    /// Bynk `///` doc comment as a real JSDoc block — printed via
591    /// [`crate::printer::print_stmt`], not [`crate::printer::print`], since
592    /// every real call site today is a shared helper spliced into a
593    /// still-unconverted caller's own buffer, the same P7.9 pattern
594    /// `ts_type_ref`/`ts_ty` already used (keep the caller's own signature,
595    /// build a real node internally, print just that fragment). Escaping
596    /// (a literal `*/` inside the text becomes `*\/`, so it can't
597    /// prematurely close the comment and let trailing text land as
598    /// executable top-level TypeScript — issue #720) and the blank-line
599    /// convention (a blank source line prints as a bare ` *`, no trailing
600    /// space) live in the printer (`render_stmt`'s own `DocComment` arm,
601    /// `crate::printer`'s own private renderer), matching
602    /// where every other statement kind's rendering rules live.
603    DocComment(String),
604    /// A bare blank line — no statement content. #1323's own real, narrow
605    /// need: `workers_entry.rs`'s `fetch` method body has an unconditional
606    /// blank line after its internal Service-Binding dispatch block, and a
607    /// conditional one after the WebSocket-upgrade dispatch (when present)
608    /// and the Events dispatch (when present) — three specific points the
609    /// pre-conversion `writeln!(out).unwrap()` wrote directly, nested
610    /// *inside* the `try` block's own statement list. Distinct from
611    /// [`crate::printer::print`]'s own top-level blank-line policy (this
612    /// module's own printer doc), which only separates `TsProgram`'s own
613    /// top-level statements — nothing before this slice needed a blank line
614    /// anywhere inside a nested block, so that policy was never generalized
615    /// to every nesting depth. A single, narrow statement kind represents
616    /// exactly this, rather than widening the top-level-only policy to a
617    /// depth it has no other real content to justify.
618    Blank,
619    /// `switch (<discriminant>) { <cases> }` — #1323's own largest gap: the
620    /// first genuinely new statement-*grouping* construct in this tree
621    /// (every prior addition was a single expression/type variant or a
622    /// straightforward one-block wrapper). `workers_entry.rs` has four real
623    /// `switch` statements (the internal `/_bynk/call/` and `/_bynk/event/`
624    /// dispatches, the `scheduled` handler's `event.cron` dispatch, the
625    /// `queue` handler's `batch.queue` dispatch); every real `case` there is
626    /// a `{ ... }`-blocked body ending in a terminal statement (`return`/
627    /// `continue`), so no fallthrough/shared-body grouping is represented —
628    /// extend narrowly, the same posture every other addition here takes.
629    /// Arc E slice 6 (#1445) widens this one further, narrowly: a
630    /// non-`default` case's own bracing is now per-case
631    /// ([`TsSwitchCase::case_braced`]) rather than unconditional —
632    /// `emit_sum_codec`'s own payload-free-variant case is real, unbraced
633    /// content, not a hypothetical.
634    Switch {
635        discriminant: TsExpr,
636        cases: Vec<TsSwitchCase>,
637    },
638    /// `{ stmt; stmt; ... }` — a braced, multi-statement block printed on
639    /// ONE generated line, distinct from [`TsStmtKind::Block`] (always
640    /// multi-line). #1323's own real gap: `workers_entry.rs`'s queue
641    /// consumer has two real sites (`if (__r.tag === "Err") { console.
642    /// error(...); msg.retry(); continue; }`; the `else` branch of the
643    /// ack/retry dispatch) where the pre-conversion `writeln!` code
644    /// deliberately packed several short statements onto one physical
645    /// line — a real, distinct formatting choice from every other
646    /// multi-statement body in this tree, which always prints one
647    /// statement per line. Only reachable through [`TsStmt::if_stmt`]/
648    /// [`TsStmt::if_else_stmt`]'s own branches in real content today.
649    InlineBlock(Vec<TsStmt>),
650    /// `<expr>++;` — a postfix increment used as a whole statement. #1325's
651    /// own real, narrow gap: `emit_test_main`'s own `passed++;`/`failed++;`
652    /// counters. No prefix form, no decrement, no expression-position use
653    /// (every real site increments a bare counter as its own statement) —
654    /// [`TsExpr::Unary`] stays prefix-only (`!`/`typeof`), since a postfix
655    /// operator used as a *statement* is a different grammatical position
656    /// from a prefix operator used as an *operand*, not a shape `Unary`
657    /// could represent by adding a variant.
658    Increment(TsExpr),
659    /// A pre-rendered, unconditional-passthrough statement blob — printed
660    /// exactly as given, with no leading indent, no added semicolon, no
661    /// added braces (matching [`TsStmtKind::Verbatim`]'s own `out.push_str(
662    /// text)` rendering, not the ordinary per-kind `indent(depth)`-prefixed
663    /// shape every other variant gets). #1337's own real need:
664    /// `emit_method`'s own body is delegated wholesale to
665    /// `emit_block_as_function_body_with_return` (`bynk-emit`'s
666    /// `emitter/lower.rs:201`) — the one splice boundary ADR
667    /// `arc-c-lower-rs-permanent-exclusion` names as a *permanent*,
668    /// deliberate exclusion from this tree (`lower.rs` is the compiler's
669    /// own second code-generation pass, comprehensive language-surface
670    /// work Arc C was never scoped to cover), not a not-yet-converted
671    /// residue [`TsStmtKind::Verbatim`] would misrepresent it as.
672    ///
673    /// Deliberately a SEPARATE variant from `Verbatim`, not a reuse of it,
674    /// even though both render identically (`out.push_str(text)`): the
675    /// `verbatim_sites`/`verbatim_origins` probes exist specifically to
676    /// track Arc C's own *temporary* conversion residue trending toward
677    /// zero — a `Raw` site is never counted there (it has no
678    /// `VerbatimOrigin` at all), because using `Verbatim` here would make
679    /// this permanent exclusion look like unfinished Arc C work a future
680    /// slice is expected to close, which it structurally cannot.
681    ///
682    /// #1339's own second real use, broadening (not narrowing) the
683    /// above: `emit_refined_type`'s own `of()` guard block splices
684    /// `emit_refined_checks`'s own output this same way — that function
685    /// keeps its exact `out: &mut String` signature (the P7.9/step-1
686    /// pattern applied one level down, not a `lower.rs`-style permanent
687    /// exclusion), but its real content is ALREADY built and printed from
688    /// real `bynk_ts::TsStmt`/`print_stmt` calls internally — genuinely
689    /// statement-shaped pre-rendered text, the same mechanical fit `Raw`
690    /// already provides, just for a different underlying reason than
691    /// `lower.rs`'s own permanent exclusion.
692    ///
693    /// #1445's own third real use (Arc E slice 6, `serialisation.rs`'s
694    /// `emit_sum_codec`, via that file's own `raw_stmts_at_depth_one`
695    /// helper): a real, currently-shipped BYTE-LEVEL quirk, not a scope
696    /// boundary. `emit_field_deserialise_wire`'s own field guards, spliced
697    /// into a sum's deserialise-side payload case, have always printed at
698    /// depth 1 (`splice_stmts`'s own fixed indent) even though they sit
699    /// structurally inside a `TsStmtKind::Switch` case's own body (which
700    /// this printer would otherwise render two levels deeper, at depth 3) —
701    /// confirmed byte-for-byte against `212_json_codec`/
702    /// `407_workers_generic_sum_boundary`'s own real fixtures. Letting the
703    /// switch case render these at their structurally-correct depth instead
704    /// would be a real, deliberate formatting change with no fixture-corpus
705    /// backing either way, which that slice's own zero-diff mandate ruled
706    /// out choosing unprompted.
707    ///
708    /// All three real uses share the one property that actually matters for
709    /// this variant's own existence: *real, already-correctly-indented (or,
710    /// for the third, real-but-deliberately-NOT-restructured) statement text
711    /// this call site cannot turn into a properly-nested `Vec<TsStmt>`
712    /// without changing scope or bytes it isn't the one authorised to
713    /// change* — not whether the reason is permanent, temporary, or a
714    /// preserved historical quirk.
715    Raw(String),
716}
717
718/// One `case`/`default` arm of a `TsStmtKind::Switch`. `test: None` is the
719/// `default:` case — every real `default` in `workers_entry.rs` prints its
720/// body directly under `default:` with no `{ }` block, while every real
721/// non-`default` `case` (regardless of `test`) always prints a `{ }`-blocked
722/// body.
723///
724/// `default_braced` (Arc C slice 33, `tests_emit.rs` slice C, #1401) —
725/// `emit_stub_class`'s own `ReturnsEach` dispatch is the first real site
726/// with a BRACED `default: { ... }`, a genuinely different convention from
727/// `workers_entry.rs`'s own unbraced one; only meaningful when `test` is
728/// `None` (a non-`default` case was unconditionally braced before
729/// `case_braced` existed, below). Kept a per-case flag rather than changing
730/// the existing unbraced-default rendering, since that would risk
731/// `workers_entry.rs`'s own real, already-zero-diff content for no benefit —
732/// the same "don't touch working, unrelated content" judgment this track
733/// makes repeatedly.
734///
735/// `case_braced` (Arc E slice 6, #1445) — the mirror-image gap `default_braced`
736/// left open: `serialisation.rs`'s `emit_sum_codec` (this tree's first real
737/// non-`default` case that is NOT `{ }`-blocked) needs `case "Pending":
738/// return { kind: "Pending" };` — unbraced — right beside a sibling
739/// payload-carrying `case "Shipped": { ... }` — braced — in the *same*
740/// switch (`212_json_codec`'s own mixed `Status` fixture, confirmed by
741/// direct grep before this field was added: a payload-free variant's case
742/// is always unbraced, a payload-carrying variant's is always braced, and a
743/// real fixture exercises both side by side). Every prior real non-`default`
744/// case (`workers_entry.rs`'s dispatches, `tests_emit.rs`'s own sequential-
745/// outcome cases) already wants braces, so those call sites all set this
746/// `true` — the same "narrow, argued, backward-compatible extension"
747/// shape `default_braced` itself set as precedent. Only meaningful when
748/// `test` is `Some(..)` (a `default` case's bracing is `default_braced`'s
749/// business, not this field's — the inverse of that field's own scoping).
750#[derive(Debug, Clone)]
751pub struct TsSwitchCase {
752    pub test: Option<TsExpr>,
753    pub body: Vec<TsStmt>,
754    pub default_braced: bool,
755    pub case_braced: bool,
756}
757
758impl TsStmt {
759    /// The one constructor for a `Verbatim`-kinded statement.
760    pub fn verbatim(origin: VerbatimOrigin, text: impl Into<String>, span: Option<Span>) -> Self {
761        Self {
762            kind: TsStmtKind::Verbatim {
763                origin,
764                text: text.into(),
765            },
766            span,
767            nested_map: None,
768            no_blank_before: false,
769            nested_map_source_id: 0,
770        }
771    }
772
773    /// Whether this is a named `import { … } from "…"` declaration (#1817): a
774    /// module assembler hoists those into its import block.
775    pub fn is_named_import(&self) -> bool {
776        matches!(self.kind, TsStmtKind::Decl(TsDecl::Import { .. }))
777    }
778
779    /// The names this statement exports (#1820): an `export` of a named
780    /// declaration, or an `export { … }` list, with or without `from`. A
781    /// module assembler re-exporting another module's names reads them here.
782    /// A `Raw` or `Verbatim` statement's text is not parsed, so an export
783    /// written that way is not reported.
784    ///
785    /// Each name says whether this statement exports a value under it (#1862
786    /// review): an `interface` or `type` alias exports only a type, and under
787    /// `isolatedModules` a type-only name is re-exported only as `type X`
788    /// (TS1205). A list entry follows [`TsDecl::Import`]'s spelling: `type X`
789    /// is type-only, and `X as Y` exports `Y`. Every other entry is taken to
790    /// carry a value, the form that re-exports both meanings.
791    pub fn exported_names(&self) -> Vec<ExportedName<'_>> {
792        let TsStmtKind::Decl(decl) = &self.kind else {
793            return Vec::new();
794        };
795        match decl {
796            TsDecl::ReExport { names, .. } | TsDecl::ExportNames { names } => names
797                .iter()
798                .map(|spec| {
799                    let (spec, has_value) = match spec.trim().strip_prefix("type ") {
800                        Some(rest) => (rest, false),
801                        None => (spec.trim(), true),
802                    };
803                    let name = spec.rsplit(" as ").next().unwrap_or(spec).trim();
804                    ExportedName { name, has_value }
805                })
806                .collect(),
807            TsDecl::Export(inner) => match inner.as_ref() {
808                TsDecl::Interface { name, .. } | TsDecl::TypeAlias { name, .. } => {
809                    vec![ExportedName {
810                        name,
811                        has_value: false,
812                    }]
813                }
814                TsDecl::ConstDecl { name, .. }
815                | TsDecl::Class { name, .. }
816                | TsDecl::Function { name, .. } => vec![ExportedName {
817                    name,
818                    has_value: true,
819                }],
820                _ => Vec::new(),
821            },
822            _ => Vec::new(),
823        }
824    }
825
826    pub fn decl(decl: TsDecl, span: Option<Span>) -> Self {
827        Self {
828            kind: TsStmtKind::Decl(decl),
829            span,
830            nested_map: None,
831            no_blank_before: false,
832            nested_map_source_id: 0,
833        }
834    }
835
836    pub fn const_stmt(
837        name: TsBindingName,
838        ty: Option<TsType>,
839        init: TsExpr,
840        span: Option<Span>,
841    ) -> Self {
842        Self {
843            kind: TsStmtKind::Const { name, ty, init },
844            span,
845            nested_map: None,
846            no_blank_before: false,
847            nested_map_source_id: 0,
848        }
849    }
850
851    pub fn let_stmt(
852        name: TsBindingName,
853        ty: Option<TsType>,
854        init: Option<TsExpr>,
855        span: Option<Span>,
856    ) -> Self {
857        Self {
858            kind: TsStmtKind::Let { name, ty, init },
859            span,
860            nested_map: None,
861            no_blank_before: false,
862            nested_map_source_id: 0,
863        }
864    }
865
866    pub fn expr_stmt(expr: TsExpr, span: Option<Span>) -> Self {
867        Self {
868            kind: TsStmtKind::ExprStmt(expr),
869            span,
870            nested_map: None,
871            no_blank_before: false,
872            nested_map_source_id: 0,
873        }
874    }
875
876    pub fn return_stmt(expr: Option<TsExpr>, span: Option<Span>) -> Self {
877        Self {
878            kind: TsStmtKind::Return(expr),
879            span,
880            nested_map: None,
881            no_blank_before: false,
882            nested_map_source_id: 0,
883        }
884    }
885
886    pub fn throw_stmt(expr: TsExpr, span: Option<Span>) -> Self {
887        Self {
888            kind: TsStmtKind::Throw(expr),
889            span,
890            nested_map: None,
891            no_blank_before: false,
892            nested_map_source_id: 0,
893        }
894    }
895
896    pub fn if_stmt(cond: TsExpr, then_branch: TsStmt, span: Option<Span>) -> Self {
897        Self {
898            kind: TsStmtKind::If {
899                cond,
900                then_branch: Box::new(then_branch),
901                else_branch: None,
902                same_line_else: false,
903            },
904            span,
905            nested_map: None,
906            no_blank_before: false,
907            nested_map_source_id: 0,
908        }
909    }
910
911    pub fn if_else_stmt(
912        cond: TsExpr,
913        then_branch: TsStmt,
914        else_branch: TsStmt,
915        span: Option<Span>,
916    ) -> Self {
917        Self {
918            kind: TsStmtKind::If {
919                cond,
920                then_branch: Box::new(then_branch),
921                else_branch: Some(Box::new(else_branch)),
922                same_line_else: false,
923            },
924            span,
925            nested_map: None,
926            no_blank_before: false,
927            nested_map_source_id: 0,
928        }
929    }
930
931    /// [`TsStmt::if_else_stmt`]'s own sibling with `} else {` on one line —
932    /// #1325's own real gap, `emit_test_main`'s own real `else` spacing. See
933    /// `TsStmtKind::If`'s own doc for why this needs to be a distinct
934    /// constructor rather than a change to the existing default.
935    pub fn if_else_same_line_stmt(
936        cond: TsExpr,
937        then_branch: TsStmt,
938        else_branch: TsStmt,
939        span: Option<Span>,
940    ) -> Self {
941        Self {
942            kind: TsStmtKind::If {
943                cond,
944                then_branch: Box::new(then_branch),
945                else_branch: Some(Box::new(else_branch)),
946                same_line_else: true,
947            },
948            span,
949            nested_map: None,
950            no_blank_before: false,
951            nested_map_source_id: 0,
952        }
953    }
954
955    pub fn for_of(
956        binding: impl Into<String>,
957        iter: TsExpr,
958        body: TsStmt,
959        span: Option<Span>,
960    ) -> Self {
961        Self {
962            kind: TsStmtKind::ForOf {
963                binding: binding.into(),
964                iter,
965                body: Box::new(body),
966            },
967            span,
968            nested_map: None,
969            no_blank_before: false,
970            nested_map_source_id: 0,
971        }
972    }
973
974    /// `for (let <name> = <init>; <test>; <update>++) <body>` — see
975    /// `TsStmtKind::For`'s own doc for exactly what this construct does
976    /// and does not represent.
977    pub fn for_stmt(
978        name: impl Into<String>,
979        init: TsExpr,
980        test: TsExpr,
981        body: TsStmt,
982        span: Option<Span>,
983    ) -> Self {
984        Self {
985            kind: TsStmtKind::For {
986                name: name.into(),
987                init,
988                test,
989                body: Box::new(body),
990            },
991            span,
992            nested_map: None,
993            no_blank_before: false,
994            nested_map_source_id: 0,
995        }
996    }
997
998    pub fn try_catch(
999        try_block: TsStmt,
1000        catch_param: Option<impl Into<String>>,
1001        catch_block: TsStmt,
1002        span: Option<Span>,
1003    ) -> Self {
1004        Self {
1005            kind: TsStmtKind::TryCatch {
1006                try_block: Box::new(try_block),
1007                catch_param: catch_param.map(Into::into),
1008                catch_block: Box::new(catch_block),
1009            },
1010            span,
1011            nested_map: None,
1012            no_blank_before: false,
1013            nested_map_source_id: 0,
1014        }
1015    }
1016
1017    pub fn block(stmts: Vec<TsStmt>, span: Option<Span>) -> Self {
1018        Self {
1019            kind: TsStmtKind::Block(stmts),
1020            span,
1021            nested_map: None,
1022            no_blank_before: false,
1023            nested_map_source_id: 0,
1024        }
1025    }
1026
1027    pub fn continue_stmt(span: Option<Span>) -> Self {
1028        Self {
1029            kind: TsStmtKind::Continue,
1030            span,
1031            nested_map: None,
1032            no_blank_before: false,
1033            nested_map_source_id: 0,
1034        }
1035    }
1036
1037    pub fn assign(target: TsExpr, value: TsExpr, span: Option<Span>) -> Self {
1038        Self {
1039            kind: TsStmtKind::Assign { target, value },
1040            span,
1041            nested_map: None,
1042            no_blank_before: false,
1043            nested_map_source_id: 0,
1044        }
1045    }
1046
1047    pub fn comment(text: impl Into<String>, span: Option<Span>) -> Self {
1048        Self {
1049            kind: TsStmtKind::Comment(text.into()),
1050            span,
1051            nested_map: None,
1052            no_blank_before: false,
1053            nested_map_source_id: 0,
1054        }
1055    }
1056
1057    pub fn doc_comment(text: impl Into<String>, span: Option<Span>) -> Self {
1058        Self {
1059            kind: TsStmtKind::DocComment(text.into()),
1060            span,
1061            nested_map: None,
1062            no_blank_before: false,
1063            nested_map_source_id: 0,
1064        }
1065    }
1066
1067    pub fn blank(span: Option<Span>) -> Self {
1068        Self {
1069            kind: TsStmtKind::Blank,
1070            span,
1071            nested_map: None,
1072            no_blank_before: false,
1073            nested_map_source_id: 0,
1074        }
1075    }
1076
1077    pub fn switch_stmt(discriminant: TsExpr, cases: Vec<TsSwitchCase>, span: Option<Span>) -> Self {
1078        Self {
1079            kind: TsStmtKind::Switch {
1080                discriminant,
1081                cases,
1082            },
1083            span,
1084            nested_map: None,
1085            no_blank_before: false,
1086            nested_map_source_id: 0,
1087        }
1088    }
1089
1090    pub fn inline_block(stmts: Vec<TsStmt>, span: Option<Span>) -> Self {
1091        Self {
1092            kind: TsStmtKind::InlineBlock(stmts),
1093            span,
1094            nested_map: None,
1095            no_blank_before: false,
1096            nested_map_source_id: 0,
1097        }
1098    }
1099
1100    pub fn increment(expr: TsExpr, span: Option<Span>) -> Self {
1101        Self {
1102            kind: TsStmtKind::Increment(expr),
1103            span,
1104            nested_map: None,
1105            no_blank_before: false,
1106            nested_map_source_id: 0,
1107        }
1108    }
1109
1110    /// The one constructor for a `Raw`-kinded statement — `text` is printed
1111    /// verbatim, exactly as given (see `TsStmtKind::Raw`'s own doc for
1112    /// why this is a distinct kind from `Verbatim`, not a reuse of it).
1113    pub fn raw(text: impl Into<String>, span: Option<Span>) -> Self {
1114        Self {
1115            kind: TsStmtKind::Raw(text.into()),
1116            span,
1117            nested_map: None,
1118            no_blank_before: false,
1119            nested_map_source_id: 0,
1120        }
1121    }
1122}
1123
1124/// A binding's own name, in either of the two shapes `events_fanout.rs`
1125/// itself uses: a plain identifier (`const subs = ...`), or an
1126/// object-destructuring pattern (`const { events } = ...`) — naming only
1127/// the destructured properties themselves (`{ a, b }`), not the renamed
1128/// (`{ a: renamed }`) or nested (`{ a: { b } }`) forms, since nothing in
1129/// the grounding file needs either.
1130#[derive(Debug, Clone)]
1131pub enum TsBindingName {
1132    Ident(String),
1133    ObjectPattern(Vec<String>),
1134}
1135
1136/// An expression. Only the shapes `events_fanout.rs` concretely uses
1137/// (Decision B) — not the reference sketch's full `TsExpr` list (`Arrow`,
1138/// `Cond`, `TemplateLit`, `Spread` are all unused in the grounding file and
1139/// deliberately not built here). Arc C slice 3 (#1321, `workers.rs`) adds
1140/// `Arrow`, `OptionalMember`/`OptionalIndex` (Decision A, gaps 3/4) — this
1141/// slice's own real, grounded needs.
1142#[derive(Debug, Clone)]
1143pub enum TsExpr {
1144    /// A real identifier — the only content this variant is for. #1539
1145    /// (review of `design/reviews/2026-08-30-post-restructuring-review.md`
1146    /// Part 3/Part 5 §1): before this doc existed, `bynk-emit` also smuggled
1147    /// non-identifier text through it at 15 call sites in `emit.rs` alone —
1148    /// a quoted string literal, `this.<method>` member access, a complete
1149    /// `!(<pred>)` unary expression — an untagged `Verbatim`-shaped escape
1150    /// hatch [`TsStmt::verbatim`]'s own doc already named the concern for
1151    /// (Q2's "makes the ratchet a compile-time construct, not a grep"), just
1152    /// with no tag and no probe watching it. Every one of those call sites
1153    /// now either builds the matching real node (`Lit(TsLit::Str)`, `Member`,
1154    /// `Unary`) or routes through [`TsExpr::VerbatimExpr`] instead — this
1155    /// variant no longer holds anything but a genuine identifier.
1156    Ident(String),
1157    /// A residual, not-yet-converted expression carrying a [`VerbatimOrigin`]
1158    /// tag — [`TsStmt::verbatim`]'s own discipline, closed for expressions
1159    /// too (#1539). Printed exactly as [`TsExpr::Ident`] always was (raw
1160    /// text, no escaping of its own — the caller is responsible for
1161    /// producing valid, already-escaped TypeScript, the same contract
1162    /// `TsStmt::verbatim`'s own text carries), but now visible to
1163    /// [`crate::verbatim_violations`] and `xtask`'s own `verbatim_origins`/
1164    /// `verbatim_sites` gated probes, which had nothing to watch at the
1165    /// expression level before this variant existed. What ends up here after
1166    /// #1539's own conversion pass: a generic-typed method/constructor
1167    /// callee (`Call`/`New` have no `type_args` field), a nested
1168    /// `As`-under-`As` cast chain the printer's own operand-parenthesisation
1169    /// rule would otherwise mis-wrap (review of #1390), a block-bodied IIFE
1170    /// with no matching `TsArrowBody` shape, and a `pred_condition_and_
1171    /// message`-style message that is already pre-escaped or unescaped by
1172    /// contract (a second escaping pass through `TsLit::Str` would corrupt
1173    /// it) — none of these has a real node to convert to without expanding
1174    /// this crate's own type algebra well past what #1539 asks for.
1175    VerbatimExpr(String, VerbatimOrigin),
1176    /// `object.property`.
1177    Member {
1178        object: Box<TsExpr>,
1179        property: String,
1180    },
1181    /// `object?.property` — the optional-chaining form of [`TsExpr::Member`].
1182    /// A distinct variant, not an `optional: bool` flag on `Member` itself
1183    /// (#1321's own Decision A, gap 4, left the mechanism to the
1184    /// implementation): a flag would touch every one of `Member`'s
1185    /// already-many real call sites (`events_fanout.rs`, this crate's own
1186    /// printer tests) that never need one, where a separate variant touches
1187    /// none of them — narrower, matching this track's own repeated
1188    /// "smallest correct scope" judgment. `workers.rs`'s own secret-probe
1189    /// idiom (`emit_websocket_upgrade`/`emit_http_wrapper`/
1190    /// `emit_secret_lookup`, three identical real sites) is the only
1191    /// grounding.
1192    OptionalMember {
1193        object: Box<TsExpr>,
1194        property: String,
1195    },
1196    /// `object[index]`.
1197    Index {
1198        object: Box<TsExpr>,
1199        index: Box<TsExpr>,
1200    },
1201    /// `object?.[index]` — the optional-chaining form of [`TsExpr::Index`],
1202    /// the same real grounding and the same "separate variant, not a flag"
1203    /// reasoning as [`TsExpr::OptionalMember`].
1204    OptionalIndex {
1205        object: Box<TsExpr>,
1206        index: Box<TsExpr>,
1207    },
1208    /// `(params) => body` (`is_async: false`) or `async (params) => body`
1209    /// (`is_async: true`) — an arrow function, expression- or block-bodied
1210    /// (see [`TsArrowBody`]). `is_async` added by #1327: `emit_composition_root`'s
1211    /// own `__eventsDispatch` closure (`async (events: Array<...>) => {...}`)
1212    /// is the first real `Arrow` site that's async — mirrors
1213    /// `TsDecl::Function`'s own `is_async` field (#1325), added for the same
1214    /// reason. `generics`/`return_type` added by #1339: `emit_sum_type`'s
1215    /// own generic payload-constructor arrows (`<T>(name: T): Sum<T> =>
1216    /// (...)`) need both — bare generic names only (empty for every
1217    /// non-generic site, the overwhelming majority), matching
1218    /// `TsObjectEntry::Method.generics`'s own (#1337) identical convention;
1219    /// `return_type` mirrors `TsDecl::Function.return_type`'s own existing
1220    /// `Option<TsType>` shape — a real gap the accepted proposal's own
1221    /// grounding named `generics` for but missed: every one of this file's
1222    /// own real generic-payload arrows carries an explicit return-type
1223    /// annotation the arrow itself owns (`: {name}{params}`), not something
1224    /// the body's own type alone determines.
1225    ///
1226    /// `body`'s type became [`TsArrowBody`] (#1435, Arc E slice 1): from
1227    /// P7.8 through #1434 this field was a bare `Box<TsExpr>` — expression-
1228    /// bodied only, "extend narrowly" (the same posture `TsBinaryOp` takes
1229    /// for its own operator table), since every real site up to and
1230    /// including #1339's generic payload-constructor arrows had an
1231    /// expression body. `serialisation.rs`'s `serialise_field_expr_wire`
1232    /// (`bynk-emit`) is the first real site that doesn't: its `Float`
1233    /// non-finite guard is a genuine statement-bodied IIFE (`((v: number) =>
1234    /// { if (!Number.isFinite(v)) throw new Error(...); return v as
1235    /// JsonValue; })(value)`), not reducible to one expression. Widening the
1236    /// existing field (`TsArrowBody::Expr(Box<TsExpr>)` |
1237    /// `TsArrowBody::Block(Vec<TsStmt>)`) rather than adding a second
1238    /// sibling `TsExpr` variant matches this file's own repeated
1239    /// "extend the existing variant when every real site still needs the
1240    /// same node kind, only the body shape differs" precedent
1241    /// (`TsObjectEntry::Method.inline`, #1337) — every prior `Arrow`
1242    /// construction site across the workspace wraps its already-correct
1243    /// expression body in `TsArrowBody::Expr(..)`, a mechanical change with
1244    /// no behavior difference.
1245    Arrow {
1246        params: Vec<TsParam>,
1247        is_async: bool,
1248        generics: Vec<String>,
1249        return_type: Option<TsType>,
1250        body: Box<TsArrowBody>,
1251    },
1252    Call {
1253        callee: Box<TsExpr>,
1254        args: Vec<TsExpr>,
1255    },
1256    New {
1257        callee: Box<TsExpr>,
1258        args: Vec<TsExpr>,
1259    },
1260    /// A value object literal, e.g. `{ status: 204 }` — comma-separated.
1261    /// `multiline: false` (the ordinary case, via [`TsExpr::object`])
1262    /// always prints on one line, matching [`TsType::Object`]'s own
1263    /// (semicolon-separated) single-line convention for the *type*-position
1264    /// shape. `multiline: true` (via [`TsExpr::multiline_object`]) is a
1265    /// real, grounded gap found implementing Arc C's first slice (#1317):
1266    /// `events_fanout.rs`'s own `__eventRoutes` table is a top-level
1267    /// `const` initializer with one entry per line, each with its own
1268    /// trailing comma, closing brace at the *statement's* own indent —
1269    /// TypeScript's ordinary multi-line object-literal convention, which
1270    /// nothing in this crate could represent before this addition. Only
1271    /// statement/declaration-level renderers (which already carry `depth`)
1272    /// can render this correctly — `printer.rs`'s own `render_stmt_level_
1273    /// expr`, and (#1355) `render_multiline_object_entry`'s own `Prop` arm,
1274    /// for a `multiline: true` object nested one level inside ANOTHER
1275    /// multiline object as one of its own entries' values —
1276    /// `emit_messages_bundle`'s own real doubly-nested `{ locale: { code:
1277    /// expr, ... }, ... }` table. A `multiline: true` object nested any
1278    /// OTHER way (an array element, a call argument, a `Prop`'s value
1279    /// inside a non-multiline object, …) still renders via the ordinary
1280    /// depth-unaware `render_expr` recursion, which cannot honour
1281    /// `multiline` — not reachable from any real `bynk-emit` call site
1282    /// today, but worth knowing before nesting one that way.
1283    Object {
1284        entries: Vec<TsObjectEntry>,
1285        multiline: bool,
1286    },
1287    /// An array literal, e.g. `[{ binding: "x", service: "y" }]`.
1288    /// `multiline: false` (the ordinary case, via [`TsExpr::array`]) always
1289    /// prints on one line, comma-separated — every real site before this
1290    /// slice. `multiline: true` (via [`TsExpr::multiline_array`]) is #1325's
1291    /// own real gap: `emit_test_main`'s own `modules` array (one `{ name,
1292    /// run }` entry per test, one per line, each with its own trailing
1293    /// comma, closing `]` at the statement's own indent) — the exact same
1294    /// shape [`TsExpr::Object`]'s own `multiline` field already represents
1295    /// for object literals, just for an array. Same reachability boundary as
1296    /// `Object`'s own `multiline` field (see its own doc, updated by
1297    /// #1355): `render_stmt_level_expr` and `render_multiline_object_
1298    /// entry`'s own `Prop` arm both honour it; nested any other way, a
1299    /// `multiline: true` array falls back to single-line via the ordinary
1300    /// depth-unaware `render_expr` recursion.
1301    Array {
1302        items: Vec<TsExpr>,
1303        multiline: bool,
1304    },
1305    /// `` `text${expr}more text` `` — a template literal. `parts.len()` is
1306    /// always `exprs.len() + 1` (`parts[0]` before the first substitution,
1307    /// `parts[i+1]` after `exprs[i]`, …). #1325's own real, first grounding
1308    /// for this shape (`bynk-ts`'s own module doc named `TemplateLit`
1309    /// explicitly "unused in the grounding file" until now):
1310    /// `emit_test_main`'s own `` `${m.name}:` ``/`` `${passed} passed,
1311    /// ${failed} failed.` `` lines.
1312    ///
1313    /// **`parts` are printed verbatim — the printer applies no escaping of
1314    /// its own.** The same "the field is already a raw-text slot" reasoning
1315    /// [`TsDecl::Import`]'s own `names` field doc already uses, not a new
1316    /// pattern: `emit_test_main`'s own two real ✓/✗ substitution lines embed
1317    /// a literal `✓`/`✗` JS unicode escape as pre-formed ASCII
1318    /// text (six literal characters, not the actual glyph) — an escaper
1319    /// mirroring [`TsLit::Str`]'s own (which escapes every `\` it sees)
1320    /// would double that literal backslash into `\\u2713`, corrupting the
1321    /// exact byte-golden output this slice must match. Every real part in
1322    /// `emit_test_main` is static, compiler-authored text (never Bynk user
1323    /// data), so there is no real content this boundary loses safety on
1324    /// today — a future caller passing untrusted/dynamic text into `parts`
1325    /// is responsible for pre-escaping backtick/`` ${ ``/`\` itself before
1326    /// constructing one.
1327    TemplateLit {
1328        parts: Vec<String>,
1329        exprs: Vec<TsExpr>,
1330    },
1331    Await(Box<TsExpr>),
1332    /// `expr as ty`.
1333    As {
1334        expr: Box<TsExpr>,
1335        ty: TsType,
1336    },
1337    Unary {
1338        op: TsUnaryOp,
1339        expr: Box<TsExpr>,
1340    },
1341    Binary {
1342        op: TsBinaryOp,
1343        left: Box<TsExpr>,
1344        right: Box<TsExpr>,
1345    },
1346    /// `test ? consequent : alternate` — #1323's own real gap: `method ===
1347    /// "HEAD" ? headResponse(__response) : __response` (once per `GET`
1348    /// route) and `method === "OPTIONS" ? 204 : 405` (the method-fallthrough
1349    /// path). The lowest-precedence expression form after `Arrow` — needs
1350    /// the same parenthesization-rule coverage `Arrow` got in review of
1351    /// #1322 (`needs_parens_as_operand`/`render_binary_operand`/`As`'s own
1352    /// local rule), added proactively in this same slice rather than left
1353    /// for a review round to re-find.
1354    Conditional {
1355        test: Box<TsExpr>,
1356        consequent: Box<TsExpr>,
1357        alternate: Box<TsExpr>,
1358    },
1359    /// An explicit, printer-preserved parenthesization — distinct from every
1360    /// other variant's own precedence-*derived* parens (`render_operand`/
1361    /// `render_binary_operand`), which the printer adds or omits based on
1362    /// what the wrapped expression *is*. `Paren` instead always prints
1363    /// `(<inner>)` regardless of `inner`'s own shape or precedence. #1323's
1364    /// own real, narrow need: `workers_entry.rs`'s CORS-preflight guard
1365    /// wraps its own path-match condition in unconditional parens
1366    /// (`&& ({cond})`) even when `cond` reduces to a single equality check
1367    /// with no operator lower-precedence than the outer `&&` — a real case
1368    /// the precedence-derived rules correctly do *not* parenthesize (they're
1369    /// answering "is this needed for correctness", not "did the source
1370    /// always write parens here"). Not a general escape hatch: every other
1371    /// real site in this file's own content still goes through the ordinary
1372    /// precedence machinery unchanged.
1373    Paren(Box<TsExpr>),
1374    Lit(TsLit),
1375}
1376
1377/// [`TsExpr::Arrow`]'s own `body` shape (#1435, Arc E slice 1) — see that
1378/// field's own doc for why this is a widened field rather than a second
1379/// `TsExpr` variant. `Expr` is every real site before this slice (and the
1380/// overwhelming majority after it): the arrow's body is one expression,
1381/// printed with no surrounding braces. `Block` is `serialisation.rs`'s own
1382/// `Float` non-finite guard, the first real statement-bodied arrow anywhere
1383/// in this tree — printed as a real braced block, reusing this crate's
1384/// own printer's existing compact-statement-list renderer
1385/// (`render_compact_stmts`, the same one `TsStmtKind::InlineBlock` already
1386/// shares with `render_branch`'s own same-line `if`/`else`) rather than a
1387/// third copy of that "one physical line, semicolon-separated" logic. Every
1388/// real `Block` site today is exactly this one-line IIFE shape (an arrow
1389/// with no other real use of a genuinely multi-line block body has been
1390/// found); a future multi-line block-bodied arrow is a real, separate gap
1391/// this variant does not yet cover.
1392#[derive(Debug, Clone)]
1393pub enum TsArrowBody {
1394    Expr(Box<TsExpr>),
1395    Block(Vec<TsStmt>),
1396}
1397
1398impl TsExpr {
1399    /// The ordinary, single-line object literal, `Prop`-only — every entry
1400    /// is `key: value`. `events_fanout.rs`'s own entries (and this crate's
1401    /// own printer tests) are all this shape; kept taking a plain
1402    /// `Vec<(String, TsExpr)>` rather than `Vec<TsObjectEntry>` so none of
1403    /// those existing call sites needed to change when `TsObjectEntry` was
1404    /// added (#1321) — see [`TsExpr::object_entries`] for the mixed-entry
1405    /// form `workers.rs` itself needs (shorthand/spread/method entries).
1406    pub fn object(entries: Vec<(String, TsExpr)>) -> Self {
1407        TsExpr::Object {
1408            entries: entries
1409                .into_iter()
1410                .map(|(k, v)| TsObjectEntry::Prop(k, v))
1411                .collect(),
1412            multiline: false,
1413        }
1414    }
1415
1416    /// One entry per line, each with its own trailing comma — see
1417    /// [`TsExpr::Object`]'s own doc for the real shape and the
1418    /// depth-awareness this needs at the print site. `Prop`-only, the same
1419    /// convenience [`TsExpr::object`]'s own doc explains.
1420    pub fn multiline_object(entries: Vec<(String, TsExpr)>) -> Self {
1421        TsExpr::Object {
1422            entries: entries
1423                .into_iter()
1424                .map(|(k, v)| TsObjectEntry::Prop(k, v))
1425                .collect(),
1426            multiline: true,
1427        }
1428    }
1429
1430    /// The single-line object literal, taking [`TsObjectEntry`] directly —
1431    /// for a mixed entry list (shorthand/spread/method alongside `Prop`),
1432    /// which [`TsExpr::object`]'s own `Vec<(String, TsExpr)>` convenience
1433    /// can't represent. #1321's own real grounding: `workers.rs`'s local
1434    /// capability-provider `deps` object mixes shorthand names
1435    /// (`{ cap1, cap2 }`) with explicit `key: value` entries
1436    /// (`__exec: exec`) in one literal.
1437    pub fn object_entries(entries: Vec<TsObjectEntry>) -> Self {
1438        TsExpr::Object {
1439            entries,
1440            multiline: false,
1441        }
1442    }
1443
1444    /// [`TsExpr::multiline_object`]'s own `TsObjectEntry` sibling —
1445    /// #1321's own real grounding: `workers.rs`'s `compose`-returned
1446    /// surface object is one shorthand-async-`Method` entry per wrapper,
1447    /// one per line.
1448    pub fn multiline_object_entries(entries: Vec<TsObjectEntry>) -> Self {
1449        TsExpr::Object {
1450            entries,
1451            multiline: true,
1452        }
1453    }
1454
1455    /// The ordinary, single-line array literal — every real site before
1456    /// #1325.
1457    pub fn array(items: Vec<TsExpr>) -> Self {
1458        TsExpr::Array {
1459            items,
1460            multiline: false,
1461        }
1462    }
1463
1464    /// One entry per line, each with its own trailing comma — see
1465    /// [`TsExpr::Array`]'s own doc for the real shape and the
1466    /// depth-awareness this needs at the print site. #1325's own real
1467    /// grounding: `emit_test_main`'s own `modules` array.
1468    pub fn multiline_array(items: Vec<TsExpr>) -> Self {
1469        TsExpr::Array {
1470            items,
1471            multiline: true,
1472        }
1473    }
1474
1475    /// A template literal — see [`TsExpr::TemplateLit`]'s own doc for the
1476    /// real shape and its no-escaping boundary. The sole caller-facing
1477    /// invariant (`parts.len() == exprs.len() + 1`) is asserted here rather
1478    /// than left to `render_expr`'s own `parts`-driven loop, which would
1479    /// otherwise silently drop trailing `exprs` on a malformed tree instead
1480    /// of failing loudly (review of #1326, finding 1).
1481    pub fn template_lit(parts: Vec<String>, exprs: Vec<TsExpr>) -> Self {
1482        debug_assert_eq!(
1483            parts.len(),
1484            exprs.len() + 1,
1485            "TsExpr::template_lit: parts.len() must be exprs.len() + 1 \
1486             (parts[0] before the first substitution, parts[i+1] after \
1487             exprs[i], …) — got {} parts and {} exprs",
1488            parts.len(),
1489            exprs.len()
1490        );
1491        TsExpr::TemplateLit { parts, exprs }
1492    }
1493}
1494
1495/// One entry of a [`TsExpr::Object`] literal. Only `Prop` existed before
1496/// #1321 (`events_fanout.rs`'s own grounding never needed the other three) —
1497/// `workers.rs`'s own dominant shape (Decision A, gap 1) is a literal
1498/// object whose entries are shorthand async methods (every `on call`/`on
1499/// http`/… wrapper attaches this way), its local capability-`deps` object
1500/// mixes bare shorthand names with explicit `key: value` pairs, and three
1501/// real sites spread another object into one (gap 2, folded in here rather
1502/// than as its own top-level `TsExpr` variant — a spread only ever appears
1503/// as an object- or array-literal entry in this file, never as a
1504/// standalone expression, so scoping it to entry position is the narrower
1505/// correct change).
1506#[derive(Debug, Clone)]
1507pub enum TsObjectEntry {
1508    /// `key: value`.
1509    Prop(String, TsExpr),
1510    /// `key` alone — object-literal property shorthand, e.g. `{ cap1,
1511    /// cap2 }`. Distinct from `Prop(name, TsExpr::Ident(name))`, which
1512    /// prints `name: name`, not the shorthand form real TypeScript
1513    /// property-shorthand text actually is.
1514    Shorthand(String),
1515    /// A shorthand async method entry, e.g. `async foo(a: T) { ... },` —
1516    /// `workers.rs`'s own dominant shape (Decision A, gap 1): every
1517    /// wrapper helper (`emit_call_wrapper`, `emit_event_wrapper`, …)
1518    /// attaches to the returned `compose` surface this way — none of
1519    /// `workers.rs`'s own real sites annotates a return type, so
1520    /// `return_type` stayed unneeded until #1323's own real gap:
1521    /// `workers_entry.rs`'s `export default { fetch, scheduled?, queue? }`
1522    /// entries all carry one (`: Promise<Response>`/`: Promise<void>`), the
1523    /// same field [`TsClassMethod::return_type`] already has for the
1524    /// declaration-position sibling this mirrors.
1525    ///
1526    /// `generics` and `doc` (#1337's own real gaps, both found only by the
1527    /// zero-diff fixture check — not the accepted proposal's own citation,
1528    /// which searched the project-form fixture corpus only;
1529    /// `402_generic_instance_method` is single-file form, and while
1530    /// `65_money_uses_time`/`64_full_time_commons` ARE project form, the
1531    /// citation's own doc-comment search used the wrong marker, `///`
1532    /// rather than this language's real `---`-delimited doc block —
1533    /// both gaps outside what that first pass actually checked):
1534    ///
1535    /// - `generics`: a method on a generic type erases to
1536    ///   `{name}<{generics}>(self: {Type}<{generics}>, …)` — `Box.map<A,
1537    ///   U>`, the type's own `A` plus the method's own `U`. Bare names
1538    ///   only, no bounds/defaults — every real generic parameter list this
1539    ///   tree ever builds is (matching [`crate::printer::print_type`]'s
1540    ///   own `TsType::Named`-only-name convention for the identical
1541    ///   reason), so a full `Vec<TsParam>` (with its own unneeded
1542    ///   `ty`/`optional` fields) would be the wrong shape here.
1543    /// - `doc`: a JSDoc block immediately preceding the method entry, at
1544    ///   the same indent — `Timestamp.diff`/`Timestamp.add`
1545    ///   (`65_money_uses_time`) both carry one. Printed the identical way
1546    ///   `TsStmtKind::DocComment` is (same escaping, same blank-line
1547    ///   convention), reusing that one renderer rather than a second copy
1548    ///   — this field exists because an object-entry method has no
1549    ///   `TsStmt` slot of its own to hold a preceding statement in.
1550    Method {
1551        name: String,
1552        is_async: bool,
1553        generics: Vec<String>,
1554        params: Vec<TsParam>,
1555        return_type: Option<TsType>,
1556        doc: Option<String>,
1557        /// `false` (every method entry landed before #1337): `body` renders
1558        /// as an ordinary braced, multi-line block. `true`: `body` renders
1559        /// compactly on the SAME line as the signature — `{ <stmts>; }`,
1560        /// reusing this crate's own established compact-statement
1561        /// machinery (`TsStmtKind::InlineBlock`'s own sibling shape). A
1562        /// real gap #1337's own zero-diff check found:
1563        /// `emit_forwarded_methods`'s own pre-conversion `writeln!` always
1564        /// built the WHOLE entry — signature and one-statement body alike —
1565        /// on one physical line (`"  {method}({params}): {ret} {{ return
1566        /// …; }},"`), a genuinely different real shape from every other
1567        /// `Method` entry in this tree, all of which are multi-line.
1568        inline: bool,
1569        body: Vec<TsStmt>,
1570    },
1571    /// `...expr` — an object-spread entry (Decision A, gap 2), e.g. `{
1572    /// ...deps, identity: __caller }`.
1573    Spread(TsExpr),
1574}
1575
1576/// `!x` (`events_fanout.rs`'s `!Array.isArray(subs)`, `!binding`) and
1577/// `typeof x` (#1321, `workers.rs`'s own secret-probe idiom: `typeof
1578/// __secret !== "string"`) — the two unary operators real content uses, not
1579/// the full JS/TS table.
1580#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1581pub enum TsUnaryOp {
1582    Not,
1583    Typeof,
1584}
1585
1586/// `??` (`events_fanout.rs`'s `env ?? {}`), plus three more real operators
1587/// #1321 (`workers.rs`) grounds: `||`/`&&` (the Bearer-header presence
1588/// checks, `__authz === null || !__authz.startsWith(...)` /
1589/// `__authz !== null && __authz.startsWith(...)`) and `===`/`!==`
1590/// (pervasive throughout the file's own tagged-result and header checks).
1591/// #1323 (`workers_entry.rs`) grounds one more: `>` (the request-body-
1592/// ceiling guard's own `Number(__contentLength) > <cap>` — the one real
1593/// site anywhere in `bynk-emit` that needs a relational, not equality,
1594/// comparison). Not the
1595/// full JS/TS operator table (Decision B's own "extend narrowly" posture) —
1596/// see the printer's own `binary_precedence` (`bynk-ts/src/printer.rs`,
1597/// private) for why a nested `Binary` operand's parenthesisation needed to
1598/// become precedence-aware once more than one operator existed. Arc C, step
1599/// (11) (#1388) grounds one more: `+` (string concatenation) — the
1600/// ICU-formatting cluster's own dominant structural pattern, every
1601/// literal/placeholder segment in a message template joins this way. Real
1602/// JS/TS precedence (binds tighter than every comparison/logical operator
1603/// this table already has) and real left-associativity (`"a" + "b" + "c"`
1604/// needs no parens, the same way a same-operator `||`/`&&` chain already
1605/// prints flat) both matter here, not just the operator symbol itself.
1606#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1607pub enum TsBinaryOp {
1608    NullishCoalescing,
1609    Or,
1610    And,
1611    StrictEq,
1612    StrictNotEq,
1613    GreaterThan,
1614    /// `instanceof` — Arc C slice 34 (`tests_emit.rs` slice D, #1403)'s own
1615    /// real gap: `emit_test_case_function`'s own catch clause
1616    /// (`e instanceof ExpectationError`) is the first real `instanceof`
1617    /// anywhere in `bynk-emit`'s own content. Real JS/TS precedence puts it
1618    /// at the same tier as the relational comparisons (`<`/`>`) — sharing
1619    /// [`TsBinaryOp::LessThan`]'s own tier, not a new one — rendered as the
1620    /// keyword `" instanceof "`, textually the same shape as `&&`/`||`
1621    /// rather than symbol punctuation.
1622    InstanceOf,
1623    /// `<` — Arc C slice 33 (`tests_emit.rs` slice C, #1401)'s own real gap:
1624    /// `emit_stub_class`'s `ReturnsEach` sequence-cursor guard
1625    /// (`this.__seq_N < <bound>`) is the first real `<` comparison anywhere
1626    /// in `bynk-emit`'s own content. Same precedence tier as
1627    /// [`TsBinaryOp::GreaterThan`] (real JS/TS relational operators all
1628    /// share one level) — added alongside it rather than folding into a
1629    /// single "relational" variant, matching this enum's own existing
1630    /// one-variant-per-real-operator convention.
1631    LessThan,
1632    Add,
1633    /// `in` — Arc E slice 5 (`serialisation.rs`, #1443)'s own real gap:
1634    /// `emit_record_codec`'s per-field default-value prevalidation line
1635    /// (`"<field>" in obj ? obj["<field>"] : <default>`) needs a real "is
1636    /// this wire key present at all" test, distinct from `!== undefined`
1637    /// (Events slice 3a, #972's own Decision D: a wire key present with an
1638    /// explicit `null`/`{"kind":"None"}` value must NOT fall through to the
1639    /// default, only a genuinely *absent* key may). Real JS/TS grammar puts
1640    /// `in` at the exact same `RelationalExpression` precedence tier as
1641    /// `<`/`>`/`instanceof` (all five — plus `<=`/`>=`, unmodelled here —
1642    /// share one level in the spec), so it joins [`TsBinaryOp::LessThan`]'s
1643    /// tier rather than a new one, the same "share, don't multiply, tiers"
1644    /// convention `InstanceOf`/`LessThan` already set. Rendered as the
1645    /// keyword `" in "`, the same textual-keyword-operator shape
1646    /// `InstanceOf` already established (not symbol punctuation).
1647    In,
1648    /// `>=` — #1471's own real gap: `pred_condition_and_message`'s
1649    /// (`bynk-emit`) `NonNegative`/`InRange`/`InRangeF`/`MinLength` arms
1650    /// each build a real "at least" comparison (`{receiver} >= 0`,
1651    /// `{receiver} >= {a}`, `{receiver}.length >= {n}`) that this enum had
1652    /// no home for — [`TsBinaryOp::In`]'s own doc already named this exact
1653    /// gap ("plus `<=`/`>=`, unmodelled here"). Same `RelationalExpression`
1654    /// precedence tier as [`TsBinaryOp::GreaterThan`]/[`TsBinaryOp::
1655    /// LessThan`]/[`TsBinaryOp::InstanceOf`]/[`TsBinaryOp::In`] (real
1656    /// JS/TS puts all six at one level), rendered `" >= "` — symbol
1657    /// punctuation, matching `GreaterThan`/`LessThan`'s own convention,
1658    /// not a keyword like `InstanceOf`/`In`.
1659    GreaterThanEq,
1660    /// `<=` — the same gap's other half: `InRange`/`InRangeF`/`MaxLength`'s
1661    /// own "at most" comparison (`{receiver} <= {b}`, `{receiver}.length
1662    /// <= {n}`). Same tier and rendering convention as
1663    /// [`TsBinaryOp::GreaterThanEq`], symmetric with it the way
1664    /// `GreaterThan`/`LessThan` already are.
1665    LessThanEq,
1666}
1667
1668/// A literal — the three kinds `events_fanout.rs` uses (a string, a number,
1669/// `null`), plus `Bool` (#1323's own real gap: `workers_entry.rs`'s
1670/// `CorsPolicy.credentials`/`SecurityPolicy.nosniff` object-literal fields
1671/// are real booleans, e.g. `credentials: true`, `nosniff: false` — nothing
1672/// before this slice's own grounding ever built one).
1673#[derive(Debug, Clone)]
1674pub enum TsLit {
1675    Str(String),
1676    /// Rendered verbatim, as text — TypeScript's own numeric-literal
1677    /// grammar is not this crate's problem to re-derive; the caller passes
1678    /// the exact digits it wants printed.
1679    Num(String),
1680    Null,
1681    Bool(bool),
1682    /// The complete literal text, printed exactly as given — no quoting, no
1683    /// escaping, nothing added or removed. #1325's own real, narrow gap:
1684    /// `emit_test_main`'s own `const PREFIX = "system · ";` embeds
1685    /// a literal JS unicode escape (six ASCII characters, `·`) as
1686    /// pre-formed text — [`TsLit::Str`]'s own escaper (which turns every `\`
1687    /// it sees into `\\`) would double that literal backslash, corrupting
1688    /// this specific byte-golden output. The same "already a raw-text slot"
1689    /// boundary [`TsExpr::TemplateLit`]'s own `parts` field documents, just
1690    /// for a whole literal (quotes included) rather than a template
1691    /// literal's own static segments. One real site — not a general
1692    /// escaping bypass for ordinary string content, which stays on
1693    /// `TsLit::Str`.
1694    Raw(String),
1695}
1696
1697/// A type-position node. `Named` (extended with type arguments — a real gap
1698/// the reference sketch left unaddressed: `Record<string,
1699/// Array<{...}>>`/`Promise<Response>` both need one, and a bare `Named`
1700/// with no type-argument slot cannot represent either), `Array` (extended
1701/// with a `readonly` modifier — P7.9's own real gap: every `List`/`Query`
1702/// element type `bynk-emit`'s `ts_type_ref*`/`ts_ty` families build is
1703/// `readonly T[]`, not plain `T[]`), `Object`, and `Fn` (P7.9's own second
1704/// real gap — the query-thunk wrapper `(() => readonly T[])` and a real
1705/// parametered function type `(a0: T0, …) => Ret` both need one) — not the
1706/// sketch's `Union`/`Intersection`/`Literal`/`TypeParam`/`Readonly` (still
1707/// unused; `readonly` here is a modifier on `Array`, not the sketch's own
1708/// separate `Readonly` wrapper variant).
1709#[derive(Debug, Clone)]
1710pub enum TsType {
1711    /// A named type, optionally generic — `string`/`unknown` (no type
1712    /// arguments) or `Record<string, T>`/`Promise<Response>` (some).
1713    Named {
1714        name: String,
1715        type_args: Vec<TsType>,
1716    },
1717    /// `T[]` (`readonly: false`) or `readonly T[]` (`readonly: true`) —
1718    /// TypeScript's own postfix array-type syntax (not the equivalent
1719    /// `Array<T>`/`ReadonlyArray<T>` generic spelling either family uses).
1720    ///
1721    /// The printer parenthesises `element` wherever a bare postfix `[]`
1722    /// would bind to the wrong thing (#1898): a readonly array (`readonly
1723    /// T[][]` is TS1354), a [`TsType::Fn`] (whose `[]` would bind to its
1724    /// return type), a [`TsType::Union`] and a [`TsType::Intersection`]
1725    /// (`A | B[]` reads as `A | (B[])`). So `Array` over any element prints
1726    /// correctly as built — a caller must not wrap the element itself, or
1727    /// it double-parenthesises. The rule is `needs_parens_as_array_element`
1728    /// in `printer.rs`. This closes the hazard the review of #1315/#1316
1729    /// recorded here.
1730    Array {
1731        element: Box<TsType>,
1732        readonly: bool,
1733    },
1734    /// A type-position object shape, e.g. `{ type: string; payload: unknown }`
1735    /// — semicolon-separated, always printed on one line (see
1736    /// [`TsExpr::Object`]'s own doc for the value-position contrast). An
1737    /// `interface`'s own *members* are each printed on their own line by
1738    /// [`TsDecl::Interface`]'s printer, independent of this variant's own
1739    /// (always-inline) rendering — the two only look different because
1740    /// `TsDecl::Interface` is the thing choosing to put each member on its
1741    /// own line, not because `Object` has two rendering modes.
1742    ///
1743    /// #1323 replaced the plain `Vec<(String, TsType, bool)>` (`optional`
1744    /// only, #1321) with [`TsTypeMember`] — `workers_entry.rs`'s own real
1745    /// gap needed both a `readonly` modifier (every structural type this
1746    /// file builds is `readonly`-qualified, e.g. `{ readonly cron: string;
1747    /// readonly scheduledTime: number }`) and a method-signature member
1748    /// (`ack(): void`, `retry(): void`, `waitUntil(promise: Promise<
1749    /// unknown>): void`), neither representable by one more anonymous
1750    /// tuple `bool` — a fourth positional `bool` next to `optional` would
1751    /// be genuinely ambiguous at call sites (`readonly` vs. `optional`, ordered
1752    /// how?), the same reasoning [`TsObjectEntry`] already used over a raw
1753    /// tuple for the value-position sibling.
1754    Object(Vec<TsTypeMember>),
1755    /// `(a0: T0, a1: T1, …) => Ret` — a function type. Parameters carry no
1756    /// name of their own (just their type); the printer numbers them
1757    /// positionally (`a0`, `a1`, …), matching the exact convention
1758    /// `bynk-emit`'s own pre-P7.9 `ts_type_ref_with`/`ts_ty` already used
1759    /// (TypeScript requires *some* name in function-type syntax, and
1760    /// nothing about a `TypeRef::Fn`/`Ty::Fn` parameter carries a real one
1761    /// to use instead). A zero-`params` `Fn` is the query-thunk wrapper
1762    /// shape, `() => Ret`. Inside a [`TsType::Array`] the printer
1763    /// parenthesises it (`readonly ((a0: T) => U)[]`); see that variant's
1764    /// own doc.
1765    Fn {
1766        params: Vec<TsType>,
1767        ret: Box<TsType>,
1768    },
1769    /// `A | B | C` — a type-position union. Added in review of #1315:
1770    /// `bynk-emit`'s `ts_ty` builds a real union type for a resolved
1771    /// multi-actor sum (`Ty::ActorSum`, discriminated-union members tagged
1772    /// by actor name), a shape none of `Named`/`Array`/`Object`/`Fn` can
1773    /// represent — a real, grounded gap the same way `readonly`/`Fn`
1774    /// themselves were (P7.9's own accepted proposal), not a speculative
1775    /// addition. Each member prints through the ordinary `render_type`
1776    /// recursion; a member that is itself a `Union` is legal to construct
1777    /// but nothing in `bynk-emit` builds one today. Inside a
1778    /// [`TsType::Array`] the printer parenthesises it (`(A | B)[]`); see that
1779    /// variant's own doc.
1780    ///
1781    /// `multiline` (#1339): `false` (via [`TsType::union`]) is this variant's
1782    /// original single-line `A | B | C` form, unchanged. `true` (via
1783    /// [`TsType::multiline_union`]) is `emit_sum_type`'s own real
1784    /// discriminated-union shape — one variant per line, a leading `|` on
1785    /// every line *except* the first (which gets equivalent spacing
1786    /// instead), the closing `;` appended directly to the last variant's own
1787    /// line. Mirrors [`TsExpr::Object`]'s own `multiline` field precedent
1788    /// (#1317), but — unlike that one — needs no depth-aware wrapper: the
1789    /// pre-conversion `writeln!` code this reproduces always used a fixed
1790    /// two-space indent regardless of nesting (this shape is only ever a
1791    /// top-level `export type` alias body, never nested inside another
1792    /// type), so `render_type` stays entirely depth-unaware here too. Only
1793    /// rendered correctly from [`TsDecl::TypeAlias`]'s own top-level
1794    /// render — not reachable, and not given a defined rendering, from any
1795    /// nested position (an array element, a type argument, …), the same
1796    /// named boundary [`TsExpr::Object`]'s own `multiline` doc already
1797    /// draws for its own nested case.
1798    Union {
1799        members: Vec<TsType>,
1800        multiline: bool,
1801    },
1802    /// `A & B` — a type-position intersection. #1339's own real gap:
1803    /// `emit_refined_type`'s own branded-type alias,
1804    /// `{base} & { readonly __brand: "..." }`, has no representation among
1805    /// `Named`/`Array`/`Object`/`Fn`/`Union` — mirrors `Union`'s own
1806    /// shape/precedent exactly (a flat `Vec`, each member printed through
1807    /// the ordinary `render_type` recursion, joined by ` & `), single-line
1808    /// only (nothing in `bynk-emit` builds a multi-line intersection).
1809    Intersection(Vec<TsType>),
1810}
1811
1812impl TsType {
1813    /// A plain named type with no type arguments — `string`, `unknown`,
1814    /// `Request`, …
1815    pub fn named(name: impl Into<String>) -> Self {
1816        TsType::Named {
1817            name: name.into(),
1818            type_args: Vec::new(),
1819        }
1820    }
1821
1822    /// A generic named type — `Record<K, V>`, `Promise<T>`, …
1823    pub fn named_with_args(name: impl Into<String>, type_args: Vec<TsType>) -> Self {
1824        TsType::Named {
1825            name: name.into(),
1826            type_args,
1827        }
1828    }
1829
1830    /// `T[]` — the non-`readonly` array shape.
1831    pub fn array(element: TsType) -> Self {
1832        TsType::Array {
1833            element: Box::new(element),
1834            readonly: false,
1835        }
1836    }
1837
1838    /// `readonly T[]`.
1839    pub fn readonly_array(element: TsType) -> Self {
1840        TsType::Array {
1841            element: Box::new(element),
1842            readonly: true,
1843        }
1844    }
1845
1846    /// `A | B | C` — the original single-line union form, unchanged.
1847    pub fn union(members: Vec<TsType>) -> Self {
1848        TsType::Union {
1849            members,
1850            multiline: false,
1851        }
1852    }
1853
1854    /// `emit_sum_type`'s own real multi-line discriminated-union shape — see
1855    /// [`TsType::Union`]'s own doc for the exact rendering rules and why
1856    /// this needs no depth parameter.
1857    pub fn multiline_union(members: Vec<TsType>) -> Self {
1858        TsType::Union {
1859            members,
1860            multiline: true,
1861        }
1862    }
1863
1864    /// `A & B` — an intersection type.
1865    pub fn intersection(members: Vec<TsType>) -> Self {
1866        TsType::Intersection(members)
1867    }
1868}
1869
1870/// One member of a [`TsType::Object`] structural type — a property (`Prop`)
1871/// or a method signature (`Method`, #1323's own real gap: `ack(): void`,
1872/// `retry(): void`, `waitUntil(promise: Promise<unknown>): void` — no body,
1873/// a type-position sibling to [`TsObjectEntry::Method`], which does carry
1874/// one). `Method`'s own parameters reuse [`TsParam`] directly (not a bare
1875/// `Vec<TsType>` the way [`TsType::Fn`]'s anonymous, positionally-numbered
1876/// parameters do) — `waitUntil`'s own real parameter has a real name
1877/// (`promise`) the printed text must show, unlike `Fn`'s callers, none of
1878/// which have one to show.
1879#[derive(Debug, Clone)]
1880pub enum TsTypeMember {
1881    Prop {
1882        name: String,
1883        ty: TsType,
1884        optional: bool,
1885        readonly: bool,
1886    },
1887    /// `generics`/`doc` added by #1357: `emit_capability`'s own interface
1888    /// methods are genuinely generic (`op<T>(...): ret;`, no
1889    /// monomorphisation) and doc-commented — bare generic names, matching
1890    /// every other real generics-list precedent in this crate; `doc`
1891    /// mirrors `TsObjectEntry::Method.doc`'s own identical field (#1337).
1892    /// Both default empty/`None` via [`TsTypeMember::method`]'s own
1893    /// existing constructor — every one of its 6 real pre-#1357 callers is
1894    /// unaffected.
1895    ///
1896    /// `doc` renders from exactly one of this variant's two reachable
1897    /// positions: `TsDecl::Interface`'s own render arm (a real, multi-line
1898    /// declaration body with `depth` available, so it calls
1899    /// `render_doc_comment` before a documented member's own line — the
1900    /// only place `doc` is honoured). A `Method` reached through
1901    /// `TsType::Object`'s own inline, single-line shape (a type-position
1902    /// object literal, e.g. `{ a: X; b(): Y }`) has no line budget for a
1903    /// JSDoc block at all — `doc: Some(_)` there is a real, `debug_assert`-
1904    /// guarded misuse (`render_type`'s own `TsType::Object` arm), the same
1905    /// "loud, not silently dropped" precedent review of #1338 already
1906    /// established for `render_object_entry_inline`'s identical
1907    /// `TsObjectEntry::Method.doc` case.
1908    Method {
1909        name: String,
1910        generics: Vec<String>,
1911        params: Vec<TsParam>,
1912        ret: TsType,
1913        doc: Option<String>,
1914    },
1915    /// `[key_name: key_ty]: value_ty` — a TypeScript index signature.
1916    /// #1323's own real gap: `workers_entry.rs`'s multi-param `on call`
1917    /// dispatch casts its raw JSON args object through `{ [k: string]:
1918    /// JsonValue }` before indexing it by field name — textually distinct
1919    /// from the semantically-equivalent `Record<string, JsonValue>` (a
1920    /// `Named` type with type arguments), which `bynk-emit`'s own
1921    /// pre-conversion `writeln!` never wrote here. The one real site.
1922    Index {
1923        key_name: String,
1924        key_ty: TsType,
1925        value_ty: TsType,
1926    },
1927}
1928
1929impl TsTypeMember {
1930    /// `name: ty` — the ordinary, non-`optional`, non-`readonly` case.
1931    pub fn prop(name: impl Into<String>, ty: TsType) -> Self {
1932        TsTypeMember::Prop {
1933            name: name.into(),
1934            ty,
1935            optional: false,
1936            readonly: false,
1937        }
1938    }
1939
1940    /// `name?: ty`.
1941    pub fn optional_prop(name: impl Into<String>, ty: TsType) -> Self {
1942        TsTypeMember::Prop {
1943            name: name.into(),
1944            ty,
1945            optional: true,
1946            readonly: false,
1947        }
1948    }
1949
1950    /// `readonly name: ty`.
1951    pub fn readonly_prop(name: impl Into<String>, ty: TsType) -> Self {
1952        TsTypeMember::Prop {
1953            name: name.into(),
1954            ty,
1955            optional: false,
1956            readonly: true,
1957        }
1958    }
1959
1960    /// `name(params): ret` — no body, no generics, no doc comment.
1961    pub fn method(name: impl Into<String>, params: Vec<TsParam>, ret: TsType) -> Self {
1962        TsTypeMember::Method {
1963            name: name.into(),
1964            generics: Vec::new(),
1965            params,
1966            ret,
1967            doc: None,
1968        }
1969    }
1970
1971    /// `[key_name: key_ty]: value_ty`.
1972    pub fn index(key_name: impl Into<String>, key_ty: TsType, value_ty: TsType) -> Self {
1973        TsTypeMember::Index {
1974            key_name: key_name.into(),
1975            key_ty,
1976            value_ty,
1977        }
1978    }
1979}
1980
1981/// One function/method/constructor parameter.
1982#[derive(Debug, Clone)]
1983pub struct TsParam {
1984    pub name: String,
1985    pub ty: Option<TsType>,
1986    /// `name?: ty` — `events_fanout.rs`'s own `env?: unknown` constructor
1987    /// parameter needs this; nothing in the grounding file needs a default
1988    /// value, so only optionality is represented, not defaults.
1989    pub optional: bool,
1990}
1991
1992/// One `class` field.
1993#[derive(Debug, Clone)]
1994pub struct TsClassField {
1995    pub name: String,
1996    pub ty: TsType,
1997    /// `private` — the one visibility modifier `events_fanout.rs` uses
1998    /// (`private env: ...`). Not a decorator or a constructor parameter
1999    /// property (R7.1 forbids both categorically — there is no variant
2000    /// shape here that could construct either).
2001    pub private: bool,
2002}
2003
2004/// A class's own constructor.
2005#[derive(Debug, Clone)]
2006pub struct TsClassCtor {
2007    pub params: Vec<TsParam>,
2008    pub body: Vec<TsStmt>,
2009}
2010
2011/// One class method.
2012#[derive(Debug, Clone)]
2013pub struct TsClassMethod {
2014    pub name: String,
2015    /// `private {name}(...)`, e.g. `loadState`/`commitState` — the
2016    /// grounding pass's own predicted gap (#1366), closed by Arc C's own
2017    /// `emit_agent` class-scaffold slice: every real `TsClassMethod` site
2018    /// before this one (`emit_provider` #1359's own op methods) was
2019    /// public-only. Rendered before `async`, matching the one real site's
2020    /// own modifier order (`private async loadState()`, not `async private
2021    /// loadState()`).
2022    pub private: bool,
2023    pub is_async: bool,
2024    pub params: Vec<TsParam>,
2025    pub return_type: Option<TsType>,
2026    /// A JSDoc block immediately preceding the method — the grounding
2027    /// pass's own second predicted gap (#1366), closed alongside `private`'s
2028    /// own sibling site: `emit_agent`'s own per-handler methods each carry
2029    /// a doc comment (`emit_doc_block`'s pre-conversion standalone call),
2030    /// the same need `TsObjectEntry::Method.doc` (#1337) and
2031    /// `TsTypeMember::Method.doc` (#1357) already solved for their own node
2032    /// kinds. Rendered the identical way those two already are — a
2033    /// `TsStmtKind::DocComment`-shaped block at the method's own indent,
2034    /// immediately before its header line.
2035    pub doc: Option<String>,
2036    pub body: Vec<TsStmt>,
2037}
2038
2039/// A top-level declaration. `Import`, `Export`, `Interface`, `ConstDecl`,
2040/// and `Class` were `events_fanout.rs`'s own grounding (P7.8's own note:
2041/// "not the sketch's `Function`/`TypeAlias` — unused in the grounding
2042/// file"). #1321 (`workers.rs`) needed both after all — a real gap the
2043/// accepted proposal's own Framing didn't name (its own "no other gap
2044/// surfaced" checked `TsStmt`/`TsExpr`/`TsType` shapes, not `TsDecl` ones):
2045/// `compose.ts`'s own `export function compose(env: Env, …) { … }` is a
2046/// top-level function declaration, and (when the Worker has agents or
2047/// publishes events) a `type DurableObjectNamespace = { … };` fallback
2048/// alias sits alongside it. Named explicitly as a deviation from the
2049/// accepted proposal's own catalogue, not silently added — both are
2050/// mechanical, direct `TsDecl` siblings of `ConstDecl`/`Class` already
2051/// here, the same "small, grounded, same class of gap" this track's own
2052/// history repeatedly found and closed (P7.8's `Assign`/`Continue`/
2053/// `TryCatch`, Arc C slice 1's `Comment`).
2054#[derive(Debug, Clone)]
2055pub enum TsDecl {
2056    /// `import { a, b } from "spec";` (`type_only: false`) or
2057    /// `import type { a, b } from "spec";` (`type_only: true`). Only the
2058    /// named-imports form — `events_fanout.rs` never uses a default
2059    /// import, so none is represented. `names` entries are pushed verbatim
2060    /// (`printer.rs`'s own `names.join(", ")`), so a renamed import
2061    /// (`__messagesLocales as __locale_declaredLocales`, `workers.rs`'s own
2062    /// locale-negotiation import) is one opaque `String` entry, not a
2063    /// structured rename — the same "the field is already a raw-text slot"
2064    /// reasoning that also covers a `type`-prefixed single specifier
2065    /// (`"type KVNamespace"`) inside an otherwise non-`type_only` import.
2066    Import {
2067        type_only: bool,
2068        names: Vec<String>,
2069        from: String,
2070    },
2071    /// `import * as alias from "spec";` — a namespace import, structurally
2072    /// different from [`TsDecl::Import`]'s braced named-imports form (no
2073    /// `{ }`, one bound name for the whole module). #1321's own real gap:
2074    /// `workers.rs`'s `compose.ts` imports `handlers.js` and each
2075    /// referenced unit's binding module this way; `events_fanout.rs` never
2076    /// used one. `type_only` (Arc C, step (10), #1392) mirrors
2077    /// [`TsDecl::Import`]'s own identical field, a parallel gap by
2078    /// omission, not deliberate design — `emit_cross_context_namespace_
2079    /// imports`'s own real `import type * as ns from "...";` form (a
2080    /// Workers-mode consumed-context import reaching the callee's types
2081    /// only, #661) had no way to represent the `type` keyword until now.
2082    ImportNamespace {
2083        type_only: bool,
2084        alias: String,
2085        from: String,
2086    },
2087    /// `import name from "spec";` — a default import, structurally
2088    /// different from both [`TsDecl::Import`] (braced named-imports form)
2089    /// and [`TsDecl::ImportNamespace`] (`* as` form) — no braces, no `* as`,
2090    /// one bound local name for the module's own default export. No
2091    /// `type_only` form — nothing in the grounding file default-imports a
2092    /// type. Arc C, slice 37 (#1409, `tests_emit.rs`'s own slice G): a
2093    /// per-participant `import worker_{ns} from "../workers/{dir}/
2094    /// index.js";` (the participant's own Worker entry module's default
2095    /// export) is the first real default import anywhere in `bynk-emit`'s
2096    /// own converted content — every prior import site is either named
2097    /// ([`TsDecl::Import`]) or namespace ([`TsDecl::ImportNamespace`]).
2098    ImportDefault { alias: String, from: String },
2099    /// `export { a, b } from "spec";` — a re-export, structurally distinct
2100    /// from both [`TsDecl::Import`] (which binds locally, carries no
2101    /// `export` keyword) and [`TsDecl::Export`] (which wraps a whole
2102    /// declaration, not a braced name list re-pointed at another module).
2103    /// #1323's own real gap: `workers_entry.rs` re-exports each agent's
2104    /// Durable Object class from `./handlers.js`, and (when the context
2105    /// publishes events) the fan-out DO's class from `./events_fanout.js`.
2106    /// No `type_only` form — nothing in the grounding file re-exports a
2107    /// type-only name.
2108    ReExport { names: Vec<String>, from: String },
2109    /// `export { a, b };` — exports names already bound in this module (an
2110    /// import's, typically), with no `from` clause, which [`TsDecl::ReExport`]
2111    /// always carries. A Workers context re-exports the boundary codecs it
2112    /// imports from a `uses`d commons this way. #1820: a structured variant,
2113    /// not opaque text, so [`TsStmt::exported_names`] reports what the module
2114    /// exports to a barrel that must find the names two modules both export.
2115    ExportNames { names: Vec<String> },
2116    /// `export * from "spec";` — a wildcard re-export, structurally distinct
2117    /// from [`TsDecl::ReExport`] (which always carries a braced name list —
2118    /// an empty `names` there would render the ill-formed `export {  }
2119    /// from "spec";`, not this). Mirrors [`TsDecl::ImportNamespace`]'s own
2120    /// "no braces, no name list, one bound target" shape, on the export
2121    /// side. #1329's own real gap: `emit_commons_barrel`
2122    /// (`bynk-emit/src/project/tests_emit.rs`) builds a multi-file
2123    /// `commons` unit's barrel module as one `export *` line per
2124    /// constituent source file.
2125    ReExportAll { from: String },
2126    /// Marks the wrapped declaration `export`ed — `export class Foo { .. }`
2127    /// is `Export(Box::new(Class { .. }))`. A wrapper, not a per-variant
2128    /// `exported: bool` field, matching the reference sketch's own naming
2129    /// (`TsDecl::Export` is a peer variant, not a modifier on each other
2130    /// one).
2131    Export(Box<TsDecl>),
2132    /// `type_params`/per-member `readonly` (#1339's own real gap):
2133    /// `emit_record_type`'s own `export interface {name}{params} {
2134    /// readonly {field}: {ty}; ... }` — bare generic names (matching
2135    /// `ts_type_params`'s/`TsObjectEntry::Method.generics`'s own
2136    /// convention) and a `readonly` modifier every real field here
2137    /// carries. `members` reuses [`TsTypeMember`] (the exact same shape
2138    /// [`TsType::Object`]'s own structural members already use) rather
2139    /// than a bespoke tuple, since `readonly` is already that type's own
2140    /// field — this interface's real content has never needed
2141    /// `Method`/`Index`, but nothing about reusing the shared type
2142    /// forecloses a future slice that does.
2143    Interface {
2144        name: String,
2145        type_params: Vec<String>,
2146        members: Vec<TsTypeMember>,
2147    },
2148    /// A top-level `const` — distinct from `TsStmtKind::Const` (private —
2149    /// reachable through [`TsStmt::const_stmt`]), the local
2150    /// form.
2151    ConstDecl {
2152        name: String,
2153        ty: Option<TsType>,
2154        init: TsExpr,
2155    },
2156    Class {
2157        name: String,
2158        fields: Vec<TsClassField>,
2159        constructor: Option<TsClassCtor>,
2160        methods: Vec<TsClassMethod>,
2161    },
2162    /// `function name(params): ret { body }` (`is_async: false`) or `async
2163    /// function name(params): ret { body }` (`is_async: true`) — a top-level
2164    /// function declaration. `is_async` added by #1325: `workers.rs`'s own
2165    /// one real site (`compose`) is never async, so the field didn't exist
2166    /// until `emit_test_main`'s own top-level `async function main() {...}`
2167    /// needed it — the exact "extend narrowly, add it when a future slice's
2168    /// own grounding needs it" deferral this variant's own history already
2169    /// named. `generics` added by #1351: `emit_free_fn`'s own v0.20a erased
2170    /// generics (`export function foo<T>(...)`) — bare names only, empty
2171    /// for every non-generic site (`compose`/`main`), matching every other
2172    /// real generics-list precedent in this crate
2173    /// (`TsObjectEntry::Method.generics` #1337, `TsExpr::Arrow.generics`
2174    /// #1339). `inline` added by #1369 (Arc C, slice 20, step (9)'s own
2175    /// second sub-slice): `emit_agent`'s own zero-factory function
2176    /// (`function __zeroOf{Name}State(): {Name}State { return {...}; }`) is
2177    /// a genuinely single-physical-line declaration — braces and body share
2178    /// the header's own line, not `render_block_stmts`'s always-multi-line
2179    /// shape. `false` (every site landed before #1369) renders the ordinary
2180    /// multi-line body; `true` reuses `render_inline_block`'s own compact
2181    /// `{ stmt; stmt; }` shape directly at the declaration's own header
2182    /// line, the same "one more bool, mirroring an existing precedent"
2183    /// scope `TsObjectEntry::Method.inline` (#1337) already set for the
2184    /// identical single-line-vs-multi-line tension at a different node kind.
2185    Function {
2186        name: String,
2187        generics: Vec<String>,
2188        params: Vec<TsParam>,
2189        return_type: Option<TsType>,
2190        body: Vec<TsStmt>,
2191        is_async: bool,
2192        inline: bool,
2193    },
2194    /// `type name = ty;` (non-generic) or `type name<T, U> = ty;` (via
2195    /// `type_params`, #1339's own real gap: `emit_sum_type`'s own generic
2196    /// sum types erase to `export type Foo<T> = ...`) — a top-level type
2197    /// alias. `workers.rs`'s own real site: the `DurableObjectNamespace`
2198    /// local fallback type (emitted only when the Worker has agents or
2199    /// publishes events), never generic — `type_params` is empty there.
2200    TypeAlias {
2201        name: String,
2202        type_params: Vec<String>,
2203        ty: TsType,
2204    },
2205    /// `export default <expr>;` — a default export of an *expression*, not
2206    /// a declaration. #1323's own real gap (`workers_entry.rs`'s own
2207    /// top-level shape, `export default { fetch, scheduled?, queue? }`):
2208    /// [`TsDecl::Export`] wraps a [`TsDecl`] (a `Class`/`ConstDecl`/etc.,
2209    /// something with its own name), which cannot represent exporting a
2210    /// bare, unnamed object-literal expression — so this is its own
2211    /// variant, not `Export(Box::new(ConstDecl { .. }))` with a synthetic
2212    /// name that would print wrong.
2213    ExportDefault(TsExpr),
2214    /// `declare const name: ty;` — an ambient binding with no initialiser,
2215    /// narrowing a global TypeScript otherwise doesn't know about. #1325's
2216    /// own real, narrow site: `emit_test_main`'s own `declare const process:
2217    /// { exit(code: number): never; env: { [k: string]: string | undefined
2218    /// } };`, narrowing Node's `process` global without a `@types/node`
2219    /// dependency. Distinct from [`TsDecl::ConstDecl`] (always has a real
2220    /// `init` expression) — a `declare const` has none, by definition; a
2221    /// `TsDecl::ConstDecl` with a synthetic placeholder `init` would print
2222    /// wrong (`= <something>;` where nothing should appear at all).
2223    DeclareConst { name: String, ty: TsType },
2224}
2225
2226/// Which family of residual, not-yet-converted emission a [`TsStmt::verbatim`]
2227/// statement came from. A closed enum, deliberately — "makes the ratchet a
2228/// compile-time construct, not a grep" (Q2's own settling text). Named
2229/// file-by-file as Arc C actually needs them (`ast_importers`'s own five-file
2230/// floor is the precedent for how this track names residue), not
2231/// pre-populated for the whole ~19-slice Arc C schedule up front.
2232///
2233/// Deliberately **not** `#[non_exhaustive]`: a `match` over every variant —
2234/// in this crate, or in `bynk-emit` once Arc C reads it — must fail to
2235/// compile the moment a new variant is added, forcing every consumer to
2236/// account for it explicitly. A non-exhaustive enum would let a wildcard arm
2237/// silently absorb a new residue family instead, exactly the "grep, not a
2238/// compile-time construct" Q2's own settling text rejected. P7.8's own
2239/// grounding work found that `Contracts`/`Secrets`/`RuntimeUse` were seeded
2240/// against files that turn out not to need `bynk-ts` conversion at all
2241/// (`design/tracks/the-typescript-tree.md` §9) — recorded there, not fixed
2242/// by removing the variants here, since that's separate follow-on work.
2243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2244pub enum VerbatimOrigin {
2245    /// `bynk-emit/src/emitter/contracts.rs`.
2246    Contracts,
2247    /// `bynk-emit/src/emitter/secrets.rs`.
2248    Secrets,
2249    /// `bynk-emit/src/emitter/runtime_use.rs`.
2250    RuntimeUse,
2251    /// P7.6's own transitional wrap (#1309): the whole of a still-`String`-
2252    /// producing `bynk-emit` document (an entry point, `compose.ts`, the
2253    /// runtime module, an adapter binding, a test module, …), carried into
2254    /// `Document::Ts` so `Artefacts` never stores a bare `String` for TS
2255    /// output (R7.8) even before Arc C converts the function that built it.
2256    /// Deliberately **not** file-specific like the three variants above —
2257    /// this is Arc B's own infrastructure slice, not an Arc C conversion, so
2258    /// it covers everything Arc C hasn't reached yet, one call site per
2259    /// document `bynk-emit/src/project.rs`/`project/tests_emit.rs`
2260    /// constructs (not funnelled through a shared helper: a shared wrap
2261    /// point would collapse every one of those call sites to a single
2262    /// textual `TsStmt::verbatim(` occurrence, defeating `verbatim_sites`'
2263    /// own purpose of counting how much is genuinely still unconverted).
2264    /// Retires site by site as Arc C converts each underlying emitter
2265    /// function to return a real `TsProgram` directly — at which point that
2266    /// document's own construction site stops calling `TsStmt::verbatim` at
2267    /// all, not by this variant being deleted first.
2268    NotYetConverted,
2269    /// `bynk-emit/src/emitter/emit.rs`'s own residual [`TsExpr::VerbatimExpr`]
2270    /// leaves (#1539) — a generic-typed callee, a printer-unsafe nested `As`
2271    /// chain, a block-bodied ICU IIFE, and a pre-escaped/unescaped predicate
2272    /// message, none reducible to a real node without expanding this crate's
2273    /// own type algebra past what #1539 asks for. Expression-level, not
2274    /// statement-level like the three file-specific variants above (this is
2275    /// the first `VerbatimOrigin` a `TsExpr` carries, not a `TsStmt`), but
2276    /// named the same file-by-file way.
2277    Emit,
2278}
2279
2280#[cfg(test)]
2281mod tests {
2282    use super::*;
2283
2284    #[test]
2285    fn verbatim_carries_its_own_span() {
2286        let span = Span::new(3, 8);
2287        let stmt = TsStmt::verbatim(VerbatimOrigin::RuntimeUse, "x", Some(span));
2288        assert_eq!(stmt.span, Some(span));
2289    }
2290
2291    #[test]
2292    fn a_program_prints_its_statements_in_push_order() {
2293        // Kept as a construction-order check (not a print check — printer.rs
2294        // owns that) since `TsStmtKind` is `pub(crate)` and no longer
2295        // exposes a uniform `text()` accessor once non-`Verbatim` kinds
2296        // exist; span order is what's left to check at this layer.
2297        let mut program = TsProgram::new();
2298        program.push(TsStmt::verbatim(VerbatimOrigin::Contracts, "a", None));
2299        program.push(TsStmt::verbatim(
2300            VerbatimOrigin::Secrets,
2301            "b",
2302            Some(Span::new(0, 1)),
2303        ));
2304        assert_eq!(program.stmts[0].span, None);
2305        assert_eq!(program.stmts[1].span, Some(Span::new(0, 1)));
2306    }
2307
2308    /// Review of #1326, finding 1: `TsExpr::template_lit`'s own
2309    /// `debug_assert_eq!` must actually fire on a malformed tree, not just
2310    /// exist as documentation — proves the guard guards, the same "prove
2311    /// it" discipline this crate applies to every other invariant.
2312    #[test]
2313    #[should_panic(expected = "parts.len() must be exprs.len() + 1")]
2314    fn template_lit_rejects_a_parts_exprs_count_mismatch() {
2315        let _ = TsExpr::template_lit(
2316            vec!["a".to_string()],
2317            vec![TsExpr::Ident("too_many".to_string())],
2318        );
2319    }
2320}