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};
22
23use self::source_map::SourceMapBuilder;
24
25use crate::project::{BuildTarget, EmitProjectCtx, ImportExt, UnitKind};
26use bynk_check::builtin_names::methods::{FOLD_EFF, RAW};
27use bynk_check::builtin_names::types::*;
28use bynk_check::checker::{NamedKind, Ty, TypedCommons};
29use bynk_syntax::ast::*;
30
31pub mod serialisation;
32pub mod workers;
33pub mod workers_entry;
34pub mod wrangler;
35
36pub use workers::emit_worker_compose;
37pub use workers_entry::emit_worker_entry;
38pub use wrangler::emit_wrangler_toml;
39
40mod lower;
41pub(crate) mod source_map;
42pub(crate) use lower::*;
43mod emit;
44pub(crate) use emit::*;
45pub(crate) mod websocket;
46
47const INDENT_STEP: usize = 2;
48
49/// Emit the contents of `out/runtime.ts`. This module ships with every
50/// project so the per-context / per-test emissions can `import { Ok, Err,
51/// Some, None, ... }` from a single source. It includes:
52///
53/// - `Result`/`Option` discriminated unions (using `tag` for the
54///   discriminant — same shape user sum types lower to).
55/// - `ValidationError` (the record shape refined-value constructors return).
56/// - The `DurableObjectState`/`DurableObjectStorage` interfaces that agent
57///   classes consume, plus an `InMemoryStorage` implementation and a
58///   `makeTestState(name)` factory for use in test execution.
59///
60/// The content is identical across projects — there is no per-project
61/// tailoring. Dead code is harmless; tsc handles it.
62pub fn emit_runtime_module() -> String {
63    RUNTIME_TS.to_string()
64}
65
66/// The embedded runtime. This is a BUILD OUTPUT, not a hand-edited file: it is
67/// bundled from the focused TypeScript modules in `bynk-emit/runtime/src` by
68/// that package's `scripts/bundle.mjs`. Edit the modules there and run
69/// `npm run bundle` (CI's `runtime` job guards against drift); never edit this
70/// file by hand. Keeping it a committed artifact means `cargo build` stays
71/// Node-free and the emitter stays lockstep with the runtime it embeds.
72const RUNTIME_TS: &str = include_str!("emitter/runtime.ts");
73
74/// Emit the contents of `out/tsconfig.json`. The CLI uses `tsc -p` against
75/// this when running `bynkc test`; users can also drive `tsc` against it
76/// directly to produce JS for deployment.
77pub fn emit_tsconfig() -> String {
78    TSCONFIG_JSON.to_string()
79}
80
81const TSCONFIG_JSON: &str = r#"{
82  "compilerOptions": {
83    "target": "ES2022",
84    "module": "NodeNext",
85    "moduleResolution": "NodeNext",
86    "strict": true,
87    "noImplicitAny": true,
88    "esModuleInterop": true,
89    "skipLibCheck": true,
90    "resolveJsonModule": true,
91    "isolatedModules": true,
92    "noEmit": false,
93    "outDir": "../out-js",
94    "rootDir": "."
95  },
96  "include": ["**/*.ts"]
97}
98"#;
99
100/// Compute the runtime import specifier for a module at `from_source`. For a
101/// file at `commerce/payment.ts` the runtime sits two levels up, so this
102/// returns `../runtime.js`; for a top-level file it returns `./runtime.js`.
103pub fn runtime_import_for(from_source: &Path, ext: ImportExt) -> String {
104    let depth = from_source
105        .parent()
106        .map(|p| {
107            p.components()
108                .filter(|c| matches!(c, std::path::Component::Normal(_)))
109                .count()
110        })
111        .unwrap_or(0);
112    let ext = ext.as_str();
113    if depth == 0 {
114        format!("./runtime.{ext}")
115    } else {
116        let prefix: String = "../".repeat(depth);
117        format!("{prefix}runtime.{ext}")
118    }
119}
120
121/// Emit TypeScript source for the typed commons (single-file mode).
122pub fn emit(commons: &TypedCommons) -> String {
123    // Emit the body first so the header can decide which runtime helpers to
124    // import from what the body actually references (v0.110: the `__bynkBytes*`
125    // helpers are imported only when a `Bytes` value is constructed/compared).
126    let mut body = String::new();
127    write_commons_doc(&mut body, commons);
128    let dummy_ctx = single_file_ctx();
129    // Types come first (they define interfaces and namespaces).
130    for item in &commons.commons.items {
131        if let CommonsItem::Type(t) = item {
132            emit_type(&mut body, t, commons, &dummy_ctx);
133        }
134    }
135    // Free functions afterward.
136    for item in &commons.commons.items {
137        if let CommonsItem::Fn(f) = item
138            && let FnName::Free(_) = &f.name
139        {
140            emit_free_fn(&mut body, f, commons, None);
141        }
142    }
143    // v0.22b: module-local codec helpers for Json.encode/decode targets.
144    emit_json_codec_helpers(
145        &mut body,
146        commons,
147        &dummy_ctx,
148        &HashSet::new(),
149        &HashSet::new(),
150    );
151    let mut out = String::new();
152    write_header_single(&mut out, commons, body.contains("__bynkBytes"));
153    out.push_str(&body);
154    out
155}
156
157/// A no-op project context for single-file emission. Single-file mode never
158/// involves contexts or cross-unit imports, so most fields default to empty.
159fn single_file_ctx() -> EmitProjectCtx {
160    EmitProjectCtx {
161        import_ext: crate::project::ImportExt::Js,
162        source_path: PathBuf::new(),
163        commons_name: String::new(),
164        local_files: Vec::new(),
165        file_decl_index: crate::project::FileDeclIndex {
166            types: HashMap::new(),
167            fns: HashMap::new(),
168            methods: HashMap::new(),
169        },
170        imported_from: HashMap::new(),
171        imported_from_kind: HashMap::new(),
172        imported_decl_paths: HashMap::new(),
173        commons_dir: PathBuf::new(),
174        unit_kind: UnitKind::Commons,
175        owning_context: None,
176        exports_local: HashMap::new(),
177        exports_for_consumed: HashMap::new(),
178        consumed_types: HashMap::new(),
179        cross_context: bynk_check::resolver::CrossContextInfo::default(),
180        is_consumed_by_others: false,
181        target: BuildTarget::Bundle,
182        boundary_type_owners: HashMap::new(),
183        local_agents: HashSet::new(),
184        actors: HashMap::new(),
185        consumed_adapters: HashSet::new(),
186    }
187}
188
189/// Emit TypeScript source for a single file inside a multi-file project,
190/// including cross-file and cross-commons imports computed from
191/// [`EmitProjectCtx`].
192/// Emit one unit's TypeScript, plus its source map (slice 1, ADR 0103).
193///
194/// `source_text` is the originating `.bynk` file's text and `source_name` its
195/// project-root-relative path; together they let the source-map builder resolve
196/// each recorded span to a `(line, col)` and embed `sourcesContent`. Returns the
197/// generated TS and the serialised source-map v3 JSON (`None` when nothing
198/// mapped — e.g. a unit whose items all came from sibling files).
199pub fn emit_project(
200    commons: &TypedCommons,
201    ctx: &EmitProjectCtx,
202    source_text: &str,
203    source_name: &str,
204) -> (String, Option<String>) {
205    let mut out = String::new();
206    // The file's source-map builder. The free-function bodies record statement /
207    // match-arm checkpoints through their `LowerCtx`; the declaration loops below
208    // record one checkpoint per top-level item so signatures (and the bodies of
209    // services/agents, which lower via spliced local buffers) anchor to their
210    // declaration (ADR 0103 D2, nearest-enclosing).
211    let smb = RefCell::new(SourceMapBuilder::new());
212    // The file's `.bynk` source is the primary map source (id 0); `record` targets
213    // it and spliced handler bodies in the same file merge against it (v0.70).
214    smb.borrow_mut().add_source(source_name, source_text);
215    write_header(&mut out, commons, ctx);
216    // Compute which names this file actually references that live elsewhere
217    // (sibling file in the same commons/context, or a used commons / consumed
218    // context).
219    let references = collect_external_references(commons, ctx);
220    emit_project_imports(&mut out, commons, ctx, &references);
221    if !references.is_empty() {
222        writeln!(out).unwrap();
223    }
224    // v0.6: namespace imports for each consumed context that exposes services.
225    // v0.15: also for consumed contexts whose capabilities this context uses.
226    emit_cross_context_namespace_imports(&mut out, commons, ctx);
227    // For contexts: emit per-context nominal rebrand aliases for each type
228    // imported via `uses` that this file references. The structural shape is
229    // inherited from the original commons type; the brand makes the
230    // rebranded type nominally distinct (v0.4 §6.2).
231    if ctx.unit_kind == UnitKind::Context {
232        emit_context_rebrands(&mut out, &references, commons, ctx);
233    }
234    write_commons_doc(&mut out, commons);
235    for item in &commons.commons.items {
236        if let CommonsItem::Type(t) = item {
237            smb.borrow_mut().record(out.len(), t.span);
238            emit_type(&mut out, t, commons, ctx);
239        }
240    }
241    for item in &commons.commons.items {
242        if let CommonsItem::Fn(f) = item
243            && let FnName::Free(_) = &f.name
244        {
245            smb.borrow_mut().record(out.len(), f.span);
246            emit_free_fn(&mut out, f, commons, Some(&smb));
247        }
248    }
249    // v0.5: behavioural items follow the type/fn declarations.
250    for item in &commons.commons.items {
251        match item {
252            CommonsItem::Capability(c) => {
253                smb.borrow_mut().record(out.len(), c.span);
254                emit_capability(&mut out, c);
255            }
256            CommonsItem::Provider(p) => {
257                smb.borrow_mut().record(out.len(), p.span);
258                emit_provider(&mut out, p, commons, ctx, Some(&smb));
259            }
260            CommonsItem::Service(s) => {
261                smb.borrow_mut().record(out.len(), s.span);
262                emit_service(&mut out, s, commons, ctx, Some(&smb));
263            }
264            CommonsItem::Agent(a) => {
265                smb.borrow_mut().record(out.len(), a.span);
266                emit_agent(&mut out, a, commons, ctx, Some(&smb));
267            }
268            _ => {}
269        }
270    }
271    // v0.9.2: per-test registry reset. The test runner calls this before each
272    // test so a fresh test sees clean agent state (finding #10's "fresh per
273    // test" half).
274    let agent_names: Vec<&str> = commons
275        .commons
276        .items
277        .iter()
278        .filter_map(|i| match i {
279            CommonsItem::Agent(a) => Some(a.name.name.as_str()),
280            _ => None,
281        })
282        .collect();
283    if !agent_names.is_empty() {
284        writeln!(out, "export function __resetAgents(): void {{").unwrap();
285        for name in &agent_names {
286            writeln!(out, "  {}.reset();", agent_registry_name(name)).unwrap();
287        }
288        writeln!(out, "}}").unwrap();
289        writeln!(out).unwrap();
290    }
291    // v0.6: cross-context surface assembly. Emit `makeSurface` for any
292    // context that declares services — the composition root references it
293    // for every such context, not just those consumed by others. Skipped
294    // in workers mode where each Worker has its own `compose(env)` root.
295    if ctx.unit_kind == UnitKind::Context && matches!(ctx.target, BuildTarget::Bundle) {
296        let has_services = commons
297            .commons
298            .items
299            .iter()
300            .any(|i| matches!(i, CommonsItem::Service(_)));
301        if has_services {
302            emit_make_surface(&mut out, commons, ctx);
303        }
304    }
305    // v0.8: in workers mode, the context module also exports per-type
306    // serialise/deserialise helpers for every type that crosses a
307    // boundary. The commons modules likewise carry helpers for their
308    // own commons-declared boundary types.
309    // v0.96 (ADR 0124): runs on both targets — workers emits service-call +
310    // agent-rehydration boundary helpers; bundle emits only the agent-rehydration
311    // ones (the gate's deserialisers), since in-process calls need no wire codec.
312    let (boundary_names, boundary_insts) = emit_boundary_helpers(&mut out, commons, ctx);
313    // v0.22b: module-local codec helpers for this file's Json.encode/decode
314    // targets, deduped against the workers boundary helpers above.
315    emit_json_codec_helpers(&mut out, commons, ctx, &boundary_names, &boundary_insts);
316    // The generated `file` name: the source basename with `.bynk` → `.ts`.
317    let generated_file = Path::new(source_name)
318        .file_stem()
319        .map(|s| format!("{}.ts", s.to_string_lossy()))
320        .unwrap_or_else(|| "module.ts".to_string());
321    let source_map = smb.borrow().to_v3(&out, &generated_file);
322    // v0.110 (ADR 0142): import the `Bytes` runtime helpers iff the emitted body
323    // actually references them. Injected into the existing runtime import line
324    // (no new line, no body-column shift), so the source map computed above from
325    // the pre-injection text stays valid.
326    if out.contains("__bynkBytes") {
327        out = inject_bytes_runtime_imports(out);
328    }
329    (out, source_map)
330}
331
332/// v0.110 (ADR 0142): append the `Bytes` runtime helpers to a module's existing
333/// `./runtime.js` import (the line carrying `type ValidationError`). Done as a
334/// post-pass so the decision keys on what the body references, without a second
335/// emission or a source-map-shifting reorder.
336fn inject_bytes_runtime_imports(out: String) -> String {
337    let mut result = String::with_capacity(out.len() + BYTES_RUNTIME_IMPORTS.len());
338    let mut injected = false;
339    for line in out.split_inclusive('\n') {
340        if !injected
341            && line.starts_with("import {")
342            && line.contains("type ValidationError")
343            && let Some(pos) = line.rfind(" } from \"")
344        {
345            result.push_str(&line[..pos]);
346            result.push_str(BYTES_RUNTIME_IMPORTS);
347            result.push_str(&line[pos..]);
348            injected = true;
349            continue;
350        }
351        result.push_str(line);
352    }
353    result
354}
355
356/// v0.22b: pre-order expression visitor — visits `e`, then every
357/// sub-expression, including statements and tails of nested blocks.
358fn walk_exprs(e: &Expr, f: &mut impl FnMut(&Expr)) {
359    f(e);
360    match &e.kind {
361        ExprKind::IntLit(_)
362        | ExprKind::FloatLit { .. }
363        | ExprKind::DurationLit { .. }
364        | ExprKind::StrLit(_)
365        | ExprKind::BoolLit(_)
366        | ExprKind::Ident(_)
367        | ExprKind::None
368        | ExprKind::UnitLit => {}
369        // v0.43: visit each interpolation hole's expression.
370        ExprKind::InterpStr(parts) => {
371            for part in parts {
372                if let InterpPart::Hole(hole) = part {
373                    walk_exprs(hole, f);
374                }
375            }
376        }
377        ExprKind::Lambda(l) => walk_exprs(&l.body, f),
378        ExprKind::EffectPure(i)
379        | ExprKind::Expect(i)
380        | ExprKind::UnaryOp(_, i)
381        | ExprKind::Paren(i)
382        | ExprKind::Ok(i)
383        | ExprKind::Err(i)
384        | ExprKind::Some(i)
385        | ExprKind::Question(i) => walk_exprs(i, f),
386        ExprKind::Val { args, .. }
387        | ExprKind::Call { args, .. }
388        | ExprKind::ConstructorCall { args, .. } => {
389            for a in args {
390                walk_exprs(a, f);
391            }
392        }
393        ExprKind::ListLit(elems) => {
394            for el in elems {
395                walk_exprs(el, f);
396            }
397        }
398        ExprKind::RecordConstruction { fields, .. } => {
399            for fld in fields {
400                if let Some(v) = &fld.value {
401                    walk_exprs(v, f);
402                }
403            }
404        }
405        ExprKind::RecordSpread {
406            base, overrides, ..
407        } => {
408            walk_exprs(base, f);
409            for fld in overrides {
410                if let Some(v) = &fld.value {
411                    walk_exprs(v, f);
412                }
413            }
414        }
415        ExprKind::BinOp(_, l, r) => {
416            walk_exprs(l, f);
417            walk_exprs(r, f);
418        }
419        ExprKind::Block(b) => walk_block_exprs(b, f),
420        ExprKind::If {
421            cond,
422            then_block,
423            else_block,
424        } => {
425            walk_exprs(cond, f);
426            walk_block_exprs(then_block, f);
427            walk_block_exprs(else_block, f);
428        }
429        ExprKind::FieldAccess { receiver, .. } => walk_exprs(receiver, f),
430        ExprKind::MethodCall { receiver, args, .. } => {
431            walk_exprs(receiver, f);
432            for a in args {
433                walk_exprs(a, f);
434            }
435        }
436        ExprKind::Match { discriminant, arms } => {
437            walk_exprs(discriminant, f);
438            for arm in arms {
439                match &arm.body {
440                    MatchBody::Expr(e) => walk_exprs(e, f),
441                    MatchBody::Block(b) => walk_block_exprs(b, f),
442                }
443            }
444        }
445        ExprKind::Is { value, .. } => walk_exprs(value, f),
446    }
447}
448
449/// v0.79: does this block contain a `~>` send anywhere — including nested
450/// branches, match arms, and lambdas? Gates execution-context threading
451/// (`deps.__exec`) so a context that never sends keeps byte-identical output.
452pub(crate) fn block_uses_send(b: &Block) -> bool {
453    fn stmt(s: &Statement) -> bool {
454        match s {
455            Statement::Send(_) => true,
456            Statement::Let(l) | Statement::EffectLet(l) => expr(&l.value),
457            Statement::Expect(a) => expr(&a.value),
458            Statement::Assign(a) => expr(&a.value),
459        }
460    }
461    fn expr(e: &Expr) -> bool {
462        match &e.kind {
463            ExprKind::Block(b) => block_uses_send(b),
464            ExprKind::If {
465                cond,
466                then_block,
467                else_block,
468            } => expr(cond) || block_uses_send(then_block) || block_uses_send(else_block),
469            ExprKind::Match { discriminant, arms } => {
470                expr(discriminant)
471                    || arms.iter().any(|a| match &a.body {
472                        MatchBody::Expr(e) => expr(e),
473                        MatchBody::Block(b) => block_uses_send(b),
474                    })
475            }
476            ExprKind::Lambda(l) => expr(&l.body),
477            _ => false,
478        }
479    }
480    b.statements.iter().any(stmt) || expr(&b.tail)
481}
482
483/// v0.81–v0.87: does this block write durable state — a `:=` `Cell` write, a
484/// mutating storage-`Map`/`Cache` op (`put`/`remove`/`update`/`upsert`), or a
485/// The names of an agent's `store` fields grouped by kind:
486/// `(maps, sets, caches, logs, cells)`. Threaded through the write-detection
487/// walk so each kind's mutating ops can be recognised.
488type StoreKinds<'a> = (
489    &'a HashSet<String>,
490    &'a HashSet<String>,
491    &'a HashSet<String>,
492    &'a HashSet<String>,
493    &'a HashSet<String>,
494);
495
496/// mutating `Set` op (`add`/`remove`) on a `store` field — anywhere, including
497/// nested `if`/`match`/block expressions? Drives whether a store-agent handler
498/// needs the implicit-commit wrapper (read-only handlers skip it). The kinds are
499/// `(maps, sets, caches, logs, cells)`; all empty for a read-only agent.
500pub(crate) fn block_writes_state(b: &Block, m: StoreKinds<'_>) -> bool {
501    fn mutating_op(e: &Expr, (maps, sets, caches, logs, cells): StoreKinds<'_>) -> bool {
502        if let ExprKind::MethodCall {
503            receiver, method, ..
504        } = &e.kind
505            && let ExprKind::Ident(id) = &receiver.kind
506        {
507            if (maps.contains(&id.name) || caches.contains(&id.name))
508                && matches!(method.name.as_str(), "put" | "remove" | "update" | "upsert")
509            {
510                return true;
511            }
512            if sets.contains(&id.name) && matches!(method.name.as_str(), "add" | "remove") {
513                return true;
514            }
515            // v0.95: `Log.append` mutates the durable array.
516            if logs.contains(&id.name) && method.name == "append" {
517                return true;
518            }
519            // v0.98 (ADR 0125): `Cell.update` is a read-modify-write of the
520            // working state, so a handler whose only mutation is `cell.update`
521            // still needs the end-of-handler commit flush.
522            if cells.contains(&id.name) && method.name == "update" {
523                return true;
524            }
525        }
526        false
527    }
528    fn stmt(s: &Statement, m: StoreKinds<'_>) -> bool {
529        match s {
530            Statement::Assign(_) => true,
531            Statement::Let(l) | Statement::EffectLet(l) => expr(&l.value, m),
532            Statement::Expect(a) => expr(&a.value, m),
533            Statement::Send(s) => expr(&s.value, m),
534        }
535    }
536    fn expr(e: &Expr, m: StoreKinds<'_>) -> bool {
537        if mutating_op(e, m) {
538            return true;
539        }
540        match &e.kind {
541            ExprKind::Block(b) => block_writes_state(b, m),
542            ExprKind::If {
543                cond,
544                then_block,
545                else_block,
546            } => {
547                expr(cond, m)
548                    || block_writes_state(then_block, m)
549                    || block_writes_state(else_block, m)
550            }
551            ExprKind::Match { discriminant, arms } => {
552                expr(discriminant, m)
553                    || arms.iter().any(|a| match &a.body {
554                        MatchBody::Expr(e) => expr(e, m),
555                        MatchBody::Block(b) => block_writes_state(b, m),
556                    })
557            }
558            ExprKind::Paren(inner) => expr(inner, m),
559            ExprKind::MethodCall { receiver, args, .. } => {
560                expr(receiver, m) || args.iter().any(|x| expr(x, m))
561            }
562            ExprKind::Call { args, .. } => args.iter().any(|x| expr(x, m)),
563            _ => false,
564        }
565    }
566    b.statements.iter().any(|s| stmt(s, m)) || expr(&b.tail, m)
567}
568
569fn walk_block_exprs(b: &Block, f: &mut impl FnMut(&Expr)) {
570    for s in &b.statements {
571        match s {
572            Statement::Let(l) | Statement::EffectLet(l) => walk_exprs(&l.value, f),
573            Statement::Expect(a) => walk_exprs(&a.value, f),
574            Statement::Send(s) => walk_exprs(&s.value, f),
575            Statement::Assign(a) => walk_exprs(&a.value, f),
576        }
577    }
578    walk_exprs(&b.tail, f);
579}
580
581/// v0.22b: whether any signature or type declaration in this file names
582/// `JsonError` — drives the conditional `type JsonError` runtime import.
583fn file_mentions_json_error(commons: &TypedCommons) -> bool {
584    fn in_type_ref(t: &TypeRef) -> bool {
585        match t {
586            TypeRef::JsonError(_) => true,
587            TypeRef::Result(a, b, _) | TypeRef::Map(a, b, _) => in_type_ref(a) || in_type_ref(b),
588            TypeRef::Option(a, _)
589            | TypeRef::Effect(a, _)
590            | TypeRef::HttpResult(a, _)
591            | TypeRef::Query(a, _)
592            | TypeRef::Stream(a, _)
593            | TypeRef::Connection(a, _)
594            | TypeRef::List(a, _) => in_type_ref(a),
595            TypeRef::Fn(params, ret, _) => params.iter().any(in_type_ref) || in_type_ref(ret),
596            TypeRef::Base(..)
597            | TypeRef::Named(_)
598            | TypeRef::QueueResult(_)
599            | TypeRef::ValidationError(_)
600            | TypeRef::Unit(_) => false,
601        }
602    }
603    let sig = |params: &[Param], ret: &TypeRef| {
604        params.iter().any(|p| in_type_ref(&p.type_ref)) || in_type_ref(ret)
605    };
606    commons.commons.items.iter().any(|item| match item {
607        CommonsItem::Fn(f) => sig(&f.params, &f.return_type),
608        CommonsItem::Service(s) => s.handlers.iter().any(|h| sig(&h.params, &h.return_type)),
609        CommonsItem::Agent(a) => a.handlers.iter().any(|h| sig(&h.params, &h.return_type)),
610        CommonsItem::Provider(p) => p.ops.iter().any(|op| sig(&op.params, &op.return_type)),
611        CommonsItem::Type(t) => match &t.body {
612            TypeBody::Record(r) => r.fields.iter().any(|f| in_type_ref(&f.type_ref)),
613            TypeBody::Sum(s) => s
614                .variants
615                .iter()
616                .any(|v| v.payload.iter().any(|p| in_type_ref(&p.type_ref))),
617            TypeBody::Refined { .. } | TypeBody::Opaque { .. } => false,
618        },
619        _ => false,
620    })
621}
622
623/// v0.102: true if a file's signatures or store fields mention `Connection[F]`,
624/// so the header imports the runtime `Connection` interface. Covers the held
625/// sites: capability-operation returns, service/agent handler parameters, and
626/// `store` field value types (`Map[K, Connection]` / `Cell[Option[Connection]]`).
627fn file_mentions_connection(commons: &TypedCommons) -> bool {
628    fn in_type_ref(t: &TypeRef) -> bool {
629        match t {
630            TypeRef::Connection(..) => true,
631            TypeRef::Result(a, b, _) | TypeRef::Map(a, b, _) => in_type_ref(a) || in_type_ref(b),
632            TypeRef::Option(a, _)
633            | TypeRef::Effect(a, _)
634            | TypeRef::HttpResult(a, _)
635            | TypeRef::Query(a, _)
636            | TypeRef::Stream(a, _)
637            | TypeRef::List(a, _) => in_type_ref(a),
638            TypeRef::Fn(params, ret, _) => params.iter().any(in_type_ref) || in_type_ref(ret),
639            _ => false,
640        }
641    }
642    let sig = |params: &[Param], ret: &TypeRef| {
643        params.iter().any(|p| in_type_ref(&p.type_ref)) || in_type_ref(ret)
644    };
645    commons.commons.items.iter().any(|item| match item {
646        CommonsItem::Fn(f) => sig(&f.params, &f.return_type),
647        CommonsItem::Service(s) => s.handlers.iter().any(|h| sig(&h.params, &h.return_type)),
648        CommonsItem::Agent(a) => {
649            a.handlers.iter().any(|h| sig(&h.params, &h.return_type))
650                || a.store_fields
651                    .iter()
652                    .any(|f| f.kind.args.iter().any(in_type_ref))
653        }
654        CommonsItem::Capability(c) => c.ops.iter().any(|op| sig(&op.params, &op.return_type)),
655        CommonsItem::Provider(p) => p.ops.iter().any(|op| sig(&op.params, &op.return_type)),
656        _ => false,
657    })
658}
659
660/// v0.22b: a checker `Ty` rendered back to a `TypeRef` for the codec
661/// machinery (which is `TypeRef`-driven). `None` for types the codec
662/// rejects anyway (functions, effects, type variables).
663fn ty_to_type_ref(t: &Ty) -> Option<TypeRef> {
664    let sp = bynk_syntax::span::Span::new(0, 0);
665    Some(match t {
666        Ty::Base(b) => TypeRef::Base(*b, sp),
667        Ty::Named { name, .. } => TypeRef::Named(Ident {
668            name: name.clone(),
669            span: sp,
670        }),
671        Ty::Result(a, b) => TypeRef::Result(
672            Box::new(ty_to_type_ref(a)?),
673            Box::new(ty_to_type_ref(b)?),
674            sp,
675        ),
676        Ty::Option(a) => TypeRef::Option(Box::new(ty_to_type_ref(a)?), sp),
677        Ty::List(a) => TypeRef::List(Box::new(ty_to_type_ref(a)?), sp),
678        Ty::Map(k, v) => TypeRef::Map(
679            Box::new(ty_to_type_ref(k)?),
680            Box::new(ty_to_type_ref(v)?),
681            sp,
682        ),
683        Ty::Unit => TypeRef::Unit(sp),
684        Ty::ValidationError => TypeRef::ValidationError(sp),
685        Ty::JsonError => TypeRef::JsonError(sp),
686        Ty::Effect(_)
687        | Ty::Query(_)
688        | Ty::Stream(_)
689        | Ty::Connection(_)
690        | Ty::HttpResult(_)
691        | Ty::QueueResult
692        | Ty::Fn { .. }
693        | Ty::Var(_)
694        | Ty::Actor(_)
695        | Ty::ActorSum(_) => {
696            return None;
697        }
698    })
699}
700
701/// v0.22b: collect the `Json.encode`/`Json.decode[T]` target type-refs in
702/// this file's bodies — the roots of the module-local codec-helper closure.
703fn collect_json_codec_roots(commons: &TypedCommons) -> Vec<TypeRef> {
704    let mut roots: Vec<TypeRef> = Vec::new();
705    {
706        let mut visit = |e: &Expr| {
707            let ExprKind::MethodCall {
708                receiver,
709                method,
710                args,
711                ..
712            } = &e.kind
713            else {
714                return;
715            };
716            let ExprKind::Ident(id) = &receiver.kind else {
717                return;
718            };
719            if id.name != JSON {
720                return;
721            }
722            match method.name.as_str() {
723                "decode" => {
724                    if let Some(Ty::Result(t, _)) = commons.expr_types.get(&e.span)
725                        && let Some(tr) = ty_to_type_ref(t)
726                    {
727                        roots.push(tr);
728                    }
729                }
730                "encode" => {
731                    if let Some(a) = args.first()
732                        && let Some(t) = commons.expr_types.get(&a.span)
733                        && let Some(tr) = ty_to_type_ref(t)
734                    {
735                        roots.push(tr);
736                    }
737                }
738                _ => {}
739            }
740        };
741        for item in &commons.commons.items {
742            match item {
743                CommonsItem::Fn(f) => walk_block_exprs(&f.body, &mut visit),
744                CommonsItem::Service(s) => {
745                    for h in &s.handlers {
746                        walk_block_exprs(&h.body, &mut visit);
747                    }
748                }
749                CommonsItem::Agent(a) => {
750                    for h in &a.handlers {
751                        walk_block_exprs(&h.body, &mut visit);
752                    }
753                }
754                CommonsItem::Provider(p) => {
755                    for op in &p.ops {
756                        walk_block_exprs(&op.body, &mut visit);
757                    }
758                }
759                _ => {}
760            }
761        }
762    }
763    roots
764}
765
766/// v0.22b: module-local serialise/deserialise helpers for the types this
767/// file's `Json.encode`/`Json.decode[T]` calls reference (ADR 0045). The
768/// closure machinery is shared with the workers boundary path; `skip_names`
769/// / `skip_insts` dedupe against helpers that path already emitted into
770/// this module.
771fn emit_json_codec_helpers(
772    out: &mut String,
773    commons: &TypedCommons,
774    ctx: &EmitProjectCtx,
775    skip_names: &HashSet<String>,
776    skip_insts: &HashSet<String>,
777) {
778    use serialisation::{collect_codec_closure, emit_generic_helpers, emit_helpers_for_owner};
779    let roots = collect_json_codec_roots(commons);
780    if roots.is_empty() {
781        return;
782    }
783    let (names, insts) = collect_codec_closure(&roots, &commons.types);
784    let names: Vec<String> = names
785        .into_iter()
786        .filter(|n| !skip_names.contains(n))
787        .collect();
788    emit_helpers_for_owner(out, &names, &commons.types, &ctx.commons_name);
789    let insts: Vec<serialisation::GenericInst> = insts
790        .into_iter()
791        .filter(|i| !skip_insts.contains(&i.ts_name()))
792        .collect();
793    if !insts.is_empty() {
794        emit_generic_helpers(out, &insts);
795    }
796}
797
798/// Emit boundary serialise/deserialise helpers (v0.8 §3.4 / §5.2) for
799/// every named type declared in this file that flows through a
800/// cross-context call, plus the specialised generic helpers for any
801/// Result/Option instantiation used at the boundary. Returns the emitted
802/// (or locally-bound) helper type names and generic-instantiation names so
803/// the v0.22b codec emission can dedupe against them.
804fn emit_boundary_helpers(
805    out: &mut String,
806    commons: &TypedCommons,
807    ctx: &EmitProjectCtx,
808) -> (HashSet<String>, HashSet<String>) {
809    use serialisation::{
810        collect_boundary_types, collect_generic_instantiations, emit_generic_helpers,
811        emit_helpers_for_owner,
812    };
813
814    // For contexts: walk the local services to discover boundary types.
815    // For commons: walk every consumer's services that reference us
816    // (approximated as: emit for every type declared in this file).
817    //
818    // Service handler types cross the *cross-Worker call* boundary, which only
819    // exists on the `workers` target; on `bundle` calls are in-process, so their
820    // serialise/deserialise helpers are not emitted. The agent **rehydration**
821    // boundary (ADR 0124), in contrast, exists on both targets, so agent
822    // store-field types are always collected (below).
823    let workers = matches!(ctx.target, BuildTarget::Workers);
824    let services: HashMap<String, ServiceDecl> = if workers {
825        commons
826            .commons
827            .items
828            .iter()
829            .filter_map(|i| match i {
830                CommonsItem::Service(s) => Some((s.name.name.clone(), s.clone())),
831                _ => None,
832            })
833            .collect()
834    } else {
835        HashMap::new()
836    };
837
838    // v0.96 (ADR 0124): an agent's `store`-field types are rehydration-boundary
839    // types — their deserialisers drive the load-time validation gate.
840    let agents: HashMap<String, AgentDecl> = commons
841        .commons
842        .items
843        .iter()
844        .filter_map(|i| match i {
845            CommonsItem::Agent(a) => Some((a.name.name.clone(), a.clone())),
846            _ => None,
847        })
848        .collect();
849
850    let locally_declared: HashSet<String> = ctx.file_decl_index.types.keys().cloned().collect();
851    if ctx.unit_kind == UnitKind::Context {
852        let boundary_types_all = collect_boundary_types(&commons.types, &services, &agents);
853        // Locally-declared boundary types get full helpers in this module. On
854        // `bundle` (v0.96, ADR 0124) the commons modules emit no boundary helpers,
855        // so a cross-commons *agent-state* type's deserialiser — needed by the
856        // rehydration gate — is emitted here in the context instead of re-exported.
857        let local_boundary: Vec<String> = boundary_types_all
858            .iter()
859            .filter(|n| !workers || locally_declared.contains(*n))
860            .cloned()
861            .collect();
862        emit_helpers_for_owner(
863            out,
864            &local_boundary,
865            &commons.types,
866            ctx.commons_name.as_str(),
867        );
868
869        // Re-export helpers for commons-owned boundary types so consumers
870        // can address them through this context's handlers.ts namespace
871        // (matching the namespace import they already use for cross-
872        // context types). Grouped by source commons. Workers only — on `bundle`
873        // the commons emit no helpers, so cross-commons types are emitted
874        // locally above (v0.96) rather than imported.
875        let mut by_commons: HashMap<String, Vec<String>> = HashMap::new();
876        for n in &boundary_types_all {
877            if !workers || locally_declared.contains(n) {
878                continue;
879            }
880            if matches!(ctx.imported_from_kind.get(n), Some(UnitKind::Commons))
881                && let Some(commons_name) = ctx.imported_from.get(n)
882            {
883                by_commons
884                    .entry(commons_name.clone())
885                    .or_default()
886                    .push(n.clone());
887            }
888        }
889        let mut commons_keys: Vec<&String> = by_commons.keys().collect();
890        commons_keys.sort();
891        for commons_name in commons_keys {
892            let names = by_commons.get(commons_name).unwrap();
893            let mut sorted_names: Vec<String> = names.clone();
894            sorted_names.sort();
895            sorted_names.dedup();
896            let target_path = ctx
897                .imported_decl_paths
898                .get(commons_name)
899                .and_then(|m| sorted_names.iter().find_map(|n| m.get(n).cloned()))
900                .unwrap_or_else(|| EmitProjectCtx::commons_path(commons_name));
901            let import_spec = cross_commons_import_specifier_for_path(
902                &ctx.source_path,
903                &target_path,
904                ctx.import_ext,
905            );
906            let mut parts: Vec<String> = Vec::new();
907            for n in &sorted_names {
908                parts.push(format!("serialise_{n}"));
909                parts.push(format!("deserialise_{n}"));
910            }
911            // v0.9.1: emit both a regular import (so the names are bound
912            // locally for use inside this file's serialisation helpers) and a
913            // re-export (so downstream consumers can still reach them
914            // through this module). A bare `export { ... } from "..."`
915            // re-export does not create a local binding, which `tsc --strict`
916            // catches when the body calls one of the helpers directly.
917            writeln!(
918                out,
919                "import {{ {} }} from \"{import_spec}\";",
920                parts.join(", ")
921            )
922            .unwrap();
923            writeln!(out, "export {{ {} }};", parts.join(", ")).unwrap();
924        }
925        if !by_commons.is_empty() {
926            writeln!(out).unwrap();
927        }
928
929        // Specialised Result_/Option_ helpers for the instantiations used —
930        // in handler signatures or in boundary-type fields (v0.18).
931        let insts =
932            collect_generic_instantiations(&services, &agents, &boundary_types_all, &commons.types);
933        emit_generic_helpers(out, &insts);
934        (
935            boundary_types_all.into_iter().collect(),
936            insts.iter().map(|i| i.ts_name()).collect(),
937        )
938    } else if !workers {
939        // Commons/adapters have no agents (no rehydration boundary), and on
940        // `bundle` there is no cross-Worker call boundary either — so emit no
941        // boundary helpers, matching pre-v0.96 bundle output (the rehydration
942        // pass that now always runs is for context-declared agents only).
943        (HashSet::new(), HashSet::new())
944    } else {
945        // Commons/adapters (workers): emit helpers for every type declared in
946        // this file, plus (v0.18) the generic instantiations their fields use —
947        // a record like the bynk surface's `Request` carries
948        // `Option[String]` fields whose serialisers delegate to the
949        // specialised helpers.
950        let mut locally: Vec<String> = locally_declared.into_iter().collect();
951        locally.sort();
952        emit_helpers_for_owner(out, &locally, &commons.types, ctx.commons_name.as_str());
953        let insts = collect_generic_instantiations(
954            &HashMap::new(),
955            &HashMap::new(),
956            &locally,
957            &commons.types,
958        );
959        emit_generic_helpers(out, &insts);
960        (
961            locally.into_iter().collect(),
962            insts.iter().map(|i| i.ts_name()).collect(),
963        )
964    }
965}
966
967/// For each type imported via `uses` that's referenced in this file, emit:
968/// 1. (Done in imports) an aliased import: `import { Money as __CommonsMoney } from ...`
969/// 2. A rebranded type alias: `export type Money = __CommonsMoney & { readonly __ctxBrand: "..." }`
970///
971/// The brand makes two contexts that both `uses` the same commons see distinct
972/// nominal `Money` types in their TypeScript output (v0.4 §3.4 / §6.2).
973fn emit_context_rebrands(
974    out: &mut String,
975    refs: &ExternalReferences,
976    commons: &TypedCommons,
977    ctx: &EmitProjectCtx,
978) {
979    let Some(owning) = &ctx.owning_context else {
980        return;
981    };
982    // Collect names imported via `uses` (kind == Commons in imported_from_kind).
983    let mut names: Vec<String> = Vec::new();
984    for set in refs.by_commons.values() {
985        for n in set {
986            // v0.20b: only *types* get the context rebrand — a
987            // `uses`-imported function is a value and imports plainly.
988            if matches!(ctx.imported_from_kind.get(n), Some(UnitKind::Commons))
989                && commons.types.contains_key(n)
990            {
991                names.push(n.clone());
992            }
993        }
994    }
995    names.sort();
996    names.dedup();
997    if names.is_empty() {
998        return;
999    }
1000    for name in &names {
1001        writeln!(
1002            out,
1003            "export type {name} = __Commons{name} & {{ readonly __ctxBrand: \"{owning}\" }};",
1004        )
1005        .unwrap();
1006        // v0.9.2: a commons refined/opaque type carries a value-side
1007        // constructor (`.of` / `.unsafe`). Re-export it under the rebranded
1008        // name so a context calling `ShortCode.of(...)` resolves to a value —
1009        // delegating to the imported commons constructor but reporting the
1010        // context-branded type. (Without this, `ShortCode` is type-only in the
1011        // context and `.of` fails to resolve.)
1012        if let Some(base) = commons.types.get(name).and_then(refined_or_opaque_base) {
1013            let ts_base = ts_base(base);
1014            writeln!(out, "export const {name} = {{").unwrap();
1015            writeln!(
1016                out,
1017                "  of(value: {ts_base}): Result<{name}, ValidationError> {{ return __Commons{name}.of(value) as unknown as Result<{name}, ValidationError>; }},",
1018            )
1019            .unwrap();
1020            writeln!(
1021                out,
1022                "  unsafe(value: {ts_base}): {name} {{ return __Commons{name}.unsafe(value) as unknown as {name}; }},",
1023            )
1024            .unwrap();
1025            writeln!(out, "}};").unwrap();
1026        }
1027    }
1028    writeln!(out).unwrap();
1029}
1030
1031/// If a type declaration is a refined or opaque base type, return its base
1032/// (both lower to a branded base with a `.of` / `.unsafe` constructor object).
1033fn refined_or_opaque_base(decl: &TypeDecl) -> Option<BaseType> {
1034    match &decl.body {
1035        TypeBody::Refined { base, .. } | TypeBody::Opaque { base, .. } => Some(*base),
1036        _ => None,
1037    }
1038}
1039
1040/// Names that this file needs to import from elsewhere (sibling files of
1041/// the same commons, or other commons via `uses`).
1042#[derive(Default)]
1043struct ExternalReferences {
1044    /// `commons name` → set of names to import.
1045    by_commons: HashMap<String, HashSet<String>>,
1046    /// `sibling source path` → set of names to import (same-commons).
1047    by_sibling: HashMap<PathBuf, HashSet<String>>,
1048}
1049
1050impl ExternalReferences {
1051    fn is_empty(&self) -> bool {
1052        self.by_commons.is_empty() && self.by_sibling.is_empty()
1053    }
1054}
1055
1056fn collect_external_references(commons: &TypedCommons, ctx: &EmitProjectCtx) -> ExternalReferences {
1057    // Names declared in this file (so we know what's local-to-file).
1058    let local_to_file: HashSet<String> = commons
1059        .commons
1060        .items
1061        .iter()
1062        .map(|i| i.name().name.clone())
1063        .collect();
1064
1065    let mut refs = ExternalReferences::default();
1066
1067    // Walk every expression and TypeRef in this file's items, recording
1068    // any reference that resolves to a name declared in a sibling file or
1069    // an imported commons.
1070    for item in &commons.commons.items {
1071        match item {
1072            CommonsItem::Type(t) => {
1073                collect_refs_in_type_decl(t, &local_to_file, ctx, &mut refs);
1074            }
1075            CommonsItem::Fn(f) => {
1076                collect_refs_in_fn(f, &local_to_file, commons, ctx, &mut refs);
1077            }
1078            CommonsItem::Capability(c) => {
1079                for op in &c.ops {
1080                    for p in &op.params {
1081                        collect_refs_in_typeref(&p.type_ref, &local_to_file, ctx, &mut refs);
1082                    }
1083                    collect_refs_in_typeref(&op.return_type, &local_to_file, ctx, &mut refs);
1084                }
1085            }
1086            CommonsItem::Provider(p) => {
1087                // Reference to the capability so we can import it (locally
1088                // declared, so usually no extra work).
1089                let _ = &p.capability;
1090                for op in &p.ops {
1091                    for param in &op.params {
1092                        collect_refs_in_typeref(&param.type_ref, &local_to_file, ctx, &mut refs);
1093                    }
1094                    collect_refs_in_typeref(&op.return_type, &local_to_file, ctx, &mut refs);
1095                    collect_refs_in_block(&op.body, &local_to_file, commons, ctx, &mut refs);
1096                }
1097            }
1098            CommonsItem::Service(s) => {
1099                for h in &s.handlers {
1100                    for p in &h.params {
1101                        collect_refs_in_typeref(&p.type_ref, &local_to_file, ctx, &mut refs);
1102                    }
1103                    collect_refs_in_typeref(&h.return_type, &local_to_file, ctx, &mut refs);
1104                    collect_refs_in_block(&h.body, &local_to_file, commons, ctx, &mut refs);
1105                }
1106            }
1107            CommonsItem::Agent(a) => {
1108                collect_refs_in_typeref(&a.key_type, &local_to_file, ctx, &mut refs);
1109                for f in &a.store_fields {
1110                    for arg in &f.kind.args {
1111                        collect_refs_in_typeref(arg, &local_to_file, ctx, &mut refs);
1112                    }
1113                }
1114                for h in &a.handlers {
1115                    for p in &h.params {
1116                        collect_refs_in_typeref(&p.type_ref, &local_to_file, ctx, &mut refs);
1117                    }
1118                    collect_refs_in_typeref(&h.return_type, &local_to_file, ctx, &mut refs);
1119                    collect_refs_in_block(&h.body, &local_to_file, commons, ctx, &mut refs);
1120                }
1121            }
1122            CommonsItem::Actor(a) => {
1123                if let Some(id) = &a.identity {
1124                    collect_refs_in_typeref(id, &local_to_file, ctx, &mut refs);
1125                }
1126            }
1127        }
1128    }
1129    refs
1130}
1131
1132fn collect_refs_in_type_decl(
1133    t: &TypeDecl,
1134    local_to_file: &HashSet<String>,
1135    ctx: &EmitProjectCtx,
1136    out: &mut ExternalReferences,
1137) {
1138    match &t.body {
1139        TypeBody::Record(r) => {
1140            for f in &r.fields {
1141                collect_refs_in_typeref(&f.type_ref, local_to_file, ctx, out);
1142            }
1143        }
1144        TypeBody::Sum(s) => {
1145            for v in &s.variants {
1146                for p in &v.payload {
1147                    collect_refs_in_typeref(&p.type_ref, local_to_file, ctx, out);
1148                }
1149            }
1150        }
1151        _ => {}
1152    }
1153}
1154
1155fn collect_refs_in_fn(
1156    f: &FnDecl,
1157    local_to_file: &HashSet<String>,
1158    commons: &TypedCommons,
1159    ctx: &EmitProjectCtx,
1160    out: &mut ExternalReferences,
1161) {
1162    for p in &f.params {
1163        collect_refs_in_typeref(&p.type_ref, local_to_file, ctx, out);
1164    }
1165    collect_refs_in_typeref(&f.return_type, local_to_file, ctx, out);
1166    // For methods: the attached type may also be elsewhere.
1167    if let FnName::Method { type_name, .. } = &f.name {
1168        record_name_ref(&type_name.name, local_to_file, ctx, out);
1169    }
1170    collect_refs_in_block(&f.body, local_to_file, commons, ctx, out);
1171}
1172
1173fn collect_refs_in_typeref(
1174    r: &TypeRef,
1175    local_to_file: &HashSet<String>,
1176    ctx: &EmitProjectCtx,
1177    out: &mut ExternalReferences,
1178) {
1179    match r {
1180        TypeRef::Named(id) => record_name_ref(&id.name, local_to_file, ctx, out),
1181        TypeRef::Result(t, e, _) => {
1182            collect_refs_in_typeref(t, local_to_file, ctx, out);
1183            collect_refs_in_typeref(e, local_to_file, ctx, out);
1184        }
1185        TypeRef::Option(t, _) => collect_refs_in_typeref(t, local_to_file, ctx, out),
1186        TypeRef::Effect(t, _) => collect_refs_in_typeref(t, local_to_file, ctx, out),
1187        TypeRef::HttpResult(t, _) => collect_refs_in_typeref(t, local_to_file, ctx, out),
1188        _ => {}
1189    }
1190}
1191
1192fn collect_refs_in_block(
1193    b: &Block,
1194    local_to_file: &HashSet<String>,
1195    commons: &TypedCommons,
1196    ctx: &EmitProjectCtx,
1197    out: &mut ExternalReferences,
1198) {
1199    for stmt in &b.statements {
1200        match stmt {
1201            Statement::Let(l) | Statement::EffectLet(l) => {
1202                if let Some(t) = &l.type_annot {
1203                    collect_refs_in_typeref(t, local_to_file, ctx, out);
1204                }
1205                collect_refs_in_expr(&l.value, local_to_file, commons, ctx, out);
1206            }
1207            Statement::Expect(a) => {
1208                collect_refs_in_expr(&a.value, local_to_file, commons, ctx, out);
1209            }
1210            Statement::Send(s) => {
1211                collect_refs_in_expr(&s.value, local_to_file, commons, ctx, out);
1212            }
1213            Statement::Assign(a) => {
1214                collect_refs_in_expr(&a.value, local_to_file, commons, ctx, out);
1215            }
1216        }
1217    }
1218    collect_refs_in_expr(&b.tail, local_to_file, commons, ctx, out);
1219}
1220
1221fn collect_refs_in_expr(
1222    e: &Expr,
1223    local_to_file: &HashSet<String>,
1224    commons: &TypedCommons,
1225    ctx: &EmitProjectCtx,
1226    out: &mut ExternalReferences,
1227) {
1228    match &e.kind {
1229        // A bare ident the checker typed as a sum is a nullary variant
1230        // constructor — the lowering qualifies it to `Type.Variant`, so the
1231        // owning type must be imported (v0.18: first hit by `Get` from the
1232        // consumed bynk surface's `Method`).
1233        ExprKind::Ident(id) => {
1234            if let Some(type_name) = sum_owner_of_variant(&id.name, e.span, commons) {
1235                record_name_ref(&type_name, local_to_file, ctx, out);
1236            }
1237        }
1238        ExprKind::IntLit(_)
1239        | ExprKind::FloatLit { .. }
1240        | ExprKind::DurationLit { .. }
1241        | ExprKind::StrLit(_)
1242        | ExprKind::BoolLit(_)
1243        | ExprKind::None
1244        | ExprKind::UnitLit => {}
1245        // v0.43: a hole's expression may reference imported names.
1246        ExprKind::InterpStr(parts) => {
1247            for part in parts {
1248                if let InterpPart::Hole(hole) = part {
1249                    collect_refs_in_expr(hole, local_to_file, commons, ctx, out);
1250                }
1251            }
1252        }
1253        // v0.20a: a lambda — its annotated param types may reference
1254        // imported types; the body walks like any expression.
1255        ExprKind::Lambda(lambda) => {
1256            for p in &lambda.params {
1257                if let Some(tr) = &p.type_ref {
1258                    collect_refs_in_typeref(tr, local_to_file, ctx, out);
1259                }
1260            }
1261            collect_refs_in_expr(&lambda.body, local_to_file, commons, ctx, out);
1262        }
1263        ExprKind::EffectPure(inner) => {
1264            collect_refs_in_expr(inner, local_to_file, commons, ctx, out);
1265        }
1266        ExprKind::Expect(inner) => {
1267            collect_refs_in_expr(inner, local_to_file, commons, ctx, out);
1268        }
1269        ExprKind::Val { args, .. } => {
1270            for a in args {
1271                collect_refs_in_expr(a, local_to_file, commons, ctx, out);
1272            }
1273        }
1274        ExprKind::ListLit(elems) => {
1275            for el in elems {
1276                collect_refs_in_expr(el, local_to_file, commons, ctx, out);
1277            }
1278        }
1279        ExprKind::RecordSpread {
1280            type_name,
1281            base,
1282            overrides,
1283        } => {
1284            if let Some(tn) = type_name {
1285                record_name_ref(&tn.name, local_to_file, ctx, out);
1286            }
1287            collect_refs_in_expr(base, local_to_file, commons, ctx, out);
1288            for f in overrides {
1289                if let Some(v) = &f.value {
1290                    collect_refs_in_expr(v, local_to_file, commons, ctx, out);
1291                }
1292            }
1293        }
1294        ExprKind::Call { name, args, .. } => {
1295            record_name_ref(&name.name, local_to_file, ctx, out);
1296            // A payload-carrying bare variant call (`Won(prize)`) lowers to
1297            // `Type.Variant(…)` — import the owning sum type too.
1298            if let Some(type_name) = sum_owner_of_variant(&name.name, e.span, commons) {
1299                record_name_ref(&type_name, local_to_file, ctx, out);
1300            }
1301            for a in args {
1302                collect_refs_in_expr(a, local_to_file, commons, ctx, out);
1303            }
1304        }
1305        ExprKind::BinOp(_, l, r) => {
1306            collect_refs_in_expr(l, local_to_file, commons, ctx, out);
1307            collect_refs_in_expr(r, local_to_file, commons, ctx, out);
1308        }
1309        ExprKind::UnaryOp(_, i)
1310        | ExprKind::Paren(i)
1311        | ExprKind::Ok(i)
1312        | ExprKind::Err(i)
1313        | ExprKind::Some(i)
1314        | ExprKind::Question(i) => collect_refs_in_expr(i, local_to_file, commons, ctx, out),
1315        ExprKind::Block(b) => collect_refs_in_block(b, local_to_file, commons, ctx, out),
1316        ExprKind::If {
1317            cond,
1318            then_block,
1319            else_block,
1320        } => {
1321            collect_refs_in_expr(cond, local_to_file, commons, ctx, out);
1322            collect_refs_in_block(then_block, local_to_file, commons, ctx, out);
1323            collect_refs_in_block(else_block, local_to_file, commons, ctx, out);
1324        }
1325        ExprKind::ConstructorCall {
1326            type_name,
1327            method: _,
1328            args,
1329        } => {
1330            record_name_ref(&type_name.name, local_to_file, ctx, out);
1331            for a in args {
1332                collect_refs_in_expr(a, local_to_file, commons, ctx, out);
1333            }
1334        }
1335        ExprKind::RecordConstruction { type_name, fields } => {
1336            record_name_ref(&type_name.name, local_to_file, ctx, out);
1337            for f in fields {
1338                if let Some(v) = &f.value {
1339                    collect_refs_in_expr(v, local_to_file, commons, ctx, out);
1340                }
1341            }
1342        }
1343        ExprKind::FieldAccess { receiver, field: _ } => {
1344            // The bare-ident-as-type case (`TypeName.Variant`) — record the
1345            // name so we import the type.
1346            if let ExprKind::Ident(id) = &receiver.kind {
1347                record_name_ref(&id.name, local_to_file, ctx, out);
1348            } else {
1349                collect_refs_in_expr(receiver, local_to_file, commons, ctx, out);
1350            }
1351        }
1352        ExprKind::MethodCall {
1353            receiver,
1354            method: _,
1355            args,
1356            ..
1357        } => {
1358            if let ExprKind::Ident(id) = &receiver.kind {
1359                record_name_ref(&id.name, local_to_file, ctx, out);
1360            } else {
1361                collect_refs_in_expr(receiver, local_to_file, commons, ctx, out);
1362            }
1363            for a in args {
1364                collect_refs_in_expr(a, local_to_file, commons, ctx, out);
1365            }
1366        }
1367        ExprKind::Match { discriminant, arms } => {
1368            collect_refs_in_expr(discriminant, local_to_file, commons, ctx, out);
1369            for arm in arms {
1370                if let Pattern::Variant {
1371                    type_name: Some(tn),
1372                    ..
1373                } = &arm.pattern
1374                {
1375                    record_name_ref(&tn.name, local_to_file, ctx, out);
1376                }
1377                match &arm.body {
1378                    MatchBody::Expr(e) => collect_refs_in_expr(e, local_to_file, commons, ctx, out),
1379                    MatchBody::Block(b) => {
1380                        collect_refs_in_block(b, local_to_file, commons, ctx, out)
1381                    }
1382                }
1383            }
1384        }
1385        ExprKind::Is { value, pattern } => {
1386            collect_refs_in_expr(value, local_to_file, commons, ctx, out);
1387            if let Pattern::Variant {
1388                type_name: Some(tn),
1389                ..
1390            } = pattern
1391            {
1392                record_name_ref(&tn.name, local_to_file, ctx, out);
1393            }
1394        }
1395    }
1396}
1397
1398/// If `name` at `span` is a bare reference to a variant of a sum type (per
1399/// the checker's expression type), return the owning sum's name — the same
1400/// test the lowering uses to qualify it as `Type.Variant` (see the
1401/// `ExprKind::Ident` arm of `lower_expr`).
1402fn sum_owner_of_variant(
1403    name: &str,
1404    span: bynk_syntax::span::Span,
1405    commons: &TypedCommons,
1406) -> Option<String> {
1407    if let Some(Ty::Named {
1408        kind: NamedKind::Sum,
1409        name: type_name,
1410    }) = commons.expr_types.get(&span)
1411        && let Some(decl) = commons.types.get(type_name)
1412        && let TypeBody::Sum(s) = &decl.body
1413        && s.variants.iter().any(|v| v.name.name == name)
1414    {
1415        return Some(type_name.clone());
1416    }
1417    None
1418}
1419
1420fn record_name_ref(
1421    name: &str,
1422    local_to_file: &HashSet<String>,
1423    ctx: &EmitProjectCtx,
1424    out: &mut ExternalReferences,
1425) {
1426    if local_to_file.contains(name) {
1427        return;
1428    }
1429    // Imported from another commons?
1430    if let Some(commons_name) = ctx.imported_from.get(name) {
1431        out.by_commons
1432            .entry(commons_name.clone())
1433            .or_default()
1434            .insert(name.to_string());
1435        return;
1436    }
1437    // Sibling file in the same commons?
1438    if let Some(path) = ctx.file_decl_index.types.get(name)
1439        && path != &ctx.source_path
1440    {
1441        out.by_sibling
1442            .entry(path.clone())
1443            .or_default()
1444            .insert(name.to_string());
1445        return;
1446    }
1447    if let Some(path) = ctx.file_decl_index.fns.get(name)
1448        && path != &ctx.source_path
1449    {
1450        out.by_sibling
1451            .entry(path.clone())
1452            .or_default()
1453            .insert(name.to_string());
1454    }
1455}
1456
1457/// Emit `import * as <ns> from "..."` for each consumed context that
1458/// exposes services (so the consuming file can reference its `makeSurface`
1459/// return type and brand the cross-context call arguments).
1460fn emit_cross_context_namespace_imports(
1461    out: &mut String,
1462    commons: &TypedCommons,
1463    ctx: &EmitProjectCtx,
1464) {
1465    let info = &ctx.cross_context;
1466    // Consumed contexts that expose services (v0.6) plus, v0.15, those whose
1467    // capabilities this context references via `given B.Cap`.
1468    let mut needed: std::collections::BTreeSet<String> = info
1469        .consumed_services
1470        .iter()
1471        .filter(|(_, svcs)| !svcs.is_empty())
1472        .map(|(q, _)| q.clone())
1473        .collect();
1474    needed.extend(cross_context_cap_namespaces(commons, info));
1475    if needed.is_empty() {
1476        return;
1477    }
1478    let consumed_with_services: Vec<&String> = needed.iter().collect();
1479    for q in &consumed_with_services {
1480        // Pick the first known file path for the consumed context as the
1481        // import target. (The composition root lives in the consumed
1482        // context's directory; any of its files would work as an import
1483        // target since they're all in the same module namespace, but we
1484        // currently emit one file per .bynk source so a single import per
1485        // consumed name suffices for the surface contract.)
1486        let target_paths = ctx.imported_decl_paths.get(q.as_str());
1487        let target = target_paths
1488            .and_then(|m| m.values().next().cloned())
1489            .unwrap_or_else(|| {
1490                // No imported declaration pins the path (e.g. a capability-only
1491                // consumed context, v0.15). Fall back to the unit's own module:
1492                // its per-Worker handlers in workers mode, or its <segment>.bynk
1493                // source in bundle mode. v0.17: a consumed *adapter* is not a
1494                // Worker — its capability types live in its root module
1495                // (`<adapter>.ts`) in both targets.
1496                if ctx.consumed_adapters.contains(q.as_str()) {
1497                    let mut p = EmitProjectCtx::commons_path(q);
1498                    p.set_extension("bynk");
1499                    p
1500                } else {
1501                    match ctx.target {
1502                        BuildTarget::Workers => crate::project::worker_handlers_source_path(q),
1503                        BuildTarget::Bundle => {
1504                            let mut p = EmitProjectCtx::commons_path(q);
1505                            p.set_extension("bynk");
1506                            p
1507                        }
1508                    }
1509                }
1510            });
1511        let import =
1512            cross_commons_import_specifier_for_path(&ctx.source_path, &target, ctx.import_ext);
1513        let ns = qualified_to_ns(q);
1514        writeln!(out, "import * as {ns} from \"{import}\";").unwrap();
1515    }
1516    writeln!(out).unwrap();
1517}
1518
1519fn emit_project_imports(
1520    out: &mut String,
1521    commons: &TypedCommons,
1522    ctx: &EmitProjectCtx,
1523    refs: &ExternalReferences,
1524) {
1525    // Sibling imports: relative path within the same commons/context directory.
1526    let mut sibling_paths: Vec<(&PathBuf, &HashSet<String>)> = refs.by_sibling.iter().collect();
1527    sibling_paths.sort_by(|a, b| a.0.cmp(b.0));
1528    for (path, names) in sibling_paths {
1529        let import = sibling_import_specifier(&ctx.source_path, path, ctx.import_ext);
1530        let mut sorted: Vec<&String> = names.iter().collect();
1531        sorted.sort();
1532        let joined = sorted
1533            .iter()
1534            .map(|s| s.as_str())
1535            .collect::<Vec<_>>()
1536            .join(", ");
1537        writeln!(out, "import {{ {joined} }} from \"{import}\";").unwrap();
1538    }
1539    // Cross-unit imports: group by *target file path*.
1540    let mut unit_names: Vec<(&String, &HashSet<String>)> = refs.by_commons.iter().collect();
1541    unit_names.sort_by(|a, b| a.0.cmp(b.0));
1542    for (unit_name, names) in unit_names {
1543        let target_paths = ctx.imported_decl_paths.get(unit_name.as_str());
1544        let mut by_target: std::collections::BTreeMap<PathBuf, Vec<&String>> =
1545            std::collections::BTreeMap::new();
1546        for n in names {
1547            let path = target_paths
1548                .and_then(|p| p.get(n))
1549                .cloned()
1550                .unwrap_or_else(|| EmitProjectCtx::commons_path(unit_name));
1551            by_target.entry(path).or_default().push(n);
1552        }
1553        for (target, mut name_list) in by_target {
1554            name_list.sort();
1555            let import =
1556                cross_commons_import_specifier_for_path(&ctx.source_path, &target, ctx.import_ext);
1557            // For context units, aliase commons-source imports so we can emit
1558            // rebrand aliases of the same short name. Imports from consumed
1559            // contexts keep their original name. v0.20b: the rebrand applies
1560            // to *types* only — a `uses`-imported function (bynk.list's
1561            // `traverse`) is a value, imports plainly, and is never branded.
1562            let mut parts: Vec<String> = Vec::new();
1563            for n in &name_list {
1564                let from_kind = ctx.imported_from_kind.get(n.as_str()).copied();
1565                if ctx.unit_kind == UnitKind::Context
1566                    && from_kind == Some(UnitKind::Commons)
1567                    && commons.types.contains_key(n.as_str())
1568                {
1569                    parts.push(format!("{n} as __Commons{n}"));
1570                } else {
1571                    parts.push((*n).clone());
1572                }
1573            }
1574            let joined = parts.join(", ");
1575            writeln!(out, "import {{ {joined} }} from \"{import}\";").unwrap();
1576        }
1577    }
1578}
1579
1580/// Compute a relative import specifier from `from_source` (a `.bynk` path)
1581/// to `to_source` (another `.bynk` path), with `.bynk` rewritten to `.js`
1582/// for compatibility with NodeNext/strict TS resolution.
1583fn sibling_import_specifier(from_source: &Path, to_source: &Path, ext: ImportExt) -> String {
1584    let from_dir = from_source.parent().unwrap_or(Path::new(""));
1585    let target = to_source.with_extension(ext.as_str());
1586    let rel = relative_to(from_dir, &target);
1587    format!("./{}", ts_specifier(&rel))
1588}
1589
1590/// Render a path as a TypeScript module specifier: **always forward
1591/// slashes**. `Path::display()` uses the platform separator, and on Windows
1592/// that emitted `import ... from "./commerce\orders.js"` — broken ESM
1593/// output, caught by the first CI matrix run on windows-latest.
1594pub(crate) fn ts_specifier(p: &Path) -> String {
1595    p.to_string_lossy().replace('\\', "/")
1596}
1597
1598/// Compute a relative import specifier from this file's location to a
1599/// specific source file in another commons. `target_source` is the project-
1600/// relative path of the target `.bynk` file. The result is suitable for
1601/// `import { ... } from "..."` in NodeNext/strict TypeScript.
1602fn cross_commons_import_specifier_for_path(
1603    from_source: &Path,
1604    target_source: &Path,
1605    ext: ImportExt,
1606) -> String {
1607    let from_dir = from_source.parent().unwrap_or(Path::new(""));
1608    let target = target_source.with_extension(ext.as_str());
1609    let rel = relative_to(from_dir, &target);
1610    let display = ts_specifier(&rel);
1611    if display.starts_with("../") || display.starts_with("./") {
1612        display
1613    } else {
1614        format!("./{display}")
1615    }
1616}
1617
1618/// Compute `target` as a path relative to `from`. Handles parent traversal
1619/// (`..`) for cases where `target` lives in a sibling directory.
1620fn relative_to(from: &Path, target: &Path) -> PathBuf {
1621    use std::path::Component as C;
1622    let f_comps: Vec<C> = from.components().collect();
1623    let t_comps: Vec<C> = target.components().collect();
1624    let mut shared = 0;
1625    while shared < f_comps.len() && shared < t_comps.len() && f_comps[shared] == t_comps[shared] {
1626        shared += 1;
1627    }
1628    let mut out = PathBuf::new();
1629    for _ in shared..f_comps.len() {
1630        out.push("..");
1631    }
1632    for c in &t_comps[shared..] {
1633        out.push(c.as_os_str());
1634    }
1635    if out.as_os_str().is_empty() {
1636        out.push(".");
1637    }
1638    out
1639}
1640
1641fn write_header(out: &mut String, commons: &TypedCommons, ctx: &EmitProjectCtx) {
1642    writeln!(out, "// Generated by bynkc — do not edit by hand.").unwrap();
1643    let kind = match ctx.unit_kind {
1644        UnitKind::Commons => "commons",
1645        UnitKind::Context => "context",
1646        UnitKind::Test => "test",
1647        UnitKind::Integration => "integration test",
1648        UnitKind::Adapter => "adapter",
1649    };
1650    writeln!(out, "// {kind} {}", commons.commons.name.joined()).unwrap();
1651    writeln!(out).unwrap();
1652    if !commons.commons.items.is_empty() {
1653        let runtime_import = runtime_import_for(&ctx.source_path, ctx.import_ext);
1654        let has_agent = commons
1655            .commons
1656            .items
1657            .iter()
1658            .any(|i| matches!(i, CommonsItem::Agent(_)));
1659        // v0.80: a file with any agent invariant imports the `invariantViolation`
1660        // fault helper used by the generated `commitState` gate.
1661        let has_agent_invariants = commons.commons.items.iter().any(|i| match i {
1662            CommonsItem::Agent(a) => !a.invariants.is_empty(),
1663            _ => false,
1664        });
1665        let has_http = commons.commons.items.iter().any(|i| match i {
1666            CommonsItem::Service(s) => s
1667                .handlers
1668                .iter()
1669                .any(|h| matches!(h.kind, HandlerKind::Http { .. })),
1670            _ => false,
1671        });
1672        // A `from queue` `on message` is the queue consumer (imports `QueueResult`);
1673        // a `from WebSocket` `on message` (slice 3b-iii) is the inbound handler and
1674        // is not a queue concern.
1675        let has_queue = commons.commons.items.iter().any(|i| match i {
1676            CommonsItem::Service(s) => {
1677                !matches!(s.protocol, ServiceProtocol::WebSocket { .. })
1678                    && s.handlers
1679                        .iter()
1680                        .any(|h| matches!(h.kind, HandlerKind::Message))
1681            }
1682            _ => false,
1683        });
1684        let workers = matches!(ctx.target, BuildTarget::Workers);
1685        let mut parts: Vec<&str> = vec![
1686            "Ok",
1687            "Err",
1688            "Some",
1689            "None",
1690            "type Result",
1691            "type Option",
1692            "type ValidationError",
1693        ];
1694        // v0.22b: the codec types are imported only when the file uses the
1695        // `Json` codec (or names `JsonError` in a signature) — keeping every
1696        // non-codec module's header byte-identical to v0.22a.
1697        let uses_codec = !collect_json_codec_roots(commons).is_empty();
1698        let mentions_json_error = file_mentions_json_error(commons);
1699        if uses_codec || mentions_json_error {
1700            parts.push("type JsonError");
1701        }
1702        // v0.102: a file naming `Connection[F]` imports the runtime interface.
1703        if file_mentions_connection(commons) {
1704            parts.push("type Connection");
1705        }
1706        if has_agent {
1707            // v0.9.2: agent-declaring files lower instantiation through the
1708            // `makeAgent` helper and a per-agent `StateRegistry`, and the
1709            // generated factory's signature names `DurableObjectNamespace`.
1710            parts.push("type DurableObjectState");
1711            parts.push("type DurableObjectNamespace");
1712            parts.push("StateRegistry");
1713            parts.push("makeAgent");
1714        }
1715        if has_agent_invariants {
1716            parts.push("invariantViolation");
1717        }
1718        // v0.96 (ADR 0124): an agent whose load-time validation gate fires imports
1719        // the `rehydrationViolation` fault helper.
1720        let has_rehydration_gate = commons.commons.items.iter().any(|i| match i {
1721            CommonsItem::Agent(a) => emit::agent_needs_rehydrate(a, &commons.types),
1722            _ => false,
1723        });
1724        if has_rehydration_gate {
1725            parts.push("rehydrationViolation");
1726        }
1727        // v0.104/v0.105 (real-time track slice 3b): on Workers a `store Map[K,
1728        // Connection]` persists the connection id; its entry ops re-resolve the live
1729        // socket via `resolveConnection` and read a connection's id via `connIdOf`.
1730        if workers
1731            && commons.commons.items.iter().any(|i| match i {
1732                CommonsItem::Agent(a) => emit::agent_has_held_storage(a),
1733                _ => false,
1734            })
1735        {
1736            parts.push("resolveConnection");
1737            parts.push("connIdOf");
1738        }
1739        // v0.104/v0.105 (real-time track slice 3b): on Workers a context hosting a
1740        // `from WebSocket` `on open` accepts the socket inside its Durable Object via
1741        // the hibernatable API — `acceptHibernatableConnection` (accept + tag + wrap),
1742        // a `WebSocketPair`, and the `101` upgrade response. (The service and its
1743        // hosting agent share the one Worker module, so these land in one
1744        // `handlers.ts`.)
1745        let hosts_ws_open = commons.commons.items.iter().any(|i| match i {
1746            CommonsItem::Service(s) => s
1747                .handlers
1748                .iter()
1749                .any(|h| matches!(h.kind, HandlerKind::Open)),
1750            _ => false,
1751        });
1752        if workers && hosts_ws_open {
1753            parts.push("acceptHibernatableConnection");
1754            parts.push("newWebSocketPair");
1755            parts.push("webSocketUpgradeResponse");
1756        }
1757        // v0.106 (slice 3b-iii): a context with an inbound/close handler re-wraps
1758        // the firing socket as a `WorkersConnection` in `webSocketMessage`/
1759        // `webSocketClose`.
1760        let hosts_ws_inbound = commons.commons.items.iter().any(|i| match i {
1761            CommonsItem::Service(s) => {
1762                matches!(s.protocol, ServiceProtocol::WebSocket { .. })
1763                    && s.handlers
1764                        .iter()
1765                        .any(|h| matches!(h.kind, HandlerKind::Message | HandlerKind::Close))
1766            }
1767            _ => false,
1768        });
1769        if workers && hosts_ws_inbound {
1770            parts.push("WorkersConnection");
1771        }
1772        if has_http {
1773            // `HttpResult` is both a value (the constructor namespace) and a
1774            // type (the discriminated union). A bare named import brings both
1775            // in — `type HttpResult` would duplicate the identifier.
1776            parts.push(HTTP_RESULT);
1777        }
1778        if has_queue {
1779            // v0.44: `QueueResult` is both a value (the verdict namespace) and a
1780            // type; a bare named import brings both in.
1781            parts.push(QUEUE_RESULT);
1782        }
1783        if workers {
1784            parts.push("type JsonValue");
1785            parts.push("type BoundaryError");
1786            parts.push("type ServiceBinding");
1787            parts.push("callService");
1788            parts.push("boundaryError");
1789        } else if uses_codec || has_agent {
1790            // v0.22b: the bundle-mode codec helpers reference JsonValue and
1791            // BoundaryError. v0.96 (ADR 0124): so do an agent's emitted
1792            // rehydration deserialisers and the gate's inline base checks — the
1793            // boundary helpers now emit on bundle too (for the rehydration gate).
1794            parts.push("type JsonValue");
1795            parts.push("type BoundaryError");
1796        }
1797        writeln!(
1798            out,
1799            "import {{ {} }} from \"{runtime_import}\";",
1800            parts.join(", ")
1801        )
1802        .unwrap();
1803        writeln!(out).unwrap();
1804    }
1805}
1806
1807/// Variant of write_header for single-file (no project context) emission.
1808fn write_header_single(out: &mut String, commons: &TypedCommons, uses_bytes: bool) {
1809    writeln!(out, "// Generated by bynkc — do not edit by hand.").unwrap();
1810    writeln!(out, "// commons {}", commons.commons.name.joined()).unwrap();
1811    writeln!(out).unwrap();
1812    if !commons.commons.items.is_empty() {
1813        // v0.22b: codec imports only when the file uses the `Json` codec.
1814        let uses_codec = !collect_json_codec_roots(commons).is_empty();
1815        let codec_imports = if uses_codec {
1816            ", type JsonError, type JsonValue, type BoundaryError"
1817        } else if file_mentions_json_error(commons) {
1818            ", type JsonError"
1819        } else {
1820            ""
1821        };
1822        // v0.110 (ADR 0142): the `Bytes` runtime helpers, imported only when a
1823        // `Bytes` value is constructed or compared in the body.
1824        let bytes_imports = if uses_bytes {
1825            BYTES_RUNTIME_IMPORTS
1826        } else {
1827            ""
1828        };
1829        writeln!(
1830            out,
1831            "import {{ Ok, Err, Some, None, type Result, type Option, type ValidationError{codec_imports}{bytes_imports} }} from \"./runtime.js\";",
1832        )
1833        .unwrap();
1834        writeln!(out).unwrap();
1835    }
1836}
1837
1838/// v0.110 (ADR 0142): the `Bytes` runtime helpers, appended to a module's
1839/// import list when the emitted body references them. `bytesEqual` backs `==`;
1840/// the base64/UTF-8 helpers back the kernel and codec.
1841const BYTES_RUNTIME_IMPORTS: &str =
1842    ", __bynkBytesEqual, __bynkBytesToBase64, __bynkBytesFromBase64, __bynkBytesDecodeUtf8";
1843
1844/// Emit the commons-level doc block (if any) at the current position.
1845fn write_commons_doc(out: &mut String, commons: &TypedCommons) {
1846    if let Some(doc) = &commons.commons.documentation {
1847        emit_doc_block(out, Some(doc), 0);
1848        writeln!(out).unwrap();
1849    }
1850}
1851
1852/// The module-level state-registry constant name for an agent class.
1853fn agent_registry_name(agent: &str) -> String {
1854    format!("__{agent}Registry")
1855}
1856
1857/// The exported agent-construction factory name for an agent class.
1858pub fn agent_factory_name(agent: &str) -> String {
1859    format!("__make{agent}")
1860}
1861
1862/// Per-function lowering context: fresh-temp counter + typed-commons handle
1863/// (used to look up receiver types for method-call UFCS lowering).
1864pub(crate) struct LowerCtx<'a> {
1865    next_tmp: u32,
1866    commons: &'a TypedCommons,
1867    /// Names of capabilities in scope as `given C1, C2, ...`. Used to lower
1868    /// `Capability.op(args)` calls to `deps.Capability.op(args)`.
1869    capabilities: HashSet<String>,
1870    /// True when lowering an agent handler body. Used to rewrite `self.state`
1871    /// and `self.<keyField>` access into the appropriate locals.
1872    in_agent_handler: bool,
1873    /// The local variable holding the loaded state inside an agent handler.
1874    agent_state_var: Option<String>,
1875    /// The name of the agent's `key id` field (so `self.<id>` resolves).
1876    agent_key_field: Option<String>,
1877    /// v0.80: when lowering an agent invariant predicate, the name of the
1878    /// proposed-state variable (the `commitState` parameter) and the set of
1879    /// state field names. A bare ident matching a state field lowers to
1880    /// `<var>.<field>` — invariants read state fields directly (§14).
1881    invariant_state: Option<(String, HashSet<String>)>,
1882    /// v0.81 (storage track): when lowering a `store`-agent handler body, the
1883    /// name of the mutable working-state variable (`__state`) and the set of
1884    /// `Cell` field names. A bare `Cell` read lowers to `<var>.<cell>`, and a
1885    /// `cell := v` write lowers to `<var>.<cell> = <v>` — read-your-writes via the
1886    /// in-memory record, flushed once at handler end (ADR 0109).
1887    agent_store_state: Option<(String, HashSet<String>)>,
1888    /// v0.82 (ADR 0110): the agent's `store` `Map` field names. A method call
1889    /// whose receiver is one lowers to an entry operation over `__state.<map>`
1890    /// (a JSON-serialisable `Record<string, V>`), staged in the working record and
1891    /// flushed at commit like any other state field.
1892    agent_store_maps: HashSet<String>,
1893    /// v0.83: the agent's `store` `Set` field names. A method call whose receiver
1894    /// is one lowers to an entry operation over `__state.<set>` (a
1895    /// `Record<string, boolean>`), staged in the working record.
1896    agent_store_sets: HashSet<String>,
1897    /// v0.87 (ADR 0113): the agent's `store` `Cache` fields (name → ttl millis).
1898    /// A method call whose receiver is one lowers to an entry op over
1899    /// `__state.<cache>` (a `Record<string, { v, exp }>`), applying TTL expiry
1900    /// against the injected `Clock`.
1901    agent_store_caches: HashMap<String, i64>,
1902    /// v0.95 (ADR 0121): the agent's `store` `Log` fields (name → optional
1903    /// `@retain` millis). `<log>.append` pushes `{ t: now(), v }` to
1904    /// `__state.<log>` (an array) and prunes past the retain horizon; the
1905    /// time-window roots / builders lower to a query pipeline over the array.
1906    agent_store_logs: HashMap<String, Option<i64>>,
1907    /// v0.93 (ADR 0118): the agent's `@indexed` secondary indexes (map name →
1908    /// the value-record fields indexed on). A mutating op on the map maintains a
1909    /// sibling posting-list `Record<string, string[]>` per field (`<map>__idx_<f>`);
1910    /// an equality `filter` on an indexed field routes to a posting lookup.
1911    agent_store_indexes: HashMap<String, Vec<String>>,
1912    /// v0.104 (real-time track slice 3b): when lowering a `from WebSocket`
1913    /// `on open` body **into its hosting Durable Object** (the agent the upgrade
1914    /// transfers the connection to), the name of that agent. A transfer call
1915    /// `<Agent>(<key>).method(args)` whose `<Agent>` is this self-agent lowers to a
1916    /// direct `this.method(args, deps)` self-call rather than the cross-instance
1917    /// `__make<Agent>(key)` factory — the connection is already in this DO, so it
1918    /// never crosses an RPC boundary (DECISION A). `None` everywhere else.
1919    pub ws_self_agent: Option<String>,
1920    /// v0.104/v0.105 (real-time track slice 3b): the agent's held `store Map[K,
1921    /// Connection]` fields (name → the connection's **frame type** `F`, e.g.
1922    /// `ServerFrame`). On Workers these persist `K → connId` in the durable state
1923    /// record; a method call whose receiver is one lowers to an entry op over
1924    /// `__state.<map>` (the connId record) with `connIdOf`/`resolveConnection<F>` —
1925    /// not the plain `Record<string, V>` ops (held maps are excluded from
1926    /// `agent_store_maps`).
1927    pub agent_held_maps: HashMap<String, String>,
1928    /// Cross-context info for v0.6 cross-context call lowering.
1929    cross_context: &'a bynk_check::resolver::CrossContextInfo,
1930    /// True if the current handler made at least one cross-context call
1931    /// (drives whether `deps` gets a `surface` field type).
1932    cross_context_used: bool,
1933    /// v0.7: when lowering a test case body, the target context's local
1934    /// service names. A `service.call(args)` or `service(args)` invocation
1935    /// where `service` is in this set lowers to `<service>.call(args, deps)`
1936    /// so the test wires its `deps` through.
1937    pub test_services: HashSet<String>,
1938    /// v0.7: when lowering a test case body, the target context's local
1939    /// agent names. `<Agent>(<key>).method(args)` lowers to
1940    /// `new Agent(makeTestState(...)).method(args, {})`.
1941    pub test_agents: HashSet<String>,
1942    /// Agent names declared in the surrounding context. Drives lowering of
1943    /// `Agent(key)` (to `new Agent(makeTestState(String(key)))`) and of
1944    /// `agent_instance.method(args)` (to `instance.method(args, deps)`) in
1945    /// service and agent-handler bodies. Populated by the caller for non-test
1946    /// emission and from `test_agents` in test emission.
1947    pub local_agents: HashSet<String>,
1948    /// Variable bindings that point at agent instances. Updated by the
1949    /// statement emitter when it sees `let x = AgentName(key)`. Used by
1950    /// the method-call lowering so `x.method(args)` resolves through
1951    /// the agent's class rather than via the receiver-namespace lookup.
1952    pub local_agent_vars: HashMap<String, String>,
1953    /// v0.8 build target. In workers mode cross-context calls lower to
1954    /// `callService(...)` instead of `deps.surface.<key>.<method>(...)`.
1955    pub target: BuildTarget,
1956    /// v0.9.2: set when the body instantiates a local agent. In workers mode
1957    /// this drives `env` (carrying the DO namespaces) into the handler's deps
1958    /// type so the agent factory can reach its Durable Object binding.
1959    pub agents_instantiated: bool,
1960    /// When an `is` receiver is not a simple, repeatable lvalue (e.g. a call
1961    /// like `parse(x) is Ok(n)`), it is evaluated once into a temp; the temp
1962    /// name is cached here keyed by the receiver expression's span so the
1963    /// `.tag` check and every pattern binding reference the *same* single
1964    /// evaluation. Simple receivers (idents / field chains) are never cached
1965    /// and continue to be rendered inline as before.
1966    is_receiver_temps: HashMap<bynk_syntax::span::Span, String>,
1967    /// v0.12: the receiver expression a capability call resolves against —
1968    /// `deps` in a handler body, `this.deps` in a composed provider body.
1969    cap_deps_expr: String,
1970    /// v0.47: when lowering a Bearer handler body, the `by` binder whose
1971    /// `.identity` is threaded through `deps` (so `<binder>.identity` lowers to
1972    /// `deps.identity` rather than the unit-value `undefined`).
1973    pub deps_identity_binder: Option<String>,
1974    /// v0.52: when lowering a multi-actor sum handler body, the `by` binder that
1975    /// names the resolved-actor value (threaded through `deps`, so the binder
1976    /// ident lowers to `deps.who` — the tagged union the body `match`es).
1977    pub actor_sum_binder: Option<String>,
1978    /// v0.59: when lowering a **test case body**, the source text and
1979    /// project-relative path of the file the body came from, so an `assert`
1980    /// can emit a real `path:line:col` location (for `--format json`
1981    /// click-through) rather than a bare byte offset. `None` for non-test
1982    /// emission, where `assert` doesn't appear.
1983    pub assert_loc: Option<AssertLoc>,
1984    /// Slice 1 (ADR 0103): the source-map builder for the file being emitted, if
1985    /// any. The deep lowering chain records `(generated offset → source span)`
1986    /// checkpoints here; `emit_project` owns the `RefCell` and threads a shared
1987    /// borrow in. `None` for the single-file `emit()` path and any body emitted
1988    /// outside a project, where no map is produced.
1989    pub source_map: Option<&'a RefCell<SourceMapBuilder>>,
1990}
1991
1992/// v0.59: the source context an `assert` lowering needs to turn its span into a
1993/// `path:line:col` location. Owned (cloned once per test-case body) to keep the
1994/// lowering free of extra lifetime threading; test-file sources are small and
1995/// this is compile-time only.
1996#[derive(Clone)]
1997pub(crate) struct AssertLoc {
1998    pub source: String,
1999    pub rel_path: String,
2000}
2001
2002impl<'a> LowerCtx<'a> {
2003    fn new(
2004        commons: &'a TypedCommons,
2005        cross_context: &'a bynk_check::resolver::CrossContextInfo,
2006    ) -> Self {
2007        Self {
2008            next_tmp: 0,
2009            commons,
2010            capabilities: HashSet::new(),
2011            in_agent_handler: false,
2012            agent_state_var: None,
2013            agent_key_field: None,
2014            invariant_state: None,
2015            agent_store_state: None,
2016            agent_store_maps: HashSet::new(),
2017            agent_store_sets: HashSet::new(),
2018            agent_store_caches: HashMap::new(),
2019            agent_store_logs: HashMap::new(),
2020            agent_store_indexes: HashMap::new(),
2021            ws_self_agent: None,
2022            agent_held_maps: HashMap::new(),
2023            cross_context,
2024            cross_context_used: false,
2025            test_services: HashSet::new(),
2026            test_agents: HashSet::new(),
2027            local_agents: HashSet::new(),
2028            local_agent_vars: HashMap::new(),
2029            target: BuildTarget::Bundle,
2030            agents_instantiated: false,
2031            is_receiver_temps: HashMap::new(),
2032            cap_deps_expr: "deps".to_string(),
2033            deps_identity_binder: None,
2034            actor_sum_binder: None,
2035            assert_loc: None,
2036            source_map: None,
2037        }
2038    }
2039
2040    /// Attach the file's source-map builder (slice 1, ADR 0103). Builder-style so
2041    /// the existing `LowerCtx::new(commons, cross)` call sites stay untouched —
2042    /// only the project-emission path that has a builder calls this.
2043    fn with_source_map(mut self, map: Option<&'a RefCell<SourceMapBuilder>>) -> Self {
2044        self.source_map = map;
2045        self
2046    }
2047
2048    /// Record a checkpoint: generated text from `out_len` onward originates at
2049    /// `span`, until the next checkpoint (ADR 0103 D2, nearest-enclosing). A
2050    /// no-op when no builder is attached. `out_len` is the buffer length *before*
2051    /// the statement's text is appended.
2052    fn record_span(&self, out_len: usize, span: bynk_syntax::span::Span) {
2053        if let Some(map) = self.source_map {
2054            map.borrow_mut().record(out_len, span);
2055        }
2056    }
2057    /// v0.9.2: lower an agent instantiation `AgentName(key)` to its factory
2058    /// call. Bundle/test mode passes only the key; workers mode also threads
2059    /// `deps.env` so the factory can reach the agent's DO namespace.
2060    fn agent_construct(&mut self, agent: &str, key_expr: &str) -> String {
2061        self.agents_instantiated = true;
2062        let factory = agent_factory_name(agent);
2063        if matches!(self.target, BuildTarget::Workers) {
2064            format!("{factory}({key_expr}, deps.env)")
2065        } else {
2066            format!("{factory}({key_expr})")
2067        }
2068    }
2069    fn fresh(&mut self) -> String {
2070        let n = self.next_tmp;
2071        self.next_tmp += 1;
2072        format!("__r{n}")
2073    }
2074    /// Return a stable textual reference to an `is` receiver, used by the
2075    /// `.tag` check in `lower_is`. A simple, repeatable lvalue is lowered
2076    /// inline exactly as before (preserving rewrites such as `self.state` or
2077    /// capability access). A complex receiver (anything `value_text_for_is`
2078    /// could not render — e.g. a call) is evaluated once into a fresh temp
2079    /// emitted into `stmts` and cached by span, so the bindings gathered later
2080    /// reference the same evaluation rather than re-running the expression.
2081    fn is_receiver_ref(&mut self, value: &Expr, stmts: &mut Vec<String>) -> String {
2082        if let Some(t) = self.is_receiver_temps.get(&value.span) {
2083            return t.clone();
2084        }
2085        let lowered = lower_expr(value, stmts, self);
2086        if is_simple_is_receiver(value) {
2087            return lowered;
2088        }
2089        let tmp = self.fresh();
2090        stmts.push(format!("const {tmp} = {lowered};"));
2091        self.is_receiver_temps.insert(value.span, tmp.clone());
2092        tmp
2093    }
2094
2095    /// v0.13: like `is_receiver_ref` but always lifts to a temp, even for a
2096    /// simple ident. A refined `is`-narrowing re-binds the value's name to the
2097    /// branded refined type (`const n = <temp> as Quantity`); that shadowing
2098    /// const cannot reference the same name (TDZ), so the value is captured in a
2099    /// temp first and both the check and the binding read the temp.
2100    fn is_receiver_ref_forced(&mut self, value: &Expr, stmts: &mut Vec<String>) -> String {
2101        if let Some(t) = self.is_receiver_temps.get(&value.span) {
2102            return t.clone();
2103        }
2104        let lowered = lower_expr(value, stmts, self);
2105        let tmp = self.fresh();
2106        stmts.push(format!("const {tmp} = {lowered};"));
2107        self.is_receiver_temps.insert(value.span, tmp.clone());
2108        tmp
2109    }
2110
2111    /// v0.13: true when `value is Name` is a *refinement* check — the value is a
2112    /// base/refined value and `Name` is a refined type — rather than a sum
2113    /// variant test. Mirrors the checker's disambiguation.
2114    fn is_refined_is_check(&self, value: &Expr, name: &str) -> bool {
2115        let value_baseish = matches!(
2116            self.commons.expr_types.get(&value.span),
2117            Some(Ty::Base(_))
2118                | Some(Ty::Named {
2119                    kind: NamedKind::Refined(_),
2120                    ..
2121                })
2122        );
2123        let name_refined = matches!(
2124            self.commons.types.get(name).map(|d| &d.body),
2125            Some(TypeBody::Refined { .. })
2126        );
2127        value_baseish && name_refined
2128    }
2129    /// Read-only counterpart for the binding gatherer (which has no `stmts`
2130    /// and cannot lift). If the receiver was already lifted to a temp during
2131    /// condition lowering, reuse that temp; otherwise it must be a simple
2132    /// repeatable lvalue, rendered inline. The "lower the condition before
2133    /// gathering its bindings" ordering in `emit_if_tail` / `lower_and_with_is`
2134    /// guarantees the temp exists before this is called for complex receivers.
2135    fn is_receiver_text(&self, value: &Expr) -> String {
2136        if let Some(t) = self.is_receiver_temps.get(&value.span) {
2137            return t.clone();
2138        }
2139        value_text_for_is(value)
2140    }
2141    fn receiver_namespace(&self, e: &Expr) -> Option<String> {
2142        let ty = self.commons.expr_types.get(&e.span)?;
2143        if let Ty::Named { name, .. } = ty {
2144            Some(name.clone())
2145        } else {
2146            None
2147        }
2148    }
2149    /// Resolve the payload field name for the i-th positional binding of
2150    /// a variant. Built-ins are recognised by name; user variants are
2151    /// looked up via the type tables.
2152    fn positional_field_name(
2153        &self,
2154        discriminant_ty: Option<&Ty>,
2155        variant: &str,
2156        idx: usize,
2157    ) -> String {
2158        match (variant, idx) {
2159            ("Ok", 0) | ("Some", 0) => return "value".to_string(),
2160            ("Err", 0) => return "error".to_string(),
2161            _ => {}
2162        }
2163        // v0.52: a multi-actor sum arm binds the resolved actor's identity,
2164        // carried in the `identity` field of the tagged object.
2165        if let Some(Ty::ActorSum(_)) = discriminant_ty {
2166            return "identity".to_string();
2167        }
2168        if let Some(Ty::Named {
2169            kind: NamedKind::Sum,
2170            name,
2171        }) = discriminant_ty
2172            && let Some(decl) = self.commons.types.get(name)
2173            && let TypeBody::Sum(s) = &decl.body
2174            && let Some(v) = s.variants.iter().find(|v| v.name.name == variant)
2175            && let Some(f) = v.payload.get(idx)
2176        {
2177            return f.name.name.clone();
2178        }
2179        // Single-field fallback. The checker rejects mixed bindings already.
2180        "value".to_string()
2181    }
2182}
2183
2184fn ts_base(b: BaseType) -> &'static str {
2185    match b {
2186        BaseType::Int => "number",
2187        BaseType::String => "string",
2188        BaseType::Bool => "boolean",
2189        BaseType::Float => "number",
2190        BaseType::Duration | BaseType::Instant => "number",
2191        // v0.110 (ADR 0142): `Bytes` is the one base type that does NOT erase
2192        // to `number` — it lowers to an immutable octet sequence, `Uint8Array`.
2193        BaseType::Bytes => "Uint8Array",
2194    }
2195}
2196
2197pub(crate) fn ts_type_ref(r: &TypeRef) -> String {
2198    ts_type_ref_with(r, None)
2199}
2200
2201/// Like `ts_type_ref`, but qualifies named types that live in `scope` with the
2202/// namespace `ns` (`Order` → `Ns.Order`). Used by the test-emission harness for
2203/// mock method signatures that sit outside the destructuring that brings a
2204/// namespace's value-side names into local scope, so the types must be
2205/// referenced fully qualified. Qualification recurses through generic
2206/// arguments; base/unit types are unaffected.
2207pub(crate) fn ts_type_ref_qualified(r: &TypeRef, scope: &HashSet<String>, ns: &str) -> String {
2208    ts_type_ref_with(r, Some((scope, ns)))
2209}
2210
2211/// Shared renderer behind `ts_type_ref` (`qualify = None`) and
2212/// `ts_type_ref_qualified` (`qualify = Some((scope, ns))`). With `None` it is
2213/// output-identical to the historic `ts_type_ref`; the only divergence is the
2214/// `Named` arm, which qualifies in-scope names when `qualify` is set.
2215fn ts_type_ref_with(r: &TypeRef, qualify: Option<(&HashSet<String>, &str)>) -> String {
2216    match r {
2217        TypeRef::Base(b, _) => ts_base(*b).to_string(),
2218        TypeRef::Named(id) => {
2219            if let Some((scope, ns)) = qualify
2220                && scope.contains(&id.name)
2221            {
2222                format!("{ns}.{}", id.name)
2223            } else {
2224                id.name.clone()
2225            }
2226        }
2227        TypeRef::Result(t, e, _) => format!(
2228            "Result<{}, {}>",
2229            ts_type_ref_with(t, qualify),
2230            ts_type_ref_with(e, qualify)
2231        ),
2232        TypeRef::Option(t, _) => format!("Option<{}>", ts_type_ref_with(t, qualify)),
2233        TypeRef::Effect(t, _) => {
2234            let inner = ts_type_ref_with(t, qualify);
2235            if inner == "()" || inner == "void" {
2236                "Promise<void>".to_string()
2237            } else {
2238                format!("Promise<{inner}>")
2239            }
2240        }
2241        TypeRef::HttpResult(t, _) => format!("HttpResult<{}>", ts_type_ref_with(t, qualify)),
2242        // v0.20b: collections lower to immutable TS shapes.
2243        TypeRef::List(t, _) => format!("readonly {}[]", ts_type_ref_with(t, qualify)),
2244        TypeRef::Query(t, _) => {
2245            format!("(() => readonly {}[])", ts_type_ref_with(t, qualify))
2246        }
2247        // v0.100: `Stream[T]` lowers to a host async iterable.
2248        TypeRef::Stream(t, _) => format!("AsyncIterable<{}>", ts_type_ref_with(t, qualify)),
2249        // v0.102: a `Connection[F]` lowers to the runtime `Connection<F>`
2250        // interface (the concrete implementation arrives with the protocol).
2251        TypeRef::Connection(t, _) => format!("Connection<{}>", ts_type_ref_with(t, qualify)),
2252        TypeRef::Map(k, v, _) => {
2253            format!(
2254                "ReadonlyMap<{}, {}>",
2255                ts_type_ref_with(k, qualify),
2256                ts_type_ref_with(v, qualify)
2257            )
2258        }
2259        TypeRef::QueueResult(_) => "QueueResult".to_string(),
2260        TypeRef::ValidationError(_) => "ValidationError".to_string(),
2261        TypeRef::JsonError(_) => "JsonError".to_string(),
2262        TypeRef::Unit(_) => "void".to_string(),
2263        // v0.20a: a function type lowers to a TS function type. Positional
2264        // parameter names (`a0`, `a1`, …) — TS requires names in function
2265        // type syntax; an Effect return is already Promise via recursion.
2266        TypeRef::Fn(params, ret, _) => {
2267            let params: Vec<String> = params
2268                .iter()
2269                .enumerate()
2270                .map(|(i, p)| format!("a{i}: {}", ts_type_ref_with(p, qualify)))
2271                .collect();
2272            let ret = match ts_type_ref_with(ret, qualify).as_str() {
2273                "()" => "void".to_string(),
2274                other => other.to_string(),
2275            };
2276            format!("({}) => {ret}", params.join(", "))
2277        }
2278    }
2279}
2280
2281/// v0.20b: render a checker `Ty` as a TypeScript type. Used by the inline
2282/// kernel-method lowerings, whose IIFE parameters must be annotated
2283/// (`noImplicitAny`). Rigid type variables render as themselves — inside an
2284/// emitted generic function they are in scope as TS type parameters.
2285fn ts_ty(t: &Ty) -> String {
2286    match t {
2287        Ty::Base(BaseType::Int) => "number".to_string(),
2288        Ty::Base(BaseType::String) => "string".to_string(),
2289        Ty::Base(BaseType::Bool) => "boolean".to_string(),
2290        Ty::Base(BaseType::Float) => "number".to_string(),
2291        Ty::Base(BaseType::Duration | BaseType::Instant) => "number".to_string(),
2292        // v0.110 (ADR 0142): `Bytes` erases to `Uint8Array`, not `number`.
2293        Ty::Base(BaseType::Bytes) => "Uint8Array".to_string(),
2294        Ty::Named { name, .. } => name.clone(),
2295        Ty::Result(t, e) => format!("Result<{}, {}>", ts_ty(t), ts_ty(e)),
2296        Ty::Option(t) => format!("Option<{}>", ts_ty(t)),
2297        Ty::Effect(t) => match &**t {
2298            Ty::Unit => "Promise<void>".to_string(),
2299            other => format!("Promise<{}>", ts_ty(other)),
2300        },
2301        Ty::HttpResult(t) => format!("HttpResult<{}>", ts_ty(t)),
2302        Ty::List(t) => format!("readonly {}[]", ts_ty(t)),
2303        // v0.91 (ADR 0119): a `Query[T]` lowers to a deferred producer of its
2304        // elements — a thunk run by the terminal.
2305        Ty::Query(t) => format!("(() => readonly {}[])", ts_ty(t)),
2306        // v0.100: a `Stream[T]` lowers to a host async iterable.
2307        Ty::Stream(t) => format!("AsyncIterable<{}>", ts_ty(t)),
2308        // v0.102: a `Connection[F]` lowers to the runtime `Connection<F>` interface.
2309        Ty::Connection(t) => format!("Connection<{}>", ts_ty(t)),
2310        Ty::Map(k, v) => format!("ReadonlyMap<{}, {}>", ts_ty(k), ts_ty(v)),
2311        Ty::QueueResult => "QueueResult".to_string(),
2312        Ty::ValidationError => "ValidationError".to_string(),
2313        Ty::JsonError => "JsonError".to_string(),
2314        Ty::Unit => "void".to_string(),
2315        Ty::Fn { params, ret } => {
2316            let params: Vec<String> = params
2317                .iter()
2318                .enumerate()
2319                .map(|(i, p)| format!("a{i}: {}", ts_ty(p)))
2320                .collect();
2321            format!("({}) => {}", params.join(", "), ts_ty(ret))
2322        }
2323        Ty::Var(n) => n.clone(),
2324        // The identity type the actor binding yields (`name.identity`).
2325        Ty::Actor(id) => ts_ty(id),
2326        // v0.52: a resolved multi-actor sum lowers to a discriminated union
2327        // tagged by actor name; non-unit members carry their identity.
2328        Ty::ActorSum(members) => members
2329            .iter()
2330            .map(|(name, id)| match id {
2331                Ty::Unit => format!("{{ tag: \"{name}\" }}"),
2332                _ => format!("{{ tag: \"{name}\", identity: {} }}", ts_ty(id)),
2333            })
2334            .collect::<Vec<_>>()
2335            .join(" | "),
2336    }
2337}
2338
2339fn ts_binop(op: BinOp) -> &'static str {
2340    match op {
2341        // `implies` has no single TS operator — `lower_bin_op` rewrites it to
2342        // `(!(P) || Q)` before reaching here, so this arm is never used.
2343        BinOp::Implies => "||",
2344        BinOp::Or => "||",
2345        BinOp::And => "&&",
2346        BinOp::Eq => "===",
2347        BinOp::NotEq => "!==",
2348        BinOp::Lt => "<",
2349        BinOp::LtEq => "<=",
2350        BinOp::Gt => ">",
2351        BinOp::GtEq => ">=",
2352        BinOp::Add => "+",
2353        BinOp::Sub => "-",
2354        BinOp::Mul => "*",
2355        BinOp::Div => "/",
2356    }
2357}
2358
2359pub(crate) fn escape_ts_string(s: &str) -> String {
2360    let mut out = String::with_capacity(s.len());
2361    for c in s.chars() {
2362        match c {
2363            '\\' => out.push_str("\\\\"),
2364            '"' => out.push_str("\\\""),
2365            '\n' => out.push_str("\\n"),
2366            '\t' => out.push_str("\\t"),
2367            '\r' => out.push_str("\\r"),
2368            c => out.push(c),
2369        }
2370    }
2371    out
2372}
2373
2374#[allow(dead_code)]
2375fn _unused_hashmap(_h: HashMap<String, ()>) {}
2376
2377#[cfg(test)]
2378mod runtime_tests {
2379    use super::*;
2380
2381    #[test]
2382    fn runtime_emits_all_required_exports() {
2383        let s = emit_runtime_module();
2384        // Core types and constructors used by every emitted module.
2385        assert!(s.contains("export type Result<T, E>"));
2386        assert!(s.contains("export const Ok"));
2387        assert!(s.contains("export const Err"));
2388        assert!(s.contains("export type Option<T>"));
2389        assert!(s.contains("export const Some"));
2390        assert!(s.contains("export const None"));
2391        assert!(s.contains("export interface ValidationError"));
2392        // Durable Object surface used by agent classes.
2393        assert!(s.contains("export interface DurableObjectStorage"));
2394        assert!(s.contains("export interface DurableObjectState"));
2395        assert!(s.contains("export class InMemoryStorage"));
2396        assert!(s.contains("export function makeTestState"));
2397        // Discriminator must be `tag` to match emitted code.
2398        assert!(s.contains("tag: \"Ok\""));
2399        assert!(s.contains("tag: \"Err\""));
2400        assert!(s.contains("tag: \"Some\""));
2401        assert!(s.contains("tag: \"None\""));
2402    }
2403
2404    #[test]
2405    fn tsconfig_is_well_formed_json() {
2406        let s = emit_tsconfig();
2407        // Spot-check the key fields; we don't reach for a JSON parser.
2408        assert!(s.contains("\"target\": \"ES2022\""));
2409        assert!(s.contains("\"strict\": true"));
2410        assert!(s.contains("\"include\""));
2411    }
2412
2413    #[test]
2414    fn workers_dir_name_replaces_dots_with_dashes() {
2415        assert_eq!(
2416            crate::project::worker_dir_name("commerce.payment"),
2417            "commerce-payment"
2418        );
2419        assert_eq!(crate::project::worker_dir_name("a.b.c"), "a-b-c");
2420    }
2421
2422    // Refactor track: characterisation pin for the canonical `escape_ts_string`.
2423    // It escapes backslash/quote/newline/tab and carriage return (`\r` → `\r`).
2424    #[test]
2425    fn escape_ts_string_escapes_cr() {
2426        assert_eq!(escape_ts_string("a\\b"), "a\\\\b");
2427        assert_eq!(escape_ts_string("a\"b"), "a\\\"b");
2428        assert_eq!(escape_ts_string("a\nb"), "a\\nb");
2429        assert_eq!(escape_ts_string("a\tb"), "a\\tb");
2430        assert_eq!(escape_ts_string("a\rb"), "a\\rb"); // CR escaped here; raw in project copy
2431    }
2432
2433    #[test]
2434    fn runtime_import_depth_resolves_correctly() {
2435        assert_eq!(
2436            runtime_import_for(Path::new("compose.ts"), ImportExt::Js),
2437            "./runtime.js"
2438        );
2439        assert_eq!(
2440            runtime_import_for(Path::new("commerce/payment.ts"), ImportExt::Js),
2441            "../runtime.js"
2442        );
2443        assert_eq!(
2444            runtime_import_for(Path::new("commerce/orders/types.ts"), ImportExt::Js),
2445            "../../runtime.js"
2446        );
2447        assert_eq!(
2448            runtime_import_for(Path::new("tests/commerce_payment.test.ts"), ImportExt::Js),
2449            "../runtime.js"
2450        );
2451    }
2452}