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