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<ExprId, TypedExpr>,
29    callees: &mut HashMap<ExprId, Callee>,
30    errors: &mut Vec<CompileError>,
31    refs: &mut RefSink,
32    hints: &mut HintSink,
33    locals: &mut LocalsSink,
34    requirements: &mut RequirementSink,
35    tys: &Types,
36) {
37    // v0.20a: the fn's type parameters are *rigid* type variables while
38    // checking its own body. A type param shadowing a declared type is
39    // confusing — diagnose the collision.
40    let mut vars: HashSet<String> = f
41        .type_params
42        .iter()
43        .map(|tp| tp.name.name.clone())
44        .collect();
45    for tp in &f.type_params {
46        if input.types.contains_key(&tp.name.name) {
47            errors.push(
48                CompileError::new(
49                    "bynk.generics.type_arg_mismatch",
50                    tp.span,
51                    format!(
52                        "type parameter `{}` shadows the declared type of the same name",
53                        tp.name.name
54                    ),
55                )
56                .with_note("rename the type parameter"),
57            );
58        }
59    }
60    // #594: an instance method on a generic type inherits the receiver type's
61    // parameters as additional rigid vars, so its signature and body may name
62    // them (`self: Box[A]`, `f: A -> U`). The receiver's parameters are the
63    // type's own — they never shadow a declared type — so they bypass the guard
64    // above.
65    if let FnName::Method { type_name, .. } = &f.name
66        && let Some(decl) = input.types.get(&type_name.name)
67    {
68        for tp in &decl.type_params {
69            vars.insert(tp.name.name.clone());
70        }
71    }
72    let return_ty = match resolve_type_ref_in(&f.return_type, &input.types, &vars, tys) {
73        Some(t) => t,
74        None => return,
75    };
76    record_type_refs(&f.return_type, &input.types, &vars, refs);
77    let mut param_scope: HashMap<String, TyId> = HashMap::new();
78    // For methods, the implicit `self` parameter has the attached type. #594: on
79    // a generic receiver, `self` is the type applied to its own parameters as
80    // rigid vars (`Box[A]`), so field access substitutes them and the body's
81    // uses of `A` line up with the signature.
82    if let FnName::Method { type_name, .. } = &f.name
83        && f.has_self
84        && let Some(decl) = input.types.get(&type_name.name)
85    {
86        let self_args = decl
87            .type_params
88            .iter()
89            .map(|tp| tys.intern(Ty::Var(tp.name.name.clone())))
90            .collect();
91        param_scope.insert("self".to_string(), named_ty_with_args(decl, self_args, tys));
92    }
93    for p in &f.params {
94        if let Some(ty) = resolve_type_ref_in(&p.type_ref, &input.types, &vars, tys) {
95            record_type_refs(&p.type_ref, &input.types, &vars, refs);
96            // v0.31: a fn parameter is in scope over the whole body.
97            if p.name.name != "_" {
98                locals.record(
99                    p.name.name.clone(),
100                    p.name.span,
101                    crate::locals::LocalKind::Param,
102                    ty.display(tys),
103                    f.body.span,
104                );
105            }
106            param_scope.insert(p.name.name.clone(), ty);
107        }
108    }
109    // v0.115: check the function's contract clauses (`requires`/`ensures`) in
110    // the parameter scope before the body — a contract is the invariant
111    // predicate attached to a function (ADR 0144). `result` in an `ensures` is
112    // the return value, awaited for an `Effect`.
113    if !f.requires.is_empty() || !f.ensures.is_empty() {
114        let result_ty = match &*tys.get(return_ty) {
115            Ty::Effect(inner) => *inner,
116            _ => return_ty,
117        };
118        let has_result_param = f.params.iter().any(|p| p.name.name == "result");
119        let fn_label = format!("function `{}`", f.name.display());
120        check_contracts(
121            &f.requires,
122            &f.ensures,
123            &param_scope,
124            result_ty,
125            has_result_param,
126            &fn_label,
127            input,
128            expr_types,
129            errors,
130            refs,
131            hints,
132            locals,
133            requirements,
134            callees,
135            &vars,
136            tys,
137        );
138    }
139    let effectful = return_ty.is_effect(tys);
140    let mut ctx = Ctx {
141        input,
142        tys,
143        expr_types,
144        errors,
145        refs,
146        hints,
147        locals,
148        requirements,
149        callees,
150        scopes: vec![param_scope],
151        is_binding_cache: HashMap::new(),
152        pattern_binding_types: HashMap::new(),
153        return_ty,
154        return_ty_span: f.return_type.span(),
155        effectful,
156        agent_state_ty: None,
157        commit_seen: false,
158        caps: CapabilityCtx::default(),
159        in_test_body: false,
160        test_services: HashMap::new(),
161        test_actors: HashMap::new(),
162        type_vars: vars.clone(),
163        store_fields: HashMap::new(),
164    };
165    let Some(body_ty) = type_of_block(&f.body, Some(return_ty), &mut ctx) else {
166        return;
167    };
168    // #718: run the held-resource linearity pass over `fn`/method bodies too, not
169    // just handlers. The caller side treats passing a held value into a function
170    // as a transfer (disposal), so the callee *owns* any held parameter and must
171    // dispose it (close, store, or transfer) before returning — otherwise a
172    // `swallow(c)` leaks and a double `c.close()` goes undiagnosed. No parameter
173    // is borrowed here (the borrowed case is a handler's firing connection), so
174    // the borrowed set is empty; every held param is seeded owned.
175    linearity::check(
176        &f.body,
177        &f.params,
178        &input.types,
179        ctx.expr_types,
180        &ctx.pattern_binding_types,
181        &HashSet::new(),
182        ctx.errors,
183        tys,
184    );
185    if !compatible(body_ty, return_ty, tys) {
186        ctx.errors.push(
187            CompileError::new(
188                "bynk.types.return_mismatch",
189                f.body.tail.span,
190                format!(
191                    "function body has type `{}`, but the declared return type is `{}`",
192                    body_ty.display(tys),
193                    return_ty.display(tys)
194                ),
195            )
196            .with_label(f.return_type.span(), "declared return type"),
197        );
198    }
199}
200
201/// The shared discipline behind both `check_state_initialiser` (agent `store`
202/// fields) and `check_event_field_default` (Events slice 3a, #972, event
203/// record fields): the initialiser must be a *static* value of the field
204/// type — checked in an empty, pure scope (so `self`, parameters,
205/// capabilities, and effects are all out of reach) with the field type as
206/// the expected type, so refined literals admit (v0.9.4) and sum variants
207/// resolve. The init's expression types are recorded into `expr_types` for
208/// emission, and its `Callee` classification (a `.of`/`.unsafe`/variant-
209/// constructor call, at most) into `callees` (P6.7, #1163) — every
210/// `Callee`-driven reader of a call-shaped expression reads `program.callees`
211/// unconditionally (ADR 0334), so a call-shaped static initialiser with no
212/// entry there would panic on a certified program the checker legitimately
213/// accepted, not a recoverable state. (The IR-side `Cell`-init lowering that
214/// first motivated this was deleted by Slice D1 of #1542; the invariant
215/// outlives it because `bynk-emit`'s own emitter reads the same table.)
216/// `code`/`subject`
217/// name the caller's own diagnostic and the noun used in its message ("state
218/// field initialiser" / "event field default").
219#[allow(clippy::too_many_arguments)]
220fn check_static_initialiser(
221    init: &Expr,
222    field_type: &TypeRef,
223    input: &ResolvedCommons,
224    expr_types: &mut HashMap<ExprId, TypedExpr>,
225    callees: &mut HashMap<ExprId, Callee>,
226    errors: &mut Vec<CompileError>,
227    refs: &mut RefSink,
228    hints: &mut HintSink,
229    locals: &mut LocalsSink,
230    code: &'static str,
231    subject: &str,
232    tys: &Types,
233) {
234    let Some(field_ty) = resolve_type_ref(field_type, &input.types, tys) else {
235        return; // an unresolved field type is reported elsewhere
236    };
237    let mut local_errors: Vec<CompileError> = Vec::new();
238    // A static initialiser is a pure value — no capability calls reach here —
239    // so requirements are discarded into a throwaway sink.
240    let mut init_requirements = RequirementSink::new();
241    let result = {
242        let mut ctx = Ctx {
243            input,
244            tys,
245            expr_types,
246            errors: &mut local_errors,
247            refs,
248            hints,
249            locals,
250            requirements: &mut init_requirements,
251            callees,
252            scopes: vec![HashMap::new()],
253            is_binding_cache: HashMap::new(),
254            pattern_binding_types: HashMap::new(),
255            return_ty: field_ty,
256            return_ty_span: init.span,
257            effectful: false,
258            agent_state_ty: None,
259            commit_seen: false,
260            caps: CapabilityCtx::default(),
261            in_test_body: false,
262            test_services: HashMap::new(),
263            test_actors: HashMap::new(),
264            type_vars: HashSet::new(),
265            store_fields: HashMap::new(),
266        };
267        type_of(init, Some(field_ty), &mut ctx)
268    };
269    let compatible_result = matches!(&result, Some(t) if compatible(*t, field_ty, tys));
270    if !compatible_result || !local_errors.is_empty() {
271        let got = result
272            .map(|t| t.display(tys))
273            .unwrap_or_else(|| "an invalid expression".to_string());
274        errors.push(
275            CompileError::new(
276                code,
277                init.span,
278                format!(
279                    "{subject} must be a static value of type `{}` (got `{got}`)",
280                    field_ty.display(tys),
281                ),
282            )
283            .with_note(
284                "an initialiser is a compile-time value — a literal (including one admitted to a \
285                 refined type), a sum variant, `Some`/`None`/`Ok`/`Err`, a record, or — for an \
286                 opaque type — `T.unsafe(lit)` — with no reference to `self`, parameters, or \
287                 capabilities",
288            ),
289        );
290    }
291}
292
293/// v0.11: type-check an agent state-field initialiser (`field: T = init`).
294/// See `check_static_initialiser`. Pushes `bynk.agents.bad_state_initialiser`.
295#[allow(clippy::too_many_arguments)]
296pub fn check_state_initialiser(
297    init: &Expr,
298    field_type: &TypeRef,
299    input: &ResolvedCommons,
300    tys: &Types,
301    expr_types: &mut HashMap<ExprId, TypedExpr>,
302    callees: &mut HashMap<ExprId, Callee>,
303    errors: &mut Vec<CompileError>,
304    refs: &mut RefSink,
305    hints: &mut HintSink,
306    locals: &mut LocalsSink,
307) {
308    check_static_initialiser(
309        init,
310        field_type,
311        input,
312        expr_types,
313        callees,
314        errors,
315        refs,
316        hints,
317        locals,
318        "bynk.agents.bad_state_initialiser",
319        "state field initialiser",
320        tys,
321    );
322}
323
324/// Events slice 3a (#972): type-check an event field's default expression
325/// (`field: T = init`), reusing `check_static_initialiser`'s empty-pure-scope
326/// discipline. Pushes `bynk.event.bad_field_default`.
327///
328/// One admission `check_static_initialiser` doesn't cover on its own: an
329/// opaque type's `T.unsafe(lit)` is, by design (ADR 0182), a bypass of its
330/// own refinement — `type_of`'s ordinary `ConstructorCall` handling only
331/// checks `lit`'s *base* type, not the refinement, since that's the whole
332/// point of `unsafe`. An event field default is different: it becomes part
333/// of the wire codec (slice 3a lowers it to its *wire* JSON form and splices
334/// it into `deserialise_<Event>`, which validates a defaulted value exactly
335/// like a real one), so a default that bypasses its own refinement would
336/// compile cleanly and then fail at runtime the first time an old event
337/// actually triggers it — a deferred, surprising failure this check closes
338/// statically instead. Only `T.unsafe(lit)` on a *literal* argument is
339/// checked here (anything else already fails the static-value requirement
340/// above); a violated refinement pushes the same `bynk.event.bad_field_default`.
341#[allow(clippy::too_many_arguments)]
342pub fn check_event_field_default(
343    init: &Expr,
344    field_type: &TypeRef,
345    input: &ResolvedCommons,
346    tys: &Types,
347    expr_types: &mut HashMap<ExprId, TypedExpr>,
348    callees: &mut HashMap<ExprId, Callee>,
349    errors: &mut Vec<CompileError>,
350    refs: &mut RefSink,
351    hints: &mut HintSink,
352    locals: &mut LocalsSink,
353) {
354    check_static_initialiser(
355        init,
356        field_type,
357        input,
358        expr_types,
359        callees,
360        errors,
361        refs,
362        hints,
363        locals,
364        "bynk.event.bad_field_default",
365        "event field default",
366        tys,
367    );
368    // `T.unsafe(lit)` parses as `ExprKind::MethodCall { receiver: Ident(T),
369    // method: "unsafe", .. }` — confirmed by direct AST inspection; the
370    // parser never distinguishes a type-qualified call from an ordinary
371    // instance method call (that's a resolver-time decision), so
372    // `ExprKind::ConstructorCall` is not what this actually produces.
373    if let ExprKind::MethodCall {
374        receiver,
375        method,
376        args,
377        ..
378    } = &init.kind
379        && let ExprKind::Ident(type_name) = &receiver.kind
380        && method.name == "unsafe"
381        && let [lit_expr] = args.as_slice()
382        && let Some(decl) = input.types.get(&type_name.name)
383        && matches!(decl.body, TypeBody::Opaque { .. })
384        && let Some(refinement) = refinements::type_decl_refinement(decl)
385        && let Some(lit) = refinements::const_literal(lit_expr)
386        && let Some(failed) = refinements::first_failed_predicate(refinement, &lit)
387    {
388        errors.push(
389            CompileError::new(
390                "bynk.event.bad_field_default",
391                lit_expr.span,
392                format!(
393                    "`{}.unsafe(...)` bypasses its own refinement, but an event field default \
394                     must be a value the wire could actually carry — this literal fails `{}`",
395                    type_name.name,
396                    failed.name(),
397                ),
398            )
399            .with_note(
400                "a default is spliced into the same codec that validates a real wire value on \
401                 receipt, so a refinement-violating `.unsafe(lit)` default would compile cleanly \
402                 and then fail at runtime the first time an old event actually triggers it",
403            ),
404        );
405    }
406}
407
408/// v0.91 (ADR 0116 D6): the `bynk.list` free functions whose method forms
409/// shipped with slice 1 are **deprecated** in favour of the method-chain
410/// vocabulary. `reverse`/`traverse` stay (no method form yet). Emits a
411/// non-failing warning (ADR 0117) with a machine-applicable rewrite to the
412/// method form — `map(xs, f)` → `xs.map(f)`, `find(xs, p)` → `xs.filter(p).first()`.
413fn warn_bynk_list_deprecation(name: &Ident, args: &[Expr], call_span: Span, ctx: &mut Ctx) {
414    if ctx.input.imported_from.get(&name.name).map(String::as_str) != Some("bynk.list") {
415        return;
416    }
417    // The method-form spelling each free function rewrites to.
418    let method_form: &str = match name.name.as_str() {
419        "map" => "xs.map(f)",
420        "filter" => "xs.filter(p)",
421        "any" => "xs.any(p)",
422        "all" => "xs.all(p)",
423        "find" => "xs.filter(p).first()",
424        _ => return, // reverse / traverse keep their free-function form for now
425    };
426    let mut err = CompileError::new(
427        "bynk.list.deprecated_function",
428        name.span,
429        format!(
430            "`bynk.list.{}` is deprecated — use the `List` method form `{method_form}`",
431            name.name
432        ),
433    )
434    .with_note(
435        "the `bynk.list.*` free functions are superseded by the method-chain vocabulary (ADR 0116); the method form reads left-to-right and chains",
436    );
437    // The auto-fix needs the (list, fn) shape — a wrong-arity call is reported
438    // elsewhere; only offer the rewrite when it is well-formed.
439    if args.len() == 2 {
440        let mut edits = vec![
441            // delete `name(` — the receiver becomes the first argument.
442            (
443                Span::new(name.span.start, args[0].span.start),
444                String::new(),
445            ),
446            // the `, ` between the two args becomes `.<method>(`.
447            (
448                Span::new(args[0].span.end, args[1].span.start),
449                format!(
450                    ".{}(",
451                    if name.name == "find" {
452                        "filter"
453                    } else {
454                        &name.name
455                    }
456                ),
457            ),
458        ];
459        if name.name == "find" {
460            // `filter(p)` then `.first()` after the closing `)`.
461            edits.push((
462                Span::new(call_span.end, call_span.end),
463                ".first()".to_string(),
464            ));
465        }
466        err = err.with_suggestion(
467            format!("rewrite to the `List` method form `{method_form}`"),
468            edits,
469            Applicability::MachineApplicable,
470        );
471    }
472    ctx.errors.push(err);
473}
474
475pub(crate) fn check_call(
476    name: &Ident,
477    type_args: &[TypeRef],
478    args: &[Expr],
479    span: Span,
480    // #593: the binding's expected type grounds a generic variant constructor
481    // whose payload cannot determine every parameter (`let o: Opt[Int] = Nil`).
482    expected: Option<TyId>,
483    // P6.0 (#1139): the outer `Call` expression's own identity — keys the
484    // `Callee` classification recorded at whichever branch below dispatches.
485    expr_id: ExprId,
486    ctx: &mut Ctx,
487) -> Option<TyId> {
488    let tys = ctx.tys;
489    if let Some(fn_decl) = ctx.input.fns.get(&name.name) {
490        ctx.refs.record(name.span, SymbolKind::Fn, &name.name);
491        ctx.callees.insert(expr_id, Callee::Fn(Arc::clone(fn_decl)));
492        warn_bynk_list_deprecation(name, args, span, ctx);
493        return check_call_against_fn(name, fn_decl, type_args, args, ctx);
494    }
495    // v0.20a: explicit type arguments only apply to (generic) functions.
496    if !type_args.is_empty() {
497        ctx.errors.push(CompileError::new(
498            "bynk.generics.type_arg_mismatch",
499            span,
500            format!(
501                "`{}` is not a generic function — it takes no type arguments",
502                name.name
503            ),
504        ));
505        for a in args {
506            let _ = type_of(a, None, ctx);
507        }
508        return None;
509    }
510    // Could be a bare variant constructor with payload. Borrow the matching
511    // sum decls' own `Arc`s (a pointer bump each, no full-body clones); the
512    // ambiguity check below reuses the same list.
513    let owners: Vec<&Arc<TypeDecl>> = ctx
514        .input
515        .types
516        .values()
517        .filter(|t| matches!(&t.body, TypeBody::Sum(s) if s.variants.iter().any(|v| v.name.name == name.name)))
518        .collect();
519    if owners.len() == 1 {
520        ctx.callees.insert(
521            expr_id,
522            Callee::Ctor {
523                sum: Arc::clone(owners[0]),
524                tag: name.name.clone(),
525            },
526        );
527        return check_variant_construction(owners[0], &name.name, args, span, expected, ctx);
528    }
529    // Agent instantiation: `AgentName(key)` constructs an instance keyed by
530    // `key`. The result type carries the agent's name so subsequent
531    // `agent_instance.method(args)` lookups can find the agent's handler set.
532    if let Some(agent) = ctx.input.agents.get(&name.name).cloned() {
533        ctx.refs.record(name.span, SymbolKind::Agent, &name.name);
534        ctx.callees
535            .insert(expr_id, Callee::AgentInit(name.name.clone()));
536        let key_ty = resolve_type_ref(&agent.key_type, &ctx.input.types, tys);
537        if args.len() != 1 {
538            ctx.errors.push(CompileError::new(
539                "bynk.agent.construction_arity",
540                span,
541                format!(
542                    "agent `{}` is constructed with one key argument, but {} were given",
543                    name.name,
544                    args.len()
545                ),
546            ));
547            for a in args {
548                let _ = type_of(a, None, ctx);
549            }
550            return None;
551        }
552        let arg_ty = type_of(&args[0], key_ty, ctx);
553        if let (Some(a), Some(k)) = (arg_ty, key_ty)
554            && !compatible(a, k, tys)
555        {
556            ctx.errors.push(CompileError::new(
557                "bynk.agent.key_mismatch",
558                args[0].span,
559                format!(
560                    "agent `{}` key is `{}`, but a value of type `{}` was given",
561                    name.name,
562                    k.display(tys),
563                    a.display(tys)
564                ),
565            ));
566        }
567        return Some(tys.intern(Ty::Named {
568            name: name.name.clone(),
569            kind: NamedKind::Record,
570            args: Vec::new(),
571        }));
572    }
573    // v0.20a: value application — calling a scope binding (param/local) of
574    // function type. Placed AFTER fns/variants/agents: putting scope first
575    // would change the meaning of currently-passing programs (the additive
576    // guard); the resulting ident/call precedence asymmetry is pre-existing
577    // and documented in §5.
578    if let Some(ty) = ctx.lookup(&name.name) {
579        return match &*tys.get(ty) {
580            Ty::Fn { params, ret } => {
581                ctx.callees
582                    .insert(expr_id, Callee::Value(name.name.clone()));
583                check_value_application(name, params, *ret, args, span, ctx)
584            }
585            _ => {
586                // Relocated from the resolver (which has no type info): a
587                // non-function-typed value called as a function.
588                ctx.errors.push(
589                    CompileError::new(
590                        "bynk.resolve.param_as_function",
591                        span,
592                        format!(
593                            "`{}` has type `{}` and is not callable",
594                            name.name,
595                            ty.display(tys)
596                        ),
597                    )
598                    .with_note("only values of function type can be applied"),
599                );
600                for a in args {
601                    let _ = type_of(a, None, ctx);
602                }
603                None
604            }
605        };
606    }
607    // Nothing owns the call. The resolver's reference walk reports these in
608    // `fn`/method bodies, but handler/service/agent bodies and test bodies
609    // never pass through that walk — the checker is their only backstop, and
610    // a silent `None` here admitted any unknown call and emitted it verbatim.
611    // Mirror the resolver's ladder. #1708: test bodies were exempt, for the
612    // same since-repaired reason as `check_ident`'s twin gate; dropped with it.
613    for a in args {
614        let _ = type_of(a, None, ctx);
615    }
616    if owners.len() > 1 {
617        ctx.errors.push(CompileError::new(
618            "bynk.resolve.ambiguous_variant",
619            name.span,
620            format!(
621                "the variant name `{}` is declared on multiple sum types — qualify it as `TypeName.{}(...)`",
622                name.name, name.name
623            ),
624        ));
625        return None;
626    }
627    if ctx.input.types.contains_key(&name.name) {
628        ctx.errors.push(CompileError::new(
629            "bynk.resolve.type_as_function",
630            span,
631            format!(
632                "`{}` is a type, not a function — use `{}.of(value)` or `{} {{ ... }}` instead",
633                name.name, name.name, name.name
634            ),
635        ));
636        return None;
637    }
638    ctx.errors.push(
639        CompileError::new(
640            "bynk.resolve.unknown_function",
641            span,
642            format!("unknown function `{}`", name.name),
643        )
644        .with_note("only functions declared in this commons are callable"),
645    );
646    None
647}
648
649fn check_value_application(
650    name: &Ident,
651    params: &[TyId],
652    ret: TyId,
653    args: &[Expr],
654    span: Span,
655    ctx: &mut Ctx,
656) -> Option<TyId> {
657    let tys = ctx.tys;
658    if ret.is_effect(tys) && !ctx.effectful {
659        ctx.errors.push(
660            CompileError::new(
661                "bynk.effect.fn_value_in_pure_context",
662                span,
663                format!(
664                    "`{}` is an effectful function (`{}`) and cannot be called in a pure context",
665                    name.name,
666                    Ty::Fn {
667                        params: params.to_vec(),
668                        ret,
669                    }
670                    .display(tys)
671                ),
672            )
673            .with_note(
674                "effectful function values may only be called where the enclosing body is effectful (its return type is an Effect)",
675            ),
676        );
677    }
678    if params.len() != args.len() {
679        ctx.errors.push(CompileError::new(
680            "bynk.types.call_arity",
681            span,
682            format!(
683                "`{}` takes {} argument(s), but {} were given",
684                name.name,
685                params.len(),
686                args.len()
687            ),
688        ));
689        for a in args {
690            let _ = type_of(a, None, ctx);
691        }
692        return None;
693    }
694    for (arg, param_ty) in args.iter().zip(params) {
695        let arg_ty = type_of(arg, Some(*param_ty), ctx);
696        if let Some(a) = arg_ty
697            && !compatible(a, *param_ty, tys)
698        {
699            ctx.errors.push(CompileError::new(
700                "bynk.types.argument_mismatch",
701                arg.span,
702                format!(
703                    "argument has type `{}`, but `{}` expects `{}`",
704                    a.display(tys),
705                    name.name,
706                    param_ty.display(tys)
707                ),
708            ));
709        }
710    }
711    Some(ret)
712}
713
714/// v0.20a: instantiate and check a call to a generic function. Two-pass
715/// argument-directed inference: pass 1 types every **non-lambda** argument
716/// left-to-right against the (possibly still Var-bearing) expected — safe
717/// because every expected-driven feature in `type_of` matches concrete
718/// variants, so a Var falls through benignly (pinned by a unit test) — and
719/// unifies; pass 2 types **lambda** arguments against the now-substituted
720/// expecteds (a lambda whose expected params are still Var-bearing is
721/// uninferable) and unifies the result, capturing return-position variables.
722/// Conflicts demand exact equality (`bynk.generics.type_arg_mismatch`); the
723/// explicit `name[T](…)` form builds the substitution directly.
724fn check_generic_call(
725    name: &Ident,
726    fn_decl: &FnDecl,
727    type_args: &[TypeRef],
728    args: &[Expr],
729    ctx: &mut Ctx,
730) -> Option<TyId> {
731    let tys = ctx.tys;
732    let vars: HashSet<String> = fn_decl
733        .type_params
734        .iter()
735        .map(|tp| tp.name.name.clone())
736        .collect();
737    if fn_decl.params.len() != args.len() {
738        // Same backstop as the non-generic path: the resolver only covers
739        // `fn`/method bodies, so the checker must report for handler bodies.
740        ctx.errors.push(
741            CompileError::new(
742                "bynk.resolve.arity_mismatch",
743                name.span,
744                format!(
745                    "function `{}` expects {} argument(s), but {} were given",
746                    name.name,
747                    fn_decl.params.len(),
748                    args.len()
749                ),
750            )
751            .with_label(fn_decl.name.ident().span, "function declared here"),
752        );
753        for a in args {
754            let _ = type_of(a, None, ctx);
755        }
756        return None;
757    }
758    let var_params: Vec<Option<TyId>> = fn_decl
759        .params
760        .iter()
761        .map(|p| resolve_type_ref_in(&p.type_ref, &ctx.input.types, &vars, tys))
762        .collect();
763    let ret_pattern = resolve_type_ref_in(&fn_decl.return_type, &ctx.input.types, &vars, tys)?;
764
765    let mut subst: HashMap<String, TyId> = HashMap::new();
766    if !type_args.is_empty() {
767        if type_args.len() != fn_decl.type_params.len() {
768            ctx.errors.push(CompileError::new(
769                "bynk.generics.type_arg_mismatch",
770                name.span,
771                format!(
772                    "`{}` takes {} type argument(s), but {} were given",
773                    name.name,
774                    fn_decl.type_params.len(),
775                    type_args.len()
776                ),
777            ));
778            return None;
779        }
780        for (tp, ta) in fn_decl.type_params.iter().zip(type_args) {
781            // Resolve explicit type args with the *enclosing* fn's type
782            // params in scope, so `identity[A](x)` inside a generic body
783            // works. #712: report (not silently drop) an unknown type arg —
784            // the resolver never validates `Call::type_args` (it destructures
785            // `Call { name, args, .. }`), so this is the only guard.
786            let ty = resolve_expr_type_ref(ta, ctx)?;
787            subst.insert(tp.name.name.clone(), ty);
788        }
789    }
790
791    let mut arg_tys: Vec<Option<TyId>> = vec![None; args.len()];
792    // Pass 1 — non-lambda arguments.
793    for (i, arg) in args.iter().enumerate() {
794        if matches!(arg.kind, ExprKind::Lambda(_)) {
795            continue;
796        }
797        let expected = var_params[i].map(|p| substitute(p, &subst, tys));
798        let ty = type_of(arg, expected, ctx);
799        if let (Some(pattern), Some(actual)) = (var_params[i], ty)
800            && !unify(pattern, actual, &mut subst, tys)
801        {
802            ctx.errors.push(CompileError::new(
803                "bynk.generics.type_arg_mismatch",
804                arg.span,
805                format!(
806                    "argument {} infers a type for `{}`'s type parameter that conflicts with an earlier argument — annotate with `{}[T](…)`",
807                    i + 1,
808                    name.name,
809                    name.name
810                ),
811            ));
812            return None;
813        }
814        arg_tys[i] = ty;
815    }
816    // Pass 2 — lambda arguments, against substituted expecteds.
817    for (i, arg) in args.iter().enumerate() {
818        if !matches!(arg.kind, ExprKind::Lambda(_)) {
819            continue;
820        }
821        let expected = var_params[i].map(|p| substitute(p, &subst, tys));
822        let params_unconstrained = expected.is_some_and(|e| {
823            matches!(&*tys.get(e), Ty::Fn { params, .. }
824                if params.iter().any(|p| contains_var(*p, tys)))
825        });
826        let fully_annotated = matches!(
827            &arg.kind,
828            ExprKind::Lambda(l) if l.params.iter().all(|p| p.type_ref.is_some())
829        );
830        if params_unconstrained && !fully_annotated {
831            ctx.errors.push(
832                CompileError::new(
833                    "bynk.generics.uninferable_type_arg",
834                    arg.span,
835                    format!(
836                        "the lambda's parameter types depend on `{}`'s type parameters, which the other arguments do not determine",
837                        name.name
838                    ),
839                )
840                .with_note("annotate the lambda's parameters, or give explicit type arguments: `name[T](…)`"),
841            );
842            return None;
843        }
844        // A fully-annotated lambda grounds the variables itself: type it
845        // bottom-up and let unify capture them.
846        let ty = if params_unconstrained {
847            type_of(arg, None, ctx)
848        } else {
849            type_of(arg, expected, ctx)
850        };
851        if let (Some(pattern), Some(actual)) = (var_params[i], ty)
852            && !unify(pattern, actual, &mut subst, tys)
853        {
854            ctx.errors.push(CompileError::new(
855                "bynk.generics.type_arg_mismatch",
856                arg.span,
857                format!(
858                    "the lambda's type conflicts with `{}`'s inferred type arguments",
859                    name.name
860                ),
861            ));
862            return None;
863        }
864        arg_tys[i] = ty;
865    }
866    // Every type parameter must now be determined.
867    for tp in &fn_decl.type_params {
868        if !subst.contains_key(&tp.name.name) {
869            ctx.errors.push(
870                CompileError::new(
871                    "bynk.generics.uninferable_type_arg",
872                    name.span,
873                    format!(
874                        "type parameter `{}` of `{}` is neither inferable from the arguments nor given explicitly",
875                        tp.name.name, name.name
876                    ),
877                )
878                .with_label(tp.span, "declared here")
879                .with_note("give explicit type arguments: `name[T](…)`"),
880            );
881            return None;
882        }
883    }
884    // Final compatibility over the fully-ground parameter types.
885    let mut ok = true;
886    for (i, (pattern, arg)) in var_params.iter().zip(args).enumerate() {
887        record_param_hint(ctx.hints, &fn_decl.params[i].name.name, arg);
888        let (Some(pattern), Some(arg_ty)) = (pattern, arg_tys[i].as_ref()) else {
889            continue;
890        };
891        let ground = substitute(*pattern, &subst, tys);
892        if !compatible(*arg_ty, ground, tys) {
893            ctx.errors.push(CompileError::new(
894                "bynk.types.argument_mismatch",
895                arg.span,
896                format!(
897                    "argument {} to `{}` has type `{}`, but `{}` is expected",
898                    i + 1,
899                    name.name,
900                    arg_ty.display(tys),
901                    ground.display(tys)
902                ),
903            ));
904            ok = false;
905        }
906    }
907    if !ok {
908        return None;
909    }
910    // #1688: a type parameter the callee compares (`==` on it, directly or
911    // through another generic call) must be instantiated with an
912    // equality-supporting type.
913    let compared = match ctx.input.fns.get(&name.name) {
914        Some(decl) if std::ptr::eq(decl.as_ref(), fn_decl) => {
915            super::equality::compared_type_params(decl, ctx)
916        }
917        _ => Default::default(),
918    };
919    let params: Vec<(String, Option<Span>)> = fn_decl
920        .type_params
921        .iter()
922        .map(|tp| (tp.name.name.clone(), Some(tp.span)))
923        .collect();
924    if !super::equality::check_compared_args(&name.name, name.span, &params, &compared, &subst, ctx)
925    {
926        return None;
927    }
928    // v0.39 (ADR 0072): when the user omitted the type arguments, show the
929    // inferred ones as a `Type`-kind hint after the function name —
930    // `identity` ⟨`[Int]`⟩ `(5)`. Declaration order; skipped if any var stayed
931    // unresolved (defensive — the arg loop above already grounds them).
932    if type_args.is_empty() && !fn_decl.type_params.is_empty() {
933        let rendered: Option<Vec<String>> = fn_decl
934            .type_params
935            .iter()
936            .map(|tp| subst.get(&tp.name.name).map(|t| t.display(tys)))
937            .collect();
938        if let Some(parts) = rendered {
939            ctx.hints
940                .record(name.span, format!("[{}]", parts.join(", ")));
941        }
942    }
943    let ret = substitute(ret_pattern, &subst, tys);
944    // v0.20b: the return is ground *up to the caller's rigid type
945    // parameters* — a generic fn calling another generic fn (bynk.list's
946    // `map` calling `reverse`) legitimately instantiates the callee at its
947    // own rigid vars, which flow through `compatible` by name-equality.
948    Some(ret)
949}
950
951fn check_call_against_fn(
952    name: &Ident,
953    fn_decl: &FnDecl,
954    type_args: &[TypeRef],
955    args: &[Expr],
956    ctx: &mut Ctx,
957) -> Option<TyId> {
958    let tys = ctx.tys;
959    // v0.20a: generic functions take the instantiation path; the
960    // non-generic path below runs byte-identically to v0.19 (the additive
961    // guard). Explicit type args on a non-generic fn are rejected.
962    if !fn_decl.type_params.is_empty() {
963        return check_generic_call(name, fn_decl, type_args, args, ctx);
964    }
965    if !type_args.is_empty() {
966        ctx.errors.push(CompileError::new(
967            "bynk.generics.type_arg_mismatch",
968            name.span,
969            format!(
970                "`{}` is not a generic function — it takes no type arguments",
971                name.name
972            ),
973        ));
974        for a in args {
975            let _ = type_of(a, None, ctx);
976        }
977        return None;
978    }
979    if fn_decl.params.len() != args.len() {
980        // The resolver reports this in `fn`/method bodies; handler/service/
981        // agent/test bodies only reach the checker, so report here too (a
982        // resolve error stops the pipeline first, so this cannot double up).
983        ctx.errors.push(
984            CompileError::new(
985                "bynk.resolve.arity_mismatch",
986                name.span,
987                format!(
988                    "function `{}` expects {} argument(s), but {} were given",
989                    name.name,
990                    fn_decl.params.len(),
991                    args.len()
992                ),
993            )
994            .with_label(fn_decl.name.ident().span, "function declared here"),
995        );
996        for a in args {
997            let _ = type_of(a, None, ctx);
998        }
999        return None;
1000    }
1001    let resolved_params: Vec<(Option<TyId>, &Param)> = fn_decl
1002        .params
1003        .iter()
1004        .map(|p| (resolve_type_ref(&p.type_ref, &ctx.input.types, tys), p))
1005        .collect();
1006    let mut ok = true;
1007    for (i, ((param_ty, param), arg)) in resolved_params.iter().zip(args.iter()).enumerate() {
1008        record_param_hint(ctx.hints, &param.name.name, arg);
1009        let arg_ty = type_of(arg, *param_ty, ctx);
1010        let (Some(arg_ty), Some(param_ty)) = (arg_ty, *param_ty) else {
1011            ok = false;
1012            continue;
1013        };
1014        if !compatible(arg_ty, param_ty, tys) {
1015            ctx.errors.push(
1016                CompileError::new(
1017                    "bynk.types.argument_mismatch",
1018                    arg.span,
1019                    format!(
1020                        "argument {} to `{}` has type `{}`, but parameter `{}` expects `{}`",
1021                        i + 1,
1022                        name.name,
1023                        arg_ty.display(tys),
1024                        param.name.name,
1025                        param_ty.display(tys)
1026                    ),
1027                )
1028                .with_label(param.span, "parameter declared here"),
1029            );
1030            ok = false;
1031        }
1032    }
1033    if !ok {
1034        return None;
1035    }
1036    resolve_type_ref(&fn_decl.return_type, &ctx.input.types, tys)
1037}
1038
1039/// Type-check a kernel-method argument against its expected type, with the
1040/// expected type propagated in (so lambdas and literals type contextually).
1041pub(crate) fn check_arg(arg: &Expr, expected: TyId, what: &str, ctx: &mut Ctx) {
1042    let tys = ctx.tys;
1043    let Some(actual) = type_of(arg, Some(expected), ctx) else {
1044        return;
1045    };
1046    if !compatible(actual, expected, tys) {
1047        ctx.errors.push(CompileError::new(
1048            "bynk.types.type_mismatch",
1049            arg.span,
1050            format!(
1051                "{what} has type `{}`, but `{}` is required",
1052                actual.display(tys),
1053                expected.display(tys)
1054            ),
1055        ));
1056    }
1057}
1058
1059/// Record a capability reference's binding edge, qualifying flattened bare
1060/// names (`consumes U { Cap }`) to their providing unit (v0.25). A bare
1061/// non-flattened name is the consuming unit's own declaration — qualified
1062/// at assembly.
1063fn record_capability_ref(span: Span, name: &str, ctx: &mut Ctx) {
1064    if let Some(unit) = ctx.input.cross_context.flattened_caps.get(name) {
1065        ctx.refs
1066            .record_in_unit(span, SymbolKind::Capability, name, unit);
1067    } else {
1068        ctx.refs.record(span, SymbolKind::Capability, name);
1069    }
1070}
1071
1072#[allow(clippy::too_many_arguments)]
1073pub(crate) fn check_static_call(
1074    type_name: &Ident,
1075    method: &Ident,
1076    // #926: explicit type arguments for a generic capability operation
1077    // (`Cap.op[T](…)`). Consumed only by the capability-dispatch branch below
1078    // — the user-declared-static branch never receives a non-empty slice,
1079    // since `check_method_call`'s gate only lets type args through for a
1080    // capability (or the `Json` codec, handled separately) receiver.
1081    type_args: &[TypeRef],
1082    args: &[Expr],
1083    span: Span,
1084    // #593: threaded to `check_variant_construction` so a qualified generic
1085    // variant (`Opt.Nil`) can ground its arguments from the binding's type.
1086    expected: Option<TyId>,
1087    // P6.0 (#1139): the outer expression's identity — the `ConstructorCall`/
1088    // `MethodCall` node this dispatch is checking on behalf of (this
1089    // function is also reached from inside `check_method_call`, not only
1090    // from `type_of` directly).
1091    expr_id: ExprId,
1092    ctx: &mut Ctx,
1093) -> Option<TyId> {
1094    let tys = ctx.tys;
1095    // Capability dispatch (v0.5): if `type_name` names a capability declared
1096    // in the context, dispatch via the capability table. If the capability is
1097    // declared but not in `given`, error specifically.
1098    if ctx.caps.declared_capabilities.contains_key(&type_name.name)
1099        && !ctx.caps.capabilities.contains_key(&type_name.name)
1100    {
1101        record_capability_ref(type_name.span, &type_name.name, ctx);
1102        ctx.callees.insert(
1103            expr_id,
1104            Callee::Capability {
1105                cap: type_name.name.clone(),
1106                op: method.name.clone(),
1107            },
1108        );
1109        let mut err = CompileError::new(
1110            "bynk.given.undeclared_capability",
1111            type_name.span,
1112            format!(
1113                "capability `{}` is used but not listed in the handler's `given` clause",
1114                type_name.name
1115            ),
1116        )
1117        .with_note(format!(
1118            "add `{}` to the handler's `given` clause so the dependency surface is visible at the declaration site",
1119            type_name.name
1120        ));
1121        // v0.26 (ADR 0054): the one-click counterpart of the note.
1122        if let Some((span, insert)) = given_insertion_edit(
1123            &ctx.caps.given_entries,
1124            ctx.caps.given_anchor,
1125            &type_name.name,
1126        ) {
1127            err = err.with_suggestion(
1128                format!("add `{}` to the `given` clause", type_name.name),
1129                vec![(span, insert)],
1130                Applicability::MachineApplicable,
1131            );
1132        }
1133        ctx.errors.push(err);
1134        // v0.99: an uncovered direct capability call — record it so the ghost
1135        // `given` inlay hint can offer the same clause at the declaration site
1136        // (DECISION D/E). `span` is the call site (the inner `given_insertion_edit`
1137        // binding shadowed it only within the `if let` above).
1138        record_requirement(
1139            ctx,
1140            &type_name.name,
1141            span,
1142            RequirementSource::DirectCall {
1143                op: method.name.clone(),
1144            },
1145            false,
1146        );
1147        for a in args {
1148            let _ = type_of(a, None, ctx);
1149        }
1150        return None;
1151    }
1152    if let Some(cap) = ctx.caps.capabilities.get(&type_name.name).cloned() {
1153        record_capability_ref(type_name.span, &type_name.name, ctx);
1154        ctx.callees.insert(
1155            expr_id,
1156            Callee::Capability {
1157                cap: type_name.name.clone(),
1158                op: method.name.clone(),
1159            },
1160        );
1161        if !ctx.effectful {
1162            ctx.errors.push(
1163                CompileError::new(
1164                    "bynk.effect.capability_in_pure_context",
1165                    span,
1166                    format!(
1167                        "capability `{}` can only be called inside an effectful body (one returning `Effect[T]`)",
1168                        type_name.name
1169                    ),
1170                ),
1171            );
1172        }
1173        ctx.caps.given_used.insert(type_name.name.clone());
1174        // v0.99: a covered direct capability call — the call site *is* the
1175        // reason (DECISION C). Recorded so hover can explain what a declared
1176        // `given Cap` is for; no inlay hint (already covered).
1177        record_requirement(
1178            ctx,
1179            &type_name.name,
1180            span,
1181            RequirementSource::DirectCall {
1182                op: method.name.clone(),
1183            },
1184            true,
1185        );
1186        let Some(op) = cap.ops.iter().find(|o| o.name == method.name) else {
1187            ctx.errors.push(CompileError::new(
1188                "bynk.capability.unknown_operation",
1189                method.span,
1190                format!(
1191                    "capability `{}` has no operation named `{}`",
1192                    type_name.name, method.name
1193                ),
1194            ));
1195            for a in args {
1196                let _ = type_of(a, None, ctx);
1197            }
1198            return None;
1199        };
1200        // v0.36 (ADR 0069, slice 2): the op is an index symbol keyed by the
1201        // compound `"Cap.op"` name; this local call is a reference.
1202        ctx.refs.record(
1203            method.span,
1204            SymbolKind::CapabilityOp,
1205            &format!("{}.{}", type_name.name, method.name),
1206        );
1207        if op.params.len() != args.len() {
1208            ctx.errors.push(CompileError::new(
1209                "bynk.capability.op_arity",
1210                span,
1211                format!(
1212                    "capability operation `{}.{}` expects {} argument(s), but {} were given",
1213                    type_name.name,
1214                    method.name,
1215                    op.params.len(),
1216                    args.len()
1217                ),
1218            ));
1219            for a in args {
1220                let _ = type_of(a, None, ctx);
1221            }
1222            return None;
1223        }
1224        let op_clone = op.clone();
1225        // #926: resolve the op's own type parameter(s) from an explicit
1226        // call-site type argument — explicit only, never inferred (mirrors
1227        // `Json.decode[T]`, not `#594`'s inference). `check_generic_call`
1228        // (this file) is the model; only its arity-check + substitution-build
1229        // half applies here, since a capability op has no argument-driven
1230        // inference pass.
1231        let mut subst: HashMap<String, TyId> = HashMap::new();
1232        if !op_clone.type_params.is_empty() || !type_args.is_empty() {
1233            if type_args.is_empty() {
1234                ctx.errors.push(
1235                    CompileError::new(
1236                        "bynk.generics.uninferable_type_arg",
1237                        span,
1238                        format!(
1239                            "capability operation `{}.{}` takes a type parameter, but none of its arguments determine it",
1240                            type_name.name, method.name
1241                        ),
1242                    )
1243                    .with_note(format!(
1244                        "give it explicitly: `{}.{}[T](…)`",
1245                        type_name.name, method.name
1246                    )),
1247                );
1248                for a in args {
1249                    let _ = type_of(a, None, ctx);
1250                }
1251                return None;
1252            }
1253            if type_args.len() != op_clone.type_params.len() {
1254                ctx.errors.push(CompileError::new(
1255                    "bynk.generics.type_arg_mismatch",
1256                    span,
1257                    format!(
1258                        "capability operation `{}.{}` takes {} type argument(s), but {} were given",
1259                        type_name.name,
1260                        method.name,
1261                        op_clone.type_params.len(),
1262                        type_args.len()
1263                    ),
1264                ));
1265                for a in args {
1266                    let _ = type_of(a, None, ctx);
1267                }
1268                return None;
1269            }
1270            for (tp, ta) in op_clone.type_params.iter().zip(type_args) {
1271                let ty = resolve_expr_type_ref(ta, ctx)?;
1272                // Events track, slice 0 (spine #936): owner-only emission —
1273                // `Events.emit[E]` may only name an event `E` declared in
1274                // *this* context, even though a `consumes`-visible foreign
1275                // event resolves here just as validly for every other
1276                // purpose. `is_local_type` already distinguishes "declared
1277                // here" from "visible via uses/consumes" (the same table
1278                // ADR 0256's locale-types-split gap analysis used), so this
1279                // is a check over existing data, not new provenance
1280                // plumbing — the primary boundary guarantee the threat
1281                // model (events.md §6) names.
1282                //
1283                // First-party-gated like every other Events-special-cased
1284                // site (mirrors #934's Idempotency precedent): a bare
1285                // `type_name.name == "Events"` string match would also fire
1286                // for a third-party capability that happens to declare its
1287                // own `Events` with an `emit` method — only bynk's own
1288                // capability (declared here, in `bynk` itself, or flattened
1289                // in via `consumes bynk { Events }`) gets this check.
1290                let is_first_party_events = type_name.name == "Events"
1291                    && method.name == "emit"
1292                    && (ctx.input.commons.name.joined() == crate::firstparty::BYNK_UNIT
1293                        || ctx
1294                            .input
1295                            .cross_context
1296                            .flattened_caps
1297                            .get("Events")
1298                            .map(String::as_str)
1299                            == Some(crate::firstparty::BYNK_UNIT));
1300                if is_first_party_events && let Ty::Named { name: ename, .. } = &*tys.get(ty) {
1301                    if ctx.input.is_local_event(ename) {
1302                        // Locally-declared event — the owner. Fine.
1303                    } else if ctx.input.is_local_type(ename) {
1304                        // Events track, slice 0: `UnitTable`/`ResolvedCommons`
1305                        // record which local names are specifically events
1306                        // (as opposed to any other locally-declared type) —
1307                        // `Events.emit[SomeLocalRecord]` compiled clean before
1308                        // this check existed, silently buffering an emission
1309                        // no `from Events(...)` subscriber could ever match
1310                        // (`discover_event_subscribers` finds no owner for a
1311                        // non-event name and just drops it).
1312                        ctx.errors.push(
1313                            CompileError::new(
1314                                "bynk.event.emit_not_an_event",
1315                                ta.span(),
1316                                format!(
1317                                    "`{ename}` is not a declared `event` — `Events.emit` may only name an event type"
1318                                ),
1319                            )
1320                            .with_note(
1321                                "declare it with `event Name = { ... }`, or check that the type argument names the event you meant",
1322                            ),
1323                        );
1324                    } else {
1325                        ctx.errors.push(
1326                            CompileError::new(
1327                                "bynk.event.emit_outside_owner",
1328                                ta.span(),
1329                                format!(
1330                                    "`{ename}` is not declared in this context — only the context that declares an event may emit it"
1331                                ),
1332                            )
1333                            .with_note(
1334                                "a foreign event is visible via `consumes` for subscription (`from Events(...)`), but only its owning context may `Events.emit` it",
1335                            ),
1336                        );
1337                    }
1338                }
1339                subst.insert(tp.clone(), ty);
1340            }
1341        }
1342        for (i, (param_ty, arg)) in op_clone.params.iter().zip(args.iter()).enumerate() {
1343            let param_ty = substitute(*param_ty, &subst, tys);
1344            let arg_ty = type_of(arg, Some(param_ty), ctx);
1345            if let Some(actual) = arg_ty
1346                && !compatible(actual, param_ty, tys)
1347            {
1348                ctx.errors.push(CompileError::new(
1349                    "bynk.types.argument_mismatch",
1350                    arg.span,
1351                    format!(
1352                        "argument {} to capability `{}.{}` has type `{}`, but parameter expects `{}`",
1353                        i + 1,
1354                        type_name.name,
1355                        method.name,
1356                        actual.display(tys),
1357                        param_ty.display(tys)
1358                    ),
1359                ));
1360            }
1361        }
1362        return Some(substitute(op_clone.return_ty, &subst, tys));
1363    }
1364    let decl = ctx.input.types.get(&type_name.name)?;
1365    ctx.refs
1366        .record(type_name.span, SymbolKind::Type, &type_name.name);
1367
1368    // 1) User-declared static method. Borrow the method table and decl
1369    // straight out of the (immutable) input rather than cloning the whole
1370    // `MethodTable`/`FnDecl` on every static-shaped call.
1371    if let Some(method_decl) = ctx
1372        .input
1373        .methods
1374        .get(&type_name.name)
1375        .and_then(|table| table.statics.get(&method.name))
1376    {
1377        ctx.callees
1378            .insert(expr_id, Callee::Static(Arc::clone(method_decl)));
1379        return check_method_args(method_decl, args, ctx, type_name, method);
1380    }
1381
1382    // 2) Built-in `of` constructor on refined or opaque types.
1383    if method.name == OF
1384        && let Some(base) = type_decl_base(decl)
1385    {
1386        ctx.callees
1387            .insert(expr_id, Callee::Refine(Arc::clone(decl)));
1388        if args.len() != 1 {
1389            ctx.errors.push(CompileError::new(
1390                "bynk.types.constructor_arity",
1391                span,
1392                format!(
1393                    "constructor `{}.of` expects 1 argument, but {} were given",
1394                    type_name.name,
1395                    args.len()
1396                ),
1397            ));
1398            return None;
1399        }
1400        let arg = &args[0];
1401        let expected = tys.intern(Ty::Base(base));
1402        let arg_ty = type_of(arg, Some(expected), ctx)?;
1403        if !compatible(arg_ty, expected, tys) {
1404            ctx.errors.push(CompileError::new(
1405                "bynk.types.constructor_base_mismatch",
1406                arg.span,
1407                format!(
1408                    "constructor `{}.of` expects a `{}` argument, but got `{}`",
1409                    type_name.name,
1410                    base.name(),
1411                    arg_ty.display(tys)
1412                ),
1413            ));
1414            return None;
1415        }
1416        // `.of` is always the runtime constructor: it returns
1417        // `Result[T, ValidationError]`. Compile-time literal admission (v0.9.4)
1418        // happens instead wherever an expected refined type is known — see
1419        // `admit_refined_literal`, used by `type_of` — so `.of`'s type never
1420        // depends on the form of its argument.
1421        return Some(tys.intern(Ty::Result(
1422            named_ty(decl, tys),
1423            tys.intern(Ty::ValidationError),
1424        )));
1425    }
1426
1427    // 2b) Built-in `unsafe` constructor on opaque types — only available
1428    // inside the defining commons.
1429    if method.name == UNSAFE
1430        && let TypeBody::Opaque { base, .. } = &decl.body
1431    {
1432        ctx.callees
1433            .insert(expr_id, Callee::Unsafe(Arc::clone(decl)));
1434        if !ctx.input.is_local_type(&decl.name.name) {
1435            ctx.errors.push(
1436                CompileError::new(
1437                    "bynk.types.opaque_unsafe_outside",
1438                    method.span,
1439                    format!(
1440                        "`{}.unsafe(...)` is only available within the commons that defines the opaque type `{}`",
1441                        type_name.name, type_name.name
1442                    ),
1443                )
1444                .with_note(
1445                    "outside the defining commons, opaque values are constructed via `T.of(value)`",
1446                ),
1447            );
1448            return None;
1449        }
1450        if args.len() != 1 {
1451            ctx.errors.push(CompileError::new(
1452                "bynk.types.constructor_arity",
1453                span,
1454                format!(
1455                    "`{}.unsafe` expects 1 argument, but {} were given",
1456                    type_name.name,
1457                    args.len()
1458                ),
1459            ));
1460            return None;
1461        }
1462        let arg = &args[0];
1463        let expected = tys.intern(Ty::Base(*base));
1464        let arg_ty = type_of(arg, Some(expected), ctx)?;
1465        if !compatible(arg_ty, expected, tys) {
1466            ctx.errors.push(CompileError::new(
1467                "bynk.types.constructor_base_mismatch",
1468                arg.span,
1469                format!(
1470                    "`{}.unsafe` expects a `{}` argument, but got `{}`",
1471                    type_name.name,
1472                    base.name(),
1473                    arg_ty.display(tys)
1474                ),
1475            ));
1476            return None;
1477        }
1478        return Some(named_ty(decl, tys));
1479    }
1480
1481    // 3) Qualified variant construction `TypeName.Variant(args)`.
1482    if let TypeBody::Sum(_) = &decl.body {
1483        ctx.callees.insert(
1484            expr_id,
1485            Callee::Ctor {
1486                sum: Arc::clone(decl),
1487                tag: method.name.clone(),
1488            },
1489        );
1490        return check_variant_construction(decl, &method.name, args, span, expected, ctx);
1491    }
1492
1493    ctx.errors.push(
1494        CompileError::new(
1495            "bynk.types.unknown_static_member",
1496            method.span,
1497            format!(
1498                "type `{}` has no static method or variant named `{}`",
1499                type_name.name, method.name
1500            ),
1501        )
1502        // Finding #46: `decl` comes from the combined cross-file symbol
1503        // table — see resolver.rs:1029 for the full rationale.
1504        .with_note("type declared here"),
1505    );
1506    None
1507}
1508
1509fn check_method_args(
1510    method_decl: &FnDecl,
1511    args: &[Expr],
1512    ctx: &mut Ctx,
1513    type_name: &Ident,
1514    method: &Ident,
1515) -> Option<TyId> {
1516    let tys = ctx.tys;
1517    if method_decl.params.len() != args.len() {
1518        ctx.errors.push(
1519            CompileError::new(
1520                "bynk.types.method_arity",
1521                method.span,
1522                format!(
1523                    "static method `{}.{}` expects {} argument(s), but {} were given",
1524                    type_name.name,
1525                    method.name,
1526                    method_decl.params.len(),
1527                    args.len()
1528                ),
1529            )
1530            .with_label(method_decl.name.ident().span, "method declared here"),
1531        );
1532        for a in args {
1533            let _ = type_of(a, None, ctx);
1534        }
1535        return None;
1536    }
1537    let mut ok = true;
1538    for (i, (param, arg)) in method_decl.params.iter().zip(args.iter()).enumerate() {
1539        record_param_hint(ctx.hints, &param.name.name, arg);
1540        let expected = resolve_type_ref(&param.type_ref, &ctx.input.types, tys);
1541        let actual = type_of(arg, expected, ctx);
1542        let (Some(actual), Some(expected)) = (actual, expected) else {
1543            ok = false;
1544            continue;
1545        };
1546        if !compatible(actual, expected, tys) {
1547            ctx.errors.push(CompileError::new(
1548                "bynk.types.argument_mismatch",
1549                arg.span,
1550                format!(
1551                    "argument {} to `{}.{}` has type `{}`, but parameter `{}` expects `{}`",
1552                    i + 1,
1553                    type_name.name,
1554                    method.name,
1555                    actual.display(tys),
1556                    param.name.name,
1557                    expected.display(tys)
1558                ),
1559            ));
1560            ok = false;
1561        }
1562    }
1563    if !ok {
1564        return None;
1565    }
1566    resolve_type_ref(&method_decl.return_type, &ctx.input.types, tys)
1567}
1568
1569/// v0.82 (ADR 0110): resolve a storage-map operation `<map>.<op>(args)` on a
1570/// `store Map[K, V]` field. The ops are effect-typed (storage I/O, awaited with
1571/// `<-`): `put`/`update`/`upsert`/`remove` → `Effect[()]`, `get` →
1572/// `Effect[Option[V]]`, `contains` → `Effect[Bool]`, `size` → `Effect[Int]`.
1573/// `update` on an absent key is a runtime fault; `upsert` is the default-if-absent
1574/// form. Dispatched by receiver provenance, so it never shadows the immutable
1575/// value `Map`'s pure methods.
1576pub(crate) fn check_store_map_op(
1577    method: &Ident,
1578    args: &[Expr],
1579    k: TyId,
1580    v: TyId,
1581    span: Span,
1582    ctx: &mut Ctx,
1583) -> Option<TyId> {
1584    let tys = ctx.tys;
1585    let vfn = || Ty::Fn {
1586        params: vec![v],
1587        ret: v,
1588    };
1589    // v0.105 (slice 3b-ii): a held `Map[K, Connection]` stores connection ids and
1590    // resolves them by identity; `update`/`upsert` transform the value through a
1591    // `(V) -> V` function, which has no meaning for a held resource — you cannot
1592    // derive a new connection from an old one. Reject them (the other entry ops —
1593    // `put`/`get`/`remove`/`contains`/`size` — are admitted) so the program is a
1594    // clean compile error rather than a silent miscompile.
1595    if v.is_held(tys) && matches!(method.name.as_str(), "update" | "upsert") {
1596        ctx.errors.push(
1597            CompileError::new(
1598                "bynk.held.unsupported_map_op",
1599                method.span,
1600                format!(
1601                    "a held `Map[K, Connection]` has no `{}` operation — a held resource cannot be transformed by a `(Connection) -> Connection` function",
1602                    method.name
1603                ),
1604            )
1605            .with_note(
1606                "held connections are stored and resolved by identity; use `put`/`get`/`remove`",
1607            ),
1608        );
1609        for a in args {
1610            type_of(a, None, ctx);
1611        }
1612        return None;
1613    }
1614    let (expected, result): (Vec<TyId>, TyId) = match method.name.as_str() {
1615        "put" => (vec![k, v], tys.intern(Ty::Unit)),
1616        "get" => (vec![k], tys.intern(Ty::Option(v))),
1617        "remove" => (vec![k], tys.intern(Ty::Unit)),
1618        "contains" => (vec![k], tys.intern(Ty::Base(BaseType::Bool))),
1619        "size" => (vec![], tys.intern(Ty::Base(BaseType::Int))),
1620        "update" => (vec![k, tys.intern(vfn())], tys.intern(Ty::Unit)),
1621        "upsert" => (vec![k, v, tys.intern(vfn())], tys.intern(Ty::Unit)),
1622        other => {
1623            ctx.errors.push(
1624                CompileError::new(
1625                    "bynk.store.unknown_op",
1626                    method.span,
1627                    format!(
1628                        "a `Map` store field has no operation `{other}` — expected `put`, `get`, \
1629                         `update`, `upsert`, `remove`, `contains`, or `size`"
1630                    ),
1631                )
1632                .with_note("storage-map ops are entry-level and effectful (await with `<-`)"),
1633            );
1634            for a in args {
1635                type_of(a, None, ctx);
1636            }
1637            return None;
1638        }
1639    };
1640    let effect = Ty::Effect(result);
1641    if args.len() != expected.len() {
1642        ctx.errors.push(CompileError::new(
1643            "bynk.types.call_arity",
1644            span,
1645            format!(
1646                "`Map.{}` takes {} argument(s), found {}",
1647                method.name,
1648                expected.len(),
1649                args.len()
1650            ),
1651        ));
1652        for a in args {
1653            type_of(a, None, ctx);
1654        }
1655        return Some(tys.intern(effect));
1656    }
1657    for (a, exp) in args.iter().zip(expected.iter()) {
1658        if let Some(at) = type_of(a, Some(*exp), ctx)
1659            && !compatible(at, *exp, tys)
1660        {
1661            ctx.errors.push(CompileError::new(
1662                "bynk.types.argument_mismatch",
1663                a.span,
1664                format!(
1665                    "expected `{}`, found `{}`",
1666                    exp.display(tys),
1667                    at.display(tys)
1668                ),
1669            ));
1670        }
1671    }
1672    Some(tys.intern(effect))
1673}
1674
1675/// v0.99: record a capability requirement at `site` into the ledger, and — when
1676/// the enclosing handler's `given` does not cover it — push the diagnostic. The
1677/// single producer behind both the bare diagnostic and the editor surfaces
1678/// (DECISION D): every requirement is *recorded*, covered or not; only an
1679/// uncovered one errors. The `code`/`message` are the consuming feature's, so a
1680/// store op keeps its precise diagnostic; the ledger's *reason* renders from
1681/// `source` alone (DECISION C), never from `code`.
1682fn require_capability(
1683    site: Span,
1684    capability: &str,
1685    source: RequirementSource,
1686    ctx: &mut Ctx,
1687    code: &'static str,
1688    message: &str,
1689) {
1690    let covered = ctx.caps.capabilities.contains_key(capability);
1691    if covered {
1692        ctx.caps.given_used.insert(capability.to_string());
1693    } else {
1694        ctx.errors
1695            .push(CompileError::new(code, site, message).with_note(format!(
1696                "add `{capability}` to the handler's `given` clause"
1697            )));
1698    }
1699    record_requirement(ctx, capability, site, source, covered);
1700}
1701
1702/// Push a [`Requirement`] into the ledger. For an uncovered requirement it also
1703/// computes the materialization edit (the ghost `given` inlay hint's one-click
1704/// apply) from the handler's existing `given` entries and anchor — the same
1705/// `given_insertion_edit` the undeclared-capability quick-fix uses.
1706fn record_requirement(
1707    ctx: &mut Ctx,
1708    capability: &str,
1709    site: Span,
1710    source: RequirementSource,
1711    covered: bool,
1712) {
1713    let materialize = if covered {
1714        None
1715    } else {
1716        given_insertion_edit(&ctx.caps.given_entries, ctx.caps.given_anchor, capability).map(
1717            |(edit_span, edit_text)| Materialize {
1718                anchor: ctx.caps.given_anchor.unwrap_or(ctx.return_ty_span),
1719                edit_span,
1720                edit_text,
1721            },
1722        )
1723    };
1724    ctx.requirements.record(Requirement {
1725        capability: capability.to_string(),
1726        site,
1727        source,
1728        covered,
1729        materialize,
1730    });
1731}
1732
1733/// v0.87 (ADR 0113): resolve a storage-`Cache` operation `<cache>.<op>(args)` on
1734/// a `store Cache[K, V]` field. The op set is the storage `Map`'s
1735/// (`put`/`get`/`update`/`upsert`/`remove`/`contains`/`size`); every op but
1736/// `remove` additionally requires `given Clock` (eviction reads the clock).
1737pub(crate) fn check_store_cache_op(
1738    method: &Ident,
1739    args: &[Expr],
1740    k: TyId,
1741    v: TyId,
1742    span: Span,
1743    ctx: &mut Ctx,
1744) -> Option<TyId> {
1745    let tys = ctx.tys;
1746    let vfn = || Ty::Fn {
1747        params: vec![v],
1748        ret: v,
1749    };
1750    let (expected, result): (Vec<TyId>, TyId) = match method.name.as_str() {
1751        "put" => (vec![k, v], tys.intern(Ty::Unit)),
1752        "get" => (vec![k], tys.intern(Ty::Option(v))),
1753        "remove" => (vec![k], tys.intern(Ty::Unit)),
1754        "contains" => (vec![k], tys.intern(Ty::Base(BaseType::Bool))),
1755        "size" => (vec![], tys.intern(Ty::Base(BaseType::Int))),
1756        "update" => (vec![k, tys.intern(vfn())], tys.intern(Ty::Unit)),
1757        "upsert" => (vec![k, v, tys.intern(vfn())], tys.intern(Ty::Unit)),
1758        other => {
1759            ctx.errors.push(
1760                CompileError::new(
1761                    "bynk.store.unknown_op",
1762                    method.span,
1763                    format!(
1764                        "a `Cache` store field has no operation `{other}` — expected `put`, \
1765                         `get`, `update`, `upsert`, `remove`, `contains`, or `size`"
1766                    ),
1767                )
1768                .with_note("storage-cache ops are entry-level and effectful (await with `<-`)"),
1769            );
1770            for a in args {
1771                type_of(a, None, ctx);
1772            }
1773            return None;
1774        }
1775    };
1776    // D4: every op but `remove` reads the clock for TTL expiry.
1777    if method.name != "remove" {
1778        require_capability(
1779            method.span,
1780            "Clock",
1781            RequirementSource::StoreOp {
1782                kind: StoreKind::Cache,
1783                op: method.name.clone(),
1784            },
1785            ctx,
1786            "bynk.store.cache_needs_clock",
1787            "a `Cache` operation applies TTL expiry, which reads the clock — the handler must declare `given Clock`",
1788        );
1789    }
1790    let effect = Ty::Effect(result);
1791    if args.len() != expected.len() {
1792        ctx.errors.push(CompileError::new(
1793            "bynk.types.call_arity",
1794            span,
1795            format!(
1796                "`Cache.{}` takes {} argument(s), found {}",
1797                method.name,
1798                expected.len(),
1799                args.len()
1800            ),
1801        ));
1802        for a in args {
1803            type_of(a, None, ctx);
1804        }
1805        return Some(tys.intern(effect));
1806    }
1807    for (a, exp) in args.iter().zip(expected.iter()) {
1808        if let Some(at) = type_of(a, Some(*exp), ctx)
1809            && !compatible(at, *exp, tys)
1810        {
1811            ctx.errors.push(CompileError::new(
1812                "bynk.types.argument_mismatch",
1813                a.span,
1814                format!(
1815                    "expected `{}`, found `{}`",
1816                    exp.display(tys),
1817                    at.display(tys)
1818                ),
1819            ));
1820        }
1821    }
1822    Some(tys.intern(effect))
1823}
1824
1825/// v0.95 (ADR 0121): resolve a storage-`Log` operation `<log>.<op>(args)` on a
1826/// `store Log[T]` field. `append(e)` is the effectful, **non-idempotent** write
1827/// (`Effect[()]`) and the one clock-consuming op — it stamps `Clock.now()`, so it
1828/// requires `given Clock`. The time-window roots — `since(Instant)`/
1829/// `before(Instant)` / `between(Instant, Instant)` / `recent(Int)` / `reversed()`
1830/// — and the general query vocabulary lift the log into a lazy `Query[T]` over its
1831/// entry values; these need no clock (window bounds are explicit `Instant`s).
1832pub(crate) fn check_store_log_op(
1833    method: &Ident,
1834    args: &[Expr],
1835    elem: TyId,
1836    span: Span,
1837    ctx: &mut Ctx,
1838) -> Option<TyId> {
1839    let tys = ctx.tys;
1840    let query = || Ty::Query(elem);
1841    let arity = |n: usize, ctx: &mut Ctx| {
1842        if args.len() != n {
1843            ctx.errors.push(CompileError::new(
1844                "bynk.types.call_arity",
1845                span,
1846                format!(
1847                    "`Log.{}` takes {n} argument(s), found {}",
1848                    method.name,
1849                    args.len()
1850                ),
1851            ));
1852            for a in args {
1853                type_of(a, None, ctx);
1854            }
1855            return false;
1856        }
1857        true
1858    };
1859    let window_arg = |a: &Expr, what: &str, ctx: &mut Ctx| {
1860        if let Some(at) = type_of(a, Some(tys.intern(Ty::Base(BaseType::Instant))), ctx)
1861            && !compatible(at, tys.intern(Ty::Base(BaseType::Instant)), tys)
1862        {
1863            ctx.errors.push(CompileError::new(
1864                "bynk.types.argument_mismatch",
1865                a.span,
1866                format!("{what} expects `Instant`, found `{}`", at.display(tys)),
1867            ));
1868        }
1869    };
1870    match method.name.as_str() {
1871        // The one effectful write — non-idempotent; stamps the clock.
1872        "append" => {
1873            require_capability(
1874                method.span,
1875                "Clock",
1876                RequirementSource::StoreOp {
1877                    kind: StoreKind::Log,
1878                    op: method.name.clone(),
1879                },
1880                ctx,
1881                "bynk.store.log_needs_clock",
1882                "`Log.append` stamps the current time, which reads the clock — the handler must declare `given Clock`",
1883            );
1884            if !arity(1, ctx) {
1885                return Some(tys.intern(Ty::Effect(tys.intern(Ty::Unit))));
1886            }
1887            if let Some(at) = type_of(&args[0], Some(elem), ctx)
1888                && !compatible(at, elem, tys)
1889            {
1890                ctx.errors.push(CompileError::new(
1891                    "bynk.types.argument_mismatch",
1892                    args[0].span,
1893                    format!(
1894                        "expected `{}`, found `{}`",
1895                        elem.display(tys),
1896                        at.display(tys)
1897                    ),
1898                ));
1899            }
1900            Some(tys.intern(Ty::Effect(tys.intern(Ty::Unit))))
1901        }
1902        // Time-window query roots → `Query[T]` (lazy; no clock).
1903        "since" | "before" => {
1904            if !arity(1, ctx) {
1905                return Some(tys.intern(query()));
1906            }
1907            window_arg(&args[0], &format!("`Log.{}`", method.name), ctx);
1908            Some(tys.intern(query()))
1909        }
1910        "between" => {
1911            if !arity(2, ctx) {
1912                return Some(tys.intern(query()));
1913            }
1914            window_arg(&args[0], "`Log.between` start", ctx);
1915            window_arg(&args[1], "`Log.between` end", ctx);
1916            Some(tys.intern(query()))
1917        }
1918        "recent" => {
1919            if !arity(1, ctx) {
1920                return Some(tys.intern(query()));
1921            }
1922            check_arg(
1923                &args[0],
1924                tys.intern(Ty::Base(BaseType::Int)),
1925                "the `Log.recent` count",
1926                ctx,
1927            );
1928            Some(tys.intern(query()))
1929        }
1930        "reversed" => {
1931            if !arity(0, ctx) {
1932                return Some(tys.intern(query()));
1933            }
1934            Some(tys.intern(query()))
1935        }
1936        // The general query vocabulary over the entry values.
1937        name if is_query_op(name) => check_query_kernel_method(method, args, elem, span, ctx),
1938        other => {
1939            ctx.errors.push(
1940                CompileError::new(
1941                    "bynk.store.unknown_op",
1942                    method.span,
1943                    format!(
1944                        "a `Log` store field has no operation `{other}` — `append`, the \
1945                         time-window roots (`since`/`before`/`between`/`recent`/`reversed`), \
1946                         and the query builders/terminals"
1947                    ),
1948                )
1949                .with_note(
1950                    "`Log` reads are lazy `Query[T]`; only `append` is effectful and writes",
1951                ),
1952            );
1953            for a in args {
1954                type_of(a, None, ctx);
1955            }
1956            None
1957        }
1958    }
1959}
1960
1961/// v0.83: resolve a storage-set operation `<set>.<op>(args)` on a `store Set[T]`
1962/// field. Effect-typed entry ops: `add(t)`/`remove(t)` → `Effect[()]` (both
1963/// idempotent), `contains(t)` → `Effect[Bool]`, `size()` → `Effect[Int]`. Set
1964/// algebra (`union`/`intersection`/`difference`) is deferred (it needs a value
1965/// `Set` return type). Dispatched by receiver provenance.
1966pub(crate) fn check_store_set_op(
1967    method: &Ident,
1968    args: &[Expr],
1969    t: TyId,
1970    span: Span,
1971    ctx: &mut Ctx,
1972) -> Option<TyId> {
1973    let tys = ctx.tys;
1974    let (expected, result): (Vec<TyId>, TyId) = match method.name.as_str() {
1975        "add" => (vec![t], tys.intern(Ty::Unit)),
1976        "remove" => (vec![t], tys.intern(Ty::Unit)),
1977        "contains" => (vec![t], tys.intern(Ty::Base(BaseType::Bool))),
1978        "size" => (vec![], tys.intern(Ty::Base(BaseType::Int))),
1979        other => {
1980            ctx.errors.push(
1981                CompileError::new(
1982                    "bynk.store.unknown_op",
1983                    method.span,
1984                    format!(
1985                        "a `Set` store field has no operation `{other}` — expected `add`, \
1986                         `remove`, `contains`, or `size`"
1987                    ),
1988                )
1989                .with_note(
1990                    "set algebra (`union`/`intersection`/`difference`) is not in this slice",
1991                ),
1992            );
1993            for a in args {
1994                type_of(a, None, ctx);
1995            }
1996            return None;
1997        }
1998    };
1999    let effect = Ty::Effect(result);
2000    if args.len() != expected.len() {
2001        ctx.errors.push(CompileError::new(
2002            "bynk.types.call_arity",
2003            span,
2004            format!(
2005                "`Set.{}` takes {} argument(s), found {}",
2006                method.name,
2007                expected.len(),
2008                args.len()
2009            ),
2010        ));
2011        for a in args {
2012            type_of(a, None, ctx);
2013        }
2014        return Some(tys.intern(effect));
2015    }
2016    for (a, exp) in args.iter().zip(expected.iter()) {
2017        if let Some(at) = type_of(a, Some(*exp), ctx)
2018            && !compatible(at, *exp, tys)
2019        {
2020            ctx.errors.push(CompileError::new(
2021                "bynk.types.argument_mismatch",
2022                a.span,
2023                format!(
2024                    "expected `{}`, found `{}`",
2025                    exp.display(tys),
2026                    at.display(tys)
2027                ),
2028            ));
2029        }
2030    }
2031    Some(tys.intern(effect))
2032}
2033
2034/// v0.98 (ADR 0125): resolve a storage-`Cell` operation `<cell>.<op>(args)` on a
2035/// `store Cell[T]` field. The single method-shaped cell op is `update(f)` —
2036/// `f: (T) -> T` — a read-modify-write typed `Effect[()]`. Reading a cell is the
2037/// bare-name sugar and writing it is `:=`, so `read`/`write` are not callable
2038/// methods (DECISION B). The combiner is a non-effectful `Ty::Fn`, so an
2039/// effectful body (including a bare read of another cell) fails the existing
2040/// function-type check (DECISION E). Dispatched by receiver provenance.
2041pub(crate) fn check_store_cell_op(
2042    method: &Ident,
2043    args: &[Expr],
2044    t: TyId,
2045    span: Span,
2046    ctx: &mut Ctx,
2047) -> Option<TyId> {
2048    let tys = ctx.tys;
2049    let tfn = || Ty::Fn {
2050        params: vec![t],
2051        ret: t,
2052    };
2053    let (expected, result): (Vec<TyId>, TyId) = match method.name.as_str() {
2054        "update" => (vec![tys.intern(tfn())], tys.intern(Ty::Unit)),
2055        other => {
2056            ctx.errors.push(
2057                CompileError::new(
2058                    "bynk.store.unknown_op",
2059                    method.span,
2060                    format!("a `Cell` store field has no operation `{other}` — expected `update`"),
2061                )
2062                .with_note(
2063                    "a cell is read by its bare name and written with `:=`; `update` is the only \
2064                     method-shaped op",
2065                ),
2066            );
2067            for a in args {
2068                type_of(a, None, ctx);
2069            }
2070            return None;
2071        }
2072    };
2073    let effect = Ty::Effect(result);
2074    if args.len() != expected.len() {
2075        ctx.errors.push(CompileError::new(
2076            "bynk.types.call_arity",
2077            span,
2078            format!(
2079                "`Cell.{}` takes {} argument(s), found {}",
2080                method.name,
2081                expected.len(),
2082                args.len()
2083            ),
2084        ));
2085        for a in args {
2086            type_of(a, None, ctx);
2087        }
2088        return Some(tys.intern(effect));
2089    }
2090    for (a, exp) in args.iter().zip(expected.iter()) {
2091        if let Some(at) = type_of(a, Some(*exp), ctx)
2092            && !compatible(at, *exp, tys)
2093        {
2094            ctx.errors.push(CompileError::new(
2095                "bynk.types.argument_mismatch",
2096                a.span,
2097                format!(
2098                    "expected `{}`, found `{}`",
2099                    exp.display(tys),
2100                    at.display(tys)
2101                ),
2102            ));
2103        }
2104    }
2105    Some(tys.intern(effect))
2106}
2107
2108#[allow(clippy::too_many_arguments)]
2109pub(crate) fn check_method_call(
2110    receiver: &Expr,
2111    method: &Ident,
2112    type_args: &[TypeRef],
2113    args: &[Expr],
2114    span: Span,
2115    expected: Option<TyId>,
2116    // P6.0 (#1139): the outer `MethodCall` expression's own identity.
2117    expr_id: ExprId,
2118    ctx: &mut Ctx,
2119) -> Option<TyId> {
2120    let tys = ctx.tys;
2121    // v0.22b: explicit type arguments apply only to the `Json.decode[T]`
2122    // static — every other method/static takes none (the 0039/0045 rule). A
2123    // user-declared type named `Json` shadows the codec module and takes no type
2124    // arguments. #594: a generic *user* instance method infers its type
2125    // arguments from the receiver and the argument types — the explicit
2126    // `x.map[U](…)` form is deferred, so explicit type args are rejected here.
2127    // #926: a capability operation may declare its own type parameter(s),
2128    // resolved only from an explicit call-site type argument (never
2129    // inferred) — the same explicit-only discipline as `Json.decode[T]`, so
2130    // it joins that carve-out rather than `#594`'s inference. Covers a bare
2131    // local/declared capability receiver and a cross-context one (flattened
2132    // `Cap.op[T](…)` or qualified `B.Cap.op[T](…)`); arity/substitution
2133    // happens downstream in `check_static_call` /
2134    // `check_cross_context_capability_call`.
2135    let receiver_is_capability = matches!(&receiver.kind, ExprKind::Ident(id)
2136            if ctx.caps.capabilities.contains_key(&id.name)
2137                || ctx.caps.declared_capabilities.contains_key(&id.name))
2138        || flatten_ident_chain(receiver).is_some_and(|chain| {
2139            ctx.input
2140                .cross_context
2141                .resolve_cross_capability(&chain)
2142                .is_some()
2143        });
2144    if !type_args.is_empty()
2145        && !matches!(&receiver.kind, ExprKind::Ident(id) if id.name == JSON
2146            && !ctx.input.types.contains_key(JSON))
2147        && !receiver_is_capability
2148    {
2149        ctx.errors.push(CompileError::new(
2150            "bynk.generics.type_arg_mismatch",
2151            span,
2152            format!(
2153                "`{}` does not take explicit type arguments — a generic method infers them from the receiver and arguments",
2154                method.name
2155            ),
2156        ));
2157        for a in args {
2158            let _ = type_of(a, None, ctx);
2159        }
2160        return None;
2161    }
2162    // v0.25 / v0.178: a test body invokes the target's service as
2163    // `svc.call(args)`. The emitter wires the call from the same service set;
2164    // the checker resolves the service's `on call` handler and verifies the
2165    // call's arity and argument types (v0.178, #662 — before this, the branch
2166    // matched the literal method name `call` without checking the handler
2167    // exists, so `api.call(…)` on a `from http` service passed `check` and
2168    // crashed at runtime, #654). The outcome type stays loose — the runner
2169    // recovers `Result`/`Effect` shapes at runtime — but the binding edge is
2170    // real, so it is recorded for test-file references. Returns here: the
2171    // resolution must not fall through to the receiver walk, which would
2172    // report the service name as unknown (#504 backstop).
2173    if let ExprKind::Ident(id) = &receiver.kind
2174        && ctx.lookup(id.name.as_str()).is_none()
2175        && let Some(sig) = ctx.test_services.get(&id.name).cloned()
2176    {
2177        if let Some(unit) = ctx.input.cross_context.self_context.clone() {
2178            ctx.refs
2179                .record_in_unit(id.span, SymbolKind::Service, &id.name, &unit);
2180        }
2181        return check_test_service_address(&sig, id, method, args, expr_id, ctx);
2182    }
2183    // v0.6: cross-context service call. Two shapes:
2184    //   - `Alias.service(args)`           where Alias is from `consumes X as Alias`
2185    //   - `prefix.tail.service(args)`     where `prefix.tail` is a consumed context's
2186    //                                     qualified name (parsed as nested FieldAccess).
2187    // The full-qualified-name form must be checked before the bare-ident form
2188    // (the prefix's first segment doesn't resolve as anything local).
2189    if ctx.lookup_root_ident(receiver).is_none() && !ctx.root_ident_is_store_field(receiver) {
2190        // v0.15: cross-context capability call — `B.Cap.op(args)` /
2191        // `Alias.Cap.op(args)`. Checked before the service-call shape because
2192        // the receiver carries an extra (capability) segment.
2193        if let Some(chain) = flatten_ident_chain(receiver)
2194            && let Some((consumed, cap)) = ctx.input.cross_context.resolve_cross_capability(&chain)
2195        {
2196            // v0.25: the capability name-segment (`Cap` in `B.Cap` /
2197            // `Alias.Cap`) is the outermost field of the receiver chain.
2198            if let ExprKind::FieldAccess { field, .. } = &receiver.kind {
2199                ctx.refs
2200                    .record_in_unit(field.span, SymbolKind::Capability, &cap, &consumed);
2201            }
2202            return check_cross_context_capability_call(
2203                receiver, &consumed, &cap, method, type_args, args, span, expr_id, ctx,
2204            );
2205        }
2206        if let Some(consumed) = cross_context_prefix(receiver, ctx) {
2207            return check_cross_context_call(receiver, &consumed, method, args, span, expr_id, ctx);
2208        }
2209        // Looks like a dotted prefix (no local binding for the root). If the
2210        // chain matches the shape of a consumed-context call but the prefix
2211        // isn't actually consumed, surface an explicit diagnostic so the user
2212        // can fix the missing `consumes` clause rather than seeing a silent
2213        // "no methods" error later.
2214        if let ExprKind::FieldAccess { .. } = &receiver.kind
2215            && let Some(chain) = flatten_ident_chain(receiver)
2216            && chain.contains('.')
2217        {
2218            let info = &ctx.input.cross_context;
2219            let in_context = info.self_context.is_some();
2220            if in_context && info.resolve_prefix(&chain).is_none() {
2221                ctx.errors.push(
2222                    CompileError::new(
2223                        "bynk.resolve.unconsumed_context",
2224                        receiver.span,
2225                        format!(
2226                            "`{chain}.{}` looks like a cross-context service call, but `{chain}` is not in this context's `consumes` clauses",
2227                            method.name
2228                        ),
2229                    )
2230                    .with_note(
2231                        "add a `consumes {chain}` clause at the top of the context, or use an alias and call it through the alias",
2232                    ),
2233                );
2234                for a in args {
2235                    let _ = type_of(a, None, ctx);
2236                }
2237                return None;
2238            }
2239        }
2240    }
2241    // Detect capability call (v0.5): receiver is a bare Ident naming a
2242    // capability declared in the context (in scope via `given`, or declared
2243    // but undeclared in `given` — the static-call path emits the error).
2244    if let ExprKind::Ident(id) = &receiver.kind
2245        && ctx.lookup(id.name.as_str()).is_none()
2246        && (ctx.caps.capabilities.contains_key(&id.name)
2247            || ctx.caps.declared_capabilities.contains_key(&id.name))
2248    {
2249        return check_static_call(id, method, type_args, args, span, expected, expr_id, ctx);
2250    }
2251    // Detect static-call shape: receiver is a bare Ident naming a declared
2252    // type (not a local/param). Dispatch to check_static_call. `type_args` is
2253    // always empty here — `check_method_call`'s gate above only admits a
2254    // non-empty slice for a capability (handled just above) or the `Json`
2255    // codec (handled separately, `check_json_static`).
2256    if let ExprKind::Ident(id) = &receiver.kind
2257        && ctx.lookup(id.name.as_str()).is_none()
2258        && ctx.input.types.contains_key(&id.name)
2259    {
2260        return check_static_call(id, method, type_args, args, span, expected, expr_id, ctx);
2261    }
2262    // v0.20b: qualified statics on the built-in collection types —
2263    // `List.empty()` / `Map.empty()`. Like an empty `[]`, they need an
2264    // expected type to pin their element/key/value types.
2265    if let ExprKind::Ident(id) = &receiver.kind
2266        && ctx.lookup(id.name.as_str()).is_none()
2267        && !ctx.input.types.contains_key(&id.name)
2268        && (id.name == LIST || id.name == MAP)
2269    {
2270        let ns = if id.name == LIST { LIST } else { MAP };
2271        ctx.callees.insert(
2272            expr_id,
2273            Callee::Intrinsic {
2274                ns,
2275                op: method.name.clone(),
2276            },
2277        );
2278        return check_collection_static(id, method, args, span, expected, ctx);
2279    }
2280    // v0.22a: the numeric parse statics — `Int.parse(s)` / `Float.parse(s)`.
2281    // The parser only admits these keywords in receiver position when
2282    // followed by `.`, so the Ident shape here is exactly the static form.
2283    if let ExprKind::Ident(id) = &receiver.kind
2284        && (id.name == INT || id.name == FLOAT)
2285    {
2286        let ns = if id.name == INT { INT } else { FLOAT };
2287        ctx.callees.insert(
2288            expr_id,
2289            Callee::Intrinsic {
2290                ns,
2291                op: method.name.clone(),
2292            },
2293        );
2294        return check_numeric_parse_static(id, method, args, span, ctx);
2295    }
2296    // v0.86 (ADR 0112): the `Duration.millis(n)` static constructor — the way to
2297    // build a `Duration` from a runtime `Int` (the literal covers constants).
2298    if let ExprKind::Ident(id) = &receiver.kind
2299        && id.name == DURATION
2300        && ctx.lookup(DURATION).is_none()
2301        && !ctx.input.types.contains_key(DURATION)
2302    {
2303        ctx.callees.insert(
2304            expr_id,
2305            Callee::Intrinsic {
2306                ns: DURATION,
2307                op: method.name.clone(),
2308            },
2309        );
2310        return check_duration_static(method, args, span, ctx);
2311    }
2312    // v0.90 (ADR 0114 D6): the `Instant.fromEpochMillis(n)` static constructor.
2313    if let ExprKind::Ident(id) = &receiver.kind
2314        && id.name == INSTANT
2315        && ctx.lookup(INSTANT).is_none()
2316        && !ctx.input.types.contains_key(INSTANT)
2317    {
2318        ctx.callees.insert(
2319            expr_id,
2320            Callee::Intrinsic {
2321                ns: INSTANT,
2322                op: method.name.clone(),
2323            },
2324        );
2325        return check_instant_static(method, args, span, ctx);
2326    }
2327    // v0.110 (ADR 0142 D2): the `Bytes` static constructors —
2328    // `Bytes.fromUtf8(s)` / `Bytes.fromBase64(s)` / `Bytes.empty()`.
2329    if let ExprKind::Ident(id) = &receiver.kind
2330        && id.name == BYTES
2331        && ctx.lookup(BYTES).is_none()
2332        && !ctx.input.types.contains_key(BYTES)
2333    {
2334        ctx.callees.insert(
2335            expr_id,
2336            Callee::Intrinsic {
2337                ns: BYTES,
2338                op: method.name.clone(),
2339            },
2340        );
2341        return check_bytes_static(method, args, span, ctx);
2342    }
2343    // v0.22b: the typed JSON codec statics (ADR 0045).
2344    if let ExprKind::Ident(id) = &receiver.kind
2345        && id.name == JSON
2346        && ctx.lookup(JSON).is_none()
2347        && !ctx.input.types.contains_key(JSON)
2348    {
2349        ctx.callees.insert(
2350            expr_id,
2351            Callee::Intrinsic {
2352                ns: JSON,
2353                op: method.name.clone(),
2354            },
2355        );
2356        return check_json_static(method, type_args, args, span, expected, ctx);
2357    }
2358    // v0.100: the `Stream.of(xs)` static constructor (real-time track slice 0).
2359    if let ExprKind::Ident(id) = &receiver.kind
2360        && id.name == STREAM
2361        && ctx.lookup(STREAM).is_none()
2362        && !ctx.input.types.contains_key(STREAM)
2363    {
2364        ctx.callees.insert(
2365            expr_id,
2366            Callee::Intrinsic {
2367                ns: STREAM,
2368                op: method.name.clone(),
2369            },
2370        );
2371        return check_stream_static(method, args, span, ctx);
2372    }
2373    // v0.20b: `insert`/`prepend` return their receiver's collection type —
2374    // propagate an expected collection type down the chain so
2375    // `let m: Map[String, Int] = Map.empty().insert("a", 1)` infers.
2376    let recv_expected = match (expected, method.name.as_str()) {
2377        (Some(t), "insert") => peel_to_map(t, tys).map(|(k, v)| tys.intern(Ty::Map(k, v))),
2378        (Some(t), "prepend") => peel_to_list(t, tys).map(|e| tys.intern(Ty::List(e))),
2379        _ => None,
2380    };
2381    let recv_ty = type_of(receiver, recv_expected, ctx)?;
2382    // v0.20b: built-in kernel methods on the collection types. These are
2383    // compiler-known special forms typed directly here — generic in their
2384    // accumulator without the (deferred) declared-generic-methods feature;
2385    // the deferral bites only on declared methods (ADR 0037).
2386    match &*tys.get(recv_ty) {
2387        Ty::List(elem) => {
2388            ctx.callees.insert(
2389                expr_id,
2390                Callee::Kernel {
2391                    recv: recv_ty,
2392                    op: method.name.clone(),
2393                },
2394            );
2395            return check_list_kernel_method(method, args, *elem, span, ctx);
2396        }
2397        // v0.91 (ADR 0115): a chained builder/terminal on a lazy `Query[T]`.
2398        Ty::Query(elem) => {
2399            ctx.callees.insert(
2400                expr_id,
2401                Callee::Kernel {
2402                    recv: recv_ty,
2403                    op: method.name.clone(),
2404                },
2405            );
2406            return check_query_kernel_method(method, args, *elem, span, ctx);
2407        }
2408        // v0.100: a chained builder/terminal on a `Stream[T]`.
2409        Ty::Stream(elem) => {
2410            ctx.callees.insert(
2411                expr_id,
2412                Callee::Kernel {
2413                    recv: recv_ty,
2414                    op: method.name.clone(),
2415                },
2416            );
2417            return check_stream_kernel_method(method, args, *elem, span, ctx);
2418        }
2419        // v0.102: the held-resource operations on a `Connection[F]` — `send(f)`
2420        // (non-consuming) and `close()` (consuming). The linearity pass tracks
2421        // the ownership transitions; this types the operations.
2422        Ty::Connection(frame) => {
2423            ctx.callees.insert(
2424                expr_id,
2425                Callee::Kernel {
2426                    recv: recv_ty,
2427                    op: method.name.clone(),
2428                },
2429            );
2430            return check_connection_method(method, args, *frame, span, ctx);
2431        }
2432        Ty::Map(key, val) => {
2433            ctx.callees.insert(
2434                expr_id,
2435                Callee::Kernel {
2436                    recv: recv_ty,
2437                    op: method.name.clone(),
2438                },
2439            );
2440            return check_map_kernel_method(method, args, *key, *val, span, ctx);
2441        }
2442        // v0.21: the numeric kernel — conversions as value methods on the
2443        // bare base types. A refined value reaches these too, but via the
2444        // refined-receiver fallback below (after its own declared methods),
2445        // not via any `.raw` surface — a refined type's only source-level
2446        // constructors are `.of` and literal admission (ADR 0182); unlike an
2447        // opaque type it exposes no `.unsafe` (that is opaque-only, confined
2448        // to the defining commons).
2449        Ty::Base(base @ (BaseType::Int | BaseType::Float)) => {
2450            ctx.callees.insert(
2451                expr_id,
2452                Callee::Kernel {
2453                    recv: recv_ty,
2454                    op: method.name.clone(),
2455                },
2456            );
2457            return check_numeric_kernel_method(method, args, *base, span, ctx);
2458        }
2459        // v0.86 (ADR 0112): the `Duration` kernel — `toMillis`/`toString`.
2460        Ty::Base(BaseType::Duration) => {
2461            ctx.callees.insert(
2462                expr_id,
2463                Callee::Kernel {
2464                    recv: recv_ty,
2465                    op: method.name.clone(),
2466                },
2467            );
2468            return check_duration_kernel_method(method, args, span, ctx);
2469        }
2470        // v0.90 (ADR 0114): the `Instant` kernel — `toEpochMillis`/`toString`.
2471        Ty::Base(BaseType::Instant) => {
2472            ctx.callees.insert(
2473                expr_id,
2474                Callee::Kernel {
2475                    recv: recv_ty,
2476                    op: method.name.clone(),
2477                },
2478            );
2479            return check_instant_kernel_method(method, args, span, ctx);
2480        }
2481        // v0.110 (ADR 0142): the `Bytes` kernel — `length`/`toBase64`/`decodeUtf8`.
2482        Ty::Base(BaseType::Bytes) => {
2483            ctx.callees.insert(
2484                expr_id,
2485                Callee::Kernel {
2486                    recv: recv_ty,
2487                    op: method.name.clone(),
2488                },
2489            );
2490            return check_bytes_kernel_method(method, args, span, ctx);
2491        }
2492        // v0.22a: the string kernel (ADR 0046).
2493        Ty::Base(BaseType::String) => {
2494            ctx.callees.insert(
2495                expr_id,
2496                Callee::Kernel {
2497                    recv: recv_ty,
2498                    op: method.name.clone(),
2499                },
2500            );
2501            return check_string_kernel_method(method, args, span, ctx);
2502        }
2503        // v0.22a: the Option/Result combinators as kernel methods (ADR 0048).
2504        Ty::Option(inner) => {
2505            ctx.callees.insert(
2506                expr_id,
2507                Callee::Kernel {
2508                    recv: recv_ty,
2509                    op: method.name.clone(),
2510                },
2511            );
2512            return check_option_kernel_method(method, args, *inner, span, ctx);
2513        }
2514        Ty::Result(ok, err) => {
2515            ctx.callees.insert(
2516                expr_id,
2517                Callee::Kernel {
2518                    recv: recv_ty,
2519                    op: method.name.clone(),
2520                },
2521            );
2522            return check_result_kernel_method(method, args, *ok, *err, span, ctx);
2523        }
2524        // The `Effect[Result[T, E]]` combinators (§2.8.3): `mapOk`/`mapErr`/
2525        // `flatMapOk`/`flatMapErr` on the universal cross-context shape. Only an
2526        // `Effect` wrapping a `Result` has methods; any other `Effect[_]` falls
2527        // through to the "no methods" error below.
2528        Ty::Effect(inner) => {
2529            if let Ty::Result(ok, err) = &*tys.get(*inner) {
2530                ctx.callees.insert(
2531                    expr_id,
2532                    Callee::Kernel {
2533                        recv: recv_ty,
2534                        op: method.name.clone(),
2535                    },
2536                );
2537                return check_effect_result_kernel_method(method, args, *ok, *err, span, ctx);
2538            }
2539        }
2540        _ => {}
2541    }
2542    // Find a named type for the receiver, then look up its instance methods.
2543    let type_name = match &*tys.get(recv_ty) {
2544        Ty::Named { name, .. } => name.clone(),
2545        _ => {
2546            ctx.errors.push(CompileError::new(
2547                "bynk.types.method_on_non_named_type",
2548                method.span,
2549                format!(
2550                    "type `{}` has no methods — only user-declared types support method calls",
2551                    recv_ty.display(tys)
2552                ),
2553            ));
2554            return None;
2555        }
2556    };
2557    // Agent handler dispatch: when the receiver is an agent instance, look
2558    // up the method against the agent's declared `on call` handlers and
2559    // resolve to the handler's return type.
2560    if let Some(agent) = ctx.input.agents.get(&type_name).cloned() {
2561        let Some(handler) = agent.handlers.iter().find(|h| {
2562            h.method_name
2563                .as_ref()
2564                .is_some_and(|n| n.name == method.name)
2565        }) else {
2566            ctx.errors.push(CompileError::new(
2567                "bynk.agent.handler_not_found",
2568                method.span,
2569                format!(
2570                    "agent `{}` has no handler named `{}`",
2571                    type_name, method.name
2572                ),
2573            ));
2574            for a in args {
2575                let _ = type_of(a, None, ctx);
2576            }
2577            return None;
2578        };
2579        ctx.callees.insert(
2580            expr_id,
2581            Callee::Agent {
2582                agent: type_name.clone(),
2583                handler: method.name.clone(),
2584            },
2585        );
2586        // #304: the handler is a first-class index symbol, keyed by the
2587        // compound `"Agent.handler"` name — same convention and same
2588        // recorded-regardless-of-downstream-errors placement as the
2589        // ordinary instance-method call below.
2590        ctx.refs.record(
2591            method.span,
2592            SymbolKind::Handler,
2593            &format!("{type_name}.{}", method.name),
2594        );
2595        if handler.params.len() != args.len() {
2596            ctx.errors.push(CompileError::new(
2597                "bynk.agent.handler_arity",
2598                method.span,
2599                format!(
2600                    "agent handler `{}.{}` expects {} argument(s), but {} were given",
2601                    type_name,
2602                    method.name,
2603                    handler.params.len(),
2604                    args.len()
2605                ),
2606            ));
2607            for a in args {
2608                let _ = type_of(a, None, ctx);
2609            }
2610            return None;
2611        }
2612        for (p, arg) in handler.params.iter().zip(args.iter()) {
2613            let pty = resolve_type_ref(&p.type_ref, &ctx.input.types, tys);
2614            let arg_ty = type_of(arg, pty, ctx);
2615            if let (Some(a), Some(p_ty)) = (arg_ty, pty.as_ref())
2616                && !compatible(a, *p_ty, tys)
2617            {
2618                ctx.errors.push(CompileError::new(
2619                    "bynk.types.argument_mismatch",
2620                    arg.span,
2621                    format!(
2622                        "argument has type `{}`, but `{}.{}` expects `{}`",
2623                        a.display(tys),
2624                        type_name,
2625                        method.name,
2626                        p_ty.display(tys)
2627                    ),
2628                ));
2629            }
2630        }
2631        return resolve_type_ref(&handler.return_type, &ctx.input.types, tys);
2632    }
2633    let table = ctx
2634        .input
2635        .methods
2636        .get(&type_name)
2637        .cloned()
2638        .unwrap_or_default();
2639    let Some(method_decl) = table.instance.get(&method.name).cloned() else {
2640        // #561: a refined receiver inherits its base type's read-only kernel
2641        // methods as a fallback *after* its own declared methods (DECISION D).
2642        // A refined value erases to its base at runtime, so the base methods
2643        // already apply bit-for-bit; only the checker had to be taught to look.
2644        // Results are base-typed (DECISION B) and refined arguments widen via
2645        // `compatible`, so the base kernel checkers need no change. The match
2646        // is `Refined` only — opaque types deliberately do not widen, so they
2647        // keep reporting `method_not_found`. `Bool` (and any base without a
2648        // kernel) inherits nothing and falls through to the same error.
2649        if let Ty::Named {
2650            kind: NamedKind::Refined(base),
2651            ..
2652        } = &*tys.get(recv_ty)
2653        {
2654            if !matches!(base, BaseType::Bool) {
2655                ctx.callees.insert(
2656                    expr_id,
2657                    Callee::Kernel {
2658                        recv: recv_ty,
2659                        op: method.name.clone(),
2660                    },
2661                );
2662            }
2663            match base {
2664                BaseType::Int | BaseType::Float => {
2665                    return check_numeric_kernel_method(method, args, *base, span, ctx);
2666                }
2667                BaseType::String => return check_string_kernel_method(method, args, span, ctx),
2668                BaseType::Duration => return check_duration_kernel_method(method, args, span, ctx),
2669                BaseType::Instant => return check_instant_kernel_method(method, args, span, ctx),
2670                BaseType::Bytes => return check_bytes_kernel_method(method, args, span, ctx),
2671                BaseType::Bool => {}
2672            }
2673        }
2674        ctx.errors.push(CompileError::new(
2675            "bynk.types.method_not_found",
2676            method.span,
2677            format!(
2678                "type `{}` has no instance method named `{}`",
2679                type_name, method.name
2680            ),
2681        ));
2682        return None;
2683    };
2684    // v0.36 (ADR 0069): the method is a first-class index symbol, keyed by the
2685    // compound `"Type.method"` name. Recorded already-spelled from the resolved
2686    // receiver type; the bare edge resolves through the same `uses`/`consumes`
2687    // qualification as cross-file type references.
2688    ctx.refs.record(
2689        method.span,
2690        SymbolKind::Method,
2691        &format!("{type_name}.{}", method.name),
2692    );
2693    ctx.callees
2694        .insert(expr_id, Callee::Method(Arc::clone(&method_decl)));
2695    // #594: a generic instance method — one on a generic receiver, or one
2696    // carrying its own type parameters — resolves its parameter and return
2697    // types against a substitution seeded from the receiver's type arguments,
2698    // then infers the method's own parameters from the argument types
2699    // (argument-directed unification, ADR 0029). A method on a non-generic
2700    // receiver with no own type parameters takes the plain path below,
2701    // byte-identically to before.
2702    let recv_type_params: Vec<String> = ctx
2703        .input
2704        .types
2705        .get(&type_name)
2706        .map(|d| d.type_params.iter().map(|p| p.name.name.clone()).collect())
2707        .unwrap_or_default();
2708    if !recv_type_params.is_empty() || !method_decl.type_params.is_empty() {
2709        return check_generic_method_call(
2710            &type_name,
2711            &recv_type_params,
2712            recv_ty,
2713            &method_decl,
2714            method,
2715            args,
2716            ctx,
2717        );
2718    }
2719    // Param count excludes the implicit `self`.
2720    if method_decl.params.len() != args.len() {
2721        ctx.errors.push(
2722            CompileError::new(
2723                "bynk.types.method_arity",
2724                method.span,
2725                format!(
2726                    "method `{}.{}` expects {} argument(s), but {} were given",
2727                    type_name,
2728                    method.name,
2729                    method_decl.params.len(),
2730                    args.len()
2731                ),
2732            )
2733            .with_label(method_decl.name.ident().span, "method declared here"),
2734        );
2735        for a in args {
2736            let _ = type_of(a, None, ctx);
2737        }
2738        return None;
2739    }
2740    let mut ok = true;
2741    for (i, (param, arg)) in method_decl.params.iter().zip(args.iter()).enumerate() {
2742        record_param_hint(ctx.hints, &param.name.name, arg);
2743        let expected = resolve_type_ref(&param.type_ref, &ctx.input.types, tys);
2744        let actual = type_of(arg, expected, ctx);
2745        let (Some(actual), Some(expected)) = (actual, expected) else {
2746            ok = false;
2747            continue;
2748        };
2749        if !compatible(actual, expected, tys) {
2750            ctx.errors.push(CompileError::new(
2751                "bynk.types.argument_mismatch",
2752                arg.span,
2753                format!(
2754                    "argument {} to `{}.{}` has type `{}`, but parameter `{}` expects `{}`",
2755                    i + 1,
2756                    type_name,
2757                    method.name,
2758                    actual.display(tys),
2759                    param.name.name,
2760                    expected.display(tys)
2761                ),
2762            ));
2763            ok = false;
2764        }
2765    }
2766    let _ = span;
2767    if !ok {
2768        return None;
2769    }
2770    resolve_type_ref(&method_decl.return_type, &ctx.input.types, tys)
2771}
2772
2773/// #594: type-check a call to a generic instance method — one attached to a
2774/// generic type (so `self` carries the type's parameters) and/or carrying its
2775/// own `[U]` parameters. Mirrors [`check_generic_call`] (the free-function
2776/// path, ADR 0029): resolve the method's parameter/return patterns with the
2777/// rigid vars in scope, seed the substitution from the receiver's concrete type
2778/// arguments, then drive argument-directed inference over the method's own
2779/// parameters. The receiver's parameters are already ground (from the receiver
2780/// type), so only the method's own parameters need inferring.
2781fn check_generic_method_call(
2782    type_name: &str,
2783    recv_type_params: &[String],
2784    recv_ty: TyId,
2785    method_decl: &FnDecl,
2786    method: &Ident,
2787    args: &[Expr],
2788    ctx: &mut Ctx,
2789) -> Option<TyId> {
2790    let tys = ctx.tys;
2791    // Rigid vars: the receiver type's parameters plus the method's own.
2792    let mut vars: HashSet<String> = recv_type_params.iter().cloned().collect();
2793    for tp in &method_decl.type_params {
2794        vars.insert(tp.name.name.clone());
2795    }
2796    // Param count excludes the implicit `self`.
2797    if method_decl.params.len() != args.len() {
2798        ctx.errors.push(
2799            CompileError::new(
2800                "bynk.types.method_arity",
2801                method.span,
2802                format!(
2803                    "method `{}.{}` expects {} argument(s), but {} were given",
2804                    type_name,
2805                    method.name,
2806                    method_decl.params.len(),
2807                    args.len()
2808                ),
2809            )
2810            .with_label(method_decl.name.ident().span, "method declared here"),
2811        );
2812        for a in args {
2813            let _ = type_of(a, None, ctx);
2814        }
2815        return None;
2816    }
2817    // Seed the substitution from the receiver's concrete type arguments: the
2818    // type's parameters are ground the moment the receiver's type is known
2819    // (`self.map(g)` on a `Box[User]` binds the type's `A` to `User`; on `self`
2820    // it binds `A` to its own rigid var). An arity mismatch here means the
2821    // receiver was under-applied — reported where the receiver was typed — so we
2822    // leave those parameters unseeded and fail below as uninferable.
2823    let mut subst: HashMap<String, TyId> = HashMap::new();
2824    if let Ty::Named {
2825        args: recv_args, ..
2826    } = &*tys.get(recv_ty)
2827        && recv_args.len() == recv_type_params.len()
2828    {
2829        for (name, arg) in recv_type_params.iter().zip(recv_args.iter()) {
2830            subst.insert(name.clone(), *arg);
2831        }
2832    }
2833    // Resolve the method's parameter and return patterns with every rigid var in
2834    // scope (a name matching one resolves to `Ty::Var`, not an unknown type).
2835    let var_params: Vec<Option<TyId>> = method_decl
2836        .params
2837        .iter()
2838        .map(|p| resolve_type_ref_in(&p.type_ref, &ctx.input.types, &vars, tys))
2839        .collect();
2840    let ret_pattern = resolve_type_ref_in(&method_decl.return_type, &ctx.input.types, &vars, tys)?;
2841
2842    let mut arg_tys: Vec<Option<TyId>> = vec![None; args.len()];
2843    // Pass 1 — non-lambda arguments (mirrors `check_generic_call`).
2844    for (i, arg) in args.iter().enumerate() {
2845        if matches!(arg.kind, ExprKind::Lambda(_)) {
2846            continue;
2847        }
2848        let expected = var_params[i].map(|p| substitute(p, &subst, tys));
2849        let ty = type_of(arg, expected, ctx);
2850        if let (Some(pattern), Some(actual)) = (var_params[i], ty)
2851            && !unify(pattern, actual, &mut subst, tys)
2852        {
2853            ctx.errors.push(CompileError::new(
2854                "bynk.generics.type_arg_mismatch",
2855                arg.span,
2856                format!(
2857                    "argument {} infers a type for `{}.{}`'s type parameter that conflicts with an earlier argument or the receiver",
2858                    i + 1,
2859                    type_name,
2860                    method.name
2861                ),
2862            ));
2863            return None;
2864        }
2865        arg_tys[i] = ty;
2866    }
2867    // Pass 2 — lambda arguments, against the now-substituted expecteds.
2868    for (i, arg) in args.iter().enumerate() {
2869        if !matches!(arg.kind, ExprKind::Lambda(_)) {
2870            continue;
2871        }
2872        let expected = var_params[i].map(|p| substitute(p, &subst, tys));
2873        let params_unconstrained = expected.is_some_and(|e| {
2874            matches!(&*tys.get(e), Ty::Fn { params, .. }
2875                if params.iter().any(|p| contains_var(*p, tys)))
2876        });
2877        let fully_annotated = matches!(
2878            &arg.kind,
2879            ExprKind::Lambda(l) if l.params.iter().all(|p| p.type_ref.is_some())
2880        );
2881        if params_unconstrained && !fully_annotated {
2882            ctx.errors.push(
2883                CompileError::new(
2884                    "bynk.generics.uninferable_type_arg",
2885                    arg.span,
2886                    format!(
2887                        "the lambda's parameter types depend on `{}.{}`'s type parameters, which the receiver and other arguments do not determine",
2888                        type_name, method.name
2889                    ),
2890                )
2891                .with_note("annotate the lambda's parameters"),
2892            );
2893            return None;
2894        }
2895        let ty = if params_unconstrained {
2896            type_of(arg, None, ctx)
2897        } else {
2898            type_of(arg, expected, ctx)
2899        };
2900        if let (Some(pattern), Some(actual)) = (var_params[i], ty)
2901            && !unify(pattern, actual, &mut subst, tys)
2902        {
2903            ctx.errors.push(CompileError::new(
2904                "bynk.generics.type_arg_mismatch",
2905                arg.span,
2906                format!(
2907                    "the lambda's type conflicts with `{}.{}`'s inferred type arguments",
2908                    type_name, method.name
2909                ),
2910            ));
2911            return None;
2912        }
2913        arg_tys[i] = ty;
2914    }
2915    // Every one of the method's own type parameters must now be determined (the
2916    // receiver's parameters were seeded above).
2917    for tp in &method_decl.type_params {
2918        if !subst.contains_key(&tp.name.name) {
2919            ctx.errors.push(
2920                CompileError::new(
2921                    "bynk.generics.uninferable_type_arg",
2922                    method.span,
2923                    format!(
2924                        "type parameter `{}` of `{}.{}` is not inferable from the receiver or the arguments",
2925                        tp.name.name, type_name, method.name
2926                    ),
2927                )
2928                .with_label(tp.span, "declared here"),
2929            );
2930            return None;
2931        }
2932    }
2933    // Final compatibility over the fully-ground parameter types.
2934    let mut ok = true;
2935    for (i, (pattern, arg)) in var_params.iter().zip(args).enumerate() {
2936        record_param_hint(ctx.hints, &method_decl.params[i].name.name, arg);
2937        let (Some(pattern), Some(arg_ty)) = (pattern, arg_tys[i].as_ref()) else {
2938            continue;
2939        };
2940        let ground = substitute(*pattern, &subst, tys);
2941        if !compatible(*arg_ty, ground, tys) {
2942            ctx.errors.push(CompileError::new(
2943                "bynk.types.argument_mismatch",
2944                arg.span,
2945                format!(
2946                    "argument {} to `{}.{}` has type `{}`, but `{}` is expected",
2947                    i + 1,
2948                    type_name,
2949                    method.name,
2950                    arg_ty.display(tys),
2951                    ground.display(tys)
2952                ),
2953            ));
2954            ok = false;
2955        }
2956    }
2957    if !ok {
2958        return None;
2959    }
2960    // #1702 review: the same call-site bound as a generic function's, over the
2961    // receiver type's parameters and the method's own.
2962    let decl = ctx.input.methods.get(type_name).and_then(|t| {
2963        t.instance
2964            .get(&method.name)
2965            .or_else(|| t.statics.get(&method.name))
2966            .cloned()
2967    });
2968    let compared = match &decl {
2969        Some(decl) if std::ptr::eq(decl.as_ref(), method_decl) => {
2970            super::equality::compared_type_params(decl, ctx)
2971        }
2972        _ => Default::default(),
2973    };
2974    let mut params: Vec<(String, Option<Span>)> = method_decl
2975        .type_params
2976        .iter()
2977        .map(|tp| (tp.name.name.clone(), Some(tp.span)))
2978        .collect();
2979    params.extend(recv_type_params.iter().map(|p| (p.clone(), None)));
2980    let callee = format!("{type_name}.{}", method.name);
2981    if !super::equality::check_compared_args(&callee, method.span, &params, &compared, &subst, ctx)
2982    {
2983        return None;
2984    }
2985    // v0.39 (ADR 0072)-style hint: show the inferred method type arguments after
2986    // the method name (`box.map` [Int] `(f)`). Only the method's own
2987    // parameters — the receiver's are visible in the receiver's type.
2988    if !method_decl.type_params.is_empty() {
2989        let rendered: Option<Vec<String>> = method_decl
2990            .type_params
2991            .iter()
2992            .map(|tp| subst.get(&tp.name.name).map(|t| t.display(tys)))
2993            .collect();
2994        if let Some(parts) = rendered {
2995            ctx.hints
2996                .record(method.span, format!("[{}]", parts.join(", ")));
2997        }
2998    }
2999    Some(substitute(ret_pattern, &subst, tys))
3000}
3001
3002/// v0.178 (#662) / v0.182 (#664): resolve a test-body service invocation against
3003/// the target's declared handlers and check its arity and argument types. The
3004/// address form depends on the method name:
3005///
3006/// - `svc.call(args)` — the `on call` handler (#662);
3007/// - `svc.<VERB>("/path", …)` — an http route (`GET`/`POST`/`PUT`/`PATCH`/`DELETE`);
3008/// - `svc.schedule("<expr>", …)` — a cron handler by its schedule string;
3009/// - `svc.message(msg)` — the queue message handler.
3010///
3011/// The outcome type stays loose (the runner recovers `Result`/`Effect` at
3012/// runtime); the call-site principal (`by <Actor>`) is checked separately at the
3013/// statement level. Returns `None` (the loose outcome) in every arm.
3014fn check_test_service_address(
3015    sig: &TestServiceSig,
3016    id: &Ident,
3017    method: &Ident,
3018    args: &[Expr],
3019    // P6.0 (#1139): the outer `MethodCall` expression's own identity.
3020    expr_id: ExprId,
3021    ctx: &mut Ctx,
3022) -> Option<TyId> {
3023    use bynk_syntax::ast::{ExprKind as EK, HandlerKind};
3024
3025    // `svc.call(...)` — the `on call` handler (Slice 0).
3026    if method.name == "call" {
3027        ctx.callees.insert(
3028            expr_id,
3029            Callee::TestService {
3030                service: id.name.clone(),
3031                address: "call".to_string(),
3032            },
3033        );
3034        let Some(handler) = sig.call_handler() else {
3035            let message = match &sig.protocol {
3036                Some(protocol) => format!(
3037                    "`{}` is a `from {protocol}` service and has no `on call` handler to invoke",
3038                    id.name
3039                ),
3040                None => format!("service `{}` has no `on call` handler to invoke", id.name),
3041            };
3042            ctx.errors.push(
3043                CompileError::new("bynk.test.service_no_call_handler", method.span, message)
3044                    .with_note(
3045                        "call an `on call` service with `svc.call(...)`, an http route with `svc.GET(\"/path\")`, cron with `svc.schedule(\"…\")`, or a queue with `svc.message(m)`",
3046                    ),
3047            );
3048            for a in args {
3049                let _ = type_of(a, None, ctx);
3050            }
3051            return None;
3052        };
3053        let params = handler.params.clone();
3054        check_address_args(&id.name, "call", &params, args, method.span, ctx);
3055        return None;
3056    }
3057
3058    // `svc.<VERB>("/path", …)` — an http route. The first argument is the route
3059    // *pattern* (a compile-time-resolved name), the rest are positional path
3060    // params then the body, matched against the handler's declared params.
3061    // `HttpMethod::from_ident` is the single source of truth the emitter shares.
3062    if bynk_syntax::ast::HttpMethod::from_ident(&method.name).is_some() {
3063        ctx.callees.insert(
3064            expr_id,
3065            Callee::TestService {
3066                service: id.name.clone(),
3067                address: method.name.clone(),
3068            },
3069        );
3070        let Some(EK::StrLit(path)) = args.first().map(|a| &a.kind) else {
3071            ctx.errors.push(
3072                CompileError::new(
3073                    "bynk.test.service_bad_address",
3074                    method.span,
3075                    format!(
3076                        "`{}.{}` addresses an http route, so its first argument must be the route pattern string (e.g. `\"/todos\"`)",
3077                        id.name, method.name
3078                    ),
3079                ),
3080            );
3081            for a in args {
3082                let _ = type_of(a, None, ctx);
3083            }
3084            return None;
3085        };
3086        // The classification recorded above (verb only) is enriched here,
3087        // now that the route pattern — the thing `sig.handlers` is actually
3088        // matched against below — is known; a later go-to-definition
3089        // consumer needs the path, not just the verb, to resolve a handler.
3090        ctx.callees.insert(
3091            expr_id,
3092            Callee::TestService {
3093                service: id.name.clone(),
3094                address: format!("{} {path}", method.name),
3095            },
3096        );
3097        let matched = sig.handlers.iter().find(|h| {
3098            matches!(&h.kind, HandlerKind::Http { method: m, path: p } if m.as_str() == method.name && p == path)
3099        });
3100        let Some(handler) = matched else {
3101            // #707: the method has no handler at this path. If the *path* is
3102            // declared (for some other method), this is a **wrong-method** call —
3103            // the `405` fall-through test. Allow it: it drives the router with a
3104            // method the path has no handler for and observes `Rejected(
3105            // MethodNotAllowed)`. There is no handler, so no args are matched (a
3106            // `405` is synthesised before the body is read); the outcome is loose.
3107            let path_declared = sig
3108                .handlers
3109                .iter()
3110                .any(|h| matches!(&h.kind, HandlerKind::Http { path: p, .. } if p == path));
3111            if path_declared {
3112                let _ = type_of(&args[0], None, ctx);
3113                // A wrong-method call has no handler, so it takes only the path;
3114                // diagnose extra args rather than silently dropping them (the
3115                // lowering forwards only method+path to the generic driver).
3116                if args.len() > 1 {
3117                    ctx.errors.push(
3118                        CompileError::new(
3119                            "bynk.test.service_call_arity",
3120                            method.span,
3121                            format!(
3122                                "`{}.{}(\"{}\")` is a wrong-method `405` test and takes only the route path, but {} argument(s) were given",
3123                                id.name,
3124                                method.name,
3125                                path,
3126                                args.len() - 1
3127                            ),
3128                        )
3129                        .with_note("a wrong-method call reaches no handler, so it passes no body or params"),
3130                    );
3131                    for a in &args[1..] {
3132                        let _ = type_of(a, None, ctx);
3133                    }
3134                }
3135                return None;
3136            }
3137            ctx.errors.push(
3138                CompileError::new(
3139                    "bynk.test.service_unknown_route",
3140                    method.span,
3141                    format!(
3142                        "`{}` declares no route at `\"{}\"` (no handler for any method)",
3143                        id.name, path
3144                    ),
3145                )
3146                .with_note("the path must match a declared route; drive a wrong method against an existing path to test the `405` fall-through"),
3147            );
3148            for a in args {
3149                let _ = type_of(a, None, ctx);
3150            }
3151            return None;
3152        };
3153        let params = handler.params.clone();
3154        // Type the route-pattern string as an ordinary string; check the rest.
3155        let _ = type_of(&args[0], None, ctx);
3156        check_address_args(
3157            &id.name,
3158            &method.name,
3159            &params,
3160            &args[1..],
3161            method.span,
3162            ctx,
3163        );
3164        return None;
3165    }
3166
3167    // `svc.schedule("<expr>", …)` — a cron handler, matched by its schedule.
3168    if method.name == "schedule" {
3169        ctx.callees.insert(
3170            expr_id,
3171            Callee::TestService {
3172                service: id.name.clone(),
3173                address: "schedule".to_string(),
3174            },
3175        );
3176        let Some(EK::StrLit(expr)) = args.first().map(|a| &a.kind) else {
3177            ctx.errors.push(CompileError::new(
3178                "bynk.test.service_bad_address",
3179                method.span,
3180                format!(
3181                    "`{}.schedule` addresses a cron handler, so its first argument must be the schedule string",
3182                    id.name
3183                ),
3184            ));
3185            for a in args {
3186                let _ = type_of(a, None, ctx);
3187            }
3188            return None;
3189        };
3190        // Enrich with the schedule expression `sig.handlers` is matched
3191        // against below — same reasoning as the http-verb branch above.
3192        ctx.callees.insert(
3193            expr_id,
3194            Callee::TestService {
3195                service: id.name.clone(),
3196                address: format!("schedule {expr}"),
3197            },
3198        );
3199        let matched = sig
3200            .handlers
3201            .iter()
3202            .find(|h| matches!(&h.kind, HandlerKind::Cron { expr: e } if e == expr));
3203        let Some(handler) = matched else {
3204            ctx.errors.push(CompileError::new(
3205                "bynk.test.service_unknown_route",
3206                method.span,
3207                format!(
3208                    "`{}` declares no `on schedule(\"{}\")` handler",
3209                    id.name, expr
3210                ),
3211            ));
3212            for a in args {
3213                let _ = type_of(a, None, ctx);
3214            }
3215            return None;
3216        };
3217        let params = handler.params.clone();
3218        let _ = type_of(&args[0], None, ctx);
3219        check_address_args(&id.name, "schedule", &params, &args[1..], method.span, ctx);
3220        return None;
3221    }
3222
3223    // `svc.message(msg)` — the queue message handler.
3224    if method.name == "message" {
3225        ctx.callees.insert(
3226            expr_id,
3227            Callee::TestService {
3228                service: id.name.clone(),
3229                address: "message".to_string(),
3230            },
3231        );
3232        let matched = sig
3233            .handlers
3234            .iter()
3235            .find(|h| matches!(&h.kind, HandlerKind::Message));
3236        let Some(handler) = matched else {
3237            ctx.errors.push(CompileError::new(
3238                "bynk.test.service_unknown_route",
3239                method.span,
3240                format!("`{}` declares no `on message(...)` handler", id.name),
3241            ));
3242            for a in args {
3243                let _ = type_of(a, None, ctx);
3244            }
3245            return None;
3246        };
3247        let params = handler.params.clone();
3248        check_address_args(&id.name, "message", &params, args, method.span, ctx);
3249        return None;
3250    }
3251
3252    // Not a recognised address form for this service.
3253    let proto = sig.protocol.as_deref().unwrap_or("call");
3254    ctx.errors.push(CompileError::new(
3255        "bynk.test.service_bad_address",
3256        method.span,
3257        format!(
3258            "`{}.{}` is not a way to address a `from {proto}` service in a test body",
3259            id.name, method.name
3260        ),
3261    ));
3262    for a in args {
3263        let _ = type_of(a, None, ctx);
3264    }
3265    None
3266}
3267
3268/// What identity, if any, a resolved actor expects (v0.182).
3269enum ActorIdentity {
3270    Typed(TyId),
3271    CallerString,
3272    Unit,
3273    Unknown,
3274}
3275
3276/// Resolve an actor name to the identity it carries (v0.182).
3277fn resolve_actor_identity(name: &str, ctx: &Ctx) -> ActorIdentity {
3278    let tys = ctx.tys;
3279    use crate::actors::{Identity, prelude_actor};
3280    if let Some(decl) = ctx.test_actors.get(name) {
3281        return match &decl.identity {
3282            Some(t) => match resolve_type_ref(t, &ctx.input.types, tys) {
3283                Some(ty) => ActorIdentity::Typed(ty),
3284                None => ActorIdentity::Unit,
3285            },
3286            None => ActorIdentity::Unit,
3287        };
3288    }
3289    match prelude_actor(name) {
3290        Some(c) => match c.identity {
3291            Identity::Unit => ActorIdentity::Unit,
3292            Identity::CallerId => ActorIdentity::CallerString,
3293            Identity::Declared(_) => ActorIdentity::Unit,
3294        },
3295        None => ActorIdentity::Unknown,
3296    }
3297}
3298
3299/// The actor a handler runs as: its declared `by <Actor>`, or the protocol
3300/// default (`Call`->`Caller`, `Cron`->`Scheduler`, `Queue`->`Producer`; http
3301/// has no default, so an http handler always declares one) (v0.182).
3302fn handler_actor_name(handler: &TestHandler, protocol: Option<&str>) -> Option<String> {
3303    if let Some(by) = &handler.by_clause {
3304        return Some(by.primary().name.clone());
3305    }
3306    match protocol {
3307        None => Some("Caller".to_string()),
3308        Some("cron") => Some("Scheduler".to_string()),
3309        Some("queue") => Some("Producer".to_string()),
3310        _ => None,
3311    }
3312}
3313
3314/// v0.182 (#664): resolve the handler a test-body address invocation targets,
3315/// without side effects. Shared by the argument check and the principal check.
3316fn resolve_test_address<'a>(
3317    sig: &'a TestServiceSig,
3318    method: &str,
3319    args: &[Expr],
3320) -> Option<&'a TestHandler> {
3321    use bynk_syntax::ast::{ExprKind as EK, HandlerKind, HttpMethod};
3322    if method == "call" {
3323        return sig.call_handler();
3324    }
3325    if HttpMethod::from_ident(method).is_some() {
3326        let EK::StrLit(path) = &args.first()?.kind else {
3327            return None;
3328        };
3329        return sig.handlers.iter().find(|h| {
3330            matches!(&h.kind, HandlerKind::Http { method: m, path: p } if m.as_str() == method && p == path)
3331        });
3332    }
3333    if method == "schedule" {
3334        let EK::StrLit(expr) = &args.first()?.kind else {
3335            return None;
3336        };
3337        return sig
3338            .handlers
3339            .iter()
3340            .find(|h| matches!(&h.kind, HandlerKind::Cron { expr: e } if e == expr));
3341    }
3342    if method == "message" {
3343        return sig
3344            .handlers
3345            .iter()
3346            .find(|h| matches!(&h.kind, HandlerKind::Message));
3347    }
3348    None
3349}
3350
3351/// v0.182 (#664): validate the call-site principal of a test `effect_let`
3352/// against the ADDRESSED HANDLER. This is where per-case identity isolation is
3353/// enforced: an identity-carrying handler driven with a unit principal (`by
3354/// Visitor`) or no `by` at all would otherwise emit a call with
3355/// `deps.identity === undefined`. The identity value is typed against the
3356/// handler's required identity type, not the principal actor's.
3357pub(crate) fn check_effect_let_principal(
3358    value: &Expr,
3359    principal: Option<&bynk_syntax::ast::CallSiteActor>,
3360    ctx: &mut Ctx,
3361) {
3362    let tys = ctx.tys;
3363    let ExprKind::MethodCall {
3364        receiver,
3365        method,
3366        args,
3367        ..
3368    } = &value.kind
3369    else {
3370        if let Some(p) = principal {
3371            report_principal_actor(p, ctx);
3372        }
3373        return;
3374    };
3375    let ExprKind::Ident(id) = &receiver.kind else {
3376        if let Some(p) = principal {
3377            report_principal_actor(p, ctx);
3378        }
3379        return;
3380    };
3381    let Some(sig) = ctx.test_services.get(&id.name).cloned() else {
3382        if let Some(p) = principal {
3383            report_principal_actor(p, ctx);
3384        }
3385        return;
3386    };
3387    let Some(handler) = resolve_test_address(&sig, &method.name, args).cloned() else {
3388        if let Some(p) = principal {
3389            // #707: a wrong-method `405` test (an http method whose path is
3390            // declared for *another* method) reaches no handler, so a `by` clause
3391            // is meaningless — reject it clearly. (An unresolvable address of any
3392            // other shape already errors in `check_test_service_address`; only the
3393            // now-allowed wrong-method call would otherwise drop the `by`.)
3394            let wrong_method = bynk_syntax::ast::HttpMethod::from_ident(&method.name).is_some()
3395                && matches!(args.first().map(|a| &a.kind), Some(bynk_syntax::ast::ExprKind::StrLit(path))
3396                    if sig.handlers.iter().any(|h| matches!(&h.kind, bynk_syntax::ast::HandlerKind::Http { path: p, .. } if p == path)));
3397            if wrong_method {
3398                ctx.errors.push(
3399                    CompileError::new(
3400                        "bynk.test.principal_on_wrong_method",
3401                        p.span,
3402                        format!(
3403                            "a wrong-method `405` test reaches no handler, so `by {}` is meaningless",
3404                            p.actor.name
3405                        ),
3406                    )
3407                    .with_note("drop the `by` clause on a wrong-method call"),
3408                );
3409                if let Some(id) = &p.identity {
3410                    let _ = type_of(id, None, ctx);
3411                }
3412            } else {
3413                report_principal_actor(p, ctx);
3414            }
3415        }
3416        return;
3417    };
3418
3419    // #706: `by Nobody` is the reserved "no credential" principal. It drives the
3420    // route with no `Authorization` header so the real auth seam rejects it
3421    // (`401` → `Rejected(Unauthorized)`), so it is valid on any http handler
3422    // regardless of the required identity — the whole point is that *no* valid
3423    // credential is presented. It carries no identity (`by Nobody(...)` is
3424    // meaningless). The `system`-only tier rule is enforced at emit time, like
3425    // `Wire`'s (the checker has no tier).
3426    if let Some(p) = principal
3427        && p.actor.name == "Nobody"
3428    {
3429        // #710-style: `by Nobody` is only *implemented* for a Bearer-secured
3430        // route — the no-auth driver leaves out the `Authorization` header the
3431        // Bearer seam checks. On an unsecured (`Visitor`/`None`) or
3432        // `Signature`/`Oidc` route there is no such seam to reject a missing
3433        // credential, so reject it here rather than emit a call to a driver that
3434        // was never generated.
3435        let secured = handler
3436            .by_clause
3437            .as_ref()
3438            .is_some_and(|by| crate::actors::by_clause_is_bearer(by, &ctx.test_actors));
3439        if !secured {
3440            ctx.errors.push(
3441                CompileError::new(
3442                    "bynk.test.nobody_needs_secured_route",
3443                    p.span,
3444                    "`by Nobody` drives the Bearer auth seam to a `401`, but this handler's route is not Bearer-secured — there is no credential check to reject",
3445                )
3446                .with_note(
3447                    "use `by Nobody` only on a route guarded by a `Bearer` actor; a public (`Visitor`) route has no seam to test",
3448                ),
3449            );
3450        }
3451        if let Some(idv) = &p.identity {
3452            ctx.errors.push(CompileError::new(
3453                "bynk.test.actor_no_identity",
3454                p.span,
3455                "`Nobody` presents no credential, so it takes no identity — write `by Nobody`",
3456            ));
3457            let _ = type_of(idv, None, ctx);
3458        }
3459        return;
3460    }
3461
3462    let required = handler_actor_name(&handler, sig.protocol.as_deref())
3463        .map(|actor| resolve_actor_identity(&actor, ctx));
3464
3465    match (required, principal) {
3466        (Some(ActorIdentity::Typed(ty)), principal) => match principal {
3467            Some(p) => match resolve_actor_identity(&p.actor.name, ctx) {
3468                ActorIdentity::Unknown => report_principal_actor(p, ctx),
3469                ActorIdentity::Unit | ActorIdentity::CallerString => {
3470                    ctx.errors.push(CompileError::new(
3471                        "bynk.test.principal_identity_mismatch",
3472                        p.span,
3473                        format!(
3474                            "this handler runs as an actor carrying `{}`, but `by {}` supplies no matching identity",
3475                            ty.display(tys),
3476                            p.actor.name
3477                        ),
3478                    ));
3479                    if let Some(idv) = &p.identity {
3480                        let _ = type_of(idv, None, ctx);
3481                    }
3482                }
3483                ActorIdentity::Typed(_) => match &p.identity {
3484                    Some(idv) => {
3485                        let got = type_of(idv, Some(ty), ctx);
3486                        if let Some(g) = got
3487                            && !compatible(g, ty, tys)
3488                        {
3489                            ctx.errors.push(CompileError::new(
3490                                "bynk.types.argument_mismatch",
3491                                idv.span,
3492                                format!(
3493                                    "identity has type `{}`, but the handler expects `{}`",
3494                                    g.display(tys),
3495                                    ty.display(tys)
3496                                ),
3497                            ));
3498                        }
3499                    }
3500                    None => ctx.errors.push(CompileError::new(
3501                        "bynk.test.actor_identity_required",
3502                        p.span,
3503                        format!(
3504                            "actor `{}` carries an identity, so write `by {}(...)`",
3505                            p.actor.name, p.actor.name
3506                        ),
3507                    )),
3508                },
3509            },
3510            None => ctx.errors.push(
3511                CompileError::new(
3512                    "bynk.test.principal_required",
3513                    value.span,
3514                    format!(
3515                        "this handler runs as a verified actor carrying `{}`; the case must act as it with `by <Actor>(<identity>)`",
3516                        ty.display(tys)
3517                    ),
3518                )
3519                .with_note("append a call-site actor, e.g. `... by User(\"alice\")`"),
3520            ),
3521        },
3522        (_, Some(p)) => report_principal_actor(p, ctx),
3523        (_, None) => {}
3524    }
3525}
3526
3527/// Resolve a call-site principal actor for its own diagnostics (v0.182).
3528fn report_principal_actor(p: &bynk_syntax::ast::CallSiteActor, ctx: &mut Ctx) {
3529    let tys = ctx.tys;
3530    let name = &p.actor.name;
3531    match resolve_actor_identity(name, ctx) {
3532        ActorIdentity::Unknown => {
3533            ctx.errors.push(
3534                CompileError::new(
3535                    "bynk.test.unknown_actor",
3536                    p.actor.span,
3537                    format!("`{name}` is not an actor of the target context or a prelude actor"),
3538                )
3539                .with_note("name an `actor` the target declares, or a prelude actor (`Visitor`, `Caller`, …)"),
3540            );
3541            if let Some(id) = &p.identity {
3542                let _ = type_of(id, None, ctx);
3543            }
3544        }
3545        ActorIdentity::Typed(ty) => match &p.identity {
3546            Some(id) => {
3547                let got = type_of(id, Some(ty), ctx);
3548                if let Some(g) = got
3549                    && !compatible(g, ty, tys)
3550                {
3551                    ctx.errors.push(CompileError::new(
3552                        "bynk.types.argument_mismatch",
3553                        id.span,
3554                        format!(
3555                            "identity has type `{}`, but actor `{name}` expects `{}`",
3556                            g.display(tys),
3557                            ty.display(tys)
3558                        ),
3559                    ));
3560                }
3561            }
3562            None => ctx.errors.push(CompileError::new(
3563                "bynk.test.actor_identity_required",
3564                p.span,
3565                format!("actor `{name}` carries an identity, so `by {name}(...)` needs an identity value"),
3566            )),
3567        },
3568        ActorIdentity::CallerString => {
3569            if let Some(id) = &p.identity {
3570                let _ = type_of(id, None, ctx);
3571            }
3572        }
3573        ActorIdentity::Unit => {
3574            if let Some(id) = &p.identity {
3575                let _ = type_of(id, None, ctx);
3576                ctx.errors.push(CompileError::new(
3577                    "bynk.test.actor_no_identity",
3578                    p.span,
3579                    format!("actor `{name}` has no identity, so write `by {name}` with no argument"),
3580                ));
3581            }
3582        }
3583    }
3584}
3585
3586/// Shared arity + argument-type check for a resolved test-body service address.
3587/// `positional` are the arguments that map to the handler's declared params (for
3588/// http/cron the leading pattern string has already been split off).
3589fn check_address_args(
3590    svc: &str,
3591    addr: &str,
3592    params: &[bynk_syntax::ast::Param],
3593    positional: &[Expr],
3594    err_span: Span,
3595    ctx: &mut Ctx,
3596) {
3597    let tys = ctx.tys;
3598    if params.len() != positional.len() {
3599        ctx.errors.push(
3600            CompileError::new(
3601                "bynk.test.service_call_arity",
3602                err_span,
3603                format!(
3604                    "`{svc}.{addr}` expects {} argument(s), but {} were given",
3605                    params.len(),
3606                    positional.len()
3607                ),
3608            )
3609            // Finding #46: `handler_span` is the target unit's handler — a
3610            // test file and the unit it tests are normally different files.
3611            .with_note("handler declared here"),
3612        );
3613        for a in positional {
3614            let _ = type_of(a, None, ctx);
3615        }
3616        return;
3617    }
3618    for (i, (param, arg)) in params.iter().zip(positional.iter()).enumerate() {
3619        record_param_hint(ctx.hints, &param.name.name, arg);
3620        // Slice C: a `Wire(<String>)` argument is *raw* — it deliberately bypasses
3621        // the param's type so a case can drive the boundary with input the type
3622        // forbids. Validate only that the inner is a `String` (the wire form); the
3623        // `system`-only tier rule is enforced at emit time (where the tier is
3624        // known), alongside `system_needs_wire`. Intercept before the generic
3625        // `type_of`, which would otherwise report the address-arg `Wire` as
3626        // misplaced.
3627        if let bynk_syntax::ast::ExprKind::Wire(inner) = &arg.kind {
3628            let _ = type_of(inner, Some(tys.intern(Ty::Base(BaseType::String))), ctx);
3629            continue;
3630        }
3631        let expected = resolve_type_ref(&param.type_ref, &ctx.input.types, tys);
3632        let arg_ty = type_of(arg, expected, ctx);
3633        if let (Some(a), Some(p)) = (arg_ty, expected)
3634            && !compatible(a, p, tys)
3635        {
3636            ctx.errors.push(CompileError::new(
3637                "bynk.types.argument_mismatch",
3638                arg.span,
3639                format!(
3640                    "argument {} has type `{}`, but `{svc}.{addr}` expects `{}` for `{}`",
3641                    i + 1,
3642                    a.display(tys),
3643                    p.display(tys),
3644                    param.name.name
3645                ),
3646            ));
3647        }
3648    }
3649}
3650
3651/// If `receiver` resolves to a consumed-context prefix (an alias or a
3652/// dotted qualified name appearing in `consumes`), return the consumed
3653/// context's qualified name. Otherwise None. Local bindings, types, and
3654/// capabilities take precedence — those are checked at the call site.
3655fn cross_context_prefix(receiver: &Expr, ctx: &Ctx) -> Option<String> {
3656    let info = &ctx.input.cross_context;
3657    if info.consumed_contexts.is_empty() && info.aliases.is_empty() {
3658        return None;
3659    }
3660    // Walk the receiver to assemble a candidate dotted name. Supports:
3661    //   Ident(X)                                 -> "X"
3662    //   FieldAccess { Ident(A), B }              -> "A.B"
3663    //   FieldAccess { FieldAccess { Ident(A), B }, C } -> "A.B.C"
3664    let candidate = flatten_ident_chain(receiver)?;
3665    let head = candidate.split('.').next().unwrap_or("");
3666    // The head must not shadow a local binding / capability / declared type.
3667    if ctx.lookup(head).is_some() {
3668        return None;
3669    }
3670    if ctx.caps.capabilities.contains_key(head) || ctx.caps.declared_capabilities.contains_key(head)
3671    {
3672        return None;
3673    }
3674    // If the head is a known local type, only an alias whose name happens to
3675    // collide could redirect this; aliases conflicting with types are an
3676    // error in project.rs, so a clash here is impossible at this point.
3677    info.resolve_prefix(candidate.as_str())
3678}
3679
3680/// Flatten an `Ident`/`FieldAccess` chain into its dotted name, or None if
3681/// any segment isn't a bare identifier.
3682fn flatten_ident_chain(expr: &Expr) -> Option<String> {
3683    match &expr.kind {
3684        ExprKind::Ident(id) => Some(id.name.clone()),
3685        ExprKind::FieldAccess { receiver, field } => {
3686            let head = flatten_ident_chain(receiver)?;
3687            Some(format!("{head}.{}", field.name))
3688        }
3689        _ => None,
3690    }
3691}
3692
3693/// Type-check a cross-context service call (v0.6 §4.2). `receiver` carries
3694/// the prefix's source span for diagnostics. `consumed` is the resolved
3695/// qualified name of the consumed context.
3696/// v0.15: type-check a cross-context capability call `B.Cap.op(args)` /
3697/// `Alias.Cap.op(args)`. The capability operation signatures are carried in
3698/// `consumed_capabilities` (in the providing context's namespace); the
3699/// capability must be listed in the handler/provider's `given` clause.
3700#[allow(clippy::too_many_arguments)]
3701fn check_cross_context_capability_call(
3702    receiver: &Expr,
3703    consumed: &str,
3704    cap: &str,
3705    method: &Ident,
3706    // #926 (Decision G): explicit type arguments for a generic capability
3707    // operation, qualified form (`B.Cap.op[T](…)` / `Alias.Cap.op[T](…)`).
3708    type_args: &[TypeRef],
3709    args: &[Expr],
3710    _span: Span,
3711    // P6.0 (#1139): the outer `MethodCall` expression's own identity.
3712    expr_id: ExprId,
3713    ctx: &mut Ctx,
3714) -> Option<TyId> {
3715    let tys = ctx.tys;
3716    ctx.callees.insert(
3717        expr_id,
3718        Callee::CrossCap {
3719            unit: consumed.to_string(),
3720            cap: cap.to_string(),
3721            op: method.name.clone(),
3722        },
3723    );
3724    // Capability calls require an effectful body (same rule as local ones).
3725    if !ctx.effectful {
3726        ctx.errors.push(CompileError::new(
3727            "bynk.effect.capability_in_pure_context",
3728            method.span,
3729            format!(
3730                "capability `{consumed}.{cap}` can only be called inside an effectful body (one returning `Effect[T]`)"
3731            ),
3732        ));
3733    }
3734    // The capability must be declared in this handler/provider's `given`.
3735    // The local deps key is the capability's simple name.
3736    if !ctx.caps.given_remaining.contains(cap) {
3737        let mut err = CompileError::new(
3738            "bynk.given.undeclared_capability",
3739            receiver.span,
3740            format!("capability `{consumed}.{cap}` is used but not listed in the `given` clause"),
3741        )
3742        .with_note(format!(
3743            "add `{consumed}.{cap}` to the handler's `given` clause so the dependency surface is visible at the declaration site"
3744        ));
3745        // v0.26 (ADR 0054): the one-click counterpart of the note — the
3746        // clause entry is the qualified form the user writes (`B.Cap`).
3747        if let Some((span, insert)) = given_insertion_edit(
3748            &ctx.caps.given_entries,
3749            ctx.caps.given_anchor,
3750            &format!("{consumed}.{cap}"),
3751        ) {
3752            err = err.with_suggestion(
3753                format!("add `{consumed}.{cap}` to the `given` clause"),
3754                vec![(span, insert)],
3755                Applicability::MachineApplicable,
3756            );
3757        }
3758        ctx.errors.push(err);
3759        for a in args {
3760            let _ = type_of(a, None, ctx);
3761        }
3762        return None;
3763    }
3764    ctx.caps.given_used.insert(cap.to_string());
3765
3766    let info = &ctx.input.cross_context;
3767    let op = info
3768        .consumed_capabilities
3769        .get(consumed)
3770        .and_then(|caps| caps.get(cap))
3771        .and_then(|c| c.ops.iter().find(|o| o.name == method.name))
3772        .cloned();
3773    let Some(op) = op else {
3774        ctx.errors.push(CompileError::new(
3775            "bynk.capability.unknown_operation",
3776            method.span,
3777            format!(
3778                "capability `{consumed}.{cap}` has no operation named `{}`",
3779                method.name
3780            ),
3781        ));
3782        for a in args {
3783            let _ = type_of(a, None, ctx);
3784        }
3785        return None;
3786    };
3787    // v0.36 (ADR 0069, slice 2): a cross-context op call references the op,
3788    // recorded already-qualified into the providing unit (where the op is
3789    // declared), mirroring the cross-context capability reference.
3790    ctx.refs.record_in_unit(
3791        method.span,
3792        SymbolKind::CapabilityOp,
3793        &format!("{cap}.{}", method.name),
3794        consumed,
3795    );
3796    if op.params.len() != args.len() {
3797        ctx.errors.push(CompileError::new(
3798            "bynk.capability.op_arity",
3799            method.span,
3800            format!(
3801                "capability operation `{consumed}.{cap}.{}` expects {} argument(s), but {} were given",
3802                method.name,
3803                op.params.len(),
3804                args.len()
3805            ),
3806        ));
3807        for a in args {
3808            let _ = type_of(a, None, ctx);
3809        }
3810        return None;
3811    }
3812
3813    // Resolve parameter / return types in the consumed context's namespace.
3814    let consumed_types = info
3815        .consumed_types
3816        .get(consumed)
3817        .cloned()
3818        .unwrap_or_default();
3819    // #926: resolve the op's own type parameter(s) — declared in the
3820    // *consumed* context's namespace but instantiated from an explicit type
3821    // argument named in the *calling* context's own namespace (same split
3822    // `resolve_type_ref`/`resolve_expr_type_ref` already draw for
3823    // params/return vs. call-site type args in the local-capability path,
3824    // `check_static_call`). Explicit only, never inferred.
3825    let vars: HashSet<String> = op.type_params.iter().cloned().collect();
3826    let mut subst: HashMap<String, TyId> = HashMap::new();
3827    if !op.type_params.is_empty() || !type_args.is_empty() {
3828        if type_args.is_empty() {
3829            ctx.errors.push(
3830                CompileError::new(
3831                    "bynk.generics.uninferable_type_arg",
3832                    method.span,
3833                    format!(
3834                        "capability operation `{consumed}.{cap}.{}` takes a type parameter, but none of its arguments determine it",
3835                        method.name
3836                    ),
3837                )
3838                .with_note(format!(
3839                    "give it explicitly: `{consumed}.{cap}.{}[T](…)`",
3840                    method.name
3841                )),
3842            );
3843            for a in args {
3844                let _ = type_of(a, None, ctx);
3845            }
3846            return None;
3847        }
3848        if type_args.len() != op.type_params.len() {
3849            ctx.errors.push(CompileError::new(
3850                "bynk.generics.type_arg_mismatch",
3851                method.span,
3852                format!(
3853                    "capability operation `{consumed}.{cap}.{}` takes {} type argument(s), but {} were given",
3854                    method.name,
3855                    op.type_params.len(),
3856                    type_args.len()
3857                ),
3858            ));
3859            for a in args {
3860                let _ = type_of(a, None, ctx);
3861            }
3862            return None;
3863        }
3864        for (tp, ta) in op.type_params.iter().zip(type_args) {
3865            let ty = resolve_expr_type_ref(ta, ctx)?;
3866            subst.insert(tp.clone(), ty);
3867        }
3868    }
3869    let mut all_ok = true;
3870    for (i, ((pname, ptype_ref), arg)) in op.params.iter().zip(args.iter()).enumerate() {
3871        record_param_hint(ctx.hints, pname, arg);
3872        let param_ty = resolve_type_ref_in(ptype_ref, &consumed_types, &vars, tys)
3873            .unwrap_or(tys.intern(Ty::Unit));
3874        let param_ty = substitute(param_ty, &subst, tys);
3875        let Some(arg_ty) = type_of(arg, None, ctx) else {
3876            all_ok = false;
3877            continue;
3878        };
3879        if !structurally_compatible(arg_ty, param_ty, &ctx.input.types, &consumed_types, tys) {
3880            ctx.errors.push(CompileError::new(
3881                "bynk.boundary.structural_mismatch",
3882                arg.span,
3883                format!(
3884                    "cross-context argument {} to `{consumed}.{cap}.{}` has type `{}`, but parameter `{pname}` expects `{}`",
3885                    i + 1,
3886                    method.name,
3887                    arg_ty.display(tys),
3888                    param_ty.display(tys),
3889                ),
3890            ));
3891            all_ok = false;
3892        }
3893    }
3894    if !all_ok {
3895        return None;
3896    }
3897    let raw_ret = resolve_type_ref_in(&op.return_type, &consumed_types, &vars, tys)
3898        .unwrap_or(tys.intern(Ty::Unit));
3899    let raw_ret = substitute(raw_ret, &subst, tys);
3900    Some(rebrand_return_type(raw_ret, &ctx.input.types, tys))
3901}
3902
3903fn check_cross_context_call(
3904    receiver: &Expr,
3905    consumed: &str,
3906    method: &Ident,
3907    args: &[Expr],
3908    _span: Span,
3909    // P6.0 (#1139): the outer `MethodCall` expression's own identity.
3910    expr_id: ExprId,
3911    ctx: &mut Ctx,
3912) -> Option<TyId> {
3913    let tys = ctx.tys;
3914    ctx.callees.insert(
3915        expr_id,
3916        Callee::Cross {
3917            unit: consumed.to_string(),
3918            service: method.name.clone(),
3919        },
3920    );
3921    // The consuming context must be effectful at this call site (services
3922    // and agent handlers are; pure free fns are not).
3923    if !ctx.effectful {
3924        ctx.errors.push(
3925            CompileError::new(
3926                "bynk.effect.cross_context_in_pure_context",
3927                method.span,
3928                format!(
3929                    "cross-context service call `{}.{}` can only be made inside an effectful body (one returning `Effect[T]`)",
3930                    consumed, method.name
3931                ),
3932            )
3933            .with_label(receiver.span, "consumed context prefix"),
3934        );
3935    }
3936    let info = &ctx.input.cross_context;
3937    let Some(svcs) = info.consumed_services.get(consumed) else {
3938        ctx.errors.push(
3939            CompileError::new(
3940                "bynk.consumes.unknown_context",
3941                receiver.span,
3942                format!("context `{consumed}` is not in scope here"),
3943            )
3944            .with_note(
3945                "add a `consumes` clause for the target context at the top of the consuming context",
3946            ),
3947        );
3948        for a in args {
3949            let _ = type_of(a, None, ctx);
3950        }
3951        return None;
3952    };
3953    let Some(service) = svcs.get(&method.name).cloned() else {
3954        ctx.errors.push(
3955            CompileError::new(
3956                "bynk.consumes.unknown_service",
3957                method.span,
3958                format!(
3959                    "context `{consumed}` has no service named `{}`",
3960                    method.name
3961                ),
3962            )
3963            .with_note(
3964                "cross-context calls require an `on call` service handler in the consumed context",
3965            ),
3966        );
3967        for a in args {
3968            let _ = type_of(a, None, ctx);
3969        }
3970        return None;
3971    };
3972    ctx.refs
3973        .record_in_unit(method.span, SymbolKind::Service, &method.name, consumed);
3974
3975    if service.params.len() != args.len() {
3976        ctx.errors.push(
3977            CompileError::new(
3978                "bynk.consumes.service_arity",
3979                method.span,
3980                format!(
3981                    "cross-context service `{consumed}.{}` expects {} argument(s), but {} were given",
3982                    method.name,
3983                    service.params.len(),
3984                    args.len()
3985                ),
3986            )
3987            // Finding #46: `service` belongs to the *consumed*
3988            // context — a different unit/file than this call, almost
3989            // always. A label would risk underlining unrelated text
3990            // there; a note carries the information safely (per-label
3991            // file identity is a Wave 8 follow-up).
3992            .with_note("service declared here"),
3993        );
3994        for a in args {
3995            let _ = type_of(a, None, ctx);
3996        }
3997        return None;
3998    }
3999
4000    // Resolve the consumed-context types so we can describe parameter shapes.
4001    let consumed_types = info
4002        .consumed_types
4003        .get(consumed)
4004        .cloned()
4005        .unwrap_or_default();
4006
4007    // Walk each argument, checking structural compatibility (Phase 4).
4008    let mut all_ok = true;
4009    for (i, ((pname, ptype_ref), arg)) in service.params.iter().zip(args.iter()).enumerate() {
4010        record_param_hint(ctx.hints, pname, arg);
4011        let param_ty =
4012            resolve_type_ref(ptype_ref, &consumed_types, tys).unwrap_or(tys.intern(Ty::Unit));
4013        // Type-check the argument in the caller's context.
4014        let arg_ty = type_of(arg, None, ctx);
4015        let Some(arg_ty) = arg_ty else {
4016            all_ok = false;
4017            continue;
4018        };
4019        if !structurally_compatible(arg_ty, param_ty, &ctx.input.types, &consumed_types, tys) {
4020            ctx.errors.push(
4021                CompileError::new(
4022                    "bynk.boundary.structural_mismatch",
4023                    arg.span,
4024                    format!(
4025                        "cross-context argument {} to `{consumed}.{}` has type `{}` in `{}`, but parameter `{pname}` expects `{}` in `{}`",
4026                        i + 1,
4027                        method.name,
4028                        arg_ty.display(tys),
4029                        ctx.input
4030                            .cross_context
4031                            .self_context
4032                            .as_deref()
4033                            .unwrap_or("?"),
4034                        param_ty.display(tys),
4035                        consumed,
4036                    ),
4037                )
4038                // Finding #46: same cross-unit provenance as above.
4039                .with_note("service declared here")
4040                .with_note(
4041                    "values crossing a context boundary must have structurally compatible types (same commons-derived type, or identical record/sum shape)",
4042                ),
4043            );
4044            all_ok = false;
4045        }
4046    }
4047    if !all_ok {
4048        return None;
4049    }
4050
4051    // Return type rebrand: project the consumed context's return type into
4052    // the calling context's namespace by renaming named types whose unqualified
4053    // name appears in the caller's type table (v0.6 §4.5).
4054    let raw_ret = resolve_type_ref(&service.return_type, &consumed_types, tys)
4055        .unwrap_or(tys.intern(Ty::Unit));
4056    let rebranded = rebrand_return_type(raw_ret, &ctx.input.types, tys);
4057    Some(rebranded)
4058}