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