1use std::cell::RefCell;
19use std::collections::{HashMap, HashSet};
20use std::fmt::Write as _;
21use std::path::{Path, PathBuf};
22
23use self::source_map::SourceMapBuilder;
24
25use crate::project::{BuildTarget, EmitProjectCtx, ImportExt, UnitKind};
26use bynk_check::builtin_names::methods::{FOLD_EFF, RAW};
27use bynk_check::builtin_names::types::*;
28use bynk_check::checker::{NamedKind, Ty, TypedCommons};
29use bynk_syntax::ast::*;
30
31pub mod serialisation;
32pub mod workers;
33pub mod workers_entry;
34pub mod wrangler;
35
36pub use workers::emit_worker_compose;
37pub use workers_entry::emit_worker_entry;
38pub use wrangler::emit_wrangler_toml;
39
40mod lower;
41pub(crate) mod source_map;
42pub(crate) use lower::*;
43mod emit;
44pub(crate) use emit::*;
45pub(crate) mod websocket;
46
47const INDENT_STEP: usize = 2;
48
49pub fn emit_runtime_module() -> String {
63 RUNTIME_TS.to_string()
64}
65
66const RUNTIME_TS: &str = include_str!("emitter/runtime.ts");
73
74pub fn emit_tsconfig() -> String {
78 TSCONFIG_JSON.to_string()
79}
80
81const TSCONFIG_JSON: &str = r#"{
82 "compilerOptions": {
83 "target": "ES2022",
84 "module": "NodeNext",
85 "moduleResolution": "NodeNext",
86 "strict": true,
87 "noImplicitAny": true,
88 "esModuleInterop": true,
89 "skipLibCheck": true,
90 "resolveJsonModule": true,
91 "isolatedModules": true,
92 "noEmit": false,
93 "outDir": "../out-js",
94 "rootDir": "."
95 },
96 "include": ["**/*.ts"]
97}
98"#;
99
100pub fn runtime_import_for(from_source: &Path, ext: ImportExt) -> String {
104 let depth = from_source
105 .parent()
106 .map(|p| {
107 p.components()
108 .filter(|c| matches!(c, std::path::Component::Normal(_)))
109 .count()
110 })
111 .unwrap_or(0);
112 let ext = ext.as_str();
113 if depth == 0 {
114 format!("./runtime.{ext}")
115 } else {
116 let prefix: String = "../".repeat(depth);
117 format!("{prefix}runtime.{ext}")
118 }
119}
120
121pub fn emit(commons: &TypedCommons) -> String {
123 let mut body = String::new();
127 write_commons_doc(&mut body, commons);
128 let dummy_ctx = single_file_ctx();
129 for item in &commons.commons.items {
131 if let CommonsItem::Type(t) = item {
132 emit_type(&mut body, t, commons, &dummy_ctx);
133 }
134 }
135 for item in &commons.commons.items {
137 if let CommonsItem::Fn(f) = item
138 && let FnName::Free(_) = &f.name
139 {
140 emit_free_fn(&mut body, f, commons, None);
141 }
142 }
143 emit_json_codec_helpers(
145 &mut body,
146 commons,
147 &dummy_ctx,
148 &HashSet::new(),
149 &HashSet::new(),
150 );
151 let mut out = String::new();
152 write_header_single(&mut out, commons, body.contains("__bynkBytes"));
153 out.push_str(&body);
154 out
155}
156
157fn single_file_ctx() -> EmitProjectCtx {
160 EmitProjectCtx {
161 import_ext: crate::project::ImportExt::Js,
162 source_path: PathBuf::new(),
163 commons_name: String::new(),
164 local_files: Vec::new(),
165 file_decl_index: crate::project::FileDeclIndex {
166 types: HashMap::new(),
167 fns: HashMap::new(),
168 methods: HashMap::new(),
169 },
170 imported_from: HashMap::new(),
171 imported_from_kind: HashMap::new(),
172 imported_decl_paths: HashMap::new(),
173 commons_dir: PathBuf::new(),
174 unit_kind: UnitKind::Commons,
175 owning_context: None,
176 exports_local: HashMap::new(),
177 exports_for_consumed: HashMap::new(),
178 consumed_types: HashMap::new(),
179 cross_context: bynk_check::resolver::CrossContextInfo::default(),
180 is_consumed_by_others: false,
181 target: BuildTarget::Bundle,
182 boundary_type_owners: HashMap::new(),
183 local_agents: HashSet::new(),
184 actors: HashMap::new(),
185 consumed_adapters: HashSet::new(),
186 }
187}
188
189pub fn emit_project(
200 commons: &TypedCommons,
201 ctx: &EmitProjectCtx,
202 source_text: &str,
203 source_name: &str,
204) -> (String, Option<String>) {
205 let mut out = String::new();
206 let smb = RefCell::new(SourceMapBuilder::new());
212 smb.borrow_mut().add_source(source_name, source_text);
215 write_header(&mut out, commons, ctx);
216 let references = collect_external_references(commons, ctx);
220 emit_project_imports(&mut out, commons, ctx, &references);
221 if !references.is_empty() {
222 writeln!(out).unwrap();
223 }
224 emit_cross_context_namespace_imports(&mut out, commons, ctx);
227 if ctx.unit_kind == UnitKind::Context {
232 emit_context_rebrands(&mut out, &references, commons, ctx);
233 }
234 write_commons_doc(&mut out, commons);
235 for item in &commons.commons.items {
236 if let CommonsItem::Type(t) = item {
237 smb.borrow_mut().record(out.len(), t.span);
238 emit_type(&mut out, t, commons, ctx);
239 }
240 }
241 for item in &commons.commons.items {
242 if let CommonsItem::Fn(f) = item
243 && let FnName::Free(_) = &f.name
244 {
245 smb.borrow_mut().record(out.len(), f.span);
246 emit_free_fn(&mut out, f, commons, Some(&smb));
247 }
248 }
249 for item in &commons.commons.items {
251 match item {
252 CommonsItem::Capability(c) => {
253 smb.borrow_mut().record(out.len(), c.span);
254 emit_capability(&mut out, c);
255 }
256 CommonsItem::Provider(p) => {
257 smb.borrow_mut().record(out.len(), p.span);
258 emit_provider(&mut out, p, commons, ctx, Some(&smb));
259 }
260 CommonsItem::Service(s) => {
261 smb.borrow_mut().record(out.len(), s.span);
262 emit_service(&mut out, s, commons, ctx, Some(&smb));
263 }
264 CommonsItem::Agent(a) => {
265 smb.borrow_mut().record(out.len(), a.span);
266 emit_agent(&mut out, a, commons, ctx, Some(&smb));
267 }
268 _ => {}
269 }
270 }
271 let agent_names: Vec<&str> = commons
275 .commons
276 .items
277 .iter()
278 .filter_map(|i| match i {
279 CommonsItem::Agent(a) => Some(a.name.name.as_str()),
280 _ => None,
281 })
282 .collect();
283 if !agent_names.is_empty() {
284 writeln!(out, "export function __resetAgents(): void {{").unwrap();
285 for name in &agent_names {
286 writeln!(out, " {}.reset();", agent_registry_name(name)).unwrap();
287 }
288 writeln!(out, "}}").unwrap();
289 writeln!(out).unwrap();
290 }
291 if ctx.unit_kind == UnitKind::Context && matches!(ctx.target, BuildTarget::Bundle) {
296 let has_services = commons
297 .commons
298 .items
299 .iter()
300 .any(|i| matches!(i, CommonsItem::Service(_)));
301 if has_services {
302 emit_make_surface(&mut out, commons, ctx);
303 }
304 }
305 let (boundary_names, boundary_insts) = emit_boundary_helpers(&mut out, commons, ctx);
313 emit_json_codec_helpers(&mut out, commons, ctx, &boundary_names, &boundary_insts);
316 let generated_file = Path::new(source_name)
318 .file_stem()
319 .map(|s| format!("{}.ts", s.to_string_lossy()))
320 .unwrap_or_else(|| "module.ts".to_string());
321 let source_map = smb.borrow().to_v3(&out, &generated_file);
322 if out.contains("__bynkBytes") {
327 out = inject_bytes_runtime_imports(out);
328 }
329 (out, source_map)
330}
331
332fn inject_bytes_runtime_imports(out: String) -> String {
337 let mut result = String::with_capacity(out.len() + BYTES_RUNTIME_IMPORTS.len());
338 let mut injected = false;
339 for line in out.split_inclusive('\n') {
340 if !injected
341 && line.starts_with("import {")
342 && line.contains("type ValidationError")
343 && let Some(pos) = line.rfind(" } from \"")
344 {
345 result.push_str(&line[..pos]);
346 result.push_str(BYTES_RUNTIME_IMPORTS);
347 result.push_str(&line[pos..]);
348 injected = true;
349 continue;
350 }
351 result.push_str(line);
352 }
353 result
354}
355
356fn walk_exprs(e: &Expr, f: &mut impl FnMut(&Expr)) {
359 f(e);
360 match &e.kind {
361 ExprKind::IntLit(_)
362 | ExprKind::FloatLit { .. }
363 | ExprKind::DurationLit { .. }
364 | ExprKind::StrLit(_)
365 | ExprKind::BoolLit(_)
366 | ExprKind::Ident(_)
367 | ExprKind::None
368 | ExprKind::UnitLit => {}
369 ExprKind::InterpStr(parts) => {
371 for part in parts {
372 if let InterpPart::Hole(hole) = part {
373 walk_exprs(hole, f);
374 }
375 }
376 }
377 ExprKind::Lambda(l) => walk_exprs(&l.body, f),
378 ExprKind::EffectPure(i)
379 | ExprKind::Expect(i)
380 | ExprKind::UnaryOp(_, i)
381 | ExprKind::Paren(i)
382 | ExprKind::Ok(i)
383 | ExprKind::Err(i)
384 | ExprKind::Some(i)
385 | ExprKind::Question(i) => walk_exprs(i, f),
386 ExprKind::Val { args, .. }
387 | ExprKind::Call { args, .. }
388 | ExprKind::ConstructorCall { args, .. } => {
389 for a in args {
390 walk_exprs(a, f);
391 }
392 }
393 ExprKind::ListLit(elems) => {
394 for el in elems {
395 walk_exprs(el, f);
396 }
397 }
398 ExprKind::RecordConstruction { fields, .. } => {
399 for fld in fields {
400 if let Some(v) = &fld.value {
401 walk_exprs(v, f);
402 }
403 }
404 }
405 ExprKind::RecordSpread {
406 base, overrides, ..
407 } => {
408 walk_exprs(base, f);
409 for fld in overrides {
410 if let Some(v) = &fld.value {
411 walk_exprs(v, f);
412 }
413 }
414 }
415 ExprKind::BinOp(_, l, r) => {
416 walk_exprs(l, f);
417 walk_exprs(r, f);
418 }
419 ExprKind::Block(b) => walk_block_exprs(b, f),
420 ExprKind::If {
421 cond,
422 then_block,
423 else_block,
424 } => {
425 walk_exprs(cond, f);
426 walk_block_exprs(then_block, f);
427 walk_block_exprs(else_block, f);
428 }
429 ExprKind::FieldAccess { receiver, .. } => walk_exprs(receiver, f),
430 ExprKind::MethodCall { receiver, args, .. } => {
431 walk_exprs(receiver, f);
432 for a in args {
433 walk_exprs(a, f);
434 }
435 }
436 ExprKind::Match { discriminant, arms } => {
437 walk_exprs(discriminant, f);
438 for arm in arms {
439 match &arm.body {
440 MatchBody::Expr(e) => walk_exprs(e, f),
441 MatchBody::Block(b) => walk_block_exprs(b, f),
442 }
443 }
444 }
445 ExprKind::Is { value, .. } => walk_exprs(value, f),
446 }
447}
448
449pub(crate) fn block_uses_send(b: &Block) -> bool {
453 fn stmt(s: &Statement) -> bool {
454 match s {
455 Statement::Send(_) => true,
456 Statement::Let(l) | Statement::EffectLet(l) => expr(&l.value),
457 Statement::Expect(a) => expr(&a.value),
458 Statement::Assign(a) => expr(&a.value),
459 }
460 }
461 fn expr(e: &Expr) -> bool {
462 match &e.kind {
463 ExprKind::Block(b) => block_uses_send(b),
464 ExprKind::If {
465 cond,
466 then_block,
467 else_block,
468 } => expr(cond) || block_uses_send(then_block) || block_uses_send(else_block),
469 ExprKind::Match { discriminant, arms } => {
470 expr(discriminant)
471 || arms.iter().any(|a| match &a.body {
472 MatchBody::Expr(e) => expr(e),
473 MatchBody::Block(b) => block_uses_send(b),
474 })
475 }
476 ExprKind::Lambda(l) => expr(&l.body),
477 _ => false,
478 }
479 }
480 b.statements.iter().any(stmt) || expr(&b.tail)
481}
482
483type StoreKinds<'a> = (
489 &'a HashSet<String>,
490 &'a HashSet<String>,
491 &'a HashSet<String>,
492 &'a HashSet<String>,
493 &'a HashSet<String>,
494);
495
496pub(crate) fn block_writes_state(b: &Block, m: StoreKinds<'_>) -> bool {
501 fn mutating_op(e: &Expr, (maps, sets, caches, logs, cells): StoreKinds<'_>) -> bool {
502 if let ExprKind::MethodCall {
503 receiver, method, ..
504 } = &e.kind
505 && let ExprKind::Ident(id) = &receiver.kind
506 {
507 if (maps.contains(&id.name) || caches.contains(&id.name))
508 && matches!(method.name.as_str(), "put" | "remove" | "update" | "upsert")
509 {
510 return true;
511 }
512 if sets.contains(&id.name) && matches!(method.name.as_str(), "add" | "remove") {
513 return true;
514 }
515 if logs.contains(&id.name) && method.name == "append" {
517 return true;
518 }
519 if cells.contains(&id.name) && method.name == "update" {
523 return true;
524 }
525 }
526 false
527 }
528 fn stmt(s: &Statement, m: StoreKinds<'_>) -> bool {
529 match s {
530 Statement::Assign(_) => true,
531 Statement::Let(l) | Statement::EffectLet(l) => expr(&l.value, m),
532 Statement::Expect(a) => expr(&a.value, m),
533 Statement::Send(s) => expr(&s.value, m),
534 }
535 }
536 fn expr(e: &Expr, m: StoreKinds<'_>) -> bool {
537 if mutating_op(e, m) {
538 return true;
539 }
540 match &e.kind {
541 ExprKind::Block(b) => block_writes_state(b, m),
542 ExprKind::If {
543 cond,
544 then_block,
545 else_block,
546 } => {
547 expr(cond, m)
548 || block_writes_state(then_block, m)
549 || block_writes_state(else_block, m)
550 }
551 ExprKind::Match { discriminant, arms } => {
552 expr(discriminant, m)
553 || arms.iter().any(|a| match &a.body {
554 MatchBody::Expr(e) => expr(e, m),
555 MatchBody::Block(b) => block_writes_state(b, m),
556 })
557 }
558 ExprKind::Paren(inner) => expr(inner, m),
559 ExprKind::MethodCall { receiver, args, .. } => {
560 expr(receiver, m) || args.iter().any(|x| expr(x, m))
561 }
562 ExprKind::Call { args, .. } => args.iter().any(|x| expr(x, m)),
563 _ => false,
564 }
565 }
566 b.statements.iter().any(|s| stmt(s, m)) || expr(&b.tail, m)
567}
568
569fn walk_block_exprs(b: &Block, f: &mut impl FnMut(&Expr)) {
570 for s in &b.statements {
571 match s {
572 Statement::Let(l) | Statement::EffectLet(l) => walk_exprs(&l.value, f),
573 Statement::Expect(a) => walk_exprs(&a.value, f),
574 Statement::Send(s) => walk_exprs(&s.value, f),
575 Statement::Assign(a) => walk_exprs(&a.value, f),
576 }
577 }
578 walk_exprs(&b.tail, f);
579}
580
581fn file_mentions_json_error(commons: &TypedCommons) -> bool {
584 fn in_type_ref(t: &TypeRef) -> bool {
585 match t {
586 TypeRef::JsonError(_) => true,
587 TypeRef::Result(a, b, _) | TypeRef::Map(a, b, _) => in_type_ref(a) || in_type_ref(b),
588 TypeRef::Option(a, _)
589 | TypeRef::Effect(a, _)
590 | TypeRef::HttpResult(a, _)
591 | TypeRef::Query(a, _)
592 | TypeRef::Stream(a, _)
593 | TypeRef::Connection(a, _)
594 | TypeRef::List(a, _) => in_type_ref(a),
595 TypeRef::Fn(params, ret, _) => params.iter().any(in_type_ref) || in_type_ref(ret),
596 TypeRef::Base(..)
597 | TypeRef::Named(_)
598 | TypeRef::QueueResult(_)
599 | TypeRef::ValidationError(_)
600 | TypeRef::Unit(_) => false,
601 }
602 }
603 let sig = |params: &[Param], ret: &TypeRef| {
604 params.iter().any(|p| in_type_ref(&p.type_ref)) || in_type_ref(ret)
605 };
606 commons.commons.items.iter().any(|item| match item {
607 CommonsItem::Fn(f) => sig(&f.params, &f.return_type),
608 CommonsItem::Service(s) => s.handlers.iter().any(|h| sig(&h.params, &h.return_type)),
609 CommonsItem::Agent(a) => a.handlers.iter().any(|h| sig(&h.params, &h.return_type)),
610 CommonsItem::Provider(p) => p.ops.iter().any(|op| sig(&op.params, &op.return_type)),
611 CommonsItem::Type(t) => match &t.body {
612 TypeBody::Record(r) => r.fields.iter().any(|f| in_type_ref(&f.type_ref)),
613 TypeBody::Sum(s) => s
614 .variants
615 .iter()
616 .any(|v| v.payload.iter().any(|p| in_type_ref(&p.type_ref))),
617 TypeBody::Refined { .. } | TypeBody::Opaque { .. } => false,
618 },
619 _ => false,
620 })
621}
622
623fn file_mentions_connection(commons: &TypedCommons) -> bool {
628 fn in_type_ref(t: &TypeRef) -> bool {
629 match t {
630 TypeRef::Connection(..) => true,
631 TypeRef::Result(a, b, _) | TypeRef::Map(a, b, _) => in_type_ref(a) || in_type_ref(b),
632 TypeRef::Option(a, _)
633 | TypeRef::Effect(a, _)
634 | TypeRef::HttpResult(a, _)
635 | TypeRef::Query(a, _)
636 | TypeRef::Stream(a, _)
637 | TypeRef::List(a, _) => in_type_ref(a),
638 TypeRef::Fn(params, ret, _) => params.iter().any(in_type_ref) || in_type_ref(ret),
639 _ => false,
640 }
641 }
642 let sig = |params: &[Param], ret: &TypeRef| {
643 params.iter().any(|p| in_type_ref(&p.type_ref)) || in_type_ref(ret)
644 };
645 commons.commons.items.iter().any(|item| match item {
646 CommonsItem::Fn(f) => sig(&f.params, &f.return_type),
647 CommonsItem::Service(s) => s.handlers.iter().any(|h| sig(&h.params, &h.return_type)),
648 CommonsItem::Agent(a) => {
649 a.handlers.iter().any(|h| sig(&h.params, &h.return_type))
650 || a.store_fields
651 .iter()
652 .any(|f| f.kind.args.iter().any(in_type_ref))
653 }
654 CommonsItem::Capability(c) => c.ops.iter().any(|op| sig(&op.params, &op.return_type)),
655 CommonsItem::Provider(p) => p.ops.iter().any(|op| sig(&op.params, &op.return_type)),
656 _ => false,
657 })
658}
659
660fn ty_to_type_ref(t: &Ty) -> Option<TypeRef> {
664 let sp = bynk_syntax::span::Span::new(0, 0);
665 Some(match t {
666 Ty::Base(b) => TypeRef::Base(*b, sp),
667 Ty::Named { name, .. } => TypeRef::Named(Ident {
668 name: name.clone(),
669 span: sp,
670 }),
671 Ty::Result(a, b) => TypeRef::Result(
672 Box::new(ty_to_type_ref(a)?),
673 Box::new(ty_to_type_ref(b)?),
674 sp,
675 ),
676 Ty::Option(a) => TypeRef::Option(Box::new(ty_to_type_ref(a)?), sp),
677 Ty::List(a) => TypeRef::List(Box::new(ty_to_type_ref(a)?), sp),
678 Ty::Map(k, v) => TypeRef::Map(
679 Box::new(ty_to_type_ref(k)?),
680 Box::new(ty_to_type_ref(v)?),
681 sp,
682 ),
683 Ty::Unit => TypeRef::Unit(sp),
684 Ty::ValidationError => TypeRef::ValidationError(sp),
685 Ty::JsonError => TypeRef::JsonError(sp),
686 Ty::Effect(_)
687 | Ty::Query(_)
688 | Ty::Stream(_)
689 | Ty::Connection(_)
690 | Ty::HttpResult(_)
691 | Ty::QueueResult
692 | Ty::Fn { .. }
693 | Ty::Var(_)
694 | Ty::Actor(_)
695 | Ty::ActorSum(_) => {
696 return None;
697 }
698 })
699}
700
701fn collect_json_codec_roots(commons: &TypedCommons) -> Vec<TypeRef> {
704 let mut roots: Vec<TypeRef> = Vec::new();
705 {
706 let mut visit = |e: &Expr| {
707 let ExprKind::MethodCall {
708 receiver,
709 method,
710 args,
711 ..
712 } = &e.kind
713 else {
714 return;
715 };
716 let ExprKind::Ident(id) = &receiver.kind else {
717 return;
718 };
719 if id.name != JSON {
720 return;
721 }
722 match method.name.as_str() {
723 "decode" => {
724 if let Some(Ty::Result(t, _)) = commons.expr_types.get(&e.span)
725 && let Some(tr) = ty_to_type_ref(t)
726 {
727 roots.push(tr);
728 }
729 }
730 "encode" => {
731 if let Some(a) = args.first()
732 && let Some(t) = commons.expr_types.get(&a.span)
733 && let Some(tr) = ty_to_type_ref(t)
734 {
735 roots.push(tr);
736 }
737 }
738 _ => {}
739 }
740 };
741 for item in &commons.commons.items {
742 match item {
743 CommonsItem::Fn(f) => walk_block_exprs(&f.body, &mut visit),
744 CommonsItem::Service(s) => {
745 for h in &s.handlers {
746 walk_block_exprs(&h.body, &mut visit);
747 }
748 }
749 CommonsItem::Agent(a) => {
750 for h in &a.handlers {
751 walk_block_exprs(&h.body, &mut visit);
752 }
753 }
754 CommonsItem::Provider(p) => {
755 for op in &p.ops {
756 walk_block_exprs(&op.body, &mut visit);
757 }
758 }
759 _ => {}
760 }
761 }
762 }
763 roots
764}
765
766fn emit_json_codec_helpers(
772 out: &mut String,
773 commons: &TypedCommons,
774 ctx: &EmitProjectCtx,
775 skip_names: &HashSet<String>,
776 skip_insts: &HashSet<String>,
777) {
778 use serialisation::{collect_codec_closure, emit_generic_helpers, emit_helpers_for_owner};
779 let roots = collect_json_codec_roots(commons);
780 if roots.is_empty() {
781 return;
782 }
783 let (names, insts) = collect_codec_closure(&roots, &commons.types);
784 let names: Vec<String> = names
785 .into_iter()
786 .filter(|n| !skip_names.contains(n))
787 .collect();
788 emit_helpers_for_owner(out, &names, &commons.types, &ctx.commons_name);
789 let insts: Vec<serialisation::GenericInst> = insts
790 .into_iter()
791 .filter(|i| !skip_insts.contains(&i.ts_name()))
792 .collect();
793 if !insts.is_empty() {
794 emit_generic_helpers(out, &insts);
795 }
796}
797
798fn emit_boundary_helpers(
805 out: &mut String,
806 commons: &TypedCommons,
807 ctx: &EmitProjectCtx,
808) -> (HashSet<String>, HashSet<String>) {
809 use serialisation::{
810 collect_boundary_types, collect_generic_instantiations, emit_generic_helpers,
811 emit_helpers_for_owner,
812 };
813
814 let workers = matches!(ctx.target, BuildTarget::Workers);
824 let services: HashMap<String, ServiceDecl> = if workers {
825 commons
826 .commons
827 .items
828 .iter()
829 .filter_map(|i| match i {
830 CommonsItem::Service(s) => Some((s.name.name.clone(), s.clone())),
831 _ => None,
832 })
833 .collect()
834 } else {
835 HashMap::new()
836 };
837
838 let agents: HashMap<String, AgentDecl> = commons
841 .commons
842 .items
843 .iter()
844 .filter_map(|i| match i {
845 CommonsItem::Agent(a) => Some((a.name.name.clone(), a.clone())),
846 _ => None,
847 })
848 .collect();
849
850 let locally_declared: HashSet<String> = ctx.file_decl_index.types.keys().cloned().collect();
851 if ctx.unit_kind == UnitKind::Context {
852 let boundary_types_all = collect_boundary_types(&commons.types, &services, &agents);
853 let local_boundary: Vec<String> = boundary_types_all
858 .iter()
859 .filter(|n| !workers || locally_declared.contains(*n))
860 .cloned()
861 .collect();
862 emit_helpers_for_owner(
863 out,
864 &local_boundary,
865 &commons.types,
866 ctx.commons_name.as_str(),
867 );
868
869 let mut by_commons: HashMap<String, Vec<String>> = HashMap::new();
876 for n in &boundary_types_all {
877 if !workers || locally_declared.contains(n) {
878 continue;
879 }
880 if matches!(ctx.imported_from_kind.get(n), Some(UnitKind::Commons))
881 && let Some(commons_name) = ctx.imported_from.get(n)
882 {
883 by_commons
884 .entry(commons_name.clone())
885 .or_default()
886 .push(n.clone());
887 }
888 }
889 let mut commons_keys: Vec<&String> = by_commons.keys().collect();
890 commons_keys.sort();
891 for commons_name in commons_keys {
892 let names = by_commons.get(commons_name).unwrap();
893 let mut sorted_names: Vec<String> = names.clone();
894 sorted_names.sort();
895 sorted_names.dedup();
896 let target_path = ctx
897 .imported_decl_paths
898 .get(commons_name)
899 .and_then(|m| sorted_names.iter().find_map(|n| m.get(n).cloned()))
900 .unwrap_or_else(|| EmitProjectCtx::commons_path(commons_name));
901 let import_spec = cross_commons_import_specifier_for_path(
902 &ctx.source_path,
903 &target_path,
904 ctx.import_ext,
905 );
906 let mut parts: Vec<String> = Vec::new();
907 for n in &sorted_names {
908 parts.push(format!("serialise_{n}"));
909 parts.push(format!("deserialise_{n}"));
910 }
911 writeln!(
918 out,
919 "import {{ {} }} from \"{import_spec}\";",
920 parts.join(", ")
921 )
922 .unwrap();
923 writeln!(out, "export {{ {} }};", parts.join(", ")).unwrap();
924 }
925 if !by_commons.is_empty() {
926 writeln!(out).unwrap();
927 }
928
929 let insts =
932 collect_generic_instantiations(&services, &agents, &boundary_types_all, &commons.types);
933 emit_generic_helpers(out, &insts);
934 (
935 boundary_types_all.into_iter().collect(),
936 insts.iter().map(|i| i.ts_name()).collect(),
937 )
938 } else if !workers {
939 (HashSet::new(), HashSet::new())
944 } else {
945 let mut locally: Vec<String> = locally_declared.into_iter().collect();
951 locally.sort();
952 emit_helpers_for_owner(out, &locally, &commons.types, ctx.commons_name.as_str());
953 let insts = collect_generic_instantiations(
954 &HashMap::new(),
955 &HashMap::new(),
956 &locally,
957 &commons.types,
958 );
959 emit_generic_helpers(out, &insts);
960 (
961 locally.into_iter().collect(),
962 insts.iter().map(|i| i.ts_name()).collect(),
963 )
964 }
965}
966
967fn emit_context_rebrands(
974 out: &mut String,
975 refs: &ExternalReferences,
976 commons: &TypedCommons,
977 ctx: &EmitProjectCtx,
978) {
979 let Some(owning) = &ctx.owning_context else {
980 return;
981 };
982 let mut names: Vec<String> = Vec::new();
984 for set in refs.by_commons.values() {
985 for n in set {
986 if matches!(ctx.imported_from_kind.get(n), Some(UnitKind::Commons))
989 && commons.types.contains_key(n)
990 {
991 names.push(n.clone());
992 }
993 }
994 }
995 names.sort();
996 names.dedup();
997 if names.is_empty() {
998 return;
999 }
1000 for name in &names {
1001 writeln!(
1002 out,
1003 "export type {name} = __Commons{name} & {{ readonly __ctxBrand: \"{owning}\" }};",
1004 )
1005 .unwrap();
1006 if let Some(base) = commons.types.get(name).and_then(refined_or_opaque_base) {
1013 let ts_base = ts_base(base);
1014 writeln!(out, "export const {name} = {{").unwrap();
1015 writeln!(
1016 out,
1017 " of(value: {ts_base}): Result<{name}, ValidationError> {{ return __Commons{name}.of(value) as unknown as Result<{name}, ValidationError>; }},",
1018 )
1019 .unwrap();
1020 writeln!(
1021 out,
1022 " unsafe(value: {ts_base}): {name} {{ return __Commons{name}.unsafe(value) as unknown as {name}; }},",
1023 )
1024 .unwrap();
1025 writeln!(out, "}};").unwrap();
1026 }
1027 }
1028 writeln!(out).unwrap();
1029}
1030
1031fn refined_or_opaque_base(decl: &TypeDecl) -> Option<BaseType> {
1034 match &decl.body {
1035 TypeBody::Refined { base, .. } | TypeBody::Opaque { base, .. } => Some(*base),
1036 _ => None,
1037 }
1038}
1039
1040#[derive(Default)]
1043struct ExternalReferences {
1044 by_commons: HashMap<String, HashSet<String>>,
1046 by_sibling: HashMap<PathBuf, HashSet<String>>,
1048}
1049
1050impl ExternalReferences {
1051 fn is_empty(&self) -> bool {
1052 self.by_commons.is_empty() && self.by_sibling.is_empty()
1053 }
1054}
1055
1056fn collect_external_references(commons: &TypedCommons, ctx: &EmitProjectCtx) -> ExternalReferences {
1057 let local_to_file: HashSet<String> = commons
1059 .commons
1060 .items
1061 .iter()
1062 .map(|i| i.name().name.clone())
1063 .collect();
1064
1065 let mut refs = ExternalReferences::default();
1066
1067 for item in &commons.commons.items {
1071 match item {
1072 CommonsItem::Type(t) => {
1073 collect_refs_in_type_decl(t, &local_to_file, ctx, &mut refs);
1074 }
1075 CommonsItem::Fn(f) => {
1076 collect_refs_in_fn(f, &local_to_file, commons, ctx, &mut refs);
1077 }
1078 CommonsItem::Capability(c) => {
1079 for op in &c.ops {
1080 for p in &op.params {
1081 collect_refs_in_typeref(&p.type_ref, &local_to_file, ctx, &mut refs);
1082 }
1083 collect_refs_in_typeref(&op.return_type, &local_to_file, ctx, &mut refs);
1084 }
1085 }
1086 CommonsItem::Provider(p) => {
1087 let _ = &p.capability;
1090 for op in &p.ops {
1091 for param in &op.params {
1092 collect_refs_in_typeref(¶m.type_ref, &local_to_file, ctx, &mut refs);
1093 }
1094 collect_refs_in_typeref(&op.return_type, &local_to_file, ctx, &mut refs);
1095 collect_refs_in_block(&op.body, &local_to_file, commons, ctx, &mut refs);
1096 }
1097 }
1098 CommonsItem::Service(s) => {
1099 for h in &s.handlers {
1100 for p in &h.params {
1101 collect_refs_in_typeref(&p.type_ref, &local_to_file, ctx, &mut refs);
1102 }
1103 collect_refs_in_typeref(&h.return_type, &local_to_file, ctx, &mut refs);
1104 collect_refs_in_block(&h.body, &local_to_file, commons, ctx, &mut refs);
1105 }
1106 }
1107 CommonsItem::Agent(a) => {
1108 collect_refs_in_typeref(&a.key_type, &local_to_file, ctx, &mut refs);
1109 for f in &a.store_fields {
1110 for arg in &f.kind.args {
1111 collect_refs_in_typeref(arg, &local_to_file, ctx, &mut refs);
1112 }
1113 }
1114 for h in &a.handlers {
1115 for p in &h.params {
1116 collect_refs_in_typeref(&p.type_ref, &local_to_file, ctx, &mut refs);
1117 }
1118 collect_refs_in_typeref(&h.return_type, &local_to_file, ctx, &mut refs);
1119 collect_refs_in_block(&h.body, &local_to_file, commons, ctx, &mut refs);
1120 }
1121 }
1122 CommonsItem::Actor(a) => {
1123 if let Some(id) = &a.identity {
1124 collect_refs_in_typeref(id, &local_to_file, ctx, &mut refs);
1125 }
1126 }
1127 }
1128 }
1129 refs
1130}
1131
1132fn collect_refs_in_type_decl(
1133 t: &TypeDecl,
1134 local_to_file: &HashSet<String>,
1135 ctx: &EmitProjectCtx,
1136 out: &mut ExternalReferences,
1137) {
1138 match &t.body {
1139 TypeBody::Record(r) => {
1140 for f in &r.fields {
1141 collect_refs_in_typeref(&f.type_ref, local_to_file, ctx, out);
1142 }
1143 }
1144 TypeBody::Sum(s) => {
1145 for v in &s.variants {
1146 for p in &v.payload {
1147 collect_refs_in_typeref(&p.type_ref, local_to_file, ctx, out);
1148 }
1149 }
1150 }
1151 _ => {}
1152 }
1153}
1154
1155fn collect_refs_in_fn(
1156 f: &FnDecl,
1157 local_to_file: &HashSet<String>,
1158 commons: &TypedCommons,
1159 ctx: &EmitProjectCtx,
1160 out: &mut ExternalReferences,
1161) {
1162 for p in &f.params {
1163 collect_refs_in_typeref(&p.type_ref, local_to_file, ctx, out);
1164 }
1165 collect_refs_in_typeref(&f.return_type, local_to_file, ctx, out);
1166 if let FnName::Method { type_name, .. } = &f.name {
1168 record_name_ref(&type_name.name, local_to_file, ctx, out);
1169 }
1170 collect_refs_in_block(&f.body, local_to_file, commons, ctx, out);
1171}
1172
1173fn collect_refs_in_typeref(
1174 r: &TypeRef,
1175 local_to_file: &HashSet<String>,
1176 ctx: &EmitProjectCtx,
1177 out: &mut ExternalReferences,
1178) {
1179 match r {
1180 TypeRef::Named(id) => record_name_ref(&id.name, local_to_file, ctx, out),
1181 TypeRef::Result(t, e, _) => {
1182 collect_refs_in_typeref(t, local_to_file, ctx, out);
1183 collect_refs_in_typeref(e, local_to_file, ctx, out);
1184 }
1185 TypeRef::Option(t, _) => collect_refs_in_typeref(t, local_to_file, ctx, out),
1186 TypeRef::Effect(t, _) => collect_refs_in_typeref(t, local_to_file, ctx, out),
1187 TypeRef::HttpResult(t, _) => collect_refs_in_typeref(t, local_to_file, ctx, out),
1188 _ => {}
1189 }
1190}
1191
1192fn collect_refs_in_block(
1193 b: &Block,
1194 local_to_file: &HashSet<String>,
1195 commons: &TypedCommons,
1196 ctx: &EmitProjectCtx,
1197 out: &mut ExternalReferences,
1198) {
1199 for stmt in &b.statements {
1200 match stmt {
1201 Statement::Let(l) | Statement::EffectLet(l) => {
1202 if let Some(t) = &l.type_annot {
1203 collect_refs_in_typeref(t, local_to_file, ctx, out);
1204 }
1205 collect_refs_in_expr(&l.value, local_to_file, commons, ctx, out);
1206 }
1207 Statement::Expect(a) => {
1208 collect_refs_in_expr(&a.value, local_to_file, commons, ctx, out);
1209 }
1210 Statement::Send(s) => {
1211 collect_refs_in_expr(&s.value, local_to_file, commons, ctx, out);
1212 }
1213 Statement::Assign(a) => {
1214 collect_refs_in_expr(&a.value, local_to_file, commons, ctx, out);
1215 }
1216 }
1217 }
1218 collect_refs_in_expr(&b.tail, local_to_file, commons, ctx, out);
1219}
1220
1221fn collect_refs_in_expr(
1222 e: &Expr,
1223 local_to_file: &HashSet<String>,
1224 commons: &TypedCommons,
1225 ctx: &EmitProjectCtx,
1226 out: &mut ExternalReferences,
1227) {
1228 match &e.kind {
1229 ExprKind::Ident(id) => {
1234 if let Some(type_name) = sum_owner_of_variant(&id.name, e.span, commons) {
1235 record_name_ref(&type_name, local_to_file, ctx, out);
1236 }
1237 }
1238 ExprKind::IntLit(_)
1239 | ExprKind::FloatLit { .. }
1240 | ExprKind::DurationLit { .. }
1241 | ExprKind::StrLit(_)
1242 | ExprKind::BoolLit(_)
1243 | ExprKind::None
1244 | ExprKind::UnitLit => {}
1245 ExprKind::InterpStr(parts) => {
1247 for part in parts {
1248 if let InterpPart::Hole(hole) = part {
1249 collect_refs_in_expr(hole, local_to_file, commons, ctx, out);
1250 }
1251 }
1252 }
1253 ExprKind::Lambda(lambda) => {
1256 for p in &lambda.params {
1257 if let Some(tr) = &p.type_ref {
1258 collect_refs_in_typeref(tr, local_to_file, ctx, out);
1259 }
1260 }
1261 collect_refs_in_expr(&lambda.body, local_to_file, commons, ctx, out);
1262 }
1263 ExprKind::EffectPure(inner) => {
1264 collect_refs_in_expr(inner, local_to_file, commons, ctx, out);
1265 }
1266 ExprKind::Expect(inner) => {
1267 collect_refs_in_expr(inner, local_to_file, commons, ctx, out);
1268 }
1269 ExprKind::Val { args, .. } => {
1270 for a in args {
1271 collect_refs_in_expr(a, local_to_file, commons, ctx, out);
1272 }
1273 }
1274 ExprKind::ListLit(elems) => {
1275 for el in elems {
1276 collect_refs_in_expr(el, local_to_file, commons, ctx, out);
1277 }
1278 }
1279 ExprKind::RecordSpread {
1280 type_name,
1281 base,
1282 overrides,
1283 } => {
1284 if let Some(tn) = type_name {
1285 record_name_ref(&tn.name, local_to_file, ctx, out);
1286 }
1287 collect_refs_in_expr(base, local_to_file, commons, ctx, out);
1288 for f in overrides {
1289 if let Some(v) = &f.value {
1290 collect_refs_in_expr(v, local_to_file, commons, ctx, out);
1291 }
1292 }
1293 }
1294 ExprKind::Call { name, args, .. } => {
1295 record_name_ref(&name.name, local_to_file, ctx, out);
1296 if let Some(type_name) = sum_owner_of_variant(&name.name, e.span, commons) {
1299 record_name_ref(&type_name, local_to_file, ctx, out);
1300 }
1301 for a in args {
1302 collect_refs_in_expr(a, local_to_file, commons, ctx, out);
1303 }
1304 }
1305 ExprKind::BinOp(_, l, r) => {
1306 collect_refs_in_expr(l, local_to_file, commons, ctx, out);
1307 collect_refs_in_expr(r, local_to_file, commons, ctx, out);
1308 }
1309 ExprKind::UnaryOp(_, i)
1310 | ExprKind::Paren(i)
1311 | ExprKind::Ok(i)
1312 | ExprKind::Err(i)
1313 | ExprKind::Some(i)
1314 | ExprKind::Question(i) => collect_refs_in_expr(i, local_to_file, commons, ctx, out),
1315 ExprKind::Block(b) => collect_refs_in_block(b, local_to_file, commons, ctx, out),
1316 ExprKind::If {
1317 cond,
1318 then_block,
1319 else_block,
1320 } => {
1321 collect_refs_in_expr(cond, local_to_file, commons, ctx, out);
1322 collect_refs_in_block(then_block, local_to_file, commons, ctx, out);
1323 collect_refs_in_block(else_block, local_to_file, commons, ctx, out);
1324 }
1325 ExprKind::ConstructorCall {
1326 type_name,
1327 method: _,
1328 args,
1329 } => {
1330 record_name_ref(&type_name.name, local_to_file, ctx, out);
1331 for a in args {
1332 collect_refs_in_expr(a, local_to_file, commons, ctx, out);
1333 }
1334 }
1335 ExprKind::RecordConstruction { type_name, fields } => {
1336 record_name_ref(&type_name.name, local_to_file, ctx, out);
1337 for f in fields {
1338 if let Some(v) = &f.value {
1339 collect_refs_in_expr(v, local_to_file, commons, ctx, out);
1340 }
1341 }
1342 }
1343 ExprKind::FieldAccess { receiver, field: _ } => {
1344 if let ExprKind::Ident(id) = &receiver.kind {
1347 record_name_ref(&id.name, local_to_file, ctx, out);
1348 } else {
1349 collect_refs_in_expr(receiver, local_to_file, commons, ctx, out);
1350 }
1351 }
1352 ExprKind::MethodCall {
1353 receiver,
1354 method: _,
1355 args,
1356 ..
1357 } => {
1358 if let ExprKind::Ident(id) = &receiver.kind {
1359 record_name_ref(&id.name, local_to_file, ctx, out);
1360 } else {
1361 collect_refs_in_expr(receiver, local_to_file, commons, ctx, out);
1362 }
1363 for a in args {
1364 collect_refs_in_expr(a, local_to_file, commons, ctx, out);
1365 }
1366 }
1367 ExprKind::Match { discriminant, arms } => {
1368 collect_refs_in_expr(discriminant, local_to_file, commons, ctx, out);
1369 for arm in arms {
1370 if let Pattern::Variant {
1371 type_name: Some(tn),
1372 ..
1373 } = &arm.pattern
1374 {
1375 record_name_ref(&tn.name, local_to_file, ctx, out);
1376 }
1377 match &arm.body {
1378 MatchBody::Expr(e) => collect_refs_in_expr(e, local_to_file, commons, ctx, out),
1379 MatchBody::Block(b) => {
1380 collect_refs_in_block(b, local_to_file, commons, ctx, out)
1381 }
1382 }
1383 }
1384 }
1385 ExprKind::Is { value, pattern } => {
1386 collect_refs_in_expr(value, local_to_file, commons, ctx, out);
1387 if let Pattern::Variant {
1388 type_name: Some(tn),
1389 ..
1390 } = pattern
1391 {
1392 record_name_ref(&tn.name, local_to_file, ctx, out);
1393 }
1394 }
1395 }
1396}
1397
1398fn sum_owner_of_variant(
1403 name: &str,
1404 span: bynk_syntax::span::Span,
1405 commons: &TypedCommons,
1406) -> Option<String> {
1407 if let Some(Ty::Named {
1408 kind: NamedKind::Sum,
1409 name: type_name,
1410 }) = commons.expr_types.get(&span)
1411 && let Some(decl) = commons.types.get(type_name)
1412 && let TypeBody::Sum(s) = &decl.body
1413 && s.variants.iter().any(|v| v.name.name == name)
1414 {
1415 return Some(type_name.clone());
1416 }
1417 None
1418}
1419
1420fn record_name_ref(
1421 name: &str,
1422 local_to_file: &HashSet<String>,
1423 ctx: &EmitProjectCtx,
1424 out: &mut ExternalReferences,
1425) {
1426 if local_to_file.contains(name) {
1427 return;
1428 }
1429 if let Some(commons_name) = ctx.imported_from.get(name) {
1431 out.by_commons
1432 .entry(commons_name.clone())
1433 .or_default()
1434 .insert(name.to_string());
1435 return;
1436 }
1437 if let Some(path) = ctx.file_decl_index.types.get(name)
1439 && path != &ctx.source_path
1440 {
1441 out.by_sibling
1442 .entry(path.clone())
1443 .or_default()
1444 .insert(name.to_string());
1445 return;
1446 }
1447 if let Some(path) = ctx.file_decl_index.fns.get(name)
1448 && path != &ctx.source_path
1449 {
1450 out.by_sibling
1451 .entry(path.clone())
1452 .or_default()
1453 .insert(name.to_string());
1454 }
1455}
1456
1457fn emit_cross_context_namespace_imports(
1461 out: &mut String,
1462 commons: &TypedCommons,
1463 ctx: &EmitProjectCtx,
1464) {
1465 let info = &ctx.cross_context;
1466 let mut needed: std::collections::BTreeSet<String> = info
1469 .consumed_services
1470 .iter()
1471 .filter(|(_, svcs)| !svcs.is_empty())
1472 .map(|(q, _)| q.clone())
1473 .collect();
1474 needed.extend(cross_context_cap_namespaces(commons, info));
1475 if needed.is_empty() {
1476 return;
1477 }
1478 let consumed_with_services: Vec<&String> = needed.iter().collect();
1479 for q in &consumed_with_services {
1480 let target_paths = ctx.imported_decl_paths.get(q.as_str());
1487 let target = target_paths
1488 .and_then(|m| m.values().next().cloned())
1489 .unwrap_or_else(|| {
1490 if ctx.consumed_adapters.contains(q.as_str()) {
1497 let mut p = EmitProjectCtx::commons_path(q);
1498 p.set_extension("bynk");
1499 p
1500 } else {
1501 match ctx.target {
1502 BuildTarget::Workers => crate::project::worker_handlers_source_path(q),
1503 BuildTarget::Bundle => {
1504 let mut p = EmitProjectCtx::commons_path(q);
1505 p.set_extension("bynk");
1506 p
1507 }
1508 }
1509 }
1510 });
1511 let import =
1512 cross_commons_import_specifier_for_path(&ctx.source_path, &target, ctx.import_ext);
1513 let ns = qualified_to_ns(q);
1514 writeln!(out, "import * as {ns} from \"{import}\";").unwrap();
1515 }
1516 writeln!(out).unwrap();
1517}
1518
1519fn emit_project_imports(
1520 out: &mut String,
1521 commons: &TypedCommons,
1522 ctx: &EmitProjectCtx,
1523 refs: &ExternalReferences,
1524) {
1525 let mut sibling_paths: Vec<(&PathBuf, &HashSet<String>)> = refs.by_sibling.iter().collect();
1527 sibling_paths.sort_by(|a, b| a.0.cmp(b.0));
1528 for (path, names) in sibling_paths {
1529 let import = sibling_import_specifier(&ctx.source_path, path, ctx.import_ext);
1530 let mut sorted: Vec<&String> = names.iter().collect();
1531 sorted.sort();
1532 let joined = sorted
1533 .iter()
1534 .map(|s| s.as_str())
1535 .collect::<Vec<_>>()
1536 .join(", ");
1537 writeln!(out, "import {{ {joined} }} from \"{import}\";").unwrap();
1538 }
1539 let mut unit_names: Vec<(&String, &HashSet<String>)> = refs.by_commons.iter().collect();
1541 unit_names.sort_by(|a, b| a.0.cmp(b.0));
1542 for (unit_name, names) in unit_names {
1543 let target_paths = ctx.imported_decl_paths.get(unit_name.as_str());
1544 let mut by_target: std::collections::BTreeMap<PathBuf, Vec<&String>> =
1545 std::collections::BTreeMap::new();
1546 for n in names {
1547 let path = target_paths
1548 .and_then(|p| p.get(n))
1549 .cloned()
1550 .unwrap_or_else(|| EmitProjectCtx::commons_path(unit_name));
1551 by_target.entry(path).or_default().push(n);
1552 }
1553 for (target, mut name_list) in by_target {
1554 name_list.sort();
1555 let import =
1556 cross_commons_import_specifier_for_path(&ctx.source_path, &target, ctx.import_ext);
1557 let mut parts: Vec<String> = Vec::new();
1563 for n in &name_list {
1564 let from_kind = ctx.imported_from_kind.get(n.as_str()).copied();
1565 if ctx.unit_kind == UnitKind::Context
1566 && from_kind == Some(UnitKind::Commons)
1567 && commons.types.contains_key(n.as_str())
1568 {
1569 parts.push(format!("{n} as __Commons{n}"));
1570 } else {
1571 parts.push((*n).clone());
1572 }
1573 }
1574 let joined = parts.join(", ");
1575 writeln!(out, "import {{ {joined} }} from \"{import}\";").unwrap();
1576 }
1577 }
1578}
1579
1580fn sibling_import_specifier(from_source: &Path, to_source: &Path, ext: ImportExt) -> String {
1584 let from_dir = from_source.parent().unwrap_or(Path::new(""));
1585 let target = to_source.with_extension(ext.as_str());
1586 let rel = relative_to(from_dir, &target);
1587 format!("./{}", ts_specifier(&rel))
1588}
1589
1590pub(crate) fn ts_specifier(p: &Path) -> String {
1595 p.to_string_lossy().replace('\\', "/")
1596}
1597
1598fn cross_commons_import_specifier_for_path(
1603 from_source: &Path,
1604 target_source: &Path,
1605 ext: ImportExt,
1606) -> String {
1607 let from_dir = from_source.parent().unwrap_or(Path::new(""));
1608 let target = target_source.with_extension(ext.as_str());
1609 let rel = relative_to(from_dir, &target);
1610 let display = ts_specifier(&rel);
1611 if display.starts_with("../") || display.starts_with("./") {
1612 display
1613 } else {
1614 format!("./{display}")
1615 }
1616}
1617
1618fn relative_to(from: &Path, target: &Path) -> PathBuf {
1621 use std::path::Component as C;
1622 let f_comps: Vec<C> = from.components().collect();
1623 let t_comps: Vec<C> = target.components().collect();
1624 let mut shared = 0;
1625 while shared < f_comps.len() && shared < t_comps.len() && f_comps[shared] == t_comps[shared] {
1626 shared += 1;
1627 }
1628 let mut out = PathBuf::new();
1629 for _ in shared..f_comps.len() {
1630 out.push("..");
1631 }
1632 for c in &t_comps[shared..] {
1633 out.push(c.as_os_str());
1634 }
1635 if out.as_os_str().is_empty() {
1636 out.push(".");
1637 }
1638 out
1639}
1640
1641fn write_header(out: &mut String, commons: &TypedCommons, ctx: &EmitProjectCtx) {
1642 writeln!(out, "// Generated by bynkc — do not edit by hand.").unwrap();
1643 let kind = match ctx.unit_kind {
1644 UnitKind::Commons => "commons",
1645 UnitKind::Context => "context",
1646 UnitKind::Test => "test",
1647 UnitKind::Integration => "integration test",
1648 UnitKind::Adapter => "adapter",
1649 };
1650 writeln!(out, "// {kind} {}", commons.commons.name.joined()).unwrap();
1651 writeln!(out).unwrap();
1652 if !commons.commons.items.is_empty() {
1653 let runtime_import = runtime_import_for(&ctx.source_path, ctx.import_ext);
1654 let has_agent = commons
1655 .commons
1656 .items
1657 .iter()
1658 .any(|i| matches!(i, CommonsItem::Agent(_)));
1659 let has_agent_invariants = commons.commons.items.iter().any(|i| match i {
1662 CommonsItem::Agent(a) => !a.invariants.is_empty(),
1663 _ => false,
1664 });
1665 let has_http = commons.commons.items.iter().any(|i| match i {
1666 CommonsItem::Service(s) => s
1667 .handlers
1668 .iter()
1669 .any(|h| matches!(h.kind, HandlerKind::Http { .. })),
1670 _ => false,
1671 });
1672 let has_queue = commons.commons.items.iter().any(|i| match i {
1676 CommonsItem::Service(s) => {
1677 !matches!(s.protocol, ServiceProtocol::WebSocket { .. })
1678 && s.handlers
1679 .iter()
1680 .any(|h| matches!(h.kind, HandlerKind::Message))
1681 }
1682 _ => false,
1683 });
1684 let workers = matches!(ctx.target, BuildTarget::Workers);
1685 let mut parts: Vec<&str> = vec![
1686 "Ok",
1687 "Err",
1688 "Some",
1689 "None",
1690 "type Result",
1691 "type Option",
1692 "type ValidationError",
1693 ];
1694 let uses_codec = !collect_json_codec_roots(commons).is_empty();
1698 let mentions_json_error = file_mentions_json_error(commons);
1699 if uses_codec || mentions_json_error {
1700 parts.push("type JsonError");
1701 }
1702 if file_mentions_connection(commons) {
1704 parts.push("type Connection");
1705 }
1706 if has_agent {
1707 parts.push("type DurableObjectState");
1711 parts.push("type DurableObjectNamespace");
1712 parts.push("StateRegistry");
1713 parts.push("makeAgent");
1714 }
1715 if has_agent_invariants {
1716 parts.push("invariantViolation");
1717 }
1718 let has_rehydration_gate = commons.commons.items.iter().any(|i| match i {
1721 CommonsItem::Agent(a) => emit::agent_needs_rehydrate(a, &commons.types),
1722 _ => false,
1723 });
1724 if has_rehydration_gate {
1725 parts.push("rehydrationViolation");
1726 }
1727 if workers
1731 && commons.commons.items.iter().any(|i| match i {
1732 CommonsItem::Agent(a) => emit::agent_has_held_storage(a),
1733 _ => false,
1734 })
1735 {
1736 parts.push("resolveConnection");
1737 parts.push("connIdOf");
1738 }
1739 let hosts_ws_open = commons.commons.items.iter().any(|i| match i {
1746 CommonsItem::Service(s) => s
1747 .handlers
1748 .iter()
1749 .any(|h| matches!(h.kind, HandlerKind::Open)),
1750 _ => false,
1751 });
1752 if workers && hosts_ws_open {
1753 parts.push("acceptHibernatableConnection");
1754 parts.push("newWebSocketPair");
1755 parts.push("webSocketUpgradeResponse");
1756 }
1757 let hosts_ws_inbound = commons.commons.items.iter().any(|i| match i {
1761 CommonsItem::Service(s) => {
1762 matches!(s.protocol, ServiceProtocol::WebSocket { .. })
1763 && s.handlers
1764 .iter()
1765 .any(|h| matches!(h.kind, HandlerKind::Message | HandlerKind::Close))
1766 }
1767 _ => false,
1768 });
1769 if workers && hosts_ws_inbound {
1770 parts.push("WorkersConnection");
1771 }
1772 if has_http {
1773 parts.push(HTTP_RESULT);
1777 }
1778 if has_queue {
1779 parts.push(QUEUE_RESULT);
1782 }
1783 if workers {
1784 parts.push("type JsonValue");
1785 parts.push("type BoundaryError");
1786 parts.push("type ServiceBinding");
1787 parts.push("callService");
1788 parts.push("boundaryError");
1789 } else if uses_codec || has_agent {
1790 parts.push("type JsonValue");
1795 parts.push("type BoundaryError");
1796 }
1797 writeln!(
1798 out,
1799 "import {{ {} }} from \"{runtime_import}\";",
1800 parts.join(", ")
1801 )
1802 .unwrap();
1803 writeln!(out).unwrap();
1804 }
1805}
1806
1807fn write_header_single(out: &mut String, commons: &TypedCommons, uses_bytes: bool) {
1809 writeln!(out, "// Generated by bynkc — do not edit by hand.").unwrap();
1810 writeln!(out, "// commons {}", commons.commons.name.joined()).unwrap();
1811 writeln!(out).unwrap();
1812 if !commons.commons.items.is_empty() {
1813 let uses_codec = !collect_json_codec_roots(commons).is_empty();
1815 let codec_imports = if uses_codec {
1816 ", type JsonError, type JsonValue, type BoundaryError"
1817 } else if file_mentions_json_error(commons) {
1818 ", type JsonError"
1819 } else {
1820 ""
1821 };
1822 let bytes_imports = if uses_bytes {
1825 BYTES_RUNTIME_IMPORTS
1826 } else {
1827 ""
1828 };
1829 writeln!(
1830 out,
1831 "import {{ Ok, Err, Some, None, type Result, type Option, type ValidationError{codec_imports}{bytes_imports} }} from \"./runtime.js\";",
1832 )
1833 .unwrap();
1834 writeln!(out).unwrap();
1835 }
1836}
1837
1838const BYTES_RUNTIME_IMPORTS: &str =
1842 ", __bynkBytesEqual, __bynkBytesToBase64, __bynkBytesFromBase64, __bynkBytesDecodeUtf8";
1843
1844fn write_commons_doc(out: &mut String, commons: &TypedCommons) {
1846 if let Some(doc) = &commons.commons.documentation {
1847 emit_doc_block(out, Some(doc), 0);
1848 writeln!(out).unwrap();
1849 }
1850}
1851
1852fn agent_registry_name(agent: &str) -> String {
1854 format!("__{agent}Registry")
1855}
1856
1857pub fn agent_factory_name(agent: &str) -> String {
1859 format!("__make{agent}")
1860}
1861
1862pub(crate) struct LowerCtx<'a> {
1865 next_tmp: u32,
1866 commons: &'a TypedCommons,
1867 capabilities: HashSet<String>,
1870 in_agent_handler: bool,
1873 agent_state_var: Option<String>,
1875 agent_key_field: Option<String>,
1877 invariant_state: Option<(String, HashSet<String>)>,
1882 agent_store_state: Option<(String, HashSet<String>)>,
1888 agent_store_maps: HashSet<String>,
1893 agent_store_sets: HashSet<String>,
1897 agent_store_caches: HashMap<String, i64>,
1902 agent_store_logs: HashMap<String, Option<i64>>,
1907 agent_store_indexes: HashMap<String, Vec<String>>,
1912 pub ws_self_agent: Option<String>,
1920 pub agent_held_maps: HashMap<String, String>,
1928 cross_context: &'a bynk_check::resolver::CrossContextInfo,
1930 cross_context_used: bool,
1933 pub test_services: HashSet<String>,
1938 pub test_agents: HashSet<String>,
1942 pub local_agents: HashSet<String>,
1948 pub local_agent_vars: HashMap<String, String>,
1953 pub target: BuildTarget,
1956 pub agents_instantiated: bool,
1960 is_receiver_temps: HashMap<bynk_syntax::span::Span, String>,
1967 cap_deps_expr: String,
1970 pub deps_identity_binder: Option<String>,
1974 pub actor_sum_binder: Option<String>,
1978 pub assert_loc: Option<AssertLoc>,
1984 pub source_map: Option<&'a RefCell<SourceMapBuilder>>,
1990}
1991
1992#[derive(Clone)]
1997pub(crate) struct AssertLoc {
1998 pub source: String,
1999 pub rel_path: String,
2000}
2001
2002impl<'a> LowerCtx<'a> {
2003 fn new(
2004 commons: &'a TypedCommons,
2005 cross_context: &'a bynk_check::resolver::CrossContextInfo,
2006 ) -> Self {
2007 Self {
2008 next_tmp: 0,
2009 commons,
2010 capabilities: HashSet::new(),
2011 in_agent_handler: false,
2012 agent_state_var: None,
2013 agent_key_field: None,
2014 invariant_state: None,
2015 agent_store_state: None,
2016 agent_store_maps: HashSet::new(),
2017 agent_store_sets: HashSet::new(),
2018 agent_store_caches: HashMap::new(),
2019 agent_store_logs: HashMap::new(),
2020 agent_store_indexes: HashMap::new(),
2021 ws_self_agent: None,
2022 agent_held_maps: HashMap::new(),
2023 cross_context,
2024 cross_context_used: false,
2025 test_services: HashSet::new(),
2026 test_agents: HashSet::new(),
2027 local_agents: HashSet::new(),
2028 local_agent_vars: HashMap::new(),
2029 target: BuildTarget::Bundle,
2030 agents_instantiated: false,
2031 is_receiver_temps: HashMap::new(),
2032 cap_deps_expr: "deps".to_string(),
2033 deps_identity_binder: None,
2034 actor_sum_binder: None,
2035 assert_loc: None,
2036 source_map: None,
2037 }
2038 }
2039
2040 fn with_source_map(mut self, map: Option<&'a RefCell<SourceMapBuilder>>) -> Self {
2044 self.source_map = map;
2045 self
2046 }
2047
2048 fn record_span(&self, out_len: usize, span: bynk_syntax::span::Span) {
2053 if let Some(map) = self.source_map {
2054 map.borrow_mut().record(out_len, span);
2055 }
2056 }
2057 fn agent_construct(&mut self, agent: &str, key_expr: &str) -> String {
2061 self.agents_instantiated = true;
2062 let factory = agent_factory_name(agent);
2063 if matches!(self.target, BuildTarget::Workers) {
2064 format!("{factory}({key_expr}, deps.env)")
2065 } else {
2066 format!("{factory}({key_expr})")
2067 }
2068 }
2069 fn fresh(&mut self) -> String {
2070 let n = self.next_tmp;
2071 self.next_tmp += 1;
2072 format!("__r{n}")
2073 }
2074 fn is_receiver_ref(&mut self, value: &Expr, stmts: &mut Vec<String>) -> String {
2082 if let Some(t) = self.is_receiver_temps.get(&value.span) {
2083 return t.clone();
2084 }
2085 let lowered = lower_expr(value, stmts, self);
2086 if is_simple_is_receiver(value) {
2087 return lowered;
2088 }
2089 let tmp = self.fresh();
2090 stmts.push(format!("const {tmp} = {lowered};"));
2091 self.is_receiver_temps.insert(value.span, tmp.clone());
2092 tmp
2093 }
2094
2095 fn is_receiver_ref_forced(&mut self, value: &Expr, stmts: &mut Vec<String>) -> String {
2101 if let Some(t) = self.is_receiver_temps.get(&value.span) {
2102 return t.clone();
2103 }
2104 let lowered = lower_expr(value, stmts, self);
2105 let tmp = self.fresh();
2106 stmts.push(format!("const {tmp} = {lowered};"));
2107 self.is_receiver_temps.insert(value.span, tmp.clone());
2108 tmp
2109 }
2110
2111 fn is_refined_is_check(&self, value: &Expr, name: &str) -> bool {
2115 let value_baseish = matches!(
2116 self.commons.expr_types.get(&value.span),
2117 Some(Ty::Base(_))
2118 | Some(Ty::Named {
2119 kind: NamedKind::Refined(_),
2120 ..
2121 })
2122 );
2123 let name_refined = matches!(
2124 self.commons.types.get(name).map(|d| &d.body),
2125 Some(TypeBody::Refined { .. })
2126 );
2127 value_baseish && name_refined
2128 }
2129 fn is_receiver_text(&self, value: &Expr) -> String {
2136 if let Some(t) = self.is_receiver_temps.get(&value.span) {
2137 return t.clone();
2138 }
2139 value_text_for_is(value)
2140 }
2141 fn receiver_namespace(&self, e: &Expr) -> Option<String> {
2142 let ty = self.commons.expr_types.get(&e.span)?;
2143 if let Ty::Named { name, .. } = ty {
2144 Some(name.clone())
2145 } else {
2146 None
2147 }
2148 }
2149 fn positional_field_name(
2153 &self,
2154 discriminant_ty: Option<&Ty>,
2155 variant: &str,
2156 idx: usize,
2157 ) -> String {
2158 match (variant, idx) {
2159 ("Ok", 0) | ("Some", 0) => return "value".to_string(),
2160 ("Err", 0) => return "error".to_string(),
2161 _ => {}
2162 }
2163 if let Some(Ty::ActorSum(_)) = discriminant_ty {
2166 return "identity".to_string();
2167 }
2168 if let Some(Ty::Named {
2169 kind: NamedKind::Sum,
2170 name,
2171 }) = discriminant_ty
2172 && let Some(decl) = self.commons.types.get(name)
2173 && let TypeBody::Sum(s) = &decl.body
2174 && let Some(v) = s.variants.iter().find(|v| v.name.name == variant)
2175 && let Some(f) = v.payload.get(idx)
2176 {
2177 return f.name.name.clone();
2178 }
2179 "value".to_string()
2181 }
2182}
2183
2184fn ts_base(b: BaseType) -> &'static str {
2185 match b {
2186 BaseType::Int => "number",
2187 BaseType::String => "string",
2188 BaseType::Bool => "boolean",
2189 BaseType::Float => "number",
2190 BaseType::Duration | BaseType::Instant => "number",
2191 BaseType::Bytes => "Uint8Array",
2194 }
2195}
2196
2197pub(crate) fn ts_type_ref(r: &TypeRef) -> String {
2198 ts_type_ref_with(r, None)
2199}
2200
2201pub(crate) fn ts_type_ref_qualified(r: &TypeRef, scope: &HashSet<String>, ns: &str) -> String {
2208 ts_type_ref_with(r, Some((scope, ns)))
2209}
2210
2211fn ts_type_ref_with(r: &TypeRef, qualify: Option<(&HashSet<String>, &str)>) -> String {
2216 match r {
2217 TypeRef::Base(b, _) => ts_base(*b).to_string(),
2218 TypeRef::Named(id) => {
2219 if let Some((scope, ns)) = qualify
2220 && scope.contains(&id.name)
2221 {
2222 format!("{ns}.{}", id.name)
2223 } else {
2224 id.name.clone()
2225 }
2226 }
2227 TypeRef::Result(t, e, _) => format!(
2228 "Result<{}, {}>",
2229 ts_type_ref_with(t, qualify),
2230 ts_type_ref_with(e, qualify)
2231 ),
2232 TypeRef::Option(t, _) => format!("Option<{}>", ts_type_ref_with(t, qualify)),
2233 TypeRef::Effect(t, _) => {
2234 let inner = ts_type_ref_with(t, qualify);
2235 if inner == "()" || inner == "void" {
2236 "Promise<void>".to_string()
2237 } else {
2238 format!("Promise<{inner}>")
2239 }
2240 }
2241 TypeRef::HttpResult(t, _) => format!("HttpResult<{}>", ts_type_ref_with(t, qualify)),
2242 TypeRef::List(t, _) => format!("readonly {}[]", ts_type_ref_with(t, qualify)),
2244 TypeRef::Query(t, _) => {
2245 format!("(() => readonly {}[])", ts_type_ref_with(t, qualify))
2246 }
2247 TypeRef::Stream(t, _) => format!("AsyncIterable<{}>", ts_type_ref_with(t, qualify)),
2249 TypeRef::Connection(t, _) => format!("Connection<{}>", ts_type_ref_with(t, qualify)),
2252 TypeRef::Map(k, v, _) => {
2253 format!(
2254 "ReadonlyMap<{}, {}>",
2255 ts_type_ref_with(k, qualify),
2256 ts_type_ref_with(v, qualify)
2257 )
2258 }
2259 TypeRef::QueueResult(_) => "QueueResult".to_string(),
2260 TypeRef::ValidationError(_) => "ValidationError".to_string(),
2261 TypeRef::JsonError(_) => "JsonError".to_string(),
2262 TypeRef::Unit(_) => "void".to_string(),
2263 TypeRef::Fn(params, ret, _) => {
2267 let params: Vec<String> = params
2268 .iter()
2269 .enumerate()
2270 .map(|(i, p)| format!("a{i}: {}", ts_type_ref_with(p, qualify)))
2271 .collect();
2272 let ret = match ts_type_ref_with(ret, qualify).as_str() {
2273 "()" => "void".to_string(),
2274 other => other.to_string(),
2275 };
2276 format!("({}) => {ret}", params.join(", "))
2277 }
2278 }
2279}
2280
2281fn ts_ty(t: &Ty) -> String {
2286 match t {
2287 Ty::Base(BaseType::Int) => "number".to_string(),
2288 Ty::Base(BaseType::String) => "string".to_string(),
2289 Ty::Base(BaseType::Bool) => "boolean".to_string(),
2290 Ty::Base(BaseType::Float) => "number".to_string(),
2291 Ty::Base(BaseType::Duration | BaseType::Instant) => "number".to_string(),
2292 Ty::Base(BaseType::Bytes) => "Uint8Array".to_string(),
2294 Ty::Named { name, .. } => name.clone(),
2295 Ty::Result(t, e) => format!("Result<{}, {}>", ts_ty(t), ts_ty(e)),
2296 Ty::Option(t) => format!("Option<{}>", ts_ty(t)),
2297 Ty::Effect(t) => match &**t {
2298 Ty::Unit => "Promise<void>".to_string(),
2299 other => format!("Promise<{}>", ts_ty(other)),
2300 },
2301 Ty::HttpResult(t) => format!("HttpResult<{}>", ts_ty(t)),
2302 Ty::List(t) => format!("readonly {}[]", ts_ty(t)),
2303 Ty::Query(t) => format!("(() => readonly {}[])", ts_ty(t)),
2306 Ty::Stream(t) => format!("AsyncIterable<{}>", ts_ty(t)),
2308 Ty::Connection(t) => format!("Connection<{}>", ts_ty(t)),
2310 Ty::Map(k, v) => format!("ReadonlyMap<{}, {}>", ts_ty(k), ts_ty(v)),
2311 Ty::QueueResult => "QueueResult".to_string(),
2312 Ty::ValidationError => "ValidationError".to_string(),
2313 Ty::JsonError => "JsonError".to_string(),
2314 Ty::Unit => "void".to_string(),
2315 Ty::Fn { params, ret } => {
2316 let params: Vec<String> = params
2317 .iter()
2318 .enumerate()
2319 .map(|(i, p)| format!("a{i}: {}", ts_ty(p)))
2320 .collect();
2321 format!("({}) => {}", params.join(", "), ts_ty(ret))
2322 }
2323 Ty::Var(n) => n.clone(),
2324 Ty::Actor(id) => ts_ty(id),
2326 Ty::ActorSum(members) => members
2329 .iter()
2330 .map(|(name, id)| match id {
2331 Ty::Unit => format!("{{ tag: \"{name}\" }}"),
2332 _ => format!("{{ tag: \"{name}\", identity: {} }}", ts_ty(id)),
2333 })
2334 .collect::<Vec<_>>()
2335 .join(" | "),
2336 }
2337}
2338
2339fn ts_binop(op: BinOp) -> &'static str {
2340 match op {
2341 BinOp::Implies => "||",
2344 BinOp::Or => "||",
2345 BinOp::And => "&&",
2346 BinOp::Eq => "===",
2347 BinOp::NotEq => "!==",
2348 BinOp::Lt => "<",
2349 BinOp::LtEq => "<=",
2350 BinOp::Gt => ">",
2351 BinOp::GtEq => ">=",
2352 BinOp::Add => "+",
2353 BinOp::Sub => "-",
2354 BinOp::Mul => "*",
2355 BinOp::Div => "/",
2356 }
2357}
2358
2359pub(crate) fn escape_ts_string(s: &str) -> String {
2360 let mut out = String::with_capacity(s.len());
2361 for c in s.chars() {
2362 match c {
2363 '\\' => out.push_str("\\\\"),
2364 '"' => out.push_str("\\\""),
2365 '\n' => out.push_str("\\n"),
2366 '\t' => out.push_str("\\t"),
2367 '\r' => out.push_str("\\r"),
2368 c => out.push(c),
2369 }
2370 }
2371 out
2372}
2373
2374#[allow(dead_code)]
2375fn _unused_hashmap(_h: HashMap<String, ()>) {}
2376
2377#[cfg(test)]
2378mod runtime_tests {
2379 use super::*;
2380
2381 #[test]
2382 fn runtime_emits_all_required_exports() {
2383 let s = emit_runtime_module();
2384 assert!(s.contains("export type Result<T, E>"));
2386 assert!(s.contains("export const Ok"));
2387 assert!(s.contains("export const Err"));
2388 assert!(s.contains("export type Option<T>"));
2389 assert!(s.contains("export const Some"));
2390 assert!(s.contains("export const None"));
2391 assert!(s.contains("export interface ValidationError"));
2392 assert!(s.contains("export interface DurableObjectStorage"));
2394 assert!(s.contains("export interface DurableObjectState"));
2395 assert!(s.contains("export class InMemoryStorage"));
2396 assert!(s.contains("export function makeTestState"));
2397 assert!(s.contains("tag: \"Ok\""));
2399 assert!(s.contains("tag: \"Err\""));
2400 assert!(s.contains("tag: \"Some\""));
2401 assert!(s.contains("tag: \"None\""));
2402 }
2403
2404 #[test]
2405 fn tsconfig_is_well_formed_json() {
2406 let s = emit_tsconfig();
2407 assert!(s.contains("\"target\": \"ES2022\""));
2409 assert!(s.contains("\"strict\": true"));
2410 assert!(s.contains("\"include\""));
2411 }
2412
2413 #[test]
2414 fn workers_dir_name_replaces_dots_with_dashes() {
2415 assert_eq!(
2416 crate::project::worker_dir_name("commerce.payment"),
2417 "commerce-payment"
2418 );
2419 assert_eq!(crate::project::worker_dir_name("a.b.c"), "a-b-c");
2420 }
2421
2422 #[test]
2425 fn escape_ts_string_escapes_cr() {
2426 assert_eq!(escape_ts_string("a\\b"), "a\\\\b");
2427 assert_eq!(escape_ts_string("a\"b"), "a\\\"b");
2428 assert_eq!(escape_ts_string("a\nb"), "a\\nb");
2429 assert_eq!(escape_ts_string("a\tb"), "a\\tb");
2430 assert_eq!(escape_ts_string("a\rb"), "a\\rb"); }
2432
2433 #[test]
2434 fn runtime_import_depth_resolves_correctly() {
2435 assert_eq!(
2436 runtime_import_for(Path::new("compose.ts"), ImportExt::Js),
2437 "./runtime.js"
2438 );
2439 assert_eq!(
2440 runtime_import_for(Path::new("commerce/payment.ts"), ImportExt::Js),
2441 "../runtime.js"
2442 );
2443 assert_eq!(
2444 runtime_import_for(Path::new("commerce/orders/types.ts"), ImportExt::Js),
2445 "../../runtime.js"
2446 );
2447 assert_eq!(
2448 runtime_import_for(Path::new("tests/commerce_payment.test.ts"), ImportExt::Js),
2449 "../runtime.js"
2450 );
2451 }
2452}