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 pattern_binding_types: HashMap::new(),
153 return_ty,
154 return_ty_span: f.return_type.span(),
155 effectful,
156 agent_state_ty: None,
157 commit_seen: false,
158 caps: CapabilityCtx::default(),
159 in_test_body: false,
160 test_services: HashMap::new(),
161 test_actors: HashMap::new(),
162 type_vars: vars.clone(),
163 store_fields: HashMap::new(),
164 };
165 let Some(body_ty) = type_of_block(&f.body, Some(return_ty), &mut ctx) else {
166 return;
167 };
168 linearity::check(
176 &f.body,
177 &f.params,
178 &input.types,
179 ctx.expr_types,
180 &ctx.pattern_binding_types,
181 &HashSet::new(),
182 ctx.errors,
183 tys,
184 );
185 if !compatible(body_ty, return_ty, tys) {
186 ctx.errors.push(
187 CompileError::new(
188 "bynk.types.return_mismatch",
189 f.body.tail.span,
190 format!(
191 "function body has type `{}`, but the declared return type is `{}`",
192 body_ty.display(tys),
193 return_ty.display(tys)
194 ),
195 )
196 .with_label(f.return_type.span(), "declared return type"),
197 );
198 }
199}
200
201#[allow(clippy::too_many_arguments)]
220fn check_static_initialiser(
221 init: &Expr,
222 field_type: &TypeRef,
223 input: &ResolvedCommons,
224 expr_types: &mut HashMap<ExprId, TypedExpr>,
225 callees: &mut HashMap<ExprId, Callee>,
226 errors: &mut Vec<CompileError>,
227 refs: &mut RefSink,
228 hints: &mut HintSink,
229 locals: &mut LocalsSink,
230 code: &'static str,
231 subject: &str,
232 tys: &Types,
233) {
234 let Some(field_ty) = resolve_type_ref(field_type, &input.types, tys) else {
235 return; };
237 let mut local_errors: Vec<CompileError> = Vec::new();
238 let mut init_requirements = RequirementSink::new();
241 let result = {
242 let mut ctx = Ctx {
243 input,
244 tys,
245 expr_types,
246 errors: &mut local_errors,
247 refs,
248 hints,
249 locals,
250 requirements: &mut init_requirements,
251 callees,
252 scopes: vec![HashMap::new()],
253 is_binding_cache: HashMap::new(),
254 pattern_binding_types: HashMap::new(),
255 return_ty: field_ty,
256 return_ty_span: init.span,
257 effectful: false,
258 agent_state_ty: None,
259 commit_seen: false,
260 caps: CapabilityCtx::default(),
261 in_test_body: false,
262 test_services: HashMap::new(),
263 test_actors: HashMap::new(),
264 type_vars: HashSet::new(),
265 store_fields: HashMap::new(),
266 };
267 type_of(init, Some(field_ty), &mut ctx)
268 };
269 let compatible_result = matches!(&result, Some(t) if compatible(*t, field_ty, tys));
270 if !compatible_result || !local_errors.is_empty() {
271 let got = result
272 .map(|t| t.display(tys))
273 .unwrap_or_else(|| "an invalid expression".to_string());
274 errors.push(
275 CompileError::new(
276 code,
277 init.span,
278 format!(
279 "{subject} must be a static value of type `{}` (got `{got}`)",
280 field_ty.display(tys),
281 ),
282 )
283 .with_note(
284 "an initialiser is a compile-time value — a literal (including one admitted to a \
285 refined type), a sum variant, `Some`/`None`/`Ok`/`Err`, a record, or — for an \
286 opaque type — `T.unsafe(lit)` — with no reference to `self`, parameters, or \
287 capabilities",
288 ),
289 );
290 }
291}
292
293#[allow(clippy::too_many_arguments)]
296pub fn check_state_initialiser(
297 init: &Expr,
298 field_type: &TypeRef,
299 input: &ResolvedCommons,
300 tys: &Types,
301 expr_types: &mut HashMap<ExprId, TypedExpr>,
302 callees: &mut HashMap<ExprId, Callee>,
303 errors: &mut Vec<CompileError>,
304 refs: &mut RefSink,
305 hints: &mut HintSink,
306 locals: &mut LocalsSink,
307) {
308 check_static_initialiser(
309 init,
310 field_type,
311 input,
312 expr_types,
313 callees,
314 errors,
315 refs,
316 hints,
317 locals,
318 "bynk.agents.bad_state_initialiser",
319 "state field initialiser",
320 tys,
321 );
322}
323
324#[allow(clippy::too_many_arguments)]
342pub fn check_event_field_default(
343 init: &Expr,
344 field_type: &TypeRef,
345 input: &ResolvedCommons,
346 tys: &Types,
347 expr_types: &mut HashMap<ExprId, TypedExpr>,
348 callees: &mut HashMap<ExprId, Callee>,
349 errors: &mut Vec<CompileError>,
350 refs: &mut RefSink,
351 hints: &mut HintSink,
352 locals: &mut LocalsSink,
353) {
354 check_static_initialiser(
355 init,
356 field_type,
357 input,
358 expr_types,
359 callees,
360 errors,
361 refs,
362 hints,
363 locals,
364 "bynk.event.bad_field_default",
365 "event field default",
366 tys,
367 );
368 if let ExprKind::MethodCall {
374 receiver,
375 method,
376 args,
377 ..
378 } = &init.kind
379 && let ExprKind::Ident(type_name) = &receiver.kind
380 && method.name == "unsafe"
381 && let [lit_expr] = args.as_slice()
382 && let Some(decl) = input.types.get(&type_name.name)
383 && matches!(decl.body, TypeBody::Opaque { .. })
384 && let Some(refinement) = refinements::type_decl_refinement(decl)
385 && let Some(lit) = refinements::const_literal(lit_expr)
386 && let Some(failed) = refinements::first_failed_predicate(refinement, &lit)
387 {
388 errors.push(
389 CompileError::new(
390 "bynk.event.bad_field_default",
391 lit_expr.span,
392 format!(
393 "`{}.unsafe(...)` bypasses its own refinement, but an event field default \
394 must be a value the wire could actually carry — this literal fails `{}`",
395 type_name.name,
396 failed.name(),
397 ),
398 )
399 .with_note(
400 "a default is spliced into the same codec that validates a real wire value on \
401 receipt, so a refinement-violating `.unsafe(lit)` default would compile cleanly \
402 and then fail at runtime the first time an old event actually triggers it",
403 ),
404 );
405 }
406}
407
408fn warn_bynk_list_deprecation(name: &Ident, args: &[Expr], call_span: Span, ctx: &mut Ctx) {
414 if ctx.input.imported_from.get(&name.name).map(String::as_str) != Some("bynk.list") {
415 return;
416 }
417 let method_form: &str = match name.name.as_str() {
419 "map" => "xs.map(f)",
420 "filter" => "xs.filter(p)",
421 "any" => "xs.any(p)",
422 "all" => "xs.all(p)",
423 "find" => "xs.filter(p).first()",
424 _ => return, };
426 let mut err = CompileError::new(
427 "bynk.list.deprecated_function",
428 name.span,
429 format!(
430 "`bynk.list.{}` is deprecated — use the `List` method form `{method_form}`",
431 name.name
432 ),
433 )
434 .with_note(
435 "the `bynk.list.*` free functions are superseded by the method-chain vocabulary (ADR 0116); the method form reads left-to-right and chains",
436 );
437 if args.len() == 2 {
440 let mut edits = vec![
441 (
443 Span::new(name.span.start, args[0].span.start),
444 String::new(),
445 ),
446 (
448 Span::new(args[0].span.end, args[1].span.start),
449 format!(
450 ".{}(",
451 if name.name == "find" {
452 "filter"
453 } else {
454 &name.name
455 }
456 ),
457 ),
458 ];
459 if name.name == "find" {
460 edits.push((
462 Span::new(call_span.end, call_span.end),
463 ".first()".to_string(),
464 ));
465 }
466 err = err.with_suggestion(
467 format!("rewrite to the `List` method form `{method_form}`"),
468 edits,
469 Applicability::MachineApplicable,
470 );
471 }
472 ctx.errors.push(err);
473}
474
475pub(crate) fn check_call(
476 name: &Ident,
477 type_args: &[TypeRef],
478 args: &[Expr],
479 span: Span,
480 expected: Option<TyId>,
483 expr_id: ExprId,
486 ctx: &mut Ctx,
487) -> Option<TyId> {
488 let tys = ctx.tys;
489 if let Some(fn_decl) = ctx.input.fns.get(&name.name) {
490 ctx.refs.record(name.span, SymbolKind::Fn, &name.name);
491 ctx.callees.insert(expr_id, Callee::Fn(Arc::clone(fn_decl)));
492 warn_bynk_list_deprecation(name, args, span, ctx);
493 return check_call_against_fn(name, fn_decl, type_args, args, ctx);
494 }
495 if !type_args.is_empty() {
497 ctx.errors.push(CompileError::new(
498 "bynk.generics.type_arg_mismatch",
499 span,
500 format!(
501 "`{}` is not a generic function — it takes no type arguments",
502 name.name
503 ),
504 ));
505 for a in args {
506 let _ = type_of(a, None, ctx);
507 }
508 return None;
509 }
510 let owners: Vec<&Arc<TypeDecl>> = ctx
514 .input
515 .types
516 .values()
517 .filter(|t| matches!(&t.body, TypeBody::Sum(s) if s.variants.iter().any(|v| v.name.name == name.name)))
518 .collect();
519 if owners.len() == 1 {
520 ctx.callees.insert(
521 expr_id,
522 Callee::Ctor {
523 sum: Arc::clone(owners[0]),
524 tag: name.name.clone(),
525 },
526 );
527 return check_variant_construction(owners[0], &name.name, args, span, expected, ctx);
528 }
529 if let Some(agent) = ctx.input.agents.get(&name.name).cloned() {
533 ctx.refs.record(name.span, SymbolKind::Agent, &name.name);
534 ctx.callees
535 .insert(expr_id, Callee::AgentInit(name.name.clone()));
536 let key_ty = resolve_type_ref(&agent.key_type, &ctx.input.types, tys);
537 if args.len() != 1 {
538 ctx.errors.push(CompileError::new(
539 "bynk.agent.construction_arity",
540 span,
541 format!(
542 "agent `{}` is constructed with one key argument, but {} were given",
543 name.name,
544 args.len()
545 ),
546 ));
547 for a in args {
548 let _ = type_of(a, None, ctx);
549 }
550 return None;
551 }
552 let arg_ty = type_of(&args[0], key_ty, ctx);
553 if let (Some(a), Some(k)) = (arg_ty, key_ty)
554 && !compatible(a, k, tys)
555 {
556 ctx.errors.push(CompileError::new(
557 "bynk.agent.key_mismatch",
558 args[0].span,
559 format!(
560 "agent `{}` key is `{}`, but a value of type `{}` was given",
561 name.name,
562 k.display(tys),
563 a.display(tys)
564 ),
565 ));
566 }
567 return Some(tys.intern(Ty::Named {
568 name: name.name.clone(),
569 kind: NamedKind::Record,
570 args: Vec::new(),
571 }));
572 }
573 if let Some(ty) = ctx.lookup(&name.name) {
579 return match &*tys.get(ty) {
580 Ty::Fn { params, ret } => {
581 ctx.callees
582 .insert(expr_id, Callee::Value(name.name.clone()));
583 check_value_application(name, params, *ret, args, span, ctx)
584 }
585 _ => {
586 ctx.errors.push(
589 CompileError::new(
590 "bynk.resolve.param_as_function",
591 span,
592 format!(
593 "`{}` has type `{}` and is not callable",
594 name.name,
595 ty.display(tys)
596 ),
597 )
598 .with_note("only values of function type can be applied"),
599 );
600 for a in args {
601 let _ = type_of(a, None, ctx);
602 }
603 None
604 }
605 };
606 }
607 for a in args {
614 let _ = type_of(a, None, ctx);
615 }
616 if owners.len() > 1 {
617 ctx.errors.push(CompileError::new(
618 "bynk.resolve.ambiguous_variant",
619 name.span,
620 format!(
621 "the variant name `{}` is declared on multiple sum types — qualify it as `TypeName.{}(...)`",
622 name.name, name.name
623 ),
624 ));
625 return None;
626 }
627 if ctx.input.types.contains_key(&name.name) {
628 ctx.errors.push(CompileError::new(
629 "bynk.resolve.type_as_function",
630 span,
631 format!(
632 "`{}` is a type, not a function — use `{}.of(value)` or `{} {{ ... }}` instead",
633 name.name, name.name, name.name
634 ),
635 ));
636 return None;
637 }
638 ctx.errors.push(
639 CompileError::new(
640 "bynk.resolve.unknown_function",
641 span,
642 format!("unknown function `{}`", name.name),
643 )
644 .with_note("only functions declared in this commons are callable"),
645 );
646 None
647}
648
649fn check_value_application(
650 name: &Ident,
651 params: &[TyId],
652 ret: TyId,
653 args: &[Expr],
654 span: Span,
655 ctx: &mut Ctx,
656) -> Option<TyId> {
657 let tys = ctx.tys;
658 if ret.is_effect(tys) && !ctx.effectful {
659 ctx.errors.push(
660 CompileError::new(
661 "bynk.effect.fn_value_in_pure_context",
662 span,
663 format!(
664 "`{}` is an effectful function (`{}`) and cannot be called in a pure context",
665 name.name,
666 Ty::Fn {
667 params: params.to_vec(),
668 ret,
669 }
670 .display(tys)
671 ),
672 )
673 .with_note(
674 "effectful function values may only be called where the enclosing body is effectful (its return type is an Effect)",
675 ),
676 );
677 }
678 if params.len() != args.len() {
679 ctx.errors.push(CompileError::new(
680 "bynk.types.call_arity",
681 span,
682 format!(
683 "`{}` takes {} argument(s), but {} were given",
684 name.name,
685 params.len(),
686 args.len()
687 ),
688 ));
689 for a in args {
690 let _ = type_of(a, None, ctx);
691 }
692 return None;
693 }
694 for (arg, param_ty) in args.iter().zip(params) {
695 let arg_ty = type_of(arg, Some(*param_ty), ctx);
696 if let Some(a) = arg_ty
697 && !compatible(a, *param_ty, tys)
698 {
699 ctx.errors.push(CompileError::new(
700 "bynk.types.argument_mismatch",
701 arg.span,
702 format!(
703 "argument has type `{}`, but `{}` expects `{}`",
704 a.display(tys),
705 name.name,
706 param_ty.display(tys)
707 ),
708 ));
709 }
710 }
711 Some(ret)
712}
713
714fn check_generic_call(
725 name: &Ident,
726 fn_decl: &FnDecl,
727 type_args: &[TypeRef],
728 args: &[Expr],
729 ctx: &mut Ctx,
730) -> Option<TyId> {
731 let tys = ctx.tys;
732 let vars: HashSet<String> = fn_decl
733 .type_params
734 .iter()
735 .map(|tp| tp.name.name.clone())
736 .collect();
737 if fn_decl.params.len() != args.len() {
738 ctx.errors.push(
741 CompileError::new(
742 "bynk.resolve.arity_mismatch",
743 name.span,
744 format!(
745 "function `{}` expects {} argument(s), but {} were given",
746 name.name,
747 fn_decl.params.len(),
748 args.len()
749 ),
750 )
751 .with_label(fn_decl.name.ident().span, "function declared here"),
752 );
753 for a in args {
754 let _ = type_of(a, None, ctx);
755 }
756 return None;
757 }
758 let var_params: Vec<Option<TyId>> = fn_decl
759 .params
760 .iter()
761 .map(|p| resolve_type_ref_in(&p.type_ref, &ctx.input.types, &vars, tys))
762 .collect();
763 let ret_pattern = resolve_type_ref_in(&fn_decl.return_type, &ctx.input.types, &vars, tys)?;
764
765 let mut subst: HashMap<String, TyId> = HashMap::new();
766 if !type_args.is_empty() {
767 if type_args.len() != fn_decl.type_params.len() {
768 ctx.errors.push(CompileError::new(
769 "bynk.generics.type_arg_mismatch",
770 name.span,
771 format!(
772 "`{}` takes {} type argument(s), but {} were given",
773 name.name,
774 fn_decl.type_params.len(),
775 type_args.len()
776 ),
777 ));
778 return None;
779 }
780 for (tp, ta) in fn_decl.type_params.iter().zip(type_args) {
781 let ty = resolve_expr_type_ref(ta, ctx)?;
787 subst.insert(tp.name.name.clone(), ty);
788 }
789 }
790
791 let mut arg_tys: Vec<Option<TyId>> = vec![None; args.len()];
792 for (i, arg) in args.iter().enumerate() {
794 if matches!(arg.kind, ExprKind::Lambda(_)) {
795 continue;
796 }
797 let expected = var_params[i].map(|p| substitute(p, &subst, tys));
798 let ty = type_of(arg, expected, ctx);
799 if let (Some(pattern), Some(actual)) = (var_params[i], ty)
800 && !unify(pattern, actual, &mut subst, tys)
801 {
802 ctx.errors.push(CompileError::new(
803 "bynk.generics.type_arg_mismatch",
804 arg.span,
805 format!(
806 "argument {} infers a type for `{}`'s type parameter that conflicts with an earlier argument — annotate with `{}[T](…)`",
807 i + 1,
808 name.name,
809 name.name
810 ),
811 ));
812 return None;
813 }
814 arg_tys[i] = ty;
815 }
816 for (i, arg) in args.iter().enumerate() {
818 if !matches!(arg.kind, ExprKind::Lambda(_)) {
819 continue;
820 }
821 let expected = var_params[i].map(|p| substitute(p, &subst, tys));
822 let params_unconstrained = expected.is_some_and(|e| {
823 matches!(&*tys.get(e), Ty::Fn { params, .. }
824 if params.iter().any(|p| contains_var(*p, tys)))
825 });
826 let fully_annotated = matches!(
827 &arg.kind,
828 ExprKind::Lambda(l) if l.params.iter().all(|p| p.type_ref.is_some())
829 );
830 if params_unconstrained && !fully_annotated {
831 ctx.errors.push(
832 CompileError::new(
833 "bynk.generics.uninferable_type_arg",
834 arg.span,
835 format!(
836 "the lambda's parameter types depend on `{}`'s type parameters, which the other arguments do not determine",
837 name.name
838 ),
839 )
840 .with_note("annotate the lambda's parameters, or give explicit type arguments: `name[T](…)`"),
841 );
842 return None;
843 }
844 let ty = if params_unconstrained {
847 type_of(arg, None, ctx)
848 } else {
849 type_of(arg, expected, ctx)
850 };
851 if let (Some(pattern), Some(actual)) = (var_params[i], ty)
852 && !unify(pattern, actual, &mut subst, tys)
853 {
854 ctx.errors.push(CompileError::new(
855 "bynk.generics.type_arg_mismatch",
856 arg.span,
857 format!(
858 "the lambda's type conflicts with `{}`'s inferred type arguments",
859 name.name
860 ),
861 ));
862 return None;
863 }
864 arg_tys[i] = ty;
865 }
866 for tp in &fn_decl.type_params {
868 if !subst.contains_key(&tp.name.name) {
869 ctx.errors.push(
870 CompileError::new(
871 "bynk.generics.uninferable_type_arg",
872 name.span,
873 format!(
874 "type parameter `{}` of `{}` is neither inferable from the arguments nor given explicitly",
875 tp.name.name, name.name
876 ),
877 )
878 .with_label(tp.span, "declared here")
879 .with_note("give explicit type arguments: `name[T](…)`"),
880 );
881 return None;
882 }
883 }
884 let mut ok = true;
886 for (i, (pattern, arg)) in var_params.iter().zip(args).enumerate() {
887 record_param_hint(ctx.hints, &fn_decl.params[i].name.name, arg);
888 let (Some(pattern), Some(arg_ty)) = (pattern, arg_tys[i].as_ref()) else {
889 continue;
890 };
891 let ground = substitute(*pattern, &subst, tys);
892 if !compatible(*arg_ty, ground, tys) {
893 ctx.errors.push(CompileError::new(
894 "bynk.types.argument_mismatch",
895 arg.span,
896 format!(
897 "argument {} to `{}` has type `{}`, but `{}` is expected",
898 i + 1,
899 name.name,
900 arg_ty.display(tys),
901 ground.display(tys)
902 ),
903 ));
904 ok = false;
905 }
906 }
907 if !ok {
908 return None;
909 }
910 let compared = match ctx.input.fns.get(&name.name) {
914 Some(decl) if std::ptr::eq(decl.as_ref(), fn_decl) => {
915 super::equality::compared_type_params(decl, ctx)
916 }
917 _ => Default::default(),
918 };
919 let params: Vec<(String, Option<Span>)> = fn_decl
920 .type_params
921 .iter()
922 .map(|tp| (tp.name.name.clone(), Some(tp.span)))
923 .collect();
924 if !super::equality::check_compared_args(&name.name, name.span, ¶ms, &compared, &subst, ctx)
925 {
926 return None;
927 }
928 if type_args.is_empty() && !fn_decl.type_params.is_empty() {
933 let rendered: Option<Vec<String>> = fn_decl
934 .type_params
935 .iter()
936 .map(|tp| subst.get(&tp.name.name).map(|t| t.display(tys)))
937 .collect();
938 if let Some(parts) = rendered {
939 ctx.hints
940 .record(name.span, format!("[{}]", parts.join(", ")));
941 }
942 }
943 let ret = substitute(ret_pattern, &subst, tys);
944 Some(ret)
949}
950
951fn check_call_against_fn(
952 name: &Ident,
953 fn_decl: &FnDecl,
954 type_args: &[TypeRef],
955 args: &[Expr],
956 ctx: &mut Ctx,
957) -> Option<TyId> {
958 let tys = ctx.tys;
959 if !fn_decl.type_params.is_empty() {
963 return check_generic_call(name, fn_decl, type_args, args, ctx);
964 }
965 if !type_args.is_empty() {
966 ctx.errors.push(CompileError::new(
967 "bynk.generics.type_arg_mismatch",
968 name.span,
969 format!(
970 "`{}` is not a generic function — it takes no type arguments",
971 name.name
972 ),
973 ));
974 for a in args {
975 let _ = type_of(a, None, ctx);
976 }
977 return None;
978 }
979 if fn_decl.params.len() != args.len() {
980 ctx.errors.push(
984 CompileError::new(
985 "bynk.resolve.arity_mismatch",
986 name.span,
987 format!(
988 "function `{}` expects {} argument(s), but {} were given",
989 name.name,
990 fn_decl.params.len(),
991 args.len()
992 ),
993 )
994 .with_label(fn_decl.name.ident().span, "function declared here"),
995 );
996 for a in args {
997 let _ = type_of(a, None, ctx);
998 }
999 return None;
1000 }
1001 let resolved_params: Vec<(Option<TyId>, &Param)> = fn_decl
1002 .params
1003 .iter()
1004 .map(|p| (resolve_type_ref(&p.type_ref, &ctx.input.types, tys), p))
1005 .collect();
1006 let mut ok = true;
1007 for (i, ((param_ty, param), arg)) in resolved_params.iter().zip(args.iter()).enumerate() {
1008 record_param_hint(ctx.hints, ¶m.name.name, arg);
1009 let arg_ty = type_of(arg, *param_ty, ctx);
1010 let (Some(arg_ty), Some(param_ty)) = (arg_ty, *param_ty) else {
1011 ok = false;
1012 continue;
1013 };
1014 if !compatible(arg_ty, param_ty, tys) {
1015 ctx.errors.push(
1016 CompileError::new(
1017 "bynk.types.argument_mismatch",
1018 arg.span,
1019 format!(
1020 "argument {} to `{}` has type `{}`, but parameter `{}` expects `{}`",
1021 i + 1,
1022 name.name,
1023 arg_ty.display(tys),
1024 param.name.name,
1025 param_ty.display(tys)
1026 ),
1027 )
1028 .with_label(param.span, "parameter declared here"),
1029 );
1030 ok = false;
1031 }
1032 }
1033 if !ok {
1034 return None;
1035 }
1036 resolve_type_ref(&fn_decl.return_type, &ctx.input.types, tys)
1037}
1038
1039pub(crate) fn check_arg(arg: &Expr, expected: TyId, what: &str, ctx: &mut Ctx) {
1042 let tys = ctx.tys;
1043 let Some(actual) = type_of(arg, Some(expected), ctx) else {
1044 return;
1045 };
1046 if !compatible(actual, expected, tys) {
1047 ctx.errors.push(CompileError::new(
1048 "bynk.types.type_mismatch",
1049 arg.span,
1050 format!(
1051 "{what} has type `{}`, but `{}` is required",
1052 actual.display(tys),
1053 expected.display(tys)
1054 ),
1055 ));
1056 }
1057}
1058
1059fn record_capability_ref(span: Span, name: &str, ctx: &mut Ctx) {
1064 if let Some(unit) = ctx.input.cross_context.flattened_caps.get(name) {
1065 ctx.refs
1066 .record_in_unit(span, SymbolKind::Capability, name, unit);
1067 } else {
1068 ctx.refs.record(span, SymbolKind::Capability, name);
1069 }
1070}
1071
1072#[allow(clippy::too_many_arguments)]
1073pub(crate) fn check_static_call(
1074 type_name: &Ident,
1075 method: &Ident,
1076 type_args: &[TypeRef],
1082 args: &[Expr],
1083 span: Span,
1084 expected: Option<TyId>,
1087 expr_id: ExprId,
1092 ctx: &mut Ctx,
1093) -> Option<TyId> {
1094 let tys = ctx.tys;
1095 if ctx.caps.declared_capabilities.contains_key(&type_name.name)
1099 && !ctx.caps.capabilities.contains_key(&type_name.name)
1100 {
1101 record_capability_ref(type_name.span, &type_name.name, ctx);
1102 ctx.callees.insert(
1103 expr_id,
1104 Callee::Capability {
1105 cap: type_name.name.clone(),
1106 op: method.name.clone(),
1107 },
1108 );
1109 let mut err = CompileError::new(
1110 "bynk.given.undeclared_capability",
1111 type_name.span,
1112 format!(
1113 "capability `{}` is used but not listed in the handler's `given` clause",
1114 type_name.name
1115 ),
1116 )
1117 .with_note(format!(
1118 "add `{}` to the handler's `given` clause so the dependency surface is visible at the declaration site",
1119 type_name.name
1120 ));
1121 if let Some((span, insert)) = given_insertion_edit(
1123 &ctx.caps.given_entries,
1124 ctx.caps.given_anchor,
1125 &type_name.name,
1126 ) {
1127 err = err.with_suggestion(
1128 format!("add `{}` to the `given` clause", type_name.name),
1129 vec![(span, insert)],
1130 Applicability::MachineApplicable,
1131 );
1132 }
1133 ctx.errors.push(err);
1134 record_requirement(
1139 ctx,
1140 &type_name.name,
1141 span,
1142 RequirementSource::DirectCall {
1143 op: method.name.clone(),
1144 },
1145 false,
1146 );
1147 for a in args {
1148 let _ = type_of(a, None, ctx);
1149 }
1150 return None;
1151 }
1152 if let Some(cap) = ctx.caps.capabilities.get(&type_name.name).cloned() {
1153 record_capability_ref(type_name.span, &type_name.name, ctx);
1154 ctx.callees.insert(
1155 expr_id,
1156 Callee::Capability {
1157 cap: type_name.name.clone(),
1158 op: method.name.clone(),
1159 },
1160 );
1161 if !ctx.effectful {
1162 ctx.errors.push(
1163 CompileError::new(
1164 "bynk.effect.capability_in_pure_context",
1165 span,
1166 format!(
1167 "capability `{}` can only be called inside an effectful body (one returning `Effect[T]`)",
1168 type_name.name
1169 ),
1170 ),
1171 );
1172 }
1173 ctx.caps.given_used.insert(type_name.name.clone());
1174 record_requirement(
1178 ctx,
1179 &type_name.name,
1180 span,
1181 RequirementSource::DirectCall {
1182 op: method.name.clone(),
1183 },
1184 true,
1185 );
1186 let Some(op) = cap.ops.iter().find(|o| o.name == method.name) else {
1187 ctx.errors.push(CompileError::new(
1188 "bynk.capability.unknown_operation",
1189 method.span,
1190 format!(
1191 "capability `{}` has no operation named `{}`",
1192 type_name.name, method.name
1193 ),
1194 ));
1195 for a in args {
1196 let _ = type_of(a, None, ctx);
1197 }
1198 return None;
1199 };
1200 ctx.refs.record(
1203 method.span,
1204 SymbolKind::CapabilityOp,
1205 &format!("{}.{}", type_name.name, method.name),
1206 );
1207 if op.params.len() != args.len() {
1208 ctx.errors.push(CompileError::new(
1209 "bynk.capability.op_arity",
1210 span,
1211 format!(
1212 "capability operation `{}.{}` expects {} argument(s), but {} were given",
1213 type_name.name,
1214 method.name,
1215 op.params.len(),
1216 args.len()
1217 ),
1218 ));
1219 for a in args {
1220 let _ = type_of(a, None, ctx);
1221 }
1222 return None;
1223 }
1224 let op_clone = op.clone();
1225 let mut subst: HashMap<String, TyId> = HashMap::new();
1232 if !op_clone.type_params.is_empty() || !type_args.is_empty() {
1233 if type_args.is_empty() {
1234 ctx.errors.push(
1235 CompileError::new(
1236 "bynk.generics.uninferable_type_arg",
1237 span,
1238 format!(
1239 "capability operation `{}.{}` takes a type parameter, but none of its arguments determine it",
1240 type_name.name, method.name
1241 ),
1242 )
1243 .with_note(format!(
1244 "give it explicitly: `{}.{}[T](…)`",
1245 type_name.name, method.name
1246 )),
1247 );
1248 for a in args {
1249 let _ = type_of(a, None, ctx);
1250 }
1251 return None;
1252 }
1253 if type_args.len() != op_clone.type_params.len() {
1254 ctx.errors.push(CompileError::new(
1255 "bynk.generics.type_arg_mismatch",
1256 span,
1257 format!(
1258 "capability operation `{}.{}` takes {} type argument(s), but {} were given",
1259 type_name.name,
1260 method.name,
1261 op_clone.type_params.len(),
1262 type_args.len()
1263 ),
1264 ));
1265 for a in args {
1266 let _ = type_of(a, None, ctx);
1267 }
1268 return None;
1269 }
1270 for (tp, ta) in op_clone.type_params.iter().zip(type_args) {
1271 let ty = resolve_expr_type_ref(ta, ctx)?;
1272 let is_first_party_events = type_name.name == "Events"
1291 && method.name == "emit"
1292 && (ctx.input.commons.name.joined() == crate::firstparty::BYNK_UNIT
1293 || ctx
1294 .input
1295 .cross_context
1296 .flattened_caps
1297 .get("Events")
1298 .map(String::as_str)
1299 == Some(crate::firstparty::BYNK_UNIT));
1300 if is_first_party_events && let Ty::Named { name: ename, .. } = &*tys.get(ty) {
1301 if ctx.input.is_local_event(ename) {
1302 } else if ctx.input.is_local_type(ename) {
1304 ctx.errors.push(
1313 CompileError::new(
1314 "bynk.event.emit_not_an_event",
1315 ta.span(),
1316 format!(
1317 "`{ename}` is not a declared `event` — `Events.emit` may only name an event type"
1318 ),
1319 )
1320 .with_note(
1321 "declare it with `event Name = { ... }`, or check that the type argument names the event you meant",
1322 ),
1323 );
1324 } else {
1325 ctx.errors.push(
1326 CompileError::new(
1327 "bynk.event.emit_outside_owner",
1328 ta.span(),
1329 format!(
1330 "`{ename}` is not declared in this context — only the context that declares an event may emit it"
1331 ),
1332 )
1333 .with_note(
1334 "a foreign event is visible via `consumes` for subscription (`from Events(...)`), but only its owning context may `Events.emit` it",
1335 ),
1336 );
1337 }
1338 }
1339 subst.insert(tp.clone(), ty);
1340 }
1341 }
1342 for (i, (param_ty, arg)) in op_clone.params.iter().zip(args.iter()).enumerate() {
1343 let param_ty = substitute(*param_ty, &subst, tys);
1344 let arg_ty = type_of(arg, Some(param_ty), ctx);
1345 if let Some(actual) = arg_ty
1346 && !compatible(actual, param_ty, tys)
1347 {
1348 ctx.errors.push(CompileError::new(
1349 "bynk.types.argument_mismatch",
1350 arg.span,
1351 format!(
1352 "argument {} to capability `{}.{}` has type `{}`, but parameter expects `{}`",
1353 i + 1,
1354 type_name.name,
1355 method.name,
1356 actual.display(tys),
1357 param_ty.display(tys)
1358 ),
1359 ));
1360 }
1361 }
1362 return Some(substitute(op_clone.return_ty, &subst, tys));
1363 }
1364 let decl = ctx.input.types.get(&type_name.name)?;
1365 ctx.refs
1366 .record(type_name.span, SymbolKind::Type, &type_name.name);
1367
1368 if let Some(method_decl) = ctx
1372 .input
1373 .methods
1374 .get(&type_name.name)
1375 .and_then(|table| table.statics.get(&method.name))
1376 {
1377 ctx.callees
1378 .insert(expr_id, Callee::Static(Arc::clone(method_decl)));
1379 return check_method_args(method_decl, args, ctx, type_name, method);
1380 }
1381
1382 if method.name == OF
1384 && let Some(base) = type_decl_base(decl)
1385 {
1386 ctx.callees
1387 .insert(expr_id, Callee::Refine(Arc::clone(decl)));
1388 if args.len() != 1 {
1389 ctx.errors.push(CompileError::new(
1390 "bynk.types.constructor_arity",
1391 span,
1392 format!(
1393 "constructor `{}.of` expects 1 argument, but {} were given",
1394 type_name.name,
1395 args.len()
1396 ),
1397 ));
1398 return None;
1399 }
1400 let arg = &args[0];
1401 let expected = tys.intern(Ty::Base(base));
1402 let arg_ty = type_of(arg, Some(expected), ctx)?;
1403 if !compatible(arg_ty, expected, tys) {
1404 ctx.errors.push(CompileError::new(
1405 "bynk.types.constructor_base_mismatch",
1406 arg.span,
1407 format!(
1408 "constructor `{}.of` expects a `{}` argument, but got `{}`",
1409 type_name.name,
1410 base.name(),
1411 arg_ty.display(tys)
1412 ),
1413 ));
1414 return None;
1415 }
1416 return Some(tys.intern(Ty::Result(
1422 named_ty(decl, tys),
1423 tys.intern(Ty::ValidationError),
1424 )));
1425 }
1426
1427 if method.name == UNSAFE
1430 && let TypeBody::Opaque { base, .. } = &decl.body
1431 {
1432 ctx.callees
1433 .insert(expr_id, Callee::Unsafe(Arc::clone(decl)));
1434 if !ctx.input.is_local_type(&decl.name.name) {
1435 ctx.errors.push(
1436 CompileError::new(
1437 "bynk.types.opaque_unsafe_outside",
1438 method.span,
1439 format!(
1440 "`{}.unsafe(...)` is only available within the commons that defines the opaque type `{}`",
1441 type_name.name, type_name.name
1442 ),
1443 )
1444 .with_note(
1445 "outside the defining commons, opaque values are constructed via `T.of(value)`",
1446 ),
1447 );
1448 return None;
1449 }
1450 if args.len() != 1 {
1451 ctx.errors.push(CompileError::new(
1452 "bynk.types.constructor_arity",
1453 span,
1454 format!(
1455 "`{}.unsafe` expects 1 argument, but {} were given",
1456 type_name.name,
1457 args.len()
1458 ),
1459 ));
1460 return None;
1461 }
1462 let arg = &args[0];
1463 let expected = tys.intern(Ty::Base(*base));
1464 let arg_ty = type_of(arg, Some(expected), ctx)?;
1465 if !compatible(arg_ty, expected, tys) {
1466 ctx.errors.push(CompileError::new(
1467 "bynk.types.constructor_base_mismatch",
1468 arg.span,
1469 format!(
1470 "`{}.unsafe` expects a `{}` argument, but got `{}`",
1471 type_name.name,
1472 base.name(),
1473 arg_ty.display(tys)
1474 ),
1475 ));
1476 return None;
1477 }
1478 return Some(named_ty(decl, tys));
1479 }
1480
1481 if let TypeBody::Sum(_) = &decl.body {
1483 ctx.callees.insert(
1484 expr_id,
1485 Callee::Ctor {
1486 sum: Arc::clone(decl),
1487 tag: method.name.clone(),
1488 },
1489 );
1490 return check_variant_construction(decl, &method.name, args, span, expected, ctx);
1491 }
1492
1493 ctx.errors.push(
1494 CompileError::new(
1495 "bynk.types.unknown_static_member",
1496 method.span,
1497 format!(
1498 "type `{}` has no static method or variant named `{}`",
1499 type_name.name, method.name
1500 ),
1501 )
1502 .with_note("type declared here"),
1505 );
1506 None
1507}
1508
1509fn check_method_args(
1510 method_decl: &FnDecl,
1511 args: &[Expr],
1512 ctx: &mut Ctx,
1513 type_name: &Ident,
1514 method: &Ident,
1515) -> Option<TyId> {
1516 let tys = ctx.tys;
1517 if method_decl.params.len() != args.len() {
1518 ctx.errors.push(
1519 CompileError::new(
1520 "bynk.types.method_arity",
1521 method.span,
1522 format!(
1523 "static method `{}.{}` expects {} argument(s), but {} were given",
1524 type_name.name,
1525 method.name,
1526 method_decl.params.len(),
1527 args.len()
1528 ),
1529 )
1530 .with_label(method_decl.name.ident().span, "method declared here"),
1531 );
1532 for a in args {
1533 let _ = type_of(a, None, ctx);
1534 }
1535 return None;
1536 }
1537 let mut ok = true;
1538 for (i, (param, arg)) in method_decl.params.iter().zip(args.iter()).enumerate() {
1539 record_param_hint(ctx.hints, ¶m.name.name, arg);
1540 let expected = resolve_type_ref(¶m.type_ref, &ctx.input.types, tys);
1541 let actual = type_of(arg, expected, ctx);
1542 let (Some(actual), Some(expected)) = (actual, expected) else {
1543 ok = false;
1544 continue;
1545 };
1546 if !compatible(actual, expected, tys) {
1547 ctx.errors.push(CompileError::new(
1548 "bynk.types.argument_mismatch",
1549 arg.span,
1550 format!(
1551 "argument {} to `{}.{}` has type `{}`, but parameter `{}` expects `{}`",
1552 i + 1,
1553 type_name.name,
1554 method.name,
1555 actual.display(tys),
1556 param.name.name,
1557 expected.display(tys)
1558 ),
1559 ));
1560 ok = false;
1561 }
1562 }
1563 if !ok {
1564 return None;
1565 }
1566 resolve_type_ref(&method_decl.return_type, &ctx.input.types, tys)
1567}
1568
1569pub(crate) fn check_store_map_op(
1577 method: &Ident,
1578 args: &[Expr],
1579 k: TyId,
1580 v: TyId,
1581 span: Span,
1582 ctx: &mut Ctx,
1583) -> Option<TyId> {
1584 let tys = ctx.tys;
1585 let vfn = || Ty::Fn {
1586 params: vec![v],
1587 ret: v,
1588 };
1589 if v.is_held(tys) && matches!(method.name.as_str(), "update" | "upsert") {
1596 ctx.errors.push(
1597 CompileError::new(
1598 "bynk.held.unsupported_map_op",
1599 method.span,
1600 format!(
1601 "a held `Map[K, Connection]` has no `{}` operation — a held resource cannot be transformed by a `(Connection) -> Connection` function",
1602 method.name
1603 ),
1604 )
1605 .with_note(
1606 "held connections are stored and resolved by identity; use `put`/`get`/`remove`",
1607 ),
1608 );
1609 for a in args {
1610 type_of(a, None, ctx);
1611 }
1612 return None;
1613 }
1614 let (expected, result): (Vec<TyId>, TyId) = match method.name.as_str() {
1615 "put" => (vec![k, v], tys.intern(Ty::Unit)),
1616 "get" => (vec![k], tys.intern(Ty::Option(v))),
1617 "remove" => (vec![k], tys.intern(Ty::Unit)),
1618 "contains" => (vec![k], tys.intern(Ty::Base(BaseType::Bool))),
1619 "size" => (vec![], tys.intern(Ty::Base(BaseType::Int))),
1620 "update" => (vec![k, tys.intern(vfn())], tys.intern(Ty::Unit)),
1621 "upsert" => (vec![k, v, tys.intern(vfn())], tys.intern(Ty::Unit)),
1622 other => {
1623 ctx.errors.push(
1624 CompileError::new(
1625 "bynk.store.unknown_op",
1626 method.span,
1627 format!(
1628 "a `Map` store field has no operation `{other}` — expected `put`, `get`, \
1629 `update`, `upsert`, `remove`, `contains`, or `size`"
1630 ),
1631 )
1632 .with_note("storage-map ops are entry-level and effectful (await with `<-`)"),
1633 );
1634 for a in args {
1635 type_of(a, None, ctx);
1636 }
1637 return None;
1638 }
1639 };
1640 let effect = Ty::Effect(result);
1641 if args.len() != expected.len() {
1642 ctx.errors.push(CompileError::new(
1643 "bynk.types.call_arity",
1644 span,
1645 format!(
1646 "`Map.{}` takes {} argument(s), found {}",
1647 method.name,
1648 expected.len(),
1649 args.len()
1650 ),
1651 ));
1652 for a in args {
1653 type_of(a, None, ctx);
1654 }
1655 return Some(tys.intern(effect));
1656 }
1657 for (a, exp) in args.iter().zip(expected.iter()) {
1658 if let Some(at) = type_of(a, Some(*exp), ctx)
1659 && !compatible(at, *exp, tys)
1660 {
1661 ctx.errors.push(CompileError::new(
1662 "bynk.types.argument_mismatch",
1663 a.span,
1664 format!(
1665 "expected `{}`, found `{}`",
1666 exp.display(tys),
1667 at.display(tys)
1668 ),
1669 ));
1670 }
1671 }
1672 Some(tys.intern(effect))
1673}
1674
1675fn require_capability(
1683 site: Span,
1684 capability: &str,
1685 source: RequirementSource,
1686 ctx: &mut Ctx,
1687 code: &'static str,
1688 message: &str,
1689) {
1690 let covered = ctx.caps.capabilities.contains_key(capability);
1691 if covered {
1692 ctx.caps.given_used.insert(capability.to_string());
1693 } else {
1694 ctx.errors
1695 .push(CompileError::new(code, site, message).with_note(format!(
1696 "add `{capability}` to the handler's `given` clause"
1697 )));
1698 }
1699 record_requirement(ctx, capability, site, source, covered);
1700}
1701
1702fn record_requirement(
1707 ctx: &mut Ctx,
1708 capability: &str,
1709 site: Span,
1710 source: RequirementSource,
1711 covered: bool,
1712) {
1713 let materialize = if covered {
1714 None
1715 } else {
1716 given_insertion_edit(&ctx.caps.given_entries, ctx.caps.given_anchor, capability).map(
1717 |(edit_span, edit_text)| Materialize {
1718 anchor: ctx.caps.given_anchor.unwrap_or(ctx.return_ty_span),
1719 edit_span,
1720 edit_text,
1721 },
1722 )
1723 };
1724 ctx.requirements.record(Requirement {
1725 capability: capability.to_string(),
1726 site,
1727 source,
1728 covered,
1729 materialize,
1730 });
1731}
1732
1733pub(crate) fn check_store_cache_op(
1738 method: &Ident,
1739 args: &[Expr],
1740 k: TyId,
1741 v: TyId,
1742 span: Span,
1743 ctx: &mut Ctx,
1744) -> Option<TyId> {
1745 let tys = ctx.tys;
1746 let vfn = || Ty::Fn {
1747 params: vec![v],
1748 ret: v,
1749 };
1750 let (expected, result): (Vec<TyId>, TyId) = match method.name.as_str() {
1751 "put" => (vec![k, v], tys.intern(Ty::Unit)),
1752 "get" => (vec![k], tys.intern(Ty::Option(v))),
1753 "remove" => (vec![k], tys.intern(Ty::Unit)),
1754 "contains" => (vec![k], tys.intern(Ty::Base(BaseType::Bool))),
1755 "size" => (vec![], tys.intern(Ty::Base(BaseType::Int))),
1756 "update" => (vec![k, tys.intern(vfn())], tys.intern(Ty::Unit)),
1757 "upsert" => (vec![k, v, tys.intern(vfn())], tys.intern(Ty::Unit)),
1758 other => {
1759 ctx.errors.push(
1760 CompileError::new(
1761 "bynk.store.unknown_op",
1762 method.span,
1763 format!(
1764 "a `Cache` store field has no operation `{other}` — expected `put`, \
1765 `get`, `update`, `upsert`, `remove`, `contains`, or `size`"
1766 ),
1767 )
1768 .with_note("storage-cache ops are entry-level and effectful (await with `<-`)"),
1769 );
1770 for a in args {
1771 type_of(a, None, ctx);
1772 }
1773 return None;
1774 }
1775 };
1776 if method.name != "remove" {
1778 require_capability(
1779 method.span,
1780 "Clock",
1781 RequirementSource::StoreOp {
1782 kind: StoreKind::Cache,
1783 op: method.name.clone(),
1784 },
1785 ctx,
1786 "bynk.store.cache_needs_clock",
1787 "a `Cache` operation applies TTL expiry, which reads the clock — the handler must declare `given Clock`",
1788 );
1789 }
1790 let effect = Ty::Effect(result);
1791 if args.len() != expected.len() {
1792 ctx.errors.push(CompileError::new(
1793 "bynk.types.call_arity",
1794 span,
1795 format!(
1796 "`Cache.{}` takes {} argument(s), found {}",
1797 method.name,
1798 expected.len(),
1799 args.len()
1800 ),
1801 ));
1802 for a in args {
1803 type_of(a, None, ctx);
1804 }
1805 return Some(tys.intern(effect));
1806 }
1807 for (a, exp) in args.iter().zip(expected.iter()) {
1808 if let Some(at) = type_of(a, Some(*exp), ctx)
1809 && !compatible(at, *exp, tys)
1810 {
1811 ctx.errors.push(CompileError::new(
1812 "bynk.types.argument_mismatch",
1813 a.span,
1814 format!(
1815 "expected `{}`, found `{}`",
1816 exp.display(tys),
1817 at.display(tys)
1818 ),
1819 ));
1820 }
1821 }
1822 Some(tys.intern(effect))
1823}
1824
1825pub(crate) fn check_store_log_op(
1833 method: &Ident,
1834 args: &[Expr],
1835 elem: TyId,
1836 span: Span,
1837 ctx: &mut Ctx,
1838) -> Option<TyId> {
1839 let tys = ctx.tys;
1840 let query = || Ty::Query(elem);
1841 let arity = |n: usize, ctx: &mut Ctx| {
1842 if args.len() != n {
1843 ctx.errors.push(CompileError::new(
1844 "bynk.types.call_arity",
1845 span,
1846 format!(
1847 "`Log.{}` takes {n} argument(s), found {}",
1848 method.name,
1849 args.len()
1850 ),
1851 ));
1852 for a in args {
1853 type_of(a, None, ctx);
1854 }
1855 return false;
1856 }
1857 true
1858 };
1859 let window_arg = |a: &Expr, what: &str, ctx: &mut Ctx| {
1860 if let Some(at) = type_of(a, Some(tys.intern(Ty::Base(BaseType::Instant))), ctx)
1861 && !compatible(at, tys.intern(Ty::Base(BaseType::Instant)), tys)
1862 {
1863 ctx.errors.push(CompileError::new(
1864 "bynk.types.argument_mismatch",
1865 a.span,
1866 format!("{what} expects `Instant`, found `{}`", at.display(tys)),
1867 ));
1868 }
1869 };
1870 match method.name.as_str() {
1871 "append" => {
1873 require_capability(
1874 method.span,
1875 "Clock",
1876 RequirementSource::StoreOp {
1877 kind: StoreKind::Log,
1878 op: method.name.clone(),
1879 },
1880 ctx,
1881 "bynk.store.log_needs_clock",
1882 "`Log.append` stamps the current time, which reads the clock — the handler must declare `given Clock`",
1883 );
1884 if !arity(1, ctx) {
1885 return Some(tys.intern(Ty::Effect(tys.intern(Ty::Unit))));
1886 }
1887 if let Some(at) = type_of(&args[0], Some(elem), ctx)
1888 && !compatible(at, elem, tys)
1889 {
1890 ctx.errors.push(CompileError::new(
1891 "bynk.types.argument_mismatch",
1892 args[0].span,
1893 format!(
1894 "expected `{}`, found `{}`",
1895 elem.display(tys),
1896 at.display(tys)
1897 ),
1898 ));
1899 }
1900 Some(tys.intern(Ty::Effect(tys.intern(Ty::Unit))))
1901 }
1902 "since" | "before" => {
1904 if !arity(1, ctx) {
1905 return Some(tys.intern(query()));
1906 }
1907 window_arg(&args[0], &format!("`Log.{}`", method.name), ctx);
1908 Some(tys.intern(query()))
1909 }
1910 "between" => {
1911 if !arity(2, ctx) {
1912 return Some(tys.intern(query()));
1913 }
1914 window_arg(&args[0], "`Log.between` start", ctx);
1915 window_arg(&args[1], "`Log.between` end", ctx);
1916 Some(tys.intern(query()))
1917 }
1918 "recent" => {
1919 if !arity(1, ctx) {
1920 return Some(tys.intern(query()));
1921 }
1922 check_arg(
1923 &args[0],
1924 tys.intern(Ty::Base(BaseType::Int)),
1925 "the `Log.recent` count",
1926 ctx,
1927 );
1928 Some(tys.intern(query()))
1929 }
1930 "reversed" => {
1931 if !arity(0, ctx) {
1932 return Some(tys.intern(query()));
1933 }
1934 Some(tys.intern(query()))
1935 }
1936 name if is_query_op(name) => check_query_kernel_method(method, args, elem, span, ctx),
1938 other => {
1939 ctx.errors.push(
1940 CompileError::new(
1941 "bynk.store.unknown_op",
1942 method.span,
1943 format!(
1944 "a `Log` store field has no operation `{other}` — `append`, the \
1945 time-window roots (`since`/`before`/`between`/`recent`/`reversed`), \
1946 and the query builders/terminals"
1947 ),
1948 )
1949 .with_note(
1950 "`Log` reads are lazy `Query[T]`; only `append` is effectful and writes",
1951 ),
1952 );
1953 for a in args {
1954 type_of(a, None, ctx);
1955 }
1956 None
1957 }
1958 }
1959}
1960
1961pub(crate) fn check_store_set_op(
1967 method: &Ident,
1968 args: &[Expr],
1969 t: TyId,
1970 span: Span,
1971 ctx: &mut Ctx,
1972) -> Option<TyId> {
1973 let tys = ctx.tys;
1974 let (expected, result): (Vec<TyId>, TyId) = match method.name.as_str() {
1975 "add" => (vec![t], tys.intern(Ty::Unit)),
1976 "remove" => (vec![t], tys.intern(Ty::Unit)),
1977 "contains" => (vec![t], tys.intern(Ty::Base(BaseType::Bool))),
1978 "size" => (vec![], tys.intern(Ty::Base(BaseType::Int))),
1979 other => {
1980 ctx.errors.push(
1981 CompileError::new(
1982 "bynk.store.unknown_op",
1983 method.span,
1984 format!(
1985 "a `Set` store field has no operation `{other}` — expected `add`, \
1986 `remove`, `contains`, or `size`"
1987 ),
1988 )
1989 .with_note(
1990 "set algebra (`union`/`intersection`/`difference`) is not in this slice",
1991 ),
1992 );
1993 for a in args {
1994 type_of(a, None, ctx);
1995 }
1996 return None;
1997 }
1998 };
1999 let effect = Ty::Effect(result);
2000 if args.len() != expected.len() {
2001 ctx.errors.push(CompileError::new(
2002 "bynk.types.call_arity",
2003 span,
2004 format!(
2005 "`Set.{}` takes {} argument(s), found {}",
2006 method.name,
2007 expected.len(),
2008 args.len()
2009 ),
2010 ));
2011 for a in args {
2012 type_of(a, None, ctx);
2013 }
2014 return Some(tys.intern(effect));
2015 }
2016 for (a, exp) in args.iter().zip(expected.iter()) {
2017 if let Some(at) = type_of(a, Some(*exp), ctx)
2018 && !compatible(at, *exp, tys)
2019 {
2020 ctx.errors.push(CompileError::new(
2021 "bynk.types.argument_mismatch",
2022 a.span,
2023 format!(
2024 "expected `{}`, found `{}`",
2025 exp.display(tys),
2026 at.display(tys)
2027 ),
2028 ));
2029 }
2030 }
2031 Some(tys.intern(effect))
2032}
2033
2034pub(crate) fn check_store_cell_op(
2042 method: &Ident,
2043 args: &[Expr],
2044 t: TyId,
2045 span: Span,
2046 ctx: &mut Ctx,
2047) -> Option<TyId> {
2048 let tys = ctx.tys;
2049 let tfn = || Ty::Fn {
2050 params: vec![t],
2051 ret: t,
2052 };
2053 let (expected, result): (Vec<TyId>, TyId) = match method.name.as_str() {
2054 "update" => (vec![tys.intern(tfn())], tys.intern(Ty::Unit)),
2055 other => {
2056 ctx.errors.push(
2057 CompileError::new(
2058 "bynk.store.unknown_op",
2059 method.span,
2060 format!("a `Cell` store field has no operation `{other}` — expected `update`"),
2061 )
2062 .with_note(
2063 "a cell is read by its bare name and written with `:=`; `update` is the only \
2064 method-shaped op",
2065 ),
2066 );
2067 for a in args {
2068 type_of(a, None, ctx);
2069 }
2070 return None;
2071 }
2072 };
2073 let effect = Ty::Effect(result);
2074 if args.len() != expected.len() {
2075 ctx.errors.push(CompileError::new(
2076 "bynk.types.call_arity",
2077 span,
2078 format!(
2079 "`Cell.{}` takes {} argument(s), found {}",
2080 method.name,
2081 expected.len(),
2082 args.len()
2083 ),
2084 ));
2085 for a in args {
2086 type_of(a, None, ctx);
2087 }
2088 return Some(tys.intern(effect));
2089 }
2090 for (a, exp) in args.iter().zip(expected.iter()) {
2091 if let Some(at) = type_of(a, Some(*exp), ctx)
2092 && !compatible(at, *exp, tys)
2093 {
2094 ctx.errors.push(CompileError::new(
2095 "bynk.types.argument_mismatch",
2096 a.span,
2097 format!(
2098 "expected `{}`, found `{}`",
2099 exp.display(tys),
2100 at.display(tys)
2101 ),
2102 ));
2103 }
2104 }
2105 Some(tys.intern(effect))
2106}
2107
2108#[allow(clippy::too_many_arguments)]
2109pub(crate) fn check_method_call(
2110 receiver: &Expr,
2111 method: &Ident,
2112 type_args: &[TypeRef],
2113 args: &[Expr],
2114 span: Span,
2115 expected: Option<TyId>,
2116 expr_id: ExprId,
2118 ctx: &mut Ctx,
2119) -> Option<TyId> {
2120 let tys = ctx.tys;
2121 let receiver_is_capability = matches!(&receiver.kind, ExprKind::Ident(id)
2136 if ctx.caps.capabilities.contains_key(&id.name)
2137 || ctx.caps.declared_capabilities.contains_key(&id.name))
2138 || flatten_ident_chain(receiver).is_some_and(|chain| {
2139 ctx.input
2140 .cross_context
2141 .resolve_cross_capability(&chain)
2142 .is_some()
2143 });
2144 if !type_args.is_empty()
2145 && !matches!(&receiver.kind, ExprKind::Ident(id) if id.name == JSON
2146 && !ctx.input.types.contains_key(JSON))
2147 && !receiver_is_capability
2148 {
2149 ctx.errors.push(CompileError::new(
2150 "bynk.generics.type_arg_mismatch",
2151 span,
2152 format!(
2153 "`{}` does not take explicit type arguments — a generic method infers them from the receiver and arguments",
2154 method.name
2155 ),
2156 ));
2157 for a in args {
2158 let _ = type_of(a, None, ctx);
2159 }
2160 return None;
2161 }
2162 if let ExprKind::Ident(id) = &receiver.kind
2174 && ctx.lookup(id.name.as_str()).is_none()
2175 && let Some(sig) = ctx.test_services.get(&id.name).cloned()
2176 {
2177 if let Some(unit) = ctx.input.cross_context.self_context.clone() {
2178 ctx.refs
2179 .record_in_unit(id.span, SymbolKind::Service, &id.name, &unit);
2180 }
2181 return check_test_service_address(&sig, id, method, args, expr_id, ctx);
2182 }
2183 if ctx.lookup_root_ident(receiver).is_none() && !ctx.root_ident_is_store_field(receiver) {
2190 if let Some(chain) = flatten_ident_chain(receiver)
2194 && let Some((consumed, cap)) = ctx.input.cross_context.resolve_cross_capability(&chain)
2195 {
2196 if let ExprKind::FieldAccess { field, .. } = &receiver.kind {
2199 ctx.refs
2200 .record_in_unit(field.span, SymbolKind::Capability, &cap, &consumed);
2201 }
2202 return check_cross_context_capability_call(
2203 receiver, &consumed, &cap, method, type_args, args, span, expr_id, ctx,
2204 );
2205 }
2206 if let Some(consumed) = cross_context_prefix(receiver, ctx) {
2207 return check_cross_context_call(receiver, &consumed, method, args, span, expr_id, ctx);
2208 }
2209 if let ExprKind::FieldAccess { .. } = &receiver.kind
2215 && let Some(chain) = flatten_ident_chain(receiver)
2216 && chain.contains('.')
2217 {
2218 let info = &ctx.input.cross_context;
2219 let in_context = info.self_context.is_some();
2220 if in_context && info.resolve_prefix(&chain).is_none() {
2221 ctx.errors.push(
2222 CompileError::new(
2223 "bynk.resolve.unconsumed_context",
2224 receiver.span,
2225 format!(
2226 "`{chain}.{}` looks like a cross-context service call, but `{chain}` is not in this context's `consumes` clauses",
2227 method.name
2228 ),
2229 )
2230 .with_note(
2231 "add a `consumes {chain}` clause at the top of the context, or use an alias and call it through the alias",
2232 ),
2233 );
2234 for a in args {
2235 let _ = type_of(a, None, ctx);
2236 }
2237 return None;
2238 }
2239 }
2240 }
2241 if let ExprKind::Ident(id) = &receiver.kind
2245 && ctx.lookup(id.name.as_str()).is_none()
2246 && (ctx.caps.capabilities.contains_key(&id.name)
2247 || ctx.caps.declared_capabilities.contains_key(&id.name))
2248 {
2249 return check_static_call(id, method, type_args, args, span, expected, expr_id, ctx);
2250 }
2251 if let ExprKind::Ident(id) = &receiver.kind
2257 && ctx.lookup(id.name.as_str()).is_none()
2258 && ctx.input.types.contains_key(&id.name)
2259 {
2260 return check_static_call(id, method, type_args, args, span, expected, expr_id, ctx);
2261 }
2262 if let ExprKind::Ident(id) = &receiver.kind
2266 && ctx.lookup(id.name.as_str()).is_none()
2267 && !ctx.input.types.contains_key(&id.name)
2268 && (id.name == LIST || id.name == MAP)
2269 {
2270 let ns = if id.name == LIST { LIST } else { MAP };
2271 ctx.callees.insert(
2272 expr_id,
2273 Callee::Intrinsic {
2274 ns,
2275 op: method.name.clone(),
2276 },
2277 );
2278 return check_collection_static(id, method, args, span, expected, ctx);
2279 }
2280 if let ExprKind::Ident(id) = &receiver.kind
2284 && (id.name == INT || id.name == FLOAT)
2285 {
2286 let ns = if id.name == INT { INT } else { FLOAT };
2287 ctx.callees.insert(
2288 expr_id,
2289 Callee::Intrinsic {
2290 ns,
2291 op: method.name.clone(),
2292 },
2293 );
2294 return check_numeric_parse_static(id, method, args, span, ctx);
2295 }
2296 if let ExprKind::Ident(id) = &receiver.kind
2299 && id.name == DURATION
2300 && ctx.lookup(DURATION).is_none()
2301 && !ctx.input.types.contains_key(DURATION)
2302 {
2303 ctx.callees.insert(
2304 expr_id,
2305 Callee::Intrinsic {
2306 ns: DURATION,
2307 op: method.name.clone(),
2308 },
2309 );
2310 return check_duration_static(method, args, span, ctx);
2311 }
2312 if let ExprKind::Ident(id) = &receiver.kind
2314 && id.name == INSTANT
2315 && ctx.lookup(INSTANT).is_none()
2316 && !ctx.input.types.contains_key(INSTANT)
2317 {
2318 ctx.callees.insert(
2319 expr_id,
2320 Callee::Intrinsic {
2321 ns: INSTANT,
2322 op: method.name.clone(),
2323 },
2324 );
2325 return check_instant_static(method, args, span, ctx);
2326 }
2327 if let ExprKind::Ident(id) = &receiver.kind
2330 && id.name == BYTES
2331 && ctx.lookup(BYTES).is_none()
2332 && !ctx.input.types.contains_key(BYTES)
2333 {
2334 ctx.callees.insert(
2335 expr_id,
2336 Callee::Intrinsic {
2337 ns: BYTES,
2338 op: method.name.clone(),
2339 },
2340 );
2341 return check_bytes_static(method, args, span, ctx);
2342 }
2343 if let ExprKind::Ident(id) = &receiver.kind
2345 && id.name == JSON
2346 && ctx.lookup(JSON).is_none()
2347 && !ctx.input.types.contains_key(JSON)
2348 {
2349 ctx.callees.insert(
2350 expr_id,
2351 Callee::Intrinsic {
2352 ns: JSON,
2353 op: method.name.clone(),
2354 },
2355 );
2356 return check_json_static(method, type_args, args, span, expected, ctx);
2357 }
2358 if let ExprKind::Ident(id) = &receiver.kind
2360 && id.name == STREAM
2361 && ctx.lookup(STREAM).is_none()
2362 && !ctx.input.types.contains_key(STREAM)
2363 {
2364 ctx.callees.insert(
2365 expr_id,
2366 Callee::Intrinsic {
2367 ns: STREAM,
2368 op: method.name.clone(),
2369 },
2370 );
2371 return check_stream_static(method, args, span, ctx);
2372 }
2373 let recv_expected = match (expected, method.name.as_str()) {
2377 (Some(t), "insert") => peel_to_map(t, tys).map(|(k, v)| tys.intern(Ty::Map(k, v))),
2378 (Some(t), "prepend") => peel_to_list(t, tys).map(|e| tys.intern(Ty::List(e))),
2379 _ => None,
2380 };
2381 let recv_ty = type_of(receiver, recv_expected, ctx)?;
2382 match &*tys.get(recv_ty) {
2387 Ty::List(elem) => {
2388 ctx.callees.insert(
2389 expr_id,
2390 Callee::Kernel {
2391 recv: recv_ty,
2392 op: method.name.clone(),
2393 },
2394 );
2395 return check_list_kernel_method(method, args, *elem, span, ctx);
2396 }
2397 Ty::Query(elem) => {
2399 ctx.callees.insert(
2400 expr_id,
2401 Callee::Kernel {
2402 recv: recv_ty,
2403 op: method.name.clone(),
2404 },
2405 );
2406 return check_query_kernel_method(method, args, *elem, span, ctx);
2407 }
2408 Ty::Stream(elem) => {
2410 ctx.callees.insert(
2411 expr_id,
2412 Callee::Kernel {
2413 recv: recv_ty,
2414 op: method.name.clone(),
2415 },
2416 );
2417 return check_stream_kernel_method(method, args, *elem, span, ctx);
2418 }
2419 Ty::Connection(frame) => {
2423 ctx.callees.insert(
2424 expr_id,
2425 Callee::Kernel {
2426 recv: recv_ty,
2427 op: method.name.clone(),
2428 },
2429 );
2430 return check_connection_method(method, args, *frame, span, ctx);
2431 }
2432 Ty::Map(key, val) => {
2433 ctx.callees.insert(
2434 expr_id,
2435 Callee::Kernel {
2436 recv: recv_ty,
2437 op: method.name.clone(),
2438 },
2439 );
2440 return check_map_kernel_method(method, args, *key, *val, span, ctx);
2441 }
2442 Ty::Base(base @ (BaseType::Int | BaseType::Float)) => {
2450 ctx.callees.insert(
2451 expr_id,
2452 Callee::Kernel {
2453 recv: recv_ty,
2454 op: method.name.clone(),
2455 },
2456 );
2457 return check_numeric_kernel_method(method, args, *base, span, ctx);
2458 }
2459 Ty::Base(BaseType::Duration) => {
2461 ctx.callees.insert(
2462 expr_id,
2463 Callee::Kernel {
2464 recv: recv_ty,
2465 op: method.name.clone(),
2466 },
2467 );
2468 return check_duration_kernel_method(method, args, span, ctx);
2469 }
2470 Ty::Base(BaseType::Instant) => {
2472 ctx.callees.insert(
2473 expr_id,
2474 Callee::Kernel {
2475 recv: recv_ty,
2476 op: method.name.clone(),
2477 },
2478 );
2479 return check_instant_kernel_method(method, args, span, ctx);
2480 }
2481 Ty::Base(BaseType::Bytes) => {
2483 ctx.callees.insert(
2484 expr_id,
2485 Callee::Kernel {
2486 recv: recv_ty,
2487 op: method.name.clone(),
2488 },
2489 );
2490 return check_bytes_kernel_method(method, args, span, ctx);
2491 }
2492 Ty::Base(BaseType::String) => {
2494 ctx.callees.insert(
2495 expr_id,
2496 Callee::Kernel {
2497 recv: recv_ty,
2498 op: method.name.clone(),
2499 },
2500 );
2501 return check_string_kernel_method(method, args, span, ctx);
2502 }
2503 Ty::Option(inner) => {
2505 ctx.callees.insert(
2506 expr_id,
2507 Callee::Kernel {
2508 recv: recv_ty,
2509 op: method.name.clone(),
2510 },
2511 );
2512 return check_option_kernel_method(method, args, *inner, span, ctx);
2513 }
2514 Ty::Result(ok, err) => {
2515 ctx.callees.insert(
2516 expr_id,
2517 Callee::Kernel {
2518 recv: recv_ty,
2519 op: method.name.clone(),
2520 },
2521 );
2522 return check_result_kernel_method(method, args, *ok, *err, span, ctx);
2523 }
2524 Ty::Effect(inner) => {
2529 if let Ty::Result(ok, err) = &*tys.get(*inner) {
2530 ctx.callees.insert(
2531 expr_id,
2532 Callee::Kernel {
2533 recv: recv_ty,
2534 op: method.name.clone(),
2535 },
2536 );
2537 return check_effect_result_kernel_method(method, args, *ok, *err, span, ctx);
2538 }
2539 }
2540 _ => {}
2541 }
2542 let type_name = match &*tys.get(recv_ty) {
2544 Ty::Named { name, .. } => name.clone(),
2545 _ => {
2546 ctx.errors.push(CompileError::new(
2547 "bynk.types.method_on_non_named_type",
2548 method.span,
2549 format!(
2550 "type `{}` has no methods — only user-declared types support method calls",
2551 recv_ty.display(tys)
2552 ),
2553 ));
2554 return None;
2555 }
2556 };
2557 if let Some(agent) = ctx.input.agents.get(&type_name).cloned() {
2561 let Some(handler) = agent.handlers.iter().find(|h| {
2562 h.method_name
2563 .as_ref()
2564 .is_some_and(|n| n.name == method.name)
2565 }) else {
2566 ctx.errors.push(CompileError::new(
2567 "bynk.agent.handler_not_found",
2568 method.span,
2569 format!(
2570 "agent `{}` has no handler named `{}`",
2571 type_name, method.name
2572 ),
2573 ));
2574 for a in args {
2575 let _ = type_of(a, None, ctx);
2576 }
2577 return None;
2578 };
2579 ctx.callees.insert(
2580 expr_id,
2581 Callee::Agent {
2582 agent: type_name.clone(),
2583 handler: method.name.clone(),
2584 },
2585 );
2586 ctx.refs.record(
2591 method.span,
2592 SymbolKind::Handler,
2593 &format!("{type_name}.{}", method.name),
2594 );
2595 if handler.params.len() != args.len() {
2596 ctx.errors.push(CompileError::new(
2597 "bynk.agent.handler_arity",
2598 method.span,
2599 format!(
2600 "agent handler `{}.{}` expects {} argument(s), but {} were given",
2601 type_name,
2602 method.name,
2603 handler.params.len(),
2604 args.len()
2605 ),
2606 ));
2607 for a in args {
2608 let _ = type_of(a, None, ctx);
2609 }
2610 return None;
2611 }
2612 for (p, arg) in handler.params.iter().zip(args.iter()) {
2613 let pty = resolve_type_ref(&p.type_ref, &ctx.input.types, tys);
2614 let arg_ty = type_of(arg, pty, ctx);
2615 if let (Some(a), Some(p_ty)) = (arg_ty, pty.as_ref())
2616 && !compatible(a, *p_ty, tys)
2617 {
2618 ctx.errors.push(CompileError::new(
2619 "bynk.types.argument_mismatch",
2620 arg.span,
2621 format!(
2622 "argument has type `{}`, but `{}.{}` expects `{}`",
2623 a.display(tys),
2624 type_name,
2625 method.name,
2626 p_ty.display(tys)
2627 ),
2628 ));
2629 }
2630 }
2631 return resolve_type_ref(&handler.return_type, &ctx.input.types, tys);
2632 }
2633 let table = ctx
2634 .input
2635 .methods
2636 .get(&type_name)
2637 .cloned()
2638 .unwrap_or_default();
2639 let Some(method_decl) = table.instance.get(&method.name).cloned() else {
2640 if let Ty::Named {
2650 kind: NamedKind::Refined(base),
2651 ..
2652 } = &*tys.get(recv_ty)
2653 {
2654 if !matches!(base, BaseType::Bool) {
2655 ctx.callees.insert(
2656 expr_id,
2657 Callee::Kernel {
2658 recv: recv_ty,
2659 op: method.name.clone(),
2660 },
2661 );
2662 }
2663 match base {
2664 BaseType::Int | BaseType::Float => {
2665 return check_numeric_kernel_method(method, args, *base, span, ctx);
2666 }
2667 BaseType::String => return check_string_kernel_method(method, args, span, ctx),
2668 BaseType::Duration => return check_duration_kernel_method(method, args, span, ctx),
2669 BaseType::Instant => return check_instant_kernel_method(method, args, span, ctx),
2670 BaseType::Bytes => return check_bytes_kernel_method(method, args, span, ctx),
2671 BaseType::Bool => {}
2672 }
2673 }
2674 ctx.errors.push(CompileError::new(
2675 "bynk.types.method_not_found",
2676 method.span,
2677 format!(
2678 "type `{}` has no instance method named `{}`",
2679 type_name, method.name
2680 ),
2681 ));
2682 return None;
2683 };
2684 ctx.refs.record(
2689 method.span,
2690 SymbolKind::Method,
2691 &format!("{type_name}.{}", method.name),
2692 );
2693 ctx.callees
2694 .insert(expr_id, Callee::Method(Arc::clone(&method_decl)));
2695 let recv_type_params: Vec<String> = ctx
2703 .input
2704 .types
2705 .get(&type_name)
2706 .map(|d| d.type_params.iter().map(|p| p.name.name.clone()).collect())
2707 .unwrap_or_default();
2708 if !recv_type_params.is_empty() || !method_decl.type_params.is_empty() {
2709 return check_generic_method_call(
2710 &type_name,
2711 &recv_type_params,
2712 recv_ty,
2713 &method_decl,
2714 method,
2715 args,
2716 ctx,
2717 );
2718 }
2719 if method_decl.params.len() != args.len() {
2721 ctx.errors.push(
2722 CompileError::new(
2723 "bynk.types.method_arity",
2724 method.span,
2725 format!(
2726 "method `{}.{}` expects {} argument(s), but {} were given",
2727 type_name,
2728 method.name,
2729 method_decl.params.len(),
2730 args.len()
2731 ),
2732 )
2733 .with_label(method_decl.name.ident().span, "method declared here"),
2734 );
2735 for a in args {
2736 let _ = type_of(a, None, ctx);
2737 }
2738 return None;
2739 }
2740 let mut ok = true;
2741 for (i, (param, arg)) in method_decl.params.iter().zip(args.iter()).enumerate() {
2742 record_param_hint(ctx.hints, ¶m.name.name, arg);
2743 let expected = resolve_type_ref(¶m.type_ref, &ctx.input.types, tys);
2744 let actual = type_of(arg, expected, ctx);
2745 let (Some(actual), Some(expected)) = (actual, expected) else {
2746 ok = false;
2747 continue;
2748 };
2749 if !compatible(actual, expected, tys) {
2750 ctx.errors.push(CompileError::new(
2751 "bynk.types.argument_mismatch",
2752 arg.span,
2753 format!(
2754 "argument {} to `{}.{}` has type `{}`, but parameter `{}` expects `{}`",
2755 i + 1,
2756 type_name,
2757 method.name,
2758 actual.display(tys),
2759 param.name.name,
2760 expected.display(tys)
2761 ),
2762 ));
2763 ok = false;
2764 }
2765 }
2766 let _ = span;
2767 if !ok {
2768 return None;
2769 }
2770 resolve_type_ref(&method_decl.return_type, &ctx.input.types, tys)
2771}
2772
2773fn check_generic_method_call(
2782 type_name: &str,
2783 recv_type_params: &[String],
2784 recv_ty: TyId,
2785 method_decl: &FnDecl,
2786 method: &Ident,
2787 args: &[Expr],
2788 ctx: &mut Ctx,
2789) -> Option<TyId> {
2790 let tys = ctx.tys;
2791 let mut vars: HashSet<String> = recv_type_params.iter().cloned().collect();
2793 for tp in &method_decl.type_params {
2794 vars.insert(tp.name.name.clone());
2795 }
2796 if method_decl.params.len() != args.len() {
2798 ctx.errors.push(
2799 CompileError::new(
2800 "bynk.types.method_arity",
2801 method.span,
2802 format!(
2803 "method `{}.{}` expects {} argument(s), but {} were given",
2804 type_name,
2805 method.name,
2806 method_decl.params.len(),
2807 args.len()
2808 ),
2809 )
2810 .with_label(method_decl.name.ident().span, "method declared here"),
2811 );
2812 for a in args {
2813 let _ = type_of(a, None, ctx);
2814 }
2815 return None;
2816 }
2817 let mut subst: HashMap<String, TyId> = HashMap::new();
2824 if let Ty::Named {
2825 args: recv_args, ..
2826 } = &*tys.get(recv_ty)
2827 && recv_args.len() == recv_type_params.len()
2828 {
2829 for (name, arg) in recv_type_params.iter().zip(recv_args.iter()) {
2830 subst.insert(name.clone(), *arg);
2831 }
2832 }
2833 let var_params: Vec<Option<TyId>> = method_decl
2836 .params
2837 .iter()
2838 .map(|p| resolve_type_ref_in(&p.type_ref, &ctx.input.types, &vars, tys))
2839 .collect();
2840 let ret_pattern = resolve_type_ref_in(&method_decl.return_type, &ctx.input.types, &vars, tys)?;
2841
2842 let mut arg_tys: Vec<Option<TyId>> = vec![None; args.len()];
2843 for (i, arg) in args.iter().enumerate() {
2845 if matches!(arg.kind, ExprKind::Lambda(_)) {
2846 continue;
2847 }
2848 let expected = var_params[i].map(|p| substitute(p, &subst, tys));
2849 let ty = type_of(arg, expected, ctx);
2850 if let (Some(pattern), Some(actual)) = (var_params[i], ty)
2851 && !unify(pattern, actual, &mut subst, tys)
2852 {
2853 ctx.errors.push(CompileError::new(
2854 "bynk.generics.type_arg_mismatch",
2855 arg.span,
2856 format!(
2857 "argument {} infers a type for `{}.{}`'s type parameter that conflicts with an earlier argument or the receiver",
2858 i + 1,
2859 type_name,
2860 method.name
2861 ),
2862 ));
2863 return None;
2864 }
2865 arg_tys[i] = ty;
2866 }
2867 for (i, arg) in args.iter().enumerate() {
2869 if !matches!(arg.kind, ExprKind::Lambda(_)) {
2870 continue;
2871 }
2872 let expected = var_params[i].map(|p| substitute(p, &subst, tys));
2873 let params_unconstrained = expected.is_some_and(|e| {
2874 matches!(&*tys.get(e), Ty::Fn { params, .. }
2875 if params.iter().any(|p| contains_var(*p, tys)))
2876 });
2877 let fully_annotated = matches!(
2878 &arg.kind,
2879 ExprKind::Lambda(l) if l.params.iter().all(|p| p.type_ref.is_some())
2880 );
2881 if params_unconstrained && !fully_annotated {
2882 ctx.errors.push(
2883 CompileError::new(
2884 "bynk.generics.uninferable_type_arg",
2885 arg.span,
2886 format!(
2887 "the lambda's parameter types depend on `{}.{}`'s type parameters, which the receiver and other arguments do not determine",
2888 type_name, method.name
2889 ),
2890 )
2891 .with_note("annotate the lambda's parameters"),
2892 );
2893 return None;
2894 }
2895 let ty = if params_unconstrained {
2896 type_of(arg, None, ctx)
2897 } else {
2898 type_of(arg, expected, ctx)
2899 };
2900 if let (Some(pattern), Some(actual)) = (var_params[i], ty)
2901 && !unify(pattern, actual, &mut subst, tys)
2902 {
2903 ctx.errors.push(CompileError::new(
2904 "bynk.generics.type_arg_mismatch",
2905 arg.span,
2906 format!(
2907 "the lambda's type conflicts with `{}.{}`'s inferred type arguments",
2908 type_name, method.name
2909 ),
2910 ));
2911 return None;
2912 }
2913 arg_tys[i] = ty;
2914 }
2915 for tp in &method_decl.type_params {
2918 if !subst.contains_key(&tp.name.name) {
2919 ctx.errors.push(
2920 CompileError::new(
2921 "bynk.generics.uninferable_type_arg",
2922 method.span,
2923 format!(
2924 "type parameter `{}` of `{}.{}` is not inferable from the receiver or the arguments",
2925 tp.name.name, type_name, method.name
2926 ),
2927 )
2928 .with_label(tp.span, "declared here"),
2929 );
2930 return None;
2931 }
2932 }
2933 let mut ok = true;
2935 for (i, (pattern, arg)) in var_params.iter().zip(args).enumerate() {
2936 record_param_hint(ctx.hints, &method_decl.params[i].name.name, arg);
2937 let (Some(pattern), Some(arg_ty)) = (pattern, arg_tys[i].as_ref()) else {
2938 continue;
2939 };
2940 let ground = substitute(*pattern, &subst, tys);
2941 if !compatible(*arg_ty, ground, tys) {
2942 ctx.errors.push(CompileError::new(
2943 "bynk.types.argument_mismatch",
2944 arg.span,
2945 format!(
2946 "argument {} to `{}.{}` has type `{}`, but `{}` is expected",
2947 i + 1,
2948 type_name,
2949 method.name,
2950 arg_ty.display(tys),
2951 ground.display(tys)
2952 ),
2953 ));
2954 ok = false;
2955 }
2956 }
2957 if !ok {
2958 return None;
2959 }
2960 let decl = ctx.input.methods.get(type_name).and_then(|t| {
2963 t.instance
2964 .get(&method.name)
2965 .or_else(|| t.statics.get(&method.name))
2966 .cloned()
2967 });
2968 let compared = match &decl {
2969 Some(decl) if std::ptr::eq(decl.as_ref(), method_decl) => {
2970 super::equality::compared_type_params(decl, ctx)
2971 }
2972 _ => Default::default(),
2973 };
2974 let mut params: Vec<(String, Option<Span>)> = method_decl
2975 .type_params
2976 .iter()
2977 .map(|tp| (tp.name.name.clone(), Some(tp.span)))
2978 .collect();
2979 params.extend(recv_type_params.iter().map(|p| (p.clone(), None)));
2980 let callee = format!("{type_name}.{}", method.name);
2981 if !super::equality::check_compared_args(&callee, method.span, ¶ms, &compared, &subst, ctx)
2982 {
2983 return None;
2984 }
2985 if !method_decl.type_params.is_empty() {
2989 let rendered: Option<Vec<String>> = method_decl
2990 .type_params
2991 .iter()
2992 .map(|tp| subst.get(&tp.name.name).map(|t| t.display(tys)))
2993 .collect();
2994 if let Some(parts) = rendered {
2995 ctx.hints
2996 .record(method.span, format!("[{}]", parts.join(", ")));
2997 }
2998 }
2999 Some(substitute(ret_pattern, &subst, tys))
3000}
3001
3002fn check_test_service_address(
3015 sig: &TestServiceSig,
3016 id: &Ident,
3017 method: &Ident,
3018 args: &[Expr],
3019 expr_id: ExprId,
3021 ctx: &mut Ctx,
3022) -> Option<TyId> {
3023 use bynk_syntax::ast::{ExprKind as EK, HandlerKind};
3024
3025 if method.name == "call" {
3027 ctx.callees.insert(
3028 expr_id,
3029 Callee::TestService {
3030 service: id.name.clone(),
3031 address: "call".to_string(),
3032 },
3033 );
3034 let Some(handler) = sig.call_handler() else {
3035 let message = match &sig.protocol {
3036 Some(protocol) => format!(
3037 "`{}` is a `from {protocol}` service and has no `on call` handler to invoke",
3038 id.name
3039 ),
3040 None => format!("service `{}` has no `on call` handler to invoke", id.name),
3041 };
3042 ctx.errors.push(
3043 CompileError::new("bynk.test.service_no_call_handler", method.span, message)
3044 .with_note(
3045 "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)`",
3046 ),
3047 );
3048 for a in args {
3049 let _ = type_of(a, None, ctx);
3050 }
3051 return None;
3052 };
3053 let params = handler.params.clone();
3054 check_address_args(&id.name, "call", ¶ms, args, method.span, ctx);
3055 return None;
3056 }
3057
3058 if bynk_syntax::ast::HttpMethod::from_ident(&method.name).is_some() {
3063 ctx.callees.insert(
3064 expr_id,
3065 Callee::TestService {
3066 service: id.name.clone(),
3067 address: method.name.clone(),
3068 },
3069 );
3070 let Some(EK::StrLit(path)) = args.first().map(|a| &a.kind) else {
3071 ctx.errors.push(
3072 CompileError::new(
3073 "bynk.test.service_bad_address",
3074 method.span,
3075 format!(
3076 "`{}.{}` addresses an http route, so its first argument must be the route pattern string (e.g. `\"/todos\"`)",
3077 id.name, method.name
3078 ),
3079 ),
3080 );
3081 for a in args {
3082 let _ = type_of(a, None, ctx);
3083 }
3084 return None;
3085 };
3086 ctx.callees.insert(
3091 expr_id,
3092 Callee::TestService {
3093 service: id.name.clone(),
3094 address: format!("{} {path}", method.name),
3095 },
3096 );
3097 let matched = sig.handlers.iter().find(|h| {
3098 matches!(&h.kind, HandlerKind::Http { method: m, path: p } if m.as_str() == method.name && p == path)
3099 });
3100 let Some(handler) = matched else {
3101 let path_declared = sig
3108 .handlers
3109 .iter()
3110 .any(|h| matches!(&h.kind, HandlerKind::Http { path: p, .. } if p == path));
3111 if path_declared {
3112 let _ = type_of(&args[0], None, ctx);
3113 if args.len() > 1 {
3117 ctx.errors.push(
3118 CompileError::new(
3119 "bynk.test.service_call_arity",
3120 method.span,
3121 format!(
3122 "`{}.{}(\"{}\")` is a wrong-method `405` test and takes only the route path, but {} argument(s) were given",
3123 id.name,
3124 method.name,
3125 path,
3126 args.len() - 1
3127 ),
3128 )
3129 .with_note("a wrong-method call reaches no handler, so it passes no body or params"),
3130 );
3131 for a in &args[1..] {
3132 let _ = type_of(a, None, ctx);
3133 }
3134 }
3135 return None;
3136 }
3137 ctx.errors.push(
3138 CompileError::new(
3139 "bynk.test.service_unknown_route",
3140 method.span,
3141 format!(
3142 "`{}` declares no route at `\"{}\"` (no handler for any method)",
3143 id.name, path
3144 ),
3145 )
3146 .with_note("the path must match a declared route; drive a wrong method against an existing path to test the `405` fall-through"),
3147 );
3148 for a in args {
3149 let _ = type_of(a, None, ctx);
3150 }
3151 return None;
3152 };
3153 let params = handler.params.clone();
3154 let _ = type_of(&args[0], None, ctx);
3156 check_address_args(
3157 &id.name,
3158 &method.name,
3159 ¶ms,
3160 &args[1..],
3161 method.span,
3162 ctx,
3163 );
3164 return None;
3165 }
3166
3167 if method.name == "schedule" {
3169 ctx.callees.insert(
3170 expr_id,
3171 Callee::TestService {
3172 service: id.name.clone(),
3173 address: "schedule".to_string(),
3174 },
3175 );
3176 let Some(EK::StrLit(expr)) = args.first().map(|a| &a.kind) else {
3177 ctx.errors.push(CompileError::new(
3178 "bynk.test.service_bad_address",
3179 method.span,
3180 format!(
3181 "`{}.schedule` addresses a cron handler, so its first argument must be the schedule string",
3182 id.name
3183 ),
3184 ));
3185 for a in args {
3186 let _ = type_of(a, None, ctx);
3187 }
3188 return None;
3189 };
3190 ctx.callees.insert(
3193 expr_id,
3194 Callee::TestService {
3195 service: id.name.clone(),
3196 address: format!("schedule {expr}"),
3197 },
3198 );
3199 let matched = sig
3200 .handlers
3201 .iter()
3202 .find(|h| matches!(&h.kind, HandlerKind::Cron { expr: e } if e == expr));
3203 let Some(handler) = matched else {
3204 ctx.errors.push(CompileError::new(
3205 "bynk.test.service_unknown_route",
3206 method.span,
3207 format!(
3208 "`{}` declares no `on schedule(\"{}\")` handler",
3209 id.name, expr
3210 ),
3211 ));
3212 for a in args {
3213 let _ = type_of(a, None, ctx);
3214 }
3215 return None;
3216 };
3217 let params = handler.params.clone();
3218 let _ = type_of(&args[0], None, ctx);
3219 check_address_args(&id.name, "schedule", ¶ms, &args[1..], method.span, ctx);
3220 return None;
3221 }
3222
3223 if method.name == "message" {
3225 ctx.callees.insert(
3226 expr_id,
3227 Callee::TestService {
3228 service: id.name.clone(),
3229 address: "message".to_string(),
3230 },
3231 );
3232 let matched = sig
3233 .handlers
3234 .iter()
3235 .find(|h| matches!(&h.kind, HandlerKind::Message));
3236 let Some(handler) = matched else {
3237 ctx.errors.push(CompileError::new(
3238 "bynk.test.service_unknown_route",
3239 method.span,
3240 format!("`{}` declares no `on message(...)` handler", id.name),
3241 ));
3242 for a in args {
3243 let _ = type_of(a, None, ctx);
3244 }
3245 return None;
3246 };
3247 let params = handler.params.clone();
3248 check_address_args(&id.name, "message", ¶ms, args, method.span, ctx);
3249 return None;
3250 }
3251
3252 let proto = sig.protocol.as_deref().unwrap_or("call");
3254 ctx.errors.push(CompileError::new(
3255 "bynk.test.service_bad_address",
3256 method.span,
3257 format!(
3258 "`{}.{}` is not a way to address a `from {proto}` service in a test body",
3259 id.name, method.name
3260 ),
3261 ));
3262 for a in args {
3263 let _ = type_of(a, None, ctx);
3264 }
3265 None
3266}
3267
3268enum ActorIdentity {
3270 Typed(TyId),
3271 CallerString,
3272 Unit,
3273 Unknown,
3274}
3275
3276fn resolve_actor_identity(name: &str, ctx: &Ctx) -> ActorIdentity {
3278 let tys = ctx.tys;
3279 use crate::actors::{Identity, prelude_actor};
3280 if let Some(decl) = ctx.test_actors.get(name) {
3281 return match &decl.identity {
3282 Some(t) => match resolve_type_ref(t, &ctx.input.types, tys) {
3283 Some(ty) => ActorIdentity::Typed(ty),
3284 None => ActorIdentity::Unit,
3285 },
3286 None => ActorIdentity::Unit,
3287 };
3288 }
3289 match prelude_actor(name) {
3290 Some(c) => match c.identity {
3291 Identity::Unit => ActorIdentity::Unit,
3292 Identity::CallerId => ActorIdentity::CallerString,
3293 Identity::Declared(_) => ActorIdentity::Unit,
3294 },
3295 None => ActorIdentity::Unknown,
3296 }
3297}
3298
3299fn handler_actor_name(handler: &TestHandler, protocol: Option<&str>) -> Option<String> {
3303 if let Some(by) = &handler.by_clause {
3304 return Some(by.primary().name.clone());
3305 }
3306 match protocol {
3307 None => Some("Caller".to_string()),
3308 Some("cron") => Some("Scheduler".to_string()),
3309 Some("queue") => Some("Producer".to_string()),
3310 _ => None,
3311 }
3312}
3313
3314fn resolve_test_address<'a>(
3317 sig: &'a TestServiceSig,
3318 method: &str,
3319 args: &[Expr],
3320) -> Option<&'a TestHandler> {
3321 use bynk_syntax::ast::{ExprKind as EK, HandlerKind, HttpMethod};
3322 if method == "call" {
3323 return sig.call_handler();
3324 }
3325 if HttpMethod::from_ident(method).is_some() {
3326 let EK::StrLit(path) = &args.first()?.kind else {
3327 return None;
3328 };
3329 return sig.handlers.iter().find(|h| {
3330 matches!(&h.kind, HandlerKind::Http { method: m, path: p } if m.as_str() == method && p == path)
3331 });
3332 }
3333 if method == "schedule" {
3334 let EK::StrLit(expr) = &args.first()?.kind else {
3335 return None;
3336 };
3337 return sig
3338 .handlers
3339 .iter()
3340 .find(|h| matches!(&h.kind, HandlerKind::Cron { expr: e } if e == expr));
3341 }
3342 if method == "message" {
3343 return sig
3344 .handlers
3345 .iter()
3346 .find(|h| matches!(&h.kind, HandlerKind::Message));
3347 }
3348 None
3349}
3350
3351pub(crate) fn check_effect_let_principal(
3358 value: &Expr,
3359 principal: Option<&bynk_syntax::ast::CallSiteActor>,
3360 ctx: &mut Ctx,
3361) {
3362 let tys = ctx.tys;
3363 let ExprKind::MethodCall {
3364 receiver,
3365 method,
3366 args,
3367 ..
3368 } = &value.kind
3369 else {
3370 if let Some(p) = principal {
3371 report_principal_actor(p, ctx);
3372 }
3373 return;
3374 };
3375 let ExprKind::Ident(id) = &receiver.kind else {
3376 if let Some(p) = principal {
3377 report_principal_actor(p, ctx);
3378 }
3379 return;
3380 };
3381 let Some(sig) = ctx.test_services.get(&id.name).cloned() else {
3382 if let Some(p) = principal {
3383 report_principal_actor(p, ctx);
3384 }
3385 return;
3386 };
3387 let Some(handler) = resolve_test_address(&sig, &method.name, args).cloned() else {
3388 if let Some(p) = principal {
3389 let wrong_method = bynk_syntax::ast::HttpMethod::from_ident(&method.name).is_some()
3395 && matches!(args.first().map(|a| &a.kind), Some(bynk_syntax::ast::ExprKind::StrLit(path))
3396 if sig.handlers.iter().any(|h| matches!(&h.kind, bynk_syntax::ast::HandlerKind::Http { path: p, .. } if p == path)));
3397 if wrong_method {
3398 ctx.errors.push(
3399 CompileError::new(
3400 "bynk.test.principal_on_wrong_method",
3401 p.span,
3402 format!(
3403 "a wrong-method `405` test reaches no handler, so `by {}` is meaningless",
3404 p.actor.name
3405 ),
3406 )
3407 .with_note("drop the `by` clause on a wrong-method call"),
3408 );
3409 if let Some(id) = &p.identity {
3410 let _ = type_of(id, None, ctx);
3411 }
3412 } else {
3413 report_principal_actor(p, ctx);
3414 }
3415 }
3416 return;
3417 };
3418
3419 if let Some(p) = principal
3427 && p.actor.name == "Nobody"
3428 {
3429 let secured = handler
3436 .by_clause
3437 .as_ref()
3438 .is_some_and(|by| crate::actors::by_clause_is_bearer(by, &ctx.test_actors));
3439 if !secured {
3440 ctx.errors.push(
3441 CompileError::new(
3442 "bynk.test.nobody_needs_secured_route",
3443 p.span,
3444 "`by Nobody` drives the Bearer auth seam to a `401`, but this handler's route is not Bearer-secured — there is no credential check to reject",
3445 )
3446 .with_note(
3447 "use `by Nobody` only on a route guarded by a `Bearer` actor; a public (`Visitor`) route has no seam to test",
3448 ),
3449 );
3450 }
3451 if let Some(idv) = &p.identity {
3452 ctx.errors.push(CompileError::new(
3453 "bynk.test.actor_no_identity",
3454 p.span,
3455 "`Nobody` presents no credential, so it takes no identity — write `by Nobody`",
3456 ));
3457 let _ = type_of(idv, None, ctx);
3458 }
3459 return;
3460 }
3461
3462 let required = handler_actor_name(&handler, sig.protocol.as_deref())
3463 .map(|actor| resolve_actor_identity(&actor, ctx));
3464
3465 match (required, principal) {
3466 (Some(ActorIdentity::Typed(ty)), principal) => match principal {
3467 Some(p) => match resolve_actor_identity(&p.actor.name, ctx) {
3468 ActorIdentity::Unknown => report_principal_actor(p, ctx),
3469 ActorIdentity::Unit | ActorIdentity::CallerString => {
3470 ctx.errors.push(CompileError::new(
3471 "bynk.test.principal_identity_mismatch",
3472 p.span,
3473 format!(
3474 "this handler runs as an actor carrying `{}`, but `by {}` supplies no matching identity",
3475 ty.display(tys),
3476 p.actor.name
3477 ),
3478 ));
3479 if let Some(idv) = &p.identity {
3480 let _ = type_of(idv, None, ctx);
3481 }
3482 }
3483 ActorIdentity::Typed(_) => match &p.identity {
3484 Some(idv) => {
3485 let got = type_of(idv, Some(ty), ctx);
3486 if let Some(g) = got
3487 && !compatible(g, ty, tys)
3488 {
3489 ctx.errors.push(CompileError::new(
3490 "bynk.types.argument_mismatch",
3491 idv.span,
3492 format!(
3493 "identity has type `{}`, but the handler expects `{}`",
3494 g.display(tys),
3495 ty.display(tys)
3496 ),
3497 ));
3498 }
3499 }
3500 None => ctx.errors.push(CompileError::new(
3501 "bynk.test.actor_identity_required",
3502 p.span,
3503 format!(
3504 "actor `{}` carries an identity, so write `by {}(...)`",
3505 p.actor.name, p.actor.name
3506 ),
3507 )),
3508 },
3509 },
3510 None => ctx.errors.push(
3511 CompileError::new(
3512 "bynk.test.principal_required",
3513 value.span,
3514 format!(
3515 "this handler runs as a verified actor carrying `{}`; the case must act as it with `by <Actor>(<identity>)`",
3516 ty.display(tys)
3517 ),
3518 )
3519 .with_note("append a call-site actor, e.g. `... by User(\"alice\")`"),
3520 ),
3521 },
3522 (_, Some(p)) => report_principal_actor(p, ctx),
3523 (_, None) => {}
3524 }
3525}
3526
3527fn report_principal_actor(p: &bynk_syntax::ast::CallSiteActor, ctx: &mut Ctx) {
3529 let tys = ctx.tys;
3530 let name = &p.actor.name;
3531 match resolve_actor_identity(name, ctx) {
3532 ActorIdentity::Unknown => {
3533 ctx.errors.push(
3534 CompileError::new(
3535 "bynk.test.unknown_actor",
3536 p.actor.span,
3537 format!("`{name}` is not an actor of the target context or a prelude actor"),
3538 )
3539 .with_note("name an `actor` the target declares, or a prelude actor (`Visitor`, `Caller`, …)"),
3540 );
3541 if let Some(id) = &p.identity {
3542 let _ = type_of(id, None, ctx);
3543 }
3544 }
3545 ActorIdentity::Typed(ty) => match &p.identity {
3546 Some(id) => {
3547 let got = type_of(id, Some(ty), ctx);
3548 if let Some(g) = got
3549 && !compatible(g, ty, tys)
3550 {
3551 ctx.errors.push(CompileError::new(
3552 "bynk.types.argument_mismatch",
3553 id.span,
3554 format!(
3555 "identity has type `{}`, but actor `{name}` expects `{}`",
3556 g.display(tys),
3557 ty.display(tys)
3558 ),
3559 ));
3560 }
3561 }
3562 None => ctx.errors.push(CompileError::new(
3563 "bynk.test.actor_identity_required",
3564 p.span,
3565 format!("actor `{name}` carries an identity, so `by {name}(...)` needs an identity value"),
3566 )),
3567 },
3568 ActorIdentity::CallerString => {
3569 if let Some(id) = &p.identity {
3570 let _ = type_of(id, None, ctx);
3571 }
3572 }
3573 ActorIdentity::Unit => {
3574 if let Some(id) = &p.identity {
3575 let _ = type_of(id, None, ctx);
3576 ctx.errors.push(CompileError::new(
3577 "bynk.test.actor_no_identity",
3578 p.span,
3579 format!("actor `{name}` has no identity, so write `by {name}` with no argument"),
3580 ));
3581 }
3582 }
3583 }
3584}
3585
3586fn check_address_args(
3590 svc: &str,
3591 addr: &str,
3592 params: &[bynk_syntax::ast::Param],
3593 positional: &[Expr],
3594 err_span: Span,
3595 ctx: &mut Ctx,
3596) {
3597 let tys = ctx.tys;
3598 if params.len() != positional.len() {
3599 ctx.errors.push(
3600 CompileError::new(
3601 "bynk.test.service_call_arity",
3602 err_span,
3603 format!(
3604 "`{svc}.{addr}` expects {} argument(s), but {} were given",
3605 params.len(),
3606 positional.len()
3607 ),
3608 )
3609 .with_note("handler declared here"),
3612 );
3613 for a in positional {
3614 let _ = type_of(a, None, ctx);
3615 }
3616 return;
3617 }
3618 for (i, (param, arg)) in params.iter().zip(positional.iter()).enumerate() {
3619 record_param_hint(ctx.hints, ¶m.name.name, arg);
3620 if let bynk_syntax::ast::ExprKind::Wire(inner) = &arg.kind {
3628 let _ = type_of(inner, Some(tys.intern(Ty::Base(BaseType::String))), ctx);
3629 continue;
3630 }
3631 let expected = resolve_type_ref(¶m.type_ref, &ctx.input.types, tys);
3632 let arg_ty = type_of(arg, expected, ctx);
3633 if let (Some(a), Some(p)) = (arg_ty, expected)
3634 && !compatible(a, p, tys)
3635 {
3636 ctx.errors.push(CompileError::new(
3637 "bynk.types.argument_mismatch",
3638 arg.span,
3639 format!(
3640 "argument {} has type `{}`, but `{svc}.{addr}` expects `{}` for `{}`",
3641 i + 1,
3642 a.display(tys),
3643 p.display(tys),
3644 param.name.name
3645 ),
3646 ));
3647 }
3648 }
3649}
3650
3651fn cross_context_prefix(receiver: &Expr, ctx: &Ctx) -> Option<String> {
3656 let info = &ctx.input.cross_context;
3657 if info.consumed_contexts.is_empty() && info.aliases.is_empty() {
3658 return None;
3659 }
3660 let candidate = flatten_ident_chain(receiver)?;
3665 let head = candidate.split('.').next().unwrap_or("");
3666 if ctx.lookup(head).is_some() {
3668 return None;
3669 }
3670 if ctx.caps.capabilities.contains_key(head) || ctx.caps.declared_capabilities.contains_key(head)
3671 {
3672 return None;
3673 }
3674 info.resolve_prefix(candidate.as_str())
3678}
3679
3680fn flatten_ident_chain(expr: &Expr) -> Option<String> {
3683 match &expr.kind {
3684 ExprKind::Ident(id) => Some(id.name.clone()),
3685 ExprKind::FieldAccess { receiver, field } => {
3686 let head = flatten_ident_chain(receiver)?;
3687 Some(format!("{head}.{}", field.name))
3688 }
3689 _ => None,
3690 }
3691}
3692
3693#[allow(clippy::too_many_arguments)]
3701fn check_cross_context_capability_call(
3702 receiver: &Expr,
3703 consumed: &str,
3704 cap: &str,
3705 method: &Ident,
3706 type_args: &[TypeRef],
3709 args: &[Expr],
3710 _span: Span,
3711 expr_id: ExprId,
3713 ctx: &mut Ctx,
3714) -> Option<TyId> {
3715 let tys = ctx.tys;
3716 ctx.callees.insert(
3717 expr_id,
3718 Callee::CrossCap {
3719 unit: consumed.to_string(),
3720 cap: cap.to_string(),
3721 op: method.name.clone(),
3722 },
3723 );
3724 if !ctx.effectful {
3726 ctx.errors.push(CompileError::new(
3727 "bynk.effect.capability_in_pure_context",
3728 method.span,
3729 format!(
3730 "capability `{consumed}.{cap}` can only be called inside an effectful body (one returning `Effect[T]`)"
3731 ),
3732 ));
3733 }
3734 if !ctx.caps.given_remaining.contains(cap) {
3737 let mut err = CompileError::new(
3738 "bynk.given.undeclared_capability",
3739 receiver.span,
3740 format!("capability `{consumed}.{cap}` is used but not listed in the `given` clause"),
3741 )
3742 .with_note(format!(
3743 "add `{consumed}.{cap}` to the handler's `given` clause so the dependency surface is visible at the declaration site"
3744 ));
3745 if let Some((span, insert)) = given_insertion_edit(
3748 &ctx.caps.given_entries,
3749 ctx.caps.given_anchor,
3750 &format!("{consumed}.{cap}"),
3751 ) {
3752 err = err.with_suggestion(
3753 format!("add `{consumed}.{cap}` to the `given` clause"),
3754 vec![(span, insert)],
3755 Applicability::MachineApplicable,
3756 );
3757 }
3758 ctx.errors.push(err);
3759 for a in args {
3760 let _ = type_of(a, None, ctx);
3761 }
3762 return None;
3763 }
3764 ctx.caps.given_used.insert(cap.to_string());
3765
3766 let info = &ctx.input.cross_context;
3767 let op = info
3768 .consumed_capabilities
3769 .get(consumed)
3770 .and_then(|caps| caps.get(cap))
3771 .and_then(|c| c.ops.iter().find(|o| o.name == method.name))
3772 .cloned();
3773 let Some(op) = op else {
3774 ctx.errors.push(CompileError::new(
3775 "bynk.capability.unknown_operation",
3776 method.span,
3777 format!(
3778 "capability `{consumed}.{cap}` has no operation named `{}`",
3779 method.name
3780 ),
3781 ));
3782 for a in args {
3783 let _ = type_of(a, None, ctx);
3784 }
3785 return None;
3786 };
3787 ctx.refs.record_in_unit(
3791 method.span,
3792 SymbolKind::CapabilityOp,
3793 &format!("{cap}.{}", method.name),
3794 consumed,
3795 );
3796 if op.params.len() != args.len() {
3797 ctx.errors.push(CompileError::new(
3798 "bynk.capability.op_arity",
3799 method.span,
3800 format!(
3801 "capability operation `{consumed}.{cap}.{}` expects {} argument(s), but {} were given",
3802 method.name,
3803 op.params.len(),
3804 args.len()
3805 ),
3806 ));
3807 for a in args {
3808 let _ = type_of(a, None, ctx);
3809 }
3810 return None;
3811 }
3812
3813 let consumed_types = info
3815 .consumed_types
3816 .get(consumed)
3817 .cloned()
3818 .unwrap_or_default();
3819 let vars: HashSet<String> = op.type_params.iter().cloned().collect();
3826 let mut subst: HashMap<String, TyId> = HashMap::new();
3827 if !op.type_params.is_empty() || !type_args.is_empty() {
3828 if type_args.is_empty() {
3829 ctx.errors.push(
3830 CompileError::new(
3831 "bynk.generics.uninferable_type_arg",
3832 method.span,
3833 format!(
3834 "capability operation `{consumed}.{cap}.{}` takes a type parameter, but none of its arguments determine it",
3835 method.name
3836 ),
3837 )
3838 .with_note(format!(
3839 "give it explicitly: `{consumed}.{cap}.{}[T](…)`",
3840 method.name
3841 )),
3842 );
3843 for a in args {
3844 let _ = type_of(a, None, ctx);
3845 }
3846 return None;
3847 }
3848 if type_args.len() != op.type_params.len() {
3849 ctx.errors.push(CompileError::new(
3850 "bynk.generics.type_arg_mismatch",
3851 method.span,
3852 format!(
3853 "capability operation `{consumed}.{cap}.{}` takes {} type argument(s), but {} were given",
3854 method.name,
3855 op.type_params.len(),
3856 type_args.len()
3857 ),
3858 ));
3859 for a in args {
3860 let _ = type_of(a, None, ctx);
3861 }
3862 return None;
3863 }
3864 for (tp, ta) in op.type_params.iter().zip(type_args) {
3865 let ty = resolve_expr_type_ref(ta, ctx)?;
3866 subst.insert(tp.clone(), ty);
3867 }
3868 }
3869 let mut all_ok = true;
3870 for (i, ((pname, ptype_ref), arg)) in op.params.iter().zip(args.iter()).enumerate() {
3871 record_param_hint(ctx.hints, pname, arg);
3872 let param_ty = resolve_type_ref_in(ptype_ref, &consumed_types, &vars, tys)
3873 .unwrap_or(tys.intern(Ty::Unit));
3874 let param_ty = substitute(param_ty, &subst, tys);
3875 let Some(arg_ty) = type_of(arg, None, ctx) else {
3876 all_ok = false;
3877 continue;
3878 };
3879 if !structurally_compatible(arg_ty, param_ty, &ctx.input.types, &consumed_types, tys) {
3880 ctx.errors.push(CompileError::new(
3881 "bynk.boundary.structural_mismatch",
3882 arg.span,
3883 format!(
3884 "cross-context argument {} to `{consumed}.{cap}.{}` has type `{}`, but parameter `{pname}` expects `{}`",
3885 i + 1,
3886 method.name,
3887 arg_ty.display(tys),
3888 param_ty.display(tys),
3889 ),
3890 ));
3891 all_ok = false;
3892 }
3893 }
3894 if !all_ok {
3895 return None;
3896 }
3897 let raw_ret = resolve_type_ref_in(&op.return_type, &consumed_types, &vars, tys)
3898 .unwrap_or(tys.intern(Ty::Unit));
3899 let raw_ret = substitute(raw_ret, &subst, tys);
3900 Some(rebrand_return_type(raw_ret, &ctx.input.types, tys))
3901}
3902
3903fn check_cross_context_call(
3904 receiver: &Expr,
3905 consumed: &str,
3906 method: &Ident,
3907 args: &[Expr],
3908 _span: Span,
3909 expr_id: ExprId,
3911 ctx: &mut Ctx,
3912) -> Option<TyId> {
3913 let tys = ctx.tys;
3914 ctx.callees.insert(
3915 expr_id,
3916 Callee::Cross {
3917 unit: consumed.to_string(),
3918 service: method.name.clone(),
3919 },
3920 );
3921 if !ctx.effectful {
3924 ctx.errors.push(
3925 CompileError::new(
3926 "bynk.effect.cross_context_in_pure_context",
3927 method.span,
3928 format!(
3929 "cross-context service call `{}.{}` can only be made inside an effectful body (one returning `Effect[T]`)",
3930 consumed, method.name
3931 ),
3932 )
3933 .with_label(receiver.span, "consumed context prefix"),
3934 );
3935 }
3936 let info = &ctx.input.cross_context;
3937 let Some(svcs) = info.consumed_services.get(consumed) else {
3938 ctx.errors.push(
3939 CompileError::new(
3940 "bynk.consumes.unknown_context",
3941 receiver.span,
3942 format!("context `{consumed}` is not in scope here"),
3943 )
3944 .with_note(
3945 "add a `consumes` clause for the target context at the top of the consuming context",
3946 ),
3947 );
3948 for a in args {
3949 let _ = type_of(a, None, ctx);
3950 }
3951 return None;
3952 };
3953 let Some(service) = svcs.get(&method.name).cloned() else {
3954 ctx.errors.push(
3955 CompileError::new(
3956 "bynk.consumes.unknown_service",
3957 method.span,
3958 format!(
3959 "context `{consumed}` has no service named `{}`",
3960 method.name
3961 ),
3962 )
3963 .with_note(
3964 "cross-context calls require an `on call` service handler in the consumed context",
3965 ),
3966 );
3967 for a in args {
3968 let _ = type_of(a, None, ctx);
3969 }
3970 return None;
3971 };
3972 ctx.refs
3973 .record_in_unit(method.span, SymbolKind::Service, &method.name, consumed);
3974
3975 if service.params.len() != args.len() {
3976 ctx.errors.push(
3977 CompileError::new(
3978 "bynk.consumes.service_arity",
3979 method.span,
3980 format!(
3981 "cross-context service `{consumed}.{}` expects {} argument(s), but {} were given",
3982 method.name,
3983 service.params.len(),
3984 args.len()
3985 ),
3986 )
3987 .with_note("service declared here"),
3993 );
3994 for a in args {
3995 let _ = type_of(a, None, ctx);
3996 }
3997 return None;
3998 }
3999
4000 let consumed_types = info
4002 .consumed_types
4003 .get(consumed)
4004 .cloned()
4005 .unwrap_or_default();
4006
4007 let mut all_ok = true;
4009 for (i, ((pname, ptype_ref), arg)) in service.params.iter().zip(args.iter()).enumerate() {
4010 record_param_hint(ctx.hints, pname, arg);
4011 let param_ty =
4012 resolve_type_ref(ptype_ref, &consumed_types, tys).unwrap_or(tys.intern(Ty::Unit));
4013 let arg_ty = type_of(arg, None, ctx);
4015 let Some(arg_ty) = arg_ty else {
4016 all_ok = false;
4017 continue;
4018 };
4019 if !structurally_compatible(arg_ty, param_ty, &ctx.input.types, &consumed_types, tys) {
4020 ctx.errors.push(
4021 CompileError::new(
4022 "bynk.boundary.structural_mismatch",
4023 arg.span,
4024 format!(
4025 "cross-context argument {} to `{consumed}.{}` has type `{}` in `{}`, but parameter `{pname}` expects `{}` in `{}`",
4026 i + 1,
4027 method.name,
4028 arg_ty.display(tys),
4029 ctx.input
4030 .cross_context
4031 .self_context
4032 .as_deref()
4033 .unwrap_or("?"),
4034 param_ty.display(tys),
4035 consumed,
4036 ),
4037 )
4038 .with_note("service declared here")
4040 .with_note(
4041 "values crossing a context boundary must have structurally compatible types (same commons-derived type, or identical record/sum shape)",
4042 ),
4043 );
4044 all_ok = false;
4045 }
4046 }
4047 if !all_ok {
4048 return None;
4049 }
4050
4051 let raw_ret = resolve_type_ref(&service.return_type, &consumed_types, tys)
4055 .unwrap_or(tys.intern(Ty::Unit));
4056 let rebranded = rebrand_return_type(raw_ret, &ctx.input.types, tys);
4057 Some(rebranded)
4058}