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