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