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        admitted_literals: HashMap::new(),
153        pattern_binding_types: HashMap::new(),
154        return_ty,
155        return_ty_span: f.return_type.span(),
156        effectful,
157        agent_state_ty: None,
158        commit_seen: false,
159        caps: CapabilityCtx::default(),
160        in_test_body: false,
161        oidc_binder: None,
162        test_services: HashMap::new(),
163        test_actors: HashMap::new(),
164        type_vars: vars.clone(),
165        store_fields: HashMap::new(),
166        in_agent_handler: false,
167    };
168    let Some(body_ty) = type_of_block(&f.body, Some(return_ty), &mut ctx) else {
169        return;
170    };
171    // #718: run the held-resource linearity pass over `fn`/method bodies too, not
172    // just handlers. The caller side treats passing a held value into a function
173    // as a transfer (disposal), so the callee *owns* any held parameter and must
174    // dispose it (close, store, or transfer) before returning — otherwise a
175    // `swallow(c)` leaks and a double `c.close()` goes undiagnosed. No parameter
176    // is borrowed here (the borrowed case is a handler's firing connection), so
177    // the borrowed set is empty; every held param is seeded owned.
178    linearity::check(
179        &f.body,
180        &f.params,
181        &input.types,
182        ctx.expr_types,
183        &ctx.pattern_binding_types,
184        &HashSet::new(),
185        ctx.errors,
186        tys,
187    );
188    if !compatible(body_ty, return_ty, tys) {
189        ctx.errors.push(
190            CompileError::new(
191                "bynk.types.return_mismatch",
192                f.body.tail.span,
193                format!(
194                    "function body has type `{}`, but the declared return type is `{}`",
195                    body_ty.display(tys),
196                    return_ty.display(tys)
197                ),
198            )
199            .with_label(f.return_type.span(), "declared return type"),
200        );
201    }
202}
203
204/// The shared discipline behind both `check_state_initialiser` (agent `store`
205/// fields) and `check_event_field_default` (Events slice 3a, #972, event
206/// record fields): the initialiser must be a *static* value of the field
207/// type — checked in an empty, pure scope (so `self`, parameters,
208/// capabilities, and effects are all out of reach) with the field type as
209/// the expected type, so refined literals admit (v0.9.4) and sum variants
210/// resolve. The init's expression types are recorded into `expr_types` for
211/// emission, and its `Callee` classification (a `.of`/`.unsafe`/variant-
212/// constructor call, at most) into `callees` (P6.7, #1163) — every
213/// `Callee`-driven reader of a call-shaped expression reads `program.callees`
214/// unconditionally (ADR 0334), so a call-shaped static initialiser with no
215/// entry there would panic on a certified program the checker legitimately
216/// accepted, not a recoverable state. (The IR-side `Cell`-init lowering that
217/// first motivated this was deleted by Slice D1 of #1542; the invariant
218/// outlives it because `bynk-emit`'s own emitter reads the same table.)
219/// `code`/`subject`
220/// name the caller's own diagnostic and the noun used in its message ("state
221/// field initialiser" / "event field default").
222#[allow(clippy::too_many_arguments)]
223fn check_static_initialiser(
224    init: &Expr,
225    field_type: &TypeRef,
226    input: &ResolvedCommons,
227    expr_types: &mut HashMap<ExprId, TypedExpr>,
228    callees: &mut HashMap<ExprId, Callee>,
229    errors: &mut Vec<CompileError>,
230    refs: &mut RefSink,
231    hints: &mut HintSink,
232    locals: &mut LocalsSink,
233    code: &'static str,
234    subject: &str,
235    tys: &Types,
236) {
237    let Some(field_ty) = resolve_type_ref(field_type, &input.types, tys) else {
238        return; // an unresolved field type is reported elsewhere
239    };
240    let mut local_errors: Vec<CompileError> = Vec::new();
241    // A static initialiser is a pure value — no capability calls reach here —
242    // so requirements are discarded into a throwaway sink.
243    let mut init_requirements = RequirementSink::new();
244    let result = {
245        let mut ctx = Ctx {
246            input,
247            tys,
248            expr_types,
249            errors: &mut local_errors,
250            refs,
251            hints,
252            locals,
253            requirements: &mut init_requirements,
254            callees,
255            scopes: vec![HashMap::new()],
256            is_binding_cache: HashMap::new(),
257            admitted_literals: HashMap::new(),
258            pattern_binding_types: HashMap::new(),
259            return_ty: field_ty,
260            return_ty_span: init.span,
261            effectful: false,
262            agent_state_ty: None,
263            commit_seen: false,
264            caps: CapabilityCtx::default(),
265            in_test_body: false,
266            oidc_binder: None,
267            test_services: HashMap::new(),
268            test_actors: HashMap::new(),
269            type_vars: HashSet::new(),
270            store_fields: HashMap::new(),
271            in_agent_handler: false,
272        };
273        type_of(init, Some(field_ty), &mut ctx)
274    };
275    let compatible_result = matches!(&result, Some(t) if compatible(*t, field_ty, tys));
276    if !compatible_result || !local_errors.is_empty() {
277        let got = result
278            .map(|t| t.display(tys))
279            .unwrap_or_else(|| "an invalid expression".to_string());
280        errors.push(
281            CompileError::new(
282                code,
283                init.span,
284                format!(
285                    "{subject} must be a static value of type `{}` (got `{got}`)",
286                    field_ty.display(tys),
287                ),
288            )
289            .with_note(
290                "an initialiser is a compile-time value — a literal (including one admitted to a \
291                 refined type), a sum variant, `Some`/`None`/`Ok`/`Err`, a record, or — for an \
292                 opaque type — `T.unsafe(lit)` — with no reference to `self`, parameters, or \
293                 capabilities",
294            ),
295        );
296    }
297}
298
299/// v0.11: type-check an agent state-field initialiser (`field: T = init`).
300/// See `check_static_initialiser`. Pushes `bynk.agents.bad_state_initialiser`.
301#[allow(clippy::too_many_arguments)]
302pub fn check_state_initialiser(
303    init: &Expr,
304    field_type: &TypeRef,
305    input: &ResolvedCommons,
306    tys: &Types,
307    expr_types: &mut HashMap<ExprId, TypedExpr>,
308    callees: &mut HashMap<ExprId, Callee>,
309    errors: &mut Vec<CompileError>,
310    refs: &mut RefSink,
311    hints: &mut HintSink,
312    locals: &mut LocalsSink,
313) {
314    check_static_initialiser(
315        init,
316        field_type,
317        input,
318        expr_types,
319        callees,
320        errors,
321        refs,
322        hints,
323        locals,
324        "bynk.agents.bad_state_initialiser",
325        "state field initialiser",
326        tys,
327    );
328}
329
330/// Events slice 3a (#972): type-check an event field's default expression
331/// (`field: T = init`), reusing `check_static_initialiser`'s empty-pure-scope
332/// discipline. Pushes `bynk.event.bad_field_default`.
333///
334/// One admission `check_static_initialiser` doesn't cover on its own: an
335/// opaque type's `T.unsafe(lit)` is, by design (ADR 0182), a bypass of its
336/// own refinement — `type_of`'s ordinary static-call handling only
337/// checks `lit`'s *base* type, not the refinement, since that's the whole
338/// point of `unsafe`. An event field default is different: it becomes part
339/// of the wire codec (slice 3a lowers it to its *wire* JSON form and splices
340/// it into `deserialise_<Event>`, which validates a defaulted value exactly
341/// like a real one), so a default that bypasses its own refinement would
342/// compile cleanly and then fail at runtime the first time an old event
343/// actually triggers it — a deferred, surprising failure this check closes
344/// statically instead. Only `T.unsafe(lit)` on a *literal* argument is
345/// checked here (anything else already fails the static-value requirement
346/// above); a violated refinement pushes the same `bynk.event.bad_field_default`.
347#[allow(clippy::too_many_arguments)]
348pub fn check_event_field_default(
349    init: &Expr,
350    field_type: &TypeRef,
351    input: &ResolvedCommons,
352    tys: &Types,
353    expr_types: &mut HashMap<ExprId, TypedExpr>,
354    callees: &mut HashMap<ExprId, Callee>,
355    errors: &mut Vec<CompileError>,
356    refs: &mut RefSink,
357    hints: &mut HintSink,
358    locals: &mut LocalsSink,
359) {
360    check_static_initialiser(
361        init,
362        field_type,
363        input,
364        expr_types,
365        callees,
366        errors,
367        refs,
368        hints,
369        locals,
370        "bynk.event.bad_field_default",
371        "event field default",
372        tys,
373    );
374    // `T.unsafe(lit)` parses as `ExprKind::MethodCall { receiver: Ident(T),
375    // method: "unsafe", .. }` — confirmed by direct AST inspection; the
376    // parser never distinguishes a type-qualified call from an ordinary
377    // instance method call (that's a resolver-time decision).
378    if let ExprKind::MethodCall {
379        receiver,
380        method,
381        args,
382        ..
383    } = &init.kind
384        && let ExprKind::Ident(type_name) = &receiver.kind
385        && method.name == "unsafe"
386        && let [lit_expr] = args.as_slice()
387        && let Some(decl) = input.types.get(&type_name.name)
388        && matches!(decl.body, TypeBody::Opaque { .. })
389        && let Some(refinement) = refinements::type_decl_refinement(decl)
390        && let Some(lit) = refinements::const_literal(lit_expr)
391        && let Some(failed) = refinements::first_failed_predicate(refinement, &lit)
392    {
393        errors.push(
394            CompileError::new(
395                "bynk.event.bad_field_default",
396                lit_expr.span,
397                format!(
398                    "`{}.unsafe(...)` bypasses its own refinement, but an event field default \
399                     must be a value the wire could actually carry — this literal fails `{}`",
400                    type_name.name,
401                    failed.name(),
402                ),
403            )
404            .with_note(
405                "a default is spliced into the same codec that validates a real wire value on \
406                 receipt, so a refinement-violating `.unsafe(lit)` default would compile cleanly \
407                 and then fail at runtime the first time an old event actually triggers it",
408            ),
409        );
410    }
411}
412
413/// Standard-library S3 (#1963): refuse a use of the experimental first-party
414/// item `path` at `span` unless the project's `[unstable] allow` covers it.
415fn refuse_experimental(path: &str, span: Span, ctx: &mut Ctx) {
416    if let Some(err) = ctx.input.stability.policy.check(path, span) {
417        ctx.errors.push(err);
418    }
419}
420
421/// S3 (#1963): a reference to an imported first-party name the project's policy
422/// blocks, called or passed as a value. The unit's blocked imports were decided where its provenance is
423/// known, so this holds in handler bodies too.
424pub(crate) fn refuse_experimental_import(name: &Ident, ctx: &mut Ctx) {
425    if let Some(path) = ctx.input.stability.blocked_imports.get(&name.name).cloned() {
426        refuse_experimental(&path, name.span, ctx);
427    }
428}
429
430/// v0.91 (ADR 0116 D6): the `bynk.list` free functions whose method forms
431/// shipped with slice 1 are **deprecated** in favour of the method-chain
432/// vocabulary. `reverse`/`traverse` stay (no method form yet). Emits a
433/// non-failing warning (ADR 0117) with a machine-applicable rewrite to the
434/// method form — `map(xs, f)` → `xs.map(f)`, `find(xs, p)` → `xs.filter(p).first()`.
435fn warn_bynk_list_deprecation(name: &Ident, args: &[Expr], call_span: Span, ctx: &mut Ctx) {
436    if ctx.input.imported_from.get(&name.name).map(String::as_str) != Some("bynk.list") {
437        return;
438    }
439    // The method-form spelling each free function rewrites to.
440    let method_form: &str = match name.name.as_str() {
441        "map" => "xs.map(f)",
442        "filter" => "xs.filter(p)",
443        "any" => "xs.any(p)",
444        "all" => "xs.all(p)",
445        "find" => "xs.filter(p).first()",
446        _ => return, // reverse / traverse keep their free-function form for now
447    };
448    let mut err = CompileError::new(
449        "bynk.list.deprecated_function",
450        name.span,
451        format!(
452            "`bynk.list.{}` is deprecated — use the `List` method form `{method_form}`",
453            name.name
454        ),
455    )
456    .with_note(
457        "the `bynk.list.*` free functions are superseded by the method-chain vocabulary (ADR 0116); the method form reads left-to-right and chains",
458    );
459    // The auto-fix needs the (list, fn) shape — a wrong-arity call is reported
460    // elsewhere; only offer the rewrite when it is well-formed.
461    if args.len() == 2 {
462        let mut edits = vec![
463            // delete `name(` — the receiver becomes the first argument.
464            (
465                Span::new(name.span.start, args[0].span.start),
466                String::new(),
467            ),
468            // the `, ` between the two args becomes `.<method>(`.
469            (
470                Span::new(args[0].span.end, args[1].span.start),
471                format!(
472                    ".{}(",
473                    if name.name == "find" {
474                        "filter"
475                    } else {
476                        &name.name
477                    }
478                ),
479            ),
480        ];
481        if name.name == "find" {
482            // `filter(p)` then `.first()` after the closing `)`.
483            edits.push((
484                Span::new(call_span.end, call_span.end),
485                ".first()".to_string(),
486            ));
487        }
488        err = err.with_suggestion(
489            format!("rewrite to the `List` method form `{method_form}`"),
490            edits,
491            Applicability::MachineApplicable,
492        );
493    }
494    ctx.errors.push(err);
495}
496
497pub(crate) fn check_call(
498    name: &Ident,
499    type_args: &[TypeRef],
500    args: &[Expr],
501    span: Span,
502    // #593: the binding's expected type grounds a generic variant constructor
503    // whose payload cannot determine every parameter (`let o: Opt[Int] = Nil`).
504    expected: Option<TyId>,
505    // P6.0 (#1139): the outer `Call` expression's own identity — keys the
506    // `Callee` classification recorded at whichever branch below dispatches.
507    expr_id: ExprId,
508    ctx: &mut Ctx,
509) -> Option<TyId> {
510    let tys = ctx.tys;
511    if let Some(fn_decl) = ctx.input.fns.get(&name.name) {
512        ctx.refs.record(name.span, SymbolKind::Fn, &name.name);
513        ctx.callees.insert(expr_id, Callee::Fn(Arc::clone(fn_decl)));
514        warn_bynk_list_deprecation(name, args, span, ctx);
515        refuse_experimental_import(name, ctx);
516        return check_call_against_fn(name, fn_decl, type_args, args, ctx);
517    }
518    // v0.20a: explicit type arguments only apply to (generic) functions.
519    if !type_args.is_empty() {
520        ctx.errors.push(CompileError::new(
521            "bynk.generics.type_arg_mismatch",
522            span,
523            format!(
524                "`{}` is not a generic function — it takes no type arguments",
525                name.name
526            ),
527        ));
528        for a in args {
529            let _ = type_of(a, None, ctx);
530        }
531        return None;
532    }
533    // Could be a bare variant constructor with payload. Borrow the matching
534    // sum decls' own `Arc`s (a pointer bump each, no full-body clones); the
535    // ambiguity check below reuses the same list.
536    let owners: Vec<&Arc<TypeDecl>> = ctx
537        .input
538        .types
539        .values()
540        .filter(|t| matches!(&t.body, TypeBody::Sum(s) if s.variants.iter().any(|v| v.name.name == name.name)))
541        .collect();
542    if owners.len() == 1 {
543        ctx.callees.insert(
544            expr_id,
545            Callee::Ctor {
546                sum: Arc::clone(owners[0]),
547                tag: name.name.clone(),
548            },
549        );
550        return check_variant_construction(owners[0], &name.name, args, span, expected, ctx);
551    }
552    // Agent instantiation: `AgentName(key)` constructs an instance keyed by
553    // `key`. The result type carries the agent's name so subsequent
554    // `agent_instance.method(args)` lookups can find the agent's handler set.
555    if let Some(agent) = ctx.input.agents.get(&name.name).cloned() {
556        ctx.refs.record(name.span, SymbolKind::Agent, &name.name);
557        ctx.callees
558            .insert(expr_id, Callee::AgentInit(name.name.clone()));
559        let key_ty = resolve_type_ref(&agent.key_type, &ctx.input.types, tys);
560        if args.len() != 1 {
561            let mut err = CompileError::new(
562                "bynk.agent.construction_arity",
563                span,
564                format!(
565                    "agent `{}` is constructed with one key argument, but {} were given",
566                    name.name,
567                    args.len()
568                ),
569            );
570            // #1925: the agent hides a `uses`-imported value of its name,
571            // which may be what the call meant.
572            if let Some(s) = ctx.input.shadowed_imports.get(&name.name) {
573                err = err.with_note(format!(
574                    "this unit's agent `{}` hides the {} of that name from `{}`; rename one to use the {}",
575                    name.name, s.imported, s.unit, s.imported
576                ));
577            }
578            ctx.errors.push(err);
579            for a in args {
580                let _ = type_of(a, None, ctx);
581            }
582            return None;
583        }
584        let arg_ty = type_of(&args[0], key_ty, ctx);
585        if let (Some(a), Some(k)) = (arg_ty, key_ty)
586            && !compatible(a, k, tys)
587        {
588            ctx.errors.push(CompileError::new(
589                "bynk.agent.key_mismatch",
590                args[0].span,
591                format!(
592                    "agent `{}` key is `{}`, but a value of type `{}` was given",
593                    name.name,
594                    k.display(tys),
595                    a.display(tys)
596                ),
597            ));
598        }
599        return Some(tys.intern(Ty::Named {
600            name: name.name.clone(),
601            kind: NamedKind::Record,
602            args: Vec::new(),
603        }));
604    }
605    // v0.20a: value application — calling a scope binding (param/local) of
606    // function type. Placed AFTER fns/variants/agents: putting scope first
607    // would change the meaning of currently-passing programs (the additive
608    // guard); the resulting ident/call precedence asymmetry is pre-existing
609    // and documented in §5.
610    if let Some(ty) = ctx.lookup(&name.name) {
611        return match &*tys.get(ty) {
612            Ty::Fn { params, ret } => {
613                ctx.callees
614                    .insert(expr_id, Callee::Value(name.name.clone()));
615                check_value_application(name, params, *ret, args, span, ctx)
616            }
617            _ => {
618                // Relocated from the resolver (which has no type info): a
619                // non-function-typed value called as a function.
620                ctx.errors.push(
621                    CompileError::new(
622                        "bynk.resolve.param_as_function",
623                        span,
624                        format!(
625                            "`{}` has type `{}` and is not callable",
626                            name.name,
627                            ty.display(tys)
628                        ),
629                    )
630                    .with_note("only values of function type can be applied"),
631                );
632                for a in args {
633                    let _ = type_of(a, None, ctx);
634                }
635                None
636            }
637        };
638    }
639    // Nothing owns the call. The resolver's reference walk reports these in
640    // `fn`/method bodies, but handler/service/agent bodies and test bodies
641    // never pass through that walk — the checker is their only backstop, and
642    // a silent `None` here admitted any unknown call and emitted it verbatim.
643    // Mirror the resolver's ladder. #1708: test bodies were exempt, for the
644    // same since-repaired reason as `check_ident`'s twin gate; dropped with it.
645    for a in args {
646        let _ = type_of(a, None, ctx);
647    }
648    if owners.len() > 1 {
649        ctx.errors.push(CompileError::new(
650            "bynk.resolve.ambiguous_variant",
651            name.span,
652            format!(
653                "the variant name `{}` is declared on multiple sum types — qualify it as `TypeName.{}(...)`",
654                name.name, name.name
655            ),
656        ));
657        return None;
658    }
659    // #1925: no local function or agent answers the call, and a local
660    // declaration of another kind hides the import.
661    if let Some(err) = ctx.input.shadowed_import_error(&name.name, name.span) {
662        ctx.errors.push(err);
663        return None;
664    }
665    if ctx.input.types.contains_key(&name.name) {
666        ctx.errors.push(CompileError::new(
667            "bynk.resolve.type_as_function",
668            span,
669            format!(
670                "`{}` is a type, not a function — use `{}.of(value)` or `{} {{ ... }}` instead",
671                name.name, name.name, name.name
672            ),
673        ));
674        return None;
675    }
676    ctx.errors.push(
677        CompileError::new(
678            "bynk.resolve.unknown_function",
679            span,
680            format!("unknown function `{}`", name.name),
681        )
682        .with_note("only functions declared in this commons are callable"),
683    );
684    None
685}
686
687fn check_value_application(
688    name: &Ident,
689    params: &[TyId],
690    ret: TyId,
691    args: &[Expr],
692    span: Span,
693    ctx: &mut Ctx,
694) -> Option<TyId> {
695    let tys = ctx.tys;
696    if ret.is_effect(tys) && !ctx.effectful {
697        ctx.errors.push(
698            CompileError::new(
699                "bynk.effect.fn_value_in_pure_context",
700                span,
701                format!(
702                    "`{}` is an effectful function (`{}`) and cannot be called in a pure context",
703                    name.name,
704                    Ty::Fn {
705                        params: params.to_vec(),
706                        ret,
707                    }
708                    .display(tys)
709                ),
710            )
711            .with_note(
712                "effectful function values may only be called where the enclosing body is effectful (its return type is an Effect)",
713            ),
714        );
715    }
716    if params.len() != args.len() {
717        ctx.errors.push(CompileError::new(
718            "bynk.types.call_arity",
719            span,
720            format!(
721                "`{}` takes {} argument(s), but {} were given",
722                name.name,
723                params.len(),
724                args.len()
725            ),
726        ));
727        for a in args {
728            let _ = type_of(a, None, ctx);
729        }
730        return None;
731    }
732    for (arg, param_ty) in args.iter().zip(params) {
733        let arg_ty = type_of(arg, Some(*param_ty), ctx);
734        if let Some(a) = arg_ty
735            && !compatible(a, *param_ty, tys)
736        {
737            ctx.errors.push(CompileError::new(
738                "bynk.types.argument_mismatch",
739                arg.span,
740                format!(
741                    "argument has type `{}`, but `{}` expects `{}`",
742                    a.display(tys),
743                    name.name,
744                    param_ty.display(tys)
745                ),
746            ));
747        }
748    }
749    Some(ret)
750}
751
752/// v0.20a: instantiate and check a call to a generic function. Two-pass
753/// argument-directed inference: pass 1 types every **non-lambda** argument
754/// left-to-right against the (possibly still Var-bearing) expected — safe
755/// because every expected-driven feature in `type_of` matches concrete
756/// variants, so a Var falls through benignly (pinned by a unit test) — and
757/// unifies; pass 2 types **lambda** arguments against the now-substituted
758/// expecteds (a lambda whose expected params are still Var-bearing is
759/// uninferable) and unifies the result, capturing return-position variables.
760/// Conflicts demand exact equality (`bynk.generics.type_arg_mismatch`); the
761/// explicit `name[T](…)` form builds the substitution directly.
762fn check_generic_call(
763    name: &Ident,
764    fn_decl: &FnDecl,
765    type_args: &[TypeRef],
766    args: &[Expr],
767    ctx: &mut Ctx,
768) -> Option<TyId> {
769    let tys = ctx.tys;
770    let vars: HashSet<String> = fn_decl
771        .type_params
772        .iter()
773        .map(|tp| tp.name.name.clone())
774        .collect();
775    if fn_decl.params.len() != args.len() {
776        // Same backstop as the non-generic path: the resolver only covers
777        // `fn`/method bodies, so the checker must report for handler bodies.
778        ctx.errors.push(
779            CompileError::new(
780                "bynk.resolve.arity_mismatch",
781                name.span,
782                format!(
783                    "function `{}` expects {} argument(s), but {} were given",
784                    name.name,
785                    fn_decl.params.len(),
786                    args.len()
787                ),
788            )
789            .with_label(fn_decl.name.ident().span, "function declared here"),
790        );
791        for a in args {
792            let _ = type_of(a, None, ctx);
793        }
794        return None;
795    }
796    let var_params: Vec<Option<TyId>> = fn_decl
797        .params
798        .iter()
799        .map(|p| resolve_type_ref_in(&p.type_ref, &ctx.input.types, &vars, tys))
800        .collect();
801    let ret_pattern = resolve_type_ref_in(&fn_decl.return_type, &ctx.input.types, &vars, tys)?;
802
803    let mut subst: HashMap<String, TyId> = HashMap::new();
804    if !type_args.is_empty() {
805        if type_args.len() != fn_decl.type_params.len() {
806            ctx.errors.push(CompileError::new(
807                "bynk.generics.type_arg_mismatch",
808                name.span,
809                format!(
810                    "`{}` takes {} type argument(s), but {} were given",
811                    name.name,
812                    fn_decl.type_params.len(),
813                    type_args.len()
814                ),
815            ));
816            return None;
817        }
818        for (tp, ta) in fn_decl.type_params.iter().zip(type_args) {
819            // Resolve explicit type args with the *enclosing* fn's type
820            // params in scope, so `identity[A](x)` inside a generic body
821            // works. #712: report (not silently drop) an unknown type arg —
822            // the resolver never validates `Call::type_args` (it destructures
823            // `Call { name, args, .. }`), so this is the only guard.
824            let ty = resolve_expr_type_ref(ta, ctx)?;
825            subst.insert(tp.name.name.clone(), ty);
826        }
827    }
828
829    let mut arg_tys: Vec<Option<TyId>> = vec![None; args.len()];
830    // Pass 1 — non-lambda arguments.
831    for (i, arg) in args.iter().enumerate() {
832        if matches!(arg.kind, ExprKind::Lambda(_)) {
833            continue;
834        }
835        let expected = var_params[i].map(|p| substitute(p, &subst, tys));
836        let ty = type_of(arg, expected, ctx);
837        if let (Some(pattern), Some(actual)) = (var_params[i], ty)
838            && !unify(pattern, actual, &mut subst, tys)
839        {
840            ctx.errors.push(CompileError::new(
841                "bynk.generics.type_arg_mismatch",
842                arg.span,
843                format!(
844                    "argument {} infers a type for `{}`'s type parameter that conflicts with an earlier argument — annotate with `{}[T](…)`",
845                    i + 1,
846                    name.name,
847                    name.name
848                ),
849            ));
850            return None;
851        }
852        arg_tys[i] = ty;
853    }
854    // Pass 2 — lambda arguments, against substituted expecteds.
855    for (i, arg) in args.iter().enumerate() {
856        if !matches!(arg.kind, ExprKind::Lambda(_)) {
857            continue;
858        }
859        let expected = var_params[i].map(|p| substitute(p, &subst, tys));
860        let params_unconstrained = expected.is_some_and(|e| {
861            matches!(&*tys.get(e), Ty::Fn { params, .. }
862                if params.iter().any(|p| contains_var(*p, tys)))
863        });
864        let fully_annotated = matches!(
865            &arg.kind,
866            ExprKind::Lambda(l) if l.params.iter().all(|p| p.type_ref.is_some())
867        );
868        if params_unconstrained && !fully_annotated {
869            ctx.errors.push(
870                CompileError::new(
871                    "bynk.generics.uninferable_type_arg",
872                    arg.span,
873                    format!(
874                        "the lambda's parameter types depend on `{}`'s type parameters, which the other arguments do not determine",
875                        name.name
876                    ),
877                )
878                .with_note("annotate the lambda's parameters, or give explicit type arguments: `name[T](…)`"),
879            );
880            return None;
881        }
882        // A fully-annotated lambda grounds the variables itself: type it
883        // bottom-up and let unify capture them.
884        let ty = if params_unconstrained {
885            type_of(arg, None, ctx)
886        } else {
887            type_of(arg, expected, ctx)
888        };
889        if let (Some(pattern), Some(actual)) = (var_params[i], ty)
890            && !unify(pattern, actual, &mut subst, tys)
891        {
892            ctx.errors.push(CompileError::new(
893                "bynk.generics.type_arg_mismatch",
894                arg.span,
895                format!(
896                    "the lambda's type conflicts with `{}`'s inferred type arguments",
897                    name.name
898                ),
899            ));
900            return None;
901        }
902        arg_tys[i] = ty;
903    }
904    // Every type parameter must now be determined.
905    for tp in &fn_decl.type_params {
906        if !subst.contains_key(&tp.name.name) {
907            ctx.errors.push(
908                CompileError::new(
909                    "bynk.generics.uninferable_type_arg",
910                    name.span,
911                    format!(
912                        "type parameter `{}` of `{}` is neither inferable from the arguments nor given explicitly",
913                        tp.name.name, name.name
914                    ),
915                )
916                .with_label(tp.span, "declared here")
917                .with_note("give explicit type arguments: `name[T](…)`"),
918            );
919            return None;
920        }
921    }
922    // Final compatibility over the fully-ground parameter types.
923    let mut ok = true;
924    for (i, (pattern, arg)) in var_params.iter().zip(args).enumerate() {
925        record_param_hint(ctx.hints, &fn_decl.params[i].name.name, arg);
926        let (Some(pattern), Some(arg_ty)) = (pattern, arg_tys[i].as_ref()) else {
927            continue;
928        };
929        let ground = substitute(*pattern, &subst, tys);
930        if !compatible(*arg_ty, ground, tys) {
931            ctx.errors.push(CompileError::new(
932                "bynk.types.argument_mismatch",
933                arg.span,
934                format!(
935                    "argument {} to `{}` has type `{}`, but `{}` is expected",
936                    i + 1,
937                    name.name,
938                    arg_ty.display(tys),
939                    ground.display(tys)
940                ),
941            ));
942            ok = false;
943        }
944    }
945    if !ok {
946        return None;
947    }
948    // #1688: a type parameter the callee compares (`==` on it, directly or
949    // through another generic call) must be instantiated with an
950    // equality-supporting type.
951    let compared = match ctx.input.fns.get(&name.name) {
952        Some(decl) if std::ptr::eq(decl.as_ref(), fn_decl) => {
953            super::equality::compared_type_params(decl, ctx)
954        }
955        _ => Default::default(),
956    };
957    let params: Vec<(String, Option<Span>)> = fn_decl
958        .type_params
959        .iter()
960        .map(|tp| (tp.name.name.clone(), Some(tp.span)))
961        .collect();
962    if !super::equality::check_compared_args(&name.name, name.span, &params, &compared, &subst, ctx)
963    {
964        return None;
965    }
966    // v0.39 (ADR 0072): when the user omitted the type arguments, show the
967    // inferred ones as a `Type`-kind hint after the function name —
968    // `identity` ⟨`[Int]`⟩ `(5)`. Declaration order; skipped if any var stayed
969    // unresolved (defensive — the arg loop above already grounds them).
970    if type_args.is_empty() && !fn_decl.type_params.is_empty() {
971        let rendered: Option<Vec<String>> = fn_decl
972            .type_params
973            .iter()
974            .map(|tp| subst.get(&tp.name.name).map(|t| t.display(tys)))
975            .collect();
976        if let Some(parts) = rendered {
977            ctx.hints
978                .record(name.span, format!("[{}]", parts.join(", ")));
979        }
980    }
981    let ret = substitute(ret_pattern, &subst, tys);
982    // v0.20b: the return is ground *up to the caller's rigid type
983    // parameters* — a generic fn calling another generic fn (bynk.list's
984    // `map` calling `reverse`) legitimately instantiates the callee at its
985    // own rigid vars, which flow through `compatible` by name-equality.
986    Some(ret)
987}
988
989fn check_call_against_fn(
990    name: &Ident,
991    fn_decl: &FnDecl,
992    type_args: &[TypeRef],
993    args: &[Expr],
994    ctx: &mut Ctx,
995) -> Option<TyId> {
996    let tys = ctx.tys;
997    // v0.20a: generic functions take the instantiation path; the
998    // non-generic path below runs byte-identically to v0.19 (the additive
999    // guard). Explicit type args on a non-generic fn are rejected.
1000    if !fn_decl.type_params.is_empty() {
1001        return check_generic_call(name, fn_decl, type_args, args, ctx);
1002    }
1003    if !type_args.is_empty() {
1004        ctx.errors.push(CompileError::new(
1005            "bynk.generics.type_arg_mismatch",
1006            name.span,
1007            format!(
1008                "`{}` is not a generic function — it takes no type arguments",
1009                name.name
1010            ),
1011        ));
1012        for a in args {
1013            let _ = type_of(a, None, ctx);
1014        }
1015        return None;
1016    }
1017    if fn_decl.params.len() != args.len() {
1018        // The resolver reports this in `fn`/method bodies; handler/service/
1019        // agent/test bodies only reach the checker, so report here too (a
1020        // resolve error stops the pipeline first, so this cannot double up).
1021        ctx.errors.push(
1022            CompileError::new(
1023                "bynk.resolve.arity_mismatch",
1024                name.span,
1025                format!(
1026                    "function `{}` expects {} argument(s), but {} were given",
1027                    name.name,
1028                    fn_decl.params.len(),
1029                    args.len()
1030                ),
1031            )
1032            .with_label(fn_decl.name.ident().span, "function declared here"),
1033        );
1034        for a in args {
1035            let _ = type_of(a, None, ctx);
1036        }
1037        return None;
1038    }
1039    let resolved_params: Vec<(Option<TyId>, &Param)> = fn_decl
1040        .params
1041        .iter()
1042        .map(|p| (resolve_type_ref(&p.type_ref, &ctx.input.types, tys), p))
1043        .collect();
1044    let mut ok = true;
1045    for (i, ((param_ty, param), arg)) in resolved_params.iter().zip(args.iter()).enumerate() {
1046        record_param_hint(ctx.hints, &param.name.name, arg);
1047        let arg_ty = type_of(arg, *param_ty, ctx);
1048        let (Some(arg_ty), Some(param_ty)) = (arg_ty, *param_ty) else {
1049            ok = false;
1050            continue;
1051        };
1052        if !compatible(arg_ty, param_ty, tys) {
1053            ctx.errors.push(
1054                CompileError::new(
1055                    "bynk.types.argument_mismatch",
1056                    arg.span,
1057                    format!(
1058                        "argument {} to `{}` has type `{}`, but parameter `{}` expects `{}`",
1059                        i + 1,
1060                        name.name,
1061                        arg_ty.display(tys),
1062                        param.name.name,
1063                        param_ty.display(tys)
1064                    ),
1065                )
1066                .with_label(param.span, "parameter declared here"),
1067            );
1068            ok = false;
1069        }
1070    }
1071    if !ok {
1072        return None;
1073    }
1074    resolve_type_ref(&fn_decl.return_type, &ctx.input.types, tys)
1075}
1076
1077/// Type-check a kernel-method argument against its expected type, with the
1078/// expected type propagated in (so lambdas and literals type contextually).
1079pub(crate) fn check_arg(arg: &Expr, expected: TyId, what: &str, ctx: &mut Ctx) {
1080    let tys = ctx.tys;
1081    let Some(actual) = type_of(arg, Some(expected), ctx) else {
1082        return;
1083    };
1084    if !compatible(actual, expected, tys) {
1085        ctx.errors.push(CompileError::new(
1086            "bynk.types.type_mismatch",
1087            arg.span,
1088            format!(
1089                "{what} has type `{}`, but `{}` is required",
1090                actual.display(tys),
1091                expected.display(tys)
1092            ),
1093        ));
1094    }
1095}
1096
1097/// Record a capability reference's binding edge, qualifying flattened bare
1098/// names (`consumes U { Cap }`) to their providing unit (v0.25). A bare
1099/// non-flattened name is the consuming unit's own declaration — qualified
1100/// at assembly.
1101fn record_capability_ref(span: Span, name: &str, ctx: &mut Ctx) {
1102    if let Some(unit) = ctx.input.cross_context.flattened_caps.get(name) {
1103        ctx.refs
1104            .record_in_unit(span, SymbolKind::Capability, name, unit);
1105    } else {
1106        ctx.refs.record(span, SymbolKind::Capability, name);
1107    }
1108}
1109
1110#[allow(clippy::too_many_arguments)]
1111pub(crate) fn check_static_call(
1112    type_name: &Ident,
1113    method: &Ident,
1114    // #926: explicit type arguments for a generic capability operation
1115    // (`Cap.op[T](…)`). Consumed only by the capability-dispatch branch below
1116    // — the user-declared-static branch never receives a non-empty slice,
1117    // since `check_method_call`'s gate only lets type args through for a
1118    // capability (or the `Json` codec, handled separately) receiver.
1119    type_args: &[TypeRef],
1120    args: &[Expr],
1121    span: Span,
1122    // #593: threaded to `check_variant_construction` so a qualified generic
1123    // variant (`Opt.Nil`) can ground its arguments from the binding's type.
1124    expected: Option<TyId>,
1125    // P6.0 (#1139): the outer expression's identity — the `MethodCall` node
1126    // `check_method_call` is dispatching on behalf of.
1127    expr_id: ExprId,
1128    ctx: &mut Ctx,
1129) -> Option<TyId> {
1130    let tys = ctx.tys;
1131    // Capability dispatch (v0.5): if `type_name` names a capability declared
1132    // in the context, dispatch via the capability table. If the capability is
1133    // declared but not in `given`, error specifically.
1134    if ctx.caps.declared_capabilities.contains_key(&type_name.name)
1135        && !ctx.caps.capabilities.contains_key(&type_name.name)
1136    {
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        let mut err = CompileError::new(
1146            "bynk.given.undeclared_capability",
1147            type_name.span,
1148            format!(
1149                "capability `{}` is used but not listed in the handler's `given` clause",
1150                type_name.name
1151            ),
1152        )
1153        .with_note(format!(
1154            "add `{}` to the handler's `given` clause so the dependency surface is visible at the declaration site",
1155            type_name.name
1156        ));
1157        // v0.26 (ADR 0054): the one-click counterpart of the note.
1158        if let Some((span, insert)) = given_insertion_edit(
1159            &ctx.caps.given_entries,
1160            ctx.caps.given_anchor,
1161            &type_name.name,
1162        ) {
1163            err = err.with_suggestion(
1164                format!("add `{}` to the `given` clause", type_name.name),
1165                vec![(span, insert)],
1166                Applicability::MachineApplicable,
1167            );
1168        }
1169        ctx.errors.push(err);
1170        // v0.99: an uncovered direct capability call — record it so the ghost
1171        // `given` inlay hint can offer the same clause at the declaration site
1172        // (DECISION D/E). `span` is the call site (the inner `given_insertion_edit`
1173        // binding shadowed it only within the `if let` above).
1174        record_requirement(
1175            ctx,
1176            &type_name.name,
1177            span,
1178            RequirementSource::DirectCall {
1179                op: method.name.clone(),
1180            },
1181            false,
1182        );
1183        for a in args {
1184            let _ = type_of(a, None, ctx);
1185        }
1186        return None;
1187    }
1188    if let Some(cap) = ctx.caps.capabilities.get(&type_name.name).cloned() {
1189        record_capability_ref(type_name.span, &type_name.name, ctx);
1190        // S3 (#1963): a flattened capability's operation, by its owning unit.
1191        if let Some(unit) = ctx
1192            .input
1193            .cross_context
1194            .flattened_caps
1195            .get(&type_name.name)
1196            .cloned()
1197        {
1198            refuse_experimental(
1199                &format!("{unit}.{}.{}", type_name.name, method.name),
1200                method.span,
1201                ctx,
1202            );
1203        }
1204        ctx.callees.insert(
1205            expr_id,
1206            Callee::Capability {
1207                cap: type_name.name.clone(),
1208                op: method.name.clone(),
1209            },
1210        );
1211        if !ctx.effectful {
1212            ctx.errors.push(
1213                CompileError::new(
1214                    "bynk.effect.capability_in_pure_context",
1215                    span,
1216                    format!(
1217                        "capability `{}` can only be called inside an effectful body (one returning `Effect[T]`)",
1218                        type_name.name
1219                    ),
1220                ),
1221            );
1222        }
1223        ctx.caps.given_used.insert(type_name.name.clone());
1224        // v0.99: a covered direct capability call — the call site *is* the
1225        // reason (DECISION C). Recorded so hover can explain what a declared
1226        // `given Cap` is for; no inlay hint (already covered).
1227        record_requirement(
1228            ctx,
1229            &type_name.name,
1230            span,
1231            RequirementSource::DirectCall {
1232                op: method.name.clone(),
1233            },
1234            true,
1235        );
1236        let Some(op) = cap.ops.iter().find(|o| o.name == method.name) else {
1237            ctx.errors.push(CompileError::new(
1238                "bynk.capability.unknown_operation",
1239                method.span,
1240                format!(
1241                    "capability `{}` has no operation named `{}`",
1242                    type_name.name, method.name
1243                ),
1244            ));
1245            for a in args {
1246                let _ = type_of(a, None, ctx);
1247            }
1248            return None;
1249        };
1250        // v0.36 (ADR 0069, slice 2): the op is an index symbol keyed by the
1251        // compound `"Cap.op"` name; this local call is a reference. S4a
1252        // (#1966): a derived capability declares no operations, so its call
1253        // references the base's, in the base's unit, and is refused as the
1254        // base's would be.
1255        match &cap.derived_from {
1256            Some((unit, base)) => {
1257                refuse_experimental(&format!("{unit}.{base}.{}", method.name), method.span, ctx);
1258                ctx.refs.record_in_unit(
1259                    method.span,
1260                    SymbolKind::CapabilityOp,
1261                    &format!("{base}.{}", method.name),
1262                    unit,
1263                );
1264            }
1265            None => ctx.refs.record(
1266                method.span,
1267                SymbolKind::CapabilityOp,
1268                &format!("{}.{}", type_name.name, method.name),
1269            ),
1270        }
1271        if op.params.len() != args.len() {
1272            ctx.errors.push(CompileError::new(
1273                "bynk.capability.op_arity",
1274                span,
1275                format!(
1276                    "capability operation `{}.{}` expects {} argument(s), but {} were given",
1277                    type_name.name,
1278                    method.name,
1279                    op.params.len(),
1280                    args.len()
1281                ),
1282            ));
1283            for a in args {
1284                let _ = type_of(a, None, ctx);
1285            }
1286            return None;
1287        }
1288        let op_clone = op.clone();
1289        // #926: resolve the op's own type parameter(s) from an explicit
1290        // call-site type argument — explicit only, never inferred (mirrors
1291        // `Json.decode[T]`, not `#594`'s inference). `check_generic_call`
1292        // (this file) is the model; only its arity-check + substitution-build
1293        // half applies here, since a capability op has no argument-driven
1294        // inference pass.
1295        let mut subst: HashMap<String, TyId> = HashMap::new();
1296        if !op_clone.type_params.is_empty() || !type_args.is_empty() {
1297            if type_args.is_empty() {
1298                ctx.errors.push(
1299                    CompileError::new(
1300                        "bynk.generics.uninferable_type_arg",
1301                        span,
1302                        format!(
1303                            "capability operation `{}.{}` takes a type parameter, but none of its arguments determine it",
1304                            type_name.name, method.name
1305                        ),
1306                    )
1307                    .with_note(format!(
1308                        "give it explicitly: `{}.{}[T](…)`",
1309                        type_name.name, method.name
1310                    )),
1311                );
1312                for a in args {
1313                    let _ = type_of(a, None, ctx);
1314                }
1315                return None;
1316            }
1317            if type_args.len() != op_clone.type_params.len() {
1318                ctx.errors.push(CompileError::new(
1319                    "bynk.generics.type_arg_mismatch",
1320                    span,
1321                    format!(
1322                        "capability operation `{}.{}` takes {} type argument(s), but {} were given",
1323                        type_name.name,
1324                        method.name,
1325                        op_clone.type_params.len(),
1326                        type_args.len()
1327                    ),
1328                ));
1329                for a in args {
1330                    let _ = type_of(a, None, ctx);
1331                }
1332                return None;
1333            }
1334            for (tp, ta) in op_clone.type_params.iter().zip(type_args) {
1335                let ty = resolve_expr_type_ref(ta, ctx)?;
1336                // Events track, slice 0 (spine #936): owner-only emission —
1337                // `Events.emit[E]` may only name an event `E` declared in
1338                // *this* context, even though a `consumes`-visible foreign
1339                // event resolves here just as validly for every other
1340                // purpose. `is_local_type` already distinguishes "declared
1341                // here" from "visible via uses/consumes" (the same table
1342                // ADR 0256's locale-types-split gap analysis used), so this
1343                // is a check over existing data, not new provenance
1344                // plumbing — the primary boundary guarantee the threat
1345                // model (events.md §6) names.
1346                //
1347                // First-party-gated like every other Events-special-cased
1348                // site (mirrors #934's Idempotency precedent): a bare
1349                // `type_name.name == "Events"` string match would also fire
1350                // for a third-party capability that happens to declare its
1351                // own `Events` with an `emit` method — only bynk's own
1352                // capability (declared here, in `bynk` itself, or flattened
1353                // in via `consumes bynk { Events }`) gets this check.
1354                let is_first_party_events = type_name.name == "Events"
1355                    && method.name == "emit"
1356                    && (ctx.input.commons.name.joined() == crate::firstparty::BYNK_UNIT
1357                        || ctx
1358                            .input
1359                            .cross_context
1360                            .flattened_caps
1361                            .get("Events")
1362                            .map(String::as_str)
1363                            == Some(crate::firstparty::BYNK_UNIT));
1364                if is_first_party_events && let Ty::Named { name: ename, .. } = &*tys.get(ty) {
1365                    if ctx.input.is_local_event(ename) {
1366                        // Locally-declared event — the owner. Fine.
1367                    } else if ctx.input.is_local_type(ename) {
1368                        // Events track, slice 0: `UnitTable`/`ResolvedCommons`
1369                        // record which local names are specifically events
1370                        // (as opposed to any other locally-declared type) —
1371                        // `Events.emit[SomeLocalRecord]` compiled clean before
1372                        // this check existed, silently buffering an emission
1373                        // no `from Events(...)` subscriber could ever match
1374                        // (`discover_event_subscribers` finds no owner for a
1375                        // non-event name and just drops it).
1376                        ctx.errors.push(
1377                            CompileError::new(
1378                                "bynk.event.emit_not_an_event",
1379                                ta.span(),
1380                                format!(
1381                                    "`{ename}` is not a declared `event` — `Events.emit` may only name an event type"
1382                                ),
1383                            )
1384                            .with_note(
1385                                "declare it with `event Name = { ... }`, or check that the type argument names the event you meant",
1386                            ),
1387                        );
1388                    } else {
1389                        ctx.errors.push(
1390                            CompileError::new(
1391                                "bynk.event.emit_outside_owner",
1392                                ta.span(),
1393                                format!(
1394                                    "`{ename}` is not declared in this context — only the context that declares an event may emit it"
1395                                ),
1396                            )
1397                            .with_note(
1398                                "a foreign event is visible via `consumes` for subscription (`from Events(...)`), but only its owning context may `Events.emit` it",
1399                            ),
1400                        );
1401                    }
1402                }
1403                subst.insert(tp.clone(), ty);
1404            }
1405        }
1406        for (i, (param_ty, arg)) in op_clone.params.iter().zip(args.iter()).enumerate() {
1407            let param_ty = substitute(*param_ty, &subst, tys);
1408            let arg_ty = type_of(arg, Some(param_ty), ctx);
1409            if let Some(actual) = arg_ty
1410                && !compatible(actual, param_ty, tys)
1411            {
1412                ctx.errors.push(CompileError::new(
1413                    "bynk.types.argument_mismatch",
1414                    arg.span,
1415                    format!(
1416                        "argument {} to capability `{}.{}` has type `{}`, but parameter expects `{}`",
1417                        i + 1,
1418                        type_name.name,
1419                        method.name,
1420                        actual.display(tys),
1421                        param_ty.display(tys)
1422                    ),
1423                ));
1424            }
1425        }
1426        return Some(substitute(op_clone.return_ty, &subst, tys));
1427    }
1428    let decl = ctx.input.types.get(&type_name.name)?;
1429    ctx.refs
1430        .record(type_name.span, SymbolKind::Type, &type_name.name);
1431
1432    // 1) User-declared static method. Borrow the method table and decl
1433    // straight out of the (immutable) input rather than cloning the whole
1434    // `MethodTable`/`FnDecl` on every static-shaped call.
1435    if let Some(method_decl) = ctx
1436        .input
1437        .methods
1438        .get(&type_name.name)
1439        .and_then(|table| table.statics.get(&method.name))
1440    {
1441        ctx.callees
1442            .insert(expr_id, Callee::Static(Arc::clone(method_decl)));
1443        return check_method_args(method_decl, args, ctx, type_name, method);
1444    }
1445
1446    // 2) Built-in `of` constructor on refined or opaque types.
1447    if method.name == OF
1448        && let Some(base) = type_decl_base(decl)
1449    {
1450        ctx.callees
1451            .insert(expr_id, Callee::Refine(Arc::clone(decl)));
1452        if args.len() != 1 {
1453            ctx.errors.push(CompileError::new(
1454                "bynk.types.constructor_arity",
1455                span,
1456                format!(
1457                    "constructor `{}.of` expects 1 argument, but {} were given",
1458                    type_name.name,
1459                    args.len()
1460                ),
1461            ));
1462            return None;
1463        }
1464        let arg = &args[0];
1465        let expected = tys.intern(Ty::Base(base));
1466        let arg_ty = type_of(arg, Some(expected), ctx)?;
1467        if !compatible(arg_ty, expected, tys) {
1468            ctx.errors.push(CompileError::new(
1469                "bynk.types.constructor_base_mismatch",
1470                arg.span,
1471                format!(
1472                    "constructor `{}.of` expects a `{}` argument, but got `{}`",
1473                    type_name.name,
1474                    base.name(),
1475                    arg_ty.display(tys)
1476                ),
1477            ));
1478            return None;
1479        }
1480        // `.of` is always the runtime constructor: it returns
1481        // `Result[T, ValidationError]`. Compile-time literal admission (v0.9.4)
1482        // happens instead wherever an expected refined type is known — see
1483        // `admit_refined_literal`, used by `type_of` — so `.of`'s type never
1484        // depends on the form of its argument.
1485        return Some(tys.intern(Ty::Result(
1486            named_ty(decl, tys),
1487            tys.intern(Ty::ValidationError),
1488        )));
1489    }
1490
1491    // 2b) Built-in `unsafe` constructor on opaque types — only available
1492    // inside the defining commons.
1493    if method.name == UNSAFE
1494        && let TypeBody::Opaque { base, .. } = &decl.body
1495    {
1496        ctx.callees
1497            .insert(expr_id, Callee::Unsafe(Arc::clone(decl)));
1498        if !ctx.input.is_local_type(&decl.name.name) {
1499            ctx.errors.push(
1500                CompileError::new(
1501                    "bynk.types.opaque_unsafe_outside",
1502                    method.span,
1503                    format!(
1504                        "`{}.unsafe(...)` is only available within the commons that defines the opaque type `{}`",
1505                        type_name.name, type_name.name
1506                    ),
1507                )
1508                .with_note(
1509                    "outside the defining commons, opaque values are constructed via `T.of(value)`",
1510                ),
1511            );
1512            return None;
1513        }
1514        if args.len() != 1 {
1515            ctx.errors.push(CompileError::new(
1516                "bynk.types.constructor_arity",
1517                span,
1518                format!(
1519                    "`{}.unsafe` expects 1 argument, but {} were given",
1520                    type_name.name,
1521                    args.len()
1522                ),
1523            ));
1524            return None;
1525        }
1526        let arg = &args[0];
1527        let expected = tys.intern(Ty::Base(*base));
1528        let arg_ty = type_of(arg, Some(expected), ctx)?;
1529        if !compatible(arg_ty, expected, tys) {
1530            ctx.errors.push(CompileError::new(
1531                "bynk.types.constructor_base_mismatch",
1532                arg.span,
1533                format!(
1534                    "`{}.unsafe` expects a `{}` argument, but got `{}`",
1535                    type_name.name,
1536                    base.name(),
1537                    arg_ty.display(tys)
1538                ),
1539            ));
1540            return None;
1541        }
1542        return Some(named_ty(decl, tys));
1543    }
1544
1545    // 3) Qualified variant construction `TypeName.Variant(args)`.
1546    if let TypeBody::Sum(_) = &decl.body {
1547        ctx.callees.insert(
1548            expr_id,
1549            Callee::Ctor {
1550                sum: Arc::clone(decl),
1551                tag: method.name.clone(),
1552            },
1553        );
1554        return check_variant_construction(decl, &method.name, args, span, expected, ctx);
1555    }
1556
1557    ctx.errors.push(
1558        CompileError::new(
1559            "bynk.types.unknown_static_member",
1560            method.span,
1561            format!(
1562                "type `{}` has no static method or variant named `{}`",
1563                type_name.name, method.name
1564            ),
1565        )
1566        // Finding #46: `decl` comes from the combined cross-file symbol
1567        // table — see resolver.rs:1029 for the full rationale.
1568        .with_note("type declared here"),
1569    );
1570    None
1571}
1572
1573fn check_method_args(
1574    method_decl: &FnDecl,
1575    args: &[Expr],
1576    ctx: &mut Ctx,
1577    type_name: &Ident,
1578    method: &Ident,
1579) -> Option<TyId> {
1580    let tys = ctx.tys;
1581    if method_decl.params.len() != args.len() {
1582        ctx.errors.push(
1583            CompileError::new(
1584                "bynk.types.method_arity",
1585                method.span,
1586                format!(
1587                    "static method `{}.{}` expects {} argument(s), but {} were given",
1588                    type_name.name,
1589                    method.name,
1590                    method_decl.params.len(),
1591                    args.len()
1592                ),
1593            )
1594            .with_label(method_decl.name.ident().span, "method declared here"),
1595        );
1596        for a in args {
1597            let _ = type_of(a, None, ctx);
1598        }
1599        return None;
1600    }
1601    let mut ok = true;
1602    for (i, (param, arg)) in method_decl.params.iter().zip(args.iter()).enumerate() {
1603        record_param_hint(ctx.hints, &param.name.name, arg);
1604        let expected = resolve_type_ref(&param.type_ref, &ctx.input.types, tys);
1605        let actual = type_of(arg, expected, ctx);
1606        let (Some(actual), Some(expected)) = (actual, expected) else {
1607            ok = false;
1608            continue;
1609        };
1610        if !compatible(actual, expected, tys) {
1611            ctx.errors.push(CompileError::new(
1612                "bynk.types.argument_mismatch",
1613                arg.span,
1614                format!(
1615                    "argument {} to `{}.{}` has type `{}`, but parameter `{}` expects `{}`",
1616                    i + 1,
1617                    type_name.name,
1618                    method.name,
1619                    actual.display(tys),
1620                    param.name.name,
1621                    expected.display(tys)
1622                ),
1623            ));
1624            ok = false;
1625        }
1626    }
1627    if !ok {
1628        return None;
1629    }
1630    resolve_type_ref(&method_decl.return_type, &ctx.input.types, tys)
1631}
1632
1633/// v0.82 (ADR 0110): resolve a storage-map operation `<map>.<op>(args)` on a
1634/// `store Map[K, V]` field. The ops are effect-typed (storage I/O, awaited with
1635/// `<-`): `put`/`update`/`upsert`/`remove` → `Effect[()]`, `get` →
1636/// `Effect[Option[V]]`, `contains` → `Effect[Bool]`, `size` → `Effect[Int]`.
1637/// `update` on an absent key is a runtime fault; `upsert` is the default-if-absent
1638/// form. Dispatched by receiver provenance, so it never shadows the immutable
1639/// value `Map`'s pure methods.
1640pub(crate) fn check_store_map_op(
1641    method: &Ident,
1642    args: &[Expr],
1643    k: TyId,
1644    v: TyId,
1645    span: Span,
1646    ctx: &mut Ctx,
1647) -> Option<TyId> {
1648    let tys = ctx.tys;
1649    let vfn = || Ty::Fn {
1650        params: vec![v],
1651        ret: v,
1652    };
1653    // v0.105 (slice 3b-ii): a held `Map[K, Connection]` stores connection ids and
1654    // resolves them by identity; `update`/`upsert` transform the value through a
1655    // `(V) -> V` function, which has no meaning for a held resource — you cannot
1656    // derive a new connection from an old one. Reject them (the other entry ops —
1657    // `put`/`get`/`remove`/`contains`/`size` — are admitted) so the program is a
1658    // clean compile error rather than a silent miscompile.
1659    if v.is_held(tys) && matches!(method.name.as_str(), "update" | "upsert") {
1660        ctx.errors.push(
1661            CompileError::new(
1662                "bynk.held.unsupported_map_op",
1663                method.span,
1664                format!(
1665                    "a held `Map[K, Connection]` has no `{}` operation — a held resource cannot be transformed by a `(Connection) -> Connection` function",
1666                    method.name
1667                ),
1668            )
1669            .with_note(
1670                "held connections are stored and resolved by identity; use `put`/`get`/`remove`",
1671            ),
1672        );
1673        for a in args {
1674            type_of(a, None, ctx);
1675        }
1676        return None;
1677    }
1678    let (expected, result): (Vec<TyId>, TyId) = match method.name.as_str() {
1679        "put" => (vec![k, v], tys.intern(Ty::Unit)),
1680        "get" => (vec![k], tys.intern(Ty::Option(v))),
1681        "remove" => (vec![k], tys.intern(Ty::Unit)),
1682        "contains" => (vec![k], tys.intern(Ty::Base(BaseType::Bool))),
1683        "size" => (vec![], tys.intern(Ty::Base(BaseType::Int))),
1684        "update" => (vec![k, tys.intern(vfn())], tys.intern(Ty::Unit)),
1685        "upsert" => (vec![k, v, tys.intern(vfn())], tys.intern(Ty::Unit)),
1686        other => {
1687            ctx.errors.push(
1688                CompileError::new(
1689                    "bynk.store.unknown_op",
1690                    method.span,
1691                    format!(
1692                        "a `Map` store field has no operation `{other}` — expected `put`, `get`, \
1693                         `update`, `upsert`, `remove`, `contains`, or `size`"
1694                    ),
1695                )
1696                .with_note("storage-map ops are entry-level and effectful (await with `<-`)"),
1697            );
1698            for a in args {
1699                type_of(a, None, ctx);
1700            }
1701            return None;
1702        }
1703    };
1704    let effect = Ty::Effect(result);
1705    if args.len() != expected.len() {
1706        ctx.errors.push(CompileError::new(
1707            "bynk.types.call_arity",
1708            span,
1709            format!(
1710                "`Map.{}` takes {} argument(s), found {}",
1711                method.name,
1712                expected.len(),
1713                args.len()
1714            ),
1715        ));
1716        for a in args {
1717            type_of(a, None, ctx);
1718        }
1719        return Some(tys.intern(effect));
1720    }
1721    for (a, exp) in args.iter().zip(expected.iter()) {
1722        if let Some(at) = type_of(a, Some(*exp), ctx)
1723            && !compatible(at, *exp, tys)
1724        {
1725            ctx.errors.push(CompileError::new(
1726                "bynk.types.argument_mismatch",
1727                a.span,
1728                format!(
1729                    "expected `{}`, found `{}`",
1730                    exp.display(tys),
1731                    at.display(tys)
1732                ),
1733            ));
1734        }
1735    }
1736    Some(tys.intern(effect))
1737}
1738
1739/// v0.99: record a capability requirement at `site` into the ledger, and — when
1740/// the enclosing handler's `given` does not cover it — push the diagnostic. The
1741/// single producer behind both the bare diagnostic and the editor surfaces
1742/// (DECISION D): every requirement is *recorded*, covered or not; only an
1743/// uncovered one errors. The `code`/`message` are the consuming feature's, so a
1744/// store op keeps its precise diagnostic; the ledger's *reason* renders from
1745/// `source` alone (DECISION C), never from `code`.
1746fn require_capability(
1747    site: Span,
1748    capability: &str,
1749    source: RequirementSource,
1750    ctx: &mut Ctx,
1751    code: &'static str,
1752    message: &str,
1753) {
1754    let covered = ctx.caps.capabilities.contains_key(capability);
1755    if covered {
1756        ctx.caps.given_used.insert(capability.to_string());
1757    } else {
1758        ctx.errors
1759            .push(CompileError::new(code, site, message).with_note(format!(
1760                "add `{capability}` to the handler's `given` clause"
1761            )));
1762    }
1763    record_requirement(ctx, capability, site, source, covered);
1764}
1765
1766/// Push a [`Requirement`] into the ledger. For an uncovered requirement it also
1767/// computes the materialization edit (the ghost `given` inlay hint's one-click
1768/// apply) from the handler's existing `given` entries and anchor — the same
1769/// `given_insertion_edit` the undeclared-capability quick-fix uses.
1770fn record_requirement(
1771    ctx: &mut Ctx,
1772    capability: &str,
1773    site: Span,
1774    source: RequirementSource,
1775    covered: bool,
1776) {
1777    let materialize = if covered {
1778        None
1779    } else {
1780        given_insertion_edit(&ctx.caps.given_entries, ctx.caps.given_anchor, capability).map(
1781            |(edit_span, edit_text)| Materialize {
1782                anchor: ctx.caps.given_anchor.unwrap_or(ctx.return_ty_span),
1783                edit_span,
1784                edit_text,
1785            },
1786        )
1787    };
1788    ctx.requirements.record(Requirement {
1789        capability: capability.to_string(),
1790        site,
1791        source,
1792        covered,
1793        materialize,
1794    });
1795}
1796
1797/// v0.87 (ADR 0113): resolve a storage-`Cache` operation `<cache>.<op>(args)` on
1798/// a `store Cache[K, V]` field. The op set is the storage `Map`'s
1799/// (`put`/`get`/`update`/`upsert`/`remove`/`contains`/`size`); every op but
1800/// `remove` additionally requires `given Clock` (eviction reads the clock).
1801pub(crate) fn check_store_cache_op(
1802    method: &Ident,
1803    args: &[Expr],
1804    k: TyId,
1805    v: TyId,
1806    span: Span,
1807    ctx: &mut Ctx,
1808) -> Option<TyId> {
1809    let tys = ctx.tys;
1810    let vfn = || Ty::Fn {
1811        params: vec![v],
1812        ret: v,
1813    };
1814    let (expected, result): (Vec<TyId>, TyId) = match method.name.as_str() {
1815        "put" => (vec![k, v], tys.intern(Ty::Unit)),
1816        "get" => (vec![k], tys.intern(Ty::Option(v))),
1817        "remove" => (vec![k], tys.intern(Ty::Unit)),
1818        "contains" => (vec![k], tys.intern(Ty::Base(BaseType::Bool))),
1819        "size" => (vec![], tys.intern(Ty::Base(BaseType::Int))),
1820        "update" => (vec![k, tys.intern(vfn())], tys.intern(Ty::Unit)),
1821        "upsert" => (vec![k, v, tys.intern(vfn())], tys.intern(Ty::Unit)),
1822        other => {
1823            ctx.errors.push(
1824                CompileError::new(
1825                    "bynk.store.unknown_op",
1826                    method.span,
1827                    format!(
1828                        "a `Cache` store field has no operation `{other}` — expected `put`, \
1829                         `get`, `update`, `upsert`, `remove`, `contains`, or `size`"
1830                    ),
1831                )
1832                .with_note("storage-cache ops are entry-level and effectful (await with `<-`)"),
1833            );
1834            for a in args {
1835                type_of(a, None, ctx);
1836            }
1837            return None;
1838        }
1839    };
1840    // D4: every op but `remove` reads the clock for TTL expiry.
1841    if method.name != "remove" {
1842        require_capability(
1843            method.span,
1844            "Clock",
1845            RequirementSource::StoreOp {
1846                kind: StoreKind::Cache,
1847                op: method.name.clone(),
1848            },
1849            ctx,
1850            "bynk.store.cache_needs_clock",
1851            "a `Cache` operation applies TTL expiry, which reads the clock — the handler must declare `given Clock`",
1852        );
1853    }
1854    let effect = Ty::Effect(result);
1855    if args.len() != expected.len() {
1856        ctx.errors.push(CompileError::new(
1857            "bynk.types.call_arity",
1858            span,
1859            format!(
1860                "`Cache.{}` takes {} argument(s), found {}",
1861                method.name,
1862                expected.len(),
1863                args.len()
1864            ),
1865        ));
1866        for a in args {
1867            type_of(a, None, ctx);
1868        }
1869        return Some(tys.intern(effect));
1870    }
1871    for (a, exp) in args.iter().zip(expected.iter()) {
1872        if let Some(at) = type_of(a, Some(*exp), ctx)
1873            && !compatible(at, *exp, tys)
1874        {
1875            ctx.errors.push(CompileError::new(
1876                "bynk.types.argument_mismatch",
1877                a.span,
1878                format!(
1879                    "expected `{}`, found `{}`",
1880                    exp.display(tys),
1881                    at.display(tys)
1882                ),
1883            ));
1884        }
1885    }
1886    Some(tys.intern(effect))
1887}
1888
1889/// v0.95 (ADR 0121): resolve a storage-`Log` operation `<log>.<op>(args)` on a
1890/// `store Log[T]` field. `append(e)` is the effectful, **non-idempotent** write
1891/// (`Effect[()]`) and the one clock-consuming op — it stamps `Clock.now()`, so it
1892/// requires `given Clock`. The time-window roots — `since(Instant)`/
1893/// `before(Instant)` / `between(Instant, Instant)` / `recent(Int)` / `reversed()`
1894/// — and the general query vocabulary lift the log into a lazy `Query[T]` over its
1895/// entry values; these need no clock (window bounds are explicit `Instant`s).
1896pub(crate) fn check_store_log_op(
1897    method: &Ident,
1898    args: &[Expr],
1899    elem: TyId,
1900    span: Span,
1901    ctx: &mut Ctx,
1902) -> Option<TyId> {
1903    let tys = ctx.tys;
1904    let query = || Ty::Query(elem);
1905    let arity = |n: usize, ctx: &mut Ctx| {
1906        if args.len() != n {
1907            ctx.errors.push(CompileError::new(
1908                "bynk.types.call_arity",
1909                span,
1910                format!(
1911                    "`Log.{}` takes {n} argument(s), found {}",
1912                    method.name,
1913                    args.len()
1914                ),
1915            ));
1916            for a in args {
1917                type_of(a, None, ctx);
1918            }
1919            return false;
1920        }
1921        true
1922    };
1923    let window_arg = |a: &Expr, what: &str, ctx: &mut Ctx| {
1924        if let Some(at) = type_of(a, Some(tys.intern(Ty::Base(BaseType::Instant))), ctx)
1925            && !compatible(at, tys.intern(Ty::Base(BaseType::Instant)), tys)
1926        {
1927            ctx.errors.push(CompileError::new(
1928                "bynk.types.argument_mismatch",
1929                a.span,
1930                format!("{what} expects `Instant`, found `{}`", at.display(tys)),
1931            ));
1932        }
1933    };
1934    match method.name.as_str() {
1935        // The one effectful write — non-idempotent; stamps the clock.
1936        "append" => {
1937            require_capability(
1938                method.span,
1939                "Clock",
1940                RequirementSource::StoreOp {
1941                    kind: StoreKind::Log,
1942                    op: method.name.clone(),
1943                },
1944                ctx,
1945                "bynk.store.log_needs_clock",
1946                "`Log.append` stamps the current time, which reads the clock — the handler must declare `given Clock`",
1947            );
1948            if !arity(1, ctx) {
1949                return Some(tys.intern(Ty::Effect(tys.intern(Ty::Unit))));
1950            }
1951            if let Some(at) = type_of(&args[0], Some(elem), ctx)
1952                && !compatible(at, elem, tys)
1953            {
1954                ctx.errors.push(CompileError::new(
1955                    "bynk.types.argument_mismatch",
1956                    args[0].span,
1957                    format!(
1958                        "expected `{}`, found `{}`",
1959                        elem.display(tys),
1960                        at.display(tys)
1961                    ),
1962                ));
1963            }
1964            Some(tys.intern(Ty::Effect(tys.intern(Ty::Unit))))
1965        }
1966        // Time-window query roots → `Query[T]` (lazy; no clock).
1967        "since" | "before" => {
1968            if !arity(1, ctx) {
1969                return Some(tys.intern(query()));
1970            }
1971            window_arg(&args[0], &format!("`Log.{}`", method.name), ctx);
1972            Some(tys.intern(query()))
1973        }
1974        "between" => {
1975            if !arity(2, ctx) {
1976                return Some(tys.intern(query()));
1977            }
1978            window_arg(&args[0], "`Log.between` start", ctx);
1979            window_arg(&args[1], "`Log.between` end", ctx);
1980            Some(tys.intern(query()))
1981        }
1982        "recent" => {
1983            if !arity(1, ctx) {
1984                return Some(tys.intern(query()));
1985            }
1986            check_arg(
1987                &args[0],
1988                tys.intern(Ty::Base(BaseType::Int)),
1989                "the `Log.recent` count",
1990                ctx,
1991            );
1992            Some(tys.intern(query()))
1993        }
1994        "reversed" => {
1995            if !arity(0, ctx) {
1996                return Some(tys.intern(query()));
1997            }
1998            Some(tys.intern(query()))
1999        }
2000        // The general query vocabulary over the entry values.
2001        name if is_query_op(name) => check_query_kernel_method(method, args, elem, span, ctx),
2002        other => {
2003            ctx.errors.push(
2004                CompileError::new(
2005                    "bynk.store.unknown_op",
2006                    method.span,
2007                    format!(
2008                        "a `Log` store field has no operation `{other}` — `append`, the \
2009                         time-window roots (`since`/`before`/`between`/`recent`/`reversed`), \
2010                         and the query builders/terminals"
2011                    ),
2012                )
2013                .with_note(
2014                    "`Log` reads are lazy `Query[T]`; only `append` is effectful and writes",
2015                ),
2016            );
2017            for a in args {
2018                type_of(a, None, ctx);
2019            }
2020            None
2021        }
2022    }
2023}
2024
2025/// v0.83: resolve a storage-set operation `<set>.<op>(args)` on a `store Set[T]`
2026/// field. Effect-typed entry ops: `add(t)`/`remove(t)` → `Effect[()]` (both
2027/// idempotent), `contains(t)` → `Effect[Bool]`, `size()` → `Effect[Int]`. Set
2028/// algebra (`union`/`intersection`/`difference`) is deferred (it needs a value
2029/// `Set` return type). Dispatched by receiver provenance.
2030pub(crate) fn check_store_set_op(
2031    method: &Ident,
2032    args: &[Expr],
2033    t: TyId,
2034    span: Span,
2035    ctx: &mut Ctx,
2036) -> Option<TyId> {
2037    let tys = ctx.tys;
2038    let (expected, result): (Vec<TyId>, TyId) = match method.name.as_str() {
2039        "add" => (vec![t], tys.intern(Ty::Unit)),
2040        "remove" => (vec![t], tys.intern(Ty::Unit)),
2041        "contains" => (vec![t], tys.intern(Ty::Base(BaseType::Bool))),
2042        "size" => (vec![], tys.intern(Ty::Base(BaseType::Int))),
2043        other => {
2044            ctx.errors.push(
2045                CompileError::new(
2046                    "bynk.store.unknown_op",
2047                    method.span,
2048                    format!(
2049                        "a `Set` store field has no operation `{other}` — expected `add`, \
2050                         `remove`, `contains`, or `size`"
2051                    ),
2052                )
2053                .with_note(
2054                    "set algebra (`union`/`intersection`/`difference`) is not in this slice",
2055                ),
2056            );
2057            for a in args {
2058                type_of(a, None, ctx);
2059            }
2060            return None;
2061        }
2062    };
2063    let effect = Ty::Effect(result);
2064    if args.len() != expected.len() {
2065        ctx.errors.push(CompileError::new(
2066            "bynk.types.call_arity",
2067            span,
2068            format!(
2069                "`Set.{}` takes {} argument(s), found {}",
2070                method.name,
2071                expected.len(),
2072                args.len()
2073            ),
2074        ));
2075        for a in args {
2076            type_of(a, None, ctx);
2077        }
2078        return Some(tys.intern(effect));
2079    }
2080    for (a, exp) in args.iter().zip(expected.iter()) {
2081        if let Some(at) = type_of(a, Some(*exp), ctx)
2082            && !compatible(at, *exp, tys)
2083        {
2084            ctx.errors.push(CompileError::new(
2085                "bynk.types.argument_mismatch",
2086                a.span,
2087                format!(
2088                    "expected `{}`, found `{}`",
2089                    exp.display(tys),
2090                    at.display(tys)
2091                ),
2092            ));
2093        }
2094    }
2095    Some(tys.intern(effect))
2096}
2097
2098/// v0.98 (ADR 0125): resolve a storage-`Cell` operation `<cell>.<op>(args)` on a
2099/// `store Cell[T]` field. The single method-shaped cell op is `update(f)` —
2100/// `f: (T) -> T` — a read-modify-write typed `Effect[()]`. Reading a cell is the
2101/// bare-name sugar and writing it is `:=`, so `read`/`write` are not callable
2102/// methods (DECISION B). The combiner is a non-effectful `Ty::Fn`, so an
2103/// effectful body (including a bare read of another cell) fails the existing
2104/// function-type check (DECISION E). Dispatched by receiver provenance.
2105pub(crate) fn check_store_cell_op(
2106    method: &Ident,
2107    args: &[Expr],
2108    t: TyId,
2109    span: Span,
2110    ctx: &mut Ctx,
2111) -> Option<TyId> {
2112    let tys = ctx.tys;
2113    let tfn = || Ty::Fn {
2114        params: vec![t],
2115        ret: t,
2116    };
2117    let (expected, result): (Vec<TyId>, TyId) = match method.name.as_str() {
2118        "update" => (vec![tys.intern(tfn())], tys.intern(Ty::Unit)),
2119        other => {
2120            ctx.errors.push(
2121                CompileError::new(
2122                    "bynk.store.unknown_op",
2123                    method.span,
2124                    format!("a `Cell` store field has no operation `{other}` — expected `update`"),
2125                )
2126                .with_note(
2127                    "a cell is read by its bare name and written with `:=`; `update` is the only \
2128                     method-shaped op",
2129                ),
2130            );
2131            for a in args {
2132                type_of(a, None, ctx);
2133            }
2134            return None;
2135        }
2136    };
2137    let effect = Ty::Effect(result);
2138    if args.len() != expected.len() {
2139        ctx.errors.push(CompileError::new(
2140            "bynk.types.call_arity",
2141            span,
2142            format!(
2143                "`Cell.{}` takes {} argument(s), found {}",
2144                method.name,
2145                expected.len(),
2146                args.len()
2147            ),
2148        ));
2149        for a in args {
2150            type_of(a, None, ctx);
2151        }
2152        return Some(tys.intern(effect));
2153    }
2154    for (a, exp) in args.iter().zip(expected.iter()) {
2155        if let Some(at) = type_of(a, Some(*exp), ctx)
2156            && !compatible(at, *exp, tys)
2157        {
2158            ctx.errors.push(CompileError::new(
2159                "bynk.types.argument_mismatch",
2160                a.span,
2161                format!(
2162                    "expected `{}`, found `{}`",
2163                    exp.display(tys),
2164                    at.display(tys)
2165                ),
2166            ));
2167        }
2168    }
2169    Some(tys.intern(effect))
2170}
2171
2172#[allow(clippy::too_many_arguments)]
2173pub(crate) fn check_method_call(
2174    receiver: &Expr,
2175    method: &Ident,
2176    type_args: &[TypeRef],
2177    args: &[Expr],
2178    span: Span,
2179    expected: Option<TyId>,
2180    // P6.0 (#1139): the outer `MethodCall` expression's own identity.
2181    expr_id: ExprId,
2182    ctx: &mut Ctx,
2183) -> Option<TyId> {
2184    let tys = ctx.tys;
2185    // v0.22b: explicit type arguments apply only to the `Json.decode[T]`
2186    // static — every other method/static takes none (the 0039/0045 rule). A
2187    // user-declared type named `Json` shadows the codec module and takes no type
2188    // arguments. #594: a generic *user* instance method infers its type
2189    // arguments from the receiver and the argument types — the explicit
2190    // `x.map[U](…)` form is deferred, so explicit type args are rejected here.
2191    // #926: a capability operation may declare its own type parameter(s),
2192    // resolved only from an explicit call-site type argument (never
2193    // inferred) — the same explicit-only discipline as `Json.decode[T]`, so
2194    // it joins that carve-out rather than `#594`'s inference. Covers a bare
2195    // local/declared capability receiver and a cross-context one (flattened
2196    // `Cap.op[T](…)` or qualified `B.Cap.op[T](…)`); arity/substitution
2197    // happens downstream in `check_static_call` /
2198    // `check_cross_context_capability_call`.
2199    let receiver_is_capability = matches!(&receiver.kind, ExprKind::Ident(id)
2200            if ctx.caps.capabilities.contains_key(&id.name)
2201                || ctx.caps.declared_capabilities.contains_key(&id.name))
2202        || flatten_ident_chain(receiver).is_some_and(|chain| {
2203            ctx.input
2204                .cross_context
2205                .resolve_cross_capability(&chain)
2206                .is_some()
2207        });
2208    if !type_args.is_empty()
2209        && !matches!(&receiver.kind, ExprKind::Ident(id) if id.name == JSON
2210            && !ctx.input.types.contains_key(JSON))
2211        && !receiver_is_capability
2212    {
2213        ctx.errors.push(CompileError::new(
2214            "bynk.generics.type_arg_mismatch",
2215            span,
2216            format!(
2217                "`{}` does not take explicit type arguments — a generic method infers them from the receiver and arguments",
2218                method.name
2219            ),
2220        ));
2221        for a in args {
2222            let _ = type_of(a, None, ctx);
2223        }
2224        return None;
2225    }
2226    // v0.25 / v0.178: a test body invokes the target's service as
2227    // `svc.call(args)`. The emitter wires the call from the same service set;
2228    // the checker resolves the service's `on call` handler and verifies the
2229    // call's arity and argument types (v0.178, #662 — before this, the branch
2230    // matched the literal method name `call` without checking the handler
2231    // exists, so `api.call(…)` on a `from http` service passed `check` and
2232    // crashed at runtime, #654). #1934: the call is typed by what the handler
2233    // yields, and the binding edge is recorded for test-file references. Returns here: the
2234    // resolution must not fall through to the receiver walk, which would
2235    // report the service name as unknown (#504 backstop).
2236    if let ExprKind::Ident(id) = &receiver.kind
2237        && ctx.lookup(id.name.as_str()).is_none()
2238        && let Some(sig) = ctx.test_services.get(&id.name).cloned()
2239    {
2240        if let Some(unit) = ctx.input.cross_context.self_context.clone() {
2241            ctx.refs
2242                .record_in_unit(id.span, SymbolKind::Service, &id.name, &unit);
2243        }
2244        return check_test_service_address(&sig, id, method, args, expr_id, ctx);
2245    }
2246    // v0.6: cross-context service call. Two shapes:
2247    //   - `Alias.service(args)`           where Alias is from `consumes X as Alias`
2248    //   - `prefix.tail.service(args)`     where `prefix.tail` is a consumed context's
2249    //                                     qualified name (parsed as nested FieldAccess).
2250    // The full-qualified-name form must be checked before the bare-ident form
2251    // (the prefix's first segment doesn't resolve as anything local).
2252    if ctx.lookup_root_ident(receiver).is_none() && !ctx.root_ident_is_store_field(receiver) {
2253        // v0.15: cross-context capability call — `B.Cap.op(args)` /
2254        // `Alias.Cap.op(args)`. Checked before the service-call shape because
2255        // the receiver carries an extra (capability) segment.
2256        if let Some(chain) = flatten_ident_chain(receiver)
2257            && let Some((consumed, cap)) = ctx.input.cross_context.resolve_cross_capability(&chain)
2258        {
2259            // v0.25: the capability name-segment (`Cap` in `B.Cap` /
2260            // `Alias.Cap`) is the outermost field of the receiver chain.
2261            if let ExprKind::FieldAccess { field, .. } = &receiver.kind {
2262                ctx.refs
2263                    .record_in_unit(field.span, SymbolKind::Capability, &cap, &consumed);
2264            }
2265            return check_cross_context_capability_call(
2266                receiver, &consumed, &cap, method, type_args, args, span, expr_id, ctx,
2267            );
2268        }
2269        if let Some(consumed) = cross_context_prefix(receiver, ctx) {
2270            return check_cross_context_call(receiver, &consumed, method, args, span, expr_id, ctx);
2271        }
2272        // Looks like a dotted prefix (no local binding for the root). If the
2273        // chain matches the shape of a consumed-context call but the prefix
2274        // isn't actually consumed, surface an explicit diagnostic so the user
2275        // can fix the missing `consumes` clause rather than seeing a silent
2276        // "no methods" error later.
2277        //
2278        // #1930: not for a chain rooted in an asset (`sheet.url.startsWith(…)`),
2279        // which is a method on the asset's `url`. Only here, after both
2280        // resolutions above failed: an asset named like a consumed context's
2281        // root segment (`shop` beside `consumes shop.payment`) still lets a
2282        // real cross-context call resolve (review of #1937).
2283        if let ExprKind::FieldAccess { .. } = &receiver.kind
2284            && !ctx.root_ident_is_asset(receiver)
2285            && let Some(chain) = flatten_ident_chain(receiver)
2286            && chain.contains('.')
2287        {
2288            let info = &ctx.input.cross_context;
2289            let in_context = info.self_context.is_some();
2290            if in_context && info.resolve_prefix(&chain).is_none() {
2291                ctx.errors.push(
2292                    CompileError::new(
2293                        "bynk.resolve.unconsumed_context",
2294                        receiver.span,
2295                        format!(
2296                            "`{chain}.{}` looks like a cross-context service call, but `{chain}` is not in this context's `consumes` clauses",
2297                            method.name
2298                        ),
2299                    )
2300                    .with_note(
2301                        "add a `consumes {chain}` clause at the top of the context, or use an alias and call it through the alias",
2302                    ),
2303                );
2304                for a in args {
2305                    let _ = type_of(a, None, ctx);
2306                }
2307                return None;
2308            }
2309        }
2310    }
2311    // Detect capability call (v0.5): receiver is a bare Ident naming a
2312    // capability declared in the context (in scope via `given`, or declared
2313    // but undeclared in `given` — the static-call path emits the error).
2314    if let ExprKind::Ident(id) = &receiver.kind
2315        && ctx.lookup(id.name.as_str()).is_none()
2316        && (ctx.caps.capabilities.contains_key(&id.name)
2317            || ctx.caps.declared_capabilities.contains_key(&id.name))
2318    {
2319        return check_static_call(id, method, type_args, args, span, expected, expr_id, ctx);
2320    }
2321    // Detect static-call shape: receiver is a bare Ident naming a declared
2322    // type (not a local/param). Dispatch to check_static_call. `type_args` is
2323    // always empty here — `check_method_call`'s gate above only admits a
2324    // non-empty slice for a capability (handled just above) or the `Json`
2325    // codec (handled separately, `check_json_static`).
2326    if let ExprKind::Ident(id) = &receiver.kind
2327        && ctx.lookup(id.name.as_str()).is_none()
2328        && ctx.input.types.contains_key(&id.name)
2329    {
2330        return check_static_call(id, method, type_args, args, span, expected, expr_id, ctx);
2331    }
2332    // v0.20b: qualified statics on the built-in collection types —
2333    // `List.empty()` / `Map.empty()`. Like an empty `[]`, they need an
2334    // expected type to pin their element/key/value types.
2335    if let ExprKind::Ident(id) = &receiver.kind
2336        && ctx.lookup(id.name.as_str()).is_none()
2337        && !ctx.input.types.contains_key(&id.name)
2338        && (id.name == LIST || id.name == MAP)
2339    {
2340        let ns = if id.name == LIST { LIST } else { MAP };
2341        ctx.callees.insert(
2342            expr_id,
2343            Callee::Intrinsic {
2344                ns,
2345                op: method.name.clone(),
2346            },
2347        );
2348        return check_collection_static(id, method, args, span, expected, ctx);
2349    }
2350    // v0.22a: the numeric parse statics — `Int.parse(s)` / `Float.parse(s)`.
2351    // The parser only admits these keywords in receiver position when
2352    // followed by `.`, so the Ident shape here is exactly the static form.
2353    if let ExprKind::Ident(id) = &receiver.kind
2354        && (id.name == INT || id.name == FLOAT)
2355    {
2356        let ns = if id.name == INT { INT } else { FLOAT };
2357        ctx.callees.insert(
2358            expr_id,
2359            Callee::Intrinsic {
2360                ns,
2361                op: method.name.clone(),
2362            },
2363        );
2364        return check_numeric_parse_static(id, method, args, span, ctx);
2365    }
2366    // v0.86 (ADR 0112): the `Duration.millis(n)` static constructor — the way to
2367    // build a `Duration` from a runtime `Int` (the literal covers constants).
2368    if let ExprKind::Ident(id) = &receiver.kind
2369        && id.name == DURATION
2370        && ctx.lookup(DURATION).is_none()
2371        && !ctx.input.types.contains_key(DURATION)
2372    {
2373        ctx.callees.insert(
2374            expr_id,
2375            Callee::Intrinsic {
2376                ns: DURATION,
2377                op: method.name.clone(),
2378            },
2379        );
2380        return check_duration_static(method, args, span, ctx);
2381    }
2382    // v0.90 (ADR 0114 D6): the `Instant.fromEpochMillis(n)` static constructor.
2383    if let ExprKind::Ident(id) = &receiver.kind
2384        && id.name == INSTANT
2385        && ctx.lookup(INSTANT).is_none()
2386        && !ctx.input.types.contains_key(INSTANT)
2387    {
2388        ctx.callees.insert(
2389            expr_id,
2390            Callee::Intrinsic {
2391                ns: INSTANT,
2392                op: method.name.clone(),
2393            },
2394        );
2395        return check_instant_static(method, args, span, ctx);
2396    }
2397    // v0.110 (ADR 0142 D2): the `Bytes` static constructors —
2398    // `Bytes.fromUtf8(s)` / `Bytes.fromBase64(s)` / `Bytes.empty()`.
2399    if let ExprKind::Ident(id) = &receiver.kind
2400        && id.name == BYTES
2401        && ctx.lookup(BYTES).is_none()
2402        && !ctx.input.types.contains_key(BYTES)
2403    {
2404        ctx.callees.insert(
2405            expr_id,
2406            Callee::Intrinsic {
2407                ns: BYTES,
2408                op: method.name.clone(),
2409            },
2410        );
2411        return check_bytes_static(method, args, span, ctx);
2412    }
2413    // #2380 (L5): `TokenClaims.of(lines)` — how a test builds the claims an
2414    // `Oidc` actor's handler reads.
2415    if let ExprKind::Ident(id) = &receiver.kind
2416        && id.name == TOKEN_CLAIMS
2417        && ctx.lookup(TOKEN_CLAIMS).is_none()
2418        && !ctx.input.types.contains_key(TOKEN_CLAIMS)
2419    {
2420        ctx.callees.insert(
2421            expr_id,
2422            Callee::Intrinsic {
2423                ns: TOKEN_CLAIMS,
2424                op: method.name.clone(),
2425            },
2426        );
2427        return check_token_claims_static(method, args, span, ctx);
2428    }
2429    // #2341 (L4): `RequestHeaders.of(lines)` / `RequestHeaders.empty()` — how
2430    // a test builds the headers an HTTP handler reads.
2431    if let ExprKind::Ident(id) = &receiver.kind
2432        && id.name == REQUEST_HEADERS
2433        && ctx.lookup(REQUEST_HEADERS).is_none()
2434        && !ctx.input.types.contains_key(REQUEST_HEADERS)
2435    {
2436        ctx.callees.insert(
2437            expr_id,
2438            Callee::Intrinsic {
2439                ns: REQUEST_HEADERS,
2440                op: method.name.clone(),
2441            },
2442        );
2443        return check_request_headers_static(method, args, span, ctx);
2444    }
2445    // v0.22b: the typed JSON codec statics (ADR 0045).
2446    if let ExprKind::Ident(id) = &receiver.kind
2447        && id.name == JSON
2448        && ctx.lookup(JSON).is_none()
2449        && !ctx.input.types.contains_key(JSON)
2450    {
2451        ctx.callees.insert(
2452            expr_id,
2453            Callee::Intrinsic {
2454                ns: JSON,
2455                op: method.name.clone(),
2456            },
2457        );
2458        return check_json_static(method, type_args, args, span, expected, ctx);
2459    }
2460    // v0.100: the `Stream.of(xs)` static constructor (real-time track slice 0).
2461    if let ExprKind::Ident(id) = &receiver.kind
2462        && id.name == STREAM
2463        && ctx.lookup(STREAM).is_none()
2464        && !ctx.input.types.contains_key(STREAM)
2465    {
2466        ctx.callees.insert(
2467            expr_id,
2468            Callee::Intrinsic {
2469                ns: STREAM,
2470                op: method.name.clone(),
2471            },
2472        );
2473        return check_stream_static(method, args, span, ctx);
2474    }
2475    // v0.20b: `insert`/`prepend` return their receiver's collection type —
2476    // propagate an expected collection type down the chain so
2477    // `let m: Map[String, Int] = Map.empty().insert("a", 1)` infers. #1889:
2478    // `append`/`concat` return their receiver's list type too.
2479    let recv_expected = match (expected, method.name.as_str()) {
2480        (Some(t), "insert") => peel_to_map(t, tys).map(|(k, v)| tys.intern(Ty::Map(k, v))),
2481        (Some(t), "prepend" | "append" | "concat") => {
2482            peel_to_list(t, tys).map(|e| tys.intern(Ty::List(e)))
2483        }
2484        _ => None,
2485    };
2486    let recv_ty = type_of(receiver, recv_expected, ctx)?;
2487    // v0.20b: built-in kernel methods on the collection types. These are
2488    // compiler-known special forms typed directly here — generic in their
2489    // accumulator without the (deferred) declared-generic-methods feature;
2490    // the deferral bites only on declared methods (ADR 0037).
2491    match &*tys.get(recv_ty) {
2492        Ty::List(elem) => {
2493            ctx.callees.insert(
2494                expr_id,
2495                Callee::Kernel {
2496                    recv: recv_ty,
2497                    op: method.name.clone(),
2498                },
2499            );
2500            return check_list_kernel_method(method, args, *elem, span, ctx);
2501        }
2502        // v0.91 (ADR 0115): a chained builder/terminal on a lazy `Query[T]`.
2503        Ty::Query(elem) => {
2504            ctx.callees.insert(
2505                expr_id,
2506                Callee::Kernel {
2507                    recv: recv_ty,
2508                    op: method.name.clone(),
2509                },
2510            );
2511            return check_query_kernel_method(method, args, *elem, span, ctx);
2512        }
2513        // v0.100: a chained builder/terminal on a `Stream[T]`.
2514        Ty::Stream(elem) => {
2515            ctx.callees.insert(
2516                expr_id,
2517                Callee::Kernel {
2518                    recv: recv_ty,
2519                    op: method.name.clone(),
2520                },
2521            );
2522            return check_stream_kernel_method(method, args, *elem, span, ctx);
2523        }
2524        // v0.102: the held-resource operations on a `Connection[F]` — `send(f)`
2525        // (non-consuming) and `close()` (consuming). The linearity pass tracks
2526        // the ownership transitions; this types the operations.
2527        Ty::Connection(frame) => {
2528            ctx.callees.insert(
2529                expr_id,
2530                Callee::Kernel {
2531                    recv: recv_ty,
2532                    op: method.name.clone(),
2533                },
2534            );
2535            return check_connection_method(method, args, *frame, span, ctx);
2536        }
2537        Ty::Map(key, val) => {
2538            ctx.callees.insert(
2539                expr_id,
2540                Callee::Kernel {
2541                    recv: recv_ty,
2542                    op: method.name.clone(),
2543                },
2544            );
2545            return check_map_kernel_method(method, args, *key, *val, span, ctx);
2546        }
2547        // v0.21: the numeric kernel — conversions as value methods on the
2548        // bare base types. A refined value reaches these too, but via the
2549        // refined-receiver fallback below (after its own declared methods),
2550        // not via any `.raw` surface — a refined type's only source-level
2551        // constructors are `.of` and literal admission (ADR 0182); unlike an
2552        // opaque type it exposes no `.unsafe` (that is opaque-only, confined
2553        // to the defining commons).
2554        Ty::Base(base @ (BaseType::Int | BaseType::Float)) => {
2555            ctx.callees.insert(
2556                expr_id,
2557                Callee::Kernel {
2558                    recv: recv_ty,
2559                    op: method.name.clone(),
2560                },
2561            );
2562            return check_numeric_kernel_method(method, args, *base, span, ctx);
2563        }
2564        // v0.86 (ADR 0112): the `Duration` kernel — `toMillis`/`toString`.
2565        Ty::Base(BaseType::Duration) => {
2566            ctx.callees.insert(
2567                expr_id,
2568                Callee::Kernel {
2569                    recv: recv_ty,
2570                    op: method.name.clone(),
2571                },
2572            );
2573            return check_duration_kernel_method(method, args, span, ctx);
2574        }
2575        // v0.90 (ADR 0114): the `Instant` kernel — `toEpochMillis`/`toString`.
2576        Ty::Base(BaseType::Instant) => {
2577            ctx.callees.insert(
2578                expr_id,
2579                Callee::Kernel {
2580                    recv: recv_ty,
2581                    op: method.name.clone(),
2582                },
2583            );
2584            return check_instant_kernel_method(method, args, span, ctx);
2585        }
2586        // #2341 (decision A): an HTTP handler's `headers` — `get`, `bearer`,
2587        // `clientIp`.
2588        Ty::RequestHeaders => {
2589            ctx.callees.insert(
2590                expr_id,
2591                Callee::Kernel {
2592                    recv: recv_ty,
2593                    op: method.name.clone(),
2594                },
2595            );
2596            return check_request_headers_method(method, args, span, ctx);
2597        }
2598        // #2380 (L5): an `Oidc` binder's claims — `get`, `has`.
2599        Ty::TokenClaims => {
2600            ctx.callees.insert(
2601                expr_id,
2602                Callee::Kernel {
2603                    recv: recv_ty,
2604                    op: method.name.clone(),
2605                },
2606            );
2607            return check_token_claims_method(method, args, span, ctx);
2608        }
2609        // v0.110 (ADR 0142): the `Bytes` kernel — `length`/`toBase64`/`decodeUtf8`.
2610        Ty::Base(BaseType::Bytes) => {
2611            ctx.callees.insert(
2612                expr_id,
2613                Callee::Kernel {
2614                    recv: recv_ty,
2615                    op: method.name.clone(),
2616                },
2617            );
2618            return check_bytes_kernel_method(method, args, span, ctx);
2619        }
2620        // v0.22a: the string kernel (ADR 0046).
2621        Ty::Base(BaseType::String) => {
2622            ctx.callees.insert(
2623                expr_id,
2624                Callee::Kernel {
2625                    recv: recv_ty,
2626                    op: method.name.clone(),
2627                },
2628            );
2629            return check_string_kernel_method(method, args, span, ctx);
2630        }
2631        // v0.22a: the Option/Result combinators as kernel methods (ADR 0048).
2632        Ty::Option(inner) => {
2633            ctx.callees.insert(
2634                expr_id,
2635                Callee::Kernel {
2636                    recv: recv_ty,
2637                    op: method.name.clone(),
2638                },
2639            );
2640            return check_option_kernel_method(method, args, *inner, span, ctx);
2641        }
2642        Ty::Result(ok, err) => {
2643            ctx.callees.insert(
2644                expr_id,
2645                Callee::Kernel {
2646                    recv: recv_ty,
2647                    op: method.name.clone(),
2648                },
2649            );
2650            return check_result_kernel_method(method, args, *ok, *err, span, ctx);
2651        }
2652        // The `Effect[Result[T, E]]` combinators (§2.8.3): `mapOk`/`mapErr`/
2653        // `flatMapOk`/`flatMapErr` on the universal cross-context shape. Only an
2654        // `Effect` wrapping a `Result` has methods; any other `Effect[_]` falls
2655        // through to the "no methods" error below.
2656        Ty::Effect(inner) => {
2657            if let Ty::Result(ok, err) = &*tys.get(*inner) {
2658                ctx.callees.insert(
2659                    expr_id,
2660                    Callee::Kernel {
2661                        recv: recv_ty,
2662                        op: method.name.clone(),
2663                    },
2664                );
2665                return check_effect_result_kernel_method(method, args, *ok, *err, span, ctx);
2666            }
2667        }
2668        _ => {}
2669    }
2670    // Find a named type for the receiver, then look up its instance methods.
2671    let type_name = match &*tys.get(recv_ty) {
2672        Ty::Named { name, .. } => name.clone(),
2673        _ => {
2674            ctx.errors.push(CompileError::new(
2675                "bynk.types.method_on_non_named_type",
2676                method.span,
2677                format!(
2678                    "type `{}` has no methods — only user-declared types support method calls",
2679                    recv_ty.display(tys)
2680                ),
2681            ));
2682            return None;
2683        }
2684    };
2685    // Agent handler dispatch: when the receiver is an agent instance, look
2686    // up the method against the agent's declared `on call` handlers and
2687    // resolve to the handler's return type.
2688    if let Some(agent) = ctx.input.agents.get(&type_name).cloned() {
2689        let Some(handler) = agent.handlers.iter().find(|h| {
2690            h.method_name
2691                .as_ref()
2692                .is_some_and(|n| n.name == method.name)
2693        }) else {
2694            ctx.errors.push(CompileError::new(
2695                "bynk.agent.handler_not_found",
2696                method.span,
2697                format!(
2698                    "agent `{}` has no handler named `{}`",
2699                    type_name, method.name
2700                ),
2701            ));
2702            for a in args {
2703                let _ = type_of(a, None, ctx);
2704            }
2705            return None;
2706        };
2707        ctx.callees.insert(
2708            expr_id,
2709            Callee::Agent {
2710                agent: type_name.clone(),
2711                handler: method.name.clone(),
2712            },
2713        );
2714        // #304: the handler is a first-class index symbol, keyed by the
2715        // compound `"Agent.handler"` name — same convention and same
2716        // recorded-regardless-of-downstream-errors placement as the
2717        // ordinary instance-method call below.
2718        ctx.refs.record(
2719            method.span,
2720            SymbolKind::Handler,
2721            &format!("{type_name}.{}", method.name),
2722        );
2723        if handler.params.len() != args.len() {
2724            ctx.errors.push(CompileError::new(
2725                "bynk.agent.handler_arity",
2726                method.span,
2727                format!(
2728                    "agent handler `{}.{}` expects {} argument(s), but {} were given",
2729                    type_name,
2730                    method.name,
2731                    handler.params.len(),
2732                    args.len()
2733                ),
2734            ));
2735            for a in args {
2736                let _ = type_of(a, None, ctx);
2737            }
2738            return None;
2739        }
2740        for (p, arg) in handler.params.iter().zip(args.iter()) {
2741            let pty = resolve_type_ref(&p.type_ref, &ctx.input.types, tys);
2742            let arg_ty = type_of(arg, pty, ctx);
2743            if let (Some(a), Some(p_ty)) = (arg_ty, pty.as_ref())
2744                && !compatible(a, *p_ty, tys)
2745            {
2746                ctx.errors.push(CompileError::new(
2747                    "bynk.types.argument_mismatch",
2748                    arg.span,
2749                    format!(
2750                        "argument has type `{}`, but `{}.{}` expects `{}`",
2751                        a.display(tys),
2752                        type_name,
2753                        method.name,
2754                        p_ty.display(tys)
2755                    ),
2756                ));
2757            }
2758        }
2759        return resolve_type_ref(&handler.return_type, &ctx.input.types, tys);
2760    }
2761    let table = ctx
2762        .input
2763        .methods
2764        .get(&type_name)
2765        .cloned()
2766        .unwrap_or_default();
2767    let Some(method_decl) = table.instance.get(&method.name).cloned() else {
2768        // #561: a refined receiver inherits its base type's read-only kernel
2769        // methods as a fallback *after* its own declared methods (DECISION D).
2770        // A refined value erases to its base at runtime, so the base methods
2771        // already apply bit-for-bit; only the checker had to be taught to look.
2772        // Results are base-typed (DECISION B) and refined arguments widen via
2773        // `compatible`, so the base kernel checkers need no change. The match
2774        // is `Refined` only — opaque types deliberately do not widen, so they
2775        // keep reporting `method_not_found`. `Bool` (and any base without a
2776        // kernel) inherits nothing and falls through to the same error.
2777        if let Ty::Named {
2778            kind: NamedKind::Refined(base),
2779            ..
2780        } = &*tys.get(recv_ty)
2781        {
2782            if !matches!(base, BaseType::Bool) {
2783                ctx.callees.insert(
2784                    expr_id,
2785                    Callee::Kernel {
2786                        recv: recv_ty,
2787                        op: method.name.clone(),
2788                    },
2789                );
2790            }
2791            match base {
2792                BaseType::Int | BaseType::Float => {
2793                    return check_numeric_kernel_method(method, args, *base, span, ctx);
2794                }
2795                BaseType::String => return check_string_kernel_method(method, args, span, ctx),
2796                BaseType::Duration => return check_duration_kernel_method(method, args, span, ctx),
2797                BaseType::Instant => return check_instant_kernel_method(method, args, span, ctx),
2798                BaseType::Bytes => return check_bytes_kernel_method(method, args, span, ctx),
2799                BaseType::Bool => {}
2800            }
2801        }
2802        ctx.errors.push(CompileError::new(
2803            "bynk.types.method_not_found",
2804            method.span,
2805            format!(
2806                "type `{}` has no instance method named `{}`",
2807                type_name, method.name
2808            ),
2809        ));
2810        return None;
2811    };
2812    // v0.36 (ADR 0069): the method is a first-class index symbol, keyed by the
2813    // compound `"Type.method"` name. Recorded already-spelled from the resolved
2814    // receiver type; the bare edge resolves through the same `uses`/`consumes`
2815    // qualification as cross-file type references.
2816    ctx.refs.record(
2817        method.span,
2818        SymbolKind::Method,
2819        &format!("{type_name}.{}", method.name),
2820    );
2821    ctx.callees
2822        .insert(expr_id, Callee::Method(Arc::clone(&method_decl)));
2823    // #594: a generic instance method — one on a generic receiver, or one
2824    // carrying its own type parameters — resolves its parameter and return
2825    // types against a substitution seeded from the receiver's type arguments,
2826    // then infers the method's own parameters from the argument types
2827    // (argument-directed unification, ADR 0029). A method on a non-generic
2828    // receiver with no own type parameters takes the plain path below,
2829    // byte-identically to before.
2830    let recv_type_params: Vec<String> = ctx
2831        .input
2832        .types
2833        .get(&type_name)
2834        .map(|d| d.type_params.iter().map(|p| p.name.name.clone()).collect())
2835        .unwrap_or_default();
2836    if !recv_type_params.is_empty() || !method_decl.type_params.is_empty() {
2837        return check_generic_method_call(
2838            &type_name,
2839            &recv_type_params,
2840            recv_ty,
2841            &method_decl,
2842            method,
2843            args,
2844            ctx,
2845        );
2846    }
2847    // Param count excludes the implicit `self`.
2848    if method_decl.params.len() != args.len() {
2849        ctx.errors.push(
2850            CompileError::new(
2851                "bynk.types.method_arity",
2852                method.span,
2853                format!(
2854                    "method `{}.{}` expects {} argument(s), but {} were given",
2855                    type_name,
2856                    method.name,
2857                    method_decl.params.len(),
2858                    args.len()
2859                ),
2860            )
2861            .with_label(method_decl.name.ident().span, "method declared here"),
2862        );
2863        for a in args {
2864            let _ = type_of(a, None, ctx);
2865        }
2866        return None;
2867    }
2868    let mut ok = true;
2869    for (i, (param, arg)) in method_decl.params.iter().zip(args.iter()).enumerate() {
2870        record_param_hint(ctx.hints, &param.name.name, arg);
2871        let expected = resolve_type_ref(&param.type_ref, &ctx.input.types, tys);
2872        let actual = type_of(arg, expected, ctx);
2873        let (Some(actual), Some(expected)) = (actual, expected) else {
2874            ok = false;
2875            continue;
2876        };
2877        if !compatible(actual, expected, tys) {
2878            ctx.errors.push(CompileError::new(
2879                "bynk.types.argument_mismatch",
2880                arg.span,
2881                format!(
2882                    "argument {} to `{}.{}` has type `{}`, but parameter `{}` expects `{}`",
2883                    i + 1,
2884                    type_name,
2885                    method.name,
2886                    actual.display(tys),
2887                    param.name.name,
2888                    expected.display(tys)
2889                ),
2890            ));
2891            ok = false;
2892        }
2893    }
2894    let _ = span;
2895    if !ok {
2896        return None;
2897    }
2898    resolve_type_ref(&method_decl.return_type, &ctx.input.types, tys)
2899}
2900
2901/// #594: type-check a call to a generic instance method — one attached to a
2902/// generic type (so `self` carries the type's parameters) and/or carrying its
2903/// own `[U]` parameters. Mirrors [`check_generic_call`] (the free-function
2904/// path, ADR 0029): resolve the method's parameter/return patterns with the
2905/// rigid vars in scope, seed the substitution from the receiver's concrete type
2906/// arguments, then drive argument-directed inference over the method's own
2907/// parameters. The receiver's parameters are already ground (from the receiver
2908/// type), so only the method's own parameters need inferring.
2909fn check_generic_method_call(
2910    type_name: &str,
2911    recv_type_params: &[String],
2912    recv_ty: TyId,
2913    method_decl: &FnDecl,
2914    method: &Ident,
2915    args: &[Expr],
2916    ctx: &mut Ctx,
2917) -> Option<TyId> {
2918    let tys = ctx.tys;
2919    // Rigid vars: the receiver type's parameters plus the method's own.
2920    let mut vars: HashSet<String> = recv_type_params.iter().cloned().collect();
2921    for tp in &method_decl.type_params {
2922        vars.insert(tp.name.name.clone());
2923    }
2924    // Param count excludes the implicit `self`.
2925    if method_decl.params.len() != args.len() {
2926        ctx.errors.push(
2927            CompileError::new(
2928                "bynk.types.method_arity",
2929                method.span,
2930                format!(
2931                    "method `{}.{}` expects {} argument(s), but {} were given",
2932                    type_name,
2933                    method.name,
2934                    method_decl.params.len(),
2935                    args.len()
2936                ),
2937            )
2938            .with_label(method_decl.name.ident().span, "method declared here"),
2939        );
2940        for a in args {
2941            let _ = type_of(a, None, ctx);
2942        }
2943        return None;
2944    }
2945    // Seed the substitution from the receiver's concrete type arguments: the
2946    // type's parameters are ground the moment the receiver's type is known
2947    // (`self.map(g)` on a `Box[User]` binds the type's `A` to `User`; on `self`
2948    // it binds `A` to its own rigid var). An arity mismatch here means the
2949    // receiver was under-applied — reported where the receiver was typed — so we
2950    // leave those parameters unseeded and fail below as uninferable.
2951    let mut subst: HashMap<String, TyId> = HashMap::new();
2952    if let Ty::Named {
2953        args: recv_args, ..
2954    } = &*tys.get(recv_ty)
2955        && recv_args.len() == recv_type_params.len()
2956    {
2957        for (name, arg) in recv_type_params.iter().zip(recv_args.iter()) {
2958            subst.insert(name.clone(), *arg);
2959        }
2960    }
2961    // Resolve the method's parameter and return patterns with every rigid var in
2962    // scope (a name matching one resolves to `Ty::Var`, not an unknown type).
2963    let var_params: Vec<Option<TyId>> = method_decl
2964        .params
2965        .iter()
2966        .map(|p| resolve_type_ref_in(&p.type_ref, &ctx.input.types, &vars, tys))
2967        .collect();
2968    let ret_pattern = resolve_type_ref_in(&method_decl.return_type, &ctx.input.types, &vars, tys)?;
2969
2970    let mut arg_tys: Vec<Option<TyId>> = vec![None; args.len()];
2971    // Pass 1 — non-lambda arguments (mirrors `check_generic_call`).
2972    for (i, arg) in args.iter().enumerate() {
2973        if matches!(arg.kind, ExprKind::Lambda(_)) {
2974            continue;
2975        }
2976        let expected = var_params[i].map(|p| substitute(p, &subst, tys));
2977        let ty = type_of(arg, expected, ctx);
2978        if let (Some(pattern), Some(actual)) = (var_params[i], ty)
2979            && !unify(pattern, actual, &mut subst, tys)
2980        {
2981            ctx.errors.push(CompileError::new(
2982                "bynk.generics.type_arg_mismatch",
2983                arg.span,
2984                format!(
2985                    "argument {} infers a type for `{}.{}`'s type parameter that conflicts with an earlier argument or the receiver",
2986                    i + 1,
2987                    type_name,
2988                    method.name
2989                ),
2990            ));
2991            return None;
2992        }
2993        arg_tys[i] = ty;
2994    }
2995    // Pass 2 — lambda arguments, against the now-substituted expecteds.
2996    for (i, arg) in args.iter().enumerate() {
2997        if !matches!(arg.kind, ExprKind::Lambda(_)) {
2998            continue;
2999        }
3000        let expected = var_params[i].map(|p| substitute(p, &subst, tys));
3001        let params_unconstrained = expected.is_some_and(|e| {
3002            matches!(&*tys.get(e), Ty::Fn { params, .. }
3003                if params.iter().any(|p| contains_var(*p, tys)))
3004        });
3005        let fully_annotated = matches!(
3006            &arg.kind,
3007            ExprKind::Lambda(l) if l.params.iter().all(|p| p.type_ref.is_some())
3008        );
3009        if params_unconstrained && !fully_annotated {
3010            ctx.errors.push(
3011                CompileError::new(
3012                    "bynk.generics.uninferable_type_arg",
3013                    arg.span,
3014                    format!(
3015                        "the lambda's parameter types depend on `{}.{}`'s type parameters, which the receiver and other arguments do not determine",
3016                        type_name, method.name
3017                    ),
3018                )
3019                .with_note("annotate the lambda's parameters"),
3020            );
3021            return None;
3022        }
3023        let ty = if params_unconstrained {
3024            type_of(arg, None, ctx)
3025        } else {
3026            type_of(arg, expected, ctx)
3027        };
3028        if let (Some(pattern), Some(actual)) = (var_params[i], ty)
3029            && !unify(pattern, actual, &mut subst, tys)
3030        {
3031            ctx.errors.push(CompileError::new(
3032                "bynk.generics.type_arg_mismatch",
3033                arg.span,
3034                format!(
3035                    "the lambda's type conflicts with `{}.{}`'s inferred type arguments",
3036                    type_name, method.name
3037                ),
3038            ));
3039            return None;
3040        }
3041        arg_tys[i] = ty;
3042    }
3043    // Every one of the method's own type parameters must now be determined (the
3044    // receiver's parameters were seeded above).
3045    for tp in &method_decl.type_params {
3046        if !subst.contains_key(&tp.name.name) {
3047            ctx.errors.push(
3048                CompileError::new(
3049                    "bynk.generics.uninferable_type_arg",
3050                    method.span,
3051                    format!(
3052                        "type parameter `{}` of `{}.{}` is not inferable from the receiver or the arguments",
3053                        tp.name.name, type_name, method.name
3054                    ),
3055                )
3056                .with_label(tp.span, "declared here"),
3057            );
3058            return None;
3059        }
3060    }
3061    // Final compatibility over the fully-ground parameter types.
3062    let mut ok = true;
3063    for (i, (pattern, arg)) in var_params.iter().zip(args).enumerate() {
3064        record_param_hint(ctx.hints, &method_decl.params[i].name.name, arg);
3065        let (Some(pattern), Some(arg_ty)) = (pattern, arg_tys[i].as_ref()) else {
3066            continue;
3067        };
3068        let ground = substitute(*pattern, &subst, tys);
3069        if !compatible(*arg_ty, ground, tys) {
3070            ctx.errors.push(CompileError::new(
3071                "bynk.types.argument_mismatch",
3072                arg.span,
3073                format!(
3074                    "argument {} to `{}.{}` has type `{}`, but `{}` is expected",
3075                    i + 1,
3076                    type_name,
3077                    method.name,
3078                    arg_ty.display(tys),
3079                    ground.display(tys)
3080                ),
3081            ));
3082            ok = false;
3083        }
3084    }
3085    if !ok {
3086        return None;
3087    }
3088    // #1702 review: the same call-site bound as a generic function's, over the
3089    // receiver type's parameters and the method's own.
3090    let decl = ctx.input.methods.get(type_name).and_then(|t| {
3091        t.instance
3092            .get(&method.name)
3093            .or_else(|| t.statics.get(&method.name))
3094            .cloned()
3095    });
3096    let compared = match &decl {
3097        Some(decl) if std::ptr::eq(decl.as_ref(), method_decl) => {
3098            super::equality::compared_type_params(decl, ctx)
3099        }
3100        _ => Default::default(),
3101    };
3102    let mut params: Vec<(String, Option<Span>)> = method_decl
3103        .type_params
3104        .iter()
3105        .map(|tp| (tp.name.name.clone(), Some(tp.span)))
3106        .collect();
3107    params.extend(recv_type_params.iter().map(|p| (p.clone(), None)));
3108    let callee = format!("{type_name}.{}", method.name);
3109    if !super::equality::check_compared_args(&callee, method.span, &params, &compared, &subst, ctx)
3110    {
3111        return None;
3112    }
3113    // v0.39 (ADR 0072)-style hint: show the inferred method type arguments after
3114    // the method name (`box.map` [Int] `(f)`). Only the method's own
3115    // parameters — the receiver's are visible in the receiver's type.
3116    if !method_decl.type_params.is_empty() {
3117        let rendered: Option<Vec<String>> = method_decl
3118            .type_params
3119            .iter()
3120            .map(|tp| subst.get(&tp.name.name).map(|t| t.display(tys)))
3121            .collect();
3122        if let Some(parts) = rendered {
3123            ctx.hints
3124                .record(method.span, format!("[{}]", parts.join(", ")));
3125        }
3126    }
3127    Some(substitute(ret_pattern, &subst, tys))
3128}
3129
3130/// v0.178 (#662) / v0.182 (#664): resolve a test-body service invocation against
3131/// the target's declared handlers and check its arity and argument types. The
3132/// address form depends on the method name:
3133///
3134/// - `svc.call(args)` — the `on call` handler (#662);
3135/// - `svc.<VERB>("/path", …)` — an http route (`GET`/`POST`/`PUT`/`PATCH`/`DELETE`);
3136/// - `svc.schedule("<expr>", …)` — a cron handler by its schedule string;
3137/// - `svc.message(msg)` — the queue message handler.
3138///
3139/// #1934 (re-opening ADR 0203): the call is typed by what it yields. A
3140/// resolved handler yields its declared return type. An http
3141/// address carrying a `Wire(…)` argument, or naming a method the path declares
3142/// no handler for, yields `Effect[HttpOutcome[T]]` instead (`by Nobody`, which
3143/// lives on the statement, is applied by [`nobody_outcome`]); an asset fetch
3144/// yields `Effect[HttpResult[()]]`. The call-site principal (`by <Actor>`) is
3145/// checked separately at the statement level. Returns `None` in an arm that
3146/// reported an error.
3147fn check_test_service_address(
3148    sig: &TestServiceSig,
3149    id: &Ident,
3150    method: &Ident,
3151    args: &[Expr],
3152    // P6.0 (#1139): the outer `MethodCall` expression's own identity.
3153    expr_id: ExprId,
3154    ctx: &mut Ctx,
3155) -> Option<TyId> {
3156    use bynk_syntax::ast::{ExprKind as EK, HandlerKind};
3157
3158    // `svc.call(...)` — the `on call` handler (Slice 0).
3159    if method.name == "call" {
3160        ctx.callees.insert(
3161            expr_id,
3162            Callee::TestService {
3163                service: id.name.clone(),
3164                address: "call".to_string(),
3165            },
3166        );
3167        let Some(handler) = sig.call_handler() else {
3168            let message = match &sig.protocol {
3169                Some(protocol) => format!(
3170                    "`{}` is a `from {protocol}` service and has no `on call` handler to invoke",
3171                    id.name
3172                ),
3173                None => format!("service `{}` has no `on call` handler to invoke", id.name),
3174            };
3175            ctx.errors.push(
3176                CompileError::new("bynk.test.service_no_call_handler", method.span, message)
3177                    .with_note(
3178                        "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)`",
3179                    ),
3180            );
3181            for a in args {
3182                let _ = type_of(a, None, ctx);
3183            }
3184            return None;
3185        };
3186        let params = handler.params.clone();
3187        check_address_args(&id.name, "call", &params, args, method.span, ctx);
3188        return handler_outcome(handler, ctx);
3189    }
3190
3191    // `svc.<VERB>("/path", …)` — an http route. The first argument is the route
3192    // *pattern* (a compile-time-resolved name), the rest are positional path
3193    // params then the body, matched against the handler's declared params.
3194    // `HttpMethod::from_ident` is the single source of truth the emitter shares.
3195    if bynk_syntax::ast::HttpMethod::from_ident(&method.name).is_some() {
3196        ctx.callees.insert(
3197            expr_id,
3198            Callee::TestService {
3199                service: id.name.clone(),
3200                address: method.name.clone(),
3201            },
3202        );
3203        // #1883, slice 3 ([DECISION F]): `svc.GET(style)` — an `Asset` as the
3204        // address. The platform serves an asset before the Worker runs, so no
3205        // handler is involved: the call answers with the file the target's
3206        // Worker ships, as the platform would (`HttpResult.Raw`).
3207        if let Some(first) = args.first()
3208            && !matches!(first.kind, EK::StrLit(_))
3209            && is_asset_value(first, ctx)
3210        {
3211            let _ = type_of(first, None, ctx);
3212            // Review of #1922: only an HTTP service fronts a Worker the
3213            // platform serves assets beside.
3214            if sig.protocol.as_deref() != Some("http") {
3215                ctx.errors.push(
3216                    CompileError::new(
3217                        "bynk.test.service_bad_address",
3218                        method.span,
3219                        format!(
3220                            "`{}` is not a `from http` service, so it cannot fetch an asset",
3221                            id.name
3222                        ),
3223                    )
3224                    .with_note("fetch an asset through one of the target's `from http` services: `api.GET(asset)`"),
3225                );
3226                // Review of #1923: still type the rest, as every other
3227                // error arm here does, so a mistake inside them is reported.
3228                for a in &args[1..] {
3229                    let _ = type_of(a, None, ctx);
3230                }
3231                return None;
3232            }
3233            if method.name != "GET" {
3234                ctx.errors.push(
3235                    CompileError::new(
3236                        "bynk.test.asset_address_method",
3237                        method.span,
3238                        format!(
3239                            "an asset is fetched with `{}.GET(asset)`, not `{}`",
3240                            id.name, method.name
3241                        ),
3242                    )
3243                    .with_note(
3244                        "the platform serves an asset to `GET` and answers every other method with `405`",
3245                    ),
3246                );
3247            }
3248            if args.len() > 1 {
3249                ctx.errors.push(CompileError::new(
3250                    "bynk.test.service_call_arity",
3251                    args[1].span,
3252                    format!(
3253                        "`{}.{}(asset)` takes the asset alone; remove the other arguments",
3254                        id.name, method.name
3255                    ),
3256                ));
3257                for a in &args[1..] {
3258                    let _ = type_of(a, None, ctx);
3259                }
3260            }
3261            ctx.callees.insert(
3262                expr_id,
3263                Callee::TestAssetFetch {
3264                    service: id.name.clone(),
3265                },
3266            );
3267            // `Raw(body, contentType)` or `NotFound`: no handler's `T` is involved.
3268            let unit = ctx.tys.intern(Ty::Unit);
3269            return Some(effect_of(ctx.tys.intern(Ty::HttpResult(unit)), ctx));
3270        }
3271        let Some(EK::StrLit(path)) = args.first().map(|a| &a.kind) else {
3272            ctx.errors.push(
3273                CompileError::new(
3274                    "bynk.test.service_bad_address",
3275                    method.span,
3276                    format!(
3277                        "`{}.{}` addresses an http route, so its first argument must be the route pattern string (e.g. `\"/todos\"`)",
3278                        id.name, method.name
3279                    ),
3280                ),
3281            );
3282            for a in args {
3283                let _ = type_of(a, None, ctx);
3284            }
3285            return None;
3286        };
3287        // The classification recorded above (verb only) is enriched here,
3288        // now that the route pattern — the thing `sig.handlers` is actually
3289        // matched against below — is known; a later go-to-definition
3290        // consumer needs the path, not just the verb, to resolve a handler.
3291        ctx.callees.insert(
3292            expr_id,
3293            Callee::TestService {
3294                service: id.name.clone(),
3295                address: format!("{} {path}", method.name),
3296            },
3297        );
3298        let matched = sig.handlers.iter().find(|h| {
3299            matches!(&h.kind, HandlerKind::Http { method: m, path: p } if m.as_str() == method.name && p == path)
3300        });
3301        let Some(handler) = matched else {
3302            // #707: the method has no handler at this path. If the *path* is
3303            // declared (for some other method), this is a **wrong-method** call —
3304            // the `405` fall-through test. Allow it: it drives the router with a
3305            // method the path has no handler for and observes `Rejected(
3306            // MethodNotAllowed)`. There is no handler, so no args are matched (a
3307            // `405` is synthesised before the body is read), and the outcome is an
3308            // `HttpOutcome` over `()`: no handler's `T` is involved.
3309            let path_declared = sig
3310                .handlers
3311                .iter()
3312                .any(|h| matches!(&h.kind, HandlerKind::Http { path: p, .. } if p == path));
3313            if path_declared {
3314                let _ = type_of(&args[0], None, ctx);
3315                // A wrong-method call has no handler, so it takes only the path;
3316                // diagnose extra args rather than silently dropping them (the
3317                // lowering forwards only method+path to the generic driver).
3318                if args.len() > 1 {
3319                    ctx.errors.push(
3320                        CompileError::new(
3321                            "bynk.test.service_call_arity",
3322                            method.span,
3323                            format!(
3324                                "`{}.{}(\"{}\")` is a wrong-method `405` test and takes only the route path, but {} argument(s) were given",
3325                                id.name,
3326                                method.name,
3327                                path,
3328                                args.len() - 1
3329                            ),
3330                        )
3331                        .with_note("a wrong-method call reaches no handler, so it passes no body or params"),
3332                    );
3333                    for a in &args[1..] {
3334                        let _ = type_of(a, None, ctx);
3335                    }
3336                }
3337                let unit = ctx.tys.intern(Ty::Unit);
3338                return Some(effect_of(ctx.tys.intern(Ty::HttpOutcome(unit)), ctx));
3339            }
3340            ctx.errors.push(
3341                CompileError::new(
3342                    "bynk.test.service_unknown_route",
3343                    method.span,
3344                    format!(
3345                        "`{}` declares no route at `\"{}\"` (no handler for any method)",
3346                        id.name, path
3347                    ),
3348                )
3349                .with_note("the path must match a declared route; drive a wrong method against an existing path to test the `405` fall-through"),
3350            );
3351            for a in args {
3352                let _ = type_of(a, None, ctx);
3353            }
3354            return None;
3355        };
3356        let params = handler.params.clone();
3357        // Type the route-pattern string as an ordinary string; check the rest.
3358        let _ = type_of(&args[0], None, ctx);
3359        check_address_args(
3360            &id.name,
3361            &method.name,
3362            &params,
3363            &args[1..],
3364            method.span,
3365            ctx,
3366        );
3367        // A `Wire(…)` argument reaches the router unvalidated, so the call
3368        // observes the boundary as well as the handler: the raw driver decodes
3369        // an `HttpOutcome`. The emitter picks that driver on the same test.
3370        let outcome = handler_outcome(handler, ctx);
3371        if args[1..].iter().any(|a| matches!(a.kind, EK::Wire(_))) {
3372            return outcome.map(|t| wire_outcome(t, ctx));
3373        }
3374        return outcome;
3375    }
3376
3377    // `svc.schedule("<expr>", …)` — a cron handler, matched by its schedule.
3378    if method.name == "schedule" {
3379        ctx.callees.insert(
3380            expr_id,
3381            Callee::TestService {
3382                service: id.name.clone(),
3383                address: "schedule".to_string(),
3384            },
3385        );
3386        let Some(EK::StrLit(expr)) = args.first().map(|a| &a.kind) else {
3387            ctx.errors.push(CompileError::new(
3388                "bynk.test.service_bad_address",
3389                method.span,
3390                format!(
3391                    "`{}.schedule` addresses a cron handler, so its first argument must be the schedule string",
3392                    id.name
3393                ),
3394            ));
3395            for a in args {
3396                let _ = type_of(a, None, ctx);
3397            }
3398            return None;
3399        };
3400        // Enrich with the schedule expression `sig.handlers` is matched
3401        // against below — same reasoning as the http-verb branch above.
3402        ctx.callees.insert(
3403            expr_id,
3404            Callee::TestService {
3405                service: id.name.clone(),
3406                address: format!("schedule {expr}"),
3407            },
3408        );
3409        let matched = sig
3410            .handlers
3411            .iter()
3412            .find(|h| matches!(&h.kind, HandlerKind::Cron { expr: e } if e == expr));
3413        let Some(handler) = matched else {
3414            ctx.errors.push(CompileError::new(
3415                "bynk.test.service_unknown_route",
3416                method.span,
3417                format!(
3418                    "`{}` declares no `on schedule(\"{}\")` handler",
3419                    id.name, expr
3420                ),
3421            ));
3422            for a in args {
3423                let _ = type_of(a, None, ctx);
3424            }
3425            return None;
3426        };
3427        let params = handler.params.clone();
3428        let _ = type_of(&args[0], None, ctx);
3429        check_address_args(&id.name, "schedule", &params, &args[1..], method.span, ctx);
3430        return handler_outcome(handler, ctx);
3431    }
3432
3433    // `svc.message(msg)` — the queue message handler.
3434    if method.name == "message" {
3435        ctx.callees.insert(
3436            expr_id,
3437            Callee::TestService {
3438                service: id.name.clone(),
3439                address: "message".to_string(),
3440            },
3441        );
3442        let matched = sig
3443            .handlers
3444            .iter()
3445            .find(|h| matches!(&h.kind, HandlerKind::Message));
3446        let Some(handler) = matched else {
3447            ctx.errors.push(CompileError::new(
3448                "bynk.test.service_unknown_route",
3449                method.span,
3450                format!("`{}` declares no `on message(...)` handler", id.name),
3451            ));
3452            for a in args {
3453                let _ = type_of(a, None, ctx);
3454            }
3455            return None;
3456        };
3457        let params = handler.params.clone();
3458        check_address_args(&id.name, "message", &params, args, method.span, ctx);
3459        return handler_outcome(handler, ctx);
3460    }
3461
3462    // #2052: `svc.event(payload)` / `svc.event(payload, envelope)` — one
3463    // delivery to a `from Events(E)` service's `on event` handler.
3464    if method.name == "event"
3465        && let Some(handler) = sig
3466            .handlers
3467            .iter()
3468            .find(|h| matches!(&h.kind, HandlerKind::Event))
3469    {
3470        ctx.callees.insert(
3471            expr_id,
3472            Callee::TestService {
3473                service: id.name.clone(),
3474                address: "event".to_string(),
3475            },
3476        );
3477        let handler = handler.clone();
3478        check_event_address(sig, id, &handler, args, method.span, ctx);
3479        return handler_outcome(&handler, ctx);
3480    }
3481
3482    // Not a recognised address form for this service.
3483    let proto = sig.protocol.as_deref().unwrap_or("call");
3484    let mut err = CompileError::new(
3485        "bynk.test.service_bad_address",
3486        method.span,
3487        format!(
3488            "`{}.{}` is not a way to address a `from {proto}` service in a test body",
3489            id.name, method.name
3490        ),
3491    );
3492    if method.name == "event" {
3493        err = err.with_note(
3494            "only a `from Events(E)` service is addressed with `svc.event(e)`; it delivers one event to the service's `on event` handler",
3495        );
3496    }
3497    ctx.errors.push(err);
3498    for a in args {
3499        let _ = type_of(a, None, ctx);
3500    }
3501    None
3502}
3503
3504/// #1934: what a case's call to `handler` yields — its declared return type,
3505/// an `Effect[_]` (`bynk.service.return_not_effect`). `None` if the type does
3506/// not resolve in the case, which a checked target's handler always does.
3507fn handler_outcome(handler: &TestHandler, ctx: &mut Ctx) -> Option<TyId> {
3508    resolve_type_ref(&handler.return_type, &ctx.input.types, ctx.tys)
3509}
3510
3511/// `Effect[ty]`.
3512fn effect_of(ty: TyId, ctx: &Ctx) -> TyId {
3513    ctx.tys.intern(Ty::Effect(ty))
3514}
3515
3516/// #1934: an http handler's outcome `Effect[HttpResult[T]]` as a `system`-tier
3517/// case observes it past the handler's types: `Effect[HttpOutcome[T]]`.
3518fn wire_outcome(outcome: TyId, ctx: &Ctx) -> TyId {
3519    let tys = ctx.tys;
3520    let inner = match &*tys.get(outcome) {
3521        Ty::Effect(t) => *t,
3522        _ => outcome,
3523    };
3524    let t = match &*tys.get(inner) {
3525        Ty::HttpResult(t) => *t,
3526        _ => return outcome,
3527    };
3528    tys.intern(Ty::Effect(tys.intern(Ty::HttpOutcome(t))))
3529}
3530
3531/// #1934: the type an effect-let binds for `value` driven `by Nobody`. The
3532/// no-credential driver decodes an `HttpOutcome` (a `401` from the auth seam is
3533/// `Rejected(Unauthorized)`), so a typed http address's `Effect[HttpResult[T]]`
3534/// becomes `Effect[HttpOutcome[T]]`; anything else is unchanged.
3535/// #2155: on an actor sum with an open member (`Visitor`), say why the seam
3536/// never answers `401`.
3537fn with_open_sum_note(err: CompileError, handler: &TestHandler, ctx: &Ctx) -> CompileError {
3538    let open = handler
3539        .by_clause
3540        .as_ref()
3541        .is_some_and(|by| by.is_sum() && crate::actors::sum_has_open_member(by, &ctx.test_actors));
3542    if open {
3543        err.with_note(
3544            "this actor sum admits an open member (`Visitor`): a request with a missing or bad credential resolves as it, so the seam never answers `401`",
3545        )
3546    } else {
3547        err
3548    }
3549}
3550
3551/// #2135: whether `name` is a reserved bad-signature principal — `Forged`
3552/// (a wrong secret) or `Stale` (a timestamp past the tolerance).
3553pub(crate) fn is_bad_signature_principal(name: &str) -> bool {
3554    matches!(name, "Forged" | "Stale")
3555}
3556
3557pub(crate) fn nobody_outcome(value: &Expr, ty: TyId, ctx: &Ctx) -> TyId {
3558    let is_http_address = matches!(
3559        ctx.callees.get(&value.id),
3560        Some(Callee::TestService { address, .. })
3561            if address.split(' ').next().is_some_and(|m| bynk_syntax::ast::HttpMethod::from_ident(m).is_some())
3562    );
3563    if is_http_address {
3564        wire_outcome(ty, ctx)
3565    } else {
3566        ty
3567    }
3568}
3569
3570/// What identity, if any, a resolved actor expects (v0.182).
3571enum ActorIdentity {
3572    Typed(TyId),
3573    CallerString,
3574    Unit,
3575    Unknown,
3576}
3577
3578/// Resolve an actor name to the identity it carries (v0.182).
3579fn resolve_actor_identity(name: &str, ctx: &Ctx) -> ActorIdentity {
3580    let tys = ctx.tys;
3581    use crate::actors::{Identity, prelude_actor};
3582    if let Some(decl) = ctx.test_actors.get(name) {
3583        // #2019: a refinement (`actor Admin = User where …`) narrows its base by
3584        // a claim and inherits the base's identity: the Worker's Bearer seam
3585        // follows the refinement to its base and mints the base identity
3586        // (`bearer_seam_for`). A refinement whose base is missing or is itself a
3587        // refinement is malformed (`bynk.actor.refinement_base_unsupported`),
3588        // so it resolves to no identity at all rather than passing as `Unit`.
3589        let decl = match &decl.refinement {
3590            Some(r) => match ctx.test_actors.get(&r.base.name) {
3591                Some(base) if base.refinement.is_none() => base,
3592                _ => return ActorIdentity::Unknown,
3593            },
3594            None => decl,
3595        };
3596        return match &decl.identity {
3597            Some(t) => match resolve_type_ref(t, &ctx.input.types, tys) {
3598                Some(ty) => ActorIdentity::Typed(ty),
3599                None => ActorIdentity::Unit,
3600            },
3601            None => ActorIdentity::Unit,
3602        };
3603    }
3604    match prelude_actor(name) {
3605        Some(c) => match c.identity {
3606            Identity::Unit => ActorIdentity::Unit,
3607            Identity::CallerId => ActorIdentity::CallerString,
3608            Identity::Declared(_) => ActorIdentity::Unit,
3609        },
3610        None => ActorIdentity::Unknown,
3611    }
3612}
3613
3614/// The actor a handler runs as: its declared `by <Actor>`, or the protocol
3615/// default (`Call`->`Caller`, `Cron`->`Scheduler`, `Queue`->`Producer`; http
3616/// has no default, so an http handler always declares one) (v0.182).
3617fn handler_actor_name(handler: &TestHandler, protocol: Option<&str>) -> Option<String> {
3618    if let Some(by) = &handler.by_clause {
3619        return Some(by.primary().name.clone());
3620    }
3621    match protocol {
3622        None => Some("Caller".to_string()),
3623        Some("cron") => Some("Scheduler".to_string()),
3624        Some("queue") => Some("Producer".to_string()),
3625        _ => None,
3626    }
3627}
3628
3629/// #1883, slice 3: whether `e` is an `Asset` value — an asset name in scope
3630/// (not shadowed by a local), or a field/expression that types as `Asset` —
3631/// without recording anything. Decides the `svc.GET(asset)` address form.
3632fn is_asset_value(e: &Expr, ctx: &Ctx) -> bool {
3633    match &e.kind {
3634        ExprKind::Ident(id) => ctx
3635            .lookup(id.name.as_str())
3636            .map(|t| matches!(&*ctx.tys.get(t), Ty::Asset))
3637            .unwrap_or_else(|| ctx.input.assets.contains_key(&id.name)),
3638        _ => false,
3639    }
3640}
3641
3642/// v0.182 (#664): resolve the handler a test-body address invocation targets,
3643/// without side effects. Shared by the argument check and the principal check.
3644fn resolve_test_address<'a>(
3645    sig: &'a TestServiceSig,
3646    method: &str,
3647    args: &[Expr],
3648) -> Option<&'a TestHandler> {
3649    use bynk_syntax::ast::{ExprKind as EK, HandlerKind, HttpMethod};
3650    if method == "call" {
3651        return sig.call_handler();
3652    }
3653    if HttpMethod::from_ident(method).is_some() {
3654        let EK::StrLit(path) = &args.first()?.kind else {
3655            return None;
3656        };
3657        return sig.handlers.iter().find(|h| {
3658            matches!(&h.kind, HandlerKind::Http { method: m, path: p } if m.as_str() == method && p == path)
3659        });
3660    }
3661    if method == "schedule" {
3662        let EK::StrLit(expr) = &args.first()?.kind else {
3663            return None;
3664        };
3665        return sig
3666            .handlers
3667            .iter()
3668            .find(|h| matches!(&h.kind, HandlerKind::Cron { expr: e } if e == expr));
3669    }
3670    if method == "message" {
3671        return sig
3672            .handlers
3673            .iter()
3674            .find(|h| matches!(&h.kind, HandlerKind::Message));
3675    }
3676    // #2052: the `on event` handler of a `from Events(E)` service.
3677    if method == "event" {
3678        return sig
3679            .handlers
3680            .iter()
3681            .find(|h| matches!(&h.kind, HandlerKind::Event));
3682    }
3683    None
3684}
3685
3686/// v0.182 (#664): validate the call-site principal of a test `effect_let`
3687/// against the ADDRESSED HANDLER. This is where per-case identity isolation is
3688/// enforced: an identity-carrying handler driven with a unit principal (`by
3689/// Visitor`) or no `by` at all would otherwise emit a call with
3690/// `deps.identity === undefined`. The identity value is typed against the
3691/// handler's required identity type, not the principal actor's.
3692pub(crate) fn check_effect_let_principal(
3693    value: &Expr,
3694    principal: Option<&bynk_syntax::ast::CallSiteActor>,
3695    ctx: &mut Ctx,
3696) {
3697    let tys = ctx.tys;
3698    // #2380 (L5): a call-site principal's claims (`by W("id", claims)`) are a
3699    // `TokenClaims`, and only an `Oidc` actor's handler reads them.
3700    if let Some(p) = principal
3701        && let Some(claims) = &p.claims
3702    {
3703        let want = tys.intern(Ty::TokenClaims);
3704        check_arg(claims, want, "the principal's claims", ctx);
3705        let oidc = ctx.test_actors.get(&p.actor.name).is_some_and(|decl| {
3706            decl.auth
3707                .as_ref()
3708                .is_some_and(|a| a.name.as_str() == "Oidc")
3709        });
3710        if !oidc {
3711            ctx.errors.push(
3712                CompileError::new(
3713                    "bynk.test.principal_claims_not_oidc",
3714                    claims.span,
3715                    format!(
3716                        "`{}` isn't an `Oidc` actor, so its handlers read no token claims",
3717                        p.actor.name
3718                    ),
3719                )
3720                .with_note("claims are passed as `by Workload(\"id\", TokenClaims.of([\"name: value\"]))` for an `Oidc` actor"),
3721            );
3722        }
3723    }
3724    let ExprKind::MethodCall {
3725        receiver,
3726        method,
3727        args,
3728        ..
3729    } = &value.kind
3730    else {
3731        if let Some(p) = principal {
3732            report_principal_actor(p, ctx);
3733        }
3734        return;
3735    };
3736    let ExprKind::Ident(id) = &receiver.kind else {
3737        if let Some(p) = principal {
3738            report_principal_actor(p, ctx);
3739        }
3740        return;
3741    };
3742    let Some(sig) = ctx.test_services.get(&id.name).cloned() else {
3743        if let Some(p) = principal {
3744            report_principal_actor(p, ctx);
3745        }
3746        return;
3747    };
3748    let Some(handler) = resolve_test_address(&sig, &method.name, args).cloned() else {
3749        if let Some(p) = principal
3750            && args.first().is_some_and(|a| is_asset_value(a, ctx))
3751        {
3752            // #1883, slice 3: an asset is public and served before the Worker
3753            // runs, so no principal reaches it.
3754            ctx.errors.push(
3755                CompileError::new(
3756                    "bynk.test.principal_on_asset",
3757                    p.span,
3758                    format!(
3759                        "an asset is public and served before the Worker runs, so `by {}` is meaningless",
3760                        p.actor.name
3761                    ),
3762                )
3763                .with_note("drop the `by` clause on an asset fetch"),
3764            );
3765            if let Some(id) = &p.identity {
3766                let _ = type_of(id, None, ctx);
3767            }
3768            return;
3769        }
3770        if let Some(p) = principal {
3771            // #707: a wrong-method `405` test (an http method whose path is
3772            // declared for *another* method) reaches no handler, so a `by` clause
3773            // is meaningless — reject it clearly. (An unresolvable address of any
3774            // other shape already errors in `check_test_service_address`; only the
3775            // now-allowed wrong-method call would otherwise drop the `by`.)
3776            let wrong_method = bynk_syntax::ast::HttpMethod::from_ident(&method.name).is_some()
3777                && matches!(args.first().map(|a| &a.kind), Some(bynk_syntax::ast::ExprKind::StrLit(path))
3778                    if sig.handlers.iter().any(|h| matches!(&h.kind, bynk_syntax::ast::HandlerKind::Http { path: p, .. } if p == path)));
3779            if wrong_method {
3780                ctx.errors.push(
3781                    CompileError::new(
3782                        "bynk.test.principal_on_wrong_method",
3783                        p.span,
3784                        format!(
3785                            "a wrong-method `405` test reaches no handler, so `by {}` is meaningless",
3786                            p.actor.name
3787                        ),
3788                    )
3789                    .with_note("drop the `by` clause on a wrong-method call"),
3790                );
3791                if let Some(id) = &p.identity {
3792                    let _ = type_of(id, None, ctx);
3793                }
3794            } else {
3795                report_principal_actor(p, ctx);
3796            }
3797        }
3798        return;
3799    };
3800
3801    // #706: `by Nobody` is the reserved "no credential" principal. It drives the
3802    // route with no `Authorization` header so the real auth seam rejects it
3803    // (`401` → `Rejected(Unauthorized)`), so it is valid on any http handler
3804    // regardless of the required identity — the whole point is that *no* valid
3805    // credential is presented. It carries no identity (`by Nobody(...)` is
3806    // meaningless). The `system`-only tier rule is enforced at emit time, like
3807    // `Wire`'s (the checker has no tier).
3808    if let Some(p) = principal
3809        && p.actor.name == "Nobody"
3810    {
3811        // #710-style: `by Nobody` is only *implemented* for a Bearer- or
3812        // (#2135) Signature-secured route — the no-auth driver leaves out the
3813        // `Authorization` header the Bearer seam checks, or the signature
3814        // headers the Signature seam checks. On an unsecured
3815        // (`Visitor`/`None`) or `Oidc` route there is no such seam to reject a
3816        // missing credential, so reject it here rather than emit a call to a
3817        // driver that was never generated.
3818        // #2155: an actor sum is secured when a member is.
3819        let secured = handler
3820            .by_clause
3821            .as_ref()
3822            .is_some_and(|by| crate::actors::route_is_secured(by, &ctx.test_actors));
3823        if !secured {
3824            let err = CompileError::new(
3825                "bynk.test.nobody_needs_secured_route",
3826                p.span,
3827                "`by Nobody` drives the auth seam to a `401`, but this handler's route has no seam that refuses a missing credential",
3828            )
3829            .with_note(
3830                "use `by Nobody` only on a route guarded by a `Bearer` or `Signature` actor; a public (`Visitor`) route has no seam to test",
3831            );
3832            ctx.errors.push(with_open_sum_note(err, &handler, ctx));
3833        }
3834        if let Some(idv) = &p.identity {
3835            ctx.errors.push(CompileError::new(
3836                "bynk.test.actor_no_identity",
3837                p.span,
3838                "`Nobody` presents no credential, so it takes no identity — write `by Nobody`",
3839            ));
3840            let _ = type_of(idv, None, ctx);
3841        }
3842        return;
3843    }
3844
3845    // #2135: `by Forged` / `by Stale` are reserved principals, like `Nobody`:
3846    // the `system` driver signs a `Signature` route with a wrong secret, or
3847    // `tolerance + 60` seconds in the past, so the real seam refuses it. They
3848    // name no actor and take no identity. The `system`-only tier rule is
3849    // enforced with the case's tier (`bynk.test.forged_needs_system`).
3850    if let Some(p) = principal
3851        && is_bad_signature_principal(&p.actor.name)
3852    {
3853        let name = p.actor.name.as_str();
3854        match handler
3855            .by_clause
3856            .as_ref()
3857            .and_then(|by| crate::actors::signature_seam_of_route(by, &ctx.test_actors))
3858        {
3859            None => {
3860                let err = CompileError::new(
3861                    "bynk.test.forged_needs_signature_route",
3862                    p.span,
3863                    format!(
3864                        "`by {name}` sends a bad signature to a `Signature` seam, but this handler's route has no `Signature` seam that refuses one"
3865                    ),
3866                )
3867                .with_note(
3868                    "use `by Forged` and `by Stale` on a route guarded by a `Signature` actor; on a Bearer route, `by Nobody` drives the seam to a `401`",
3869                );
3870                ctx.errors.push(with_open_sum_note(err, &handler, ctx));
3871            }
3872            Some(seam)
3873                if name == "Stale"
3874                    && (seam.timestamp_header.is_none() || seam.tolerance_secs.is_none()) =>
3875            {
3876                ctx.errors.push(
3877                    CompileError::new(
3878                        "bynk.test.stale_needs_timestamp",
3879                        p.span,
3880                        "`by Stale` signs in the past, but this route's `Signature` actor declares no `timestamp` header and `tolerance` for the seam to refuse it by",
3881                    )
3882                    .with_note(
3883                        "declare `timestamp = \"<Header>\"` and `tolerance = <seconds>` on the actor, or test the seam with `by Forged`",
3884                    ),
3885                );
3886            }
3887            Some(_) => {}
3888        }
3889        if let Some(idv) = &p.identity {
3890            ctx.errors.push(CompileError::new(
3891                "bynk.test.actor_no_identity",
3892                p.span,
3893                format!("`{name}` presents a signature, not an identity, so it takes none — write `by {name}`"),
3894            ));
3895            let _ = type_of(idv, None, ctx);
3896        }
3897        return;
3898    }
3899
3900    // #2020: a sum (`by who: User | Visitor`) runs as whichever member
3901    // resolves, so the case may act as any member, each with its own identity.
3902    if let Some(by) = handler.by_clause.as_ref().filter(|by| by.is_sum()) {
3903        check_sum_principal(by, principal, value.span, ctx);
3904        return;
3905    }
3906
3907    let required = handler_actor_name(&handler, sig.protocol.as_deref())
3908        .map(|actor| resolve_actor_identity(&actor, ctx));
3909
3910    match (required, principal) {
3911        (Some(ActorIdentity::Typed(ty)), principal) => match principal {
3912            Some(p) => match resolve_actor_identity(&p.actor.name, ctx) {
3913                ActorIdentity::Unknown => report_principal_actor(p, ctx),
3914                ActorIdentity::Unit | ActorIdentity::CallerString => {
3915                    ctx.errors.push(CompileError::new(
3916                        "bynk.test.principal_identity_mismatch",
3917                        p.span,
3918                        format!(
3919                            "this handler runs as an actor carrying `{}`, but `by {}` supplies no matching identity",
3920                            ty.display(tys),
3921                            p.actor.name
3922                        ),
3923                    ));
3924                    if let Some(idv) = &p.identity {
3925                        let _ = type_of(idv, None, ctx);
3926                    }
3927                }
3928                ActorIdentity::Typed(_) => match &p.identity {
3929                    Some(idv) => {
3930                        let got = type_of(idv, Some(ty), ctx);
3931                        if let Some(g) = got
3932                            && !compatible(g, ty, tys)
3933                        {
3934                            ctx.errors.push(CompileError::new(
3935                                "bynk.types.argument_mismatch",
3936                                idv.span,
3937                                format!(
3938                                    "identity has type `{}`, but the handler expects `{}`",
3939                                    g.display(tys),
3940                                    ty.display(tys)
3941                                ),
3942                            ));
3943                        }
3944                    }
3945                    None => ctx.errors.push(CompileError::new(
3946                        "bynk.test.actor_identity_required",
3947                        p.span,
3948                        format!(
3949                            "actor `{}` carries an identity, so write `by {}(...)`",
3950                            p.actor.name, p.actor.name
3951                        ),
3952                    )),
3953                },
3954            },
3955            None => ctx.errors.push(
3956                CompileError::new(
3957                    "bynk.test.principal_required",
3958                    value.span,
3959                    format!(
3960                        "this handler runs as a verified actor carrying `{}`; the case must act as it with `by <Actor>(<identity>)`",
3961                        ty.display(tys)
3962                    ),
3963                )
3964                .with_note("append a call-site actor, e.g. `... by User(\"alice\")`"),
3965            ),
3966        },
3967        // #1978: an `on call` handler that binds its caller (`by c: Caller`)
3968        // reads `deps.identity`, the calling context's name. A test has no
3969        // calling context, so the case must name one with `by Caller("…")`;
3970        // without it the emitted call carries no identity and fails `tsc`.
3971        (Some(ActorIdentity::CallerString), principal) if handler_binds_caller(&handler, ctx) => {
3972            match principal {
3973                None => ctx.errors.push(
3974                    CompileError::new(
3975                        "bynk.test.actor_identity_required",
3976                        value.span,
3977                        "this handler binds its caller's identity (`by <binder>: Caller`), so the case must name the calling context with `by Caller(\"...\")`",
3978                    )
3979                    .with_note("append a call-site actor, e.g. `... by Caller(\"orders\")`"),
3980                ),
3981                Some(p) => match resolve_actor_identity(&p.actor.name, ctx) {
3982                    ActorIdentity::CallerString if p.identity.is_none() => {
3983                        ctx.errors.push(CompileError::new(
3984                            "bynk.test.actor_identity_required",
3985                            p.span,
3986                            format!(
3987                                "this handler binds its caller's identity, so write `by {}(\"...\")` naming the calling context",
3988                                p.actor.name
3989                            ),
3990                        ))
3991                    }
3992                    ActorIdentity::Unit => {
3993                        ctx.errors.push(CompileError::new(
3994                            "bynk.test.principal_identity_mismatch",
3995                            p.span,
3996                            format!(
3997                                "this handler binds its caller's identity, but `by {}` supplies none — write `by Caller(\"...\")`",
3998                                p.actor.name
3999                            ),
4000                        ));
4001                        if let Some(idv) = &p.identity {
4002                            let _ = type_of(idv, None, ctx);
4003                        }
4004                    }
4005                    _ => report_principal_actor(p, ctx),
4006                },
4007            }
4008        }
4009        (_, Some(p)) => report_principal_actor(p, ctx),
4010        (_, None) => {}
4011    }
4012}
4013
4014/// #2020: check a call-site principal against a handler whose `by` clause is
4015/// an actor sum (`by who: User | Visitor`, or an `on call` sum such as
4016/// `Caller | Legacy`). The handler reads the resolved member as `deps.who`, so
4017/// the case must name a member, with exactly the identity that member
4018/// carries: `by User("ada")` or `by Visitor`. A non-member, or no `by` at all,
4019/// would leave `deps.who` unbuilt.
4020fn check_sum_principal(
4021    by: &bynk_syntax::ast::ByClause,
4022    principal: Option<&bynk_syntax::ast::CallSiteActor>,
4023    value_span: Span,
4024    ctx: &mut Ctx,
4025) {
4026    let members = format!(
4027        "`{}`",
4028        by.actors
4029            .iter()
4030            .map(|a| a.name.as_str())
4031            .collect::<Vec<_>>()
4032            .join(" | ")
4033    );
4034    let Some(p) = principal else {
4035        ctx.errors.push(
4036            CompileError::new(
4037                "bynk.test.principal_required",
4038                value_span,
4039                format!(
4040                    "this handler runs as one of the actors {members}; the case must act as one with `by <Actor>`"
4041                ),
4042            )
4043            .with_note(format!(
4044                "append a call-site actor naming a member, e.g. `... by {}`",
4045                sum_member_example(&by.actors[0].name, ctx)
4046            )),
4047        );
4048        return;
4049    };
4050    let name = &p.actor.name;
4051    if !by.actors.iter().any(|a| &a.name == name) {
4052        if matches!(resolve_actor_identity(name, ctx), ActorIdentity::Unknown) {
4053            report_principal_actor(p, ctx);
4054            return;
4055        }
4056        ctx.errors.push(
4057            CompileError::new(
4058                "bynk.test.principal_identity_mismatch",
4059                p.span,
4060                format!(
4061                    "this handler runs as one of the actors {members}, but `by {name}` names none of them"
4062                ),
4063            )
4064            .with_note("act as a member of the handler's `by` sum"),
4065        );
4066        if let Some(idv) = &p.identity {
4067            let _ = type_of(idv, None, ctx);
4068        }
4069        return;
4070    }
4071    match resolve_actor_identity(name, ctx) {
4072        // The sum's `Caller` arm carries the calling context's name
4073        // (`{ tag: "Caller", identity: string }`), so it must be named.
4074        ActorIdentity::CallerString if p.identity.is_none() => ctx.errors.push(CompileError::new(
4075            "bynk.test.actor_identity_required",
4076            p.span,
4077            format!(
4078                "the `{name}` arm carries the calling context's name, so write `by {name}(\"...\")`"
4079            ),
4080        )),
4081        _ => report_principal_actor(p, ctx),
4082    }
4083}
4084
4085/// An example call-site principal for a sum member, for a note.
4086fn sum_member_example(name: &str, ctx: &Ctx) -> String {
4087    match resolve_actor_identity(name, ctx) {
4088        ActorIdentity::Typed(_) | ActorIdentity::CallerString => format!("{name}(...)"),
4089        ActorIdentity::Unit | ActorIdentity::Unknown => name.to_string(),
4090    }
4091}
4092
4093/// #1978: whether an `on call` handler binds the prelude `Caller`'s identity
4094/// (`by c: Caller`), so its emitted `deps` requires `identity`. This mirrors
4095/// `bynk_lower::lower_actor_seam_ir`, which resolves a sum `by` clause
4096/// (`by who: Caller | …`) as a sum (`deps.who`) before it consults
4097/// [`crate::actors::caller_binder_for`]; hence the `!is_sum()` that
4098/// `caller_binder_for` alone does not need.
4099fn handler_binds_caller(handler: &TestHandler, ctx: &Ctx) -> bool {
4100    use crate::actors::{Identity, prelude_actor};
4101    let Some(by) = &handler.by_clause else {
4102        return false;
4103    };
4104    let Some(binder) = &by.binder else {
4105        return false;
4106    };
4107    let name = &by.primary().name;
4108    matches!(handler.kind, bynk_syntax::ast::HandlerKind::Call)
4109        && !by.is_sum()
4110        && !ctx.test_actors.contains_key(name)
4111        && prelude_actor(name).map(|c| c.identity) == Some(Identity::CallerId)
4112        && !handler.params.iter().any(|p| p.name.name == binder.name)
4113}
4114
4115/// Resolve a call-site principal actor for its own diagnostics (v0.182).
4116fn report_principal_actor(p: &bynk_syntax::ast::CallSiteActor, ctx: &mut Ctx) {
4117    let tys = ctx.tys;
4118    let name = &p.actor.name;
4119    match resolve_actor_identity(name, ctx) {
4120        // A declared actor that resolves to nothing is a malformed refinement,
4121        // already reported at its declaration
4122        // (`bynk.actor.refinement_base_unsupported`). It is still an actor of
4123        // the target, so `unknown_actor` would mislead.
4124        ActorIdentity::Unknown if ctx.test_actors.contains_key(name) => {
4125            if let Some(id) = &p.identity {
4126                let _ = type_of(id, None, ctx);
4127            }
4128        }
4129        // #2135: a reserved bad-signature principal reaching no http route
4130        // (a `call`, an event, a cron or queue address) has no `Signature`
4131        // seam to refuse it.
4132        ActorIdentity::Unknown if is_bad_signature_principal(name) => {
4133            ctx.errors.push(
4134                CompileError::new(
4135                    "bynk.test.forged_needs_signature_route",
4136                    p.span,
4137                    format!("`by {name}` sends a bad signature to a `Signature` seam, but this address reaches no `Signature`-secured http route"),
4138                )
4139                .with_note("use `by Forged` and `by Stale` on an http route guarded by one `Signature` actor"),
4140            );
4141            if let Some(id) = &p.identity {
4142                let _ = type_of(id, None, ctx);
4143            }
4144        }
4145        ActorIdentity::Unknown => {
4146            ctx.errors.push(
4147                CompileError::new(
4148                    "bynk.test.unknown_actor",
4149                    p.actor.span,
4150                    format!("`{name}` is not an actor of the target context or a prelude actor"),
4151                )
4152                .with_note("name an `actor` the target declares, or a prelude actor (`Visitor`, `Caller`, …)"),
4153            );
4154            if let Some(id) = &p.identity {
4155                let _ = type_of(id, None, ctx);
4156            }
4157        }
4158        ActorIdentity::Typed(ty) => match &p.identity {
4159            Some(id) => {
4160                let got = type_of(id, Some(ty), ctx);
4161                if let Some(g) = got
4162                    && !compatible(g, ty, tys)
4163                {
4164                    ctx.errors.push(CompileError::new(
4165                        "bynk.types.argument_mismatch",
4166                        id.span,
4167                        format!(
4168                            "identity has type `{}`, but actor `{name}` expects `{}`",
4169                            g.display(tys),
4170                            ty.display(tys)
4171                        ),
4172                    ));
4173                }
4174            }
4175            None => ctx.errors.push(CompileError::new(
4176                "bynk.test.actor_identity_required",
4177                p.span,
4178                format!("actor `{name}` carries an identity, so `by {name}(...)` needs an identity value"),
4179            )),
4180        },
4181        ActorIdentity::CallerString => {
4182            if let Some(id) = &p.identity {
4183                let _ = type_of(id, None, ctx);
4184            }
4185        }
4186        ActorIdentity::Unit => {
4187            if let Some(id) = &p.identity {
4188                let _ = type_of(id, None, ctx);
4189                ctx.errors.push(CompileError::new(
4190                    "bynk.test.actor_no_identity",
4191                    p.span,
4192                    format!("actor `{name}` has no identity, so write `by {name}` with no argument"),
4193                ));
4194            }
4195        }
4196    }
4197}
4198
4199/// Shared arity + argument-type check for a resolved test-body service address.
4200/// `positional` are the arguments that map to the handler's declared params (for
4201/// http/cron the leading pattern string has already been split off).
4202fn check_address_args(
4203    svc: &str,
4204    addr: &str,
4205    params: &[bynk_syntax::ast::Param],
4206    positional: &[Expr],
4207    err_span: Span,
4208    ctx: &mut Ctx,
4209) {
4210    let tys = ctx.tys;
4211    if params.len() != positional.len() {
4212        ctx.errors.push(
4213            CompileError::new(
4214                "bynk.test.service_call_arity",
4215                err_span,
4216                format!(
4217                    "`{svc}.{addr}` expects {} argument(s), but {} were given",
4218                    params.len(),
4219                    positional.len()
4220                ),
4221            )
4222            // Finding #46: `handler_span` is the target unit's handler — a
4223            // test file and the unit it tests are normally different files.
4224            .with_note("handler declared here"),
4225        );
4226        for a in positional {
4227            let _ = type_of(a, None, ctx);
4228        }
4229        return;
4230    }
4231    for (i, (param, arg)) in params.iter().zip(positional.iter()).enumerate() {
4232        record_param_hint(ctx.hints, &param.name.name, arg);
4233        // Slice C: a `Wire(<String>)` argument is *raw* — it deliberately bypasses
4234        // the param's type so a case can drive the boundary with input the type
4235        // forbids. Validate only that the inner is a `String` (the wire form); the
4236        // `system`-only tier rule is enforced at emit time (where the tier is
4237        // known), alongside `system_needs_wire`. Intercept before the generic
4238        // `type_of`, which would otherwise report the address-arg `Wire` as
4239        // misplaced.
4240        if let bynk_syntax::ast::ExprKind::Wire(inner) = &arg.kind {
4241            let _ = type_of(inner, Some(tys.intern(Ty::Base(BaseType::String))), ctx);
4242            continue;
4243        }
4244        let expected = resolve_type_ref(&param.type_ref, &ctx.input.types, tys);
4245        let arg_ty = type_of(arg, expected, ctx);
4246        if let (Some(a), Some(p)) = (arg_ty, expected)
4247            && !compatible(a, p, tys)
4248        {
4249            ctx.errors.push(CompileError::new(
4250                "bynk.types.argument_mismatch",
4251                arg.span,
4252                format!(
4253                    "argument {} has type `{}`, but `{svc}.{addr}` expects `{}` for `{}`",
4254                    i + 1,
4255                    a.display(tys),
4256                    p.display(tys),
4257                    param.name.name
4258                ),
4259            ));
4260        }
4261    }
4262}
4263
4264/// #2052: arity and argument types of `svc.event(payload)` /
4265/// `svc.event(payload, envelope)`. The payload is typed against the handler's
4266/// first parameter, which `bynk.event.handler_param_type_mismatch` holds to the
4267/// header's event type `E`; the envelope against `EventEnvelope`, whether or
4268/// not the handler declares one (a `via schema(N)` guard reads it either way).
4269fn check_event_address(
4270    sig: &TestServiceSig,
4271    id: &Ident,
4272    handler: &TestHandler,
4273    args: &[Expr],
4274    err_span: Span,
4275    ctx: &mut Ctx,
4276) {
4277    let tys = ctx.tys;
4278    let svc = &id.name;
4279    if args.is_empty() || args.len() > 2 {
4280        ctx.errors.push(
4281            CompileError::new(
4282                "bynk.test.service_call_arity",
4283                err_span,
4284                format!(
4285                    "`{svc}.event` takes the event and, optionally, an `EventEnvelope`, but {} argument(s) were given",
4286                    args.len()
4287                ),
4288            )
4289            .with_note("deliver an event with `svc.event(e)`, or `svc.event(e, env)` to choose its envelope"),
4290        );
4291        for a in args {
4292            let _ = type_of(a, None, ctx);
4293        }
4294        return;
4295    }
4296    // #2052 review: a default envelope names the event's publisher, which must
4297    // resolve to one context, as the service's header resolves `E`.
4298    if args.len() == 1
4299        && let Some(Err(candidates)) = &sig.event_publisher
4300    {
4301        let found = if candidates.is_empty() {
4302            "no context the target declares or consumes declares it".to_string()
4303        } else {
4304            format!(
4305                "it is declared by {}",
4306                candidates
4307                    .iter()
4308                    .map(|c| format!("`{c}`"))
4309                    .collect::<Vec<_>>()
4310                    .join(", ")
4311            )
4312        };
4313        ctx.errors.push(
4314            CompileError::new(
4315                "bynk.test.event_publisher_ambiguous",
4316                err_span,
4317                format!("`{svc}.event(e)` has no single publisher to name in its default envelope: {found}"),
4318            )
4319            .with_note("pass the envelope yourself: `svc.event(e, env)`"),
4320        );
4321    }
4322    let payload = &args[0];
4323    let expected = handler
4324        .params
4325        .first()
4326        .and_then(|p| resolve_type_ref(&p.type_ref, &ctx.input.types, tys));
4327    let arg_ty = type_of(payload, expected, ctx);
4328    if let (Some(a), Some(p)) = (arg_ty, expected)
4329        && !compatible(a, p, tys)
4330    {
4331        let other_event = match &*tys.get(a) {
4332            Ty::Named { name, .. } if sig.known_events.contains(name) => Some(name.clone()),
4333            _ => None,
4334        };
4335        let err = match other_event {
4336            Some(other) => CompileError::new(
4337                "bynk.test.event_not_subscribed",
4338                payload.span,
4339                format!(
4340                    "`{svc}` subscribes to `{}`, not `{other}`",
4341                    p.display(tys)
4342                ),
4343            )
4344            .with_note(
4345                "an event address delivers only the event the service's `from Events(E)` header names",
4346            ),
4347            None => CompileError::new(
4348                "bynk.types.argument_mismatch",
4349                payload.span,
4350                format!(
4351                    "argument 1 has type `{}`, but `{svc}.event` expects the event it subscribes to, `{}`",
4352                    a.display(tys),
4353                    p.display(tys)
4354                ),
4355            ),
4356        };
4357        ctx.errors.push(err);
4358    }
4359    let Some(envelope) = args.get(1) else {
4360        return;
4361    };
4362    let envelope_ref = TypeRef::Named(Ident {
4363        name: "EventEnvelope".to_string(),
4364        span: envelope.span,
4365    });
4366    let expected = resolve_type_ref(&envelope_ref, &ctx.input.types, tys);
4367    let env_ty = type_of(envelope, expected, ctx);
4368    let Some(a) = env_ty else {
4369        return;
4370    };
4371    // `EventEnvelope` is `bynk`'s; a suite names it only when its target
4372    // consumes `bynk`, and no other value is one.
4373    let mismatch = match expected {
4374        Some(p) => !compatible(a, p, tys),
4375        None => true,
4376    };
4377    if mismatch {
4378        let mut err = CompileError::new(
4379            "bynk.types.argument_mismatch",
4380            envelope.span,
4381            format!(
4382                "argument 2 has type `{}`, but `{svc}.event` expects an `EventEnvelope`",
4383                a.display(tys)
4384            ),
4385        );
4386        if expected.is_none() {
4387            err = err.with_note(
4388                "`EventEnvelope` comes from `bynk`; the suite's target must `consumes bynk` to build one",
4389            );
4390        }
4391        ctx.errors.push(err);
4392    }
4393}
4394
4395/// If `receiver` resolves to a consumed-context prefix (an alias or a
4396/// dotted qualified name appearing in `consumes`), return the consumed
4397/// context's qualified name. Otherwise None. Local bindings, types, and
4398/// capabilities take precedence — those are checked at the call site.
4399fn cross_context_prefix(receiver: &Expr, ctx: &Ctx) -> Option<String> {
4400    let info = &ctx.input.cross_context;
4401    if info.consumed_contexts.is_empty() && info.aliases.is_empty() {
4402        return None;
4403    }
4404    // Walk the receiver to assemble a candidate dotted name. Supports:
4405    //   Ident(X)                                 -> "X"
4406    //   FieldAccess { Ident(A), B }              -> "A.B"
4407    //   FieldAccess { FieldAccess { Ident(A), B }, C } -> "A.B.C"
4408    let candidate = flatten_ident_chain(receiver)?;
4409    let head = candidate.split('.').next().unwrap_or("");
4410    // The head must not shadow a local binding / capability / declared type.
4411    if ctx.lookup(head).is_some() {
4412        return None;
4413    }
4414    if ctx.caps.capabilities.contains_key(head) || ctx.caps.declared_capabilities.contains_key(head)
4415    {
4416        return None;
4417    }
4418    // If the head is a known local type, only an alias whose name happens to
4419    // collide could redirect this; aliases conflicting with types are an
4420    // error in project.rs, so a clash here is impossible at this point.
4421    info.resolve_prefix(candidate.as_str())
4422}
4423
4424/// Flatten an `Ident`/`FieldAccess` chain into its dotted name, or None if
4425/// any segment isn't a bare identifier.
4426fn flatten_ident_chain(expr: &Expr) -> Option<String> {
4427    match &expr.kind {
4428        ExprKind::Ident(id) => Some(id.name.clone()),
4429        ExprKind::FieldAccess { receiver, field } => {
4430            let head = flatten_ident_chain(receiver)?;
4431            Some(format!("{head}.{}", field.name))
4432        }
4433        _ => None,
4434    }
4435}
4436
4437/// Type-check a cross-context service call (v0.6 §4.2). `receiver` carries
4438/// the prefix's source span for diagnostics. `consumed` is the resolved
4439/// qualified name of the consumed context.
4440/// v0.15: type-check a cross-context capability call `B.Cap.op(args)` /
4441/// `Alias.Cap.op(args)`. The capability operation signatures are carried in
4442/// `consumed_capabilities` (in the providing context's namespace); the
4443/// capability must be listed in the handler/provider's `given` clause.
4444#[allow(clippy::too_many_arguments)]
4445fn check_cross_context_capability_call(
4446    receiver: &Expr,
4447    consumed: &str,
4448    cap: &str,
4449    method: &Ident,
4450    // #926 (Decision G): explicit type arguments for a generic capability
4451    // operation, qualified form (`B.Cap.op[T](…)` / `Alias.Cap.op[T](…)`).
4452    type_args: &[TypeRef],
4453    args: &[Expr],
4454    span: Span,
4455    // P6.0 (#1139): the outer `MethodCall` expression's own identity.
4456    expr_id: ExprId,
4457    ctx: &mut Ctx,
4458) -> Option<TyId> {
4459    let tys = ctx.tys;
4460    // S3 (#1963): a qualified capability's operation, `U.Cap.op`.
4461    refuse_experimental(
4462        &format!("{consumed}.{cap}.{}", method.name),
4463        method.span,
4464        ctx,
4465    );
4466    ctx.callees.insert(
4467        expr_id,
4468        Callee::CrossCap {
4469            unit: consumed.to_string(),
4470            cap: cap.to_string(),
4471            op: method.name.clone(),
4472        },
4473    );
4474    // Capability calls require an effectful body (same rule as local ones).
4475    if !ctx.effectful {
4476        ctx.errors.push(CompileError::new(
4477            "bynk.effect.capability_in_pure_context",
4478            method.span,
4479            format!(
4480                "capability `{consumed}.{cap}` can only be called inside an effectful body (one returning `Effect[T]`)"
4481            ),
4482        ));
4483    }
4484    // The capability must be declared in this handler/provider's `given`.
4485    // The local deps key is the capability's simple name.
4486    if !ctx.caps.given_remaining.contains(cap) {
4487        let mut err = CompileError::new(
4488            "bynk.given.undeclared_capability",
4489            receiver.span,
4490            format!("capability `{consumed}.{cap}` is used but not listed in the `given` clause"),
4491        )
4492        .with_note(format!(
4493            "add `{consumed}.{cap}` to the handler's `given` clause so the dependency surface is visible at the declaration site"
4494        ));
4495        // v0.26 (ADR 0054): the one-click counterpart of the note — the
4496        // clause entry is the qualified form the user writes (`B.Cap`).
4497        if let Some((span, insert)) = given_insertion_edit(
4498            &ctx.caps.given_entries,
4499            ctx.caps.given_anchor,
4500            &format!("{consumed}.{cap}"),
4501        ) {
4502            err = err.with_suggestion(
4503                format!("add `{consumed}.{cap}` to the `given` clause"),
4504                vec![(span, insert)],
4505                Applicability::MachineApplicable,
4506            );
4507        }
4508        ctx.errors.push(err);
4509        for a in args {
4510            let _ = type_of(a, None, ctx);
4511        }
4512        return None;
4513    }
4514    ctx.caps.given_used.insert(cap.to_string());
4515
4516    let info = &ctx.input.cross_context;
4517    let op = info
4518        .consumed_capabilities
4519        .get(consumed)
4520        .and_then(|caps| caps.get(cap))
4521        .and_then(|c| c.ops.iter().find(|o| o.name == method.name))
4522        .cloned();
4523    let Some(op) = op else {
4524        ctx.errors.push(CompileError::new(
4525            "bynk.capability.unknown_operation",
4526            method.span,
4527            format!(
4528                "capability `{consumed}.{cap}` has no operation named `{}`",
4529                method.name
4530            ),
4531        ));
4532        for a in args {
4533            let _ = type_of(a, None, ctx);
4534        }
4535        return None;
4536    };
4537    // v0.36 (ADR 0069, slice 2): a cross-context op call references the op,
4538    // recorded already-qualified into the providing unit (where the op is
4539    // declared), mirroring the cross-context capability reference.
4540    ctx.refs.record_in_unit(
4541        method.span,
4542        SymbolKind::CapabilityOp,
4543        &format!("{cap}.{}", method.name),
4544        consumed,
4545    );
4546    if op.params.len() != args.len() {
4547        ctx.errors.push(CompileError::new(
4548            "bynk.capability.op_arity",
4549            method.span,
4550            format!(
4551                "capability operation `{consumed}.{cap}.{}` expects {} argument(s), but {} were given",
4552                method.name,
4553                op.params.len(),
4554                args.len()
4555            ),
4556        ));
4557        for a in args {
4558            let _ = type_of(a, None, ctx);
4559        }
4560        return None;
4561    }
4562
4563    // Resolve parameter / return types in the consumed context's namespace.
4564    let consumed_types = info
4565        .consumed_types
4566        .get(consumed)
4567        .cloned()
4568        .unwrap_or_default();
4569    // #926: resolve the op's own type parameter(s) — declared in the
4570    // *consumed* context's namespace but instantiated from an explicit type
4571    // argument named in the *calling* context's own namespace (same split
4572    // `resolve_type_ref`/`resolve_expr_type_ref` already draw for
4573    // params/return vs. call-site type args in the local-capability path,
4574    // `check_static_call`). Explicit only, never inferred.
4575    let vars: HashSet<String> = op.type_params.iter().cloned().collect();
4576    let mut subst: HashMap<String, TyId> = HashMap::new();
4577    if !op.type_params.is_empty() || !type_args.is_empty() {
4578        if type_args.is_empty() {
4579            ctx.errors.push(
4580                CompileError::new(
4581                    "bynk.generics.uninferable_type_arg",
4582                    method.span,
4583                    format!(
4584                        "capability operation `{consumed}.{cap}.{}` takes a type parameter, but none of its arguments determine it",
4585                        method.name
4586                    ),
4587                )
4588                .with_note(format!(
4589                    "give it explicitly: `{consumed}.{cap}.{}[T](…)`",
4590                    method.name
4591                )),
4592            );
4593            for a in args {
4594                let _ = type_of(a, None, ctx);
4595            }
4596            return None;
4597        }
4598        if type_args.len() != op.type_params.len() {
4599            ctx.errors.push(CompileError::new(
4600                "bynk.generics.type_arg_mismatch",
4601                method.span,
4602                format!(
4603                    "capability operation `{consumed}.{cap}.{}` takes {} type argument(s), but {} were given",
4604                    method.name,
4605                    op.type_params.len(),
4606                    type_args.len()
4607                ),
4608            ));
4609            for a in args {
4610                let _ = type_of(a, None, ctx);
4611            }
4612            return None;
4613        }
4614        for (tp, ta) in op.type_params.iter().zip(type_args) {
4615            let ty = resolve_expr_type_ref(ta, ctx)?;
4616            subst.insert(tp.clone(), ty);
4617        }
4618    }
4619    // #1941: a value this caller holds may be of a consumed context's
4620    // unexported type, which only the projection resolves.
4621    let (caller_types, ambiguous) = projected_types(&ctx.input.types, &info.consumed_types, tys);
4622    let mut all_ok = true;
4623    for (i, ((pname, ptype_ref), arg)) in op.params.iter().zip(args.iter()).enumerate() {
4624        record_param_hint(ctx.hints, pname, arg);
4625        let param_ty = resolve_type_ref_in(ptype_ref, &consumed_types, &vars, tys)
4626            .unwrap_or(tys.intern(Ty::Unit));
4627        let param_ty = substitute(param_ty, &subst, tys);
4628        // #2025: against the parameter's type; see `type_cross_context_arg`.
4629        let (arg_ty, crosses) =
4630            type_cross_context_arg(arg, param_ty, &caller_types, &consumed_types, consumed, ctx);
4631        let Some(arg_ty) = arg_ty else {
4632            all_ok = false;
4633            continue;
4634        };
4635        if !crosses {
4636            let mut err = CompileError::new(
4637                "bynk.boundary.structural_mismatch",
4638                arg.span,
4639                format!(
4640                    "cross-context argument {} to `{consumed}.{cap}.{}` has type `{}`, but parameter `{pname}` expects `{}`",
4641                    i + 1,
4642                    method.name,
4643                    arg_ty.display(tys),
4644                    param_ty.display(tys),
4645                ),
4646            );
4647            if let Some(note) = ambiguous_projection_note(arg_ty, &ambiguous, tys) {
4648                err = err.with_note(note);
4649            }
4650            ctx.errors.push(err);
4651            all_ok = false;
4652        }
4653    }
4654    if !all_ok {
4655        return None;
4656    }
4657    let raw_ret = resolve_type_ref_in(&op.return_type, &consumed_types, &vars, tys)
4658        .unwrap_or(tys.intern(Ty::Unit));
4659    let raw_ret = substitute(raw_ret, &subst, tys);
4660    if let Some(name) = rebrand_conflict(
4661        raw_ret,
4662        &consumed_types,
4663        &ctx.input.types,
4664        &caller_types,
4665        tys,
4666    ) {
4667        ctx.errors.push(rebranded_return_mismatch(
4668            &format!("{consumed}.{cap}.{}", method.name),
4669            &name,
4670            consumed,
4671            span,
4672            ctx,
4673        ));
4674        return None;
4675    }
4676    Some(rebrand_return_type(raw_ret, &ctx.input.types, tys))
4677}
4678
4679/// #1941: a returned value takes the caller's type of the same name (§6.5).
4680/// When that type has a different shape, the value is not one of it: the
4681/// caller's own `Box` would otherwise type a field read of the provider's.
4682fn rebranded_return_mismatch(
4683    callee: &str,
4684    name: &str,
4685    consumed: &str,
4686    span: Span,
4687    ctx: &Ctx,
4688) -> CompileError {
4689    let caller = ctx
4690        .input
4691        .cross_context
4692        .self_context
4693        .as_deref()
4694        .unwrap_or("?");
4695    CompileError::new(
4696        "bynk.boundary.structural_mismatch",
4697        span,
4698        format!(
4699            "cross-context call `{callee}` returns `{consumed}`'s type `{name}`, but `{name}` in \
4700             `{caller}` is a different type"
4701        ),
4702    )
4703    .with_note(
4704        "a returned value takes the receiving context's type of the same name, so the two \
4705         must be structurally compatible; rename one of them",
4706    )
4707}
4708
4709/// #2025, #2039, #2072: type one cross-context argument against its
4710/// parameter's type, returning the type and whether the argument crosses the
4711/// boundary.
4712///
4713/// A literal at a refined position in the parameter's type is admitted
4714/// against that type's own declaration, from the consumed context's table.
4715/// This covers a literal passed directly (`t.pay.charge(250)`) and one
4716/// nested in an `Option`/`Result` payload, a list element, or a field of a
4717/// record literal, a generic one included (`t.pay.maybe(Some(250))`,
4718/// `Page { items: [250] }`). It is checked against the callee's predicate,
4719/// whether or not the caller `uses` the declaring commons and whatever the
4720/// caller binds under that name. Such a literal is the callee's type by
4721/// construction, so it needs no structural check; neither does a `None` or
4722/// an empty list, which take the parameter's type as their hint.
4723///
4724/// Every other part of the argument is a *leaf*, typed in the caller's own
4725/// scope with its position's type as the hint, and the structural check
4726/// decides whether each leaf crosses. A shorthand record field (`{ cents }`)
4727/// is a leaf too, checked by its binding's type. An argument with no literal
4728/// or empty constructor in it is a single leaf, so it is checked whole, as
4729/// before; a leaf `type_of` did not record falls back to the whole argument
4730/// too.
4731fn type_cross_context_arg(
4732    arg: &Expr,
4733    param_ty: TyId,
4734    caller_types: &HashMap<String, Arc<TypeDecl>>,
4735    consumed_types: &HashMap<String, Arc<TypeDecl>>,
4736    consumed: &str,
4737    ctx: &mut Ctx,
4738) -> (Option<TyId>, bool) {
4739    let tys = ctx.tys;
4740    let mut found = CrossingPositions::default();
4741    collect_crossing_positions(arg, param_ty, caller_types, consumed_types, tys, &mut found);
4742    let CrossingPositions {
4743        literals,
4744        mut leaves,
4745        bindings,
4746    } = found;
4747    for (lit, pos) in literals {
4748        let before = ctx.errors.len();
4749        match admit_refined_literal_in(lit, Some(pos), consumed_types, ctx) {
4750            Some(ty) => {
4751                for err in &mut ctx.errors[before..] {
4752                    err.notes.push(format!(
4753                        "`{}` is the type at this position of the parameter, as `{consumed}` declares it",
4754                        ty.display(tys)
4755                    ));
4756                }
4757                ctx.admitted_literals.insert(lit.id, ty);
4758            }
4759            // Not a literal of the refined type's base: an ordinary leaf.
4760            None => leaves.push((lit, pos)),
4761        }
4762    }
4763    let arg_ty = type_of(arg, Some(param_ty), ctx);
4764    let Some(ty) = arg_ty else {
4765        return (None, false);
4766    };
4767    let whole = || structurally_compatible(ty, param_ty, caller_types, consumed_types, tys);
4768    let leaves_cross = leaves.iter().all(|(leaf, pos)| {
4769        // A leaf `type_of` did not record is checked through the whole
4770        // argument instead.
4771        match ctx.expr_types.get(&leaf.id) {
4772            Some(t) => structurally_compatible(t.ty, *pos, caller_types, consumed_types, tys),
4773            None => whole(),
4774        }
4775    });
4776    let bindings_cross = bindings.iter().all(|(name, pos)| match ctx.lookup(name) {
4777        Some(t) => structurally_compatible(t, *pos, caller_types, consumed_types, tys),
4778        None => whole(),
4779    });
4780    (arg_ty, leaves_cross && bindings_cross)
4781}
4782
4783/// #2039: what [`collect_crossing_positions`] finds in a cross-context
4784/// argument, each paired with the type at its position, named in the
4785/// consumed context's table.
4786#[derive(Default)]
4787struct CrossingPositions<'e> {
4788    /// Literals at refined positions, admitted against the callee.
4789    literals: Vec<(&'e Expr, TyId)>,
4790    /// Parts the structural check still has to decide.
4791    leaves: Vec<(&'e Expr, TyId)>,
4792    /// #2072: shorthand record fields, by the binding each one reads.
4793    bindings: Vec<(&'e str, TyId)>,
4794}
4795
4796/// #2039: walk a cross-context argument alongside its parameter's type,
4797/// collecting the literals at refined positions and the leaves the
4798/// structural check still has to decide.
4799///
4800/// The walk follows `Some`/`Ok`/`Err` payloads, list elements, and the
4801/// fields of a record literal whose type is the very declaration the
4802/// callee's parameter names (the same commons type). #2072: for a generic
4803/// record, its type arguments are substituted into the field types, and a
4804/// shorthand field is collected by its binding. Anything else is a leaf.
4805fn collect_crossing_positions<'e>(
4806    e: &'e Expr,
4807    pos: TyId,
4808    caller_types: &HashMap<String, Arc<TypeDecl>>,
4809    consumed_types: &HashMap<String, Arc<TypeDecl>>,
4810    tys: &Types,
4811    found: &mut CrossingPositions<'e>,
4812) {
4813    let node = tys.get(pos);
4814    let walk = |inner: &'e Expr, at: TyId, found: &mut CrossingPositions<'e>| {
4815        collect_crossing_positions(inner, at, caller_types, consumed_types, tys, found)
4816    };
4817    // A compile-time literal, negated ones (`-3`) included.
4818    if const_literal(e).is_some()
4819        && matches!(
4820            &*node,
4821            Ty::Named {
4822                kind: NamedKind::Refined(_),
4823                ..
4824            }
4825        )
4826    {
4827        return found.literals.push((e, pos));
4828    }
4829    match (&e.kind, &*node) {
4830        (ExprKind::Paren(inner), _) => walk(inner, pos, found),
4831        // Typed from the hint: the parameter's own type at this position.
4832        (ExprKind::None, Ty::Option(_)) => {}
4833        (ExprKind::ListLit(items), Ty::List(_)) if items.is_empty() => {}
4834        // #2043: `List.empty()` / `Map.empty()`, likewise.
4835        (
4836            ExprKind::MethodCall {
4837                receiver,
4838                method,
4839                type_args,
4840                args,
4841            },
4842            Ty::List(_) | Ty::Map(..),
4843        ) if method.name == "empty"
4844            && type_args.is_empty()
4845            && args.is_empty()
4846            && matches!(
4847                &receiver.kind,
4848                ExprKind::Ident(id)
4849                    if id.name == if matches!(&*node, Ty::List(_)) { "List" } else { "Map" }
4850            ) => {}
4851        (ExprKind::Some(inner), Ty::Option(t)) => walk(inner, *t, found),
4852        (ExprKind::Ok(inner), Ty::Result(t, _)) => walk(inner, *t, found),
4853        (ExprKind::Err(inner), Ty::Result(_, t)) => walk(inner, *t, found),
4854        (ExprKind::ListLit(items), Ty::List(t)) => {
4855            for item in items {
4856                walk(item, *t, found);
4857            }
4858        }
4859        (
4860            ExprKind::RecordConstruction { type_name, fields },
4861            Ty::Named {
4862                name,
4863                kind: NamedKind::Record,
4864                args,
4865            },
4866        ) if type_name.name == *name => {
4867            let Some(field_positions) =
4868                record_field_positions(name, args, fields, caller_types, consumed_types, tys)
4869            else {
4870                return found.leaves.push((e, pos));
4871            };
4872            for (f, field_ty) in field_positions {
4873                match &f.value {
4874                    Some(value) => walk(value, field_ty, found),
4875                    // #2072: a shorthand field reads the same-named binding.
4876                    None => found.bindings.push((f.name.name.as_str(), field_ty)),
4877                }
4878            }
4879        }
4880        _ => found.leaves.push((e, pos)),
4881    }
4882}
4883
4884/// #2039, #2072: the type of each field a record literal sets, in the
4885/// consumed context's table, when the literal's type is the very
4886/// declaration the callee names (the same commons type). A generic record's
4887/// type arguments are substituted into its field types. `None` when the
4888/// record is not the callee's declaration, or a field is not one it
4889/// declares: the record is then checked whole.
4890fn record_field_positions<'f>(
4891    name: &str,
4892    args: &[TyId],
4893    fields: &'f [FieldInit],
4894    caller_types: &HashMap<String, Arc<TypeDecl>>,
4895    consumed_types: &HashMap<String, Arc<TypeDecl>>,
4896    tys: &Types,
4897) -> Option<Vec<(&'f FieldInit, TyId)>> {
4898    let decl = consumed_types.get(name)?;
4899    if !caller_types.get(name).is_some_and(|c| Arc::ptr_eq(c, decl)) {
4900        return None;
4901    }
4902    let TypeBody::Record(record) = &decl.body else {
4903        return None;
4904    };
4905    if decl.type_params.len() != args.len() {
4906        return None;
4907    }
4908    let vars: HashSet<String> = decl
4909        .type_params
4910        .iter()
4911        .map(|p| p.name.name.clone())
4912        .collect();
4913    let subst: HashMap<String, TyId> = decl
4914        .type_params
4915        .iter()
4916        .map(|p| p.name.name.clone())
4917        .zip(args.iter().copied())
4918        .collect();
4919    fields
4920        .iter()
4921        .map(|f| {
4922            let declared = record.fields.iter().find(|d| d.name.name == f.name.name)?;
4923            let ty = resolve_type_ref_in(&declared.type_ref, consumed_types, &vars, tys)?;
4924            Some((f, substitute(ty, &subst, tys)))
4925        })
4926        .collect()
4927}
4928
4929/// #2073: a consumed context's service whose signature reaches a type that
4930/// context consumes from a third (`shop.ledger.isDomestic(o: Order)`, `Order`
4931/// from `shop.order`) crosses the boundary as that third context's type. The
4932/// caller names it, and builds its codecs, through the declaring context, so
4933/// it must consume that context too; otherwise `bynk.consumes.signature_type_unconsumed`.
4934fn check_signature_owners_consumed(
4935    consumed: &str,
4936    service: &crate::resolver::CrossContextService,
4937    method: &Ident,
4938    ctx: &mut Ctx,
4939) -> bool {
4940    let info = &ctx.input.cross_context;
4941    let Some(owners) = info.consumed_type_owners.get(consumed) else {
4942        return false;
4943    };
4944    let types = info.consumed_types.get(consumed);
4945    let mut seen: HashSet<String> = HashSet::new();
4946    let mut stack: Vec<String> = Vec::new();
4947    let vars = HashSet::new();
4948    for (_, t) in &service.params {
4949        crate::project_model::type_ref_names(t, &vars, &mut stack);
4950    }
4951    crate::project_model::type_ref_names(&service.return_type, &vars, &mut stack);
4952    let mut missing: std::collections::BTreeMap<String, String> = Default::default();
4953    while let Some(n) = stack.pop() {
4954        if !seen.insert(n.clone()) {
4955            continue;
4956        }
4957        if let Some(owner) = owners.get(&n)
4958            && info.self_context.as_deref() != Some(owner.as_str())
4959            && !info.consumed_contexts.contains(owner)
4960        {
4961            // The declaration's own name, not the key `contract_types_for`
4962            // may have qualified it under (`shop_base__Region`).
4963            let declared = n
4964                .strip_prefix(&format!("{}__", owner.replace('.', "_")))
4965                .unwrap_or(&n)
4966                .to_string();
4967            missing.entry(owner.clone()).or_insert(declared);
4968        }
4969        if let Some(d) = types.and_then(|t| t.get(&n)) {
4970            stack.extend(crate::project_model::type_decl_ref_names(d));
4971        }
4972    }
4973    let refused = !missing.is_empty();
4974    for (owner, ty) in missing {
4975        ctx.errors.push(
4976            CompileError::new(
4977                "bynk.consumes.signature_type_unconsumed",
4978                method.span,
4979                format!(
4980                    "`{consumed}.{}` takes or returns `{ty}`, declared in `{owner}`, but this context does not consume `{owner}`",
4981                    method.name
4982                ),
4983            )
4984            .with_note(format!(
4985                "add `consumes {owner}`: a value of `{ty}` crosses this call as `{owner}`'s type"
4986            )),
4987        );
4988    }
4989    refused
4990}
4991
4992fn check_cross_context_call(
4993    receiver: &Expr,
4994    consumed: &str,
4995    method: &Ident,
4996    args: &[Expr],
4997    span: Span,
4998    // P6.0 (#1139): the outer `MethodCall` expression's own identity.
4999    expr_id: ExprId,
5000    ctx: &mut Ctx,
5001) -> Option<TyId> {
5002    let tys = ctx.tys;
5003    ctx.callees.insert(
5004        expr_id,
5005        Callee::Cross {
5006            unit: consumed.to_string(),
5007            service: method.name.clone(),
5008        },
5009    );
5010    // The consuming context must be effectful at this call site (services
5011    // and agent handlers are; pure free fns are not).
5012    if !ctx.effectful {
5013        ctx.errors.push(
5014            CompileError::new(
5015                "bynk.effect.cross_context_in_pure_context",
5016                method.span,
5017                format!(
5018                    "cross-context service call `{}.{}` can only be made inside an effectful body (one returning `Effect[T]`)",
5019                    consumed, method.name
5020                ),
5021            )
5022            .with_label(receiver.span, "consumed context prefix"),
5023        );
5024    }
5025    let info = &ctx.input.cross_context;
5026    let Some(svcs) = info.consumed_services.get(consumed) else {
5027        ctx.errors.push(
5028            CompileError::new(
5029                "bynk.consumes.unknown_context",
5030                receiver.span,
5031                format!("context `{consumed}` is not in scope here"),
5032            )
5033            .with_note(
5034                "add a `consumes` clause for the target context at the top of the consuming context",
5035            ),
5036        );
5037        for a in args {
5038            let _ = type_of(a, None, ctx);
5039        }
5040        return None;
5041    };
5042    let Some(service) = svcs.get(&method.name).cloned() else {
5043        ctx.errors.push(
5044            CompileError::new(
5045                "bynk.consumes.unknown_service",
5046                method.span,
5047                format!(
5048                    "context `{consumed}` has no service named `{}`",
5049                    method.name
5050                ),
5051            )
5052            .with_note(
5053                "cross-context calls require an `on call` service handler in the consumed context",
5054            ),
5055        );
5056        for a in args {
5057            let _ = type_of(a, None, ctx);
5058        }
5059        return None;
5060    };
5061    ctx.refs
5062        .record_in_unit(method.span, SymbolKind::Service, &method.name, consumed);
5063    // A refused signature's arguments would only repeat it as a mismatch.
5064    let unconsumed = check_signature_owners_consumed(consumed, &service, method, ctx);
5065
5066    if service.params.len() != args.len() {
5067        ctx.errors.push(
5068            CompileError::new(
5069                "bynk.consumes.service_arity",
5070                method.span,
5071                format!(
5072                    "cross-context service `{consumed}.{}` expects {} argument(s), but {} were given",
5073                    method.name,
5074                    service.params.len(),
5075                    args.len()
5076                ),
5077            )
5078            // Finding #46: `service` belongs to the *consumed*
5079            // context — a different unit/file than this call, almost
5080            // always. A label would risk underlining unrelated text
5081            // there; a note carries the information safely (per-label
5082            // file identity is a Wave 8 follow-up).
5083            .with_note("service declared here"),
5084        );
5085        for a in args {
5086            let _ = type_of(a, None, ctx);
5087        }
5088        return None;
5089    }
5090
5091    // Resolve the consumed-context types so we can describe parameter shapes.
5092    let consumed_types = info
5093        .consumed_types
5094        .get(consumed)
5095        .cloned()
5096        .unwrap_or_default();
5097
5098    // #1941: as for a capability operation, the caller's table plus the
5099    // consumed contexts' types it holds values of without naming.
5100    let (caller_types, ambiguous) = projected_types(&ctx.input.types, &info.consumed_types, tys);
5101
5102    // Walk each argument, checking structural compatibility (Phase 4).
5103    let mut all_ok = true;
5104    for (i, ((pname, ptype_ref), arg)) in service.params.iter().zip(args.iter()).enumerate() {
5105        record_param_hint(ctx.hints, pname, arg);
5106        let param_ty =
5107            resolve_type_ref(ptype_ref, &consumed_types, tys).unwrap_or(tys.intern(Ty::Unit));
5108        // Type-check the argument in the caller's context (#2025: against
5109        // the parameter's type; see `type_cross_context_arg`).
5110        let (arg_ty, crosses) =
5111            type_cross_context_arg(arg, param_ty, &caller_types, &consumed_types, consumed, ctx);
5112        let Some(arg_ty) = arg_ty else {
5113            all_ok = false;
5114            continue;
5115        };
5116        if !crosses && !unconsumed {
5117            let mut err = CompileError::new(
5118                    "bynk.boundary.structural_mismatch",
5119                    arg.span,
5120                    format!(
5121                        "cross-context argument {} to `{consumed}.{}` has type `{}` in `{}`, but parameter `{pname}` expects `{}` in `{}`",
5122                        i + 1,
5123                        method.name,
5124                        arg_ty.display(tys),
5125                        ctx.input
5126                            .cross_context
5127                            .self_context
5128                            .as_deref()
5129                            .unwrap_or("?"),
5130                        param_ty.display(tys),
5131                        consumed,
5132                    ),
5133                )
5134                // Finding #46: same cross-unit provenance as above.
5135                .with_note("service declared here")
5136                .with_note(
5137                    "values crossing a context boundary must have structurally compatible types (same commons-derived type, or identical record/sum shape)",
5138                );
5139            if let Some(note) = ambiguous_projection_note(arg_ty, &ambiguous, tys) {
5140                err = err.with_note(note);
5141            }
5142            ctx.errors.push(err);
5143            all_ok = false;
5144        }
5145    }
5146    if !all_ok {
5147        return None;
5148    }
5149
5150    // Return type rebrand: project the consumed context's return type into
5151    // the calling context's namespace by renaming named types whose unqualified
5152    // name appears in the caller's type table (v0.6 §4.5).
5153    let raw_ret = resolve_type_ref(&service.return_type, &consumed_types, tys)
5154        .unwrap_or(tys.intern(Ty::Unit));
5155    if let Some(name) = rebrand_conflict(
5156        raw_ret,
5157        &consumed_types,
5158        &ctx.input.types,
5159        &caller_types,
5160        tys,
5161    ) {
5162        ctx.errors.push(rebranded_return_mismatch(
5163            &format!("{consumed}.{}", method.name),
5164            &name,
5165            consumed,
5166            span,
5167            ctx,
5168        ));
5169        return None;
5170    }
5171    Some(rebrand_return_type(raw_ret, &ctx.input.types, tys))
5172}
5173
5174#[cfg(test)]
5175mod tests {
5176    use crate::testkit::analyse;
5177
5178    /// A single-file `commons demo` project.
5179    fn commons(body: &str) -> crate::testkit::Analysed {
5180        analyse(&[("demo.bynk", &format!("commons demo\n\n{body}"))])
5181    }
5182
5183    /// A single-file `context demo` project.
5184    fn context(body: &str) -> crate::testkit::Analysed {
5185        analyse(&[("demo.bynk", &format!("context demo\n\n{body}"))])
5186    }
5187
5188    const CLOCK: &str = "capability Clock {\n  fn now() -> Effect[Int]\n}\n\n\
5189                         provides Clock = FixedClock {\n  fn now() -> Effect[Int] {\n    Effect.pure(0)\n  }\n}\n";
5190
5191    #[test]
5192    fn a_call_to_a_declared_fn_has_its_return_type() {
5193        let a = commons("fn double(x: Int) -> Int { x * 2 }\n\nfn four() -> Int { double(2) }\n");
5194        a.assert_clean();
5195        assert_eq!(a.type_at("demo.bynk", "double(2)"), "Int");
5196    }
5197
5198    #[test]
5199    fn an_argument_of_the_wrong_type_is_a_mismatch() {
5200        let a = commons(
5201            "fn double(x: Int) -> Int { x * 2 }\n\nfn bad(s: String) -> Int { double(s) }\n",
5202        );
5203        a.assert_reports("bynk.types.argument_mismatch");
5204    }
5205
5206    #[test]
5207    fn a_fn_called_with_too_many_arguments_is_an_arity_error() {
5208        let a = commons("fn double(x: Int) -> Int { x * 2 }\n\nfn bad() -> Int { double(1, 2) }\n");
5209        a.assert_reports("bynk.resolve.arity_mismatch");
5210    }
5211
5212    #[test]
5213    fn a_fn_value_called_with_too_many_arguments_is_a_call_arity_error() {
5214        let a = commons("fn f(g: (Int) -> Int) -> Int { g(1, 2) }\n");
5215        a.assert_reports("bynk.types.call_arity");
5216    }
5217
5218    #[test]
5219    fn a_fn_value_call_has_the_value_s_return_type() {
5220        let a = commons("fn f(g: (Int) -> String) -> String { g(1) }\n");
5221        a.assert_clean();
5222        assert_eq!(a.type_at("demo.bynk", "g(1)"), "String");
5223    }
5224
5225    #[test]
5226    fn a_generic_call_infers_its_type_argument_from_the_argument() {
5227        let a = commons("fn id[A](x: A) -> A { x }\n\nfn s() -> String { id(\"s\") }\n");
5228        a.assert_clean();
5229        assert_eq!(a.type_at("demo.bynk", "id(\"s\")"), "String");
5230    }
5231
5232    #[test]
5233    fn a_generic_call_binding_one_type_argument_two_ways_is_a_conflict() {
5234        let a =
5235            commons("fn both[A](x: A, y: A) -> A { x }\n\nfn bad() -> Int { both(1, \"two\") }\n");
5236        a.assert_reports("bynk.generics.type_arg_mismatch");
5237    }
5238
5239    #[test]
5240    fn a_type_argument_no_parameter_mentions_is_uninferable() {
5241        let a = commons(
5242            "fn first_of[A](x: Int) -> A { identity_helper(x) }\n\n\
5243             fn identity_helper[A](x: A) -> A { x }\n\n\
5244             fn misuse() -> Int { first_of(5) }\n",
5245        );
5246        a.assert_reports("bynk.generics.uninferable_type_arg");
5247    }
5248
5249    #[test]
5250    fn calling_an_undeclared_name_is_an_unknown_function() {
5251        let a = commons("fn bad() -> Int { nope(1) }\n");
5252        a.assert_reports("bynk.resolve.unknown_function");
5253    }
5254
5255    #[test]
5256    fn calling_a_record_type_like_a_function_is_rejected() {
5257        let a = commons("type P = { a: Int }\n\nfn f() -> P { P(1) }\n");
5258        a.assert_reports("bynk.resolve.type_as_function");
5259    }
5260
5261    #[test]
5262    fn a_capability_op_call_has_the_op_s_return_type() {
5263        let a = context(&format!(
5264            "{CLOCK}\nservice probe {{\n  on call() -> Effect[Int] given Clock {{\n    Clock.now()\n  }}\n}}\n"
5265        ));
5266        a.assert_clean();
5267        assert_eq!(a.type_at("demo.bynk", "Clock.now()"), "Effect[Int]");
5268    }
5269
5270    #[test]
5271    fn a_capability_op_called_with_an_extra_argument_is_an_arity_error() {
5272        let a = context(&format!(
5273            "{CLOCK}\nservice probe {{\n  on call() -> Effect[Int] given Clock {{\n    Clock.now(1)\n  }}\n}}\n"
5274        ));
5275        a.assert_reports("bynk.capability.op_arity");
5276    }
5277
5278    #[test]
5279    fn a_capability_called_inside_a_pure_lambda_is_rejected() {
5280        let a = context(&format!(
5281            "{CLOCK}\nservice probe {{\n  on call() -> Effect[Int] given Clock {{\n    \
5282             let f: (Int) -> Int = (n) => Clock.now()\n    Effect.pure(1)\n  }}\n}}\n"
5283        ));
5284        a.assert_reports("bynk.effect.capability_in_pure_context");
5285    }
5286
5287    #[test]
5288    fn an_unknown_cell_op_is_rejected() {
5289        let a = context(
5290            "agent Counter {\n  key id: String\n  store n: Cell[Int] = 0\n\n  \
5291             on call bad() -> Effect[()] {\n    let _ <- n.foo()\n    Effect.pure(())\n  }\n}\n",
5292        );
5293        a.assert_reports("bynk.store.unknown_op");
5294    }
5295
5296    #[test]
5297    fn a_cell_update_is_an_effect_of_unit() {
5298        let a = context(
5299            "agent Counter {\n  key id: String\n  store n: Cell[Int] = 0\n\n  \
5300             on call bump() -> Effect[()] {\n    n.update((c) => c + 1)\n  }\n}\n",
5301        );
5302        a.assert_clean();
5303        assert_eq!(
5304            a.type_at("demo.bynk", "n.update((c) => c + 1)"),
5305            "Effect[()]"
5306        );
5307    }
5308
5309    #[test]
5310    fn a_cell_update_whose_function_returns_another_type_is_a_mismatch() {
5311        let a = context(
5312            "agent Counter {\n  key id: String\n  store n: Cell[Int] = 0\n\n  \
5313             on call bad() -> Effect[()] {\n    n.update((c) => \"x\")\n  }\n}\n",
5314        );
5315        a.assert_reports("bynk.types.lambda_mismatch");
5316    }
5317
5318    #[test]
5319    fn a_cell_update_with_no_function_is_a_call_arity_error() {
5320        let a = context(
5321            "agent Counter {\n  key id: String\n  store n: Cell[Int] = 0\n\n  \
5322             on call bad() -> Effect[()] {\n    n.update()\n  }\n}\n",
5323        );
5324        a.assert_reports("bynk.types.call_arity");
5325    }
5326
5327    /// #1941: a provider context with an unexported `Box` (and its field's
5328    /// `Amount`), a service returning one and a service taking one back, and
5329    /// a capability operation doing the same.
5330    const SHOP: &str = "context shop\n\nexports capability { Store }\n\n\
5331                        type Amount = { cents: Int }\n\ntype Box = { a: Amount }\n\n\
5332                        capability Store {\n  fn get() -> Effect[Box]\n  fn put(b: Box) -> Effect[Int]\n}\n\n\
5333                        provides Store = Shelf {\n  fn get() -> Effect[Box] {\n    Effect.pure(Box { a: Amount { cents: 1 } })\n  }\n\n  \
5334                        fn put(b: Box) -> Effect[Int] {\n    Effect.pure(b.a.cents)\n  }\n}\n\n\
5335                        service quote {\n  on call() -> Effect[Box] {\n    Effect.pure(Box { a: Amount { cents: 1 } })\n  }\n}\n\n\
5336                        service take {\n  on call(b: Box) -> Effect[Int] {\n    Effect.pure(b.a.cents)\n  }\n}\n";
5337
5338    /// A `till` context consuming `shop`, declaring `decls`, with `body` as
5339    /// a service handler's body (given `shop.Store` when it calls it).
5340    fn till(decls: &str, body: &str) -> String {
5341        let given = if body.contains("Store") {
5342            " given shop.Store"
5343        } else {
5344            ""
5345        };
5346        format!(
5347            "context till\n\nconsumes shop\n\n{decls}service total {{\n  \
5348             on call() -> Effect[Int]{given} {{\n{body}\n  }}\n}}\n"
5349        )
5350    }
5351
5352    #[test]
5353    fn an_unexported_value_passes_back_to_its_service() {
5354        let a = analyse(&[
5355            ("shop.bynk", SHOP),
5356            (
5357                "till.bynk",
5358                &till("", "    let b <- shop.quote()\n    shop.take(b)"),
5359            ),
5360        ]);
5361        a.assert_clean();
5362    }
5363
5364    #[test]
5365    fn an_unexported_value_passes_back_to_its_capability() {
5366        let a = analyse(&[
5367            ("shop.bynk", SHOP),
5368            (
5369                "till.bynk",
5370                &till("", "    let b <- shop.Store.get()\n    shop.Store.put(b)"),
5371            ),
5372        ]);
5373        a.assert_clean();
5374    }
5375
5376    #[test]
5377    fn a_caller_type_of_another_shape_cannot_receive_a_service_return() {
5378        let a = analyse(&[
5379            ("shop.bynk", SHOP),
5380            (
5381                "till.bynk",
5382                &till(
5383                    "type Box = { label: String }\n\n",
5384                    "    let b <- shop.quote()\n    Effect.pure(1)",
5385                ),
5386            ),
5387        ]);
5388        a.assert_reports("bynk.boundary.structural_mismatch");
5389    }
5390
5391    #[test]
5392    fn a_caller_type_of_another_shape_cannot_receive_a_capability_return() {
5393        let a = analyse(&[
5394            ("shop.bynk", SHOP),
5395            (
5396                "till.bynk",
5397                &till(
5398                    "type Box = { label: String }\n\n",
5399                    "    let b <- shop.Store.get()\n    Effect.pure(1)",
5400                ),
5401            ),
5402        ]);
5403        a.assert_reports("bynk.boundary.structural_mismatch");
5404    }
5405
5406    #[test]
5407    fn a_caller_type_of_the_same_shape_receives_a_return() {
5408        let a = analyse(&[
5409            ("shop.bynk", SHOP),
5410            (
5411                "till.bynk",
5412                &till(
5413                    "type Coins = { cents: Int }\n\ntype Box = { a: Coins }\n\n",
5414                    "    let b <- shop.quote()\n    let c <- shop.Store.get()\n    \
5415                     Effect.pure(b.a.cents + c.a.cents)",
5416                ),
5417            ),
5418        ]);
5419        a.assert_clean();
5420    }
5421
5422    #[test]
5423    fn an_ambiguous_unexported_name_explains_its_rejection() {
5424        let other = "context depot\n\ntype Box = { label: String }\n\n\
5425                     service quote {\n  on call() -> Effect[Box] {\n    Effect.pure(Box { label: \"x\" })\n  }\n}\n";
5426        let a = analyse(&[
5427            ("shop.bynk", SHOP),
5428            ("depot.bynk", other),
5429            (
5430                "till.bynk",
5431                "context till\n\nconsumes shop\n\nconsumes depot\n\n\
5432                 service total {\n  on call() -> Effect[Int] {\n    \
5433                 let b <- shop.quote()\n    shop.take(b)\n  }\n}\n",
5434            ),
5435        ]);
5436        let err = a
5437            .analysis
5438            .errors
5439            .iter()
5440            .find(|e| e.error.category == "bynk.boundary.structural_mismatch")
5441            .expect("the argument is rejected");
5442        assert!(
5443            err.error
5444                .notes
5445                .iter()
5446                .any(|n| n.contains("more than one consumed context declares a type `Box`")),
5447            "{:?}",
5448            err.error.notes
5449        );
5450    }
5451
5452    #[test]
5453    fn a_return_the_caller_does_not_rebrand_is_not_checked_against_a_projection() {
5454        // Review of #1949: `shop` and `depot` declare the same-shaped `Box`, so
5455        // the projection holds one of them (`depot`'s, in name order). `till`
5456        // declares no `Box`, so `quote`'s `Box` is not rebranded and needs
5457        // no check, however `depot`'s `Box.a` would resolve against `till`'s
5458        // own, unrelated `Amount`.
5459        let pair = |name: &str, svc: &str| {
5460            format!(
5461                "context {name}\n\ntype Amount = {{ cents: Int }}\n\ntype Box = {{ a: Amount }}\n\n{svc}"
5462            )
5463        };
5464        let shop = pair(
5465            "shop",
5466            "service quote {\n  on call() -> Effect[Box] {\n    Effect.pure(Box { a: Amount { cents: 1 } })\n  }\n}\n",
5467        );
5468        let depot = pair(
5469            "depot",
5470            "service store {\n  on call(b: Box) -> Effect[Int] {\n    Effect.pure(b.a.cents)\n  }\n}\n",
5471        );
5472        let a = analyse(&[
5473            ("shop.bynk", &shop),
5474            ("depot.bynk", &depot),
5475            (
5476                "till.bynk",
5477                "context till\n\nconsumes shop\n\nconsumes depot\n\n\
5478                 type Amount = { dollars: Int }\n\n\
5479                 fn mine() -> Amount {\n  Amount { dollars: 1 }\n}\n\n\
5480                 service total {\n  on call() -> Effect[Int] {\n    \
5481                 let b <- shop.quote()\n    Effect.pure(1)\n  }\n}\n",
5482            ),
5483        ]);
5484        a.assert_clean();
5485    }
5486
5487    /// Review of #1949: a provider whose `Box` is returned inside each
5488    /// container a cross-context return can carry, for a caller whose own
5489    /// `Box` has another shape.
5490    fn caller_box_differs_inside(ret: &str, value: &str) {
5491        let shop = format!(
5492            "context shop\n\ntype Box = {{ a: Int }}\n\n\
5493             service quote {{\n  on call() -> Effect[{ret}] {{\n    Effect.pure({value})\n  }}\n}}\n"
5494        );
5495        let a = analyse(&[
5496            ("shop.bynk", &shop),
5497            (
5498                "till.bynk",
5499                "context till\n\nconsumes shop\n\ntype Box = { label: String }\n\n\
5500                 service total {\n  on call() -> Effect[Int] {\n    \
5501                 let b <- shop.quote()\n    Effect.pure(1)\n  }\n}\n",
5502            ),
5503        ]);
5504        a.assert_reports("bynk.boundary.structural_mismatch");
5505    }
5506
5507    #[test]
5508    fn a_differing_caller_type_is_found_in_a_result() {
5509        caller_box_differs_inside("Result[Box, String]", "Ok(Box { a: 1 })");
5510    }
5511
5512    #[test]
5513    fn a_differing_caller_type_is_found_in_a_map_value() {
5514        caller_box_differs_inside("Map[String, Box]", "Map.empty()");
5515    }
5516
5517    #[test]
5518    fn a_differing_caller_type_is_found_in_an_option() {
5519        caller_box_differs_inside("Option[Box]", "None");
5520    }
5521
5522    #[test]
5523    fn a_differing_caller_type_is_found_in_a_list() {
5524        caller_box_differs_inside("List[Box]", "[]");
5525    }
5526
5527    #[test]
5528    fn a_differing_caller_type_is_found_in_an_http_result() {
5529        caller_box_differs_inside("HttpResult[Box]", "Ok(Box { a: 1 })");
5530    }
5531
5532    #[test]
5533    fn agreeing_declarations_project_in_context_name_order() {
5534        // Review of #1949: `depot`'s `Box` names `Coins` and `shop`'s names
5535        // `Amount`, of the same shape, so the two agree and either may be
5536        // projected. `till`'s own `Amount` differs: `shop`'s `Box` would
5537        // read its field as `till`'s, and `depot`'s would not. The projection
5538        // takes contexts in name order, so `depot`'s is the one, on every run.
5539        let a = analyse(&[
5540            (
5541                "shop.bynk",
5542                "context shop\n\ntype Amount = { cents: Int }\n\ntype Box = { a: Amount }\n\n\
5543                 service quote {\n  on call() -> Effect[Box] {\n    \
5544                 Effect.pure(Box { a: Amount { cents: 1 } })\n  }\n}\n",
5545            ),
5546            (
5547                "depot.bynk",
5548                "context depot\n\ntype Coins = { cents: Int }\n\ntype Box = { a: Coins }\n\n\
5549                 service store {\n  on call(b: Box) -> Effect[Int] {\n    Effect.pure(b.a.cents)\n  }\n}\n",
5550            ),
5551            (
5552                "till.bynk",
5553                "context till\n\nconsumes shop\n\nconsumes depot\n\n\
5554                 type Amount = { dollars: Int }\n\n\
5555                 fn mine() -> Amount {\n  Amount { dollars: 1 }\n}\n\n\
5556                 service total {\n  on call() -> Effect[Int] {\n    \
5557                 let b <- shop.quote()\n    depot.store(b)\n  }\n}\n",
5558            ),
5559        ]);
5560        a.assert_clean();
5561    }
5562}