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
24use self::source_map::SourceMapBuilder;
25
26use crate::ir::lower::{
27    lower_capability_item_ir, lower_protocol_ir, lower_service_handler_signature_ir,
28    lower_store_field_shape_ir, lower_type_item_ir,
29};
30use crate::ir::{IrItem, TypeShape};
31use crate::project::{BuildTarget, EmitProjectCtx, ImportExt, UnitKind};
32use bynk_check::builtin_names::map_query;
33use bynk_check::builtin_names::methods::{
34    FOLD_EFF, FOR_EACH, PAR_TRAVERSE, PAR_TRAVERSE_ALL, PAR_TRAVERSE_TRY, RAW, TRAVERSE_ALL,
35    TRAVERSE_TRY,
36};
37use bynk_check::builtin_names::types::*;
38use bynk_check::checker::{CheckedProgram, NamedKind, Ty, TyId, TypedCommons, Types};
39use bynk_syntax::ast::*;
40
41pub mod contracts;
42pub(crate) mod events_fanout;
43pub mod secrets;
44pub(crate) mod serialisation;
45pub(crate) mod workers;
46pub(crate) mod workers_entry;
47pub mod wrangler;
48
49pub(crate) use events_fanout::emit_events_fanout_do;
50pub(crate) use secrets::emit_secrets_manifest;
51pub(crate) use workers::emit_worker_compose;
52pub(crate) use workers_entry::emit_worker_entry;
53pub(crate) use wrangler::emit_wrangler_toml;
54
55mod lower;
56pub(crate) mod runtime_use;
57pub(crate) use runtime_use::RuntimeUse;
58pub(crate) mod source_map;
59pub(crate) use lower::*;
60pub(crate) mod emit;
61pub(crate) use bynk_check::icu::{self, *};
62pub(crate) use bynk_check::websocket;
63pub(crate) use emit::*;
64
65const INDENT_STEP: usize = 2;
66
67/// Emit the contents of `out/runtime.ts`. This module ships with every
68/// project so the per-context / per-test emissions can `import { Ok, Err,
69/// Some, None, ... }` from a single source. It includes:
70///
71/// - `Result`/`Option` discriminated unions (using `tag` for the
72///   discriminant — same shape user sum types lower to).
73/// - `ValidationError` (the record shape refined-value constructors return).
74/// - The `DurableObjectState`/`DurableObjectStorage` interfaces that agent
75///   classes consume, plus an `InMemoryStorage` implementation and a
76///   `makeTestState(name)` factory for use in test execution.
77///
78/// The content is identical across projects — there is no per-project
79/// tailoring. Dead code is harmless; tsc handles it.
80pub fn emit_runtime_module() -> String {
81    RUNTIME_TS.to_string()
82}
83
84/// The embedded runtime. This is a BUILD OUTPUT, not a hand-edited file: it is
85/// bundled from the focused TypeScript modules in `bynk-emit/runtime/src` by
86/// that package's `scripts/bundle.mjs`. Edit the modules there and run
87/// `npm run bundle` (CI's `runtime` job guards against drift); never edit this
88/// file by hand. Keeping it a committed artifact means `cargo build` stays
89/// Node-free and the emitter stays lockstep with the runtime it embeds.
90const RUNTIME_TS: &str = include_str!("emitter/runtime.ts");
91
92/// Events track, slice 2 (spine #936): the TS type of one buffered/fanned-out
93/// event — a handler's `__events` local, the `__eventsDispatch` deps field
94/// (service, agent, provider, and the Bundle/Workers dispatch closures that
95/// implement it), and the fan-out DO's `FanoutEvent` (`events_fanout.rs`) all
96/// declare this same shape independently, with no shared type today. Routing
97/// every Rust-side site through one constant means the envelope field can
98/// never drift by being added at 8 of 9 of them. The two TypeScript runtime
99/// sources that also declare it (`runtime/src/agent.ts`'s
100/// `dispatchToEventsFanout`, `runtime/src/boundary.ts`'s `deliverEvent`) are
101/// hand-edited files this constant cannot reach — keep them textually
102/// identical to this shape by hand; `cargo test -p bynkc --test
103/// events_workers_wiring` and `events_envelope_behaviour` both exercise every
104/// hop and would fail on a real mismatch.
105///
106/// #973: `runtime/src/boundary.ts`'s `deserialiseEventEnvelope` is a related
107/// but distinct hand-written piece — it validates the *inner* `envelope`
108/// object's shape at the receiving `/_bynk/event/` route, not this outer
109/// wire wrapper. Keep its field list in sync with the `envelope: { ... }`
110/// portion of this shape by hand; nothing generates either from the other.
111pub(crate) const EVENTS_WIRE_EVENT_TS_TYPE: &str = "{ type: string; payload: unknown; envelope: { eventId: string; publisherId: string; emittedAt: number; schemaVersion: number } }";
112
113/// Emit the contents of `out/tsconfig.json`. The CLI uses `tsc -p` against
114/// this when running `bynkc test`; users can also drive `tsc` against it
115/// directly to produce JS for deployment.
116pub fn emit_tsconfig() -> String {
117    TSCONFIG_JSON.to_string()
118}
119
120/// The `bynkc test --coverage` variant (#854): the same config with `sourceMap`
121/// enabled, so `tsc` emits the `.js.map`s the coverage remap consumes (hop 1,
122/// `.js` → emitted `.ts`). Kept coverage-only rather than folded into the
123/// default so a normal `bynkc test` / deployment `tsc` run ships no `.js.map`s.
124/// The runner overwrites the default `out/tsconfig.json` with this before `tsc`.
125pub fn emit_tsconfig_with_source_maps() -> String {
126    TSCONFIG_JSON.replace(
127        "\"outDir\": \"../out-js\",",
128        "\"sourceMap\": true,\n    \"outDir\": \"../out-js\",",
129    )
130}
131
132const TSCONFIG_JSON: &str = r#"{
133  "compilerOptions": {
134    "target": "ES2022",
135    "module": "NodeNext",
136    "moduleResolution": "NodeNext",
137    "strict": true,
138    "noImplicitAny": true,
139    "esModuleInterop": true,
140    "skipLibCheck": true,
141    "resolveJsonModule": true,
142    "isolatedModules": true,
143    "noEmit": false,
144    "outDir": "../out-js",
145    "rootDir": "."
146  },
147  "include": ["**/*.ts"]
148}
149"#;
150
151/// Compute the runtime import specifier for a module at `from_source`. For a
152/// file at `commerce/payment.ts` the runtime sits two levels up, so this
153/// returns `../runtime.js`; for a top-level file it returns `./runtime.js`.
154pub(crate) fn runtime_import_for(from_source: &Path, ext: ImportExt) -> String {
155    let depth = from_source
156        .parent()
157        .map(|p| {
158            p.components()
159                .filter(|c| matches!(c, std::path::Component::Normal(_)))
160                .count()
161        })
162        .unwrap_or(0);
163    let ext = ext.as_str();
164    if depth == 0 {
165        format!("./runtime.{ext}")
166    } else {
167        let prefix: String = "../".repeat(depth);
168        format!("{prefix}runtime.{ext}")
169    }
170}
171
172/// Emit TypeScript source for the typed commons (single-file mode).
173///
174/// Takes a [`CheckedProgram`] rather than a bare `TypedCommons` (T3.7, R3.10):
175/// the only way to obtain one is [`bynk_check::checker::certify`], so this
176/// function can no longer be called with an unchecked or partially-checked
177/// program by construction.
178pub(crate) fn emit(program: &CheckedProgram) -> String {
179    let commons = program.program();
180    // Emit the body first so the header can decide which runtime helpers to
181    // import from what the body actually referenced (v0.110: the `__bynkBytes*`
182    // helpers are imported only when a `Bytes` value is constructed/compared).
183    // "What it referenced" comes from `dummy_ctx.runtime_use`, which the `Bytes`
184    // lowerings write as they emit — not from scanning `body` for the helper's
185    // name, which a user string literal or doc comment could also contain.
186    let mut body = String::new();
187    write_commons_doc(&mut body, commons);
188    let dummy_ctx = single_file_ctx();
189    // Types come first (they define interfaces and namespaces).
190    for item in &commons.commons.items {
191        if let CommonsItem::Type(t) = item {
192            let shape = type_shape_for(t, program);
193            emit_type(&mut body, t, &shape, commons, &dummy_ctx);
194        }
195    }
196    // Free functions afterward.
197    for item in &commons.commons.items {
198        if let CommonsItem::Fn(f) = item
199            && let FnName::Free(_) = &f.name
200        {
201            emit_free_fn(&mut body, f, commons, None, false, &dummy_ctx.runtime_use);
202        }
203    }
204    // v0.22b: module-local codec helpers for Json.encode/decode targets.
205    emit_json_codec_helpers(
206        &mut body,
207        commons,
208        &dummy_ctx,
209        &HashSet::new(),
210        &HashSet::new(),
211    );
212    let mut out = String::new();
213    // v0.153 (ADR 0177): a commons that names `HttpResult` in any signature —
214    // e.g. a free `fn -> HttpResult[T]` using the `?`-Option lift — imports it.
215    // Structural (over the AST), not a body-string scan, so a comment or string
216    // literal mentioning `HttpResult` never triggers a spurious import.
217    let uses_http = file_mentions_http_result(commons);
218    write_header_single(&mut out, commons, dummy_ctx.runtime_use.bytes(), uses_http);
219    out.push_str(&body);
220    out
221}
222
223/// `t`'s already-lowered `TypeShape` (`bynk-emit::ir`, P6.6/#1188) — reuses the
224/// canonical `Arc<TypeDecl>` `TypedCommons::types` already holds for `t` rather
225/// than a fresh `Arc::new(t.clone())` per call (Decision B, #1188). `types`
226/// holds an entry for every `CommonsItem::Type` *and* every `CommonsItem::Event`
227/// (`resolver.rs`'s own resolve pass inserts both under the same table, the
228/// event's own synthetic `TypeDecl` — `EventDecl::as_type_decl` — keyed
229/// identically), so this one helper serves both emission loops below with no
230/// special-casing for the event mirror.
231///
232/// Derives `commons` from `program` itself rather than taking it as a
233/// separate parameter (review on #1190): the `TyId`s this returns are minted
234/// from `program.program().ty_intern`, and every caller renders them straight
235/// back through that same `commons.ty_intern` (`emit_record_type`/
236/// `emit_sum_type`'s `ts_ty` calls) — a caller free to pass a `TypedCommons`
237/// from a *different* check run would hit `Types::get`'s cross-table panic
238/// instead of a diagnostic. One parameter makes that invariant
239/// unrepresentable instead of merely true today.
240fn type_shape_for(t: &TypeDecl, program: &CheckedProgram) -> TypeShape {
241    let commons = program.program();
242    let def = commons.types.get(&t.name.name).unwrap_or_else(|| {
243        panic!(
244            "bynk internal error (ADR 0334): type `{}` is not in TypedCommons::types, but the \
245             checker already accepted this declaration",
246            t.name.name
247        )
248    });
249    let IrItem::Type { shape, .. } = lower_type_item_ir(def, program) else {
250        unreachable!("lower_type_item_ir always returns IrItem::Type")
251    };
252    shape
253}
254
255/// A no-op project context for single-file emission. Single-file mode never
256/// involves contexts or cross-unit imports, so most fields default to empty.
257fn single_file_ctx() -> EmitProjectCtx {
258    EmitProjectCtx {
259        import_ext: crate::project::ImportExt::Js,
260        contracts: false,
261        source_path: PathBuf::new(),
262        commons_name: String::new(),
263        file_decl_index: crate::project::FileDeclIndex {
264            types: HashMap::new(),
265            fns: HashMap::new(),
266            methods: HashMap::new(),
267        },
268        imported_from: HashMap::new(),
269        imported_from_kind: HashMap::new(),
270        imported_decl_paths: HashMap::new(),
271        unit_kind: UnitKind::Commons,
272        owning_context: None,
273        exports_for_consumed: HashMap::new(),
274        cross_context: bynk_check::resolver::CrossContextInfo::default(),
275        target: BuildTarget::Bundle,
276        local_agents: HashSet::new(),
277        agent_given_deps: HashMap::new(),
278        extra_import_lines: Vec::new(),
279        agent_method_givens: HashMap::new(),
280        actors: HashMap::new(),
281        event_schema_versions: HashMap::new(),
282        consumed_adapters: HashSet::new(),
283        history_target_agents: HashSet::new(),
284        imported_methods: HashMap::new(),
285        runtime_use: Default::default(),
286    }
287}
288
289/// Emit TypeScript source for a single file inside a multi-file project,
290/// including cross-file and cross-commons imports computed from
291/// [`EmitProjectCtx`].
292/// Emit one unit's TypeScript, plus its source map (slice 1, ADR 0103).
293///
294/// `source_text` is the originating `.bynk` file's text and `source_name` its
295/// project-root-relative path; together they let the source-map builder resolve
296/// each recorded span to a `(line, col)` and embed `sourcesContent`. Returns the
297/// generated TS and the serialised source-map v3 JSON (`None` when nothing
298/// mapped — e.g. a unit whose items all came from sibling files).
299pub(crate) fn emit_project(
300    program: &CheckedProgram,
301    ctx: &EmitProjectCtx,
302    source_text: &str,
303    source_name: &str,
304) -> (String, Option<String>) {
305    let commons = program.program();
306    let mut out = String::new();
307    // The file's source-map builder. The free-function bodies record statement /
308    // match-arm checkpoints through their `LowerCtx`; the declaration loops below
309    // record one checkpoint per top-level item so signatures (and the bodies of
310    // services/agents, which lower via spliced local buffers) anchor to their
311    // declaration (ADR 0103 D2, nearest-enclosing).
312    let smb = RefCell::new(SourceMapBuilder::new());
313    // The file's `.bynk` source is the primary map source (id 0); `record` targets
314    // it and spliced handler bodies in the same file merge against it (v0.70).
315    smb.borrow_mut().add_source(source_name, source_text);
316    write_header(&mut out, commons, ctx);
317    // Compute which names this file actually references that live elsewhere
318    // (sibling file in the same commons/context, or a used commons / consumed
319    // context).
320    let references = collect_external_references(commons, ctx);
321    emit_project_imports(&mut out, commons, ctx, &references);
322    if !references.is_empty() {
323        writeln!(out).unwrap();
324    }
325    // v0.6: namespace imports for each consumed context that exposes services.
326    // v0.15: also for consumed contexts whose capabilities this context uses.
327    emit_cross_context_namespace_imports(&mut out, commons, ctx);
328    // For contexts: emit per-context nominal rebrand aliases for each type
329    // imported via `uses` that this file references. The structural shape is
330    // inherited from the original commons type; the brand makes the
331    // rebranded type nominally distinct (v0.4 §6.2).
332    if ctx.unit_kind == UnitKind::Context {
333        emit_context_rebrands(&mut out, &references, commons, ctx);
334    }
335    write_commons_doc(&mut out, commons);
336    for item in &commons.commons.items {
337        if let CommonsItem::Type(t) = item {
338            smb.borrow_mut().record(out.len(), t.span);
339            let shape = type_shape_for(t, program);
340            emit_type(&mut out, t, &shape, commons, ctx);
341        }
342    }
343    // Events track, slice 0 (spine #936): an `event` is checker-visible as
344    // a type (via `EventDecl::as_type_decl`, so exports/consumes/
345    // construction all worked from day one), but nothing emitted its actual
346    // TS declaration — this loop only ever matched `CommonsItem::Type`, so
347    // a subscriber importing an event type across contexts (`from
348    // Events(E)`, or `E` named in a cross-context signature) got a real
349    // `tsc` "has no exported member" error. Reuses the identical synthetic
350    // `TypeDecl` the checker already builds.
351    for item in &commons.commons.items {
352        if let CommonsItem::Event(e) = item {
353            let t = e.as_type_decl();
354            smb.borrow_mut().record(out.len(), t.span);
355            let shape = type_shape_for(&t, program);
356            emit_type(&mut out, &t, &shape, commons, ctx);
357        }
358    }
359    for item in &commons.commons.items {
360        if let CommonsItem::Fn(f) = item
361            && let FnName::Free(_) = &f.name
362        {
363            smb.borrow_mut().record(out.len(), f.span);
364            emit_free_fn(
365                &mut out,
366                f,
367                commons,
368                Some(&smb),
369                ctx.contracts,
370                &ctx.runtime_use,
371            );
372        }
373    }
374    // message-bundles slice 2 (#874): every `messages` block in the commons
375    // is emitted together, once, as a single multi-locale bundle — not
376    // per-item like the other behavioural kinds below — so the generated
377    // `render` can dispatch across every declared locale's own table rather
378    // than reading only the `@reference` one (slice 1's scope). Recorded at
379    // the `@reference` block's own span, matching how a single-item emission
380    // records at that item's span elsewhere in this loop.
381    let messages_blocks: Vec<&MessagesDecl> = commons
382        .commons
383        .items
384        .iter()
385        .filter_map(|item| match item {
386            CommonsItem::Messages(m) => Some(m),
387            _ => None,
388        })
389        .collect();
390    if let Some(reference) = messages_blocks
391        .iter()
392        .find(|m| m.annotations.iter().any(|a| a.name.name == "reference"))
393    {
394        smb.borrow_mut().record(out.len(), reference.span);
395        emit_messages_bundle(&mut out, &messages_blocks, reference, &ctx.runtime_use);
396    }
397    // v0.5: behavioural items follow the type/fn declarations.
398    for item in &commons.commons.items {
399        match item {
400            CommonsItem::Capability(c) => {
401                smb.borrow_mut().record(out.len(), c.span);
402                // P6.x (#1193, slice 3 of #1187): `emit_capability` reads
403                // each op's resolved types off `ops`, not `c`'s own raw
404                // `TypeRef`s (Decision B, #1193) — no separate helper, this
405                // is `emit_capability`'s one and only call site.
406                let IrItem::Capability { ops, .. } = lower_capability_item_ir(c, program) else {
407                    unreachable!("lower_capability_item_ir always returns IrItem::Capability")
408                };
409                emit_capability(&mut out, c, &ops, commons);
410            }
411            CommonsItem::Provider(p) => {
412                smb.borrow_mut().record(out.len(), p.span);
413                emit_provider(&mut out, p, commons, ctx, Some(&smb));
414            }
415            CommonsItem::Service(s) => {
416                smb.borrow_mut().record(out.len(), s.span);
417                // #1187's slice 5: `emit_service` reads the protocol's own
418                // resolved data (`ProtocolIr`) and each handler's resolved
419                // signature (params/ret/effectful) instead of `s`'s own raw
420                // `ServiceProtocol`/`TypeRef`s — not a full `IrItem::Service`
421                // (see `lower_service_handler_signature_ir`'s own doc
422                // comment for why: a real `IrHandler` would unconditionally
423                // lower every handler's body, panicking on an ordinary
424                // `Ok`/`Err`-returning Http handler). No separate helper,
425                // this is `emit_service`'s one and only call site.
426                let protocol = lower_protocol_ir(&s.protocol, program);
427                let signatures: Vec<_> = s
428                    .handlers
429                    .iter()
430                    .map(|h| lower_service_handler_signature_ir(h, program))
431                    .collect();
432                emit_service(
433                    &mut out,
434                    s,
435                    &protocol,
436                    &signatures,
437                    commons,
438                    ctx,
439                    Some(&smb),
440                );
441            }
442            CommonsItem::Agent(a) => {
443                smb.borrow_mut().record(out.len(), a.span);
444                let state: Vec<_> = a
445                    .store_fields
446                    .iter()
447                    .map(|f| lower_store_field_shape_ir(f, program))
448                    .collect();
449                emit_agent(&mut out, a, &state, commons, ctx, Some(&smb));
450            }
451            _ => {}
452        }
453    }
454    // v0.9.2: per-test registry reset. The test runner calls this before each
455    // test so a fresh test sees clean agent state (finding #10's "fresh per
456    // test" half).
457    let agent_names: Vec<&str> = commons
458        .commons
459        .items
460        .iter()
461        .filter_map(|i| match i {
462            CommonsItem::Agent(a) => Some(a.name.name.as_str()),
463            _ => None,
464        })
465        .collect();
466    if !agent_names.is_empty() {
467        writeln!(out, "export function __resetAgents(): void {{").unwrap();
468        for name in &agent_names {
469            writeln!(out, "  {}.reset();", agent_registry_name(name)).unwrap();
470        }
471        writeln!(out, "}}").unwrap();
472        writeln!(out).unwrap();
473    }
474    // v0.6: cross-context surface assembly. Emit `makeSurface` for any
475    // context that declares services — the composition root references it
476    // for every such context, not just those consumed by others. Skipped
477    // in workers mode where each Worker has its own `compose(env)` root.
478    if ctx.unit_kind == UnitKind::Context && matches!(ctx.target, BuildTarget::Bundle) {
479        let has_services = commons
480            .commons
481            .items
482            .iter()
483            .any(|i| matches!(i, CommonsItem::Service(_)));
484        if has_services {
485            emit_make_surface(&mut out, commons, ctx);
486        }
487    }
488    // v0.8: in workers mode, the context module also exports per-type
489    // serialise/deserialise helpers for every type that crosses a
490    // boundary. The commons modules likewise carry helpers for their
491    // own commons-declared boundary types.
492    // v0.96 (ADR 0124): runs on both targets — workers emits service-call +
493    // agent-rehydration boundary helpers; bundle emits only the agent-rehydration
494    // ones (the gate's deserialisers), since in-process calls need no wire codec.
495    let (boundary_names, boundary_insts) = emit_boundary_helpers(&mut out, commons, ctx);
496    // v0.22b: module-local codec helpers for this file's Json.encode/decode
497    // targets, deduped against the workers boundary helpers above.
498    emit_json_codec_helpers(&mut out, commons, ctx, &boundary_names, &boundary_insts);
499    // The generated `file` name: the source basename with `.bynk` → `.ts`.
500    let generated_file = Path::new(source_name)
501        .file_stem()
502        .map(|s| format!("{}.ts", s.to_string_lossy()))
503        .unwrap_or_else(|| "module.ts".to_string());
504    let source_map = smb.borrow().to_v3(&out, &generated_file);
505    // Both injections below key on `ctx.runtime_use`, which the producers wrote as
506    // they emitted. They used to key on `out.contains("<helper name>")`, which was
507    // wrong in both directions: `out` also carries user string literals and doc
508    // comments (a spurious import), and the ICU scan additionally depended on the
509    // call being emitted with no space before its paren — so an unrelated
510    // formatting change could silently drop a *required* import and produce a
511    // module that does not compile. See `emitter::runtime_use`.
512    // v0.110 (ADR 0142): import the `Bytes` runtime helpers iff the emitted body
513    // actually references them. Injected into the existing runtime import line
514    // (no new line, no body-column shift), so the source map computed above from
515    // the pre-injection text stays valid.
516    if ctx.runtime_use.bytes() {
517        out = inject_runtime_imports(
518            out,
519            &runtime_import_for(&ctx.source_path, ctx.import_ext),
520            BYTES_RUNTIME_IMPORTS,
521        );
522    }
523    // message-bundles slice 3 (#878, Decision G): same mechanism, for the
524    // three ICU-formatting runtime helpers. `emit_messages_bundle` (called
525    // above, before this post-pass) is the only place that can reference
526    // them; a project with no `plural`/`select`/`number`/`date` placeholder
527    // anywhere never triggers this. All three are imported together
528    // (mirrors `BYTES_RUNTIME_IMPORTS`'s own all-or-nothing shape) rather
529    // than cherry-picked per-name — the emitted `tsconfig.json` has no
530    // `noUnusedLocals`, so an unused named import is inert.
531    if ctx.runtime_use.icu() {
532        out = inject_runtime_imports(
533            out,
534            &runtime_import_for(&ctx.source_path, ctx.import_ext),
535            MESSAGES_RUNTIME_IMPORTS,
536        );
537    }
538    (out, source_map)
539}
540
541/// v0.110 (ADR 0142): append a set of runtime helpers to a module's existing
542/// runtime import. Done as a post-pass so the decision keys on what the body
543/// references, without a second emission or a source-map-shifting reorder.
544/// Generalised in message-bundles slice 3 (#878) from a `Bytes`-only helper
545/// to take `extra` as a parameter, shared with the ICU-formatting helpers.
546///
547/// v0.176 (#642): anchored on the runtime import's **exact specifier** rather
548/// than on the `type ValidationError` binding it happens to carry. With `Bytes`
549/// now able to cross a workers boundary (ADR 0142 D8's guard retired), the
550/// *Worker entry* references `__bynkBytesFromBase64` too — and its import line
551/// names no `ValidationError`, so the old anchor silently failed to inject and
552/// `tsc` reported an unresolved name.
553///
554/// The specifier is matched exactly (`from "<specifier>"`), not by substring: a
555/// `contains("runtime.js")` would also match a *user* module that happens to be
556/// named `runtime` — or anything like `"./my-runtime.js"` — and appending
557/// `extra`'s bindings to that import would produce an unresolved export. The
558/// caller already knows the exact path it emitted, so there is no reason to
559/// guess.
560pub(crate) fn inject_runtime_imports(out: String, runtime_specifier: &str, extra: &str) -> String {
561    let mut result = String::with_capacity(out.len() + extra.len());
562    let mut injected = false;
563    let from_runtime = format!(" }} from \"{runtime_specifier}\"");
564    for line in out.split_inclusive('\n') {
565        if !injected
566            && line.starts_with("import {")
567            && line.contains(&from_runtime)
568            && let Some(pos) = line.rfind(&from_runtime)
569        {
570            result.push_str(&line[..pos]);
571            result.push_str(&missing_bindings(&line[..pos], extra));
572            result.push_str(&line[pos..]);
573            injected = true;
574            continue;
575        }
576        result.push_str(line);
577    }
578    result
579}
580
581/// The subset of `extra` not already bound on `existing` — the head of an import
582/// line, e.g. `import { Ok, Err, type Result`.
583///
584/// #914: an injection target may already import some of what a group carries. The
585/// test-scaffold module lists `Ok`/`Err` in its fixed set but not `BoundaryError`,
586/// so injecting the boundary group wholesale would emit `import { Ok, …, Ok, … }` —
587/// a duplicate-identifier error, i.e. trading one uncompilable module for another.
588/// Comparing on the bare name lets `type BoundaryError` match an existing
589/// `BoundaryError` and vice versa.
590///
591/// Invariant: a group is a list of plain bindings, optionally `type`-prefixed —
592/// **never an alias**. `bare("Foo as Ok")` is the whole phrase, so an aliased
593/// binding on either side would compare unequal and inject a duplicate. No group
594/// carries one today; keep it that way rather than teaching this to split on
595/// `as`.
596fn missing_bindings(existing: &str, extra: &str) -> String {
597    fn bare(binding: &str) -> &str {
598        binding
599            .trim()
600            .strip_prefix("type ")
601            .unwrap_or(binding.trim())
602    }
603    let present: HashSet<&str> = existing
604        .strip_prefix("import {")
605        .unwrap_or(existing)
606        .split(',')
607        .map(bare)
608        .collect();
609    let wanted: Vec<&str> = extra
610        .split(',')
611        .map(str::trim)
612        .filter(|b| !b.is_empty() && !present.contains(bare(b)))
613        .collect();
614    if wanted.is_empty() {
615        String::new()
616    } else {
617        format!(", {}", wanted.join(", "))
618    }
619}
620
621/// v0.22b: pre-order expression visitor — visits `e`, then every
622/// sub-expression, including statements and tails of nested blocks. Driven by
623/// `ast::expr_children`, the exhaustive total child iterator, rather than a
624/// hand-matched recursion duplicating it — a new `ExprKind` variant fails to
625/// compile in `expr_children` until it is taught to visit it, instead of
626/// silently under-visiting here.
627pub(crate) fn walk_exprs(e: &Expr, f: &mut impl FnMut(&Expr)) {
628    f(e);
629    for child in expr_children(e) {
630        walk_exprs(child, f);
631    }
632}
633
634/// v0.79: does this block contain a `~>` send anywhere — including nested
635/// branches, match arms, and lambdas? Gates execution-context threading
636/// (`deps.__exec`) so a context that never sends keeps byte-identical output.
637///
638/// A `~>` send is a [`Statement`] variant, not an [`ExprKind`] one, and a bare
639/// `{ … }` block is only parseable in a handful of positions (an `if`/`else`
640/// body, a `match` arm, a lambda body) — never as an arbitrary sub-expression
641/// — so `Block`/`If`/`Match`/`Lambda` were already the complete reachable set
642/// and the old `_ => false` tail never actually dropped a send. It is
643/// rewritten to recurse over `expr_children`, the total child iterator,
644/// anyway: a `Statement`-only construct like this is exactly the shape that
645/// silently drifts if a later `ExprKind` variant *does* start admitting a
646/// nested block and this list isn't updated to match — see
647/// `block_writes_state`, whose traversal was converted alongside this one for
648/// the same reason. Both now also enumerate `ExprKind` explicitly instead of
649/// ending in a `_` arm, so that drift is a build failure rather than a silent
650/// miss.
651pub(crate) fn block_uses_send(b: &Block) -> bool {
652    fn stmt(s: &Statement) -> bool {
653        match s {
654            Statement::Send(_) => true,
655            Statement::Let(l) | Statement::EffectLet(l) => expr(&l.value),
656            Statement::Expect(a) => expr(&a.value),
657            Statement::Do(d) => expr(&d.value),
658            Statement::Assign(a) => expr(&a.value),
659        }
660    }
661    fn expr(e: &Expr) -> bool {
662        match &e.kind {
663            ExprKind::Block(b) => block_uses_send(b),
664            ExprKind::If {
665                cond,
666                then_block,
667                else_block,
668            } => expr(cond) || block_uses_send(then_block) || block_uses_send(else_block),
669            ExprKind::Match { discriminant, arms } => {
670                expr(discriminant)
671                    || arms.iter().any(|a| match &a.body {
672                        MatchBody::Expr(e) => expr(e),
673                        MatchBody::Block(b) => block_uses_send(b),
674                    })
675            }
676            // No variant below carries a `Block` *field*, so `expr_children`'s
677            // total descent is complete for it — a block reached through a
678            // child (a braced lambda body, say) comes back as an `Expr` and
679            // re-enters this match at the `Block` arm above. A *new* variant
680            // that holds a `Block` directly must be hand-matched up there
681            // alongside `Block`/`If`/`Match`: appending it here loses the
682            // `Statement::Send` tag (`expr_children` flattens a block to its
683            // statements' values), and with it `deps.__exec` threading for a
684            // context that does send.
685            ExprKind::IntLit { .. }
686            | ExprKind::FloatLit { .. }
687            | ExprKind::DurationLit { .. }
688            | ExprKind::StrLit(_)
689            | ExprKind::InterpStr(_)
690            | ExprKind::BoolLit(_)
691            | ExprKind::Ident(_)
692            | ExprKind::Call { .. }
693            | ExprKind::Lambda(_)
694            | ExprKind::BinOp(..)
695            | ExprKind::UnaryOp(..)
696            | ExprKind::Paren(_)
697            | ExprKind::Ok(_)
698            | ExprKind::Err(_)
699            | ExprKind::Question(_)
700            | ExprKind::ConstructorCall { .. }
701            | ExprKind::RecordConstruction { .. }
702            | ExprKind::FieldAccess { .. }
703            | ExprKind::MethodCall { .. }
704            | ExprKind::Is { .. }
705            | ExprKind::Some(_)
706            | ExprKind::None
707            | ExprKind::UnitLit
708            | ExprKind::RecordSpread { .. }
709            | ExprKind::EffectPure(_)
710            | ExprKind::Expect(_)
711            | ExprKind::Val { .. }
712            | ExprKind::Wire(_)
713            | ExprKind::ListLit(_)
714            | ExprKind::Observation(_)
715            | ExprKind::Trace { .. } => expr_children(e).into_iter().any(expr),
716        }
717    }
718    b.statements.iter().any(stmt) || expr(&b.tail)
719}
720
721/// Events track, slice 0 (spine #936): does this block contain a real
722/// `Events.emit[...]` call anywhere — including nested branches, match arms,
723/// lambdas, and any other expression position (a `Paren`, an `Ok`/`Err`
724/// wrapper, a `Call`/`RecordConstruction` argument, a `BinOp` operand, …)?
725/// Gates release-at-commit buffer threading (`deps.__events`) so a handler
726/// that never emits keeps byte-identical output, mirroring `block_uses_send`'s
727/// gate on `deps.__exec`.
728///
729/// Driven off the exhaustive `walk_block_exprs`/`walk_exprs` visitor rather
730/// than a hand-rolled `ExprKind` match — a bespoke match here previously
731/// covered only `MethodCall`/`Block`/`If`/`Match`/`Lambda` and silently
732/// disagreed with `lower_expr_into` (which recurses into every expression
733/// position), so `do (Events.emit[E](event))` — one added paren — compiled
734/// clean but emitted a body that referenced an undeclared `__events` local
735/// (`tsc`-only failure, no bynk diagnostic). Riding the walker means this
736/// can't drift from the lowering again: a new `ExprKind` variant fails to
737/// compile here until `walk_exprs` itself is taught to visit it.
738///
739/// #1187's slice 6 plumbing (review of #1202): reads the checker's own
740/// already-resolved `Callee::Capability{cap:"Events",op:"emit"}` for each
741/// visited call site instead of a bare-`Ident("Events")`-receiver name
742/// match. Was deliberately syntactic before this — this function's own
743/// prior doc comment named the locally-shadowed-`Events` false positive an
744/// "accepted approximation," matching `block_uses_send`'s own precedent —
745/// but that approximation stopped being harmless once `crate::project::
746/// unit_table_uses_emit` (the project-wide compose-gating twin this
747/// function's own callers must agree with) became precise first: the two
748/// disagreeing on exactly the shadowed case produces a real `tsc` type
749/// error (a `deps.__eventsDispatch` call site with nothing supplying it),
750/// not just an unused interface field. `block_uses_send` needs no matching
751/// fix — a `~>` send is a real `Statement::Send` AST variant, not a method
752/// call that could be shadowed, so it was never approximate to begin with.
753pub(crate) fn block_uses_emit(
754    b: &Block,
755    callees: &HashMap<ExprId, bynk_check::checker::Callee>,
756) -> bool {
757    let mut found = false;
758    walk_block_exprs(b, &mut |e| {
759        if !found
760            && matches!(
761                callees.get(&e.id),
762                Some(bynk_check::checker::Callee::Capability { cap, op })
763                    if cap == "Events" && op == "emit"
764            )
765        {
766            found = true;
767        }
768    });
769    found
770}
771
772/// Decision C (#1165): the closed sets of mutating storage-op names, one
773/// `pub(crate)` constant per kind group — read by `ir::lower`'s own
774/// `Callee::Store`-keyed write-detection walk (P6.8, Decision B;
775/// [`crate::ir::lower::body_writes_state`]), which needs no receiver-name
776/// gate at all: a `Callee::Store` already carries the field's own resolved
777/// identity, not a name that could be shadowed. Until #1196, this module
778/// also had its own bare-`Ident`-receiver-name-matching reader
779/// (`block_writes_state`'s own `mutating_op`, deleted) — a single shared
780/// source avoided the class of drift #1164's own review caught twice for a
781/// different pair of independently hand-maintained copies
782/// (`cache_ttl_millis`'s `DurationLit` extraction, `store_map_indexes`'s
783/// dedup); now there is only the one reader, but these stay `pub(crate)`
784/// here (not moved into `ir::lower`) since a future emitter-side reader
785/// (a `Service` handler's own write detection, say) may need them again.
786/// `Map`/`Cache` share one list — both support the same four entry ops —
787/// rather than two identical ones.
788pub(crate) const MUTATING_MAP_CACHE_OPS: &[&str] = &["put", "remove", "update", "upsert"];
789/// v0.83: `<set>.add`/`<set>.remove` mutate a `store Set[T]` field.
790pub(crate) const MUTATING_SET_OPS: &[&str] = &["add", "remove"];
791/// v0.95: `<log>.append` mutates the durable array (ADR 0121) — every other
792/// `Log` method is a query-lifting read.
793pub(crate) const MUTATING_LOG_OPS: &[&str] = &["append"];
794/// v0.98 (ADR 0125): `<cell>.update(f)` is a read-modify-write of the
795/// working state — the bare `:=` write form is `Statement::Assign`, checked
796/// separately and unconditionally, no method name involved.
797pub(crate) const MUTATING_CELL_OPS: &[&str] = &["update"];
798
799pub(crate) fn walk_block_exprs(b: &Block, f: &mut impl FnMut(&Expr)) {
800    let mut exprs = Vec::new();
801    for s in &b.statements {
802        statement_exprs(s, &mut exprs);
803    }
804    exprs.push(&b.tail);
805    for e in exprs {
806        walk_exprs(e, f);
807    }
808}
809
810/// v0.22b: whether any signature or type declaration in this file names
811/// `JsonError` — drives the conditional `type JsonError` runtime import.
812fn file_mentions_json_error(commons: &TypedCommons) -> bool {
813    fn in_type_ref(t: &TypeRef) -> bool {
814        match t {
815            TypeRef::JsonError(_) => true,
816            TypeRef::Result(a, b, _) | TypeRef::Map(a, b, _) => in_type_ref(a) || in_type_ref(b),
817            TypeRef::Option(a, _)
818            | TypeRef::Effect(a, _)
819            | TypeRef::HttpResult(a, _)
820            | TypeRef::Query(a, _)
821            | TypeRef::Stream(a, _)
822            | TypeRef::Connection(a, _)
823            | TypeRef::History(a, _)
824            | TypeRef::List(a, _) => in_type_ref(a),
825            TypeRef::Fn(params, ret, _) => params.iter().any(in_type_ref) || in_type_ref(ret),
826            // v0.157 (ADR 0183): recurse into a generic application's arguments.
827            TypeRef::App { args, .. } => args.iter().any(in_type_ref),
828            TypeRef::Base(..)
829            | TypeRef::Named(_)
830            | TypeRef::QueueResult(_)
831            | TypeRef::ValidationError(_)
832            | TypeRef::Unit(_) => false,
833        }
834    }
835    let sig = |params: &[Param], ret: &TypeRef| {
836        params.iter().any(|p| in_type_ref(&p.type_ref)) || in_type_ref(ret)
837    };
838    commons.commons.items.iter().any(|item| match item {
839        CommonsItem::Fn(f) => sig(&f.params, &f.return_type),
840        CommonsItem::Service(s) => s.handlers.iter().any(|h| sig(&h.params, &h.return_type)),
841        CommonsItem::Agent(a) => a.handlers.iter().any(|h| sig(&h.params, &h.return_type)),
842        CommonsItem::Capability(c) => c.ops.iter().any(|op| sig(&op.params, &op.return_type)),
843        CommonsItem::Provider(p) => p.ops.iter().any(|op| sig(&op.params, &op.return_type)),
844        CommonsItem::Type(t) => match &t.body {
845            TypeBody::Record(r) => r.fields.iter().any(|f| in_type_ref(&f.type_ref)),
846            TypeBody::Sum(s) => s
847                .variants
848                .iter()
849                .any(|v| v.payload.iter().any(|p| in_type_ref(&p.type_ref))),
850            TypeBody::Refined { .. } | TypeBody::Opaque { .. } => false,
851        },
852        // An `event` registers into the `types` table and is checked over
853        // the same record-field path as `CommonsItem::Type`'s `Record` arm.
854        CommonsItem::Event(e) => e.body.fields.iter().any(|f| in_type_ref(&f.type_ref)),
855        CommonsItem::Actor(_) | CommonsItem::Messages(_) => false,
856    })
857}
858
859/// v0.153 (ADR 0177): true if any signature or type declaration in this file
860/// names `HttpResult` — a service HTTP handler, or a free `fn` / provider /
861/// capability whose parameter or return type mentions it (the `?`-Option lift
862/// makes a bare `fn -> HttpResult[T]` emit `HttpResult.NotFound`). Drives the
863/// conditional `HttpResult` runtime import in both single-file and project
864/// headers, so the import can never be missing nor spuriously added.
865fn file_mentions_http_result(commons: &TypedCommons) -> bool {
866    fn in_type_ref(t: &TypeRef) -> bool {
867        match t {
868            TypeRef::HttpResult(..) => true,
869            TypeRef::Result(a, b, _) | TypeRef::Map(a, b, _) => in_type_ref(a) || in_type_ref(b),
870            TypeRef::Option(a, _)
871            | TypeRef::Effect(a, _)
872            | TypeRef::Query(a, _)
873            | TypeRef::Stream(a, _)
874            | TypeRef::Connection(a, _)
875            | TypeRef::History(a, _)
876            | TypeRef::List(a, _) => in_type_ref(a),
877            TypeRef::Fn(params, ret, _) => params.iter().any(in_type_ref) || in_type_ref(ret),
878            // v0.157 (ADR 0183): recurse into a generic application's arguments.
879            TypeRef::App { args, .. } => args.iter().any(in_type_ref),
880            TypeRef::Base(..)
881            | TypeRef::Named(_)
882            | TypeRef::QueueResult(_)
883            | TypeRef::ValidationError(_)
884            | TypeRef::JsonError(_)
885            | TypeRef::Unit(_) => false,
886        }
887    }
888    let sig = |params: &[Param], ret: &TypeRef| {
889        params.iter().any(|p| in_type_ref(&p.type_ref)) || in_type_ref(ret)
890    };
891    commons.commons.items.iter().any(|item| match item {
892        CommonsItem::Fn(f) => sig(&f.params, &f.return_type),
893        CommonsItem::Service(s) => s.handlers.iter().any(|h| sig(&h.params, &h.return_type)),
894        CommonsItem::Agent(a) => a.handlers.iter().any(|h| sig(&h.params, &h.return_type)),
895        CommonsItem::Capability(c) => c.ops.iter().any(|op| sig(&op.params, &op.return_type)),
896        CommonsItem::Provider(p) => p.ops.iter().any(|op| sig(&op.params, &op.return_type)),
897        CommonsItem::Type(t) => match &t.body {
898            TypeBody::Record(r) => r.fields.iter().any(|f| in_type_ref(&f.type_ref)),
899            TypeBody::Sum(s) => s
900                .variants
901                .iter()
902                .any(|v| v.payload.iter().any(|p| in_type_ref(&p.type_ref))),
903            TypeBody::Refined { .. } | TypeBody::Opaque { .. } => false,
904        },
905        // An `event` registers into the `types` table and is checked over
906        // the same record-field path as `CommonsItem::Type`'s `Record` arm.
907        CommonsItem::Event(e) => e.body.fields.iter().any(|f| in_type_ref(&f.type_ref)),
908        CommonsItem::Actor(_) | CommonsItem::Messages(_) => false,
909    })
910}
911
912/// v0.102: true if a file's signatures or store fields mention `Connection[F]`,
913/// so the header imports the runtime `Connection` interface. Covers the held
914/// sites: capability-operation returns, service/agent handler parameters, and
915/// `store` field value types (`Map[K, Connection]` / `Cell[Option[Connection]]`).
916fn file_mentions_connection(commons: &TypedCommons) -> bool {
917    fn in_type_ref(t: &TypeRef) -> bool {
918        match t {
919            TypeRef::Connection(..) => true,
920            TypeRef::Result(a, b, _) | TypeRef::Map(a, b, _) => in_type_ref(a) || in_type_ref(b),
921            TypeRef::Option(a, _)
922            | TypeRef::Effect(a, _)
923            | TypeRef::HttpResult(a, _)
924            | TypeRef::Query(a, _)
925            | TypeRef::Stream(a, _)
926            | TypeRef::History(a, _)
927            | TypeRef::List(a, _) => in_type_ref(a),
928            TypeRef::Fn(params, ret, _) => params.iter().any(in_type_ref) || in_type_ref(ret),
929            // v0.157 (ADR 0183): recurse into a generic application's arguments.
930            TypeRef::App { args, .. } => args.iter().any(in_type_ref),
931            TypeRef::Base(..)
932            | TypeRef::Named(_)
933            | TypeRef::QueueResult(_)
934            | TypeRef::ValidationError(_)
935            | TypeRef::JsonError(_)
936            | TypeRef::Unit(_) => false,
937        }
938    }
939    let sig = |params: &[Param], ret: &TypeRef| {
940        params.iter().any(|p| in_type_ref(&p.type_ref)) || in_type_ref(ret)
941    };
942    commons.commons.items.iter().any(|item| match item {
943        CommonsItem::Fn(f) => sig(&f.params, &f.return_type),
944        CommonsItem::Service(s) => s.handlers.iter().any(|h| sig(&h.params, &h.return_type)),
945        CommonsItem::Agent(a) => {
946            a.handlers.iter().any(|h| sig(&h.params, &h.return_type))
947                || a.store_fields
948                    .iter()
949                    .any(|f| f.kind.args.iter().any(in_type_ref))
950        }
951        CommonsItem::Capability(c) => c.ops.iter().any(|op| sig(&op.params, &op.return_type)),
952        CommonsItem::Provider(p) => p.ops.iter().any(|op| sig(&op.params, &op.return_type)),
953        // A `Connection` is a held resource storable only in a `store` field
954        // (handled above) — never in a plain record field, so `Type`/`Event`
955        // need no case here; `Actor`/`Messages` carry no `TypeRef` at all.
956        CommonsItem::Type(_)
957        | CommonsItem::Actor(_)
958        | CommonsItem::Messages(_)
959        | CommonsItem::Event(_) => false,
960    })
961}
962
963/// v0.22b: a checker `Ty` rendered back to a `TypeRef` for the codec
964/// machinery (which is `TypeRef`-driven). `None` for types the codec
965/// rejects anyway (functions, effects, type variables).
966fn ty_to_type_ref(t: TyId, tys: &Arc<Types>) -> Option<TypeRef> {
967    let sp = bynk_syntax::span::Span::new(0, 0);
968    Some(match &*tys.get(t) {
969        Ty::Base(b) => TypeRef::Base(*b, sp),
970        // v0.174 (#592): a generic-record instantiation (`Paginated[User]`,
971        // `args` non-empty) round-trips as a `TypeRef::App` so the codec closure
972        // reaches its monomorphised helper; a non-generic named type stays a
973        // bare `Named`.
974        Ty::Named { name, args, .. } if !args.is_empty() => TypeRef::App {
975            name: Ident {
976                name: name.clone(),
977                span: sp,
978            },
979            args: args
980                .iter()
981                .map(|a| ty_to_type_ref(*a, tys))
982                .collect::<Option<Vec<_>>>()?,
983            span: sp,
984        },
985        Ty::Named { name, .. } => TypeRef::Named(Ident {
986            name: name.clone(),
987            span: sp,
988        }),
989        Ty::Result(a, b) => TypeRef::Result(
990            Box::new(ty_to_type_ref(*a, tys)?),
991            Box::new(ty_to_type_ref(*b, tys)?),
992            sp,
993        ),
994        Ty::Option(a) => TypeRef::Option(Box::new(ty_to_type_ref(*a, tys)?), sp),
995        Ty::List(a) => TypeRef::List(Box::new(ty_to_type_ref(*a, tys)?), sp),
996        Ty::Map(k, v) => TypeRef::Map(
997            Box::new(ty_to_type_ref(*k, tys)?),
998            Box::new(ty_to_type_ref(*v, tys)?),
999            sp,
1000        ),
1001        Ty::Unit => TypeRef::Unit(sp),
1002        Ty::ValidationError => TypeRef::ValidationError(sp),
1003        Ty::JsonError => TypeRef::JsonError(sp),
1004        // R4.3: `Ty::Error` has no codec — same as the other non-boundary
1005        // types below, but for the additional reason that a checked program
1006        // should never contain one at a codec-generation site.
1007        Ty::Error
1008        | Ty::Effect(_)
1009        | Ty::Query(_)
1010        | Ty::Stream(_)
1011        | Ty::Connection(_)
1012        | Ty::HttpResult(_)
1013        | Ty::QueueResult
1014        | Ty::Fn { .. }
1015        | Ty::Var(_)
1016        | Ty::Actor(_)
1017        | Ty::ActorSum(_) => {
1018            return None;
1019        }
1020    })
1021}
1022
1023/// v0.22b: collect the `Json.encode`/`Json.decode[T]` target type-refs in
1024/// this file's bodies — the roots of the module-local codec-helper closure.
1025fn collect_json_codec_roots(commons: &TypedCommons) -> Vec<TypeRef> {
1026    let tys = commons.tys();
1027    let mut roots: Vec<TypeRef> = Vec::new();
1028    {
1029        let mut visit = |e: &Expr| {
1030            let ExprKind::MethodCall {
1031                receiver,
1032                method,
1033                args,
1034                ..
1035            } = &e.kind
1036            else {
1037                return;
1038            };
1039            let ExprKind::Ident(id) = &receiver.kind else {
1040                return;
1041            };
1042            if id.name != JSON {
1043                return;
1044            }
1045            match method.name.as_str() {
1046                "decode" => {
1047                    if let Some(Ty::Result(t, _)) = commons.expr_ty(e.id).as_deref()
1048                        && let Some(tr) = ty_to_type_ref(*t, tys)
1049                    {
1050                        roots.push(tr);
1051                    }
1052                }
1053                "encode" => {
1054                    if let Some(a) = args.first()
1055                        && let Some(t) = commons.expr_types.get(&a.id).map(|te| te.ty)
1056                        && let Some(tr) = ty_to_type_ref(t, tys)
1057                    {
1058                        roots.push(tr);
1059                    }
1060                }
1061                _ => {}
1062            }
1063        };
1064        for item in &commons.commons.items {
1065            match item {
1066                CommonsItem::Fn(f) => walk_block_exprs(&f.body, &mut visit),
1067                CommonsItem::Service(s) => {
1068                    for h in &s.handlers {
1069                        walk_block_exprs(&h.body, &mut visit);
1070                    }
1071                }
1072                CommonsItem::Agent(a) => {
1073                    for h in &a.handlers {
1074                        walk_block_exprs(&h.body, &mut visit);
1075                    }
1076                }
1077                CommonsItem::Provider(p) => {
1078                    for op in &p.ops {
1079                        walk_block_exprs(&op.body, &mut visit);
1080                    }
1081                }
1082                _ => {}
1083            }
1084        }
1085    }
1086    roots
1087}
1088
1089/// v0.22b: module-local serialise/deserialise helpers for the types this
1090/// file's `Json.encode`/`Json.decode[T]` calls reference (ADR 0045). The
1091/// closure machinery is shared with the workers boundary path; `skip_names`
1092/// / `skip_insts` dedupe against helpers that path already emitted into
1093/// this module.
1094fn emit_json_codec_helpers(
1095    out: &mut String,
1096    commons: &TypedCommons,
1097    ctx: &EmitProjectCtx,
1098    skip_names: &HashSet<String>,
1099    skip_insts: &HashSet<String>,
1100) {
1101    use serialisation::{collect_codec_closure, emit_generic_helpers, emit_helpers_for_owner};
1102    let roots = collect_json_codec_roots(commons);
1103    if roots.is_empty() {
1104        return;
1105    }
1106    let (names, insts) = collect_codec_closure(&roots, &commons.types);
1107    let names: Vec<String> = names
1108        .into_iter()
1109        .filter(|n| !skip_names.contains(n))
1110        .collect();
1111    emit_helpers_for_owner(
1112        out,
1113        &names,
1114        &commons.types,
1115        &ctx.commons_name,
1116        &ctx.runtime_use,
1117    );
1118    let insts: Vec<serialisation::GenericInst> = insts
1119        .into_iter()
1120        .filter(|i| !skip_insts.contains(&i.ts_name()))
1121        .collect();
1122    if !insts.is_empty() {
1123        emit_generic_helpers(out, &insts, &commons.types, &ctx.runtime_use);
1124    }
1125}
1126
1127/// Emit boundary serialise/deserialise helpers (v0.8 §3.4 / §5.2) for
1128/// every named type declared in this file that flows through a
1129/// cross-context call, plus the specialised generic helpers for any
1130/// Result/Option instantiation used at the boundary. Returns the emitted
1131/// (or locally-bound) helper type names and generic-instantiation names so
1132/// the v0.22b codec emission can dedupe against them.
1133fn emit_boundary_helpers(
1134    out: &mut String,
1135    commons: &TypedCommons,
1136    ctx: &EmitProjectCtx,
1137) -> (HashSet<String>, HashSet<String>) {
1138    use serialisation::{
1139        collect_boundary_types, collect_generic_instantiations, emit_generic_helpers,
1140        emit_helpers_for_owner,
1141    };
1142
1143    // For contexts: walk the local services to discover boundary types.
1144    // For commons: walk every consumer's services that reference us
1145    // (approximated as: emit for every type declared in this file).
1146    //
1147    // Service handler types cross the *cross-Worker call* boundary, which only
1148    // exists on the `workers` target; on `bundle` calls are in-process, so their
1149    // serialise/deserialise helpers are not emitted. The agent **rehydration**
1150    // boundary (ADR 0124), in contrast, exists on both targets, so agent
1151    // store-field types are always collected (below).
1152    let workers = matches!(ctx.target, BuildTarget::Workers);
1153    let services: HashMap<String, ServiceDecl> = if workers {
1154        commons
1155            .commons
1156            .items
1157            .iter()
1158            .filter_map(|i| match i {
1159                CommonsItem::Service(s) => Some((s.name.name.clone(), s.clone())),
1160                _ => None,
1161            })
1162            .collect()
1163    } else {
1164        HashMap::new()
1165    };
1166
1167    // v0.96 (ADR 0124): an agent's `store`-field types are rehydration-boundary
1168    // types — their deserialisers drive the load-time validation gate.
1169    let agents: HashMap<String, AgentDecl> = commons
1170        .commons
1171        .items
1172        .iter()
1173        .filter_map(|i| match i {
1174            CommonsItem::Agent(a) => Some((a.name.name.clone(), a.clone())),
1175            _ => None,
1176        })
1177        .collect();
1178
1179    let locally_declared: HashSet<String> = ctx.file_decl_index.types.keys().cloned().collect();
1180    if ctx.unit_kind == UnitKind::Context {
1181        let boundary_types_all = collect_boundary_types(&commons.types, &services, &agents);
1182        // Locally-declared boundary types get full helpers in this module. On
1183        // `bundle` (v0.96, ADR 0124) the commons modules emit no boundary helpers,
1184        // so a cross-commons *agent-state* type's deserialiser — needed by the
1185        // rehydration gate — is emitted here in the context instead of re-exported.
1186        let local_boundary: Vec<String> = boundary_types_all
1187            .iter()
1188            .filter(|n| !workers || locally_declared.contains(*n))
1189            .cloned()
1190            .collect();
1191        emit_helpers_for_owner(
1192            out,
1193            &local_boundary,
1194            &commons.types,
1195            ctx.commons_name.as_str(),
1196            &ctx.runtime_use,
1197        );
1198
1199        // Re-export helpers for commons-owned boundary types so consumers
1200        // can address them through this context's handlers.ts namespace
1201        // (matching the namespace import they already use for cross-
1202        // context types). Grouped by source commons. Workers only — on `bundle`
1203        // the commons emit no helpers, so cross-commons types are emitted
1204        // locally above (v0.96) rather than imported.
1205        let mut by_commons: HashMap<String, Vec<String>> = HashMap::new();
1206        for n in &boundary_types_all {
1207            if !workers || locally_declared.contains(n) {
1208                continue;
1209            }
1210            if matches!(ctx.imported_from_kind.get(n), Some(UnitKind::Commons))
1211                && let Some(commons_name) = ctx.imported_from.get(n)
1212            {
1213                by_commons
1214                    .entry(commons_name.clone())
1215                    .or_default()
1216                    .push(n.clone());
1217            }
1218        }
1219        let mut commons_keys: Vec<&String> = by_commons.keys().collect();
1220        commons_keys.sort();
1221        for commons_name in commons_keys {
1222            let names = by_commons.get(commons_name).unwrap();
1223            let mut sorted_names: Vec<String> = names.clone();
1224            sorted_names.sort();
1225            sorted_names.dedup();
1226            let target_path = ctx
1227                .imported_decl_paths
1228                .get(commons_name)
1229                .and_then(|m| sorted_names.iter().find_map(|n| m.get(n).cloned()))
1230                .unwrap_or_else(|| EmitProjectCtx::commons_path(commons_name));
1231            let import_spec = cross_commons_import_specifier_for_path(
1232                &ctx.source_path,
1233                &target_path,
1234                ctx.import_ext,
1235            );
1236            let mut parts: Vec<String> = Vec::new();
1237            for n in &sorted_names {
1238                parts.push(format!("serialise_{n}"));
1239                parts.push(format!("deserialise_{n}"));
1240            }
1241            // v0.9.1: emit both a regular import (so the names are bound
1242            // locally for use inside this file's serialisation helpers) and a
1243            // re-export (so downstream consumers can still reach them
1244            // through this module). A bare `export { ... } from "..."`
1245            // re-export does not create a local binding, which `tsc --strict`
1246            // catches when the body calls one of the helpers directly.
1247            writeln!(
1248                out,
1249                "import {{ {} }} from \"{import_spec}\";",
1250                parts.join(", ")
1251            )
1252            .unwrap();
1253            writeln!(out, "export {{ {} }};", parts.join(", ")).unwrap();
1254        }
1255        if !by_commons.is_empty() {
1256            writeln!(out).unwrap();
1257        }
1258
1259        // Specialised Result_/Option_ helpers for the instantiations used —
1260        // in handler signatures or in boundary-type fields (v0.18).
1261        //
1262        // #977: the field walk follows `local_boundary`, not `boundary_types_all`
1263        // — the same narrowing `emit_helpers_for_owner` applies just above, and
1264        // for the same reason. A boundary type this context does not *declare* is
1265        // either commons-owned (its codec, and its own instantiations, come from
1266        // the commons module) or consumed (its codec is regenerated below by
1267        // `emit_consumed_context_helpers`, whose `Qual` map reaches the owner's
1268        // `import type * as <ns>` alias). Walking a foreign type's fields here
1269        // emitted its instantiations *unqualified* — `Option<Region>` for a
1270        // consumed `Region` — and then seeded `emitted_insts` so the qualified
1271        // pass below skipped it, leaving `tsc --strict` with `TS2304: Cannot find
1272        // name 'Region'`. Handler signatures still walk in full: a *local*
1273        // handler naming `Option[ConsumedRegion]` directly is this module's own
1274        // boundary either way.
1275        let insts =
1276            collect_generic_instantiations(&services, &agents, &local_boundary, &commons.types);
1277        emit_generic_helpers(out, &insts, &commons.types, &ctx.runtime_use);
1278
1279        // #661 (ADR 0199 Decision G discharged): the caller's own view of each
1280        // consumed context's boundary codecs, so a cross-context call reaches
1281        // `deserialise_Result_AuthId_PaymentError` **locally** instead of through
1282        // the callee's module. Workers only — on `bundle` the call is in-process
1283        // and needs no wire codec. Everything the caller already emits (its own
1284        // boundary types, the commons re-exports above, its own generic
1285        // instantiations) is skipped, so only the callee-*owned* types the caller
1286        // lacks a local view of are generated.
1287        let (consumed_names, consumed_insts) = if workers {
1288            let mut emitted_names: HashSet<String> = local_boundary.iter().cloned().collect();
1289            for names in by_commons.values() {
1290                emitted_names.extend(names.iter().cloned());
1291            }
1292            let mut emitted_insts: HashSet<String> = insts.iter().map(|i| i.ts_name()).collect();
1293            emit_consumed_context_helpers(out, commons, ctx, &mut emitted_names, &mut emitted_insts)
1294        } else {
1295            (Vec::new(), Vec::new())
1296        };
1297
1298        let mut ret_names: HashSet<String> = boundary_types_all.into_iter().collect();
1299        ret_names.extend(consumed_names);
1300        let mut ret_insts: HashSet<String> = insts.iter().map(|i| i.ts_name()).collect();
1301        ret_insts.extend(consumed_insts);
1302        (ret_names, ret_insts)
1303    } else if !workers {
1304        // Commons/adapters have no agents (no rehydration boundary), and on
1305        // `bundle` there is no cross-Worker call boundary either — so emit no
1306        // boundary helpers, matching pre-v0.96 bundle output (the rehydration
1307        // pass that now always runs is for context-declared agents only).
1308        (HashSet::new(), HashSet::new())
1309    } else {
1310        // Commons/adapters (workers): emit helpers for every type declared in
1311        // this file, plus (v0.18) the generic instantiations their fields use —
1312        // a record like the bynk surface's `Request` carries
1313        // `Option[String]` fields whose serialisers delegate to the
1314        // specialised helpers.
1315        //
1316        // v0.132 (#479): scope to types declared in *this* file, not the whole
1317        // unit. `file_decl_index` is unit-wide (name -> declaring-file path), so a
1318        // multi-file commons must filter by the current file — otherwise a
1319        // non-declaring sibling (e.g. the file holding `fn T.make`, not `type T`)
1320        // emits an orphan `serialise_T`/`deserialise_T` with `T` out of scope, and
1321        // the codec is duplicated across files. Unlike a workers *context* (which
1322        // collapses to one `handlers.ts` under a synthetic source_path, so its
1323        // unit-wide `locally_declared` above is correct), a commons emits per file.
1324        let mut locally: Vec<String> = ctx
1325            .file_decl_index
1326            .types
1327            .iter()
1328            .filter(|(_, path)| path.as_path() == ctx.source_path.as_path())
1329            .map(|(name, _)| name.clone())
1330            .collect();
1331        locally.sort();
1332        emit_helpers_for_owner(
1333            out,
1334            &locally,
1335            &commons.types,
1336            ctx.commons_name.as_str(),
1337            &ctx.runtime_use,
1338        );
1339        let insts = collect_generic_instantiations(
1340            &HashMap::new(),
1341            &HashMap::new(),
1342            &locally,
1343            &commons.types,
1344        );
1345        emit_generic_helpers(out, &insts, &commons.types, &ctx.runtime_use);
1346        (
1347            locally.into_iter().collect(),
1348            insts.iter().map(|i| i.ts_name()).collect(),
1349        )
1350    }
1351}
1352
1353/// #661: emit the caller's own `serialise_*`/`deserialise_*` for every
1354/// callee-owned boundary type reachable from the services this context
1355/// **calls**, so a `workers` cross-context call resolves its codecs locally
1356/// rather than importing the callee's module as a value.
1357///
1358/// The codec function names stay bare and local; only the TS *type* positions
1359/// reach through the callee's `import type * as <ns>` alias (via the `Qual`
1360/// map built here). Refinement validation follows the export visibility: an
1361/// opaque type casts structurally (Decision C), a transparent refined type
1362/// inlines its predicates (Decision D) — both decided inside the codec emitter
1363/// from the type's own body.
1364///
1365/// Only the callee-*owned* types (its `exports`) are generated. Commons types
1366/// reachable through the boundary (`Money`) are already emitted or re-exported
1367/// by the caller's own path, so they are left out here and deduped against
1368/// `emitted_names` / `emitted_insts`, which the caller seeds with everything it
1369/// has already emitted. Returns the names and generic-instantiation names newly
1370/// emitted, so the Json-codec pass dedupes against them too.
1371fn emit_consumed_context_helpers(
1372    out: &mut String,
1373    commons: &TypedCommons,
1374    ctx: &EmitProjectCtx,
1375    emitted_names: &mut HashSet<String>,
1376    emitted_insts: &mut HashSet<String>,
1377) -> (Vec<String>, Vec<String>) {
1378    use serialisation::{
1379        collect_codec_closure, emit_generic_helpers_qualified, emit_helpers_for_owner_qualified,
1380    };
1381    let info = &ctx.cross_context;
1382    let mut consumed_names_out: Vec<String> = Vec::new();
1383    let mut consumed_insts_out: Vec<String> = Vec::new();
1384
1385    // Only the services this context actually **calls** — not the callee's whole
1386    // provided surface. `consumed_services` carries every service the dependency
1387    // provides; generating a codec for one this context never reaches would bloat
1388    // the bundle with a contract it does not participate in (and pull in the
1389    // uncalled service's own boundary types). Mirrors the `called` narrowing the
1390    // contract manifest applies to `expects` (ADR 0200 Decision E, one layer up).
1391    let called = called_consumed_services(commons, info);
1392
1393    // #973: this context's own `from Events(E)` subscriptions, keyed by the
1394    // consumed context that declares each `E` — a subscriber calls no method
1395    // on the publisher, so `called_consumed_services` alone would never see
1396    // it, and the `continue` below on an empty `called_here` would skip the
1397    // event's payload type entirely (the root cause of #973: a subscriber's
1398    // generated module had no `deserialise_<Payload>` at all).
1399    let mut consumed_event_roots: HashMap<String, Vec<bynk_syntax::ast::TypeRef>> = HashMap::new();
1400    for item in &commons.commons.items {
1401        let CommonsItem::Service(svc) = item else {
1402            continue;
1403        };
1404        let bynk_syntax::ast::ServiceProtocol::Events { event_type, .. } = &svc.protocol else {
1405            continue;
1406        };
1407        let bynk_syntax::ast::TypeRef::Named(id) = event_type else {
1408            continue;
1409        };
1410        for (c, names) in &info.consumed_event_names {
1411            if names.contains(&id.name) {
1412                consumed_event_roots
1413                    .entry(c.clone())
1414                    .or_default()
1415                    .push(event_type.clone());
1416            }
1417        }
1418    }
1419
1420    let empty_svcs: HashMap<String, bynk_check::resolver::CrossContextService> = HashMap::new();
1421    let empty_called: HashSet<String> = HashSet::new();
1422    let empty_event_roots: Vec<bynk_syntax::ast::TypeRef> = Vec::new();
1423
1424    let mut consumed_keys: HashSet<&String> = info.consumed_services.keys().collect();
1425    consumed_keys.extend(consumed_event_roots.keys());
1426    let mut consumed_keys: Vec<&String> = consumed_keys.into_iter().collect();
1427    consumed_keys.sort();
1428    for c in consumed_keys {
1429        let svcs = info.consumed_services.get(c).unwrap_or(&empty_svcs);
1430        let event_roots = consumed_event_roots.get(c).unwrap_or(&empty_event_roots);
1431        if svcs.is_empty() && event_roots.is_empty() {
1432            continue;
1433        }
1434        let called_here = called.get(c).unwrap_or(&empty_called);
1435        if called_here.is_empty() && event_roots.is_empty() {
1436            continue;
1437        }
1438        let Some(types_table) = info.consumed_types.get(c) else {
1439            continue;
1440        };
1441        // The callee's exports — the set of types it *owns* and a consumer may
1442        // name. A closure type outside this set (a commons type the callee only
1443        // `uses`, e.g. `Money`) is the caller's own already, not the callee's to
1444        // hand out, so the caller never regenerates it under the callee's ns.
1445        let exports = ctx.exports_for_consumed.get(c);
1446        let owned = |n: &str| exports.is_some_and(|e| e.contains_key(n));
1447
1448        // Roots: every called service's parameter and return types, plus (#973)
1449        // any event type this context subscribes to from `c` — a subscriber
1450        // participates in the event's contract as its receiving half, so its
1451        // payload is not an uncalled surface the way an unreached method is
1452        // (the narrowing this loop otherwise applies, mirroring ADR 0200
1453        // Decision E one layer up, at `called_consumed_services` above).
1454        let mut svc_names: Vec<&String> =
1455            svcs.keys().filter(|s| called_here.contains(*s)).collect();
1456        svc_names.sort();
1457        let mut roots: Vec<bynk_syntax::ast::TypeRef> = event_roots.clone();
1458        for sn in svc_names {
1459            let svc = &svcs[sn];
1460            for (_, t) in &svc.params {
1461                roots.push(t.clone());
1462            }
1463            roots.push(svc.return_type.clone());
1464        }
1465        let (names, cinsts) = collect_codec_closure(&roots, types_table);
1466
1467        let ns = format!("{}.", qualified_to_ns(c));
1468        let mut qual: HashMap<String, String> = HashMap::new();
1469        for n in &names {
1470            if owned(n) {
1471                qual.insert(n.clone(), ns.clone());
1472            }
1473        }
1474
1475        let mut to_emit: Vec<String> = names
1476            .iter()
1477            .filter(|n| owned(n) && emitted_names.insert((*n).clone()))
1478            .cloned()
1479            .collect();
1480        to_emit.sort();
1481        emit_helpers_for_owner_qualified(
1482            out,
1483            &to_emit,
1484            types_table,
1485            ctx.commons_name.as_str(),
1486            &qual,
1487            &ctx.runtime_use,
1488        );
1489        consumed_names_out.extend(to_emit);
1490
1491        let to_emit_insts: Vec<serialisation::GenericInst> = cinsts
1492            .into_iter()
1493            .filter(|i| emitted_insts.insert(i.ts_name()))
1494            .collect();
1495        for i in &to_emit_insts {
1496            consumed_insts_out.push(i.ts_name());
1497        }
1498        emit_generic_helpers_qualified(out, &to_emit_insts, types_table, &qual, &ctx.runtime_use);
1499    }
1500
1501    (consumed_names_out, consumed_insts_out)
1502}
1503
1504/// #661: the cross-context services this unit actually **calls**, as `consumed
1505/// context → service names`. A copy of `project::called_cross_context_services`
1506/// over the emitter's AST view (`commons`) — the caller-side codec set follows
1507/// the *called* subset, not the callee's full provided surface, so it stays in
1508/// step with what the contract manifest records under `expects`.
1509fn called_consumed_services(
1510    commons: &TypedCommons,
1511    info: &bynk_check::resolver::CrossContextInfo,
1512) -> HashMap<String, HashSet<String>> {
1513    let mut out: HashMap<String, HashSet<String>> = HashMap::new();
1514    if info.consumed_contexts.is_empty() && info.aliases.is_empty() {
1515        return out;
1516    }
1517    let mut visit = |e: &Expr| {
1518        if let ExprKind::MethodCall {
1519            receiver, method, ..
1520        } = &e.kind
1521            && let Some(chain) = flatten_emit_ident_chain(receiver)
1522            && let Some(target) = info.resolve_prefix(&chain)
1523        {
1524            out.entry(target).or_default().insert(method.name.clone());
1525        }
1526    };
1527    for item in &commons.commons.items {
1528        match item {
1529            CommonsItem::Service(s) => {
1530                for h in &s.handlers {
1531                    walk_block_exprs(&h.body, &mut visit);
1532                }
1533            }
1534            CommonsItem::Agent(a) => {
1535                for h in &a.handlers {
1536                    walk_block_exprs(&h.body, &mut visit);
1537                }
1538            }
1539            CommonsItem::Provider(p) => {
1540                for op in &p.ops {
1541                    walk_block_exprs(&op.body, &mut visit);
1542                }
1543            }
1544            _ => {}
1545        }
1546    }
1547    out
1548}
1549
1550/// For each type imported via `uses` that's referenced in this file, emit:
1551/// 1. (Done in imports) an aliased import: `import { Money as __CommonsMoney } from ...`
1552/// 2. A rebranded type alias: `export type Money = __CommonsMoney & { readonly __ctxBrand: "..." }`
1553///
1554/// The brand makes two contexts that both `uses` the same commons see distinct
1555/// nominal `Money` types in their TypeScript output (v0.4 §3.4 / §6.2).
1556fn emit_context_rebrands(
1557    out: &mut String,
1558    refs: &ExternalReferences,
1559    commons: &TypedCommons,
1560    ctx: &EmitProjectCtx,
1561) {
1562    let Some(owning) = &ctx.owning_context else {
1563        return;
1564    };
1565    // Collect names imported via `uses` (kind == Commons in imported_from_kind).
1566    let mut names: Vec<String> = Vec::new();
1567    for set in refs.by_commons.values() {
1568        for n in set {
1569            // v0.20b: only *types* get the context rebrand — a
1570            // `uses`-imported function is a value and imports plainly.
1571            if matches!(ctx.imported_from_kind.get(n), Some(UnitKind::Commons))
1572                && commons.types.contains_key(n)
1573            {
1574                names.push(n.clone());
1575            }
1576        }
1577    }
1578    names.sort();
1579    names.dedup();
1580    if names.is_empty() {
1581        return;
1582    }
1583    for name in &names {
1584        // v0.174 (#592): a generic commons type keeps its parameters across the
1585        // rebrand — `Paginated[T]` aliases as `Paginated<T> =
1586        // __CommonsPaginated<T> & { … }`, not a bare `Paginated`, which would
1587        // both drop the parameter and make every `Paginated<User>` reference in
1588        // the context a "type is not generic" error.
1589        let params: Vec<&str> = commons
1590            .types
1591            .get(name)
1592            .map(|d| d.type_params.iter().map(|p| p.name.name.as_str()).collect())
1593            .unwrap_or_default();
1594        let generics = if params.is_empty() {
1595            String::new()
1596        } else {
1597            format!("<{}>", params.join(", "))
1598        };
1599        writeln!(
1600            out,
1601            "export type {name}{generics} = __Commons{name}{generics} & {{ readonly __ctxBrand: \"{owning}\" }};",
1602        )
1603        .unwrap();
1604        // v0.9.2: a commons refined/opaque type carries a value-side
1605        // constructor (`.of`, and `.unsafe` for opaque). Re-export it under the
1606        // rebranded name so a context calling `ShortCode.of(...)` resolves to a
1607        // value — delegating to the imported commons constructor but reporting
1608        // the context-branded type. (Without this, `ShortCode` is type-only in
1609        // the context and `.of` fails to resolve.)
1610        if let Some(base) = commons
1611            .types
1612            .get(name)
1613            .and_then(|d| refined_or_opaque_base(d))
1614        {
1615            let ts_base = ts_base(base);
1616            let is_opaque = matches!(
1617                commons.types.get(name).map(|d| &d.body),
1618                Some(TypeBody::Opaque { .. })
1619            );
1620            writeln!(out, "export const {name} = {{").unwrap();
1621            writeln!(
1622                out,
1623                "  of(value: {ts_base}): Result<{name}, ValidationError> {{ return __Commons{name}.of(value) as unknown as Result<{name}, ValidationError>; }},",
1624            )
1625            .unwrap();
1626            // ADR 0182: only opaque types have a public `.unsafe` to forward.
1627            // A refined/alias type has none — a consuming context brands an
1628            // admitted literal with an inline `as` cast, not a forwarder call.
1629            if is_opaque {
1630                writeln!(
1631                    out,
1632                    "  unsafe(value: {ts_base}): {name} {{ return __Commons{name}.unsafe(value) as unknown as {name}; }},",
1633                )
1634                .unwrap();
1635            }
1636            // v0.132.1 (#481): forward the commons' user-defined attached methods
1637            // (`Cents.fromInt`, …) so the rebranded const carries more than the
1638            // built-in `of`/`unsafe`. Without this a consumer's `Cents.fromInt(n)`
1639            // — which `bynkc check` accepts — fails `tsc`. The methods aren't in
1640            // this context's own `commons` (only imported *types* are merged);
1641            // they arrive via `ctx.imported_methods`, keyed by type name.
1642            if let Some(methods) = ctx.imported_methods.get(name) {
1643                emit_forwarded_methods(out, name, methods);
1644            }
1645            writeln!(out, "}};").unwrap();
1646        }
1647    }
1648    writeln!(out).unwrap();
1649}
1650
1651/// If a type declaration is a refined or opaque base type, return its base
1652/// (both lower to a branded base with a `.of` / `.unsafe` constructor object).
1653fn refined_or_opaque_base(decl: &TypeDecl) -> Option<BaseType> {
1654    match &decl.body {
1655        TypeBody::Refined { base, .. } | TypeBody::Opaque { base, .. } => Some(*base),
1656        _ => None,
1657    }
1658}
1659
1660/// Names that this file needs to import from elsewhere (sibling files of
1661/// the same commons, or other commons via `uses`).
1662#[derive(Default)]
1663struct ExternalReferences {
1664    /// `commons name` → set of names to import.
1665    by_commons: HashMap<String, HashSet<String>>,
1666    /// `sibling source path` → set of names to import (same-commons).
1667    by_sibling: HashMap<PathBuf, HashSet<String>>,
1668}
1669
1670impl ExternalReferences {
1671    fn is_empty(&self) -> bool {
1672        self.by_commons.is_empty() && self.by_sibling.is_empty()
1673    }
1674}
1675
1676fn collect_external_references(commons: &TypedCommons, ctx: &EmitProjectCtx) -> ExternalReferences {
1677    // Names declared in this file (so we know what's local-to-file).
1678    // A `messages` block declares no importable identifier of its own (its
1679    // `render` is synthesised separately), so `name()` is `None` there and it
1680    // contributes nothing to the local-name set.
1681    let local_to_file: HashSet<String> = commons
1682        .commons
1683        .items
1684        .iter()
1685        .filter_map(|i| i.name().map(|n| n.name.clone()))
1686        .collect();
1687
1688    let mut refs = ExternalReferences::default();
1689
1690    // Walk every expression and TypeRef in this file's items, recording
1691    // any reference that resolves to a name declared in a sibling file or
1692    // an imported commons.
1693    for item in &commons.commons.items {
1694        match item {
1695            CommonsItem::Type(t) => {
1696                collect_refs_in_type_decl(t, &local_to_file, ctx, &mut refs);
1697            }
1698            // Events track, slice 0 (spine #936): an `event`'s field types
1699            // are collected exactly like a `type`'s, via the same synthetic
1700            // `TypeDecl` `EventDecl::as_type_decl` builds.
1701            CommonsItem::Event(e) => {
1702                collect_refs_in_type_decl(&e.as_type_decl(), &local_to_file, ctx, &mut refs);
1703            }
1704            CommonsItem::Fn(f) => {
1705                collect_refs_in_fn(f, &local_to_file, commons, ctx, &mut refs);
1706            }
1707            CommonsItem::Capability(c) => {
1708                for op in &c.ops {
1709                    for p in &op.params {
1710                        collect_refs_in_typeref(&p.type_ref, &local_to_file, ctx, &mut refs);
1711                    }
1712                    collect_refs_in_typeref(&op.return_type, &local_to_file, ctx, &mut refs);
1713                }
1714            }
1715            CommonsItem::Provider(p) => {
1716                // Reference to the capability so we can import it (locally
1717                // declared, so usually no extra work).
1718                let _ = &p.capability;
1719                for op in &p.ops {
1720                    for param in &op.params {
1721                        collect_refs_in_typeref(&param.type_ref, &local_to_file, ctx, &mut refs);
1722                    }
1723                    collect_refs_in_typeref(&op.return_type, &local_to_file, ctx, &mut refs);
1724                    collect_refs_in_block(&op.body, &local_to_file, commons, ctx, &mut refs);
1725                }
1726            }
1727            CommonsItem::Service(s) => {
1728                for h in &s.handlers {
1729                    for p in &h.params {
1730                        collect_refs_in_typeref(&p.type_ref, &local_to_file, ctx, &mut refs);
1731                    }
1732                    collect_refs_in_typeref(&h.return_type, &local_to_file, ctx, &mut refs);
1733                    collect_refs_in_block(&h.body, &local_to_file, commons, ctx, &mut refs);
1734                }
1735            }
1736            CommonsItem::Agent(a) => {
1737                collect_refs_in_typeref(&a.key_type, &local_to_file, ctx, &mut refs);
1738                for f in &a.store_fields {
1739                    for arg in &f.kind.args {
1740                        collect_refs_in_typeref(arg, &local_to_file, ctx, &mut refs);
1741                    }
1742                }
1743                for h in &a.handlers {
1744                    for p in &h.params {
1745                        collect_refs_in_typeref(&p.type_ref, &local_to_file, ctx, &mut refs);
1746                    }
1747                    collect_refs_in_typeref(&h.return_type, &local_to_file, ctx, &mut refs);
1748                    collect_refs_in_block(&h.body, &local_to_file, commons, ctx, &mut refs);
1749                }
1750            }
1751            CommonsItem::Actor(a) => {
1752                if let Some(id) = &a.identity {
1753                    collect_refs_in_typeref(id, &local_to_file, ctx, &mut refs);
1754                }
1755            }
1756            // `MessageEntry.code`/`.template` are plain string literals with
1757            // no TypeRefs/exprs of their own to walk — but the generated
1758            // `render` (emit_messages) has a signature and body that name
1759            // `LocaleTag`/`Message`/`MessageArg` even though no expression in
1760            // this file's *source* does, so those three are registered here
1761            // by hand, the same way a real reference would be. `render`/
1762            // `renderArg` are deliberately NOT registered this way —
1763            // `render` collides with the generated function of the same
1764            // name, and both are instead imported together under
1765            // `emit_unit`'s (project.rs) hand-written, aliased extra import
1766            // line, bypassing this dedup/merge path entirely (importing
1767            // `renderArg` there too, alongside the aliased `render`, avoids a
1768            // duplicate import of it from here).
1769            CommonsItem::Messages(_) => {
1770                for name in ["LocaleTag", "Message", "MessageArg"] {
1771                    record_name_ref(name, &local_to_file, ctx, &mut refs);
1772                }
1773            }
1774        }
1775    }
1776    refs
1777}
1778
1779fn collect_refs_in_type_decl(
1780    t: &TypeDecl,
1781    local_to_file: &HashSet<String>,
1782    ctx: &EmitProjectCtx,
1783    out: &mut ExternalReferences,
1784) {
1785    match &t.body {
1786        TypeBody::Record(r) => {
1787            for f in &r.fields {
1788                collect_refs_in_typeref(&f.type_ref, local_to_file, ctx, out);
1789            }
1790        }
1791        TypeBody::Sum(s) => {
1792            for v in &s.variants {
1793                for p in &v.payload {
1794                    collect_refs_in_typeref(&p.type_ref, local_to_file, ctx, out);
1795                }
1796            }
1797        }
1798        _ => {}
1799    }
1800}
1801
1802fn collect_refs_in_fn(
1803    f: &FnDecl,
1804    local_to_file: &HashSet<String>,
1805    commons: &TypedCommons,
1806    ctx: &EmitProjectCtx,
1807    out: &mut ExternalReferences,
1808) {
1809    for p in &f.params {
1810        collect_refs_in_typeref(&p.type_ref, local_to_file, ctx, out);
1811    }
1812    collect_refs_in_typeref(&f.return_type, local_to_file, ctx, out);
1813    // For methods: the attached type may also be elsewhere.
1814    if let FnName::Method { type_name, .. } = &f.name {
1815        record_name_ref(&type_name.name, local_to_file, ctx, out);
1816    }
1817    collect_refs_in_block(&f.body, local_to_file, commons, ctx, out);
1818}
1819
1820fn collect_refs_in_typeref(
1821    r: &TypeRef,
1822    local_to_file: &HashSet<String>,
1823    ctx: &EmitProjectCtx,
1824    out: &mut ExternalReferences,
1825) {
1826    match r {
1827        TypeRef::Named(id) => record_name_ref(&id.name, local_to_file, ctx, out),
1828        TypeRef::Result(t, e, _) => {
1829            collect_refs_in_typeref(t, local_to_file, ctx, out);
1830            collect_refs_in_typeref(e, local_to_file, ctx, out);
1831        }
1832        // Exhaustive over the compound constructors (#527, the #507 disease):
1833        // the old `_ => {}` catch-all dropped `List[KindCount]` and friends,
1834        // so a name referenced only inside such a position was never
1835        // imported and the emitted module failed `tsc`.
1836        TypeRef::Option(t, _)
1837        | TypeRef::Effect(t, _)
1838        | TypeRef::HttpResult(t, _)
1839        | TypeRef::List(t, _)
1840        | TypeRef::Query(t, _)
1841        | TypeRef::Stream(t, _)
1842        | TypeRef::Connection(t, _)
1843        | TypeRef::History(t, _) => collect_refs_in_typeref(t, local_to_file, ctx, out),
1844        TypeRef::Map(k, v, _) => {
1845            collect_refs_in_typeref(k, local_to_file, ctx, out);
1846            collect_refs_in_typeref(v, local_to_file, ctx, out);
1847        }
1848        TypeRef::Fn(params, ret, _) => {
1849            for t in params {
1850                collect_refs_in_typeref(t, local_to_file, ctx, out);
1851            }
1852            collect_refs_in_typeref(ret, local_to_file, ctx, out);
1853        }
1854        // v0.157 (ADR 0183): a `Name[Arg, …]` application references the
1855        // generic type plus every argument — all must be imported.
1856        TypeRef::App { name, args, .. } => {
1857            record_name_ref(&name.name, local_to_file, ctx, out);
1858            for t in args {
1859                collect_refs_in_typeref(t, local_to_file, ctx, out);
1860            }
1861        }
1862        TypeRef::Base(..)
1863        | TypeRef::QueueResult(_)
1864        | TypeRef::ValidationError(_)
1865        | TypeRef::JsonError(_)
1866        | TypeRef::Unit(_) => {}
1867    }
1868}
1869
1870fn collect_refs_in_block(
1871    b: &Block,
1872    local_to_file: &HashSet<String>,
1873    commons: &TypedCommons,
1874    ctx: &EmitProjectCtx,
1875    out: &mut ExternalReferences,
1876) {
1877    for stmt in &b.statements {
1878        match stmt {
1879            Statement::Let(l) | Statement::EffectLet(l) => {
1880                if let Some(t) = &l.type_annot {
1881                    collect_refs_in_typeref(t, local_to_file, ctx, out);
1882                }
1883                collect_refs_in_expr(&l.value, local_to_file, commons, ctx, out);
1884            }
1885            Statement::Expect(a) => {
1886                collect_refs_in_expr(&a.value, local_to_file, commons, ctx, out);
1887            }
1888            Statement::Send(s) => {
1889                collect_refs_in_expr(&s.value, local_to_file, commons, ctx, out);
1890            }
1891            Statement::Do(d) => {
1892                collect_refs_in_expr(&d.value, local_to_file, commons, ctx, out);
1893            }
1894            Statement::Assign(a) => {
1895                collect_refs_in_expr(&a.value, local_to_file, commons, ctx, out);
1896            }
1897        }
1898    }
1899    collect_refs_in_expr(&b.tail, local_to_file, commons, ctx, out);
1900}
1901
1902fn collect_refs_in_expr(
1903    e: &Expr,
1904    local_to_file: &HashSet<String>,
1905    commons: &TypedCommons,
1906    ctx: &EmitProjectCtx,
1907    out: &mut ExternalReferences,
1908) {
1909    match &e.kind {
1910        // A bare ident the checker typed as a sum is a nullary variant
1911        // constructor — the lowering qualifies it to `Type.Variant`, so the
1912        // owning type must be imported (v0.18: first hit by `Get` from the
1913        // consumed bynk surface's `Method`).
1914        ExprKind::Ident(id) => {
1915            if let Some(type_name) = sum_owner_of_variant(&id.name, e.id, commons) {
1916                record_name_ref(&type_name, local_to_file, ctx, out);
1917            }
1918        }
1919        ExprKind::IntLit { .. }
1920        | ExprKind::FloatLit { .. }
1921        | ExprKind::DurationLit { .. }
1922        | ExprKind::StrLit(_)
1923        | ExprKind::BoolLit(_)
1924        | ExprKind::None
1925        | ExprKind::UnitLit => {}
1926        // v0.43: a hole's expression may reference imported names.
1927        ExprKind::Wire(inner) => collect_refs_in_expr(inner, local_to_file, commons, ctx, out),
1928        ExprKind::InterpStr(parts) => {
1929            for part in parts {
1930                if let InterpPart::Hole(hole) = part {
1931                    collect_refs_in_expr(hole, local_to_file, commons, ctx, out);
1932                }
1933            }
1934        }
1935        // v0.20a: a lambda — its annotated param types may reference
1936        // imported types; the body walks like any expression.
1937        ExprKind::Lambda(lambda) => {
1938            for p in &lambda.params {
1939                if let Some(tr) = &p.type_ref {
1940                    collect_refs_in_typeref(tr, local_to_file, ctx, out);
1941                }
1942            }
1943            collect_refs_in_expr(&lambda.body, local_to_file, commons, ctx, out);
1944        }
1945        ExprKind::EffectPure(inner) => {
1946            collect_refs_in_expr(inner, local_to_file, commons, ctx, out);
1947        }
1948        ExprKind::Expect(inner) => {
1949            collect_refs_in_expr(inner, local_to_file, commons, ctx, out);
1950        }
1951        ExprKind::Val { args, .. } => {
1952            for a in args {
1953                collect_refs_in_expr(a, local_to_file, commons, ctx, out);
1954            }
1955        }
1956        ExprKind::ListLit(elems) => {
1957            for el in elems {
1958                collect_refs_in_expr(el, local_to_file, commons, ctx, out);
1959            }
1960        }
1961        // v0.117: observation predicates may reference types/fns; `trace` does not.
1962        ExprKind::Observation(o) => {
1963            if let ObservationMatcher::Called { count, with_pred } = &o.matcher {
1964                if let Some(c) = count {
1965                    collect_refs_in_expr(c, local_to_file, commons, ctx, out);
1966                }
1967                if let Some(p) = with_pred {
1968                    collect_refs_in_expr(p, local_to_file, commons, ctx, out);
1969                }
1970            }
1971        }
1972        ExprKind::Trace { .. } => {}
1973        ExprKind::RecordSpread {
1974            type_name,
1975            base,
1976            overrides,
1977        } => {
1978            if let Some(tn) = type_name {
1979                record_name_ref(&tn.name, local_to_file, ctx, out);
1980            }
1981            collect_refs_in_expr(base, local_to_file, commons, ctx, out);
1982            for f in overrides {
1983                if let Some(v) = &f.value {
1984                    collect_refs_in_expr(v, local_to_file, commons, ctx, out);
1985                }
1986            }
1987        }
1988        ExprKind::Call { name, args, .. } => {
1989            record_name_ref(&name.name, local_to_file, ctx, out);
1990            // A payload-carrying bare variant call (`Won(prize)`) lowers to
1991            // `Type.Variant(…)` — import the owning sum type too.
1992            if let Some(type_name) = sum_owner_of_variant(&name.name, e.id, commons) {
1993                record_name_ref(&type_name, local_to_file, ctx, out);
1994            }
1995            // #527: a call to a commons-imported fn may lower with a rebrand
1996            // assertion naming its return type (`(decide(…) as Decision)`),
1997            // so the return type's names must be imported (and rebranded)
1998            // in step with the cast.
1999            if ctx.unit_kind == UnitKind::Context
2000                && ctx.imported_from_kind.get(&name.name) == Some(&UnitKind::Commons)
2001                && let Some(f) = commons.fns.get(&name.name)
2002            {
2003                collect_refs_in_typeref(&f.return_type, local_to_file, ctx, out);
2004            }
2005            for a in args {
2006                collect_refs_in_expr(a, local_to_file, commons, ctx, out);
2007            }
2008        }
2009        ExprKind::BinOp(_, l, r) => {
2010            collect_refs_in_expr(l, local_to_file, commons, ctx, out);
2011            collect_refs_in_expr(r, local_to_file, commons, ctx, out);
2012        }
2013        ExprKind::UnaryOp(_, i)
2014        | ExprKind::Paren(i)
2015        | ExprKind::Ok(i)
2016        | ExprKind::Err(i)
2017        | ExprKind::Some(i)
2018        | ExprKind::Question(i) => collect_refs_in_expr(i, local_to_file, commons, ctx, out),
2019        ExprKind::Block(b) => collect_refs_in_block(b, local_to_file, commons, ctx, out),
2020        ExprKind::If {
2021            cond,
2022            then_block,
2023            else_block,
2024        } => {
2025            collect_refs_in_expr(cond, local_to_file, commons, ctx, out);
2026            collect_refs_in_block(then_block, local_to_file, commons, ctx, out);
2027            collect_refs_in_block(else_block, local_to_file, commons, ctx, out);
2028        }
2029        ExprKind::ConstructorCall {
2030            type_name,
2031            method: _,
2032            args,
2033        } => {
2034            record_name_ref(&type_name.name, local_to_file, ctx, out);
2035            for a in args {
2036                collect_refs_in_expr(a, local_to_file, commons, ctx, out);
2037            }
2038        }
2039        ExprKind::RecordConstruction { type_name, fields } => {
2040            record_name_ref(&type_name.name, local_to_file, ctx, out);
2041            for f in fields {
2042                if let Some(v) = &f.value {
2043                    collect_refs_in_expr(v, local_to_file, commons, ctx, out);
2044                }
2045            }
2046        }
2047        ExprKind::FieldAccess { receiver, field: _ } => {
2048            // The bare-ident-as-type case (`TypeName.Variant`) — record the
2049            // name so we import the type.
2050            if let ExprKind::Ident(id) = &receiver.kind {
2051                record_name_ref(&id.name, local_to_file, ctx, out);
2052            } else {
2053                collect_refs_in_expr(receiver, local_to_file, commons, ctx, out);
2054            }
2055        }
2056        ExprKind::MethodCall {
2057            receiver,
2058            method: _,
2059            args,
2060            ..
2061        } => {
2062            if let ExprKind::Ident(id) = &receiver.kind {
2063                record_name_ref(&id.name, local_to_file, ctx, out);
2064            } else {
2065                collect_refs_in_expr(receiver, local_to_file, commons, ctx, out);
2066            }
2067            for a in args {
2068                collect_refs_in_expr(a, local_to_file, commons, ctx, out);
2069            }
2070        }
2071        ExprKind::Match { discriminant, arms } => {
2072            collect_refs_in_expr(discriminant, local_to_file, commons, ctx, out);
2073            for arm in arms {
2074                if let Pattern::Variant {
2075                    type_name: Some(tn),
2076                    ..
2077                } = &arm.pattern
2078                {
2079                    record_name_ref(&tn.name, local_to_file, ctx, out);
2080                }
2081                match &arm.body {
2082                    MatchBody::Expr(e) => collect_refs_in_expr(e, local_to_file, commons, ctx, out),
2083                    MatchBody::Block(b) => {
2084                        collect_refs_in_block(b, local_to_file, commons, ctx, out)
2085                    }
2086                }
2087            }
2088        }
2089        ExprKind::Is { value, pattern } => {
2090            collect_refs_in_expr(value, local_to_file, commons, ctx, out);
2091            if let Pattern::Variant {
2092                type_name: Some(tn),
2093                ..
2094            } = pattern.as_ref()
2095            {
2096                record_name_ref(&tn.name, local_to_file, ctx, out);
2097            }
2098        }
2099    }
2100}
2101
2102/// If `name` at `span` is a bare reference to a variant of a sum type (per
2103/// the checker's expression type), return the owning sum's name — the same
2104/// test the lowering uses to qualify it as `Type.Variant` (see the
2105/// `ExprKind::Ident` arm of `lower_expr_into`).
2106fn sum_owner_of_variant(
2107    name: &str,
2108    id: bynk_syntax::ast::ExprId,
2109    commons: &TypedCommons,
2110) -> Option<String> {
2111    if let Some(Ty::Named {
2112        kind: NamedKind::Sum,
2113        name: type_name,
2114        ..
2115    }) = commons.expr_ty(id).as_deref()
2116        && let Some(decl) = commons.types.get(type_name)
2117        && let TypeBody::Sum(s) = &decl.body
2118        && s.variants.iter().any(|v| v.name.name == name)
2119    {
2120        return Some(type_name.clone());
2121    }
2122    None
2123}
2124
2125fn record_name_ref(
2126    name: &str,
2127    local_to_file: &HashSet<String>,
2128    ctx: &EmitProjectCtx,
2129    out: &mut ExternalReferences,
2130) {
2131    if local_to_file.contains(name) {
2132        return;
2133    }
2134    // Imported from another commons?
2135    if let Some(commons_name) = ctx.imported_from.get(name) {
2136        out.by_commons
2137            .entry(commons_name.clone())
2138            .or_default()
2139            .insert(name.to_string());
2140        return;
2141    }
2142    // Sibling file in the same commons?
2143    if let Some(path) = ctx.file_decl_index.types.get(name)
2144        && path != &ctx.source_path
2145    {
2146        out.by_sibling
2147            .entry(path.clone())
2148            .or_default()
2149            .insert(name.to_string());
2150        return;
2151    }
2152    if let Some(path) = ctx.file_decl_index.fns.get(name)
2153        && path != &ctx.source_path
2154    {
2155        out.by_sibling
2156            .entry(path.clone())
2157            .or_default()
2158            .insert(name.to_string());
2159    }
2160}
2161
2162/// Emit `import * as <ns> from "..."` for each consumed context that
2163/// exposes services (so the consuming file can reference its `makeSurface`
2164/// return type and brand the cross-context call arguments).
2165fn emit_cross_context_namespace_imports(
2166    out: &mut String,
2167    commons: &TypedCommons,
2168    ctx: &EmitProjectCtx,
2169) {
2170    let info = &ctx.cross_context;
2171    // Consumed contexts that expose services (v0.6) plus, v0.15, those whose
2172    // capabilities this context references via `given B.Cap`.
2173    let mut needed: std::collections::BTreeSet<String> = info
2174        .consumed_services
2175        .iter()
2176        .filter(|(_, svcs)| !svcs.is_empty())
2177        .map(|(q, _)| q.clone())
2178        .collect();
2179    needed.extend(cross_context_cap_namespaces(commons, info));
2180    if needed.is_empty() {
2181        return;
2182    }
2183    let consumed_with_services: Vec<&String> = needed.iter().collect();
2184    for q in &consumed_with_services {
2185        // Pick the first known file path for the consumed context as the
2186        // import target. (The composition root lives in the consumed
2187        // context's directory; any of its files would work as an import
2188        // target since they're all in the same module namespace, but we
2189        // currently emit one file per .bynk source so a single import per
2190        // consumed name suffices for the surface contract.)
2191        let target_paths = ctx.imported_decl_paths.get(q.as_str());
2192        let target = target_paths
2193            .and_then(|m| m.values().next().cloned())
2194            .unwrap_or_else(|| {
2195                // No imported declaration pins the path (e.g. a capability-only
2196                // consumed context, v0.15). Fall back to the unit's own module:
2197                // its per-Worker handlers in workers mode, or its <segment>.bynk
2198                // source in bundle mode. v0.17: a consumed *adapter* is not a
2199                // Worker — its capability types live in its root module
2200                // (`<adapter>.ts`) in both targets.
2201                if ctx.consumed_adapters.contains(q.as_str()) {
2202                    let mut p = EmitProjectCtx::commons_path(q);
2203                    p.set_extension("bynk");
2204                    p
2205                } else {
2206                    match ctx.target {
2207                        BuildTarget::Workers => crate::project::worker_handlers_source_path(q),
2208                        BuildTarget::Bundle => {
2209                            let mut p = EmitProjectCtx::commons_path(q);
2210                            p.set_extension("bynk");
2211                            p
2212                        }
2213                    }
2214                }
2215            });
2216        let import =
2217            cross_commons_import_specifier_for_path(&ctx.source_path, &target, ctx.import_ext);
2218        let ns = qualified_to_ns(q);
2219        // #661: under `workers`, a consumed *context*'s module is imported for
2220        // its **types only** — the caller now generates its own codecs
2221        // (`emit_boundary_helpers`) and reaches the callee's types through this
2222        // alias in type position (`deps: { Clock: platform_time.Clock }`,
2223        // `Result<commerce_payment.AuthId, …>`). An `import type` is erased
2224        // outright, so the callee's *module* — and its provider implementation —
2225        // never enters the caller's Worker bundle. This does **not** apply to a
2226        // consumed *adapter* (its binding namespace, e.g. `tokens`, is a real
2227        // value import used by `compose.ts`) nor on `bundle` (contexts compile
2228        // together, and the value uses in `compose.ts` are legitimate).
2229        let type_only = matches!(ctx.target, BuildTarget::Workers)
2230            && !ctx.consumed_adapters.contains(q.as_str());
2231        let kw = if type_only { "import type" } else { "import" };
2232        writeln!(out, "{kw} * as {ns} from \"{import}\";").unwrap();
2233    }
2234    writeln!(out).unwrap();
2235}
2236
2237fn emit_project_imports(
2238    out: &mut String,
2239    commons: &TypedCommons,
2240    ctx: &EmitProjectCtx,
2241    refs: &ExternalReferences,
2242) {
2243    // Events track, slice 0 (spine #936): the bare event-type names this
2244    // context's own `from Events(E)` service headers name — see the
2245    // Workers type-only-import narrowing below.
2246    let subscribed_event_type_names: HashSet<String> = commons
2247        .commons
2248        .items
2249        .iter()
2250        .filter_map(|item| match item {
2251            CommonsItem::Service(s) => match &s.protocol {
2252                ServiceProtocol::Events {
2253                    event_type: TypeRef::Named(id),
2254                    ..
2255                } => Some(id.name.clone()),
2256                _ => None,
2257            },
2258            _ => None,
2259        })
2260        .collect();
2261    // Sibling imports: relative path within the same commons/context directory.
2262    let mut sibling_paths: Vec<(&PathBuf, &HashSet<String>)> = refs.by_sibling.iter().collect();
2263    sibling_paths.sort_by(|a, b| a.0.cmp(b.0));
2264    for (path, names) in sibling_paths {
2265        let import = sibling_import_specifier(&ctx.source_path, path, ctx.import_ext);
2266        let mut sorted: Vec<&String> = names.iter().collect();
2267        sorted.sort();
2268        let joined = sorted
2269            .iter()
2270            .map(|s| ts_ident(s))
2271            .collect::<Vec<_>>()
2272            .join(", ");
2273        writeln!(out, "import {{ {joined} }} from \"{import}\";").unwrap();
2274    }
2275    // Cross-unit imports: group by *target file path*.
2276    let mut unit_names: Vec<(&String, &HashSet<String>)> = refs.by_commons.iter().collect();
2277    unit_names.sort_by(|a, b| a.0.cmp(b.0));
2278    for (unit_name, names) in unit_names {
2279        let target_paths = ctx.imported_decl_paths.get(unit_name.as_str());
2280        let mut by_target: std::collections::BTreeMap<PathBuf, Vec<&String>> =
2281            std::collections::BTreeMap::new();
2282        for n in names {
2283            let path = target_paths
2284                .and_then(|p| p.get(n))
2285                .cloned()
2286                .unwrap_or_else(|| EmitProjectCtx::commons_path(unit_name));
2287            by_target.entry(path).or_default().push(n);
2288        }
2289        for (target, mut name_list) in by_target {
2290            name_list.sort();
2291            let import =
2292                cross_commons_import_specifier_for_path(&ctx.source_path, &target, ctx.import_ext);
2293            // For context units, aliase commons-source imports so we can emit
2294            // rebrand aliases of the same short name. Imports from consumed
2295            // contexts keep their original name. v0.20b: the rebrand applies
2296            // to *types* only — a `uses`-imported function (bynk.list's
2297            // `traverse`) is a value, imports plainly, and is never branded.
2298            let mut parts: Vec<String> = Vec::new();
2299            for n in &name_list {
2300                let from_kind = ctx.imported_from_kind.get(n.as_str()).copied();
2301                let is_subscribed_event_type = ctx.target == BuildTarget::Workers
2302                    && subscribed_event_type_names.contains(n.as_str());
2303                if ctx.unit_kind == UnitKind::Context
2304                    && from_kind == Some(UnitKind::Commons)
2305                    && commons.types.contains_key(n.as_str())
2306                {
2307                    parts.push(format!("{n} as __Commons{n}"));
2308                } else if is_subscribed_event_type {
2309                    // Events track, slice 0 (spine #936): under Workers, a
2310                    // context deploys as its own separate Worker script —
2311                    // there is no shared module graph to import a peer
2312                    // context's *value* across (the #661 hazard this
2313                    // mirrors: a caller generates its own codec rather than
2314                    // importing the callee's runtime code). `from
2315                    // Events(E)`'s `E` is the one plain named type crossing
2316                    // a context boundary directly by name (every other
2317                    // cross-context reference goes through a generated
2318                    // Service-Binding codec instead) — used only in type
2319                    // position (`e: E`), so this specific name is type-only.
2320                    // Narrowly scoped to event types specifically, not every
2321                    // cross-context import: a `type`/`enum` crossing via
2322                    // `uses`/`consumes` (e.g. `bynk`'s `Method`) is often
2323                    // used as a *value* too (`Method.Get`), which a blanket
2324                    // `import type` would wrongly break.
2325                    parts.push(format!("type {}", ts_ident(n)));
2326                } else {
2327                    parts.push(ts_ident(n));
2328                }
2329            }
2330            let joined = parts.join(", ");
2331            writeln!(out, "import {{ {joined} }} from \"{import}\";").unwrap();
2332        }
2333    }
2334    // #527: imports the DO-side agent-deps expressions need (binding modules,
2335    // other Workers' handlers). Precomputed by the project driver.
2336    for line in &ctx.extra_import_lines {
2337        writeln!(out, "{line}").unwrap();
2338    }
2339}
2340
2341/// Compute a relative import specifier from `from_source` (a `.bynk` path)
2342/// to `to_source` (another `.bynk` path), with `.bynk` rewritten to `.js`
2343/// for compatibility with NodeNext/strict TS resolution.
2344fn sibling_import_specifier(from_source: &Path, to_source: &Path, ext: ImportExt) -> String {
2345    let from_dir = from_source.parent().unwrap_or(Path::new(""));
2346    let target = to_source.with_extension(ext.as_str());
2347    let rel = relative_to(from_dir, &target);
2348    format!("./{}", ts_specifier(&rel))
2349}
2350
2351/// Render a path as a TypeScript module specifier: **always forward
2352/// slashes**. `Path::display()` uses the platform separator, and on Windows
2353/// that emitted `import ... from "./commerce\orders.js"` — broken ESM
2354/// output, caught by the first CI matrix run on windows-latest.
2355pub(crate) fn ts_specifier(p: &Path) -> String {
2356    p.to_string_lossy().replace('\\', "/")
2357}
2358
2359/// Compute a relative import specifier from this file's location to a
2360/// specific source file in another commons. `target_source` is the project-
2361/// relative path of the target `.bynk` file. The result is suitable for
2362/// `import { ... } from "..."` in NodeNext/strict TypeScript.
2363pub(crate) fn cross_commons_import_specifier_for_path(
2364    from_source: &Path,
2365    target_source: &Path,
2366    ext: ImportExt,
2367) -> String {
2368    let from_dir = from_source.parent().unwrap_or(Path::new(""));
2369    let target = target_source.with_extension(ext.as_str());
2370    let rel = relative_to(from_dir, &target);
2371    let display = ts_specifier(&rel);
2372    if display.starts_with("../") || display.starts_with("./") {
2373        display
2374    } else {
2375        format!("./{display}")
2376    }
2377}
2378
2379/// Compute `target` as a path relative to `from`. Handles parent traversal
2380/// (`..`) for cases where `target` lives in a sibling directory.
2381fn relative_to(from: &Path, target: &Path) -> PathBuf {
2382    use std::path::Component as C;
2383    let f_comps: Vec<C> = from.components().collect();
2384    let t_comps: Vec<C> = target.components().collect();
2385    let mut shared = 0;
2386    while shared < f_comps.len() && shared < t_comps.len() && f_comps[shared] == t_comps[shared] {
2387        shared += 1;
2388    }
2389    let mut out = PathBuf::new();
2390    for _ in shared..f_comps.len() {
2391        out.push("..");
2392    }
2393    for c in &t_comps[shared..] {
2394        out.push(c.as_os_str());
2395    }
2396    if out.as_os_str().is_empty() {
2397        out.push(".");
2398    }
2399    out
2400}
2401
2402fn write_header(out: &mut String, commons: &TypedCommons, ctx: &EmitProjectCtx) {
2403    writeln!(out, "// Generated by bynkc — do not edit by hand.").unwrap();
2404    let kind = match ctx.unit_kind {
2405        UnitKind::Commons => "commons",
2406        UnitKind::Context => "context",
2407        UnitKind::Test => "test",
2408        UnitKind::Integration => "integration test",
2409        UnitKind::Adapter => "adapter",
2410    };
2411    writeln!(out, "// {kind} {}", commons.commons.name.joined()).unwrap();
2412    writeln!(out).unwrap();
2413    if !commons.commons.items.is_empty() {
2414        let runtime_import = runtime_import_for(&ctx.source_path, ctx.import_ext);
2415        let has_agent = commons
2416            .commons
2417            .items
2418            .iter()
2419            .any(|i| matches!(i, CommonsItem::Agent(_)));
2420        // v0.80: a file with any agent invariant imports the `invariantViolation`
2421        // fault helper used by the generated `commitState` gate. v0.116: a step
2422        // invariant (`transition`) uses the same fault helper, so a transition-only
2423        // agent must import it too.
2424        let has_agent_invariants = commons.commons.items.iter().any(|i| match i {
2425            CommonsItem::Agent(a) => !a.invariants.is_empty() || !a.transitions.is_empty(),
2426            _ => false,
2427        });
2428        // v0.153 (ADR 0177): a service HTTP handler imports `HttpResult`, and so
2429        // does any *free* `fn` / provider / capability whose signature names it
2430        // (the `?`-Option lift makes a bare `fn -> HttpResult[T]` emit
2431        // `HttpResult.NotFound`) — the structural scan covers both, closing the
2432        // free-fn gap the single-file path already handles.
2433        let has_http = commons.commons.items.iter().any(|i| match i {
2434            CommonsItem::Service(s) => s
2435                .handlers
2436                .iter()
2437                .any(|h| matches!(h.kind, HandlerKind::Http { .. })),
2438            _ => false,
2439        }) || file_mentions_http_result(commons);
2440        // A `from queue` `on message` is the queue consumer (imports `QueueResult`);
2441        // a `from websocket` `on message` (slice 3b-iii) is the inbound handler and
2442        // is not a queue concern.
2443        let has_queue = commons.commons.items.iter().any(|i| match i {
2444            CommonsItem::Service(s) => {
2445                !matches!(s.protocol, ServiceProtocol::WebSocket { .. })
2446                    && s.handlers
2447                        .iter()
2448                        .any(|h| matches!(h.kind, HandlerKind::Message))
2449            }
2450            _ => false,
2451        });
2452        let workers = matches!(ctx.target, BuildTarget::Workers);
2453        let mut parts: Vec<&str> = vec![
2454            "Ok",
2455            "Err",
2456            "Some",
2457            "None",
2458            "type Result",
2459            "type Option",
2460            "type ValidationError",
2461        ];
2462        // v0.22b: the codec types are imported only when the file uses the
2463        // `Json` codec (or names `JsonError` in a signature) — keeping every
2464        // non-codec module's header byte-identical to v0.22a.
2465        let uses_codec = !collect_json_codec_roots(commons).is_empty();
2466        let mentions_json_error = file_mentions_json_error(commons);
2467        if uses_codec || mentions_json_error {
2468            parts.push("type JsonError");
2469        }
2470        // v0.102: a file naming `Connection[F]` imports the runtime interface.
2471        if file_mentions_connection(commons) {
2472            parts.push("type Connection");
2473        }
2474        if has_agent {
2475            // v0.9.2: agent-declaring files lower instantiation through the
2476            // `makeAgent` helper and a per-agent `StateRegistry`, and the
2477            // generated factory's signature names `DurableObjectNamespace`.
2478            parts.push("type DurableObjectState");
2479            parts.push("type DurableObjectNamespace");
2480            parts.push("StateRegistry");
2481            parts.push("makeAgent");
2482        }
2483        if has_agent_invariants {
2484            parts.push("invariantViolation");
2485        }
2486        // Events track, slice 0 (spine #936): an agent whose own handler body
2487        // emits directly needs its Workers-mode DO fetch dispatcher to
2488        // rebuild `deps.__eventsDispatch` from `env.EVENTS_FANOUT` (mirrors
2489        // the `#527` `given`-provider rebuild — see `emit_agent`) — a
2490        // function does not survive the JSON wire any better than a
2491        // provider does.
2492        let has_agent_uses_emit = workers
2493            && commons.commons.items.iter().any(|i| match i {
2494                CommonsItem::Agent(a) => a
2495                    .handlers
2496                    .iter()
2497                    .any(|h| block_uses_emit(&h.body, &commons.callees)),
2498                _ => false,
2499            });
2500        if has_agent_uses_emit {
2501            parts.push("dispatchToEventsFanout");
2502        }
2503        // v0.96 (ADR 0124): an agent whose load-time validation gate fires imports
2504        // the `rehydrationViolation` fault helper.
2505        let has_rehydration_gate = commons.commons.items.iter().any(|i| match i {
2506            CommonsItem::Agent(a) => emit::agent_needs_rehydrate(a, &commons.types),
2507            _ => false,
2508        });
2509        if has_rehydration_gate {
2510            parts.push("rehydrationViolation");
2511        }
2512        // v0.104/v0.105 (real-time track slice 3b): on Workers a `store Map[K,
2513        // Connection]` persists the connection id; its entry ops re-resolve the live
2514        // socket via `resolveConnection` and read a connection's id via `connIdOf`.
2515        if workers
2516            && commons.commons.items.iter().any(|i| match i {
2517                CommonsItem::Agent(a) => emit::agent_has_held_storage(a),
2518                _ => false,
2519            })
2520        {
2521            parts.push("resolveConnection");
2522            parts.push("connIdOf");
2523        }
2524        // v0.104/v0.105 (real-time track slice 3b): on Workers a context hosting a
2525        // `from websocket` `on open` accepts the socket inside its Durable Object via
2526        // the hibernatable API — `acceptHibernatableConnection` (accept + tag + wrap),
2527        // a `WebSocketPair`, and the `101` upgrade response. (The service and its
2528        // hosting agent share the one Worker module, so these land in one
2529        // `handlers.ts`.)
2530        let hosts_ws_open = commons.commons.items.iter().any(|i| match i {
2531            CommonsItem::Service(s) => s
2532                .handlers
2533                .iter()
2534                .any(|h| matches!(h.kind, HandlerKind::Open)),
2535            _ => false,
2536        });
2537        if workers && hosts_ws_open {
2538            parts.push("acceptHibernatableConnection");
2539            parts.push("newWebSocketPair");
2540            parts.push("webSocketUpgradeResponse");
2541        }
2542        // v0.106 (slice 3b-iii): a context with an inbound/close handler re-wraps
2543        // the firing socket as a `WorkersConnection` in `webSocketMessage`/
2544        // `webSocketClose`.
2545        let hosts_ws_inbound = commons.commons.items.iter().any(|i| match i {
2546            CommonsItem::Service(s) => {
2547                matches!(s.protocol, ServiceProtocol::WebSocket { .. })
2548                    && s.handlers
2549                        .iter()
2550                        .any(|h| matches!(h.kind, HandlerKind::Message | HandlerKind::Close))
2551            }
2552            _ => false,
2553        });
2554        if workers && hosts_ws_inbound {
2555            parts.push("WorkersConnection");
2556        }
2557        if has_http {
2558            // `HttpResult` is both a value (the constructor namespace) and a
2559            // type (the discriminated union). A bare named import brings both
2560            // in — `type HttpResult` would duplicate the identifier.
2561            parts.push(HTTP_RESULT);
2562        }
2563        if has_queue {
2564            // v0.44: `QueueResult` is both a value (the verdict namespace) and a
2565            // type; a bare named import brings both in.
2566            parts.push(QUEUE_RESULT);
2567        }
2568        if workers {
2569            parts.push("type JsonValue");
2570            parts.push("type BoundaryError");
2571            parts.push("type ServiceBinding");
2572            parts.push("callService");
2573            parts.push("boundaryError");
2574        } else if uses_codec || has_agent {
2575            // v0.22b: the bundle-mode codec helpers reference JsonValue and
2576            // BoundaryError. v0.96 (ADR 0124): so do an agent's emitted
2577            // rehydration deserialisers and the gate's inline base checks — the
2578            // boundary helpers now emit on bundle too (for the rehydration gate).
2579            parts.push("type JsonValue");
2580            parts.push("type BoundaryError");
2581        }
2582        writeln!(
2583            out,
2584            "import {{ {} }} from \"{runtime_import}\";",
2585            parts.join(", ")
2586        )
2587        .unwrap();
2588        writeln!(out).unwrap();
2589    }
2590}
2591
2592/// Variant of write_header for single-file (no project context) emission.
2593fn write_header_single(
2594    out: &mut String,
2595    commons: &TypedCommons,
2596    uses_bytes: bool,
2597    uses_http: bool,
2598) {
2599    writeln!(out, "// Generated by bynkc — do not edit by hand.").unwrap();
2600    writeln!(out, "// commons {}", commons.commons.name.joined()).unwrap();
2601    writeln!(out).unwrap();
2602    if !commons.commons.items.is_empty() {
2603        // v0.22b: codec imports only when the file uses the `Json` codec.
2604        let uses_codec = !collect_json_codec_roots(commons).is_empty();
2605        let codec_imports = if uses_codec {
2606            ", type JsonError, type JsonValue, type BoundaryError"
2607        } else if file_mentions_json_error(commons) {
2608            ", type JsonError"
2609        } else {
2610            ""
2611        };
2612        // v0.110 (ADR 0142): the `Bytes` runtime helpers, imported only when a
2613        // `Bytes` value is constructed or compared in the body.
2614        let bytes_imports = if uses_bytes {
2615            BYTES_RUNTIME_IMPORTS
2616        } else {
2617            ""
2618        };
2619        // v0.153 (ADR 0177): `HttpResult` is a value (its variant namespace) and
2620        // a type, so it imports without a `type` prefix — one binding serves
2621        // both `HttpResult.NotFound` and the `HttpResult<T>` annotation.
2622        let http_imports = if uses_http { ", HttpResult" } else { "" };
2623        writeln!(
2624            out,
2625            "import {{ Ok, Err, Some, None, type Result, type Option, type ValidationError{codec_imports}{bytes_imports}{http_imports} }} from \"./runtime.js\";",
2626        )
2627        .unwrap();
2628        writeln!(out).unwrap();
2629    }
2630}
2631
2632/// v0.110 (ADR 0142): the `Bytes` runtime helpers, appended to a module's
2633/// import list when the emitted body references them. `bytesEqual` backs `==`;
2634/// the base64/UTF-8 helpers back the kernel and codec.
2635pub(crate) const BYTES_RUNTIME_IMPORTS: &str =
2636    ", __bynkBytesEqual, __bynkBytesToBase64, __bynkBytesFromBase64, __bynkBytesDecodeUtf8";
2637
2638/// message-bundles slice 3 (#878, Decision G): the ICU-formatting runtime
2639/// helpers, appended to a module's import list when an emitted `messages`
2640/// bundle's `render` references any of them (`emit_icu_placeholder`,
2641/// `bynk-emit/src/emitter/emit.rs`).
2642const MESSAGES_RUNTIME_IMPORTS: &str = ", selectPluralArm, formatIcuNumber, formatIcuDate";
2643
2644/// #914: the names an **inlined** boundary deserialiser builds directly, appended
2645/// to the import list of a module that curates its own — a Worker's `compose.ts`
2646/// and the test-scaffold modules. Every other module imports `Ok`/`Err`/`Result`
2647/// unconditionally, so most of this is inert there and never applied.
2648///
2649/// A codec for a *named* type delegates (`handlers.deserialise_Order(…)`) and needs
2650/// none of these; one for a base type or a `Bytes` inlines the construction. Two
2651/// arms — `Unit` and the runtime-owned error types — additionally annotate the
2652/// result (`Ok(undefined) as Result<void, BoundaryError>`), which `compose.ts`'s
2653/// structural list never carries; `Result` is in the group for those. The dedupe
2654/// in [`inject_runtime_imports`] makes it free wherever it is already imported.
2655pub(crate) const BOUNDARY_CODEC_RUNTIME_IMPORTS: &str =
2656    ", Ok, Err, type Result, type BoundaryError";
2657
2658/// #914: the names the `Json.decode[T]` wrapper puts in the module — its own
2659/// `Result<T, JsonError>` signature and the `JsonValue` it parses into
2660/// (`lower_json_codec_call`, `bynk-emit/src/emitter/lower.rs`).
2661///
2662/// A sibling group rather than part of [`BOUNDARY_CODEC_RUNTIME_IMPORTS`]: the
2663/// producer is the wrapper, not the codec, and it names these whichever arm the
2664/// inner deserialiser takes — including the delegating ones, which set no
2665/// boundary-codec flag at all. `Json.encode` needs nothing for its own wrapper
2666/// text either (it lowers to a bare `JSON.stringify`).
2667///
2668/// The delegating arm — `Json.decode[SomeRecord]` / `Json.encode(someRecord)`
2669/// — used to be broken in a test-scaffold module for an unrelated reason
2670/// (issue #917): the call lowers to a bare `deserialise_SomeRecord(…)` /
2671/// `serialise_SomeRecord(…)`, and no such codec was emitted anywhere — the
2672/// unit module exports the record's interface and no codec of its own. Fixed
2673/// by generating the test module's *own* closure for every root a case body's
2674/// `Json` call reaches for (`RuntimeUse::note_json_codec_root`, drained by
2675/// `tests_emit.rs`'s `emit_test_module`), namespace-qualifying the TS type
2676/// positions through the target/`uses` unit's own namespace import
2677/// (`RuntimeUse::json_codec_qual`) — the same caller-generates-its-own-codec
2678/// pattern `emit_consumed_context_helpers` uses for a workers cross-context
2679/// caller's consumed-boundary types (#661). See
2680/// `918_json_decode_in_test_case` (base type, delegation-free) and
2681/// `919_json_decode_named_record_in_test_case` (named record, delegating).
2682pub(crate) const JSON_CODEC_RUNTIME_IMPORTS: &str =
2683    ", Ok, Err, type Result, type JsonValue, type JsonError";
2684
2685/// Emit the commons-level doc block (if any) at the current position.
2686fn write_commons_doc(out: &mut String, commons: &TypedCommons) {
2687    if let Some(doc) = &commons.commons.documentation {
2688        emit_doc_block(out, Some(doc), 0);
2689        writeln!(out).unwrap();
2690    }
2691}
2692
2693/// The module-level state-registry constant name for an agent class.
2694fn agent_registry_name(agent: &str) -> String {
2695    format!("__{agent}Registry")
2696}
2697
2698/// The exported agent-construction factory name for an agent class.
2699pub(crate) fn agent_factory_name(agent: &str) -> String {
2700    format!("__make{agent}")
2701}
2702
2703/// Lowering state that is genuinely **module-invariant**: the same value at
2704/// every body-lowering site within one emitted module, and never written by the
2705/// recursive lowering itself. Grouped out of [`LowerCtx`] so a new lowering kind
2706/// inherits the whole set wholesale instead of re-deriving each default.
2707///
2708/// Nothing the lowering mutates *per body* may move in here — see the
2709/// scratch-state fields at the bottom of [`LowerCtx`]. A `ModuleCtx` is still
2710/// built fresh alongside each `LowerCtx` today, but filing a per-body counter
2711/// under a name that says "module" is exactly how such state starts leaking
2712/// between handler bodies.
2713pub(crate) struct ModuleCtx<'a> {
2714    /// Typed-commons handle (used to look up receiver types for method-call
2715    /// UFCS lowering).
2716    commons: &'a TypedCommons,
2717    /// Cross-context info for v0.6 cross-context call lowering.
2718    cross_context: &'a bynk_check::resolver::CrossContextInfo,
2719    /// The emitted module's conditional-runtime-helper accumulator.
2720    ///
2721    /// The `Bytes` lowerings (kernel, `==`, base64 codec) call
2722    /// [`RuntimeUse::note_bytes`] through this, so the module's import line is
2723    /// decided from what lowering actually emitted rather than by scanning the
2724    /// generated text for the helper's own name.
2725    ///
2726    /// Required rather than optional: a missing accumulator would make
2727    /// `note_bytes` a silent no-op, which is exactly the failure this replaces —
2728    /// a module that references `__bynkBytesEqual` without importing it. A
2729    /// lowering whose imports are decided elsewhere (the test scaffolds) owns a
2730    /// throwaway one, which reads as the deliberate choice it is.
2731    runtime_use: &'a RuntimeUse,
2732    /// v0.8 build target. In workers mode cross-context calls lower to
2733    /// `callService(...)` instead of `deps.surface.<key>.<method>(...)`.
2734    target: BuildTarget,
2735    /// #527: agent → method → the method's `given` caps (mirrors
2736    /// [`crate::project::EmitProjectCtx::agent_method_givens`]). Consulted by
2737    /// the agent-call lowering to record capability requirements.
2738    agent_method_givens: HashMap<String, HashMap<String, Vec<crate::ir::CapRefIr>>>,
2739    /// Events slice 3b (#978): each locally-declared event's resolved
2740    /// `@schema(N)` version (mirrors
2741    /// [`crate::project::EmitProjectCtx::event_schema_versions`]). Default-
2742    /// empty like `agent_method_givens`, not a required constructor
2743    /// parameter like `runtime_use` — a miss here degrades to `schemaVersion:
2744    /// 1`, exactly every event's behaviour before this map existed, not a
2745    /// hard failure the way a missing `runtime_use` would be.
2746    event_schema_versions: HashMap<String, i64>,
2747    /// #527: type names this context *rebrands* (`uses`-imported commons
2748    /// types re-exported as `T & { __ctxBrand }`). Drives brand-assertion
2749    /// casts where unbranded commons values meet branded local positions.
2750    rebranded_types: HashSet<String>,
2751    /// #527: fn names imported from a commons. Such a fn's signature uses the
2752    /// *unbranded* commons types, so calls whose return mentions a rebranded
2753    /// type are asserted back into the local (branded) namespace.
2754    commons_imported_fns: HashSet<String>,
2755    /// #934: true when the unit being emitted is the reserved first-party
2756    /// `bynk` adapter itself. `bynk` is a reserved namespace, so a capability
2757    /// literally named `Idempotency` declared *in this unit* is unambiguously
2758    /// the real one — used alongside `CrossContextInfo::flattened_caps` (the
2759    /// consumed-from-elsewhere case) to confirm a flattened `Idempotency`
2760    /// call is genuinely first-party before scoping its key, not a same-named
2761    /// capability some other adapter or context happens to declare.
2762    in_bynk_unit: bool,
2763}
2764
2765impl<'a> ModuleCtx<'a> {
2766    fn new(
2767        commons: &'a TypedCommons,
2768        cross_context: &'a bynk_check::resolver::CrossContextInfo,
2769        runtime_use: &'a RuntimeUse,
2770    ) -> Self {
2771        Self {
2772            commons,
2773            cross_context,
2774            runtime_use,
2775            target: BuildTarget::Bundle,
2776            agent_method_givens: HashMap::new(),
2777            event_schema_versions: HashMap::new(),
2778            rebranded_types: HashSet::new(),
2779            commons_imported_fns: HashSet::new(),
2780            in_bynk_unit: false,
2781        }
2782    }
2783
2784    /// #527: derive which imported names this context rebrands and which fns
2785    /// come from a commons (and so speak the unbranded types). Mirrors the
2786    /// alias predicate in `emit_project_imports`.
2787    pub(crate) fn set_rebrand_info(
2788        &mut self,
2789        commons: &TypedCommons,
2790        ctx: &crate::project::EmitProjectCtx,
2791    ) {
2792        if ctx.unit_kind != UnitKind::Context {
2793            return;
2794        }
2795        for (name, kind) in &ctx.imported_from_kind {
2796            if *kind != UnitKind::Commons {
2797                continue;
2798            }
2799            if commons.types.contains_key(name) {
2800                self.rebranded_types.insert(name.clone());
2801            } else if commons.fns.contains_key(name) {
2802                self.commons_imported_fns.insert(name.clone());
2803            }
2804        }
2805    }
2806}
2807
2808/// The lowering state shared by the four **capability-bearing** body kinds — a
2809/// service handler, a composed provider op, an agent handler, and a websocket
2810/// lifecycle DO method. Every other kind carries no `HandlerShared` at all, and
2811/// the [`LowerCtx`] accessors below hand those kinds the same defaults the flat
2812/// struct used to give them (an empty capability set, no scope, `deps`).
2813pub(crate) struct HandlerShared {
2814    /// Names of capabilities in scope as `given C1, C2, ...`. Used to lower
2815    /// `Capability.op(args)` calls to `deps.Capability.op(args)`.
2816    capabilities: HashSet<String>,
2817    /// #934: the calling handler's own qualified name (`<unit>.<service or
2818    /// agent>.<handler>`, e.g. `shop.reserve.ordering.call`). Read only by the
2819    /// `Idempotency.dedup`/`remember` lowering, which prefixes the
2820    /// developer-supplied key with it so two unrelated handlers using the same
2821    /// literal key never collide (design/tracks/idempotency-capability.md
2822    /// §3.4). `None` anywhere a capability call cannot occur (a plain method, a
2823    /// free fn, an invariant/transition predicate, a static field initialiser) —
2824    /// those kinds carry no `HandlerShared`, and the accessor reports `None`.
2825    handler_scope: Option<String>,
2826    /// Events track, slice 2 (spine #936): the qualified name of the unit
2827    /// this body is emitted into (`ctx.commons_name`), read by the
2828    /// `Events.emit[E](event)` lowering to mint the envelope's
2829    /// `publisherId`. Context-scoped rather than agent-scoped: `Events.emit`
2830    /// is legal from a plain, keyless service handler with no agent
2831    /// instance to report, so this is the only identity available
2832    /// uniformly at every legal emission site (an amendment to
2833    /// design/bynk-design-notes.md §7's "the publisher is the emitting
2834    /// agent" framing — see the events-envelope ADR). Always populated
2835    /// alongside `handler_scope` at every construction site; never `None`
2836    /// in practice for a body that could contain an `Events.emit` call.
2837    owning_context: String,
2838    /// v0.12: the receiver expression a capability call resolves against —
2839    /// `deps` in a handler body, `this.deps` in a composed provider body.
2840    cap_deps_expr: String,
2841    /// True if the current handler made at least one cross-context call
2842    /// (drives whether `deps` gets a `surface` field type).
2843    cross_context_used: bool,
2844    /// v0.9.2: set when the body instantiates a local agent. In workers mode
2845    /// this drives `env` (carrying the DO namespaces) into the handler's deps
2846    /// type so the agent factory can reach its Durable Object binding.
2847    agents_instantiated: bool,
2848    /// #527: capabilities required by agent methods this body calls, keyed by
2849    /// deps key. After body lowering these widen the handler's deps *type* to
2850    /// match the runtime value compose builds (which always carried them).
2851    agent_given_caps_used: std::collections::BTreeMap<String, crate::ir::CapRefIr>,
2852}
2853
2854impl Default for HandlerShared {
2855    fn default() -> Self {
2856        Self {
2857            capabilities: HashSet::new(),
2858            handler_scope: None,
2859            owning_context: String::new(),
2860            cap_deps_expr: "deps".to_string(),
2861            cross_context_used: false,
2862            agents_instantiated: false,
2863            agent_given_caps_used: std::collections::BTreeMap::new(),
2864        }
2865    }
2866}
2867
2868/// The lowering state shared by the three **generated test-scaffold** body
2869/// kinds — a `stub`/`where`/`requires` predicate value, a unit test case, and an
2870/// integration test case.
2871#[derive(Default)]
2872pub(crate) struct TestShared {
2873    /// True when lowering **any** generated test-scaffold body — distinct from
2874    /// `assert_loc`, which is only ever `Some` for the two real `case` bodies
2875    /// and carries an unrelated payload (a diagnostic location). Kept as its own
2876    /// field (Locale capability track, slice 1, #844 review) rather than
2877    /// overloading `assert_loc.is_some()`, since that conflated "has a location"
2878    /// with "is test scaffolding" for a caller that has no location to give.
2879    test_scaffold: bool,
2880    /// v0.59: the source text and project-relative path of the file the body
2881    /// came from, so an `assert` can emit a real `path:line:col` location (for
2882    /// `--format json` click-through) rather than a bare byte offset. Stays
2883    /// `None` for a predicate scaffold, which emits no `assert`.
2884    assert_loc: Option<AssertLoc>,
2885}
2886
2887/// The `store`-agent sub-state of an [`BodyMode::AgentHandler`] body: present
2888/// only when the hosting agent is a `store` agent, absent for a plain
2889/// state-record agent (whose handler reads `currentState`/`self.state` instead).
2890pub(crate) struct AgentStoreState {
2891    /// v0.81 (storage track): the name of the mutable working-state variable
2892    /// (`__state`) and the set of `Cell` field names. A bare `Cell` read lowers
2893    /// to `<var>.<cell>`, and a `cell := v` write lowers to `<var>.<cell> = <v>`
2894    /// — read-your-writes via the in-memory record, flushed once at handler end
2895    /// (ADR 0109).
2896    state: (String, HashSet<String>),
2897    /// v0.82 (ADR 0110): the agent's `store` `Map` field names. A method call
2898    /// whose receiver is one lowers to an entry operation over `__state.<map>`
2899    /// (a JSON-serialisable `Record<string, V>`), staged in the working record
2900    /// and flushed at commit like any other state field.
2901    maps: HashSet<String>,
2902    /// v0.83: the agent's `store` `Set` field names. A method call whose
2903    /// receiver is one lowers to an entry operation over `__state.<set>` (a
2904    /// `Record<string, boolean>`), staged in the working record.
2905    sets: HashSet<String>,
2906    /// v0.87 (ADR 0113): the agent's `store` `Cache` fields (name → ttl millis).
2907    /// A method call whose receiver is one lowers to an entry op over
2908    /// `__state.<cache>` (a `Record<string, { v, exp }>`), applying TTL expiry
2909    /// against the injected `Clock`.
2910    caches: HashMap<String, i64>,
2911    /// v0.95 (ADR 0121): the agent's `store` `Log` fields (name → optional
2912    /// `@retain` millis). `<log>.append` pushes `{ t: now(), v }` to
2913    /// `__state.<log>` (an array) and prunes past the retain horizon; the
2914    /// time-window roots / builders lower to a query pipeline over the array.
2915    logs: HashMap<String, Option<i64>>,
2916    /// v0.93 (ADR 0118): the agent's `@indexed` secondary indexes (map name →
2917    /// the value-record fields indexed on). A mutating op on the map maintains a
2918    /// sibling posting-list `Record<string, string[]>` per field (`<map>__idx_<f>`);
2919    /// an equality `filter` on an indexed field routes to a posting lookup.
2920    indexes: HashMap<String, Vec<String>>,
2921    /// v0.104/v0.105 (real-time track slice 3b): the agent's held `store Map[K,
2922    /// Connection]` fields (name → the connection's **frame type** `F`, e.g.
2923    /// `ServerFrame`). On Workers these persist `K → connId` in the durable state
2924    /// record; a method call whose receiver is one lowers to an entry op over
2925    /// `__state.<map>` (the connId record) with `connIdOf`/`resolveConnection<F>` —
2926    /// not the plain `Record<string, V>` ops (held maps are excluded from
2927    /// [`AgentStoreState::maps`]).
2928    held_maps: HashMap<String, String>,
2929}
2930
2931/// What [`LowerCtx`] is lowering *right now*. One variant per real body-emission
2932/// site; each carries exactly the state that site populates and nothing else, so
2933/// "not applicable to this kind" is expressed in the type rather than left
2934/// indistinguishable from "deliberately defaulted".
2935pub(crate) enum BodyMode {
2936    /// A type's method body (`emit_method`).
2937    Method,
2938    /// A free function body (`emit_free_fn`).
2939    FreeFn,
2940    /// An agent field's static initialiser expression (`emit_agent`).
2941    StaticInit,
2942    /// v0.80: an agent invariant predicate. Carries the name of the
2943    /// proposed-state variable (the `commitState` parameter) and the set of
2944    /// state field names — a bare ident matching a state field lowers to
2945    /// `<var>.<field>`, since invariants read state fields directly (§14).
2946    Invariant {
2947        name: String,
2948        fields: HashSet<String>,
2949    },
2950    /// v0.116 (testing track slice 4): a `transition` predicate. Carries the JS
2951    /// names bound to the contextual `old` and `new` state records. The Bynk
2952    /// identifiers `old`/`new` lower to these (`new` is a JS reserved word, so
2953    /// both are renamed), and field access `old.<field>` reads off the `old`
2954    /// record.
2955    Transition { old: String, new: String },
2956    /// A service handler body (`emit_service`).
2957    ServiceHandler {
2958        handler: HandlerShared,
2959        /// v0.47: the `by` binder whose `.identity` is threaded through `deps`
2960        /// (so `<binder>.identity` lowers to `deps.identity` rather than the
2961        /// unit-value `undefined`).
2962        deps_identity_binder: Option<String>,
2963        /// v0.52: when lowering a multi-actor sum handler body, the `by` binder
2964        /// that names the resolved-actor value (threaded through `deps`, so the
2965        /// binder ident lowers to `deps.who` — the tagged union the body
2966        /// `match`es).
2967        actor_sum_binder: Option<String>,
2968    },
2969    /// A composed provider's operation body (`emit_provider`).
2970    ProviderOp { handler: HandlerShared },
2971    /// An agent handler body (`emit_agent`).
2972    AgentHandler {
2973        handler: HandlerShared,
2974        /// True when lowering an agent handler body. Used to rewrite
2975        /// `self.<keyField>` access into the appropriate local.
2976        in_agent_handler: bool,
2977        /// The name of the agent's `key id` field (so `self.<id>` resolves).
2978        agent_key_field: Option<String>,
2979        /// The `store`-agent working-record state, when the hosting agent is a
2980        /// `store` agent. Boxed: it is by far the largest payload in this enum,
2981        /// and every other body kind would otherwise pay for it.
2982        store: Option<Box<AgentStoreState>>,
2983    },
2984    /// A websocket lifecycle method on the hosting Durable Object
2985    /// (`emit_ws_do_method`).
2986    WsDoMethod {
2987        handler: HandlerShared,
2988        /// v0.47: as [`BodyMode::ServiceHandler::deps_identity_binder`].
2989        deps_identity_binder: Option<String>,
2990        /// v0.104 (real-time track slice 3b): when lowering a `from websocket`
2991        /// `on open` body **into its hosting Durable Object** (the agent the
2992        /// upgrade transfers the connection to), the name of that agent. A
2993        /// transfer call `<Agent>(<key>).method(args)` whose `<Agent>` is this
2994        /// self-agent lowers to a direct `this.method(args, deps)` self-call
2995        /// rather than the cross-instance `__make<Agent>(key)` factory — the
2996        /// connection is already in this DO, so it never crosses an RPC boundary
2997        /// (DECISION A).
2998        ws_self_agent: Option<String>,
2999    },
3000    /// A `stub`/`where`/`requires` predicate value lowered via
3001    /// `lower_block_to_async_body` — test/property/contract scaffolding, never a
3002    /// real production provider body.
3003    PredicateScaffold { test: TestShared },
3004    /// A unit test `case` body (`lower_test_case_body`).
3005    TestCase {
3006        test: TestShared,
3007        /// v0.117 (testing track slice 5): the name of the recorded-call trace
3008        /// object (`__obs`), over which an observation (`Cap.op called …`) and
3009        /// `trace(Cap.op)` are lowered.
3010        observation_trace: Option<String>,
3011        /// v0.7: the target context's local service names. A `service.call(args)`
3012        /// or `service(args)` invocation where `service` is in this set lowers to
3013        /// `<service>.call(args, deps)` so the test wires its `deps` through.
3014        test_services: HashSet<String>,
3015        /// v0.182 (#664): the ordered handler kinds of each test service, so a
3016        /// cron (`svc.schedule("…")`) or queue (`svc.message(m)`) address can
3017        /// recover the position index the emitted key encodes (`cron_<svc>_<i>` /
3018        /// `queue_…`). http keys are a pure function of verb + path and need no
3019        /// lookup here.
3020        test_service_handlers: HashMap<String, Vec<bynk_syntax::ast::HandlerKind>>,
3021    },
3022    /// An integration test `case` body (`lower_integration_case_body`).
3023    IntegrationCase {
3024        test: TestShared,
3025        /// v0.182 (Slice B, #667): the target's http service names. An http
3026        /// address on one of these lowers to a driver call
3027        /// (`__sysdrive_<svc>_<key>(args, sub)`) that drives a real
3028        /// `worker.fetch` with a signed credential, instead of the unit-tier
3029        /// direct handler call. Empty at the unit tier.
3030        system_http_services: std::collections::HashSet<String>,
3031        /// #707: the declared `(service, method, path)` http routes of the system
3032        /// target. A `(method, path)` call whose method is absent here but whose
3033        /// path is present is a **wrong-method** call — it drives the `405`
3034        /// fall-through through the generic `__sysdrive_wrongmethod_<svc>` driver.
3035        system_http_routes: std::collections::HashSet<(String, String, String)>,
3036        /// #708: for each declared `(service, method, path)` route that has a
3037        /// body param, the body's zero-based position among the route's
3038        /// positional call args (i.e. within `args[1..]`, matching handler-param
3039        /// declaration order) and its declared type. The raw driver
3040        /// (`__sysdrive_raw_*`, Slice C) forwards every slot as a `string`; a
3041        /// `Wire(…)` arg already lowers to that raw string, but a *typed* arg
3042        /// mixed into the same call must be converted: the body slot serialises
3043        /// through the same wire codec the typed driver uses
3044        /// (`JSON.stringify(serialise_expr_via(...))`), any other (path) slot
3045        /// just coerces via `String(...)`. Absent for a bodyless route.
3046        system_http_route_body:
3047            HashMap<(String, String, String), (usize, bynk_syntax::ast::TypeRef)>,
3048        /// #708: the type namespace (`<target>.`) `serialise_expr_via` needs to
3049        /// resolve a body param's custom codec when converting a typed arg for
3050        /// the raw driver. Mirrors the `type_ns` `emit_system_http_support`
3051        /// computes from the same suite target.
3052        system_http_type_ns: String,
3053    },
3054}
3055
3056/// Per-body lowering context: what module we are emitting into ([`ModuleCtx`]),
3057/// what kind of body we are lowering ([`BodyMode`]), and the scratch state the
3058/// recursive lowering accumulates as it goes.
3059///
3060/// Everything below `mode` is deliberately **not** in `ModuleCtx`: a fresh
3061/// `LowerCtx` is built at every body-emission site and never reused across two
3062/// bodies, so these are implicitly reset per body today. Moving any of them up a
3063/// level would leak state between handlers in the same module — most visibly the
3064/// `next_tmp` counter, which would stop restarting `__r0` at each function and
3065/// so rename every generated temp in the emitted TypeScript.
3066pub(crate) struct LowerCtx<'a> {
3067    module: ModuleCtx<'a>,
3068    mode: BodyMode,
3069    /// Agent names declared in the surrounding context. Drives lowering of
3070    /// `Agent(key)` (to `new Agent(makeTestState(String(key)))`) and of
3071    /// `agent_instance.method(args)` (to `instance.method(args, deps)`) in
3072    /// service and agent-handler bodies. Populated by the caller for non-test
3073    /// emission and from the *test's own* agent set in test emission — which is
3074    /// why this is not a [`ModuleCtx`] field despite being module-wide at the
3075    /// nine non-test sites.
3076    pub local_agents: HashSet<String>,
3077    /// v0.154 (ADR 0178): the enclosing function/handler's resolved return type,
3078    /// set at each body-emission site that has one. The `?` lowering reads it to
3079    /// decide whether a declared error embedding (`embeds E as V`) converts the
3080    /// propagated `Err` — via the same `embedding_for` rule the checker used.
3081    /// Genuinely cross-cutting rather than kind-specific: it is saved/restored
3082    /// around lambda bodies, `?`-embedding and match arms *within* whichever
3083    /// kind is being lowered.
3084    return_ty: Option<bynk_check::checker::TyId>,
3085    next_tmp: u32,
3086    /// #908: a stack of per-block frames tracking `let`/`let <-` names that
3087    /// needed a fresh emitted identifier because the name was already bound
3088    /// by an enclosing (or the same) block's `let` — the checker allows
3089    /// re-binding a name (`let x = 1; let x = x + 1`, a deliberate ML-family
3090    /// idiom, ADR 0064), but each `let` still lowers to its own `const`, so
3091    /// without renaming a same-block re-`let` collides with the first
3092    /// (TS2451), and a nested block's re-`let` — while a legal *redeclaration*
3093    /// on its own — would put an RHS read of the outer binding in its own
3094    /// declaration's temporal dead zone. Pushed/popped in lock-step with
3095    /// [`emit_block_inner`] — the single choke point every block (function,
3096    /// lambda, if/else branch, match arm) lowers through — so a read
3097    /// (`lower_ident`, and the agent-dispatch receiver text) resolves a name
3098    /// by walking the stack innermost-out and falls back to the natural
3099    /// `ts_ident` name when no frame renamed it.
3100    pub shadow_scopes: Vec<HashMap<String, String>>,
3101    /// When an `is` receiver is not a simple, repeatable lvalue (e.g. a call
3102    /// like `parse(x) is Ok(n)`), it is evaluated once into a temp; the temp
3103    /// name is cached here keyed by the receiver expression's span so the
3104    /// `.tag` check and every pattern binding reference the *same* single
3105    /// evaluation. Simple receivers (idents / field chains) are never cached
3106    /// and continue to be rendered inline as before.
3107    is_receiver_temps: HashMap<bynk_syntax::span::Span, String>,
3108    /// Variable bindings that point at agent instances. Updated by the
3109    /// statement emitter when it sees `let x = AgentName(key)`. Used by
3110    /// the method-call lowering so `x.method(args)` resolves through
3111    /// the agent's class rather than via the receiver-namespace lookup.
3112    pub local_agent_vars: HashMap<String, String>,
3113    /// v0.182 (#664): while lowering an `EffectLet` whose value addresses a
3114    /// service handler, the call-site principal's identity expression (already
3115    /// lowered), if the statement carries `by <Actor>(<identity>)`. The
3116    /// address-call lowering reads it to build the handler's `deps.identity`.
3117    /// `None` for a unit-identity actor or a non-principal statement.
3118    pub call_site_identity: Option<String>,
3119    /// #706: the call-site principal is `by Nobody` — drive the route with no
3120    /// `Authorization` header so the real auth seam rejects it (`401` →
3121    /// `Rejected(Unauthorized)`). Routes a `system` http address to the no-auth
3122    /// driver. `false` for any other (or no) principal.
3123    pub call_site_no_credential: bool,
3124    /// Slice 1 (ADR 0103): the source-map builder for the file being emitted, if
3125    /// any. The deep lowering chain records `(generated offset → source span)`
3126    /// checkpoints here; `emit_project` owns the `RefCell` and threads a shared
3127    /// borrow in. `None` for the single-file `emit()` path and any body emitted
3128    /// outside a project, where no map is produced.
3129    pub source_map: Option<&'a RefCell<SourceMapBuilder>>,
3130    /// T2.2 (R6.4): set at the two statement sites that emit a literal `await`
3131    /// (`EffectLet`, `Do`) and read-and-reset around a value-position `match`/`if`
3132    /// IIFE's own body construction — the flag a synchronous arrow reads to decide
3133    /// whether it must become `async` and be awaited at its call site. Replaces a
3134    /// scan of the built string for the substring `"await "`, which over-matched
3135    /// on a self-contained `async (...) => {...}` embedded as an arm's value (an
3136    /// iterator terminal like `forEach`) without anything in *this* arrow's own
3137    /// scope needing to await. Not isolated around a lambda body — a nested
3138    /// effectful lambda still marks the enclosing IIFE async, exactly as the old
3139    /// scan did (its own body text also contained `"await "`); closing that is a
3140    /// separate, unscoped defect, not this one.
3141    pub(crate) emitted_await: bool,
3142    /// T2.3 (R6.3): set when lowering a `?` pushes a propagating early-return
3143    /// statement (`if (...) return ...;`) into the current `Pre`. Read (and
3144    /// reset) around a short-circuit operand's own lowering in `lower_bin_op`/
3145    /// `lower_and_with_is`, so those can tell a hoisted `?` apart from an
3146    /// ordinary hoisted statement: a plain `(() => { ...; return expr; })()`
3147    /// wrap is safe for the latter (nothing inside needs to escape the arrow)
3148    /// but captures the former's `return` instead of letting it exit the
3149    /// enclosing function — the residual gap `hoist_if_as_statement` (built for
3150    /// T2.1's `if`-hoisting) also closes here, once this flag says it's needed.
3151    pub(crate) emitted_early_return: bool,
3152}
3153
3154/// v0.59: the source context an `assert` lowering needs to turn its span into a
3155/// `path:line:col` location. Owned (cloned once per test-case body) to keep the
3156/// lowering free of extra lifetime threading; test-file sources are small and
3157/// this is compile-time only.
3158#[derive(Clone)]
3159pub(crate) struct AssertLoc {
3160    pub source: String,
3161    pub rel_path: String,
3162}
3163
3164impl<'a> LowerCtx<'a> {
3165    fn new(module: ModuleCtx<'a>, mode: BodyMode) -> Self {
3166        Self {
3167            module,
3168            mode,
3169            local_agents: HashSet::new(),
3170            return_ty: None,
3171            // Every field below is per-body scratch state: a fresh `LowerCtx` is
3172            // built at each body-emission site and never reused, so these must
3173            // re-initialise here on every construction. In particular `next_tmp`
3174            // restarting at 0 is what makes each emitted function's temps begin
3175            // at `__r0`.
3176            next_tmp: 0,
3177            shadow_scopes: vec![HashMap::new()],
3178            is_receiver_temps: HashMap::new(),
3179            local_agent_vars: HashMap::new(),
3180            call_site_identity: None,
3181            call_site_no_credential: false,
3182            source_map: None,
3183            emitted_await: false,
3184            emitted_early_return: false,
3185        }
3186    }
3187
3188    // ---- `ModuleCtx` passthroughs -----------------------------------------
3189    //
3190    // Returned with the `'a` module lifetime rather than the `&self` borrow, so
3191    // a `&mut self` lowering step can hold onto a commons/runtime handle across
3192    // its own recursive calls exactly as it did when these were plain fields.
3193
3194    /// Typed-commons handle for the module being emitted.
3195    pub(crate) fn commons(&self) -> &'a TypedCommons {
3196        self.module.commons
3197    }
3198
3199    /// Cross-context info for v0.6 cross-context call lowering.
3200    pub(crate) fn cross_context(&self) -> &'a bynk_check::resolver::CrossContextInfo {
3201        self.module.cross_context
3202    }
3203
3204    /// The emitted module's conditional-runtime-helper accumulator.
3205    pub(crate) fn runtime_use(&self) -> &'a RuntimeUse {
3206        self.module.runtime_use
3207    }
3208
3209    /// v0.8 build target.
3210    pub(crate) fn target(&self) -> BuildTarget {
3211        self.module.target
3212    }
3213
3214    /// #527: type names this context rebrands.
3215    pub(crate) fn rebranded_types(&self) -> &HashSet<String> {
3216        &self.module.rebranded_types
3217    }
3218
3219    /// #527: fn names imported from a commons.
3220    pub(crate) fn commons_imported_fns(&self) -> &HashSet<String> {
3221        &self.module.commons_imported_fns
3222    }
3223
3224    /// #934: true when the unit being emitted is the first-party `bynk` adapter.
3225    pub(crate) fn in_bynk_unit(&self) -> bool {
3226        self.module.in_bynk_unit
3227    }
3228
3229    // ---- capability-bearing (`HandlerShared`) state ------------------------
3230    //
3231    // Every accessor here reports the same default a non-handler kind used to
3232    // get from the flat struct (no capabilities, no scope, `deps`), so a caller
3233    // that does not care which kind it is in reads unchanged.
3234
3235    fn handler(&self) -> Option<&HandlerShared> {
3236        match &self.mode {
3237            BodyMode::ServiceHandler { handler, .. }
3238            | BodyMode::ProviderOp { handler }
3239            | BodyMode::AgentHandler { handler, .. }
3240            | BodyMode::WsDoMethod { handler, .. } => Some(handler),
3241            _ => None,
3242        }
3243    }
3244
3245    fn handler_mut(&mut self) -> Option<&mut HandlerShared> {
3246        match &mut self.mode {
3247            BodyMode::ServiceHandler { handler, .. }
3248            | BodyMode::ProviderOp { handler }
3249            | BodyMode::AgentHandler { handler, .. }
3250            | BodyMode::WsDoMethod { handler, .. } => Some(handler),
3251            _ => None,
3252        }
3253    }
3254
3255    /// Whether `name` is a capability in scope as `given C1, C2, ...`.
3256    pub(crate) fn has_capability(&self, name: &str) -> bool {
3257        self.handler()
3258            .is_some_and(|h| h.capabilities.contains(name))
3259    }
3260
3261    /// #934: the calling handler's own qualified name, if a capability call can
3262    /// occur in this body at all. `None` for every non-handler kind — the
3263    /// `Idempotency` key-scoping lowering treats that as a compiler bug and
3264    /// panics, exactly as it did when this was a flat `Option` field.
3265    pub(crate) fn handler_scope(&self) -> Option<&str> {
3266        self.handler().and_then(|h| h.handler_scope.as_deref())
3267    }
3268
3269    /// Events track, slice 2: the qualified name of the unit this body is
3270    /// emitted into, for the `Events.emit[E](event)` lowering's
3271    /// `publisherId`. `None` for a body kind that carries no `HandlerShared`
3272    /// at all (an `Events.emit` call cannot occur there).
3273    pub(crate) fn owning_context(&self) -> Option<&str> {
3274        self.handler().map(|h| h.owning_context.as_str())
3275    }
3276
3277    /// v0.12: the receiver expression a capability call resolves against.
3278    pub(crate) fn cap_deps_expr(&self) -> &str {
3279        self.handler().map_or("deps", |h| h.cap_deps_expr.as_str())
3280    }
3281
3282    /// Note that this body made a cross-context call. A no-op in a body kind
3283    /// that carries no deps shape to widen (a plain method, a predicate, a test
3284    /// case) — those never read the flag back.
3285    pub(crate) fn note_cross_context_used(&mut self) {
3286        if let Some(h) = self.handler_mut() {
3287            h.cross_context_used = true;
3288        }
3289    }
3290
3291    /// True if this handler made at least one cross-context call.
3292    pub(crate) fn cross_context_used(&self) -> bool {
3293        self.handler().is_some_and(|h| h.cross_context_used)
3294    }
3295
3296    /// v0.9.2: true if this body instantiated a local agent.
3297    pub(crate) fn agents_instantiated(&self) -> bool {
3298        self.handler().is_some_and(|h| h.agents_instantiated)
3299    }
3300
3301    /// #527: capabilities required by agent methods this body calls.
3302    pub(crate) fn agent_given_caps_used(
3303        &self,
3304    ) -> Option<&std::collections::BTreeMap<String, crate::ir::CapRefIr>> {
3305        self.handler().map(|h| &h.agent_given_caps_used)
3306    }
3307
3308    // ---- test-scaffold (`TestShared`) state --------------------------------
3309
3310    fn test(&self) -> Option<&TestShared> {
3311        match &self.mode {
3312            BodyMode::PredicateScaffold { test }
3313            | BodyMode::TestCase { test, .. }
3314            | BodyMode::IntegrationCase { test, .. } => Some(test),
3315            _ => None,
3316        }
3317    }
3318
3319    /// True when lowering **generated test-scaffold** TypeScript (a test-case
3320    /// body, or a `stub`/`where`/`requires` predicate value), where branded
3321    /// types are destructured into `any`-typed value bindings rather than
3322    /// referenced as types. Callers that emit a branded `as`-cast consult this
3323    /// to pick `unchecked_construct_test` (→ `(v as any)`) over the production
3324    /// `(v as T)` form, which cannot resolve `T` in the test module's scope.
3325    pub(crate) fn in_test_scaffold(&self) -> bool {
3326        self.test().is_some_and(|t| t.test_scaffold)
3327    }
3328
3329    /// v0.59: the test body's source context, for `assert`/`expect` locations.
3330    pub(crate) fn assert_loc(&self) -> Option<&AssertLoc> {
3331        self.test().and_then(|t| t.assert_loc.as_ref())
3332    }
3333
3334    // ---- single-kind state -------------------------------------------------
3335
3336    /// v0.80: inside an invariant predicate, the proposed-state variable and the
3337    /// agent's state field names.
3338    pub(crate) fn invariant_state(&self) -> Option<(&str, &HashSet<String>)> {
3339        match &self.mode {
3340            BodyMode::Invariant { name, fields } => Some((name.as_str(), fields)),
3341            _ => None,
3342        }
3343    }
3344
3345    /// v0.116: inside a `transition` predicate, the JS names bound to the
3346    /// contextual `old`/`new` state records.
3347    pub(crate) fn transition_states(&self) -> Option<(&str, &str)> {
3348        match &self.mode {
3349            BodyMode::Transition { old, new } => Some((old.as_str(), new.as_str())),
3350            _ => None,
3351        }
3352    }
3353
3354    /// v0.117: the recorded-call trace object a test case's observations read.
3355    pub(crate) fn observation_trace(&self) -> Option<&str> {
3356        match &self.mode {
3357            BodyMode::TestCase {
3358                observation_trace, ..
3359            } => observation_trace.as_deref(),
3360            _ => None,
3361        }
3362    }
3363
3364    fn agent_store(&self) -> Option<&AgentStoreState> {
3365        match &self.mode {
3366            BodyMode::AgentHandler { store, .. } => store.as_deref(),
3367            _ => None,
3368        }
3369    }
3370
3371    /// v0.81: the mutable working-state variable a `store`-agent handler stages
3372    /// its writes into. `__state` is the name every real site uses; the fallback
3373    /// keeps the (defensive) non-store paths rendering as they did before.
3374    pub(crate) fn agent_store_var(&self) -> &str {
3375        self.agent_store().map_or("__state", |s| s.state.0.as_str())
3376    }
3377
3378    /// v0.81: the working-state variable plus the `Cell` field names it holds.
3379    pub(crate) fn agent_store_cells(&self) -> Option<(&str, &HashSet<String>)> {
3380        self.agent_store().map(|s| (s.state.0.as_str(), &s.state.1))
3381    }
3382
3383    /// v0.82: whether `name` is a persisted `store Map` field (held connection
3384    /// maps are deliberately excluded — they use the connId lowering).
3385    pub(crate) fn is_agent_store_map(&self, name: &str) -> bool {
3386        self.agent_store().is_some_and(|s| s.maps.contains(name))
3387    }
3388
3389    /// v0.83: whether `name` is a `store Set` field.
3390    pub(crate) fn is_agent_store_set(&self, name: &str) -> bool {
3391        self.agent_store().is_some_and(|s| s.sets.contains(name))
3392    }
3393
3394    /// v0.87: the ttl (millis) of the `store Cache` field `name`, if it is one.
3395    pub(crate) fn agent_store_cache_ttl(&self, name: &str) -> Option<i64> {
3396        self.agent_store().and_then(|s| s.caches.get(name).copied())
3397    }
3398
3399    /// v0.95: the `@retain` horizon of the `store Log` field `name`, if it is
3400    /// one. The outer `Option` is "is a log"; the inner is "has a retain".
3401    pub(crate) fn agent_store_log_retain(&self, name: &str) -> Option<Option<i64>> {
3402        self.agent_store().and_then(|s| s.logs.get(name).copied())
3403    }
3404
3405    /// v0.95: whether `name` is a `store Log` field.
3406    pub(crate) fn is_agent_store_log(&self, name: &str) -> bool {
3407        self.agent_store()
3408            .is_some_and(|s| s.logs.contains_key(name))
3409    }
3410
3411    /// v0.93: the value-record fields the `store Map` `name` is `@indexed(by:)`
3412    /// on — empty when it has no secondary index.
3413    pub(crate) fn agent_store_index_fields(&self, name: &str) -> Vec<String> {
3414        self.agent_store()
3415            .and_then(|s| s.indexes.get(name).cloned())
3416            .unwrap_or_default()
3417    }
3418
3419    /// v0.105: the connection **frame type** of the held `store Map[K,
3420    /// Connection]` field `name`, if it is one.
3421    pub(crate) fn agent_held_map_frame(&self, name: &str) -> Option<&String> {
3422        self.agent_store().and_then(|s| s.held_maps.get(name))
3423    }
3424
3425    /// v0.105: whether `name` is a held `store Map[K, Connection]` field.
3426    pub(crate) fn is_agent_held_map(&self, name: &str) -> bool {
3427        self.agent_store()
3428            .is_some_and(|s| s.held_maps.contains_key(name))
3429    }
3430
3431    /// True when lowering an agent handler body — drives the `self.<keyField>`
3432    /// rewrite.
3433    pub(crate) fn in_agent_handler(&self) -> bool {
3434        match &self.mode {
3435            BodyMode::AgentHandler {
3436                in_agent_handler, ..
3437            } => *in_agent_handler,
3438            _ => false,
3439        }
3440    }
3441
3442    /// The name of the agent's `key id` field, inside an agent handler body.
3443    pub(crate) fn agent_key_field(&self) -> Option<&str> {
3444        match &self.mode {
3445            BodyMode::AgentHandler {
3446                agent_key_field, ..
3447            } => agent_key_field.as_deref(),
3448            _ => None,
3449        }
3450    }
3451
3452    /// v0.104: the agent hosting the websocket lifecycle body being lowered.
3453    pub(crate) fn ws_self_agent(&self) -> Option<&str> {
3454        match &self.mode {
3455            BodyMode::WsDoMethod { ws_self_agent, .. } => ws_self_agent.as_deref(),
3456            _ => None,
3457        }
3458    }
3459
3460    /// v0.47: the `by` binder whose `.identity` is threaded through `deps`.
3461    pub(crate) fn deps_identity_binder(&self) -> Option<&str> {
3462        match &self.mode {
3463            BodyMode::ServiceHandler {
3464                deps_identity_binder,
3465                ..
3466            }
3467            | BodyMode::WsDoMethod {
3468                deps_identity_binder,
3469                ..
3470            } => deps_identity_binder.as_deref(),
3471            _ => None,
3472        }
3473    }
3474
3475    /// v0.52: the multi-actor sum handler's resolved-actor binder.
3476    pub(crate) fn actor_sum_binder(&self) -> Option<&str> {
3477        match &self.mode {
3478            BodyMode::ServiceHandler {
3479                actor_sum_binder, ..
3480            } => actor_sum_binder.as_deref(),
3481            _ => None,
3482        }
3483    }
3484
3485    /// v0.7: whether `name` is a local service of the test case's target context.
3486    pub(crate) fn is_test_service(&self, name: &str) -> bool {
3487        match &self.mode {
3488            BodyMode::TestCase { test_services, .. } => test_services.contains(name),
3489            _ => false,
3490        }
3491    }
3492
3493    /// v0.182 (#664): the ordered handler kinds of the test service `name`.
3494    pub(crate) fn test_service_handlers(
3495        &self,
3496        name: &str,
3497    ) -> Option<&[bynk_syntax::ast::HandlerKind]> {
3498        match &self.mode {
3499            BodyMode::TestCase {
3500                test_service_handlers,
3501                ..
3502            } => test_service_handlers.get(name).map(Vec::as_slice),
3503            _ => None,
3504        }
3505    }
3506
3507    /// v0.182 (Slice B, #667): whether `name` is an http service of the system
3508    /// target being driven.
3509    pub(crate) fn is_system_http_service(&self, name: &str) -> bool {
3510        match &self.mode {
3511            BodyMode::IntegrationCase {
3512                system_http_services,
3513                ..
3514            } => system_http_services.contains(name),
3515            _ => false,
3516        }
3517    }
3518
3519    /// #707: whether `(service, verb, path)` is a declared route of the system
3520    /// target — an undeclared one drives the `405` fall-through.
3521    pub(crate) fn has_system_http_route(&self, route: &(String, String, String)) -> bool {
3522        match &self.mode {
3523            BodyMode::IntegrationCase {
3524                system_http_routes, ..
3525            } => system_http_routes.contains(route),
3526            _ => false,
3527        }
3528    }
3529
3530    /// #708: the body param position and declared type of a system http route.
3531    pub(crate) fn system_http_route_body(
3532        &self,
3533        route: &(String, String, String),
3534    ) -> Option<&(usize, bynk_syntax::ast::TypeRef)> {
3535        match &self.mode {
3536            BodyMode::IntegrationCase {
3537                system_http_route_body,
3538                ..
3539            } => system_http_route_body.get(route),
3540            _ => None,
3541        }
3542    }
3543
3544    /// #708: the type namespace a system http body param's codec resolves in.
3545    pub(crate) fn system_http_type_ns(&self) -> &str {
3546        match &self.mode {
3547            BodyMode::IntegrationCase {
3548                system_http_type_ns,
3549                ..
3550            } => system_http_type_ns.as_str(),
3551            _ => "",
3552        }
3553    }
3554
3555    /// Events track, slice 0 (spine #936): true when a bare `Events`
3556    /// receiver in this unit is genuinely the first-party `bynk.Events`
3557    /// capability — declared here because this unit *is* `bynk`, or
3558    /// flattened in from it (`consumes bynk { Events }`) — not some other,
3559    /// unrelated capability that merely happens to share the name. Mirrors
3560    /// #934's `Idempotency` distinction (`is_first_party` at the
3561    /// `Idempotency.dedup`/`remember` lowering site). Both the call-site
3562    /// interception (`lower.rs`) and the `__events` buffer declaration
3563    /// (`block_uses_emit`'s gate in `emit.rs`) must agree on this, or a
3564    /// custom same-named `Events` capability's calls get silently rewritten
3565    /// into a buffer nothing constructs a provider for.
3566    pub(crate) fn is_first_party_events(&self) -> bool {
3567        self.in_bynk_unit()
3568            || self
3569                .cross_context()
3570                .flattened_caps
3571                .get("Events")
3572                .map(String::as_str)
3573                == Some("bynk")
3574    }
3575
3576    /// Events slice 3b (#978): the declared `@schema(N)` version of the
3577    /// locally-declared event `name`, or `1` if it has none (including if
3578    /// `name` isn't a locally-declared event at all — `Events.emit[E]` only
3579    /// ever names an owned event, checker-enforced, so a miss here can only
3580    /// mean a broken build already reported elsewhere, and this degrades to
3581    /// today's pre-existing output rather than panicking).
3582    pub(crate) fn event_schema_version(&self, name: &str) -> i64 {
3583        self.module
3584            .event_schema_versions
3585            .get(name)
3586            .copied()
3587            .unwrap_or(1)
3588    }
3589
3590    /// Attach the file's source-map builder (slice 1, ADR 0103). Builder-style so
3591    /// the emission sites with no builder leave `LowerCtx::new(module, mode)`
3592    /// untouched — only the project-emission path that has one calls this.
3593    fn with_source_map(mut self, map: Option<&'a RefCell<SourceMapBuilder>>) -> Self {
3594        self.source_map = map;
3595        self
3596    }
3597
3598    /// Record that this lowering emitted a reference to the `Bytes` runtime
3599    /// helpers, so the module imports them.
3600    fn note_bytes(&self) {
3601        self.runtime_use().note_bytes();
3602    }
3603
3604    /// Record a checkpoint: generated text from `out_len` onward originates at
3605    /// `span`, until the next checkpoint (ADR 0103 D2, nearest-enclosing). A
3606    /// no-op when no builder is attached. `out_len` is the buffer length *before*
3607    /// the statement's text is appended.
3608    ///
3609    /// `out_len` only means something relative to the *top-level module
3610    /// buffer* the attached builder is tracking. A caller building an IIFE
3611    /// into its own local `String` — `lower_if`'s value-position wrapper,
3612    /// `build_match_iife`'s — before splicing it elsewhere must not call this
3613    /// with that buffer's own length; see [`Self::without_source_map`].
3614    fn record_span(&self, out_len: usize, span: bynk_syntax::span::Span) {
3615        if let Some(map) = self.source_map {
3616            map.borrow_mut().record(out_len, span);
3617        }
3618    }
3619
3620    /// #4 review: run `f` with source-map recording suppressed, restoring it
3621    /// after. For lowering into a local IIFE buffer that will later be
3622    /// spliced into the real module text at some other offset — `record_span`
3623    /// has no way to know that offset, so a checkpoint taken here would
3624    /// silently corrupt the map with a position relative to the wrong
3625    /// buffer. `SourceMapBuilder::merge` already solves the equivalent
3626    /// problem one level up (a handler/test body's own local buffer, spliced
3627    /// into the module) by recording into a *sub*-builder and rebasing at the
3628    /// splice — but that needs a builder that outlives the call, and
3629    /// `source_map` is `Option<&'a RefCell<SourceMapBuilder>>` tied to the
3630    /// whole emission's lifetime, so a function-local sub-builder can't be
3631    /// substituted in. Suppressing instead of mis-recording means the
3632    /// nearest-enclosing-checkpoint rule (ADR 0103 D2) falls back to whatever
3633    /// was correctly mapped just before the IIFE started, rather than a wrong
3634    /// one silently taking over — degraded stepping through the IIFE's own
3635    /// lines in `bynkc test --inspect`, not a corrupted map.
3636    fn without_source_map<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
3637        let saved = self.source_map.take();
3638        let result = f(self);
3639        self.source_map = saved;
3640        result
3641    }
3642    /// v0.9.2: lower an agent instantiation `AgentName(key)` to its factory
3643    /// call. Bundle/test mode passes only the key; workers mode also threads
3644    /// `deps.env` so the factory can reach the agent's DO namespace.
3645    fn agent_construct(&mut self, agent: &str, key_expr: &str) -> String {
3646        if let Some(h) = self.handler_mut() {
3647            h.agents_instantiated = true;
3648        }
3649        let factory = agent_factory_name(agent);
3650        if matches!(self.target(), BuildTarget::Workers) {
3651            format!("{factory}({key_expr}, deps.env)")
3652        } else {
3653            format!("{factory}({key_expr})")
3654        }
3655    }
3656
3657    /// #527: note that the body calls `agent.method`, folding the method's
3658    /// `given` capabilities into this handler's requirement set. A no-op in a
3659    /// body kind that has no deps shape to widen — those never read it back.
3660    pub(crate) fn record_agent_call(&mut self, agent: &str, method: &str) {
3661        let givens = self
3662            .module
3663            .agent_method_givens
3664            .get(agent)
3665            .and_then(|m| m.get(method))
3666            .cloned()
3667            .unwrap_or_default();
3668        if let Some(h) = self.handler_mut() {
3669            for c in givens {
3670                h.agent_given_caps_used.entry(c.name.clone()).or_insert(c);
3671            }
3672        }
3673    }
3674    fn fresh(&mut self) -> String {
3675        let n = self.next_tmp;
3676        self.next_tmp += 1;
3677        format!("__r{n}")
3678    }
3679    /// #908: bind a `let`/`let <-` LHS to its emitted JS identifier. Returns
3680    /// the natural `ts_ident` name unless `name` is already bound *anywhere*
3681    /// in the enclosing block chain — not only the current block. A nested
3682    /// block re-`let`-ing an outer name is ordinary, valid lexical shadowing
3683    /// in JS on its own, but this `let`'s own RHS may still read the outer
3684    /// binding (`let n = n + 1` one block in); a plain `const n` there
3685    /// would put the read inside its own declaration's temporal dead zone
3686    /// (JS hoists a block's `let`/`const` names to the top of that block),
3687    /// turning a correct read of the outer value into a TDZ ReferenceError.
3688    /// Renaming whenever *any* enclosing frame already has the name sidesteps
3689    /// that regardless of whether this particular RHS reads it. Allocates a
3690    /// fresh name via [`Self::fresh`] when so. `_` never collides (each is
3691    /// already a fresh throwaway) and is never registered, since it is never
3692    /// read.
3693    pub(crate) fn bind_local_name(&mut self, name: &str) -> String {
3694        if name == "_" {
3695            return self.fresh();
3696        }
3697        let natural = ts_ident(name);
3698        let js_name = if self.shadow_scopes.iter().any(|f| f.contains_key(name)) {
3699            self.fresh()
3700        } else {
3701            natural
3702        };
3703        self.shadow_scopes
3704            .last_mut()
3705            .expect("shadow_scopes always has a root frame")
3706            .insert(name.to_string(), js_name.clone());
3707        js_name
3708    }
3709    /// #908: the emitted JS identifier currently bound to a local name, if a
3710    /// `let` re-bind renamed it somewhere in the enclosing block chain.
3711    /// Walked innermost-out so a nested block sees an outer rename that was
3712    /// still active when it was entered. `None` means no rename applies —
3713    /// callers fall back to the natural `ts_ident` name.
3714    pub(crate) fn resolved_local_name(&self, name: &str) -> Option<String> {
3715        self.shadow_scopes
3716            .iter()
3717            .rev()
3718            .find_map(|f| f.get(name).cloned())
3719    }
3720    /// Whether `name` is bound by an enclosing local (a `let`, match-arm/`is`
3721    /// binding, or lambda param) rather than free to refer to a store field.
3722    /// A local always wins: store-field dispatch by bare receiver name must
3723    /// check this first, or a parameter/binding that happens to share a store
3724    /// field's name is silently treated as the store field.
3725    pub(crate) fn is_local(&self, name: &str) -> bool {
3726        self.shadow_scopes.iter().any(|f| f.contains_key(name))
3727    }
3728    /// #908: register a non-`let` binder (a match-arm/`is` pattern binding, or
3729    /// a lambda param) into the current frame under its natural `ts_ident`
3730    /// name — never renamed, since each such binder already lowers inside its
3731    /// own JS block/arrow scope with no risk of colliding with a sibling
3732    /// declaration of the same name. Without this, a read inside the binder's
3733    /// scope would fall through [`Self::resolved_local_name`]'s stack walk
3734    /// past this (unregistered) declaration to an outer `let` rename that is
3735    /// no longer the right value here — silently wrong output, not a `tsc`
3736    /// error. Every construct that introduces a binder outside `bind_local_name`
3737    /// (match arms, `is`, lambda params) must call this for each name it binds.
3738    pub(crate) fn declare_binder(&mut self, name: &str) {
3739        if name == "_" {
3740            return;
3741        }
3742        self.shadow_scopes
3743            .last_mut()
3744            .expect("shadow_scopes always has a root frame")
3745            .insert(name.to_string(), ts_ident(name));
3746    }
3747    /// Return a stable textual reference to an `is` receiver, used by the
3748    /// `.tag` check in `lower_is`. A simple, repeatable lvalue is lowered
3749    /// inline exactly as before (preserving rewrites such as `self.state` or
3750    /// capability access). A complex receiver (anything `value_text_for_is`
3751    /// could not render — e.g. a call) is evaluated once into a fresh temp
3752    /// hoisted into the returned `Lowered` and cached by span, so the bindings
3753    /// gathered later reference the same evaluation rather than re-running the
3754    /// expression.
3755    fn is_receiver_ref(&mut self, value: &Expr) -> Lowered {
3756        if let Some(t) = self.is_receiver_temps.get(&value.span) {
3757            return Lowered::bare(t.clone());
3758        }
3759        let mut pre = Pre::new();
3760        let lowered = pre.lower(value, self);
3761        if is_simple_is_receiver(value) {
3762            return pre.finish(lowered);
3763        }
3764        let tmp = self.fresh();
3765        pre.push(format!("const {tmp} = {lowered};"));
3766        self.is_receiver_temps.insert(value.span, tmp.clone());
3767        pre.finish(tmp)
3768    }
3769
3770    /// v0.13: like `is_receiver_ref` but always lifts to a temp, even for a
3771    /// simple ident. A refined `is`-narrowing re-binds the value's name to the
3772    /// branded refined type (`const n = <temp> as Quantity`); that shadowing
3773    /// const cannot reference the same name (TDZ), so the value is captured in a
3774    /// temp first and both the check and the binding read the temp.
3775    fn is_receiver_ref_forced(&mut self, value: &Expr) -> Lowered {
3776        if let Some(t) = self.is_receiver_temps.get(&value.span) {
3777            return Lowered::bare(t.clone());
3778        }
3779        let mut pre = Pre::new();
3780        let lowered = pre.lower(value, self);
3781        let tmp = self.fresh();
3782        pre.push(format!("const {tmp} = {lowered};"));
3783        self.is_receiver_temps.insert(value.span, tmp.clone());
3784        pre.finish(tmp)
3785    }
3786
3787    /// v0.13: true when `value is Name` is a *refinement* check — the value is a
3788    /// base/refined value and `Name` is a refined type — rather than a sum
3789    /// variant test. Mirrors the checker's disambiguation.
3790    fn is_refined_is_check(&self, value: &Expr, name: &str) -> bool {
3791        let value_baseish = matches!(
3792            self.commons().expr_ty(value.id).as_deref(),
3793            Some(Ty::Base(_))
3794                | Some(Ty::Named {
3795                    kind: NamedKind::Refined(_),
3796                    ..
3797                })
3798        );
3799        let name_refined = matches!(
3800            self.commons().types.get(name).map(|d| &d.body),
3801            Some(TypeBody::Refined { .. })
3802        );
3803        value_baseish && name_refined
3804    }
3805    /// Read-only counterpart for the binding gatherer (which returns no
3806    /// `Lowered`, so it has nowhere to hoist and cannot lift). If the receiver was already lifted to a temp during
3807    /// condition lowering, reuse that temp; otherwise it must be a simple
3808    /// repeatable lvalue, rendered inline. The "lower the condition before
3809    /// gathering its bindings" ordering in `emit_if_tail` / `lower_and_with_is`
3810    /// guarantees the temp exists before this is called for complex receivers.
3811    fn is_receiver_text(&self, value: &Expr) -> String {
3812        if let Some(t) = self.is_receiver_temps.get(&value.span) {
3813            return t.clone();
3814        }
3815        value_text_for_is(value)
3816    }
3817    fn receiver_namespace(&self, e: &Expr) -> Option<String> {
3818        let ty = self.commons().expr_ty(e.id)?;
3819        if let Ty::Named { name, .. } = &*ty {
3820            Some(name.clone())
3821        } else {
3822            None
3823        }
3824    }
3825    /// Resolve the payload field name for the i-th positional binding of
3826    /// a variant. Built-ins are recognised by name; user variants are
3827    /// looked up via the type tables.
3828    fn positional_field_name(
3829        &self,
3830        discriminant_ty: Option<TyId>,
3831        variant: &str,
3832        idx: usize,
3833        tys: &Arc<Types>,
3834    ) -> String {
3835        match (variant, idx) {
3836            ("Ok", 0) | ("Some", 0) => return "value".to_string(),
3837            ("Err", 0) => return "error".to_string(),
3838            _ => {}
3839        }
3840        // v0.52: a multi-actor sum arm binds the resolved actor's identity,
3841        // carried in the `identity` field of the tagged object.
3842        let disc_node = discriminant_ty.map(|t| tys.get(t));
3843        if let Some(Ty::ActorSum(_)) = disc_node.as_deref() {
3844            return "identity".to_string();
3845        }
3846        if let Some(Ty::Named {
3847            kind: NamedKind::Sum,
3848            name,
3849            ..
3850        }) = disc_node.as_deref()
3851            && let Some(decl) = self.commons().types.get(name)
3852            && let TypeBody::Sum(s) = &decl.body
3853            && let Some(v) = s.variants.iter().find(|v| v.name.name == variant)
3854            && let Some(f) = v.payload.get(idx)
3855        {
3856            return f.name.name.clone();
3857        }
3858        // Single-field fallback. The checker rejects mixed bindings already.
3859        "value".to_string()
3860    }
3861
3862    /// The type of a variant's `idx`-th payload field, when resolvable — used to
3863    /// recurse field-name resolution through nested payload patterns (ADR 0169).
3864    /// Precise for `Result`/`Option`/`HttpResult` and user sums; `None` otherwise
3865    /// (callers fall back to the single-field `"value"` name).
3866    fn payload_field_ty(
3867        &self,
3868        ty: Option<TyId>,
3869        variant: &str,
3870        idx: usize,
3871        tys: &Arc<Types>,
3872    ) -> Option<TyId> {
3873        match ty.map(|t| tys.get(t)).as_deref() {
3874            Some(Ty::Result(t, e)) => match (variant, idx) {
3875                ("Ok", 0) => Some(*t),
3876                ("Err", 0) => Some(*e),
3877                _ => None,
3878            },
3879            Some(Ty::HttpResult(t)) if variant == "Ok" && idx == 0 => Some(*t),
3880            Some(Ty::Option(t)) if variant == "Some" && idx == 0 => Some(*t),
3881            Some(Ty::Named {
3882                kind: NamedKind::Sum,
3883                name,
3884                args,
3885            }) => {
3886                let decl = self.commons().types.get(name)?;
3887                let TypeBody::Sum(s) = &decl.body else {
3888                    return None;
3889                };
3890                let v = s.variants.iter().find(|v| v.name.name == variant)?;
3891                let f = v.payload.get(idx)?;
3892                // #593: substitute the instantiation's type arguments into the
3893                // payload field type — a bare type parameter (`Loaded(value: T)`)
3894                // resolves to its concrete argument, exactly as the checker's
3895                // `variants_of` does. Plain resolve for a non-generic sum (empty
3896                // `args`), so a nested positional binding recovers the real field
3897                // name instead of falling back to the generic `"value"`.
3898                bynk_check::checker::instantiate_field_ty(
3899                    decl,
3900                    args,
3901                    &f.type_ref,
3902                    &self.commons().types,
3903                    tys,
3904                )
3905            }
3906            _ => None,
3907        }
3908    }
3909}
3910
3911/// Unchecked construction of a branded value in emitted TypeScript.
3912///
3913/// ADR 0182: an **opaque** type exposes a runtime `.unsafe(value)` constructor
3914/// (source-callable within its defining commons, and the target of its internal
3915/// uses), so opaque construction stays `T.unsafe(value)`. A **refined** or
3916/// **alias** type has **no** public `.unsafe`: exposing one let hand-written host
3917/// or adapter code bypass the refinement predicate, the credibility hole #545
3918/// closed. Its admitted / generated values are branded with an inline `as` cast
3919/// — byte-for-byte the old `.unsafe` body (`return value as T`) at the call site,
3920/// but not a callable API surface a consumer can reach.
3921pub(crate) fn unchecked_construct(name: &str, value: &str, is_opaque: bool) -> String {
3922    if is_opaque {
3923        format!("{name}.unsafe({value})")
3924    } else {
3925        format!("({value} as {name})")
3926    }
3927}
3928
3929/// Unchecked construction inside GENERATED TEST scaffolding (`tests/*.test.ts`).
3930///
3931/// There a branded type is in scope only as an `any`-typed value binding
3932/// (`const {{ T }} = ns as any`) — never as a type — so the production
3933/// `(value as T)` form fails to resolve `T`. Opaque still constructs through its
3934/// `.unsafe` value method (kept, ADR 0182); a refined/alias value brands to `any`,
3935/// which is exactly the type the pre-0182 `T.unsafe(value)` already produced here
3936/// (`T` being `any`) and erases to the raw value at runtime — without
3937/// reintroducing a callable refined `.unsafe`.
3938pub(crate) fn unchecked_construct_test(name: &str, value: &str, is_opaque: bool) -> String {
3939    if is_opaque {
3940        format!("{name}.unsafe({value})")
3941    } else {
3942        format!("({value} as any)")
3943    }
3944}
3945
3946fn ts_base(b: BaseType) -> &'static str {
3947    match b {
3948        BaseType::Int => "number",
3949        BaseType::String => "string",
3950        BaseType::Bool => "boolean",
3951        BaseType::Float => "number",
3952        BaseType::Duration | BaseType::Instant => "number",
3953        // v0.110 (ADR 0142): `Bytes` is the one base type that does NOT erase
3954        // to `number` — it lowers to an immutable octet sequence, `Uint8Array`.
3955        BaseType::Bytes => "Uint8Array",
3956    }
3957}
3958
3959pub(crate) fn ts_type_ref(r: &TypeRef) -> String {
3960    ts_type_ref_with(r, None)
3961}
3962
3963/// Like `ts_type_ref`, but qualifies named types that live in `scope` with the
3964/// namespace `ns` (`Order` → `Ns.Order`). Used by the test-emission harness for
3965/// mock method signatures that sit outside the destructuring that brings a
3966/// namespace's value-side names into local scope, so the types must be
3967/// referenced fully qualified. Qualification recurses through generic
3968/// arguments; base/unit types are unaffected.
3969pub(crate) fn ts_type_ref_qualified(r: &TypeRef, scope: &HashSet<String>, ns: &str) -> String {
3970    ts_type_ref_with(
3971        r,
3972        Some(&|name| scope.contains(name).then(|| ns.to_string())),
3973    )
3974}
3975
3976/// Like `ts_type_ref_qualified`, but each in-scope name can carry its *own*
3977/// namespace rather than one shared `ns` — needed when a signature mixes
3978/// names owned by the target unit with names reached only through a `uses`d
3979/// commons (e.g. a stub class implementing an adapter-sourced capability
3980/// whose return type lives in a commons the capability's own unit `uses`,
3981/// never in the target context itself — Locale capability track, slice 1,
3982/// #844). Qualifying such a name under the target's own namespace would
3983/// reference an export `emit_context_rebrands` never emits (it only rebrands
3984/// names the target's *own* lowered body references), so each name is
3985/// qualified under the namespace that actually exports it.
3986pub(crate) fn ts_type_ref_qualified_multi(
3987    r: &TypeRef,
3988    type_ns: &HashMap<String, String>,
3989) -> String {
3990    ts_type_ref_with(r, Some(&|name| type_ns.get(name).cloned()))
3991}
3992
3993/// A name → owning-namespace lookup for `ts_type_ref_with`'s `qualify` arm.
3994type QualifyFn<'a> = &'a dyn Fn(&str) -> Option<String>;
3995
3996/// Shared renderer behind `ts_type_ref` (`qualify = None`) and the two
3997/// `ts_type_ref_qualified*` helpers above (`qualify = Some(name -> namespace)`).
3998/// With `None` it is output-identical to the historic `ts_type_ref`; the only
3999/// divergence is the `Named`/`App` arms, which qualify in-scope names when
4000/// `qualify` is set.
4001fn ts_type_ref_with(r: &TypeRef, qualify: Option<QualifyFn<'_>>) -> String {
4002    match r {
4003        TypeRef::Base(b, _) => ts_base(*b).to_string(),
4004        TypeRef::Named(id) => {
4005            if let Some(f) = qualify
4006                && let Some(ns) = f(&id.name)
4007            {
4008                format!("{ns}.{}", id.name)
4009            } else {
4010                id.name.clone()
4011            }
4012        }
4013        TypeRef::Result(t, e, _) => format!(
4014            "Result<{}, {}>",
4015            ts_type_ref_with(t, qualify),
4016            ts_type_ref_with(e, qualify)
4017        ),
4018        TypeRef::Option(t, _) => format!("Option<{}>", ts_type_ref_with(t, qualify)),
4019        TypeRef::Effect(t, _) => {
4020            let inner = ts_type_ref_with(t, qualify);
4021            if inner == "()" || inner == "void" {
4022                "Promise<void>".to_string()
4023            } else {
4024                format!("Promise<{inner}>")
4025            }
4026        }
4027        TypeRef::HttpResult(t, _) => format!("HttpResult<{}>", ts_type_ref_with(t, qualify)),
4028        // v0.20b: collections lower to immutable TS shapes.
4029        TypeRef::List(t, _) => format!("readonly {}[]", ts_type_ref_with(t, qualify)),
4030        TypeRef::Query(t, _) => {
4031            format!("(() => readonly {}[])", ts_type_ref_with(t, qualify))
4032        }
4033        // v0.100: `Stream[T]` lowers to a host async iterable.
4034        TypeRef::Stream(t, _) => format!("AsyncIterable<{}>", ts_type_ref_with(t, qualify)),
4035        // v0.102: a `Connection[F]` lowers to the runtime `Connection<F>`
4036        // interface (the concrete implementation arrives with the protocol).
4037        TypeRef::Connection(t, _) => format!("Connection<{}>", ts_type_ref_with(t, qualify)),
4038        // v0.119: `History[Agent]` is a test-only generator with no emitted TS
4039        // type — it never reaches a signature/field position (the property runner
4040        // binds the driven history as an ordinary array). Rendered defensively.
4041        TypeRef::History(_, _) => "never".to_string(),
4042        TypeRef::Map(k, v, _) => {
4043            format!(
4044                "ReadonlyMap<{}, {}>",
4045                ts_type_ref_with(k, qualify),
4046                ts_type_ref_with(v, qualify)
4047            )
4048        }
4049        TypeRef::QueueResult(_) => "QueueResult".to_string(),
4050        TypeRef::ValidationError(_) => "ValidationError".to_string(),
4051        TypeRef::JsonError(_) => "JsonError".to_string(),
4052        TypeRef::Unit(_) => "void".to_string(),
4053        // v0.157 (ADR 0183): `Name[Arg, …]` lowers to the erased TS generic
4054        // `Name<Arg, …>` — the generic record's interface is emitted with the
4055        // same type parameters (like a generic function's erased `<A, B>`).
4056        TypeRef::App { name, args, .. } => {
4057            let head = if let Some(f) = qualify
4058                && let Some(ns) = f(&name.name)
4059            {
4060                format!("{ns}.{}", name.name)
4061            } else {
4062                name.name.clone()
4063            };
4064            let rendered: Vec<String> = args.iter().map(|a| ts_type_ref_with(a, qualify)).collect();
4065            format!("{head}<{}>", rendered.join(", "))
4066        }
4067        // v0.20a: a function type lowers to a TS function type. Positional
4068        // parameter names (`a0`, `a1`, …) — TS requires names in function
4069        // type syntax; an Effect return is already Promise via recursion.
4070        TypeRef::Fn(params, ret, _) => {
4071            let params: Vec<String> = params
4072                .iter()
4073                .enumerate()
4074                .map(|(i, p)| format!("a{i}: {}", ts_type_ref_with(p, qualify)))
4075                .collect();
4076            let ret = match ts_type_ref_with(ret, qualify).as_str() {
4077                "()" => "void".to_string(),
4078                other => other.to_string(),
4079            };
4080            format!("({}) => {ret}", params.join(", "))
4081        }
4082    }
4083}
4084
4085/// v0.20b: render a checker `Ty` as a TypeScript type. Used by the inline
4086/// kernel-method lowerings, whose IIFE parameters must be annotated
4087/// (`noImplicitAny`). Rigid type variables render as themselves — inside an
4088/// emitted generic function they are in scope as TS type parameters.
4089fn ts_ty(t: TyId, tys: &Arc<Types>) -> String {
4090    match &*tys.get(t) {
4091        // bynk internal error (finding #28, R4.3): `Ty::Error` records a
4092        // resolution failure, which per R4.3 is always accompanied by a
4093        // pushed diagnostic — the check that produced it should have failed
4094        // the whole program and never reached emission. A loud failure here
4095        // beats silently emitting a type for a node the checker gave up on.
4096        Ty::Error => panic!(
4097            "bynk internal error (finding #28): emitter asked to render `Ty::Error` as a \
4098             TypeScript type — a checked program should never contain one"
4099        ),
4100        Ty::Base(BaseType::Int) => "number".to_string(),
4101        Ty::Base(BaseType::String) => "string".to_string(),
4102        Ty::Base(BaseType::Bool) => "boolean".to_string(),
4103        Ty::Base(BaseType::Float) => "number".to_string(),
4104        Ty::Base(BaseType::Duration | BaseType::Instant) => "number".to_string(),
4105        // v0.110 (ADR 0142): `Bytes` erases to `Uint8Array`, not `number`.
4106        Ty::Base(BaseType::Bytes) => "Uint8Array".to_string(),
4107        // v0.157 (ADR 0183): a generic record instantiation renders as the
4108        // erased TS generic `Name<Arg, …>`; a non-generic named type is bare.
4109        Ty::Named { name, args, .. } if args.is_empty() => name.clone(),
4110        Ty::Named { name, args, .. } => format!(
4111            "{name}<{}>",
4112            args.iter()
4113                .map(|a| ts_ty(*a, tys))
4114                .collect::<Vec<_>>()
4115                .join(", ")
4116        ),
4117        Ty::Result(t, e) => format!("Result<{}, {}>", ts_ty(*t, tys), ts_ty(*e, tys)),
4118        Ty::Option(t) => format!("Option<{}>", ts_ty(*t, tys)),
4119        Ty::Effect(t) => match &*tys.get(*t) {
4120            Ty::Unit => "Promise<void>".to_string(),
4121            _ => format!("Promise<{}>", ts_ty(*t, tys)),
4122        },
4123        Ty::HttpResult(t) => format!("HttpResult<{}>", ts_ty(*t, tys)),
4124        Ty::List(t) => format!("readonly {}[]", ts_ty(*t, tys)),
4125        // v0.91 (ADR 0119): a `Query[T]` lowers to a deferred producer of its
4126        // elements — a thunk run by the terminal.
4127        Ty::Query(t) => format!("(() => readonly {}[])", ts_ty(*t, tys)),
4128        // v0.100: a `Stream[T]` lowers to a host async iterable.
4129        Ty::Stream(t) => format!("AsyncIterable<{}>", ts_ty(*t, tys)),
4130        // v0.102: a `Connection[F]` lowers to the runtime `Connection<F>` interface.
4131        Ty::Connection(t) => format!("Connection<{}>", ts_ty(*t, tys)),
4132        Ty::Map(k, v) => format!("ReadonlyMap<{}, {}>", ts_ty(*k, tys), ts_ty(*v, tys)),
4133        Ty::QueueResult => "QueueResult".to_string(),
4134        Ty::ValidationError => "ValidationError".to_string(),
4135        Ty::JsonError => "JsonError".to_string(),
4136        Ty::Unit => "void".to_string(),
4137        Ty::Fn { params, ret } => {
4138            let params: Vec<String> = params
4139                .iter()
4140                .enumerate()
4141                .map(|(i, p)| format!("a{i}: {}", ts_ty(*p, tys)))
4142                .collect();
4143            format!("({}) => {}", params.join(", "), ts_ty(*ret, tys))
4144        }
4145        Ty::Var(n) => n.clone(),
4146        // The identity type the actor binding yields (`name.identity`).
4147        Ty::Actor(id) => ts_ty(*id, tys),
4148        // v0.52: a resolved multi-actor sum lowers to a discriminated union
4149        // tagged by actor name; non-unit members carry their identity.
4150        Ty::ActorSum(members) => members
4151            .iter()
4152            .map(|(name, id)| match &*tys.get(*id) {
4153                Ty::Unit => format!("{{ tag: \"{name}\" }}"),
4154                _ => format!("{{ tag: \"{name}\", identity: {} }}", ts_ty(*id, tys)),
4155            })
4156            .collect::<Vec<_>>()
4157            .join(" | "),
4158    }
4159}
4160
4161fn ts_binop(op: BinOp) -> &'static str {
4162    match op {
4163        // `implies` has no single TS operator — `lower_bin_op` rewrites it to
4164        // `(!(P) || Q)` before reaching here, so this arm is never used.
4165        BinOp::Implies => "||",
4166        BinOp::Or => "||",
4167        BinOp::And => "&&",
4168        BinOp::Eq => "===",
4169        BinOp::NotEq => "!==",
4170        BinOp::Lt => "<",
4171        BinOp::LtEq => "<=",
4172        BinOp::Gt => ">",
4173        BinOp::GtEq => ">=",
4174        BinOp::Add => "+",
4175        BinOp::Sub => "-",
4176        BinOp::Mul => "*",
4177        BinOp::Div => "/",
4178    }
4179}
4180
4181/// The TypeScript spelling of a user identifier in a *binding or reference*
4182/// position (params, locals, function names, import names). Bynk identifiers
4183/// that are illegal as TS binding names — the JS reserved words plus the
4184/// strict-mode/module sets (emitted modules are always strict ESM) — and
4185/// names the emitter itself introduces alongside user bindings (`deps`) are
4186/// renamed into the generated-name namespace (`__id_<name>`), which the
4187/// parser keeps free of user identifiers. Property/field names never pass
4188/// through here: reserved words are legal there, and record field names are
4189/// wire format.
4190pub(crate) fn ts_ident(name: &str) -> String {
4191    const RESERVED: &[&str] = &[
4192        // ES reserved words.
4193        "break",
4194        "case",
4195        "catch",
4196        "class",
4197        "const",
4198        "continue",
4199        "debugger",
4200        "default",
4201        "delete",
4202        "do",
4203        "else",
4204        "enum",
4205        "export",
4206        "extends",
4207        "false",
4208        "finally",
4209        "for",
4210        "function",
4211        "if",
4212        "import",
4213        "in",
4214        "instanceof",
4215        "new",
4216        "null",
4217        "return",
4218        "super",
4219        "switch",
4220        "this",
4221        "throw",
4222        "true",
4223        "try",
4224        "typeof",
4225        "var",
4226        "void",
4227        "while",
4228        "with",
4229        // Strict-mode reserved (emitted modules are always strict).
4230        "implements",
4231        "interface",
4232        "let",
4233        "package",
4234        "private",
4235        "protected",
4236        "public",
4237        "static",
4238        "yield",
4239        // Module-code reserved.
4240        "await",
4241        // Illegal binding targets in strict mode.
4242        "arguments",
4243        "eval",
4244        // Generated identifiers a user binding may sit next to: handler
4245        // signatures append a `deps` parameter, so a user param named `deps`
4246        // would otherwise duplicate it.
4247        "deps",
4248    ];
4249    if RESERVED.contains(&name) {
4250        format!("__id_{name}")
4251    } else {
4252        name.to_string()
4253    }
4254}
4255
4256/// Delegates to `bynk_check::wire_default::escape_ts_literal` — the two
4257/// splice into generated TypeScript from opposite sides (real emission here,
4258/// event-field wire defaults there), so a correction to the escaping rules
4259/// must land once, not drift between two copies.
4260pub(crate) fn escape_ts_string(s: &str) -> String {
4261    bynk_check::wire_default::escape_ts_literal(s)
4262}
4263
4264/// #661 (Decision D)/#70 review: the one `PredKind` → runtime-check mapping,
4265/// shared by the owner-side check (`emit::emit_pred_check`, over a `value`
4266/// binding) and the boundary-side inline check
4267/// (`serialisation::emit_inline_pred_check`, over a `json` binding) — the
4268/// two used to hand-roll this mapping independently, pinned identical only by
4269/// a comment, so amending one (e.g. the `Matches` regex's `^(?:…)$` anchoring)
4270/// could silently drift from the other. `receiver` is the bound name the
4271/// generated condition reads (`value` or `json`); the returned message is the
4272/// same either side of the boundary by construction.
4273pub(crate) fn pred_condition_and_message(pred: &PredKind, receiver: &str) -> (String, String) {
4274    match pred {
4275        PredKind::NonNegative => (
4276            format!("{receiver} >= 0"),
4277            "must be non-negative".to_string(),
4278        ),
4279        PredKind::Positive => (format!("{receiver} > 0"), "must be positive".to_string()),
4280        PredKind::InRange(a, b) => {
4281            let (a, b) = (a.value, b.value);
4282            (
4283                format!("{receiver} >= {a} && {receiver} <= {b}"),
4284                format!("must be in range [{a}, {b}]"),
4285            )
4286        }
4287        PredKind::InRangeF(a, b) => {
4288            let (a, b) = (&a.lexeme, &b.lexeme);
4289            (
4290                format!("{receiver} >= {a} && {receiver} <= {b}"),
4291                format!("must be in range [{a}, {b}]"),
4292            )
4293        }
4294        PredKind::NonEmpty => (
4295            format!("{receiver}.length > 0"),
4296            "must be non-empty".to_string(),
4297        ),
4298        PredKind::MinLength(n) => (
4299            format!("{receiver}.length >= {n}"),
4300            format!("length must be at least {n}"),
4301        ),
4302        PredKind::MaxLength(n) => (
4303            format!("{receiver}.length <= {n}"),
4304            format!("length must be at most {n}"),
4305        ),
4306        PredKind::Length(n) => (
4307            format!("{receiver}.length === {n}"),
4308            format!("length must be exactly {n}"),
4309        ),
4310        PredKind::Matches(pat) => {
4311            let escaped = escape_ts_string(pat);
4312            (
4313                format!("new RegExp(\"^(?:\" + \"{escaped}\" + \")$\").test({receiver})"),
4314                format!("must match /{escaped}/"),
4315            )
4316        }
4317    }
4318}
4319
4320#[allow(dead_code)]
4321fn _unused_hashmap(_h: HashMap<String, ()>) {}
4322
4323#[cfg(test)]
4324mod runtime_tests {
4325    use super::*;
4326
4327    #[test]
4328    fn runtime_emits_all_required_exports() {
4329        let s = emit_runtime_module();
4330        // Core types and constructors used by every emitted module.
4331        assert!(s.contains("export type Result<T, E>"));
4332        assert!(s.contains("export const Ok"));
4333        assert!(s.contains("export const Err"));
4334        assert!(s.contains("export type Option<T>"));
4335        assert!(s.contains("export const Some"));
4336        assert!(s.contains("export const None"));
4337        assert!(s.contains("export interface ValidationError"));
4338        // Durable Object surface used by agent classes.
4339        assert!(s.contains("export interface DurableObjectStorage"));
4340        assert!(s.contains("export interface DurableObjectState"));
4341        assert!(s.contains("export class InMemoryStorage"));
4342        assert!(s.contains("export function makeTestState"));
4343        // Discriminator must be `tag` to match emitted code.
4344        assert!(s.contains("tag: \"Ok\""));
4345        assert!(s.contains("tag: \"Err\""));
4346        assert!(s.contains("tag: \"Some\""));
4347        assert!(s.contains("tag: \"None\""));
4348    }
4349
4350    #[test]
4351    fn tsconfig_is_well_formed_json() {
4352        let s = emit_tsconfig();
4353        // Spot-check the key fields; we don't reach for a JSON parser.
4354        assert!(s.contains("\"target\": \"ES2022\""));
4355        assert!(s.contains("\"strict\": true"));
4356        assert!(s.contains("\"include\""));
4357    }
4358
4359    #[test]
4360    fn coverage_tsconfig_enables_source_maps() {
4361        // #854: the coverage remap consumes tsc's `.js.map`s, so the variant must
4362        // set `sourceMap` — a guard against a silent string-replace miss if the
4363        // base config's `outDir` line is ever reworded. The default stays map-free
4364        // so a normal `bynkc test` / deployment build ships no `.js.map`s.
4365        let cov = emit_tsconfig_with_source_maps();
4366        assert!(
4367            cov.contains("\"sourceMap\": true"),
4368            "coverage config: {cov}"
4369        );
4370        assert!(cov.contains("\"outDir\": \"../out-js\""));
4371        assert!(!emit_tsconfig().contains("sourceMap"));
4372    }
4373
4374    #[test]
4375    fn workers_dir_name_replaces_dots_with_dashes() {
4376        assert_eq!(
4377            crate::project::worker_dir_name("commerce.payment"),
4378            "commerce-payment"
4379        );
4380        assert_eq!(crate::project::worker_dir_name("a.b.c"), "a-b-c");
4381    }
4382
4383    // Refactor track: characterisation pin for the canonical `escape_ts_string`.
4384    // It escapes backslash/quote/newline/tab and carriage return (`\r` → `\r`).
4385    #[test]
4386    fn escape_ts_string_escapes_cr() {
4387        assert_eq!(escape_ts_string("a\\b"), "a\\\\b");
4388        assert_eq!(escape_ts_string("a\"b"), "a\\\"b");
4389        assert_eq!(escape_ts_string("a\nb"), "a\\nb");
4390        assert_eq!(escape_ts_string("a\tb"), "a\\tb");
4391        assert_eq!(escape_ts_string("a\rb"), "a\\rb"); // CR escaped here; raw in project copy
4392    }
4393
4394    #[test]
4395    fn runtime_import_depth_resolves_correctly() {
4396        assert_eq!(
4397            runtime_import_for(Path::new("compose.ts"), ImportExt::Js),
4398            "./runtime.js"
4399        );
4400        assert_eq!(
4401            runtime_import_for(Path::new("commerce/payment.ts"), ImportExt::Js),
4402            "../runtime.js"
4403        );
4404        assert_eq!(
4405            runtime_import_for(Path::new("commerce/orders/types.ts"), ImportExt::Js),
4406            "../../runtime.js"
4407        );
4408        assert_eq!(
4409            runtime_import_for(Path::new("tests/commerce_payment.test.ts"), ImportExt::Js),
4410            "../runtime.js"
4411        );
4412    }
4413}
4414
4415/// Which conditional runtime helpers a module's import line ends up carrying.
4416///
4417/// These drive the single-file `emit()` path end-to-end (parse → resolve → check
4418/// → emit), so they exercise the real producers rather than the accumulator in
4419/// isolation. Before `RuntimeUse`, the decision was `body.contains("__bynkBytes")`
4420/// — a scan of the generated text — and `escapes_a_marker_in_a_string_literal`
4421/// below is the case that got wrong.
4422#[cfg(test)]
4423mod conditional_runtime_import_tests {
4424    use crate::testkit::{emit_bundle, emit_source};
4425
4426    /// The import line is the first `import { … } from "./runtime.js"` in the
4427    /// emitted module.
4428    fn runtime_import_line(ts: &str) -> &str {
4429        ts.lines()
4430            .find(|l| l.starts_with("import {") && l.contains("runtime.js"))
4431            .unwrap_or("")
4432    }
4433
4434    #[test]
4435    fn bytes_helpers_are_imported_when_a_bytes_value_is_built() {
4436        let ts = emit_source(
4437            "commons b\n\nfn decode(s: String) -> Option[Bytes] {\n  Bytes.fromBase64(s)\n}\n",
4438        );
4439        assert!(
4440            runtime_import_line(&ts).contains("__bynkBytesFromBase64"),
4441            "{ts}"
4442        );
4443    }
4444
4445    #[test]
4446    fn bytes_helpers_are_imported_for_content_equality() {
4447        let ts = emit_source("commons b\n\nfn same(a: Bytes, b: Bytes) -> Bool {\n  a == b\n}\n");
4448        assert!(
4449            runtime_import_line(&ts).contains("__bynkBytesEqual"),
4450            "{ts}"
4451        );
4452    }
4453
4454    #[test]
4455    fn bytes_helpers_are_absent_from_a_module_that_never_uses_bytes() {
4456        let ts = emit_source("commons b\n\nfn double(n: Int) -> Int {\n  n * 2\n}\n");
4457        assert!(!runtime_import_line(&ts).contains("__bynkBytes"), "{ts}");
4458    }
4459
4460    /// The regression this replaced a text scan for: a `Bytes` helper name
4461    /// appearing inside a user **string literal** is not a reference to the
4462    /// helper, and must not pull the import in. `body.contains("__bynkBytes")`
4463    /// could not tell the two apart, because the literal is emitted verbatim
4464    /// into the same buffer it scanned.
4465    #[test]
4466    fn escapes_a_marker_in_a_string_literal() {
4467        let ts = emit_source("commons b\n\nfn label() -> String {\n  \"__bynkBytesEqual\"\n}\n");
4468        assert!(
4469            ts.contains("\"__bynkBytesEqual\""),
4470            "the literal should survive into the body: {ts}"
4471        );
4472        assert!(
4473            !runtime_import_line(&ts).contains("__bynkBytes"),
4474            "a marker inside a string literal is not a helper reference: {ts}"
4475        );
4476    }
4477
4478    // -- the ICU formatters ---------------------------------------------------
4479
4480    const ICU_HELPERS: [&str; 3] = ["selectPluralArm", "formatIcuNumber", "formatIcuDate"];
4481
4482    /// The case the per-arm recording exists for. A `select` placeholder lowers to
4483    /// `Object.hasOwn` over an arm table and calls no formatter, so a bundle whose
4484    /// only ICU construct is a `select` must import none of the three — recording
4485    /// once per placeholder instead of per arm would import all three here.
4486    #[test]
4487    fn a_select_only_bundle_imports_no_icu_formatter() {
4488        let ts = emit_bundle(
4489            "messages \"en\" @reference {\n  \"greeting\" => \"{g, select, male {He} female {She} other {They}} liked this.\"\n}\n",
4490        );
4491        assert!(
4492            ts.contains("Object.hasOwn"),
4493            "the select arm table should have been emitted, else this proves nothing: {ts}"
4494        );
4495        for helper in ICU_HELPERS {
4496            assert!(
4497                !runtime_import_line(&ts).contains(helper),
4498                "a select-only bundle calls no formatter, so `{helper}` must not be imported: {ts}"
4499            );
4500        }
4501    }
4502
4503    /// The opposite direction: a `plural` placeholder does call a formatter, and
4504    /// the three are imported as a group.
4505    #[test]
4506    fn a_plural_bundle_imports_the_icu_formatters() {
4507        let ts = emit_bundle(
4508            "messages \"en\" @reference {\n  \"cart\" => \"You have {n, plural, one {# item} other {# items}} in your cart\"\n}\n",
4509        );
4510        assert!(
4511            ts.contains("selectPluralArm("),
4512            "the plural dispatch should have been emitted: {ts}"
4513        );
4514        for helper in ICU_HELPERS {
4515            assert!(
4516                runtime_import_line(&ts).contains(helper),
4517                "`{helper}` should be imported for a plural bundle: {ts}"
4518            );
4519        }
4520    }
4521
4522    /// A bundle with no ICU dispatch at all — a plain `{name}` placeholder goes
4523    /// through `renderArg`, not a formatter.
4524    #[test]
4525    fn a_plain_placeholder_bundle_imports_no_icu_formatter() {
4526        let ts =
4527            emit_bundle("messages \"en\" @reference {\n  \"hello\" => \"Hello, {name}!\"\n}\n");
4528        for helper in ICU_HELPERS {
4529            assert!(
4530                !runtime_import_line(&ts).contains(helper),
4531                "`{helper}` must not be imported for a bundle with no ICU dispatch: {ts}"
4532            );
4533        }
4534    }
4535}
4536
4537/// #914: `inject_runtime_imports` must not add a binding the target line already
4538/// has. The test-scaffold module lists `Ok`/`Err`/`Result` but not
4539/// `BoundaryError`, so the boundary group is a partial overlap — injecting it
4540/// wholesale would emit a duplicate identifier, trading one uncompilable module
4541/// for another.
4542#[cfg(test)]
4543mod inject_runtime_imports_tests {
4544    use super::*;
4545
4546    const SPEC: &str = "./runtime.js";
4547
4548    fn line(bindings: &str) -> String {
4549        format!("import {{ {bindings} }} from \"{SPEC}\";\nconst x = 1;\n")
4550    }
4551
4552    #[test]
4553    fn appends_bindings_that_are_absent() {
4554        let out = inject_runtime_imports(line("Ok, Err"), SPEC, BYTES_RUNTIME_IMPORTS);
4555        assert!(out.contains("Ok, Err, __bynkBytesEqual"), "{out}");
4556        assert!(out.contains("__bynkBytesDecodeUtf8 } from"), "{out}");
4557    }
4558
4559    #[test]
4560    fn skips_bindings_already_present() {
4561        let out = inject_runtime_imports(
4562            line("Ok, Err, type Result"),
4563            SPEC,
4564            BOUNDARY_CODEC_RUNTIME_IMPORTS,
4565        );
4566        assert_eq!(
4567            out.matches("Ok").count(),
4568            1,
4569            "`Ok` was already imported and must not repeat: {out}"
4570        );
4571        assert!(
4572            out.contains("Ok, Err, type Result, type BoundaryError"),
4573            "{out}"
4574        );
4575    }
4576
4577    /// The bare name is what collides, so `type BoundaryError` must match an
4578    /// existing `BoundaryError`.
4579    #[test]
4580    fn matches_a_type_prefixed_group_binding_against_a_bare_one() {
4581        let out = inject_runtime_imports(
4582            line("Ok, Err, type Result, BoundaryError"),
4583            SPEC,
4584            BOUNDARY_CODEC_RUNTIME_IMPORTS,
4585        );
4586        assert_eq!(
4587            out,
4588            line("Ok, Err, type Result, BoundaryError"),
4589            "every binding was already present, so the line is untouched"
4590        );
4591    }
4592
4593    /// …and the other direction: a bare group binding against an existing
4594    /// `type`-prefixed one. This is the case that would regress if `bare` were
4595    /// applied to only one side of the comparison.
4596    #[test]
4597    fn matches_a_bare_group_binding_against_a_type_prefixed_one() {
4598        // A group whose bindings are bare, against a line that `type`-prefixes
4599        // them. `Result` is the realistic instance — the fixed test-scaffold
4600        // list writes `type Result`.
4601        let out = inject_runtime_imports(line("type Ok, type Err"), SPEC, ", Ok, Err");
4602        assert_eq!(
4603            out,
4604            line("type Ok, type Err"),
4605            "a bare binding must match an existing `type`-prefixed one: {out}"
4606        );
4607    }
4608
4609    /// The two injections run back to back over the same line, so the second
4610    /// sees the first's output as `existing` — the overlap between the groups
4611    /// (`Ok`, `Err`, `type Result`) must not double up.
4612    #[test]
4613    fn composes_across_two_sequential_injections() {
4614        let out = inject_runtime_imports(
4615            line("Ok, Err, type Result"),
4616            SPEC,
4617            BOUNDARY_CODEC_RUNTIME_IMPORTS,
4618        );
4619        let out = inject_runtime_imports(out, SPEC, JSON_CODEC_RUNTIME_IMPORTS);
4620        assert_eq!(
4621            out,
4622            line("Ok, Err, type Result, type BoundaryError, type JsonValue, type JsonError"),
4623            "the shared bindings must be injected once: {out}"
4624        );
4625    }
4626
4627    #[test]
4628    fn leaves_a_line_for_another_specifier_alone() {
4629        let other = "import { Ok } from \"./elsewhere.js\";\n".to_string();
4630        assert_eq!(
4631            inject_runtime_imports(other.clone(), SPEC, BYTES_RUNTIME_IMPORTS),
4632            other
4633        );
4634    }
4635}