Skip to main content

bynk_check/checker/
calls.rs

1//! Call / application dispatch.
2//!
3//! Split out of `checker.rs` (v0.29.10) verbatim; the parent module
4//! re-exports these via `use calls::*`.
5
6use super::*;
7
8/// v0.39 (ADR 0072): record a parameter-name inlay hint for one call argument,
9/// unless it would be noise — the `_`/`self` placeholders, or an argument that
10/// is already the identically-named identifier (`f(count)` for parameter
11/// `count`, matching rust-analyzer's suppression).
12fn 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    // v0.20a: the fn's type parameters are *rigid* type variables while
36    // checking its own body. A type param shadowing a declared type is
37    // confusing — diagnose the collision.
38    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    // For methods, the implicit `self` parameter has the attached type.
65    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            // v0.31: a fn parameter is in scope over the whole body.
75            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/// v0.11: type-check an agent state-field initialiser (`field: T = init`). The
126/// initialiser must be a *static* value of the field type — it is checked in an
127/// empty, pure scope (so `self`, parameters, capabilities, and effects are all
128/// out of reach) with the field type as the expected type, so refined literals
129/// admit (v0.9.4) and sum variants resolve. The init's expression types are
130/// recorded into `expr_types` for emission; a single
131/// `bynk.agents.bad_state_initialiser` is pushed on any failure.
132#[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; // an unresolved field type is reported elsewhere
145    };
146    let mut local_errors: Vec<CompileError> = Vec::new();
147    // A state initialiser is a static, pure value — no capability calls reach
148    // here — so requirements are discarded into a throwaway sink.
149    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
201/// v0.91 (ADR 0116 D6): the `bynk.list` free functions whose method forms
202/// shipped with slice 1 are **deprecated** in favour of the method-chain
203/// vocabulary. `reverse`/`traverse` stay (no method form yet). Emits a
204/// non-failing warning (ADR 0117) with a machine-applicable rewrite to the
205/// method form — `map(xs, f)` → `xs.map(f)`, `find(xs, p)` → `xs.filter(p).first()`.
206fn 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    // The method-form spelling each free function rewrites to.
211    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, // reverse / traverse keep their free-function form for now
218    };
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    // The auto-fix needs the (list, fn) shape — a wrong-arity call is reported
231    // elsewhere; only offer the rewrite when it is well-formed.
232    if args.len() == 2 {
233        let mut edits = vec![
234            // delete `name(` — the receiver becomes the first argument.
235            (
236                Span::new(name.span.start, args[0].span.start),
237                String::new(),
238            ),
239            // the `, ` between the two args becomes `.<method>(`.
240            (
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            // `filter(p)` then `.first()` after the closing `)`.
254            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    // v0.20a: explicit type arguments only apply to (generic) functions.
281    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    // Could be a bare variant constructor with payload.
296    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    // Agent instantiation: `AgentName(key)` constructs an instance keyed by
308    // `key`. The result type carries the agent's name so subsequent
309    // `agent_instance.method(args)` lookups can find the agent's handler set.
310    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    // v0.20a: value application — calling a scope binding (param/local) of
349    // function type. Placed AFTER fns/variants/agents: putting scope first
350    // would change the meaning of currently-passing programs (the additive
351    // guard); the resulting ident/call precedence asymmetry is pre-existing
352    // and documented in §5.
353    if let Some(ty) = ctx.lookup(&name.name) {
354        return match ty {
355            Ty::Fn { params, ret } => check_value_application(name, &params, &ret, args, span, ctx),
356            other => {
357                // Relocated from the resolver (which has no type info): a
358                // non-function-typed value called as a function.
359                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
446/// v0.20a: instantiate and check a call to a generic function. Two-pass
447/// argument-directed inference: pass 1 types every **non-lambda** argument
448/// left-to-right against the (possibly still Var-bearing) expected — safe
449/// because every expected-driven feature in `type_of` matches concrete
450/// variants, so a Var falls through benignly (pinned by a unit test) — and
451/// unifies; pass 2 types **lambda** arguments against the now-substituted
452/// expecteds (a lambda whose expected params are still Var-bearing is
453/// uninferable) and unifies the result, capturing return-position variables.
454/// Conflicts demand exact equality (`bynk.generics.type_arg_mismatch`); the
455/// explicit `name[T](…)` form builds the substitution directly.
456fn 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            // Resolve explicit type args with the *enclosing* fn's type
498            // params in scope, so `identity[A](x)` inside a generic body
499            // works.
500            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    // Pass 1 — non-lambda arguments.
507    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    // Pass 2 — lambda arguments, against substituted expecteds.
531    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        // A fully-annotated lambda grounds the variables itself: type it
559        // bottom-up and let unify capture them.
560        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    // Every type parameter must now be determined.
581    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    // Final compatibility over the fully-ground parameter types.
599    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    // v0.39 (ADR 0072): when the user omitted the type arguments, show the
625    // inferred ones as a `Type`-kind hint after the function name —
626    // `identity` ⟨`[Int]`⟩ `(5)`. Declaration order; skipped if any var stayed
627    // unresolved (defensive — the arg loop above already grounds them).
628    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    // v0.20b: the return is ground *up to the caller's rigid type
641    // parameters* — a generic fn calling another generic fn (bynk.list's
642    // `map` calling `reverse`) legitimately instantiates the callee at its
643    // own rigid vars, which flow through `compatible` by name-equality.
644    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    // v0.20a: generic functions take the instantiation path; the
655    // non-generic path below runs byte-identically to v0.19 (the additive
656    // guard). Explicit type args on a non-generic fn are rejected.
657    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, &param.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
718/// Type-check a kernel-method argument against its expected type, with the
719/// expected type propagated in (so lambdas and literals type contextually).
720pub(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
737/// Record a capability reference's binding edge, qualifying flattened bare
738/// names (`consumes U { Cap }`) to their providing unit (v0.25). A bare
739/// non-flattened name is the consuming unit's own declaration — qualified
740/// at assembly.
741fn 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    // Capability dispatch (v0.5): if `type_name` names a capability declared
758    // in the context, dispatch via the capability table. If the capability is
759    // declared but not in `given`, error specifically.
760    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        // v0.26 (ADR 0054): the one-click counterpart of the note.
777        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        // v0.99: an uncovered direct capability call — record it so the ghost
790        // `given` inlay hint can offer the same clause at the declaration site
791        // (DECISION D/E). `span` is the call site (the inner `given_insertion_edit`
792        // binding shadowed it only within the `if let` above).
793        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        // v0.99: a covered direct capability call — the call site *is* the
823        // reason (DECISION C). Recorded so hover can explain what a declared
824        // `given Cap` is for; no inlay hint (already covered).
825        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        // v0.36 (ADR 0069, slice 2): the op is an index symbol keyed by the
849        // compound `"Cap.op"` name; this local call is a reference.
850        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    // 1) User-declared static method.
905    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    // 2) Built-in `of` constructor on refined or opaque types.
910    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        // `.of` is always the runtime constructor: it returns
942        // `Result[T, ValidationError]`. Compile-time literal admission (v0.9.4)
943        // happens instead wherever an expected refined type is known — see
944        // `admit_refined_literal`, used by `type_of` — so `.of`'s type never
945        // depends on the form of its argument.
946        return Some(Ty::Result(
947            Box::new(named_ty(&decl)),
948            Box::new(Ty::ValidationError),
949        ));
950    }
951
952    // 2b) Built-in `unsafe` constructor on opaque types — only available
953    // inside the defining commons.
954    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    // 3) Qualified variant construction `TypeName.Variant(args)`.
1005    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, &param.name.name, arg);
1053        let expected = resolve_type_ref(&param.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
1082/// v0.82 (ADR 0110): resolve a storage-map operation `<map>.<op>(args)` on a
1083/// `store Map[K, V]` field. The ops are effect-typed (storage I/O, awaited with
1084/// `<-`): `put`/`update`/`upsert`/`remove` → `Effect[()]`, `get` →
1085/// `Effect[Option[V]]`, `contains` → `Effect[Bool]`, `size` → `Effect[Int]`.
1086/// `update` on an absent key is a runtime fault; `upsert` is the default-if-absent
1087/// form. Dispatched by receiver provenance, so it never shadows the immutable
1088/// value `Map`'s pure methods.
1089pub(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    // v0.105 (slice 3b-ii): a held `Map[K, Connection]` stores connection ids and
1102    // resolves them by identity; `update`/`upsert` transform the value through a
1103    // `(V) -> V` function, which has no meaning for a held resource — you cannot
1104    // derive a new connection from an old one. Reject them (the other entry ops —
1105    // `put`/`get`/`remove`/`contains`/`size` — are admitted) so the program is a
1106    // clean compile error rather than a silent miscompile.
1107    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
1183/// v0.99: record a capability requirement at `site` into the ledger, and — when
1184/// the enclosing handler's `given` does not cover it — push the diagnostic. The
1185/// single producer behind both the bare diagnostic and the editor surfaces
1186/// (DECISION D): every requirement is *recorded*, covered or not; only an
1187/// uncovered one errors. The `code`/`message` are the consuming feature's, so a
1188/// store op keeps its precise diagnostic; the ledger's *reason* renders from
1189/// `source` alone (DECISION C), never from `code`.
1190fn 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
1210/// Push a [`Requirement`] into the ledger. For an uncovered requirement it also
1211/// computes the materialization edit (the ghost `given` inlay hint's one-click
1212/// apply) from the handler's existing `given` entries and anchor — the same
1213/// `given_insertion_edit` the undeclared-capability quick-fix uses.
1214fn 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
1241/// v0.87 (ADR 0113): resolve a storage-`Cache` operation `<cache>.<op>(args)` on
1242/// a `store Cache[K, V]` field. The op set is the storage `Map`'s
1243/// (`put`/`get`/`update`/`upsert`/`remove`/`contains`/`size`); every op but
1244/// `remove` additionally requires `given Clock` (eviction reads the clock).
1245pub(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    // D4: every op but `remove` reads the clock for TTL expiry.
1284    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
1328/// v0.95 (ADR 0121): resolve a storage-`Log` operation `<log>.<op>(args)` on a
1329/// `store Log[T]` field. `append(e)` is the effectful, **non-idempotent** write
1330/// (`Effect[()]`) and the one clock-consuming op — it stamps `Clock.now()`, so it
1331/// requires `given Clock`. The time-window roots — `since(Instant)`/
1332/// `before(Instant)` / `between(Instant, Instant)` / `recent(Int)` / `reversed()`
1333/// — and the general query vocabulary lift the log into a lazy `Query[T]` over its
1334/// entry values; these need no clock (window bounds are explicit `Instant`s).
1335pub(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        // The one effectful write — non-idempotent; stamps the clock.
1374        "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        // Time-window query roots → `Query[T]` (lazy; no clock).
1401        "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        // The general query vocabulary over the entry values.
1435        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
1459/// v0.83: resolve a storage-set operation `<set>.<op>(args)` on a `store Set[T]`
1460/// field. Effect-typed entry ops: `add(t)`/`remove(t)` → `Effect[()]` (both
1461/// idempotent), `contains(t)` → `Effect[Bool]`, `size()` → `Effect[Int]`. Set
1462/// algebra (`union`/`intersection`/`difference`) is deferred (it needs a value
1463/// `Set` return type). Dispatched by receiver provenance.
1464pub(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
1527/// v0.98 (ADR 0125): resolve a storage-`Cell` operation `<cell>.<op>(args)` on a
1528/// `store Cell[T]` field. The single method-shaped cell op is `update(f)` —
1529/// `f: (T) -> T` — a read-modify-write typed `Effect[()]`. Reading a cell is the
1530/// bare-name sugar and writing it is `:=`, so `read`/`write` are not callable
1531/// methods (DECISION B). The combiner is a non-effectful `Ty::Fn`, so an
1532/// effectful body (including a bare read of another cell) fails the existing
1533/// function-type check (DECISION E). Dispatched by receiver provenance.
1534pub(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    // v0.22b: explicit type arguments apply only to the `Json.decode[T]`
1606    // static — every other method/static takes none (the 0039/0045 rule;
1607    // generic *user* methods remain deferred). A user-declared type named
1608    // `Json` shadows the codec module and takes no type arguments.
1609    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    // v0.25: a test body invokes the target's service as `svc.call(args)`.
1627    // The emitter wires it from the same service set; the checker types it
1628    // loosely (the runner recovers outcomes at runtime), but the binding
1629    // edge is real — record it so test-file references index.
1630    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    // v0.6: cross-context service call. Two shapes:
1640    //   - `Alias.service(args)`           where Alias is from `consumes X as Alias`
1641    //   - `prefix.tail.service(args)`     where `prefix.tail` is a consumed context's
1642    //                                     qualified name (parsed as nested FieldAccess).
1643    // The full-qualified-name form must be checked before the bare-ident form
1644    // (the prefix's first segment doesn't resolve as anything local).
1645    if ctx.lookup_root_ident(receiver).is_none() {
1646        // v0.15: cross-context capability call — `B.Cap.op(args)` /
1647        // `Alias.Cap.op(args)`. Checked before the service-call shape because
1648        // the receiver carries an extra (capability) segment.
1649        if let Some(chain) = flatten_ident_chain(receiver)
1650            && let Some((consumed, cap)) = ctx.input.cross_context.resolve_cross_capability(&chain)
1651        {
1652            // v0.25: the capability name-segment (`Cap` in `B.Cap` /
1653            // `Alias.Cap`) is the outermost field of the receiver chain.
1654            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        // Looks like a dotted prefix (no local binding for the root). If the
1666        // chain matches the shape of a consumed-context call but the prefix
1667        // isn't actually consumed, surface an explicit diagnostic so the user
1668        // can fix the missing `consumes` clause rather than seeing a silent
1669        // "no methods" error later.
1670        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    // Detect capability call (v0.5): receiver is a bare Ident naming a
1698    // capability declared in the context (in scope via `given`, or declared
1699    // but undeclared in `given` — the static-call path emits the error).
1700    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    // Detect static-call shape: receiver is a bare Ident naming a declared
1708    // type (not a local/param). Dispatch to check_static_call.
1709    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    // v0.20b: qualified statics on the built-in collection types —
1716    // `List.empty()` / `Map.empty()`. Like an empty `[]`, they need an
1717    // expected type to pin their element/key/value types.
1718    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    // v0.22a: the numeric parse statics — `Int.parse(s)` / `Float.parse(s)`.
1726    // The parser only admits these keywords in receiver position when
1727    // followed by `.`, so the Ident shape here is exactly the static form.
1728    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    // v0.86 (ADR 0112): the `Duration.millis(n)` static constructor — the way to
1734    // build a `Duration` from a runtime `Int` (the literal covers constants).
1735    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    // v0.90 (ADR 0114 D6): the `Instant.fromEpochMillis(n)` static constructor.
1743    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    // v0.110 (ADR 0142 D2): the `Bytes` static constructors —
1751    // `Bytes.fromUtf8(s)` / `Bytes.fromBase64(s)` / `Bytes.empty()`.
1752    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    // v0.22b: the typed JSON codec statics (ADR 0045).
1760    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    // v0.100: the `Stream.of(xs)` static constructor (real-time track slice 0).
1768    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    // v0.20b: `insert`/`prepend` return their receiver's collection type —
1776    // propagate an expected collection type down the chain so
1777    // `let m: Map[String, Int] = Map.empty().insert("a", 1)` infers.
1778    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    // v0.20b: built-in kernel methods on the collection types. These are
1785    // compiler-known special forms typed directly here — generic in their
1786    // accumulator without the (deferred) declared-generic-methods feature;
1787    // the deferral bites only on declared methods (ADR 0037).
1788    match recv_ty.clone() {
1789        Ty::List(elem) => {
1790            return check_list_kernel_method(method, args, &elem, span, ctx);
1791        }
1792        // v0.91 (ADR 0115): a chained builder/terminal on a lazy `Query[T]`.
1793        Ty::Query(elem) => {
1794            return check_query_kernel_method(method, args, &elem, span, ctx);
1795        }
1796        // v0.100: a chained builder/terminal on a `Stream[T]`.
1797        Ty::Stream(elem) => {
1798            return check_stream_kernel_method(method, args, &elem, span, ctx);
1799        }
1800        // v0.102: the held-resource operations on a `Connection[F]` — `send(f)`
1801        // (non-consuming) and `close()` (consuming). The linearity pass tracks
1802        // the ownership transitions; this types the operations.
1803        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        // v0.21: the numeric kernel — conversions as value methods on the
1810        // bare base types (a refined value reaches them via `.raw`).
1811        Ty::Base(base @ (BaseType::Int | BaseType::Float)) => {
1812            return check_numeric_kernel_method(method, args, base, span, ctx);
1813        }
1814        // v0.86 (ADR 0112): the `Duration` kernel — `toMillis`/`toString`.
1815        Ty::Base(BaseType::Duration) => {
1816            return check_duration_kernel_method(method, args, span, ctx);
1817        }
1818        // v0.90 (ADR 0114): the `Instant` kernel — `toEpochMillis`/`toString`.
1819        Ty::Base(BaseType::Instant) => {
1820            return check_instant_kernel_method(method, args, span, ctx);
1821        }
1822        // v0.110 (ADR 0142): the `Bytes` kernel — `length`/`toBase64`/`decodeUtf8`.
1823        Ty::Base(BaseType::Bytes) => {
1824            return check_bytes_kernel_method(method, args, span, ctx);
1825        }
1826        // v0.22a: the string kernel (ADR 0046).
1827        Ty::Base(BaseType::String) => {
1828            return check_string_kernel_method(method, args, span, ctx);
1829        }
1830        // v0.22a: the Option/Result combinators as kernel methods (ADR 0048).
1831        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    // Find a named type for the receiver, then look up its instance methods.
1840    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    // Agent handler dispatch: when the receiver is an agent instance, look
1855    // up the method against the agent's declared `on call` handlers and
1856    // resolve to the handler's return type.
1857    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    // v0.36 (ADR 0069): the method is a first-class index symbol, keyed by the
1917    // compound `"Type.method"` name. Recorded already-spelled from the resolved
1918    // receiver type; the bare edge resolves through the same `uses`/`consumes`
1919    // qualification as cross-file type references.
1920    ctx.refs.record(
1921        method.span,
1922        SymbolKind::Method,
1923        &format!("{type_name}.{}", method.name),
1924    );
1925    // Param count excludes the implicit `self`.
1926    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, &param.name.name, arg);
1949        let expected = resolve_type_ref(&param.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
1979/// If `receiver` resolves to a consumed-context prefix (an alias or a
1980/// dotted qualified name appearing in `consumes`), return the consumed
1981/// context's qualified name. Otherwise None. Local bindings, types, and
1982/// capabilities take precedence — those are checked at the call site.
1983fn 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    // Walk the receiver to assemble a candidate dotted name. Supports:
1989    //   Ident(X)                                 -> "X"
1990    //   FieldAccess { Ident(A), B }              -> "A.B"
1991    //   FieldAccess { FieldAccess { Ident(A), B }, C } -> "A.B.C"
1992    let candidate = flatten_ident_chain(receiver)?;
1993    let head = candidate.split('.').next().unwrap_or("");
1994    // The head must not shadow a local binding / capability / declared type.
1995    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    // If the head is a known local type, only an alias whose name happens to
2003    // collide could redirect this; aliases conflicting with types are an
2004    // error in project.rs, so a clash here is impossible at this point.
2005    info.resolve_prefix(candidate.as_str())
2006}
2007
2008/// Flatten an `Ident`/`FieldAccess` chain into its dotted name, or None if
2009/// any segment isn't a bare identifier.
2010fn 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
2021/// Type-check a cross-context service call (v0.6 §4.2). `receiver` carries
2022/// the prefix's source span for diagnostics. `consumed` is the resolved
2023/// qualified name of the consumed context.
2024/// v0.15: type-check a cross-context capability call `B.Cap.op(args)` /
2025/// `Alias.Cap.op(args)`. The capability operation signatures are carried in
2026/// `consumed_capabilities` (in the providing context's namespace); the
2027/// capability must be listed in the handler/provider's `given` clause.
2028fn 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    // Capability calls require an effectful body (same rule as local ones).
2038    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    // The capability must be declared in this handler/provider's `given`.
2048    // The local deps key is the capability's simple name.
2049    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        // v0.26 (ADR 0054): the one-click counterpart of the note — the
2059        // clause entry is the qualified form the user writes (`B.Cap`).
2060        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    // v0.36 (ADR 0069, slice 2): a cross-context op call references the op,
2101    // recorded already-qualified into the providing unit (where the op is
2102    // declared), mirroring the cross-context capability reference.
2103    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    // Resolve parameter / return types in the consumed context's namespace.
2127    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, &param_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    // The consuming context must be effectful at this call site (services
2171    // and agent handlers are; pure free fns are not).
2172    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    // Resolve the consumed-context types so we can describe parameter shapes.
2245    let consumed_types = info
2246        .consumed_types
2247        .get(consumed)
2248        .cloned()
2249        .unwrap_or_default();
2250
2251    // Walk each argument, checking structural compatibility (Phase 4).
2252    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        // Type-check the argument in the caller's context.
2257        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, &param_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    // Return type rebrand: project the consumed context's return type into
2294    // the calling context's namespace by renaming named types whose unqualified
2295    // name appears in the caller's type table (v0.6 §4.5).
2296    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}