Skip to main content

bynk_emit/
emitter.rs

1//! TypeScript emission (spec §7, v0.1 §6, v0.2 §6).
2//!
3//! Walks the typed AST and writes a single TypeScript module.
4//!
5//! v0.2 lowering rules:
6//! - Refined-base types: branded type alias + constructor object with
7//!   `of`/`unsafe` (+ any user-declared methods).
8//! - Record types: TypeScript `interface` + namespace object with methods.
9//! - Sum types: discriminated-union type alias + namespace object with
10//!   variant constructors and methods.
11//! - Field access lowers to property access.
12//! - Method calls lower to `Type.method(receiver, args)` (UFCS).
13//! - `match` lowers to a switch on `.tag`; in tail position it inlines,
14//!   otherwise it becomes an IIFE.
15//! - `is` lowers to a tag check; bindings become `const` declarations
16//!   on the truthy side of `if`/`&&`.
17
18use std::cell::RefCell;
19use std::collections::{HashMap, HashSet};
20use std::fmt::Write as _;
21use std::path::{Path, PathBuf};
22use std::sync::Arc;
23
24// P7.5 (#1307): relocated from `emitter/source_map.rs` to `bynk-ts`,
25// unchanged — `bynk-ts/src/source_map.rs`'s own module doc has the full
26// grounding for why this API stays as-is while `bynk-ts`'s own printer
27// gets a second, simpler way to use the same type.
28use bynk_ts::{SourceMapBuilder, TsType};
29
30use crate::project::{BuildTarget, EmitProjectCtx, ImportExt, UnitKind, UnitTable};
31use bynk_check::builtin_names::map_query;
32use bynk_check::builtin_names::methods::{
33    FOLD_EFF, FOR_EACH, PAR_TRAVERSE, PAR_TRAVERSE_ALL, PAR_TRAVERSE_TRY, RAW, TRAVERSE_ALL,
34    TRAVERSE_TRY,
35};
36use bynk_check::builtin_names::types::*;
37use bynk_check::checker::{CheckedProgram, ExprId, NamedKind, Ty, TyId, TypedCommons, Types};
38use bynk_ir::TypeShape;
39use bynk_ir::{block_uses_emit, walk_block_exprs};
40use bynk_lower::{
41    lower_capability_ops_ir, lower_protocol_ir, lower_service_handler_signature_ir,
42    lower_store_field_shape_ir, lower_type_shape_ir,
43};
44use bynk_syntax::ast::{
45    AgentDecl, BaseType, BinOp, Block, CommonsItem, Expr, ExprKind, FnDecl, FnName, Ident,
46    InterpPart, MatchBody, MessagesDecl, ObservationMatcher, Param, Pattern, PredKind, ServiceDecl,
47    Statement, TypeBody, TypeDecl, TypeRef, expr_children,
48};
49
50pub mod client;
51pub mod contracts;
52pub(crate) mod events_fanout;
53pub mod secrets;
54pub(crate) mod serialisation;
55pub mod toml_doc;
56pub(crate) mod workers;
57pub(crate) mod workers_entry;
58pub mod wrangler;
59
60pub(crate) use events_fanout::emit_events_fanout_do;
61pub(crate) use secrets::emit_secrets_manifest;
62pub use toml_doc::print_toml_document;
63pub(crate) use workers::emit_worker_compose;
64pub(crate) use workers_entry::emit_worker_entry;
65pub(crate) use wrangler::{ClassTombstones, emit_wrangler_toml};
66
67mod lower;
68pub(crate) mod runtime_use;
69pub(crate) use lower::*;
70pub(crate) use runtime_use::RuntimeUse;
71pub(crate) mod emit;
72pub(crate) use bynk_check::icu::{self, *};
73pub(crate) use bynk_check::websocket;
74pub(crate) use emit::*;
75
76const INDENT_STEP: usize = 2;
77
78pub(crate) mod runtime_files;
79/// ADR 0615 (#2426): the runtime as one source per `@bynk/runtime` module. A
80/// BUILD OUTPUT, not a hand-edited file: `bynk-emit/runtime/scripts/bundle.mjs`
81/// writes `runtime/<module>.ts` and this table from the focused modules in
82/// `bynk-emit/runtime/src` (CI's `runtime` job guards against drift). Keeping it
83/// committed means `cargo build` stays Node-free and the emitter stays lockstep
84/// with the runtime it embeds.
85mod runtime_modules;
86pub use runtime_files::{RUNTIME_DIR, emit_all_runtime_modules, rewrite_runtime_imports};
87
88/// Events track, slice 2 (spine #936): the TS type of one buffered/fanned-out
89/// event — a handler's `__events` local, the `__eventsDispatch` deps field
90/// (service, agent, provider, and the Bundle/Workers dispatch closures that
91/// implement it), and the fan-out DO's `FanoutEvent` (`events_fanout.rs`) all
92/// declare this same shape independently, with no shared type today. Routing
93/// every Rust-side site through one constant means the envelope field can
94/// never drift by being added at 8 of 9 of them. The two TypeScript runtime
95/// sources that also declare it (`runtime/src/agent.ts`'s
96/// `dispatchToEventsFanout`, `runtime/src/boundary.ts`'s `deliverEvent`) are
97/// hand-edited files this constant cannot reach — keep them textually
98/// identical to this shape by hand; `cargo test -p bynkc --test it --
99/// events_workers_wiring:: events_envelope_behaviour::` exercises every hop
100/// and would fail on a real mismatch.
101///
102/// #973: `runtime/src/boundary.ts`'s `deserialiseEventEnvelope` is a related
103/// but distinct hand-written piece — it validates the *inner* `envelope`
104/// object's shape at the receiving `/_bynk/event/` route, not this outer
105/// wire wrapper. Keep its field list in sync with the `envelope: { ... }`
106/// portion of this shape by hand; nothing generates either from the other.
107pub(crate) const EVENTS_WIRE_EVENT_TS_TYPE: &str = "{ type: string; payload: unknown; envelope: { eventId: string; publisherId: string; emittedAt: number; schemaVersion: number } }";
108
109/// Emit the contents of `out/tsconfig.json`. The CLI uses `tsc -p` against
110/// this when running `bynkc test`; users can also drive `tsc` against it
111/// directly to produce JS for deployment.
112///
113/// I4 (#1970): a context's generated TypeScript client (`bynk-client.ts`) is
114/// excluded. It is for a foreign Worker's build, where `@bynk/wire-client`
115/// resolves; the Bynk build neither imports it nor has that package.
116/// Excluding a file drops the default `node_modules` exclusion, so it is
117/// restated.
118///
119/// #2148: `module: NodeNext` takes each file's format from the nearest
120/// `package.json`, so the compiler also emits `out/package.json` declaring
121/// `"type": "module"`. `tsc` then checks the output (hand-written adapter
122/// bindings included) under the ES-module rules Node runs it with, and does
123/// not down-level it to CommonJS, whose transform reserves the top-level names
124/// `require` and (from TypeScript 7) `Object`.
125pub fn emit_tsconfig() -> String {
126    TSCONFIG_JSON.to_string()
127}
128
129/// The `bynkc test --coverage` variant (#854): the same config with `sourceMap`
130/// enabled, so `tsc` emits the `.js.map`s the coverage remap consumes (hop 1,
131/// `.js` → emitted `.ts`). Kept coverage-only rather than folded into the
132/// default so a normal `bynkc test` / deployment `tsc` run ships no `.js.map`s.
133/// The runner overwrites the default `out/tsconfig.json` with this before `tsc`.
134pub fn emit_tsconfig_with_source_maps() -> String {
135    TSCONFIG_JSON.replace(
136        "\"outDir\": \"../out-js\",",
137        "\"sourceMap\": true,\n    \"outDir\": \"../out-js\",",
138    )
139}
140
141const TSCONFIG_JSON: &str = r#"{
142  "compilerOptions": {
143    "target": "ES2022",
144    "module": "NodeNext",
145    "moduleResolution": "NodeNext",
146    "strict": true,
147    "noImplicitAny": true,
148    "esModuleInterop": true,
149    "skipLibCheck": true,
150    "resolveJsonModule": true,
151    "isolatedModules": true,
152    "noEmit": false,
153    "outDir": "../out-js",
154    "rootDir": "."
155  },
156  "include": ["**/*.ts"],
157  "exclude": ["node_modules", "**/bynk-client.ts"]
158}
159"#;
160
161/// Compute the runtime import specifier for a module at `from_source`. For a
162/// file at `commerce/payment.ts` the runtime sits two levels up, so this
163/// returns `../runtime.js`; for a top-level file it returns `./runtime.js`.
164pub(crate) fn runtime_import_for(from_source: &Path, ext: ImportExt) -> String {
165    let depth = from_source
166        .parent()
167        .map(|p| {
168            p.components()
169                .filter(|c| matches!(c, std::path::Component::Normal(_)))
170                .count()
171        })
172        .unwrap_or(0);
173    let ext = ext.as_str();
174    if depth == 0 {
175        format!("./runtime.{ext}")
176    } else {
177        let prefix: String = "../".repeat(depth);
178        format!("{prefix}runtime.{ext}")
179    }
180}
181
182/// #1478: appends `stmts` to `out`, printing each at `depth` — the shared
183/// "consume a real-node-returning callee's own output" step. #1479 adds the
184/// `depth` parameter (was hardcoded to 0): `project/tests_emit.rs`'s own
185/// scaffold-body callees (e.g. `emit_ns_destructure`) print nested inside a
186/// generated function body, at that body's own depth — its own remaining
187/// callers, `emit`/`emit_project` converted off this (#1486) to a real
188/// `Vec<TsStmt>`/`TsProgram` collected and printed once via `bynk_ts::print`
189/// instead.
190pub(crate) fn extend_printed_at(out: &mut String, stmts: Vec<bynk_ts::TsStmt>, depth: usize) {
191    for stmt in stmts {
192        out.push_str(&bynk_ts::print_stmt(&stmt, depth));
193    }
194}
195
196/// Emit TypeScript source for the typed commons (single-file mode).
197///
198/// Takes a [`CheckedProgram`] rather than a bare `TypedCommons` (T3.7, R3.10):
199/// the only way to obtain one is [`bynk_check::checker::certify`], so this
200/// function can no longer be called with an unchecked or partially-checked
201/// program by construction.
202pub(crate) fn emit(program: &CheckedProgram) -> String {
203    let commons = program.program();
204    // Emit the body first so the header can decide which runtime helpers to
205    // import from what the body actually referenced (v0.110: the `__bynkBytes*`
206    // helpers are imported only when a `Bytes` value is constructed/compared).
207    // "What it referenced" comes from `dummy_ctx.runtime_use`, which the `Bytes`
208    // lowerings write as they emit — not from scanning `body` for the helper's
209    // name, which a user string literal or doc comment could also contain.
210    // #1486: collects into a real `Vec<TsStmt>` and prints once via
211    // `bynk_ts::print`, rather than `extend_printed`'s flat per-stmt loop —
212    // `emit_type`/`emit_free_fn`/`emit_json_codec_helpers` (this function's
213    // own callees, shared with `emit_project`) dropped their own explicit
214    // inter-declaration blank `TsStmt`s as part of that conversion, relying
215    // on the printer's automatic top-level spacing policy; this single-file
216    // path needs the same policy to stay correctly spaced.
217    let mut body_stmts: Vec<bynk_ts::TsStmt> = Vec::new();
218    body_stmts.extend(write_commons_doc(commons));
219    let dummy_ctx = single_file_ctx();
220    // Types come first (they define interfaces and namespaces).
221    for item in &commons.commons.items {
222        if let CommonsItem::Type(t) = item {
223            let shape = type_shape_for(t, program);
224            body_stmts.extend(emit_type(t, &shape, commons, &dummy_ctx));
225        }
226    }
227    // Free functions afterward.
228    for item in &commons.commons.items {
229        if let CommonsItem::Fn(f) = item
230            && let FnName::Free(_) = &f.name
231        {
232            body_stmts.extend(emit_free_fn(f, commons, false, &dummy_ctx.runtime_use));
233        }
234    }
235    // v0.22b: module-local codec helpers for Json.encode/decode targets.
236    body_stmts
237        .extend(emit_json_codec_helpers(commons, &dummy_ctx, &HashSet::new(), &HashSet::new()).0);
238    let body_program = bynk_ts::TsProgram { stmts: body_stmts };
239    let body = bynk_ts::print(&body_program, "", "", "").text;
240    let mut out = String::new();
241    // v0.153 (ADR 0177): a commons that names `HttpResult` in any signature —
242    // e.g. a free `fn -> HttpResult[T]` using the `?`-Option lift — imports it.
243    // Structural (over the AST), not a body-string scan, so a comment or string
244    // literal mentioning `HttpResult` never triggers a spurious import.
245    let uses_http = file_mentions_http_result(commons);
246    // #1319: same structural-scan pattern as `uses_http` — a commons naming
247    // `QueueResult` in a field/type declaration imports it, independent of
248    // any queue-consumer handler (which this single-file path has no
249    // concept of anyway).
250    let uses_queue = file_mentions_queue_result(commons);
251    write_header_single(
252        &mut out,
253        commons,
254        &dummy_ctx.runtime_use,
255        uses_http,
256        uses_queue,
257    );
258    out.push_str(&body);
259    out
260}
261
262/// `t`'s already-lowered `TypeShape` (`bynk-emit::ir`, P6.6/#1188) — reuses the
263/// canonical `Arc<TypeDecl>` `TypedCommons::types` already holds for `t` rather
264/// than a fresh `Arc::new(t.clone())` per call (Decision B, #1188). `types`
265/// holds an entry for every `CommonsItem::Type` *and* every `CommonsItem::Event`
266/// (`resolver.rs`'s own resolve pass inserts both under the same table, the
267/// event's own synthetic `TypeDecl` — `EventDecl::as_type_decl` — keyed
268/// identically), so this one helper serves both emission loops below with no
269/// special-casing for the event mirror.
270///
271/// Derives `commons` from `program` itself rather than taking it as a
272/// separate parameter (review on #1190): the `TyId`s this returns are minted
273/// from `program.program().ty_intern`, and every caller renders them straight
274/// back through that same `commons.ty_intern` (`emit_record_type`/
275/// `emit_sum_type`'s `ts_ty` calls) — a caller free to pass a `TypedCommons`
276/// from a *different* check run would hit `Types::get`'s cross-table panic
277/// instead of a diagnostic. One parameter makes that invariant
278/// unrepresentable instead of merely true today.
279fn type_shape_for(t: &TypeDecl, program: &CheckedProgram) -> TypeShape {
280    let commons = program.program();
281    let def = commons.types.get(&t.name.name).unwrap_or_else(|| {
282        panic!(
283            "bynk internal error (ADR 0334): type `{}` is not in TypedCommons::types, but the \
284             checker already accepted this declaration",
285            t.name.name
286        )
287    });
288    lower_type_shape_ir(def, program)
289}
290
291/// A no-op project context for single-file emission. Single-file mode never
292/// involves contexts or cross-unit imports, so most fields default to empty.
293fn single_file_ctx() -> EmitProjectCtx {
294    EmitProjectCtx {
295        import_ext: crate::project::ImportExt::Js,
296        contracts: false,
297        source_path: PathBuf::new(),
298        identity_path: String::new(),
299        commons_name: String::new(),
300        file_decl_index: crate::project::FileDeclIndex {
301            types: HashMap::new(),
302            fns: HashMap::new(),
303            methods: HashMap::new(),
304            agents: HashMap::new(),
305            capabilities: HashMap::new(),
306        },
307        imported_from: HashMap::new(),
308        imported_from_kind: HashMap::new(),
309        imported_decl_paths: HashMap::new(),
310        unit_kind: UnitKind::Commons,
311        owning_context: None,
312        exports_for_consumed: HashMap::new(),
313        cross_context: bynk_check::resolver::CrossContextInfo::default(),
314        target: BuildTarget::Bundle,
315        local_agents: HashSet::new(),
316        agent_given_deps: HashMap::new(),
317        extra_import_lines: Vec::new(),
318        agent_method_givens: HashMap::new(),
319        agent_called_contexts: Vec::new(),
320        actors: HashMap::new(),
321        agent_schema_versions: HashMap::new(),
322        event_schema_versions: HashMap::new(),
323        consumed_adapters: HashSet::new(),
324        consumed_types_with_methods: HashSet::new(),
325        history_target_agents: HashSet::new(),
326        asset_served: Default::default(),
327        embed_text: Default::default(),
328        imported_methods: HashMap::new(),
329        runtime_use: Default::default(),
330    }
331}
332
333/// Emit TypeScript source for a single file inside a multi-file project,
334/// including cross-file and cross-commons imports computed from
335/// [`EmitProjectCtx`].
336/// Emit one unit's TypeScript, plus its source map (slice 1, ADR 0103).
337///
338/// `source_text` is the originating `.bynk` file's text and `source_name` its
339/// project-root-relative path; together they let the source-map builder resolve
340/// each recorded span to a `(line, col)` and embed `sourcesContent`. Returns the
341/// generated TS and the serialised source-map v3 JSON (`None` when nothing
342/// mapped — e.g. a unit whose items all came from sibling files).
343pub(crate) fn emit_project(
344    program: &CheckedProgram,
345    ctx: &EmitProjectCtx,
346    source_text: &str,
347    source_name: &str,
348) -> (bynk_ts::TsProgram, Option<String>) {
349    let commons = program.program();
350    // #1486: no more `RefCell<SourceMapBuilder>` — every top-level item's
351    // own checkpoint is `stmt.span` on the first statement its own emitter
352    // returns, and `bynk_ts::print`'s own top-level loop (per #1477's own
353    // extension) records it, and merges any `nested_map` (body-bearing
354    // statements — service/agent handler bodies, `emit_free_fn`'s own body,
355    // …) at its real print-time offset, automatically. This replaces the
356    // old `smb.borrow_mut().record(out.len(), item.span)` + `print_stmt_
357    // and_merge`/`extend_printed_and_merged` dance below with the printer
358    // doing the same work at the one place (R7.4) it always should have.
359    let mut stmts: Vec<bynk_ts::TsStmt> = Vec::new();
360    // Compute which names this file actually references that live elsewhere
361    // (sibling file in the same commons/context, or a used commons / consumed
362    // context).
363    // #1778: `mut` — the implied names settle against the body built below.
364    let mut references = collect_external_references(commons, ctx);
365    // #1778: the body first, so the implied references can settle against
366    // what it spells before the imports and rebrands are built from them.
367    let mut body: Vec<bynk_ts::TsStmt> = Vec::new();
368    body.extend(write_commons_doc(commons));
369    for item in &commons.commons.items {
370        if let CommonsItem::Type(t) = item {
371            let shape = type_shape_for(t, program);
372            let mut item_stmts = emit_type(t, &shape, commons, ctx);
373            if let Some(first) = item_stmts.first_mut() {
374                first.span = Some(t.span);
375            }
376            body.extend(item_stmts);
377        }
378    }
379    // Events track, slice 0 (spine #936): an `event` is checker-visible as
380    // a type (via `EventDecl::as_type_decl`, so exports/consumes/
381    // construction all worked from day one), but nothing emitted its actual
382    // TS declaration — this loop only ever matched `CommonsItem::Type`, so
383    // a subscriber importing an event type across contexts (`from
384    // Events(E)`, or `E` named in a cross-context signature) got a real
385    // `tsc` "has no exported member" error. Reuses the identical synthetic
386    // `TypeDecl` the checker already builds.
387    for item in &commons.commons.items {
388        if let CommonsItem::Event(e) = item {
389            let t = e.as_type_decl();
390            let shape = type_shape_for(&t, program);
391            let mut item_stmts = emit_type(&t, &shape, commons, ctx);
392            if let Some(first) = item_stmts.first_mut() {
393                first.span = Some(t.span);
394            }
395            body.extend(item_stmts);
396        }
397    }
398    // #1883, slice 1b: each asset is an exported constant holding its served
399    // URL, emitted before the functions so no top-level initializer can reach
400    // it before it is defined. It is typed with the structural `Asset` shape,
401    // so it reads the same as any other `Asset` value. A unit that `uses` this one imports it like a
402    // function (`record_name_ref`).
403    for item in &commons.commons.items {
404        if let CommonsItem::Asset(a) = item {
405            let url = ctx
406                .asset_served
407                .get(&bynk_check::assets::asset_key(a))
408                .cloned()
409                .unwrap_or_else(|| {
410                    panic!(
411                        "bynk internal error: asset `{}` reached emission without a served path",
412                        a.name.name
413                    )
414                });
415            body.push(bynk_ts::TsStmt::decl(
416                bynk_ts::TsDecl::Export(Box::new(bynk_ts::TsDecl::ConstDecl {
417                    name: ts_ident(&a.name.name),
418                    ty: Some(asset_ts_type()),
419                    init: bynk_ts::TsExpr::object(vec![(
420                        "url".to_string(),
421                        bynk_ts::TsExpr::Lit(bynk_ts::TsLit::Str(url)),
422                    )]),
423                })),
424                Some(a.span),
425            ));
426        }
427    }
428    // #2387, slice 2: each embed is an exported `string` constant holding its
429    // file's text, emitted before the functions for the same reason as an
430    // asset. A unit that `uses` this one imports it like a function
431    // (`record_name_ref`); the bundler drops one nothing references.
432    for item in &commons.commons.items {
433        if let CommonsItem::Embed(e) = item {
434            let key = crate::project::assets::normalise_asset_path(&e.path);
435            let text = ctx.embed_text.get(&key).cloned().unwrap_or_else(|| {
436                panic!(
437                    "bynk internal error: embed `{}` reached emission without its text",
438                    e.name.name
439                )
440            });
441            body.push(bynk_ts::TsStmt::decl(
442                bynk_ts::TsDecl::Export(Box::new(bynk_ts::TsDecl::ConstDecl {
443                    name: ts_ident(&e.name.name),
444                    ty: Some(TsType::named("string")),
445                    init: bynk_ts::TsExpr::Lit(bynk_ts::TsLit::Str(text)),
446                })),
447                Some(e.span),
448            ));
449        }
450    }
451    for item in &commons.commons.items {
452        if let CommonsItem::Fn(f) = item
453            && let FnName::Free(_) = &f.name
454        {
455            let mut item_stmts = emit_free_fn(f, commons, ctx.contracts, &ctx.runtime_use);
456            if let Some(first) = item_stmts.first_mut() {
457                first.span = Some(f.span);
458            }
459            body.extend(item_stmts);
460        }
461    }
462    // message-bundles slice 2 (#874): every `messages` block in the commons
463    // is emitted together, once, as a single multi-locale bundle — not
464    // per-item like the other behavioural kinds below — so the generated
465    // `render` can dispatch across every declared locale's own table rather
466    // than reading only the `@reference` one (slice 1's scope). Recorded at
467    // the `@reference` block's own span, matching how a single-item emission
468    // records at that item's span elsewhere in this loop.
469    let messages_blocks: Vec<&MessagesDecl> = commons
470        .commons
471        .items
472        .iter()
473        .filter_map(|item| match item {
474            CommonsItem::Messages(m) => Some(m),
475            _ => None,
476        })
477        .collect();
478    if let Some(reference) = messages_blocks
479        .iter()
480        .find(|m| m.annotations.iter().any(|a| a.name.name == "reference"))
481    {
482        let mut item_stmts = emit_messages_bundle(&messages_blocks, reference, &ctx.runtime_use);
483        if let Some(first) = item_stmts.first_mut() {
484            first.span = Some(reference.span);
485        }
486        body.extend(item_stmts);
487    }
488    // v0.5: behavioural items follow the type/fn declarations.
489    for item in &commons.commons.items {
490        match item {
491            CommonsItem::Capability(c) if c.derived.is_some() => {
492                let mut item_stmts = emit_derived_capability(c, ctx);
493                if let Some(first) = item_stmts.first_mut() {
494                    first.span = Some(c.span);
495                }
496                body.extend(item_stmts);
497            }
498            CommonsItem::Capability(c) => {
499                // P6.x (#1193, slice 3 of #1187): `emit_capability` reads
500                // each op's resolved types off `ops`, not `c`'s own raw
501                // `TypeRef`s (Decision B, #1193) — no separate helper, this
502                // is `emit_capability`'s one and only call site.
503                let ops = lower_capability_ops_ir(c, program);
504                let mut item_stmts = emit_capability(c, &ops, commons);
505                if let Some(first) = item_stmts.first_mut() {
506                    first.span = Some(c.span);
507                }
508                body.extend(item_stmts);
509            }
510            CommonsItem::Provider(p) => {
511                if let Some(mut stmt) = emit_provider(p, commons, ctx) {
512                    stmt.span = Some(p.span);
513                    body.push(stmt);
514                }
515            }
516            CommonsItem::Service(s) => {
517                // #1187's slice 5: `emit_service` reads the protocol's own
518                // resolved data (`ProtocolIr`) and each handler's resolved
519                // signature (params/ret/effectful) instead of `s`'s own raw
520                // `ServiceProtocol`/`TypeRef`s — shape readers only, never
521                // a handler body (the full service/handler IR constructors
522                // that lowered bodies were deleted by Slices D1/D2 of
523                // #1542). No separate helper, this is `emit_service`'s one
524                // and only call site.
525                let protocol = lower_protocol_ir(&s.protocol, program);
526                let signatures: Vec<_> = s
527                    .handlers
528                    .iter()
529                    .map(|h| lower_service_handler_signature_ir(h, program))
530                    .collect();
531                let mut stmt = emit_service(s, &protocol, &signatures, commons, ctx);
532                stmt.span = Some(s.span);
533                body.push(stmt);
534            }
535            CommonsItem::Agent(a) => {
536                let state: Vec<_> = a
537                    .store_fields
538                    .iter()
539                    .map(|f| lower_store_field_shape_ir(f, program))
540                    .collect();
541                let mut item_stmts = emit_agent(a, &state, commons, ctx, source_text);
542                if let Some(first) = item_stmts.first_mut() {
543                    first.span = Some(a.span);
544                }
545                body.extend(item_stmts);
546            }
547            _ => {}
548        }
549    }
550    // v0.9.2: per-test registry reset. The test runner calls this before each
551    // test so a fresh test sees clean agent state (finding #10's "fresh per
552    // test" half).
553    let agent_names: Vec<&str> = commons
554        .commons
555        .items
556        .iter()
557        .filter_map(|i| match i {
558            CommonsItem::Agent(a) => Some(a.name.name.as_str()),
559            _ => None,
560        })
561        .collect();
562    if !agent_names.is_empty() {
563        // Arc C, slice 30 (#1392): a real `TsDecl::Function`.
564        let reset_fn = bynk_ts::TsStmt::decl(
565            bynk_ts::TsDecl::Export(Box::new(bynk_ts::TsDecl::Function {
566                name: "__resetAgents".to_string(),
567                generics: Vec::new(),
568                params: Vec::new(),
569                return_type: Some(bynk_ts::TsType::named("void")),
570                body: agent_names
571                    .iter()
572                    .map(|name| {
573                        bynk_ts::TsStmt::expr_stmt(
574                            bynk_ts::TsExpr::Call {
575                                callee: Box::new(bynk_ts::TsExpr::Member {
576                                    object: Box::new(bynk_ts::TsExpr::Ident(agent_registry_name(
577                                        name,
578                                    ))),
579                                    property: "reset".to_string(),
580                                }),
581                                args: Vec::new(),
582                            },
583                            None,
584                        )
585                    })
586                    .collect(),
587                is_async: false,
588                inline: false,
589            })),
590            None,
591        );
592        body.push(reset_fn);
593    }
594    // v0.6: cross-context surface assembly. Emit `__makeSurface` for any
595    // context that declares services — the composition root references it
596    // for every such context, not just those consumed by others. Skipped
597    // in workers mode where each Worker has its own `compose(env)` root.
598    if ctx.unit_kind == UnitKind::Context && matches!(ctx.target, BuildTarget::Bundle) {
599        let has_services = commons
600            .commons
601            .items
602            .iter()
603            .any(|i| matches!(i, CommonsItem::Service(_)));
604        if has_services {
605            body.extend(emit_make_surface(commons, ctx));
606        }
607    }
608    // v0.8: in workers mode, the context module also exports per-type
609    // serialise/deserialise helpers for every type that crosses a
610    // boundary. The commons modules likewise carry helpers for their
611    // own commons-declared boundary types.
612    // v0.96 (ADR 0124): runs on both targets — workers emits service-call +
613    // agent-rehydration boundary helpers; bundle emits only the agent-rehydration
614    // ones (the gate's deserialisers), since in-process calls need no wire codec.
615    let (boundary_stmts, boundary_names, boundary_insts) = emit_boundary_helpers(program, ctx);
616    // #1817: a boundary helper's `import` goes in the module's import block,
617    // not where the helpers are. A top-level use above the helpers (an agent's
618    // wire table names its codecs) otherwise ran before the import bound them
619    // once compiled to CommonJS: `ReferenceError: Cannot access … before
620    // initialization`.
621    let (mut boundary_imports, boundary_stmts): (Vec<_>, Vec<_>) = boundary_stmts
622        .into_iter()
623        .partition(|s| s.is_named_import());
624    body.extend(boundary_stmts);
625    // v0.22b: module-local codec helpers for this file's Json.encode/decode
626    // targets, deduped against the workers boundary helpers above.
627    let (json_stmts, json_names) =
628        emit_json_codec_helpers(commons, ctx, &boundary_names, &boundary_insts);
629    body.extend(json_stmts);
630    // #1829: a helper this module emits for a commons type names the types
631    // that type reaches, and the source may never mention one (an agent
632    // stores a used commons' `Item`, or `Json.encode`s one, and nothing reads
633    // its `note`). Each is implied, so `settle_implied` imports it when a
634    // helper spells it.
635    references.imply_names(boundary_names.iter().chain(&json_names), commons, ctx);
636    // #2027: an instantiation codec spells a commons type by its unbranded
637    // alias (`__CommonsPage`), and a generic one has no codec of its own to
638    // import, so nothing above implies it. Each alias the body spells
639    // implies its type, which `settle_implied` then keeps.
640    let unbranded = unbranded_commons_qual(commons, ctx);
641    if !unbranded.is_empty() {
642        let spelled = body_words(&body);
643        let reached: Vec<String> = unbranded
644            .into_iter()
645            .filter(|(_, alias)| spelled.contains(alias))
646            .map(|(name, _)| name)
647            .collect();
648        references.imply_names(reached.iter(), commons, ctx);
649    }
650    references.settle_implied(&body);
651    let mut project_imports = emit_project_imports(commons, ctx, &references);
652    let mut cross_context_imports = emit_cross_context_namespace_imports(commons, ctx, &body);
653    // #1486: the boundary between `emit_project_imports`'s own last
654    // statement and `emit_cross_context_namespace_imports`'s own first
655    // (when non-empty) must reproduce the pre-#1486 code's own unconditional
656    // rule exactly — no blank, *except* when `references` is non-empty,
657    // which wants exactly one separating "this file's own sibling imports"
658    // from "this file's cross-context namespace imports" — regardless of
659    // either side's real `TsStmtKind`, which the printer's automatic
660    // "no blank between adjacent imports" exemption cannot see through:
661    // `emit_project_imports`'s own last statement is not always a real
662    // import decl (its own `extra_import_lines` tail is pre-formatted `Raw`
663    // text that *reads* as an import but isn't classified as one —
664    // `328_agent_given_workers`'s own real fixture output: a `Raw`
665    // adapter-binding import directly followed by a real `ImportNamespace`,
666    // no blank between them despite neither side of the exemption matching).
667    if !cross_context_imports.is_empty() {
668        if references.is_empty() {
669            // Force the "no blank" default explicitly — the automatic
670            // exemption only reliably suppresses it when *both* sides are a
671            // real import decl, which is not guaranteed here.
672            cross_context_imports[0].no_blank_before = true;
673        } else {
674            // Force exactly one blank regardless of whether the automatic
675            // exemption would otherwise suppress it (both sides real import
676            // decls) or already supply it for free (either side `Raw`) —
677            // `no_blank_before` on both the spacer and the statement right
678            // after it suppresses the automatic policy on both sides,
679            // leaving only the spacer's own single rendered blank line.
680            let mut spacer = bynk_ts::TsStmt::blank(None);
681            spacer.no_blank_before = true;
682            project_imports.push(spacer);
683            cross_context_imports[0].no_blank_before = true;
684        }
685    }
686    stmts.extend(project_imports);
687    stmts.extend(cross_context_imports);
688    if let Some(first) = boundary_imports.first_mut() {
689        first.no_blank_before = true;
690    }
691    stmts.extend(boundary_imports);
692    // For contexts: emit per-context nominal rebrand aliases for each type
693    // imported via `uses` that this file references. The structural shape is
694    // inherited from the original commons type; the brand makes the
695    // rebranded type nominally distinct (v0.4 §6.2).
696    if ctx.unit_kind == UnitKind::Context {
697        stmts.extend(emit_context_rebrands(&references, commons, ctx));
698        stmts.extend(emit_consumed_type_reexports(&references, ctx));
699    }
700    stmts.append(&mut body);
701    // #1476: `ctx.runtime_use` is fully populated now — every producer above has
702    // had its chance to note `bytes()`/`icu()` (`emitter::runtime_use`'s own doc:
703    // this used to key on `out.contains("<helper name>")`, wrong in both
704    // directions — a user string literal/doc comment false-positive, or an
705    // unrelated formatting change silently dropping a *required* import). Build
706    // the header — including its own runtime-import line, `bytes()`/`icu()`
707    // folded in directly rather than spliced in afterward — then prepend it.
708    // #1486: no more `shift_checkpoints` — every item's own checkpoint lives
709    // on `stmt.span`/`stmt.nested_map`, which travel with the statement
710    // itself; prepending the header is now ordinary `Vec` splicing, nothing
711    // to rebase.
712    let mut program_stmts = write_header(commons, ctx);
713    // #1486: `write_header`'s own last statement (the runtime import, when
714    // the file has any commons items) sits directly adjacent to whatever
715    // `stmts` starts with — genuinely import-kind itself whenever
716    // `emit_project_imports` contributed any sibling/cross-unit imports,
717    // which exempts this boundary from the printer's automatic "no blank
718    // between adjacent imports" policy. This boundary always wants exactly
719    // one blank regardless (the pre-#1486 code's own unconditional-when-
720    // items-non-empty blank, replicated exactly), so it's forced the same
721    // way the project/cross-context import boundary above is, rather than
722    // relying on the automatic policy.
723    if !commons.commons.items.is_empty() {
724        let mut spacer = bynk_ts::TsStmt::blank(None);
725        spacer.no_blank_before = true;
726        program_stmts.push(spacer);
727        if let Some(first) = stmts.first_mut() {
728            first.no_blank_before = true;
729        }
730    } else if stmts.is_empty() {
731        // Review of #1486, finding 2: pre-conversion, `write_header`
732        // unconditionally seeded a trailing blank line after the banner
733        // regardless of whether the file had any commons items — the
734        // automatic policy alone reproduces that gap everywhere else (it
735        // fires between the banner and whatever `stmts` starts with), but
736        // not in this fully-degenerate case, where there is no later
737        // statement left for it to fire before. Restores the old trailing
738        // blank line this module's own output would otherwise lose.
739        program_stmts.push(bynk_ts::TsStmt::blank(None));
740    }
741    program_stmts.append(&mut stmts);
742    let program = bynk_ts::TsProgram {
743        stmts: program_stmts,
744    };
745    // The generated `file` name: the source basename with `.bynk` → `.ts`.
746    let generated_file = Path::new(source_name)
747        .file_stem()
748        .map(|s| format!("{}.ts", s.to_string_lossy()))
749        .unwrap_or_else(|| "module.ts".to_string());
750    let printed = bynk_ts::print(&program, source_name, source_text, &generated_file);
751    (program, printed.source_map)
752}
753
754/// v0.79: does this block contain a `~>` send anywhere — including nested
755/// branches, match arms, and lambdas? Gates execution-context threading
756/// (`deps.__exec`) so a context that never sends keeps byte-identical output.
757///
758/// A `~>` send is a [`Statement`] variant, not an [`ExprKind`] one, and a bare
759/// `{ … }` block is only parseable in a handful of positions (an `if`/`else`
760/// body, a `match` arm, a lambda body) — never as an arbitrary sub-expression
761/// — so `Block`/`If`/`Match`/`Lambda` were already the complete reachable set
762/// and the old `_ => false` tail never actually dropped a send. It is
763/// rewritten to recurse over `expr_children`, the total child iterator,
764/// anyway: a `Statement`-only construct like this is exactly the shape that
765/// silently drifts if a later `ExprKind` variant *does* start admitting a
766/// nested block and this list isn't updated to match — see
767/// `block_writes_state`, whose traversal was converted alongside this one for
768/// the same reason. Both now also enumerate `ExprKind` explicitly instead of
769/// ending in a `_` arm, so that drift is a build failure rather than a silent
770/// miss.
771///
772/// A `match` arm's guard is walked too (#1769). A guard is an expression, so
773/// it cannot hold a `~>` itself, but it can hold an `if` whose branch blocks
774/// do (`Some(x) if if x > 0 { … } else { false } => …`, a `then` block that
775/// sends and then yields `true`): the handler is effectful, so the send
776/// type-checks, and missing it emitted
777/// `deps.__exec.waitUntil(…)` against a `deps` that had no `__exec`.
778pub(crate) fn block_uses_send(b: &Block) -> bool {
779    fn stmt(s: &Statement) -> bool {
780        match s {
781            Statement::Send(_) => true,
782            Statement::Let(l) | Statement::EffectLet(l) => expr(&l.value),
783            Statement::Expect(a) => expr(&a.value),
784            Statement::Do(d) => expr(&d.value),
785            Statement::Assign(a) => expr(&a.value),
786            Statement::Advance(a) => expr(&a.amount),
787        }
788    }
789    fn expr(e: &Expr) -> bool {
790        match &e.kind {
791            ExprKind::Block(b) => block_uses_send(b),
792            ExprKind::If {
793                cond,
794                then_block,
795                else_block,
796            } => expr(cond) || block_uses_send(then_block) || block_uses_send(else_block),
797            ExprKind::Match { discriminant, arms } => {
798                expr(discriminant)
799                    || arms.iter().any(|a| {
800                        a.guard.as_ref().is_some_and(expr)
801                            || match &a.body {
802                                MatchBody::Expr(e) => expr(e),
803                                MatchBody::Block(b) => block_uses_send(b),
804                            }
805                    })
806            }
807            // No variant below carries a `Block` *field*, so `expr_children`'s
808            // total descent is complete for it — a block reached through a
809            // child (a braced lambda body, say) comes back as an `Expr` and
810            // re-enters this match at the `Block` arm above. A *new* variant
811            // that holds a `Block` directly must be hand-matched up there
812            // alongside `Block`/`If`/`Match`: appending it here loses the
813            // `Statement::Send` tag (`expr_children` flattens a block to its
814            // statements' values), and with it `deps.__exec` threading for a
815            // context that does send.
816            ExprKind::IntLit { .. }
817            | ExprKind::FloatLit { .. }
818            | ExprKind::DurationLit { .. }
819            | ExprKind::StrLit(_)
820            | ExprKind::InterpStr(_)
821            | ExprKind::BoolLit(_)
822            | ExprKind::Ident(_)
823            | ExprKind::Call { .. }
824            | ExprKind::Lambda(_)
825            | ExprKind::BinOp(..)
826            | ExprKind::UnaryOp(..)
827            | ExprKind::Paren(_)
828            | ExprKind::Ok(_)
829            | ExprKind::Err(_)
830            | ExprKind::Question(_)
831            | ExprKind::RecordConstruction { .. }
832            | ExprKind::FieldAccess { .. }
833            | ExprKind::MethodCall { .. }
834            | ExprKind::Is { .. }
835            | ExprKind::Some(_)
836            | ExprKind::None
837            | ExprKind::UnitLit
838            | ExprKind::RecordSpread { .. }
839            | ExprKind::EffectPure(_)
840            | ExprKind::Expect(_)
841            | ExprKind::Val { .. }
842            | ExprKind::Wire(_)
843            | ExprKind::ListLit(_)
844            | ExprKind::Observation(_)
845            | ExprKind::Faults(_)
846            | ExprKind::Trace { .. } => expr_children(e).into_iter().any(expr),
847        }
848    }
849    b.statements.iter().any(stmt) || expr(&b.tail)
850}
851
852/// P6.48 (design/tracks/the-ir.md §6b): every handler/op body in `table` —
853/// every service handler, every agent handler, every provider op — visited
854/// via [`walk_block_exprs`]. The shared walk `project::unit_table_uses_emit`'s
855/// own `body_uses_emit` and `project::called_cross_context_services` each
856/// hand-rolled a separate copy of, over exactly the same three `UnitTable`
857/// collections in the same order. Lives here (already counted regardless by
858/// [`ast_importers`]) so those two `project.rs` callers need only a `&Expr`
859/// closure parameter, never a `&Block` or `&bynk_syntax::ast::Expr` of their
860/// own.
861pub(crate) fn walk_unit_table_bodies(table: &UnitTable, f: &mut impl FnMut(&Expr)) {
862    for service in table.services.values() {
863        for h in &service.handlers {
864            walk_block_exprs(&h.body, f);
865        }
866    }
867    for agent in table.agents.values() {
868        for h in &agent.handlers {
869            walk_block_exprs(&h.body, f);
870        }
871    }
872    for provider in table.providers.values() {
873        for op in &provider.ops {
874            walk_block_exprs(&op.body, f);
875        }
876    }
877}
878
879/// #1961: the consumed contexts an agent handler body calls a service of,
880/// read from the receivers' syntax (`B.svc(…)` / `Alias.svc(…)`) through
881/// `resolve` — `CrossContextInfo::resolve_prefix`, or its equivalent over the
882/// unit's `consumes`. A handler that constructs an agent hands its own `deps`
883/// to the agent's methods (#1960), so its `deps.env` type must name these, and
884/// the Worker must bind them. Syntactic because a file's `handlers.ts` is
885/// emitted before the unit's other files are checked; it over-counts only a
886/// local that shadows a consumed alias, which costs an unused binding.
887pub(crate) fn agent_cross_context_targets(
888    table: &UnitTable,
889    resolve: impl Fn(&str) -> Option<String>,
890) -> std::collections::BTreeSet<String> {
891    let mut out = std::collections::BTreeSet::new();
892    for agent in table.agents.values() {
893        for h in &agent.handlers {
894            walk_block_exprs(&h.body, &mut |e| {
895                if let ExprKind::MethodCall { receiver, .. } = &e.kind
896                    && let Some(chain) = emit::flatten_emit_ident_chain(receiver)
897                    && let Some(consumed) = resolve(&chain)
898                {
899                    out.insert(consumed);
900                }
901            });
902        }
903    }
904    out
905}
906
907/// P6.32 (design/tracks/the-ir.md §6a): the one built-in wrapper type
908/// [`type_ref_mentions`] is looking for — `file_mentions_json_error`/
909/// `_http_result`/`_connection` used to carry three separately hand-written
910/// copies of the identical recursive walk below, differing in exactly one
911/// line each (which wrapper variant stops the recursion and reports `true`).
912#[derive(Clone, Copy, PartialEq, Eq)]
913enum TypeRefMarker {
914    JsonError,
915    HttpResult,
916    Connection,
917    /// Closes the `ts_any` residual (#1319): `has_queue` (`write_header`'s
918    /// own `QueueResult` import gate) only detected a real `from queue on
919    /// message` consumer handler, never a bare field/type-declaration
920    /// mention — the one gap among the four runtime-owned error types that
921    /// `JsonError`/`HttpResult` already closed via this same marker
922    /// mechanism (`ValidationError` needs no marker at all; it's imported
923    /// unconditionally).
924    QueueResult,
925}
926
927/// The shared recursive walk `file_mentions_json_error`/`_http_result`/
928/// `_connection` each used to hand-roll their own copy of. `marker`'s own
929/// wrapper variant reports `true` immediately without recursing into its
930/// payload (matching each original function's own `=> true` arm exactly —
931/// `matches!(marker, ..) || type_ref_mentions(a, marker)` short-circuits on
932/// the left, so the right side never evaluates when `t` itself is a match);
933/// every other wrapper variant recurses into its inner type(s) exactly as
934/// each original's own "recurse" bucket did.
935fn type_ref_mentions(t: &TypeRef, marker: TypeRefMarker) -> bool {
936    match t {
937        TypeRef::JsonError(_) => marker == TypeRefMarker::JsonError,
938        TypeRef::HttpResult(a, _) => {
939            marker == TypeRefMarker::HttpResult || type_ref_mentions(a, marker)
940        }
941        TypeRef::Connection(a, _) => {
942            marker == TypeRefMarker::Connection || type_ref_mentions(a, marker)
943        }
944        TypeRef::Result(a, b, _) | TypeRef::Map(a, b, _) => {
945            type_ref_mentions(a, marker) || type_ref_mentions(b, marker)
946        }
947        TypeRef::Option(a, _)
948        | TypeRef::Effect(a, _)
949        | TypeRef::Query(a, _)
950        | TypeRef::Stream(a, _)
951        | TypeRef::History(a, _)
952        | TypeRef::List(a, _) => type_ref_mentions(a, marker),
953        TypeRef::Fn(params, ret, _) => {
954            params.iter().any(|p| type_ref_mentions(p, marker)) || type_ref_mentions(ret, marker)
955        }
956        // v0.157 (ADR 0183): recurse into a generic application's arguments.
957        TypeRef::App { args, .. } => args.iter().any(|a| type_ref_mentions(a, marker)),
958        TypeRef::QueueResult(_) => marker == TypeRefMarker::QueueResult,
959        TypeRef::Base(..)
960        | TypeRef::Named(_)
961        | TypeRef::Asset(_)
962        | TypeRef::RequestHeaders(_)
963        | TypeRef::TokenClaims(_)
964        | TypeRef::BoundaryRejection(_)
965        | TypeRef::ValidationError(_)
966        | TypeRef::Unit(_) => false,
967    }
968}
969
970/// The shared outer walk `file_mentions_json_error`/`_http_result` used to
971/// hand-roll two byte-identical copies of (P6.32) — every signature or type
972/// declaration in the file, checked via [`type_ref_mentions`].
973/// `file_mentions_connection` keeps its own distinct outer walk (a
974/// `Connection` can additionally live in a `store` field, which neither
975/// marker below can) rather than being folded in here.
976fn commons_mentions_type(commons: &TypedCommons, marker: TypeRefMarker) -> bool {
977    let in_type_ref = |t: &TypeRef| type_ref_mentions(t, marker);
978    let sig = |params: &[Param], ret: &TypeRef| {
979        params.iter().any(|p| in_type_ref(&p.type_ref)) || in_type_ref(ret)
980    };
981    commons.commons.items.iter().any(|item| match item {
982        CommonsItem::Fn(f) => sig(&f.params, &f.return_type),
983        CommonsItem::Service(s) => s.handlers.iter().any(|h| sig(&h.params, &h.return_type)),
984        CommonsItem::Agent(a) => a.handlers.iter().any(|h| sig(&h.params, &h.return_type)),
985        CommonsItem::Capability(c) => c.ops.iter().any(|op| sig(&op.params, &op.return_type)),
986        CommonsItem::Provider(p) => p.ops.iter().any(|op| sig(&op.params, &op.return_type)),
987        CommonsItem::Type(t) => match &t.body {
988            TypeBody::Record(r) => r.fields.iter().any(|f| in_type_ref(&f.type_ref)),
989            TypeBody::Sum(s) => s
990                .variants
991                .iter()
992                .any(|v| v.payload.iter().any(|p| in_type_ref(&p.type_ref))),
993            TypeBody::Refined { .. } | TypeBody::Opaque { .. } => false,
994        },
995        // An `event` registers into the `types` table and is checked over
996        // the same record-field path as `CommonsItem::Type`'s `Record` arm.
997        CommonsItem::Event(e) => e.body.fields.iter().any(|f| in_type_ref(&f.type_ref)),
998        CommonsItem::Actor(_)
999        | CommonsItem::Messages(_)
1000        | CommonsItem::Asset(_)
1001        | CommonsItem::Embed(_) => false,
1002    })
1003}
1004
1005/// v0.22b: whether any signature or type declaration in this file names
1006/// `JsonError` — drives the conditional `type JsonError` runtime import.
1007fn file_mentions_json_error(commons: &TypedCommons) -> bool {
1008    commons_mentions_type(commons, TypeRefMarker::JsonError)
1009}
1010
1011/// Closes the `ts_any` residual (#1319): true if any signature or type
1012/// declaration in this file names `QueueResult` — a field/sum-payload
1013/// mention, not just a `from queue on message` consumer handler (which
1014/// `has_queue`, `write_header`'s own other `QueueResult` gate, already
1015/// covers). Drives the conditional `QueueResult` runtime import the same
1016/// way `file_mentions_json_error`/`file_mentions_http_result` already do
1017/// for their own types.
1018fn file_mentions_queue_result(commons: &TypedCommons) -> bool {
1019    commons_mentions_type(commons, TypeRefMarker::QueueResult)
1020}
1021
1022/// v0.153 (ADR 0177): true if any signature or type declaration in this file
1023/// names `HttpResult` — a service HTTP handler, or a free `fn` / provider /
1024/// capability whose parameter or return type mentions it (the `?`-Option lift
1025/// makes a bare `fn -> HttpResult[T]` emit `HttpResult.NotFound`). Drives the
1026/// conditional `HttpResult` runtime import in both single-file and project
1027/// headers, so the import can never be missing nor spuriously added.
1028fn file_mentions_http_result(commons: &TypedCommons) -> bool {
1029    commons_mentions_type(commons, TypeRefMarker::HttpResult)
1030}
1031
1032/// v0.102: true if a file's signatures or store fields mention `Connection[F]`,
1033/// so the header imports the runtime `Connection` interface. Covers the held
1034/// sites: capability-operation returns, service/agent handler parameters, and
1035/// `store` field value types (`Map[K, Connection]` / `Cell[Option[Connection]]`).
1036fn file_mentions_connection(commons: &TypedCommons) -> bool {
1037    let in_type_ref = |t: &TypeRef| type_ref_mentions(t, TypeRefMarker::Connection);
1038    let sig = |params: &[Param], ret: &TypeRef| {
1039        params.iter().any(|p| in_type_ref(&p.type_ref)) || in_type_ref(ret)
1040    };
1041    commons.commons.items.iter().any(|item| match item {
1042        CommonsItem::Fn(f) => sig(&f.params, &f.return_type),
1043        CommonsItem::Service(s) => s.handlers.iter().any(|h| sig(&h.params, &h.return_type)),
1044        CommonsItem::Agent(a) => {
1045            a.handlers.iter().any(|h| sig(&h.params, &h.return_type))
1046                || a.store_fields
1047                    .iter()
1048                    .any(|f| f.kind.args.iter().any(in_type_ref))
1049        }
1050        CommonsItem::Capability(c) => c.ops.iter().any(|op| sig(&op.params, &op.return_type)),
1051        CommonsItem::Provider(p) => p.ops.iter().any(|op| sig(&op.params, &op.return_type)),
1052        // A `Connection` is a held resource storable only in a `store` field
1053        // (handled above) — never in a plain record field, so `Type`/`Event`
1054        // need no case here; `Actor`/`Messages` carry no `TypeRef` at all.
1055        CommonsItem::Type(_)
1056        | CommonsItem::Actor(_)
1057        | CommonsItem::Messages(_)
1058        | CommonsItem::Event(_)
1059        | CommonsItem::Asset(_)
1060        | CommonsItem::Embed(_) => false,
1061    })
1062}
1063
1064/// v0.22b: a checker `Ty` rendered back to a `TypeRef` for the codec
1065/// machinery (which is `TypeRef`-driven). `None` for types the codec
1066/// rejects anyway (functions, effects, type variables). `pub(crate)` since
1067/// P6.28 (design/tracks/the-ir.md §6a): `project/tests_emit.rs`'s own drain
1068/// of `RuntimeUse::json_codec_roots` (a sibling module tree, not a
1069/// descendant of `emitter`) needs this same conversion, once, right before
1070/// `collect_codec_closure` — the one remaining consumer still genuinely
1071/// `TypeRef`-driven.
1072pub(crate) fn ty_to_type_ref(t: TyId, tys: &Arc<Types>) -> Option<TypeRef> {
1073    let sp = bynk_syntax::span::Span::new(0, 0);
1074    Some(match &*tys.get(t) {
1075        Ty::Base(b) => TypeRef::Base(*b, sp),
1076        // v0.174 (#592): a generic-record instantiation (`Paginated[User]`,
1077        // `args` non-empty) round-trips as a `TypeRef::App` so the codec closure
1078        // reaches its monomorphised helper; a non-generic named type stays a
1079        // bare `Named`.
1080        Ty::Named { name, args, .. } if !args.is_empty() => TypeRef::App {
1081            name: Ident {
1082                name: name.clone(),
1083                span: sp,
1084            },
1085            args: args
1086                .iter()
1087                .map(|a| ty_to_type_ref(*a, tys))
1088                .collect::<Option<Vec<_>>>()?,
1089            span: sp,
1090        },
1091        Ty::Named { name, .. } => TypeRef::Named(Ident {
1092            name: name.clone(),
1093            span: sp,
1094        }),
1095        Ty::Result(a, b) => TypeRef::Result(
1096            Box::new(ty_to_type_ref(*a, tys)?),
1097            Box::new(ty_to_type_ref(*b, tys)?),
1098            sp,
1099        ),
1100        Ty::Option(a) => TypeRef::Option(Box::new(ty_to_type_ref(*a, tys)?), sp),
1101        Ty::List(a) => TypeRef::List(Box::new(ty_to_type_ref(*a, tys)?), sp),
1102        Ty::Map(k, v) => TypeRef::Map(
1103            Box::new(ty_to_type_ref(*k, tys)?),
1104            Box::new(ty_to_type_ref(*v, tys)?),
1105            sp,
1106        ),
1107        Ty::Unit => TypeRef::Unit(sp),
1108        Ty::ValidationError => TypeRef::ValidationError(sp),
1109        Ty::JsonError => TypeRef::JsonError(sp),
1110        // R4.3: `Ty::Error` has no codec — same as the other non-boundary
1111        // types below, but for the additional reason that a checked program
1112        // should never contain one at a codec-generation site.
1113        Ty::Error
1114        | Ty::Effect(_)
1115        | Ty::Query(_)
1116        | Ty::Stream(_)
1117        | Ty::Connection(_)
1118        | Ty::HttpResult(_)
1119        | Ty::HttpOutcome(_)
1120        | Ty::QueueResult
1121        | Ty::BoundaryRejection
1122        | Ty::Asset
1123        | Ty::RequestHeaders
1124        | Ty::TokenClaims
1125        | Ty::Fn { .. }
1126        | Ty::Var(_)
1127        | Ty::Actor(_)
1128        | Ty::ActorSum(_) => {
1129            return None;
1130        }
1131    })
1132}
1133
1134/// v0.22b: collect the `Json.encode`/`Json.decode[T]` target type-refs in
1135/// this file's bodies — the roots of the module-local codec-helper closure.
1136///
1137/// R8.14 (Arc D, P7.d3), revisiting P6.56's own declined attempt: P6.56 found
1138/// no `Callee`-classification for `Json.encode`/`decode` to read at the time
1139/// ("`commons.callees` carries no entry for this call site at all"). That is
1140/// no longer true — `checker::calls`'s own JSON-static dispatch now inserts
1141/// `Callee::Intrinsic { ns: JSON, op }` for exactly this call shape (the same
1142/// `ctx.lookup(JSON).is_none() && !ctx.input.types.contains_key(JSON)` guard
1143/// against a local shadow that this function's own bare `id.name == JSON`
1144/// match had no way to apply). Reading it here closes two things at once:
1145/// R8.14's own "AST-shaped, not checker-resolved" framing, and a real
1146/// (if narrow) correctness gap the old syntactic match carried — a local
1147/// variable or type named `Json` shadowing the builtin would have been
1148/// misread as a real `Json.encode`/`decode` call; `Callee::Intrinsic`'s own
1149/// presence is exactly the checker's already-verified "no, this really is
1150/// the builtin" answer.
1151fn collect_json_codec_roots(commons: &TypedCommons) -> Vec<TypeRef> {
1152    let tys = commons.tys();
1153    let mut roots: Vec<TypeRef> = Vec::new();
1154    {
1155        let mut visit = |e: &Expr| {
1156            let ExprKind::MethodCall { args, .. } = &e.kind else {
1157                return;
1158            };
1159            let Some(bynk_check::checker::Callee::Intrinsic { ns, op }) =
1160                commons.callees.get(&e.id)
1161            else {
1162                return;
1163            };
1164            if *ns != JSON {
1165                return;
1166            }
1167            match op.as_str() {
1168                "decode" => {
1169                    if let Some(Ty::Result(t, _)) = commons.expr_ty(e.id).as_deref()
1170                        && let Some(tr) = ty_to_type_ref(*t, tys)
1171                    {
1172                        roots.push(tr);
1173                    }
1174                }
1175                "encode" => {
1176                    if let Some(a) = args.first()
1177                        && let Some(t) = commons.expr_types.get(&a.id).map(|te| te.ty)
1178                        && let Some(tr) = ty_to_type_ref(t, tys)
1179                    {
1180                        roots.push(tr);
1181                    }
1182                }
1183                _ => {}
1184            }
1185        };
1186        for item in &commons.commons.items {
1187            match item {
1188                CommonsItem::Fn(f) => walk_block_exprs(&f.body, &mut visit),
1189                CommonsItem::Service(s) => {
1190                    for h in &s.handlers {
1191                        walk_block_exprs(&h.body, &mut visit);
1192                    }
1193                }
1194                CommonsItem::Agent(a) => {
1195                    for h in &a.handlers {
1196                        walk_block_exprs(&h.body, &mut visit);
1197                    }
1198                }
1199                CommonsItem::Provider(p) => {
1200                    for op in &p.ops {
1201                        walk_block_exprs(&op.body, &mut visit);
1202                    }
1203                }
1204                _ => {}
1205            }
1206        }
1207    }
1208    roots
1209}
1210
1211/// v0.22b: module-local serialise/deserialise helpers for the types this
1212/// file's `Json.encode`/`Json.decode[T]` calls reference (ADR 0045). The
1213/// closure machinery is shared with the workers boundary path; `skip_names`
1214/// / `skip_insts` dedupe against helpers that path already emitted into
1215/// this module.
1216/// #1478: returns real [`bynk_ts::TsStmt`]s (was `out: &mut String`) — the
1217/// same `decls_as_stmts[_block]` conversion `emit_boundary_helpers`/
1218/// `emit_consumed_context_helpers` just used. #1829: also returns the type
1219/// names it emitted helpers for, which the module must bring into scope.
1220fn emit_json_codec_helpers(
1221    commons: &TypedCommons,
1222    ctx: &EmitProjectCtx,
1223    skip_names: &HashSet<String>,
1224    skip_insts: &HashSet<String>,
1225) -> (Vec<bynk_ts::TsStmt>, Vec<String>) {
1226    use serialisation::{collect_codec_closure, emit_generic_helpers, emit_helpers_for_owner};
1227    let roots = collect_json_codec_roots(commons);
1228    if roots.is_empty() {
1229        return (Vec::new(), Vec::new());
1230    }
1231    let (names, insts) = collect_codec_closure(&roots, &commons.types);
1232    let names: Vec<String> = names
1233        .into_iter()
1234        .filter(|n| !skip_names.contains(n))
1235        .collect();
1236    let mut stmts = serialisation::decls_as_stmts_block(emit_helpers_for_owner(
1237        &names,
1238        &commons.types,
1239        &ctx.commons_name,
1240        &ctx.runtime_use,
1241    ));
1242    let insts: Vec<serialisation::GenericInst> = insts
1243        .into_iter()
1244        .filter(|i| !skip_insts.contains(&i.ts_name()))
1245        .collect();
1246    if !insts.is_empty() {
1247        stmts.extend(serialisation::decls_as_stmts(emit_generic_helpers(
1248            &insts,
1249            &commons.types,
1250            &ctx.runtime_use,
1251        )));
1252    }
1253    (stmts, names)
1254}
1255
1256/// Emit boundary serialise/deserialise helpers (v0.8 §3.4 / §5.2) for
1257/// every named type declared in this file that flows through a
1258/// cross-context call, plus the specialised generic helpers for any
1259/// Result/Option instantiation used at the boundary. Returns the emitted
1260/// (or locally-bound) helper type names and generic-instantiation names so
1261/// the v0.22b codec emission can dedupe against them.
1262/// #1478: returns real [`bynk_ts::TsStmt`]s (was `out: &mut String`) as a
1263/// new first element of the tuple — every `out`-write here is already
1264/// exclusively through `serialisation::decls_as_stmts[_block]` or a real
1265/// `bynk_ts::print_stmt` call, so each becomes a `stmts.extend`/`stmts.push`
1266/// into the same local `stmts`, threaded through `emit_consumed_context_
1267/// helpers`'s own identical conversion.
1268/// #1678: the type positions an agent call puts on the wire on the `workers`
1269/// target — every handler's parameters and its return type — in agent then
1270/// handler order. Positions with no codec (`agent_wire_passes_through`) carry
1271/// no named types to collect, so they are left out.
1272pub(crate) fn agent_call_boundary_roots(agents: &HashMap<String, AgentDecl>) -> Vec<TypeRef> {
1273    let mut names: Vec<&String> = agents.keys().collect();
1274    names.sort();
1275    let mut roots = Vec::new();
1276    for name in names {
1277        for h in &agents[name].handlers {
1278            for p in &h.params {
1279                roots.push(p.type_ref.clone());
1280            }
1281            roots.push(h.return_type.clone());
1282        }
1283        // #1818: an agent that reads its own key is sent it with each call,
1284        // through the key type's codec.
1285        if crate::emitter::emit::agent_reads_key(&agents[name]) {
1286            roots.push(agents[name].key_type.clone());
1287        }
1288    }
1289    roots
1290        .into_iter()
1291        .filter(|t| !serialisation::agent_wire_passes_through(t))
1292        .collect()
1293}
1294
1295fn emit_boundary_helpers(
1296    program: &CheckedProgram,
1297    ctx: &EmitProjectCtx,
1298) -> (Vec<bynk_ts::TsStmt>, HashSet<String>, HashSet<String>) {
1299    use serialisation::{
1300        collect_boundary_types, collect_generic_instantiations, emit_generic_helpers,
1301        emit_helpers_for_owner,
1302    };
1303    // Review of #1211: `commons` is always `program.program()` — derived
1304    // rather than taken as a second parameter, so the two can never alias
1305    // to different tables. `lower_protocol_ir`/`ty_to_type_ref` below mint
1306    // and resolve `TyId`s against the same `program`, and `Types::get`'s
1307    // own doc comment (`bynk-check/src/checker.rs`) names exactly what a
1308    // mismatched pair would silently do in a release build: resolve an
1309    // in-range foreign id to an unrelated `Ty`, not panic.
1310    let commons = program.program();
1311
1312    // For contexts: walk the local services to discover boundary types.
1313    // For commons: walk every consumer's services that reference us
1314    // (approximated as: emit for every type declared in this file).
1315    //
1316    // Service handler types cross the *cross-Worker call* boundary, which only
1317    // exists on the `workers` target; on `bundle` calls are in-process, so their
1318    // serialise/deserialise helpers are not emitted. The agent **rehydration**
1319    // boundary (ADR 0124), in contrast, exists on both targets, so agent
1320    // store-field types are always collected (below).
1321    let workers = matches!(ctx.target, BuildTarget::Workers);
1322    let services: HashMap<String, ServiceDecl> = if workers {
1323        commons
1324            .commons
1325            .items
1326            .iter()
1327            .filter_map(|i| match i {
1328                CommonsItem::Service(s) => Some((s.name.name.clone(), s.clone())),
1329                _ => None,
1330            })
1331            .collect()
1332    } else {
1333        HashMap::new()
1334    };
1335
1336    // v0.96 (ADR 0124): an agent's `store`-field types are rehydration-boundary
1337    // types — their deserialisers drive the load-time validation gate.
1338    let agents: HashMap<String, AgentDecl> = commons
1339        .commons
1340        .items
1341        .iter()
1342        .filter_map(|i| match i {
1343            CommonsItem::Agent(a) => Some((a.name.name.clone(), a.clone())),
1344            _ => None,
1345        })
1346        .collect();
1347
1348    let locally_declared: HashSet<String> = ctx.file_decl_index.types.keys().cloned().collect();
1349    if ctx.unit_kind == UnitKind::Context {
1350        let mut stmts: Vec<bynk_ts::TsStmt> = Vec::new();
1351        // #1678: on `workers` an agent call crosses a Durable Object `fetch`, so
1352        // every agent handler's parameter and return types are boundary types
1353        // too. On `bundle` the call is in-process and needs no codec.
1354        let agent_call_roots: Vec<TypeRef> = if workers {
1355            agent_call_boundary_roots(&agents)
1356        } else {
1357            Vec::new()
1358        };
1359        let boundary_types_all =
1360            collect_boundary_types(&commons.types, &services, &agents, &agent_call_roots);
1361        // Locally-declared boundary types get full helpers in this module. On
1362        // `bundle` (v0.96, ADR 0124) the commons modules emit no boundary helpers,
1363        // so a cross-commons *agent-state* type's deserialiser — needed by the
1364        // rehydration gate — is emitted here in the context instead of re-exported.
1365        let local_boundary: Vec<String> = boundary_types_all
1366            .iter()
1367            .filter(|n| !workers || locally_declared.contains(*n))
1368            .cloned()
1369            .collect();
1370        stmts.extend(serialisation::decls_as_stmts_block(emit_helpers_for_owner(
1371            &local_boundary,
1372            &commons.types,
1373            ctx.commons_name.as_str(),
1374            &ctx.runtime_use,
1375        )));
1376
1377        // Re-export helpers for commons-owned boundary types so consumers
1378        // can address them through this context's handlers.ts namespace
1379        // (matching the namespace import they already use for cross-
1380        // context types). Grouped by source commons. Workers only — on `bundle`
1381        // the commons emit no helpers, so cross-commons types are emitted
1382        // locally above (v0.96) rather than imported.
1383        let mut by_commons: HashMap<String, Vec<String>> = HashMap::new();
1384        for n in &boundary_types_all {
1385            if !workers || locally_declared.contains(n) {
1386                continue;
1387            }
1388            if let Some(commons_name) = codec_owner(ctx, n) {
1389                by_commons
1390                    .entry(commons_name.clone())
1391                    .or_default()
1392                    .push(n.clone());
1393            }
1394        }
1395        // #1836 review: grouped by the *declaring file*, not the commons. A
1396        // multi-file commons emits each type's codecs in the file declaring it
1397        // (#479), so one import per commons named members a sibling file
1398        // exports (TS2305).
1399        let mut by_path: std::collections::BTreeMap<PathBuf, std::collections::BTreeSet<String>> =
1400            std::collections::BTreeMap::new();
1401        for (commons_name, names) in &by_commons {
1402            for n in names {
1403                by_path
1404                    .entry(commons_decl_path(ctx, commons_name, n))
1405                    .or_default()
1406                    .insert(n.clone());
1407            }
1408        }
1409        for (group_index, (target_path, names)) in by_path.into_iter().enumerate() {
1410            let import_spec = cross_commons_import_specifier_for_path(
1411                &ctx.source_path,
1412                &target_path,
1413                ctx.import_ext,
1414            );
1415            let parts: Vec<String> = names
1416                .iter()
1417                .flat_map(|n| [format!("__serialise_{n}"), format!("__deserialise_{n}")])
1418                .collect();
1419            // v0.9.1: emit both a regular import (so the names are bound
1420            // locally for use inside this file's serialisation helpers) and a
1421            // re-export (so downstream consumers can still reach them
1422            // through this module). A bare `export { ... } from "..."`
1423            // re-export does not create a local binding, which `tsc --strict`
1424            // catches when the body calls one of the helpers directly.
1425            //
1426            // Arc C, slice 30 (#1392): the import is a real `TsDecl::Import`.
1427            // The re-export is a BARE `export { ... };` — already-bound
1428            // local names, no `from` clause — which `TsDecl::ReExport`
1429            // cannot represent (it always carries one). #1820: it was opaque
1430            // `TsStmt::raw`, but a split context's `handlers.ts` must see
1431            // what each file exports (two files can both re-export a
1432            // commons type's codecs), so it is `TsDecl::ExportNames`.
1433            //
1434            // #1817: the module assembler hoists the import into the import
1435            // block; the re-export stays here with the helpers. Every import
1436            // after the first is glued to the one before it there.
1437            let mut import_stmt = bynk_ts::TsStmt::decl(
1438                bynk_ts::TsDecl::Import {
1439                    type_only: false,
1440                    names: parts.clone(),
1441                    from: import_spec,
1442                },
1443                None,
1444            );
1445            import_stmt.no_blank_before = group_index > 0;
1446            stmts.push(import_stmt);
1447            stmts.push(bynk_ts::TsStmt::decl(
1448                bynk_ts::TsDecl::ExportNames {
1449                    names: parts.clone(),
1450                },
1451                None,
1452            ));
1453        }
1454        // #1486: no explicit blank stmt here — the printer's automatic
1455        // top-level spacing policy supplies the single blank before the
1456        // Result_/Option_ helpers below (neither import- nor comment-kind,
1457        // so never exempted from it).
1458
1459        // Specialised Result_/Option_ helpers for the instantiations used —
1460        // in handler signatures or in boundary-type fields (v0.18).
1461        //
1462        // #977: the field walk follows `local_boundary`, not `boundary_types_all`
1463        // — the same narrowing `emit_helpers_for_owner` applies just above, and
1464        // for the same reason. A boundary type this context does not *declare* is
1465        // either commons-owned (its codec, and its own instantiations, come from
1466        // the commons module) or consumed (its codec is regenerated below by
1467        // `emit_consumed_context_helpers`, whose `Qual` map reaches the owner's
1468        // `import type * as <ns>` alias). Walking a foreign type's fields here
1469        // emitted its instantiations *unqualified* — `Option<Region>` for a
1470        // consumed `Region` — and then seeded `emitted_insts` so the qualified
1471        // pass below skipped it, leaving `tsc --strict` with `TS2304: Cannot find
1472        // name 'Region'`. Handler signatures still walk in full: a *local*
1473        // handler naming `Option[ConsumedRegion]` directly is this module's own
1474        // boundary either way.
1475        let insts = collect_generic_instantiations(
1476            &services,
1477            &agents,
1478            &local_boundary,
1479            &commons.types,
1480            &agent_call_roots,
1481        );
1482        stmts.extend(serialisation::decls_as_stmts(
1483            serialisation::emit_generic_helpers_qualified(
1484                &insts,
1485                &commons.types,
1486                &unbranded_commons_qual(commons, ctx),
1487                &ctx.runtime_use,
1488            ),
1489        ));
1490
1491        // #661 (ADR 0199 Decision G discharged): the caller's own view of each
1492        // consumed context's boundary codecs, so a cross-context call reaches
1493        // `deserialise_Result_AuthId_PaymentError` **locally** instead of through
1494        // the callee's module. Workers only — on `bundle` the call is in-process
1495        // and needs no wire codec. Everything the caller already emits (its own
1496        // boundary types, the commons re-exports above, its own generic
1497        // instantiations) is skipped, so only the callee-*owned* types the caller
1498        // lacks a local view of are generated.
1499        let (consumed_names, consumed_insts) = if workers {
1500            let mut emitted_names: HashSet<String> = local_boundary.iter().cloned().collect();
1501            for names in by_commons.values() {
1502                emitted_names.extend(names.iter().cloned());
1503            }
1504            let mut emitted_insts: HashSet<String> = insts.iter().map(|i| i.ts_name()).collect();
1505            // #2036: a consumed context's type on this context's *own* boundary
1506            // (`on call(b: Batch)`) needs its codecs here even when no call to
1507            // that context reaches it: the entry decodes the argument through
1508            // `handlers.__deserialise_Batch`. Neither path above emits it (it is
1509            // not declared here, and no commons owns it), so it roots the
1510            // consumed context's codec closure alongside the called services.
1511            let mut own_roots: HashMap<String, Vec<TypeRef>> = HashMap::new();
1512            for n in &boundary_types_all {
1513                if locally_declared.contains(n)
1514                    || ctx.imported_from_kind.get(n) != Some(&UnitKind::Context)
1515                {
1516                    continue;
1517                }
1518                let Some(owner) = ctx.imported_from.get(n) else {
1519                    continue;
1520                };
1521                if ctx.consumed_adapters.contains(owner) {
1522                    continue;
1523                }
1524                own_roots
1525                    .entry(owner.clone())
1526                    .or_default()
1527                    .push(TypeRef::Named(Ident {
1528                        name: n.clone(),
1529                        span: Default::default(),
1530                    }));
1531            }
1532            let (consumed_stmts, consumed_names, consumed_insts, foreign_names) =
1533                emit_consumed_context_helpers(
1534                    program,
1535                    ctx,
1536                    &own_roots,
1537                    &mut emitted_names,
1538                    &mut emitted_insts,
1539                );
1540            stmts.extend(consumed_stmts);
1541            // #1823: codecs of commons types a call to another context needs,
1542            // beyond those this context's own boundary already imports.
1543            let (imports, imported) =
1544                commons_codec_imports(foreign_names, &emitted_names, false, ctx);
1545            stmts.extend(imports);
1546            // #1836 review: registered, so the Json-codec pass does not emit a
1547            // local copy beside the import (TS2440).
1548            let mut consumed_names = consumed_names;
1549            consumed_names.extend(imported);
1550            (consumed_names, consumed_insts)
1551        } else {
1552            (Vec::new(), Vec::new())
1553        };
1554
1555        let mut ret_names: HashSet<String> = boundary_types_all.into_iter().collect();
1556        ret_names.extend(consumed_names);
1557        let mut ret_insts: HashSet<String> = insts.iter().map(|i| i.ts_name()).collect();
1558        ret_insts.extend(consumed_insts);
1559        (stmts, ret_names, ret_insts)
1560    } else if !workers {
1561        // Commons/adapters have no agents (no rehydration boundary), and on
1562        // `bundle` there is no cross-Worker call boundary either — so emit no
1563        // boundary helpers, matching pre-v0.96 bundle output (the rehydration
1564        // pass that now always runs is for context-declared agents only).
1565        (Vec::new(), HashSet::new(), HashSet::new())
1566    } else {
1567        // Commons/adapters (workers): emit helpers for every type declared in
1568        // this file, plus (v0.18) the generic instantiations their fields use —
1569        // a record like the bynk surface's `Request` carries
1570        // `Option[String]` fields whose serialisers delegate to the
1571        // specialised helpers.
1572        //
1573        // v0.132 (#479): scope to types declared in *this* file, not the whole
1574        // unit. `file_decl_index` is unit-wide (name -> declaring-file path), so a
1575        // multi-file commons must filter by the current file — otherwise a
1576        // non-declaring sibling (e.g. the file holding `fn T.make`, not `type T`)
1577        // emits an orphan `serialise_T`/`deserialise_T` with `T` out of scope, and
1578        // the codec is duplicated across files. Unlike a workers *context* (which
1579        // collapses to one `handlers.ts` under a synthetic source_path, so its
1580        // unit-wide `locally_declared` above is correct), a commons emits per file.
1581        let mut locally: Vec<String> = ctx
1582            .file_decl_index
1583            .types
1584            .iter()
1585            .filter(|(_, path)| path.as_path() == ctx.source_path.as_path())
1586            .map(|(name, _)| name.clone())
1587            .collect();
1588        locally.sort();
1589        let mut stmts: Vec<bynk_ts::TsStmt> = Vec::new();
1590        // #1815: a local type's codec calls the codec of each type its fields
1591        // reach. One owned by another commons (`Repo` in `t.core`, reached from
1592        // `t.model`'s `Run`) is imported from that commons, which exports it.
1593        let roots: Vec<TypeRef> = locally
1594            .iter()
1595            .map(|n| {
1596                TypeRef::Named(Ident {
1597                    name: n.clone(),
1598                    span: Default::default(),
1599                })
1600            })
1601            .collect();
1602        let reached =
1603            collect_boundary_types(&commons.types, &HashMap::new(), &HashMap::new(), &roots);
1604        let declared: HashSet<String> = locally.iter().cloned().collect();
1605        let (imports, imported) = commons_codec_imports(reached, &declared, true, ctx);
1606        stmts.extend(imports);
1607        stmts.extend(serialisation::decls_as_stmts_block(emit_helpers_for_owner(
1608            &locally,
1609            &commons.types,
1610            ctx.commons_name.as_str(),
1611            &ctx.runtime_use,
1612        )));
1613        let insts = collect_generic_instantiations(
1614            &HashMap::new(),
1615            &HashMap::new(),
1616            &locally,
1617            &commons.types,
1618            &[],
1619        );
1620        stmts.extend(serialisation::decls_as_stmts(emit_generic_helpers(
1621            &insts,
1622            &commons.types,
1623            &ctx.runtime_use,
1624        )));
1625        // #1836 review: the imported names count as emitted, so the Json-codec
1626        // pass does not emit a local copy beside the import (TS2440).
1627        let mut names: HashSet<String> = locally.into_iter().collect();
1628        names.extend(imported);
1629        (stmts, names, insts.iter().map(|i| i.ts_name()).collect())
1630    }
1631}
1632
1633/// Whether `name` is a type a consumed *context* declares. An adapter's types
1634/// are recorded as a consumed context's too, but an adapter is linked into each
1635/// consuming Worker, so its module is in reach (P5, #2323: an `R2Object` the
1636/// source never spells is imported like any other implied name).
1637fn is_consumed_context_type(ctx: &EmitProjectCtx, name: &str) -> bool {
1638    ctx.imported_from_kind.get(name) == Some(&UnitKind::Context)
1639        && !ctx
1640            .imported_from
1641            .get(name)
1642            .is_some_and(|owner| ctx.consumed_adapters.contains(owner))
1643}
1644
1645/// The unit whose module exports `name`'s codecs for this one to import: a
1646/// commons it `uses`, or (#1845) an adapter it consumes. An adapter is
1647/// recorded in `imported_from_kind` as a consumed *context*, but unlike one it
1648/// is linked into each consuming Worker, so its module and codecs are in
1649/// reach. A consumed context's types get their codecs from
1650/// `emit_consumed_context_helpers` instead.
1651fn codec_owner<'a>(ctx: &'a EmitProjectCtx, name: &str) -> Option<&'a String> {
1652    let owner = ctx.imported_from.get(name)?;
1653    match ctx.imported_from_kind.get(name) {
1654        Some(UnitKind::Commons) => Some(owner),
1655        // `Adapter` too, should the checker ever record an adapter's types as
1656        // its own kind.
1657        Some(UnitKind::Context | UnitKind::Adapter) if ctx.consumed_adapters.contains(owner) => {
1658            Some(owner)
1659        }
1660        _ => None,
1661    }
1662}
1663
1664/// The file declaring `name` in the commons `commons_name`, as a module
1665/// imports it: the declaring file of a multi-file commons, or the commons'
1666/// single-file path.
1667fn commons_decl_path(ctx: &EmitProjectCtx, commons_name: &str, name: &str) -> PathBuf {
1668    ctx.imported_decl_paths
1669        .get(commons_name)
1670        .and_then(|m| m.get(name).cloned())
1671        .unwrap_or_else(|| EmitProjectCtx::commons_path(commons_name))
1672}
1673
1674/// #1815/#1823: `import { __serialise_X, __deserialise_X } from "<file>"` for
1675/// each type in `names` whose codecs this module calls but neither declares nor
1676/// already imports (`already`): one owned by a commons this module uses, and,
1677/// when `siblings` (a file of a multi-file commons), one declared in another
1678/// file of the same commons. Grouped by declaring file (#1836 review), in
1679/// name order. Returns the imports and the names they bring in. The module
1680/// assembler hoists the imports into the import block (#1817).
1681fn commons_codec_imports(
1682    names: impl IntoIterator<Item = String>,
1683    already: &HashSet<String>,
1684    siblings: bool,
1685    ctx: &EmitProjectCtx,
1686) -> (Vec<bynk_ts::TsStmt>, HashSet<String>) {
1687    let mut by_path: std::collections::BTreeMap<PathBuf, std::collections::BTreeSet<String>> =
1688        std::collections::BTreeMap::new();
1689    for n in names {
1690        if already.contains(&n) {
1691            continue;
1692        }
1693        let path = if let Some(commons_name) = codec_owner(ctx, &n) {
1694            commons_decl_path(ctx, commons_name, &n)
1695        } else if siblings
1696            && let Some(path) = ctx.file_decl_index.types.get(&n)
1697            && path.as_path() != ctx.source_path.as_path()
1698        {
1699            path.clone()
1700        } else {
1701            continue;
1702        };
1703        by_path.entry(path).or_default().insert(n);
1704    }
1705    let mut imported = HashSet::new();
1706    let stmts = by_path
1707        .into_iter()
1708        .map(|(target_path, names)| {
1709            let from = cross_commons_import_specifier_for_path(
1710                &ctx.source_path,
1711                &target_path,
1712                ctx.import_ext,
1713            );
1714            let parts = names
1715                .iter()
1716                .flat_map(|n| [format!("__serialise_{n}"), format!("__deserialise_{n}")])
1717                .collect();
1718            imported.extend(names);
1719            bynk_ts::TsStmt::decl(
1720                bynk_ts::TsDecl::Import {
1721                    type_only: false,
1722                    names: parts,
1723                    from,
1724                },
1725                None,
1726            )
1727        })
1728        .collect();
1729    (stmts, imported)
1730}
1731
1732/// #2027: a whole-name [`serialisation`] `Qual` naming each `uses`-imported
1733/// commons type by its unbranded import, `Line` as `__CommonsLine`, for a
1734/// context's generic-instantiation codecs (`__serialise_List_Line`).
1735///
1736/// The context's own `Line` is rebranded (`emit_context_rebrands`), and so is
1737/// every other context's. One instantiation codec serves both this context's
1738/// values and, through a consumed context's codec (`value: ev_store.Batch`),
1739/// the callee's, whose `Line` carries the callee's brand: named by the local
1740/// rebrand, the codec rejected the callee's value (`TS2345`). The unbranded
1741/// type accepts either, as the commons' own `__serialise_Line` does, and a
1742/// decoded unbranded value is assignable to any context's optionally-branded
1743/// type. Empty outside a context, which has no rebrands. `settle_implied`
1744/// reads a spelled `__CommonsLine` as a use of `Line`, so its import and
1745/// rebrand are emitted.
1746fn unbranded_commons_qual(commons: &TypedCommons, ctx: &EmitProjectCtx) -> HashMap<String, String> {
1747    if ctx.unit_kind != UnitKind::Context || ctx.owning_context.is_none() {
1748        return HashMap::new();
1749    }
1750    commons
1751        .types
1752        .keys()
1753        .filter(|n| {
1754            bynk_check::resolver::compute_is_uses_commons_type(
1755                &ctx.imported_from_kind,
1756                &commons.types,
1757                n,
1758            )
1759        })
1760        .map(|n| (n.clone(), format!("__Commons{n}")))
1761        .collect()
1762}
1763
1764/// #661: emit the caller's own `serialise_*`/`deserialise_*` for every
1765/// callee-owned boundary type reachable from the services this context
1766/// **calls**, so a `workers` cross-context call resolves its codecs locally
1767/// rather than importing the callee's module as a value.
1768///
1769/// The codec function names stay bare and local; only the TS *type* positions
1770/// reach through the callee's `import type * as <ns>` alias (via the `Qual`
1771/// map built here). Refinement validation follows the export visibility: an
1772/// opaque type casts structurally (Decision C), a transparent refined type
1773/// inlines its predicates (Decision D) — both decided inside the codec emitter
1774/// from the type's own body.
1775///
1776/// Only the callee-*owned* types (its `exports`) are generated. Commons types
1777/// reachable through the boundary (`Money`) are already emitted or re-exported
1778/// by the caller's own path, so they are left out here and deduped against
1779/// `emitted_names` / `emitted_insts`, which the caller seeds with everything it
1780/// has already emitted. Returns the names and generic-instantiation names newly
1781/// emitted, so the Json-codec pass dedupes against them too.
1782/// #1478: returns real [`bynk_ts::TsStmt`]s (was `out: &mut String`) as a
1783/// new first element of the tuple — every `out`-write here is already
1784/// exclusively through `serialisation::decls_as_stmts[_block]`, called
1785/// directly since that slice, appended into `stmts`.
1786fn emit_consumed_context_helpers(
1787    program: &CheckedProgram,
1788    ctx: &EmitProjectCtx,
1789    own_roots: &HashMap<String, Vec<bynk_syntax::ast::TypeRef>>,
1790    emitted_names: &mut HashSet<String>,
1791    emitted_insts: &mut HashSet<String>,
1792) -> (Vec<bynk_ts::TsStmt>, Vec<String>, Vec<String>, Vec<String>) {
1793    use serialisation::{
1794        collect_codec_closure, emit_generic_helpers_qualified, emit_helpers_for_owner_qualified,
1795    };
1796    // Review of #1211: derived, not taken as a second parameter — see
1797    // `emit_boundary_helpers`'s own identical note.
1798    let commons = program.program();
1799    let info = &ctx.cross_context;
1800    let mut stmts: Vec<bynk_ts::TsStmt> = Vec::new();
1801    let mut consumed_names_out: Vec<String> = Vec::new();
1802    let mut consumed_insts_out: Vec<String> = Vec::new();
1803    let mut foreign_names_out: Vec<String> = Vec::new();
1804
1805    // Only the services this context actually **calls** — not the callee's whole
1806    // provided surface. `consumed_services` carries every service the dependency
1807    // provides; generating a codec for one this context never reaches would bloat
1808    // the bundle with a contract it does not participate in (and pull in the
1809    // uncalled service's own boundary types). Mirrors the `called` narrowing the
1810    // contract manifest applies to `expects` (ADR 0200 Decision E, one layer up).
1811    let called = called_consumed_services(commons, info);
1812
1813    // #973: this context's own `from Events(E)` subscriptions, keyed by the
1814    // consumed context that declares each `E` — a subscriber calls no method
1815    // on the publisher, so `called_consumed_services` alone would never see
1816    // it, and the `continue` below on an empty `called_here` would skip the
1817    // event's payload type entirely (the root cause of #973: a subscriber's
1818    // generated module had no `deserialise_<Payload>` at all).
1819    // #1187's own closing scoping pass: the event's own identity now reads
1820    // `ProtocolIr::Events`'s already-resolved `event: TyId` (via
1821    // `lower_protocol_ir`) instead of a raw match on `svc.protocol`/
1822    // `event_type`. `collect_codec_closure` below is still `TypeRef`-driven
1823    // (confirmed: no resolved-field-type table exists anywhere in
1824    // `bynk-check` to substitute), so `ty_to_type_ref` converts back at the
1825    // end — the same TyId-in, TypeRef-out shape `lower_json_codec_call`
1826    // already uses for the identical reason. `event`'s own type is always
1827    // non-generic in practice: `context_checks.rs`'s own event/handler
1828    // param-type-agreement check (`type_ref_named`) only ever admits a bare
1829    // `Named` event type, so `ty_to_type_ref`'s generic-instantiation arm
1830    // never actually fires here — not relied on silently, just not the
1831    // shape a certified program can produce.
1832    let mut consumed_event_roots: HashMap<String, Vec<bynk_syntax::ast::TypeRef>> = HashMap::new();
1833    for item in &commons.commons.items {
1834        let CommonsItem::Service(svc) = item else {
1835            continue;
1836        };
1837        let bynk_ir::ProtocolIr::Events { event, .. } =
1838            bynk_lower::lower_protocol_ir(&svc.protocol, program)
1839        else {
1840            continue;
1841        };
1842        // Review of #1211: unlike the raw-AST match this replaced, a
1843        // resolve miss here (`lower_protocol_ir` degrades to `Ty::Unit`
1844        // rather than propagating a failure, `ir/lower.rs`'s own
1845        // `resolve_type_ref`/`unit_ty()` fallback) is indistinguishable
1846        // from a legitimately non-`Named` header — a silent `continue`
1847        // would reproduce #973's own regression shape (a subscriber module
1848        // missing its `deserialise_<Payload>`) with no diagnostic. The
1849        // event/handler param-type-agreement check (`context_checks.rs`'s
1850        // `bynk.event.handler_param_type_mismatch`) makes this
1851        // unreachable on a certified program, but that argument is longer
1852        // than the old syntactic match needed — enforced here, not just
1853        // narrated, so a day this stops holding fails loud in the bless
1854        // suite (which runs debug) instead of quietly shipping a
1855        // subscriber with no deserialiser.
1856        let resolved = commons.tys().get(event);
1857        debug_assert!(
1858            matches!(&*resolved, Ty::Named { .. }),
1859            "bynk internal error: a `from Events(...)` header resolved to a non-named \
1860             type — a certified program cannot produce this (see #973)"
1861        );
1862        let Ty::Named { name, .. } = &*resolved else {
1863            continue;
1864        };
1865        let Some(event_type) = ty_to_type_ref(event, commons.tys()) else {
1866            continue;
1867        };
1868        for (c, names) in &info.consumed_event_names {
1869            if names.contains(name) {
1870                consumed_event_roots
1871                    .entry(c.clone())
1872                    .or_default()
1873                    .push(event_type.clone());
1874            }
1875        }
1876    }
1877
1878    let empty_svcs: HashMap<String, bynk_check::resolver::CrossContextService> = HashMap::new();
1879    let empty_called: HashSet<String> = HashSet::new();
1880    let empty_event_roots: Vec<bynk_syntax::ast::TypeRef> = Vec::new();
1881
1882    let mut consumed_keys: HashSet<&String> = info.consumed_services.keys().collect();
1883    consumed_keys.extend(consumed_event_roots.keys());
1884    consumed_keys.extend(own_roots.keys());
1885    let mut consumed_keys: Vec<&String> = consumed_keys.into_iter().collect();
1886    consumed_keys.sort();
1887    for c in consumed_keys {
1888        let svcs = info.consumed_services.get(c).unwrap_or(&empty_svcs);
1889        let event_roots = consumed_event_roots.get(c).unwrap_or(&empty_event_roots);
1890        // #2036: `c`'s types on this context's own boundary.
1891        let own = own_roots.get(c).unwrap_or(&empty_event_roots);
1892        let called_here = called.get(c).unwrap_or(&empty_called);
1893        if (svcs.is_empty() || called_here.is_empty()) && event_roots.is_empty() && own.is_empty() {
1894            continue;
1895        }
1896        let Some(types_table) = info.consumed_types.get(c) else {
1897            continue;
1898        };
1899        // The callee's exports — the set of types it *owns* and a consumer may
1900        // name. A closure type outside this set (a commons type the callee only
1901        // `uses`, e.g. `Money`) is the caller's own already, not the callee's to
1902        // hand out, so the caller never regenerates it under the callee's ns.
1903        let exports = ctx.exports_for_consumed.get(c);
1904        let owned = |n: &str| exports.is_some_and(|e| e.contains_key(n));
1905        // #1846: a type the callee declares but does not export (`Code`, a
1906        // field of its exported `Ticket`) is the callee's too, though the
1907        // caller can't name it and may declare its own `Code`. It is renamed
1908        // in this copy of the callee's table (`t_vault__Code`), so its codecs
1909        // and instantiations can't collide with the caller's, and generated
1910        // here like an owned type, its TS type still the callee's own.
1911        let (renamed_table, hidden) = rename_unexported_types(
1912            c,
1913            types_table,
1914            &owned,
1915            &commons.types,
1916            &ctx.imported_from_kind,
1917        );
1918        let types_table = &renamed_table;
1919        let hidden_subst = hidden_subst(&hidden);
1920
1921        // Roots: every called service's parameter and return types, plus (#973)
1922        // any event type this context subscribes to from `c` — a subscriber
1923        // participates in the event's contract as its receiving half, so its
1924        // payload is not an uncalled surface the way an unreached method is
1925        // (the narrowing this loop otherwise applies, mirroring ADR 0200
1926        // Decision E one layer up, at `called_consumed_services` above).
1927        let mut svc_names: Vec<&String> =
1928            svcs.keys().filter(|s| called_here.contains(*s)).collect();
1929        svc_names.sort();
1930        let mut roots: Vec<bynk_syntax::ast::TypeRef> = event_roots.clone();
1931        roots.extend(own.iter().cloned());
1932        for sn in svc_names {
1933            let svc = &svcs[sn];
1934            for (_, t) in &svc.params {
1935                roots.push(t.clone());
1936            }
1937            roots.push(svc.return_type.clone());
1938        }
1939        let roots: Vec<bynk_syntax::ast::TypeRef> = roots
1940            .iter()
1941            .map(|r| serialisation::rename_type_ref(r, &hidden_subst))
1942            .collect();
1943        let (names, cinsts) = collect_codec_closure(&roots, types_table);
1944
1945        let ns = format!("{}.", qualified_to_ns(c));
1946        let mut qual: HashMap<String, String> = HashMap::new();
1947        for n in &names {
1948            if owned(n) {
1949                qual.insert(n.clone(), ns.clone());
1950            }
1951        }
1952        // #1846: the whole TS name, `t_vault.Code` (see `serialisation::qual_type`).
1953        // #2073: a type `c` consumes from another context is that context's,
1954        // so its TS name goes through the declaring context (`shop_order.Order`),
1955        // which the caller consumes too (the checker refuses the call
1956        // otherwise). Its codecs keep `c`'s renamed names, which the call sites
1957        // share.
1958        // A key `contract_types_for` qualified by its declaring context
1959        // (`shop_order__Region`, the declaration's name too, #2087) is named in
1960        // TS by the name that context gives it (`shop_order.Region`). The
1961        // declaring context's namespace is reached when this module consumes
1962        // it (imported by `emit_cross_context_namespace_imports`); otherwise
1963        // through `c`'s, whose `handlers.ts` re-exports each consumed type it
1964        // imports (#2044).
1965        let owners = info.consumed_type_owners.get(c);
1966        for (renamed, original) in &hidden {
1967            let owner = owners.and_then(|o| o.get(original));
1968            let owner_ns = owner
1969                .filter(|owner| info.consumed_contexts.contains(*owner))
1970                .map(|owner| format!("{}.", qualified_to_ns(owner)))
1971                .unwrap_or_else(|| ns.clone());
1972            let ts_name = owner
1973                .and_then(|owner| original.strip_prefix(&format!("{}__", qualified_to_ns(owner))))
1974                .unwrap_or(original.as_str());
1975            qual.insert(renamed.clone(), format!("{owner_ns}{ts_name}"));
1976        }
1977        // #1736: a generic type the callee owns reaches the closure only as the
1978        // base of an instantiation (`Envelope[Int]`), never as a plain name, so
1979        // the loop above can't see it. Without this the instantiation's codec
1980        // named it bare (`Envelope<number>`), which the caller doesn't declare:
1981        // `TS2304`, failing every consumer of a service that returns one.
1982        for i in &cinsts {
1983            if let serialisation::GenericInst::RecordInst { name, .. }
1984            | serialisation::GenericInst::SumInst { name, .. } = i
1985                && owned(name)
1986            {
1987                qual.insert(name.clone(), ns.clone());
1988            }
1989        }
1990
1991        // #1823: a closure type the callee does not own is a commons type
1992        // (`Cents`); the caller imports its codec from the commons. Before,
1993        // that was assumed done by the caller's own boundary path, which only
1994        // covers types in the caller's own signatures.
1995        let generated = |n: &str| owned(n) || hidden.contains_key(n);
1996        foreign_names_out.extend(names.iter().filter(|n| !generated(n)).cloned());
1997        let mut to_emit: Vec<String> = names
1998            .iter()
1999            .filter(|n| generated(n) && emitted_names.insert((*n).clone()))
2000            .cloned()
2001            .collect();
2002        to_emit.sort();
2003        stmts.extend(serialisation::decls_as_stmts_block(
2004            emit_helpers_for_owner_qualified(
2005                &to_emit,
2006                types_table,
2007                ctx.commons_name.as_str(),
2008                &qual,
2009                &ctx.runtime_use,
2010            ),
2011        ));
2012        consumed_names_out.extend(to_emit);
2013
2014        let to_emit_insts: Vec<serialisation::GenericInst> = cinsts
2015            .into_iter()
2016            .filter(|i| emitted_insts.insert(i.ts_name()))
2017            .collect();
2018        for i in &to_emit_insts {
2019            consumed_insts_out.push(i.ts_name());
2020        }
2021        // #2027: a commons type argument is named unbranded, as the caller's
2022        // own instantiations name it (`unbranded_commons_qual`); the callee's
2023        // own types keep their namespace.
2024        let mut inst_qual = unbranded_commons_qual(commons, ctx);
2025        inst_qual.extend(qual);
2026        stmts.extend(serialisation::decls_as_stmts(
2027            emit_generic_helpers_qualified(
2028                &to_emit_insts,
2029                types_table,
2030                &inst_qual,
2031                &ctx.runtime_use,
2032            ),
2033        ));
2034    }
2035
2036    (
2037        stmts,
2038        consumed_names_out,
2039        consumed_insts_out,
2040        foreign_names_out,
2041    )
2042}
2043
2044/// #1846: `types_table`, a consumed context `c`'s type table, with each type
2045/// `c` declares but does not export renamed `<ns>__<Name>` (`t_vault__Code`),
2046/// wherever it is declared or named; and those types, renamed to original.
2047/// A type that isn't `c`'s own (a commons type it `uses`, whose codecs the
2048/// caller imports, [`commons_codec_imports`]) keeps its name: it is the
2049/// caller's same declaration, under a codec owner.
2050fn rename_unexported_types(
2051    c: &str,
2052    types_table: &HashMap<String, Arc<TypeDecl>>,
2053    owned: &dyn Fn(&str) -> bool,
2054    caller_types: &HashMap<String, Arc<TypeDecl>>,
2055    imported_from_kind: &HashMap<String, UnitKind>,
2056) -> (
2057    HashMap<String, Arc<TypeDecl>>,
2058    std::collections::BTreeMap<String, String>,
2059) {
2060    let hidden = unexported_type_names(c, types_table, owned, caller_types, imported_from_kind);
2061    if hidden.is_empty() {
2062        return (types_table.clone(), hidden);
2063    }
2064    let subst = hidden_subst(&hidden);
2065    let renamed = types_table
2066        .iter()
2067        .map(|(n, d)| {
2068            let name = subst.get(n).cloned().unwrap_or_else(|| n.clone());
2069            (name, Arc::new(rename_in_decl(d, &subst)))
2070        })
2071        .collect();
2072    (renamed, hidden)
2073}
2074
2075/// #1846, #1932: which of consumed context `c`'s types the consumer renames,
2076/// as `renamed → original`: those it neither owns through `c`'s exports nor
2077/// shares through a commons. The one decision both [`rename_unexported_types`]
2078/// (the codec generator's table) and [`unexported_type_renames`] (the call
2079/// sites' map) take, so the two always agree.
2080fn unexported_type_names(
2081    c: &str,
2082    types_table: &HashMap<String, Arc<TypeDecl>>,
2083    owned: &dyn Fn(&str) -> bool,
2084    caller_types: &HashMap<String, Arc<TypeDecl>>,
2085    imported_from_kind: &HashMap<String, UnitKind>,
2086) -> std::collections::BTreeMap<String, String> {
2087    let shared = |n: &str, d: &Arc<TypeDecl>| {
2088        matches!(imported_from_kind.get(n), Some(UnitKind::Commons))
2089            && caller_types
2090                .get(n)
2091                .is_some_and(|mine| Arc::ptr_eq(mine, d) || mine.span == d.span)
2092    };
2093    let prefix = qualified_to_ns(c);
2094    types_table
2095        .iter()
2096        .filter(|(n, d)| !owned(n) && !shared(n, d))
2097        .map(|(n, _)| (format!("{prefix}__{n}"), n.clone()))
2098        .collect()
2099}
2100
2101/// #1932: the renames `rename_unexported_types` gives consumed context `c`'s
2102/// types, as `original → renamed`, for the call site: a cross-context call
2103/// names the codecs [`emit_consumed_context_helpers`] generates under the
2104/// same names. Computed once per consumed context, where the emitter's
2105/// `CrossContextInfo` is assembled, from the inputs the generator uses.
2106pub(crate) fn unexported_type_renames(
2107    c: &str,
2108    types_table: &HashMap<String, Arc<TypeDecl>>,
2109    exports: Option<&HashMap<String, bynk_syntax::ast::Visibility>>,
2110    caller_types: &HashMap<String, Arc<TypeDecl>>,
2111    imported_from_kind: &HashMap<String, UnitKind>,
2112) -> HashMap<String, String> {
2113    let owned = |n: &str| exports.is_some_and(|e| e.contains_key(n));
2114    hidden_subst(&unexported_type_names(
2115        c,
2116        types_table,
2117        &owned,
2118        caller_types,
2119        imported_from_kind,
2120    ))
2121}
2122
2123/// #1846: `original → renamed`, as [`serialisation::rename_type_ref`] takes it.
2124fn hidden_subst(hidden: &std::collections::BTreeMap<String, String>) -> HashMap<String, String> {
2125    hidden
2126        .iter()
2127        .map(|(renamed, original)| (original.clone(), renamed.clone()))
2128        .collect()
2129}
2130
2131/// #1846: `decl` with every type it names, and its own name, renamed by `subst`.
2132pub(crate) fn rename_in_decl(decl: &TypeDecl, subst: &HashMap<String, String>) -> TypeDecl {
2133    let mut out = decl.clone();
2134    if let Some(to) = subst.get(&decl.name.name) {
2135        out.name.name = to.clone();
2136    }
2137    match &mut out.body {
2138        TypeBody::Record(r) => {
2139            for f in &mut r.fields {
2140                f.type_ref = serialisation::rename_type_ref(&f.type_ref, subst);
2141            }
2142        }
2143        TypeBody::Sum(sum) => {
2144            for v in &mut sum.variants {
2145                for f in &mut v.payload {
2146                    f.type_ref = serialisation::rename_type_ref(&f.type_ref, subst);
2147                }
2148            }
2149            for e in &mut sum.embeds {
2150                e.source_type = serialisation::rename_type_ref(&e.source_type, subst);
2151            }
2152        }
2153        TypeBody::Refined { .. } | TypeBody::Opaque { .. } => {}
2154    }
2155    out
2156}
2157
2158/// #661: the cross-context services this unit actually **calls**, as `consumed
2159/// context → service names`. A copy of `project::called_cross_context_services`
2160/// over the emitter's AST view (`commons`) — the caller-side codec set follows
2161/// the *called* subset, not the callee's full provided surface, so it stays in
2162/// step with what the contract manifest records under `expects`.
2163///
2164/// P6.56 (design/tracks/the-ir.md §6b): reads the checker's own
2165/// already-resolved `Callee::Cross { unit, service }` for each visited call
2166/// site instead of reconstructing cross-context-ness syntactically
2167/// (`flatten_emit_ident_chain` on a `MethodCall` receiver, then
2168/// `info.resolve_prefix`) — the identical resolution `CrossContextInfo::
2169/// resolve_prefix` already performed once, at check time, per call site
2170/// (`project::called_cross_context_services`'s own #1187 conversion, the
2171/// direct precedent this mirrors). Same shadowing-hazard class `block_uses_emit`
2172/// closed for `Events.emit`.
2173fn called_consumed_services(
2174    commons: &TypedCommons,
2175    info: &bynk_check::resolver::CrossContextInfo,
2176) -> HashMap<String, HashSet<String>> {
2177    let mut out: HashMap<String, HashSet<String>> = HashMap::new();
2178    if info.consumed_contexts.is_empty() && info.aliases.is_empty() {
2179        return out;
2180    }
2181    let mut visit = |e: &Expr| {
2182        if let Some(bynk_check::checker::Callee::Cross { unit, service }) =
2183            commons.callees.get(&e.id)
2184        {
2185            out.entry(unit.clone()).or_default().insert(service.clone());
2186        }
2187    };
2188    for item in &commons.commons.items {
2189        match item {
2190            CommonsItem::Service(s) => {
2191                for h in &s.handlers {
2192                    walk_block_exprs(&h.body, &mut visit);
2193                }
2194            }
2195            CommonsItem::Agent(a) => {
2196                for h in &a.handlers {
2197                    walk_block_exprs(&h.body, &mut visit);
2198                }
2199            }
2200            CommonsItem::Provider(p) => {
2201                for op in &p.ops {
2202                    walk_block_exprs(&op.body, &mut visit);
2203                }
2204            }
2205            _ => {}
2206        }
2207    }
2208    out
2209}
2210
2211/// For each type imported via `uses` that's referenced in this file, emit:
2212/// 1. (Done in imports) an aliased import: `import { Money as __CommonsMoney } from ...`
2213/// 2. A rebranded type alias: `export type Money = __CommonsMoney & { readonly __ctxBrand?: "..." }`
2214///
2215/// The brand makes two contexts that both `uses` the same commons see distinct
2216/// nominal `Money` types in their TypeScript output (v0.4 §3.4 / §6.2). It is
2217/// optional (#1704), so an unbranded commons value (a record literal, a nested
2218/// field of another rebranded record, a commons fn's result) is accepted where
2219/// the context's type is expected, while a value carrying another context's
2220/// brand is still rejected.
2221/// #1478: real-node-internally already — returns its real declarations
2222/// (a rebrand type alias, and — for a refined/opaque base — a forwarding
2223/// const, per name) instead of writing into `out: &mut String`.
2224/// #2036: on `workers`, re-export as types the names this `handlers.ts`
2225/// imports from a consumed context. `compose.ts` names every type in a
2226/// wrapper's signature as `handlers.<Name>` (it imports no type names of its
2227/// own), so a consumed `Batch` in a handler's parameters must be a member of
2228/// `handlers` too, the way a `uses`d commons type is through its rebrand
2229/// alias. Type-only (`export { type Batch }`): `compose.ts` uses the name in
2230/// type position only, a consumed refined alias has no value to re-export
2231/// (TS1205 under `isolatedModules`), and a value re-export would deepen the
2232/// caller's dependency on the callee's module. Only names the module imports
2233/// are re-exported, so each is bound here.
2234fn emit_consumed_type_reexports(
2235    refs: &ExternalReferences,
2236    ctx: &EmitProjectCtx,
2237) -> Vec<bynk_ts::TsStmt> {
2238    if ctx.target != BuildTarget::Workers {
2239        return Vec::new();
2240    }
2241    let mut names: Vec<String> = refs
2242        .by_commons
2243        .iter()
2244        .filter(|(unit, _)| !ctx.consumed_adapters.contains(unit.as_str()))
2245        .flat_map(|(_, set)| set.iter())
2246        .filter(|n| ctx.imported_from_kind.get(n.as_str()) == Some(&UnitKind::Context))
2247        .map(|n| format!("type {}", ts_ident(n)))
2248        .collect();
2249    if names.is_empty() {
2250        return Vec::new();
2251    }
2252    names.sort();
2253    names.dedup();
2254    vec![bynk_ts::TsStmt::decl(
2255        bynk_ts::TsDecl::ExportNames { names },
2256        None,
2257    )]
2258}
2259
2260fn emit_context_rebrands(
2261    refs: &ExternalReferences,
2262    commons: &TypedCommons,
2263    ctx: &EmitProjectCtx,
2264) -> Vec<bynk_ts::TsStmt> {
2265    let Some(owning) = &ctx.owning_context else {
2266        return Vec::new();
2267    };
2268    // Collect names imported via `uses` (kind == Commons in imported_from_kind).
2269    // R4.10/R8.2: reads `bynk_check::resolver::compute_is_uses_commons_type`, the same
2270    // predicate `prepare_unit_check_ctx` (`check_pipeline.rs`) computes its own
2271    // `uses_commons_type_names` from — one definition, not two independently
2272    // maintained copies linked only by a doc-comment promise (see that
2273    // function's own doc comment for the real defect this closes, ADR 0226).
2274    let mut names: Vec<String> = Vec::new();
2275    for set in refs.by_commons.values() {
2276        for n in set {
2277            // v0.20b: only *types* get the context rebrand — a
2278            // `uses`-imported function is a value and imports plainly.
2279            if bynk_check::resolver::compute_is_uses_commons_type(
2280                &ctx.imported_from_kind,
2281                &commons.types,
2282                n,
2283            ) {
2284                names.push(n.clone());
2285            }
2286        }
2287    }
2288    names.sort();
2289    names.dedup();
2290    if names.is_empty() {
2291        return Vec::new();
2292    }
2293    let mut stmts = Vec::new();
2294    for name in &names {
2295        // v0.174 (#592): a generic commons type keeps its parameters across the
2296        // rebrand — `Paginated[T]` aliases as `Paginated<T> =
2297        // __CommonsPaginated<T> & { … }`, not a bare `Paginated`, which would
2298        // both drop the parameter and make every `Paginated<User>` reference in
2299        // the context a "type is not generic" error.
2300        let params: Vec<&str> = commons
2301            .types
2302            .get(name)
2303            .map(|d| d.type_params.iter().map(|p| p.name.name.as_str()).collect())
2304            .unwrap_or_default();
2305        let generic_args: Vec<bynk_ts::TsType> =
2306            params.iter().map(|p| bynk_ts::TsType::named(*p)).collect();
2307        // Arc C, slice 30 (#1392): the SAME `TsDecl::TypeAlias` over
2308        // `TsType::Intersection` shape #1339's own `emit_refined_type`
2309        // already established for its sibling `__brand` alias (a real,
2310        // proven precedent, not this slice's own new gap).
2311        let type_alias = bynk_ts::TsStmt::decl(
2312            bynk_ts::TsDecl::Export(Box::new(bynk_ts::TsDecl::TypeAlias {
2313                name: name.clone(),
2314                type_params: params.iter().map(|p| p.to_string()).collect(),
2315                ty: bynk_ts::TsType::intersection(vec![
2316                    bynk_ts::TsType::named_with_args(format!("__Commons{name}"), generic_args),
2317                    // #1704: the brand is *optional*. A plain commons value (a
2318                    // record literal, a nested field, a commons fn's result)
2319                    // has no brand, and it is assignable here; another
2320                    // context's brand (`"a"` against `"b"`) still is not, so
2321                    // the two contexts' types stay nominally distinct.
2322                    bynk_ts::TsType::Object(vec![bynk_ts::TsTypeMember::Prop {
2323                        name: "__ctxBrand".to_string(),
2324                        ty: bynk_ts::TsType::named(format!("\"{owning}\"")),
2325                        optional: true,
2326                        readonly: true,
2327                    }]),
2328                ]),
2329            })),
2330            None,
2331        );
2332        stmts.push(type_alias);
2333        // v0.9.2: a commons refined/opaque type carries a value-side
2334        // constructor (`.of`, and `.unsafe` for opaque). Re-export it under the
2335        // rebranded name so a context calling `ShortCode.of(...)` resolves to a
2336        // value — delegating to the imported commons constructor but reporting
2337        // the context-branded type. (Without this, `ShortCode` is type-only in
2338        // the context and `.of` fails to resolve.)
2339        if let Some(base) = commons
2340            .types
2341            .get(name)
2342            .and_then(|d| refined_or_opaque_base(d))
2343        {
2344            let ts_base_name = ts_base(base);
2345            let is_opaque = matches!(
2346                commons.types.get(name).map(|d| &d.body),
2347                Some(TypeBody::Opaque { .. })
2348            );
2349            // #1704: `return __CommonsX.of(value);` with no cast — the
2350            // commons result is unbranded, which fits the context's
2351            // optionally-branded `Result<X, ValidationError>` directly.
2352            let of_entry = bynk_ts::TsObjectEntry::Method {
2353                name: "of".to_string(),
2354                is_async: false,
2355                generics: Vec::new(),
2356                params: vec![bynk_ts::TsParam {
2357                    name: "value".to_string(),
2358                    ty: Some(bynk_ts::TsType::named(ts_base_name)),
2359                    optional: false,
2360                }],
2361                return_type: Some(bynk_ts::TsType::named_with_args(
2362                    "Result",
2363                    vec![
2364                        bynk_ts::TsType::named(name.clone()),
2365                        bynk_ts::TsType::named("ValidationError"),
2366                    ],
2367                )),
2368                doc: None,
2369                inline: true,
2370                body: vec![bynk_ts::TsStmt::return_stmt(
2371                    Some(bynk_ts::TsExpr::Call {
2372                        callee: Box::new(bynk_ts::TsExpr::Member {
2373                            object: Box::new(bynk_ts::TsExpr::Ident(format!("__Commons{name}"))),
2374                            property: "of".to_string(),
2375                        }),
2376                        args: vec![bynk_ts::TsExpr::Ident("value".to_string())],
2377                    }),
2378                    None,
2379                )],
2380            };
2381            let mut entries = vec![of_entry];
2382            // ADR 0182: only opaque types have a public `.unsafe` to forward.
2383            // A refined/alias type has none — a consuming context brands an
2384            // admitted literal with an inline `as` cast, not a forwarder call.
2385            if is_opaque {
2386                entries.push(bynk_ts::TsObjectEntry::Method {
2387                    name: "unsafe".to_string(),
2388                    is_async: false,
2389                    generics: Vec::new(),
2390                    params: vec![bynk_ts::TsParam {
2391                        name: "value".to_string(),
2392                        ty: Some(bynk_ts::TsType::named(ts_base_name)),
2393                        optional: false,
2394                    }],
2395                    return_type: Some(bynk_ts::TsType::named(name.clone())),
2396                    doc: None,
2397                    inline: true,
2398                    body: vec![bynk_ts::TsStmt::return_stmt(
2399                        Some(bynk_ts::TsExpr::Call {
2400                            callee: Box::new(bynk_ts::TsExpr::Member {
2401                                object: Box::new(bynk_ts::TsExpr::Ident(format!(
2402                                    "__Commons{name}"
2403                                ))),
2404                                property: "unsafe".to_string(),
2405                            }),
2406                            args: vec![bynk_ts::TsExpr::Ident("value".to_string())],
2407                        }),
2408                        None,
2409                    )],
2410                });
2411            }
2412            // v0.132.1 (#481): forward the commons' user-defined attached methods
2413            // (`Cents.fromInt`, …) so the rebranded const carries more than the
2414            // built-in `of`/`unsafe`. Without this a consumer's `Cents.fromInt(n)`
2415            // — which `bynkc check` accepts — fails `tsc`. The methods aren't in
2416            // this context's own `commons` (only imported *types* are merged);
2417            // they arrive via `ctx.imported_methods`, keyed by type name.
2418            if let Some(methods) = ctx.imported_methods.get(name) {
2419                entries.extend(emit_forwarded_methods(name, methods, &commons.ty_intern));
2420            }
2421            let const_decl = bynk_ts::TsStmt::decl(
2422                bynk_ts::TsDecl::Export(Box::new(bynk_ts::TsDecl::ConstDecl {
2423                    name: name.clone(),
2424                    ty: None,
2425                    init: bynk_ts::TsExpr::multiline_object_entries(entries),
2426                })),
2427                None,
2428            );
2429            stmts.push(const_decl);
2430        }
2431    }
2432    // #1486: no explicit trailing blank stmt here — the printer's automatic
2433    // top-level spacing policy supplies the single blank before whatever
2434    // follows.
2435    //
2436    // Every rebrand entry (a lone `type_alias`, or a `type_alias`+`const_decl`
2437    // pair) sits directly under its predecessor with no blank between them —
2438    // confirmed against `129_full_orders_http_api`'s own real fixture output,
2439    // several rebrands stacked with zero blank lines anywhere among them.
2440    // Only the very first statement in this whole function's own returned
2441    // `Vec` keeps the printer's ordinary automatic blank relative to
2442    // whatever precedes `emit_context_rebrands`'s own call site.
2443    for stmt in stmts.iter_mut().skip(1) {
2444        stmt.no_blank_before = true;
2445    }
2446    stmts
2447}
2448
2449/// If a type declaration is a refined or opaque base type, return its base
2450/// (both lower to a branded base with a `.of` / `.unsafe` constructor object).
2451///
2452/// P6.56 (design/tracks/the-ir.md §6b): investigated routing this through
2453/// `TypeShape::Refined` and declined — that variant's own `base` field is
2454/// `bynk_syntax::ast::BaseType` (P6.41 ruled it stays, phase 7 — the bounds
2455/// keep source lexemes for byte-stable emission), so this function's own
2456/// `Option<BaseType>` return type is identical either way. Converting would
2457/// add a `CheckedProgram`/`TypedCommons` dependency this function doesn't
2458/// have today for zero reduction in AST-type surface.
2459fn refined_or_opaque_base(decl: &TypeDecl) -> Option<BaseType> {
2460    match &decl.body {
2461        TypeBody::Refined { base, .. } | TypeBody::Opaque { base, .. } => Some(*base),
2462        _ => None,
2463    }
2464}
2465
2466/// Names that this file needs to import from elsewhere (sibling files of
2467/// the same commons, or other commons via `uses`).
2468#[derive(Default)]
2469struct ExternalReferences {
2470    /// `commons name` → set of names to import.
2471    by_commons: HashMap<String, HashSet<String>>,
2472    /// `sibling source path` → set of names to import (same-commons).
2473    by_sibling: HashMap<PathBuf, HashSet<String>>,
2474    /// #1778: names found only in an expression's checked type, keyed like
2475    /// `by_commons`/`by_sibling`. The lowering spells only some of them, so
2476    /// each is held here until [`ExternalReferences::settle_implied`] sees the
2477    /// emitted body. Importing the rest would add an unused import, and in a
2478    /// context a rebrand the module exports for nothing.
2479    implied_by_commons: HashMap<String, HashSet<String>>,
2480    implied_by_sibling: HashMap<PathBuf, HashSet<String>>,
2481    /// #1820: names imported from a sibling file as types only: a capability
2482    /// is an `interface`, which has no value to import once the module is
2483    /// stripped to JavaScript.
2484    types_by_sibling: HashMap<PathBuf, HashSet<String>>,
2485    /// #1820: every capability a sibling file declares, settled like
2486    /// `implied_by_sibling` into `types_by_sibling`. A handler's `deps` type
2487    /// names the capabilities it needs, and those include what the agents
2488    /// and services it calls need, so which a file spells is known only
2489    /// once its body is emitted.
2490    implied_types_by_sibling: HashMap<PathBuf, HashSet<String>>,
2491}
2492
2493impl ExternalReferences {
2494    fn is_empty(&self) -> bool {
2495        self.by_commons.is_empty() && self.by_sibling.is_empty() && self.types_by_sibling.is_empty()
2496    }
2497
2498    /// #1829: record each of `names`, the types a module's emitted helpers
2499    /// name, as an *implied* import, resolved the way a source reference is.
2500    /// A name this file declares, or a consumed context declares, is skipped,
2501    /// as [`record_ty_refs`] skips it.
2502    fn imply_names<'a>(
2503        &mut self,
2504        names: impl Iterator<Item = &'a String>,
2505        commons: &TypedCommons,
2506        ctx: &EmitProjectCtx,
2507    ) {
2508        let local_to_file = local_names(commons);
2509        let mut found = ExternalReferences::default();
2510        for name in names {
2511            if !is_consumed_context_type(ctx, name) {
2512                record_name_ref(name, &local_to_file, ctx, &mut found);
2513            }
2514        }
2515        for (unit, names) in found.by_commons {
2516            self.implied_by_commons
2517                .entry(unit)
2518                .or_default()
2519                .extend(names);
2520        }
2521        for (path, names) in found.by_sibling {
2522            self.implied_by_sibling
2523                .entry(path)
2524                .or_default()
2525                .extend(names);
2526        }
2527    }
2528
2529    /// #1778: promote each implied name the emitted body spells, as a whole
2530    /// TypeScript identifier, to a real import, and drop the rest. Called
2531    /// once the body is built and before the imports and rebrands are, which
2532    /// read only `by_commons`/`by_sibling`.
2533    fn settle_implied(&mut self, body: &[bynk_ts::TsStmt]) {
2534        let mut spelled = body_words(body);
2535        // #2027: `__CommonsLine` (`unbranded_commons_qual`) is `Line`'s own
2536        // import alias, so it spells `Line`.
2537        let aliased: Vec<String> = spelled
2538            .iter()
2539            .filter_map(|w| w.strip_prefix("__Commons").map(str::to_string))
2540            .collect();
2541        spelled.extend(aliased);
2542        for (unit, names) in std::mem::take(&mut self.implied_by_commons) {
2543            let kept: HashSet<String> = names.intersection(&spelled).cloned().collect();
2544            if !kept.is_empty() {
2545                self.by_commons.entry(unit).or_default().extend(kept);
2546            }
2547        }
2548        for (path, names) in std::mem::take(&mut self.implied_by_sibling) {
2549            let kept: HashSet<String> = names.intersection(&spelled).cloned().collect();
2550            if !kept.is_empty() {
2551                self.by_sibling.entry(path).or_default().extend(kept);
2552            }
2553        }
2554        for (path, names) in std::mem::take(&mut self.implied_types_by_sibling) {
2555            let kept: HashSet<String> = names.intersection(&spelled).cloned().collect();
2556            if !kept.is_empty() {
2557                self.types_by_sibling.entry(path).or_default().extend(kept);
2558            }
2559        }
2560    }
2561}
2562
2563/// Every whole TypeScript identifier `body` spells, as printed.
2564fn body_words(body: &[bynk_ts::TsStmt]) -> HashSet<String> {
2565    let mut words = HashSet::new();
2566    for stmt in body {
2567        let text = bynk_ts::print_stmt(stmt, 0);
2568        words.extend(
2569            text.split(|c: char| !(c.is_ascii_alphanumeric() || c == '_' || c == '$'))
2570                .filter(|w| !w.is_empty())
2571                .map(str::to_string),
2572        );
2573    }
2574    words
2575}
2576
2577/// The names this file declares, so a reference to one needs no import. A
2578/// `messages` block declares no importable identifier of its own (its
2579/// `render` is synthesised separately), so `name()` is `None` there and it
2580/// contributes nothing.
2581fn local_names(commons: &TypedCommons) -> HashSet<String> {
2582    commons
2583        .commons
2584        .items
2585        .iter()
2586        .filter_map(|i| i.name().map(|n| n.name.clone()))
2587        .collect()
2588}
2589
2590fn collect_external_references(commons: &TypedCommons, ctx: &EmitProjectCtx) -> ExternalReferences {
2591    // Names declared in this file (so we know what's local-to-file).
2592    let local_to_file = local_names(commons);
2593
2594    let mut refs = ExternalReferences::default();
2595
2596    // Walk every expression and TypeRef in this file's items, recording
2597    // any reference that resolves to a name declared in a sibling file or
2598    // an imported commons.
2599    for item in &commons.commons.items {
2600        match item {
2601            CommonsItem::Type(t) => {
2602                collect_refs_in_type_decl(t, &local_to_file, ctx, &mut refs);
2603            }
2604            // Events track, slice 0 (spine #936): an `event`'s field types
2605            // are collected exactly like a `type`'s, via the same synthetic
2606            // `TypeDecl` `EventDecl::as_type_decl` builds.
2607            CommonsItem::Event(e) => {
2608                collect_refs_in_type_decl(&e.as_type_decl(), &local_to_file, ctx, &mut refs);
2609            }
2610            CommonsItem::Fn(f) => {
2611                collect_refs_in_fn(f, &local_to_file, commons, ctx, &mut refs);
2612            }
2613            CommonsItem::Capability(c) => {
2614                for op in &c.ops {
2615                    for p in &op.params {
2616                        collect_refs_in_typeref(&p.type_ref, &local_to_file, ctx, &mut refs);
2617                    }
2618                    collect_refs_in_typeref(&op.return_type, &local_to_file, ctx, &mut refs);
2619                }
2620            }
2621            CommonsItem::Provider(p) => {
2622                // Reference to the capability so we can import it: a sibling
2623                // file's is implied, below (#1820).
2624                let _ = &p.capability;
2625                for op in &p.ops {
2626                    for param in &op.params {
2627                        collect_refs_in_typeref(&param.type_ref, &local_to_file, ctx, &mut refs);
2628                    }
2629                    collect_refs_in_typeref(&op.return_type, &local_to_file, ctx, &mut refs);
2630                    collect_refs_in_block(&op.body, &local_to_file, commons, ctx, &mut refs);
2631                }
2632            }
2633            CommonsItem::Service(s) => {
2634                for h in &s.handlers {
2635                    for p in &h.params {
2636                        collect_refs_in_typeref(&p.type_ref, &local_to_file, ctx, &mut refs);
2637                    }
2638                    collect_refs_in_typeref(&h.return_type, &local_to_file, ctx, &mut refs);
2639                    collect_refs_in_block(&h.body, &local_to_file, commons, ctx, &mut refs);
2640                }
2641            }
2642            CommonsItem::Agent(a) => {
2643                collect_refs_in_typeref(&a.key_type, &local_to_file, ctx, &mut refs);
2644                for f in &a.store_fields {
2645                    for arg in &f.kind.args {
2646                        collect_refs_in_typeref(arg, &local_to_file, ctx, &mut refs);
2647                    }
2648                }
2649                for h in &a.handlers {
2650                    for p in &h.params {
2651                        collect_refs_in_typeref(&p.type_ref, &local_to_file, ctx, &mut refs);
2652                    }
2653                    collect_refs_in_typeref(&h.return_type, &local_to_file, ctx, &mut refs);
2654                    collect_refs_in_block(&h.body, &local_to_file, commons, ctx, &mut refs);
2655                }
2656            }
2657            CommonsItem::Actor(a) => {
2658                if let Some(id) = &a.identity {
2659                    collect_refs_in_typeref(id, &local_to_file, ctx, &mut refs);
2660                }
2661            }
2662            // A `messages` block's generated table and `render` name
2663            // `LocaleTag`/`Message`/`MessageArg` and `render`/`renderArg`
2664            // only under private aliases (#1697), imported by `emit_unit`'s
2665            // (project.rs) hand-written lines, so a user declaration of the
2666            // same name in this unit can't capture them. Nothing is
2667            // registered here; user code that names `LocaleTag` itself
2668            // imports it the ordinary way.
2669            // #1883, slice 1a: an asset references no other declaration.
2670            CommonsItem::Messages(_) | CommonsItem::Asset(_) => {}
2671            // #2387, slice 1: an embed references no other declaration.
2672            CommonsItem::Embed(_) => {}
2673        }
2674    }
2675    for (cap, path) in &ctx.file_decl_index.capabilities {
2676        if !local_to_file.contains(cap) && path != &ctx.source_path {
2677            refs.implied_types_by_sibling
2678                .entry(path.clone())
2679                .or_default()
2680                .insert(cap.clone());
2681        }
2682    }
2683    refs
2684}
2685
2686fn collect_refs_in_type_decl(
2687    t: &TypeDecl,
2688    local_to_file: &HashSet<String>,
2689    ctx: &EmitProjectCtx,
2690    out: &mut ExternalReferences,
2691) {
2692    match &t.body {
2693        TypeBody::Record(r) => {
2694            for f in &r.fields {
2695                collect_refs_in_typeref(&f.type_ref, local_to_file, ctx, out);
2696            }
2697        }
2698        TypeBody::Sum(s) => {
2699            for v in &s.variants {
2700                for p in &v.payload {
2701                    collect_refs_in_typeref(&p.type_ref, local_to_file, ctx, out);
2702                }
2703            }
2704        }
2705        _ => {}
2706    }
2707}
2708
2709fn collect_refs_in_fn(
2710    f: &FnDecl,
2711    local_to_file: &HashSet<String>,
2712    commons: &TypedCommons,
2713    ctx: &EmitProjectCtx,
2714    out: &mut ExternalReferences,
2715) {
2716    for p in &f.params {
2717        collect_refs_in_typeref(&p.type_ref, local_to_file, ctx, out);
2718    }
2719    collect_refs_in_typeref(&f.return_type, local_to_file, ctx, out);
2720    // For methods: the attached type may also be elsewhere.
2721    if let FnName::Method { type_name, .. } = &f.name {
2722        record_name_ref(&type_name.name, local_to_file, ctx, out);
2723    }
2724    collect_refs_in_block(&f.body, local_to_file, commons, ctx, out);
2725}
2726
2727fn collect_refs_in_typeref(
2728    r: &TypeRef,
2729    local_to_file: &HashSet<String>,
2730    ctx: &EmitProjectCtx,
2731    out: &mut ExternalReferences,
2732) {
2733    match r {
2734        TypeRef::Named(id) => record_name_ref(&id.name, local_to_file, ctx, out),
2735        TypeRef::Result(t, e, _) => {
2736            collect_refs_in_typeref(t, local_to_file, ctx, out);
2737            collect_refs_in_typeref(e, local_to_file, ctx, out);
2738        }
2739        // Exhaustive over the compound constructors (#527, the #507 disease):
2740        // the old `_ => {}` catch-all dropped `List[KindCount]` and friends,
2741        // so a name referenced only inside such a position was never
2742        // imported and the emitted module failed `tsc`.
2743        TypeRef::Option(t, _)
2744        | TypeRef::Effect(t, _)
2745        | TypeRef::HttpResult(t, _)
2746        | TypeRef::List(t, _)
2747        | TypeRef::Query(t, _)
2748        | TypeRef::Stream(t, _)
2749        | TypeRef::Connection(t, _)
2750        | TypeRef::History(t, _) => collect_refs_in_typeref(t, local_to_file, ctx, out),
2751        TypeRef::Map(k, v, _) => {
2752            collect_refs_in_typeref(k, local_to_file, ctx, out);
2753            collect_refs_in_typeref(v, local_to_file, ctx, out);
2754        }
2755        TypeRef::Fn(params, ret, _) => {
2756            for t in params {
2757                collect_refs_in_typeref(t, local_to_file, ctx, out);
2758            }
2759            collect_refs_in_typeref(ret, local_to_file, ctx, out);
2760        }
2761        // v0.157 (ADR 0183): a `Name[Arg, …]` application references the
2762        // generic type plus every argument — all must be imported.
2763        TypeRef::App { name, args, .. } => {
2764            record_name_ref(&name.name, local_to_file, ctx, out);
2765            for t in args {
2766                collect_refs_in_typeref(t, local_to_file, ctx, out);
2767            }
2768        }
2769        TypeRef::Base(..)
2770        | TypeRef::QueueResult(_)
2771        | TypeRef::Asset(_)
2772        | TypeRef::RequestHeaders(_)
2773        | TypeRef::TokenClaims(_)
2774        | TypeRef::BoundaryRejection(_)
2775        | TypeRef::ValidationError(_)
2776        | TypeRef::JsonError(_)
2777        | TypeRef::Unit(_) => {}
2778    }
2779}
2780
2781fn collect_refs_in_block(
2782    b: &Block,
2783    local_to_file: &HashSet<String>,
2784    commons: &TypedCommons,
2785    ctx: &EmitProjectCtx,
2786    out: &mut ExternalReferences,
2787) {
2788    for stmt in &b.statements {
2789        match stmt {
2790            Statement::Let(l) | Statement::EffectLet(l) => {
2791                if let Some(t) = &l.type_annot {
2792                    collect_refs_in_typeref(t, local_to_file, ctx, out);
2793                }
2794                collect_refs_in_expr(&l.value, local_to_file, commons, ctx, out);
2795            }
2796            Statement::Expect(a) => {
2797                collect_refs_in_expr(&a.value, local_to_file, commons, ctx, out);
2798            }
2799            Statement::Send(s) => {
2800                collect_refs_in_expr(&s.value, local_to_file, commons, ctx, out);
2801            }
2802            Statement::Do(d) => {
2803                collect_refs_in_expr(&d.value, local_to_file, commons, ctx, out);
2804            }
2805            Statement::Assign(a) => {
2806                collect_refs_in_expr(&a.value, local_to_file, commons, ctx, out);
2807            }
2808            Statement::Advance(a) => {
2809                collect_refs_in_expr(&a.amount, local_to_file, commons, ctx, out);
2810            }
2811        }
2812    }
2813    collect_refs_in_expr(&b.tail, local_to_file, commons, ctx, out);
2814}
2815
2816fn collect_refs_in_expr(
2817    e: &Expr,
2818    local_to_file: &HashSet<String>,
2819    commons: &TypedCommons,
2820    ctx: &EmitProjectCtx,
2821    out: &mut ExternalReferences,
2822) {
2823    // #1778: the lowering also names types the source never spells. A
2824    // literal admitted as a refined type is cast to it (`("a" as Name)`),
2825    // and the collection kernels, `if` slots and `List.empty` annotate with
2826    // the `ts_ty` of checked types (`(__x: Name) => …`). So every name in
2827    // this expression's checked type is a candidate import, kept if the
2828    // emitted body spells it (`ExternalReferences::settle_implied`).
2829    if let Some(te) = commons.expr_types.get(&e.id) {
2830        record_ty_refs(te.ty, local_to_file, commons, ctx, out);
2831    }
2832    match &e.kind {
2833        // A bare ident the checker typed as a sum is a nullary variant
2834        // constructor — the lowering qualifies it to `Type.Variant`, so the
2835        // owning type must be imported (v0.18: first hit by `Get` from the
2836        // consumed bynk surface's `Method`).
2837        ExprKind::Ident(id) => {
2838            if let Some(type_name) = sum_owner_of_variant(&id.name, e.id, commons) {
2839                record_name_ref(&type_name, local_to_file, ctx, out);
2840            } else if commons.assets.contains_key(&id.name) || commons.embeds.contains_key(&id.name)
2841            {
2842                // #1883, slice 1b: a bare asset name (an `Asset` argument,
2843                // not just `style.url`, which the field-access arm already
2844                // records) imports the declaring module's constant.
2845                record_name_ref(&id.name, local_to_file, ctx, out);
2846            }
2847        }
2848        ExprKind::IntLit { .. }
2849        | ExprKind::FloatLit { .. }
2850        | ExprKind::DurationLit { .. }
2851        | ExprKind::StrLit(_)
2852        | ExprKind::BoolLit(_)
2853        | ExprKind::None
2854        | ExprKind::UnitLit => {}
2855        // v0.43: a hole's expression may reference imported names.
2856        ExprKind::Wire(inner) => collect_refs_in_expr(inner, local_to_file, commons, ctx, out),
2857        ExprKind::InterpStr(parts) => {
2858            for part in parts {
2859                if let InterpPart::Hole(hole) = part {
2860                    collect_refs_in_expr(hole, local_to_file, commons, ctx, out);
2861                }
2862            }
2863        }
2864        // v0.20a: a lambda — its annotated param types may reference
2865        // imported types; the body walks like any expression.
2866        ExprKind::Lambda(lambda) => {
2867            for p in &lambda.params {
2868                if let Some(tr) = &p.type_ref {
2869                    collect_refs_in_typeref(tr, local_to_file, ctx, out);
2870                }
2871            }
2872            collect_refs_in_expr(&lambda.body, local_to_file, commons, ctx, out);
2873        }
2874        ExprKind::EffectPure(inner) => {
2875            collect_refs_in_expr(inner, local_to_file, commons, ctx, out);
2876        }
2877        ExprKind::Expect(inner) => {
2878            collect_refs_in_expr(inner, local_to_file, commons, ctx, out);
2879        }
2880        // #1812: a fault claim's call-site identity is an expression too.
2881        ExprKind::Faults(f) => {
2882            if let Some(identity) = f.principal.as_ref().and_then(|p| p.identity.as_deref()) {
2883                collect_refs_in_expr(identity, local_to_file, commons, ctx, out);
2884            }
2885            collect_refs_in_expr(&f.call, local_to_file, commons, ctx, out);
2886        }
2887        ExprKind::Val { args, .. } => {
2888            for a in args {
2889                collect_refs_in_expr(a, local_to_file, commons, ctx, out);
2890            }
2891        }
2892        ExprKind::ListLit(elems) => {
2893            for el in elems {
2894                collect_refs_in_expr(el, local_to_file, commons, ctx, out);
2895            }
2896        }
2897        // v0.117: observation predicates may reference types/fns; `trace` does not.
2898        ExprKind::Observation(o) => {
2899            if let ObservationMatcher::Called { count, with_pred } = &o.matcher {
2900                if let Some(c) = count {
2901                    collect_refs_in_expr(c, local_to_file, commons, ctx, out);
2902                }
2903                if let Some(p) = with_pred {
2904                    collect_refs_in_expr(p, local_to_file, commons, ctx, out);
2905                }
2906            }
2907        }
2908        ExprKind::Trace { .. } => {}
2909        ExprKind::RecordSpread {
2910            type_name,
2911            base,
2912            overrides,
2913        } => {
2914            if let Some(tn) = type_name {
2915                record_name_ref(&tn.name, local_to_file, ctx, out);
2916            }
2917            collect_refs_in_expr(base, local_to_file, commons, ctx, out);
2918            for f in overrides {
2919                if let Some(v) = &f.value {
2920                    collect_refs_in_expr(v, local_to_file, commons, ctx, out);
2921                }
2922            }
2923        }
2924        ExprKind::Call { name, args, .. } => {
2925            record_name_ref(&name.name, local_to_file, ctx, out);
2926            // #1820: constructing an agent declared in a sibling file of a
2927            // multi-file unit lowers to its factory, `__make<Agent>`, which
2928            // that file exports.
2929            // The checker's classification decides, as the lowering does
2930            // (`Callee::AgentInit`): a sum variant spelled like an agent is
2931            // not one. The index only supplies the declaring file.
2932            if !local_to_file.contains(&name.name)
2933                && matches!(
2934                    commons.callee(e.id),
2935                    Some(bynk_check::checker::Callee::AgentInit(_))
2936                )
2937                && let Some(path) = ctx.file_decl_index.agents.get(&name.name)
2938                && path != &ctx.source_path
2939            {
2940                out.by_sibling
2941                    .entry(path.clone())
2942                    .or_default()
2943                    .insert(agent_factory_name(&name.name));
2944            }
2945            // A payload-carrying bare variant call (`Won(prize)`) lowers to
2946            // `Type.Variant(…)` — import the owning sum type too.
2947            if let Some(type_name) = sum_owner_of_variant(&name.name, e.id, commons) {
2948                record_name_ref(&type_name, local_to_file, ctx, out);
2949            }
2950            // #527: a call to a commons-imported fn may lower with a rebrand
2951            // assertion naming its return type (`(decide(…) as Decision)`),
2952            // so the return type's names must be imported (and rebranded)
2953            // in step with the cast.
2954            if ctx.unit_kind == UnitKind::Context
2955                && ctx.imported_from_kind.get(&name.name) == Some(&UnitKind::Commons)
2956                && let Some(f) = commons.fns.get(&name.name)
2957            {
2958                collect_refs_in_typeref(&f.return_type, local_to_file, ctx, out);
2959            }
2960            for a in args {
2961                collect_refs_in_expr(a, local_to_file, commons, ctx, out);
2962            }
2963        }
2964        ExprKind::BinOp(_, l, r) => {
2965            collect_refs_in_expr(l, local_to_file, commons, ctx, out);
2966            collect_refs_in_expr(r, local_to_file, commons, ctx, out);
2967        }
2968        ExprKind::UnaryOp(_, i)
2969        | ExprKind::Paren(i)
2970        | ExprKind::Ok(i)
2971        | ExprKind::Err(i)
2972        | ExprKind::Some(i)
2973        | ExprKind::Question(i) => collect_refs_in_expr(i, local_to_file, commons, ctx, out),
2974        ExprKind::Block(b) => collect_refs_in_block(b, local_to_file, commons, ctx, out),
2975        ExprKind::If {
2976            cond,
2977            then_block,
2978            else_block,
2979        } => {
2980            collect_refs_in_expr(cond, local_to_file, commons, ctx, out);
2981            collect_refs_in_block(then_block, local_to_file, commons, ctx, out);
2982            collect_refs_in_block(else_block, local_to_file, commons, ctx, out);
2983        }
2984        ExprKind::RecordConstruction { type_name, fields } => {
2985            record_name_ref(&type_name.name, local_to_file, ctx, out);
2986            for f in fields {
2987                if let Some(v) = &f.value {
2988                    collect_refs_in_expr(v, local_to_file, commons, ctx, out);
2989                }
2990            }
2991        }
2992        ExprKind::FieldAccess { receiver, field: _ } => {
2993            // The bare-ident-as-type case (`TypeName.Variant`) — record the
2994            // name so we import the type.
2995            if let ExprKind::Ident(id) = &receiver.kind {
2996                record_name_ref(&id.name, local_to_file, ctx, out);
2997            } else {
2998                collect_refs_in_expr(receiver, local_to_file, commons, ctx, out);
2999            }
3000        }
3001        ExprKind::MethodCall {
3002            receiver,
3003            method: _,
3004            args,
3005            ..
3006        } => {
3007            if let ExprKind::Ident(id) = &receiver.kind {
3008                record_name_ref(&id.name, local_to_file, ctx, out);
3009            } else {
3010                collect_refs_in_expr(receiver, local_to_file, commons, ctx, out);
3011            }
3012            for a in args {
3013                collect_refs_in_expr(a, local_to_file, commons, ctx, out);
3014            }
3015        }
3016        ExprKind::Match { discriminant, arms } => {
3017            collect_refs_in_expr(discriminant, local_to_file, commons, ctx, out);
3018            for arm in arms {
3019                if let Pattern::Variant {
3020                    type_name: Some(tn),
3021                    ..
3022                } = &arm.pattern
3023                {
3024                    record_name_ref(&tn.name, local_to_file, ctx, out);
3025                }
3026                // #1800: a name used only in a guard needs its import too;
3027                // skipping the guard emitted a reference `tsc` cannot find.
3028                if let Some(guard) = &arm.guard {
3029                    collect_refs_in_expr(guard, local_to_file, commons, ctx, out);
3030                }
3031                match &arm.body {
3032                    MatchBody::Expr(e) => collect_refs_in_expr(e, local_to_file, commons, ctx, out),
3033                    MatchBody::Block(b) => {
3034                        collect_refs_in_block(b, local_to_file, commons, ctx, out)
3035                    }
3036                }
3037            }
3038        }
3039        ExprKind::Is { value, pattern } => {
3040            collect_refs_in_expr(value, local_to_file, commons, ctx, out);
3041            if let Pattern::Variant {
3042                type_name: Some(tn),
3043                ..
3044            } = pattern.as_ref()
3045            {
3046                record_name_ref(&tn.name, local_to_file, ctx, out);
3047            }
3048        }
3049    }
3050}
3051
3052/// If `name` at `span` is a bare reference to a variant of a sum type (per
3053/// the checker's expression type), return the owning sum's name — the same
3054/// test the lowering uses to qualify it as `Type.Variant` (see the
3055/// `ExprKind::Ident` arm of `lower_expr_into`).
3056///
3057/// P6.56 (design/tracks/the-ir.md §6b): investigated routing variant
3058/// membership through a `TypedCommons`-only `TypeShape::Sum` lowering and
3059/// declined, not built — `lower_type_item_ir`'s own `TypeBody::Sum` arm
3060/// resolves every payload field's own `TyId` for every variant (an
3061/// `.unwrap_or_else(|| panic!(..))` on any resolution miss), just to answer
3062/// a name-membership question this call site can settle with a zero-cost,
3063/// infallible string comparison today. `positional_field_name` below has
3064/// the identical shape and the identical verdict, for the identical
3065/// reason.
3066fn sum_owner_of_variant(name: &str, id: ExprId, commons: &TypedCommons) -> Option<String> {
3067    if let Some(Ty::Named {
3068        kind: NamedKind::Sum,
3069        name: type_name,
3070        ..
3071    }) = commons.expr_ty(id).as_deref()
3072        && let Some(decl) = commons.types.get(type_name)
3073        && let TypeBody::Sum(s) = &decl.body
3074        && s.variants.iter().any(|v| v.name.name == name)
3075    {
3076        return Some(type_name.clone());
3077    }
3078    None
3079}
3080
3081/// #1778: record every named type reachable in `ty`, the checked type of an
3082/// expression, as an *implied* import, which `settle_implied` keeps only if
3083/// the emitted body spells it. A name a consumed *context* declares is skipped:
3084/// the lowering never names one through a checked type unqualified, and under
3085/// Workers a context's value cannot be imported across its Worker boundary.
3086fn record_ty_refs(
3087    ty: TyId,
3088    local_to_file: &HashSet<String>,
3089    commons: &TypedCommons,
3090    ctx: &EmitProjectCtx,
3091    out: &mut ExternalReferences,
3092) {
3093    match &*commons.tys().get(ty) {
3094        Ty::Named { name, args, .. } => {
3095            if !is_consumed_context_type(ctx, name) {
3096                let mut found = ExternalReferences::default();
3097                record_name_ref(name, local_to_file, ctx, &mut found);
3098                for (unit, names) in found.by_commons {
3099                    out.implied_by_commons
3100                        .entry(unit)
3101                        .or_default()
3102                        .extend(names);
3103                }
3104                for (path, names) in found.by_sibling {
3105                    out.implied_by_sibling
3106                        .entry(path)
3107                        .or_default()
3108                        .extend(names);
3109                }
3110            }
3111            for a in args {
3112                record_ty_refs(*a, local_to_file, commons, ctx, out);
3113            }
3114        }
3115        Ty::Option(t)
3116        | Ty::Effect(t)
3117        | Ty::HttpResult(t)
3118        | Ty::HttpOutcome(t)
3119        | Ty::List(t)
3120        | Ty::Query(t)
3121        | Ty::Stream(t)
3122        | Ty::Connection(t)
3123        | Ty::Actor(t) => record_ty_refs(*t, local_to_file, commons, ctx, out),
3124        Ty::Result(a, b) | Ty::Map(a, b) => {
3125            record_ty_refs(*a, local_to_file, commons, ctx, out);
3126            record_ty_refs(*b, local_to_file, commons, ctx, out);
3127        }
3128        Ty::Fn { params, ret } => {
3129            for p in params {
3130                record_ty_refs(*p, local_to_file, commons, ctx, out);
3131            }
3132            record_ty_refs(*ret, local_to_file, commons, ctx, out);
3133        }
3134        Ty::ActorSum(members) => {
3135            for (_, t) in members {
3136                record_ty_refs(*t, local_to_file, commons, ctx, out);
3137            }
3138        }
3139        Ty::Error
3140        | Ty::Base(_)
3141        | Ty::QueueResult
3142        | Ty::BoundaryRejection
3143        | Ty::Asset
3144        | Ty::RequestHeaders
3145        | Ty::TokenClaims
3146        | Ty::ValidationError
3147        | Ty::JsonError
3148        | Ty::Unit
3149        | Ty::Var(_) => {}
3150    }
3151}
3152
3153fn record_name_ref(
3154    name: &str,
3155    local_to_file: &HashSet<String>,
3156    ctx: &EmitProjectCtx,
3157    out: &mut ExternalReferences,
3158) {
3159    if local_to_file.contains(name) {
3160        return;
3161    }
3162    // Imported from another commons?
3163    if let Some(commons_name) = ctx.imported_from.get(name) {
3164        out.by_commons
3165            .entry(commons_name.clone())
3166            .or_default()
3167            .insert(name.to_string());
3168        return;
3169    }
3170    // Sibling file in the same commons?
3171    if let Some(path) = ctx.file_decl_index.types.get(name)
3172        && path != &ctx.source_path
3173    {
3174        out.by_sibling
3175            .entry(path.clone())
3176            .or_default()
3177            .insert(name.to_string());
3178        return;
3179    }
3180    if let Some(path) = ctx.file_decl_index.fns.get(name)
3181        && path != &ctx.source_path
3182    {
3183        out.by_sibling
3184            .entry(path.clone())
3185            .or_default()
3186            .insert(name.to_string());
3187    }
3188}
3189
3190/// Emit `import * as <ns> from "..."` for each consumed context that
3191/// exposes services (so the consuming file can reference its `__makeSurface`
3192/// return type and brand the cross-context call arguments).
3193/// #1478: real-node-internally already — returns one real `TsDecl::
3194/// ImportNamespace` per consumed context instead of writing into
3195/// `out: &mut String`.
3196fn emit_cross_context_namespace_imports(
3197    commons: &TypedCommons,
3198    ctx: &EmitProjectCtx,
3199    body: &[bynk_ts::TsStmt],
3200) -> Vec<bynk_ts::TsStmt> {
3201    let info = &ctx.cross_context;
3202    // Consumed contexts that expose services (v0.6) plus, v0.15, those whose
3203    // capabilities this context references via `given B.Cap`.
3204    let mut needed: std::collections::BTreeSet<String> = info
3205        .consumed_services
3206        .iter()
3207        .filter(|(_, svcs)| !svcs.is_empty())
3208        .map(|(q, _)| q.clone())
3209        .collect();
3210    needed.extend(cross_context_cap_namespaces(commons, ctx));
3211    // #1961: on workers, the owner of each event this context subscribes to.
3212    // Its payload codec (`emit_consumed_context_helpers`) names the event
3213    // through this alias, and a publisher with no callable service (only
3214    // `http` ones) was otherwise never imported: `TS2304` on the subscriber.
3215    if matches!(ctx.target, BuildTarget::Workers) {
3216        let subscribed: HashSet<String> = commons
3217            .commons
3218            .items
3219            .iter()
3220            .filter_map(|item| match item {
3221                CommonsItem::Service(s) => {
3222                    let bynk_ir::ProtocolIr::Events { event, .. } =
3223                        bynk_lower::lower_protocol_ir_from_commons(&s.protocol, commons)
3224                    else {
3225                        return None;
3226                    };
3227                    match &*commons.tys().get(event) {
3228                        Ty::Named { name, .. } => Some(name.clone()),
3229                        _ => None,
3230                    }
3231                }
3232                _ => None,
3233            })
3234            .collect();
3235        needed.extend(
3236            info.consumed_event_names
3237                .iter()
3238                .filter(|(_, names)| names.iter().any(|n| subscribed.contains(n)))
3239                .map(|(c, _)| c.clone()),
3240        );
3241    }
3242    // #2058: a consumed context the body names a type of through its alias
3243    // (`(1 as ev_store.Code)`, a literal admitted to its refined type), even
3244    // one with no service this context calls.
3245    // Only a refined type an expression here is checked as can be cast to,
3246    // and only one the body then spells through the alias is imported.
3247    let type_ns = crate::emitter::emit::consumed_type_ns(ctx);
3248    let named: HashSet<String> = commons
3249        .expr_types
3250        .values()
3251        .filter_map(|te| match &*commons.tys().get(te.ty) {
3252            Ty::Named {
3253                name,
3254                kind: NamedKind::Refined(_),
3255                ..
3256            } => type_ns.get(name).cloned(),
3257            _ => None,
3258        })
3259        .collect();
3260    if !named.is_empty() {
3261        let spelled = body_words(body);
3262        needed.extend(
3263            info.consumed_contexts
3264                .iter()
3265                .filter(|c| {
3266                    let ns = qualified_to_ns(c);
3267                    named.contains(&ns) && spelled.contains(&ns)
3268                })
3269                .cloned(),
3270        );
3271    }
3272    // #2073: on workers, any consumed context whose alias this module's codecs
3273    // spell, even one with no service this context calls: the declaring
3274    // context of a type a borrowed signature type reaches (`shop_base.Base` in
3275    // `shop.order`'s `Order`), or of a field of a type on this context's own
3276    // boundary (#2044's codecs, `shop_base.Base` for `Order.b`).
3277    if matches!(ctx.target, BuildTarget::Workers) {
3278        let spelled = body_words(body);
3279        needed.extend(
3280            info.consumed_contexts
3281                .iter()
3282                .filter(|c| {
3283                    !ctx.consumed_adapters.contains(c.as_str())
3284                        && spelled.contains(&qualified_to_ns(c))
3285                })
3286                .cloned(),
3287        );
3288    }
3289    if needed.is_empty() {
3290        return Vec::new();
3291    }
3292    let mut stmts = Vec::new();
3293    let consumed_with_services: Vec<&String> = needed.iter().collect();
3294    for q in &consumed_with_services {
3295        // Pick the first known file path for the consumed context as the
3296        // import target. (The composition root lives in the consumed
3297        // context's directory; any of its files would work as an import
3298        // target since they're all in the same module namespace, but we
3299        // currently emit one file per .bynk source so a single import per
3300        // consumed name suffices for the surface contract.)
3301        let target_paths = ctx.imported_decl_paths.get(q.as_str());
3302        let target = target_paths
3303            .and_then(|m| m.values().next().cloned())
3304            .unwrap_or_else(|| {
3305                // No imported declaration pins the path (e.g. a capability-only
3306                // consumed context, v0.15). Fall back to the unit's own module:
3307                // its per-Worker handlers in workers mode, or its <segment>.bynk
3308                // source in bundle mode. v0.17: a consumed *adapter* is not a
3309                // Worker — its capability types live in its root module
3310                // (`<adapter>.ts`) in both targets.
3311                if ctx.consumed_adapters.contains(q.as_str()) {
3312                    let mut p = EmitProjectCtx::commons_path(q);
3313                    p.set_extension("bynk");
3314                    p
3315                } else {
3316                    match ctx.target {
3317                        BuildTarget::Workers => crate::project::worker_handlers_source_path(q),
3318                        BuildTarget::Bundle => {
3319                            let mut p = EmitProjectCtx::commons_path(q);
3320                            p.set_extension("bynk");
3321                            p
3322                        }
3323                    }
3324                }
3325            });
3326        let import =
3327            cross_commons_import_specifier_for_path(&ctx.source_path, &target, ctx.import_ext);
3328        let ns = qualified_to_ns(q);
3329        // #661: under `workers`, a consumed *context*'s module is imported for
3330        // its **types only** — the caller now generates its own codecs
3331        // (`emit_boundary_helpers`) and reaches the callee's types through this
3332        // alias in type position (`deps: { Clock: platform_time.Clock }`,
3333        // `Result<commerce_payment.AuthId, …>`). An `import type` is erased
3334        // outright, so the callee's *module* — and its provider implementation —
3335        // never enters the caller's Worker bundle. This does **not** apply to a
3336        // consumed *adapter* (its binding namespace, e.g. `tokens`, is a real
3337        // value import used by `compose.ts`) nor on `bundle` (contexts compile
3338        // together, and the value uses in `compose.ts` are legitimate).
3339        let type_only = matches!(ctx.target, BuildTarget::Workers)
3340            && !ctx.consumed_adapters.contains(q.as_str());
3341        // Arc C, slice 30 (#1392): a real `TsDecl::ImportNamespace` —
3342        // `type_only` (#1392's own new field) covers the `import type * as`
3343        // form.
3344        stmts.push(bynk_ts::TsStmt::decl(
3345            bynk_ts::TsDecl::ImportNamespace {
3346                type_only,
3347                alias: ns,
3348                from: import,
3349            },
3350            None,
3351        ));
3352    }
3353    // #1486: no explicit trailing blank stmt here — the printer's automatic
3354    // top-level spacing policy supplies the single blank before whatever
3355    // follows (never import-kind at this function's own call sites, so
3356    // never exempted from it). The boundary this function's own *first*
3357    // statement forms with `emit_project_imports`'s own last statement
3358    // (both import-kind, genuinely wanting a blank despite the "no blank
3359    // between adjacent imports" exemption) is handled at `emit_project`'s
3360    // own call site instead — the only place that knows both neighbours.
3361    stmts
3362}
3363
3364/// #1478: real-node-internally already — returns one real `TsDecl::Import`
3365/// per sibling/cross-unit import group instead of writing into
3366/// `out: &mut String`.
3367fn emit_project_imports(
3368    commons: &TypedCommons,
3369    ctx: &EmitProjectCtx,
3370    refs: &ExternalReferences,
3371) -> Vec<bynk_ts::TsStmt> {
3372    let mut stmts = Vec::new();
3373    // Events track, slice 0 (spine #936): the bare event-type names this
3374    // context's own `from Events(E)` service headers name — see the
3375    // Workers type-only-import narrowing below.
3376    // P6.24a/P6.19: reads the protocol's own resolved `ProtocolIr::Events`
3377    // instead of matching `ServiceProtocol::Events { event_type:
3378    // TypeRef::Named(id), .. }` directly — a resolve miss (`lower_protocol_
3379    // ir_from_commons` degrades to `Ty::Unit` on one, never panics) is
3380    // indistinguishable from a legitimately non-`Named` header, so falls
3381    // through to `None` exactly like the raw match's own `_ => None` arm
3382    // did, not a new failure mode.
3383    let subscribed_event_type_names: HashSet<String> = commons
3384        .commons
3385        .items
3386        .iter()
3387        .filter_map(|item| match item {
3388            CommonsItem::Service(s) => {
3389                let bynk_ir::ProtocolIr::Events { event, .. } =
3390                    bynk_lower::lower_protocol_ir_from_commons(&s.protocol, commons)
3391                else {
3392                    return None;
3393                };
3394                match &*commons.tys().get(event) {
3395                    Ty::Named { name, .. } => Some(name.clone()),
3396                    _ => None,
3397                }
3398            }
3399            _ => None,
3400        })
3401        .collect();
3402    // Sibling imports: relative path within the same commons/context directory.
3403    let mut sibling_paths: Vec<(&PathBuf, &HashSet<String>)> = refs.by_sibling.iter().collect();
3404    sibling_paths.sort_by(|a, b| a.0.cmp(b.0));
3405    for (path, names) in sibling_paths {
3406        let import = sibling_import_specifier(&ctx.source_path, path, ctx.import_ext);
3407        let mut sorted: Vec<&String> = names.iter().collect();
3408        sorted.sort();
3409        // Arc C, slice 30 (#1392): a real `TsDecl::Import`.
3410        stmts.push(bynk_ts::TsStmt::decl(
3411            bynk_ts::TsDecl::Import {
3412                type_only: false,
3413                names: sorted.iter().map(|s| ts_ident(s)).collect(),
3414                from: import,
3415            },
3416            None,
3417        ));
3418    }
3419    let mut type_paths: Vec<(&PathBuf, &HashSet<String>)> = refs.types_by_sibling.iter().collect();
3420    type_paths.sort_by(|a, b| a.0.cmp(b.0));
3421    for (path, names) in type_paths {
3422        let mut sorted: Vec<&String> = names.iter().collect();
3423        sorted.sort();
3424        stmts.push(bynk_ts::TsStmt::decl(
3425            bynk_ts::TsDecl::Import {
3426                type_only: true,
3427                names: sorted.iter().map(|s| ts_ident(s)).collect(),
3428                from: sibling_import_specifier(&ctx.source_path, path, ctx.import_ext),
3429            },
3430            None,
3431        ));
3432    }
3433    // Cross-unit imports: group by *target file path*.
3434    let mut unit_names: Vec<(&String, &HashSet<String>)> = refs.by_commons.iter().collect();
3435    unit_names.sort_by(|a, b| a.0.cmp(b.0));
3436    for (unit_name, names) in unit_names {
3437        let target_paths = ctx.imported_decl_paths.get(unit_name.as_str());
3438        let mut by_target: std::collections::BTreeMap<PathBuf, Vec<&String>> =
3439            std::collections::BTreeMap::new();
3440        for n in names {
3441            let path = target_paths
3442                .and_then(|p| p.get(n))
3443                .cloned()
3444                .unwrap_or_else(|| EmitProjectCtx::commons_path(unit_name));
3445            by_target.entry(path).or_default().push(n);
3446        }
3447        for (target, mut name_list) in by_target {
3448            name_list.sort();
3449            let import =
3450                cross_commons_import_specifier_for_path(&ctx.source_path, &target, ctx.import_ext);
3451            // For context units, aliase commons-source imports so we can emit
3452            // rebrand aliases of the same short name. Imports from consumed
3453            // contexts keep their original name. v0.20b: the rebrand applies
3454            // to *types* only — a `uses`-imported function (bynk.list's
3455            // `traverse`) is a value, imports plainly, and is never branded.
3456            let mut parts: Vec<String> = Vec::new();
3457            for n in &name_list {
3458                let is_subscribed_event_type = ctx.target == BuildTarget::Workers
3459                    && subscribed_event_type_names.contains(n.as_str());
3460                // #2045: under Workers, a consumed *context*'s type is named in
3461                // type position only — its codecs are the caller's own
3462                // (`emit_consumed_context_helpers`) — so it imports type-only and
3463                // the callee's module never enters the caller's Worker bundle
3464                // (#661). A consumed *adapter* is linked into each Worker, and
3465                // its values (`Method.Get`) are real imports.
3466                let is_consumed_context_type = ctx.target == BuildTarget::Workers
3467                    && ctx.unit_kind == UnitKind::Context
3468                    && !ctx.consumed_adapters.contains(unit_name.as_str())
3469                    && ctx.imported_from_kind.get(n.as_str()) == Some(&UnitKind::Context)
3470                    && !ctx.consumed_types_with_methods.contains(n.as_str());
3471                // R4.10/R8.2: the same shared `is_uses_commons_type` predicate
3472                // `emit_context_rebrands` below reads — this import-aliasing
3473                // site is that function's own step 1 ("Done in imports", its
3474                // own doc comment), and the two must agree exactly: an alias
3475                // narrower than the rebrand leaves an undefined name in the
3476                // generated import; a rebrand narrower than the alias leaves
3477                // an alias imported and never used. `ctx.unit_kind ==
3478                // UnitKind::Context` is the same guard as
3479                // `emit_context_rebrands`'s own `ctx.owning_context.is_some()`
3480                // early return (`owning_context` is `Some` exactly when
3481                // `unit_kind == Context`, `project.rs`'s own construction) —
3482                // kept explicit here since this loop runs for every unit
3483                // kind, not just contexts.
3484                if ctx.unit_kind == UnitKind::Context
3485                    && bynk_check::resolver::compute_is_uses_commons_type(
3486                        &ctx.imported_from_kind,
3487                        &commons.types,
3488                        n,
3489                    )
3490                {
3491                    parts.push(format!("{n} as __Commons{n}"));
3492                } else if is_subscribed_event_type || is_consumed_context_type {
3493                    // Events track, slice 0 (spine #936): under Workers, a
3494                    // context deploys as its own separate Worker script —
3495                    // there is no shared module graph to import a peer
3496                    // context's *value* across (the #661 hazard this
3497                    // mirrors: a caller generates its own codec rather than
3498                    // importing the callee's runtime code). `from
3499                    // Events(E)`'s `E` is the one plain named type crossing
3500                    // a context boundary directly by name (every other
3501                    // cross-context reference goes through a generated
3502                    // Service-Binding codec instead) — used only in type
3503                    // position (`e: E`), so this specific name is type-only.
3504                    // Narrowly scoped to event types specifically, not every
3505                    // cross-context import: a `type`/`enum` crossing via
3506                    // `uses`/`consumes` (e.g. `bynk`'s `Method`) is often
3507                    // used as a *value* too (`Method.Get`), which a blanket
3508                    // `import type` would wrongly break.
3509                    parts.push(format!("type {}", ts_ident(n)));
3510                } else {
3511                    parts.push(ts_ident(n));
3512                }
3513            }
3514            // Arc C, slice 30 (#1392): a real `TsDecl::Import` — `parts`'
3515            // own per-name `as __CommonsX`/`type X` prefixes already match
3516            // `names`'s own documented "raw text slot" convention, no new
3517            // gap.
3518            stmts.push(bynk_ts::TsStmt::decl(
3519                bynk_ts::TsDecl::Import {
3520                    type_only: false,
3521                    names: parts,
3522                    from: import,
3523                },
3524                None,
3525            ));
3526        }
3527    }
3528    // #527: imports the DO-side agent-deps expressions need (binding modules,
3529    // other Workers' handlers). Precomputed by the project driver — already
3530    // formed statement text, stays opaque `TsStmt::raw`, the established
3531    // "pre-formatted pass-through" pattern.
3532    for line in &ctx.extra_import_lines {
3533        stmts.push(bynk_ts::TsStmt::raw(format!("{line}\n"), None));
3534    }
3535    // #1486: every statement in this function's own return, whatever
3536    // section it came from (sibling imports, cross-unit imports, the
3537    // rebrand-aliasing pair, `extra_import_lines`' own pre-formatted `Raw`
3538    // text), sits directly under its predecessor with no blank between
3539    // them — this function never inserted one under the pre-#1486
3540    // flat-print regime (confirmed: no explicit blank `TsStmt` anywhere in
3541    // its own body), so every internal adjacency needs forcing regardless
3542    // of `TsStmtKind` (a `Raw` import line next to a real `ImportNamespace`
3543    // is exactly the case the printer's own kind-based "both imports"
3544    // exemption can't see). The very first statement is left alone — its
3545    // own blank-or-not relative to whatever precedes this function's call
3546    // site is `emit_project`'s own call-site decision (the header/`emit_
3547    // project_imports` boundary, and the project/cross-context import
3548    // boundary, both forced explicitly there instead).
3549    for stmt in stmts.iter_mut().skip(1) {
3550        stmt.no_blank_before = true;
3551    }
3552    stmts
3553}
3554
3555/// Compute a relative import specifier from `from_source` (a `.bynk` path)
3556/// to `to_source` (another `.bynk` path), with `.bynk` rewritten to `.js`
3557/// for compatibility with NodeNext/strict TS resolution.
3558fn sibling_import_specifier(from_source: &Path, to_source: &Path, ext: ImportExt) -> String {
3559    let from_dir = from_source.parent().unwrap_or(Path::new(""));
3560    let target = to_source.with_extension(ext.as_str());
3561    let rel = relative_to(from_dir, &target);
3562    format!("./{}", ts_specifier(&rel))
3563}
3564
3565/// Render a path as a TypeScript module specifier: **always forward
3566/// slashes**. `Path::display()` uses the platform separator, and on Windows
3567/// that emitted `import ... from "./commerce\orders.js"` — broken ESM
3568/// output, caught by the first CI matrix run on windows-latest.
3569pub(crate) fn ts_specifier(p: &Path) -> String {
3570    p.to_string_lossy().replace('\\', "/")
3571}
3572
3573/// Compute a relative import specifier from this file's location to a
3574/// specific source file in another commons. `target_source` is the project-
3575/// relative path of the target `.bynk` file. The result is suitable for
3576/// `import { ... } from "..."` in NodeNext/strict TypeScript.
3577pub(crate) fn cross_commons_import_specifier_for_path(
3578    from_source: &Path,
3579    target_source: &Path,
3580    ext: ImportExt,
3581) -> String {
3582    let from_dir = from_source.parent().unwrap_or(Path::new(""));
3583    let target = target_source.with_extension(ext.as_str());
3584    let rel = relative_to(from_dir, &target);
3585    let display = ts_specifier(&rel);
3586    if display.starts_with("../") || display.starts_with("./") {
3587        display
3588    } else {
3589        format!("./{display}")
3590    }
3591}
3592
3593/// Compute `target` as a path relative to `from`. Handles parent traversal
3594/// (`..`) for cases where `target` lives in a sibling directory.
3595fn relative_to(from: &Path, target: &Path) -> PathBuf {
3596    use std::path::Component as C;
3597    let f_comps: Vec<C> = from.components().collect();
3598    let t_comps: Vec<C> = target.components().collect();
3599    let mut shared = 0;
3600    while shared < f_comps.len() && shared < t_comps.len() && f_comps[shared] == t_comps[shared] {
3601        shared += 1;
3602    }
3603    let mut out = PathBuf::new();
3604    for _ in shared..f_comps.len() {
3605        out.push("..");
3606    }
3607    for c in &t_comps[shared..] {
3608        out.push(c.as_os_str());
3609    }
3610    if out.as_os_str().is_empty() {
3611        out.push(".");
3612    }
3613    out
3614}
3615
3616/// #1478: real-node-internally already — returns its own real statements
3617/// (the two-comment banner, plus a conditional runtime-import declaration)
3618/// instead of writing into `out: &mut String`.
3619fn write_header(commons: &TypedCommons, ctx: &EmitProjectCtx) -> Vec<bynk_ts::TsStmt> {
3620    // Arc C, slice 30 (#1392): the 2-line banner converts to two real
3621    // `TsStmt::Comment` statements, the same `events_fanout.rs`-established
3622    // precedent (#1317) every other Arc C header already uses.
3623    let kind = match ctx.unit_kind {
3624        UnitKind::Commons => "commons",
3625        UnitKind::Context => "context",
3626        UnitKind::Test => "test",
3627        UnitKind::Integration => "integration test",
3628        UnitKind::Adapter => "adapter",
3629    };
3630    // #1486: no explicit blank stmt after the banner — the printer's
3631    // automatic top-level spacing policy already supplies the single blank
3632    // after it (`Comment`→`Comment` is exempted, matching the banner's own
3633    // two adjacent lines; the automatic policy is not exempted for whatever
3634    // follows the banner, so the wanted blank appears there for free).
3635    let mut stmts = vec![
3636        bynk_ts::TsStmt::comment("Generated by bynkc — do not edit by hand.", None),
3637        bynk_ts::TsStmt::comment(format!("{kind} {}", commons.commons.name.joined()), None),
3638    ];
3639    if !commons.commons.items.is_empty() {
3640        let runtime_import = runtime_import_for(&ctx.source_path, ctx.import_ext);
3641        let has_agent = commons
3642            .commons
3643            .items
3644            .iter()
3645            .any(|i| matches!(i, CommonsItem::Agent(_)));
3646        // v0.80: a file with any agent invariant imports the `invariantViolation`
3647        // fault helper used by the generated `commitState` gate. v0.116: a step
3648        // invariant (`transition`) uses the same fault helper, so a transition-only
3649        // agent must import it too.
3650        let has_agent_invariants = commons.commons.items.iter().any(|i| match i {
3651            CommonsItem::Agent(a) => !a.invariants.is_empty() || !a.transitions.is_empty(),
3652            _ => false,
3653        });
3654        // v0.153 (ADR 0177): a service HTTP handler imports `HttpResult`, and so
3655        // does any *free* `fn` / provider / capability whose signature names it
3656        // (the `?`-Option lift makes a bare `fn -> HttpResult[T]` emit
3657        // `HttpResult.NotFound`) — the structural scan covers both, closing the
3658        // free-fn gap the single-file path already handles.
3659        // P6.24a/P6.19: `h.kind`/`s.protocol` read through the IR-native
3660        // `IrHandlerKind`/`ProtocolIr` readers below — pure syntax, no body
3661        // lowering, safe regardless of the still-open `Question`/`Is` gap
3662        // (design/tracks/the-ir.md's own P6.3 correction).
3663        let has_http = commons.commons.items.iter().any(|i| match i {
3664            CommonsItem::Service(s) => s.handlers.iter().any(|h| {
3665                matches!(
3666                    bynk_lower::lower_handler_kind_ir(&h.kind),
3667                    bynk_ir::IrHandlerKind::Http { .. }
3668                )
3669            }),
3670            _ => false,
3671        }) || file_mentions_http_result(commons);
3672        // A `from queue` `on message` is the queue consumer (imports `QueueResult`);
3673        // a `from websocket` `on message` (slice 3b-iii) is the inbound handler and
3674        // is not a queue concern. #1319: a bare field/type-declaration mention
3675        // (the `ts_any` residual's own real-type-cast sites) is a second, real
3676        // way to need the import with no consumer handler at all — closed via
3677        // `file_mentions_queue_result`, the same structural-scan pattern
3678        // `file_mentions_http_result` already uses for its own type.
3679        let has_queue = commons.commons.items.iter().any(|i| match i {
3680            CommonsItem::Service(s) => {
3681                !matches!(
3682                    bynk_lower::lower_protocol_ir_from_commons(&s.protocol, commons),
3683                    bynk_ir::ProtocolIr::WebSocket { .. }
3684                ) && s.handlers.iter().any(|h| {
3685                    matches!(
3686                        bynk_lower::lower_handler_kind_ir(&h.kind),
3687                        bynk_ir::IrHandlerKind::Message
3688                    )
3689                })
3690            }
3691            _ => false,
3692        }) || file_mentions_queue_result(commons);
3693        let workers = matches!(ctx.target, BuildTarget::Workers);
3694        let mut parts: Vec<&str> = vec![
3695            "Ok",
3696            "Err",
3697            "Some",
3698            "None",
3699            "type Result",
3700            "type Option",
3701            "type ValidationError",
3702        ];
3703        // v0.22b: the codec types are imported only when the file uses the
3704        // `Json` codec (or names `JsonError` in a signature) — keeping every
3705        // non-codec module's header byte-identical to v0.22a.
3706        let uses_codec = !collect_json_codec_roots(commons).is_empty();
3707        let mentions_json_error = file_mentions_json_error(commons);
3708        if uses_codec || mentions_json_error {
3709            parts.push("type JsonError");
3710        }
3711        // v0.102: a file naming `Connection[F]` imports the runtime interface.
3712        if file_mentions_connection(commons) {
3713            parts.push("type Connection");
3714        }
3715        if has_agent {
3716            // v0.9.2: agent-declaring files lower instantiation through the
3717            // `makeAgent` helper and a per-agent `StateRegistry`, and the
3718            // generated factory's signature names `DurableObjectNamespace`.
3719            parts.push("type __DurableObjectState");
3720            parts.push("type __DurableObjectNamespace");
3721            parts.push("__StateRegistry");
3722            parts.push("__makeAgent");
3723        } else if workers
3724            && commons
3725                .callees
3726                .values()
3727                .any(|c| matches!(c, bynk_check::checker::Callee::AgentInit(_)))
3728        {
3729            // #1820: a file of a split context that constructs only a
3730            // sibling file's agents passes `env` to its factory, so its
3731            // handlers' `deps.env` names the agents' namespaces too.
3732            parts.push("type __DurableObjectNamespace");
3733        }
3734        if has_agent_invariants {
3735            parts.push("__invariantViolation");
3736        }
3737        // #1678: on `workers` an agent's calls cross its Durable Object's
3738        // `fetch` through a wire table read by both ends.
3739        if workers && has_agent {
3740            parts.push("type __AgentWire");
3741            parts.push("__decodeAgentArgs");
3742            parts.push("__encodeAgentResult");
3743            // #1818: an agent that reads its own key decodes it on the DO side.
3744            let reads_key = |a: &AgentDecl| crate::emitter::emit::agent_reads_key(a);
3745            if commons
3746                .commons
3747                .items
3748                .iter()
3749                .any(|i| matches!(i, CommonsItem::Agent(a) if reads_key(a)))
3750            {
3751                parts.push("__decodeAgentKey");
3752            }
3753            let passes_through = commons.commons.items.iter().any(|i| match i {
3754                CommonsItem::Agent(a) => {
3755                    (reads_key(a) && serialisation::agent_wire_passes_through(&a.key_type))
3756                        || a.handlers.iter().any(|h| {
3757                            serialisation::agent_wire_passes_through(&h.return_type)
3758                                || h.params
3759                                    .iter()
3760                                    .any(|p| serialisation::agent_wire_passes_through(&p.type_ref))
3761                        })
3762                }
3763                _ => false,
3764            });
3765            if passes_through {
3766                parts.push("__AGENT_WIRE_PASS");
3767            }
3768        }
3769        // Events track, slice 0 (spine #936): an agent whose own handler body
3770        // emits directly needs its Workers-mode DO fetch dispatcher to
3771        // rebuild `deps.__eventsDispatch` from `env.EVENTS_FANOUT` (mirrors
3772        // the `#527` `given`-provider rebuild — see `emit_agent`) — a
3773        // function does not survive the JSON wire any better than a
3774        // provider does.
3775        let has_agent_uses_emit = workers
3776            && commons.commons.items.iter().any(|i| match i {
3777                CommonsItem::Agent(a) => a
3778                    .handlers
3779                    .iter()
3780                    .any(|h| block_uses_emit(&h.body, &commons.callees)),
3781                _ => false,
3782            });
3783        if has_agent_uses_emit {
3784            parts.push("__dispatchToEventsFanout");
3785        }
3786        // v0.96 (ADR 0124): an agent whose load-time validation gate fires imports
3787        // the `rehydrationViolation` fault helper.
3788        let has_rehydration_gate = commons.commons.items.iter().any(|i| match i {
3789            CommonsItem::Agent(a) => emit::agent_needs_rehydrate(a, &commons.types),
3790            _ => false,
3791        });
3792        // State-migrations S2 (#2224): a `migrate` step decodes its old shape
3793        // (a failure is a `RehydrationViolation`) and turns a throw into
3794        // `MigrationFailed`.
3795        let has_migration_step = commons.commons.items.iter().any(|i| match i {
3796            CommonsItem::Agent(a) => ctx
3797                .agent_schema_versions
3798                .get(&a.name.name)
3799                .is_some_and(|v| !v.steps.is_empty()),
3800            _ => false,
3801        });
3802        if has_rehydration_gate || has_migration_step {
3803            parts.push("__rehydrationViolation");
3804        }
3805        if has_migration_step {
3806            parts.push("__migrationFailed");
3807        }
3808        // State-migrations S1 (#2211): every agent reports stored keys its
3809        // definition no longer declares; an agent built with the schema lock
3810        // also checks its stored `__v` marker (Decision A: none without one).
3811        if has_agent {
3812            parts.push("__reportUnknownStoredKeys");
3813        }
3814        let has_versioned_agent = commons.commons.items.iter().any(|i| match i {
3815            CommonsItem::Agent(a) => ctx.agent_schema_versions.contains_key(&a.name.name),
3816            _ => false,
3817        });
3818        if has_versioned_agent {
3819            parts.push("__checkStoredSchemaVersion");
3820        }
3821        // State-migrations S4 (#2259 Decision D): on Workers, a versioned
3822        // agent's Durable Object answers `bynk migrate sweep`'s wake.
3823        if workers && has_versioned_agent {
3824            parts.push("__wakeForMigration");
3825        }
3826        // v0.104/v0.105 (real-time track slice 3b): on Workers a `store Map[K,
3827        // Connection]` persists the connection id; its entry ops re-resolve the live
3828        // socket via `resolveConnection` and read a connection's id via `connIdOf`.
3829        if workers
3830            && commons.commons.items.iter().any(|i| match i {
3831                CommonsItem::Agent(a) => emit::agent_has_held_storage(a),
3832                _ => false,
3833            })
3834        {
3835            parts.push("__resolveConnection");
3836            parts.push("__connIdOf");
3837        }
3838        // v0.104/v0.105 (real-time track slice 3b): on Workers a context hosting a
3839        // `from websocket` `on open` accepts the socket inside its Durable Object via
3840        // the hibernatable API — `acceptHibernatableConnection` (accept + tag + wrap),
3841        // a `WebSocketPair`, and the `101` upgrade response. (The service and its
3842        // hosting agent share the one Worker module, so these land in one
3843        // `handlers.ts`.)
3844        let hosts_ws_open = commons.commons.items.iter().any(|i| match i {
3845            CommonsItem::Service(s) => s.handlers.iter().any(|h| {
3846                matches!(
3847                    bynk_lower::lower_handler_kind_ir(&h.kind),
3848                    bynk_ir::IrHandlerKind::Open
3849                )
3850            }),
3851            _ => false,
3852        });
3853        if workers && hosts_ws_open {
3854            parts.push("__acceptHibernatableConnection");
3855            parts.push("__newWebSocketPair");
3856            parts.push("__webSocketUpgradeResponse");
3857        }
3858        // v0.106 (slice 3b-iii): a context with an inbound/close handler re-wraps
3859        // the firing socket as a `WorkersConnection` in `webSocketMessage`/
3860        // `webSocketClose`.
3861        let hosts_ws_inbound = commons.commons.items.iter().any(|i| match i {
3862            CommonsItem::Service(s) => {
3863                matches!(
3864                    bynk_lower::lower_protocol_ir_from_commons(&s.protocol, commons),
3865                    bynk_ir::ProtocolIr::WebSocket { .. }
3866                ) && s.handlers.iter().any(|h| {
3867                    matches!(
3868                        bynk_lower::lower_handler_kind_ir(&h.kind),
3869                        bynk_ir::IrHandlerKind::Message | bynk_ir::IrHandlerKind::Close
3870                    )
3871                })
3872            }
3873            _ => false,
3874        });
3875        if workers && hosts_ws_inbound {
3876            parts.push("__WorkersConnection");
3877        }
3878        if has_http {
3879            // `HttpResult` is both a value (the constructor namespace) and a
3880            // type (the discriminated union). A bare named import brings both
3881            // in — `type HttpResult` would duplicate the identifier.
3882            parts.push(HTTP_RESULT);
3883        }
3884        if has_queue {
3885            // v0.44: `QueueResult` is both a value (the verdict namespace) and a
3886            // type; a bare named import brings both in.
3887            parts.push(QUEUE_RESULT);
3888        }
3889        if workers {
3890            parts.push("type __JsonValue");
3891            parts.push("type __BoundaryError");
3892            parts.push("type __ServiceBinding");
3893            parts.push("__callService");
3894            parts.push("__boundaryError");
3895        } else if uses_codec || has_agent {
3896            // v0.22b: the bundle-mode codec helpers reference JsonValue and
3897            // BoundaryError. v0.96 (ADR 0124): so do an agent's emitted
3898            // rehydration deserialisers and the gate's inline base checks — the
3899            // boundary helpers now emit on bundle too (for the rehydration gate).
3900            parts.push("type __JsonValue");
3901            parts.push("type __BoundaryError");
3902        }
3903        // #1476: `bytes()`/`icu()` fold in here directly now, in the same order
3904        // the old post-print `inject_runtime_imports` pass appended them (bytes
3905        // first, then icu) — `ctx.runtime_use` is already fully populated by the
3906        // time `write_header` runs (moved to the end of `emit_project`), so this
3907        // needs no post-print text surgery at all, just reading the same two
3908        // flags one function call later than before.
3909        //
3910        // Review of #1490: this drops `inject_runtime_imports`'s own
3911        // `missing_bindings` dedup (matching a `type Foo` group binding against
3912        // an already-bound bare `Foo` and vice versa) — safe only because
3913        // neither `BYTES_RUNTIME_IMPORTS`'s nor `MESSAGES_RUNTIME_IMPORTS`'s own
3914        // names overlap anything `parts` already carries above (confirmed by
3915        // direct inspection, not assumed). A future group that *does* overlap
3916        // needs that filter reinstated here, not just appended unconditionally
3917        // — `missing_bindings`'s own doc named exactly this case (the
3918        // test-scaffold module's own `Ok`/`Err` overlap) before this issue
3919        // deleted the only place that reasoning was written down.
3920        if ctx.runtime_use.bytes() {
3921            parts.extend(BYTES_RUNTIME_IMPORTS.trim_start_matches(", ").split(", "));
3922        }
3923        if ctx.runtime_use.bytes_ops() {
3924            parts.extend(
3925                BYTES_OPS_RUNTIME_IMPORTS
3926                    .trim_start_matches(", ")
3927                    .split(", "),
3928            );
3929        }
3930        if ctx.runtime_use.request_headers() {
3931            parts.extend(
3932                REQUEST_HEADERS_RUNTIME_IMPORTS
3933                    .trim_start_matches(", ")
3934                    .split(", "),
3935            );
3936        }
3937        if ctx.runtime_use.eq() {
3938            parts.extend(EQ_RUNTIME_IMPORTS.trim_start_matches(", ").split(", "));
3939        }
3940        if ctx.runtime_use.int() {
3941            parts.extend(INT_RUNTIME_IMPORTS.trim_start_matches(", ").split(", "));
3942        }
3943        if ctx.runtime_use.icu() {
3944            parts.extend(
3945                MESSAGES_RUNTIME_IMPORTS
3946                    .trim_start_matches(", ")
3947                    .split(", "),
3948            );
3949        }
3950        let names: Vec<String> = parts.into_iter().map(str::to_string).collect();
3951        stmts.extend(runtime_files::runtime_import_stmts(
3952            false,
3953            &names,
3954            &runtime_import,
3955        ));
3956        // #1486: no explicit trailing blank stmt here — the printer's
3957        // automatic top-level spacing policy supplies the single blank
3958        // before whatever follows the header (never import-kind at this
3959        // function's own call site, so never exempted from it).
3960    }
3961    stmts
3962}
3963
3964/// Variant of write_header for single-file (no project context) emission.
3965fn write_header_single(
3966    out: &mut String,
3967    commons: &TypedCommons,
3968    runtime_use: &RuntimeUse,
3969    uses_http: bool,
3970    uses_queue: bool,
3971) {
3972    // Arc C, slice 30 (#1392): the same 2-comment-statement conversion
3973    // `write_header` above already established.
3974    out.push_str(&bynk_ts::print_stmt(
3975        &bynk_ts::TsStmt::comment("Generated by bynkc — do not edit by hand.", None),
3976        0,
3977    ));
3978    out.push_str(&bynk_ts::print_stmt(
3979        &bynk_ts::TsStmt::comment(
3980            format!(
3981                "commons {}",
3982                bynk_project::display_unit_name(&commons.commons.name.joined())
3983            ),
3984            None,
3985        ),
3986        0,
3987    ));
3988    writeln!(out).unwrap();
3989    if !commons.commons.items.is_empty() {
3990        // v0.22b: codec imports only when the file uses the `Json` codec.
3991        let uses_codec = !collect_json_codec_roots(commons).is_empty();
3992        let codec_imports = if uses_codec {
3993            ", type JsonError, type __JsonValue, type __BoundaryError"
3994        } else if file_mentions_json_error(commons) {
3995            ", type JsonError"
3996        } else {
3997            ""
3998        };
3999        // v0.110 (ADR 0142): the `Bytes` runtime helpers, imported only when a
4000        // `Bytes` value is constructed or compared in the body.
4001        let bytes_imports = if runtime_use.bytes() {
4002            BYTES_RUNTIME_IMPORTS
4003        } else {
4004            ""
4005        };
4006        // #2016: the `Bytes` operation helpers, imported only when called.
4007        let bytes_ops_imports = if runtime_use.bytes_ops() {
4008            BYTES_OPS_RUNTIME_IMPORTS
4009        } else {
4010            ""
4011        };
4012        // #2341 (L4): the `RequestHeaders` constructor, imported only when a
4013        // body built one (`RequestHeaders.of`/`empty`).
4014        let request_headers_imports = if runtime_use.request_headers() {
4015            REQUEST_HEADERS_RUNTIME_IMPORTS
4016        } else {
4017            ""
4018        };
4019        // #1652: the structural-equality walker, imported only when a
4020        // non-primitive `==`/`!=` lowered to it.
4021        let eq_imports = if runtime_use.eq() {
4022            EQ_RUNTIME_IMPORTS
4023        } else {
4024            ""
4025        };
4026        // #1657: the `Int` domain traps, imported only when a division or a
4027        // Float→`Int` conversion lowered to them.
4028        let int_imports = if runtime_use.int() {
4029            INT_RUNTIME_IMPORTS
4030        } else {
4031            ""
4032        };
4033        // v0.153 (ADR 0177): `HttpResult` is a value (its variant namespace) and
4034        // a type, so it imports without a `type` prefix — one binding serves
4035        // both `HttpResult.NotFound` and the `HttpResult<T>` annotation.
4036        let http_imports = if uses_http { ", HttpResult" } else { "" };
4037        // #1319: `QueueResult` is likewise a value (its verdict namespace) and
4038        // a type; a bare named import brings both in, matching `HttpResult`'s
4039        // own shape immediately above.
4040        let queue_imports = if uses_queue { ", QueueResult" } else { "" };
4041        // The fixed prefix plus each optional group joins as one comma-space
4042        // separated run either way — split back into individual names for
4043        // `TsDecl::Import.names` rather than re-deriving each optional
4044        // group's own name list a second time (the string-building above,
4045        // unchanged, already gets this exactly right).
4046        let inside = format!(
4047            "Ok, Err, Some, None, type Result, type Option, type ValidationError{codec_imports}{bytes_imports}{bytes_ops_imports}{request_headers_imports}{eq_imports}{int_imports}{http_imports}{queue_imports}"
4048        );
4049        let names: Vec<String> = inside.split(", ").map(str::to_string).collect();
4050        for stmt in runtime_files::runtime_import_stmts(false, &names, "./runtime.js") {
4051            out.push_str(&bynk_ts::print_stmt(&stmt, 0));
4052        }
4053        writeln!(out).unwrap();
4054    }
4055}
4056
4057/// v0.110 (ADR 0142): the `Bytes` runtime helpers, appended to a module's
4058/// import list when the emitted body references them. `bytesEqual` backs `==`;
4059/// the base64/UTF-8 helpers back the kernel and codec.
4060pub(crate) const BYTES_RUNTIME_IMPORTS: &str =
4061    ", __bynkBytesEqual, __bynkBytesToBase64, __bynkBytesFromBase64, __bynkBytesDecodeUtf8";
4062
4063/// #2016: the `Bytes` operation helpers (`concat`, `compare`, hex, base64url),
4064/// appended only when the body calls one, so a module that merely builds or
4065/// compares `Bytes` keeps the [`BYTES_RUNTIME_IMPORTS`] list it had.
4066/// #2341 (L4): the `RequestHeaders` constructor a test builds headers with.
4067pub(crate) const REQUEST_HEADERS_RUNTIME_IMPORTS: &str = ", __requestHeaders";
4068
4069pub(crate) const BYTES_OPS_RUNTIME_IMPORTS: &str = ", __bynkBytesConcat, __bynkBytesCompare, __bynkBytesToHex, __bynkBytesFromHex, __bynkBytesToBase64Url, __bynkBytesFromBase64Url";
4070
4071/// #1652 (runtime-semantics track §3.1): the structural-equality walker,
4072/// appended to a module's import list when `==`/`!=` on a non-primitive,
4073/// non-`Bytes` operand lowered to it (`lower_bin_op`).
4074pub(crate) const EQ_RUNTIME_IMPORTS: &str = ", __bynkEq";
4075
4076/// #1657: the `Int` domain traps (`bynk-emit/runtime/src/int.ts`).
4077pub(crate) const INT_RUNTIME_IMPORTS: &str = ", __bynkIntDiv, __bynkToInt";
4078
4079/// message-bundles slice 3 (#878, Decision G): the ICU-formatting runtime
4080/// helpers, appended to a module's import list when an emitted `messages`
4081/// bundle's `render` references any of them (`emit_icu_placeholder`,
4082/// `bynk-emit/src/emitter/emit.rs`).
4083const MESSAGES_RUNTIME_IMPORTS: &str = ", __selectPluralArm, __formatIcuNumber, __formatIcuDate";
4084
4085/// #914: the names an **inlined** boundary deserialiser builds directly, appended
4086/// to the import list of a module that curates its own — a Worker's `compose.ts`
4087/// and the test-scaffold modules. Every other module imports `Ok`/`Err`/`Result`
4088/// unconditionally, so most of this is inert there and never applied.
4089///
4090/// A codec for a *named* type delegates (`handlers.deserialise_Order(…)`) and needs
4091/// none of these; one for a base type or a `Bytes` inlines the construction. Two
4092/// arms — `Unit` and the runtime-owned error types — additionally annotate the
4093/// result (`Ok(undefined) as Result<void, BoundaryError>`), which `compose.ts`'s
4094/// structural list never carries; `Result` is in the group for those. The dedupe
4095/// every real caller applies this group through (`emitter/workers.rs`'s own
4096/// `append_missing_bindings`, `project/tests_emit.rs`'s own copy — #1486
4097/// retired this doc's own `inject_runtime_imports`, the pre-conversion
4098/// post-print-text-splice mechanism both are the tree-level successor to)
4099/// makes it free wherever it is already imported.
4100pub(crate) const BOUNDARY_CODEC_RUNTIME_IMPORTS: &str =
4101    ", Ok, Err, type Result, type __BoundaryError";
4102
4103/// #914: the names the `Json.decode[T]` wrapper puts in the module — its own
4104/// `Result<T, JsonError>` signature and the `JsonValue` it parses into
4105/// (`lower_json_codec_call`, `bynk-emit/src/emitter/lower.rs`).
4106///
4107/// A sibling group rather than part of [`BOUNDARY_CODEC_RUNTIME_IMPORTS`]: the
4108/// producer is the wrapper, not the codec, and it names these whichever arm the
4109/// inner deserialiser takes — including the delegating ones, which set no
4110/// boundary-codec flag at all. `Json.encode` needs nothing for its own wrapper
4111/// text either (it lowers to a bare `JSON.stringify`).
4112///
4113/// The delegating arm — `Json.decode[SomeRecord]` / `Json.encode(someRecord)`
4114/// — used to be broken in a test-scaffold module for an unrelated reason
4115/// (issue #917): the call lowers to a bare `deserialise_SomeRecord(…)` /
4116/// `serialise_SomeRecord(…)`, and no such codec was emitted anywhere — the
4117/// unit module exports the record's interface and no codec of its own. Fixed
4118/// by generating the test module's *own* closure for every root a case body's
4119/// `Json` call reaches for (`RuntimeUse::note_json_codec_root`, drained by
4120/// `tests_emit.rs`'s `emit_test_module`), namespace-qualifying the TS type
4121/// positions through the target/`uses` unit's own namespace import
4122/// (`RuntimeUse::json_codec_qual`) — the same caller-generates-its-own-codec
4123/// pattern `emit_consumed_context_helpers` uses for a workers cross-context
4124/// caller's consumed-boundary types (#661). See
4125/// `918_json_decode_in_test_case` (base type, delegation-free) and
4126/// `919_json_decode_named_record_in_test_case` (named record, delegating).
4127pub(crate) const JSON_CODEC_RUNTIME_IMPORTS: &str =
4128    ", Ok, Err, type Result, type __JsonValue, type JsonError";
4129
4130/// Emit the commons-level doc block (if any) at the current position.
4131/// #1478: real-node-internally already (a single `TsStmtKind::DocComment`,
4132/// the same shape [`emit_doc_block`] itself builds — inlined directly here
4133/// rather than calling that `out: &mut String`-shaped helper, since its own
4134/// signature and 14 real callers stay unchanged, #1333's own doc).
4135fn write_commons_doc(commons: &TypedCommons) -> Vec<bynk_ts::TsStmt> {
4136    let mut stmts = Vec::new();
4137    if let Some(doc) = &commons.commons.documentation {
4138        stmts.push(bynk_ts::TsStmt::doc_comment(doc.as_str(), None));
4139        // #1486: no explicit trailing blank stmt here — `DocComment` (unlike
4140        // `Comment`) is never exempted from the printer's automatic
4141        // top-level spacing policy, so the single blank before whatever
4142        // follows is already supplied for free.
4143    }
4144    stmts
4145}
4146
4147/// The module-level state-registry constant name for an agent class.
4148fn agent_registry_name(agent: &str) -> String {
4149    format!("__{agent}Registry")
4150}
4151
4152/// The exported agent-construction factory name for an agent class.
4153pub(crate) fn agent_factory_name(agent: &str) -> String {
4154    format!("__make{agent}")
4155}
4156
4157/// Lowering state that is genuinely **module-invariant**: the same value at
4158/// every body-lowering site within one emitted module, and never written by the
4159/// recursive lowering itself. Grouped out of [`LowerCtx`] so a new lowering kind
4160/// inherits the whole set wholesale instead of re-deriving each default.
4161///
4162/// Nothing the lowering mutates *per body* may move in here — see the
4163/// scratch-state fields at the bottom of [`LowerCtx`]. A `ModuleCtx` is still
4164/// built fresh alongside each `LowerCtx` today, but filing a per-body counter
4165/// under a name that says "module" is exactly how such state starts leaking
4166/// between handler bodies.
4167pub(crate) struct ModuleCtx<'a> {
4168    /// Typed-commons handle (used to look up receiver types for method-call
4169    /// UFCS lowering).
4170    commons: &'a TypedCommons,
4171    /// Cross-context info for v0.6 cross-context call lowering.
4172    cross_context: &'a bynk_check::resolver::CrossContextInfo,
4173    /// The emitted module's conditional-runtime-helper accumulator.
4174    ///
4175    /// The `Bytes` lowerings (kernel, `==`, base64 codec) call
4176    /// [`RuntimeUse::note_bytes`] through this, so the module's import line is
4177    /// decided from what lowering actually emitted rather than by scanning the
4178    /// generated text for the helper's own name.
4179    ///
4180    /// Required rather than optional: a missing accumulator would make
4181    /// `note_bytes` a silent no-op, which is exactly the failure this replaces —
4182    /// a module that references `__bynkBytesEqual` without importing it. A
4183    /// lowering whose imports are decided elsewhere (the test scaffolds) owns a
4184    /// throwaway one, which reads as the deliberate choice it is.
4185    runtime_use: &'a RuntimeUse,
4186    /// v0.8 build target. In workers mode cross-context calls lower to
4187    /// `callService(...)` instead of `deps.surface.<key>.<method>(...)`.
4188    target: BuildTarget,
4189    /// #527: agent → method → the method's `given` caps (mirrors
4190    /// [`crate::project::EmitProjectCtx::agent_method_givens`]). Consulted by
4191    /// the agent-call lowering to record capability requirements.
4192    agent_method_givens: HashMap<String, HashMap<String, Vec<bynk_ir::CapRefIr>>>,
4193    /// Events slice 3b (#978): each locally-declared event's resolved
4194    /// `@schema(N)` version (mirrors
4195    /// [`crate::project::EmitProjectCtx::event_schema_versions`]). Default-
4196    /// empty like `agent_method_givens`, not a required constructor
4197    /// parameter like `runtime_use` — a miss here degrades to `schemaVersion:
4198    /// 1`, exactly every event's behaviour before this map existed, not a
4199    /// hard failure the way a missing `runtime_use` would be.
4200    event_schema_versions: HashMap<String, i64>,
4201    /// #2058: the namespace each consumed context's type is named under in
4202    /// this module (mirrors [`crate::emitter::emit::consumed_type_ns`]).
4203    /// Default-empty: a miss names the type bare, as before.
4204    consumed_type_ns: HashMap<String, String>,
4205    /// #934: true when the unit being emitted is the reserved first-party
4206    /// `bynk` adapter itself. `bynk` is a reserved namespace, so a capability
4207    /// literally named `Idempotency` declared *in this unit* is unambiguously
4208    /// the real one — used alongside `CrossContextInfo::flattened_caps` (the
4209    /// consumed-from-elsewhere case) to confirm a flattened `Idempotency`
4210    /// call is genuinely first-party before scoping its key, not a same-named
4211    /// capability some other adapter or context happens to declare.
4212    in_bynk_unit: bool,
4213    /// #1961: [`crate::project::EmitProjectCtx::agent_called_contexts`].
4214    agent_called_contexts: Vec<String>,
4215    /// #2045: [`crate::project::EmitProjectCtx::consumed_context_types`]. A
4216    /// variant of one of these sums is built inline, not through the callee's
4217    /// companion object, so the callee's module is never a value import.
4218    consumed_context_types: HashSet<String>,
4219}
4220
4221impl<'a> ModuleCtx<'a> {
4222    fn new(
4223        commons: &'a TypedCommons,
4224        cross_context: &'a bynk_check::resolver::CrossContextInfo,
4225        runtime_use: &'a RuntimeUse,
4226    ) -> Self {
4227        Self {
4228            commons,
4229            cross_context,
4230            runtime_use,
4231            target: BuildTarget::Bundle,
4232            agent_method_givens: HashMap::new(),
4233            event_schema_versions: HashMap::new(),
4234            consumed_type_ns: HashMap::new(),
4235            in_bynk_unit: false,
4236            agent_called_contexts: Vec::new(),
4237            consumed_context_types: HashSet::new(),
4238        }
4239    }
4240}
4241
4242/// The lowering state shared by the four **capability-bearing** body kinds — a
4243/// service handler, a composed provider op, an agent handler, and a websocket
4244/// lifecycle DO method. Every other kind carries no `HandlerShared` at all, and
4245/// the [`LowerCtx`] accessors below hand those kinds the same defaults the flat
4246/// struct used to give them (an empty capability set, no scope, `deps`).
4247pub(crate) struct HandlerShared {
4248    /// Names of capabilities in scope as `given C1, C2, ...`. Used to lower
4249    /// `Capability.op(args)` calls to `deps.Capability.op(args)`.
4250    capabilities: HashSet<String>,
4251    /// #934: the calling handler's own qualified name (`<unit>.<service or
4252    /// agent>.<handler>`, e.g. `shop.reserve.ordering.call`). Read only by the
4253    /// `Idempotency.dedup`/`remember` lowering, which prefixes the
4254    /// developer-supplied key with it so two unrelated handlers using the same
4255    /// literal key never collide (design/tracks/idempotency-capability.md
4256    /// §3.4). `None` anywhere a capability call cannot occur (a plain method, a
4257    /// free fn, an invariant/transition predicate, a static field initialiser) —
4258    /// those kinds carry no `HandlerShared`, and the accessor reports `None`.
4259    handler_scope: Option<String>,
4260    /// Events track, slice 2 (spine #936): the qualified name of the unit
4261    /// this body is emitted into (`ctx.commons_name`), read by the
4262    /// `Events.emit[E](event)` lowering to mint the envelope's
4263    /// `publisherId`. Context-scoped rather than agent-scoped: `Events.emit`
4264    /// is legal from a plain, keyless service handler with no agent
4265    /// instance to report, so this is the only identity available
4266    /// uniformly at every legal emission site (an amendment to
4267    /// design/bynk-design-notes.md §7's "the publisher is the emitting
4268    /// agent" framing — see the events-envelope ADR). Always populated
4269    /// alongside `handler_scope` at every construction site; never `None`
4270    /// in practice for a body that could contain an `Events.emit` call.
4271    owning_context: String,
4272    /// v0.12: the receiver expression a capability call resolves against —
4273    /// `deps` in a handler body, `this.deps` in a composed provider body.
4274    cap_deps_expr: String,
4275    /// The consumed contexts the current handler calls across the boundary.
4276    /// Non-empty drives whether `deps` gets a `surface` field type; in
4277    /// workers mode it is also exactly the Service Bindings the handler's
4278    /// `deps.env` type names (#1961).
4279    cross_contexts_called: std::collections::BTreeSet<String>,
4280    /// v0.9.2: set when the body instantiates a local agent. In workers mode
4281    /// this drives `env` (carrying the DO namespaces) into the handler's deps
4282    /// type so the agent factory can reach its Durable Object binding.
4283    agents_instantiated: bool,
4284    /// #527: capabilities required by agent methods this body calls, keyed by
4285    /// deps key. After body lowering these widen the handler's deps *type* to
4286    /// match the runtime value compose builds (which always carried them).
4287    agent_given_caps_used: std::collections::BTreeMap<String, bynk_ir::CapRefIr>,
4288}
4289
4290impl Default for HandlerShared {
4291    fn default() -> Self {
4292        Self {
4293            capabilities: HashSet::new(),
4294            handler_scope: None,
4295            owning_context: String::new(),
4296            cap_deps_expr: "deps".to_string(),
4297            cross_contexts_called: std::collections::BTreeSet::new(),
4298            agents_instantiated: false,
4299            agent_given_caps_used: std::collections::BTreeMap::new(),
4300        }
4301    }
4302}
4303
4304/// The lowering state shared by the three **generated test-scaffold** body
4305/// kinds — a `stub`/`where`/`requires` predicate value, a unit test case, and an
4306/// integration test case.
4307#[derive(Default)]
4308pub(crate) struct TestShared {
4309    /// True when lowering **any** generated test-scaffold body — distinct from
4310    /// `assert_loc`, which is only ever `Some` for the two real `case` bodies
4311    /// and carries an unrelated payload (a diagnostic location). Kept as its own
4312    /// field (Locale capability track, slice 1, #844 review) rather than
4313    /// overloading `assert_loc.is_some()`, since that conflated "has a location"
4314    /// with "is test scaffolding" for a caller that has no location to give.
4315    test_scaffold: bool,
4316    /// v0.59: the source text and project-relative path of the file the body
4317    /// came from, so an `assert` can emit a real `path:line:col` location (for
4318    /// `--format json` click-through) rather than a bare byte offset. Stays
4319    /// `None` for a predicate scaffold, which emits no `assert`.
4320    assert_loc: Option<AssertLoc>,
4321}
4322
4323/// The `store`-agent sub-state of an [`BodyMode::AgentHandler`] body: present
4324/// only when the hosting agent is a `store` agent, absent for a plain
4325/// state-record agent (whose handler reads `currentState`/`self.state` instead).
4326pub(crate) struct AgentStoreState {
4327    /// v0.81 (storage track): the name of the mutable working-state variable
4328    /// (`__state`) and the set of `Cell` field names. A bare `Cell` read lowers
4329    /// to `<var>.<cell>`, and a `cell := v` write lowers to `<var>.<cell> = <v>`
4330    /// — read-your-writes via the in-memory record, flushed once at handler end
4331    /// (ADR 0109).
4332    state: (String, HashSet<String>),
4333    /// v0.82 (ADR 0110): the agent's `store` `Map` field names. A method call
4334    /// whose receiver is one lowers to an entry operation over `__state.<map>`
4335    /// (a JSON-serialisable `Record<string, V>`), staged in the working record
4336    /// and flushed at commit like any other state field.
4337    maps: HashSet<String>,
4338    /// v0.83: the agent's `store` `Set` field names. A method call whose
4339    /// receiver is one lowers to an entry operation over `__state.<set>` (a
4340    /// `Record<string, boolean>`), staged in the working record.
4341    sets: HashSet<String>,
4342    /// v0.87 (ADR 0113): the agent's `store` `Cache` fields (name → ttl millis).
4343    /// A method call whose receiver is one lowers to an entry op over
4344    /// `__state.<cache>` (a `Record<string, { v, exp }>`), applying TTL expiry
4345    /// against the injected `Clock`.
4346    caches: HashMap<String, i64>,
4347    /// v0.95 (ADR 0121): the agent's `store` `Log` fields (name → optional
4348    /// `@retain` millis). `<log>.append` pushes `{ t: now(), v }` to
4349    /// `__state.<log>` (an array) and prunes past the retain horizon; the
4350    /// time-window roots / builders lower to a query pipeline over the array.
4351    logs: HashMap<String, Option<i64>>,
4352    /// v0.93 (ADR 0118): the agent's `@indexed` secondary indexes (map name →
4353    /// the value-record fields indexed on). A mutating op on the map maintains a
4354    /// sibling posting-list `Record<string, string[]>` per field (`<map>__idx_<f>`);
4355    /// an equality `filter` on an indexed field routes to a posting lookup.
4356    indexes: HashMap<String, Vec<String>>,
4357    /// v0.104/v0.105 (real-time track slice 3b): the agent's held `store Map[K,
4358    /// Connection]` fields (name → the connection's **frame type** `F`, e.g.
4359    /// `ServerFrame`). On Workers these persist `K → connId` in the durable state
4360    /// record; a method call whose receiver is one lowers to an entry op over
4361    /// `__state.<map>` (the connId record) with `connIdOf`/`resolveConnection<F>` —
4362    /// not the plain `Record<string, V>` ops (held maps are excluded from
4363    /// [`AgentStoreState::maps`]).
4364    held_maps: HashMap<String, String>,
4365    /// Review of #1460: a plain `store Map[K, V]` field's own value type `V`
4366    /// (name → its rendered TS type), for `lower_query_method`'s own
4367    /// collection-kernel sites (`distinctBy`/`joinOn`/`leftJoin`/`groupBy`)
4368    /// when the receiver is a plain map's own value scan lifted into the
4369    /// general query vocabulary — mirrors `held_maps`'s own construction
4370    /// exactly, just for the non-held case.
4371    map_values: HashMap<String, String>,
4372    /// Review of #1460: a `store Log[T]` field's own element type `T` (name →
4373    /// its rendered TS type), for the identical reason `map_values` exists —
4374    /// a Log's value scan is also lifted into the general query vocabulary.
4375    log_values: HashMap<String, String>,
4376}
4377
4378/// What [`LowerCtx`] is lowering *right now*. One variant per real body-emission
4379/// site; each carries exactly the state that site populates and nothing else, so
4380/// "not applicable to this kind" is expressed in the type rather than left
4381/// indistinguishable from "deliberately defaulted".
4382pub(crate) enum BodyMode {
4383    /// A type's method body (`emit_method`).
4384    Method,
4385    /// A free function body (`emit_free_fn`).
4386    FreeFn,
4387    /// An agent field's static initialiser expression (`emit_agent`).
4388    StaticInit,
4389    /// v0.80: an agent invariant predicate. Carries the name of the
4390    /// proposed-state variable (the `commitState` parameter) and the set of
4391    /// state field names — a bare ident matching a state field lowers to
4392    /// `<var>.<field>`, since invariants read state fields directly (§14).
4393    Invariant {
4394        name: String,
4395        fields: HashSet<String>,
4396    },
4397    /// v0.116 (testing track slice 4): a `transition` predicate. Carries the JS
4398    /// names bound to the contextual `old` and `new` state records. The Bynk
4399    /// identifiers `old`/`new` lower to these (`new` is a JS reserved word, so
4400    /// both are renamed), and field access `old.<field>` reads off the `old`
4401    /// record.
4402    Transition { old: String, new: String },
4403    /// A service handler body (`emit_service`).
4404    ServiceHandler {
4405        handler: HandlerShared,
4406        /// v0.47: the `by` binder whose `.identity` is threaded through `deps`
4407        /// (so `<binder>.identity` lowers to `deps.identity` rather than the
4408        /// unit-value `undefined`).
4409        deps_identity_binder: Option<String>,
4410        /// v0.52: when lowering a multi-actor sum handler body, the `by` binder
4411        /// that names the resolved-actor value (threaded through `deps`, so the
4412        /// binder ident lowers to `deps.who` — the tagged union the body
4413        /// `match`es).
4414        actor_sum_binder: Option<String>,
4415    },
4416    /// A composed provider's operation body (`emit_provider`).
4417    ProviderOp { handler: HandlerShared },
4418    /// An agent handler body (`emit_agent`).
4419    AgentHandler {
4420        handler: HandlerShared,
4421        /// True when lowering an agent handler body. Used to rewrite
4422        /// `self.<keyField>` access into the appropriate local.
4423        in_agent_handler: bool,
4424        /// The name of the agent's `key id` field (so `self.<id>` resolves).
4425        agent_key_field: Option<String>,
4426        /// The `store`-agent working-record state, when the hosting agent is a
4427        /// `store` agent. Boxed: it is by far the largest payload in this enum,
4428        /// and every other body kind would otherwise pay for it.
4429        store: Option<Box<AgentStoreState>>,
4430    },
4431    /// A websocket lifecycle method on the hosting Durable Object
4432    /// (`emit_ws_do_method`).
4433    WsDoMethod {
4434        handler: HandlerShared,
4435        /// v0.47: as [`BodyMode::ServiceHandler::deps_identity_binder`].
4436        deps_identity_binder: Option<String>,
4437        /// v0.104 (real-time track slice 3b): when lowering a `from websocket`
4438        /// `on open` body **into its hosting Durable Object** (the agent the
4439        /// upgrade transfers the connection to), the name of that agent. A
4440        /// transfer call `<Agent>(<key>).method(args)` whose `<Agent>` is this
4441        /// self-agent lowers to a direct `this.method(args, deps)` self-call
4442        /// rather than the cross-instance `__make<Agent>(key)` factory — the
4443        /// connection is already in this DO, so it never crosses an RPC boundary
4444        /// (DECISION A).
4445        ws_self_agent: Option<String>,
4446    },
4447    /// A `stub`/`where`/`requires` predicate value lowered via
4448    /// `lower_block_to_async_body` — test/property/contract scaffolding, never a
4449    /// real production provider body.
4450    PredicateScaffold { test: TestShared },
4451    /// A unit test `case` body (`lower_test_case_body`).
4452    TestCase {
4453        test: TestShared,
4454        /// v0.117 (testing track slice 5): the name of the recorded-call trace
4455        /// object (`__obs`), over which an observation (`Cap.op called …`) and
4456        /// `trace(Cap.op)` are lowered.
4457        observation_trace: Option<String>,
4458        /// v0.7: the target context's local service names. A `service.call(args)`
4459        /// or `service(args)` invocation where `service` is in this set lowers to
4460        /// `<service>.call(args, deps)` so the test wires its `deps` through.
4461        test_services: HashSet<String>,
4462        /// v0.182 (#664): the ordered handler kinds of each test service, so a
4463        /// cron (`svc.schedule("…")`) or queue (`svc.message(m)`) address can
4464        /// recover the position index the emitted key encodes (`cron_<svc>_<i>` /
4465        /// `queue_…`). http keys are a pure function of verb + path and need no
4466        /// lookup here. P6.37 (design/tracks/the-ir.md §6a): `IrHandlerKind`
4467        /// (P6.24a's own pure, unconditional mirror), not the raw AST
4468        /// `HandlerKind` this field used to store.
4469        test_service_handlers: HashMap<String, Vec<bynk_ir::IrHandlerKind>>,
4470        /// I3 (#1971), #2020: the `(service, key)` handlers that bind an actor
4471        /// sum, whose `deps.who` a call-site principal builds. `key` is
4472        /// `"call"` for an `on call` handler and the emitted method name
4473        /// (`http_handler_method_name_ir`) for an http route.
4474        sum_handlers: HashSet<(String, String)>,
4475        /// #1994: the target has a handler that sends with `~>`, so `deps`
4476        /// carries the `__exec` double and each addressed service call is
4477        /// wrapped in `__bynkSettle(deps, …)`.
4478        settle_sends: bool,
4479    },
4480    /// An integration test `case` body (`lower_integration_case_body`).
4481    IntegrationCase {
4482        test: TestShared,
4483        /// v0.182 (Slice B, #667): the target's http service names. An http
4484        /// address on one of these lowers to a driver call
4485        /// (`__sysdrive_<svc>_<key>(args, sub)`) that drives a real
4486        /// `worker.fetch` with a signed credential, instead of the unit-tier
4487        /// direct handler call. Empty at the unit tier.
4488        system_http_services: std::collections::HashSet<String>,
4489        /// #707: the declared `(service, method, path)` http routes of the system
4490        /// target. A `(method, path)` call whose method is absent here but whose
4491        /// path is present is a **wrong-method** call — it drives the `405`
4492        /// fall-through through the generic `__sysdrive_wrongmethod_<svc>` driver.
4493        system_http_routes: std::collections::HashSet<(String, String, String)>,
4494        /// #708: for each declared `(service, method, path)` route that has a
4495        /// body param, the body's zero-based position among the route's
4496        /// positional call args (i.e. within `args[1..]`, matching handler-param
4497        /// declaration order) and its declared type. The raw driver
4498        /// (`__sysdrive_raw_*`, Slice C) forwards every slot as a `string`; a
4499        /// `Wire(…)` arg already lowers to that raw string, but a *typed* arg
4500        /// mixed into the same call must be converted: the body slot serialises
4501        /// through the same wire codec the typed driver uses
4502        /// (`JSON.stringify(serialise_expr_via(...))`), any other (path) slot
4503        /// just coerces via `String(...)`. Absent for a bodyless route.
4504        system_http_route_body:
4505            HashMap<(String, String, String), (usize, bynk_syntax::ast::TypeRef)>,
4506        /// #708: the type namespace (`<target>.`) `serialise_expr_via` needs to
4507        /// resolve a body param's custom codec when converting a typed arg for
4508        /// the raw driver. Mirrors the `type_ns` `emit_system_http_support`
4509        /// computes from the same suite target.
4510        system_http_type_ns: String,
4511        /// #2019: the refinement-guarded routes and the claims each
4512        /// refinement actor signs, for a call acting as a refinement.
4513        system_http_claims: Box<SystemHttpClaims>,
4514        /// #2136: the target's cron and queue services, driven at `system`
4515        /// through the Worker's `scheduled` / `queue` entry points.
4516        system_jobs: SystemJobServices,
4517    },
4518}
4519
4520/// #2136: the cron and queue services a `system`-tier case drives through the
4521/// participant Worker's real `scheduled` and `queue` entry points. A
4522/// `svc.schedule("<expr>", …)` address on a cron service here lowers to
4523/// `__sysdrive_schedule_<svc>_<index>(…)`, and `q.message(m)` on a queue
4524/// service to `__sysdrive_message_<q>(m)` (`tests_emit::emit_system_job_support`).
4525#[derive(Debug, Clone, Default)]
4526pub struct SystemJobServices {
4527    /// Each cron service's schedule expressions, in declaration order (the
4528    /// position is the handler's `cron_<svc>_<index>` index).
4529    pub cron: HashMap<String, Vec<String>>,
4530    /// The queue services.
4531    pub queue: HashSet<String>,
4532}
4533
4534/// #2019: what a `system`-tier call needs to sign the claims a refinement
4535/// actor's predicate checks (`actor Admin = User where hasClaim("admin")`).
4536/// A route guarded by a refinement has drivers that take a trailing claims
4537/// object, signed into the JWT next to `sub`; a call acting as a refinement
4538/// actor passes that actor's witness claims, and any other call passes `{}`,
4539/// so the Worker's seam answers `403` exactly as it would a real token
4540/// lacking the claim.
4541#[derive(Debug, Clone, Default)]
4542pub struct SystemHttpClaims {
4543    /// The `(service, method, path)` routes whose `by` actor is a refinement.
4544    pub routes: HashSet<(String, String, String)>,
4545    /// Each refinement actor's witness claims, as a JavaScript object literal
4546    /// (`bynk_check::actors::claim_witness_js`).
4547    pub actors: HashMap<String, String>,
4548    /// #2155: each actor-sum route with a credentialed member, and its
4549    /// primary member — the actor a call with no principal acts as. Its
4550    /// signing drivers take the acting actor's name.
4551    pub sum_routes: HashMap<(String, String, String), String>,
4552}
4553
4554/// Per-body lowering context: what module we are emitting into ([`ModuleCtx`]),
4555/// what kind of body we are lowering ([`BodyMode`]), and the scratch state the
4556/// recursive lowering accumulates as it goes.
4557///
4558/// Everything below `mode` is deliberately **not** in `ModuleCtx`: a fresh
4559/// `LowerCtx` is built at every body-emission site and never reused across two
4560/// bodies, so these are implicitly reset per body today. Moving any of them up a
4561/// level would leak state between handlers in the same module — most visibly the
4562/// `next_tmp` counter, which would stop restarting `__r0` at each function and
4563/// so rename every generated temp in the emitted TypeScript.
4564pub(crate) struct LowerCtx<'a> {
4565    module: ModuleCtx<'a>,
4566    mode: BodyMode,
4567    /// Agent names declared in the surrounding context. Drives lowering of
4568    /// `Agent(key)` (to `new Agent(makeTestState(String(key)))`) and of
4569    /// `agent_instance.method(args)` (to `instance.method(args, deps)`) in
4570    /// service and agent-handler bodies. Populated by the caller for non-test
4571    /// emission and from the *test's own* agent set in test emission — which is
4572    /// why this is not a [`ModuleCtx`] field despite being module-wide at the
4573    /// nine non-test sites.
4574    pub local_agents: HashSet<String>,
4575    /// v0.154 (ADR 0178): the enclosing function/handler's resolved return type,
4576    /// set at each body-emission site that has one. The `?` lowering reads it to
4577    /// decide whether a declared error embedding (`embeds E as V`) converts the
4578    /// propagated `Err` — via the same `embedding_for` rule the checker used.
4579    /// Genuinely cross-cutting rather than kind-specific: it is saved/restored
4580    /// around lambda bodies, `?`-embedding and match arms *within* whichever
4581    /// kind is being lowered.
4582    return_ty: Option<bynk_check::checker::TyId>,
4583    next_tmp: u32,
4584    /// #908: a stack of per-block frames tracking `let`/`let <-` names that
4585    /// needed a fresh emitted identifier because the name was already bound
4586    /// by an enclosing (or the same) block's `let` — the checker allows
4587    /// re-binding a name (`let x = 1; let x = x + 1`, a deliberate ML-family
4588    /// idiom, ADR 0064), but each `let` still lowers to its own `const`, so
4589    /// without renaming a same-block re-`let` collides with the first
4590    /// (TS2451), and a nested block's re-`let` — while a legal *redeclaration*
4591    /// on its own — would put an RHS read of the outer binding in its own
4592    /// declaration's temporal dead zone. Pushed/popped in lock-step with
4593    /// [`emit_block_inner`] — the single choke point every block (function,
4594    /// lambda, if/else branch, match arm) lowers through — so a read
4595    /// (`lower_ident`, and the agent-dispatch receiver text) resolves a name
4596    /// by walking the stack innermost-out and falls back to the natural
4597    /// `ts_ident` name when no frame renamed it.
4598    pub shadow_scopes: Vec<HashMap<String, String>>,
4599    /// #2024: identifiers the generated scaffold around this body already
4600    /// binds in the body's own JS scope — a test runner's `deps`, `__obs`, and
4601    /// the target's destructured services, agents, functions and assets. A
4602    /// `let` whose natural name is one of these gets a fresh name from
4603    /// [`Self::bind_local_name`], as a re-`let` does: the scaffold binding is
4604    /// not a Bynk local (so [`Self::is_local`] ignores it), but a second
4605    /// `const` of its name in that scope is `TS2451` under `tsc`.
4606    pub harness_bound: HashSet<String>,
4607    /// When an `is` receiver is not a simple, repeatable lvalue (e.g. a call
4608    /// like `parse(x) is Ok(n)`), it is evaluated once into a temp; the temp
4609    /// name is cached here keyed by the receiver expression's span so the
4610    /// `.tag` check and every pattern binding reference the *same* single
4611    /// evaluation. Simple receivers (idents / field chains) are never cached
4612    /// and continue to be rendered inline as before.
4613    is_receiver_temps: HashMap<bynk_syntax::span::Span, String>,
4614    /// #1654: while the right operand of an `is`-binding `&&`/`implies` is
4615    /// lowered (inside the IIFE that keeps it lazy), an `is` receiver temp is
4616    /// *declared* here (`let __rN!: T;`) and only *assigned* in place. The
4617    /// caller emits these declarations before the whole condition, so an `if`
4618    /// then-branch's binding that reads the temp (`const m = __r1 as Q;`)
4619    /// finds it in scope. `None` outside such a right operand. #1751: also set
4620    /// around the right operand of a plain `&&`/`||`/`implies`.
4621    pub(crate) is_temp_hoist: Option<Vec<String>>,
4622    /// #1752 (generalising #1751): the `is` tests, by span, whose tag test was
4623    /// emitted out of line: inside a short-circuit right operand that lowered
4624    /// to an arrow or a hoisted `if`. TypeScript cannot carry such a test's
4625    /// narrowing to a binding read elsewhere, so `emit_is_test_bindings` reads
4626    /// those bindings through a cast to the variant the checker proved.
4627    pub(crate) unnarrowed_is_tests: HashSet<bynk_syntax::span::Span>,
4628    /// Variable bindings that point at agent instances. Updated by the
4629    /// statement emitter when it sees `let x = AgentName(key)`. Used by
4630    /// the method-call lowering so `x.method(args)` resolves through
4631    /// the agent's class rather than via the receiver-namespace lookup.
4632    pub local_agent_vars: HashMap<String, String>,
4633    /// v0.182 (#664): while lowering an `EffectLet` whose value addresses a
4634    /// service handler, the call-site principal's identity expression (already
4635    /// lowered), if the statement carries `by <Actor>(<identity>)`. The
4636    /// address-call lowering reads it to build the handler's `deps.identity`.
4637    /// `None` for a unit-identity actor or a non-principal statement.
4638    pub call_site_identity: Option<String>,
4639    /// #2380 (L5): the call-site principal's claims (already lowered), for an
4640    /// `Oidc` handler's `deps.claims`.
4641    pub call_site_claims: Option<String>,
4642    /// I3 (#1971): the call-site principal's actor name, beside
4643    /// [`Self::call_site_identity`], for a `call` sum's `deps.who`.
4644    pub call_site_actor: Option<String>,
4645    /// #706: the call-site principal is `by Nobody` — drive the route with no
4646    /// `Authorization` header so the real auth seam rejects it (`401` →
4647    /// `Rejected(Unauthorized)`). Routes a `system` http address to the no-auth
4648    /// driver. `false` for any other (or no) principal.
4649    pub call_site_no_credential: bool,
4650    /// #2135: the call-site principal is `by Forged` or `by Stale` — drive a
4651    /// `Signature` route with a signature its seam refuses (`"forged"` /
4652    /// `"stale"`, the driver's name). `None` for any other principal.
4653    pub call_site_bad_signature: Option<&'static str>,
4654    /// Slice 1 (ADR 0103): the source-map builder for the file being emitted, if
4655    /// any. The deep lowering chain records `(generated offset → source span)`
4656    /// checkpoints here; `emit_project` owns the `RefCell` and threads a shared
4657    /// borrow in. `None` for the single-file `emit()` path and any body emitted
4658    /// outside a project, where no map is produced.
4659    pub source_map: Option<&'a RefCell<SourceMapBuilder>>,
4660    /// T2.2 (R6.4): set at the two statement sites that emit a literal `await`
4661    /// (`EffectLet`, `Do`) and read-and-reset around a value-position `match`/`if`
4662    /// IIFE's own body construction — the flag a synchronous arrow reads to decide
4663    /// whether it must become `async` and be awaited at its call site. Replaces a
4664    /// scan of the built string for the substring `"await "`, which over-matched
4665    /// on a self-contained `async (...) => {...}` embedded as an arm's value (an
4666    /// iterator terminal like `forEach`) without anything in *this* arrow's own
4667    /// scope needing to await. Not isolated around a lambda body — a nested
4668    /// effectful lambda still marks the enclosing IIFE async, exactly as the old
4669    /// scan did (its own body text also contained `"await "`); closing that is a
4670    /// separate, unscoped defect, not this one.
4671    pub(crate) emitted_await: bool,
4672    /// T2.3 (R6.3): set when lowering a `?` pushes a propagating early-return
4673    /// statement (`if (...) return ...;`) into the current `Pre`. Read (and
4674    /// reset) around a short-circuit operand's own lowering in `lower_bin_op`/
4675    /// `lower_and_with_is`, so those can tell a hoisted `?` apart from an
4676    /// ordinary hoisted statement: a plain `(() => { ...; return expr; })()`
4677    /// wrap is safe for the latter (nothing inside needs to escape the arrow)
4678    /// but captures the former's `return` instead of letting it exit the
4679    /// enclosing function — the residual gap `hoist_if_as_statement` (built for
4680    /// T2.1's `if`-hoisting) also closes here, once this flag says it's needed.
4681    pub(crate) emitted_early_return: bool,
4682    /// #1750: where a block's tail value goes. `None` (the default) is a
4683    /// `return`, the right sink for a function, lambda or arrow body. `Some` is
4684    /// set while a value-position `if`/`match`/block that contains a `?` is
4685    /// emitted as a real statement rather than an arrow: each tail assigns the
4686    /// slot and breaks out of the labelled block wrapping the statement, so
4687    /// the `?`'s own `return` still exits the enclosing function. Every arrow
4688    /// and function boundary resets it to `None` for its own body.
4689    pub(crate) tail_slot: Option<TailSlot>,
4690}
4691
4692/// #1750: the slot and label a statement-form value expression's tails assign
4693/// and break to. See [`LowerCtx::tail_slot`].
4694#[derive(Clone)]
4695pub(crate) struct TailSlot {
4696    pub(crate) slot: String,
4697    pub(crate) label: String,
4698}
4699
4700/// v0.59: the source context an `assert` lowering needs to turn its span into a
4701/// `path:line:col` location. Owned (cloned once per test-case body) to keep the
4702/// lowering free of extra lifetime threading; test-file sources are small and
4703/// this is compile-time only.
4704#[derive(Clone)]
4705pub(crate) struct AssertLoc {
4706    pub source: String,
4707    pub rel_path: String,
4708}
4709
4710impl<'a> LowerCtx<'a> {
4711    fn new(module: ModuleCtx<'a>, mode: BodyMode) -> Self {
4712        Self {
4713            module,
4714            mode,
4715            local_agents: HashSet::new(),
4716            return_ty: None,
4717            // Every field below is per-body scratch state: a fresh `LowerCtx` is
4718            // built at each body-emission site and never reused, so these must
4719            // re-initialise here on every construction. In particular `next_tmp`
4720            // restarting at 0 is what makes each emitted function's temps begin
4721            // at `__r0`.
4722            next_tmp: 0,
4723            shadow_scopes: vec![HashMap::new()],
4724            harness_bound: HashSet::new(),
4725            is_receiver_temps: HashMap::new(),
4726            is_temp_hoist: None,
4727            unnarrowed_is_tests: HashSet::new(),
4728            local_agent_vars: HashMap::new(),
4729            call_site_identity: None,
4730            call_site_claims: None,
4731            call_site_actor: None,
4732            call_site_no_credential: false,
4733            call_site_bad_signature: None,
4734            source_map: None,
4735            emitted_await: false,
4736            emitted_early_return: false,
4737            tail_slot: None,
4738        }
4739    }
4740
4741    // ---- `ModuleCtx` passthroughs -----------------------------------------
4742    //
4743    // Returned with the `'a` module lifetime rather than the `&self` borrow, so
4744    // a `&mut self` lowering step can hold onto a commons/runtime handle across
4745    // its own recursive calls exactly as it did when these were plain fields.
4746
4747    /// Typed-commons handle for the module being emitted.
4748    pub(crate) fn commons(&self) -> &'a TypedCommons {
4749        self.module.commons
4750    }
4751
4752    /// Cross-context info for v0.6 cross-context call lowering.
4753    pub(crate) fn cross_context(&self) -> &'a bynk_check::resolver::CrossContextInfo {
4754        self.module.cross_context
4755    }
4756
4757    /// The emitted module's conditional-runtime-helper accumulator.
4758    pub(crate) fn runtime_use(&self) -> &'a RuntimeUse {
4759        self.module.runtime_use
4760    }
4761
4762    /// v0.8 build target.
4763    pub(crate) fn target(&self) -> BuildTarget {
4764        self.module.target
4765    }
4766
4767    /// #2045: whether `name` is a consumed context's type on `workers`.
4768    pub(crate) fn is_consumed_context_type(&self, name: &str) -> bool {
4769        self.module.consumed_context_types.contains(name)
4770    }
4771
4772    /// #934: true when the unit being emitted is the first-party `bynk` adapter.
4773    pub(crate) fn in_bynk_unit(&self) -> bool {
4774        self.module.in_bynk_unit
4775    }
4776
4777    // ---- capability-bearing (`HandlerShared`) state ------------------------
4778    //
4779    // Every accessor here reports the same default a non-handler kind used to
4780    // get from the flat struct (no capabilities, no scope, `deps`), so a caller
4781    // that does not care which kind it is in reads unchanged.
4782
4783    fn handler(&self) -> Option<&HandlerShared> {
4784        match &self.mode {
4785            BodyMode::ServiceHandler { handler, .. }
4786            | BodyMode::ProviderOp { handler }
4787            | BodyMode::AgentHandler { handler, .. }
4788            | BodyMode::WsDoMethod { handler, .. } => Some(handler),
4789            _ => None,
4790        }
4791    }
4792
4793    fn handler_mut(&mut self) -> Option<&mut HandlerShared> {
4794        match &mut self.mode {
4795            BodyMode::ServiceHandler { handler, .. }
4796            | BodyMode::ProviderOp { handler }
4797            | BodyMode::AgentHandler { handler, .. }
4798            | BodyMode::WsDoMethod { handler, .. } => Some(handler),
4799            _ => None,
4800        }
4801    }
4802
4803    /// Whether `name` is a capability in scope as `given C1, C2, ...`.
4804    pub(crate) fn has_capability(&self, name: &str) -> bool {
4805        self.handler()
4806            .is_some_and(|h| h.capabilities.contains(name))
4807    }
4808
4809    /// #934: the calling handler's own qualified name, if a capability call can
4810    /// occur in this body at all. `None` for every non-handler kind — the
4811    /// `Idempotency` key-scoping lowering treats that as a compiler bug and
4812    /// panics, exactly as it did when this was a flat `Option` field.
4813    pub(crate) fn handler_scope(&self) -> Option<&str> {
4814        self.handler().and_then(|h| h.handler_scope.as_deref())
4815    }
4816
4817    /// Events track, slice 2: the qualified name of the unit this body is
4818    /// emitted into, for the `Events.emit[E](event)` lowering's
4819    /// `publisherId`. `None` for a body kind that carries no `HandlerShared`
4820    /// at all (an `Events.emit` call cannot occur there).
4821    pub(crate) fn owning_context(&self) -> Option<&str> {
4822        self.handler().map(|h| h.owning_context.as_str())
4823    }
4824
4825    /// v0.12: the receiver expression a capability call resolves against.
4826    pub(crate) fn cap_deps_expr(&self) -> &str {
4827        self.handler().map_or("deps", |h| h.cap_deps_expr.as_str())
4828    }
4829
4830    /// Note that this body called a service of `consumed`. A no-op in a body
4831    /// kind that carries no deps shape to widen (a plain method, a predicate, a
4832    /// test case) — those never read it back.
4833    pub(crate) fn note_cross_context_used(&mut self, consumed: &str) {
4834        if let Some(h) = self.handler_mut() {
4835            h.cross_contexts_called.insert(consumed.to_string());
4836        }
4837    }
4838
4839    /// True if this handler made at least one cross-context call.
4840    pub(crate) fn cross_context_used(&self) -> bool {
4841        self.handler()
4842            .is_some_and(|h| !h.cross_contexts_called.is_empty())
4843    }
4844
4845    /// The consumed contexts this handler's `deps.env` must bind, in name
4846    /// order (#1961): those it calls, plus — when it constructs an agent, whose
4847    /// methods it hands its `deps` — those the context's agents call.
4848    pub(crate) fn cross_contexts_called(&self) -> Vec<String> {
4849        let mut called: std::collections::BTreeSet<String> = self
4850            .handler()
4851            .map(|h| h.cross_contexts_called.clone())
4852            .unwrap_or_default();
4853        if self.agents_instantiated() {
4854            called.extend(self.module.agent_called_contexts.iter().cloned());
4855        }
4856        called.into_iter().collect()
4857    }
4858
4859    /// v0.9.2: true if this body instantiated a local agent.
4860    pub(crate) fn agents_instantiated(&self) -> bool {
4861        self.handler().is_some_and(|h| h.agents_instantiated)
4862    }
4863
4864    /// #527: capabilities required by agent methods this body calls.
4865    pub(crate) fn agent_given_caps_used(
4866        &self,
4867    ) -> Option<&std::collections::BTreeMap<String, bynk_ir::CapRefIr>> {
4868        self.handler().map(|h| &h.agent_given_caps_used)
4869    }
4870
4871    // ---- test-scaffold (`TestShared`) state --------------------------------
4872
4873    fn test(&self) -> Option<&TestShared> {
4874        match &self.mode {
4875            BodyMode::PredicateScaffold { test }
4876            | BodyMode::TestCase { test, .. }
4877            | BodyMode::IntegrationCase { test, .. } => Some(test),
4878            _ => None,
4879        }
4880    }
4881
4882    /// True when lowering **generated test-scaffold** TypeScript (a test-case
4883    /// body, or a `stub`/`where`/`requires` predicate value), where branded
4884    /// types are destructured into `any`-typed value bindings rather than
4885    /// referenced as types. Callers that emit a branded `as`-cast consult this
4886    /// to pick `unchecked_construct_test` (→ `(v as any)`) over the production
4887    /// `(v as T)` form, which cannot resolve `T` in the test module's scope.
4888    pub(crate) fn in_test_scaffold(&self) -> bool {
4889        self.test().is_some_and(|t| t.test_scaffold)
4890    }
4891
4892    /// v0.59: the test body's source context, for `assert`/`expect` locations.
4893    pub(crate) fn assert_loc(&self) -> Option<&AssertLoc> {
4894        self.test().and_then(|t| t.assert_loc.as_ref())
4895    }
4896
4897    // ---- single-kind state -------------------------------------------------
4898
4899    /// v0.80: inside an invariant predicate, the proposed-state variable and the
4900    /// agent's state field names.
4901    pub(crate) fn invariant_state(&self) -> Option<(&str, &HashSet<String>)> {
4902        match &self.mode {
4903            BodyMode::Invariant { name, fields } => Some((name.as_str(), fields)),
4904            _ => None,
4905        }
4906    }
4907
4908    /// v0.116: inside a `transition` predicate, the JS names bound to the
4909    /// contextual `old`/`new` state records.
4910    pub(crate) fn transition_states(&self) -> Option<(&str, &str)> {
4911        match &self.mode {
4912            BodyMode::Transition { old, new } => Some((old.as_str(), new.as_str())),
4913            _ => None,
4914        }
4915    }
4916
4917    /// v0.117: the recorded-call trace object a test case's observations read.
4918    pub(crate) fn observation_trace(&self) -> Option<&str> {
4919        match &self.mode {
4920            BodyMode::TestCase {
4921                observation_trace, ..
4922            } => observation_trace.as_deref(),
4923            _ => None,
4924        }
4925    }
4926
4927    fn agent_store(&self) -> Option<&AgentStoreState> {
4928        match &self.mode {
4929            BodyMode::AgentHandler { store, .. } => store.as_deref(),
4930            _ => None,
4931        }
4932    }
4933
4934    /// v0.81: the mutable working-state variable a `store`-agent handler stages
4935    /// its writes into. `__state` is the name every real site uses; the fallback
4936    /// keeps the (defensive) non-store paths rendering as they did before.
4937    pub(crate) fn agent_store_var(&self) -> &str {
4938        self.agent_store().map_or("__state", |s| s.state.0.as_str())
4939    }
4940
4941    /// v0.81: the working-state variable plus the `Cell` field names it holds.
4942    pub(crate) fn agent_store_cells(&self) -> Option<(&str, &HashSet<String>)> {
4943        self.agent_store().map(|s| (s.state.0.as_str(), &s.state.1))
4944    }
4945
4946    /// v0.82: whether `name` is a persisted `store Map` field (held connection
4947    /// maps are deliberately excluded — they use the connId lowering).
4948    pub(crate) fn is_agent_store_map(&self, name: &str) -> bool {
4949        self.agent_store().is_some_and(|s| s.maps.contains(name))
4950    }
4951
4952    /// v0.83: whether `name` is a `store Set` field.
4953    pub(crate) fn is_agent_store_set(&self, name: &str) -> bool {
4954        self.agent_store().is_some_and(|s| s.sets.contains(name))
4955    }
4956
4957    /// v0.87: the ttl (millis) of the `store Cache` field `name`, if it is one.
4958    pub(crate) fn agent_store_cache_ttl(&self, name: &str) -> Option<i64> {
4959        self.agent_store().and_then(|s| s.caches.get(name).copied())
4960    }
4961
4962    /// v0.95: the `@retain` horizon of the `store Log` field `name`, if it is
4963    /// one. The outer `Option` is "is a log"; the inner is "has a retain".
4964    pub(crate) fn agent_store_log_retain(&self, name: &str) -> Option<Option<i64>> {
4965        self.agent_store().and_then(|s| s.logs.get(name).copied())
4966    }
4967
4968    /// v0.95: whether `name` is a `store Log` field.
4969    pub(crate) fn is_agent_store_log(&self, name: &str) -> bool {
4970        self.agent_store()
4971            .is_some_and(|s| s.logs.contains_key(name))
4972    }
4973
4974    /// v0.93: the value-record fields the `store Map` `name` is `@indexed(by:)`
4975    /// on — empty when it has no secondary index.
4976    pub(crate) fn agent_store_index_fields(&self, name: &str) -> Vec<String> {
4977        self.agent_store()
4978            .and_then(|s| s.indexes.get(name).cloned())
4979            .unwrap_or_default()
4980    }
4981
4982    /// v0.105: the connection **frame type** of the held `store Map[K,
4983    /// Connection]` field `name`, if it is one.
4984    pub(crate) fn agent_held_map_frame(&self, name: &str) -> Option<&String> {
4985        self.agent_store().and_then(|s| s.held_maps.get(name))
4986    }
4987
4988    /// v0.105: whether `name` is a held `store Map[K, Connection]` field.
4989    pub(crate) fn is_agent_held_map(&self, name: &str) -> bool {
4990        self.agent_store()
4991            .is_some_and(|s| s.held_maps.contains_key(name))
4992    }
4993
4994    /// Review of #1460: the value type `V` of the plain `store Map[K, V]`
4995    /// field `name`, if it is one (`None` for a held map — see
4996    /// [`Self::agent_held_map_frame`] instead).
4997    pub(crate) fn agent_store_map_value_ts(&self, name: &str) -> Option<&String> {
4998        self.agent_store().and_then(|s| s.map_values.get(name))
4999    }
5000
5001    /// Review of #1460: the element type `T` of the `store Log[T]` field
5002    /// `name`, if it is one.
5003    pub(crate) fn agent_store_log_value_ts(&self, name: &str) -> Option<&String> {
5004        self.agent_store().and_then(|s| s.log_values.get(name))
5005    }
5006
5007    /// True when lowering an agent handler body — drives the `self.<keyField>`
5008    /// rewrite.
5009    pub(crate) fn in_agent_handler(&self) -> bool {
5010        match &self.mode {
5011            BodyMode::AgentHandler {
5012                in_agent_handler, ..
5013            } => *in_agent_handler,
5014            _ => false,
5015        }
5016    }
5017
5018    /// The name of the agent's `key id` field, inside an agent handler body.
5019    pub(crate) fn agent_key_field(&self) -> Option<&str> {
5020        match &self.mode {
5021            BodyMode::AgentHandler {
5022                agent_key_field, ..
5023            } => agent_key_field.as_deref(),
5024            _ => None,
5025        }
5026    }
5027
5028    /// v0.104: the agent hosting the websocket lifecycle body being lowered.
5029    pub(crate) fn ws_self_agent(&self) -> Option<&str> {
5030        match &self.mode {
5031            BodyMode::WsDoMethod { ws_self_agent, .. } => ws_self_agent.as_deref(),
5032            _ => None,
5033        }
5034    }
5035
5036    /// v0.47: the `by` binder whose `.identity` is threaded through `deps`.
5037    pub(crate) fn deps_identity_binder(&self) -> Option<&str> {
5038        match &self.mode {
5039            BodyMode::ServiceHandler {
5040                deps_identity_binder,
5041                ..
5042            }
5043            | BodyMode::WsDoMethod {
5044                deps_identity_binder,
5045                ..
5046            } => deps_identity_binder.as_deref(),
5047            _ => None,
5048        }
5049    }
5050
5051    /// v0.52: the multi-actor sum handler's resolved-actor binder.
5052    pub(crate) fn actor_sum_binder(&self) -> Option<&str> {
5053        match &self.mode {
5054            BodyMode::ServiceHandler {
5055                actor_sum_binder, ..
5056            } => actor_sum_binder.as_deref(),
5057            _ => None,
5058        }
5059    }
5060
5061    /// v0.7: whether `name` is a local service of the test case's target context.
5062    pub(crate) fn is_test_service(&self, name: &str) -> bool {
5063        match &self.mode {
5064            BodyMode::TestCase { test_services, .. } => test_services.contains(name),
5065            _ => false,
5066        }
5067    }
5068
5069    /// I3 (#1971), #2020: whether the test service `service`'s handler at
5070    /// `key` (`"call"`, or an http handler's emitted method name) binds an
5071    /// actor sum, so a call-site principal builds its `deps.who`.
5072    pub(crate) fn is_sum_handler(&self, service: &str, key: &str) -> bool {
5073        match &self.mode {
5074            BodyMode::TestCase { sum_handlers, .. } => {
5075                sum_handlers.contains(&(service.to_string(), key.to_string()))
5076            }
5077            _ => false,
5078        }
5079    }
5080
5081    /// #2380 (L5): whether the addressed http handler binds an `Oidc` actor,
5082    /// so an in-process call passes it `deps.claims`.
5083    pub(crate) fn is_oidc_handler(&self, service: &str, key: &str) -> bool {
5084        match &self.mode {
5085            BodyMode::TestCase { sum_handlers, .. } => {
5086                sum_handlers.contains(&(service.to_string(), format!("oidc:{key}")))
5087            }
5088            _ => false,
5089        }
5090    }
5091
5092    /// #1975: whether this is a test `case` body.
5093    pub(crate) fn in_test_case(&self) -> bool {
5094        matches!(&self.mode, BodyMode::TestCase { .. })
5095    }
5096
5097    /// #1994: whether an addressed test-service call settles the `~>` sends
5098    /// its handler handed `deps.__exec`.
5099    pub(crate) fn test_settles_sends(&self) -> bool {
5100        matches!(
5101            &self.mode,
5102            BodyMode::TestCase {
5103                settle_sends: true,
5104                ..
5105            }
5106        )
5107    }
5108
5109    /// v0.182 (#664): the ordered handler kinds of the test service `name`.
5110    pub(crate) fn test_service_handlers(&self, name: &str) -> Option<&[bynk_ir::IrHandlerKind]> {
5111        match &self.mode {
5112            BodyMode::TestCase {
5113                test_service_handlers,
5114                ..
5115            } => test_service_handlers.get(name).map(Vec::as_slice),
5116            _ => None,
5117        }
5118    }
5119
5120    /// v0.182 (Slice B, #667): whether `name` is an http service of the system
5121    /// target being driven.
5122    pub(crate) fn is_system_http_service(&self, name: &str) -> bool {
5123        match &self.mode {
5124            BodyMode::IntegrationCase {
5125                system_http_services,
5126                ..
5127            } => system_http_services.contains(name),
5128            _ => false,
5129        }
5130    }
5131
5132    /// #2136: the `__sysdrive_schedule_<svc>_<index>` driver for a cron address
5133    /// `svc.schedule("<expr>", …)` in a `system` case, when `svc` is a cron
5134    /// service of the target that declares `expr`.
5135    pub(crate) fn system_schedule_driver(&self, service: &str, expr: &str) -> Option<String> {
5136        match &self.mode {
5137            BodyMode::IntegrationCase { system_jobs, .. } => {
5138                let idx = system_jobs
5139                    .cron
5140                    .get(service)?
5141                    .iter()
5142                    .position(|e| e == expr)?;
5143                Some(format!("__sysdrive_schedule_{service}_{idx}"))
5144            }
5145            _ => None,
5146        }
5147    }
5148
5149    /// #2136: the `__sysdrive_message_<q>` driver for a queue address
5150    /// `q.message(m)` in a `system` case, when `q` is a queue service of the
5151    /// target.
5152    pub(crate) fn system_message_driver(&self, service: &str) -> Option<String> {
5153        match &self.mode {
5154            BodyMode::IntegrationCase { system_jobs, .. }
5155                if system_jobs.queue.contains(service) =>
5156            {
5157                Some(format!("__sysdrive_message_{service}"))
5158            }
5159            _ => None,
5160        }
5161    }
5162
5163    /// #707: whether `(service, verb, path)` is a declared route of the system
5164    /// target — an undeclared one drives the `405` fall-through.
5165    pub(crate) fn has_system_http_route(&self, route: &(String, String, String)) -> bool {
5166        match &self.mode {
5167            BodyMode::IntegrationCase {
5168                system_http_routes, ..
5169            } => system_http_routes.contains(route),
5170            _ => false,
5171        }
5172    }
5173
5174    /// #708: the body param position and declared type of a system http route.
5175    pub(crate) fn system_http_route_body(
5176        &self,
5177        route: &(String, String, String),
5178    ) -> Option<&(usize, bynk_syntax::ast::TypeRef)> {
5179        match &self.mode {
5180            BodyMode::IntegrationCase {
5181                system_http_route_body,
5182                ..
5183            } => system_http_route_body.get(route),
5184            _ => None,
5185        }
5186    }
5187
5188    /// #708: the type namespace a system http body param's codec resolves in.
5189    pub(crate) fn system_http_type_ns(&self) -> &str {
5190        match &self.mode {
5191            BodyMode::IntegrationCase {
5192                system_http_type_ns,
5193                ..
5194            } => system_http_type_ns.as_str(),
5195            _ => "",
5196        }
5197    }
5198
5199    /// #2019: the claims argument a `system`-tier call on `route` passes, if
5200    /// the route is refinement-guarded: the acting refinement's witness
5201    /// claims, or `{}` for any other actor.
5202    pub(crate) fn system_http_claims_arg(
5203        &self,
5204        route: &(String, String, String),
5205    ) -> Option<String> {
5206        match &self.mode {
5207            BodyMode::IntegrationCase {
5208                system_http_claims, ..
5209            } if system_http_claims.routes.contains(route) => Some(
5210                self.call_site_actor
5211                    .as_ref()
5212                    .and_then(|a| system_http_claims.actors.get(a))
5213                    .cloned()
5214                    .unwrap_or_else(|| "{}".to_string()),
5215            ),
5216            _ => None,
5217        }
5218    }
5219
5220    /// #2155: the acting actor's name, as a string literal, for a signing
5221    /// driver of an actor-sum route — the call-site principal, else the
5222    /// sum's primary member. `None` for any other route.
5223    pub(crate) fn system_http_sum_actor_arg(
5224        &self,
5225        route: &(String, String, String),
5226    ) -> Option<String> {
5227        match &self.mode {
5228            BodyMode::IntegrationCase {
5229                system_http_claims, ..
5230            } => system_http_claims
5231                .sum_routes
5232                .get(route)
5233                .map(|primary| format!("{:?}", self.call_site_actor.as_deref().unwrap_or(primary))),
5234            _ => None,
5235        }
5236    }
5237
5238    /// Events track, slice 0 (spine #936): true when a bare `Events`
5239    /// receiver in this unit is genuinely the first-party `bynk.Events`
5240    /// capability — declared here because this unit *is* `bynk`, or
5241    /// flattened in from it (`consumes bynk { Events }`) — not some other,
5242    /// unrelated capability that merely happens to share the name. Mirrors
5243    /// #934's `Idempotency` distinction (`is_first_party` at the
5244    /// `Idempotency.dedup`/`remember` lowering site). Both the call-site
5245    /// interception (`lower.rs`) and the `__events` buffer declaration
5246    /// (`block_uses_emit`'s gate in `emit.rs`) must agree on this, or a
5247    /// custom same-named `Events` capability's calls get silently rewritten
5248    /// into a buffer nothing constructs a provider for.
5249    pub(crate) fn is_first_party_events(&self) -> bool {
5250        self.in_bynk_unit()
5251            || self
5252                .cross_context()
5253                .flattened_caps
5254                .get("Events")
5255                .map(String::as_str)
5256                == Some("bynk")
5257    }
5258
5259    /// #2058: `name` as this module spells it in a type position: qualified
5260    /// under its context's namespace when a consumed context declares it.
5261    pub(crate) fn consumed_type_name(&self, name: &str) -> String {
5262        match self.module.consumed_type_ns.get(name) {
5263            Some(ns) => [ns.as_str(), ".", name].concat(),
5264            None => name.to_string(),
5265        }
5266    }
5267
5268    /// Events slice 3b (#978): the declared `@schema(N)` version of the
5269    /// locally-declared event `name`, or `1` if it has none (including if
5270    /// `name` isn't a locally-declared event at all — `Events.emit[E]` only
5271    /// ever names an owned event, checker-enforced, so a miss here can only
5272    /// mean a broken build already reported elsewhere, and this degrades to
5273    /// today's pre-existing output rather than panicking).
5274    pub(crate) fn event_schema_version(&self, name: &str) -> i64 {
5275        self.module
5276            .event_schema_versions
5277            .get(name)
5278            .copied()
5279            .unwrap_or(1)
5280    }
5281
5282    /// Attach the file's source-map builder (slice 1, ADR 0103). Builder-style so
5283    /// the emission sites with no builder leave `LowerCtx::new(module, mode)`
5284    /// untouched — only the project-emission path that has one calls this.
5285    fn with_source_map(mut self, map: Option<&'a RefCell<SourceMapBuilder>>) -> Self {
5286        self.source_map = map;
5287        self
5288    }
5289
5290    /// Record that this lowering emitted a reference to the `Bytes` runtime
5291    /// helpers, so the module imports them.
5292    fn note_bytes(&self) {
5293        self.runtime_use().note_bytes();
5294    }
5295
5296    /// Record that this lowering emitted a `Bytes` operation helper (#2016), so
5297    /// the module imports that group.
5298    fn note_bytes_ops(&self) {
5299        self.runtime_use().note_bytes_ops();
5300    }
5301
5302    /// Record that this lowering built a `RequestHeaders` value (#2341), so
5303    /// the module imports `__requestHeaders`.
5304    fn note_request_headers(&self) {
5305        self.runtime_use().note_request_headers();
5306    }
5307
5308    /// Record that this lowering built a `TokenClaims` value (#2380).
5309    fn note_token_claims(&self) {
5310        self.runtime_use().note_token_claims();
5311    }
5312
5313    /// Record that this lowering constructed an `HttpResult` variant (#1934),
5314    /// so a test module imports it.
5315    fn note_http_result(&self) {
5316        self.runtime_use().note_http_result();
5317    }
5318
5319    /// Record that this lowering emitted a reference to `__bynkEq` (#1652), so
5320    /// the module imports it.
5321    fn note_eq(&self) {
5322        self.runtime_use().note_eq();
5323    }
5324
5325    /// Record that this lowering emitted an `Int` domain trap (#1657), so the
5326    /// module imports the helpers.
5327    fn note_int(&self) {
5328        self.runtime_use().note_int();
5329    }
5330
5331    /// Record a checkpoint: generated text from `out_len` onward originates at
5332    /// `span`, until the next checkpoint (ADR 0103 D2, nearest-enclosing). A
5333    /// no-op when no builder is attached. `out_len` is the buffer length *before*
5334    /// the statement's text is appended.
5335    ///
5336    /// `out_len` only means something relative to the *top-level module
5337    /// buffer* the attached builder is tracking. A caller building an IIFE
5338    /// into its own local `String` — `lower_if`'s value-position wrapper,
5339    /// `build_match_iife`'s — before splicing it elsewhere must not call this
5340    /// with that buffer's own length; see [`Self::without_source_map`].
5341    fn record_span(&self, out_len: usize, span: bynk_syntax::span::Span) {
5342        if let Some(map) = self.source_map {
5343            map.borrow_mut().record(out_len, span);
5344        }
5345    }
5346
5347    /// #4 review: run `f` with source-map recording suppressed, restoring it
5348    /// after. For lowering into a local IIFE buffer that will later be
5349    /// spliced into the real module text at some other offset — `record_span`
5350    /// has no way to know that offset, so a checkpoint taken here would
5351    /// silently corrupt the map with a position relative to the wrong
5352    /// buffer. `SourceMapBuilder::merge` already solves the equivalent
5353    /// problem one level up (a handler/test body's own local buffer, spliced
5354    /// into the module) by recording into a *sub*-builder and rebasing at the
5355    /// splice — but that needs a builder that outlives the call, and
5356    /// `source_map` is `Option<&'a RefCell<SourceMapBuilder>>` tied to the
5357    /// whole emission's lifetime, so a function-local sub-builder can't be
5358    /// substituted in. Suppressing instead of mis-recording means the
5359    /// nearest-enclosing-checkpoint rule (ADR 0103 D2) falls back to whatever
5360    /// was correctly mapped just before the IIFE started, rather than a wrong
5361    /// one silently taking over — degraded stepping through the IIFE's own
5362    /// lines in `bynkc test --inspect`, not a corrupted map.
5363    fn without_source_map<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
5364        let saved = self.source_map.take();
5365        let result = f(self);
5366        self.source_map = saved;
5367        result
5368    }
5369    /// v0.9.2: lower an agent instantiation `AgentName(key)` to its factory
5370    /// call. Bundle/test mode passes only the key; workers mode also threads
5371    /// `deps.env` so the factory can reach the agent's DO namespace — #2355:
5372    /// `this.deps.env` in a provider body, whose composer supplies it.
5373    fn agent_construct(&mut self, agent: &str, key_expr: &str) -> String {
5374        if let Some(h) = self.handler_mut() {
5375            h.agents_instantiated = true;
5376        }
5377        let factory = agent_factory_name(agent);
5378        if matches!(self.target(), BuildTarget::Workers) {
5379            format!("{factory}({key_expr}, {}.env)", self.cap_deps_expr())
5380        } else {
5381            format!("{factory}({key_expr})")
5382        }
5383    }
5384
5385    /// #527: note that the body calls `agent.method`, folding the method's
5386    /// `given` capabilities into this handler's requirement set. A no-op in a
5387    /// body kind that has no deps shape to widen — those never read it back.
5388    pub(crate) fn record_agent_call(&mut self, agent: &str, method: &str) {
5389        let givens = self
5390            .module
5391            .agent_method_givens
5392            .get(agent)
5393            .and_then(|m| m.get(method))
5394            .cloned()
5395            .unwrap_or_default();
5396        if let Some(h) = self.handler_mut() {
5397            for c in givens {
5398                h.agent_given_caps_used.entry(c.name.clone()).or_insert(c);
5399            }
5400        }
5401    }
5402    fn fresh(&mut self) -> String {
5403        let n = self.next_tmp;
5404        self.next_tmp += 1;
5405        format!("__r{n}")
5406    }
5407    /// #908: bind a `let`/`let <-` LHS to its emitted JS identifier. Returns
5408    /// the natural `ts_ident` name unless `name` is already bound *anywhere*
5409    /// in the enclosing block chain — not only the current block. A nested
5410    /// block re-`let`-ing an outer name is ordinary, valid lexical shadowing
5411    /// in JS on its own, but this `let`'s own RHS may still read the outer
5412    /// binding (`let n = n + 1` one block in); a plain `const n` there
5413    /// would put the read inside its own declaration's temporal dead zone
5414    /// (JS hoists a block's `let`/`const` names to the top of that block),
5415    /// turning a correct read of the outer value into a TDZ ReferenceError.
5416    /// Renaming whenever *any* enclosing frame already has the name sidesteps
5417    /// that regardless of whether this particular RHS reads it. Allocates a
5418    /// fresh name via [`Self::fresh`] when so. `_` never collides (each is
5419    /// already a fresh throwaway) and is never registered, since it is never
5420    /// read.
5421    pub(crate) fn bind_local_name(&mut self, name: &str) -> String {
5422        if name == "_" {
5423            return self.fresh();
5424        }
5425        let natural = ts_ident(name);
5426        let js_name = if self.shadow_scopes.iter().any(|f| f.contains_key(name))
5427            || self.harness_bound.contains(&natural)
5428        {
5429            self.fresh()
5430        } else {
5431            natural
5432        };
5433        self.shadow_scopes
5434            .last_mut()
5435            .expect("shadow_scopes always has a root frame")
5436            .insert(name.to_string(), js_name.clone());
5437        js_name
5438    }
5439    /// #908: the emitted JS identifier currently bound to a local name, if a
5440    /// `let` re-bind renamed it somewhere in the enclosing block chain.
5441    /// Walked innermost-out so a nested block sees an outer rename that was
5442    /// still active when it was entered. `None` means no rename applies —
5443    /// callers fall back to the natural `ts_ident` name.
5444    pub(crate) fn resolved_local_name(&self, name: &str) -> Option<String> {
5445        self.shadow_scopes
5446            .iter()
5447            .rev()
5448            .find_map(|f| f.get(name).cloned())
5449    }
5450    /// Whether `name` is bound by an enclosing local (a `let`, match-arm/`is`
5451    /// binding, or lambda param) rather than free to refer to a store field.
5452    /// A local always wins: store-field dispatch by bare receiver name must
5453    /// check this first, or a parameter/binding that happens to share a store
5454    /// field's name is silently treated as the store field.
5455    pub(crate) fn is_local(&self, name: &str) -> bool {
5456        self.shadow_scopes.iter().any(|f| f.contains_key(name))
5457    }
5458    /// #908: register a non-`let` binder (a match-arm/`is` pattern binding, or
5459    /// a lambda param) into the current frame under its natural `ts_ident`
5460    /// name — never renamed, since each such binder already lowers inside its
5461    /// own JS block/arrow scope with no risk of colliding with a sibling
5462    /// declaration of the same name. Without this, a read inside the binder's
5463    /// scope would fall through [`Self::resolved_local_name`]'s stack walk
5464    /// past this (unregistered) declaration to an outer `let` rename that is
5465    /// no longer the right value here — silently wrong output, not a `tsc`
5466    /// error. Every construct that introduces a binder outside `bind_local_name`
5467    /// (match arms, `is`, lambda params) must call this for each name it binds.
5468    pub(crate) fn declare_binder(&mut self, name: &str) {
5469        if name == "_" {
5470            return;
5471        }
5472        self.shadow_scopes
5473            .last_mut()
5474            .expect("shadow_scopes always has a root frame")
5475            .insert(name.to_string(), ts_ident(name));
5476    }
5477    /// Return a stable textual reference to an `is` receiver, used by the
5478    /// `.tag` check in `lower_is`. A simple, repeatable lvalue is lowered
5479    /// inline exactly as before (preserving rewrites such as `self.state` or
5480    /// capability access). A complex receiver (anything `is_simple_is_receiver`
5481    /// rejects — e.g. a call) is evaluated once into a fresh temp
5482    /// hoisted into the returned `Lowered` and cached by span, so the bindings
5483    /// gathered later reference the same evaluation rather than re-running the
5484    /// expression.
5485    fn is_receiver_ref(&mut self, value: &Expr) -> Lowered {
5486        if let Some(t) = self.is_receiver_temps.get(&value.span) {
5487            return Lowered::bare(t.clone());
5488        }
5489        let mut pre = Pre::new();
5490        let lowered = pre.lower(value, self);
5491        if is_simple_is_receiver(value) {
5492            return pre.finish(lowered);
5493        }
5494        let tmp = self.fresh();
5495        self.declare_is_temp(value, &tmp, &lowered, &mut pre);
5496        self.is_receiver_temps.insert(value.span, tmp.clone());
5497        pre.finish(tmp)
5498    }
5499
5500    /// Declare an `is` receiver temp holding `lowered`: a `const` in place, or,
5501    /// inside a hoisting right operand (`is_temp_hoist`), a hoisted
5502    /// definite-assignment declaration plus an in-place assignment. The
5503    /// assignment stays where the receiver was evaluated, so the right operand
5504    /// is still evaluated only when the left operand holds.
5505    fn declare_is_temp(&mut self, value: &Expr, tmp: &str, lowered: &str, pre: &mut Pre) {
5506        let ty = self
5507            .commons()
5508            .expr_types
5509            .get(&value.id)
5510            .map(|te| ts_ty(te.ty, self.commons().tys()));
5511        match (self.is_temp_hoist.as_mut(), ty) {
5512            (Some(hoist), Some(ty)) => {
5513                hoist.push(format!("let {tmp}!: {ty};"));
5514                pre.push(format!("{tmp} = {lowered};"));
5515            }
5516            _ => pre.push(format!("const {tmp} = {lowered};")),
5517        }
5518    }
5519
5520    /// v0.13: like `is_receiver_ref` but always lifts to a temp, even for a
5521    /// simple ident. A refined `is`-narrowing re-binds the value's name to the
5522    /// branded refined type (`const n = <temp> as Quantity`); that shadowing
5523    /// const cannot reference the same name (TDZ), so the value is captured in a
5524    /// temp first and both the check and the binding read the temp.
5525    fn is_receiver_ref_forced(&mut self, value: &Expr) -> Lowered {
5526        if let Some(t) = self.is_receiver_temps.get(&value.span) {
5527            return Lowered::bare(t.clone());
5528        }
5529        let mut pre = Pre::new();
5530        let lowered = pre.lower(value, self);
5531        let tmp = self.fresh();
5532        self.declare_is_temp(value, &tmp, &lowered, &mut pre);
5533        self.is_receiver_temps.insert(value.span, tmp.clone());
5534        pre.finish(tmp)
5535    }
5536
5537    /// v0.13: true when `value is Name` is a *refinement* check — the value is a
5538    /// base/refined value and `Name` is a refined type — rather than a sum
5539    /// variant test. Mirrors the checker's disambiguation.
5540    ///
5541    /// P6.56 (design/tracks/the-ir.md §6b): `name_refined` reads only
5542    /// `TypeBody`'s own discriminant, never `base`/`refinement` — the two
5543    /// fields `TypeShape::Refined` would add real construction cost for
5544    /// (`refined_or_opaque_base`'s own doc comment has the full reasoning).
5545    /// Investigated and declined on the identical grounds.
5546    fn is_refined_is_check(&self, value: &Expr, name: &str) -> bool {
5547        let value_baseish = matches!(
5548            self.commons().expr_ty(value.id).as_deref(),
5549            Some(Ty::Base(_))
5550                | Some(Ty::Named {
5551                    kind: NamedKind::Refined(_),
5552                    ..
5553                })
5554        );
5555        let name_refined = matches!(
5556            self.commons().types.get(name).map(|d| &d.body),
5557            Some(TypeBody::Refined { .. })
5558        );
5559        value_baseish && name_refined
5560    }
5561    /// Read-only counterpart for the binding gatherer (which returns no
5562    /// `Lowered`, so it has nowhere to hoist and cannot lift). If the receiver was already lifted to a temp during
5563    /// condition lowering, reuse that temp; otherwise it must be a simple
5564    /// repeatable lvalue, rendered inline. The "lower the condition before
5565    /// gathering its bindings" ordering in `emit_if_tail`, `lower_and_with_is`
5566    /// and the value-position `if` IIFE guarantees the temp exists before this
5567    /// is called for complex receivers; `value_text_for_is` panics if it does
5568    /// not (#1668).
5569    fn is_receiver_text(&self, value: &Expr) -> String {
5570        if let Some(t) = self.is_receiver_temps.get(&value.span) {
5571            return t.clone();
5572        }
5573        value_text_for_is(value)
5574    }
5575    fn receiver_namespace(&self, e: &Expr) -> Option<String> {
5576        let ty = self.commons().expr_ty(e.id)?;
5577        if let Ty::Named { name, .. } = &*ty {
5578            Some(name.clone())
5579        } else {
5580            None
5581        }
5582    }
5583    /// Resolve the payload field name for the i-th positional binding of
5584    /// a variant. Built-ins are recognised by name; user variants are
5585    /// looked up via the type tables.
5586    ///
5587    /// P6.56 (design/tracks/the-ir.md §6b): see `sum_owner_of_variant`'s own
5588    /// doc comment (`emitter.rs`) — a `TypeShape::Sum`-routed conversion was
5589    /// investigated and declined here on the identical grounds.
5590    fn positional_field_name(
5591        &self,
5592        discriminant_ty: Option<TyId>,
5593        variant: &str,
5594        idx: usize,
5595        tys: &Arc<Types>,
5596    ) -> String {
5597        match (variant, idx) {
5598            ("Ok", 0) | ("Some", 0) => return "value".to_string(),
5599            ("Err", 0) => return "error".to_string(),
5600            _ => {}
5601        }
5602        // v0.52: a multi-actor sum arm binds the resolved actor's identity,
5603        // carried in the `identity` field of the tagged object.
5604        let disc_node = discriminant_ty.map(|t| tys.get(t));
5605        if let Some(Ty::ActorSum(_)) = disc_node.as_deref() {
5606            return "identity".to_string();
5607        }
5608        // #1934: the payloads a case binds from a queue verdict and a
5609        // boundary rejection, named as `queue.ts` and `http.ts` name them.
5610        match disc_node.as_deref() {
5611            Some(Ty::QueueResult) if variant == "Retry" && idx == 0 => {
5612                return "reason".to_string();
5613            }
5614            Some(Ty::BoundaryRejection) if idx == 0 => return "detail".to_string(),
5615            _ => {}
5616        }
5617        if let Some(Ty::Named {
5618            kind: NamedKind::Sum,
5619            name,
5620            ..
5621        }) = disc_node.as_deref()
5622            && let Some(decl) = self.commons().types.get(name)
5623            && let TypeBody::Sum(s) = &decl.body
5624            && let Some(v) = s.variants.iter().find(|v| v.name.name == variant)
5625            && let Some(f) = v.payload.get(idx)
5626        {
5627            return payload_prop(&f.name.name);
5628        }
5629        // #1883, slice 3: an `HttpResult` variant's payload fields, named as
5630        // the runtime union in `bynk-emit/runtime/src/http.ts` names them
5631        // (`Raw(body, ct)`, `BadRequest(m)`, `Found(url)`); the single-field
5632        // fallback below names them all `value`, which `tsc` rejects for
5633        // every variant but `Ok`, `Created` and `Accepted`. A typed user sum
5634        // is resolved above, first. `Ty::Error` is a program that failed
5635        // checking, which is still lowered and its output discarded.
5636        if matches!(
5637            disc_node.as_deref(),
5638            None | Some(Ty::HttpResult(_)) | Some(Ty::Error)
5639        ) && let Some(v) = bynk_syntax::ast::http_variant(variant)
5640        {
5641            use bynk_syntax::ast::HttpVariantPayload as P;
5642            let fields: &[&str] = match v.payload {
5643                P::None => &[],
5644                P::Value => &["value"],
5645                P::Message => &["message"],
5646                P::Location => &["location"],
5647                P::Streamed => &["stream"],
5648                P::Raw => &["body", "contentType"],
5649                P::RawTagged => &["body", "contentType", "etag"],
5650                P::RawError => &["status", "body", "contentType"],
5651            };
5652            if let Some(field) = fields.get(idx) {
5653                return (*field).to_string();
5654            }
5655        }
5656        // Single-field fallback. The checker rejects mixed bindings already.
5657        "value".to_string()
5658    }
5659
5660    /// The type of a variant's `idx`-th payload field, when resolvable — used to
5661    /// recurse field-name resolution through nested payload patterns (ADR 0169).
5662    /// Precise for `Result`/`Option`/`HttpResult` and user sums; `None` otherwise
5663    /// (callers fall back to the single-field `"value"` name).
5664    fn payload_field_ty(
5665        &self,
5666        ty: Option<TyId>,
5667        variant: &str,
5668        idx: usize,
5669        tys: &Arc<Types>,
5670    ) -> Option<TyId> {
5671        match ty.map(|t| tys.get(t)).as_deref() {
5672            Some(Ty::Result(t, e)) => match (variant, idx) {
5673                ("Ok", 0) => Some(*t),
5674                ("Err", 0) => Some(*e),
5675                _ => None,
5676            },
5677            Some(Ty::HttpResult(t)) if variant == "Ok" && idx == 0 => Some(*t),
5678            // #1934: `Handled(Created(i))` resolves `i`'s field through the
5679            // `HttpResult` it carries.
5680            Some(Ty::HttpOutcome(t)) if idx == 0 => match variant {
5681                "Rejected" => Some(tys.intern(Ty::BoundaryRejection)),
5682                "Handled" => Some(tys.intern(Ty::HttpResult(*t))),
5683                _ => None,
5684            },
5685            Some(Ty::Option(t)) if variant == "Some" && idx == 0 => Some(*t),
5686            Some(Ty::Named {
5687                kind: NamedKind::Sum,
5688                name,
5689                args,
5690            }) => {
5691                let decl = self.commons().types.get(name)?;
5692                let TypeBody::Sum(s) = &decl.body else {
5693                    return None;
5694                };
5695                let v = s.variants.iter().find(|v| v.name.name == variant)?;
5696                let f = v.payload.get(idx)?;
5697                // #593: substitute the instantiation's type arguments into the
5698                // payload field type — a bare type parameter (`Loaded(value: T)`)
5699                // resolves to its concrete argument, exactly as the checker's
5700                // `variants_of` does. Plain resolve for a non-generic sum (empty
5701                // `args`), so a nested positional binding recovers the real field
5702                // name instead of falling back to the generic `"value"`.
5703                bynk_check::checker::instantiate_field_ty(
5704                    decl,
5705                    args,
5706                    &f.type_ref,
5707                    &self.commons().types,
5708                    tys,
5709                )
5710            }
5711            _ => None,
5712        }
5713    }
5714}
5715
5716/// Unchecked construction of a branded value in emitted TypeScript.
5717///
5718/// ADR 0182: an **opaque** type exposes a runtime `.unsafe(value)` constructor
5719/// (source-callable within its defining commons, and the target of its internal
5720/// uses), so opaque construction stays `T.unsafe(value)`. A **refined** or
5721/// **alias** type has **no** public `.unsafe`: exposing one let hand-written host
5722/// or adapter code bypass the refinement predicate, the credibility hole #545
5723/// closed. Its admitted / generated values are branded with an inline `as` cast
5724/// — byte-for-byte the old `.unsafe` body (`return value as T`) at the call site,
5725/// but not a callable API surface a consumer can reach.
5726pub(crate) fn unchecked_construct(name: &str, value: &str, is_opaque: bool) -> String {
5727    if is_opaque {
5728        format!("{name}.unsafe({value})")
5729    } else {
5730        format!("({value} as {name})")
5731    }
5732}
5733
5734/// Unchecked construction inside GENERATED TEST scaffolding (`tests/*.test.ts`).
5735///
5736/// There a branded type is in scope only as an `any`-typed value binding
5737/// (`const {{ T }} = ns as any`) — never as a type — so the production
5738/// `(value as T)` form fails to resolve `T`. Opaque still constructs through its
5739/// `.unsafe` value method (kept, ADR 0182); a refined/alias value brands to `any`,
5740/// which is exactly the type the pre-0182 `T.unsafe(value)` already produced here
5741/// (`T` being `any`) and erases to the raw value at runtime — without
5742/// reintroducing a callable refined `.unsafe`.
5743pub(crate) fn unchecked_construct_test(name: &str, value: &str, is_opaque: bool) -> String {
5744    if is_opaque {
5745        format!("{name}.unsafe({value})")
5746    } else {
5747        // P7.2: deferred, not narrowed — checked, not skipped. This doc comment's
5748        // own explanation still holds: `name` genuinely isn't resolvable as a
5749        // type at this call site (only as an `any`-typed value binding), so
5750        // there is no real type to cast to here. Fixing this needs restructuring
5751        // what the test scaffold imports, not a same-line text change.
5752        format!("({value} as any)")
5753    }
5754}
5755
5756fn ts_base(b: BaseType) -> &'static str {
5757    match b {
5758        BaseType::Int => "number",
5759        BaseType::String => "string",
5760        BaseType::Bool => "boolean",
5761        BaseType::Float => "number",
5762        BaseType::Duration | BaseType::Instant => "number",
5763        // v0.110 (ADR 0142): `Bytes` is the one base type that does NOT erase
5764        // to `number` — it lowers to an immutable octet sequence, `Uint8Array`.
5765        BaseType::Bytes => "globalThis.Uint8Array",
5766    }
5767}
5768
5769pub(crate) fn ts_type_ref(r: &TypeRef) -> String {
5770    ts_type_ref_with(r, None)
5771}
5772
5773/// [`TsType`]-returning sibling of `ts_type_ref`, qualifying named types that
5774/// live in `scope` with the namespace `ns` (`Order` → `Ns.Order`). Used by
5775/// `observation_call_record_types` (`project/tests_emit.rs`) for mock method
5776/// signatures that sit outside the destructuring that brings a namespace's
5777/// value-side names into local scope, so the types must be referenced fully
5778/// qualified. Qualification recurses through generic arguments; base/unit
5779/// types are unaffected.
5780///
5781/// Originally paired with a `String`-returning `ts_type_ref_qualified` (Decision
5782/// B, #1321) — `workers.rs`'s own type-position needs (Arc C slice 3) wanted
5783/// the structured `TsType` this function returns, added *alongside* rather
5784/// than replacing, since `ts_type_ref_qualified` itself was still needed by a
5785/// `String`-based caller. Arc C, slice 32 (`tests_emit.rs` slice B, #1399)
5786/// converted that one remaining caller (`observation_call_record_types`) to
5787/// call this function directly instead — `ts_type_ref_qualified` had no
5788/// other production caller left, so it was deleted rather than kept as dead
5789/// code (the same "close it, don't leave a substitute-around orphan" call
5790/// slice 6's `ts_string_literal` deletion already made); its own direct unit
5791/// tests below now pin this function's output through `bynk_ts::print_type`
5792/// instead.
5793///
5794/// Not a new structural walk — `ts_type_ref_to_ts_type` (below) already
5795/// builds a real `TsType` from every `TypeRef` variant; this function is
5796/// that same build, just with the qualifying closure threaded through.
5797pub(crate) fn ts_type_ref_qualified_ts_type(
5798    r: &TypeRef,
5799    scope: &HashSet<String>,
5800    ns: &str,
5801) -> TsType {
5802    ts_type_ref_to_ts_type(
5803        r,
5804        Some(&|name| scope.contains(name).then(|| ns.to_string())),
5805    )
5806}
5807
5808/// [`TsType`]-returning sibling of `ts_type_ref`, like `ts_type_ref_qualified`
5809/// but with each in-scope name carrying its *own* namespace via `type_ns`
5810/// rather than one shared `ns` — needed when a signature mixes names owned
5811/// by the target unit with names reached only through a `uses`d commons
5812/// (e.g. a stub class implementing an adapter-sourced capability whose
5813/// return type lives in a commons the capability's own unit `uses`, never
5814/// in the target context itself — Locale capability track, slice 1, #844).
5815/// Qualifying such a name under the target's own namespace would reference
5816/// an export `emit_context_rebrands` never emits (it only rebrands names
5817/// the target's *own* lowered body references), so each name is qualified
5818/// under the namespace that actually exports it. Used by `emit_stub_class`
5819/// (`project/tests_emit.rs`) for its own method params/return-type.
5820///
5821/// Originally paired with a `String`-returning `ts_type_ref_qualified_multi`
5822/// — Arc C slice 33 (`tests_emit.rs` slice C, #1401) converted `emit_stub_
5823/// class`'s own two real call sites (params/return-type) to call this
5824/// function directly instead, the same cleanup Arc C slice 32/#1399 already
5825/// made for `ts_type_ref_qualified`'s own identical pairing — `ts_type_ref_
5826/// qualified_multi` had no other production caller left, so it was deleted
5827/// rather than kept as dead code, with its own direct unit test rerouted
5828/// through `bynk_ts::print_type` instead.
5829pub(crate) fn ts_type_ref_qualified_multi_ts_type(
5830    r: &TypeRef,
5831    type_ns: &HashMap<String, String>,
5832) -> TsType {
5833    ts_type_ref_to_ts_type(r, Some(&|name| type_ns.get(name).cloned()))
5834}
5835
5836/// A name → owning-namespace lookup for `ts_type_ref_with`'s `qualify` arm.
5837type QualifyFn<'a> = &'a dyn Fn(&str) -> Option<String>;
5838
5839/// Shared renderer behind `ts_type_ref` (`qualify = None`) and the two
5840/// `ts_type_ref_qualified*` helpers above (`qualify = Some(name -> namespace)`).
5841/// With `None` it is output-identical to the historic `ts_type_ref`; the only
5842/// divergence is the `Named`/`App` arms, which qualify in-scope names when
5843/// `qualify` is set.
5844/// P7.9 (#1315, R7.2): renders `r` by building a real [`bynk_ts::TsType`]
5845/// and printing it through [`bynk_ts::print_type`], instead of `format!`-ing
5846/// the type text by hand — this function's own signature and every one of
5847/// its ~110 real callers (via `ts_type_ref`/`ts_type_ref_qualified*`) are
5848/// unchanged; only the internal construction moved. `TypeRef` has no shape
5849/// this crate's `TsType` (`Named`/`Array`/`Object`/`Fn`, all extended for
5850/// this slice's own grounded gaps — a `readonly` modifier, and `Fn` itself)
5851/// can't represent — confirmed by matching every `TypeRef` variant below.
5852fn ts_type_ref_with(r: &TypeRef, qualify: Option<QualifyFn<'_>>) -> String {
5853    bynk_ts::print_type(&ts_type_ref_to_ts_type(r, qualify))
5854}
5855
5856/// Whether `ty` is a bare (no type arguments) `Named` type whose name is
5857/// exactly `target` — used by the two arms below that special-case a
5858/// `Promise`/function return of `void`, checking the *structure* of the
5859/// already-built `TsType` instead of the pre-P7.9 code's own `inner == "()"
5860/// || inner == "void"` text comparison. No arm in [`ts_type_ref_to_ts_type`]
5861/// ever builds a literal `"()"`-named type, so that half of the check is
5862/// dead in practice today, same as it was before this slice — inherited
5863/// unchanged rather than dropped, since removing "impossible" defensive
5864/// code isn't this slice's job.
5865fn is_bare_named(ty: &TsType, target: &str) -> bool {
5866    matches!(ty, TsType::Named { name, type_args } if type_args.is_empty() && name == target)
5867}
5868
5869/// #1883, slice 1a: the TypeScript shape of a Bynk `Asset` —
5870/// `{ readonly url: string }`, structural, so a program that only *names* the
5871/// type (`fn link(a: Asset) -> String`) compiles with no runtime support.
5872/// Each declared asset is emitted as a constant of this shape (`emit_project`).
5873/// #2341 (decision A): `RequestHeaders` as TypeScript — structural, so no
5874/// module needs a new runtime import for it: `get`, `bearer` and `clientIp`,
5875/// each answering an `Option<string>`. The runtime's `requestHeaders(request)`
5876/// builds one.
5877pub(crate) fn request_headers_ts_type() -> TsType {
5878    // `Option<string>` spelled out: a composition root imports no `Option`,
5879    // and the bare name there is the DOM's `Option` constructor.
5880    let opt = || {
5881        TsType::union(vec![
5882            TsType::Object(vec![
5883                bynk_ts::TsTypeMember::readonly_prop("tag", TsType::named("\"Some\"")),
5884                bynk_ts::TsTypeMember::readonly_prop("value", TsType::named("string")),
5885            ]),
5886            TsType::Object(vec![bynk_ts::TsTypeMember::readonly_prop(
5887                "tag",
5888                TsType::named("\"None\""),
5889            )]),
5890        ])
5891    };
5892    let name = bynk_ts::TsParam {
5893        name: "name".to_string(),
5894        ty: Some(TsType::named("string")),
5895        optional: false,
5896    };
5897    TsType::Object(vec![
5898        bynk_ts::TsTypeMember::method("get", vec![name], opt()),
5899        bynk_ts::TsTypeMember::method("bearer", vec![], opt()),
5900        bynk_ts::TsTypeMember::method("clientIp", vec![], opt()),
5901    ])
5902}
5903
5904/// #2380 (L5): `TokenClaims` as TypeScript — structural, like
5905/// `RequestHeaders`: `get(name)` for a string claim, `has(name)` for any.
5906pub(crate) fn token_claims_ts_type() -> TsType {
5907    let opt = TsType::union(vec![
5908        TsType::Object(vec![
5909            bynk_ts::TsTypeMember::readonly_prop("tag", TsType::named("\"Some\"")),
5910            bynk_ts::TsTypeMember::readonly_prop("value", TsType::named("string")),
5911        ]),
5912        TsType::Object(vec![bynk_ts::TsTypeMember::readonly_prop(
5913            "tag",
5914            TsType::named("\"None\""),
5915        )]),
5916    ]);
5917    let name = || bynk_ts::TsParam {
5918        name: "name".to_string(),
5919        ty: Some(TsType::named("string")),
5920        optional: false,
5921    };
5922    TsType::Object(vec![
5923        bynk_ts::TsTypeMember::method("get", vec![name()], opt),
5924        bynk_ts::TsTypeMember::method("has", vec![name()], TsType::named("boolean")),
5925    ])
5926}
5927
5928pub(crate) fn asset_ts_type() -> TsType {
5929    TsType::Object(vec![bynk_ts::TsTypeMember::readonly_prop(
5930        "url",
5931        TsType::named("string"),
5932    )])
5933}
5934
5935fn ts_type_ref_to_ts_type(r: &TypeRef, qualify: Option<QualifyFn<'_>>) -> TsType {
5936    match r {
5937        TypeRef::Base(b, _) => TsType::named(ts_base(*b)),
5938        TypeRef::Named(id) => {
5939            let name = if let Some(f) = qualify
5940                && let Some(ns) = f(&id.name)
5941            {
5942                format!("{ns}.{}", id.name)
5943            } else {
5944                id.name.clone()
5945            };
5946            TsType::named(name)
5947        }
5948        TypeRef::Result(t, e, _) => TsType::named_with_args(
5949            "Result",
5950            vec![
5951                ts_type_ref_to_ts_type(t, qualify),
5952                ts_type_ref_to_ts_type(e, qualify),
5953            ],
5954        ),
5955        TypeRef::Option(t, _) => {
5956            TsType::named_with_args("Option", vec![ts_type_ref_to_ts_type(t, qualify)])
5957        }
5958        TypeRef::Effect(t, _) => {
5959            let inner = ts_type_ref_to_ts_type(t, qualify);
5960            if is_bare_named(&inner, "()") || is_bare_named(&inner, "void") {
5961                TsType::named_with_args("globalThis.Promise", vec![TsType::named("void")])
5962            } else {
5963                TsType::named_with_args("globalThis.Promise", vec![inner])
5964            }
5965        }
5966        TypeRef::HttpResult(t, _) => {
5967            TsType::named_with_args("HttpResult", vec![ts_type_ref_to_ts_type(t, qualify)])
5968        }
5969        // v0.20b: collections lower to immutable TS shapes.
5970        TypeRef::List(t, _) => TsType::readonly_array(ts_type_ref_to_ts_type(t, qualify)),
5971        // A `Query[T]`'s own real shape is `(() => readonly T[])` — an
5972        // *outer* paren pair around the whole function type (found by the
5973        // zero-diff check this slice's own "Done when" requires: the
5974        // general `TsType::Fn` rendering, correctly matching the real
5975        // parametered-function-type case, has no such wrap, but every
5976        // fixture's own Query annotation does — e.g.
5977        // `bynkc/tests/fixtures/positive/302_query_annotated_let/expected/
5978        // probe.ts:46`). `TsType` has no general parenthesisation wrapper
5979        // (adding one for this single real use would be inventing a new
5980        // variant beyond this slice's own authorised gap list — `readonly`
5981        // and `Fn`, nothing else); pre-rendering the `Fn` type immediately
5982        // and wrapping the *text* in parens, carried as an opaque `Named`
5983        // with no type arguments (which prints its `name` verbatim,
5984        // byte-for-byte, whatever it contains), reproduces the real shape
5985        // exactly without widening the algebra. Worth reconsidering if a
5986        // second real case ever needs the same wrap.
5987        TypeRef::Query(t, _) => {
5988            let fn_ty = TsType::Fn {
5989                params: vec![],
5990                ret: Box::new(TsType::readonly_array(ts_type_ref_to_ts_type(t, qualify))),
5991            };
5992            TsType::named(format!("({})", bynk_ts::print_type(&fn_ty)))
5993        }
5994        // v0.100: `Stream[T]` lowers to a host async iterable.
5995        TypeRef::Stream(t, _) => {
5996            TsType::named_with_args("AsyncIterable", vec![ts_type_ref_to_ts_type(t, qualify)])
5997        }
5998        // v0.102: a `Connection[F]` lowers to the runtime `Connection<F>`
5999        // interface (the concrete implementation arrives with the protocol).
6000        TypeRef::Connection(t, _) => {
6001            TsType::named_with_args("Connection", vec![ts_type_ref_to_ts_type(t, qualify)])
6002        }
6003        // v0.119: `History[Agent]` is a test-only generator with no emitted TS
6004        // type — it never reaches a signature/field position (the property runner
6005        // binds the driven history as an ordinary array). Rendered defensively.
6006        TypeRef::History(_, _) => TsType::named("never"),
6007        TypeRef::Map(k, v, _) => TsType::named_with_args(
6008            "ReadonlyMap",
6009            vec![
6010                ts_type_ref_to_ts_type(k, qualify),
6011                ts_type_ref_to_ts_type(v, qualify),
6012            ],
6013        ),
6014        TypeRef::QueueResult(_) => TsType::named("QueueResult"),
6015        TypeRef::Asset(_) => asset_ts_type(),
6016        TypeRef::RequestHeaders(_) => request_headers_ts_type(),
6017        TypeRef::TokenClaims(_) => token_claims_ts_type(),
6018        TypeRef::BoundaryRejection(_) => {
6019            TsType::named("{ readonly tag: string; readonly detail: string }")
6020        }
6021        TypeRef::ValidationError(_) => TsType::named("ValidationError"),
6022        TypeRef::JsonError(_) => TsType::named("JsonError"),
6023        TypeRef::Unit(_) => TsType::named("void"),
6024        // v0.157 (ADR 0183): `Name[Arg, …]` lowers to the erased TS generic
6025        // `Name<Arg, …>` — the generic record's interface is emitted with the
6026        // same type parameters (like a generic function's erased `<A, B>`).
6027        TypeRef::App { name, args, .. } => {
6028            // Review of #1315/#1316: pre-slice this arm interpolated `head`
6029            // unconditionally, so a hypothetical empty-args `App` rendered
6030            // `Head<>` — `TsType::named_with_args(head, vec![])` instead
6031            // renders bare `Head` (`TsType::Named`'s own printer only opens
6032            // `<...>` when `type_args` is non-empty), a real behaviour
6033            // change *if* this arm is ever reached with empty `args`. It
6034            // isn't today: `ty_to_type_ref` only ever builds `App` with a
6035            // non-empty argument list (this file, `ty_to_type_ref`'s own
6036            // `Ty::Named` arm), and the resolver arity-checks a
6037            // `Name[Arg, …]` application before either side ever sees one.
6038            // Asserted, not silently relied on, so a future caller that
6039            // *can* reach here with no args fails loudly instead of
6040            // silently changing bytes.
6041            debug_assert!(
6042                !args.is_empty(),
6043                "TypeRef::App is only ever built with a non-empty argument list"
6044            );
6045            let head = if let Some(f) = qualify
6046                && let Some(ns) = f(&name.name)
6047            {
6048                format!("{ns}.{}", name.name)
6049            } else {
6050                name.name.clone()
6051            };
6052            let rendered: Vec<TsType> = args
6053                .iter()
6054                .map(|a| ts_type_ref_to_ts_type(a, qualify))
6055                .collect();
6056            TsType::named_with_args(head, rendered)
6057        }
6058        // v0.20a: a function type lowers to a TS function type. Positional
6059        // parameter names (`a0`, `a1`, …) are the printer's own job now
6060        // (`bynk_ts::print_type`'s `TsType::Fn` rendering) — TS requires
6061        // names in function type syntax; an Effect return is already
6062        // Promise via recursion.
6063        TypeRef::Fn(params, ret, _) => {
6064            let params: Vec<TsType> = params
6065                .iter()
6066                .map(|p| ts_type_ref_to_ts_type(p, qualify))
6067                .collect();
6068            let ret = ts_type_ref_to_ts_type(ret, qualify);
6069            let ret = if is_bare_named(&ret, "()") {
6070                TsType::named("void")
6071            } else {
6072                ret
6073            };
6074            TsType::Fn {
6075                params,
6076                ret: Box::new(ret),
6077            }
6078        }
6079    }
6080}
6081
6082/// P7.9 (#1315): pins `ts_type_ref`'s exact pre-slice text for a
6083/// representative `TypeRef` from every real shape category, the same
6084/// discipline this track's own history names for "prove the test would
6085/// actually catch it" (P7.2's `brand_assertion` gap, P7.3's escaping gap —
6086/// both invisible to the byte-golden fixture corpus alone). These pin the
6087/// literal strings `ts_type_ref_with` built by hand before this slice
6088/// rebuilt it to construct a real `bynk_ts::TsType` and print that instead
6089/// — a regression here is a regression in every type-position string the
6090/// compiler emits, not caught by any one fixture necessarily exercising
6091/// this exact shape.
6092#[cfg(test)]
6093mod ts_type_ref_tests {
6094    use super::*;
6095
6096    fn sp() -> bynk_syntax::span::Span {
6097        bynk_syntax::span::Span::new(0, 0)
6098    }
6099
6100    fn base(b: BaseType) -> TypeRef {
6101        TypeRef::Base(b, sp())
6102    }
6103
6104    fn named(name: &str) -> TypeRef {
6105        TypeRef::Named(Ident {
6106            name: name.to_string(),
6107            span: sp(),
6108        })
6109    }
6110
6111    #[test]
6112    fn bare_named_type() {
6113        assert_eq!(ts_type_ref(&named("Order")), "Order");
6114    }
6115
6116    #[test]
6117    fn result_type() {
6118        let r = TypeRef::Result(
6119            Box::new(base(BaseType::Int)),
6120            Box::new(named("MyError")),
6121            sp(),
6122        );
6123        assert_eq!(ts_type_ref(&r), "Result<number, MyError>");
6124    }
6125
6126    #[test]
6127    fn option_type() {
6128        let r = TypeRef::Option(Box::new(base(BaseType::String)), sp());
6129        assert_eq!(ts_type_ref(&r), "Option<string>");
6130    }
6131
6132    #[test]
6133    fn effect_of_a_non_unit_type_becomes_promise() {
6134        let r = TypeRef::Effect(Box::new(base(BaseType::Int)), sp());
6135        assert_eq!(ts_type_ref(&r), "globalThis.Promise<number>");
6136    }
6137
6138    #[test]
6139    fn effect_of_unit_becomes_promise_void() {
6140        let r = TypeRef::Effect(Box::new(TypeRef::Unit(sp())), sp());
6141        assert_eq!(ts_type_ref(&r), "globalThis.Promise<void>");
6142    }
6143
6144    #[test]
6145    fn http_result_type() {
6146        let r = TypeRef::HttpResult(Box::new(named("Order")), sp());
6147        assert_eq!(ts_type_ref(&r), "HttpResult<Order>");
6148    }
6149
6150    #[test]
6151    fn list_is_a_readonly_array() {
6152        let r = TypeRef::List(Box::new(named("Order")), sp());
6153        assert_eq!(ts_type_ref(&r), "readonly Order[]");
6154    }
6155
6156    /// The one shape this slice's own gap analysis found: `Query`'s real
6157    /// text wraps the whole function type in an *extra* outer paren pair
6158    /// (`(() => readonly T[])`), distinct from a bare `Fn` type's own
6159    /// convention (no outer wrap) — caught only by this zero-diff check
6160    /// against `bynkc/tests/fixtures/positive/302_query_annotated_let/
6161    /// expected/probe.ts`, not by reasoning about the algebra alone.
6162    #[test]
6163    fn query_wraps_the_whole_function_type_in_parens() {
6164        let r = TypeRef::Query(Box::new(named("Order")), sp());
6165        assert_eq!(ts_type_ref(&r), "(() => readonly Order[])");
6166    }
6167
6168    #[test]
6169    fn stream_is_an_async_iterable() {
6170        let r = TypeRef::Stream(Box::new(named("Order")), sp());
6171        assert_eq!(ts_type_ref(&r), "AsyncIterable<Order>");
6172    }
6173
6174    #[test]
6175    fn connection_type() {
6176        let r = TypeRef::Connection(Box::new(named("Frame")), sp());
6177        assert_eq!(ts_type_ref(&r), "Connection<Frame>");
6178    }
6179
6180    #[test]
6181    fn history_renders_defensively_as_never() {
6182        let r = TypeRef::History(Box::new(named("Agent")), sp());
6183        assert_eq!(ts_type_ref(&r), "never");
6184    }
6185
6186    #[test]
6187    fn map_is_a_readonly_map() {
6188        let r = TypeRef::Map(
6189            Box::new(base(BaseType::String)),
6190            Box::new(named("Order")),
6191            sp(),
6192        );
6193        assert_eq!(ts_type_ref(&r), "ReadonlyMap<string, Order>");
6194    }
6195
6196    #[test]
6197    fn the_four_bare_named_types() {
6198        assert_eq!(ts_type_ref(&TypeRef::QueueResult(sp())), "QueueResult");
6199        assert_eq!(
6200            ts_type_ref(&TypeRef::ValidationError(sp())),
6201            "ValidationError"
6202        );
6203        assert_eq!(ts_type_ref(&TypeRef::JsonError(sp())), "JsonError");
6204        assert_eq!(ts_type_ref(&TypeRef::Unit(sp())), "void");
6205    }
6206
6207    #[test]
6208    fn app_is_a_generic_instantiation() {
6209        let r = TypeRef::App {
6210            name: Ident {
6211                name: "MyGeneric".to_string(),
6212                span: sp(),
6213            },
6214            args: vec![base(BaseType::Int), named("Order")],
6215            span: sp(),
6216        };
6217        assert_eq!(ts_type_ref(&r), "MyGeneric<number, Order>");
6218    }
6219
6220    #[test]
6221    fn real_function_type_uses_positional_parameter_names() {
6222        let r = TypeRef::Fn(
6223            vec![base(BaseType::String), base(BaseType::Int)],
6224            Box::new(named("Order")),
6225            sp(),
6226        );
6227        assert_eq!(ts_type_ref(&r), "(a0: string, a1: number) => Order");
6228    }
6229
6230    #[test]
6231    fn base_types() {
6232        assert_eq!(ts_type_ref(&base(BaseType::Int)), "number");
6233        assert_eq!(ts_type_ref(&base(BaseType::Float)), "number");
6234        assert_eq!(ts_type_ref(&base(BaseType::Duration)), "number");
6235        assert_eq!(ts_type_ref(&base(BaseType::Instant)), "number");
6236        assert_eq!(ts_type_ref(&base(BaseType::String)), "string");
6237        assert_eq!(ts_type_ref(&base(BaseType::Bool)), "boolean");
6238        assert_eq!(ts_type_ref(&base(BaseType::Bytes)), "globalThis.Uint8Array");
6239    }
6240
6241    /// Coverage gap named in review of #1315/#1316: every test above goes
6242    /// through `ts_type_ref` (`qualify = None`) — the qualifying branches
6243    /// this slice's own refactor most directly restructured (`App`'s `head`
6244    /// now flows through `TsType::named_with_args` instead of being
6245    /// interpolated) were unpinned. Asserts both that the head qualifies
6246    /// *and* that qualification recurses into the type arguments.
6247    #[test]
6248    fn qualified_generic_instantiation_qualifies_head_and_recurses_into_args() {
6249        let r = TypeRef::App {
6250            name: Ident {
6251                name: "MyGeneric".to_string(),
6252                span: sp(),
6253            },
6254            args: vec![base(BaseType::Int), named("Order")],
6255            span: sp(),
6256        };
6257        let mut scope = HashSet::new();
6258        scope.insert("MyGeneric".to_string());
6259        scope.insert("Order".to_string());
6260        assert_eq!(
6261            bynk_ts::print_type(&ts_type_ref_qualified_ts_type(&r, &scope, "Ns")),
6262            "Ns.MyGeneric<number, Ns.Order>"
6263        );
6264    }
6265
6266    /// Same shape as above, through `ts_type_ref_qualified_multi_ts_type` —
6267    /// no direct test existed for this function at all before this fix.
6268    #[test]
6269    fn qualified_multi_generic_instantiation_qualifies_head_and_recurses_into_args() {
6270        let r = TypeRef::App {
6271            name: Ident {
6272                name: "MyGeneric".to_string(),
6273                span: sp(),
6274            },
6275            args: vec![base(BaseType::Int), named("Order")],
6276            span: sp(),
6277        };
6278        let mut type_ns = HashMap::new();
6279        type_ns.insert("MyGeneric".to_string(), "A".to_string());
6280        type_ns.insert("Order".to_string(), "B".to_string());
6281        assert_eq!(
6282            bynk_ts::print_type(&ts_type_ref_qualified_multi_ts_type(&r, &type_ns)),
6283            "A.MyGeneric<number, B.Order>"
6284        );
6285    }
6286}
6287
6288/// v0.20b: render a checker `Ty` as a TypeScript type. Used by the inline
6289/// kernel-method lowerings, whose IIFE parameters must be annotated
6290/// (`noImplicitAny`). Rigid type variables render as themselves — inside an
6291/// emitted generic function they are in scope as TS type parameters.
6292/// P7.9 (#1315, R7.2): renders `t` by building a real [`bynk_ts::TsType`]
6293/// and printing it through [`bynk_ts::print_type`], instead of `format!`-ing
6294/// the type text by hand — this function's own signature and every one of
6295/// its ~45 real callers are unchanged; only the internal construction moved.
6296/// `Ty::ActorSum` needed a new gap closed beyond the accepted proposal's own
6297/// `readonly`/`Fn` list — a real, grounded one (found during implementation
6298/// review, not speculative): a resolved multi-actor sum lowers to a genuine
6299/// type-position union no existing `TsType` variant could represent, closed
6300/// by adding `TsType::Union` (see its own doc).
6301fn ts_ty(t: TyId, tys: &Arc<Types>) -> String {
6302    bynk_ts::print_type(&ts_ty_to_ts_type(t, tys))
6303}
6304
6305/// `elem_ts` (a [`ts_ty`] rendering) made safe to suffix with `[]`, for the
6306/// kernel lowerings that `format!` an array type around an element's text
6307/// rather than building a [`TsType::Array`]. `[]` binds tighter than a prefix
6308/// `readonly`, a function arrow, `|` and `&`, so `readonly readonly T[][]`
6309/// (TS1354) or `(a0: T) => T[]` would be misread; such an element is
6310/// parenthesised. The check is on text, so it is conservative — an operator
6311/// nested inside a generic argument parenthesises too, which is redundant but
6312/// still valid TypeScript. The printer's own `TsType::Array` arm applies the
6313/// exact structural rule.
6314fn ts_array_elem(elem_ts: &str) -> String {
6315    if elem_ts.starts_with("readonly ")
6316        || elem_ts.contains("=>")
6317        || elem_ts.contains('|')
6318        || elem_ts.contains('&')
6319    {
6320        format!("({elem_ts})")
6321    } else {
6322        elem_ts.to_string()
6323    }
6324}
6325
6326/// #1927: how the module being emitted spells a type it names. A unit's own
6327/// value (a function, asset, service, agent or provider) can hide a
6328/// `uses`-imported type of its name. The unit's source may not write that
6329/// type, but the lowering still names the types of checked expressions (#1778:
6330/// a kernel's `(__x: Name) => …`, an `if` slot, `List.empty`), and in this
6331/// module `Name` is the local value: a `const`, or for an agent or provider a
6332/// `class`, whose instance type would capture the annotation. So while such a
6333/// module is emitted, each hidden type is spelled as an inline import type,
6334/// `import("<module>").Name`, which needs no import statement and names the
6335/// declaring module's type whatever this module declares. Agents are never
6336/// `Ty::Named`, so within the unit a hidden name always means the imported
6337/// type.
6338///
6339/// Scoped per module, not threaded: [`ts_ty`] has ~45 callers and no module
6340/// context. [`type_renames::enter`] sets the spellings for this thread until its
6341/// guard drops, and a module is lowered on one thread.
6342pub(crate) mod type_renames {
6343    use std::cell::RefCell;
6344    use std::collections::HashMap;
6345
6346    thread_local! {
6347        static SPELLINGS: RefCell<HashMap<String, String>> = RefCell::new(HashMap::new());
6348    }
6349
6350    /// Restores the spellings in force before [`enter`] when it drops.
6351    pub(crate) struct Guard(HashMap<String, String>);
6352
6353    impl Drop for Guard {
6354        fn drop(&mut self) {
6355            let previous = std::mem::take(&mut self.0);
6356            SPELLINGS.with(|s| *s.borrow_mut() = previous);
6357        }
6358    }
6359
6360    /// Spell each type in `spellings` (name → TypeScript text) until the
6361    /// returned guard drops.
6362    #[must_use]
6363    pub(crate) fn enter(spellings: HashMap<String, String>) -> Guard {
6364        Guard(SPELLINGS.with(|s| std::mem::replace(&mut *s.borrow_mut(), spellings)))
6365    }
6366
6367    /// How the current module spells the type `name`: its rename, if any.
6368    pub(crate) fn spelling(name: &str) -> String {
6369        SPELLINGS
6370            .with(|s| s.borrow().get(name).cloned())
6371            .unwrap_or_else(|| name.to_string())
6372    }
6373}
6374
6375fn ts_ty_to_ts_type(t: TyId, tys: &Arc<Types>) -> TsType {
6376    match &*tys.get(t) {
6377        // bynk internal error (finding #28, R4.3): `Ty::Error` records a
6378        // resolution failure, which per R4.3 is always accompanied by a
6379        // pushed diagnostic — the check that produced it should have failed
6380        // the whole program and never reached emission. A loud failure here
6381        // beats silently emitting a type for a node the checker gave up on.
6382        Ty::Error => panic!(
6383            "bynk internal error (finding #28): emitter asked to render `Ty::Error` as a \
6384             TypeScript type — a checked program should never contain one"
6385        ),
6386        Ty::Base(b) => TsType::named(ts_base(*b)),
6387        // #1977: the compiler-known `MapEntry[K, V]` (ADR 0184) has no
6388        // `TypeDecl`, so no module declares or imports a `MapEntry` type to
6389        // name. It renders as its own shape, the `{ key, value }` record the
6390        // `.entries` lowering builds. (A user type of the same name, which the
6391        // checker prefers for field access, interns to the same `Ty` and so
6392        // renders inline too; no program declares one today.)
6393        Ty::Named {
6394            name,
6395            kind: NamedKind::Record,
6396            args,
6397        } if name == MAP_ENTRY && args.len() == 2 => TsType::Object(vec![
6398            bynk_ts::TsTypeMember::readonly_prop("key", ts_ty_to_ts_type(args[0], tys)),
6399            bynk_ts::TsTypeMember::readonly_prop("value", ts_ty_to_ts_type(args[1], tys)),
6400        ]),
6401        // v0.157 (ADR 0183): a generic record instantiation renders as the
6402        // erased TS generic `Name<Arg, …>`; a non-generic named type is bare.
6403        Ty::Named { name, args, .. } if args.is_empty() => {
6404            TsType::named(type_renames::spelling(name))
6405        }
6406        Ty::Named { name, args, .. } => TsType::named_with_args(
6407            type_renames::spelling(name),
6408            args.iter().map(|a| ts_ty_to_ts_type(*a, tys)).collect(),
6409        ),
6410        Ty::Result(t, e) => TsType::named_with_args(
6411            "Result",
6412            vec![ts_ty_to_ts_type(*t, tys), ts_ty_to_ts_type(*e, tys)],
6413        ),
6414        Ty::Option(t) => TsType::named_with_args("Option", vec![ts_ty_to_ts_type(*t, tys)]),
6415        Ty::Effect(t) => match &*tys.get(*t) {
6416            Ty::Unit => TsType::named_with_args("globalThis.Promise", vec![TsType::named("void")]),
6417            _ => TsType::named_with_args("globalThis.Promise", vec![ts_ty_to_ts_type(*t, tys)]),
6418        },
6419        Ty::HttpResult(t) => TsType::named_with_args("HttpResult", vec![ts_ty_to_ts_type(*t, tys)]),
6420        Ty::List(t) => TsType::readonly_array(ts_ty_to_ts_type(*t, tys)),
6421        // v0.91 (ADR 0119): a `Query[T]` lowers to a deferred producer of its
6422        // elements — a thunk run by the terminal. Same outer-paren shape as
6423        // `TypeRef::Query` (`ts_type_ref_to_ts_type`'s own comment explains
6424        // the opaque-`Named` wrap this reuses, unchanged reasoning).
6425        Ty::Query(t) => {
6426            let fn_ty = TsType::Fn {
6427                params: vec![],
6428                ret: Box::new(TsType::readonly_array(ts_ty_to_ts_type(*t, tys))),
6429            };
6430            TsType::named(format!("({})", bynk_ts::print_type(&fn_ty)))
6431        }
6432        // v0.100: a `Stream[T]` lowers to a host async iterable.
6433        Ty::Stream(t) => TsType::named_with_args("AsyncIterable", vec![ts_ty_to_ts_type(*t, tys)]),
6434        // v0.102: a `Connection[F]` lowers to the runtime `Connection<F>` interface.
6435        Ty::Connection(t) => TsType::named_with_args("Connection", vec![ts_ty_to_ts_type(*t, tys)]),
6436        Ty::Map(k, v) => TsType::named_with_args(
6437            "ReadonlyMap",
6438            vec![ts_ty_to_ts_type(*k, tys), ts_ty_to_ts_type(*v, tys)],
6439        ),
6440        Ty::QueueResult => TsType::named("QueueResult"),
6441        // #1934: produced only by a `system`-tier test-service address, so
6442        // only a test module meets them: the runtime's `HttpOutcome<T>`, and
6443        // the shape of its `Rejected` payload.
6444        Ty::HttpOutcome(t) => {
6445            TsType::named_with_args("HttpOutcome", vec![ts_ty_to_ts_type(*t, tys)])
6446        }
6447        Ty::BoundaryRejection => TsType::named("{ readonly tag: string; readonly detail: string }"),
6448        Ty::Asset => asset_ts_type(),
6449        Ty::RequestHeaders => request_headers_ts_type(),
6450        Ty::TokenClaims => token_claims_ts_type(),
6451        Ty::ValidationError => TsType::named("ValidationError"),
6452        Ty::JsonError => TsType::named("JsonError"),
6453        Ty::Unit => TsType::named("void"),
6454        Ty::Fn { params, ret } => TsType::Fn {
6455            params: params.iter().map(|p| ts_ty_to_ts_type(*p, tys)).collect(),
6456            ret: Box::new(ts_ty_to_ts_type(*ret, tys)),
6457        },
6458        Ty::Var(n) => TsType::named(n.clone()),
6459        // The identity type the actor binding yields (`name.identity`).
6460        Ty::Actor(id) => ts_ty_to_ts_type(*id, tys),
6461        // v0.52: a resolved multi-actor sum lowers to a discriminated union
6462        // tagged by actor name; non-unit members carry their identity.
6463        // Each member's own real text is `, `-separated (`{ tag: "x",
6464        // identity: T }`) — `TsType::Object`'s own renderer is `; `-
6465        // separated (the ordinary TS object-*type* convention, correct for
6466        // every other real caller, e.g. `events_fanout.rs`'s own interface
6467        // members), so building each member as an `Object` would silently
6468        // change this one shape's separator and fail the zero-diff check
6469        // (caught exactly that way — a fixture-corpus-adjacent direct test
6470        // failure, not reasoning about the algebra). Each member is instead
6471        // an opaque `Named`-carries-verbatim-text node, the same convention
6472        // `ts_type_ref_to_ts_type`'s own `Query` arm already established for
6473        // a single real shape `TsType`'s general renderers don't reproduce
6474        // byte-for-byte — not a new pattern, the second real use of one.
6475        Ty::ActorSum(members) => TsType::union(
6476            members
6477                .iter()
6478                .map(|(name, id)| match &*tys.get(*id) {
6479                    Ty::Unit => TsType::named(format!("{{ tag: \"{name}\" }}")),
6480                    _ => TsType::named(format!(
6481                        "{{ tag: \"{name}\", identity: {} }}",
6482                        bynk_ts::print_type(&ts_ty_to_ts_type(*id, tys))
6483                    )),
6484                })
6485                .collect(),
6486        ),
6487    }
6488}
6489
6490/// P7.9 (#1315): pins `ts_ty`'s exact pre-slice text for a representative
6491/// `Ty` from every real shape category — the same "prove the test would
6492/// actually catch it" discipline `ts_type_ref_tests` above already applies
6493/// to `ts_type_ref`, and doubly important here since `ts_ty` is where
6494/// `TsType::Union`/`ActorSum` (added in review of this same PR, beyond the
6495/// accepted proposal's own gap list) is the one real, exercised caller.
6496#[cfg(test)]
6497mod ts_ty_tests {
6498    use super::*;
6499    use bynk_check::checker::{NamedKind, Types};
6500
6501    #[test]
6502    fn base_types() {
6503        let tys = Arc::new(Types::new());
6504        assert_eq!(ts_ty(tys.intern(Ty::Base(BaseType::Int)), &tys), "number");
6505    }
6506
6507    #[test]
6508    fn named_type_bare_and_generic() {
6509        let tys = Arc::new(Types::new());
6510        let bare = tys.intern(Ty::Named {
6511            name: "Order".to_string(),
6512            kind: NamedKind::Record,
6513            args: vec![],
6514        });
6515        assert_eq!(ts_ty(bare, &tys), "Order");
6516
6517        let arg = tys.intern(Ty::Base(BaseType::String));
6518        let generic = tys.intern(Ty::Named {
6519            name: "Paginated".to_string(),
6520            kind: NamedKind::Record,
6521            args: vec![arg],
6522        });
6523        assert_eq!(ts_ty(generic, &tys), "Paginated<string>");
6524    }
6525
6526    #[test]
6527    fn map_entry_renders_its_own_shape() {
6528        // #1977: no module declares `MapEntry`, so it is never named.
6529        let tys = Arc::new(Types::new());
6530        let k = tys.intern(Ty::Base(BaseType::String));
6531        let v = tys.intern(Ty::Base(BaseType::Int));
6532        let entry = bynk_check::checker::map_entry_ty(k, v, &tys);
6533        assert_eq!(
6534            ts_ty(entry, &tys),
6535            "{ readonly key: string; readonly value: number }"
6536        );
6537        let list = tys.intern(Ty::List(entry));
6538        assert_eq!(
6539            ts_ty(list, &tys),
6540            "readonly { readonly key: string; readonly value: number }[]"
6541        );
6542    }
6543
6544    #[test]
6545    fn result_option_effect_http_result() {
6546        let tys = Arc::new(Types::new());
6547        let int = tys.intern(Ty::Base(BaseType::Int));
6548        let err = tys.intern(Ty::Named {
6549            name: "MyError".to_string(),
6550            kind: NamedKind::Record,
6551            args: vec![],
6552        });
6553        let result = tys.intern(Ty::Result(int, err));
6554        assert_eq!(ts_ty(result, &tys), "Result<number, MyError>");
6555
6556        let option = tys.intern(Ty::Option(int));
6557        assert_eq!(ts_ty(option, &tys), "Option<number>");
6558
6559        let effect = tys.intern(Ty::Effect(int));
6560        assert_eq!(ts_ty(effect, &tys), "globalThis.Promise<number>");
6561
6562        let unit = tys.intern(Ty::Unit);
6563        let effect_unit = tys.intern(Ty::Effect(unit));
6564        assert_eq!(ts_ty(effect_unit, &tys), "globalThis.Promise<void>");
6565
6566        let http_result = tys.intern(Ty::HttpResult(int));
6567        assert_eq!(ts_ty(http_result, &tys), "HttpResult<number>");
6568    }
6569
6570    #[test]
6571    fn list_map_stream_connection() {
6572        let tys = Arc::new(Types::new());
6573        let order = tys.intern(Ty::Named {
6574            name: "Order".to_string(),
6575            kind: NamedKind::Record,
6576            args: vec![],
6577        });
6578        assert_eq!(ts_ty(tys.intern(Ty::List(order)), &tys), "readonly Order[]");
6579        // A nested list's inner readonly array is parenthesised (TS1354).
6580        let orders = tys.intern(Ty::List(order));
6581        assert_eq!(
6582            ts_ty(tys.intern(Ty::List(orders)), &tys),
6583            "readonly (readonly Order[])[]"
6584        );
6585        assert_eq!(ts_array_elem(&ts_ty(orders, &tys)), "(readonly Order[])");
6586        assert_eq!(ts_array_elem(&ts_ty(order, &tys)), "Order");
6587
6588        let s = tys.intern(Ty::Base(BaseType::String));
6589        assert_eq!(
6590            ts_ty(tys.intern(Ty::Map(s, order)), &tys),
6591            "ReadonlyMap<string, Order>"
6592        );
6593        assert_eq!(
6594            ts_ty(tys.intern(Ty::Stream(order)), &tys),
6595            "AsyncIterable<Order>"
6596        );
6597        assert_eq!(
6598            ts_ty(tys.intern(Ty::Connection(order)), &tys),
6599            "Connection<Order>"
6600        );
6601    }
6602
6603    /// The same shared shape as `TypeRef::Query` — pins that `ts_ty`'s own
6604    /// `Query` arm reproduces the identical outer-paren wrap after
6605    /// conversion, not just `ts_type_ref`'s.
6606    #[test]
6607    fn query_wraps_the_whole_function_type_in_parens() {
6608        let tys = Arc::new(Types::new());
6609        let order = tys.intern(Ty::Named {
6610            name: "Order".to_string(),
6611            kind: NamedKind::Record,
6612            args: vec![],
6613        });
6614        assert_eq!(
6615            ts_ty(tys.intern(Ty::Query(order)), &tys),
6616            "(() => readonly Order[])"
6617        );
6618    }
6619
6620    #[test]
6621    fn bare_types_and_fn() {
6622        let tys = Arc::new(Types::new());
6623        assert_eq!(ts_ty(tys.intern(Ty::QueueResult), &tys), "QueueResult");
6624        assert_eq!(
6625            ts_ty(tys.intern(Ty::ValidationError), &tys),
6626            "ValidationError"
6627        );
6628        assert_eq!(ts_ty(tys.intern(Ty::JsonError), &tys), "JsonError");
6629        assert_eq!(ts_ty(tys.intern(Ty::Unit), &tys), "void");
6630
6631        let int = tys.intern(Ty::Base(BaseType::Int));
6632        let s = tys.intern(Ty::Base(BaseType::String));
6633        let f = tys.intern(Ty::Fn {
6634            params: vec![int, s],
6635            ret: int,
6636        });
6637        assert_eq!(ts_ty(f, &tys), "(a0: number, a1: string) => number");
6638    }
6639
6640    /// The real gap found during implementation review: a resolved
6641    /// multi-actor sum lowers to a genuine type-position union, tagged by
6642    /// actor name, non-unit members carrying their identity — the shape
6643    /// that motivated adding `TsType::Union` beyond the accepted proposal's
6644    /// own `readonly`/`Fn` gap list.
6645    #[test]
6646    fn actor_sum_is_a_tagged_union() {
6647        let tys = Arc::new(Types::new());
6648        let unit = tys.intern(Ty::Unit);
6649        let identity = tys.intern(Ty::Named {
6650            name: "AdminIdentity".to_string(),
6651            kind: NamedKind::Record,
6652            args: vec![],
6653        });
6654        let sum = tys.intern(Ty::ActorSum(vec![
6655            ("Guest".to_string(), unit),
6656            ("Admin".to_string(), identity),
6657        ]));
6658        assert_eq!(
6659            ts_ty(sum, &tys),
6660            "{ tag: \"Guest\" } | { tag: \"Admin\", identity: AdminIdentity }"
6661        );
6662    }
6663
6664    /// Coverage gap named in review of #1315/#1316: `Ty::Var` (a type
6665    /// variable's own bare name) and `Ty::Actor` (delegates to its
6666    /// identity type) were unpinned.
6667    #[test]
6668    fn var_and_actor() {
6669        let tys = Arc::new(Types::new());
6670        assert_eq!(ts_ty(tys.intern(Ty::Var("T".to_string())), &tys), "T");
6671
6672        let identity = tys.intern(Ty::Named {
6673            name: "AdminIdentity".to_string(),
6674            kind: NamedKind::Record,
6675            args: vec![],
6676        });
6677        let actor = tys.intern(Ty::Actor(identity));
6678        assert_eq!(ts_ty(actor, &tys), "AdminIdentity");
6679    }
6680}
6681
6682/// Takes the AST `BinOp` directly, on purpose. Phase 6 (P6.56, ADR 0381)
6683/// investigated converting this to an IR-side operator enum and declined:
6684/// its sole caller (`emitter/lower.rs`) holds an AST `BinOp` from
6685/// `ExprKind::BinOp` and separately compares `op == BinOp::Eq` a few lines
6686/// away, so converting here would only relocate the AST read into that
6687/// still-AST-walking caller, net zero. The IR cutover track (#1542, §10)
6688/// later confirmed the general form of that finding — retyping the
6689/// lowerer's reads one function at a time produces a second lowerer, not a
6690/// retype — and deleted the IR-side operator enum with the rest of the
6691/// expression IR (Slice D2).
6692fn ts_binop(op: BinOp) -> &'static str {
6693    match op {
6694        // `implies` has no single TS operator — `lower_bin_op` rewrites it to
6695        // `(!(P) || Q)` before reaching here, so this arm is never used.
6696        BinOp::Implies => "||",
6697        BinOp::Or => "||",
6698        BinOp::And => "&&",
6699        BinOp::Eq => "===",
6700        BinOp::NotEq => "!==",
6701        BinOp::Lt => "<",
6702        BinOp::LtEq => "<=",
6703        BinOp::Gt => ">",
6704        BinOp::GtEq => ">=",
6705        BinOp::Add => "+",
6706        BinOp::Sub => "-",
6707        BinOp::Mul => "*",
6708        BinOp::Div => "/",
6709    }
6710}
6711
6712/// #1653: the TypeScript property name of a sum variant's payload field. A
6713/// variant object already carries the in-memory discriminant `tag`, so a
6714/// payload field of that name moves to `$tag` (`$` is not a Bynk identifier
6715/// character, so no other field can be spelled that way). Only the in-memory
6716/// shape changes: the wire key stays the field's own name, and the codec maps
6717/// between the two.
6718pub(crate) fn payload_prop(field: &str) -> String {
6719    if field == "tag" {
6720        "$tag".to_string()
6721    } else {
6722        field.to_string()
6723    }
6724}
6725
6726/// The TypeScript spelling of a user identifier in a *binding or reference*
6727/// position (params, locals, function names, import names). Bynk identifiers
6728/// that are illegal as TS binding names — the JS reserved words plus the
6729/// strict-mode/module sets (emitted modules are always strict ESM) — and
6730/// names the emitter itself introduces alongside user bindings (`deps`) are
6731/// renamed into the generated-name namespace (`__id_<name>`), which the
6732/// parser keeps free of user identifiers. Property/field names never pass
6733/// through here: reserved words are legal there, and record field names are
6734/// wire format.
6735pub(crate) fn ts_ident(name: &str) -> String {
6736    const RESERVED: &[&str] = &[
6737        // ES reserved words.
6738        "break",
6739        "case",
6740        "catch",
6741        "class",
6742        "const",
6743        "continue",
6744        "debugger",
6745        "default",
6746        "delete",
6747        "do",
6748        "else",
6749        "enum",
6750        "export",
6751        "extends",
6752        "false",
6753        "finally",
6754        "for",
6755        "function",
6756        "if",
6757        "import",
6758        "in",
6759        "instanceof",
6760        "new",
6761        "null",
6762        "return",
6763        "super",
6764        "switch",
6765        "this",
6766        "throw",
6767        "true",
6768        "try",
6769        "typeof",
6770        "var",
6771        "void",
6772        "while",
6773        "with",
6774        // Strict-mode reserved (emitted modules are always strict).
6775        "implements",
6776        "interface",
6777        "let",
6778        "package",
6779        "private",
6780        "protected",
6781        "public",
6782        "static",
6783        "yield",
6784        // Module-code reserved.
6785        "await",
6786        // Illegal binding targets in strict mode.
6787        "arguments",
6788        "eval",
6789        // #1653: the emitted code reaches host globals through `globalThis`
6790        // (`globalThis.JSON`), so no user binding may take that name.
6791        "globalThis",
6792        // Generated identifiers a user binding may sit next to: handler
6793        // signatures append a `deps` parameter, so a user param named `deps`
6794        // would otherwise duplicate it.
6795        "deps",
6796    ];
6797    if RESERVED.contains(&name) {
6798        format!("__id_{name}")
6799    } else {
6800        name.to_string()
6801    }
6802}
6803
6804/// Delegates to `bynk_check::wire_default::escape_ts_literal` — the two
6805/// splice into generated TypeScript from opposite sides (real emission here,
6806/// event-field wire defaults there), so a correction to the escaping rules
6807/// must land once, not drift between two copies.
6808pub(crate) fn escape_ts_string(s: &str) -> String {
6809    bynk_check::wire_default::escape_ts_literal(s)
6810}
6811
6812/// #661 (Decision D)/#70 review: the one `PredKind` → runtime-check mapping,
6813/// shared by the owner-side check (`emit::emit_pred_check`, over a `value`
6814/// binding) and the boundary-side inline check
6815/// (`serialisation::emit_inline_pred_check`, over a `json` binding) — the
6816/// two used to hand-roll this mapping independently, pinned identical only by
6817/// a comment, so amending one (e.g. the `Matches` regex's `^(?:…)$` anchoring)
6818/// could silently drift from the other. `receiver` is the bound name the
6819/// generated condition reads (`value` or `json`); the returned message is the
6820/// same either side of the boundary by construction.
6821///
6822/// #1471: `cond` is a real [`bynk_ts::TsExpr`], not opaque text — the
6823/// blocker (`bynk_ts::TsBinaryOp` had no `>=`/`<=`) is resolved by that
6824/// enum's own new `GreaterThanEq`/`LessThanEq` variants. Both real callers
6825/// wrap the returned expression in their own `Unary::Not`/`Paren` unchanged;
6826/// only this function's own arms changed, from `format!`ing condition text
6827/// to building the equivalent node tree.
6828///
6829/// Review of #1336: both real callers (`emit::emit_pred_check`,
6830/// `serialisation::emit_inline_pred_check`) splice the returned `message`
6831/// straight into a TypeScript string literal **unescaped** — safe today only
6832/// because every arm's own message is either static English text or already
6833/// `escape_ts_string`-escaped (the `Matches` arm's own pattern). A future arm
6834/// returning raw, unescaped text (a predicate carrying a user-supplied string
6835/// operand, say) would emit a malformed or injectable literal at both call
6836/// sites, and nothing in the existing fixture corpus would catch it. This
6837/// invariant must match at every arm added here, not just the ones that exist
6838/// today — return plain text or already-`escape_ts_string`-escaped text only.
6839/// `msg` stays exactly this opaque `String`, unchanged by #1471: only the
6840/// `cond` side of the pair became a real node (see the `Matches` arm below
6841/// for why the message keeps its own already-escaped copy of the pattern
6842/// separate from the condition's raw, unescaped one).
6843pub(crate) fn pred_condition_and_message(
6844    pred: &PredKind,
6845    receiver: &str,
6846) -> (bynk_ts::TsExpr, String) {
6847    use bynk_ts::{TsBinaryOp, TsExpr, TsLit};
6848
6849    let recv = || TsExpr::Ident(receiver.to_string());
6850    let recv_length = || TsExpr::Member {
6851        object: Box::new(recv()),
6852        property: "length".to_string(),
6853    };
6854    let num = |n: String| TsExpr::Lit(TsLit::Num(n));
6855    let cmp = |op, left, right| TsExpr::Binary {
6856        op,
6857        left: Box::new(left),
6858        right: Box::new(right),
6859    };
6860
6861    match pred {
6862        PredKind::NonNegative => (
6863            cmp(TsBinaryOp::GreaterThanEq, recv(), num("0".to_string())),
6864            "must be non-negative".to_string(),
6865        ),
6866        PredKind::Positive => (
6867            cmp(TsBinaryOp::GreaterThan, recv(), num("0".to_string())),
6868            "must be positive".to_string(),
6869        ),
6870        PredKind::InRange(a, b) => {
6871            let (a, b) = (a.value, b.value);
6872            (
6873                cmp(
6874                    TsBinaryOp::And,
6875                    cmp(TsBinaryOp::GreaterThanEq, recv(), num(a.to_string())),
6876                    cmp(TsBinaryOp::LessThanEq, recv(), num(b.to_string())),
6877                ),
6878                format!("must be in range [{a}, {b}]"),
6879            )
6880        }
6881        PredKind::InRangeF(a, b) => {
6882            let (a, b) = (&a.lexeme, &b.lexeme);
6883            (
6884                cmp(
6885                    TsBinaryOp::And,
6886                    cmp(TsBinaryOp::GreaterThanEq, recv(), num(a.clone())),
6887                    cmp(TsBinaryOp::LessThanEq, recv(), num(b.clone())),
6888                ),
6889                format!("must be in range [{a}, {b}]"),
6890            )
6891        }
6892        PredKind::NonEmpty => (
6893            cmp(TsBinaryOp::GreaterThan, recv_length(), num("0".to_string())),
6894            "must be non-empty".to_string(),
6895        ),
6896        PredKind::MinLength(n) => (
6897            cmp(TsBinaryOp::GreaterThanEq, recv_length(), num(n.to_string())),
6898            format!("length must be at least {n}"),
6899        ),
6900        PredKind::MaxLength(n) => (
6901            cmp(TsBinaryOp::LessThanEq, recv_length(), num(n.to_string())),
6902            format!("length must be at most {n}"),
6903        ),
6904        PredKind::Length(n) => (
6905            cmp(TsBinaryOp::StrictEq, recv_length(), num(n.to_string())),
6906            format!("length must be exactly {n}"),
6907        ),
6908        PredKind::Matches(pat) => {
6909            // The condition's own `RegExp` source uses `pat` raw, not
6910            // `escaped` — `TsLit::Str`'s printer already applies the exact
6911            // same escaping as `escape_ts_string`
6912            // (`bynk_check::wire_default::escape_ts_literal`, kept
6913            // deliberately identical — see `bynk-ts/src/printer.rs`'s own
6914            // `render_lit`), so escaping `pat` here too would double-escape
6915            // it, same bug class `msg`'s own doc above already argues
6916            // against. `escaped` is still needed for `msg`, which is opaque
6917            // text spliced with no further escaping.
6918            let escaped = escape_ts_string(pat);
6919            let pattern = cmp(
6920                TsBinaryOp::Add,
6921                cmp(
6922                    TsBinaryOp::Add,
6923                    TsExpr::Lit(TsLit::Str("^(?:".to_string())),
6924                    TsExpr::Lit(TsLit::Str(pat.clone())),
6925                ),
6926                TsExpr::Lit(TsLit::Str(")$".to_string())),
6927            );
6928            let regexp = TsExpr::New {
6929                callee: Box::new(TsExpr::Ident("globalThis.RegExp".to_string())),
6930                args: vec![pattern],
6931            };
6932            let test_call = TsExpr::Call {
6933                callee: Box::new(TsExpr::Member {
6934                    object: Box::new(regexp),
6935                    property: "test".to_string(),
6936                }),
6937                args: vec![recv()],
6938            };
6939            (test_call, format!("must match /{escaped}/"))
6940        }
6941    }
6942}
6943
6944#[allow(dead_code)]
6945fn _unused_hashmap(_h: HashMap<String, ()>) {}
6946
6947#[cfg(test)]
6948mod runtime_tests {
6949    use super::*;
6950
6951    /// Every relative import in a per-module source names another module in the
6952    /// table, so any reachable subset can be closed over its own imports.
6953    #[test]
6954    fn per_module_runtime_imports_resolve_within_the_table() {
6955        let names: Vec<&str> = runtime_modules::RUNTIME_MODULES
6956            .iter()
6957            .map(|(n, _)| *n)
6958            .collect();
6959        for (name, src) in runtime_modules::RUNTIME_MODULES {
6960            for line in src.lines().filter(|l| {
6961                (l.starts_with("import ") || l.starts_with("} from ")) && l.contains('"')
6962            }) {
6963                let from = line.rsplit('"').nth(1).expect("import specifier");
6964                let target = from.strip_prefix("./").and_then(|f| f.strip_suffix(".ts"));
6965                assert!(
6966                    target.is_some_and(|t| names.contains(&t) && t != *name),
6967                    "{name}: import {from:?} does not name another runtime module"
6968                );
6969            }
6970        }
6971    }
6972
6973    /// The `__` aliases emitted code imports live in the module that defines
6974    /// each name, so a per-module import line finds them there.
6975    #[test]
6976    fn runtime_import_splits_per_defining_module() {
6977        let names: Vec<String> = ["Ok", "type Result", "__callService", "type __JsonValue"]
6978            .map(String::from)
6979            .to_vec();
6980        let got: Vec<(bool, Vec<String>, String)> =
6981            runtime_files::split_runtime_import(false, &names, "../runtime.js")
6982                .into_iter()
6983                .map(|d| match d {
6984                    bynk_ts::TsDecl::Import {
6985                        type_only,
6986                        names,
6987                        from,
6988                    } => (type_only, names, from),
6989                    _ => unreachable!(),
6990                })
6991                .collect();
6992        let v = |xs: &[&str]| xs.iter().map(|s| s.to_string()).collect::<Vec<_>>();
6993        assert_eq!(
6994            got,
6995            vec![
6996                (
6997                    false,
6998                    v(&["Ok", "type Result"]),
6999                    "../__bynk_runtime/result.js".to_string()
7000                ),
7001                (
7002                    false,
7003                    v(&["__callService", "type __JsonValue"]),
7004                    "../__bynk_runtime/boundary.js".to_string()
7005                ),
7006            ]
7007        );
7008    }
7009
7010    /// #2443: `HttpResult` lives apart from the HTTP response helpers, so a
7011    /// bundle-target service that only names it does not reach `http` or
7012    /// (through its type edges) `boundary`.
7013    #[test]
7014    fn http_result_is_reached_without_the_http_helpers() {
7015        use std::collections::BTreeSet;
7016        let names = vec!["HttpResult".to_string()];
7017        let decls = runtime_files::split_runtime_import(false, &names, "./runtime.js");
7018        assert!(matches!(
7019            &decls[..],
7020            [bynk_ts::TsDecl::Import { from, .. }] if from == "./__bynk_runtime/httpresult.js"
7021        ));
7022        let reached: BTreeSet<(String, String)> =
7023            [("httpresult".to_string(), "js".to_string())].into();
7024        let paths: Vec<String> = runtime_files::closure_files(&reached)
7025            .iter()
7026            .map(|(p, _)| p.display().to_string())
7027            .collect();
7028        assert_eq!(paths, ["__bynk_runtime/httpresult.ts"]);
7029    }
7030
7031    #[test]
7032    fn runtime_files_close_over_module_imports() {
7033        use std::collections::BTreeSet;
7034        let reached: BTreeSet<(String, String)> =
7035            [("equality".to_string(), "js".to_string())].into();
7036        let files = runtime_files::closure_files(&reached);
7037        let paths: Vec<String> = files.iter().map(|(p, _)| p.display().to_string()).collect();
7038        // equality -> bytes -> result.
7039        assert_eq!(
7040            paths,
7041            [
7042                "__bynk_runtime/bytes.ts",
7043                "__bynk_runtime/equality.ts",
7044                "__bynk_runtime/result.ts"
7045            ]
7046        );
7047        assert!(
7048            files.iter().all(|(_, s)| !s.contains(".ts\";")),
7049            "siblings are spelled for the target extension"
7050        );
7051        assert!(runtime_files::closure_files(&BTreeSet::new()).is_empty());
7052    }
7053
7054    #[test]
7055    fn handwritten_runtime_imports_are_rewritten() {
7056        let src = "import { None, Some, type Option, type KVNamespace } from \"../runtime.js\";\nimport { x } from \"./other.js\";\n";
7057        let out = runtime_files::rewrite_runtime_imports(src);
7058        assert!(
7059            out.contains(
7060                "import { None, Some, type Option } from \"../__bynk_runtime/result.js\";"
7061            ),
7062            "{out}"
7063        );
7064        assert!(
7065            out.contains("import type { KVNamespace } from \"../__bynk_runtime/storage.js\";"),
7066            "{out}"
7067        );
7068        assert!(out.contains("import { x } from \"./other.js\";"), "{out}");
7069    }
7070
7071    #[test]
7072    fn handwritten_runtime_import_forms_are_handled_or_left_alone() {
7073        let r = runtime_files::rewrite_runtime_imports;
7074        // Multi-line, unterminated, indented, trailing comment, CRLF.
7075        let multi = "import {\n  Ok,\n  Err,\n} from \"./runtime.js\";\nconst x = 1;\n";
7076        assert_eq!(
7077            r(multi),
7078            "import { Ok, Err } from \"./__bynk_runtime/result.js\";\nconst x = 1;\n"
7079        );
7080        assert_eq!(
7081            r("  import { Ok } from \"./runtime.js\" // keep\n"),
7082            "  import { Ok } from \"./__bynk_runtime/result.js\"; // keep\n"
7083        );
7084        assert_eq!(
7085            r("import { Ok } from \"./runtime.js\"\r\nx\r\n"),
7086            "import { Ok } from \"./__bynk_runtime/result.js\";\r\nx\r\n"
7087        );
7088        // An unknown name, a namespace import and a foreign specifier stay as written.
7089        for keep in [
7090            "import { Ok, Reslt } from \"./runtime.js\";\n",
7091            "import * as rt from \"./runtime.js\";\n",
7092            "import { a } from \"./lib/other.js\";\n",
7093        ] {
7094            assert_eq!(r(keep), keep);
7095        }
7096    }
7097
7098    #[test]
7099    fn an_unknown_runtime_module_emits_nothing_instead_of_panicking() {
7100        use std::collections::BTreeSet;
7101        let reached: BTreeSet<(String, String)> = [("nope".to_string(), "js".to_string())].into();
7102        assert!(runtime_files::closure_files(&reached).is_empty());
7103    }
7104
7105    #[test]
7106    fn per_module_runtime_carries_its_aliases() {
7107        let all: String = runtime_modules::RUNTIME_MODULES
7108            .iter()
7109            .map(|(_, s)| *s)
7110            .collect();
7111        for alias in [
7112            "__callService",
7113            "__matchPath",
7114            "__makeAgent",
7115            "__JsonValue",
7116            "__formatIcuDate",
7117        ] {
7118            assert!(
7119                all.contains(alias),
7120                "{alias} missing from the per-module runtime"
7121            );
7122        }
7123        let (_, http) = runtime_modules::RUNTIME_MODULES
7124            .iter()
7125            .find(|(n, _)| *n == "http")
7126            .unwrap();
7127        assert!(http.contains("matchPath as __matchPath"));
7128    }
7129
7130    #[test]
7131    fn runtime_emits_all_required_exports() {
7132        let s: String = emit_all_runtime_modules()
7133            .into_iter()
7134            .map(|(_, src)| src)
7135            .collect();
7136        // Core types and constructors used by every emitted module.
7137        assert!(s.contains("export type Result<T, E>"));
7138        assert!(s.contains("export const Ok"));
7139        assert!(s.contains("export const Err"));
7140        assert!(s.contains("export type Option<T>"));
7141        assert!(s.contains("export const Some"));
7142        assert!(s.contains("export const None"));
7143        assert!(s.contains("export interface ValidationError"));
7144        // Durable Object surface used by agent classes.
7145        assert!(s.contains("export interface DurableObjectStorage"));
7146        assert!(s.contains("export interface DurableObjectState"));
7147        assert!(s.contains("export class InMemoryStorage"));
7148        assert!(s.contains("export function makeTestState"));
7149        // Discriminator must be `tag` to match emitted code.
7150        assert!(s.contains("tag: \"Ok\""));
7151        assert!(s.contains("tag: \"Err\""));
7152        assert!(s.contains("tag: \"Some\""));
7153        assert!(s.contains("tag: \"None\""));
7154    }
7155
7156    #[test]
7157    fn tsconfig_is_well_formed_json() {
7158        let s = emit_tsconfig();
7159        // Spot-check the key fields; we don't reach for a JSON parser.
7160        assert!(s.contains("\"target\": \"ES2022\""));
7161        assert!(s.contains("\"strict\": true"));
7162        assert!(s.contains("\"include\""));
7163    }
7164
7165    #[test]
7166    fn coverage_tsconfig_enables_source_maps() {
7167        // #854: the coverage remap consumes tsc's `.js.map`s, so the variant must
7168        // set `sourceMap` — a guard against a silent string-replace miss if the
7169        // base config's `outDir` line is ever reworded. The default stays map-free
7170        // so a normal `bynkc test` / deployment build ships no `.js.map`s.
7171        let cov = emit_tsconfig_with_source_maps();
7172        assert!(
7173            cov.contains("\"sourceMap\": true"),
7174            "coverage config: {cov}"
7175        );
7176        assert!(cov.contains("\"outDir\": \"../out-js\""));
7177        assert!(!emit_tsconfig().contains("sourceMap"));
7178    }
7179
7180    #[test]
7181    fn workers_dir_name_replaces_dots_with_dashes() {
7182        assert_eq!(
7183            crate::project::worker_dir_name("commerce.payment"),
7184            "commerce-payment"
7185        );
7186        assert_eq!(crate::project::worker_dir_name("a.b.c"), "a-b-c");
7187    }
7188
7189    // Refactor track: characterisation pin for the canonical `escape_ts_string`.
7190    // It escapes backslash/quote/newline/tab and carriage return (`\r` → `\r`).
7191    #[test]
7192    fn escape_ts_string_escapes_cr() {
7193        assert_eq!(escape_ts_string("a\\b"), "a\\\\b");
7194        assert_eq!(escape_ts_string("a\"b"), "a\\\"b");
7195        assert_eq!(escape_ts_string("a\nb"), "a\\nb");
7196        assert_eq!(escape_ts_string("a\tb"), "a\\tb");
7197        assert_eq!(escape_ts_string("a\rb"), "a\\rb"); // CR escaped here; raw in project copy
7198    }
7199
7200    #[test]
7201    fn runtime_import_depth_resolves_correctly() {
7202        assert_eq!(
7203            runtime_import_for(Path::new("compose.ts"), ImportExt::Js),
7204            "./runtime.js"
7205        );
7206        assert_eq!(
7207            runtime_import_for(Path::new("commerce/payment.ts"), ImportExt::Js),
7208            "../runtime.js"
7209        );
7210        assert_eq!(
7211            runtime_import_for(Path::new("commerce/orders/types.ts"), ImportExt::Js),
7212            "../../runtime.js"
7213        );
7214        assert_eq!(
7215            runtime_import_for(Path::new("tests/commerce_payment.test.ts"), ImportExt::Js),
7216            "../runtime.js"
7217        );
7218    }
7219}
7220
7221/// Which conditional runtime helpers a module's import line ends up carrying.
7222///
7223/// These drive the single-file `emit()` path end-to-end (parse → resolve → check
7224/// → emit), so they exercise the real producers rather than the accumulator in
7225/// isolation. Before `RuntimeUse`, the decision was `body.contains("__bynkBytes")`
7226/// — a scan of the generated text — and `escapes_a_marker_in_a_string_literal`
7227/// below is the case that got wrong.
7228#[cfg(test)]
7229mod conditional_runtime_import_tests {
7230    use crate::testkit::{emit_bundle, emit_source};
7231
7232    /// The runtime import lines of the emitted module, joined: one `import …
7233    /// from "./__bynk_runtime/<module>.js"` per module it uses.
7234    fn runtime_import_line(ts: &str) -> String {
7235        ts.lines()
7236            .filter(|l| l.starts_with("import ") && l.contains("__bynk_runtime/"))
7237            .collect::<Vec<_>>()
7238            .join("\n")
7239    }
7240
7241    #[test]
7242    fn bytes_helpers_are_imported_when_a_bytes_value_is_built() {
7243        let ts = emit_source(
7244            "commons b\n\nfn decode(s: String) -> Option[Bytes] {\n  Bytes.fromBase64(s)\n}\n",
7245        );
7246        assert!(
7247            runtime_import_line(&ts).contains("__bynkBytesFromBase64"),
7248            "{ts}"
7249        );
7250    }
7251
7252    #[test]
7253    fn bytes_helpers_are_imported_for_content_equality() {
7254        let ts = emit_source("commons b\n\nfn same(a: Bytes, b: Bytes) -> Bool {\n  a == b\n}\n");
7255        assert!(
7256            runtime_import_line(&ts).contains("__bynkBytesEqual"),
7257            "{ts}"
7258        );
7259    }
7260
7261    /// #2016: the operation helpers are their own group. A module that only
7262    /// builds or compares `Bytes` keeps its import list, and one that calls an
7263    /// operation gets that group without the base one.
7264    #[test]
7265    fn bytes_ops_helpers_are_imported_only_when_an_operation_is_called() {
7266        let base = emit_source("commons b\n\nfn same(a: Bytes, b: Bytes) -> Bool {\n  a == b\n}\n");
7267        assert!(
7268            !runtime_import_line(&base).contains("__bynkBytesConcat"),
7269            "{base}"
7270        );
7271        let ops = emit_source("commons b\n\nfn hex(b: Bytes) -> String {\n  b.toHex()\n}\n");
7272        let line = runtime_import_line(&ops);
7273        assert!(line.contains("__bynkBytesToHex"), "{ops}");
7274        assert!(!line.contains("__bynkBytesEqual"), "{ops}");
7275    }
7276
7277    /// #2016: `slice` lowers inline, so it pulls in no helper at all.
7278    #[test]
7279    fn bytes_slice_needs_no_helper() {
7280        let ts = emit_source("commons b\n\nfn tag(b: Bytes) -> Bytes {\n  b.slice(0, 4)\n}\n");
7281        assert!(!runtime_import_line(&ts).contains("__bynkBytes"), "{ts}");
7282    }
7283
7284    #[test]
7285    fn bytes_helpers_are_absent_from_a_module_that_never_uses_bytes() {
7286        let ts = emit_source("commons b\n\nfn double(n: Int) -> Int {\n  n * 2\n}\n");
7287        assert!(!runtime_import_line(&ts).contains("__bynkBytes"), "{ts}");
7288    }
7289
7290    /// The regression this replaced a text scan for: a `Bytes` helper name
7291    /// appearing inside a user **string literal** is not a reference to the
7292    /// helper, and must not pull the import in. `body.contains("__bynkBytes")`
7293    /// could not tell the two apart, because the literal is emitted verbatim
7294    /// into the same buffer it scanned.
7295    #[test]
7296    fn escapes_a_marker_in_a_string_literal() {
7297        let ts = emit_source("commons b\n\nfn label() -> String {\n  \"__bynkBytesEqual\"\n}\n");
7298        assert!(
7299            ts.contains("\"__bynkBytesEqual\""),
7300            "the literal should survive into the body: {ts}"
7301        );
7302        assert!(
7303            !runtime_import_line(&ts).contains("__bynkBytes"),
7304            "a marker inside a string literal is not a helper reference: {ts}"
7305        );
7306    }
7307
7308    // -- the ICU formatters ---------------------------------------------------
7309
7310    const ICU_HELPERS: [&str; 3] = ["__selectPluralArm", "__formatIcuNumber", "__formatIcuDate"];
7311
7312    /// The case the per-arm recording exists for. A `select` placeholder lowers to
7313    /// `Object.hasOwn` over an arm table and calls no formatter, so a bundle whose
7314    /// only ICU construct is a `select` must import none of the three — recording
7315    /// once per placeholder instead of per arm would import all three here.
7316    #[test]
7317    fn a_select_only_bundle_imports_no_icu_formatter() {
7318        let ts = emit_bundle(
7319            "messages \"en\" @reference {\n  \"greeting\" => \"{g, select, male {He} female {She} other {They}} liked this.\"\n}\n",
7320        );
7321        assert!(
7322            ts.contains("globalThis.Object.hasOwn"),
7323            "the select arm table should have been emitted, else this proves nothing: {ts}"
7324        );
7325        for helper in ICU_HELPERS {
7326            assert!(
7327                !runtime_import_line(&ts).contains(helper),
7328                "a select-only bundle calls no formatter, so `{helper}` must not be imported: {ts}"
7329            );
7330        }
7331    }
7332
7333    /// The opposite direction: a `plural` placeholder does call a formatter, and
7334    /// the three are imported as a group.
7335    #[test]
7336    fn a_plural_bundle_imports_the_icu_formatters() {
7337        let ts = emit_bundle(
7338            "messages \"en\" @reference {\n  \"cart\" => \"You have {n, plural, one {# item} other {# items}} in your cart\"\n}\n",
7339        );
7340        assert!(
7341            ts.contains("__selectPluralArm("),
7342            "the plural dispatch should have been emitted: {ts}"
7343        );
7344        for helper in ICU_HELPERS {
7345            assert!(
7346                runtime_import_line(&ts).contains(helper),
7347                "`{helper}` should be imported for a plural bundle: {ts}"
7348            );
7349        }
7350    }
7351
7352    /// A bundle with no ICU dispatch at all — a plain `{name}` placeholder goes
7353    /// through `renderArg`, not a formatter.
7354    #[test]
7355    fn a_plain_placeholder_bundle_imports_no_icu_formatter() {
7356        let ts =
7357            emit_bundle("messages \"en\" @reference {\n  \"hello\" => \"Hello, {name}!\"\n}\n");
7358        for helper in ICU_HELPERS {
7359            assert!(
7360                !runtime_import_line(&ts).contains(helper),
7361                "`{helper}` must not be imported for a bundle with no ICU dispatch: {ts}"
7362            );
7363        }
7364    }
7365}
7366
7367/// P6.32 (design/tracks/the-ir.md §6a): pins the marker-parameterised
7368/// `type_ref_mentions` against the exact truth table its three former
7369/// hand-rolled copies (`file_mentions_json_error`/`_http_result`/
7370/// `_connection`) each implemented independently — in particular, that a
7371/// marker's own wrapper variant stops the recursion (matching the original
7372/// `=> true` arms) rather than also searching that variant's own inner type,
7373/// and that a non-matching wrapper variant recurses rather than reporting
7374/// `false` outright.
7375#[cfg(test)]
7376mod type_ref_mentions_tests {
7377    use super::*;
7378
7379    fn base(b: BaseType) -> TypeRef {
7380        TypeRef::Base(b, bynk_syntax::span::Span::new(0, 0))
7381    }
7382
7383    fn http_result(inner: TypeRef) -> TypeRef {
7384        TypeRef::HttpResult(Box::new(inner), bynk_syntax::span::Span::new(0, 0))
7385    }
7386
7387    fn connection(inner: TypeRef) -> TypeRef {
7388        TypeRef::Connection(Box::new(inner), bynk_syntax::span::Span::new(0, 0))
7389    }
7390
7391    fn json_error() -> TypeRef {
7392        TypeRef::JsonError(bynk_syntax::span::Span::new(0, 0))
7393    }
7394
7395    #[test]
7396    fn a_markers_own_wrapper_matches_regardless_of_its_inner_type() {
7397        // `HttpResult[String]` matches the `HttpResult` marker even though its
7398        // own inner type (`String`) does not itself mention `HttpResult` —
7399        // the wrapper variant itself is the match, mirroring each original
7400        // function's own unconditional `=> true` arm.
7401        let t = http_result(base(BaseType::String));
7402        assert!(type_ref_mentions(&t, TypeRefMarker::HttpResult));
7403        assert!(!type_ref_mentions(&t, TypeRefMarker::Connection));
7404        assert!(!type_ref_mentions(&t, TypeRefMarker::JsonError));
7405    }
7406
7407    #[test]
7408    fn a_non_matching_wrapper_still_recurses_into_its_inner_type() {
7409        // `HttpResult[Connection[String]]` does not match the `HttpResult`
7410        // marker itself when the marker is `Connection` — it must still find
7411        // the `Connection` nested one level in.
7412        let t = http_result(connection(base(BaseType::String)));
7413        assert!(type_ref_mentions(&t, TypeRefMarker::Connection));
7414        assert!(type_ref_mentions(&t, TypeRefMarker::HttpResult));
7415        assert!(!type_ref_mentions(&t, TypeRefMarker::JsonError));
7416    }
7417
7418    #[test]
7419    fn json_error_has_no_inner_type_to_recurse_into() {
7420        let t = json_error();
7421        assert!(type_ref_mentions(&t, TypeRefMarker::JsonError));
7422        assert!(!type_ref_mentions(&t, TypeRefMarker::HttpResult));
7423        assert!(!type_ref_mentions(&t, TypeRefMarker::Connection));
7424    }
7425
7426    #[test]
7427    fn a_plain_base_type_matches_no_marker() {
7428        let t = base(BaseType::Int);
7429        assert!(!type_ref_mentions(&t, TypeRefMarker::JsonError));
7430        assert!(!type_ref_mentions(&t, TypeRefMarker::HttpResult));
7431        assert!(!type_ref_mentions(&t, TypeRefMarker::Connection));
7432    }
7433}
7434
7435/// #1832: the import collector reaches a match-arm guard, so a function named
7436/// only in a guard is imported.
7437#[cfg(test)]
7438mod guard_import_tests {
7439    use crate::testkit::emit_project;
7440
7441    #[test]
7442    fn a_function_named_only_in_a_guard_is_imported() {
7443        let out = emit_project(
7444            &[
7445                ("d/lim.bynk", "commons d.lim\n\nfn lim() -> Int { 9 }\n"),
7446                (
7447                    "d/app.bynk",
7448                    "commons d.app\n\nuses d.lim\n\nfn g(n: Int) -> Int {\n  match n {\n    k if k > lim() => 1\n    _ => 0\n  }\n}\n",
7449                ),
7450            ],
7451            crate::project::BuildTarget::Bundle,
7452        );
7453        let app = out.text("d/app.ts");
7454        assert!(
7455            app.lines()
7456                .any(|l| l.starts_with("import") && l.contains("lim") && l.contains("./lim.js")),
7457            "{app}"
7458        );
7459    }
7460}