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