Skip to main content

bynk_check/
resolver.rs

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