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