1use super::*;
7
8fn record_param_hint(hints: &mut HintSink, param_name: &str, arg: &Expr) {
13 if param_name == "_" || param_name == "self" {
14 return;
15 }
16 if let ExprKind::Ident(id) = &arg.kind
17 && id.name == param_name
18 {
19 return;
20 }
21 hints.record_param(arg.span, format!("{param_name}:"));
22}
23
24#[allow(clippy::too_many_arguments)]
25pub(crate) fn check_fn(
26 f: &FnDecl,
27 input: &ResolvedCommons,
28 expr_types: &mut HashMap<ExprId, TypedExpr>,
29 callees: &mut HashMap<ExprId, Callee>,
30 errors: &mut Vec<CompileError>,
31 refs: &mut RefSink,
32 hints: &mut HintSink,
33 locals: &mut LocalsSink,
34 requirements: &mut RequirementSink,
35 tys: &Types,
36) {
37 let mut vars: HashSet<String> = f
41 .type_params
42 .iter()
43 .map(|tp| tp.name.name.clone())
44 .collect();
45 for tp in &f.type_params {
46 if input.types.contains_key(&tp.name.name) {
47 errors.push(
48 CompileError::new(
49 "bynk.generics.type_arg_mismatch",
50 tp.span,
51 format!(
52 "type parameter `{}` shadows the declared type of the same name",
53 tp.name.name
54 ),
55 )
56 .with_note("rename the type parameter"),
57 );
58 }
59 }
60 if let FnName::Method { type_name, .. } = &f.name
66 && let Some(decl) = input.types.get(&type_name.name)
67 {
68 for tp in &decl.type_params {
69 vars.insert(tp.name.name.clone());
70 }
71 }
72 let return_ty = match resolve_type_ref_in(&f.return_type, &input.types, &vars, tys) {
73 Some(t) => t,
74 None => return,
75 };
76 record_type_refs(&f.return_type, &input.types, &vars, refs);
77 let mut param_scope: HashMap<String, TyId> = HashMap::new();
78 if let FnName::Method { type_name, .. } = &f.name
83 && f.has_self
84 && let Some(decl) = input.types.get(&type_name.name)
85 {
86 let self_args = decl
87 .type_params
88 .iter()
89 .map(|tp| tys.intern(Ty::Var(tp.name.name.clone())))
90 .collect();
91 param_scope.insert("self".to_string(), named_ty_with_args(decl, self_args, tys));
92 }
93 for p in &f.params {
94 if let Some(ty) = resolve_type_ref_in(&p.type_ref, &input.types, &vars, tys) {
95 record_type_refs(&p.type_ref, &input.types, &vars, refs);
96 if p.name.name != "_" {
98 locals.record(
99 p.name.name.clone(),
100 p.name.span,
101 crate::locals::LocalKind::Param,
102 ty.display(tys),
103 f.body.span,
104 );
105 }
106 param_scope.insert(p.name.name.clone(), ty);
107 }
108 }
109 if !f.requires.is_empty() || !f.ensures.is_empty() {
114 let result_ty = match &*tys.get(return_ty) {
115 Ty::Effect(inner) => *inner,
116 _ => return_ty,
117 };
118 let has_result_param = f.params.iter().any(|p| p.name.name == "result");
119 let fn_label = format!("function `{}`", f.name.display());
120 check_contracts(
121 &f.requires,
122 &f.ensures,
123 ¶m_scope,
124 result_ty,
125 has_result_param,
126 &fn_label,
127 input,
128 expr_types,
129 errors,
130 refs,
131 hints,
132 locals,
133 requirements,
134 callees,
135 &vars,
136 tys,
137 );
138 }
139 let effectful = return_ty.is_effect(tys);
140 let mut ctx = Ctx {
141 input,
142 tys,
143 expr_types,
144 errors,
145 refs,
146 hints,
147 locals,
148 requirements,
149 callees,
150 scopes: vec![param_scope],
151 is_binding_cache: HashMap::new(),
152 admitted_literals: HashMap::new(),
153 pattern_binding_types: HashMap::new(),
154 return_ty,
155 return_ty_span: f.return_type.span(),
156 effectful,
157 agent_state_ty: None,
158 commit_seen: false,
159 caps: CapabilityCtx::default(),
160 in_test_body: false,
161 oidc_binder: None,
162 test_services: HashMap::new(),
163 test_actors: HashMap::new(),
164 type_vars: vars.clone(),
165 store_fields: HashMap::new(),
166 in_agent_handler: false,
167 };
168 let Some(body_ty) = type_of_block(&f.body, Some(return_ty), &mut ctx) else {
169 return;
170 };
171 linearity::check(
179 &f.body,
180 &f.params,
181 &input.types,
182 ctx.expr_types,
183 &ctx.pattern_binding_types,
184 &HashSet::new(),
185 ctx.errors,
186 tys,
187 );
188 if !compatible(body_ty, return_ty, tys) {
189 ctx.errors.push(
190 CompileError::new(
191 "bynk.types.return_mismatch",
192 f.body.tail.span,
193 format!(
194 "function body has type `{}`, but the declared return type is `{}`",
195 body_ty.display(tys),
196 return_ty.display(tys)
197 ),
198 )
199 .with_label(f.return_type.span(), "declared return type"),
200 );
201 }
202}
203
204#[allow(clippy::too_many_arguments)]
223fn check_static_initialiser(
224 init: &Expr,
225 field_type: &TypeRef,
226 input: &ResolvedCommons,
227 expr_types: &mut HashMap<ExprId, TypedExpr>,
228 callees: &mut HashMap<ExprId, Callee>,
229 errors: &mut Vec<CompileError>,
230 refs: &mut RefSink,
231 hints: &mut HintSink,
232 locals: &mut LocalsSink,
233 code: &'static str,
234 subject: &str,
235 tys: &Types,
236) {
237 let Some(field_ty) = resolve_type_ref(field_type, &input.types, tys) else {
238 return; };
240 let mut local_errors: Vec<CompileError> = Vec::new();
241 let mut init_requirements = RequirementSink::new();
244 let result = {
245 let mut ctx = Ctx {
246 input,
247 tys,
248 expr_types,
249 errors: &mut local_errors,
250 refs,
251 hints,
252 locals,
253 requirements: &mut init_requirements,
254 callees,
255 scopes: vec![HashMap::new()],
256 is_binding_cache: HashMap::new(),
257 admitted_literals: HashMap::new(),
258 pattern_binding_types: HashMap::new(),
259 return_ty: field_ty,
260 return_ty_span: init.span,
261 effectful: false,
262 agent_state_ty: None,
263 commit_seen: false,
264 caps: CapabilityCtx::default(),
265 in_test_body: false,
266 oidc_binder: None,
267 test_services: HashMap::new(),
268 test_actors: HashMap::new(),
269 type_vars: HashSet::new(),
270 store_fields: HashMap::new(),
271 in_agent_handler: false,
272 };
273 type_of(init, Some(field_ty), &mut ctx)
274 };
275 let compatible_result = matches!(&result, Some(t) if compatible(*t, field_ty, tys));
276 if !compatible_result || !local_errors.is_empty() {
277 let got = result
278 .map(|t| t.display(tys))
279 .unwrap_or_else(|| "an invalid expression".to_string());
280 errors.push(
281 CompileError::new(
282 code,
283 init.span,
284 format!(
285 "{subject} must be a static value of type `{}` (got `{got}`)",
286 field_ty.display(tys),
287 ),
288 )
289 .with_note(
290 "an initialiser is a compile-time value — a literal (including one admitted to a \
291 refined type), a sum variant, `Some`/`None`/`Ok`/`Err`, a record, or — for an \
292 opaque type — `T.unsafe(lit)` — with no reference to `self`, parameters, or \
293 capabilities",
294 ),
295 );
296 }
297}
298
299#[allow(clippy::too_many_arguments)]
302pub fn check_state_initialiser(
303 init: &Expr,
304 field_type: &TypeRef,
305 input: &ResolvedCommons,
306 tys: &Types,
307 expr_types: &mut HashMap<ExprId, TypedExpr>,
308 callees: &mut HashMap<ExprId, Callee>,
309 errors: &mut Vec<CompileError>,
310 refs: &mut RefSink,
311 hints: &mut HintSink,
312 locals: &mut LocalsSink,
313) {
314 check_static_initialiser(
315 init,
316 field_type,
317 input,
318 expr_types,
319 callees,
320 errors,
321 refs,
322 hints,
323 locals,
324 "bynk.agents.bad_state_initialiser",
325 "state field initialiser",
326 tys,
327 );
328}
329
330#[allow(clippy::too_many_arguments)]
348pub fn check_event_field_default(
349 init: &Expr,
350 field_type: &TypeRef,
351 input: &ResolvedCommons,
352 tys: &Types,
353 expr_types: &mut HashMap<ExprId, TypedExpr>,
354 callees: &mut HashMap<ExprId, Callee>,
355 errors: &mut Vec<CompileError>,
356 refs: &mut RefSink,
357 hints: &mut HintSink,
358 locals: &mut LocalsSink,
359) {
360 check_static_initialiser(
361 init,
362 field_type,
363 input,
364 expr_types,
365 callees,
366 errors,
367 refs,
368 hints,
369 locals,
370 "bynk.event.bad_field_default",
371 "event field default",
372 tys,
373 );
374 if let ExprKind::MethodCall {
379 receiver,
380 method,
381 args,
382 ..
383 } = &init.kind
384 && let ExprKind::Ident(type_name) = &receiver.kind
385 && method.name == "unsafe"
386 && let [lit_expr] = args.as_slice()
387 && let Some(decl) = input.types.get(&type_name.name)
388 && matches!(decl.body, TypeBody::Opaque { .. })
389 && let Some(refinement) = refinements::type_decl_refinement(decl)
390 && let Some(lit) = refinements::const_literal(lit_expr)
391 && let Some(failed) = refinements::first_failed_predicate(refinement, &lit)
392 {
393 errors.push(
394 CompileError::new(
395 "bynk.event.bad_field_default",
396 lit_expr.span,
397 format!(
398 "`{}.unsafe(...)` bypasses its own refinement, but an event field default \
399 must be a value the wire could actually carry — this literal fails `{}`",
400 type_name.name,
401 failed.name(),
402 ),
403 )
404 .with_note(
405 "a default is spliced into the same codec that validates a real wire value on \
406 receipt, so a refinement-violating `.unsafe(lit)` default would compile cleanly \
407 and then fail at runtime the first time an old event actually triggers it",
408 ),
409 );
410 }
411}
412
413fn refuse_experimental(path: &str, span: Span, ctx: &mut Ctx) {
416 if let Some(err) = ctx.input.stability.policy.check(path, span) {
417 ctx.errors.push(err);
418 }
419}
420
421pub(crate) fn refuse_experimental_import(name: &Ident, ctx: &mut Ctx) {
425 if let Some(path) = ctx.input.stability.blocked_imports.get(&name.name).cloned() {
426 refuse_experimental(&path, name.span, ctx);
427 }
428}
429
430fn warn_bynk_list_deprecation(name: &Ident, args: &[Expr], call_span: Span, ctx: &mut Ctx) {
436 if ctx.input.imported_from.get(&name.name).map(String::as_str) != Some("bynk.list") {
437 return;
438 }
439 let method_form: &str = match name.name.as_str() {
441 "map" => "xs.map(f)",
442 "filter" => "xs.filter(p)",
443 "any" => "xs.any(p)",
444 "all" => "xs.all(p)",
445 "find" => "xs.filter(p).first()",
446 _ => return, };
448 let mut err = CompileError::new(
449 "bynk.list.deprecated_function",
450 name.span,
451 format!(
452 "`bynk.list.{}` is deprecated — use the `List` method form `{method_form}`",
453 name.name
454 ),
455 )
456 .with_note(
457 "the `bynk.list.*` free functions are superseded by the method-chain vocabulary (ADR 0116); the method form reads left-to-right and chains",
458 );
459 if args.len() == 2 {
462 let mut edits = vec![
463 (
465 Span::new(name.span.start, args[0].span.start),
466 String::new(),
467 ),
468 (
470 Span::new(args[0].span.end, args[1].span.start),
471 format!(
472 ".{}(",
473 if name.name == "find" {
474 "filter"
475 } else {
476 &name.name
477 }
478 ),
479 ),
480 ];
481 if name.name == "find" {
482 edits.push((
484 Span::new(call_span.end, call_span.end),
485 ".first()".to_string(),
486 ));
487 }
488 err = err.with_suggestion(
489 format!("rewrite to the `List` method form `{method_form}`"),
490 edits,
491 Applicability::MachineApplicable,
492 );
493 }
494 ctx.errors.push(err);
495}
496
497pub(crate) fn check_call(
498 name: &Ident,
499 type_args: &[TypeRef],
500 args: &[Expr],
501 span: Span,
502 expected: Option<TyId>,
505 expr_id: ExprId,
508 ctx: &mut Ctx,
509) -> Option<TyId> {
510 let tys = ctx.tys;
511 if let Some(fn_decl) = ctx.input.fns.get(&name.name) {
512 ctx.refs.record(name.span, SymbolKind::Fn, &name.name);
513 ctx.callees.insert(expr_id, Callee::Fn(Arc::clone(fn_decl)));
514 warn_bynk_list_deprecation(name, args, span, ctx);
515 refuse_experimental_import(name, ctx);
516 return check_call_against_fn(name, fn_decl, type_args, args, ctx);
517 }
518 if !type_args.is_empty() {
520 ctx.errors.push(CompileError::new(
521 "bynk.generics.type_arg_mismatch",
522 span,
523 format!(
524 "`{}` is not a generic function — it takes no type arguments",
525 name.name
526 ),
527 ));
528 for a in args {
529 let _ = type_of(a, None, ctx);
530 }
531 return None;
532 }
533 let owners: Vec<&Arc<TypeDecl>> = ctx
537 .input
538 .types
539 .values()
540 .filter(|t| matches!(&t.body, TypeBody::Sum(s) if s.variants.iter().any(|v| v.name.name == name.name)))
541 .collect();
542 if owners.len() == 1 {
543 ctx.callees.insert(
544 expr_id,
545 Callee::Ctor {
546 sum: Arc::clone(owners[0]),
547 tag: name.name.clone(),
548 },
549 );
550 return check_variant_construction(owners[0], &name.name, args, span, expected, ctx);
551 }
552 if let Some(agent) = ctx.input.agents.get(&name.name).cloned() {
556 ctx.refs.record(name.span, SymbolKind::Agent, &name.name);
557 ctx.callees
558 .insert(expr_id, Callee::AgentInit(name.name.clone()));
559 let key_ty = resolve_type_ref(&agent.key_type, &ctx.input.types, tys);
560 if args.len() != 1 {
561 let mut err = CompileError::new(
562 "bynk.agent.construction_arity",
563 span,
564 format!(
565 "agent `{}` is constructed with one key argument, but {} were given",
566 name.name,
567 args.len()
568 ),
569 );
570 if let Some(s) = ctx.input.shadowed_imports.get(&name.name) {
573 err = err.with_note(format!(
574 "this unit's agent `{}` hides the {} of that name from `{}`; rename one to use the {}",
575 name.name, s.imported, s.unit, s.imported
576 ));
577 }
578 ctx.errors.push(err);
579 for a in args {
580 let _ = type_of(a, None, ctx);
581 }
582 return None;
583 }
584 let arg_ty = type_of(&args[0], key_ty, ctx);
585 if let (Some(a), Some(k)) = (arg_ty, key_ty)
586 && !compatible(a, k, tys)
587 {
588 ctx.errors.push(CompileError::new(
589 "bynk.agent.key_mismatch",
590 args[0].span,
591 format!(
592 "agent `{}` key is `{}`, but a value of type `{}` was given",
593 name.name,
594 k.display(tys),
595 a.display(tys)
596 ),
597 ));
598 }
599 return Some(tys.intern(Ty::Named {
600 name: name.name.clone(),
601 kind: NamedKind::Record,
602 args: Vec::new(),
603 }));
604 }
605 if let Some(ty) = ctx.lookup(&name.name) {
611 return match &*tys.get(ty) {
612 Ty::Fn { params, ret } => {
613 ctx.callees
614 .insert(expr_id, Callee::Value(name.name.clone()));
615 check_value_application(name, params, *ret, args, span, ctx)
616 }
617 _ => {
618 ctx.errors.push(
621 CompileError::new(
622 "bynk.resolve.param_as_function",
623 span,
624 format!(
625 "`{}` has type `{}` and is not callable",
626 name.name,
627 ty.display(tys)
628 ),
629 )
630 .with_note("only values of function type can be applied"),
631 );
632 for a in args {
633 let _ = type_of(a, None, ctx);
634 }
635 None
636 }
637 };
638 }
639 for a in args {
646 let _ = type_of(a, None, ctx);
647 }
648 if owners.len() > 1 {
649 ctx.errors.push(CompileError::new(
650 "bynk.resolve.ambiguous_variant",
651 name.span,
652 format!(
653 "the variant name `{}` is declared on multiple sum types — qualify it as `TypeName.{}(...)`",
654 name.name, name.name
655 ),
656 ));
657 return None;
658 }
659 if let Some(err) = ctx.input.shadowed_import_error(&name.name, name.span) {
662 ctx.errors.push(err);
663 return None;
664 }
665 if ctx.input.types.contains_key(&name.name) {
666 ctx.errors.push(CompileError::new(
667 "bynk.resolve.type_as_function",
668 span,
669 format!(
670 "`{}` is a type, not a function — use `{}.of(value)` or `{} {{ ... }}` instead",
671 name.name, name.name, name.name
672 ),
673 ));
674 return None;
675 }
676 ctx.errors.push(
677 CompileError::new(
678 "bynk.resolve.unknown_function",
679 span,
680 format!("unknown function `{}`", name.name),
681 )
682 .with_note("only functions declared in this commons are callable"),
683 );
684 None
685}
686
687fn check_value_application(
688 name: &Ident,
689 params: &[TyId],
690 ret: TyId,
691 args: &[Expr],
692 span: Span,
693 ctx: &mut Ctx,
694) -> Option<TyId> {
695 let tys = ctx.tys;
696 if ret.is_effect(tys) && !ctx.effectful {
697 ctx.errors.push(
698 CompileError::new(
699 "bynk.effect.fn_value_in_pure_context",
700 span,
701 format!(
702 "`{}` is an effectful function (`{}`) and cannot be called in a pure context",
703 name.name,
704 Ty::Fn {
705 params: params.to_vec(),
706 ret,
707 }
708 .display(tys)
709 ),
710 )
711 .with_note(
712 "effectful function values may only be called where the enclosing body is effectful (its return type is an Effect)",
713 ),
714 );
715 }
716 if params.len() != args.len() {
717 ctx.errors.push(CompileError::new(
718 "bynk.types.call_arity",
719 span,
720 format!(
721 "`{}` takes {} argument(s), but {} were given",
722 name.name,
723 params.len(),
724 args.len()
725 ),
726 ));
727 for a in args {
728 let _ = type_of(a, None, ctx);
729 }
730 return None;
731 }
732 for (arg, param_ty) in args.iter().zip(params) {
733 let arg_ty = type_of(arg, Some(*param_ty), ctx);
734 if let Some(a) = arg_ty
735 && !compatible(a, *param_ty, tys)
736 {
737 ctx.errors.push(CompileError::new(
738 "bynk.types.argument_mismatch",
739 arg.span,
740 format!(
741 "argument has type `{}`, but `{}` expects `{}`",
742 a.display(tys),
743 name.name,
744 param_ty.display(tys)
745 ),
746 ));
747 }
748 }
749 Some(ret)
750}
751
752fn check_generic_call(
763 name: &Ident,
764 fn_decl: &FnDecl,
765 type_args: &[TypeRef],
766 args: &[Expr],
767 ctx: &mut Ctx,
768) -> Option<TyId> {
769 let tys = ctx.tys;
770 let vars: HashSet<String> = fn_decl
771 .type_params
772 .iter()
773 .map(|tp| tp.name.name.clone())
774 .collect();
775 if fn_decl.params.len() != args.len() {
776 ctx.errors.push(
779 CompileError::new(
780 "bynk.resolve.arity_mismatch",
781 name.span,
782 format!(
783 "function `{}` expects {} argument(s), but {} were given",
784 name.name,
785 fn_decl.params.len(),
786 args.len()
787 ),
788 )
789 .with_label(fn_decl.name.ident().span, "function declared here"),
790 );
791 for a in args {
792 let _ = type_of(a, None, ctx);
793 }
794 return None;
795 }
796 let var_params: Vec<Option<TyId>> = fn_decl
797 .params
798 .iter()
799 .map(|p| resolve_type_ref_in(&p.type_ref, &ctx.input.types, &vars, tys))
800 .collect();
801 let ret_pattern = resolve_type_ref_in(&fn_decl.return_type, &ctx.input.types, &vars, tys)?;
802
803 let mut subst: HashMap<String, TyId> = HashMap::new();
804 if !type_args.is_empty() {
805 if type_args.len() != fn_decl.type_params.len() {
806 ctx.errors.push(CompileError::new(
807 "bynk.generics.type_arg_mismatch",
808 name.span,
809 format!(
810 "`{}` takes {} type argument(s), but {} were given",
811 name.name,
812 fn_decl.type_params.len(),
813 type_args.len()
814 ),
815 ));
816 return None;
817 }
818 for (tp, ta) in fn_decl.type_params.iter().zip(type_args) {
819 let ty = resolve_expr_type_ref(ta, ctx)?;
825 subst.insert(tp.name.name.clone(), ty);
826 }
827 }
828
829 let mut arg_tys: Vec<Option<TyId>> = vec![None; args.len()];
830 for (i, arg) in args.iter().enumerate() {
832 if matches!(arg.kind, ExprKind::Lambda(_)) {
833 continue;
834 }
835 let expected = var_params[i].map(|p| substitute(p, &subst, tys));
836 let ty = type_of(arg, expected, ctx);
837 if let (Some(pattern), Some(actual)) = (var_params[i], ty)
838 && !unify(pattern, actual, &mut subst, tys)
839 {
840 ctx.errors.push(CompileError::new(
841 "bynk.generics.type_arg_mismatch",
842 arg.span,
843 format!(
844 "argument {} infers a type for `{}`'s type parameter that conflicts with an earlier argument — annotate with `{}[T](…)`",
845 i + 1,
846 name.name,
847 name.name
848 ),
849 ));
850 return None;
851 }
852 arg_tys[i] = ty;
853 }
854 for (i, arg) in args.iter().enumerate() {
856 if !matches!(arg.kind, ExprKind::Lambda(_)) {
857 continue;
858 }
859 let expected = var_params[i].map(|p| substitute(p, &subst, tys));
860 let params_unconstrained = expected.is_some_and(|e| {
861 matches!(&*tys.get(e), Ty::Fn { params, .. }
862 if params.iter().any(|p| contains_var(*p, tys)))
863 });
864 let fully_annotated = matches!(
865 &arg.kind,
866 ExprKind::Lambda(l) if l.params.iter().all(|p| p.type_ref.is_some())
867 );
868 if params_unconstrained && !fully_annotated {
869 ctx.errors.push(
870 CompileError::new(
871 "bynk.generics.uninferable_type_arg",
872 arg.span,
873 format!(
874 "the lambda's parameter types depend on `{}`'s type parameters, which the other arguments do not determine",
875 name.name
876 ),
877 )
878 .with_note("annotate the lambda's parameters, or give explicit type arguments: `name[T](…)`"),
879 );
880 return None;
881 }
882 let ty = if params_unconstrained {
885 type_of(arg, None, ctx)
886 } else {
887 type_of(arg, expected, ctx)
888 };
889 if let (Some(pattern), Some(actual)) = (var_params[i], ty)
890 && !unify(pattern, actual, &mut subst, tys)
891 {
892 ctx.errors.push(CompileError::new(
893 "bynk.generics.type_arg_mismatch",
894 arg.span,
895 format!(
896 "the lambda's type conflicts with `{}`'s inferred type arguments",
897 name.name
898 ),
899 ));
900 return None;
901 }
902 arg_tys[i] = ty;
903 }
904 for tp in &fn_decl.type_params {
906 if !subst.contains_key(&tp.name.name) {
907 ctx.errors.push(
908 CompileError::new(
909 "bynk.generics.uninferable_type_arg",
910 name.span,
911 format!(
912 "type parameter `{}` of `{}` is neither inferable from the arguments nor given explicitly",
913 tp.name.name, name.name
914 ),
915 )
916 .with_label(tp.span, "declared here")
917 .with_note("give explicit type arguments: `name[T](…)`"),
918 );
919 return None;
920 }
921 }
922 let mut ok = true;
924 for (i, (pattern, arg)) in var_params.iter().zip(args).enumerate() {
925 record_param_hint(ctx.hints, &fn_decl.params[i].name.name, arg);
926 let (Some(pattern), Some(arg_ty)) = (pattern, arg_tys[i].as_ref()) else {
927 continue;
928 };
929 let ground = substitute(*pattern, &subst, tys);
930 if !compatible(*arg_ty, ground, tys) {
931 ctx.errors.push(CompileError::new(
932 "bynk.types.argument_mismatch",
933 arg.span,
934 format!(
935 "argument {} to `{}` has type `{}`, but `{}` is expected",
936 i + 1,
937 name.name,
938 arg_ty.display(tys),
939 ground.display(tys)
940 ),
941 ));
942 ok = false;
943 }
944 }
945 if !ok {
946 return None;
947 }
948 let compared = match ctx.input.fns.get(&name.name) {
952 Some(decl) if std::ptr::eq(decl.as_ref(), fn_decl) => {
953 super::equality::compared_type_params(decl, ctx)
954 }
955 _ => Default::default(),
956 };
957 let params: Vec<(String, Option<Span>)> = fn_decl
958 .type_params
959 .iter()
960 .map(|tp| (tp.name.name.clone(), Some(tp.span)))
961 .collect();
962 if !super::equality::check_compared_args(&name.name, name.span, ¶ms, &compared, &subst, ctx)
963 {
964 return None;
965 }
966 if type_args.is_empty() && !fn_decl.type_params.is_empty() {
971 let rendered: Option<Vec<String>> = fn_decl
972 .type_params
973 .iter()
974 .map(|tp| subst.get(&tp.name.name).map(|t| t.display(tys)))
975 .collect();
976 if let Some(parts) = rendered {
977 ctx.hints
978 .record(name.span, format!("[{}]", parts.join(", ")));
979 }
980 }
981 let ret = substitute(ret_pattern, &subst, tys);
982 Some(ret)
987}
988
989fn check_call_against_fn(
990 name: &Ident,
991 fn_decl: &FnDecl,
992 type_args: &[TypeRef],
993 args: &[Expr],
994 ctx: &mut Ctx,
995) -> Option<TyId> {
996 let tys = ctx.tys;
997 if !fn_decl.type_params.is_empty() {
1001 return check_generic_call(name, fn_decl, type_args, args, ctx);
1002 }
1003 if !type_args.is_empty() {
1004 ctx.errors.push(CompileError::new(
1005 "bynk.generics.type_arg_mismatch",
1006 name.span,
1007 format!(
1008 "`{}` is not a generic function — it takes no type arguments",
1009 name.name
1010 ),
1011 ));
1012 for a in args {
1013 let _ = type_of(a, None, ctx);
1014 }
1015 return None;
1016 }
1017 if fn_decl.params.len() != args.len() {
1018 ctx.errors.push(
1022 CompileError::new(
1023 "bynk.resolve.arity_mismatch",
1024 name.span,
1025 format!(
1026 "function `{}` expects {} argument(s), but {} were given",
1027 name.name,
1028 fn_decl.params.len(),
1029 args.len()
1030 ),
1031 )
1032 .with_label(fn_decl.name.ident().span, "function declared here"),
1033 );
1034 for a in args {
1035 let _ = type_of(a, None, ctx);
1036 }
1037 return None;
1038 }
1039 let resolved_params: Vec<(Option<TyId>, &Param)> = fn_decl
1040 .params
1041 .iter()
1042 .map(|p| (resolve_type_ref(&p.type_ref, &ctx.input.types, tys), p))
1043 .collect();
1044 let mut ok = true;
1045 for (i, ((param_ty, param), arg)) in resolved_params.iter().zip(args.iter()).enumerate() {
1046 record_param_hint(ctx.hints, ¶m.name.name, arg);
1047 let arg_ty = type_of(arg, *param_ty, ctx);
1048 let (Some(arg_ty), Some(param_ty)) = (arg_ty, *param_ty) else {
1049 ok = false;
1050 continue;
1051 };
1052 if !compatible(arg_ty, param_ty, tys) {
1053 ctx.errors.push(
1054 CompileError::new(
1055 "bynk.types.argument_mismatch",
1056 arg.span,
1057 format!(
1058 "argument {} to `{}` has type `{}`, but parameter `{}` expects `{}`",
1059 i + 1,
1060 name.name,
1061 arg_ty.display(tys),
1062 param.name.name,
1063 param_ty.display(tys)
1064 ),
1065 )
1066 .with_label(param.span, "parameter declared here"),
1067 );
1068 ok = false;
1069 }
1070 }
1071 if !ok {
1072 return None;
1073 }
1074 resolve_type_ref(&fn_decl.return_type, &ctx.input.types, tys)
1075}
1076
1077pub(crate) fn check_arg(arg: &Expr, expected: TyId, what: &str, ctx: &mut Ctx) {
1080 let tys = ctx.tys;
1081 let Some(actual) = type_of(arg, Some(expected), ctx) else {
1082 return;
1083 };
1084 if !compatible(actual, expected, tys) {
1085 ctx.errors.push(CompileError::new(
1086 "bynk.types.type_mismatch",
1087 arg.span,
1088 format!(
1089 "{what} has type `{}`, but `{}` is required",
1090 actual.display(tys),
1091 expected.display(tys)
1092 ),
1093 ));
1094 }
1095}
1096
1097fn record_capability_ref(span: Span, name: &str, ctx: &mut Ctx) {
1102 if let Some(unit) = ctx.input.cross_context.flattened_caps.get(name) {
1103 ctx.refs
1104 .record_in_unit(span, SymbolKind::Capability, name, unit);
1105 } else {
1106 ctx.refs.record(span, SymbolKind::Capability, name);
1107 }
1108}
1109
1110#[allow(clippy::too_many_arguments)]
1111pub(crate) fn check_static_call(
1112 type_name: &Ident,
1113 method: &Ident,
1114 type_args: &[TypeRef],
1120 args: &[Expr],
1121 span: Span,
1122 expected: Option<TyId>,
1125 expr_id: ExprId,
1128 ctx: &mut Ctx,
1129) -> Option<TyId> {
1130 let tys = ctx.tys;
1131 if ctx.caps.declared_capabilities.contains_key(&type_name.name)
1135 && !ctx.caps.capabilities.contains_key(&type_name.name)
1136 {
1137 record_capability_ref(type_name.span, &type_name.name, ctx);
1138 ctx.callees.insert(
1139 expr_id,
1140 Callee::Capability {
1141 cap: type_name.name.clone(),
1142 op: method.name.clone(),
1143 },
1144 );
1145 let mut err = CompileError::new(
1146 "bynk.given.undeclared_capability",
1147 type_name.span,
1148 format!(
1149 "capability `{}` is used but not listed in the handler's `given` clause",
1150 type_name.name
1151 ),
1152 )
1153 .with_note(format!(
1154 "add `{}` to the handler's `given` clause so the dependency surface is visible at the declaration site",
1155 type_name.name
1156 ));
1157 if let Some((span, insert)) = given_insertion_edit(
1159 &ctx.caps.given_entries,
1160 ctx.caps.given_anchor,
1161 &type_name.name,
1162 ) {
1163 err = err.with_suggestion(
1164 format!("add `{}` to the `given` clause", type_name.name),
1165 vec![(span, insert)],
1166 Applicability::MachineApplicable,
1167 );
1168 }
1169 ctx.errors.push(err);
1170 record_requirement(
1175 ctx,
1176 &type_name.name,
1177 span,
1178 RequirementSource::DirectCall {
1179 op: method.name.clone(),
1180 },
1181 false,
1182 );
1183 for a in args {
1184 let _ = type_of(a, None, ctx);
1185 }
1186 return None;
1187 }
1188 if let Some(cap) = ctx.caps.capabilities.get(&type_name.name).cloned() {
1189 record_capability_ref(type_name.span, &type_name.name, ctx);
1190 if let Some(unit) = ctx
1192 .input
1193 .cross_context
1194 .flattened_caps
1195 .get(&type_name.name)
1196 .cloned()
1197 {
1198 refuse_experimental(
1199 &format!("{unit}.{}.{}", type_name.name, method.name),
1200 method.span,
1201 ctx,
1202 );
1203 }
1204 ctx.callees.insert(
1205 expr_id,
1206 Callee::Capability {
1207 cap: type_name.name.clone(),
1208 op: method.name.clone(),
1209 },
1210 );
1211 if !ctx.effectful {
1212 ctx.errors.push(
1213 CompileError::new(
1214 "bynk.effect.capability_in_pure_context",
1215 span,
1216 format!(
1217 "capability `{}` can only be called inside an effectful body (one returning `Effect[T]`)",
1218 type_name.name
1219 ),
1220 ),
1221 );
1222 }
1223 ctx.caps.given_used.insert(type_name.name.clone());
1224 record_requirement(
1228 ctx,
1229 &type_name.name,
1230 span,
1231 RequirementSource::DirectCall {
1232 op: method.name.clone(),
1233 },
1234 true,
1235 );
1236 let Some(op) = cap.ops.iter().find(|o| o.name == method.name) else {
1237 ctx.errors.push(CompileError::new(
1238 "bynk.capability.unknown_operation",
1239 method.span,
1240 format!(
1241 "capability `{}` has no operation named `{}`",
1242 type_name.name, method.name
1243 ),
1244 ));
1245 for a in args {
1246 let _ = type_of(a, None, ctx);
1247 }
1248 return None;
1249 };
1250 match &cap.derived_from {
1256 Some((unit, base)) => {
1257 refuse_experimental(&format!("{unit}.{base}.{}", method.name), method.span, ctx);
1258 ctx.refs.record_in_unit(
1259 method.span,
1260 SymbolKind::CapabilityOp,
1261 &format!("{base}.{}", method.name),
1262 unit,
1263 );
1264 }
1265 None => ctx.refs.record(
1266 method.span,
1267 SymbolKind::CapabilityOp,
1268 &format!("{}.{}", type_name.name, method.name),
1269 ),
1270 }
1271 if op.params.len() != args.len() {
1272 ctx.errors.push(CompileError::new(
1273 "bynk.capability.op_arity",
1274 span,
1275 format!(
1276 "capability operation `{}.{}` expects {} argument(s), but {} were given",
1277 type_name.name,
1278 method.name,
1279 op.params.len(),
1280 args.len()
1281 ),
1282 ));
1283 for a in args {
1284 let _ = type_of(a, None, ctx);
1285 }
1286 return None;
1287 }
1288 let op_clone = op.clone();
1289 let mut subst: HashMap<String, TyId> = HashMap::new();
1296 if !op_clone.type_params.is_empty() || !type_args.is_empty() {
1297 if type_args.is_empty() {
1298 ctx.errors.push(
1299 CompileError::new(
1300 "bynk.generics.uninferable_type_arg",
1301 span,
1302 format!(
1303 "capability operation `{}.{}` takes a type parameter, but none of its arguments determine it",
1304 type_name.name, method.name
1305 ),
1306 )
1307 .with_note(format!(
1308 "give it explicitly: `{}.{}[T](…)`",
1309 type_name.name, method.name
1310 )),
1311 );
1312 for a in args {
1313 let _ = type_of(a, None, ctx);
1314 }
1315 return None;
1316 }
1317 if type_args.len() != op_clone.type_params.len() {
1318 ctx.errors.push(CompileError::new(
1319 "bynk.generics.type_arg_mismatch",
1320 span,
1321 format!(
1322 "capability operation `{}.{}` takes {} type argument(s), but {} were given",
1323 type_name.name,
1324 method.name,
1325 op_clone.type_params.len(),
1326 type_args.len()
1327 ),
1328 ));
1329 for a in args {
1330 let _ = type_of(a, None, ctx);
1331 }
1332 return None;
1333 }
1334 for (tp, ta) in op_clone.type_params.iter().zip(type_args) {
1335 let ty = resolve_expr_type_ref(ta, ctx)?;
1336 let is_first_party_events = type_name.name == "Events"
1355 && method.name == "emit"
1356 && (ctx.input.commons.name.joined() == crate::firstparty::BYNK_UNIT
1357 || ctx
1358 .input
1359 .cross_context
1360 .flattened_caps
1361 .get("Events")
1362 .map(String::as_str)
1363 == Some(crate::firstparty::BYNK_UNIT));
1364 if is_first_party_events && let Ty::Named { name: ename, .. } = &*tys.get(ty) {
1365 if ctx.input.is_local_event(ename) {
1366 } else if ctx.input.is_local_type(ename) {
1368 ctx.errors.push(
1377 CompileError::new(
1378 "bynk.event.emit_not_an_event",
1379 ta.span(),
1380 format!(
1381 "`{ename}` is not a declared `event` — `Events.emit` may only name an event type"
1382 ),
1383 )
1384 .with_note(
1385 "declare it with `event Name = { ... }`, or check that the type argument names the event you meant",
1386 ),
1387 );
1388 } else {
1389 ctx.errors.push(
1390 CompileError::new(
1391 "bynk.event.emit_outside_owner",
1392 ta.span(),
1393 format!(
1394 "`{ename}` is not declared in this context — only the context that declares an event may emit it"
1395 ),
1396 )
1397 .with_note(
1398 "a foreign event is visible via `consumes` for subscription (`from Events(...)`), but only its owning context may `Events.emit` it",
1399 ),
1400 );
1401 }
1402 }
1403 subst.insert(tp.clone(), ty);
1404 }
1405 }
1406 for (i, (param_ty, arg)) in op_clone.params.iter().zip(args.iter()).enumerate() {
1407 let param_ty = substitute(*param_ty, &subst, tys);
1408 let arg_ty = type_of(arg, Some(param_ty), ctx);
1409 if let Some(actual) = arg_ty
1410 && !compatible(actual, param_ty, tys)
1411 {
1412 ctx.errors.push(CompileError::new(
1413 "bynk.types.argument_mismatch",
1414 arg.span,
1415 format!(
1416 "argument {} to capability `{}.{}` has type `{}`, but parameter expects `{}`",
1417 i + 1,
1418 type_name.name,
1419 method.name,
1420 actual.display(tys),
1421 param_ty.display(tys)
1422 ),
1423 ));
1424 }
1425 }
1426 return Some(substitute(op_clone.return_ty, &subst, tys));
1427 }
1428 let decl = ctx.input.types.get(&type_name.name)?;
1429 ctx.refs
1430 .record(type_name.span, SymbolKind::Type, &type_name.name);
1431
1432 if let Some(method_decl) = ctx
1436 .input
1437 .methods
1438 .get(&type_name.name)
1439 .and_then(|table| table.statics.get(&method.name))
1440 {
1441 ctx.callees
1442 .insert(expr_id, Callee::Static(Arc::clone(method_decl)));
1443 return check_method_args(method_decl, args, ctx, type_name, method);
1444 }
1445
1446 if method.name == OF
1448 && let Some(base) = type_decl_base(decl)
1449 {
1450 ctx.callees
1451 .insert(expr_id, Callee::Refine(Arc::clone(decl)));
1452 if args.len() != 1 {
1453 ctx.errors.push(CompileError::new(
1454 "bynk.types.constructor_arity",
1455 span,
1456 format!(
1457 "constructor `{}.of` expects 1 argument, but {} were given",
1458 type_name.name,
1459 args.len()
1460 ),
1461 ));
1462 return None;
1463 }
1464 let arg = &args[0];
1465 let expected = tys.intern(Ty::Base(base));
1466 let arg_ty = type_of(arg, Some(expected), ctx)?;
1467 if !compatible(arg_ty, expected, tys) {
1468 ctx.errors.push(CompileError::new(
1469 "bynk.types.constructor_base_mismatch",
1470 arg.span,
1471 format!(
1472 "constructor `{}.of` expects a `{}` argument, but got `{}`",
1473 type_name.name,
1474 base.name(),
1475 arg_ty.display(tys)
1476 ),
1477 ));
1478 return None;
1479 }
1480 return Some(tys.intern(Ty::Result(
1486 named_ty(decl, tys),
1487 tys.intern(Ty::ValidationError),
1488 )));
1489 }
1490
1491 if method.name == UNSAFE
1494 && let TypeBody::Opaque { base, .. } = &decl.body
1495 {
1496 ctx.callees
1497 .insert(expr_id, Callee::Unsafe(Arc::clone(decl)));
1498 if !ctx.input.is_local_type(&decl.name.name) {
1499 ctx.errors.push(
1500 CompileError::new(
1501 "bynk.types.opaque_unsafe_outside",
1502 method.span,
1503 format!(
1504 "`{}.unsafe(...)` is only available within the commons that defines the opaque type `{}`",
1505 type_name.name, type_name.name
1506 ),
1507 )
1508 .with_note(
1509 "outside the defining commons, opaque values are constructed via `T.of(value)`",
1510 ),
1511 );
1512 return None;
1513 }
1514 if args.len() != 1 {
1515 ctx.errors.push(CompileError::new(
1516 "bynk.types.constructor_arity",
1517 span,
1518 format!(
1519 "`{}.unsafe` expects 1 argument, but {} were given",
1520 type_name.name,
1521 args.len()
1522 ),
1523 ));
1524 return None;
1525 }
1526 let arg = &args[0];
1527 let expected = tys.intern(Ty::Base(*base));
1528 let arg_ty = type_of(arg, Some(expected), ctx)?;
1529 if !compatible(arg_ty, expected, tys) {
1530 ctx.errors.push(CompileError::new(
1531 "bynk.types.constructor_base_mismatch",
1532 arg.span,
1533 format!(
1534 "`{}.unsafe` expects a `{}` argument, but got `{}`",
1535 type_name.name,
1536 base.name(),
1537 arg_ty.display(tys)
1538 ),
1539 ));
1540 return None;
1541 }
1542 return Some(named_ty(decl, tys));
1543 }
1544
1545 if let TypeBody::Sum(_) = &decl.body {
1547 ctx.callees.insert(
1548 expr_id,
1549 Callee::Ctor {
1550 sum: Arc::clone(decl),
1551 tag: method.name.clone(),
1552 },
1553 );
1554 return check_variant_construction(decl, &method.name, args, span, expected, ctx);
1555 }
1556
1557 ctx.errors.push(
1558 CompileError::new(
1559 "bynk.types.unknown_static_member",
1560 method.span,
1561 format!(
1562 "type `{}` has no static method or variant named `{}`",
1563 type_name.name, method.name
1564 ),
1565 )
1566 .with_note("type declared here"),
1569 );
1570 None
1571}
1572
1573fn check_method_args(
1574 method_decl: &FnDecl,
1575 args: &[Expr],
1576 ctx: &mut Ctx,
1577 type_name: &Ident,
1578 method: &Ident,
1579) -> Option<TyId> {
1580 let tys = ctx.tys;
1581 if method_decl.params.len() != args.len() {
1582 ctx.errors.push(
1583 CompileError::new(
1584 "bynk.types.method_arity",
1585 method.span,
1586 format!(
1587 "static method `{}.{}` expects {} argument(s), but {} were given",
1588 type_name.name,
1589 method.name,
1590 method_decl.params.len(),
1591 args.len()
1592 ),
1593 )
1594 .with_label(method_decl.name.ident().span, "method declared here"),
1595 );
1596 for a in args {
1597 let _ = type_of(a, None, ctx);
1598 }
1599 return None;
1600 }
1601 let mut ok = true;
1602 for (i, (param, arg)) in method_decl.params.iter().zip(args.iter()).enumerate() {
1603 record_param_hint(ctx.hints, ¶m.name.name, arg);
1604 let expected = resolve_type_ref(¶m.type_ref, &ctx.input.types, tys);
1605 let actual = type_of(arg, expected, ctx);
1606 let (Some(actual), Some(expected)) = (actual, expected) else {
1607 ok = false;
1608 continue;
1609 };
1610 if !compatible(actual, expected, tys) {
1611 ctx.errors.push(CompileError::new(
1612 "bynk.types.argument_mismatch",
1613 arg.span,
1614 format!(
1615 "argument {} to `{}.{}` has type `{}`, but parameter `{}` expects `{}`",
1616 i + 1,
1617 type_name.name,
1618 method.name,
1619 actual.display(tys),
1620 param.name.name,
1621 expected.display(tys)
1622 ),
1623 ));
1624 ok = false;
1625 }
1626 }
1627 if !ok {
1628 return None;
1629 }
1630 resolve_type_ref(&method_decl.return_type, &ctx.input.types, tys)
1631}
1632
1633pub(crate) fn check_store_map_op(
1641 method: &Ident,
1642 args: &[Expr],
1643 k: TyId,
1644 v: TyId,
1645 span: Span,
1646 ctx: &mut Ctx,
1647) -> Option<TyId> {
1648 let tys = ctx.tys;
1649 let vfn = || Ty::Fn {
1650 params: vec![v],
1651 ret: v,
1652 };
1653 if v.is_held(tys) && matches!(method.name.as_str(), "update" | "upsert") {
1660 ctx.errors.push(
1661 CompileError::new(
1662 "bynk.held.unsupported_map_op",
1663 method.span,
1664 format!(
1665 "a held `Map[K, Connection]` has no `{}` operation — a held resource cannot be transformed by a `(Connection) -> Connection` function",
1666 method.name
1667 ),
1668 )
1669 .with_note(
1670 "held connections are stored and resolved by identity; use `put`/`get`/`remove`",
1671 ),
1672 );
1673 for a in args {
1674 type_of(a, None, ctx);
1675 }
1676 return None;
1677 }
1678 let (expected, result): (Vec<TyId>, TyId) = match method.name.as_str() {
1679 "put" => (vec![k, v], tys.intern(Ty::Unit)),
1680 "get" => (vec![k], tys.intern(Ty::Option(v))),
1681 "remove" => (vec![k], tys.intern(Ty::Unit)),
1682 "contains" => (vec![k], tys.intern(Ty::Base(BaseType::Bool))),
1683 "size" => (vec![], tys.intern(Ty::Base(BaseType::Int))),
1684 "update" => (vec![k, tys.intern(vfn())], tys.intern(Ty::Unit)),
1685 "upsert" => (vec![k, v, tys.intern(vfn())], tys.intern(Ty::Unit)),
1686 other => {
1687 ctx.errors.push(
1688 CompileError::new(
1689 "bynk.store.unknown_op",
1690 method.span,
1691 format!(
1692 "a `Map` store field has no operation `{other}` — expected `put`, `get`, \
1693 `update`, `upsert`, `remove`, `contains`, or `size`"
1694 ),
1695 )
1696 .with_note("storage-map ops are entry-level and effectful (await with `<-`)"),
1697 );
1698 for a in args {
1699 type_of(a, None, ctx);
1700 }
1701 return None;
1702 }
1703 };
1704 let effect = Ty::Effect(result);
1705 if args.len() != expected.len() {
1706 ctx.errors.push(CompileError::new(
1707 "bynk.types.call_arity",
1708 span,
1709 format!(
1710 "`Map.{}` takes {} argument(s), found {}",
1711 method.name,
1712 expected.len(),
1713 args.len()
1714 ),
1715 ));
1716 for a in args {
1717 type_of(a, None, ctx);
1718 }
1719 return Some(tys.intern(effect));
1720 }
1721 for (a, exp) in args.iter().zip(expected.iter()) {
1722 if let Some(at) = type_of(a, Some(*exp), ctx)
1723 && !compatible(at, *exp, tys)
1724 {
1725 ctx.errors.push(CompileError::new(
1726 "bynk.types.argument_mismatch",
1727 a.span,
1728 format!(
1729 "expected `{}`, found `{}`",
1730 exp.display(tys),
1731 at.display(tys)
1732 ),
1733 ));
1734 }
1735 }
1736 Some(tys.intern(effect))
1737}
1738
1739fn require_capability(
1747 site: Span,
1748 capability: &str,
1749 source: RequirementSource,
1750 ctx: &mut Ctx,
1751 code: &'static str,
1752 message: &str,
1753) {
1754 let covered = ctx.caps.capabilities.contains_key(capability);
1755 if covered {
1756 ctx.caps.given_used.insert(capability.to_string());
1757 } else {
1758 ctx.errors
1759 .push(CompileError::new(code, site, message).with_note(format!(
1760 "add `{capability}` to the handler's `given` clause"
1761 )));
1762 }
1763 record_requirement(ctx, capability, site, source, covered);
1764}
1765
1766fn record_requirement(
1771 ctx: &mut Ctx,
1772 capability: &str,
1773 site: Span,
1774 source: RequirementSource,
1775 covered: bool,
1776) {
1777 let materialize = if covered {
1778 None
1779 } else {
1780 given_insertion_edit(&ctx.caps.given_entries, ctx.caps.given_anchor, capability).map(
1781 |(edit_span, edit_text)| Materialize {
1782 anchor: ctx.caps.given_anchor.unwrap_or(ctx.return_ty_span),
1783 edit_span,
1784 edit_text,
1785 },
1786 )
1787 };
1788 ctx.requirements.record(Requirement {
1789 capability: capability.to_string(),
1790 site,
1791 source,
1792 covered,
1793 materialize,
1794 });
1795}
1796
1797pub(crate) fn check_store_cache_op(
1802 method: &Ident,
1803 args: &[Expr],
1804 k: TyId,
1805 v: TyId,
1806 span: Span,
1807 ctx: &mut Ctx,
1808) -> Option<TyId> {
1809 let tys = ctx.tys;
1810 let vfn = || Ty::Fn {
1811 params: vec![v],
1812 ret: v,
1813 };
1814 let (expected, result): (Vec<TyId>, TyId) = match method.name.as_str() {
1815 "put" => (vec![k, v], tys.intern(Ty::Unit)),
1816 "get" => (vec![k], tys.intern(Ty::Option(v))),
1817 "remove" => (vec![k], tys.intern(Ty::Unit)),
1818 "contains" => (vec![k], tys.intern(Ty::Base(BaseType::Bool))),
1819 "size" => (vec![], tys.intern(Ty::Base(BaseType::Int))),
1820 "update" => (vec![k, tys.intern(vfn())], tys.intern(Ty::Unit)),
1821 "upsert" => (vec![k, v, tys.intern(vfn())], tys.intern(Ty::Unit)),
1822 other => {
1823 ctx.errors.push(
1824 CompileError::new(
1825 "bynk.store.unknown_op",
1826 method.span,
1827 format!(
1828 "a `Cache` store field has no operation `{other}` — expected `put`, \
1829 `get`, `update`, `upsert`, `remove`, `contains`, or `size`"
1830 ),
1831 )
1832 .with_note("storage-cache ops are entry-level and effectful (await with `<-`)"),
1833 );
1834 for a in args {
1835 type_of(a, None, ctx);
1836 }
1837 return None;
1838 }
1839 };
1840 if method.name != "remove" {
1842 require_capability(
1843 method.span,
1844 "Clock",
1845 RequirementSource::StoreOp {
1846 kind: StoreKind::Cache,
1847 op: method.name.clone(),
1848 },
1849 ctx,
1850 "bynk.store.cache_needs_clock",
1851 "a `Cache` operation applies TTL expiry, which reads the clock — the handler must declare `given Clock`",
1852 );
1853 }
1854 let effect = Ty::Effect(result);
1855 if args.len() != expected.len() {
1856 ctx.errors.push(CompileError::new(
1857 "bynk.types.call_arity",
1858 span,
1859 format!(
1860 "`Cache.{}` takes {} argument(s), found {}",
1861 method.name,
1862 expected.len(),
1863 args.len()
1864 ),
1865 ));
1866 for a in args {
1867 type_of(a, None, ctx);
1868 }
1869 return Some(tys.intern(effect));
1870 }
1871 for (a, exp) in args.iter().zip(expected.iter()) {
1872 if let Some(at) = type_of(a, Some(*exp), ctx)
1873 && !compatible(at, *exp, tys)
1874 {
1875 ctx.errors.push(CompileError::new(
1876 "bynk.types.argument_mismatch",
1877 a.span,
1878 format!(
1879 "expected `{}`, found `{}`",
1880 exp.display(tys),
1881 at.display(tys)
1882 ),
1883 ));
1884 }
1885 }
1886 Some(tys.intern(effect))
1887}
1888
1889pub(crate) fn check_store_log_op(
1897 method: &Ident,
1898 args: &[Expr],
1899 elem: TyId,
1900 span: Span,
1901 ctx: &mut Ctx,
1902) -> Option<TyId> {
1903 let tys = ctx.tys;
1904 let query = || Ty::Query(elem);
1905 let arity = |n: usize, ctx: &mut Ctx| {
1906 if args.len() != n {
1907 ctx.errors.push(CompileError::new(
1908 "bynk.types.call_arity",
1909 span,
1910 format!(
1911 "`Log.{}` takes {n} argument(s), found {}",
1912 method.name,
1913 args.len()
1914 ),
1915 ));
1916 for a in args {
1917 type_of(a, None, ctx);
1918 }
1919 return false;
1920 }
1921 true
1922 };
1923 let window_arg = |a: &Expr, what: &str, ctx: &mut Ctx| {
1924 if let Some(at) = type_of(a, Some(tys.intern(Ty::Base(BaseType::Instant))), ctx)
1925 && !compatible(at, tys.intern(Ty::Base(BaseType::Instant)), tys)
1926 {
1927 ctx.errors.push(CompileError::new(
1928 "bynk.types.argument_mismatch",
1929 a.span,
1930 format!("{what} expects `Instant`, found `{}`", at.display(tys)),
1931 ));
1932 }
1933 };
1934 match method.name.as_str() {
1935 "append" => {
1937 require_capability(
1938 method.span,
1939 "Clock",
1940 RequirementSource::StoreOp {
1941 kind: StoreKind::Log,
1942 op: method.name.clone(),
1943 },
1944 ctx,
1945 "bynk.store.log_needs_clock",
1946 "`Log.append` stamps the current time, which reads the clock — the handler must declare `given Clock`",
1947 );
1948 if !arity(1, ctx) {
1949 return Some(tys.intern(Ty::Effect(tys.intern(Ty::Unit))));
1950 }
1951 if let Some(at) = type_of(&args[0], Some(elem), ctx)
1952 && !compatible(at, elem, tys)
1953 {
1954 ctx.errors.push(CompileError::new(
1955 "bynk.types.argument_mismatch",
1956 args[0].span,
1957 format!(
1958 "expected `{}`, found `{}`",
1959 elem.display(tys),
1960 at.display(tys)
1961 ),
1962 ));
1963 }
1964 Some(tys.intern(Ty::Effect(tys.intern(Ty::Unit))))
1965 }
1966 "since" | "before" => {
1968 if !arity(1, ctx) {
1969 return Some(tys.intern(query()));
1970 }
1971 window_arg(&args[0], &format!("`Log.{}`", method.name), ctx);
1972 Some(tys.intern(query()))
1973 }
1974 "between" => {
1975 if !arity(2, ctx) {
1976 return Some(tys.intern(query()));
1977 }
1978 window_arg(&args[0], "`Log.between` start", ctx);
1979 window_arg(&args[1], "`Log.between` end", ctx);
1980 Some(tys.intern(query()))
1981 }
1982 "recent" => {
1983 if !arity(1, ctx) {
1984 return Some(tys.intern(query()));
1985 }
1986 check_arg(
1987 &args[0],
1988 tys.intern(Ty::Base(BaseType::Int)),
1989 "the `Log.recent` count",
1990 ctx,
1991 );
1992 Some(tys.intern(query()))
1993 }
1994 "reversed" => {
1995 if !arity(0, ctx) {
1996 return Some(tys.intern(query()));
1997 }
1998 Some(tys.intern(query()))
1999 }
2000 name if is_query_op(name) => check_query_kernel_method(method, args, elem, span, ctx),
2002 other => {
2003 ctx.errors.push(
2004 CompileError::new(
2005 "bynk.store.unknown_op",
2006 method.span,
2007 format!(
2008 "a `Log` store field has no operation `{other}` — `append`, the \
2009 time-window roots (`since`/`before`/`between`/`recent`/`reversed`), \
2010 and the query builders/terminals"
2011 ),
2012 )
2013 .with_note(
2014 "`Log` reads are lazy `Query[T]`; only `append` is effectful and writes",
2015 ),
2016 );
2017 for a in args {
2018 type_of(a, None, ctx);
2019 }
2020 None
2021 }
2022 }
2023}
2024
2025pub(crate) fn check_store_set_op(
2031 method: &Ident,
2032 args: &[Expr],
2033 t: TyId,
2034 span: Span,
2035 ctx: &mut Ctx,
2036) -> Option<TyId> {
2037 let tys = ctx.tys;
2038 let (expected, result): (Vec<TyId>, TyId) = match method.name.as_str() {
2039 "add" => (vec![t], tys.intern(Ty::Unit)),
2040 "remove" => (vec![t], tys.intern(Ty::Unit)),
2041 "contains" => (vec![t], tys.intern(Ty::Base(BaseType::Bool))),
2042 "size" => (vec![], tys.intern(Ty::Base(BaseType::Int))),
2043 other => {
2044 ctx.errors.push(
2045 CompileError::new(
2046 "bynk.store.unknown_op",
2047 method.span,
2048 format!(
2049 "a `Set` store field has no operation `{other}` — expected `add`, \
2050 `remove`, `contains`, or `size`"
2051 ),
2052 )
2053 .with_note(
2054 "set algebra (`union`/`intersection`/`difference`) is not in this slice",
2055 ),
2056 );
2057 for a in args {
2058 type_of(a, None, ctx);
2059 }
2060 return None;
2061 }
2062 };
2063 let effect = Ty::Effect(result);
2064 if args.len() != expected.len() {
2065 ctx.errors.push(CompileError::new(
2066 "bynk.types.call_arity",
2067 span,
2068 format!(
2069 "`Set.{}` takes {} argument(s), found {}",
2070 method.name,
2071 expected.len(),
2072 args.len()
2073 ),
2074 ));
2075 for a in args {
2076 type_of(a, None, ctx);
2077 }
2078 return Some(tys.intern(effect));
2079 }
2080 for (a, exp) in args.iter().zip(expected.iter()) {
2081 if let Some(at) = type_of(a, Some(*exp), ctx)
2082 && !compatible(at, *exp, tys)
2083 {
2084 ctx.errors.push(CompileError::new(
2085 "bynk.types.argument_mismatch",
2086 a.span,
2087 format!(
2088 "expected `{}`, found `{}`",
2089 exp.display(tys),
2090 at.display(tys)
2091 ),
2092 ));
2093 }
2094 }
2095 Some(tys.intern(effect))
2096}
2097
2098pub(crate) fn check_store_cell_op(
2106 method: &Ident,
2107 args: &[Expr],
2108 t: TyId,
2109 span: Span,
2110 ctx: &mut Ctx,
2111) -> Option<TyId> {
2112 let tys = ctx.tys;
2113 let tfn = || Ty::Fn {
2114 params: vec![t],
2115 ret: t,
2116 };
2117 let (expected, result): (Vec<TyId>, TyId) = match method.name.as_str() {
2118 "update" => (vec![tys.intern(tfn())], tys.intern(Ty::Unit)),
2119 other => {
2120 ctx.errors.push(
2121 CompileError::new(
2122 "bynk.store.unknown_op",
2123 method.span,
2124 format!("a `Cell` store field has no operation `{other}` — expected `update`"),
2125 )
2126 .with_note(
2127 "a cell is read by its bare name and written with `:=`; `update` is the only \
2128 method-shaped op",
2129 ),
2130 );
2131 for a in args {
2132 type_of(a, None, ctx);
2133 }
2134 return None;
2135 }
2136 };
2137 let effect = Ty::Effect(result);
2138 if args.len() != expected.len() {
2139 ctx.errors.push(CompileError::new(
2140 "bynk.types.call_arity",
2141 span,
2142 format!(
2143 "`Cell.{}` takes {} argument(s), found {}",
2144 method.name,
2145 expected.len(),
2146 args.len()
2147 ),
2148 ));
2149 for a in args {
2150 type_of(a, None, ctx);
2151 }
2152 return Some(tys.intern(effect));
2153 }
2154 for (a, exp) in args.iter().zip(expected.iter()) {
2155 if let Some(at) = type_of(a, Some(*exp), ctx)
2156 && !compatible(at, *exp, tys)
2157 {
2158 ctx.errors.push(CompileError::new(
2159 "bynk.types.argument_mismatch",
2160 a.span,
2161 format!(
2162 "expected `{}`, found `{}`",
2163 exp.display(tys),
2164 at.display(tys)
2165 ),
2166 ));
2167 }
2168 }
2169 Some(tys.intern(effect))
2170}
2171
2172#[allow(clippy::too_many_arguments)]
2173pub(crate) fn check_method_call(
2174 receiver: &Expr,
2175 method: &Ident,
2176 type_args: &[TypeRef],
2177 args: &[Expr],
2178 span: Span,
2179 expected: Option<TyId>,
2180 expr_id: ExprId,
2182 ctx: &mut Ctx,
2183) -> Option<TyId> {
2184 let tys = ctx.tys;
2185 let receiver_is_capability = matches!(&receiver.kind, ExprKind::Ident(id)
2200 if ctx.caps.capabilities.contains_key(&id.name)
2201 || ctx.caps.declared_capabilities.contains_key(&id.name))
2202 || flatten_ident_chain(receiver).is_some_and(|chain| {
2203 ctx.input
2204 .cross_context
2205 .resolve_cross_capability(&chain)
2206 .is_some()
2207 });
2208 if !type_args.is_empty()
2209 && !matches!(&receiver.kind, ExprKind::Ident(id) if id.name == JSON
2210 && !ctx.input.types.contains_key(JSON))
2211 && !receiver_is_capability
2212 {
2213 ctx.errors.push(CompileError::new(
2214 "bynk.generics.type_arg_mismatch",
2215 span,
2216 format!(
2217 "`{}` does not take explicit type arguments — a generic method infers them from the receiver and arguments",
2218 method.name
2219 ),
2220 ));
2221 for a in args {
2222 let _ = type_of(a, None, ctx);
2223 }
2224 return None;
2225 }
2226 if let ExprKind::Ident(id) = &receiver.kind
2237 && ctx.lookup(id.name.as_str()).is_none()
2238 && let Some(sig) = ctx.test_services.get(&id.name).cloned()
2239 {
2240 if let Some(unit) = ctx.input.cross_context.self_context.clone() {
2241 ctx.refs
2242 .record_in_unit(id.span, SymbolKind::Service, &id.name, &unit);
2243 }
2244 return check_test_service_address(&sig, id, method, args, expr_id, ctx);
2245 }
2246 if ctx.lookup_root_ident(receiver).is_none() && !ctx.root_ident_is_store_field(receiver) {
2253 if let Some(chain) = flatten_ident_chain(receiver)
2257 && let Some((consumed, cap)) = ctx.input.cross_context.resolve_cross_capability(&chain)
2258 {
2259 if let ExprKind::FieldAccess { field, .. } = &receiver.kind {
2262 ctx.refs
2263 .record_in_unit(field.span, SymbolKind::Capability, &cap, &consumed);
2264 }
2265 return check_cross_context_capability_call(
2266 receiver, &consumed, &cap, method, type_args, args, span, expr_id, ctx,
2267 );
2268 }
2269 if let Some(consumed) = cross_context_prefix(receiver, ctx) {
2270 return check_cross_context_call(receiver, &consumed, method, args, span, expr_id, ctx);
2271 }
2272 if let ExprKind::FieldAccess { .. } = &receiver.kind
2284 && !ctx.root_ident_is_asset(receiver)
2285 && let Some(chain) = flatten_ident_chain(receiver)
2286 && chain.contains('.')
2287 {
2288 let info = &ctx.input.cross_context;
2289 let in_context = info.self_context.is_some();
2290 if in_context && info.resolve_prefix(&chain).is_none() {
2291 ctx.errors.push(
2292 CompileError::new(
2293 "bynk.resolve.unconsumed_context",
2294 receiver.span,
2295 format!(
2296 "`{chain}.{}` looks like a cross-context service call, but `{chain}` is not in this context's `consumes` clauses",
2297 method.name
2298 ),
2299 )
2300 .with_note(
2301 "add a `consumes {chain}` clause at the top of the context, or use an alias and call it through the alias",
2302 ),
2303 );
2304 for a in args {
2305 let _ = type_of(a, None, ctx);
2306 }
2307 return None;
2308 }
2309 }
2310 }
2311 if let ExprKind::Ident(id) = &receiver.kind
2315 && ctx.lookup(id.name.as_str()).is_none()
2316 && (ctx.caps.capabilities.contains_key(&id.name)
2317 || ctx.caps.declared_capabilities.contains_key(&id.name))
2318 {
2319 return check_static_call(id, method, type_args, args, span, expected, expr_id, ctx);
2320 }
2321 if let ExprKind::Ident(id) = &receiver.kind
2327 && ctx.lookup(id.name.as_str()).is_none()
2328 && ctx.input.types.contains_key(&id.name)
2329 {
2330 return check_static_call(id, method, type_args, args, span, expected, expr_id, ctx);
2331 }
2332 if let ExprKind::Ident(id) = &receiver.kind
2336 && ctx.lookup(id.name.as_str()).is_none()
2337 && !ctx.input.types.contains_key(&id.name)
2338 && (id.name == LIST || id.name == MAP)
2339 {
2340 let ns = if id.name == LIST { LIST } else { MAP };
2341 ctx.callees.insert(
2342 expr_id,
2343 Callee::Intrinsic {
2344 ns,
2345 op: method.name.clone(),
2346 },
2347 );
2348 return check_collection_static(id, method, args, span, expected, ctx);
2349 }
2350 if let ExprKind::Ident(id) = &receiver.kind
2354 && (id.name == INT || id.name == FLOAT)
2355 {
2356 let ns = if id.name == INT { INT } else { FLOAT };
2357 ctx.callees.insert(
2358 expr_id,
2359 Callee::Intrinsic {
2360 ns,
2361 op: method.name.clone(),
2362 },
2363 );
2364 return check_numeric_parse_static(id, method, args, span, ctx);
2365 }
2366 if let ExprKind::Ident(id) = &receiver.kind
2369 && id.name == DURATION
2370 && ctx.lookup(DURATION).is_none()
2371 && !ctx.input.types.contains_key(DURATION)
2372 {
2373 ctx.callees.insert(
2374 expr_id,
2375 Callee::Intrinsic {
2376 ns: DURATION,
2377 op: method.name.clone(),
2378 },
2379 );
2380 return check_duration_static(method, args, span, ctx);
2381 }
2382 if let ExprKind::Ident(id) = &receiver.kind
2384 && id.name == INSTANT
2385 && ctx.lookup(INSTANT).is_none()
2386 && !ctx.input.types.contains_key(INSTANT)
2387 {
2388 ctx.callees.insert(
2389 expr_id,
2390 Callee::Intrinsic {
2391 ns: INSTANT,
2392 op: method.name.clone(),
2393 },
2394 );
2395 return check_instant_static(method, args, span, ctx);
2396 }
2397 if let ExprKind::Ident(id) = &receiver.kind
2400 && id.name == BYTES
2401 && ctx.lookup(BYTES).is_none()
2402 && !ctx.input.types.contains_key(BYTES)
2403 {
2404 ctx.callees.insert(
2405 expr_id,
2406 Callee::Intrinsic {
2407 ns: BYTES,
2408 op: method.name.clone(),
2409 },
2410 );
2411 return check_bytes_static(method, args, span, ctx);
2412 }
2413 if let ExprKind::Ident(id) = &receiver.kind
2416 && id.name == TOKEN_CLAIMS
2417 && ctx.lookup(TOKEN_CLAIMS).is_none()
2418 && !ctx.input.types.contains_key(TOKEN_CLAIMS)
2419 {
2420 ctx.callees.insert(
2421 expr_id,
2422 Callee::Intrinsic {
2423 ns: TOKEN_CLAIMS,
2424 op: method.name.clone(),
2425 },
2426 );
2427 return check_token_claims_static(method, args, span, ctx);
2428 }
2429 if let ExprKind::Ident(id) = &receiver.kind
2432 && id.name == REQUEST_HEADERS
2433 && ctx.lookup(REQUEST_HEADERS).is_none()
2434 && !ctx.input.types.contains_key(REQUEST_HEADERS)
2435 {
2436 ctx.callees.insert(
2437 expr_id,
2438 Callee::Intrinsic {
2439 ns: REQUEST_HEADERS,
2440 op: method.name.clone(),
2441 },
2442 );
2443 return check_request_headers_static(method, args, span, ctx);
2444 }
2445 if let ExprKind::Ident(id) = &receiver.kind
2447 && id.name == JSON
2448 && ctx.lookup(JSON).is_none()
2449 && !ctx.input.types.contains_key(JSON)
2450 {
2451 ctx.callees.insert(
2452 expr_id,
2453 Callee::Intrinsic {
2454 ns: JSON,
2455 op: method.name.clone(),
2456 },
2457 );
2458 return check_json_static(method, type_args, args, span, expected, ctx);
2459 }
2460 if let ExprKind::Ident(id) = &receiver.kind
2462 && id.name == STREAM
2463 && ctx.lookup(STREAM).is_none()
2464 && !ctx.input.types.contains_key(STREAM)
2465 {
2466 ctx.callees.insert(
2467 expr_id,
2468 Callee::Intrinsic {
2469 ns: STREAM,
2470 op: method.name.clone(),
2471 },
2472 );
2473 return check_stream_static(method, args, span, ctx);
2474 }
2475 let recv_expected = match (expected, method.name.as_str()) {
2480 (Some(t), "insert") => peel_to_map(t, tys).map(|(k, v)| tys.intern(Ty::Map(k, v))),
2481 (Some(t), "prepend" | "append" | "concat") => {
2482 peel_to_list(t, tys).map(|e| tys.intern(Ty::List(e)))
2483 }
2484 _ => None,
2485 };
2486 let recv_ty = type_of(receiver, recv_expected, ctx)?;
2487 match &*tys.get(recv_ty) {
2492 Ty::List(elem) => {
2493 ctx.callees.insert(
2494 expr_id,
2495 Callee::Kernel {
2496 recv: recv_ty,
2497 op: method.name.clone(),
2498 },
2499 );
2500 return check_list_kernel_method(method, args, *elem, span, ctx);
2501 }
2502 Ty::Query(elem) => {
2504 ctx.callees.insert(
2505 expr_id,
2506 Callee::Kernel {
2507 recv: recv_ty,
2508 op: method.name.clone(),
2509 },
2510 );
2511 return check_query_kernel_method(method, args, *elem, span, ctx);
2512 }
2513 Ty::Stream(elem) => {
2515 ctx.callees.insert(
2516 expr_id,
2517 Callee::Kernel {
2518 recv: recv_ty,
2519 op: method.name.clone(),
2520 },
2521 );
2522 return check_stream_kernel_method(method, args, *elem, span, ctx);
2523 }
2524 Ty::Connection(frame) => {
2528 ctx.callees.insert(
2529 expr_id,
2530 Callee::Kernel {
2531 recv: recv_ty,
2532 op: method.name.clone(),
2533 },
2534 );
2535 return check_connection_method(method, args, *frame, span, ctx);
2536 }
2537 Ty::Map(key, val) => {
2538 ctx.callees.insert(
2539 expr_id,
2540 Callee::Kernel {
2541 recv: recv_ty,
2542 op: method.name.clone(),
2543 },
2544 );
2545 return check_map_kernel_method(method, args, *key, *val, span, ctx);
2546 }
2547 Ty::Base(base @ (BaseType::Int | BaseType::Float)) => {
2555 ctx.callees.insert(
2556 expr_id,
2557 Callee::Kernel {
2558 recv: recv_ty,
2559 op: method.name.clone(),
2560 },
2561 );
2562 return check_numeric_kernel_method(method, args, *base, span, ctx);
2563 }
2564 Ty::Base(BaseType::Duration) => {
2566 ctx.callees.insert(
2567 expr_id,
2568 Callee::Kernel {
2569 recv: recv_ty,
2570 op: method.name.clone(),
2571 },
2572 );
2573 return check_duration_kernel_method(method, args, span, ctx);
2574 }
2575 Ty::Base(BaseType::Instant) => {
2577 ctx.callees.insert(
2578 expr_id,
2579 Callee::Kernel {
2580 recv: recv_ty,
2581 op: method.name.clone(),
2582 },
2583 );
2584 return check_instant_kernel_method(method, args, span, ctx);
2585 }
2586 Ty::RequestHeaders => {
2589 ctx.callees.insert(
2590 expr_id,
2591 Callee::Kernel {
2592 recv: recv_ty,
2593 op: method.name.clone(),
2594 },
2595 );
2596 return check_request_headers_method(method, args, span, ctx);
2597 }
2598 Ty::TokenClaims => {
2600 ctx.callees.insert(
2601 expr_id,
2602 Callee::Kernel {
2603 recv: recv_ty,
2604 op: method.name.clone(),
2605 },
2606 );
2607 return check_token_claims_method(method, args, span, ctx);
2608 }
2609 Ty::Base(BaseType::Bytes) => {
2611 ctx.callees.insert(
2612 expr_id,
2613 Callee::Kernel {
2614 recv: recv_ty,
2615 op: method.name.clone(),
2616 },
2617 );
2618 return check_bytes_kernel_method(method, args, span, ctx);
2619 }
2620 Ty::Base(BaseType::String) => {
2622 ctx.callees.insert(
2623 expr_id,
2624 Callee::Kernel {
2625 recv: recv_ty,
2626 op: method.name.clone(),
2627 },
2628 );
2629 return check_string_kernel_method(method, args, span, ctx);
2630 }
2631 Ty::Option(inner) => {
2633 ctx.callees.insert(
2634 expr_id,
2635 Callee::Kernel {
2636 recv: recv_ty,
2637 op: method.name.clone(),
2638 },
2639 );
2640 return check_option_kernel_method(method, args, *inner, span, ctx);
2641 }
2642 Ty::Result(ok, err) => {
2643 ctx.callees.insert(
2644 expr_id,
2645 Callee::Kernel {
2646 recv: recv_ty,
2647 op: method.name.clone(),
2648 },
2649 );
2650 return check_result_kernel_method(method, args, *ok, *err, span, ctx);
2651 }
2652 Ty::Effect(inner) => {
2657 if let Ty::Result(ok, err) = &*tys.get(*inner) {
2658 ctx.callees.insert(
2659 expr_id,
2660 Callee::Kernel {
2661 recv: recv_ty,
2662 op: method.name.clone(),
2663 },
2664 );
2665 return check_effect_result_kernel_method(method, args, *ok, *err, span, ctx);
2666 }
2667 }
2668 _ => {}
2669 }
2670 let type_name = match &*tys.get(recv_ty) {
2672 Ty::Named { name, .. } => name.clone(),
2673 _ => {
2674 ctx.errors.push(CompileError::new(
2675 "bynk.types.method_on_non_named_type",
2676 method.span,
2677 format!(
2678 "type `{}` has no methods — only user-declared types support method calls",
2679 recv_ty.display(tys)
2680 ),
2681 ));
2682 return None;
2683 }
2684 };
2685 if let Some(agent) = ctx.input.agents.get(&type_name).cloned() {
2689 let Some(handler) = agent.handlers.iter().find(|h| {
2690 h.method_name
2691 .as_ref()
2692 .is_some_and(|n| n.name == method.name)
2693 }) else {
2694 ctx.errors.push(CompileError::new(
2695 "bynk.agent.handler_not_found",
2696 method.span,
2697 format!(
2698 "agent `{}` has no handler named `{}`",
2699 type_name, method.name
2700 ),
2701 ));
2702 for a in args {
2703 let _ = type_of(a, None, ctx);
2704 }
2705 return None;
2706 };
2707 ctx.callees.insert(
2708 expr_id,
2709 Callee::Agent {
2710 agent: type_name.clone(),
2711 handler: method.name.clone(),
2712 },
2713 );
2714 ctx.refs.record(
2719 method.span,
2720 SymbolKind::Handler,
2721 &format!("{type_name}.{}", method.name),
2722 );
2723 if handler.params.len() != args.len() {
2724 ctx.errors.push(CompileError::new(
2725 "bynk.agent.handler_arity",
2726 method.span,
2727 format!(
2728 "agent handler `{}.{}` expects {} argument(s), but {} were given",
2729 type_name,
2730 method.name,
2731 handler.params.len(),
2732 args.len()
2733 ),
2734 ));
2735 for a in args {
2736 let _ = type_of(a, None, ctx);
2737 }
2738 return None;
2739 }
2740 for (p, arg) in handler.params.iter().zip(args.iter()) {
2741 let pty = resolve_type_ref(&p.type_ref, &ctx.input.types, tys);
2742 let arg_ty = type_of(arg, pty, ctx);
2743 if let (Some(a), Some(p_ty)) = (arg_ty, pty.as_ref())
2744 && !compatible(a, *p_ty, tys)
2745 {
2746 ctx.errors.push(CompileError::new(
2747 "bynk.types.argument_mismatch",
2748 arg.span,
2749 format!(
2750 "argument has type `{}`, but `{}.{}` expects `{}`",
2751 a.display(tys),
2752 type_name,
2753 method.name,
2754 p_ty.display(tys)
2755 ),
2756 ));
2757 }
2758 }
2759 return resolve_type_ref(&handler.return_type, &ctx.input.types, tys);
2760 }
2761 let table = ctx
2762 .input
2763 .methods
2764 .get(&type_name)
2765 .cloned()
2766 .unwrap_or_default();
2767 let Some(method_decl) = table.instance.get(&method.name).cloned() else {
2768 if let Ty::Named {
2778 kind: NamedKind::Refined(base),
2779 ..
2780 } = &*tys.get(recv_ty)
2781 {
2782 if !matches!(base, BaseType::Bool) {
2783 ctx.callees.insert(
2784 expr_id,
2785 Callee::Kernel {
2786 recv: recv_ty,
2787 op: method.name.clone(),
2788 },
2789 );
2790 }
2791 match base {
2792 BaseType::Int | BaseType::Float => {
2793 return check_numeric_kernel_method(method, args, *base, span, ctx);
2794 }
2795 BaseType::String => return check_string_kernel_method(method, args, span, ctx),
2796 BaseType::Duration => return check_duration_kernel_method(method, args, span, ctx),
2797 BaseType::Instant => return check_instant_kernel_method(method, args, span, ctx),
2798 BaseType::Bytes => return check_bytes_kernel_method(method, args, span, ctx),
2799 BaseType::Bool => {}
2800 }
2801 }
2802 ctx.errors.push(CompileError::new(
2803 "bynk.types.method_not_found",
2804 method.span,
2805 format!(
2806 "type `{}` has no instance method named `{}`",
2807 type_name, method.name
2808 ),
2809 ));
2810 return None;
2811 };
2812 ctx.refs.record(
2817 method.span,
2818 SymbolKind::Method,
2819 &format!("{type_name}.{}", method.name),
2820 );
2821 ctx.callees
2822 .insert(expr_id, Callee::Method(Arc::clone(&method_decl)));
2823 let recv_type_params: Vec<String> = ctx
2831 .input
2832 .types
2833 .get(&type_name)
2834 .map(|d| d.type_params.iter().map(|p| p.name.name.clone()).collect())
2835 .unwrap_or_default();
2836 if !recv_type_params.is_empty() || !method_decl.type_params.is_empty() {
2837 return check_generic_method_call(
2838 &type_name,
2839 &recv_type_params,
2840 recv_ty,
2841 &method_decl,
2842 method,
2843 args,
2844 ctx,
2845 );
2846 }
2847 if method_decl.params.len() != args.len() {
2849 ctx.errors.push(
2850 CompileError::new(
2851 "bynk.types.method_arity",
2852 method.span,
2853 format!(
2854 "method `{}.{}` expects {} argument(s), but {} were given",
2855 type_name,
2856 method.name,
2857 method_decl.params.len(),
2858 args.len()
2859 ),
2860 )
2861 .with_label(method_decl.name.ident().span, "method declared here"),
2862 );
2863 for a in args {
2864 let _ = type_of(a, None, ctx);
2865 }
2866 return None;
2867 }
2868 let mut ok = true;
2869 for (i, (param, arg)) in method_decl.params.iter().zip(args.iter()).enumerate() {
2870 record_param_hint(ctx.hints, ¶m.name.name, arg);
2871 let expected = resolve_type_ref(¶m.type_ref, &ctx.input.types, tys);
2872 let actual = type_of(arg, expected, ctx);
2873 let (Some(actual), Some(expected)) = (actual, expected) else {
2874 ok = false;
2875 continue;
2876 };
2877 if !compatible(actual, expected, tys) {
2878 ctx.errors.push(CompileError::new(
2879 "bynk.types.argument_mismatch",
2880 arg.span,
2881 format!(
2882 "argument {} to `{}.{}` has type `{}`, but parameter `{}` expects `{}`",
2883 i + 1,
2884 type_name,
2885 method.name,
2886 actual.display(tys),
2887 param.name.name,
2888 expected.display(tys)
2889 ),
2890 ));
2891 ok = false;
2892 }
2893 }
2894 let _ = span;
2895 if !ok {
2896 return None;
2897 }
2898 resolve_type_ref(&method_decl.return_type, &ctx.input.types, tys)
2899}
2900
2901fn check_generic_method_call(
2910 type_name: &str,
2911 recv_type_params: &[String],
2912 recv_ty: TyId,
2913 method_decl: &FnDecl,
2914 method: &Ident,
2915 args: &[Expr],
2916 ctx: &mut Ctx,
2917) -> Option<TyId> {
2918 let tys = ctx.tys;
2919 let mut vars: HashSet<String> = recv_type_params.iter().cloned().collect();
2921 for tp in &method_decl.type_params {
2922 vars.insert(tp.name.name.clone());
2923 }
2924 if method_decl.params.len() != args.len() {
2926 ctx.errors.push(
2927 CompileError::new(
2928 "bynk.types.method_arity",
2929 method.span,
2930 format!(
2931 "method `{}.{}` expects {} argument(s), but {} were given",
2932 type_name,
2933 method.name,
2934 method_decl.params.len(),
2935 args.len()
2936 ),
2937 )
2938 .with_label(method_decl.name.ident().span, "method declared here"),
2939 );
2940 for a in args {
2941 let _ = type_of(a, None, ctx);
2942 }
2943 return None;
2944 }
2945 let mut subst: HashMap<String, TyId> = HashMap::new();
2952 if let Ty::Named {
2953 args: recv_args, ..
2954 } = &*tys.get(recv_ty)
2955 && recv_args.len() == recv_type_params.len()
2956 {
2957 for (name, arg) in recv_type_params.iter().zip(recv_args.iter()) {
2958 subst.insert(name.clone(), *arg);
2959 }
2960 }
2961 let var_params: Vec<Option<TyId>> = method_decl
2964 .params
2965 .iter()
2966 .map(|p| resolve_type_ref_in(&p.type_ref, &ctx.input.types, &vars, tys))
2967 .collect();
2968 let ret_pattern = resolve_type_ref_in(&method_decl.return_type, &ctx.input.types, &vars, tys)?;
2969
2970 let mut arg_tys: Vec<Option<TyId>> = vec![None; args.len()];
2971 for (i, arg) in args.iter().enumerate() {
2973 if matches!(arg.kind, ExprKind::Lambda(_)) {
2974 continue;
2975 }
2976 let expected = var_params[i].map(|p| substitute(p, &subst, tys));
2977 let ty = type_of(arg, expected, ctx);
2978 if let (Some(pattern), Some(actual)) = (var_params[i], ty)
2979 && !unify(pattern, actual, &mut subst, tys)
2980 {
2981 ctx.errors.push(CompileError::new(
2982 "bynk.generics.type_arg_mismatch",
2983 arg.span,
2984 format!(
2985 "argument {} infers a type for `{}.{}`'s type parameter that conflicts with an earlier argument or the receiver",
2986 i + 1,
2987 type_name,
2988 method.name
2989 ),
2990 ));
2991 return None;
2992 }
2993 arg_tys[i] = ty;
2994 }
2995 for (i, arg) in args.iter().enumerate() {
2997 if !matches!(arg.kind, ExprKind::Lambda(_)) {
2998 continue;
2999 }
3000 let expected = var_params[i].map(|p| substitute(p, &subst, tys));
3001 let params_unconstrained = expected.is_some_and(|e| {
3002 matches!(&*tys.get(e), Ty::Fn { params, .. }
3003 if params.iter().any(|p| contains_var(*p, tys)))
3004 });
3005 let fully_annotated = matches!(
3006 &arg.kind,
3007 ExprKind::Lambda(l) if l.params.iter().all(|p| p.type_ref.is_some())
3008 );
3009 if params_unconstrained && !fully_annotated {
3010 ctx.errors.push(
3011 CompileError::new(
3012 "bynk.generics.uninferable_type_arg",
3013 arg.span,
3014 format!(
3015 "the lambda's parameter types depend on `{}.{}`'s type parameters, which the receiver and other arguments do not determine",
3016 type_name, method.name
3017 ),
3018 )
3019 .with_note("annotate the lambda's parameters"),
3020 );
3021 return None;
3022 }
3023 let ty = if params_unconstrained {
3024 type_of(arg, None, ctx)
3025 } else {
3026 type_of(arg, expected, ctx)
3027 };
3028 if let (Some(pattern), Some(actual)) = (var_params[i], ty)
3029 && !unify(pattern, actual, &mut subst, tys)
3030 {
3031 ctx.errors.push(CompileError::new(
3032 "bynk.generics.type_arg_mismatch",
3033 arg.span,
3034 format!(
3035 "the lambda's type conflicts with `{}.{}`'s inferred type arguments",
3036 type_name, method.name
3037 ),
3038 ));
3039 return None;
3040 }
3041 arg_tys[i] = ty;
3042 }
3043 for tp in &method_decl.type_params {
3046 if !subst.contains_key(&tp.name.name) {
3047 ctx.errors.push(
3048 CompileError::new(
3049 "bynk.generics.uninferable_type_arg",
3050 method.span,
3051 format!(
3052 "type parameter `{}` of `{}.{}` is not inferable from the receiver or the arguments",
3053 tp.name.name, type_name, method.name
3054 ),
3055 )
3056 .with_label(tp.span, "declared here"),
3057 );
3058 return None;
3059 }
3060 }
3061 let mut ok = true;
3063 for (i, (pattern, arg)) in var_params.iter().zip(args).enumerate() {
3064 record_param_hint(ctx.hints, &method_decl.params[i].name.name, arg);
3065 let (Some(pattern), Some(arg_ty)) = (pattern, arg_tys[i].as_ref()) else {
3066 continue;
3067 };
3068 let ground = substitute(*pattern, &subst, tys);
3069 if !compatible(*arg_ty, ground, tys) {
3070 ctx.errors.push(CompileError::new(
3071 "bynk.types.argument_mismatch",
3072 arg.span,
3073 format!(
3074 "argument {} to `{}.{}` has type `{}`, but `{}` is expected",
3075 i + 1,
3076 type_name,
3077 method.name,
3078 arg_ty.display(tys),
3079 ground.display(tys)
3080 ),
3081 ));
3082 ok = false;
3083 }
3084 }
3085 if !ok {
3086 return None;
3087 }
3088 let decl = ctx.input.methods.get(type_name).and_then(|t| {
3091 t.instance
3092 .get(&method.name)
3093 .or_else(|| t.statics.get(&method.name))
3094 .cloned()
3095 });
3096 let compared = match &decl {
3097 Some(decl) if std::ptr::eq(decl.as_ref(), method_decl) => {
3098 super::equality::compared_type_params(decl, ctx)
3099 }
3100 _ => Default::default(),
3101 };
3102 let mut params: Vec<(String, Option<Span>)> = method_decl
3103 .type_params
3104 .iter()
3105 .map(|tp| (tp.name.name.clone(), Some(tp.span)))
3106 .collect();
3107 params.extend(recv_type_params.iter().map(|p| (p.clone(), None)));
3108 let callee = format!("{type_name}.{}", method.name);
3109 if !super::equality::check_compared_args(&callee, method.span, ¶ms, &compared, &subst, ctx)
3110 {
3111 return None;
3112 }
3113 if !method_decl.type_params.is_empty() {
3117 let rendered: Option<Vec<String>> = method_decl
3118 .type_params
3119 .iter()
3120 .map(|tp| subst.get(&tp.name.name).map(|t| t.display(tys)))
3121 .collect();
3122 if let Some(parts) = rendered {
3123 ctx.hints
3124 .record(method.span, format!("[{}]", parts.join(", ")));
3125 }
3126 }
3127 Some(substitute(ret_pattern, &subst, tys))
3128}
3129
3130fn check_test_service_address(
3148 sig: &TestServiceSig,
3149 id: &Ident,
3150 method: &Ident,
3151 args: &[Expr],
3152 expr_id: ExprId,
3154 ctx: &mut Ctx,
3155) -> Option<TyId> {
3156 use bynk_syntax::ast::{ExprKind as EK, HandlerKind};
3157
3158 if method.name == "call" {
3160 ctx.callees.insert(
3161 expr_id,
3162 Callee::TestService {
3163 service: id.name.clone(),
3164 address: "call".to_string(),
3165 },
3166 );
3167 let Some(handler) = sig.call_handler() else {
3168 let message = match &sig.protocol {
3169 Some(protocol) => format!(
3170 "`{}` is a `from {protocol}` service and has no `on call` handler to invoke",
3171 id.name
3172 ),
3173 None => format!("service `{}` has no `on call` handler to invoke", id.name),
3174 };
3175 ctx.errors.push(
3176 CompileError::new("bynk.test.service_no_call_handler", method.span, message)
3177 .with_note(
3178 "call an `on call` service with `svc.call(...)`, an http route with `svc.GET(\"/path\")`, cron with `svc.schedule(\"…\")`, or a queue with `svc.message(m)`",
3179 ),
3180 );
3181 for a in args {
3182 let _ = type_of(a, None, ctx);
3183 }
3184 return None;
3185 };
3186 let params = handler.params.clone();
3187 check_address_args(&id.name, "call", ¶ms, args, method.span, ctx);
3188 return handler_outcome(handler, ctx);
3189 }
3190
3191 if bynk_syntax::ast::HttpMethod::from_ident(&method.name).is_some() {
3196 ctx.callees.insert(
3197 expr_id,
3198 Callee::TestService {
3199 service: id.name.clone(),
3200 address: method.name.clone(),
3201 },
3202 );
3203 if let Some(first) = args.first()
3208 && !matches!(first.kind, EK::StrLit(_))
3209 && is_asset_value(first, ctx)
3210 {
3211 let _ = type_of(first, None, ctx);
3212 if sig.protocol.as_deref() != Some("http") {
3215 ctx.errors.push(
3216 CompileError::new(
3217 "bynk.test.service_bad_address",
3218 method.span,
3219 format!(
3220 "`{}` is not a `from http` service, so it cannot fetch an asset",
3221 id.name
3222 ),
3223 )
3224 .with_note("fetch an asset through one of the target's `from http` services: `api.GET(asset)`"),
3225 );
3226 for a in &args[1..] {
3229 let _ = type_of(a, None, ctx);
3230 }
3231 return None;
3232 }
3233 if method.name != "GET" {
3234 ctx.errors.push(
3235 CompileError::new(
3236 "bynk.test.asset_address_method",
3237 method.span,
3238 format!(
3239 "an asset is fetched with `{}.GET(asset)`, not `{}`",
3240 id.name, method.name
3241 ),
3242 )
3243 .with_note(
3244 "the platform serves an asset to `GET` and answers every other method with `405`",
3245 ),
3246 );
3247 }
3248 if args.len() > 1 {
3249 ctx.errors.push(CompileError::new(
3250 "bynk.test.service_call_arity",
3251 args[1].span,
3252 format!(
3253 "`{}.{}(asset)` takes the asset alone; remove the other arguments",
3254 id.name, method.name
3255 ),
3256 ));
3257 for a in &args[1..] {
3258 let _ = type_of(a, None, ctx);
3259 }
3260 }
3261 ctx.callees.insert(
3262 expr_id,
3263 Callee::TestAssetFetch {
3264 service: id.name.clone(),
3265 },
3266 );
3267 let unit = ctx.tys.intern(Ty::Unit);
3269 return Some(effect_of(ctx.tys.intern(Ty::HttpResult(unit)), ctx));
3270 }
3271 let Some(EK::StrLit(path)) = args.first().map(|a| &a.kind) else {
3272 ctx.errors.push(
3273 CompileError::new(
3274 "bynk.test.service_bad_address",
3275 method.span,
3276 format!(
3277 "`{}.{}` addresses an http route, so its first argument must be the route pattern string (e.g. `\"/todos\"`)",
3278 id.name, method.name
3279 ),
3280 ),
3281 );
3282 for a in args {
3283 let _ = type_of(a, None, ctx);
3284 }
3285 return None;
3286 };
3287 ctx.callees.insert(
3292 expr_id,
3293 Callee::TestService {
3294 service: id.name.clone(),
3295 address: format!("{} {path}", method.name),
3296 },
3297 );
3298 let matched = sig.handlers.iter().find(|h| {
3299 matches!(&h.kind, HandlerKind::Http { method: m, path: p } if m.as_str() == method.name && p == path)
3300 });
3301 let Some(handler) = matched else {
3302 let path_declared = sig
3310 .handlers
3311 .iter()
3312 .any(|h| matches!(&h.kind, HandlerKind::Http { path: p, .. } if p == path));
3313 if path_declared {
3314 let _ = type_of(&args[0], None, ctx);
3315 if args.len() > 1 {
3319 ctx.errors.push(
3320 CompileError::new(
3321 "bynk.test.service_call_arity",
3322 method.span,
3323 format!(
3324 "`{}.{}(\"{}\")` is a wrong-method `405` test and takes only the route path, but {} argument(s) were given",
3325 id.name,
3326 method.name,
3327 path,
3328 args.len() - 1
3329 ),
3330 )
3331 .with_note("a wrong-method call reaches no handler, so it passes no body or params"),
3332 );
3333 for a in &args[1..] {
3334 let _ = type_of(a, None, ctx);
3335 }
3336 }
3337 let unit = ctx.tys.intern(Ty::Unit);
3338 return Some(effect_of(ctx.tys.intern(Ty::HttpOutcome(unit)), ctx));
3339 }
3340 ctx.errors.push(
3341 CompileError::new(
3342 "bynk.test.service_unknown_route",
3343 method.span,
3344 format!(
3345 "`{}` declares no route at `\"{}\"` (no handler for any method)",
3346 id.name, path
3347 ),
3348 )
3349 .with_note("the path must match a declared route; drive a wrong method against an existing path to test the `405` fall-through"),
3350 );
3351 for a in args {
3352 let _ = type_of(a, None, ctx);
3353 }
3354 return None;
3355 };
3356 let params = handler.params.clone();
3357 let _ = type_of(&args[0], None, ctx);
3359 check_address_args(
3360 &id.name,
3361 &method.name,
3362 ¶ms,
3363 &args[1..],
3364 method.span,
3365 ctx,
3366 );
3367 let outcome = handler_outcome(handler, ctx);
3371 if args[1..].iter().any(|a| matches!(a.kind, EK::Wire(_))) {
3372 return outcome.map(|t| wire_outcome(t, ctx));
3373 }
3374 return outcome;
3375 }
3376
3377 if method.name == "schedule" {
3379 ctx.callees.insert(
3380 expr_id,
3381 Callee::TestService {
3382 service: id.name.clone(),
3383 address: "schedule".to_string(),
3384 },
3385 );
3386 let Some(EK::StrLit(expr)) = args.first().map(|a| &a.kind) else {
3387 ctx.errors.push(CompileError::new(
3388 "bynk.test.service_bad_address",
3389 method.span,
3390 format!(
3391 "`{}.schedule` addresses a cron handler, so its first argument must be the schedule string",
3392 id.name
3393 ),
3394 ));
3395 for a in args {
3396 let _ = type_of(a, None, ctx);
3397 }
3398 return None;
3399 };
3400 ctx.callees.insert(
3403 expr_id,
3404 Callee::TestService {
3405 service: id.name.clone(),
3406 address: format!("schedule {expr}"),
3407 },
3408 );
3409 let matched = sig
3410 .handlers
3411 .iter()
3412 .find(|h| matches!(&h.kind, HandlerKind::Cron { expr: e } if e == expr));
3413 let Some(handler) = matched else {
3414 ctx.errors.push(CompileError::new(
3415 "bynk.test.service_unknown_route",
3416 method.span,
3417 format!(
3418 "`{}` declares no `on schedule(\"{}\")` handler",
3419 id.name, expr
3420 ),
3421 ));
3422 for a in args {
3423 let _ = type_of(a, None, ctx);
3424 }
3425 return None;
3426 };
3427 let params = handler.params.clone();
3428 let _ = type_of(&args[0], None, ctx);
3429 check_address_args(&id.name, "schedule", ¶ms, &args[1..], method.span, ctx);
3430 return handler_outcome(handler, ctx);
3431 }
3432
3433 if method.name == "message" {
3435 ctx.callees.insert(
3436 expr_id,
3437 Callee::TestService {
3438 service: id.name.clone(),
3439 address: "message".to_string(),
3440 },
3441 );
3442 let matched = sig
3443 .handlers
3444 .iter()
3445 .find(|h| matches!(&h.kind, HandlerKind::Message));
3446 let Some(handler) = matched else {
3447 ctx.errors.push(CompileError::new(
3448 "bynk.test.service_unknown_route",
3449 method.span,
3450 format!("`{}` declares no `on message(...)` handler", id.name),
3451 ));
3452 for a in args {
3453 let _ = type_of(a, None, ctx);
3454 }
3455 return None;
3456 };
3457 let params = handler.params.clone();
3458 check_address_args(&id.name, "message", ¶ms, args, method.span, ctx);
3459 return handler_outcome(handler, ctx);
3460 }
3461
3462 if method.name == "event"
3465 && let Some(handler) = sig
3466 .handlers
3467 .iter()
3468 .find(|h| matches!(&h.kind, HandlerKind::Event))
3469 {
3470 ctx.callees.insert(
3471 expr_id,
3472 Callee::TestService {
3473 service: id.name.clone(),
3474 address: "event".to_string(),
3475 },
3476 );
3477 let handler = handler.clone();
3478 check_event_address(sig, id, &handler, args, method.span, ctx);
3479 return handler_outcome(&handler, ctx);
3480 }
3481
3482 let proto = sig.protocol.as_deref().unwrap_or("call");
3484 let mut err = CompileError::new(
3485 "bynk.test.service_bad_address",
3486 method.span,
3487 format!(
3488 "`{}.{}` is not a way to address a `from {proto}` service in a test body",
3489 id.name, method.name
3490 ),
3491 );
3492 if method.name == "event" {
3493 err = err.with_note(
3494 "only a `from Events(E)` service is addressed with `svc.event(e)`; it delivers one event to the service's `on event` handler",
3495 );
3496 }
3497 ctx.errors.push(err);
3498 for a in args {
3499 let _ = type_of(a, None, ctx);
3500 }
3501 None
3502}
3503
3504fn handler_outcome(handler: &TestHandler, ctx: &mut Ctx) -> Option<TyId> {
3508 resolve_type_ref(&handler.return_type, &ctx.input.types, ctx.tys)
3509}
3510
3511fn effect_of(ty: TyId, ctx: &Ctx) -> TyId {
3513 ctx.tys.intern(Ty::Effect(ty))
3514}
3515
3516fn wire_outcome(outcome: TyId, ctx: &Ctx) -> TyId {
3519 let tys = ctx.tys;
3520 let inner = match &*tys.get(outcome) {
3521 Ty::Effect(t) => *t,
3522 _ => outcome,
3523 };
3524 let t = match &*tys.get(inner) {
3525 Ty::HttpResult(t) => *t,
3526 _ => return outcome,
3527 };
3528 tys.intern(Ty::Effect(tys.intern(Ty::HttpOutcome(t))))
3529}
3530
3531fn with_open_sum_note(err: CompileError, handler: &TestHandler, ctx: &Ctx) -> CompileError {
3538 let open = handler
3539 .by_clause
3540 .as_ref()
3541 .is_some_and(|by| by.is_sum() && crate::actors::sum_has_open_member(by, &ctx.test_actors));
3542 if open {
3543 err.with_note(
3544 "this actor sum admits an open member (`Visitor`): a request with a missing or bad credential resolves as it, so the seam never answers `401`",
3545 )
3546 } else {
3547 err
3548 }
3549}
3550
3551pub(crate) fn is_bad_signature_principal(name: &str) -> bool {
3554 matches!(name, "Forged" | "Stale")
3555}
3556
3557pub(crate) fn nobody_outcome(value: &Expr, ty: TyId, ctx: &Ctx) -> TyId {
3558 let is_http_address = matches!(
3559 ctx.callees.get(&value.id),
3560 Some(Callee::TestService { address, .. })
3561 if address.split(' ').next().is_some_and(|m| bynk_syntax::ast::HttpMethod::from_ident(m).is_some())
3562 );
3563 if is_http_address {
3564 wire_outcome(ty, ctx)
3565 } else {
3566 ty
3567 }
3568}
3569
3570enum ActorIdentity {
3572 Typed(TyId),
3573 CallerString,
3574 Unit,
3575 Unknown,
3576}
3577
3578fn resolve_actor_identity(name: &str, ctx: &Ctx) -> ActorIdentity {
3580 let tys = ctx.tys;
3581 use crate::actors::{Identity, prelude_actor};
3582 if let Some(decl) = ctx.test_actors.get(name) {
3583 let decl = match &decl.refinement {
3590 Some(r) => match ctx.test_actors.get(&r.base.name) {
3591 Some(base) if base.refinement.is_none() => base,
3592 _ => return ActorIdentity::Unknown,
3593 },
3594 None => decl,
3595 };
3596 return match &decl.identity {
3597 Some(t) => match resolve_type_ref(t, &ctx.input.types, tys) {
3598 Some(ty) => ActorIdentity::Typed(ty),
3599 None => ActorIdentity::Unit,
3600 },
3601 None => ActorIdentity::Unit,
3602 };
3603 }
3604 match prelude_actor(name) {
3605 Some(c) => match c.identity {
3606 Identity::Unit => ActorIdentity::Unit,
3607 Identity::CallerId => ActorIdentity::CallerString,
3608 Identity::Declared(_) => ActorIdentity::Unit,
3609 },
3610 None => ActorIdentity::Unknown,
3611 }
3612}
3613
3614fn handler_actor_name(handler: &TestHandler, protocol: Option<&str>) -> Option<String> {
3618 if let Some(by) = &handler.by_clause {
3619 return Some(by.primary().name.clone());
3620 }
3621 match protocol {
3622 None => Some("Caller".to_string()),
3623 Some("cron") => Some("Scheduler".to_string()),
3624 Some("queue") => Some("Producer".to_string()),
3625 _ => None,
3626 }
3627}
3628
3629fn is_asset_value(e: &Expr, ctx: &Ctx) -> bool {
3633 match &e.kind {
3634 ExprKind::Ident(id) => ctx
3635 .lookup(id.name.as_str())
3636 .map(|t| matches!(&*ctx.tys.get(t), Ty::Asset))
3637 .unwrap_or_else(|| ctx.input.assets.contains_key(&id.name)),
3638 _ => false,
3639 }
3640}
3641
3642fn resolve_test_address<'a>(
3645 sig: &'a TestServiceSig,
3646 method: &str,
3647 args: &[Expr],
3648) -> Option<&'a TestHandler> {
3649 use bynk_syntax::ast::{ExprKind as EK, HandlerKind, HttpMethod};
3650 if method == "call" {
3651 return sig.call_handler();
3652 }
3653 if HttpMethod::from_ident(method).is_some() {
3654 let EK::StrLit(path) = &args.first()?.kind else {
3655 return None;
3656 };
3657 return sig.handlers.iter().find(|h| {
3658 matches!(&h.kind, HandlerKind::Http { method: m, path: p } if m.as_str() == method && p == path)
3659 });
3660 }
3661 if method == "schedule" {
3662 let EK::StrLit(expr) = &args.first()?.kind else {
3663 return None;
3664 };
3665 return sig
3666 .handlers
3667 .iter()
3668 .find(|h| matches!(&h.kind, HandlerKind::Cron { expr: e } if e == expr));
3669 }
3670 if method == "message" {
3671 return sig
3672 .handlers
3673 .iter()
3674 .find(|h| matches!(&h.kind, HandlerKind::Message));
3675 }
3676 if method == "event" {
3678 return sig
3679 .handlers
3680 .iter()
3681 .find(|h| matches!(&h.kind, HandlerKind::Event));
3682 }
3683 None
3684}
3685
3686pub(crate) fn check_effect_let_principal(
3693 value: &Expr,
3694 principal: Option<&bynk_syntax::ast::CallSiteActor>,
3695 ctx: &mut Ctx,
3696) {
3697 let tys = ctx.tys;
3698 if let Some(p) = principal
3701 && let Some(claims) = &p.claims
3702 {
3703 let want = tys.intern(Ty::TokenClaims);
3704 check_arg(claims, want, "the principal's claims", ctx);
3705 let oidc = ctx.test_actors.get(&p.actor.name).is_some_and(|decl| {
3706 decl.auth
3707 .as_ref()
3708 .is_some_and(|a| a.name.as_str() == "Oidc")
3709 });
3710 if !oidc {
3711 ctx.errors.push(
3712 CompileError::new(
3713 "bynk.test.principal_claims_not_oidc",
3714 claims.span,
3715 format!(
3716 "`{}` isn't an `Oidc` actor, so its handlers read no token claims",
3717 p.actor.name
3718 ),
3719 )
3720 .with_note("claims are passed as `by Workload(\"id\", TokenClaims.of([\"name: value\"]))` for an `Oidc` actor"),
3721 );
3722 }
3723 }
3724 let ExprKind::MethodCall {
3725 receiver,
3726 method,
3727 args,
3728 ..
3729 } = &value.kind
3730 else {
3731 if let Some(p) = principal {
3732 report_principal_actor(p, ctx);
3733 }
3734 return;
3735 };
3736 let ExprKind::Ident(id) = &receiver.kind else {
3737 if let Some(p) = principal {
3738 report_principal_actor(p, ctx);
3739 }
3740 return;
3741 };
3742 let Some(sig) = ctx.test_services.get(&id.name).cloned() else {
3743 if let Some(p) = principal {
3744 report_principal_actor(p, ctx);
3745 }
3746 return;
3747 };
3748 let Some(handler) = resolve_test_address(&sig, &method.name, args).cloned() else {
3749 if let Some(p) = principal
3750 && args.first().is_some_and(|a| is_asset_value(a, ctx))
3751 {
3752 ctx.errors.push(
3755 CompileError::new(
3756 "bynk.test.principal_on_asset",
3757 p.span,
3758 format!(
3759 "an asset is public and served before the Worker runs, so `by {}` is meaningless",
3760 p.actor.name
3761 ),
3762 )
3763 .with_note("drop the `by` clause on an asset fetch"),
3764 );
3765 if let Some(id) = &p.identity {
3766 let _ = type_of(id, None, ctx);
3767 }
3768 return;
3769 }
3770 if let Some(p) = principal {
3771 let wrong_method = bynk_syntax::ast::HttpMethod::from_ident(&method.name).is_some()
3777 && matches!(args.first().map(|a| &a.kind), Some(bynk_syntax::ast::ExprKind::StrLit(path))
3778 if sig.handlers.iter().any(|h| matches!(&h.kind, bynk_syntax::ast::HandlerKind::Http { path: p, .. } if p == path)));
3779 if wrong_method {
3780 ctx.errors.push(
3781 CompileError::new(
3782 "bynk.test.principal_on_wrong_method",
3783 p.span,
3784 format!(
3785 "a wrong-method `405` test reaches no handler, so `by {}` is meaningless",
3786 p.actor.name
3787 ),
3788 )
3789 .with_note("drop the `by` clause on a wrong-method call"),
3790 );
3791 if let Some(id) = &p.identity {
3792 let _ = type_of(id, None, ctx);
3793 }
3794 } else {
3795 report_principal_actor(p, ctx);
3796 }
3797 }
3798 return;
3799 };
3800
3801 if let Some(p) = principal
3809 && p.actor.name == "Nobody"
3810 {
3811 let secured = handler
3820 .by_clause
3821 .as_ref()
3822 .is_some_and(|by| crate::actors::route_is_secured(by, &ctx.test_actors));
3823 if !secured {
3824 let err = CompileError::new(
3825 "bynk.test.nobody_needs_secured_route",
3826 p.span,
3827 "`by Nobody` drives the auth seam to a `401`, but this handler's route has no seam that refuses a missing credential",
3828 )
3829 .with_note(
3830 "use `by Nobody` only on a route guarded by a `Bearer` or `Signature` actor; a public (`Visitor`) route has no seam to test",
3831 );
3832 ctx.errors.push(with_open_sum_note(err, &handler, ctx));
3833 }
3834 if let Some(idv) = &p.identity {
3835 ctx.errors.push(CompileError::new(
3836 "bynk.test.actor_no_identity",
3837 p.span,
3838 "`Nobody` presents no credential, so it takes no identity — write `by Nobody`",
3839 ));
3840 let _ = type_of(idv, None, ctx);
3841 }
3842 return;
3843 }
3844
3845 if let Some(p) = principal
3851 && is_bad_signature_principal(&p.actor.name)
3852 {
3853 let name = p.actor.name.as_str();
3854 match handler
3855 .by_clause
3856 .as_ref()
3857 .and_then(|by| crate::actors::signature_seam_of_route(by, &ctx.test_actors))
3858 {
3859 None => {
3860 let err = CompileError::new(
3861 "bynk.test.forged_needs_signature_route",
3862 p.span,
3863 format!(
3864 "`by {name}` sends a bad signature to a `Signature` seam, but this handler's route has no `Signature` seam that refuses one"
3865 ),
3866 )
3867 .with_note(
3868 "use `by Forged` and `by Stale` on a route guarded by a `Signature` actor; on a Bearer route, `by Nobody` drives the seam to a `401`",
3869 );
3870 ctx.errors.push(with_open_sum_note(err, &handler, ctx));
3871 }
3872 Some(seam)
3873 if name == "Stale"
3874 && (seam.timestamp_header.is_none() || seam.tolerance_secs.is_none()) =>
3875 {
3876 ctx.errors.push(
3877 CompileError::new(
3878 "bynk.test.stale_needs_timestamp",
3879 p.span,
3880 "`by Stale` signs in the past, but this route's `Signature` actor declares no `timestamp` header and `tolerance` for the seam to refuse it by",
3881 )
3882 .with_note(
3883 "declare `timestamp = \"<Header>\"` and `tolerance = <seconds>` on the actor, or test the seam with `by Forged`",
3884 ),
3885 );
3886 }
3887 Some(_) => {}
3888 }
3889 if let Some(idv) = &p.identity {
3890 ctx.errors.push(CompileError::new(
3891 "bynk.test.actor_no_identity",
3892 p.span,
3893 format!("`{name}` presents a signature, not an identity, so it takes none — write `by {name}`"),
3894 ));
3895 let _ = type_of(idv, None, ctx);
3896 }
3897 return;
3898 }
3899
3900 if let Some(by) = handler.by_clause.as_ref().filter(|by| by.is_sum()) {
3903 check_sum_principal(by, principal, value.span, ctx);
3904 return;
3905 }
3906
3907 let required = handler_actor_name(&handler, sig.protocol.as_deref())
3908 .map(|actor| resolve_actor_identity(&actor, ctx));
3909
3910 match (required, principal) {
3911 (Some(ActorIdentity::Typed(ty)), principal) => match principal {
3912 Some(p) => match resolve_actor_identity(&p.actor.name, ctx) {
3913 ActorIdentity::Unknown => report_principal_actor(p, ctx),
3914 ActorIdentity::Unit | ActorIdentity::CallerString => {
3915 ctx.errors.push(CompileError::new(
3916 "bynk.test.principal_identity_mismatch",
3917 p.span,
3918 format!(
3919 "this handler runs as an actor carrying `{}`, but `by {}` supplies no matching identity",
3920 ty.display(tys),
3921 p.actor.name
3922 ),
3923 ));
3924 if let Some(idv) = &p.identity {
3925 let _ = type_of(idv, None, ctx);
3926 }
3927 }
3928 ActorIdentity::Typed(_) => match &p.identity {
3929 Some(idv) => {
3930 let got = type_of(idv, Some(ty), ctx);
3931 if let Some(g) = got
3932 && !compatible(g, ty, tys)
3933 {
3934 ctx.errors.push(CompileError::new(
3935 "bynk.types.argument_mismatch",
3936 idv.span,
3937 format!(
3938 "identity has type `{}`, but the handler expects `{}`",
3939 g.display(tys),
3940 ty.display(tys)
3941 ),
3942 ));
3943 }
3944 }
3945 None => ctx.errors.push(CompileError::new(
3946 "bynk.test.actor_identity_required",
3947 p.span,
3948 format!(
3949 "actor `{}` carries an identity, so write `by {}(...)`",
3950 p.actor.name, p.actor.name
3951 ),
3952 )),
3953 },
3954 },
3955 None => ctx.errors.push(
3956 CompileError::new(
3957 "bynk.test.principal_required",
3958 value.span,
3959 format!(
3960 "this handler runs as a verified actor carrying `{}`; the case must act as it with `by <Actor>(<identity>)`",
3961 ty.display(tys)
3962 ),
3963 )
3964 .with_note("append a call-site actor, e.g. `... by User(\"alice\")`"),
3965 ),
3966 },
3967 (Some(ActorIdentity::CallerString), principal) if handler_binds_caller(&handler, ctx) => {
3972 match principal {
3973 None => ctx.errors.push(
3974 CompileError::new(
3975 "bynk.test.actor_identity_required",
3976 value.span,
3977 "this handler binds its caller's identity (`by <binder>: Caller`), so the case must name the calling context with `by Caller(\"...\")`",
3978 )
3979 .with_note("append a call-site actor, e.g. `... by Caller(\"orders\")`"),
3980 ),
3981 Some(p) => match resolve_actor_identity(&p.actor.name, ctx) {
3982 ActorIdentity::CallerString if p.identity.is_none() => {
3983 ctx.errors.push(CompileError::new(
3984 "bynk.test.actor_identity_required",
3985 p.span,
3986 format!(
3987 "this handler binds its caller's identity, so write `by {}(\"...\")` naming the calling context",
3988 p.actor.name
3989 ),
3990 ))
3991 }
3992 ActorIdentity::Unit => {
3993 ctx.errors.push(CompileError::new(
3994 "bynk.test.principal_identity_mismatch",
3995 p.span,
3996 format!(
3997 "this handler binds its caller's identity, but `by {}` supplies none — write `by Caller(\"...\")`",
3998 p.actor.name
3999 ),
4000 ));
4001 if let Some(idv) = &p.identity {
4002 let _ = type_of(idv, None, ctx);
4003 }
4004 }
4005 _ => report_principal_actor(p, ctx),
4006 },
4007 }
4008 }
4009 (_, Some(p)) => report_principal_actor(p, ctx),
4010 (_, None) => {}
4011 }
4012}
4013
4014fn check_sum_principal(
4021 by: &bynk_syntax::ast::ByClause,
4022 principal: Option<&bynk_syntax::ast::CallSiteActor>,
4023 value_span: Span,
4024 ctx: &mut Ctx,
4025) {
4026 let members = format!(
4027 "`{}`",
4028 by.actors
4029 .iter()
4030 .map(|a| a.name.as_str())
4031 .collect::<Vec<_>>()
4032 .join(" | ")
4033 );
4034 let Some(p) = principal else {
4035 ctx.errors.push(
4036 CompileError::new(
4037 "bynk.test.principal_required",
4038 value_span,
4039 format!(
4040 "this handler runs as one of the actors {members}; the case must act as one with `by <Actor>`"
4041 ),
4042 )
4043 .with_note(format!(
4044 "append a call-site actor naming a member, e.g. `... by {}`",
4045 sum_member_example(&by.actors[0].name, ctx)
4046 )),
4047 );
4048 return;
4049 };
4050 let name = &p.actor.name;
4051 if !by.actors.iter().any(|a| &a.name == name) {
4052 if matches!(resolve_actor_identity(name, ctx), ActorIdentity::Unknown) {
4053 report_principal_actor(p, ctx);
4054 return;
4055 }
4056 ctx.errors.push(
4057 CompileError::new(
4058 "bynk.test.principal_identity_mismatch",
4059 p.span,
4060 format!(
4061 "this handler runs as one of the actors {members}, but `by {name}` names none of them"
4062 ),
4063 )
4064 .with_note("act as a member of the handler's `by` sum"),
4065 );
4066 if let Some(idv) = &p.identity {
4067 let _ = type_of(idv, None, ctx);
4068 }
4069 return;
4070 }
4071 match resolve_actor_identity(name, ctx) {
4072 ActorIdentity::CallerString if p.identity.is_none() => ctx.errors.push(CompileError::new(
4075 "bynk.test.actor_identity_required",
4076 p.span,
4077 format!(
4078 "the `{name}` arm carries the calling context's name, so write `by {name}(\"...\")`"
4079 ),
4080 )),
4081 _ => report_principal_actor(p, ctx),
4082 }
4083}
4084
4085fn sum_member_example(name: &str, ctx: &Ctx) -> String {
4087 match resolve_actor_identity(name, ctx) {
4088 ActorIdentity::Typed(_) | ActorIdentity::CallerString => format!("{name}(...)"),
4089 ActorIdentity::Unit | ActorIdentity::Unknown => name.to_string(),
4090 }
4091}
4092
4093fn handler_binds_caller(handler: &TestHandler, ctx: &Ctx) -> bool {
4100 use crate::actors::{Identity, prelude_actor};
4101 let Some(by) = &handler.by_clause else {
4102 return false;
4103 };
4104 let Some(binder) = &by.binder else {
4105 return false;
4106 };
4107 let name = &by.primary().name;
4108 matches!(handler.kind, bynk_syntax::ast::HandlerKind::Call)
4109 && !by.is_sum()
4110 && !ctx.test_actors.contains_key(name)
4111 && prelude_actor(name).map(|c| c.identity) == Some(Identity::CallerId)
4112 && !handler.params.iter().any(|p| p.name.name == binder.name)
4113}
4114
4115fn report_principal_actor(p: &bynk_syntax::ast::CallSiteActor, ctx: &mut Ctx) {
4117 let tys = ctx.tys;
4118 let name = &p.actor.name;
4119 match resolve_actor_identity(name, ctx) {
4120 ActorIdentity::Unknown if ctx.test_actors.contains_key(name) => {
4125 if let Some(id) = &p.identity {
4126 let _ = type_of(id, None, ctx);
4127 }
4128 }
4129 ActorIdentity::Unknown if is_bad_signature_principal(name) => {
4133 ctx.errors.push(
4134 CompileError::new(
4135 "bynk.test.forged_needs_signature_route",
4136 p.span,
4137 format!("`by {name}` sends a bad signature to a `Signature` seam, but this address reaches no `Signature`-secured http route"),
4138 )
4139 .with_note("use `by Forged` and `by Stale` on an http route guarded by one `Signature` actor"),
4140 );
4141 if let Some(id) = &p.identity {
4142 let _ = type_of(id, None, ctx);
4143 }
4144 }
4145 ActorIdentity::Unknown => {
4146 ctx.errors.push(
4147 CompileError::new(
4148 "bynk.test.unknown_actor",
4149 p.actor.span,
4150 format!("`{name}` is not an actor of the target context or a prelude actor"),
4151 )
4152 .with_note("name an `actor` the target declares, or a prelude actor (`Visitor`, `Caller`, …)"),
4153 );
4154 if let Some(id) = &p.identity {
4155 let _ = type_of(id, None, ctx);
4156 }
4157 }
4158 ActorIdentity::Typed(ty) => match &p.identity {
4159 Some(id) => {
4160 let got = type_of(id, Some(ty), ctx);
4161 if let Some(g) = got
4162 && !compatible(g, ty, tys)
4163 {
4164 ctx.errors.push(CompileError::new(
4165 "bynk.types.argument_mismatch",
4166 id.span,
4167 format!(
4168 "identity has type `{}`, but actor `{name}` expects `{}`",
4169 g.display(tys),
4170 ty.display(tys)
4171 ),
4172 ));
4173 }
4174 }
4175 None => ctx.errors.push(CompileError::new(
4176 "bynk.test.actor_identity_required",
4177 p.span,
4178 format!("actor `{name}` carries an identity, so `by {name}(...)` needs an identity value"),
4179 )),
4180 },
4181 ActorIdentity::CallerString => {
4182 if let Some(id) = &p.identity {
4183 let _ = type_of(id, None, ctx);
4184 }
4185 }
4186 ActorIdentity::Unit => {
4187 if let Some(id) = &p.identity {
4188 let _ = type_of(id, None, ctx);
4189 ctx.errors.push(CompileError::new(
4190 "bynk.test.actor_no_identity",
4191 p.span,
4192 format!("actor `{name}` has no identity, so write `by {name}` with no argument"),
4193 ));
4194 }
4195 }
4196 }
4197}
4198
4199fn check_address_args(
4203 svc: &str,
4204 addr: &str,
4205 params: &[bynk_syntax::ast::Param],
4206 positional: &[Expr],
4207 err_span: Span,
4208 ctx: &mut Ctx,
4209) {
4210 let tys = ctx.tys;
4211 if params.len() != positional.len() {
4212 ctx.errors.push(
4213 CompileError::new(
4214 "bynk.test.service_call_arity",
4215 err_span,
4216 format!(
4217 "`{svc}.{addr}` expects {} argument(s), but {} were given",
4218 params.len(),
4219 positional.len()
4220 ),
4221 )
4222 .with_note("handler declared here"),
4225 );
4226 for a in positional {
4227 let _ = type_of(a, None, ctx);
4228 }
4229 return;
4230 }
4231 for (i, (param, arg)) in params.iter().zip(positional.iter()).enumerate() {
4232 record_param_hint(ctx.hints, ¶m.name.name, arg);
4233 if let bynk_syntax::ast::ExprKind::Wire(inner) = &arg.kind {
4241 let _ = type_of(inner, Some(tys.intern(Ty::Base(BaseType::String))), ctx);
4242 continue;
4243 }
4244 let expected = resolve_type_ref(¶m.type_ref, &ctx.input.types, tys);
4245 let arg_ty = type_of(arg, expected, ctx);
4246 if let (Some(a), Some(p)) = (arg_ty, expected)
4247 && !compatible(a, p, tys)
4248 {
4249 ctx.errors.push(CompileError::new(
4250 "bynk.types.argument_mismatch",
4251 arg.span,
4252 format!(
4253 "argument {} has type `{}`, but `{svc}.{addr}` expects `{}` for `{}`",
4254 i + 1,
4255 a.display(tys),
4256 p.display(tys),
4257 param.name.name
4258 ),
4259 ));
4260 }
4261 }
4262}
4263
4264fn check_event_address(
4270 sig: &TestServiceSig,
4271 id: &Ident,
4272 handler: &TestHandler,
4273 args: &[Expr],
4274 err_span: Span,
4275 ctx: &mut Ctx,
4276) {
4277 let tys = ctx.tys;
4278 let svc = &id.name;
4279 if args.is_empty() || args.len() > 2 {
4280 ctx.errors.push(
4281 CompileError::new(
4282 "bynk.test.service_call_arity",
4283 err_span,
4284 format!(
4285 "`{svc}.event` takes the event and, optionally, an `EventEnvelope`, but {} argument(s) were given",
4286 args.len()
4287 ),
4288 )
4289 .with_note("deliver an event with `svc.event(e)`, or `svc.event(e, env)` to choose its envelope"),
4290 );
4291 for a in args {
4292 let _ = type_of(a, None, ctx);
4293 }
4294 return;
4295 }
4296 if args.len() == 1
4299 && let Some(Err(candidates)) = &sig.event_publisher
4300 {
4301 let found = if candidates.is_empty() {
4302 "no context the target declares or consumes declares it".to_string()
4303 } else {
4304 format!(
4305 "it is declared by {}",
4306 candidates
4307 .iter()
4308 .map(|c| format!("`{c}`"))
4309 .collect::<Vec<_>>()
4310 .join(", ")
4311 )
4312 };
4313 ctx.errors.push(
4314 CompileError::new(
4315 "bynk.test.event_publisher_ambiguous",
4316 err_span,
4317 format!("`{svc}.event(e)` has no single publisher to name in its default envelope: {found}"),
4318 )
4319 .with_note("pass the envelope yourself: `svc.event(e, env)`"),
4320 );
4321 }
4322 let payload = &args[0];
4323 let expected = handler
4324 .params
4325 .first()
4326 .and_then(|p| resolve_type_ref(&p.type_ref, &ctx.input.types, tys));
4327 let arg_ty = type_of(payload, expected, ctx);
4328 if let (Some(a), Some(p)) = (arg_ty, expected)
4329 && !compatible(a, p, tys)
4330 {
4331 let other_event = match &*tys.get(a) {
4332 Ty::Named { name, .. } if sig.known_events.contains(name) => Some(name.clone()),
4333 _ => None,
4334 };
4335 let err = match other_event {
4336 Some(other) => CompileError::new(
4337 "bynk.test.event_not_subscribed",
4338 payload.span,
4339 format!(
4340 "`{svc}` subscribes to `{}`, not `{other}`",
4341 p.display(tys)
4342 ),
4343 )
4344 .with_note(
4345 "an event address delivers only the event the service's `from Events(E)` header names",
4346 ),
4347 None => CompileError::new(
4348 "bynk.types.argument_mismatch",
4349 payload.span,
4350 format!(
4351 "argument 1 has type `{}`, but `{svc}.event` expects the event it subscribes to, `{}`",
4352 a.display(tys),
4353 p.display(tys)
4354 ),
4355 ),
4356 };
4357 ctx.errors.push(err);
4358 }
4359 let Some(envelope) = args.get(1) else {
4360 return;
4361 };
4362 let envelope_ref = TypeRef::Named(Ident {
4363 name: "EventEnvelope".to_string(),
4364 span: envelope.span,
4365 });
4366 let expected = resolve_type_ref(&envelope_ref, &ctx.input.types, tys);
4367 let env_ty = type_of(envelope, expected, ctx);
4368 let Some(a) = env_ty else {
4369 return;
4370 };
4371 let mismatch = match expected {
4374 Some(p) => !compatible(a, p, tys),
4375 None => true,
4376 };
4377 if mismatch {
4378 let mut err = CompileError::new(
4379 "bynk.types.argument_mismatch",
4380 envelope.span,
4381 format!(
4382 "argument 2 has type `{}`, but `{svc}.event` expects an `EventEnvelope`",
4383 a.display(tys)
4384 ),
4385 );
4386 if expected.is_none() {
4387 err = err.with_note(
4388 "`EventEnvelope` comes from `bynk`; the suite's target must `consumes bynk` to build one",
4389 );
4390 }
4391 ctx.errors.push(err);
4392 }
4393}
4394
4395fn cross_context_prefix(receiver: &Expr, ctx: &Ctx) -> Option<String> {
4400 let info = &ctx.input.cross_context;
4401 if info.consumed_contexts.is_empty() && info.aliases.is_empty() {
4402 return None;
4403 }
4404 let candidate = flatten_ident_chain(receiver)?;
4409 let head = candidate.split('.').next().unwrap_or("");
4410 if ctx.lookup(head).is_some() {
4412 return None;
4413 }
4414 if ctx.caps.capabilities.contains_key(head) || ctx.caps.declared_capabilities.contains_key(head)
4415 {
4416 return None;
4417 }
4418 info.resolve_prefix(candidate.as_str())
4422}
4423
4424fn flatten_ident_chain(expr: &Expr) -> Option<String> {
4427 match &expr.kind {
4428 ExprKind::Ident(id) => Some(id.name.clone()),
4429 ExprKind::FieldAccess { receiver, field } => {
4430 let head = flatten_ident_chain(receiver)?;
4431 Some(format!("{head}.{}", field.name))
4432 }
4433 _ => None,
4434 }
4435}
4436
4437#[allow(clippy::too_many_arguments)]
4445fn check_cross_context_capability_call(
4446 receiver: &Expr,
4447 consumed: &str,
4448 cap: &str,
4449 method: &Ident,
4450 type_args: &[TypeRef],
4453 args: &[Expr],
4454 span: Span,
4455 expr_id: ExprId,
4457 ctx: &mut Ctx,
4458) -> Option<TyId> {
4459 let tys = ctx.tys;
4460 refuse_experimental(
4462 &format!("{consumed}.{cap}.{}", method.name),
4463 method.span,
4464 ctx,
4465 );
4466 ctx.callees.insert(
4467 expr_id,
4468 Callee::CrossCap {
4469 unit: consumed.to_string(),
4470 cap: cap.to_string(),
4471 op: method.name.clone(),
4472 },
4473 );
4474 if !ctx.effectful {
4476 ctx.errors.push(CompileError::new(
4477 "bynk.effect.capability_in_pure_context",
4478 method.span,
4479 format!(
4480 "capability `{consumed}.{cap}` can only be called inside an effectful body (one returning `Effect[T]`)"
4481 ),
4482 ));
4483 }
4484 if !ctx.caps.given_remaining.contains(cap) {
4487 let mut err = CompileError::new(
4488 "bynk.given.undeclared_capability",
4489 receiver.span,
4490 format!("capability `{consumed}.{cap}` is used but not listed in the `given` clause"),
4491 )
4492 .with_note(format!(
4493 "add `{consumed}.{cap}` to the handler's `given` clause so the dependency surface is visible at the declaration site"
4494 ));
4495 if let Some((span, insert)) = given_insertion_edit(
4498 &ctx.caps.given_entries,
4499 ctx.caps.given_anchor,
4500 &format!("{consumed}.{cap}"),
4501 ) {
4502 err = err.with_suggestion(
4503 format!("add `{consumed}.{cap}` to the `given` clause"),
4504 vec![(span, insert)],
4505 Applicability::MachineApplicable,
4506 );
4507 }
4508 ctx.errors.push(err);
4509 for a in args {
4510 let _ = type_of(a, None, ctx);
4511 }
4512 return None;
4513 }
4514 ctx.caps.given_used.insert(cap.to_string());
4515
4516 let info = &ctx.input.cross_context;
4517 let op = info
4518 .consumed_capabilities
4519 .get(consumed)
4520 .and_then(|caps| caps.get(cap))
4521 .and_then(|c| c.ops.iter().find(|o| o.name == method.name))
4522 .cloned();
4523 let Some(op) = op else {
4524 ctx.errors.push(CompileError::new(
4525 "bynk.capability.unknown_operation",
4526 method.span,
4527 format!(
4528 "capability `{consumed}.{cap}` has no operation named `{}`",
4529 method.name
4530 ),
4531 ));
4532 for a in args {
4533 let _ = type_of(a, None, ctx);
4534 }
4535 return None;
4536 };
4537 ctx.refs.record_in_unit(
4541 method.span,
4542 SymbolKind::CapabilityOp,
4543 &format!("{cap}.{}", method.name),
4544 consumed,
4545 );
4546 if op.params.len() != args.len() {
4547 ctx.errors.push(CompileError::new(
4548 "bynk.capability.op_arity",
4549 method.span,
4550 format!(
4551 "capability operation `{consumed}.{cap}.{}` expects {} argument(s), but {} were given",
4552 method.name,
4553 op.params.len(),
4554 args.len()
4555 ),
4556 ));
4557 for a in args {
4558 let _ = type_of(a, None, ctx);
4559 }
4560 return None;
4561 }
4562
4563 let consumed_types = info
4565 .consumed_types
4566 .get(consumed)
4567 .cloned()
4568 .unwrap_or_default();
4569 let vars: HashSet<String> = op.type_params.iter().cloned().collect();
4576 let mut subst: HashMap<String, TyId> = HashMap::new();
4577 if !op.type_params.is_empty() || !type_args.is_empty() {
4578 if type_args.is_empty() {
4579 ctx.errors.push(
4580 CompileError::new(
4581 "bynk.generics.uninferable_type_arg",
4582 method.span,
4583 format!(
4584 "capability operation `{consumed}.{cap}.{}` takes a type parameter, but none of its arguments determine it",
4585 method.name
4586 ),
4587 )
4588 .with_note(format!(
4589 "give it explicitly: `{consumed}.{cap}.{}[T](…)`",
4590 method.name
4591 )),
4592 );
4593 for a in args {
4594 let _ = type_of(a, None, ctx);
4595 }
4596 return None;
4597 }
4598 if type_args.len() != op.type_params.len() {
4599 ctx.errors.push(CompileError::new(
4600 "bynk.generics.type_arg_mismatch",
4601 method.span,
4602 format!(
4603 "capability operation `{consumed}.{cap}.{}` takes {} type argument(s), but {} were given",
4604 method.name,
4605 op.type_params.len(),
4606 type_args.len()
4607 ),
4608 ));
4609 for a in args {
4610 let _ = type_of(a, None, ctx);
4611 }
4612 return None;
4613 }
4614 for (tp, ta) in op.type_params.iter().zip(type_args) {
4615 let ty = resolve_expr_type_ref(ta, ctx)?;
4616 subst.insert(tp.clone(), ty);
4617 }
4618 }
4619 let (caller_types, ambiguous) = projected_types(&ctx.input.types, &info.consumed_types, tys);
4622 let mut all_ok = true;
4623 for (i, ((pname, ptype_ref), arg)) in op.params.iter().zip(args.iter()).enumerate() {
4624 record_param_hint(ctx.hints, pname, arg);
4625 let param_ty = resolve_type_ref_in(ptype_ref, &consumed_types, &vars, tys)
4626 .unwrap_or(tys.intern(Ty::Unit));
4627 let param_ty = substitute(param_ty, &subst, tys);
4628 let (arg_ty, crosses) =
4630 type_cross_context_arg(arg, param_ty, &caller_types, &consumed_types, consumed, ctx);
4631 let Some(arg_ty) = arg_ty else {
4632 all_ok = false;
4633 continue;
4634 };
4635 if !crosses {
4636 let mut err = CompileError::new(
4637 "bynk.boundary.structural_mismatch",
4638 arg.span,
4639 format!(
4640 "cross-context argument {} to `{consumed}.{cap}.{}` has type `{}`, but parameter `{pname}` expects `{}`",
4641 i + 1,
4642 method.name,
4643 arg_ty.display(tys),
4644 param_ty.display(tys),
4645 ),
4646 );
4647 if let Some(note) = ambiguous_projection_note(arg_ty, &ambiguous, tys) {
4648 err = err.with_note(note);
4649 }
4650 ctx.errors.push(err);
4651 all_ok = false;
4652 }
4653 }
4654 if !all_ok {
4655 return None;
4656 }
4657 let raw_ret = resolve_type_ref_in(&op.return_type, &consumed_types, &vars, tys)
4658 .unwrap_or(tys.intern(Ty::Unit));
4659 let raw_ret = substitute(raw_ret, &subst, tys);
4660 if let Some(name) = rebrand_conflict(
4661 raw_ret,
4662 &consumed_types,
4663 &ctx.input.types,
4664 &caller_types,
4665 tys,
4666 ) {
4667 ctx.errors.push(rebranded_return_mismatch(
4668 &format!("{consumed}.{cap}.{}", method.name),
4669 &name,
4670 consumed,
4671 span,
4672 ctx,
4673 ));
4674 return None;
4675 }
4676 Some(rebrand_return_type(raw_ret, &ctx.input.types, tys))
4677}
4678
4679fn rebranded_return_mismatch(
4683 callee: &str,
4684 name: &str,
4685 consumed: &str,
4686 span: Span,
4687 ctx: &Ctx,
4688) -> CompileError {
4689 let caller = ctx
4690 .input
4691 .cross_context
4692 .self_context
4693 .as_deref()
4694 .unwrap_or("?");
4695 CompileError::new(
4696 "bynk.boundary.structural_mismatch",
4697 span,
4698 format!(
4699 "cross-context call `{callee}` returns `{consumed}`'s type `{name}`, but `{name}` in \
4700 `{caller}` is a different type"
4701 ),
4702 )
4703 .with_note(
4704 "a returned value takes the receiving context's type of the same name, so the two \
4705 must be structurally compatible; rename one of them",
4706 )
4707}
4708
4709fn type_cross_context_arg(
4732 arg: &Expr,
4733 param_ty: TyId,
4734 caller_types: &HashMap<String, Arc<TypeDecl>>,
4735 consumed_types: &HashMap<String, Arc<TypeDecl>>,
4736 consumed: &str,
4737 ctx: &mut Ctx,
4738) -> (Option<TyId>, bool) {
4739 let tys = ctx.tys;
4740 let mut found = CrossingPositions::default();
4741 collect_crossing_positions(arg, param_ty, caller_types, consumed_types, tys, &mut found);
4742 let CrossingPositions {
4743 literals,
4744 mut leaves,
4745 bindings,
4746 } = found;
4747 for (lit, pos) in literals {
4748 let before = ctx.errors.len();
4749 match admit_refined_literal_in(lit, Some(pos), consumed_types, ctx) {
4750 Some(ty) => {
4751 for err in &mut ctx.errors[before..] {
4752 err.notes.push(format!(
4753 "`{}` is the type at this position of the parameter, as `{consumed}` declares it",
4754 ty.display(tys)
4755 ));
4756 }
4757 ctx.admitted_literals.insert(lit.id, ty);
4758 }
4759 None => leaves.push((lit, pos)),
4761 }
4762 }
4763 let arg_ty = type_of(arg, Some(param_ty), ctx);
4764 let Some(ty) = arg_ty else {
4765 return (None, false);
4766 };
4767 let whole = || structurally_compatible(ty, param_ty, caller_types, consumed_types, tys);
4768 let leaves_cross = leaves.iter().all(|(leaf, pos)| {
4769 match ctx.expr_types.get(&leaf.id) {
4772 Some(t) => structurally_compatible(t.ty, *pos, caller_types, consumed_types, tys),
4773 None => whole(),
4774 }
4775 });
4776 let bindings_cross = bindings.iter().all(|(name, pos)| match ctx.lookup(name) {
4777 Some(t) => structurally_compatible(t, *pos, caller_types, consumed_types, tys),
4778 None => whole(),
4779 });
4780 (arg_ty, leaves_cross && bindings_cross)
4781}
4782
4783#[derive(Default)]
4787struct CrossingPositions<'e> {
4788 literals: Vec<(&'e Expr, TyId)>,
4790 leaves: Vec<(&'e Expr, TyId)>,
4792 bindings: Vec<(&'e str, TyId)>,
4794}
4795
4796fn collect_crossing_positions<'e>(
4806 e: &'e Expr,
4807 pos: TyId,
4808 caller_types: &HashMap<String, Arc<TypeDecl>>,
4809 consumed_types: &HashMap<String, Arc<TypeDecl>>,
4810 tys: &Types,
4811 found: &mut CrossingPositions<'e>,
4812) {
4813 let node = tys.get(pos);
4814 let walk = |inner: &'e Expr, at: TyId, found: &mut CrossingPositions<'e>| {
4815 collect_crossing_positions(inner, at, caller_types, consumed_types, tys, found)
4816 };
4817 if const_literal(e).is_some()
4819 && matches!(
4820 &*node,
4821 Ty::Named {
4822 kind: NamedKind::Refined(_),
4823 ..
4824 }
4825 )
4826 {
4827 return found.literals.push((e, pos));
4828 }
4829 match (&e.kind, &*node) {
4830 (ExprKind::Paren(inner), _) => walk(inner, pos, found),
4831 (ExprKind::None, Ty::Option(_)) => {}
4833 (ExprKind::ListLit(items), Ty::List(_)) if items.is_empty() => {}
4834 (
4836 ExprKind::MethodCall {
4837 receiver,
4838 method,
4839 type_args,
4840 args,
4841 },
4842 Ty::List(_) | Ty::Map(..),
4843 ) if method.name == "empty"
4844 && type_args.is_empty()
4845 && args.is_empty()
4846 && matches!(
4847 &receiver.kind,
4848 ExprKind::Ident(id)
4849 if id.name == if matches!(&*node, Ty::List(_)) { "List" } else { "Map" }
4850 ) => {}
4851 (ExprKind::Some(inner), Ty::Option(t)) => walk(inner, *t, found),
4852 (ExprKind::Ok(inner), Ty::Result(t, _)) => walk(inner, *t, found),
4853 (ExprKind::Err(inner), Ty::Result(_, t)) => walk(inner, *t, found),
4854 (ExprKind::ListLit(items), Ty::List(t)) => {
4855 for item in items {
4856 walk(item, *t, found);
4857 }
4858 }
4859 (
4860 ExprKind::RecordConstruction { type_name, fields },
4861 Ty::Named {
4862 name,
4863 kind: NamedKind::Record,
4864 args,
4865 },
4866 ) if type_name.name == *name => {
4867 let Some(field_positions) =
4868 record_field_positions(name, args, fields, caller_types, consumed_types, tys)
4869 else {
4870 return found.leaves.push((e, pos));
4871 };
4872 for (f, field_ty) in field_positions {
4873 match &f.value {
4874 Some(value) => walk(value, field_ty, found),
4875 None => found.bindings.push((f.name.name.as_str(), field_ty)),
4877 }
4878 }
4879 }
4880 _ => found.leaves.push((e, pos)),
4881 }
4882}
4883
4884fn record_field_positions<'f>(
4891 name: &str,
4892 args: &[TyId],
4893 fields: &'f [FieldInit],
4894 caller_types: &HashMap<String, Arc<TypeDecl>>,
4895 consumed_types: &HashMap<String, Arc<TypeDecl>>,
4896 tys: &Types,
4897) -> Option<Vec<(&'f FieldInit, TyId)>> {
4898 let decl = consumed_types.get(name)?;
4899 if !caller_types.get(name).is_some_and(|c| Arc::ptr_eq(c, decl)) {
4900 return None;
4901 }
4902 let TypeBody::Record(record) = &decl.body else {
4903 return None;
4904 };
4905 if decl.type_params.len() != args.len() {
4906 return None;
4907 }
4908 let vars: HashSet<String> = decl
4909 .type_params
4910 .iter()
4911 .map(|p| p.name.name.clone())
4912 .collect();
4913 let subst: HashMap<String, TyId> = decl
4914 .type_params
4915 .iter()
4916 .map(|p| p.name.name.clone())
4917 .zip(args.iter().copied())
4918 .collect();
4919 fields
4920 .iter()
4921 .map(|f| {
4922 let declared = record.fields.iter().find(|d| d.name.name == f.name.name)?;
4923 let ty = resolve_type_ref_in(&declared.type_ref, consumed_types, &vars, tys)?;
4924 Some((f, substitute(ty, &subst, tys)))
4925 })
4926 .collect()
4927}
4928
4929fn check_signature_owners_consumed(
4935 consumed: &str,
4936 service: &crate::resolver::CrossContextService,
4937 method: &Ident,
4938 ctx: &mut Ctx,
4939) -> bool {
4940 let info = &ctx.input.cross_context;
4941 let Some(owners) = info.consumed_type_owners.get(consumed) else {
4942 return false;
4943 };
4944 let types = info.consumed_types.get(consumed);
4945 let mut seen: HashSet<String> = HashSet::new();
4946 let mut stack: Vec<String> = Vec::new();
4947 let vars = HashSet::new();
4948 for (_, t) in &service.params {
4949 crate::project_model::type_ref_names(t, &vars, &mut stack);
4950 }
4951 crate::project_model::type_ref_names(&service.return_type, &vars, &mut stack);
4952 let mut missing: std::collections::BTreeMap<String, String> = Default::default();
4953 while let Some(n) = stack.pop() {
4954 if !seen.insert(n.clone()) {
4955 continue;
4956 }
4957 if let Some(owner) = owners.get(&n)
4958 && info.self_context.as_deref() != Some(owner.as_str())
4959 && !info.consumed_contexts.contains(owner)
4960 {
4961 let declared = n
4964 .strip_prefix(&format!("{}__", owner.replace('.', "_")))
4965 .unwrap_or(&n)
4966 .to_string();
4967 missing.entry(owner.clone()).or_insert(declared);
4968 }
4969 if let Some(d) = types.and_then(|t| t.get(&n)) {
4970 stack.extend(crate::project_model::type_decl_ref_names(d));
4971 }
4972 }
4973 let refused = !missing.is_empty();
4974 for (owner, ty) in missing {
4975 ctx.errors.push(
4976 CompileError::new(
4977 "bynk.consumes.signature_type_unconsumed",
4978 method.span,
4979 format!(
4980 "`{consumed}.{}` takes or returns `{ty}`, declared in `{owner}`, but this context does not consume `{owner}`",
4981 method.name
4982 ),
4983 )
4984 .with_note(format!(
4985 "add `consumes {owner}`: a value of `{ty}` crosses this call as `{owner}`'s type"
4986 )),
4987 );
4988 }
4989 refused
4990}
4991
4992fn check_cross_context_call(
4993 receiver: &Expr,
4994 consumed: &str,
4995 method: &Ident,
4996 args: &[Expr],
4997 span: Span,
4998 expr_id: ExprId,
5000 ctx: &mut Ctx,
5001) -> Option<TyId> {
5002 let tys = ctx.tys;
5003 ctx.callees.insert(
5004 expr_id,
5005 Callee::Cross {
5006 unit: consumed.to_string(),
5007 service: method.name.clone(),
5008 },
5009 );
5010 if !ctx.effectful {
5013 ctx.errors.push(
5014 CompileError::new(
5015 "bynk.effect.cross_context_in_pure_context",
5016 method.span,
5017 format!(
5018 "cross-context service call `{}.{}` can only be made inside an effectful body (one returning `Effect[T]`)",
5019 consumed, method.name
5020 ),
5021 )
5022 .with_label(receiver.span, "consumed context prefix"),
5023 );
5024 }
5025 let info = &ctx.input.cross_context;
5026 let Some(svcs) = info.consumed_services.get(consumed) else {
5027 ctx.errors.push(
5028 CompileError::new(
5029 "bynk.consumes.unknown_context",
5030 receiver.span,
5031 format!("context `{consumed}` is not in scope here"),
5032 )
5033 .with_note(
5034 "add a `consumes` clause for the target context at the top of the consuming context",
5035 ),
5036 );
5037 for a in args {
5038 let _ = type_of(a, None, ctx);
5039 }
5040 return None;
5041 };
5042 let Some(service) = svcs.get(&method.name).cloned() else {
5043 ctx.errors.push(
5044 CompileError::new(
5045 "bynk.consumes.unknown_service",
5046 method.span,
5047 format!(
5048 "context `{consumed}` has no service named `{}`",
5049 method.name
5050 ),
5051 )
5052 .with_note(
5053 "cross-context calls require an `on call` service handler in the consumed context",
5054 ),
5055 );
5056 for a in args {
5057 let _ = type_of(a, None, ctx);
5058 }
5059 return None;
5060 };
5061 ctx.refs
5062 .record_in_unit(method.span, SymbolKind::Service, &method.name, consumed);
5063 let unconsumed = check_signature_owners_consumed(consumed, &service, method, ctx);
5065
5066 if service.params.len() != args.len() {
5067 ctx.errors.push(
5068 CompileError::new(
5069 "bynk.consumes.service_arity",
5070 method.span,
5071 format!(
5072 "cross-context service `{consumed}.{}` expects {} argument(s), but {} were given",
5073 method.name,
5074 service.params.len(),
5075 args.len()
5076 ),
5077 )
5078 .with_note("service declared here"),
5084 );
5085 for a in args {
5086 let _ = type_of(a, None, ctx);
5087 }
5088 return None;
5089 }
5090
5091 let consumed_types = info
5093 .consumed_types
5094 .get(consumed)
5095 .cloned()
5096 .unwrap_or_default();
5097
5098 let (caller_types, ambiguous) = projected_types(&ctx.input.types, &info.consumed_types, tys);
5101
5102 let mut all_ok = true;
5104 for (i, ((pname, ptype_ref), arg)) in service.params.iter().zip(args.iter()).enumerate() {
5105 record_param_hint(ctx.hints, pname, arg);
5106 let param_ty =
5107 resolve_type_ref(ptype_ref, &consumed_types, tys).unwrap_or(tys.intern(Ty::Unit));
5108 let (arg_ty, crosses) =
5111 type_cross_context_arg(arg, param_ty, &caller_types, &consumed_types, consumed, ctx);
5112 let Some(arg_ty) = arg_ty else {
5113 all_ok = false;
5114 continue;
5115 };
5116 if !crosses && !unconsumed {
5117 let mut err = CompileError::new(
5118 "bynk.boundary.structural_mismatch",
5119 arg.span,
5120 format!(
5121 "cross-context argument {} to `{consumed}.{}` has type `{}` in `{}`, but parameter `{pname}` expects `{}` in `{}`",
5122 i + 1,
5123 method.name,
5124 arg_ty.display(tys),
5125 ctx.input
5126 .cross_context
5127 .self_context
5128 .as_deref()
5129 .unwrap_or("?"),
5130 param_ty.display(tys),
5131 consumed,
5132 ),
5133 )
5134 .with_note("service declared here")
5136 .with_note(
5137 "values crossing a context boundary must have structurally compatible types (same commons-derived type, or identical record/sum shape)",
5138 );
5139 if let Some(note) = ambiguous_projection_note(arg_ty, &ambiguous, tys) {
5140 err = err.with_note(note);
5141 }
5142 ctx.errors.push(err);
5143 all_ok = false;
5144 }
5145 }
5146 if !all_ok {
5147 return None;
5148 }
5149
5150 let raw_ret = resolve_type_ref(&service.return_type, &consumed_types, tys)
5154 .unwrap_or(tys.intern(Ty::Unit));
5155 if let Some(name) = rebrand_conflict(
5156 raw_ret,
5157 &consumed_types,
5158 &ctx.input.types,
5159 &caller_types,
5160 tys,
5161 ) {
5162 ctx.errors.push(rebranded_return_mismatch(
5163 &format!("{consumed}.{}", method.name),
5164 &name,
5165 consumed,
5166 span,
5167 ctx,
5168 ));
5169 return None;
5170 }
5171 Some(rebrand_return_type(raw_ret, &ctx.input.types, tys))
5172}
5173
5174#[cfg(test)]
5175mod tests {
5176 use crate::testkit::analyse;
5177
5178 fn commons(body: &str) -> crate::testkit::Analysed {
5180 analyse(&[("demo.bynk", &format!("commons demo\n\n{body}"))])
5181 }
5182
5183 fn context(body: &str) -> crate::testkit::Analysed {
5185 analyse(&[("demo.bynk", &format!("context demo\n\n{body}"))])
5186 }
5187
5188 const CLOCK: &str = "capability Clock {\n fn now() -> Effect[Int]\n}\n\n\
5189 provides Clock = FixedClock {\n fn now() -> Effect[Int] {\n Effect.pure(0)\n }\n}\n";
5190
5191 #[test]
5192 fn a_call_to_a_declared_fn_has_its_return_type() {
5193 let a = commons("fn double(x: Int) -> Int { x * 2 }\n\nfn four() -> Int { double(2) }\n");
5194 a.assert_clean();
5195 assert_eq!(a.type_at("demo.bynk", "double(2)"), "Int");
5196 }
5197
5198 #[test]
5199 fn an_argument_of_the_wrong_type_is_a_mismatch() {
5200 let a = commons(
5201 "fn double(x: Int) -> Int { x * 2 }\n\nfn bad(s: String) -> Int { double(s) }\n",
5202 );
5203 a.assert_reports("bynk.types.argument_mismatch");
5204 }
5205
5206 #[test]
5207 fn a_fn_called_with_too_many_arguments_is_an_arity_error() {
5208 let a = commons("fn double(x: Int) -> Int { x * 2 }\n\nfn bad() -> Int { double(1, 2) }\n");
5209 a.assert_reports("bynk.resolve.arity_mismatch");
5210 }
5211
5212 #[test]
5213 fn a_fn_value_called_with_too_many_arguments_is_a_call_arity_error() {
5214 let a = commons("fn f(g: (Int) -> Int) -> Int { g(1, 2) }\n");
5215 a.assert_reports("bynk.types.call_arity");
5216 }
5217
5218 #[test]
5219 fn a_fn_value_call_has_the_value_s_return_type() {
5220 let a = commons("fn f(g: (Int) -> String) -> String { g(1) }\n");
5221 a.assert_clean();
5222 assert_eq!(a.type_at("demo.bynk", "g(1)"), "String");
5223 }
5224
5225 #[test]
5226 fn a_generic_call_infers_its_type_argument_from_the_argument() {
5227 let a = commons("fn id[A](x: A) -> A { x }\n\nfn s() -> String { id(\"s\") }\n");
5228 a.assert_clean();
5229 assert_eq!(a.type_at("demo.bynk", "id(\"s\")"), "String");
5230 }
5231
5232 #[test]
5233 fn a_generic_call_binding_one_type_argument_two_ways_is_a_conflict() {
5234 let a =
5235 commons("fn both[A](x: A, y: A) -> A { x }\n\nfn bad() -> Int { both(1, \"two\") }\n");
5236 a.assert_reports("bynk.generics.type_arg_mismatch");
5237 }
5238
5239 #[test]
5240 fn a_type_argument_no_parameter_mentions_is_uninferable() {
5241 let a = commons(
5242 "fn first_of[A](x: Int) -> A { identity_helper(x) }\n\n\
5243 fn identity_helper[A](x: A) -> A { x }\n\n\
5244 fn misuse() -> Int { first_of(5) }\n",
5245 );
5246 a.assert_reports("bynk.generics.uninferable_type_arg");
5247 }
5248
5249 #[test]
5250 fn calling_an_undeclared_name_is_an_unknown_function() {
5251 let a = commons("fn bad() -> Int { nope(1) }\n");
5252 a.assert_reports("bynk.resolve.unknown_function");
5253 }
5254
5255 #[test]
5256 fn calling_a_record_type_like_a_function_is_rejected() {
5257 let a = commons("type P = { a: Int }\n\nfn f() -> P { P(1) }\n");
5258 a.assert_reports("bynk.resolve.type_as_function");
5259 }
5260
5261 #[test]
5262 fn a_capability_op_call_has_the_op_s_return_type() {
5263 let a = context(&format!(
5264 "{CLOCK}\nservice probe {{\n on call() -> Effect[Int] given Clock {{\n Clock.now()\n }}\n}}\n"
5265 ));
5266 a.assert_clean();
5267 assert_eq!(a.type_at("demo.bynk", "Clock.now()"), "Effect[Int]");
5268 }
5269
5270 #[test]
5271 fn a_capability_op_called_with_an_extra_argument_is_an_arity_error() {
5272 let a = context(&format!(
5273 "{CLOCK}\nservice probe {{\n on call() -> Effect[Int] given Clock {{\n Clock.now(1)\n }}\n}}\n"
5274 ));
5275 a.assert_reports("bynk.capability.op_arity");
5276 }
5277
5278 #[test]
5279 fn a_capability_called_inside_a_pure_lambda_is_rejected() {
5280 let a = context(&format!(
5281 "{CLOCK}\nservice probe {{\n on call() -> Effect[Int] given Clock {{\n \
5282 let f: (Int) -> Int = (n) => Clock.now()\n Effect.pure(1)\n }}\n}}\n"
5283 ));
5284 a.assert_reports("bynk.effect.capability_in_pure_context");
5285 }
5286
5287 #[test]
5288 fn an_unknown_cell_op_is_rejected() {
5289 let a = context(
5290 "agent Counter {\n key id: String\n store n: Cell[Int] = 0\n\n \
5291 on call bad() -> Effect[()] {\n let _ <- n.foo()\n Effect.pure(())\n }\n}\n",
5292 );
5293 a.assert_reports("bynk.store.unknown_op");
5294 }
5295
5296 #[test]
5297 fn a_cell_update_is_an_effect_of_unit() {
5298 let a = context(
5299 "agent Counter {\n key id: String\n store n: Cell[Int] = 0\n\n \
5300 on call bump() -> Effect[()] {\n n.update((c) => c + 1)\n }\n}\n",
5301 );
5302 a.assert_clean();
5303 assert_eq!(
5304 a.type_at("demo.bynk", "n.update((c) => c + 1)"),
5305 "Effect[()]"
5306 );
5307 }
5308
5309 #[test]
5310 fn a_cell_update_whose_function_returns_another_type_is_a_mismatch() {
5311 let a = context(
5312 "agent Counter {\n key id: String\n store n: Cell[Int] = 0\n\n \
5313 on call bad() -> Effect[()] {\n n.update((c) => \"x\")\n }\n}\n",
5314 );
5315 a.assert_reports("bynk.types.lambda_mismatch");
5316 }
5317
5318 #[test]
5319 fn a_cell_update_with_no_function_is_a_call_arity_error() {
5320 let a = context(
5321 "agent Counter {\n key id: String\n store n: Cell[Int] = 0\n\n \
5322 on call bad() -> Effect[()] {\n n.update()\n }\n}\n",
5323 );
5324 a.assert_reports("bynk.types.call_arity");
5325 }
5326
5327 const SHOP: &str = "context shop\n\nexports capability { Store }\n\n\
5331 type Amount = { cents: Int }\n\ntype Box = { a: Amount }\n\n\
5332 capability Store {\n fn get() -> Effect[Box]\n fn put(b: Box) -> Effect[Int]\n}\n\n\
5333 provides Store = Shelf {\n fn get() -> Effect[Box] {\n Effect.pure(Box { a: Amount { cents: 1 } })\n }\n\n \
5334 fn put(b: Box) -> Effect[Int] {\n Effect.pure(b.a.cents)\n }\n}\n\n\
5335 service quote {\n on call() -> Effect[Box] {\n Effect.pure(Box { a: Amount { cents: 1 } })\n }\n}\n\n\
5336 service take {\n on call(b: Box) -> Effect[Int] {\n Effect.pure(b.a.cents)\n }\n}\n";
5337
5338 fn till(decls: &str, body: &str) -> String {
5341 let given = if body.contains("Store") {
5342 " given shop.Store"
5343 } else {
5344 ""
5345 };
5346 format!(
5347 "context till\n\nconsumes shop\n\n{decls}service total {{\n \
5348 on call() -> Effect[Int]{given} {{\n{body}\n }}\n}}\n"
5349 )
5350 }
5351
5352 #[test]
5353 fn an_unexported_value_passes_back_to_its_service() {
5354 let a = analyse(&[
5355 ("shop.bynk", SHOP),
5356 (
5357 "till.bynk",
5358 &till("", " let b <- shop.quote()\n shop.take(b)"),
5359 ),
5360 ]);
5361 a.assert_clean();
5362 }
5363
5364 #[test]
5365 fn an_unexported_value_passes_back_to_its_capability() {
5366 let a = analyse(&[
5367 ("shop.bynk", SHOP),
5368 (
5369 "till.bynk",
5370 &till("", " let b <- shop.Store.get()\n shop.Store.put(b)"),
5371 ),
5372 ]);
5373 a.assert_clean();
5374 }
5375
5376 #[test]
5377 fn a_caller_type_of_another_shape_cannot_receive_a_service_return() {
5378 let a = analyse(&[
5379 ("shop.bynk", SHOP),
5380 (
5381 "till.bynk",
5382 &till(
5383 "type Box = { label: String }\n\n",
5384 " let b <- shop.quote()\n Effect.pure(1)",
5385 ),
5386 ),
5387 ]);
5388 a.assert_reports("bynk.boundary.structural_mismatch");
5389 }
5390
5391 #[test]
5392 fn a_caller_type_of_another_shape_cannot_receive_a_capability_return() {
5393 let a = analyse(&[
5394 ("shop.bynk", SHOP),
5395 (
5396 "till.bynk",
5397 &till(
5398 "type Box = { label: String }\n\n",
5399 " let b <- shop.Store.get()\n Effect.pure(1)",
5400 ),
5401 ),
5402 ]);
5403 a.assert_reports("bynk.boundary.structural_mismatch");
5404 }
5405
5406 #[test]
5407 fn a_caller_type_of_the_same_shape_receives_a_return() {
5408 let a = analyse(&[
5409 ("shop.bynk", SHOP),
5410 (
5411 "till.bynk",
5412 &till(
5413 "type Coins = { cents: Int }\n\ntype Box = { a: Coins }\n\n",
5414 " let b <- shop.quote()\n let c <- shop.Store.get()\n \
5415 Effect.pure(b.a.cents + c.a.cents)",
5416 ),
5417 ),
5418 ]);
5419 a.assert_clean();
5420 }
5421
5422 #[test]
5423 fn an_ambiguous_unexported_name_explains_its_rejection() {
5424 let other = "context depot\n\ntype Box = { label: String }\n\n\
5425 service quote {\n on call() -> Effect[Box] {\n Effect.pure(Box { label: \"x\" })\n }\n}\n";
5426 let a = analyse(&[
5427 ("shop.bynk", SHOP),
5428 ("depot.bynk", other),
5429 (
5430 "till.bynk",
5431 "context till\n\nconsumes shop\n\nconsumes depot\n\n\
5432 service total {\n on call() -> Effect[Int] {\n \
5433 let b <- shop.quote()\n shop.take(b)\n }\n}\n",
5434 ),
5435 ]);
5436 let err = a
5437 .analysis
5438 .errors
5439 .iter()
5440 .find(|e| e.error.category == "bynk.boundary.structural_mismatch")
5441 .expect("the argument is rejected");
5442 assert!(
5443 err.error
5444 .notes
5445 .iter()
5446 .any(|n| n.contains("more than one consumed context declares a type `Box`")),
5447 "{:?}",
5448 err.error.notes
5449 );
5450 }
5451
5452 #[test]
5453 fn a_return_the_caller_does_not_rebrand_is_not_checked_against_a_projection() {
5454 let pair = |name: &str, svc: &str| {
5460 format!(
5461 "context {name}\n\ntype Amount = {{ cents: Int }}\n\ntype Box = {{ a: Amount }}\n\n{svc}"
5462 )
5463 };
5464 let shop = pair(
5465 "shop",
5466 "service quote {\n on call() -> Effect[Box] {\n Effect.pure(Box { a: Amount { cents: 1 } })\n }\n}\n",
5467 );
5468 let depot = pair(
5469 "depot",
5470 "service store {\n on call(b: Box) -> Effect[Int] {\n Effect.pure(b.a.cents)\n }\n}\n",
5471 );
5472 let a = analyse(&[
5473 ("shop.bynk", &shop),
5474 ("depot.bynk", &depot),
5475 (
5476 "till.bynk",
5477 "context till\n\nconsumes shop\n\nconsumes depot\n\n\
5478 type Amount = { dollars: Int }\n\n\
5479 fn mine() -> Amount {\n Amount { dollars: 1 }\n}\n\n\
5480 service total {\n on call() -> Effect[Int] {\n \
5481 let b <- shop.quote()\n Effect.pure(1)\n }\n}\n",
5482 ),
5483 ]);
5484 a.assert_clean();
5485 }
5486
5487 fn caller_box_differs_inside(ret: &str, value: &str) {
5491 let shop = format!(
5492 "context shop\n\ntype Box = {{ a: Int }}\n\n\
5493 service quote {{\n on call() -> Effect[{ret}] {{\n Effect.pure({value})\n }}\n}}\n"
5494 );
5495 let a = analyse(&[
5496 ("shop.bynk", &shop),
5497 (
5498 "till.bynk",
5499 "context till\n\nconsumes shop\n\ntype Box = { label: String }\n\n\
5500 service total {\n on call() -> Effect[Int] {\n \
5501 let b <- shop.quote()\n Effect.pure(1)\n }\n}\n",
5502 ),
5503 ]);
5504 a.assert_reports("bynk.boundary.structural_mismatch");
5505 }
5506
5507 #[test]
5508 fn a_differing_caller_type_is_found_in_a_result() {
5509 caller_box_differs_inside("Result[Box, String]", "Ok(Box { a: 1 })");
5510 }
5511
5512 #[test]
5513 fn a_differing_caller_type_is_found_in_a_map_value() {
5514 caller_box_differs_inside("Map[String, Box]", "Map.empty()");
5515 }
5516
5517 #[test]
5518 fn a_differing_caller_type_is_found_in_an_option() {
5519 caller_box_differs_inside("Option[Box]", "None");
5520 }
5521
5522 #[test]
5523 fn a_differing_caller_type_is_found_in_a_list() {
5524 caller_box_differs_inside("List[Box]", "[]");
5525 }
5526
5527 #[test]
5528 fn a_differing_caller_type_is_found_in_an_http_result() {
5529 caller_box_differs_inside("HttpResult[Box]", "Ok(Box { a: 1 })");
5530 }
5531
5532 #[test]
5533 fn agreeing_declarations_project_in_context_name_order() {
5534 let a = analyse(&[
5540 (
5541 "shop.bynk",
5542 "context shop\n\ntype Amount = { cents: Int }\n\ntype Box = { a: Amount }\n\n\
5543 service quote {\n on call() -> Effect[Box] {\n \
5544 Effect.pure(Box { a: Amount { cents: 1 } })\n }\n}\n",
5545 ),
5546 (
5547 "depot.bynk",
5548 "context depot\n\ntype Coins = { cents: Int }\n\ntype Box = { a: Coins }\n\n\
5549 service store {\n on call(b: Box) -> Effect[Int] {\n Effect.pure(b.a.cents)\n }\n}\n",
5550 ),
5551 (
5552 "till.bynk",
5553 "context till\n\nconsumes shop\n\nconsumes depot\n\n\
5554 type Amount = { dollars: Int }\n\n\
5555 fn mine() -> Amount {\n Amount { dollars: 1 }\n}\n\n\
5556 service total {\n on call() -> Effect[Int] {\n \
5557 let b <- shop.quote()\n depot.store(b)\n }\n}\n",
5558 ),
5559 ]);
5560 a.assert_clean();
5561 }
5562}