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