Skip to main content

bynk_check/
resolver.rs

1//! Name resolution (spec §5.1, v0.1 §4.1, v0.2 §4.1).
2//!
3//! Builds symbol tables for the commons and validates that:
4//! - No two top-level items share a name (types, fns, methods are all named).
5//! - Every `TypeRef::Named` resolves to a declared type.
6//! - Every free function call resolves to a function declaration.
7//! - Every identifier in expression position resolves to a parameter, a
8//!   `let` binding, or `self` (inside a method).
9//! - Constructor / static calls (`TypeName.method(args)`) resolve either to
10//!   the built-in `T.of` of a refined type, a static method on `T`, or a
11//!   variant constructor when `T` is a sum type.
12//! - Record construction targets a declared record type and uses only
13//!   declared fields.
14//! - Method calls resolve via the receiver's nominal type (the actual type
15//!   check happens in the type checker).
16//!
17//! On success returns a [`ResolvedCommons`] — the original AST plus
18//! symbol tables the type checker consumes.
19
20use std::collections::{HashMap, HashSet};
21use std::sync::Arc;
22
23use crate::index::{RefSink, SymbolKind};
24use bynk_project::UnitKind;
25use bynk_syntax::ast::*;
26use bynk_syntax::error::{Applicability, CompileError};
27use bynk_syntax::span::Span;
28
29/// Is `name` a type imported via `uses` of a *commons* specifically — the
30/// exact predicate [`ResolvedCommons::is_uses_commons_type`] caches as
31/// `uses_commons_type_names`, and `bynk-emit` needs at *two* real call
32/// sites that must never disagree: `emit_context_rebrands`'s own two steps,
33/// "alias the import" and "rebrand the type" (its own doc comment, step 1
34/// "Done in imports", step 2 the rebrand itself, both in
35/// `bynk-emit/src/emitter.rs`) — an import narrower than the rebrand leaves
36/// an undefined name in the generated module; a rebrand narrower than the
37/// import leaves an alias imported and never used.
38///
39/// R4.10/R8.2 (`design/bynk-greenfield-compiler.md`): before this function
40/// existed, `prepare_unit_check_ctx` (`check_pipeline.rs`) and *both*
41/// `bynk-emit` call sites above each independently inlined this same
42/// two-condition check, linked only by a doc comment promising they all
43/// matched exactly — a real risk ADR 0226 names (#655: "a single named
44/// binder took the entire test run down, pointing at generated code the
45/// author never wrote"). One definition, every caller reads it — an edit to
46/// either condition can no longer silently update only one side.
47pub fn compute_is_uses_commons_type(
48    imported_from_kind: &HashMap<String, UnitKind>,
49    types: &HashMap<String, Arc<TypeDecl>>,
50    name: &str,
51) -> bool {
52    matches!(imported_from_kind.get(name), Some(UnitKind::Commons)) && types.contains_key(name)
53}
54
55#[cfg(test)]
56mod compute_is_uses_commons_type_tests {
57    use super::compute_is_uses_commons_type;
58    use bynk_project::UnitKind;
59    use bynk_syntax::ast::{Ident, RecordBody, Trivia, TypeBody, TypeDecl};
60    use bynk_syntax::span::Span;
61    use std::collections::HashMap;
62    use std::sync::Arc;
63
64    fn bare_record_type(name: &str) -> Arc<TypeDecl> {
65        Arc::new(TypeDecl {
66            type_params: Vec::new(),
67            name: Ident {
68                name: name.to_string(),
69                span: Span::default(),
70            },
71            body: TypeBody::Record(RecordBody {
72                fields: Vec::new(),
73                span: Span::default(),
74            }),
75            documentation: None,
76            span: Span::default(),
77            trivia: Trivia::default(),
78        })
79    }
80
81    /// The four combinations the real callers' own doc comments describe:
82    /// a `uses`-imported commons type (the only `true` case), a
83    /// `uses`-imported commons *function* (v0.20b's own carve-out — not
84    /// rebranded, a value not a type), a name imported from a non-commons
85    /// unit kind (a consumed context, say), and a name absent from
86    /// `imported_from_kind` entirely (a local declaration).
87    #[test]
88    fn matches_a_commons_imported_type_only() {
89        let mut kinds = HashMap::new();
90        kinds.insert("Money".to_string(), UnitKind::Commons);
91        kinds.insert("traverse".to_string(), UnitKind::Commons);
92        kinds.insert("Order".to_string(), UnitKind::Context);
93        let mut types = HashMap::new();
94        types.insert("Money".to_string(), bare_record_type("Money"));
95        types.insert("Order".to_string(), bare_record_type("Order"));
96
97        assert!(compute_is_uses_commons_type(&kinds, &types, "Money"));
98        assert!(
99            !compute_is_uses_commons_type(&kinds, &types, "traverse"),
100            "a uses-imported commons function is a value, not a type — v0.20b"
101        );
102        assert!(
103            !compute_is_uses_commons_type(&kinds, &types, "Order"),
104            "imported from a context, not a commons"
105        );
106        assert!(
107            !compute_is_uses_commons_type(&kinds, &types, "Local"),
108            "absent from imported_from_kind entirely — a local declaration"
109        );
110    }
111}
112
113/// The resolver's two collection points, bundled so the reference walk
114/// threads one parameter (v0.25, ADR 0053). `push` forwards to the error
115/// list, keeping the walk's error sites unchanged; binding edges record
116/// via `refs` at the site that resolved them.
117pub(crate) struct Sinks<'a> {
118    errs: &'a mut Vec<CompileError>,
119    pub(crate) refs: &'a mut RefSink,
120}
121
122impl Sinks<'_> {
123    fn push(&mut self, e: CompileError) {
124        self.errs.push(e);
125    }
126}
127
128/// Per-type method table built during resolution: keyed by method name,
129/// values are clones of the [`FnDecl`] for that method.
130#[derive(Debug, Default, Clone)]
131pub struct MethodTable {
132    pub instance: HashMap<String, Arc<FnDecl>>,
133    pub statics: HashMap<String, Arc<FnDecl>>,
134}
135
136/// Output of resolution: the AST plus the symbol tables the checker needs.
137pub struct ResolvedCommons {
138    pub commons: Commons,
139    /// Finding #10/#51: `Arc`-wrapped (not owned) so cloning this map — done
140    /// once per synthetic per-handler `ResolvedCommons` during emission — is
141    /// a pointer bump, not a deep copy of every declaration body in the unit.
142    pub types: HashMap<String, Arc<TypeDecl>>,
143    /// Finding #10/#51: `Arc`-wrapped for the same reason as `types`.
144    pub fns: HashMap<String, Arc<FnDecl>>,
145    /// Per-type method tables (instance + static).
146    pub methods: HashMap<String, MethodTable>,
147    /// Names of types declared in *this* commons (as opposed to imported via
148    /// `uses`). Used by the checker to gate access to `.raw` and `.unsafe()`
149    /// on opaque types. Private: this field's contract is specifically
150    /// "declared here, not merely visible here", and a builder outside this
151    /// crate that populates it from the wrong (merged, rather than
152    /// pre-merge) table silently over-widens those gates — read it via
153    /// [`ResolvedCommons::is_local_type`], and build a `ResolvedCommons` via
154    /// [`ResolvedCommons::new`], which derives it correctly by construction.
155    pub(crate) local_type_names: std::collections::HashSet<String>,
156    /// Cross-context call information for v0.6. None for commons and for
157    /// single-file mode. For contexts, supplies the set of consumed contexts
158    /// and any aliases introduced via `consumes ... as Alias`.
159    pub cross_context: CrossContextInfo,
160    /// Agents declared in this context. Used to recognise the `Agent(key)`
161    /// construction shape and the `agent_instance.handler(args)` method-call
162    /// shape in handler bodies that mention other agents.
163    pub agents: HashMap<String, AgentDecl>,
164    /// v0.91 (ADR 0116 D6): for each imported function name, the qualified unit
165    /// it came from (`map` → `bynk.list`). Lets the checker flag deprecated
166    /// first-party free functions at their call sites. Empty in single-file
167    /// mode and in synthetic handler-validation resolveds.
168    pub imported_from: HashMap<String, String>,
169    /// True iff this unit is a `context` (as opposed to a commons, adapter, or
170    /// test/integration scaffold). `bynk-check` has no dependency on
171    /// `bynk-emit`'s `UnitKind`, so callers set this directly from their own
172    /// unit-kind knowledge. Used to gate the context-rebrand construction
173    /// check (#907): only a context's emission rebrands a `uses`-sourced
174    /// commons sum type's variant constructors out of value scope.
175    pub is_context: bool,
176    /// Names of types brought into scope via `uses` of a *commons*
177    /// specifically (as opposed to a local declaration, or a type surfaced
178    /// via `consumes`) — every name [`compute_is_uses_commons_type`] accepts
179    /// against this unit's own `imported_from_kind`/`combined_types`, the
180    /// single shared definition both `bynk-emit`'s `emit_context_rebrands`
181    /// (rebrand + its own import-aliasing step) and this crate's own
182    /// `prepare_unit_check_ctx` (which populates this set) read (R4.10/R8.2,
183    /// closing what used to be two independently hand-maintained copies
184    /// linked only by a doc comment promising they matched). A type surfaced
185    /// via `consumes` (a capability signature from an adapter or another
186    /// context) is *not* rebranded and must not be gated by #907's check —
187    /// only this narrower set may be. Private for the same reason as
188    /// `local_type_names`; read via [`ResolvedCommons::is_uses_commons_type`].
189    pub(crate) uses_commons_type_names: std::collections::HashSet<String>,
190    /// Events track, slice 0 (spine #936): names of `event` declarations in
191    /// *this* commons specifically — as opposed to `local_type_names`, which
192    /// answers "declared here" for any type, event-derived or not. Backs the
193    /// `Events.emit[E]` check that `E` names a real event, not merely any
194    /// local type (owner-only emission alone can't tell the two apart, since
195    /// an event's synthetic `TypeDecl` sits in the same `types` table as
196    /// every ordinary type). Private for the same reason as
197    /// `local_type_names`; read via [`ResolvedCommons::is_local_event`].
198    pub(crate) event_type_names: std::collections::HashSet<String>,
199}
200
201/// Static information about the consuming context: the set of contexts it
202/// `consumes`, and any aliases introduced via `as Alias` clauses. Used by
203/// the resolver to recognise cross-context service calls and by the checker
204/// to type them (v0.6 §4.2).
205#[derive(Debug, Default, Clone)]
206pub struct CrossContextInfo {
207    /// The qualified name of the consuming context, if this unit is a context.
208    pub self_context: Option<String>,
209    /// Qualified names of every consumed context.
210    pub consumed_contexts: Vec<String>,
211    /// alias → consumed-context qualified name.
212    pub aliases: HashMap<String, String>,
213    /// For each consumed context, its service surface plus the structural
214    /// shapes of each service handler's params and return type (as seen
215    /// from the consumed context's own namespace). Populated by the project
216    /// driver; empty in single-file mode.
217    pub consumed_services: HashMap<String, HashMap<String, CrossContextService>>,
218    /// For each consumed context, its full type table (the consumed
219    /// context's local types, plus the types it brings in via `uses`).
220    /// Used by the checker for structural shape comparisons across the
221    /// boundary (v0.6 §4.3).
222    pub consumed_types: HashMap<String, HashMap<String, Arc<TypeDecl>>>,
223    /// v0.15: for each consumed context, the capabilities it `exports
224    /// capability { … }` — keyed by capability name. Used to resolve and
225    /// type-check `given B.Cap` references and `B.Cap.op(…)` calls, and by
226    /// the emitter to instantiate the provider locally.
227    pub consumed_capabilities: HashMap<String, HashMap<String, CrossContextCapability>>,
228    /// v0.17: `consumes U { Cap, … }` flattens selected capabilities into the
229    /// consumer's local namespace under their bare names (§3.3). Maps each bare
230    /// capability name to the consumed unit (context or adapter) providing it,
231    /// so bare `given Cap` / `Cap.op(…)` resolve, the deps type imports from the
232    /// right module, and compose instantiates the provider.
233    pub flattened_caps: HashMap<String, String>,
234    /// Events track, slice 0 (spine #936): for each consumed context, the
235    /// names of its own `event` declarations. Lets a subscriber's `from
236    /// Events(E)` header be checked against a foreign owner too — `E` is
237    /// legitimate if it's a local event *or* a declared event of some
238    /// consumed context, mirroring how `discover_event_subscribers`
239    /// (`bynk-emit/src/project.rs`) already resolves ownership for wiring.
240    pub consumed_event_names: HashMap<String, HashSet<String>>,
241}
242
243/// Snapshot of one exported capability in a consumed context, as needed for
244/// v0.15 cross-context capability resolution. Operation signatures are
245/// expressed in the consumed context's own namespace (resolved against
246/// `consumed_types` at the call site, mirroring [`CrossContextService`]).
247#[derive(Debug, Clone)]
248pub struct CrossContextCapability {
249    pub name: String,
250    /// Each operation's parameter type-refs and return type-ref.
251    pub ops: Vec<CrossContextCapabilityOp>,
252    /// The provider that implements this capability in the providing context
253    /// (its generated class name), so the consumer can instantiate it.
254    pub provider_name: String,
255    /// The provider's own `given` capabilities (intra-providing-context),
256    /// needed to wire the provider's constructor when instantiated locally.
257    pub provider_given: Vec<String>,
258    pub span: bynk_syntax::span::Span,
259}
260
261#[derive(Debug, Clone)]
262pub struct CrossContextCapabilityOp {
263    pub name: String,
264    /// #926: the op's own type parameters (empty for a non-generic op),
265    /// spelled the same as the consumed context's own declaration. A cross-
266    /// context call resolves these from an explicit call-site type argument,
267    /// same as the local-capability path.
268    pub type_params: Vec<String>,
269    pub params: Vec<(String, TypeRef)>,
270    pub return_type: TypeRef,
271}
272
273/// Snapshot of one service in a consumed context, as needed for v0.6
274/// cross-context type checking. The params and return type are expressed
275/// in the consumed context's own namespace.
276#[derive(Debug, Clone)]
277pub struct CrossContextService {
278    pub name: String,
279    /// Surface (parsed) type-refs of the `on call` handler's parameters.
280    pub params: Vec<(String, TypeRef)>,
281    pub return_type: TypeRef,
282    pub span: bynk_syntax::span::Span,
283}
284
285/// Project one local `on call` handler into the [`CrossContextService`] shape
286/// both sides of a cross-context contract check need — a caller resolving a
287/// *consumed* service ([`crate::symbols::build_cross_context_info`]) and a
288/// callee stamping its *own* `X-Bynk-Contract` constant
289/// ([`crate::contract::own_contract_hashes`]). Sharing this one projection is
290/// the whole correctness argument for that symmetry: if the two sides ever
291/// diverged, a working deployment would 409 on every call instead of only on
292/// real skew. `None` when `sdecl` has no `on call` handler (e.g. an
293/// events-only or queue-only service).
294pub fn cross_context_service_for(name: &str, sdecl: &ServiceDecl) -> Option<CrossContextService> {
295    let handler = sdecl
296        .handlers
297        .iter()
298        .find(|h| matches!(h.kind, HandlerKind::Call))?;
299    Some(CrossContextService {
300        name: name.to_string(),
301        params: handler
302            .params
303            .iter()
304            .map(|p| (p.name.name.clone(), p.type_ref.clone()))
305            .collect(),
306        return_type: handler.return_type.clone(),
307        span: sdecl.span,
308    })
309}
310
311impl CrossContextInfo {
312    /// Returns the qualified name of the consumed context this prefix refers
313    /// to, treating `prefix` as either an alias or a full qualified name.
314    pub fn resolve_prefix(&self, prefix: &str) -> Option<String> {
315        if let Some(q) = self.aliases.get(prefix) {
316            return Some(q.clone());
317        }
318        if self.consumed_contexts.iter().any(|c| c == prefix) {
319            return Some(prefix.to_string());
320        }
321        None
322    }
323
324    /// v0.15: resolve a dotted receiver chain like `platform.time.Clock` or
325    /// `Time.Clock` to `(consumed_context, capability)` when the leading
326    /// segments name a consumed context (or alias) that exports the trailing
327    /// capability. Returns `None` if the chain is not a cross-context
328    /// capability reference.
329    pub fn resolve_cross_capability(&self, chain: &str) -> Option<(String, String)> {
330        let (prefix, cap) = chain.rsplit_once('.')?;
331        let ctx = self.resolve_prefix(prefix)?;
332        let caps = self.consumed_capabilities.get(&ctx)?;
333        if caps.contains_key(cap) {
334            Some((ctx, cap.to_string()))
335        } else {
336            None
337        }
338    }
339}
340
341impl ResolvedCommons {
342    /// Returns true if `name` is a type declared in the current commons
343    /// (rather than imported via `uses`). Local types alone may reach into
344    /// their opaque representation (`.raw`) or call `.unsafe(value)`.
345    pub fn is_local_type(&self, name: &str) -> bool {
346        self.local_type_names.contains(name)
347    }
348
349    /// Events track, slice 0: is `name` a declared `event` in this commons —
350    /// not merely any local type?
351    pub fn is_local_event(&self, name: &str) -> bool {
352        self.event_type_names.contains(name)
353    }
354
355    /// Is `name` in scope via `uses` of a *commons* specifically? See
356    /// `uses_commons_type_names`'s field doc for the exact predicate.
357    pub fn is_uses_commons_type(&self, name: &str) -> bool {
358        self.uses_commons_type_names.contains(name)
359    }
360
361    /// Build a `ResolvedCommons` from a merged (local + `uses`/`consumes`)
362    /// symbol table, deriving `local_type_names`/`event_type_names` from
363    /// `local_types`/`local_events` — the *pre-merge* tables — rather than
364    /// from `types`/`agents` (already merged). This is the one thing every
365    /// hand-rolled construction outside this crate got a chance to disagree
366    /// on: the pre-merge/merged distinction is exactly what backs
367    /// `.raw`/`.unsafe()`/owner-only-event-emission gating, and reusing the
368    /// merged table there silently widens all three to any consumed/used
369    /// type or event (found during the events track, slice 0, spine #936).
370    #[allow(clippy::too_many_arguments)]
371    pub fn new(
372        commons: Commons,
373        types: HashMap<String, Arc<TypeDecl>>,
374        local_types: &HashMap<String, Arc<TypeDecl>>,
375        fns: HashMap<String, Arc<FnDecl>>,
376        methods: HashMap<String, MethodTable>,
377        agents: HashMap<String, AgentDecl>,
378        local_events: &HashMap<String, EventDecl>,
379        cross_context: CrossContextInfo,
380        imported_from: HashMap<String, String>,
381        is_context: bool,
382        uses_commons_type_names: HashSet<String>,
383    ) -> Self {
384        Self {
385            commons,
386            local_type_names: local_types.keys().cloned().collect(),
387            event_type_names: local_events.keys().cloned().collect(),
388            types,
389            fns,
390            methods,
391            cross_context,
392            agents,
393            imported_from,
394            is_context,
395            uses_commons_type_names,
396        }
397    }
398}
399
400/// Resolve names in a single-file (or already-merged) commons. Use this
401/// entry point only for self-contained Bynk programs. For multi-file
402/// projects and `uses`-resolving commons, use [`resolve_file`] against a
403/// pre-built combined symbol table.
404pub fn resolve(commons: Commons) -> Result<ResolvedCommons, Vec<CompileError>> {
405    let mut errors = Vec::new();
406    let mut types: HashMap<String, Arc<TypeDecl>> = HashMap::new();
407    let mut fns: HashMap<String, Arc<FnDecl>> = HashMap::new();
408    let mut methods: HashMap<String, MethodTable> = HashMap::new();
409
410    // First pass: collect declarations and detect duplicates / name overlap.
411    for item in &commons.items {
412        match item {
413            // v0.5 declaration kinds — these don't introduce types/fns into
414            // the symbol space. They go through the context-level v0.5 path
415            // in project.rs. Skip them at the per-commons level.
416            CommonsItem::Capability(_)
417            | CommonsItem::Provider(_)
418            | CommonsItem::Service(_)
419            | CommonsItem::Agent(_)
420            | CommonsItem::Actor(_)
421            // `messages` entries are plain string literals with no type refs
422            // to resolve here; commons-only legality and the reference/
423            // duplicate-code checks live in bynk-emit's project validation.
424            | CommonsItem::Messages(_) => {}
425            CommonsItem::Type(t) => {
426                if let Some(prev) = types.get(&t.name.name) {
427                    errors.push(
428                        CompileError::new(
429                            "bynk.resolve.duplicate_type",
430                            t.name.span,
431                            format!("type `{}` is already declared", t.name.name),
432                        )
433                        .with_label(prev.name.span, "previously declared here"),
434                    );
435                } else if let Some(prev) = fns.get(&t.name.name) {
436                    errors.push(
437                        CompileError::new(
438                            "bynk.resolve.name_conflict",
439                            t.name.span,
440                            format!(
441                                "type `{}` conflicts with a function of the same name",
442                                t.name.name
443                            ),
444                        )
445                        .with_label(prev.name.ident().span, "function declared here"),
446                    );
447                } else {
448                    types.insert(t.name.name.clone(), Arc::new(t.clone()));
449                    methods.insert(t.name.name.clone(), MethodTable::default());
450                }
451            }
452            // Events track, slice 0 (spine #936): an `event` registers into
453            // the same `types` table as an ordinary `type` — via the
454            // synthetic `TypeDecl` `EventDecl::as_type_decl` builds — so it
455            // reuses every existing type-reference/construction check.
456            // Context-only legality (`bynk.event.outside_context`) and
457            // event-vs-plain-type distinctions live in bynk-emit's project
458            // validation, the same split `messages` already uses.
459            CommonsItem::Event(e) => {
460                let t = e.as_type_decl();
461                if let Some(prev) = types.get(&t.name.name) {
462                    errors.push(
463                        CompileError::new(
464                            "bynk.resolve.duplicate_type",
465                            t.name.span,
466                            format!("type `{}` is already declared", t.name.name),
467                        )
468                        .with_label(prev.name.span, "previously declared here"),
469                    );
470                } else if let Some(prev) = fns.get(&t.name.name) {
471                    errors.push(
472                        CompileError::new(
473                            "bynk.resolve.name_conflict",
474                            t.name.span,
475                            format!(
476                                "type `{}` conflicts with a function of the same name",
477                                t.name.name
478                            ),
479                        )
480                        .with_label(prev.name.ident().span, "function declared here"),
481                    );
482                } else {
483                    methods.insert(t.name.name.clone(), MethodTable::default());
484                    types.insert(t.name.name.clone(), Arc::new(t));
485                }
486            }
487            CommonsItem::Fn(f) => match &f.name {
488                FnName::Free(id) => {
489                    if let Some(prev) = fns.get(&id.name) {
490                        errors.push(
491                            CompileError::new(
492                                "bynk.resolve.duplicate_fn",
493                                id.span,
494                                format!("function `{}` is already declared", id.name),
495                            )
496                            .with_label(prev.name.ident().span, "previously declared here"),
497                        );
498                    } else if let Some(prev) = types.get(&id.name) {
499                        errors.push(
500                            CompileError::new(
501                                "bynk.resolve.name_conflict",
502                                id.span,
503                                format!(
504                                    "function `{}` conflicts with a type of the same name",
505                                    id.name
506                                ),
507                            )
508                            .with_label(prev.name.span, "type declared here"),
509                        );
510                    } else {
511                        fns.insert(id.name.clone(), Arc::new(f.clone()));
512                    }
513                }
514                FnName::Method {
515                    type_name,
516                    method_name,
517                } => {
518                    // The type the method is attached to must be declared.
519                    if !types.contains_key(&type_name.name) {
520                        errors.push(
521                            CompileError::new(
522                                "bynk.resolve.method_unknown_type",
523                                type_name.span,
524                                format!(
525                                    "method `{}.{}` attached to an unknown type `{}`",
526                                    type_name.name, method_name.name, type_name.name
527                                ),
528                            )
529                            .with_note(
530                                "methods can only be declared on types defined in the same commons",
531                            ),
532                        );
533                        continue;
534                    }
535                    // #594: an *instance* method on a generic type is a generic
536                    // method — the receiver's type arguments supply the type's
537                    // parameters (`self: Box[A]`), so it resolves and emits as an
538                    // erased TS generic method. A *static* method has no receiver
539                    // to supply those parameters, so it stays deferred (it would
540                    // need free-function-style inference of the type's params);
541                    // reject it rather than emit an under-applied `Box` signature.
542                    if !f.has_self
543                        && types
544                            .get(&type_name.name)
545                            .is_some_and(|d| !d.type_params.is_empty())
546                    {
547                        errors.push(
548                            CompileError::new(
549                                "bynk.generics.method_on_generic_type",
550                                type_name.span,
551                                format!(
552                                    "static method `{}.{}` is attached to generic type `{}` — static methods on generic types are deferred (instance methods are supported)",
553                                    type_name.name, method_name.name, type_name.name
554                                ),
555                            )
556                            .with_note(
557                                "give the method a `self` receiver, or use a free function taking the generic value as a parameter instead",
558                            ),
559                        );
560                        continue;
561                    }
562                    let table = methods.entry(type_name.name.clone()).or_default();
563                    let bucket = if f.has_self {
564                        &mut table.instance
565                    } else {
566                        &mut table.statics
567                    };
568                    if let Some(prev) = bucket.get(&method_name.name) {
569                        errors.push(
570                            CompileError::new(
571                                "bynk.resolve.duplicate_method",
572                                method_name.span,
573                                format!(
574                                    "method `{}.{}` is already declared",
575                                    type_name.name, method_name.name
576                                ),
577                            )
578                            .with_label(prev.name.ident().span, "previously declared here"),
579                        );
580                    } else {
581                        bucket.insert(method_name.name.clone(), Arc::new(f.clone()));
582                    }
583                }
584            },
585        }
586    }
587
588    // Second pass: validate references inside type-refs and function bodies.
589    let mut refs = RefSink::new(); // single-file mode: no recording context.
590    let mut sinks = Sinks {
591        errs: &mut errors,
592        refs: &mut refs,
593    };
594    for item in &commons.items {
595        match item {
596            CommonsItem::Type(t) => {
597                check_type_decl_refs(t, &types, &mut sinks);
598            }
599            CommonsItem::Event(e) => {
600                check_type_decl_refs(&e.as_type_decl(), &types, &mut sinks);
601            }
602            CommonsItem::Fn(f) => {
603                check_fn_refs(f, &types, &fns, &methods, &mut sinks);
604            }
605            // v0.5 items are resolved via a separate context-level pass.
606            CommonsItem::Capability(_)
607            | CommonsItem::Provider(_)
608            | CommonsItem::Service(_)
609            | CommonsItem::Agent(_)
610            | CommonsItem::Actor(_)
611            // `messages` entries are plain string literals with no type refs
612            // to resolve here; commons-only legality and the reference/
613            // duplicate-code checks live in bynk-emit's project validation.
614            | CommonsItem::Messages(_) => {}
615        }
616    }
617
618    if errors.is_empty() {
619        let local_type_names = types.keys().cloned().collect();
620        let event_type_names = commons
621            .items
622            .iter()
623            .filter_map(|item| match item {
624                CommonsItem::Event(e) => Some(e.name.name.clone()),
625                _ => None,
626            })
627            .collect();
628        Ok(ResolvedCommons {
629            commons,
630            types,
631            fns,
632            methods,
633            local_type_names,
634            cross_context: CrossContextInfo::default(),
635            agents: HashMap::new(),
636            // Single-file mode has no `uses`-imported functions.
637            imported_from: HashMap::new(),
638            // Single-file mode has no `uses` at all — the rebrand this flag
639            // gates is unreachable here.
640            is_context: false,
641            uses_commons_type_names: HashSet::new(),
642            event_type_names,
643        })
644    } else {
645        Err(errors)
646    }
647}
648
649/// Validate name references inside a single file's items against an
650/// already-built symbol table (`resolved.types`, `resolved.fns`,
651/// `resolved.methods`). Used by the project-level driver after combining
652/// declarations from every file in a multi-file commons and from every
653/// commons brought in by `uses`.
654pub fn resolve_file(resolved: &ResolvedCommons) -> Result<(), Vec<CompileError>> {
655    resolve_file_record(resolved, &mut RefSink::new())
656}
657
658/// [`resolve_file`], recording binding edges into `refs` as the walk
659/// resolves them (v0.25). The project pass sets the sink's per-file context;
660/// a fresh sink records nothing.
661pub fn resolve_file_record(
662    resolved: &ResolvedCommons,
663    refs: &mut RefSink,
664) -> Result<(), Vec<CompileError>> {
665    let mut errors = Vec::new();
666    let mut sinks = Sinks {
667        errs: &mut errors,
668        refs,
669    };
670    for item in &resolved.commons.items {
671        match item {
672            CommonsItem::Type(t) => {
673                sinks.refs.set_owner(&t.name.name);
674                check_type_decl_refs(t, &resolved.types, &mut sinks);
675            }
676            CommonsItem::Event(e) => {
677                sinks.refs.set_owner(&e.name.name);
678                check_type_decl_refs(&e.as_type_decl(), &resolved.types, &mut sinks);
679            }
680            CommonsItem::Fn(f) => {
681                sinks.refs.set_owner(f.name.display());
682                check_fn_refs(
683                    f,
684                    &resolved.types,
685                    &resolved.fns,
686                    &resolved.methods,
687                    &mut sinks,
688                );
689            }
690            CommonsItem::Capability(_)
691            | CommonsItem::Provider(_)
692            | CommonsItem::Service(_)
693            | CommonsItem::Agent(_)
694            | CommonsItem::Actor(_)
695            // `messages` entries are plain string literals with no type refs
696            // to resolve here; commons-only legality and the reference/
697            // duplicate-code checks live in bynk-emit's project validation.
698            | CommonsItem::Messages(_) => {}
699        }
700        sinks.refs.clear_owner();
701    }
702    if errors.is_empty() {
703        Ok(())
704    } else {
705        Err(errors)
706    }
707}
708
709/// v0.157 (ADR 0183): the name a record field *directly contains* — a top-level
710/// `Named` (`f: A`) or a generic application (`f: A[T]`). Both are direct
711/// containment edges for the cycle guards; a `List[…]`/`Option[…]` wrapper is
712/// not (its empty/`None` inhabitant breaks the cycle).
713fn direct_record_head(tr: &TypeRef) -> Option<&str> {
714    match tr {
715        TypeRef::Named(id) => Some(&id.name),
716        TypeRef::App { name, .. } => Some(&name.name),
717        _ => None,
718    }
719}
720
721/// Whether `target` is reachable from `start` over direct record-field edges
722/// (bare `Named` or generic `App` heads) — the record-containment graph. Used
723/// to reject indirect record cycles (`A = { b: B }`, `B = { a: A }`); a
724/// `visited` set bounds the walk on graphs that already contain cycles
725/// elsewhere.
726fn record_field_reaches(start: &str, target: &str, types: &HashMap<String, Arc<TypeDecl>>) -> bool {
727    let mut visited: HashSet<String> = HashSet::new();
728    let mut stack = vec![start.to_string()];
729    while let Some(name) = stack.pop() {
730        if name == target {
731            return true;
732        }
733        if !visited.insert(name.clone()) {
734            continue;
735        }
736        if let Some(decl) = types.get(&name)
737            && let TypeBody::Record(r) = &decl.body
738        {
739            for f in &r.fields {
740                if let Some(head) = direct_record_head(&f.type_ref) {
741                    stack.push(head.to_string());
742                }
743            }
744        }
745    }
746    false
747}
748
749/// v0.157 (ADR 0183): reject a repeated type-parameter name — a duplicate would
750/// collapse silently in the substitution map (the later argument winning), so a
751/// `Pair[T, T]` mis-checks its fields. Shared by `type` and `fn` declarations.
752fn check_duplicate_type_params(params: &[TypeParam], owner: &str, errors: &mut Sinks) {
753    let mut seen: HashMap<&str, bynk_syntax::span::Span> = HashMap::new();
754    for tp in params {
755        if let Some(prev) = seen.get(tp.name.name.as_str()) {
756            errors.push(
757                CompileError::new(
758                    "bynk.generics.duplicate_type_param",
759                    tp.span,
760                    format!(
761                        "type parameter `{}` is declared more than once on {owner}",
762                        tp.name.name
763                    ),
764                )
765                .with_label(*prev, "previously declared here"),
766            );
767        } else {
768            seen.insert(tp.name.name.as_str(), tp.span);
769        }
770    }
771}
772
773/// Recursively walk a type declaration to check that every type reference
774/// inside it resolves.
775fn check_type_decl_refs(t: &TypeDecl, types: &HashMap<String, Arc<TypeDecl>>, errors: &mut Sinks) {
776    // A `type` declaration may not reuse a compiler-known built-in type name
777    // (`List`, `Map`, `Query`, …). Those names are dispatched on by the type
778    // parser (`parser/types.rs`), so any *reference* to the alias would be
779    // intercepted as the built-in — the declaration would be silently shadowed
780    // (`QueueResult`) or fail with an incoherent message at the use site. Reject
781    // it here, at the declaration, with a message the user can act on. Base
782    // types and other reserved *keywords* (`Int`, `Result`, …) are already
783    // rejected earlier, by `expect_ident` at parse time.
784    if bynk_syntax::keywords::is_builtin_type_name(&t.name.name) {
785        errors.push(
786            CompileError::new(
787                "bynk.resolve.reserved_builtin_type",
788                t.name.span,
789                format!(
790                    "`{}` is a built-in type name and cannot be redeclared",
791                    t.name.name
792                ),
793            )
794            .with_note("rename the type — built-in type names are reserved in type position"),
795        );
796    }
797    // v0.157 (ADR 0183): a record body may be generic. #593: a sum body may too
798    // — its variant payloads resolve the parameters as rigid vars, exactly as
799    // record fields do. Type parameters on a refined / opaque body are still
800    // rejected; a parameter shadowing a declared type is diagnosed (mirrors the
801    // function-generics rule).
802    let type_params: HashSet<String> = t.type_params.iter().map(|p| p.name.name.clone()).collect();
803    if !t.type_params.is_empty() {
804        check_duplicate_type_params(&t.type_params, &format!("type `{}`", t.name.name), errors);
805        if !matches!(t.body, TypeBody::Record(_) | TypeBody::Sum(_)) {
806            errors.push(
807                CompileError::new(
808                    "bynk.generics.generic_non_record",
809                    t.type_params[0].span,
810                    format!(
811                        "type `{}` declares type parameters, but only a record (`{{ … }}`) or sum (`| … | …`) type may be generic",
812                        t.name.name
813                    ),
814                )
815                .with_note("refined and opaque types cannot be generic — their base is a fixed primitive"),
816            );
817        }
818        // #593: a generic sum may not carry an `embeds` clause. Embedding folds
819        // another sum's variants in by name; composing that with per-parameter
820        // substitution (the embedded source could itself be generic, or mention
821        // the host's parameters) is out of scope for this increment.
822        if let TypeBody::Sum(s) = &t.body
823            && let Some(clause) = s.embeds.first()
824        {
825            errors.push(
826                CompileError::new(
827                    "bynk.generics.generic_sum_embeds",
828                    clause.span,
829                    format!("generic sum `{}` cannot use an `embeds` clause", t.name.name),
830                )
831                .with_note("embedding into a generic sum is not supported — declare the variants directly, or make the sum non-generic"),
832            );
833        }
834        for tp in &t.type_params {
835            if types.contains_key(&tp.name.name) {
836                errors.push(
837                    CompileError::new(
838                        "bynk.generics.type_arg_mismatch",
839                        tp.span,
840                        format!(
841                            "type parameter `{}` shadows the declared type of the same name",
842                            tp.name.name
843                        ),
844                    )
845                    .with_note("rename the type parameter"),
846                );
847            }
848        }
849    }
850    match &t.body {
851        TypeBody::Refined { .. } => {
852            // Refined-type bodies only reference base types directly.
853        }
854        TypeBody::Opaque { .. } => {
855            // Opaque-type bodies only reference base types directly.
856        }
857        TypeBody::Record(r) => {
858            let mut seen = HashMap::new();
859            for f in &r.fields {
860                if let Some(prev_span) = seen.get(&f.name.name) {
861                    errors.push(
862                        CompileError::new(
863                            "bynk.resolve.duplicate_field",
864                            f.name.span,
865                            format!("field `{}` is declared more than once", f.name.name),
866                        )
867                        .with_label(*prev_span, "previously declared here"),
868                    );
869                } else {
870                    seen.insert(f.name.name.clone(), f.name.span);
871                }
872                // Detect containment cycles: a direct `type A = { f: A }`,
873                // and indirect cycles through direct record fields
874                // (`A = { b: B }`, `B = { a: A }`). Such a cycle admits no finite
875                // value, and defeats every structural walk downstream (zero-value
876                // emission, codecs). A `List[...]`/`Option[...]` wrapper (whose
877                // empty/`None` inhabitant breaks the cycle) is not a direct edge.
878                // v0.157 (ADR 0183): a generic self-reference `f: A[T]` is a
879                // `TypeRef::App` direct edge — caught here in the checker (and so
880                // in the standalone LSP), not only by the emit-side boundary pass.
881                if let Some(head) = direct_record_head(&f.type_ref) {
882                    if head == t.name.name {
883                        errors.push(
884                            CompileError::new(
885                                "bynk.resolve.recursive_record_field",
886                                f.name.span,
887                                format!(
888                                    "record `{}` cannot directly contain a field of its own type",
889                                    t.name.name
890                                ),
891                            )
892                            .with_label(t.name.span, "type declared here")
893                            .with_note(
894                                "wrap the recursive reference in `Option[...]` to break the cycle",
895                            ),
896                        );
897                    } else if record_field_reaches(head, &t.name.name, types) {
898                        errors.push(
899                            CompileError::new(
900                                "bynk.resolve.recursive_record_field",
901                                f.name.span,
902                                format!(
903                                    "record `{}` contains itself through this field — `{}` leads back to `{}`",
904                                    t.name.name, head, t.name.name
905                                ),
906                            )
907                            .with_label(t.name.span, "type declared here")
908                            .with_note(
909                                "wrap one field in the cycle in `Option[...]` to break it",
910                            ),
911                        );
912                    }
913                }
914                check_type_ref_resolves_in(&f.type_ref, types, &type_params, errors);
915            }
916        }
917        TypeBody::Sum(s) => {
918            let mut seen = HashMap::new();
919            for v in &s.variants {
920                if let Some(prev_span) = seen.get(&v.name.name) {
921                    errors.push(
922                        CompileError::new(
923                            "bynk.resolve.duplicate_variant",
924                            v.name.span,
925                            format!("variant `{}` is declared more than once", v.name.name),
926                        )
927                        .with_label(*prev_span, "previously declared here"),
928                    );
929                } else {
930                    seen.insert(v.name.name.clone(), v.name.span);
931                }
932                let mut payload_seen = HashMap::new();
933                for f in &v.payload {
934                    if let Some(prev) = payload_seen.get(&f.name.name) {
935                        errors.push(
936                            CompileError::new(
937                                "bynk.resolve.duplicate_field",
938                                f.name.span,
939                                format!(
940                                    "payload field `{}` is declared more than once in variant `{}`",
941                                    f.name.name, v.name.name
942                                ),
943                            )
944                            .with_label(*prev, "previously declared here"),
945                        );
946                    } else {
947                        payload_seen.insert(f.name.name.clone(), f.name.span);
948                    }
949                    // #593: a generic sum's declared type parameters are in scope
950                    // in its variant payloads, resolving as rigid vars (empty set
951                    // for a non-generic sum — the same reference walk as before).
952                    check_type_ref_resolves_in(&f.type_ref, types, &type_params, errors);
953                }
954            }
955            // v0.154 (ADR 0178): the `embeds E as V` clauses' source types must
956            // resolve (the target variant is checked in `check_embeds`).
957            for clause in &s.embeds {
958                check_type_ref_resolves(&clause.source_type, types, errors);
959            }
960        }
961    }
962}
963
964fn check_fn_refs(
965    f: &FnDecl,
966    types: &HashMap<String, Arc<TypeDecl>>,
967    fns: &HashMap<String, Arc<FnDecl>>,
968    methods: &HashMap<String, MethodTable>,
969    errors: &mut Sinks,
970) {
971    // Parameter types resolve.
972    // v0.20a: the fn's type parameters are legal named references in its
973    // own signature and body annotations.
974    let mut type_params: HashSet<String> = f
975        .type_params
976        .iter()
977        .map(|tp| tp.name.name.clone())
978        .collect();
979    check_duplicate_type_params(
980        &f.type_params,
981        &format!("function `{}`", f.name.display()),
982        errors,
983    );
984    // #594: an instance method on a generic type inherits the receiver type's
985    // parameters into scope, so `fn Box.map[U](self, f: A -> U) -> Box[U]` may
986    // name the type's own parameter `A` alongside the method's `U`. A method
987    // parameter that reuses one of the type's parameter names would shadow it
988    // ambiguously in the substitution — diagnose the collision.
989    if let FnName::Method { type_name, .. } = &f.name
990        && let Some(recv) = types.get(&type_name.name)
991    {
992        for tp in &recv.type_params {
993            if type_params.contains(&tp.name.name) {
994                errors.push(
995                    CompileError::new(
996                        "bynk.generics.duplicate_type_param",
997                        f.type_params
998                            .iter()
999                            .find(|mp| mp.name.name == tp.name.name)
1000                            .map_or(tp.span, |mp| mp.span),
1001                        format!(
1002                            "type parameter `{}` is already a parameter of the receiver type `{}`",
1003                            tp.name.name, type_name.name
1004                        ),
1005                    )
1006                    .with_label(tp.span, "declared on the type here"),
1007                );
1008            }
1009            type_params.insert(tp.name.name.clone());
1010        }
1011    }
1012    let mut seen_params: HashMap<&str, &Ident> = HashMap::new();
1013    for p in &f.params {
1014        check_type_ref_resolves_in(&p.type_ref, types, &type_params, errors);
1015        if let Some(prev) = seen_params.get(p.name.name.as_str()) {
1016            errors.push(
1017                CompileError::new(
1018                    "bynk.resolve.duplicate_param",
1019                    p.name.span,
1020                    format!("parameter `{}` is declared more than once", p.name.name),
1021                )
1022                .with_label(prev.span, "previously declared here"),
1023            );
1024        } else {
1025            seen_params.insert(p.name.name.as_str(), &p.name);
1026        }
1027    }
1028    check_type_ref_resolves_in(&f.return_type, types, &type_params, errors);
1029
1030    // Build the initial scope: parameters plus `self` (for instance methods).
1031    let mut params: HashMap<String, ()> =
1032        f.params.iter().map(|p| (p.name.name.clone(), ())).collect();
1033    if f.has_self {
1034        params.insert("self".to_string(), ());
1035    }
1036    let in_method = matches!(f.name, FnName::Method { .. });
1037    let mut cx = RefCheckCtx {
1038        params: &params,
1039        in_method,
1040        types,
1041        type_params: &type_params,
1042        fns,
1043        methods,
1044        scopes: Vec::new(),
1045        errors,
1046    };
1047    check_block_references(&f.body, &mut cx);
1048}
1049
1050fn unknown_type_error(id: &Ident) -> CompileError {
1051    CompileError::new(
1052        "bynk.resolve.unknown_type",
1053        id.span,
1054        format!("unknown type `{}`", id.name),
1055    )
1056    .with_note(
1057        "only base types (Int, String, Bool), types declared in this commons, \
1058         `Result[T, E]`, `Option[T]`, and `ValidationError` are in scope",
1059    )
1060}
1061
1062/// v0.157 (ADR 0183): a generic type named without its `[…]` arguments.
1063fn bare_generic_type_error(id: &Ident, arity: usize) -> CompileError {
1064    CompileError::new(
1065        "bynk.generics.type_arg_count",
1066        id.span,
1067        format!(
1068            "generic type `{}` must be applied to {} type argument{} — write `{}[…]`",
1069            id.name,
1070            arity,
1071            if arity == 1 { "" } else { "s" },
1072            id.name
1073        ),
1074    )
1075    .with_note("a generic type is used only through a concrete instantiation")
1076}
1077
1078/// Recursively check that every type reference resolves.
1079fn check_type_ref_resolves(
1080    r: &TypeRef,
1081    types: &HashMap<String, Arc<TypeDecl>>,
1082    errors: &mut Sinks,
1083) {
1084    check_type_ref_resolves_in(r, types, &HashSet::new(), errors)
1085}
1086
1087/// v0.20a: like [`check_type_ref_resolves`], with the enclosing function's
1088/// type parameters in scope — a `Named` reference matching one is a type
1089/// variable, not an unknown type.
1090fn check_type_ref_resolves_in(
1091    r: &TypeRef,
1092    types: &HashMap<String, Arc<TypeDecl>>,
1093    type_params: &HashSet<String>,
1094    errors: &mut Sinks,
1095) {
1096    match r {
1097        TypeRef::Base(_, _) => {}
1098        // v0.20a: a function type's components must each resolve.
1099        TypeRef::Fn(params, ret, _) => {
1100            for p in params {
1101                check_type_ref_resolves_in(p, types, type_params, errors);
1102            }
1103            check_type_ref_resolves_in(ret, types, type_params, errors);
1104        }
1105        TypeRef::Named(id) => {
1106            if let Some(decl) = types.get(&id.name) {
1107                errors.refs.record(id.span, SymbolKind::Type, &id.name);
1108                // v0.157 (ADR 0183): a generic type must be applied to its type
1109                // arguments — a bare `Paginated` (declared `Paginated[T]`) is an
1110                // under-application.
1111                if !decl.type_params.is_empty() {
1112                    errors.push(bare_generic_type_error(id, decl.type_params.len()));
1113                }
1114            } else if !type_params.contains(&id.name) {
1115                errors.push(unknown_type_error(id));
1116            }
1117        }
1118        // v0.157 (ADR 0183): `Name[Arg, …]` — a user generic-type application.
1119        // Validate existence, that the target is generic, and arity; then walk
1120        // the arguments.
1121        TypeRef::App { name, args, span } => {
1122            match types.get(&name.name) {
1123                None if type_params.contains(&name.name) => {
1124                    // A type parameter applied to arguments (`T[Int]`) — a type
1125                    // parameter is not itself generic (no higher-kinded types).
1126                    errors.push(
1127                        CompileError::new(
1128                            "bynk.generics.type_arg_count",
1129                            *span,
1130                            format!(
1131                                "type parameter `{}` cannot take type arguments — it is not a generic type",
1132                                name.name
1133                            ),
1134                        )
1135                        .with_note("higher-kinded type parameters are not supported"),
1136                    );
1137                }
1138                None => errors.push(unknown_type_error(name)),
1139                Some(decl) => {
1140                    errors.refs.record(name.span, SymbolKind::Type, &name.name);
1141                    let expected = decl.type_params.len();
1142                    // Finding #46: `decl` comes from the combined cross-file
1143                    // symbol table (`uses`/multi-file siblings), so its span
1144                    // may belong to a different file than `name` — a label
1145                    // can't express that without per-label file identity (a
1146                    // Wave 8 follow-up). A note keeps the same conservative
1147                    // choice `bynk-emit/src/project/consistency.rs` already
1148                    // makes for its own always-cross-file diagnostics,
1149                    // rather than risk underlining unrelated text.
1150                    if expected == 0 {
1151                        errors.push(
1152                            CompileError::new(
1153                                "bynk.generics.type_arg_count",
1154                                *span,
1155                                format!(
1156                                    "type `{}` is not generic — it takes no type arguments",
1157                                    name.name
1158                                ),
1159                            )
1160                            .with_note("type declared here"),
1161                        );
1162                    } else if expected != args.len() {
1163                        errors.push(
1164                            CompileError::new(
1165                                "bynk.generics.type_arg_count",
1166                                *span,
1167                                format!(
1168                                    "type `{}` expects {} type argument{}, but {} {} given",
1169                                    name.name,
1170                                    expected,
1171                                    if expected == 1 { "" } else { "s" },
1172                                    args.len(),
1173                                    if args.len() == 1 { "was" } else { "were" },
1174                                ),
1175                            )
1176                            .with_note("type declared here"),
1177                        );
1178                    }
1179                }
1180            }
1181            for a in args {
1182                check_type_ref_resolves_in(a, types, type_params, errors);
1183            }
1184        }
1185        TypeRef::Result(t, e, _) => {
1186            check_type_ref_resolves_in(t, types, type_params, errors);
1187            check_type_ref_resolves_in(e, types, type_params, errors);
1188        }
1189        TypeRef::Option(t, _) => {
1190            check_type_ref_resolves_in(t, types, type_params, errors);
1191        }
1192        TypeRef::Effect(t, _) => {
1193            check_type_ref_resolves_in(t, types, type_params, errors);
1194        }
1195        TypeRef::HttpResult(t, _) => {
1196            check_type_ref_resolves_in(t, types, type_params, errors);
1197        }
1198        TypeRef::QueueResult(_) => {}
1199        TypeRef::List(t, _) => {
1200            check_type_ref_resolves_in(t, types, type_params, errors);
1201        }
1202        TypeRef::Query(t, _) => {
1203            check_type_ref_resolves_in(t, types, type_params, errors);
1204        }
1205        TypeRef::Stream(t, _) => {
1206            check_type_ref_resolves_in(t, types, type_params, errors);
1207        }
1208        TypeRef::Connection(t, _) => {
1209            check_type_ref_resolves_in(t, types, type_params, errors);
1210        }
1211        // v0.119 (ADR 0155): `History[Agent]` is a test-only generator, legal only
1212        // as a `for all` binding inside a `property` (validated in
1213        // `check_property_body`). A `History[…]` reaching this declared-type walk —
1214        // a field, parameter, return, or local annotation — is out of place.
1215        TypeRef::History(_, span) => {
1216            errors.push(
1217                CompileError::new(
1218                    "bynk.history.outside_property",
1219                    *span,
1220                    "`History[…]` is only valid as a `for all` generator inside a `property`",
1221                )
1222                .with_note(
1223                    "bind a driven call-history with `for all run: History[Agent]` in a `property`",
1224                ),
1225            );
1226        }
1227        TypeRef::Map(k, v, _) => {
1228            check_type_ref_resolves_in(k, types, type_params, errors);
1229            check_type_ref_resolves_in(v, types, type_params, errors);
1230            check_map_key_keyable(k, types, type_params, errors);
1231        }
1232        TypeRef::ValidationError(_) | TypeRef::JsonError(_) => {}
1233        TypeRef::Unit(_) => {}
1234    }
1235}
1236
1237/// v0.20b: `Map` keys are confined to value-keyable types — `String`, `Int`,
1238/// and refined/opaque types over them — so the emitted `ReadonlyMap` keeps
1239/// value equality (object keys would compare by reference). A type parameter
1240/// is admitted in key position: it can only ever be instantiated through a
1241/// concrete `Map[K, V]` reference elsewhere, and that site is checked.
1242fn check_map_key_keyable(
1243    k: &TypeRef,
1244    types: &HashMap<String, Arc<TypeDecl>>,
1245    type_params: &HashSet<String>,
1246    errors: &mut Sinks,
1247) {
1248    let keyable = match k {
1249        TypeRef::Base(BaseType::String | BaseType::Int, _) => true,
1250        TypeRef::Named(id) => {
1251            // A type parameter is admitted (see above). An unknown name has
1252            // already been reported by the resolution walk; don't pile a
1253            // keyability error on top of it.
1254            if type_params.contains(&id.name) || !types.contains_key(&id.name) {
1255                return;
1256            }
1257            matches!(
1258                types.get(&id.name).map(|t| &t.body),
1259                Some(TypeBody::Refined { base, .. } | TypeBody::Opaque { base, .. })
1260                    if matches!(base, BaseType::String | BaseType::Int)
1261            )
1262        }
1263        _ => false,
1264    };
1265    if !keyable {
1266        errors.push(
1267            CompileError::new(
1268                "bynk.types.unkeyable_map_key",
1269                k.span(),
1270                "a `Map` key must be value-keyable — `String`, `Int`, or a refined/opaque type over them",
1271            )
1272            .with_note(
1273                "record, sum, collection, and function keys are rejected in v0.20b; value-equality keys need bounded generics",
1274            ),
1275        );
1276    }
1277}
1278
1279/// Lookup a name across scopes. Returns true if it's bound somewhere
1280/// (param, self, or any let-scope).
1281fn name_in_scope(name: &str, params: &HashMap<String, ()>, scopes: &[HashMap<String, ()>]) -> bool {
1282    if params.contains_key(name) {
1283        return true;
1284    }
1285    scopes.iter().rev().any(|s| s.contains_key(name))
1286}
1287
1288/// Validate a record construction's *field set* — every required field present,
1289/// no undeclared extra field, no field initialised twice, and every shorthand
1290/// `{ name }` bound in scope. Pure over the declaration and the provided fields;
1291/// the caller supplies its own scope predicate (the resolver's lexical scope via
1292/// [`name_in_scope`], the checker's binding table via `Ctx::lookup`) and its own
1293/// diagnostic sink.
1294///
1295/// #711: this walk skips `Service`/`Agent`/`Actor` items, so their handler
1296/// bodies never pass through it — the checker's `check_record_construction` is
1297/// their only backstop and calls this same function. A single implementation is
1298/// the point: an earlier fix copied three of these four checks into the checker
1299/// and dropped the shorthand one, re-opening the gap for shorthand fields. Both
1300/// callers now share this, so the two cannot re-diverge.
1301pub(crate) fn check_record_field_set(
1302    type_name: &Ident,
1303    fields: &[FieldInit],
1304    record: &RecordBody,
1305    // #852: the span of the whole `TypeName { … }` literal, so the missing-field
1306    // quick-fix knows where to insert a new field (before the closing brace when
1307    // the literal is empty).
1308    construction_span: Span,
1309    in_scope: impl Fn(&str) -> bool,
1310    errors: &mut Vec<CompileError>,
1311) {
1312    let declared: HashMap<&str, &RecordField> = record
1313        .fields
1314        .iter()
1315        .map(|f| (f.name.name.as_str(), f))
1316        .collect();
1317    let mut provided: HashMap<&str, bynk_syntax::span::Span> = HashMap::new();
1318    for f in fields {
1319        if !declared.contains_key(f.name.name.as_str()) {
1320            errors.push(
1321                CompileError::new(
1322                    "bynk.resolve.unknown_field",
1323                    f.name.span,
1324                    format!(
1325                        "record type `{}` has no field `{}`",
1326                        type_name.name, f.name.name
1327                    ),
1328                )
1329                // Finding #46: `decl_name_span` may name a declaration in a
1330                // different file than this construction site (both callers
1331                // resolve against the combined cross-file symbol table) — a
1332                // note instead of a label, matching the same conservative
1333                // choice made elsewhere for cross-file provenance without
1334                // per-label file identity (a Wave 8 follow-up).
1335                .with_note("type declared here"),
1336            );
1337        }
1338        if let Some(prev) = provided.get(f.name.name.as_str()) {
1339            errors.push(
1340                CompileError::new(
1341                    "bynk.resolve.duplicate_field_init",
1342                    f.name.span,
1343                    format!("field `{}` is initialised more than once", f.name.name),
1344                )
1345                .with_label(*prev, "previously initialised here"),
1346            );
1347        } else {
1348            provided.insert(f.name.name.as_str(), f.name.span);
1349        }
1350        // A shorthand `{ name }` (no `: value`) reads the binding `name` from
1351        // scope — it must exist. The full `field: value` form is checked by the
1352        // caller (the resolver recurses into the value, the checker types it).
1353        if f.value.is_none() && !in_scope(&f.name.name) {
1354            errors.push(
1355                CompileError::new(
1356                    "bynk.resolve.unknown_name",
1357                    f.name.span,
1358                    format!(
1359                        "shorthand field initialiser `{}` requires a binding of that name in scope",
1360                        f.name.name
1361                    ),
1362                )
1363                .with_note("either bring `{name}` into scope or use the full `field: value` form"),
1364            );
1365        }
1366    }
1367    // Missing required fields. Each is a diagnostic anchored at the type name;
1368    // a field whose type has a safe default additionally carries a
1369    // machine-applicable "add field `x`" quick-fix (#852, DECISIONS B/C) that
1370    // inserts `x: <default>` at a fmt-stable position, and — when more than one
1371    // field is missing and every missing field is defaultable — the first such
1372    // diagnostic also carries an "add all missing fields" convenience.
1373    let missing: Vec<&RecordField> = record
1374        .fields
1375        .iter()
1376        .filter(|f| !provided.contains_key(f.name.name.as_str()))
1377        .collect();
1378    // The edit for a `body` of one or more `name: default` entries. With
1379    // existing fields it appends `, body` right after the last one. With an
1380    // *empty* literal there is no field span to anchor to and the interior
1381    // spacing/trailing punctuation is unknown, so instead the whole ` { … }`
1382    // tail (from the end of the type name through the closing brace) is
1383    // **replaced** with a canonical ` { body }` — fmt-stable regardless of how
1384    // the empty braces were originally spelled (`{}`, `{ }`, `{  }`).
1385    let field_edit = |body: &str| -> (Span, String) {
1386        match fields.iter().map(|f| f.span.end).max() {
1387            Some(end) => (Span::new(end, end), format!(", {body}")),
1388            None => (
1389                Span::new(type_name.span.end, construction_span.end),
1390                format!(" {{ {body} }}"),
1391            ),
1392        }
1393    };
1394    // Defaultable missing fields, in declaration order, as `name: default`.
1395    let defaultable: Vec<String> = missing
1396        .iter()
1397        .filter_map(|f| field_default_init(f))
1398        .collect();
1399    let all_defaultable = defaultable.len() == missing.len();
1400
1401    for (i, decl_field) in missing.iter().enumerate() {
1402        let mut err = CompileError::new(
1403            "bynk.resolve.missing_field",
1404            type_name.span,
1405            format!(
1406                "missing required field `{}` for record `{}`",
1407                decl_field.name.name, type_name.name
1408            ),
1409        )
1410        .with_label(decl_field.name.span, "field declared here");
1411        if let Some(piece) = field_default_init(decl_field) {
1412            err = err.with_suggestion(
1413                format!("add field `{}`", decl_field.name.name),
1414                vec![field_edit(&piece)],
1415                Applicability::MachineApplicable,
1416            );
1417        }
1418        // The "add all missing fields" convenience rides on the first missing
1419        // diagnostic (they all share `type_name.span`, so it surfaces together
1420        // with the single-field fixes), and only when the whole set is
1421        // defaultable and there is more than one to add.
1422        if i == 0 && missing.len() > 1 && all_defaultable {
1423            err = err.with_suggestion(
1424                "add all missing fields",
1425                vec![field_edit(&defaultable.join(", "))],
1426                Applicability::MachineApplicable,
1427            );
1428        }
1429        errors.push(err);
1430    }
1431}
1432
1433/// The `name: <default>` initialiser for a missing record field, or `None` when
1434/// the field's type has no value that is guaranteed to re-check clean (#852,
1435/// DECISION B). Deliberately conservative: an inline-refined field or a
1436/// user-named type (which may itself be refined, a sum, or opaque) has no
1437/// synthesised default — only the unrefined built-in scalars, `Option` (`None`),
1438/// and `List` (`[]`) do, so the inserted value always type-checks.
1439fn field_default_init(field: &RecordField) -> Option<String> {
1440    if field.refinement.is_some() {
1441        return None;
1442    }
1443    let default = match &field.type_ref {
1444        TypeRef::Base(BaseType::Int, _) => "0",
1445        TypeRef::Base(BaseType::Float, _) => "0.0",
1446        TypeRef::Base(BaseType::String, _) => "\"\"",
1447        TypeRef::Base(BaseType::Bool, _) => "false",
1448        TypeRef::Option(..) => "None",
1449        TypeRef::List(..) => "[]",
1450        _ => return None,
1451    };
1452    Some(format!("{}: {}", field.name.name, default))
1453}
1454
1455#[allow(clippy::too_many_arguments)]
1456/// Bundles the reference-walk's read-only lookup tables and mutable
1457/// traversal state (finding #37): threading nine positional parameters
1458/// through a ~900-line walk meant 313 of resolver.rs's 2,346 lines were
1459/// argument names at recursive call sites.
1460struct RefCheckCtx<'a, 'b> {
1461    params: &'a HashMap<String, ()>,
1462    in_method: bool,
1463    types: &'a HashMap<String, Arc<TypeDecl>>,
1464    type_params: &'a HashSet<String>,
1465    fns: &'a HashMap<String, Arc<FnDecl>>,
1466    methods: &'a HashMap<String, MethodTable>,
1467    scopes: Vec<HashMap<String, ()>>,
1468    errors: &'a mut Sinks<'b>,
1469}
1470
1471fn check_block_references(block: &Block, cx: &mut RefCheckCtx) {
1472    cx.scopes.push(HashMap::new());
1473    for stmt in &block.statements {
1474        match stmt {
1475            Statement::Let(l) | Statement::EffectLet(l) => {
1476                check_expr_references(&l.value, cx);
1477                if let Some(annot) = &l.type_annot {
1478                    check_type_ref_resolves_in(annot, cx.types, cx.type_params, cx.errors);
1479                }
1480                if let Some(prev) = cx.types.get(&l.name.name) {
1481                    cx.errors.push(
1482                        CompileError::new(
1483                            "bynk.resolve.let_shadows_type",
1484                            l.name.span,
1485                            format!(
1486                                "`let {}` shadows the declared type `{}`",
1487                                l.name.name, l.name.name
1488                            ),
1489                        )
1490                        .with_label(prev.name.span, "type declared here")
1491                        .with_note("choose a different name for the let binding"),
1492                    );
1493                } else if let Some(prev) = cx.fns.get(&l.name.name) {
1494                    cx.errors.push(
1495                        CompileError::new(
1496                            "bynk.resolve.let_shadows_fn",
1497                            l.name.span,
1498                            format!(
1499                                "`let {}` shadows the declared function `{}`",
1500                                l.name.name, l.name.name
1501                            ),
1502                        )
1503                        .with_label(prev.name.ident().span, "function declared here")
1504                        .with_note("choose a different name for the let binding"),
1505                    );
1506                } else if l.name.name != "_" {
1507                    cx.scopes
1508                        .last_mut()
1509                        .unwrap()
1510                        .insert(l.name.name.clone(), ());
1511                }
1512            }
1513            Statement::Expect(a) => {
1514                check_expr_references(&a.value, cx);
1515            }
1516            Statement::Send(s) => {
1517                check_expr_references(&s.value, cx);
1518            }
1519            Statement::Do(d) => {
1520                check_expr_references(&d.value, cx);
1521            }
1522            Statement::Assign(a) => {
1523                // v0.81: walk the RHS for references; the target resolves to a
1524                // `store` field, handled in the storage-track checker slice.
1525                check_expr_references(&a.value, cx);
1526            }
1527        }
1528    }
1529    check_expr_references(&block.tail, cx);
1530    cx.scopes.pop();
1531}
1532
1533#[allow(clippy::too_many_lines)]
1534fn check_expr_references(expr: &Expr, cx: &mut RefCheckCtx) {
1535    match &expr.kind {
1536        // v0.43: resolve names referenced inside each interpolation hole.
1537        ExprKind::InterpStr(parts) => {
1538            for part in parts {
1539                if let InterpPart::Hole(hole) = part {
1540                    check_expr_references(hole, cx);
1541                }
1542            }
1543        }
1544        ExprKind::IntLit { .. }
1545        | ExprKind::FloatLit { .. }
1546        | ExprKind::DurationLit { .. }
1547        | ExprKind::StrLit(_)
1548        | ExprKind::BoolLit(_)
1549        | ExprKind::None
1550        | ExprKind::UnitLit => {}
1551        // v0.20b: a list literal — each element resolves as a value.
1552        ExprKind::ListLit(elems) => {
1553            for el in elems {
1554                check_expr_references(el, cx);
1555            }
1556        }
1557        // Slice C: `Wire(<String>)` — the raw inner expression resolves as an
1558        // ordinary value (a string literal in practice).
1559        ExprKind::Wire(inner) => {
1560            check_expr_references(inner, cx);
1561        }
1562        // v0.20a: a lambda introduces a scope frame holding its params; the
1563        // body walks with the frame in place. Annotated param types resolve
1564        // through the ordinary type-ref check.
1565        ExprKind::Lambda(lambda) => {
1566            for p in &lambda.params {
1567                if let Some(tr) = &p.type_ref {
1568                    check_type_ref_resolves_in(tr, cx.types, cx.type_params, cx.errors);
1569                }
1570            }
1571            let mut frame: HashMap<String, ()> = HashMap::new();
1572            for p in &lambda.params {
1573                frame.insert(p.name.name.clone(), ());
1574            }
1575            cx.scopes.push(frame);
1576            check_expr_references(&lambda.body, cx);
1577            cx.scopes.pop();
1578        }
1579        ExprKind::EffectPure(inner) => {
1580            check_expr_references(inner, cx);
1581        }
1582        ExprKind::Expect(inner) => {
1583            check_expr_references(inner, cx);
1584        }
1585        ExprKind::Val { args, .. } => {
1586            // v0.9.4: the mocked type is validated by the checker; resolve any
1587            // pin-argument references here.
1588            for a in args {
1589                check_expr_references(a, cx);
1590            }
1591        }
1592        ExprKind::Observation(_) => {
1593            // v0.117: a `with` predicate's free names are the operation's
1594            // parameters, bound during type checking and not visible to name
1595            // resolution; a count is a literal. Nothing to resolve here.
1596        }
1597        ExprKind::Trace { .. } => {
1598            // v0.117: `Cap.op` names a capability seam, not value references.
1599        }
1600        ExprKind::RecordSpread {
1601            type_name,
1602            base,
1603            overrides,
1604        } => {
1605            if let Some(tn) = type_name
1606                && !cx.types.contains_key(&tn.name)
1607            {
1608                cx.errors.push(unknown_type_error(tn));
1609            }
1610            check_expr_references(base, cx);
1611            for f in overrides {
1612                if let Some(v) = &f.value {
1613                    check_expr_references(v, cx);
1614                }
1615            }
1616        }
1617        ExprKind::Ident(id) => {
1618            if id.name == "self" {
1619                if !cx.in_method {
1620                    cx.errors.push(
1621                        CompileError::new(
1622                            "bynk.resolve.self_outside_method",
1623                            id.span,
1624                            "`self` can only be used inside a method body",
1625                        )
1626                        .with_note(
1627                            "declare the function as `fn TypeName.method(self, ...)` if you intended a method",
1628                        ),
1629                    );
1630                }
1631                return;
1632            }
1633            if name_in_scope(&id.name, cx.params, &cx.scopes) {
1634                // OK.
1635            } else if http_variant(&id.name).is_some() {
1636                // v0.9: predeclared HttpResult variant (e.g. `NoContent`,
1637                // `Unauthorized`). The checker validates payload arity and
1638                // expected-type disambiguation.
1639            } else if let Some(sum_owner) = find_unique_variant_owner(&id.name, cx.types) {
1640                // It's a bare variant reference. We treat it as a valid
1641                // expression in resolver — the type checker will assign
1642                // the correct sum type. Mark with no error.
1643                let _ = sum_owner;
1644            } else if cx.types.contains_key(&id.name) {
1645                cx.errors.push(
1646                    CompileError::new(
1647                        "bynk.resolve.type_in_expr",
1648                        id.span,
1649                        format!("`{}` is a type, not a value", id.name),
1650                    )
1651                    .with_note(
1652                        "types cannot appear in expression position; \
1653                         use `TypeName.of(value)` or `TypeName { ... }` to construct values",
1654                    ),
1655                );
1656            } else if cx.fns.contains_key(&id.name) {
1657                // v0.20a: a bare named-function reference may be a function
1658                // VALUE where a function type is expected. The resolver has
1659                // no type information, so the judgment (and the
1660                // `bynk.resolve.fn_without_call` diagnostic for non-function
1661                // positions) now lives in the checker's ident rule. Silent
1662                // pass here keeps `unknown_name` from misfiring.
1663                cx.errors.refs.record(id.span, SymbolKind::Fn, &id.name);
1664            } else if find_ambiguous_variant_owners(&id.name, cx.types).len() > 1 {
1665                cx.errors.push(
1666                    CompileError::new(
1667                        "bynk.resolve.ambiguous_variant",
1668                        id.span,
1669                        format!(
1670                            "the variant name `{}` is declared on multiple sum types — qualify it as `TypeName.{}`",
1671                            id.name, id.name
1672                        ),
1673                    ),
1674                );
1675            } else {
1676                cx.errors.push(
1677                    CompileError::new(
1678                        "bynk.resolve.unknown_name",
1679                        id.span,
1680                        format!("unknown name `{}`", id.name),
1681                    )
1682                    .with_note(
1683                        "only parameters, `let` bindings, and functions declared \
1684                         in this commons are in scope",
1685                    ),
1686                );
1687            }
1688        }
1689        ExprKind::Call {
1690            name,
1691            type_args,
1692            args,
1693        } => {
1694            // #712: explicit type arguments (`identity[T](…)`) are type
1695            // references and must resolve — the checker's `check_generic_call`
1696            // otherwise dropped an unknown one silently. Validated here so
1697            // `fn`/method bodies are covered; the checker backstops handler
1698            // bodies (which never reach this walk).
1699            for ta in type_args {
1700                check_type_ref_resolves_in(ta, cx.types, cx.type_params, cx.errors);
1701            }
1702            match cx.fns.get(&name.name) {
1703                Some(decl) => {
1704                    cx.errors.refs.record(name.span, SymbolKind::Fn, &name.name);
1705                    if decl.params.len() != args.len() {
1706                        cx.errors.push(
1707                            CompileError::new(
1708                                "bynk.resolve.arity_mismatch",
1709                                name.span,
1710                                format!(
1711                                    "function `{}` expects {} argument(s), but {} were given",
1712                                    name.name,
1713                                    decl.params.len(),
1714                                    args.len()
1715                                ),
1716                            )
1717                            // Finding #46: `decl` is looked up in the
1718                            // combined cross-file symbol table, so its span
1719                            // may belong to a different file than this call
1720                            // — see the same note at the type-arity checks
1721                            // above.
1722                            .with_note("function declared here"),
1723                        );
1724                    }
1725                }
1726                None => {
1727                    // Maybe it's a variant constructor with a payload (e.g., `Placed(at, total)`).
1728                    let owners = find_ambiguous_variant_owners(&name.name, cx.types);
1729                    if http_variant(&name.name).is_some() {
1730                        // v0.9: predeclared HttpResult variant constructor.
1731                    } else if owners.len() == 1 {
1732                        // Single owner — treat as variant construction. Type
1733                        // checker validates arg count and types.
1734                    } else if owners.len() > 1 {
1735                        cx.errors.push(CompileError::new(
1736                            "bynk.resolve.ambiguous_variant",
1737                            name.span,
1738                            format!(
1739                                "the variant name `{}` is declared on multiple sum types — qualify it as `TypeName.{}(...)`",
1740                                name.name, name.name
1741                            ),
1742                        ));
1743                    } else if cx.types.contains_key(&name.name) {
1744                        cx.errors.push(CompileError::new(
1745                            "bynk.resolve.type_as_function",
1746                            name.span,
1747                            format!(
1748                                "`{}` is a type, not a function — use `{}.of(value)` or `{} {{ ... }}` instead",
1749                                name.name, name.name, name.name
1750                            ),
1751                        ));
1752                    } else if name_in_scope(&name.name, cx.params, &cx.scopes) {
1753                        // v0.20a: an in-scope value being called may be a
1754                        // legal value application if its type is a function
1755                        // type. The resolver has no type information, so the
1756                        // judgment (and `bynk.resolve.param_as_function` for
1757                        // non-function-typed values) lives in the checker's
1758                        // call dispatch. Silent pass.
1759                    } else {
1760                        cx.errors.push(
1761                            CompileError::new(
1762                                "bynk.resolve.unknown_function",
1763                                name.span,
1764                                format!("unknown function `{}`", name.name),
1765                            )
1766                            .with_note("only functions declared in this commons are callable"),
1767                        );
1768                    }
1769                }
1770            }
1771            for a in args {
1772                check_expr_references(a, cx);
1773            }
1774        }
1775        ExprKind::BinOp(_, lhs, rhs) => {
1776            check_expr_references(lhs, cx);
1777            check_expr_references(rhs, cx);
1778        }
1779        ExprKind::UnaryOp(_, e) => check_expr_references(e, cx),
1780        ExprKind::Paren(e) => check_expr_references(e, cx),
1781        ExprKind::Block(b) => check_block_references(b, cx),
1782        ExprKind::If {
1783            cond,
1784            then_block,
1785            else_block,
1786        } => {
1787            check_expr_references(cond, cx);
1788            // `is`-pattern bindings inside the condition flow into the
1789            // then-branch's scope (v0.2 §3.9).
1790            let mut then_extra: HashMap<String, ()> = HashMap::new();
1791            collect_is_binding_names(cond, &mut then_extra);
1792            cx.scopes.push(then_extra);
1793            check_block_references(then_block, cx);
1794            cx.scopes.pop();
1795            check_block_references(else_block, cx);
1796        }
1797        ExprKind::Ok(inner) | ExprKind::Err(inner) | ExprKind::Question(inner) => {
1798            check_expr_references(inner, cx);
1799        }
1800        ExprKind::Some(inner) => {
1801            check_expr_references(inner, cx);
1802        }
1803        ExprKind::ConstructorCall {
1804            type_name,
1805            method,
1806            args,
1807        } => {
1808            // The expression `T.name(args)` may be:
1809            //   - a static method call (or refined-type `of`),
1810            //   - a qualified variant constructor on a sum,
1811            //   - a qualified HttpResult variant (v0.9).
1812            // The resolver only needs to ensure that *something* matches.
1813            if type_name.name == "HttpResult" {
1814                if http_variant(&method.name).is_none() {
1815                    cx.errors.push(CompileError::new(
1816                        "bynk.resolve.unknown_static_member",
1817                        method.span,
1818                        format!("`HttpResult` has no variant named `{}`", method.name),
1819                    ));
1820                }
1821                for a in args {
1822                    check_expr_references(a, cx);
1823                }
1824                return;
1825            }
1826            if let Some(decl) = cx.types.get(&type_name.name) {
1827                cx.errors
1828                    .refs
1829                    .record(type_name.span, SymbolKind::Type, &type_name.name);
1830                let table = cx.methods.get(&type_name.name).cloned().unwrap_or_default();
1831                let is_static_method = table.statics.contains_key(&method.name);
1832                let is_of_constructor = method.name == "of"
1833                    && matches!(
1834                        decl.body,
1835                        TypeBody::Refined { .. } | TypeBody::Opaque { .. }
1836                    );
1837                let is_unsafe_constructor =
1838                    method.name == "unsafe" && matches!(decl.body, TypeBody::Opaque { .. });
1839                let is_variant = match &decl.body {
1840                    TypeBody::Sum(s) => s.variants.iter().any(|v| v.name.name == method.name),
1841                    _ => false,
1842                };
1843                if !(is_static_method || is_of_constructor || is_unsafe_constructor || is_variant) {
1844                    cx.errors.push(
1845                        CompileError::new(
1846                            "bynk.resolve.unknown_static_member",
1847                            method.span,
1848                            format!(
1849                                "type `{}` has no static method or variant named `{}`",
1850                                type_name.name, method.name
1851                            ),
1852                        )
1853                        // Finding #46: cross-file table lookup — see resolver.rs:1029.
1854                        .with_note("type declared here"),
1855                    );
1856                }
1857            } else {
1858                cx.errors.push(unknown_type_error(type_name));
1859            }
1860            for a in args {
1861                check_expr_references(a, cx);
1862            }
1863        }
1864        ExprKind::RecordConstruction { type_name, fields } => {
1865            match cx.types.get(&type_name.name) {
1866                Some(decl) => {
1867                    cx.errors
1868                        .refs
1869                        .record(type_name.span, SymbolKind::Type, &type_name.name);
1870                    match &decl.body {
1871                        TypeBody::Record(r) => {
1872                            // Field-set validation (missing / unknown / duplicate
1873                            // / shorthand-in-scope) is shared with the checker's
1874                            // `check_record_construction` so the two cannot
1875                            // re-diverge (#711). The value recursion below stays
1876                            // here — it is the resolver's reference walk.
1877                            check_record_field_set(
1878                                type_name,
1879                                fields,
1880                                r,
1881                                expr.span,
1882                                |n| name_in_scope(n, cx.params, &cx.scopes),
1883                                cx.errors.errs,
1884                            );
1885                            for f in fields {
1886                                if let Some(v) = &f.value {
1887                                    check_expr_references(v, cx);
1888                                }
1889                            }
1890                        }
1891                        TypeBody::Opaque { .. } => {
1892                            cx.errors.push(
1893                            CompileError::new(
1894                                "bynk.resolve.opaque_record_construction",
1895                                type_name.span,
1896                                format!(
1897                                    "opaque type `{}` cannot be constructed with record-literal syntax",
1898                                    type_name.name
1899                                ),
1900                            )
1901                            // Finding #46: cross-file table lookup — see resolver.rs:1029.
1902                            .with_note("type declared here")
1903                            .with_note(
1904                                "construct opaque values via `T.of(value)` (validated) or `T.unsafe(value)` (inside the defining commons)",
1905                            ),
1906                        );
1907                        }
1908                        _ => {
1909                            cx.errors.push(
1910                            CompileError::new(
1911                                "bynk.resolve.not_a_record_type",
1912                                type_name.span,
1913                                format!(
1914                                    "`{}` is not a record type — only record types can be constructed with `{{ ... }}`",
1915                                    type_name.name
1916                                ),
1917                            )
1918                            // Finding #46: cross-file table lookup — see resolver.rs:1029.
1919                            .with_note("type declared here"),
1920                        );
1921                        }
1922                    }
1923                }
1924                None => cx.errors.push(unknown_type_error(type_name)),
1925            }
1926        }
1927        ExprKind::FieldAccess { receiver, field } => {
1928            // v0.9: `HttpResult.Variant` qualified nullary variant.
1929            if let ExprKind::Ident(id) = &receiver.kind
1930                && !name_in_scope(&id.name, cx.params, &cx.scopes)
1931                && id.name == "HttpResult"
1932            {
1933                if http_variant(&field.name).is_none() {
1934                    cx.errors.push(CompileError::new(
1935                        "bynk.resolve.unknown_static_member",
1936                        field.span,
1937                        format!("`HttpResult` has no variant named `{}`", field.name),
1938                    ));
1939                }
1940                return;
1941            }
1942            // `TypeName.Variant` — qualified nullary variant reference.
1943            if let ExprKind::Ident(id) = &receiver.kind
1944                && !name_in_scope(&id.name, cx.params, &cx.scopes)
1945                && let Some(decl) = cx.types.get(&id.name)
1946            {
1947                cx.errors.refs.record(id.span, SymbolKind::Type, &id.name);
1948                let known_variant = match &decl.body {
1949                    TypeBody::Sum(s) => s.variants.iter().any(|v| v.name.name == field.name),
1950                    _ => false,
1951                };
1952                if !known_variant {
1953                    cx.errors.push(
1954                        CompileError::new(
1955                            "bynk.resolve.unknown_static_member",
1956                            field.span,
1957                            format!(
1958                                "type `{}` has no static method or variant named `{}`",
1959                                id.name, field.name
1960                            ),
1961                        )
1962                        // Finding #46: cross-file table lookup — see resolver.rs:1029.
1963                        .with_note("type declared here"),
1964                    );
1965                }
1966            } else {
1967                check_expr_references(receiver, cx);
1968            }
1969        }
1970        ExprKind::MethodCall {
1971            receiver,
1972            method,
1973            args,
1974            ..
1975        } => {
1976            // v0.9: `HttpResult.Variant(args)` — qualified HttpResult constructor.
1977            if let ExprKind::Ident(id) = &receiver.kind
1978                && !name_in_scope(&id.name, cx.params, &cx.scopes)
1979                && id.name == "HttpResult"
1980            {
1981                if http_variant(&method.name).is_none() {
1982                    cx.errors.push(CompileError::new(
1983                        "bynk.resolve.unknown_static_member",
1984                        method.span,
1985                        format!("`HttpResult` has no variant named `{}`", method.name),
1986                    ));
1987                }
1988                for a in args {
1989                    check_expr_references(a, cx);
1990                }
1991                return;
1992            }
1993            // v0.20b: `List.empty()` / `Map.empty()` — qualified statics on
1994            // the built-in collection types (no user declaration to resolve
1995            // against; the checker owns their typing). v0.22a adds the
1996            // numeric parse statics, `Int.parse(…)` / `Float.parse(…)`.
1997            if let ExprKind::Ident(id) = &receiver.kind
1998                && !name_in_scope(&id.name, cx.params, &cx.scopes)
1999                && matches!(
2000                    id.name.as_str(),
2001                    "List"
2002                        | "Map"
2003                        | "Int"
2004                        | "Float"
2005                        | "Json"
2006                        | "Duration"
2007                        | "Instant"
2008                        | "Stream"
2009                        | "Bytes"
2010                )
2011                && !cx.types.contains_key(&id.name)
2012            {
2013                let allowed: &[&str] = match id.name.as_str() {
2014                    "List" | "Map" => &["empty"],
2015                    "Json" => &["encode", "decode"],
2016                    // v0.86 (ADR 0112): `Duration.millis(n)`.
2017                    "Duration" => &["millis"],
2018                    // v0.90 (ADR 0114): `Instant.fromEpochMillis(n)`.
2019                    "Instant" => &["fromEpochMillis"],
2020                    // v0.100: `Stream.of(xs)`.
2021                    "Stream" => &["of"],
2022                    // v0.110 (ADR 0142): `Bytes.fromUtf8(s)`/`fromBase64(s)`/`empty()`.
2023                    "Bytes" => &["fromUtf8", "fromBase64", "empty"],
2024                    _ => &["parse"],
2025                };
2026                let only = allowed.join("`/`");
2027                if !allowed.contains(&method.name.as_str()) {
2028                    cx.errors.push(CompileError::new(
2029                        "bynk.resolve.unknown_static_member",
2030                        method.span,
2031                        format!(
2032                            "the built-in `{}` type has no static method named `{}` — the statics are `{only}`",
2033                            id.name, method.name
2034                        ),
2035                    ));
2036                }
2037                for a in args {
2038                    check_expr_references(a, cx);
2039                }
2040                return;
2041            }
2042            // If the receiver is a bare ident of a declared type (and not a
2043            // local binding), this is a static call: `T.method(args)`.
2044            // Validate the type/method/variant resolution here, mirroring
2045            // ConstructorCall's resolver path. Otherwise recurse into the
2046            // receiver as a value expression.
2047            if let ExprKind::Ident(id) = &receiver.kind
2048                && !name_in_scope(&id.name, cx.params, &cx.scopes)
2049                && let Some(decl) = cx.types.get(&id.name)
2050            {
2051                cx.errors.refs.record(id.span, SymbolKind::Type, &id.name);
2052                let table = cx.methods.get(&id.name).cloned().unwrap_or_default();
2053                let is_static_method = table.statics.contains_key(&method.name);
2054                let is_of_constructor = method.name == "of"
2055                    && matches!(
2056                        decl.body,
2057                        TypeBody::Refined { .. } | TypeBody::Opaque { .. }
2058                    );
2059                let is_unsafe_constructor =
2060                    method.name == "unsafe" && matches!(decl.body, TypeBody::Opaque { .. });
2061                let is_variant = match &decl.body {
2062                    TypeBody::Sum(s) => s.variants.iter().any(|v| v.name.name == method.name),
2063                    _ => false,
2064                };
2065                if !(is_static_method || is_of_constructor || is_unsafe_constructor || is_variant) {
2066                    cx.errors.push(
2067                        CompileError::new(
2068                            "bynk.resolve.unknown_static_member",
2069                            method.span,
2070                            format!(
2071                                "type `{}` has no static method or variant named `{}`",
2072                                id.name, method.name
2073                            ),
2074                        )
2075                        // Finding #46: cross-file table lookup — see resolver.rs:1029.
2076                        .with_note("type declared here"),
2077                    );
2078                }
2079            } else {
2080                check_expr_references(receiver, cx);
2081            }
2082            for a in args {
2083                check_expr_references(a, cx);
2084            }
2085        }
2086        ExprKind::Match { discriminant, arms } => {
2087            check_expr_references(discriminant, cx);
2088            for arm in arms {
2089                // Pattern bindings introduce names in the arm body. The
2090                // type checker validates the pattern against the discriminant
2091                // type. Resolver pushes a scope with those binding names so
2092                // body references resolve.
2093                let mut arm_scope = HashMap::new();
2094                collect_pattern_bindings(&arm.pattern, &mut arm_scope);
2095                cx.scopes.push(arm_scope);
2096                match &arm.body {
2097                    MatchBody::Expr(e) => check_expr_references(e, cx),
2098                    MatchBody::Block(b) => check_block_references(b, cx),
2099                }
2100                cx.scopes.pop();
2101            }
2102        }
2103        ExprKind::Is { value, pattern } => {
2104            check_expr_references(value, cx);
2105            // `is` pattern bindings flow through to the truthy branch of
2106            // an enclosing context; binding scope is handled by the type
2107            // checker. Resolver doesn't introduce anything here.
2108            let _ = pattern;
2109        }
2110    }
2111}
2112
2113/// Walk an expression collecting names introduced by `is` patterns inside
2114/// it, when applied as a Boolean test. Mirrors the binding-flow rule from
2115/// v0.2 §3.9 — bindings from `expr is Pat`, `lhs && (expr is Pat)`, or
2116/// `(expr is Pat)` flow into the surrounding truthy branch.
2117fn collect_is_binding_names(expr: &Expr, into: &mut HashMap<String, ()>) {
2118    match &expr.kind {
2119        ExprKind::Is { pattern, .. } => collect_is_pattern_binding_names(pattern, into),
2120        ExprKind::BinOp(BinOp::And, l, r) => {
2121            collect_is_binding_names(l, into);
2122            collect_is_binding_names(r, into);
2123        }
2124        ExprKind::Paren(inner) => collect_is_binding_names(inner, into),
2125        _ => {}
2126    }
2127}
2128
2129/// The depth-1 names an `is` pattern introduces — a `Variant`'s own flat
2130/// bindings (`is` supports only flat, depth-1 name bindings, ADR 0169 keeps
2131/// nesting/guards match-only, matching `gather_pattern_bindings`), or — #474
2132/// — for an or-pattern, the first alternative's (Rule 2 guarantees every
2133/// alternative gives a shared name the same type, so any one alternative's
2134/// names are representative of them all).
2135fn collect_is_pattern_binding_names(pattern: &Pattern, into: &mut HashMap<String, ()>) {
2136    match pattern {
2137        Pattern::Variant { bindings, .. } => {
2138            for b in bindings {
2139                if let Pattern::Binding(name) = b.pattern() {
2140                    into.insert(name.name.clone(), ());
2141                }
2142            }
2143        }
2144        Pattern::Or(alts, _) => {
2145            if let Some(first) = alts.first() {
2146                collect_is_pattern_binding_names(first, into);
2147            }
2148        }
2149        _ => {}
2150    }
2151}
2152
2153/// Walk a pattern collecting the names it would bind, recursively through
2154/// nested payload patterns (ADR 0169) — `Some(Ok(x))` binds `x`.
2155fn collect_pattern_bindings(pattern: &Pattern, into: &mut HashMap<String, ()>) {
2156    for id in pattern.bound_names() {
2157        into.insert(id.name.clone(), ());
2158    }
2159}
2160
2161/// Find the unique sum type that owns a given variant name. Returns None
2162/// if no type owns it; ignores cases of multiple owners (those are
2163/// reported via `find_ambiguous_variant_owners`).
2164fn find_unique_variant_owner<'a>(
2165    name: &str,
2166    types: &'a HashMap<String, Arc<TypeDecl>>,
2167) -> Option<&'a TypeDecl> {
2168    let owners = find_ambiguous_variant_owners(name, types);
2169    if owners.len() == 1 {
2170        Some(owners[0])
2171    } else {
2172        None
2173    }
2174}
2175
2176fn find_ambiguous_variant_owners<'a>(
2177    name: &str,
2178    types: &'a HashMap<String, Arc<TypeDecl>>,
2179) -> Vec<&'a TypeDecl> {
2180    let mut out = Vec::new();
2181    for t in types.values() {
2182        if let TypeBody::Sum(s) = &t.body
2183            && s.variants.iter().any(|v| v.name.name == name)
2184        {
2185            out.push(t.as_ref());
2186        }
2187    }
2188    out
2189}