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<Span, Ty>,
29 errors: &mut Vec<CompileError>,
30 refs: &mut RefSink,
31 hints: &mut HintSink,
32 locals: &mut LocalsSink,
33 requirements: &mut RequirementSink,
34) {
35 let vars: HashSet<String> = f
39 .type_params
40 .iter()
41 .map(|tp| tp.name.name.clone())
42 .collect();
43 for tp in &f.type_params {
44 if input.types.contains_key(&tp.name.name) {
45 errors.push(
46 CompileError::new(
47 "bynk.generics.type_arg_mismatch",
48 tp.span,
49 format!(
50 "type parameter `{}` shadows the declared type of the same name",
51 tp.name.name
52 ),
53 )
54 .with_note("rename the type parameter"),
55 );
56 }
57 }
58 let return_ty = match resolve_type_ref_in(&f.return_type, &input.types, &vars) {
59 Some(t) => t,
60 None => return,
61 };
62 record_type_refs(&f.return_type, &input.types, &vars, refs);
63 let mut param_scope: HashMap<String, Ty> = HashMap::new();
64 if let FnName::Method { type_name, .. } = &f.name
66 && f.has_self
67 && let Some(self_ty) = type_from_decl(type_name, &input.types)
68 {
69 param_scope.insert("self".to_string(), self_ty);
70 }
71 for p in &f.params {
72 if let Some(ty) = resolve_type_ref_in(&p.type_ref, &input.types, &vars) {
73 record_type_refs(&p.type_ref, &input.types, &vars, refs);
74 if p.name.name != "_" {
76 locals.record(p.name.name.clone(), p.name.span, ty.display(), f.body.span);
77 }
78 param_scope.insert(p.name.name.clone(), ty);
79 }
80 }
81 if !f.requires.is_empty() || !f.ensures.is_empty() {
86 let result_ty = match &return_ty {
87 Ty::Effect(inner) => (**inner).clone(),
88 t => t.clone(),
89 };
90 let has_result_param = f.params.iter().any(|p| p.name.name == "result");
91 let fn_label = format!("function `{}`", f.name.display());
92 check_contracts(
93 &f.requires,
94 &f.ensures,
95 ¶m_scope,
96 &result_ty,
97 has_result_param,
98 &fn_label,
99 input,
100 expr_types,
101 errors,
102 refs,
103 hints,
104 locals,
105 requirements,
106 &vars,
107 );
108 }
109 let effectful = matches!(&return_ty, Ty::Effect(_));
110 let mut ctx = Ctx {
111 input,
112 expr_types,
113 errors,
114 refs,
115 hints,
116 locals,
117 requirements,
118 scopes: vec![param_scope],
119 return_ty: return_ty.clone(),
120 return_ty_span: f.return_type.span(),
121 effectful,
122 agent_state_ty: None,
123 commit_seen: false,
124 caps: CapabilityCtx::default(),
125 in_test_body: false,
126 test_services: HashSet::new(),
127 type_vars: vars.clone(),
128 store_cells: HashMap::new(),
129 store_maps: HashMap::new(),
130 store_sets: HashMap::new(),
131 store_caches: HashMap::new(),
132 store_logs: HashMap::new(),
133 };
134 let Some(body_ty) = type_of_block(&f.body, Some(&return_ty), &mut ctx) else {
135 return;
136 };
137 if !compatible(&body_ty, &return_ty) {
138 ctx.errors.push(
139 CompileError::new(
140 "bynk.types.return_mismatch",
141 f.body.tail.span,
142 format!(
143 "function body has type `{}`, but the declared return type is `{}`",
144 body_ty.display(),
145 return_ty.display()
146 ),
147 )
148 .with_label(f.return_type.span(), "declared return type"),
149 );
150 }
151}
152
153#[allow(clippy::too_many_arguments)]
161pub fn check_state_initialiser(
162 init: &Expr,
163 field_type: &TypeRef,
164 input: &ResolvedCommons,
165 expr_types: &mut HashMap<Span, Ty>,
166 errors: &mut Vec<CompileError>,
167 refs: &mut RefSink,
168 hints: &mut HintSink,
169 locals: &mut LocalsSink,
170) {
171 let Some(field_ty) = resolve_type_ref(field_type, &input.types) else {
172 return; };
174 let mut local_errors: Vec<CompileError> = Vec::new();
175 let mut init_requirements = RequirementSink::new();
178 let result = {
179 let mut ctx = Ctx {
180 input,
181 expr_types,
182 errors: &mut local_errors,
183 refs,
184 hints,
185 locals,
186 requirements: &mut init_requirements,
187 scopes: vec![HashMap::new()],
188 return_ty: field_ty.clone(),
189 return_ty_span: init.span,
190 effectful: false,
191 agent_state_ty: None,
192 commit_seen: false,
193 caps: CapabilityCtx::default(),
194 in_test_body: false,
195 test_services: HashSet::new(),
196 type_vars: HashSet::new(),
197 store_cells: HashMap::new(),
198 store_maps: HashMap::new(),
199 store_sets: HashMap::new(),
200 store_caches: HashMap::new(),
201 store_logs: HashMap::new(),
202 };
203 type_of(init, Some(&field_ty), &mut ctx)
204 };
205 let compatible_result = matches!(&result, Some(t) if compatible(t, &field_ty));
206 if !compatible_result || !local_errors.is_empty() {
207 let got = result
208 .as_ref()
209 .map(|t| t.display())
210 .unwrap_or_else(|| "an invalid expression".to_string());
211 errors.push(
212 CompileError::new(
213 "bynk.agents.bad_state_initialiser",
214 init.span,
215 format!(
216 "state field initialiser must be a static value of type `{}` (got `{got}`)",
217 field_ty.display(),
218 ),
219 )
220 .with_note(
221 "an initialiser is a compile-time value — a literal, a sum variant, \
222 `Some`/`None`/`Ok`/`Err`, a record, or `T.unsafe(lit)` — with no reference to \
223 `self`, parameters, or capabilities",
224 ),
225 );
226 }
227}
228
229fn warn_bynk_list_deprecation(name: &Ident, args: &[Expr], call_span: Span, ctx: &mut Ctx) {
235 if ctx.input.imported_from.get(&name.name).map(String::as_str) != Some("bynk.list") {
236 return;
237 }
238 let method_form: &str = match name.name.as_str() {
240 "map" => "xs.map(f)",
241 "filter" => "xs.filter(p)",
242 "any" => "xs.any(p)",
243 "all" => "xs.all(p)",
244 "find" => "xs.filter(p).first()",
245 _ => return, };
247 let mut err = CompileError::new(
248 "bynk.list.deprecated_function",
249 name.span,
250 format!(
251 "`bynk.list.{}` is deprecated — use the `List` method form `{method_form}`",
252 name.name
253 ),
254 )
255 .with_note(
256 "the `bynk.list.*` free functions are superseded by the method-chain vocabulary (ADR 0116); the method form reads left-to-right and chains",
257 );
258 if args.len() == 2 {
261 let mut edits = vec![
262 (
264 Span::new(name.span.start, args[0].span.start),
265 String::new(),
266 ),
267 (
269 Span::new(args[0].span.end, args[1].span.start),
270 format!(
271 ".{}(",
272 if name.name == "find" {
273 "filter"
274 } else {
275 &name.name
276 }
277 ),
278 ),
279 ];
280 if name.name == "find" {
281 edits.push((
283 Span::new(call_span.end, call_span.end),
284 ".first()".to_string(),
285 ));
286 }
287 err = err.with_suggestion(
288 format!("rewrite to the `List` method form `{method_form}`"),
289 edits,
290 Applicability::MachineApplicable,
291 );
292 }
293 ctx.errors.push(err);
294}
295
296pub(crate) fn check_call(
297 name: &Ident,
298 type_args: &[TypeRef],
299 args: &[Expr],
300 span: Span,
301 ctx: &mut Ctx,
302) -> Option<Ty> {
303 if let Some(fn_decl) = ctx.input.fns.get(&name.name).cloned() {
304 ctx.refs.record(name.span, SymbolKind::Fn, &name.name);
305 warn_bynk_list_deprecation(name, args, span, ctx);
306 return check_call_against_fn(name, &fn_decl, type_args, args, ctx);
307 }
308 if !type_args.is_empty() {
310 ctx.errors.push(CompileError::new(
311 "bynk.generics.type_arg_mismatch",
312 span,
313 format!(
314 "`{}` is not a generic function — it takes no type arguments",
315 name.name
316 ),
317 ));
318 for a in args {
319 let _ = type_of(a, None, ctx);
320 }
321 return None;
322 }
323 let owners: Vec<TypeDecl> = ctx
325 .input
326 .types
327 .values()
328 .filter(|t| matches!(&t.body, TypeBody::Sum(s) if s.variants.iter().any(|v| v.name.name == name.name)))
329 .cloned()
330 .collect();
331 if owners.len() == 1 {
332 let owner = owners.into_iter().next().unwrap();
333 return check_variant_construction(&owner, &name.name, args, span, ctx);
334 }
335 if let Some(agent) = ctx.input.agents.get(&name.name).cloned() {
339 ctx.refs.record(name.span, SymbolKind::Agent, &name.name);
340 let key_ty = resolve_type_ref(&agent.key_type, &ctx.input.types);
341 if args.len() != 1 {
342 ctx.errors.push(CompileError::new(
343 "bynk.agent.construction_arity",
344 span,
345 format!(
346 "agent `{}` is constructed with one key argument, but {} were given",
347 name.name,
348 args.len()
349 ),
350 ));
351 for a in args {
352 let _ = type_of(a, None, ctx);
353 }
354 return None;
355 }
356 let arg_ty = type_of(&args[0], key_ty.as_ref(), ctx);
357 if let (Some(a), Some(k)) = (arg_ty.as_ref(), key_ty.as_ref())
358 && !compatible(a, k)
359 {
360 ctx.errors.push(CompileError::new(
361 "bynk.agent.key_mismatch",
362 args[0].span,
363 format!(
364 "agent `{}` key is `{}`, but a value of type `{}` was given",
365 name.name,
366 k.display(),
367 a.display()
368 ),
369 ));
370 }
371 return Some(Ty::Named {
372 name: name.name.clone(),
373 kind: NamedKind::Record,
374 });
375 }
376 if let Some(ty) = ctx.lookup(&name.name) {
382 return match ty {
383 Ty::Fn { params, ret } => check_value_application(name, ¶ms, &ret, args, span, ctx),
384 other => {
385 ctx.errors.push(
388 CompileError::new(
389 "bynk.resolve.param_as_function",
390 span,
391 format!(
392 "`{}` has type `{}` and is not callable",
393 name.name,
394 other.display()
395 ),
396 )
397 .with_note("only values of function type can be applied"),
398 );
399 for a in args {
400 let _ = type_of(a, None, ctx);
401 }
402 None
403 }
404 };
405 }
406 let _ = span;
407 None
408}
409
410fn check_value_application(
411 name: &Ident,
412 params: &[Ty],
413 ret: &Ty,
414 args: &[Expr],
415 span: Span,
416 ctx: &mut Ctx,
417) -> Option<Ty> {
418 if ret.is_effect() && !ctx.effectful {
419 ctx.errors.push(
420 CompileError::new(
421 "bynk.effect.fn_value_in_pure_context",
422 span,
423 format!(
424 "`{}` is an effectful function (`{}`) and cannot be called in a pure context",
425 name.name,
426 Ty::Fn {
427 params: params.to_vec(),
428 ret: Box::new(ret.clone())
429 }
430 .display()
431 ),
432 )
433 .with_note(
434 "effectful function values may only be called where the enclosing body is effectful (its return type is an Effect)",
435 ),
436 );
437 }
438 if params.len() != args.len() {
439 ctx.errors.push(CompileError::new(
440 "bynk.types.call_arity",
441 span,
442 format!(
443 "`{}` takes {} argument(s), but {} were given",
444 name.name,
445 params.len(),
446 args.len()
447 ),
448 ));
449 for a in args {
450 let _ = type_of(a, None, ctx);
451 }
452 return None;
453 }
454 for (arg, param_ty) in args.iter().zip(params) {
455 let arg_ty = type_of(arg, Some(param_ty), ctx);
456 if let Some(a) = arg_ty.as_ref()
457 && !compatible(a, param_ty)
458 {
459 ctx.errors.push(CompileError::new(
460 "bynk.types.argument_mismatch",
461 arg.span,
462 format!(
463 "argument has type `{}`, but `{}` expects `{}`",
464 a.display(),
465 name.name,
466 param_ty.display()
467 ),
468 ));
469 }
470 }
471 Some(ret.clone())
472}
473
474fn check_generic_call(
485 name: &Ident,
486 fn_decl: &FnDecl,
487 type_args: &[TypeRef],
488 args: &[Expr],
489 ctx: &mut Ctx,
490) -> Option<Ty> {
491 let vars: HashSet<String> = fn_decl
492 .type_params
493 .iter()
494 .map(|tp| tp.name.name.clone())
495 .collect();
496 if fn_decl.params.len() != args.len() {
497 for a in args {
498 let _ = type_of(a, None, ctx);
499 }
500 return None;
501 }
502 let var_params: Vec<Option<Ty>> = fn_decl
503 .params
504 .iter()
505 .map(|p| resolve_type_ref_in(&p.type_ref, &ctx.input.types, &vars))
506 .collect();
507 let ret_pattern = resolve_type_ref_in(&fn_decl.return_type, &ctx.input.types, &vars)?;
508
509 let mut subst: HashMap<String, Ty> = HashMap::new();
510 if !type_args.is_empty() {
511 if type_args.len() != fn_decl.type_params.len() {
512 ctx.errors.push(CompileError::new(
513 "bynk.generics.type_arg_mismatch",
514 name.span,
515 format!(
516 "`{}` takes {} type argument(s), but {} were given",
517 name.name,
518 fn_decl.type_params.len(),
519 type_args.len()
520 ),
521 ));
522 return None;
523 }
524 for (tp, ta) in fn_decl.type_params.iter().zip(type_args) {
525 let ty = resolve_type_ref_in(ta, &ctx.input.types, &ctx.type_vars)?;
529 subst.insert(tp.name.name.clone(), ty);
530 }
531 }
532
533 let mut arg_tys: Vec<Option<Ty>> = vec![None; args.len()];
534 for (i, arg) in args.iter().enumerate() {
536 if matches!(arg.kind, ExprKind::Lambda(_)) {
537 continue;
538 }
539 let expected = var_params[i].as_ref().map(|p| substitute(p, &subst));
540 let ty = type_of(arg, expected.as_ref(), ctx);
541 if let (Some(pattern), Some(actual)) = (var_params[i].as_ref(), ty.as_ref())
542 && !unify(pattern, actual, &mut subst)
543 {
544 ctx.errors.push(CompileError::new(
545 "bynk.generics.type_arg_mismatch",
546 arg.span,
547 format!(
548 "argument {} infers a type for `{}`'s type parameter that conflicts with an earlier argument — annotate with `{}[T](…)`",
549 i + 1,
550 name.name,
551 name.name
552 ),
553 ));
554 return None;
555 }
556 arg_tys[i] = ty;
557 }
558 for (i, arg) in args.iter().enumerate() {
560 if !matches!(arg.kind, ExprKind::Lambda(_)) {
561 continue;
562 }
563 let expected = var_params[i].as_ref().map(|p| substitute(p, &subst));
564 let params_unconstrained = matches!(
565 expected.as_ref(),
566 Some(Ty::Fn { params, .. }) if params.iter().any(contains_var)
567 );
568 let fully_annotated = matches!(
569 &arg.kind,
570 ExprKind::Lambda(l) if l.params.iter().all(|p| p.type_ref.is_some())
571 );
572 if params_unconstrained && !fully_annotated {
573 ctx.errors.push(
574 CompileError::new(
575 "bynk.generics.uninferable_type_arg",
576 arg.span,
577 format!(
578 "the lambda's parameter types depend on `{}`'s type parameters, which the other arguments do not determine",
579 name.name
580 ),
581 )
582 .with_note("annotate the lambda's parameters, or give explicit type arguments: `name[T](…)`"),
583 );
584 return None;
585 }
586 let ty = if params_unconstrained {
589 type_of(arg, None, ctx)
590 } else {
591 type_of(arg, expected.as_ref(), ctx)
592 };
593 if let (Some(pattern), Some(actual)) = (var_params[i].as_ref(), ty.as_ref())
594 && !unify(pattern, actual, &mut subst)
595 {
596 ctx.errors.push(CompileError::new(
597 "bynk.generics.type_arg_mismatch",
598 arg.span,
599 format!(
600 "the lambda's type conflicts with `{}`'s inferred type arguments",
601 name.name
602 ),
603 ));
604 return None;
605 }
606 arg_tys[i] = ty;
607 }
608 for tp in &fn_decl.type_params {
610 if !subst.contains_key(&tp.name.name) {
611 ctx.errors.push(
612 CompileError::new(
613 "bynk.generics.uninferable_type_arg",
614 name.span,
615 format!(
616 "type parameter `{}` of `{}` is neither inferable from the arguments nor given explicitly",
617 tp.name.name, name.name
618 ),
619 )
620 .with_label(tp.span, "declared here")
621 .with_note("give explicit type arguments: `name[T](…)`"),
622 );
623 return None;
624 }
625 }
626 let mut ok = true;
628 for (i, (pattern, arg)) in var_params.iter().zip(args).enumerate() {
629 record_param_hint(ctx.hints, &fn_decl.params[i].name.name, arg);
630 let (Some(pattern), Some(arg_ty)) = (pattern.as_ref(), arg_tys[i].as_ref()) else {
631 continue;
632 };
633 let ground = substitute(pattern, &subst);
634 if !compatible(arg_ty, &ground) {
635 ctx.errors.push(CompileError::new(
636 "bynk.types.argument_mismatch",
637 arg.span,
638 format!(
639 "argument {} to `{}` has type `{}`, but `{}` is expected",
640 i + 1,
641 name.name,
642 arg_ty.display(),
643 ground.display()
644 ),
645 ));
646 ok = false;
647 }
648 }
649 if !ok {
650 return None;
651 }
652 if type_args.is_empty() && !fn_decl.type_params.is_empty() {
657 let rendered: Option<Vec<String>> = fn_decl
658 .type_params
659 .iter()
660 .map(|tp| subst.get(&tp.name.name).map(|t| t.display()))
661 .collect();
662 if let Some(parts) = rendered {
663 ctx.hints
664 .record(name.span, format!("[{}]", parts.join(", ")));
665 }
666 }
667 let ret = substitute(&ret_pattern, &subst);
668 Some(ret)
673}
674
675fn check_call_against_fn(
676 name: &Ident,
677 fn_decl: &FnDecl,
678 type_args: &[TypeRef],
679 args: &[Expr],
680 ctx: &mut Ctx,
681) -> Option<Ty> {
682 if !fn_decl.type_params.is_empty() {
686 return check_generic_call(name, fn_decl, type_args, args, ctx);
687 }
688 if !type_args.is_empty() {
689 ctx.errors.push(CompileError::new(
690 "bynk.generics.type_arg_mismatch",
691 name.span,
692 format!(
693 "`{}` is not a generic function — it takes no type arguments",
694 name.name
695 ),
696 ));
697 for a in args {
698 let _ = type_of(a, None, ctx);
699 }
700 return None;
701 }
702 if fn_decl.params.len() != args.len() {
703 for a in args {
704 let _ = type_of(a, None, ctx);
705 }
706 return None;
707 }
708 let resolved_params: Vec<(Option<Ty>, &Param)> = fn_decl
709 .params
710 .iter()
711 .map(|p| (resolve_type_ref(&p.type_ref, &ctx.input.types), p))
712 .collect();
713 let mut ok = true;
714 for (i, ((param_ty, param), arg)) in resolved_params.iter().zip(args.iter()).enumerate() {
715 record_param_hint(ctx.hints, ¶m.name.name, arg);
716 let arg_ty = type_of(arg, param_ty.as_ref(), ctx);
717 let (Some(arg_ty), Some(param_ty)) = (arg_ty, param_ty.as_ref()) else {
718 ok = false;
719 continue;
720 };
721 if !compatible(&arg_ty, param_ty) {
722 ctx.errors.push(
723 CompileError::new(
724 "bynk.types.argument_mismatch",
725 arg.span,
726 format!(
727 "argument {} to `{}` has type `{}`, but parameter `{}` expects `{}`",
728 i + 1,
729 name.name,
730 arg_ty.display(),
731 param.name.name,
732 param_ty.display()
733 ),
734 )
735 .with_label(param.span, "parameter declared here"),
736 );
737 ok = false;
738 }
739 }
740 if !ok {
741 return None;
742 }
743 resolve_type_ref(&fn_decl.return_type, &ctx.input.types)
744}
745
746pub(crate) fn check_arg(arg: &Expr, expected: &Ty, what: &str, ctx: &mut Ctx) {
749 let Some(actual) = type_of(arg, Some(expected), ctx) else {
750 return;
751 };
752 if !compatible(&actual, expected) {
753 ctx.errors.push(CompileError::new(
754 "bynk.types.type_mismatch",
755 arg.span,
756 format!(
757 "{what} has type `{}`, but `{}` is required",
758 actual.display(),
759 expected.display()
760 ),
761 ));
762 }
763}
764
765fn record_capability_ref(span: Span, name: &str, ctx: &mut Ctx) {
770 if let Some(unit) = ctx.input.cross_context.flattened_caps.get(name) {
771 ctx.refs
772 .record_in_unit(span, SymbolKind::Capability, name, unit);
773 } else {
774 ctx.refs.record(span, SymbolKind::Capability, name);
775 }
776}
777
778pub(crate) fn check_static_call(
779 type_name: &Ident,
780 method: &Ident,
781 args: &[Expr],
782 span: Span,
783 ctx: &mut Ctx,
784) -> Option<Ty> {
785 if ctx.caps.declared_capabilities.contains_key(&type_name.name)
789 && !ctx.caps.capabilities.contains_key(&type_name.name)
790 {
791 record_capability_ref(type_name.span, &type_name.name, ctx);
792 let mut err = CompileError::new(
793 "bynk.given.undeclared_capability",
794 type_name.span,
795 format!(
796 "capability `{}` is used but not listed in the handler's `given` clause",
797 type_name.name
798 ),
799 )
800 .with_note(format!(
801 "add `{}` to the handler's `given` clause so the dependency surface is visible at the declaration site",
802 type_name.name
803 ));
804 if let Some((span, insert)) = given_insertion_edit(
806 &ctx.caps.given_entries,
807 ctx.caps.given_anchor,
808 &type_name.name,
809 ) {
810 err = err.with_suggestion(
811 format!("add `{}` to the `given` clause", type_name.name),
812 vec![(span, insert)],
813 Applicability::MachineApplicable,
814 );
815 }
816 ctx.errors.push(err);
817 record_requirement(
822 ctx,
823 &type_name.name,
824 span,
825 RequirementSource::DirectCall {
826 op: method.name.clone(),
827 },
828 false,
829 );
830 for a in args {
831 let _ = type_of(a, None, ctx);
832 }
833 return None;
834 }
835 if let Some(cap) = ctx.caps.capabilities.get(&type_name.name).cloned() {
836 record_capability_ref(type_name.span, &type_name.name, ctx);
837 if !ctx.effectful {
838 ctx.errors.push(
839 CompileError::new(
840 "bynk.effect.capability_in_pure_context",
841 span,
842 format!(
843 "capability `{}` can only be called inside an effectful body (one returning `Effect[T]`)",
844 type_name.name
845 ),
846 ),
847 );
848 }
849 ctx.caps.given_used.insert(type_name.name.clone());
850 record_requirement(
854 ctx,
855 &type_name.name,
856 span,
857 RequirementSource::DirectCall {
858 op: method.name.clone(),
859 },
860 true,
861 );
862 let Some(op) = cap.ops.iter().find(|o| o.name == method.name) else {
863 ctx.errors.push(CompileError::new(
864 "bynk.capability.unknown_operation",
865 method.span,
866 format!(
867 "capability `{}` has no operation named `{}`",
868 type_name.name, method.name
869 ),
870 ));
871 for a in args {
872 let _ = type_of(a, None, ctx);
873 }
874 return None;
875 };
876 ctx.refs.record(
879 method.span,
880 SymbolKind::CapabilityOp,
881 &format!("{}.{}", type_name.name, method.name),
882 );
883 if op.params.len() != args.len() {
884 ctx.errors.push(CompileError::new(
885 "bynk.capability.op_arity",
886 span,
887 format!(
888 "capability operation `{}.{}` expects {} argument(s), but {} were given",
889 type_name.name,
890 method.name,
891 op.params.len(),
892 args.len()
893 ),
894 ));
895 for a in args {
896 let _ = type_of(a, None, ctx);
897 }
898 return None;
899 }
900 let op_clone = op.clone();
901 for (i, (param_ty, arg)) in op_clone.params.iter().zip(args.iter()).enumerate() {
902 let arg_ty = type_of(arg, Some(param_ty), ctx);
903 if let Some(actual) = arg_ty
904 && !compatible(&actual, param_ty)
905 {
906 ctx.errors.push(CompileError::new(
907 "bynk.types.argument_mismatch",
908 arg.span,
909 format!(
910 "argument {} to capability `{}.{}` has type `{}`, but parameter expects `{}`",
911 i + 1,
912 type_name.name,
913 method.name,
914 actual.display(),
915 param_ty.display()
916 ),
917 ));
918 }
919 }
920 return Some(op_clone.return_ty);
921 }
922 let decl = ctx.input.types.get(&type_name.name)?.clone();
923 ctx.refs
924 .record(type_name.span, SymbolKind::Type, &type_name.name);
925 let table = ctx
926 .input
927 .methods
928 .get(&type_name.name)
929 .cloned()
930 .unwrap_or_default();
931
932 if let Some(method_decl) = table.statics.get(&method.name).cloned() {
934 return check_method_args(&method_decl, args, ctx, type_name, method);
935 }
936
937 if method.name == OF
939 && let Some(base) = type_decl_base(&decl)
940 {
941 if args.len() != 1 {
942 ctx.errors.push(CompileError::new(
943 "bynk.types.constructor_arity",
944 span,
945 format!(
946 "constructor `{}.of` expects 1 argument, but {} were given",
947 type_name.name,
948 args.len()
949 ),
950 ));
951 return None;
952 }
953 let arg = &args[0];
954 let expected = Ty::Base(base);
955 let arg_ty = type_of(arg, Some(&expected), ctx)?;
956 if !compatible(&arg_ty, &expected) {
957 ctx.errors.push(CompileError::new(
958 "bynk.types.constructor_base_mismatch",
959 arg.span,
960 format!(
961 "constructor `{}.of` expects a `{}` argument, but got `{}`",
962 type_name.name,
963 base.name(),
964 arg_ty.display()
965 ),
966 ));
967 return None;
968 }
969 return Some(Ty::Result(
975 Box::new(named_ty(&decl)),
976 Box::new(Ty::ValidationError),
977 ));
978 }
979
980 if method.name == UNSAFE
983 && let TypeBody::Opaque { base, .. } = &decl.body
984 {
985 if !ctx.input.is_local_type(&decl.name.name) {
986 ctx.errors.push(
987 CompileError::new(
988 "bynk.types.opaque_unsafe_outside",
989 method.span,
990 format!(
991 "`{}.unsafe(...)` is only available within the commons that defines the opaque type `{}`",
992 type_name.name, type_name.name
993 ),
994 )
995 .with_note(
996 "outside the defining commons, opaque values are constructed via `T.of(value)`",
997 ),
998 );
999 return None;
1000 }
1001 if args.len() != 1 {
1002 ctx.errors.push(CompileError::new(
1003 "bynk.types.constructor_arity",
1004 span,
1005 format!(
1006 "`{}.unsafe` expects 1 argument, but {} were given",
1007 type_name.name,
1008 args.len()
1009 ),
1010 ));
1011 return None;
1012 }
1013 let arg = &args[0];
1014 let expected = Ty::Base(*base);
1015 let arg_ty = type_of(arg, Some(&expected), ctx)?;
1016 if !compatible(&arg_ty, &expected) {
1017 ctx.errors.push(CompileError::new(
1018 "bynk.types.constructor_base_mismatch",
1019 arg.span,
1020 format!(
1021 "`{}.unsafe` expects a `{}` argument, but got `{}`",
1022 type_name.name,
1023 base.name(),
1024 arg_ty.display()
1025 ),
1026 ));
1027 return None;
1028 }
1029 return Some(named_ty(&decl));
1030 }
1031
1032 if let TypeBody::Sum(_) = &decl.body {
1034 return check_variant_construction(&decl, &method.name, args, span, ctx);
1035 }
1036
1037 ctx.errors.push(
1038 CompileError::new(
1039 "bynk.types.unknown_static_member",
1040 method.span,
1041 format!(
1042 "type `{}` has no static method or variant named `{}`",
1043 type_name.name, method.name
1044 ),
1045 )
1046 .with_label(decl.name.span, "type declared here"),
1047 );
1048 None
1049}
1050
1051fn check_method_args(
1052 method_decl: &FnDecl,
1053 args: &[Expr],
1054 ctx: &mut Ctx,
1055 type_name: &Ident,
1056 method: &Ident,
1057) -> Option<Ty> {
1058 if method_decl.params.len() != args.len() {
1059 ctx.errors.push(
1060 CompileError::new(
1061 "bynk.types.method_arity",
1062 method.span,
1063 format!(
1064 "static method `{}.{}` expects {} argument(s), but {} were given",
1065 type_name.name,
1066 method.name,
1067 method_decl.params.len(),
1068 args.len()
1069 ),
1070 )
1071 .with_label(method_decl.name.ident().span, "method declared here"),
1072 );
1073 for a in args {
1074 let _ = type_of(a, None, ctx);
1075 }
1076 return None;
1077 }
1078 let mut ok = true;
1079 for (i, (param, arg)) in method_decl.params.iter().zip(args.iter()).enumerate() {
1080 record_param_hint(ctx.hints, ¶m.name.name, arg);
1081 let expected = resolve_type_ref(¶m.type_ref, &ctx.input.types);
1082 let actual = type_of(arg, expected.as_ref(), ctx);
1083 let (Some(actual), Some(expected)) = (actual, expected) else {
1084 ok = false;
1085 continue;
1086 };
1087 if !compatible(&actual, &expected) {
1088 ctx.errors.push(CompileError::new(
1089 "bynk.types.argument_mismatch",
1090 arg.span,
1091 format!(
1092 "argument {} to `{}.{}` has type `{}`, but parameter `{}` expects `{}`",
1093 i + 1,
1094 type_name.name,
1095 method.name,
1096 actual.display(),
1097 param.name.name,
1098 expected.display()
1099 ),
1100 ));
1101 ok = false;
1102 }
1103 }
1104 if !ok {
1105 return None;
1106 }
1107 resolve_type_ref(&method_decl.return_type, &ctx.input.types)
1108}
1109
1110pub(crate) fn check_store_map_op(
1118 method: &Ident,
1119 args: &[Expr],
1120 k: &Ty,
1121 v: &Ty,
1122 span: Span,
1123 ctx: &mut Ctx,
1124) -> Option<Ty> {
1125 let vfn = || Ty::Fn {
1126 params: vec![v.clone()],
1127 ret: Box::new(v.clone()),
1128 };
1129 if v.is_held() && matches!(method.name.as_str(), "update" | "upsert") {
1136 ctx.errors.push(
1137 CompileError::new(
1138 "bynk.held.unsupported_map_op",
1139 method.span,
1140 format!(
1141 "a held `Map[K, Connection]` has no `{}` operation — a held resource cannot be transformed by a `(Connection) -> Connection` function",
1142 method.name
1143 ),
1144 )
1145 .with_note(
1146 "held connections are stored and resolved by identity; use `put`/`get`/`remove`",
1147 ),
1148 );
1149 for a in args {
1150 type_of(a, None, ctx);
1151 }
1152 return None;
1153 }
1154 let (expected, result): (Vec<Ty>, Ty) = match method.name.as_str() {
1155 "put" => (vec![k.clone(), v.clone()], Ty::Unit),
1156 "get" => (vec![k.clone()], Ty::Option(Box::new(v.clone()))),
1157 "remove" => (vec![k.clone()], Ty::Unit),
1158 "contains" => (vec![k.clone()], Ty::Base(BaseType::Bool)),
1159 "size" => (vec![], Ty::Base(BaseType::Int)),
1160 "update" => (vec![k.clone(), vfn()], Ty::Unit),
1161 "upsert" => (vec![k.clone(), v.clone(), vfn()], Ty::Unit),
1162 other => {
1163 ctx.errors.push(
1164 CompileError::new(
1165 "bynk.store.unknown_op",
1166 method.span,
1167 format!(
1168 "a `Map` store field has no operation `{other}` — expected `put`, `get`, \
1169 `update`, `upsert`, `remove`, `contains`, or `size`"
1170 ),
1171 )
1172 .with_note("storage-map ops are entry-level and effectful (await with `<-`)"),
1173 );
1174 for a in args {
1175 type_of(a, None, ctx);
1176 }
1177 return None;
1178 }
1179 };
1180 let effect = Ty::Effect(Box::new(result));
1181 if args.len() != expected.len() {
1182 ctx.errors.push(CompileError::new(
1183 "bynk.types.call_arity",
1184 span,
1185 format!(
1186 "`Map.{}` takes {} argument(s), found {}",
1187 method.name,
1188 expected.len(),
1189 args.len()
1190 ),
1191 ));
1192 for a in args {
1193 type_of(a, None, ctx);
1194 }
1195 return Some(effect);
1196 }
1197 for (a, exp) in args.iter().zip(expected.iter()) {
1198 if let Some(at) = type_of(a, Some(exp), ctx)
1199 && !compatible(&at, exp)
1200 {
1201 ctx.errors.push(CompileError::new(
1202 "bynk.types.argument_mismatch",
1203 a.span,
1204 format!("expected `{}`, found `{}`", exp.display(), at.display()),
1205 ));
1206 }
1207 }
1208 Some(effect)
1209}
1210
1211fn require_capability(
1219 site: Span,
1220 capability: &str,
1221 source: RequirementSource,
1222 ctx: &mut Ctx,
1223 code: &'static str,
1224 message: &str,
1225) {
1226 let covered = ctx.caps.capabilities.contains_key(capability);
1227 if covered {
1228 ctx.caps.given_used.insert(capability.to_string());
1229 } else {
1230 ctx.errors
1231 .push(CompileError::new(code, site, message).with_note(format!(
1232 "add `{capability}` to the handler's `given` clause"
1233 )));
1234 }
1235 record_requirement(ctx, capability, site, source, covered);
1236}
1237
1238fn record_requirement(
1243 ctx: &mut Ctx,
1244 capability: &str,
1245 site: Span,
1246 source: RequirementSource,
1247 covered: bool,
1248) {
1249 let materialize = if covered {
1250 None
1251 } else {
1252 given_insertion_edit(&ctx.caps.given_entries, ctx.caps.given_anchor, capability).map(
1253 |(edit_span, edit_text)| Materialize {
1254 anchor: ctx.caps.given_anchor.unwrap_or(ctx.return_ty_span),
1255 edit_span,
1256 edit_text,
1257 },
1258 )
1259 };
1260 ctx.requirements.record(Requirement {
1261 capability: capability.to_string(),
1262 site,
1263 source,
1264 covered,
1265 materialize,
1266 });
1267}
1268
1269pub(crate) fn check_store_cache_op(
1274 method: &Ident,
1275 args: &[Expr],
1276 k: &Ty,
1277 v: &Ty,
1278 span: Span,
1279 ctx: &mut Ctx,
1280) -> Option<Ty> {
1281 let vfn = || Ty::Fn {
1282 params: vec![v.clone()],
1283 ret: Box::new(v.clone()),
1284 };
1285 let (expected, result): (Vec<Ty>, Ty) = match method.name.as_str() {
1286 "put" => (vec![k.clone(), v.clone()], Ty::Unit),
1287 "get" => (vec![k.clone()], Ty::Option(Box::new(v.clone()))),
1288 "remove" => (vec![k.clone()], Ty::Unit),
1289 "contains" => (vec![k.clone()], Ty::Base(BaseType::Bool)),
1290 "size" => (vec![], Ty::Base(BaseType::Int)),
1291 "update" => (vec![k.clone(), vfn()], Ty::Unit),
1292 "upsert" => (vec![k.clone(), v.clone(), vfn()], Ty::Unit),
1293 other => {
1294 ctx.errors.push(
1295 CompileError::new(
1296 "bynk.store.unknown_op",
1297 method.span,
1298 format!(
1299 "a `Cache` store field has no operation `{other}` — expected `put`, \
1300 `get`, `update`, `upsert`, `remove`, `contains`, or `size`"
1301 ),
1302 )
1303 .with_note("storage-cache ops are entry-level and effectful (await with `<-`)"),
1304 );
1305 for a in args {
1306 type_of(a, None, ctx);
1307 }
1308 return None;
1309 }
1310 };
1311 if method.name != "remove" {
1313 require_capability(
1314 method.span,
1315 "Clock",
1316 RequirementSource::StoreOp {
1317 kind: StoreKind::Cache,
1318 op: method.name.clone(),
1319 },
1320 ctx,
1321 "bynk.store.cache_needs_clock",
1322 "a `Cache` operation applies TTL expiry, which reads the clock — the handler must declare `given Clock`",
1323 );
1324 }
1325 let effect = Ty::Effect(Box::new(result));
1326 if args.len() != expected.len() {
1327 ctx.errors.push(CompileError::new(
1328 "bynk.types.call_arity",
1329 span,
1330 format!(
1331 "`Cache.{}` takes {} argument(s), found {}",
1332 method.name,
1333 expected.len(),
1334 args.len()
1335 ),
1336 ));
1337 for a in args {
1338 type_of(a, None, ctx);
1339 }
1340 return Some(effect);
1341 }
1342 for (a, exp) in args.iter().zip(expected.iter()) {
1343 if let Some(at) = type_of(a, Some(exp), ctx)
1344 && !compatible(&at, exp)
1345 {
1346 ctx.errors.push(CompileError::new(
1347 "bynk.types.argument_mismatch",
1348 a.span,
1349 format!("expected `{}`, found `{}`", exp.display(), at.display()),
1350 ));
1351 }
1352 }
1353 Some(effect)
1354}
1355
1356pub(crate) fn check_store_log_op(
1364 method: &Ident,
1365 args: &[Expr],
1366 elem: &Ty,
1367 span: Span,
1368 ctx: &mut Ctx,
1369) -> Option<Ty> {
1370 let query = || Ty::Query(Box::new(elem.clone()));
1371 let arity = |n: usize, ctx: &mut Ctx| {
1372 if args.len() != n {
1373 ctx.errors.push(CompileError::new(
1374 "bynk.types.call_arity",
1375 span,
1376 format!(
1377 "`Log.{}` takes {n} argument(s), found {}",
1378 method.name,
1379 args.len()
1380 ),
1381 ));
1382 for a in args {
1383 type_of(a, None, ctx);
1384 }
1385 return false;
1386 }
1387 true
1388 };
1389 let window_arg = |a: &Expr, what: &str, ctx: &mut Ctx| {
1390 if let Some(at) = type_of(a, Some(&Ty::Base(BaseType::Instant)), ctx)
1391 && !compatible(&at, &Ty::Base(BaseType::Instant))
1392 {
1393 ctx.errors.push(CompileError::new(
1394 "bynk.types.argument_mismatch",
1395 a.span,
1396 format!("{what} expects `Instant`, found `{}`", at.display()),
1397 ));
1398 }
1399 };
1400 match method.name.as_str() {
1401 "append" => {
1403 require_capability(
1404 method.span,
1405 "Clock",
1406 RequirementSource::StoreOp {
1407 kind: StoreKind::Log,
1408 op: method.name.clone(),
1409 },
1410 ctx,
1411 "bynk.store.log_needs_clock",
1412 "`Log.append` stamps the current time, which reads the clock — the handler must declare `given Clock`",
1413 );
1414 if !arity(1, ctx) {
1415 return Some(Ty::Effect(Box::new(Ty::Unit)));
1416 }
1417 if let Some(at) = type_of(&args[0], Some(elem), ctx)
1418 && !compatible(&at, elem)
1419 {
1420 ctx.errors.push(CompileError::new(
1421 "bynk.types.argument_mismatch",
1422 args[0].span,
1423 format!("expected `{}`, found `{}`", elem.display(), at.display()),
1424 ));
1425 }
1426 Some(Ty::Effect(Box::new(Ty::Unit)))
1427 }
1428 "since" | "before" => {
1430 if !arity(1, ctx) {
1431 return Some(query());
1432 }
1433 window_arg(&args[0], &format!("`Log.{}`", method.name), ctx);
1434 Some(query())
1435 }
1436 "between" => {
1437 if !arity(2, ctx) {
1438 return Some(query());
1439 }
1440 window_arg(&args[0], "`Log.between` start", ctx);
1441 window_arg(&args[1], "`Log.between` end", ctx);
1442 Some(query())
1443 }
1444 "recent" => {
1445 if !arity(1, ctx) {
1446 return Some(query());
1447 }
1448 check_arg(
1449 &args[0],
1450 &Ty::Base(BaseType::Int),
1451 "the `Log.recent` count",
1452 ctx,
1453 );
1454 Some(query())
1455 }
1456 "reversed" => {
1457 if !arity(0, ctx) {
1458 return Some(query());
1459 }
1460 Some(query())
1461 }
1462 name if is_query_op(name) => check_query_kernel_method(method, args, elem, span, ctx),
1464 other => {
1465 ctx.errors.push(
1466 CompileError::new(
1467 "bynk.store.unknown_op",
1468 method.span,
1469 format!(
1470 "a `Log` store field has no operation `{other}` — `append`, the \
1471 time-window roots (`since`/`before`/`between`/`recent`/`reversed`), \
1472 and the query builders/terminals"
1473 ),
1474 )
1475 .with_note(
1476 "`Log` reads are lazy `Query[T]`; only `append` is effectful and writes",
1477 ),
1478 );
1479 for a in args {
1480 type_of(a, None, ctx);
1481 }
1482 None
1483 }
1484 }
1485}
1486
1487pub(crate) fn check_store_set_op(
1493 method: &Ident,
1494 args: &[Expr],
1495 t: &Ty,
1496 span: Span,
1497 ctx: &mut Ctx,
1498) -> Option<Ty> {
1499 let (expected, result): (Vec<Ty>, Ty) = match method.name.as_str() {
1500 "add" => (vec![t.clone()], Ty::Unit),
1501 "remove" => (vec![t.clone()], Ty::Unit),
1502 "contains" => (vec![t.clone()], Ty::Base(BaseType::Bool)),
1503 "size" => (vec![], Ty::Base(BaseType::Int)),
1504 other => {
1505 ctx.errors.push(
1506 CompileError::new(
1507 "bynk.store.unknown_op",
1508 method.span,
1509 format!(
1510 "a `Set` store field has no operation `{other}` — expected `add`, \
1511 `remove`, `contains`, or `size`"
1512 ),
1513 )
1514 .with_note(
1515 "set algebra (`union`/`intersection`/`difference`) is not in this slice",
1516 ),
1517 );
1518 for a in args {
1519 type_of(a, None, ctx);
1520 }
1521 return None;
1522 }
1523 };
1524 let effect = Ty::Effect(Box::new(result));
1525 if args.len() != expected.len() {
1526 ctx.errors.push(CompileError::new(
1527 "bynk.types.call_arity",
1528 span,
1529 format!(
1530 "`Set.{}` takes {} argument(s), found {}",
1531 method.name,
1532 expected.len(),
1533 args.len()
1534 ),
1535 ));
1536 for a in args {
1537 type_of(a, None, ctx);
1538 }
1539 return Some(effect);
1540 }
1541 for (a, exp) in args.iter().zip(expected.iter()) {
1542 if let Some(at) = type_of(a, Some(exp), ctx)
1543 && !compatible(&at, exp)
1544 {
1545 ctx.errors.push(CompileError::new(
1546 "bynk.types.argument_mismatch",
1547 a.span,
1548 format!("expected `{}`, found `{}`", exp.display(), at.display()),
1549 ));
1550 }
1551 }
1552 Some(effect)
1553}
1554
1555pub(crate) fn check_store_cell_op(
1563 method: &Ident,
1564 args: &[Expr],
1565 t: &Ty,
1566 span: Span,
1567 ctx: &mut Ctx,
1568) -> Option<Ty> {
1569 let tfn = || Ty::Fn {
1570 params: vec![t.clone()],
1571 ret: Box::new(t.clone()),
1572 };
1573 let (expected, result): (Vec<Ty>, Ty) = match method.name.as_str() {
1574 "update" => (vec![tfn()], Ty::Unit),
1575 other => {
1576 ctx.errors.push(
1577 CompileError::new(
1578 "bynk.store.unknown_op",
1579 method.span,
1580 format!("a `Cell` store field has no operation `{other}` — expected `update`"),
1581 )
1582 .with_note(
1583 "a cell is read by its bare name and written with `:=`; `update` is the only \
1584 method-shaped op",
1585 ),
1586 );
1587 for a in args {
1588 type_of(a, None, ctx);
1589 }
1590 return None;
1591 }
1592 };
1593 let effect = Ty::Effect(Box::new(result));
1594 if args.len() != expected.len() {
1595 ctx.errors.push(CompileError::new(
1596 "bynk.types.call_arity",
1597 span,
1598 format!(
1599 "`Cell.{}` takes {} argument(s), found {}",
1600 method.name,
1601 expected.len(),
1602 args.len()
1603 ),
1604 ));
1605 for a in args {
1606 type_of(a, None, ctx);
1607 }
1608 return Some(effect);
1609 }
1610 for (a, exp) in args.iter().zip(expected.iter()) {
1611 if let Some(at) = type_of(a, Some(exp), ctx)
1612 && !compatible(&at, exp)
1613 {
1614 ctx.errors.push(CompileError::new(
1615 "bynk.types.argument_mismatch",
1616 a.span,
1617 format!("expected `{}`, found `{}`", exp.display(), at.display()),
1618 ));
1619 }
1620 }
1621 Some(effect)
1622}
1623
1624pub(crate) fn check_method_call(
1625 receiver: &Expr,
1626 method: &Ident,
1627 type_args: &[TypeRef],
1628 args: &[Expr],
1629 span: Span,
1630 expected: Option<&Ty>,
1631 ctx: &mut Ctx,
1632) -> Option<Ty> {
1633 if !type_args.is_empty()
1638 && !matches!(&receiver.kind, ExprKind::Ident(id) if id.name == JSON
1639 && !ctx.input.types.contains_key(JSON))
1640 {
1641 ctx.errors.push(CompileError::new(
1642 "bynk.generics.type_arg_mismatch",
1643 span,
1644 format!(
1645 "`{}` is not a generic method — it takes no type arguments",
1646 method.name
1647 ),
1648 ));
1649 for a in args {
1650 let _ = type_of(a, None, ctx);
1651 }
1652 return None;
1653 }
1654 if let ExprKind::Ident(id) = &receiver.kind
1659 && method.name == "call"
1660 && ctx.lookup(id.name.as_str()).is_none()
1661 && ctx.test_services.contains(&id.name)
1662 && let Some(unit) = ctx.input.cross_context.self_context.clone()
1663 {
1664 ctx.refs
1665 .record_in_unit(id.span, SymbolKind::Service, &id.name, &unit);
1666 }
1667 if ctx.lookup_root_ident(receiver).is_none() {
1674 if let Some(chain) = flatten_ident_chain(receiver)
1678 && let Some((consumed, cap)) = ctx.input.cross_context.resolve_cross_capability(&chain)
1679 {
1680 if let ExprKind::FieldAccess { field, .. } = &receiver.kind {
1683 ctx.refs
1684 .record_in_unit(field.span, SymbolKind::Capability, &cap, &consumed);
1685 }
1686 return check_cross_context_capability_call(
1687 receiver, &consumed, &cap, method, args, span, ctx,
1688 );
1689 }
1690 if let Some(consumed) = cross_context_prefix(receiver, ctx) {
1691 return check_cross_context_call(receiver, &consumed, method, args, span, ctx);
1692 }
1693 if let ExprKind::FieldAccess { .. } = &receiver.kind
1699 && let Some(chain) = flatten_ident_chain(receiver)
1700 && chain.contains('.')
1701 {
1702 let info = &ctx.input.cross_context;
1703 let in_context = info.self_context.is_some();
1704 if in_context && info.resolve_prefix(&chain).is_none() {
1705 ctx.errors.push(
1706 CompileError::new(
1707 "bynk.resolve.unconsumed_context",
1708 receiver.span,
1709 format!(
1710 "`{chain}.{}` looks like a cross-context service call, but `{chain}` is not in this context's `consumes` clauses",
1711 method.name
1712 ),
1713 )
1714 .with_note(
1715 "add a `consumes {chain}` clause at the top of the context, or use an alias and call it through the alias",
1716 ),
1717 );
1718 for a in args {
1719 let _ = type_of(a, None, ctx);
1720 }
1721 return None;
1722 }
1723 }
1724 }
1725 if let ExprKind::Ident(id) = &receiver.kind
1729 && ctx.lookup(id.name.as_str()).is_none()
1730 && (ctx.caps.capabilities.contains_key(&id.name)
1731 || ctx.caps.declared_capabilities.contains_key(&id.name))
1732 {
1733 return check_static_call(id, method, args, span, ctx);
1734 }
1735 if let ExprKind::Ident(id) = &receiver.kind
1738 && ctx.lookup(id.name.as_str()).is_none()
1739 && ctx.input.types.contains_key(&id.name)
1740 {
1741 return check_static_call(id, method, args, span, ctx);
1742 }
1743 if let ExprKind::Ident(id) = &receiver.kind
1747 && ctx.lookup(id.name.as_str()).is_none()
1748 && !ctx.input.types.contains_key(&id.name)
1749 && (id.name == LIST || id.name == MAP)
1750 {
1751 return check_collection_static(id, method, args, span, expected, ctx);
1752 }
1753 if let ExprKind::Ident(id) = &receiver.kind
1757 && (id.name == INT || id.name == FLOAT)
1758 {
1759 return check_numeric_parse_static(id, method, args, span, ctx);
1760 }
1761 if let ExprKind::Ident(id) = &receiver.kind
1764 && id.name == DURATION
1765 && ctx.lookup(DURATION).is_none()
1766 && !ctx.input.types.contains_key(DURATION)
1767 {
1768 return check_duration_static(method, args, span, ctx);
1769 }
1770 if let ExprKind::Ident(id) = &receiver.kind
1772 && id.name == INSTANT
1773 && ctx.lookup(INSTANT).is_none()
1774 && !ctx.input.types.contains_key(INSTANT)
1775 {
1776 return check_instant_static(method, args, span, ctx);
1777 }
1778 if let ExprKind::Ident(id) = &receiver.kind
1781 && id.name == BYTES
1782 && ctx.lookup(BYTES).is_none()
1783 && !ctx.input.types.contains_key(BYTES)
1784 {
1785 return check_bytes_static(method, args, span, ctx);
1786 }
1787 if let ExprKind::Ident(id) = &receiver.kind
1789 && id.name == JSON
1790 && ctx.lookup(JSON).is_none()
1791 && !ctx.input.types.contains_key(JSON)
1792 {
1793 return check_json_static(method, type_args, args, span, expected, ctx);
1794 }
1795 if let ExprKind::Ident(id) = &receiver.kind
1797 && id.name == STREAM
1798 && ctx.lookup(STREAM).is_none()
1799 && !ctx.input.types.contains_key(STREAM)
1800 {
1801 return check_stream_static(method, args, span, ctx);
1802 }
1803 let recv_expected = match (expected, method.name.as_str()) {
1807 (Some(t), "insert") => peel_to_map(t).map(|(k, v)| Ty::Map(Box::new(k), Box::new(v))),
1808 (Some(t), "prepend") => peel_to_list(t).map(|e| Ty::List(Box::new(e))),
1809 _ => None,
1810 };
1811 let recv_ty = type_of(receiver, recv_expected.as_ref(), ctx)?;
1812 match recv_ty.clone() {
1817 Ty::List(elem) => {
1818 return check_list_kernel_method(method, args, &elem, span, ctx);
1819 }
1820 Ty::Query(elem) => {
1822 return check_query_kernel_method(method, args, &elem, span, ctx);
1823 }
1824 Ty::Stream(elem) => {
1826 return check_stream_kernel_method(method, args, &elem, span, ctx);
1827 }
1828 Ty::Connection(frame) => {
1832 return check_connection_method(method, args, &frame, span, ctx);
1833 }
1834 Ty::Map(key, val) => {
1835 return check_map_kernel_method(method, args, &key, &val, span, ctx);
1836 }
1837 Ty::Base(base @ (BaseType::Int | BaseType::Float)) => {
1840 return check_numeric_kernel_method(method, args, base, span, ctx);
1841 }
1842 Ty::Base(BaseType::Duration) => {
1844 return check_duration_kernel_method(method, args, span, ctx);
1845 }
1846 Ty::Base(BaseType::Instant) => {
1848 return check_instant_kernel_method(method, args, span, ctx);
1849 }
1850 Ty::Base(BaseType::Bytes) => {
1852 return check_bytes_kernel_method(method, args, span, ctx);
1853 }
1854 Ty::Base(BaseType::String) => {
1856 return check_string_kernel_method(method, args, span, ctx);
1857 }
1858 Ty::Option(inner) => {
1860 return check_option_kernel_method(method, args, &inner, span, ctx);
1861 }
1862 Ty::Result(ok, err) => {
1863 return check_result_kernel_method(method, args, &ok, &err, span, ctx);
1864 }
1865 _ => {}
1866 }
1867 let type_name = match &recv_ty {
1869 Ty::Named { name, .. } => name.clone(),
1870 _ => {
1871 ctx.errors.push(CompileError::new(
1872 "bynk.types.method_on_non_named_type",
1873 method.span,
1874 format!(
1875 "type `{}` has no methods — only user-declared types support method calls",
1876 recv_ty.display()
1877 ),
1878 ));
1879 return None;
1880 }
1881 };
1882 if let Some(agent) = ctx.input.agents.get(&type_name).cloned() {
1886 let Some(handler) = agent.handlers.iter().find(|h| {
1887 h.method_name
1888 .as_ref()
1889 .is_some_and(|n| n.name == method.name)
1890 }) else {
1891 ctx.errors.push(CompileError::new(
1892 "bynk.agent.handler_not_found",
1893 method.span,
1894 format!(
1895 "agent `{}` has no handler named `{}`",
1896 type_name, method.name
1897 ),
1898 ));
1899 for a in args {
1900 let _ = type_of(a, None, ctx);
1901 }
1902 return None;
1903 };
1904 if handler.params.len() != args.len() {
1905 ctx.errors.push(CompileError::new(
1906 "bynk.agent.handler_arity",
1907 method.span,
1908 format!(
1909 "agent handler `{}.{}` expects {} argument(s), but {} were given",
1910 type_name,
1911 method.name,
1912 handler.params.len(),
1913 args.len()
1914 ),
1915 ));
1916 for a in args {
1917 let _ = type_of(a, None, ctx);
1918 }
1919 return None;
1920 }
1921 for (p, arg) in handler.params.iter().zip(args.iter()) {
1922 let pty = resolve_type_ref(&p.type_ref, &ctx.input.types);
1923 let _ = type_of(arg, pty.as_ref(), ctx);
1924 }
1925 return resolve_type_ref(&handler.return_type, &ctx.input.types);
1926 }
1927 let table = ctx
1928 .input
1929 .methods
1930 .get(&type_name)
1931 .cloned()
1932 .unwrap_or_default();
1933 let Some(method_decl) = table.instance.get(&method.name).cloned() else {
1934 ctx.errors.push(CompileError::new(
1935 "bynk.types.method_not_found",
1936 method.span,
1937 format!(
1938 "type `{}` has no instance method named `{}`",
1939 type_name, method.name
1940 ),
1941 ));
1942 return None;
1943 };
1944 ctx.refs.record(
1949 method.span,
1950 SymbolKind::Method,
1951 &format!("{type_name}.{}", method.name),
1952 );
1953 if method_decl.params.len() != args.len() {
1955 ctx.errors.push(
1956 CompileError::new(
1957 "bynk.types.method_arity",
1958 method.span,
1959 format!(
1960 "method `{}.{}` expects {} argument(s), but {} were given",
1961 type_name,
1962 method.name,
1963 method_decl.params.len(),
1964 args.len()
1965 ),
1966 )
1967 .with_label(method_decl.name.ident().span, "method declared here"),
1968 );
1969 for a in args {
1970 let _ = type_of(a, None, ctx);
1971 }
1972 return None;
1973 }
1974 let mut ok = true;
1975 for (i, (param, arg)) in method_decl.params.iter().zip(args.iter()).enumerate() {
1976 record_param_hint(ctx.hints, ¶m.name.name, arg);
1977 let expected = resolve_type_ref(¶m.type_ref, &ctx.input.types);
1978 let actual = type_of(arg, expected.as_ref(), ctx);
1979 let (Some(actual), Some(expected)) = (actual, expected) else {
1980 ok = false;
1981 continue;
1982 };
1983 if !compatible(&actual, &expected) {
1984 ctx.errors.push(CompileError::new(
1985 "bynk.types.argument_mismatch",
1986 arg.span,
1987 format!(
1988 "argument {} to `{}.{}` has type `{}`, but parameter `{}` expects `{}`",
1989 i + 1,
1990 type_name,
1991 method.name,
1992 actual.display(),
1993 param.name.name,
1994 expected.display()
1995 ),
1996 ));
1997 ok = false;
1998 }
1999 }
2000 let _ = span;
2001 if !ok {
2002 return None;
2003 }
2004 resolve_type_ref(&method_decl.return_type, &ctx.input.types)
2005}
2006
2007fn cross_context_prefix(receiver: &Expr, ctx: &Ctx) -> Option<String> {
2012 let info = &ctx.input.cross_context;
2013 if info.consumed_contexts.is_empty() && info.aliases.is_empty() {
2014 return None;
2015 }
2016 let candidate = flatten_ident_chain(receiver)?;
2021 let head = candidate.split('.').next().unwrap_or("");
2022 if ctx.lookup(head).is_some() {
2024 return None;
2025 }
2026 if ctx.caps.capabilities.contains_key(head) || ctx.caps.declared_capabilities.contains_key(head)
2027 {
2028 return None;
2029 }
2030 info.resolve_prefix(candidate.as_str())
2034}
2035
2036fn flatten_ident_chain(expr: &Expr) -> Option<String> {
2039 match &expr.kind {
2040 ExprKind::Ident(id) => Some(id.name.clone()),
2041 ExprKind::FieldAccess { receiver, field } => {
2042 let head = flatten_ident_chain(receiver)?;
2043 Some(format!("{head}.{}", field.name))
2044 }
2045 _ => None,
2046 }
2047}
2048
2049fn check_cross_context_capability_call(
2057 receiver: &Expr,
2058 consumed: &str,
2059 cap: &str,
2060 method: &Ident,
2061 args: &[Expr],
2062 _span: Span,
2063 ctx: &mut Ctx,
2064) -> Option<Ty> {
2065 if !ctx.effectful {
2067 ctx.errors.push(CompileError::new(
2068 "bynk.effect.capability_in_pure_context",
2069 method.span,
2070 format!(
2071 "capability `{consumed}.{cap}` can only be called inside an effectful body (one returning `Effect[T]`)"
2072 ),
2073 ));
2074 }
2075 if !ctx.caps.given_remaining.contains(cap) {
2078 let mut err = CompileError::new(
2079 "bynk.given.undeclared_capability",
2080 receiver.span,
2081 format!("capability `{consumed}.{cap}` is used but not listed in the `given` clause"),
2082 )
2083 .with_note(format!(
2084 "add `{consumed}.{cap}` to the handler's `given` clause so the dependency surface is visible at the declaration site"
2085 ));
2086 if let Some((span, insert)) = given_insertion_edit(
2089 &ctx.caps.given_entries,
2090 ctx.caps.given_anchor,
2091 &format!("{consumed}.{cap}"),
2092 ) {
2093 err = err.with_suggestion(
2094 format!("add `{consumed}.{cap}` to the `given` clause"),
2095 vec![(span, insert)],
2096 Applicability::MachineApplicable,
2097 );
2098 }
2099 ctx.errors.push(err);
2100 for a in args {
2101 let _ = type_of(a, None, ctx);
2102 }
2103 return None;
2104 }
2105 ctx.caps.given_used.insert(cap.to_string());
2106
2107 let info = &ctx.input.cross_context;
2108 let op = info
2109 .consumed_capabilities
2110 .get(consumed)
2111 .and_then(|caps| caps.get(cap))
2112 .and_then(|c| c.ops.iter().find(|o| o.name == method.name))
2113 .cloned();
2114 let Some(op) = op else {
2115 ctx.errors.push(CompileError::new(
2116 "bynk.capability.unknown_operation",
2117 method.span,
2118 format!(
2119 "capability `{consumed}.{cap}` has no operation named `{}`",
2120 method.name
2121 ),
2122 ));
2123 for a in args {
2124 let _ = type_of(a, None, ctx);
2125 }
2126 return None;
2127 };
2128 ctx.refs.record_in_unit(
2132 method.span,
2133 SymbolKind::CapabilityOp,
2134 &format!("{cap}.{}", method.name),
2135 consumed,
2136 );
2137 if op.params.len() != args.len() {
2138 ctx.errors.push(CompileError::new(
2139 "bynk.capability.op_arity",
2140 method.span,
2141 format!(
2142 "capability operation `{consumed}.{cap}.{}` expects {} argument(s), but {} were given",
2143 method.name,
2144 op.params.len(),
2145 args.len()
2146 ),
2147 ));
2148 for a in args {
2149 let _ = type_of(a, None, ctx);
2150 }
2151 return None;
2152 }
2153
2154 let consumed_types = info
2156 .consumed_types
2157 .get(consumed)
2158 .cloned()
2159 .unwrap_or_default();
2160 let mut all_ok = true;
2161 for (i, ((pname, ptype_ref), arg)) in op.params.iter().zip(args.iter()).enumerate() {
2162 record_param_hint(ctx.hints, pname, arg);
2163 let param_ty = resolve_type_ref(ptype_ref, &consumed_types).unwrap_or(Ty::Unit);
2164 let Some(arg_ty) = type_of(arg, None, ctx) else {
2165 all_ok = false;
2166 continue;
2167 };
2168 if !structurally_compatible(&arg_ty, ¶m_ty, &ctx.input.types, &consumed_types) {
2169 ctx.errors.push(CompileError::new(
2170 "bynk.boundary.structural_mismatch",
2171 arg.span,
2172 format!(
2173 "cross-context argument {} to `{consumed}.{cap}.{}` has type `{}`, but parameter `{pname}` expects `{}`",
2174 i + 1,
2175 method.name,
2176 arg_ty.display(),
2177 param_ty.display(),
2178 ),
2179 ));
2180 all_ok = false;
2181 }
2182 }
2183 if !all_ok {
2184 return None;
2185 }
2186 let raw_ret = resolve_type_ref(&op.return_type, &consumed_types).unwrap_or(Ty::Unit);
2187 Some(rebrand_return_type(&raw_ret, &ctx.input.types))
2188}
2189
2190fn check_cross_context_call(
2191 receiver: &Expr,
2192 consumed: &str,
2193 method: &Ident,
2194 args: &[Expr],
2195 _span: Span,
2196 ctx: &mut Ctx,
2197) -> Option<Ty> {
2198 if !ctx.effectful {
2201 ctx.errors.push(
2202 CompileError::new(
2203 "bynk.effect.cross_context_in_pure_context",
2204 method.span,
2205 format!(
2206 "cross-context service call `{}.{}` can only be made inside an effectful body (one returning `Effect[T]`)",
2207 consumed, method.name
2208 ),
2209 )
2210 .with_label(receiver.span, "consumed context prefix"),
2211 );
2212 }
2213 let info = &ctx.input.cross_context;
2214 let Some(svcs) = info.consumed_services.get(consumed) else {
2215 ctx.errors.push(
2216 CompileError::new(
2217 "bynk.consumes.unknown_context",
2218 receiver.span,
2219 format!("context `{consumed}` is not in scope here"),
2220 )
2221 .with_note(
2222 "add a `consumes` clause for the target context at the top of the consuming context",
2223 ),
2224 );
2225 for a in args {
2226 let _ = type_of(a, None, ctx);
2227 }
2228 return None;
2229 };
2230 let Some(service) = svcs.get(&method.name).cloned() else {
2231 ctx.errors.push(
2232 CompileError::new(
2233 "bynk.consumes.unknown_service",
2234 method.span,
2235 format!(
2236 "context `{consumed}` has no service named `{}`",
2237 method.name
2238 ),
2239 )
2240 .with_note(
2241 "cross-context calls require an `on call` service handler in the consumed context",
2242 ),
2243 );
2244 for a in args {
2245 let _ = type_of(a, None, ctx);
2246 }
2247 return None;
2248 };
2249 ctx.refs
2250 .record_in_unit(method.span, SymbolKind::Service, &method.name, consumed);
2251
2252 if service.params.len() != args.len() {
2253 ctx.errors.push(
2254 CompileError::new(
2255 "bynk.consumes.service_arity",
2256 method.span,
2257 format!(
2258 "cross-context service `{consumed}.{}` expects {} argument(s), but {} were given",
2259 method.name,
2260 service.params.len(),
2261 args.len()
2262 ),
2263 )
2264 .with_label(service.span, "service declared here"),
2265 );
2266 for a in args {
2267 let _ = type_of(a, None, ctx);
2268 }
2269 return None;
2270 }
2271
2272 let consumed_types = info
2274 .consumed_types
2275 .get(consumed)
2276 .cloned()
2277 .unwrap_or_default();
2278
2279 let mut all_ok = true;
2281 for (i, ((pname, ptype_ref), arg)) in service.params.iter().zip(args.iter()).enumerate() {
2282 record_param_hint(ctx.hints, pname, arg);
2283 let param_ty = resolve_type_ref(ptype_ref, &consumed_types).unwrap_or(Ty::Unit);
2284 let arg_ty = type_of(arg, None, ctx);
2286 let Some(arg_ty) = arg_ty else {
2287 all_ok = false;
2288 continue;
2289 };
2290 if !structurally_compatible(&arg_ty, ¶m_ty, &ctx.input.types, &consumed_types) {
2291 ctx.errors.push(
2292 CompileError::new(
2293 "bynk.boundary.structural_mismatch",
2294 arg.span,
2295 format!(
2296 "cross-context argument {} to `{consumed}.{}` has type `{}` in `{}`, but parameter `{pname}` expects `{}` in `{}`",
2297 i + 1,
2298 method.name,
2299 arg_ty.display(),
2300 ctx.input
2301 .cross_context
2302 .self_context
2303 .as_deref()
2304 .unwrap_or("?"),
2305 param_ty.display(),
2306 consumed,
2307 ),
2308 )
2309 .with_label(service.span, "service declared here")
2310 .with_note(
2311 "values crossing a context boundary must have structurally compatible types (same commons-derived type, or identical record/sum shape)",
2312 ),
2313 );
2314 all_ok = false;
2315 }
2316 }
2317 if !all_ok {
2318 return None;
2319 }
2320
2321 let raw_ret = resolve_type_ref(&service.return_type, &consumed_types).unwrap_or(Ty::Unit);
2325 let rebranded = rebrand_return_type(&raw_ret, &ctx.input.types);
2326 Some(rebranded)
2327}