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