Skip to main content

bynk_check/checker/
calls.rs

1//! Call / application dispatch.
2//!
3//! Split out of `checker.rs` (v0.29.10) verbatim; the parent module
4//! re-exports these via `use calls::*`.
5
6use super::*;
7
8/// v0.39 (ADR 0072): record a parameter-name inlay hint for one call argument,
9/// unless it would be noise — the `_`/`self` placeholders, or an argument that
10/// is already the identically-named identifier (`f(count)` for parameter
11/// `count`, matching rust-analyzer's suppression).
12fn record_param_hint(hints: &mut HintSink, param_name: &str, arg: &Expr) {
13    if param_name == "_" || param_name == "self" {
14        return;
15    }
16    if let ExprKind::Ident(id) = &arg.kind
17        && id.name == param_name
18    {
19        return;
20    }
21    hints.record_param(arg.span, format!("{param_name}:"));
22}
23
24#[allow(clippy::too_many_arguments)]
25pub(crate) fn check_fn(
26    f: &FnDecl,
27    input: &ResolvedCommons,
28    expr_types: &mut HashMap<Span, Ty>,
29    errors: &mut Vec<CompileError>,
30    refs: &mut RefSink,
31    hints: &mut HintSink,
32    locals: &mut LocalsSink,
33    requirements: &mut RequirementSink,
34) {
35    // v0.20a: the fn's type parameters are *rigid* type variables while
36    // checking its own body. A type param shadowing a declared type is
37    // confusing — diagnose the collision.
38    let vars: HashSet<String> = f
39        .type_params
40        .iter()
41        .map(|tp| tp.name.name.clone())
42        .collect();
43    for tp in &f.type_params {
44        if input.types.contains_key(&tp.name.name) {
45            errors.push(
46                CompileError::new(
47                    "bynk.generics.type_arg_mismatch",
48                    tp.span,
49                    format!(
50                        "type parameter `{}` shadows the declared type of the same name",
51                        tp.name.name
52                    ),
53                )
54                .with_note("rename the type parameter"),
55            );
56        }
57    }
58    let return_ty = match resolve_type_ref_in(&f.return_type, &input.types, &vars) {
59        Some(t) => t,
60        None => return,
61    };
62    record_type_refs(&f.return_type, &input.types, &vars, refs);
63    let mut param_scope: HashMap<String, Ty> = HashMap::new();
64    // For methods, the implicit `self` parameter has the attached type.
65    if let FnName::Method { type_name, .. } = &f.name
66        && f.has_self
67        && let Some(self_ty) = type_from_decl(type_name, &input.types)
68    {
69        param_scope.insert("self".to_string(), self_ty);
70    }
71    for p in &f.params {
72        if let Some(ty) = resolve_type_ref_in(&p.type_ref, &input.types, &vars) {
73            record_type_refs(&p.type_ref, &input.types, &vars, refs);
74            // v0.31: a fn parameter is in scope over the whole body.
75            if p.name.name != "_" {
76                locals.record(p.name.name.clone(), p.name.span, ty.display(), f.body.span);
77            }
78            param_scope.insert(p.name.name.clone(), ty);
79        }
80    }
81    // v0.115: check the function's contract clauses (`requires`/`ensures`) in
82    // the parameter scope before the body — a contract is the invariant
83    // predicate attached to a function (ADR 0144). `result` in an `ensures` is
84    // the return value, awaited for an `Effect`.
85    if !f.requires.is_empty() || !f.ensures.is_empty() {
86        let result_ty = match &return_ty {
87            Ty::Effect(inner) => (**inner).clone(),
88            t => t.clone(),
89        };
90        let has_result_param = f.params.iter().any(|p| p.name.name == "result");
91        let fn_label = format!("function `{}`", f.name.display());
92        check_contracts(
93            &f.requires,
94            &f.ensures,
95            &param_scope,
96            &result_ty,
97            has_result_param,
98            &fn_label,
99            input,
100            expr_types,
101            errors,
102            refs,
103            hints,
104            locals,
105            requirements,
106            &vars,
107        );
108    }
109    let effectful = matches!(&return_ty, Ty::Effect(_));
110    let mut ctx = Ctx {
111        input,
112        expr_types,
113        errors,
114        refs,
115        hints,
116        locals,
117        requirements,
118        scopes: vec![param_scope],
119        return_ty: return_ty.clone(),
120        return_ty_span: f.return_type.span(),
121        effectful,
122        agent_state_ty: None,
123        commit_seen: false,
124        caps: CapabilityCtx::default(),
125        in_test_body: false,
126        test_services: HashSet::new(),
127        type_vars: vars.clone(),
128        store_cells: HashMap::new(),
129        store_maps: HashMap::new(),
130        store_sets: HashMap::new(),
131        store_caches: HashMap::new(),
132        store_logs: HashMap::new(),
133    };
134    let Some(body_ty) = type_of_block(&f.body, Some(&return_ty), &mut ctx) else {
135        return;
136    };
137    if !compatible(&body_ty, &return_ty) {
138        ctx.errors.push(
139            CompileError::new(
140                "bynk.types.return_mismatch",
141                f.body.tail.span,
142                format!(
143                    "function body has type `{}`, but the declared return type is `{}`",
144                    body_ty.display(),
145                    return_ty.display()
146                ),
147            )
148            .with_label(f.return_type.span(), "declared return type"),
149        );
150    }
151}
152
153/// v0.11: type-check an agent state-field initialiser (`field: T = init`). The
154/// initialiser must be a *static* value of the field type — it is checked in an
155/// empty, pure scope (so `self`, parameters, capabilities, and effects are all
156/// out of reach) with the field type as the expected type, so refined literals
157/// admit (v0.9.4) and sum variants resolve. The init's expression types are
158/// recorded into `expr_types` for emission; a single
159/// `bynk.agents.bad_state_initialiser` is pushed on any failure.
160#[allow(clippy::too_many_arguments)]
161pub fn check_state_initialiser(
162    init: &Expr,
163    field_type: &TypeRef,
164    input: &ResolvedCommons,
165    expr_types: &mut HashMap<Span, Ty>,
166    errors: &mut Vec<CompileError>,
167    refs: &mut RefSink,
168    hints: &mut HintSink,
169    locals: &mut LocalsSink,
170) {
171    let Some(field_ty) = resolve_type_ref(field_type, &input.types) else {
172        return; // an unresolved field type is reported elsewhere
173    };
174    let mut local_errors: Vec<CompileError> = Vec::new();
175    // A state initialiser is a static, pure value — no capability calls reach
176    // here — so requirements are discarded into a throwaway sink.
177    let mut init_requirements = RequirementSink::new();
178    let result = {
179        let mut ctx = Ctx {
180            input,
181            expr_types,
182            errors: &mut local_errors,
183            refs,
184            hints,
185            locals,
186            requirements: &mut init_requirements,
187            scopes: vec![HashMap::new()],
188            return_ty: field_ty.clone(),
189            return_ty_span: init.span,
190            effectful: false,
191            agent_state_ty: None,
192            commit_seen: false,
193            caps: CapabilityCtx::default(),
194            in_test_body: false,
195            test_services: HashSet::new(),
196            type_vars: HashSet::new(),
197            store_cells: HashMap::new(),
198            store_maps: HashMap::new(),
199            store_sets: HashMap::new(),
200            store_caches: HashMap::new(),
201            store_logs: HashMap::new(),
202        };
203        type_of(init, Some(&field_ty), &mut ctx)
204    };
205    let compatible_result = matches!(&result, Some(t) if compatible(t, &field_ty));
206    if !compatible_result || !local_errors.is_empty() {
207        let got = result
208            .as_ref()
209            .map(|t| t.display())
210            .unwrap_or_else(|| "an invalid expression".to_string());
211        errors.push(
212            CompileError::new(
213                "bynk.agents.bad_state_initialiser",
214                init.span,
215                format!(
216                    "state field initialiser must be a static value of type `{}` (got `{got}`)",
217                    field_ty.display(),
218                ),
219            )
220            .with_note(
221                "an initialiser is a compile-time value — a literal, a sum variant, \
222                 `Some`/`None`/`Ok`/`Err`, a record, or `T.unsafe(lit)` — with no reference to \
223                 `self`, parameters, or capabilities",
224            ),
225        );
226    }
227}
228
229/// v0.91 (ADR 0116 D6): the `bynk.list` free functions whose method forms
230/// shipped with slice 1 are **deprecated** in favour of the method-chain
231/// vocabulary. `reverse`/`traverse` stay (no method form yet). Emits a
232/// non-failing warning (ADR 0117) with a machine-applicable rewrite to the
233/// method form — `map(xs, f)` → `xs.map(f)`, `find(xs, p)` → `xs.filter(p).first()`.
234fn warn_bynk_list_deprecation(name: &Ident, args: &[Expr], call_span: Span, ctx: &mut Ctx) {
235    if ctx.input.imported_from.get(&name.name).map(String::as_str) != Some("bynk.list") {
236        return;
237    }
238    // The method-form spelling each free function rewrites to.
239    let method_form: &str = match name.name.as_str() {
240        "map" => "xs.map(f)",
241        "filter" => "xs.filter(p)",
242        "any" => "xs.any(p)",
243        "all" => "xs.all(p)",
244        "find" => "xs.filter(p).first()",
245        _ => return, // reverse / traverse keep their free-function form for now
246    };
247    let mut err = CompileError::new(
248        "bynk.list.deprecated_function",
249        name.span,
250        format!(
251            "`bynk.list.{}` is deprecated — use the `List` method form `{method_form}`",
252            name.name
253        ),
254    )
255    .with_note(
256        "the `bynk.list.*` free functions are superseded by the method-chain vocabulary (ADR 0116); the method form reads left-to-right and chains",
257    );
258    // The auto-fix needs the (list, fn) shape — a wrong-arity call is reported
259    // elsewhere; only offer the rewrite when it is well-formed.
260    if args.len() == 2 {
261        let mut edits = vec![
262            // delete `name(` — the receiver becomes the first argument.
263            (
264                Span::new(name.span.start, args[0].span.start),
265                String::new(),
266            ),
267            // the `, ` between the two args becomes `.<method>(`.
268            (
269                Span::new(args[0].span.end, args[1].span.start),
270                format!(
271                    ".{}(",
272                    if name.name == "find" {
273                        "filter"
274                    } else {
275                        &name.name
276                    }
277                ),
278            ),
279        ];
280        if name.name == "find" {
281            // `filter(p)` then `.first()` after the closing `)`.
282            edits.push((
283                Span::new(call_span.end, call_span.end),
284                ".first()".to_string(),
285            ));
286        }
287        err = err.with_suggestion(
288            format!("rewrite to the `List` method form `{method_form}`"),
289            edits,
290            Applicability::MachineApplicable,
291        );
292    }
293    ctx.errors.push(err);
294}
295
296pub(crate) fn check_call(
297    name: &Ident,
298    type_args: &[TypeRef],
299    args: &[Expr],
300    span: Span,
301    ctx: &mut Ctx,
302) -> Option<Ty> {
303    if let Some(fn_decl) = ctx.input.fns.get(&name.name).cloned() {
304        ctx.refs.record(name.span, SymbolKind::Fn, &name.name);
305        warn_bynk_list_deprecation(name, args, span, ctx);
306        return check_call_against_fn(name, &fn_decl, type_args, args, ctx);
307    }
308    // v0.20a: explicit type arguments only apply to (generic) functions.
309    if !type_args.is_empty() {
310        ctx.errors.push(CompileError::new(
311            "bynk.generics.type_arg_mismatch",
312            span,
313            format!(
314                "`{}` is not a generic function — it takes no type arguments",
315                name.name
316            ),
317        ));
318        for a in args {
319            let _ = type_of(a, None, ctx);
320        }
321        return None;
322    }
323    // Could be a bare variant constructor with payload.
324    let owners: Vec<TypeDecl> = ctx
325        .input
326        .types
327        .values()
328        .filter(|t| matches!(&t.body, TypeBody::Sum(s) if s.variants.iter().any(|v| v.name.name == name.name)))
329        .cloned()
330        .collect();
331    if owners.len() == 1 {
332        let owner = owners.into_iter().next().unwrap();
333        return check_variant_construction(&owner, &name.name, args, span, ctx);
334    }
335    // Agent instantiation: `AgentName(key)` constructs an instance keyed by
336    // `key`. The result type carries the agent's name so subsequent
337    // `agent_instance.method(args)` lookups can find the agent's handler set.
338    if let Some(agent) = ctx.input.agents.get(&name.name).cloned() {
339        ctx.refs.record(name.span, SymbolKind::Agent, &name.name);
340        let key_ty = resolve_type_ref(&agent.key_type, &ctx.input.types);
341        if args.len() != 1 {
342            ctx.errors.push(CompileError::new(
343                "bynk.agent.construction_arity",
344                span,
345                format!(
346                    "agent `{}` is constructed with one key argument, but {} were given",
347                    name.name,
348                    args.len()
349                ),
350            ));
351            for a in args {
352                let _ = type_of(a, None, ctx);
353            }
354            return None;
355        }
356        let arg_ty = type_of(&args[0], key_ty.as_ref(), ctx);
357        if let (Some(a), Some(k)) = (arg_ty.as_ref(), key_ty.as_ref())
358            && !compatible(a, k)
359        {
360            ctx.errors.push(CompileError::new(
361                "bynk.agent.key_mismatch",
362                args[0].span,
363                format!(
364                    "agent `{}` key is `{}`, but a value of type `{}` was given",
365                    name.name,
366                    k.display(),
367                    a.display()
368                ),
369            ));
370        }
371        return Some(Ty::Named {
372            name: name.name.clone(),
373            kind: NamedKind::Record,
374        });
375    }
376    // v0.20a: value application — calling a scope binding (param/local) of
377    // function type. Placed AFTER fns/variants/agents: putting scope first
378    // would change the meaning of currently-passing programs (the additive
379    // guard); the resulting ident/call precedence asymmetry is pre-existing
380    // and documented in §5.
381    if let Some(ty) = ctx.lookup(&name.name) {
382        return match ty {
383            Ty::Fn { params, ret } => check_value_application(name, &params, &ret, args, span, ctx),
384            other => {
385                // Relocated from the resolver (which has no type info): a
386                // non-function-typed value called as a function.
387                ctx.errors.push(
388                    CompileError::new(
389                        "bynk.resolve.param_as_function",
390                        span,
391                        format!(
392                            "`{}` has type `{}` and is not callable",
393                            name.name,
394                            other.display()
395                        ),
396                    )
397                    .with_note("only values of function type can be applied"),
398                );
399                for a in args {
400                    let _ = type_of(a, None, ctx);
401                }
402                None
403            }
404        };
405    }
406    let _ = span;
407    None
408}
409
410fn check_value_application(
411    name: &Ident,
412    params: &[Ty],
413    ret: &Ty,
414    args: &[Expr],
415    span: Span,
416    ctx: &mut Ctx,
417) -> Option<Ty> {
418    if ret.is_effect() && !ctx.effectful {
419        ctx.errors.push(
420            CompileError::new(
421                "bynk.effect.fn_value_in_pure_context",
422                span,
423                format!(
424                    "`{}` is an effectful function (`{}`) and cannot be called in a pure context",
425                    name.name,
426                    Ty::Fn {
427                        params: params.to_vec(),
428                        ret: Box::new(ret.clone())
429                    }
430                    .display()
431                ),
432            )
433            .with_note(
434                "effectful function values may only be called where the enclosing body is effectful (its return type is an Effect)",
435            ),
436        );
437    }
438    if params.len() != args.len() {
439        ctx.errors.push(CompileError::new(
440            "bynk.types.call_arity",
441            span,
442            format!(
443                "`{}` takes {} argument(s), but {} were given",
444                name.name,
445                params.len(),
446                args.len()
447            ),
448        ));
449        for a in args {
450            let _ = type_of(a, None, ctx);
451        }
452        return None;
453    }
454    for (arg, param_ty) in args.iter().zip(params) {
455        let arg_ty = type_of(arg, Some(param_ty), ctx);
456        if let Some(a) = arg_ty.as_ref()
457            && !compatible(a, param_ty)
458        {
459            ctx.errors.push(CompileError::new(
460                "bynk.types.argument_mismatch",
461                arg.span,
462                format!(
463                    "argument has type `{}`, but `{}` expects `{}`",
464                    a.display(),
465                    name.name,
466                    param_ty.display()
467                ),
468            ));
469        }
470    }
471    Some(ret.clone())
472}
473
474/// v0.20a: instantiate and check a call to a generic function. Two-pass
475/// argument-directed inference: pass 1 types every **non-lambda** argument
476/// left-to-right against the (possibly still Var-bearing) expected — safe
477/// because every expected-driven feature in `type_of` matches concrete
478/// variants, so a Var falls through benignly (pinned by a unit test) — and
479/// unifies; pass 2 types **lambda** arguments against the now-substituted
480/// expecteds (a lambda whose expected params are still Var-bearing is
481/// uninferable) and unifies the result, capturing return-position variables.
482/// Conflicts demand exact equality (`bynk.generics.type_arg_mismatch`); the
483/// explicit `name[T](…)` form builds the substitution directly.
484fn check_generic_call(
485    name: &Ident,
486    fn_decl: &FnDecl,
487    type_args: &[TypeRef],
488    args: &[Expr],
489    ctx: &mut Ctx,
490) -> Option<Ty> {
491    let vars: HashSet<String> = fn_decl
492        .type_params
493        .iter()
494        .map(|tp| tp.name.name.clone())
495        .collect();
496    if fn_decl.params.len() != args.len() {
497        for a in args {
498            let _ = type_of(a, None, ctx);
499        }
500        return None;
501    }
502    let var_params: Vec<Option<Ty>> = fn_decl
503        .params
504        .iter()
505        .map(|p| resolve_type_ref_in(&p.type_ref, &ctx.input.types, &vars))
506        .collect();
507    let ret_pattern = resolve_type_ref_in(&fn_decl.return_type, &ctx.input.types, &vars)?;
508
509    let mut subst: HashMap<String, Ty> = HashMap::new();
510    if !type_args.is_empty() {
511        if type_args.len() != fn_decl.type_params.len() {
512            ctx.errors.push(CompileError::new(
513                "bynk.generics.type_arg_mismatch",
514                name.span,
515                format!(
516                    "`{}` takes {} type argument(s), but {} were given",
517                    name.name,
518                    fn_decl.type_params.len(),
519                    type_args.len()
520                ),
521            ));
522            return None;
523        }
524        for (tp, ta) in fn_decl.type_params.iter().zip(type_args) {
525            // Resolve explicit type args with the *enclosing* fn's type
526            // params in scope, so `identity[A](x)` inside a generic body
527            // works.
528            let ty = resolve_type_ref_in(ta, &ctx.input.types, &ctx.type_vars)?;
529            subst.insert(tp.name.name.clone(), ty);
530        }
531    }
532
533    let mut arg_tys: Vec<Option<Ty>> = vec![None; args.len()];
534    // Pass 1 — non-lambda arguments.
535    for (i, arg) in args.iter().enumerate() {
536        if matches!(arg.kind, ExprKind::Lambda(_)) {
537            continue;
538        }
539        let expected = var_params[i].as_ref().map(|p| substitute(p, &subst));
540        let ty = type_of(arg, expected.as_ref(), ctx);
541        if let (Some(pattern), Some(actual)) = (var_params[i].as_ref(), ty.as_ref())
542            && !unify(pattern, actual, &mut subst)
543        {
544            ctx.errors.push(CompileError::new(
545                "bynk.generics.type_arg_mismatch",
546                arg.span,
547                format!(
548                    "argument {} infers a type for `{}`'s type parameter that conflicts with an earlier argument — annotate with `{}[T](…)`",
549                    i + 1,
550                    name.name,
551                    name.name
552                ),
553            ));
554            return None;
555        }
556        arg_tys[i] = ty;
557    }
558    // Pass 2 — lambda arguments, against substituted expecteds.
559    for (i, arg) in args.iter().enumerate() {
560        if !matches!(arg.kind, ExprKind::Lambda(_)) {
561            continue;
562        }
563        let expected = var_params[i].as_ref().map(|p| substitute(p, &subst));
564        let params_unconstrained = matches!(
565            expected.as_ref(),
566            Some(Ty::Fn { params, .. }) if params.iter().any(contains_var)
567        );
568        let fully_annotated = matches!(
569            &arg.kind,
570            ExprKind::Lambda(l) if l.params.iter().all(|p| p.type_ref.is_some())
571        );
572        if params_unconstrained && !fully_annotated {
573            ctx.errors.push(
574                CompileError::new(
575                    "bynk.generics.uninferable_type_arg",
576                    arg.span,
577                    format!(
578                        "the lambda's parameter types depend on `{}`'s type parameters, which the other arguments do not determine",
579                        name.name
580                    ),
581                )
582                .with_note("annotate the lambda's parameters, or give explicit type arguments: `name[T](…)`"),
583            );
584            return None;
585        }
586        // A fully-annotated lambda grounds the variables itself: type it
587        // bottom-up and let unify capture them.
588        let ty = if params_unconstrained {
589            type_of(arg, None, ctx)
590        } else {
591            type_of(arg, expected.as_ref(), ctx)
592        };
593        if let (Some(pattern), Some(actual)) = (var_params[i].as_ref(), ty.as_ref())
594            && !unify(pattern, actual, &mut subst)
595        {
596            ctx.errors.push(CompileError::new(
597                "bynk.generics.type_arg_mismatch",
598                arg.span,
599                format!(
600                    "the lambda's type conflicts with `{}`'s inferred type arguments",
601                    name.name
602                ),
603            ));
604            return None;
605        }
606        arg_tys[i] = ty;
607    }
608    // Every type parameter must now be determined.
609    for tp in &fn_decl.type_params {
610        if !subst.contains_key(&tp.name.name) {
611            ctx.errors.push(
612                CompileError::new(
613                    "bynk.generics.uninferable_type_arg",
614                    name.span,
615                    format!(
616                        "type parameter `{}` of `{}` is neither inferable from the arguments nor given explicitly",
617                        tp.name.name, name.name
618                    ),
619                )
620                .with_label(tp.span, "declared here")
621                .with_note("give explicit type arguments: `name[T](…)`"),
622            );
623            return None;
624        }
625    }
626    // Final compatibility over the fully-ground parameter types.
627    let mut ok = true;
628    for (i, (pattern, arg)) in var_params.iter().zip(args).enumerate() {
629        record_param_hint(ctx.hints, &fn_decl.params[i].name.name, arg);
630        let (Some(pattern), Some(arg_ty)) = (pattern.as_ref(), arg_tys[i].as_ref()) else {
631            continue;
632        };
633        let ground = substitute(pattern, &subst);
634        if !compatible(arg_ty, &ground) {
635            ctx.errors.push(CompileError::new(
636                "bynk.types.argument_mismatch",
637                arg.span,
638                format!(
639                    "argument {} to `{}` has type `{}`, but `{}` is expected",
640                    i + 1,
641                    name.name,
642                    arg_ty.display(),
643                    ground.display()
644                ),
645            ));
646            ok = false;
647        }
648    }
649    if !ok {
650        return None;
651    }
652    // v0.39 (ADR 0072): when the user omitted the type arguments, show the
653    // inferred ones as a `Type`-kind hint after the function name —
654    // `identity` ⟨`[Int]`⟩ `(5)`. Declaration order; skipped if any var stayed
655    // unresolved (defensive — the arg loop above already grounds them).
656    if type_args.is_empty() && !fn_decl.type_params.is_empty() {
657        let rendered: Option<Vec<String>> = fn_decl
658            .type_params
659            .iter()
660            .map(|tp| subst.get(&tp.name.name).map(|t| t.display()))
661            .collect();
662        if let Some(parts) = rendered {
663            ctx.hints
664                .record(name.span, format!("[{}]", parts.join(", ")));
665        }
666    }
667    let ret = substitute(&ret_pattern, &subst);
668    // v0.20b: the return is ground *up to the caller's rigid type
669    // parameters* — a generic fn calling another generic fn (bynk.list's
670    // `map` calling `reverse`) legitimately instantiates the callee at its
671    // own rigid vars, which flow through `compatible` by name-equality.
672    Some(ret)
673}
674
675fn check_call_against_fn(
676    name: &Ident,
677    fn_decl: &FnDecl,
678    type_args: &[TypeRef],
679    args: &[Expr],
680    ctx: &mut Ctx,
681) -> Option<Ty> {
682    // v0.20a: generic functions take the instantiation path; the
683    // non-generic path below runs byte-identically to v0.19 (the additive
684    // guard). Explicit type args on a non-generic fn are rejected.
685    if !fn_decl.type_params.is_empty() {
686        return check_generic_call(name, fn_decl, type_args, args, ctx);
687    }
688    if !type_args.is_empty() {
689        ctx.errors.push(CompileError::new(
690            "bynk.generics.type_arg_mismatch",
691            name.span,
692            format!(
693                "`{}` is not a generic function — it takes no type arguments",
694                name.name
695            ),
696        ));
697        for a in args {
698            let _ = type_of(a, None, ctx);
699        }
700        return None;
701    }
702    if fn_decl.params.len() != args.len() {
703        for a in args {
704            let _ = type_of(a, None, ctx);
705        }
706        return None;
707    }
708    let resolved_params: Vec<(Option<Ty>, &Param)> = fn_decl
709        .params
710        .iter()
711        .map(|p| (resolve_type_ref(&p.type_ref, &ctx.input.types), p))
712        .collect();
713    let mut ok = true;
714    for (i, ((param_ty, param), arg)) in resolved_params.iter().zip(args.iter()).enumerate() {
715        record_param_hint(ctx.hints, &param.name.name, arg);
716        let arg_ty = type_of(arg, param_ty.as_ref(), ctx);
717        let (Some(arg_ty), Some(param_ty)) = (arg_ty, param_ty.as_ref()) else {
718            ok = false;
719            continue;
720        };
721        if !compatible(&arg_ty, param_ty) {
722            ctx.errors.push(
723                CompileError::new(
724                    "bynk.types.argument_mismatch",
725                    arg.span,
726                    format!(
727                        "argument {} to `{}` has type `{}`, but parameter `{}` expects `{}`",
728                        i + 1,
729                        name.name,
730                        arg_ty.display(),
731                        param.name.name,
732                        param_ty.display()
733                    ),
734                )
735                .with_label(param.span, "parameter declared here"),
736            );
737            ok = false;
738        }
739    }
740    if !ok {
741        return None;
742    }
743    resolve_type_ref(&fn_decl.return_type, &ctx.input.types)
744}
745
746/// Type-check a kernel-method argument against its expected type, with the
747/// expected type propagated in (so lambdas and literals type contextually).
748pub(crate) fn check_arg(arg: &Expr, expected: &Ty, what: &str, ctx: &mut Ctx) {
749    let Some(actual) = type_of(arg, Some(expected), ctx) else {
750        return;
751    };
752    if !compatible(&actual, expected) {
753        ctx.errors.push(CompileError::new(
754            "bynk.types.type_mismatch",
755            arg.span,
756            format!(
757                "{what} has type `{}`, but `{}` is required",
758                actual.display(),
759                expected.display()
760            ),
761        ));
762    }
763}
764
765/// Record a capability reference's binding edge, qualifying flattened bare
766/// names (`consumes U { Cap }`) to their providing unit (v0.25). A bare
767/// non-flattened name is the consuming unit's own declaration — qualified
768/// at assembly.
769fn record_capability_ref(span: Span, name: &str, ctx: &mut Ctx) {
770    if let Some(unit) = ctx.input.cross_context.flattened_caps.get(name) {
771        ctx.refs
772            .record_in_unit(span, SymbolKind::Capability, name, unit);
773    } else {
774        ctx.refs.record(span, SymbolKind::Capability, name);
775    }
776}
777
778pub(crate) fn check_static_call(
779    type_name: &Ident,
780    method: &Ident,
781    args: &[Expr],
782    span: Span,
783    ctx: &mut Ctx,
784) -> Option<Ty> {
785    // Capability dispatch (v0.5): if `type_name` names a capability declared
786    // in the context, dispatch via the capability table. If the capability is
787    // declared but not in `given`, error specifically.
788    if ctx.caps.declared_capabilities.contains_key(&type_name.name)
789        && !ctx.caps.capabilities.contains_key(&type_name.name)
790    {
791        record_capability_ref(type_name.span, &type_name.name, ctx);
792        let mut err = CompileError::new(
793            "bynk.given.undeclared_capability",
794            type_name.span,
795            format!(
796                "capability `{}` is used but not listed in the handler's `given` clause",
797                type_name.name
798            ),
799        )
800        .with_note(format!(
801            "add `{}` to the handler's `given` clause so the dependency surface is visible at the declaration site",
802            type_name.name
803        ));
804        // v0.26 (ADR 0054): the one-click counterpart of the note.
805        if let Some((span, insert)) = given_insertion_edit(
806            &ctx.caps.given_entries,
807            ctx.caps.given_anchor,
808            &type_name.name,
809        ) {
810            err = err.with_suggestion(
811                format!("add `{}` to the `given` clause", type_name.name),
812                vec![(span, insert)],
813                Applicability::MachineApplicable,
814            );
815        }
816        ctx.errors.push(err);
817        // v0.99: an uncovered direct capability call — record it so the ghost
818        // `given` inlay hint can offer the same clause at the declaration site
819        // (DECISION D/E). `span` is the call site (the inner `given_insertion_edit`
820        // binding shadowed it only within the `if let` above).
821        record_requirement(
822            ctx,
823            &type_name.name,
824            span,
825            RequirementSource::DirectCall {
826                op: method.name.clone(),
827            },
828            false,
829        );
830        for a in args {
831            let _ = type_of(a, None, ctx);
832        }
833        return None;
834    }
835    if let Some(cap) = ctx.caps.capabilities.get(&type_name.name).cloned() {
836        record_capability_ref(type_name.span, &type_name.name, ctx);
837        if !ctx.effectful {
838            ctx.errors.push(
839                CompileError::new(
840                    "bynk.effect.capability_in_pure_context",
841                    span,
842                    format!(
843                        "capability `{}` can only be called inside an effectful body (one returning `Effect[T]`)",
844                        type_name.name
845                    ),
846                ),
847            );
848        }
849        ctx.caps.given_used.insert(type_name.name.clone());
850        // v0.99: a covered direct capability call — the call site *is* the
851        // reason (DECISION C). Recorded so hover can explain what a declared
852        // `given Cap` is for; no inlay hint (already covered).
853        record_requirement(
854            ctx,
855            &type_name.name,
856            span,
857            RequirementSource::DirectCall {
858                op: method.name.clone(),
859            },
860            true,
861        );
862        let Some(op) = cap.ops.iter().find(|o| o.name == method.name) else {
863            ctx.errors.push(CompileError::new(
864                "bynk.capability.unknown_operation",
865                method.span,
866                format!(
867                    "capability `{}` has no operation named `{}`",
868                    type_name.name, method.name
869                ),
870            ));
871            for a in args {
872                let _ = type_of(a, None, ctx);
873            }
874            return None;
875        };
876        // v0.36 (ADR 0069, slice 2): the op is an index symbol keyed by the
877        // compound `"Cap.op"` name; this local call is a reference.
878        ctx.refs.record(
879            method.span,
880            SymbolKind::CapabilityOp,
881            &format!("{}.{}", type_name.name, method.name),
882        );
883        if op.params.len() != args.len() {
884            ctx.errors.push(CompileError::new(
885                "bynk.capability.op_arity",
886                span,
887                format!(
888                    "capability operation `{}.{}` expects {} argument(s), but {} were given",
889                    type_name.name,
890                    method.name,
891                    op.params.len(),
892                    args.len()
893                ),
894            ));
895            for a in args {
896                let _ = type_of(a, None, ctx);
897            }
898            return None;
899        }
900        let op_clone = op.clone();
901        for (i, (param_ty, arg)) in op_clone.params.iter().zip(args.iter()).enumerate() {
902            let arg_ty = type_of(arg, Some(param_ty), ctx);
903            if let Some(actual) = arg_ty
904                && !compatible(&actual, param_ty)
905            {
906                ctx.errors.push(CompileError::new(
907                    "bynk.types.argument_mismatch",
908                    arg.span,
909                    format!(
910                        "argument {} to capability `{}.{}` has type `{}`, but parameter expects `{}`",
911                        i + 1,
912                        type_name.name,
913                        method.name,
914                        actual.display(),
915                        param_ty.display()
916                    ),
917                ));
918            }
919        }
920        return Some(op_clone.return_ty);
921    }
922    let decl = ctx.input.types.get(&type_name.name)?.clone();
923    ctx.refs
924        .record(type_name.span, SymbolKind::Type, &type_name.name);
925    let table = ctx
926        .input
927        .methods
928        .get(&type_name.name)
929        .cloned()
930        .unwrap_or_default();
931
932    // 1) User-declared static method.
933    if let Some(method_decl) = table.statics.get(&method.name).cloned() {
934        return check_method_args(&method_decl, args, ctx, type_name, method);
935    }
936
937    // 2) Built-in `of` constructor on refined or opaque types.
938    if method.name == OF
939        && let Some(base) = type_decl_base(&decl)
940    {
941        if args.len() != 1 {
942            ctx.errors.push(CompileError::new(
943                "bynk.types.constructor_arity",
944                span,
945                format!(
946                    "constructor `{}.of` expects 1 argument, but {} were given",
947                    type_name.name,
948                    args.len()
949                ),
950            ));
951            return None;
952        }
953        let arg = &args[0];
954        let expected = Ty::Base(base);
955        let arg_ty = type_of(arg, Some(&expected), ctx)?;
956        if !compatible(&arg_ty, &expected) {
957            ctx.errors.push(CompileError::new(
958                "bynk.types.constructor_base_mismatch",
959                arg.span,
960                format!(
961                    "constructor `{}.of` expects a `{}` argument, but got `{}`",
962                    type_name.name,
963                    base.name(),
964                    arg_ty.display()
965                ),
966            ));
967            return None;
968        }
969        // `.of` is always the runtime constructor: it returns
970        // `Result[T, ValidationError]`. Compile-time literal admission (v0.9.4)
971        // happens instead wherever an expected refined type is known — see
972        // `admit_refined_literal`, used by `type_of` — so `.of`'s type never
973        // depends on the form of its argument.
974        return Some(Ty::Result(
975            Box::new(named_ty(&decl)),
976            Box::new(Ty::ValidationError),
977        ));
978    }
979
980    // 2b) Built-in `unsafe` constructor on opaque types — only available
981    // inside the defining commons.
982    if method.name == UNSAFE
983        && let TypeBody::Opaque { base, .. } = &decl.body
984    {
985        if !ctx.input.is_local_type(&decl.name.name) {
986            ctx.errors.push(
987                CompileError::new(
988                    "bynk.types.opaque_unsafe_outside",
989                    method.span,
990                    format!(
991                        "`{}.unsafe(...)` is only available within the commons that defines the opaque type `{}`",
992                        type_name.name, type_name.name
993                    ),
994                )
995                .with_note(
996                    "outside the defining commons, opaque values are constructed via `T.of(value)`",
997                ),
998            );
999            return None;
1000        }
1001        if args.len() != 1 {
1002            ctx.errors.push(CompileError::new(
1003                "bynk.types.constructor_arity",
1004                span,
1005                format!(
1006                    "`{}.unsafe` expects 1 argument, but {} were given",
1007                    type_name.name,
1008                    args.len()
1009                ),
1010            ));
1011            return None;
1012        }
1013        let arg = &args[0];
1014        let expected = Ty::Base(*base);
1015        let arg_ty = type_of(arg, Some(&expected), ctx)?;
1016        if !compatible(&arg_ty, &expected) {
1017            ctx.errors.push(CompileError::new(
1018                "bynk.types.constructor_base_mismatch",
1019                arg.span,
1020                format!(
1021                    "`{}.unsafe` expects a `{}` argument, but got `{}`",
1022                    type_name.name,
1023                    base.name(),
1024                    arg_ty.display()
1025                ),
1026            ));
1027            return None;
1028        }
1029        return Some(named_ty(&decl));
1030    }
1031
1032    // 3) Qualified variant construction `TypeName.Variant(args)`.
1033    if let TypeBody::Sum(_) = &decl.body {
1034        return check_variant_construction(&decl, &method.name, args, span, ctx);
1035    }
1036
1037    ctx.errors.push(
1038        CompileError::new(
1039            "bynk.types.unknown_static_member",
1040            method.span,
1041            format!(
1042                "type `{}` has no static method or variant named `{}`",
1043                type_name.name, method.name
1044            ),
1045        )
1046        .with_label(decl.name.span, "type declared here"),
1047    );
1048    None
1049}
1050
1051fn check_method_args(
1052    method_decl: &FnDecl,
1053    args: &[Expr],
1054    ctx: &mut Ctx,
1055    type_name: &Ident,
1056    method: &Ident,
1057) -> Option<Ty> {
1058    if method_decl.params.len() != args.len() {
1059        ctx.errors.push(
1060            CompileError::new(
1061                "bynk.types.method_arity",
1062                method.span,
1063                format!(
1064                    "static method `{}.{}` expects {} argument(s), but {} were given",
1065                    type_name.name,
1066                    method.name,
1067                    method_decl.params.len(),
1068                    args.len()
1069                ),
1070            )
1071            .with_label(method_decl.name.ident().span, "method declared here"),
1072        );
1073        for a in args {
1074            let _ = type_of(a, None, ctx);
1075        }
1076        return None;
1077    }
1078    let mut ok = true;
1079    for (i, (param, arg)) in method_decl.params.iter().zip(args.iter()).enumerate() {
1080        record_param_hint(ctx.hints, &param.name.name, arg);
1081        let expected = resolve_type_ref(&param.type_ref, &ctx.input.types);
1082        let actual = type_of(arg, expected.as_ref(), ctx);
1083        let (Some(actual), Some(expected)) = (actual, expected) else {
1084            ok = false;
1085            continue;
1086        };
1087        if !compatible(&actual, &expected) {
1088            ctx.errors.push(CompileError::new(
1089                "bynk.types.argument_mismatch",
1090                arg.span,
1091                format!(
1092                    "argument {} to `{}.{}` has type `{}`, but parameter `{}` expects `{}`",
1093                    i + 1,
1094                    type_name.name,
1095                    method.name,
1096                    actual.display(),
1097                    param.name.name,
1098                    expected.display()
1099                ),
1100            ));
1101            ok = false;
1102        }
1103    }
1104    if !ok {
1105        return None;
1106    }
1107    resolve_type_ref(&method_decl.return_type, &ctx.input.types)
1108}
1109
1110/// v0.82 (ADR 0110): resolve a storage-map operation `<map>.<op>(args)` on a
1111/// `store Map[K, V]` field. The ops are effect-typed (storage I/O, awaited with
1112/// `<-`): `put`/`update`/`upsert`/`remove` → `Effect[()]`, `get` →
1113/// `Effect[Option[V]]`, `contains` → `Effect[Bool]`, `size` → `Effect[Int]`.
1114/// `update` on an absent key is a runtime fault; `upsert` is the default-if-absent
1115/// form. Dispatched by receiver provenance, so it never shadows the immutable
1116/// value `Map`'s pure methods.
1117pub(crate) fn check_store_map_op(
1118    method: &Ident,
1119    args: &[Expr],
1120    k: &Ty,
1121    v: &Ty,
1122    span: Span,
1123    ctx: &mut Ctx,
1124) -> Option<Ty> {
1125    let vfn = || Ty::Fn {
1126        params: vec![v.clone()],
1127        ret: Box::new(v.clone()),
1128    };
1129    // v0.105 (slice 3b-ii): a held `Map[K, Connection]` stores connection ids and
1130    // resolves them by identity; `update`/`upsert` transform the value through a
1131    // `(V) -> V` function, which has no meaning for a held resource — you cannot
1132    // derive a new connection from an old one. Reject them (the other entry ops —
1133    // `put`/`get`/`remove`/`contains`/`size` — are admitted) so the program is a
1134    // clean compile error rather than a silent miscompile.
1135    if v.is_held() && matches!(method.name.as_str(), "update" | "upsert") {
1136        ctx.errors.push(
1137            CompileError::new(
1138                "bynk.held.unsupported_map_op",
1139                method.span,
1140                format!(
1141                    "a held `Map[K, Connection]` has no `{}` operation — a held resource cannot be transformed by a `(Connection) -> Connection` function",
1142                    method.name
1143                ),
1144            )
1145            .with_note(
1146                "held connections are stored and resolved by identity; use `put`/`get`/`remove`",
1147            ),
1148        );
1149        for a in args {
1150            type_of(a, None, ctx);
1151        }
1152        return None;
1153    }
1154    let (expected, result): (Vec<Ty>, Ty) = match method.name.as_str() {
1155        "put" => (vec![k.clone(), v.clone()], Ty::Unit),
1156        "get" => (vec![k.clone()], Ty::Option(Box::new(v.clone()))),
1157        "remove" => (vec![k.clone()], Ty::Unit),
1158        "contains" => (vec![k.clone()], Ty::Base(BaseType::Bool)),
1159        "size" => (vec![], Ty::Base(BaseType::Int)),
1160        "update" => (vec![k.clone(), vfn()], Ty::Unit),
1161        "upsert" => (vec![k.clone(), v.clone(), vfn()], Ty::Unit),
1162        other => {
1163            ctx.errors.push(
1164                CompileError::new(
1165                    "bynk.store.unknown_op",
1166                    method.span,
1167                    format!(
1168                        "a `Map` store field has no operation `{other}` — expected `put`, `get`, \
1169                         `update`, `upsert`, `remove`, `contains`, or `size`"
1170                    ),
1171                )
1172                .with_note("storage-map ops are entry-level and effectful (await with `<-`)"),
1173            );
1174            for a in args {
1175                type_of(a, None, ctx);
1176            }
1177            return None;
1178        }
1179    };
1180    let effect = Ty::Effect(Box::new(result));
1181    if args.len() != expected.len() {
1182        ctx.errors.push(CompileError::new(
1183            "bynk.types.call_arity",
1184            span,
1185            format!(
1186                "`Map.{}` takes {} argument(s), found {}",
1187                method.name,
1188                expected.len(),
1189                args.len()
1190            ),
1191        ));
1192        for a in args {
1193            type_of(a, None, ctx);
1194        }
1195        return Some(effect);
1196    }
1197    for (a, exp) in args.iter().zip(expected.iter()) {
1198        if let Some(at) = type_of(a, Some(exp), ctx)
1199            && !compatible(&at, exp)
1200        {
1201            ctx.errors.push(CompileError::new(
1202                "bynk.types.argument_mismatch",
1203                a.span,
1204                format!("expected `{}`, found `{}`", exp.display(), at.display()),
1205            ));
1206        }
1207    }
1208    Some(effect)
1209}
1210
1211/// v0.99: record a capability requirement at `site` into the ledger, and — when
1212/// the enclosing handler's `given` does not cover it — push the diagnostic. The
1213/// single producer behind both the bare diagnostic and the editor surfaces
1214/// (DECISION D): every requirement is *recorded*, covered or not; only an
1215/// uncovered one errors. The `code`/`message` are the consuming feature's, so a
1216/// store op keeps its precise diagnostic; the ledger's *reason* renders from
1217/// `source` alone (DECISION C), never from `code`.
1218fn require_capability(
1219    site: Span,
1220    capability: &str,
1221    source: RequirementSource,
1222    ctx: &mut Ctx,
1223    code: &'static str,
1224    message: &str,
1225) {
1226    let covered = ctx.caps.capabilities.contains_key(capability);
1227    if covered {
1228        ctx.caps.given_used.insert(capability.to_string());
1229    } else {
1230        ctx.errors
1231            .push(CompileError::new(code, site, message).with_note(format!(
1232                "add `{capability}` to the handler's `given` clause"
1233            )));
1234    }
1235    record_requirement(ctx, capability, site, source, covered);
1236}
1237
1238/// Push a [`Requirement`] into the ledger. For an uncovered requirement it also
1239/// computes the materialization edit (the ghost `given` inlay hint's one-click
1240/// apply) from the handler's existing `given` entries and anchor — the same
1241/// `given_insertion_edit` the undeclared-capability quick-fix uses.
1242fn record_requirement(
1243    ctx: &mut Ctx,
1244    capability: &str,
1245    site: Span,
1246    source: RequirementSource,
1247    covered: bool,
1248) {
1249    let materialize = if covered {
1250        None
1251    } else {
1252        given_insertion_edit(&ctx.caps.given_entries, ctx.caps.given_anchor, capability).map(
1253            |(edit_span, edit_text)| Materialize {
1254                anchor: ctx.caps.given_anchor.unwrap_or(ctx.return_ty_span),
1255                edit_span,
1256                edit_text,
1257            },
1258        )
1259    };
1260    ctx.requirements.record(Requirement {
1261        capability: capability.to_string(),
1262        site,
1263        source,
1264        covered,
1265        materialize,
1266    });
1267}
1268
1269/// v0.87 (ADR 0113): resolve a storage-`Cache` operation `<cache>.<op>(args)` on
1270/// a `store Cache[K, V]` field. The op set is the storage `Map`'s
1271/// (`put`/`get`/`update`/`upsert`/`remove`/`contains`/`size`); every op but
1272/// `remove` additionally requires `given Clock` (eviction reads the clock).
1273pub(crate) fn check_store_cache_op(
1274    method: &Ident,
1275    args: &[Expr],
1276    k: &Ty,
1277    v: &Ty,
1278    span: Span,
1279    ctx: &mut Ctx,
1280) -> Option<Ty> {
1281    let vfn = || Ty::Fn {
1282        params: vec![v.clone()],
1283        ret: Box::new(v.clone()),
1284    };
1285    let (expected, result): (Vec<Ty>, Ty) = match method.name.as_str() {
1286        "put" => (vec![k.clone(), v.clone()], Ty::Unit),
1287        "get" => (vec![k.clone()], Ty::Option(Box::new(v.clone()))),
1288        "remove" => (vec![k.clone()], Ty::Unit),
1289        "contains" => (vec![k.clone()], Ty::Base(BaseType::Bool)),
1290        "size" => (vec![], Ty::Base(BaseType::Int)),
1291        "update" => (vec![k.clone(), vfn()], Ty::Unit),
1292        "upsert" => (vec![k.clone(), v.clone(), vfn()], Ty::Unit),
1293        other => {
1294            ctx.errors.push(
1295                CompileError::new(
1296                    "bynk.store.unknown_op",
1297                    method.span,
1298                    format!(
1299                        "a `Cache` store field has no operation `{other}` — expected `put`, \
1300                         `get`, `update`, `upsert`, `remove`, `contains`, or `size`"
1301                    ),
1302                )
1303                .with_note("storage-cache ops are entry-level and effectful (await with `<-`)"),
1304            );
1305            for a in args {
1306                type_of(a, None, ctx);
1307            }
1308            return None;
1309        }
1310    };
1311    // D4: every op but `remove` reads the clock for TTL expiry.
1312    if method.name != "remove" {
1313        require_capability(
1314            method.span,
1315            "Clock",
1316            RequirementSource::StoreOp {
1317                kind: StoreKind::Cache,
1318                op: method.name.clone(),
1319            },
1320            ctx,
1321            "bynk.store.cache_needs_clock",
1322            "a `Cache` operation applies TTL expiry, which reads the clock — the handler must declare `given Clock`",
1323        );
1324    }
1325    let effect = Ty::Effect(Box::new(result));
1326    if args.len() != expected.len() {
1327        ctx.errors.push(CompileError::new(
1328            "bynk.types.call_arity",
1329            span,
1330            format!(
1331                "`Cache.{}` takes {} argument(s), found {}",
1332                method.name,
1333                expected.len(),
1334                args.len()
1335            ),
1336        ));
1337        for a in args {
1338            type_of(a, None, ctx);
1339        }
1340        return Some(effect);
1341    }
1342    for (a, exp) in args.iter().zip(expected.iter()) {
1343        if let Some(at) = type_of(a, Some(exp), ctx)
1344            && !compatible(&at, exp)
1345        {
1346            ctx.errors.push(CompileError::new(
1347                "bynk.types.argument_mismatch",
1348                a.span,
1349                format!("expected `{}`, found `{}`", exp.display(), at.display()),
1350            ));
1351        }
1352    }
1353    Some(effect)
1354}
1355
1356/// v0.95 (ADR 0121): resolve a storage-`Log` operation `<log>.<op>(args)` on a
1357/// `store Log[T]` field. `append(e)` is the effectful, **non-idempotent** write
1358/// (`Effect[()]`) and the one clock-consuming op — it stamps `Clock.now()`, so it
1359/// requires `given Clock`. The time-window roots — `since(Instant)`/
1360/// `before(Instant)` / `between(Instant, Instant)` / `recent(Int)` / `reversed()`
1361/// — and the general query vocabulary lift the log into a lazy `Query[T]` over its
1362/// entry values; these need no clock (window bounds are explicit `Instant`s).
1363pub(crate) fn check_store_log_op(
1364    method: &Ident,
1365    args: &[Expr],
1366    elem: &Ty,
1367    span: Span,
1368    ctx: &mut Ctx,
1369) -> Option<Ty> {
1370    let query = || Ty::Query(Box::new(elem.clone()));
1371    let arity = |n: usize, ctx: &mut Ctx| {
1372        if args.len() != n {
1373            ctx.errors.push(CompileError::new(
1374                "bynk.types.call_arity",
1375                span,
1376                format!(
1377                    "`Log.{}` takes {n} argument(s), found {}",
1378                    method.name,
1379                    args.len()
1380                ),
1381            ));
1382            for a in args {
1383                type_of(a, None, ctx);
1384            }
1385            return false;
1386        }
1387        true
1388    };
1389    let window_arg = |a: &Expr, what: &str, ctx: &mut Ctx| {
1390        if let Some(at) = type_of(a, Some(&Ty::Base(BaseType::Instant)), ctx)
1391            && !compatible(&at, &Ty::Base(BaseType::Instant))
1392        {
1393            ctx.errors.push(CompileError::new(
1394                "bynk.types.argument_mismatch",
1395                a.span,
1396                format!("{what} expects `Instant`, found `{}`", at.display()),
1397            ));
1398        }
1399    };
1400    match method.name.as_str() {
1401        // The one effectful write — non-idempotent; stamps the clock.
1402        "append" => {
1403            require_capability(
1404                method.span,
1405                "Clock",
1406                RequirementSource::StoreOp {
1407                    kind: StoreKind::Log,
1408                    op: method.name.clone(),
1409                },
1410                ctx,
1411                "bynk.store.log_needs_clock",
1412                "`Log.append` stamps the current time, which reads the clock — the handler must declare `given Clock`",
1413            );
1414            if !arity(1, ctx) {
1415                return Some(Ty::Effect(Box::new(Ty::Unit)));
1416            }
1417            if let Some(at) = type_of(&args[0], Some(elem), ctx)
1418                && !compatible(&at, elem)
1419            {
1420                ctx.errors.push(CompileError::new(
1421                    "bynk.types.argument_mismatch",
1422                    args[0].span,
1423                    format!("expected `{}`, found `{}`", elem.display(), at.display()),
1424                ));
1425            }
1426            Some(Ty::Effect(Box::new(Ty::Unit)))
1427        }
1428        // Time-window query roots → `Query[T]` (lazy; no clock).
1429        "since" | "before" => {
1430            if !arity(1, ctx) {
1431                return Some(query());
1432            }
1433            window_arg(&args[0], &format!("`Log.{}`", method.name), ctx);
1434            Some(query())
1435        }
1436        "between" => {
1437            if !arity(2, ctx) {
1438                return Some(query());
1439            }
1440            window_arg(&args[0], "`Log.between` start", ctx);
1441            window_arg(&args[1], "`Log.between` end", ctx);
1442            Some(query())
1443        }
1444        "recent" => {
1445            if !arity(1, ctx) {
1446                return Some(query());
1447            }
1448            check_arg(
1449                &args[0],
1450                &Ty::Base(BaseType::Int),
1451                "the `Log.recent` count",
1452                ctx,
1453            );
1454            Some(query())
1455        }
1456        "reversed" => {
1457            if !arity(0, ctx) {
1458                return Some(query());
1459            }
1460            Some(query())
1461        }
1462        // The general query vocabulary over the entry values.
1463        name if is_query_op(name) => check_query_kernel_method(method, args, elem, span, ctx),
1464        other => {
1465            ctx.errors.push(
1466                CompileError::new(
1467                    "bynk.store.unknown_op",
1468                    method.span,
1469                    format!(
1470                        "a `Log` store field has no operation `{other}` — `append`, the \
1471                         time-window roots (`since`/`before`/`between`/`recent`/`reversed`), \
1472                         and the query builders/terminals"
1473                    ),
1474                )
1475                .with_note(
1476                    "`Log` reads are lazy `Query[T]`; only `append` is effectful and writes",
1477                ),
1478            );
1479            for a in args {
1480                type_of(a, None, ctx);
1481            }
1482            None
1483        }
1484    }
1485}
1486
1487/// v0.83: resolve a storage-set operation `<set>.<op>(args)` on a `store Set[T]`
1488/// field. Effect-typed entry ops: `add(t)`/`remove(t)` → `Effect[()]` (both
1489/// idempotent), `contains(t)` → `Effect[Bool]`, `size()` → `Effect[Int]`. Set
1490/// algebra (`union`/`intersection`/`difference`) is deferred (it needs a value
1491/// `Set` return type). Dispatched by receiver provenance.
1492pub(crate) fn check_store_set_op(
1493    method: &Ident,
1494    args: &[Expr],
1495    t: &Ty,
1496    span: Span,
1497    ctx: &mut Ctx,
1498) -> Option<Ty> {
1499    let (expected, result): (Vec<Ty>, Ty) = match method.name.as_str() {
1500        "add" => (vec![t.clone()], Ty::Unit),
1501        "remove" => (vec![t.clone()], Ty::Unit),
1502        "contains" => (vec![t.clone()], Ty::Base(BaseType::Bool)),
1503        "size" => (vec![], Ty::Base(BaseType::Int)),
1504        other => {
1505            ctx.errors.push(
1506                CompileError::new(
1507                    "bynk.store.unknown_op",
1508                    method.span,
1509                    format!(
1510                        "a `Set` store field has no operation `{other}` — expected `add`, \
1511                         `remove`, `contains`, or `size`"
1512                    ),
1513                )
1514                .with_note(
1515                    "set algebra (`union`/`intersection`/`difference`) is not in this slice",
1516                ),
1517            );
1518            for a in args {
1519                type_of(a, None, ctx);
1520            }
1521            return None;
1522        }
1523    };
1524    let effect = Ty::Effect(Box::new(result));
1525    if args.len() != expected.len() {
1526        ctx.errors.push(CompileError::new(
1527            "bynk.types.call_arity",
1528            span,
1529            format!(
1530                "`Set.{}` takes {} argument(s), found {}",
1531                method.name,
1532                expected.len(),
1533                args.len()
1534            ),
1535        ));
1536        for a in args {
1537            type_of(a, None, ctx);
1538        }
1539        return Some(effect);
1540    }
1541    for (a, exp) in args.iter().zip(expected.iter()) {
1542        if let Some(at) = type_of(a, Some(exp), ctx)
1543            && !compatible(&at, exp)
1544        {
1545            ctx.errors.push(CompileError::new(
1546                "bynk.types.argument_mismatch",
1547                a.span,
1548                format!("expected `{}`, found `{}`", exp.display(), at.display()),
1549            ));
1550        }
1551    }
1552    Some(effect)
1553}
1554
1555/// v0.98 (ADR 0125): resolve a storage-`Cell` operation `<cell>.<op>(args)` on a
1556/// `store Cell[T]` field. The single method-shaped cell op is `update(f)` —
1557/// `f: (T) -> T` — a read-modify-write typed `Effect[()]`. Reading a cell is the
1558/// bare-name sugar and writing it is `:=`, so `read`/`write` are not callable
1559/// methods (DECISION B). The combiner is a non-effectful `Ty::Fn`, so an
1560/// effectful body (including a bare read of another cell) fails the existing
1561/// function-type check (DECISION E). Dispatched by receiver provenance.
1562pub(crate) fn check_store_cell_op(
1563    method: &Ident,
1564    args: &[Expr],
1565    t: &Ty,
1566    span: Span,
1567    ctx: &mut Ctx,
1568) -> Option<Ty> {
1569    let tfn = || Ty::Fn {
1570        params: vec![t.clone()],
1571        ret: Box::new(t.clone()),
1572    };
1573    let (expected, result): (Vec<Ty>, Ty) = match method.name.as_str() {
1574        "update" => (vec![tfn()], Ty::Unit),
1575        other => {
1576            ctx.errors.push(
1577                CompileError::new(
1578                    "bynk.store.unknown_op",
1579                    method.span,
1580                    format!("a `Cell` store field has no operation `{other}` — expected `update`"),
1581                )
1582                .with_note(
1583                    "a cell is read by its bare name and written with `:=`; `update` is the only \
1584                     method-shaped op",
1585                ),
1586            );
1587            for a in args {
1588                type_of(a, None, ctx);
1589            }
1590            return None;
1591        }
1592    };
1593    let effect = Ty::Effect(Box::new(result));
1594    if args.len() != expected.len() {
1595        ctx.errors.push(CompileError::new(
1596            "bynk.types.call_arity",
1597            span,
1598            format!(
1599                "`Cell.{}` takes {} argument(s), found {}",
1600                method.name,
1601                expected.len(),
1602                args.len()
1603            ),
1604        ));
1605        for a in args {
1606            type_of(a, None, ctx);
1607        }
1608        return Some(effect);
1609    }
1610    for (a, exp) in args.iter().zip(expected.iter()) {
1611        if let Some(at) = type_of(a, Some(exp), ctx)
1612            && !compatible(&at, exp)
1613        {
1614            ctx.errors.push(CompileError::new(
1615                "bynk.types.argument_mismatch",
1616                a.span,
1617                format!("expected `{}`, found `{}`", exp.display(), at.display()),
1618            ));
1619        }
1620    }
1621    Some(effect)
1622}
1623
1624pub(crate) fn check_method_call(
1625    receiver: &Expr,
1626    method: &Ident,
1627    type_args: &[TypeRef],
1628    args: &[Expr],
1629    span: Span,
1630    expected: Option<&Ty>,
1631    ctx: &mut Ctx,
1632) -> Option<Ty> {
1633    // v0.22b: explicit type arguments apply only to the `Json.decode[T]`
1634    // static — every other method/static takes none (the 0039/0045 rule;
1635    // generic *user* methods remain deferred). A user-declared type named
1636    // `Json` shadows the codec module and takes no type arguments.
1637    if !type_args.is_empty()
1638        && !matches!(&receiver.kind, ExprKind::Ident(id) if id.name == JSON
1639            && !ctx.input.types.contains_key(JSON))
1640    {
1641        ctx.errors.push(CompileError::new(
1642            "bynk.generics.type_arg_mismatch",
1643            span,
1644            format!(
1645                "`{}` is not a generic method — it takes no type arguments",
1646                method.name
1647            ),
1648        ));
1649        for a in args {
1650            let _ = type_of(a, None, ctx);
1651        }
1652        return None;
1653    }
1654    // v0.25: a test body invokes the target's service as `svc.call(args)`.
1655    // The emitter wires it from the same service set; the checker types it
1656    // loosely (the runner recovers outcomes at runtime), but the binding
1657    // edge is real — record it so test-file references index.
1658    if let ExprKind::Ident(id) = &receiver.kind
1659        && method.name == "call"
1660        && ctx.lookup(id.name.as_str()).is_none()
1661        && ctx.test_services.contains(&id.name)
1662        && let Some(unit) = ctx.input.cross_context.self_context.clone()
1663    {
1664        ctx.refs
1665            .record_in_unit(id.span, SymbolKind::Service, &id.name, &unit);
1666    }
1667    // v0.6: cross-context service call. Two shapes:
1668    //   - `Alias.service(args)`           where Alias is from `consumes X as Alias`
1669    //   - `prefix.tail.service(args)`     where `prefix.tail` is a consumed context's
1670    //                                     qualified name (parsed as nested FieldAccess).
1671    // The full-qualified-name form must be checked before the bare-ident form
1672    // (the prefix's first segment doesn't resolve as anything local).
1673    if ctx.lookup_root_ident(receiver).is_none() {
1674        // v0.15: cross-context capability call — `B.Cap.op(args)` /
1675        // `Alias.Cap.op(args)`. Checked before the service-call shape because
1676        // the receiver carries an extra (capability) segment.
1677        if let Some(chain) = flatten_ident_chain(receiver)
1678            && let Some((consumed, cap)) = ctx.input.cross_context.resolve_cross_capability(&chain)
1679        {
1680            // v0.25: the capability name-segment (`Cap` in `B.Cap` /
1681            // `Alias.Cap`) is the outermost field of the receiver chain.
1682            if let ExprKind::FieldAccess { field, .. } = &receiver.kind {
1683                ctx.refs
1684                    .record_in_unit(field.span, SymbolKind::Capability, &cap, &consumed);
1685            }
1686            return check_cross_context_capability_call(
1687                receiver, &consumed, &cap, method, args, span, ctx,
1688            );
1689        }
1690        if let Some(consumed) = cross_context_prefix(receiver, ctx) {
1691            return check_cross_context_call(receiver, &consumed, method, args, span, ctx);
1692        }
1693        // Looks like a dotted prefix (no local binding for the root). If the
1694        // chain matches the shape of a consumed-context call but the prefix
1695        // isn't actually consumed, surface an explicit diagnostic so the user
1696        // can fix the missing `consumes` clause rather than seeing a silent
1697        // "no methods" error later.
1698        if let ExprKind::FieldAccess { .. } = &receiver.kind
1699            && let Some(chain) = flatten_ident_chain(receiver)
1700            && chain.contains('.')
1701        {
1702            let info = &ctx.input.cross_context;
1703            let in_context = info.self_context.is_some();
1704            if in_context && info.resolve_prefix(&chain).is_none() {
1705                ctx.errors.push(
1706                    CompileError::new(
1707                        "bynk.resolve.unconsumed_context",
1708                        receiver.span,
1709                        format!(
1710                            "`{chain}.{}` looks like a cross-context service call, but `{chain}` is not in this context's `consumes` clauses",
1711                            method.name
1712                        ),
1713                    )
1714                    .with_note(
1715                        "add a `consumes {chain}` clause at the top of the context, or use an alias and call it through the alias",
1716                    ),
1717                );
1718                for a in args {
1719                    let _ = type_of(a, None, ctx);
1720                }
1721                return None;
1722            }
1723        }
1724    }
1725    // Detect capability call (v0.5): receiver is a bare Ident naming a
1726    // capability declared in the context (in scope via `given`, or declared
1727    // but undeclared in `given` — the static-call path emits the error).
1728    if let ExprKind::Ident(id) = &receiver.kind
1729        && ctx.lookup(id.name.as_str()).is_none()
1730        && (ctx.caps.capabilities.contains_key(&id.name)
1731            || ctx.caps.declared_capabilities.contains_key(&id.name))
1732    {
1733        return check_static_call(id, method, args, span, ctx);
1734    }
1735    // Detect static-call shape: receiver is a bare Ident naming a declared
1736    // type (not a local/param). Dispatch to check_static_call.
1737    if let ExprKind::Ident(id) = &receiver.kind
1738        && ctx.lookup(id.name.as_str()).is_none()
1739        && ctx.input.types.contains_key(&id.name)
1740    {
1741        return check_static_call(id, method, args, span, ctx);
1742    }
1743    // v0.20b: qualified statics on the built-in collection types —
1744    // `List.empty()` / `Map.empty()`. Like an empty `[]`, they need an
1745    // expected type to pin their element/key/value types.
1746    if let ExprKind::Ident(id) = &receiver.kind
1747        && ctx.lookup(id.name.as_str()).is_none()
1748        && !ctx.input.types.contains_key(&id.name)
1749        && (id.name == LIST || id.name == MAP)
1750    {
1751        return check_collection_static(id, method, args, span, expected, ctx);
1752    }
1753    // v0.22a: the numeric parse statics — `Int.parse(s)` / `Float.parse(s)`.
1754    // The parser only admits these keywords in receiver position when
1755    // followed by `.`, so the Ident shape here is exactly the static form.
1756    if let ExprKind::Ident(id) = &receiver.kind
1757        && (id.name == INT || id.name == FLOAT)
1758    {
1759        return check_numeric_parse_static(id, method, args, span, ctx);
1760    }
1761    // v0.86 (ADR 0112): the `Duration.millis(n)` static constructor — the way to
1762    // build a `Duration` from a runtime `Int` (the literal covers constants).
1763    if let ExprKind::Ident(id) = &receiver.kind
1764        && id.name == DURATION
1765        && ctx.lookup(DURATION).is_none()
1766        && !ctx.input.types.contains_key(DURATION)
1767    {
1768        return check_duration_static(method, args, span, ctx);
1769    }
1770    // v0.90 (ADR 0114 D6): the `Instant.fromEpochMillis(n)` static constructor.
1771    if let ExprKind::Ident(id) = &receiver.kind
1772        && id.name == INSTANT
1773        && ctx.lookup(INSTANT).is_none()
1774        && !ctx.input.types.contains_key(INSTANT)
1775    {
1776        return check_instant_static(method, args, span, ctx);
1777    }
1778    // v0.110 (ADR 0142 D2): the `Bytes` static constructors —
1779    // `Bytes.fromUtf8(s)` / `Bytes.fromBase64(s)` / `Bytes.empty()`.
1780    if let ExprKind::Ident(id) = &receiver.kind
1781        && id.name == BYTES
1782        && ctx.lookup(BYTES).is_none()
1783        && !ctx.input.types.contains_key(BYTES)
1784    {
1785        return check_bytes_static(method, args, span, ctx);
1786    }
1787    // v0.22b: the typed JSON codec statics (ADR 0045).
1788    if let ExprKind::Ident(id) = &receiver.kind
1789        && id.name == JSON
1790        && ctx.lookup(JSON).is_none()
1791        && !ctx.input.types.contains_key(JSON)
1792    {
1793        return check_json_static(method, type_args, args, span, expected, ctx);
1794    }
1795    // v0.100: the `Stream.of(xs)` static constructor (real-time track slice 0).
1796    if let ExprKind::Ident(id) = &receiver.kind
1797        && id.name == STREAM
1798        && ctx.lookup(STREAM).is_none()
1799        && !ctx.input.types.contains_key(STREAM)
1800    {
1801        return check_stream_static(method, args, span, ctx);
1802    }
1803    // v0.20b: `insert`/`prepend` return their receiver's collection type —
1804    // propagate an expected collection type down the chain so
1805    // `let m: Map[String, Int] = Map.empty().insert("a", 1)` infers.
1806    let recv_expected = match (expected, method.name.as_str()) {
1807        (Some(t), "insert") => peel_to_map(t).map(|(k, v)| Ty::Map(Box::new(k), Box::new(v))),
1808        (Some(t), "prepend") => peel_to_list(t).map(|e| Ty::List(Box::new(e))),
1809        _ => None,
1810    };
1811    let recv_ty = type_of(receiver, recv_expected.as_ref(), ctx)?;
1812    // v0.20b: built-in kernel methods on the collection types. These are
1813    // compiler-known special forms typed directly here — generic in their
1814    // accumulator without the (deferred) declared-generic-methods feature;
1815    // the deferral bites only on declared methods (ADR 0037).
1816    match recv_ty.clone() {
1817        Ty::List(elem) => {
1818            return check_list_kernel_method(method, args, &elem, span, ctx);
1819        }
1820        // v0.91 (ADR 0115): a chained builder/terminal on a lazy `Query[T]`.
1821        Ty::Query(elem) => {
1822            return check_query_kernel_method(method, args, &elem, span, ctx);
1823        }
1824        // v0.100: a chained builder/terminal on a `Stream[T]`.
1825        Ty::Stream(elem) => {
1826            return check_stream_kernel_method(method, args, &elem, span, ctx);
1827        }
1828        // v0.102: the held-resource operations on a `Connection[F]` — `send(f)`
1829        // (non-consuming) and `close()` (consuming). The linearity pass tracks
1830        // the ownership transitions; this types the operations.
1831        Ty::Connection(frame) => {
1832            return check_connection_method(method, args, &frame, span, ctx);
1833        }
1834        Ty::Map(key, val) => {
1835            return check_map_kernel_method(method, args, &key, &val, span, ctx);
1836        }
1837        // v0.21: the numeric kernel — conversions as value methods on the
1838        // bare base types (a refined value reaches them via `.raw`).
1839        Ty::Base(base @ (BaseType::Int | BaseType::Float)) => {
1840            return check_numeric_kernel_method(method, args, base, span, ctx);
1841        }
1842        // v0.86 (ADR 0112): the `Duration` kernel — `toMillis`/`toString`.
1843        Ty::Base(BaseType::Duration) => {
1844            return check_duration_kernel_method(method, args, span, ctx);
1845        }
1846        // v0.90 (ADR 0114): the `Instant` kernel — `toEpochMillis`/`toString`.
1847        Ty::Base(BaseType::Instant) => {
1848            return check_instant_kernel_method(method, args, span, ctx);
1849        }
1850        // v0.110 (ADR 0142): the `Bytes` kernel — `length`/`toBase64`/`decodeUtf8`.
1851        Ty::Base(BaseType::Bytes) => {
1852            return check_bytes_kernel_method(method, args, span, ctx);
1853        }
1854        // v0.22a: the string kernel (ADR 0046).
1855        Ty::Base(BaseType::String) => {
1856            return check_string_kernel_method(method, args, span, ctx);
1857        }
1858        // v0.22a: the Option/Result combinators as kernel methods (ADR 0048).
1859        Ty::Option(inner) => {
1860            return check_option_kernel_method(method, args, &inner, span, ctx);
1861        }
1862        Ty::Result(ok, err) => {
1863            return check_result_kernel_method(method, args, &ok, &err, span, ctx);
1864        }
1865        _ => {}
1866    }
1867    // Find a named type for the receiver, then look up its instance methods.
1868    let type_name = match &recv_ty {
1869        Ty::Named { name, .. } => name.clone(),
1870        _ => {
1871            ctx.errors.push(CompileError::new(
1872                "bynk.types.method_on_non_named_type",
1873                method.span,
1874                format!(
1875                    "type `{}` has no methods — only user-declared types support method calls",
1876                    recv_ty.display()
1877                ),
1878            ));
1879            return None;
1880        }
1881    };
1882    // Agent handler dispatch: when the receiver is an agent instance, look
1883    // up the method against the agent's declared `on call` handlers and
1884    // resolve to the handler's return type.
1885    if let Some(agent) = ctx.input.agents.get(&type_name).cloned() {
1886        let Some(handler) = agent.handlers.iter().find(|h| {
1887            h.method_name
1888                .as_ref()
1889                .is_some_and(|n| n.name == method.name)
1890        }) else {
1891            ctx.errors.push(CompileError::new(
1892                "bynk.agent.handler_not_found",
1893                method.span,
1894                format!(
1895                    "agent `{}` has no handler named `{}`",
1896                    type_name, method.name
1897                ),
1898            ));
1899            for a in args {
1900                let _ = type_of(a, None, ctx);
1901            }
1902            return None;
1903        };
1904        if handler.params.len() != args.len() {
1905            ctx.errors.push(CompileError::new(
1906                "bynk.agent.handler_arity",
1907                method.span,
1908                format!(
1909                    "agent handler `{}.{}` expects {} argument(s), but {} were given",
1910                    type_name,
1911                    method.name,
1912                    handler.params.len(),
1913                    args.len()
1914                ),
1915            ));
1916            for a in args {
1917                let _ = type_of(a, None, ctx);
1918            }
1919            return None;
1920        }
1921        for (p, arg) in handler.params.iter().zip(args.iter()) {
1922            let pty = resolve_type_ref(&p.type_ref, &ctx.input.types);
1923            let _ = type_of(arg, pty.as_ref(), ctx);
1924        }
1925        return resolve_type_ref(&handler.return_type, &ctx.input.types);
1926    }
1927    let table = ctx
1928        .input
1929        .methods
1930        .get(&type_name)
1931        .cloned()
1932        .unwrap_or_default();
1933    let Some(method_decl) = table.instance.get(&method.name).cloned() else {
1934        ctx.errors.push(CompileError::new(
1935            "bynk.types.method_not_found",
1936            method.span,
1937            format!(
1938                "type `{}` has no instance method named `{}`",
1939                type_name, method.name
1940            ),
1941        ));
1942        return None;
1943    };
1944    // v0.36 (ADR 0069): the method is a first-class index symbol, keyed by the
1945    // compound `"Type.method"` name. Recorded already-spelled from the resolved
1946    // receiver type; the bare edge resolves through the same `uses`/`consumes`
1947    // qualification as cross-file type references.
1948    ctx.refs.record(
1949        method.span,
1950        SymbolKind::Method,
1951        &format!("{type_name}.{}", method.name),
1952    );
1953    // Param count excludes the implicit `self`.
1954    if method_decl.params.len() != args.len() {
1955        ctx.errors.push(
1956            CompileError::new(
1957                "bynk.types.method_arity",
1958                method.span,
1959                format!(
1960                    "method `{}.{}` expects {} argument(s), but {} were given",
1961                    type_name,
1962                    method.name,
1963                    method_decl.params.len(),
1964                    args.len()
1965                ),
1966            )
1967            .with_label(method_decl.name.ident().span, "method declared here"),
1968        );
1969        for a in args {
1970            let _ = type_of(a, None, ctx);
1971        }
1972        return None;
1973    }
1974    let mut ok = true;
1975    for (i, (param, arg)) in method_decl.params.iter().zip(args.iter()).enumerate() {
1976        record_param_hint(ctx.hints, &param.name.name, arg);
1977        let expected = resolve_type_ref(&param.type_ref, &ctx.input.types);
1978        let actual = type_of(arg, expected.as_ref(), ctx);
1979        let (Some(actual), Some(expected)) = (actual, expected) else {
1980            ok = false;
1981            continue;
1982        };
1983        if !compatible(&actual, &expected) {
1984            ctx.errors.push(CompileError::new(
1985                "bynk.types.argument_mismatch",
1986                arg.span,
1987                format!(
1988                    "argument {} to `{}.{}` has type `{}`, but parameter `{}` expects `{}`",
1989                    i + 1,
1990                    type_name,
1991                    method.name,
1992                    actual.display(),
1993                    param.name.name,
1994                    expected.display()
1995                ),
1996            ));
1997            ok = false;
1998        }
1999    }
2000    let _ = span;
2001    if !ok {
2002        return None;
2003    }
2004    resolve_type_ref(&method_decl.return_type, &ctx.input.types)
2005}
2006
2007/// If `receiver` resolves to a consumed-context prefix (an alias or a
2008/// dotted qualified name appearing in `consumes`), return the consumed
2009/// context's qualified name. Otherwise None. Local bindings, types, and
2010/// capabilities take precedence — those are checked at the call site.
2011fn cross_context_prefix(receiver: &Expr, ctx: &Ctx) -> Option<String> {
2012    let info = &ctx.input.cross_context;
2013    if info.consumed_contexts.is_empty() && info.aliases.is_empty() {
2014        return None;
2015    }
2016    // Walk the receiver to assemble a candidate dotted name. Supports:
2017    //   Ident(X)                                 -> "X"
2018    //   FieldAccess { Ident(A), B }              -> "A.B"
2019    //   FieldAccess { FieldAccess { Ident(A), B }, C } -> "A.B.C"
2020    let candidate = flatten_ident_chain(receiver)?;
2021    let head = candidate.split('.').next().unwrap_or("");
2022    // The head must not shadow a local binding / capability / declared type.
2023    if ctx.lookup(head).is_some() {
2024        return None;
2025    }
2026    if ctx.caps.capabilities.contains_key(head) || ctx.caps.declared_capabilities.contains_key(head)
2027    {
2028        return None;
2029    }
2030    // If the head is a known local type, only an alias whose name happens to
2031    // collide could redirect this; aliases conflicting with types are an
2032    // error in project.rs, so a clash here is impossible at this point.
2033    info.resolve_prefix(candidate.as_str())
2034}
2035
2036/// Flatten an `Ident`/`FieldAccess` chain into its dotted name, or None if
2037/// any segment isn't a bare identifier.
2038fn flatten_ident_chain(expr: &Expr) -> Option<String> {
2039    match &expr.kind {
2040        ExprKind::Ident(id) => Some(id.name.clone()),
2041        ExprKind::FieldAccess { receiver, field } => {
2042            let head = flatten_ident_chain(receiver)?;
2043            Some(format!("{head}.{}", field.name))
2044        }
2045        _ => None,
2046    }
2047}
2048
2049/// Type-check a cross-context service call (v0.6 §4.2). `receiver` carries
2050/// the prefix's source span for diagnostics. `consumed` is the resolved
2051/// qualified name of the consumed context.
2052/// v0.15: type-check a cross-context capability call `B.Cap.op(args)` /
2053/// `Alias.Cap.op(args)`. The capability operation signatures are carried in
2054/// `consumed_capabilities` (in the providing context's namespace); the
2055/// capability must be listed in the handler/provider's `given` clause.
2056fn check_cross_context_capability_call(
2057    receiver: &Expr,
2058    consumed: &str,
2059    cap: &str,
2060    method: &Ident,
2061    args: &[Expr],
2062    _span: Span,
2063    ctx: &mut Ctx,
2064) -> Option<Ty> {
2065    // Capability calls require an effectful body (same rule as local ones).
2066    if !ctx.effectful {
2067        ctx.errors.push(CompileError::new(
2068            "bynk.effect.capability_in_pure_context",
2069            method.span,
2070            format!(
2071                "capability `{consumed}.{cap}` can only be called inside an effectful body (one returning `Effect[T]`)"
2072            ),
2073        ));
2074    }
2075    // The capability must be declared in this handler/provider's `given`.
2076    // The local deps key is the capability's simple name.
2077    if !ctx.caps.given_remaining.contains(cap) {
2078        let mut err = CompileError::new(
2079            "bynk.given.undeclared_capability",
2080            receiver.span,
2081            format!("capability `{consumed}.{cap}` is used but not listed in the `given` clause"),
2082        )
2083        .with_note(format!(
2084            "add `{consumed}.{cap}` to the handler's `given` clause so the dependency surface is visible at the declaration site"
2085        ));
2086        // v0.26 (ADR 0054): the one-click counterpart of the note — the
2087        // clause entry is the qualified form the user writes (`B.Cap`).
2088        if let Some((span, insert)) = given_insertion_edit(
2089            &ctx.caps.given_entries,
2090            ctx.caps.given_anchor,
2091            &format!("{consumed}.{cap}"),
2092        ) {
2093            err = err.with_suggestion(
2094                format!("add `{consumed}.{cap}` to the `given` clause"),
2095                vec![(span, insert)],
2096                Applicability::MachineApplicable,
2097            );
2098        }
2099        ctx.errors.push(err);
2100        for a in args {
2101            let _ = type_of(a, None, ctx);
2102        }
2103        return None;
2104    }
2105    ctx.caps.given_used.insert(cap.to_string());
2106
2107    let info = &ctx.input.cross_context;
2108    let op = info
2109        .consumed_capabilities
2110        .get(consumed)
2111        .and_then(|caps| caps.get(cap))
2112        .and_then(|c| c.ops.iter().find(|o| o.name == method.name))
2113        .cloned();
2114    let Some(op) = op else {
2115        ctx.errors.push(CompileError::new(
2116            "bynk.capability.unknown_operation",
2117            method.span,
2118            format!(
2119                "capability `{consumed}.{cap}` has no operation named `{}`",
2120                method.name
2121            ),
2122        ));
2123        for a in args {
2124            let _ = type_of(a, None, ctx);
2125        }
2126        return None;
2127    };
2128    // v0.36 (ADR 0069, slice 2): a cross-context op call references the op,
2129    // recorded already-qualified into the providing unit (where the op is
2130    // declared), mirroring the cross-context capability reference.
2131    ctx.refs.record_in_unit(
2132        method.span,
2133        SymbolKind::CapabilityOp,
2134        &format!("{cap}.{}", method.name),
2135        consumed,
2136    );
2137    if op.params.len() != args.len() {
2138        ctx.errors.push(CompileError::new(
2139            "bynk.capability.op_arity",
2140            method.span,
2141            format!(
2142                "capability operation `{consumed}.{cap}.{}` expects {} argument(s), but {} were given",
2143                method.name,
2144                op.params.len(),
2145                args.len()
2146            ),
2147        ));
2148        for a in args {
2149            let _ = type_of(a, None, ctx);
2150        }
2151        return None;
2152    }
2153
2154    // Resolve parameter / return types in the consumed context's namespace.
2155    let consumed_types = info
2156        .consumed_types
2157        .get(consumed)
2158        .cloned()
2159        .unwrap_or_default();
2160    let mut all_ok = true;
2161    for (i, ((pname, ptype_ref), arg)) in op.params.iter().zip(args.iter()).enumerate() {
2162        record_param_hint(ctx.hints, pname, arg);
2163        let param_ty = resolve_type_ref(ptype_ref, &consumed_types).unwrap_or(Ty::Unit);
2164        let Some(arg_ty) = type_of(arg, None, ctx) else {
2165            all_ok = false;
2166            continue;
2167        };
2168        if !structurally_compatible(&arg_ty, &param_ty, &ctx.input.types, &consumed_types) {
2169            ctx.errors.push(CompileError::new(
2170                "bynk.boundary.structural_mismatch",
2171                arg.span,
2172                format!(
2173                    "cross-context argument {} to `{consumed}.{cap}.{}` has type `{}`, but parameter `{pname}` expects `{}`",
2174                    i + 1,
2175                    method.name,
2176                    arg_ty.display(),
2177                    param_ty.display(),
2178                ),
2179            ));
2180            all_ok = false;
2181        }
2182    }
2183    if !all_ok {
2184        return None;
2185    }
2186    let raw_ret = resolve_type_ref(&op.return_type, &consumed_types).unwrap_or(Ty::Unit);
2187    Some(rebrand_return_type(&raw_ret, &ctx.input.types))
2188}
2189
2190fn check_cross_context_call(
2191    receiver: &Expr,
2192    consumed: &str,
2193    method: &Ident,
2194    args: &[Expr],
2195    _span: Span,
2196    ctx: &mut Ctx,
2197) -> Option<Ty> {
2198    // The consuming context must be effectful at this call site (services
2199    // and agent handlers are; pure free fns are not).
2200    if !ctx.effectful {
2201        ctx.errors.push(
2202            CompileError::new(
2203                "bynk.effect.cross_context_in_pure_context",
2204                method.span,
2205                format!(
2206                    "cross-context service call `{}.{}` can only be made inside an effectful body (one returning `Effect[T]`)",
2207                    consumed, method.name
2208                ),
2209            )
2210            .with_label(receiver.span, "consumed context prefix"),
2211        );
2212    }
2213    let info = &ctx.input.cross_context;
2214    let Some(svcs) = info.consumed_services.get(consumed) else {
2215        ctx.errors.push(
2216            CompileError::new(
2217                "bynk.consumes.unknown_context",
2218                receiver.span,
2219                format!("context `{consumed}` is not in scope here"),
2220            )
2221            .with_note(
2222                "add a `consumes` clause for the target context at the top of the consuming context",
2223            ),
2224        );
2225        for a in args {
2226            let _ = type_of(a, None, ctx);
2227        }
2228        return None;
2229    };
2230    let Some(service) = svcs.get(&method.name).cloned() else {
2231        ctx.errors.push(
2232            CompileError::new(
2233                "bynk.consumes.unknown_service",
2234                method.span,
2235                format!(
2236                    "context `{consumed}` has no service named `{}`",
2237                    method.name
2238                ),
2239            )
2240            .with_note(
2241                "cross-context calls require an `on call` service handler in the consumed context",
2242            ),
2243        );
2244        for a in args {
2245            let _ = type_of(a, None, ctx);
2246        }
2247        return None;
2248    };
2249    ctx.refs
2250        .record_in_unit(method.span, SymbolKind::Service, &method.name, consumed);
2251
2252    if service.params.len() != args.len() {
2253        ctx.errors.push(
2254            CompileError::new(
2255                "bynk.consumes.service_arity",
2256                method.span,
2257                format!(
2258                    "cross-context service `{consumed}.{}` expects {} argument(s), but {} were given",
2259                    method.name,
2260                    service.params.len(),
2261                    args.len()
2262                ),
2263            )
2264            .with_label(service.span, "service declared here"),
2265        );
2266        for a in args {
2267            let _ = type_of(a, None, ctx);
2268        }
2269        return None;
2270    }
2271
2272    // Resolve the consumed-context types so we can describe parameter shapes.
2273    let consumed_types = info
2274        .consumed_types
2275        .get(consumed)
2276        .cloned()
2277        .unwrap_or_default();
2278
2279    // Walk each argument, checking structural compatibility (Phase 4).
2280    let mut all_ok = true;
2281    for (i, ((pname, ptype_ref), arg)) in service.params.iter().zip(args.iter()).enumerate() {
2282        record_param_hint(ctx.hints, pname, arg);
2283        let param_ty = resolve_type_ref(ptype_ref, &consumed_types).unwrap_or(Ty::Unit);
2284        // Type-check the argument in the caller's context.
2285        let arg_ty = type_of(arg, None, ctx);
2286        let Some(arg_ty) = arg_ty else {
2287            all_ok = false;
2288            continue;
2289        };
2290        if !structurally_compatible(&arg_ty, &param_ty, &ctx.input.types, &consumed_types) {
2291            ctx.errors.push(
2292                CompileError::new(
2293                    "bynk.boundary.structural_mismatch",
2294                    arg.span,
2295                    format!(
2296                        "cross-context argument {} to `{consumed}.{}` has type `{}` in `{}`, but parameter `{pname}` expects `{}` in `{}`",
2297                        i + 1,
2298                        method.name,
2299                        arg_ty.display(),
2300                        ctx.input
2301                            .cross_context
2302                            .self_context
2303                            .as_deref()
2304                            .unwrap_or("?"),
2305                        param_ty.display(),
2306                        consumed,
2307                    ),
2308                )
2309                .with_label(service.span, "service declared here")
2310                .with_note(
2311                    "values crossing a context boundary must have structurally compatible types (same commons-derived type, or identical record/sum shape)",
2312                ),
2313            );
2314            all_ok = false;
2315        }
2316    }
2317    if !all_ok {
2318        return None;
2319    }
2320
2321    // Return type rebrand: project the consumed context's return type into
2322    // the calling context's namespace by renaming named types whose unqualified
2323    // name appears in the caller's type table (v0.6 §4.5).
2324    let raw_ret = resolve_type_ref(&service.return_type, &consumed_types).unwrap_or(Ty::Unit);
2325    let rebranded = rebrand_return_type(&raw_ret, &ctx.input.types);
2326    Some(rebranded)
2327}