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};
21
22use crate::index::{RefSink, SymbolKind};
23use bynk_syntax::ast::*;
24use bynk_syntax::error::CompileError;
25
26/// The resolver's two collection points, bundled so the reference walk
27/// threads one parameter (v0.25, ADR 0053). `push` forwards to the error
28/// list, keeping the walk's error sites unchanged; binding edges record
29/// via `refs` at the site that resolved them.
30pub(crate) struct Sinks<'a> {
31    errs: &'a mut Vec<CompileError>,
32    pub(crate) refs: &'a mut RefSink,
33}
34
35impl Sinks<'_> {
36    fn push(&mut self, e: CompileError) {
37        self.errs.push(e);
38    }
39}
40
41/// Per-type method table built during resolution: keyed by method name,
42/// values are clones of the [`FnDecl`] for that method.
43#[derive(Debug, Default, Clone)]
44pub struct MethodTable {
45    pub instance: HashMap<String, FnDecl>,
46    pub statics: HashMap<String, FnDecl>,
47}
48
49/// Output of resolution: the AST plus the symbol tables the checker needs.
50pub struct ResolvedCommons {
51    pub commons: Commons,
52    pub types: HashMap<String, TypeDecl>,
53    pub fns: HashMap<String, FnDecl>,
54    /// Per-type method tables (instance + static).
55    pub methods: HashMap<String, MethodTable>,
56    /// Names of types declared in *this* commons (as opposed to imported via
57    /// `uses`). Used by the checker to gate access to `.raw` and `.unsafe()`
58    /// on opaque types.
59    pub local_type_names: std::collections::HashSet<String>,
60    /// Cross-context call information for v0.6. None for commons and for
61    /// single-file mode. For contexts, supplies the set of consumed contexts
62    /// and any aliases introduced via `consumes ... as Alias`.
63    pub cross_context: CrossContextInfo,
64    /// Agents declared in this context. Used to recognise the `Agent(key)`
65    /// construction shape and the `agent_instance.handler(args)` method-call
66    /// shape in handler bodies that mention other agents.
67    pub agents: HashMap<String, AgentDecl>,
68    /// v0.91 (ADR 0116 D6): for each imported function name, the qualified unit
69    /// it came from (`map` → `bynk.list`). Lets the checker flag deprecated
70    /// first-party free functions at their call sites. Empty in single-file
71    /// mode and in synthetic handler-validation resolveds.
72    pub imported_from: HashMap<String, String>,
73}
74
75/// Static information about the consuming context: the set of contexts it
76/// `consumes`, and any aliases introduced via `as Alias` clauses. Used by
77/// the resolver to recognise cross-context service calls and by the checker
78/// to type them (v0.6 §4.2).
79#[derive(Debug, Default, Clone)]
80pub struct CrossContextInfo {
81    /// The qualified name of the consuming context, if this unit is a context.
82    pub self_context: Option<String>,
83    /// Qualified names of every consumed context.
84    pub consumed_contexts: Vec<String>,
85    /// alias → consumed-context qualified name.
86    pub aliases: HashMap<String, String>,
87    /// For each consumed context, its service surface plus the structural
88    /// shapes of each service handler's params and return type (as seen
89    /// from the consumed context's own namespace). Populated by the project
90    /// driver; empty in single-file mode.
91    pub consumed_services: HashMap<String, HashMap<String, CrossContextService>>,
92    /// For each consumed context, its full type table (the consumed
93    /// context's local types, plus the types it brings in via `uses`).
94    /// Used by the checker for structural shape comparisons across the
95    /// boundary (v0.6 §4.3).
96    pub consumed_types: HashMap<String, HashMap<String, TypeDecl>>,
97    /// v0.15: for each consumed context, the capabilities it `exports
98    /// capability { … }` — keyed by capability name. Used to resolve and
99    /// type-check `given B.Cap` references and `B.Cap.op(…)` calls, and by
100    /// the emitter to instantiate the provider locally.
101    pub consumed_capabilities: HashMap<String, HashMap<String, CrossContextCapability>>,
102    /// v0.17: `consumes U { Cap, … }` flattens selected capabilities into the
103    /// consumer's local namespace under their bare names (§3.3). Maps each bare
104    /// capability name to the consumed unit (context or adapter) providing it,
105    /// so bare `given Cap` / `Cap.op(…)` resolve, the deps type imports from the
106    /// right module, and compose instantiates the provider.
107    pub flattened_caps: HashMap<String, String>,
108}
109
110/// Snapshot of one exported capability in a consumed context, as needed for
111/// v0.15 cross-context capability resolution. Operation signatures are
112/// expressed in the consumed context's own namespace (resolved against
113/// `consumed_types` at the call site, mirroring [`CrossContextService`]).
114#[derive(Debug, Clone)]
115pub struct CrossContextCapability {
116    pub name: String,
117    /// Each operation's parameter type-refs and return type-ref.
118    pub ops: Vec<CrossContextCapabilityOp>,
119    /// The provider that implements this capability in the providing context
120    /// (its generated class name), so the consumer can instantiate it.
121    pub provider_name: String,
122    /// The provider's own `given` capabilities (intra-providing-context),
123    /// needed to wire the provider's constructor when instantiated locally.
124    pub provider_given: Vec<String>,
125    pub span: bynk_syntax::span::Span,
126}
127
128#[derive(Debug, Clone)]
129pub struct CrossContextCapabilityOp {
130    pub name: String,
131    pub params: Vec<(String, TypeRef)>,
132    pub return_type: TypeRef,
133}
134
135/// Snapshot of one service in a consumed context, as needed for v0.6
136/// cross-context type checking. The params and return type are expressed
137/// in the consumed context's own namespace.
138#[derive(Debug, Clone)]
139pub struct CrossContextService {
140    pub name: String,
141    /// Surface (parsed) type-refs of the `on call` handler's parameters.
142    pub params: Vec<(String, TypeRef)>,
143    pub return_type: TypeRef,
144    pub span: bynk_syntax::span::Span,
145}
146
147impl CrossContextInfo {
148    /// Returns the qualified name of the consumed context this prefix refers
149    /// to, treating `prefix` as either an alias or a full qualified name.
150    pub fn resolve_prefix(&self, prefix: &str) -> Option<String> {
151        if let Some(q) = self.aliases.get(prefix) {
152            return Some(q.clone());
153        }
154        if self.consumed_contexts.iter().any(|c| c == prefix) {
155            return Some(prefix.to_string());
156        }
157        None
158    }
159
160    /// v0.15: resolve a dotted receiver chain like `platform.time.Clock` or
161    /// `Time.Clock` to `(consumed_context, capability)` when the leading
162    /// segments name a consumed context (or alias) that exports the trailing
163    /// capability. Returns `None` if the chain is not a cross-context
164    /// capability reference.
165    pub fn resolve_cross_capability(&self, chain: &str) -> Option<(String, String)> {
166        let (prefix, cap) = chain.rsplit_once('.')?;
167        let ctx = self.resolve_prefix(prefix)?;
168        let caps = self.consumed_capabilities.get(&ctx)?;
169        if caps.contains_key(cap) {
170            Some((ctx, cap.to_string()))
171        } else {
172            None
173        }
174    }
175}
176
177impl ResolvedCommons {
178    /// Returns true if `name` is a type declared in the current commons
179    /// (rather than imported via `uses`). Local types alone may reach into
180    /// their opaque representation (`.raw`) or call `.unsafe(value)`.
181    pub fn is_local_type(&self, name: &str) -> bool {
182        self.local_type_names.contains(name)
183    }
184}
185
186/// Resolve names in a single-file (or already-merged) commons. Use this
187/// entry point only for self-contained Bynk programs. For multi-file
188/// projects and `uses`-resolving commons, use [`resolve_file`] against a
189/// pre-built combined symbol table.
190pub fn resolve(commons: Commons) -> Result<ResolvedCommons, Vec<CompileError>> {
191    let mut errors = Vec::new();
192    let mut types: HashMap<String, TypeDecl> = HashMap::new();
193    let mut fns: HashMap<String, FnDecl> = HashMap::new();
194    let mut methods: HashMap<String, MethodTable> = HashMap::new();
195
196    // First pass: collect declarations and detect duplicates / name overlap.
197    for item in &commons.items {
198        match item {
199            // v0.5 declaration kinds — these don't introduce types/fns into
200            // the symbol space. They go through the context-level v0.5 path
201            // in project.rs. Skip them at the per-commons level.
202            CommonsItem::Capability(_)
203            | CommonsItem::Provider(_)
204            | CommonsItem::Service(_)
205            | CommonsItem::Agent(_)
206            | CommonsItem::Actor(_) => {}
207            CommonsItem::Type(t) => {
208                if let Some(prev) = types.get(&t.name.name) {
209                    errors.push(
210                        CompileError::new(
211                            "bynk.resolve.duplicate_type",
212                            t.name.span,
213                            format!("type `{}` is already declared", t.name.name),
214                        )
215                        .with_label(prev.name.span, "previously declared here"),
216                    );
217                } else if let Some(prev) = fns.get(&t.name.name) {
218                    errors.push(
219                        CompileError::new(
220                            "bynk.resolve.name_conflict",
221                            t.name.span,
222                            format!(
223                                "type `{}` conflicts with a function of the same name",
224                                t.name.name
225                            ),
226                        )
227                        .with_label(prev.name.ident().span, "function declared here"),
228                    );
229                } else {
230                    types.insert(t.name.name.clone(), t.clone());
231                    methods.insert(t.name.name.clone(), MethodTable::default());
232                }
233            }
234            CommonsItem::Fn(f) => match &f.name {
235                FnName::Free(id) => {
236                    if let Some(prev) = fns.get(&id.name) {
237                        errors.push(
238                            CompileError::new(
239                                "bynk.resolve.duplicate_fn",
240                                id.span,
241                                format!("function `{}` is already declared", id.name),
242                            )
243                            .with_label(prev.name.ident().span, "previously declared here"),
244                        );
245                    } else if let Some(prev) = types.get(&id.name) {
246                        errors.push(
247                            CompileError::new(
248                                "bynk.resolve.name_conflict",
249                                id.span,
250                                format!(
251                                    "function `{}` conflicts with a type of the same name",
252                                    id.name
253                                ),
254                            )
255                            .with_label(prev.name.span, "type declared here"),
256                        );
257                    } else {
258                        fns.insert(id.name.clone(), f.clone());
259                    }
260                }
261                FnName::Method {
262                    type_name,
263                    method_name,
264                } => {
265                    // The type the method is attached to must be declared.
266                    if !types.contains_key(&type_name.name) {
267                        errors.push(
268                            CompileError::new(
269                                "bynk.resolve.method_unknown_type",
270                                type_name.span,
271                                format!(
272                                    "method `{}.{}` attached to an unknown type `{}`",
273                                    type_name.name, method_name.name, type_name.name
274                                ),
275                            )
276                            .with_note(
277                                "methods can only be declared on types defined in the same commons",
278                            ),
279                        );
280                        continue;
281                    }
282                    let table = methods.entry(type_name.name.clone()).or_default();
283                    let bucket = if f.has_self {
284                        &mut table.instance
285                    } else {
286                        &mut table.statics
287                    };
288                    if let Some(prev) = bucket.get(&method_name.name) {
289                        errors.push(
290                            CompileError::new(
291                                "bynk.resolve.duplicate_method",
292                                method_name.span,
293                                format!(
294                                    "method `{}.{}` is already declared",
295                                    type_name.name, method_name.name
296                                ),
297                            )
298                            .with_label(prev.name.ident().span, "previously declared here"),
299                        );
300                    } else {
301                        bucket.insert(method_name.name.clone(), f.clone());
302                    }
303                }
304            },
305        }
306    }
307
308    // Second pass: validate references inside type-refs and function bodies.
309    let mut refs = RefSink::new(); // single-file mode: no recording context.
310    let mut sinks = Sinks {
311        errs: &mut errors,
312        refs: &mut refs,
313    };
314    for item in &commons.items {
315        match item {
316            CommonsItem::Type(t) => {
317                check_type_decl_refs(t, &types, &mut sinks);
318            }
319            CommonsItem::Fn(f) => {
320                check_fn_refs(f, &types, &fns, &methods, &mut sinks);
321            }
322            // v0.5 items are resolved via a separate context-level pass.
323            CommonsItem::Capability(_)
324            | CommonsItem::Provider(_)
325            | CommonsItem::Service(_)
326            | CommonsItem::Agent(_)
327            | CommonsItem::Actor(_) => {}
328        }
329    }
330
331    if errors.is_empty() {
332        let local_type_names = types.keys().cloned().collect();
333        Ok(ResolvedCommons {
334            commons,
335            types,
336            fns,
337            methods,
338            local_type_names,
339            cross_context: CrossContextInfo::default(),
340            agents: HashMap::new(),
341            // Single-file mode has no `uses`-imported functions.
342            imported_from: HashMap::new(),
343        })
344    } else {
345        Err(errors)
346    }
347}
348
349/// Validate name references inside a single file's items against an
350/// already-built symbol table (`resolved.types`, `resolved.fns`,
351/// `resolved.methods`). Used by the project-level driver after combining
352/// declarations from every file in a multi-file commons and from every
353/// commons brought in by `uses`.
354pub fn resolve_file(resolved: &ResolvedCommons) -> Result<(), Vec<CompileError>> {
355    resolve_file_record(resolved, &mut RefSink::new())
356}
357
358/// [`resolve_file`], recording binding edges into `refs` as the walk
359/// resolves them (v0.25). The project pass sets the sink's per-file context;
360/// a fresh sink records nothing.
361pub fn resolve_file_record(
362    resolved: &ResolvedCommons,
363    refs: &mut RefSink,
364) -> Result<(), Vec<CompileError>> {
365    let mut errors = Vec::new();
366    let mut sinks = Sinks {
367        errs: &mut errors,
368        refs,
369    };
370    for item in &resolved.commons.items {
371        match item {
372            CommonsItem::Type(t) => {
373                sinks.refs.set_owner(&t.name.name);
374                check_type_decl_refs(t, &resolved.types, &mut sinks);
375            }
376            CommonsItem::Fn(f) => {
377                sinks.refs.set_owner(f.name.display());
378                check_fn_refs(
379                    f,
380                    &resolved.types,
381                    &resolved.fns,
382                    &resolved.methods,
383                    &mut sinks,
384                );
385            }
386            CommonsItem::Capability(_)
387            | CommonsItem::Provider(_)
388            | CommonsItem::Service(_)
389            | CommonsItem::Agent(_)
390            | CommonsItem::Actor(_) => {}
391        }
392        sinks.refs.clear_owner();
393    }
394    if errors.is_empty() {
395        Ok(())
396    } else {
397        Err(errors)
398    }
399}
400
401/// Recursively walk a type declaration to check that every type reference
402/// inside it resolves.
403fn check_type_decl_refs(t: &TypeDecl, types: &HashMap<String, TypeDecl>, errors: &mut Sinks) {
404    match &t.body {
405        TypeBody::Refined { .. } => {
406            // Refined-type bodies only reference base types directly.
407        }
408        TypeBody::Opaque { .. } => {
409            // Opaque-type bodies only reference base types directly.
410        }
411        TypeBody::Record(r) => {
412            let mut seen = HashMap::new();
413            for f in &r.fields {
414                if let Some(prev_span) = seen.get(&f.name.name) {
415                    errors.push(
416                        CompileError::new(
417                            "bynk.resolve.duplicate_field",
418                            f.name.span,
419                            format!("field `{}` is declared more than once", f.name.name),
420                        )
421                        .with_label(*prev_span, "previously declared here"),
422                    );
423                } else {
424                    seen.insert(f.name.name.clone(), f.name.span);
425                }
426                // Detect direct self-reference: `type A = { f: A }`.
427                // v0.2 has no indirection (no `Option[A]`, no records-of-records
428                // wrapping the same type) for this check to defeat; a direct
429                // `Named(A)` field where A is the enclosing type is forbidden.
430                if let TypeRef::Named(id) = &f.type_ref
431                    && id.name == t.name.name
432                {
433                    errors.push(
434                        CompileError::new(
435                            "bynk.resolve.recursive_record_field",
436                            f.name.span,
437                            format!(
438                                "record `{}` cannot directly contain a field of its own type",
439                                t.name.name
440                            ),
441                        )
442                        .with_label(t.name.span, "type declared here")
443                        .with_note(
444                            "wrap the recursive reference in `Option[...]` to break the cycle",
445                        ),
446                    );
447                }
448                check_type_ref_resolves(&f.type_ref, types, errors);
449            }
450        }
451        TypeBody::Sum(s) => {
452            let mut seen = HashMap::new();
453            for v in &s.variants {
454                if let Some(prev_span) = seen.get(&v.name.name) {
455                    errors.push(
456                        CompileError::new(
457                            "bynk.resolve.duplicate_variant",
458                            v.name.span,
459                            format!("variant `{}` is declared more than once", v.name.name),
460                        )
461                        .with_label(*prev_span, "previously declared here"),
462                    );
463                } else {
464                    seen.insert(v.name.name.clone(), v.name.span);
465                }
466                let mut payload_seen = HashMap::new();
467                for f in &v.payload {
468                    if let Some(prev) = payload_seen.get(&f.name.name) {
469                        errors.push(
470                            CompileError::new(
471                                "bynk.resolve.duplicate_field",
472                                f.name.span,
473                                format!(
474                                    "payload field `{}` is declared more than once in variant `{}`",
475                                    f.name.name, v.name.name
476                                ),
477                            )
478                            .with_label(*prev, "previously declared here"),
479                        );
480                    } else {
481                        payload_seen.insert(f.name.name.clone(), f.name.span);
482                    }
483                    check_type_ref_resolves(&f.type_ref, types, errors);
484                }
485            }
486        }
487    }
488}
489
490fn check_fn_refs(
491    f: &FnDecl,
492    types: &HashMap<String, TypeDecl>,
493    fns: &HashMap<String, FnDecl>,
494    methods: &HashMap<String, MethodTable>,
495    errors: &mut Sinks,
496) {
497    // Parameter types resolve.
498    // v0.20a: the fn's type parameters are legal named references in its
499    // own signature and body annotations.
500    let type_params: HashSet<String> = f
501        .type_params
502        .iter()
503        .map(|tp| tp.name.name.clone())
504        .collect();
505    let mut seen_params: HashMap<&str, &Ident> = HashMap::new();
506    for p in &f.params {
507        check_type_ref_resolves_in(&p.type_ref, types, &type_params, errors);
508        if let Some(prev) = seen_params.get(p.name.name.as_str()) {
509            errors.push(
510                CompileError::new(
511                    "bynk.resolve.duplicate_param",
512                    p.name.span,
513                    format!("parameter `{}` is declared more than once", p.name.name),
514                )
515                .with_label(prev.span, "previously declared here"),
516            );
517        } else {
518            seen_params.insert(p.name.name.as_str(), &p.name);
519        }
520    }
521    check_type_ref_resolves_in(&f.return_type, types, &type_params, errors);
522
523    // Build the initial scope: parameters plus `self` (for instance methods).
524    let mut params: HashMap<String, ()> =
525        f.params.iter().map(|p| (p.name.name.clone(), ())).collect();
526    if f.has_self {
527        params.insert("self".to_string(), ());
528    }
529    let in_method = matches!(f.name, FnName::Method { .. });
530    let mut scopes: Vec<HashMap<String, ()>> = Vec::new();
531    check_block_references(
532        &f.body,
533        &params,
534        in_method,
535        &mut scopes,
536        types,
537        &type_params,
538        fns,
539        methods,
540        errors,
541    );
542}
543
544fn unknown_type_error(id: &Ident) -> CompileError {
545    CompileError::new(
546        "bynk.resolve.unknown_type",
547        id.span,
548        format!("unknown type `{}`", id.name),
549    )
550    .with_note(
551        "only base types (Int, String, Bool), types declared in this commons, \
552         `Result[T, E]`, `Option[T]`, and `ValidationError` are in scope",
553    )
554}
555
556/// Recursively check that every type reference resolves.
557fn check_type_ref_resolves(r: &TypeRef, types: &HashMap<String, TypeDecl>, errors: &mut Sinks) {
558    check_type_ref_resolves_in(r, types, &HashSet::new(), errors)
559}
560
561/// v0.20a: like [`check_type_ref_resolves`], with the enclosing function's
562/// type parameters in scope — a `Named` reference matching one is a type
563/// variable, not an unknown type.
564fn check_type_ref_resolves_in(
565    r: &TypeRef,
566    types: &HashMap<String, TypeDecl>,
567    type_params: &HashSet<String>,
568    errors: &mut Sinks,
569) {
570    match r {
571        TypeRef::Base(_, _) => {}
572        // v0.20a: a function type's components must each resolve.
573        TypeRef::Fn(params, ret, _) => {
574            for p in params {
575                check_type_ref_resolves_in(p, types, type_params, errors);
576            }
577            check_type_ref_resolves_in(ret, types, type_params, errors);
578        }
579        TypeRef::Named(id) => {
580            if types.contains_key(&id.name) {
581                errors.refs.record(id.span, SymbolKind::Type, &id.name);
582            } else if !type_params.contains(&id.name) {
583                errors.push(unknown_type_error(id));
584            }
585        }
586        TypeRef::Result(t, e, _) => {
587            check_type_ref_resolves_in(t, types, type_params, errors);
588            check_type_ref_resolves_in(e, types, type_params, errors);
589        }
590        TypeRef::Option(t, _) => {
591            check_type_ref_resolves_in(t, types, type_params, errors);
592        }
593        TypeRef::Effect(t, _) => {
594            check_type_ref_resolves_in(t, types, type_params, errors);
595        }
596        TypeRef::HttpResult(t, _) => {
597            check_type_ref_resolves_in(t, types, type_params, errors);
598        }
599        TypeRef::QueueResult(_) => {}
600        TypeRef::List(t, _) => {
601            check_type_ref_resolves_in(t, types, type_params, errors);
602        }
603        TypeRef::Query(t, _) => {
604            check_type_ref_resolves_in(t, types, type_params, errors);
605        }
606        TypeRef::Stream(t, _) => {
607            check_type_ref_resolves_in(t, types, type_params, errors);
608        }
609        TypeRef::Connection(t, _) => {
610            check_type_ref_resolves_in(t, types, type_params, errors);
611        }
612        TypeRef::Map(k, v, _) => {
613            check_type_ref_resolves_in(k, types, type_params, errors);
614            check_type_ref_resolves_in(v, types, type_params, errors);
615            check_map_key_keyable(k, types, type_params, errors);
616        }
617        TypeRef::ValidationError(_) | TypeRef::JsonError(_) => {}
618        TypeRef::Unit(_) => {}
619    }
620}
621
622/// v0.20b: `Map` keys are confined to value-keyable types — `String`, `Int`,
623/// and refined/opaque types over them — so the emitted `ReadonlyMap` keeps
624/// value equality (object keys would compare by reference). A type parameter
625/// is admitted in key position: it can only ever be instantiated through a
626/// concrete `Map[K, V]` reference elsewhere, and that site is checked.
627fn check_map_key_keyable(
628    k: &TypeRef,
629    types: &HashMap<String, TypeDecl>,
630    type_params: &HashSet<String>,
631    errors: &mut Sinks,
632) {
633    let keyable = match k {
634        TypeRef::Base(BaseType::String | BaseType::Int, _) => true,
635        TypeRef::Named(id) => {
636            // A type parameter is admitted (see above). An unknown name has
637            // already been reported by the resolution walk; don't pile a
638            // keyability error on top of it.
639            if type_params.contains(&id.name) || !types.contains_key(&id.name) {
640                return;
641            }
642            matches!(
643                types.get(&id.name).map(|t| &t.body),
644                Some(TypeBody::Refined { base, .. } | TypeBody::Opaque { base, .. })
645                    if matches!(base, BaseType::String | BaseType::Int)
646            )
647        }
648        _ => false,
649    };
650    if !keyable {
651        errors.push(
652            CompileError::new(
653                "bynk.types.unkeyable_map_key",
654                k.span(),
655                "a `Map` key must be value-keyable — `String`, `Int`, or a refined/opaque type over them",
656            )
657            .with_note(
658                "record, sum, collection, and function keys are rejected in v0.20b; value-equality keys need bounded generics",
659            ),
660        );
661    }
662}
663
664/// Lookup a name across scopes. Returns true if it's bound somewhere
665/// (param, self, or any let-scope).
666fn name_in_scope(name: &str, params: &HashMap<String, ()>, scopes: &[HashMap<String, ()>]) -> bool {
667    if params.contains_key(name) {
668        return true;
669    }
670    scopes.iter().rev().any(|s| s.contains_key(name))
671}
672
673#[allow(clippy::too_many_arguments)]
674fn check_block_references(
675    block: &Block,
676    params: &HashMap<String, ()>,
677    in_method: bool,
678    scopes: &mut Vec<HashMap<String, ()>>,
679    types: &HashMap<String, TypeDecl>,
680    type_params: &HashSet<String>,
681    fns: &HashMap<String, FnDecl>,
682    methods: &HashMap<String, MethodTable>,
683    errors: &mut Sinks,
684) {
685    scopes.push(HashMap::new());
686    for stmt in &block.statements {
687        match stmt {
688            Statement::Let(l) | Statement::EffectLet(l) => {
689                check_expr_references(
690                    &l.value,
691                    params,
692                    in_method,
693                    scopes,
694                    types,
695                    type_params,
696                    fns,
697                    methods,
698                    errors,
699                );
700                if let Some(annot) = &l.type_annot {
701                    check_type_ref_resolves_in(annot, types, type_params, errors);
702                }
703                if let Some(prev) = types.get(&l.name.name) {
704                    errors.push(
705                        CompileError::new(
706                            "bynk.resolve.let_shadows_type",
707                            l.name.span,
708                            format!(
709                                "`let {}` shadows the declared type `{}`",
710                                l.name.name, l.name.name
711                            ),
712                        )
713                        .with_label(prev.name.span, "type declared here")
714                        .with_note("choose a different name for the let binding"),
715                    );
716                } else if let Some(prev) = fns.get(&l.name.name) {
717                    errors.push(
718                        CompileError::new(
719                            "bynk.resolve.let_shadows_fn",
720                            l.name.span,
721                            format!(
722                                "`let {}` shadows the declared function `{}`",
723                                l.name.name, l.name.name
724                            ),
725                        )
726                        .with_label(prev.name.ident().span, "function declared here")
727                        .with_note("choose a different name for the let binding"),
728                    );
729                } else if l.name.name != "_" {
730                    scopes.last_mut().unwrap().insert(l.name.name.clone(), ());
731                }
732            }
733            Statement::Expect(a) => {
734                check_expr_references(
735                    &a.value,
736                    params,
737                    in_method,
738                    scopes,
739                    types,
740                    type_params,
741                    fns,
742                    methods,
743                    errors,
744                );
745            }
746            Statement::Send(s) => {
747                check_expr_references(
748                    &s.value,
749                    params,
750                    in_method,
751                    scopes,
752                    types,
753                    type_params,
754                    fns,
755                    methods,
756                    errors,
757                );
758            }
759            Statement::Assign(a) => {
760                // v0.81: walk the RHS for references; the target resolves to a
761                // `store` field, handled in the storage-track checker slice.
762                check_expr_references(
763                    &a.value,
764                    params,
765                    in_method,
766                    scopes,
767                    types,
768                    type_params,
769                    fns,
770                    methods,
771                    errors,
772                );
773            }
774        }
775    }
776    check_expr_references(
777        &block.tail,
778        params,
779        in_method,
780        scopes,
781        types,
782        type_params,
783        fns,
784        methods,
785        errors,
786    );
787    scopes.pop();
788}
789
790#[allow(clippy::too_many_arguments)]
791fn check_expr_references(
792    expr: &Expr,
793    params: &HashMap<String, ()>,
794    in_method: bool,
795    scopes: &mut Vec<HashMap<String, ()>>,
796    types: &HashMap<String, TypeDecl>,
797    type_params: &HashSet<String>,
798    fns: &HashMap<String, FnDecl>,
799    methods: &HashMap<String, MethodTable>,
800    errors: &mut Sinks,
801) {
802    match &expr.kind {
803        // v0.43: resolve names referenced inside each interpolation hole.
804        ExprKind::InterpStr(parts) => {
805            for part in parts {
806                if let InterpPart::Hole(hole) = part {
807                    check_expr_references(
808                        hole,
809                        params,
810                        in_method,
811                        scopes,
812                        types,
813                        type_params,
814                        fns,
815                        methods,
816                        errors,
817                    );
818                }
819            }
820        }
821        ExprKind::IntLit(_)
822        | ExprKind::FloatLit { .. }
823        | ExprKind::DurationLit { .. }
824        | ExprKind::StrLit(_)
825        | ExprKind::BoolLit(_)
826        | ExprKind::None
827        | ExprKind::UnitLit => {}
828        // v0.20b: a list literal — each element resolves as a value.
829        ExprKind::ListLit(elems) => {
830            for el in elems {
831                check_expr_references(
832                    el,
833                    params,
834                    in_method,
835                    scopes,
836                    types,
837                    type_params,
838                    fns,
839                    methods,
840                    errors,
841                );
842            }
843        }
844        // v0.20a: a lambda introduces a scope frame holding its params; the
845        // body walks with the frame in place. Annotated param types resolve
846        // through the ordinary type-ref check.
847        ExprKind::Lambda(lambda) => {
848            for p in &lambda.params {
849                if let Some(tr) = &p.type_ref {
850                    check_type_ref_resolves_in(tr, types, type_params, errors);
851                }
852            }
853            let mut frame: HashMap<String, ()> = HashMap::new();
854            for p in &lambda.params {
855                frame.insert(p.name.name.clone(), ());
856            }
857            scopes.push(frame);
858            check_expr_references(
859                &lambda.body,
860                params,
861                in_method,
862                scopes,
863                types,
864                type_params,
865                fns,
866                methods,
867                errors,
868            );
869            scopes.pop();
870        }
871        ExprKind::EffectPure(inner) => {
872            check_expr_references(
873                inner,
874                params,
875                in_method,
876                scopes,
877                types,
878                type_params,
879                fns,
880                methods,
881                errors,
882            );
883        }
884        ExprKind::Expect(inner) => {
885            check_expr_references(
886                inner,
887                params,
888                in_method,
889                scopes,
890                types,
891                type_params,
892                fns,
893                methods,
894                errors,
895            );
896        }
897        ExprKind::Val { args, .. } => {
898            // v0.9.4: the mocked type is validated by the checker; resolve any
899            // pin-argument references here.
900            for a in args {
901                check_expr_references(
902                    a,
903                    params,
904                    in_method,
905                    scopes,
906                    types,
907                    type_params,
908                    fns,
909                    methods,
910                    errors,
911                );
912            }
913        }
914        ExprKind::RecordSpread {
915            type_name,
916            base,
917            overrides,
918        } => {
919            if let Some(tn) = type_name
920                && !types.contains_key(&tn.name)
921            {
922                errors.push(unknown_type_error(tn));
923            }
924            check_expr_references(
925                base,
926                params,
927                in_method,
928                scopes,
929                types,
930                type_params,
931                fns,
932                methods,
933                errors,
934            );
935            for f in overrides {
936                if let Some(v) = &f.value {
937                    check_expr_references(
938                        v,
939                        params,
940                        in_method,
941                        scopes,
942                        types,
943                        type_params,
944                        fns,
945                        methods,
946                        errors,
947                    );
948                }
949            }
950        }
951        ExprKind::Ident(id) => {
952            if id.name == "self" {
953                if !in_method {
954                    errors.push(
955                        CompileError::new(
956                            "bynk.resolve.self_outside_method",
957                            id.span,
958                            "`self` can only be used inside a method body",
959                        )
960                        .with_note(
961                            "declare the function as `fn TypeName.method(self, ...)` if you intended a method",
962                        ),
963                    );
964                }
965                return;
966            }
967            if name_in_scope(&id.name, params, scopes) {
968                // OK.
969            } else if http_variant(&id.name).is_some() {
970                // v0.9: predeclared HttpResult variant (e.g. `NoContent`,
971                // `Unauthorized`). The checker validates payload arity and
972                // expected-type disambiguation.
973            } else if let Some(sum_owner) = find_unique_variant_owner(&id.name, types) {
974                // It's a bare variant reference. We treat it as a valid
975                // expression in resolver — the type checker will assign
976                // the correct sum type. Mark with no error.
977                let _ = sum_owner;
978            } else if types.contains_key(&id.name) {
979                errors.push(
980                    CompileError::new(
981                        "bynk.resolve.type_in_expr",
982                        id.span,
983                        format!("`{}` is a type, not a value", id.name),
984                    )
985                    .with_note(
986                        "types cannot appear in expression position; \
987                         use `TypeName.of(value)` or `TypeName { ... }` to construct values",
988                    ),
989                );
990            } else if fns.contains_key(&id.name) {
991                // v0.20a: a bare named-function reference may be a function
992                // VALUE where a function type is expected. The resolver has
993                // no type information, so the judgment (and the
994                // `bynk.resolve.fn_without_call` diagnostic for non-function
995                // positions) now lives in the checker's ident rule. Silent
996                // pass here keeps `unknown_name` from misfiring.
997                errors.refs.record(id.span, SymbolKind::Fn, &id.name);
998            } else if find_ambiguous_variant_owners(&id.name, types).len() > 1 {
999                errors.push(
1000                    CompileError::new(
1001                        "bynk.resolve.ambiguous_variant",
1002                        id.span,
1003                        format!(
1004                            "the variant name `{}` is declared on multiple sum types — qualify it as `TypeName.{}`",
1005                            id.name, id.name
1006                        ),
1007                    ),
1008                );
1009            } else {
1010                errors.push(
1011                    CompileError::new(
1012                        "bynk.resolve.unknown_name",
1013                        id.span,
1014                        format!("unknown name `{}`", id.name),
1015                    )
1016                    .with_note(
1017                        "only parameters, `let` bindings, and functions declared \
1018                         in this commons are in scope",
1019                    ),
1020                );
1021            }
1022        }
1023        ExprKind::Call { name, args, .. } => {
1024            match fns.get(&name.name) {
1025                Some(decl) => {
1026                    errors.refs.record(name.span, SymbolKind::Fn, &name.name);
1027                    if decl.params.len() != args.len() {
1028                        errors.push(
1029                            CompileError::new(
1030                                "bynk.resolve.arity_mismatch",
1031                                name.span,
1032                                format!(
1033                                    "function `{}` expects {} argument(s), but {} were given",
1034                                    name.name,
1035                                    decl.params.len(),
1036                                    args.len()
1037                                ),
1038                            )
1039                            .with_label(decl.name.ident().span, "function declared here"),
1040                        );
1041                    }
1042                }
1043                None => {
1044                    // Maybe it's a variant constructor with a payload (e.g., `Placed(at, total)`).
1045                    let owners = find_ambiguous_variant_owners(&name.name, types);
1046                    if http_variant(&name.name).is_some() {
1047                        // v0.9: predeclared HttpResult variant constructor.
1048                    } else if owners.len() == 1 {
1049                        // Single owner — treat as variant construction. Type
1050                        // checker validates arg count and types.
1051                    } else if owners.len() > 1 {
1052                        errors.push(CompileError::new(
1053                            "bynk.resolve.ambiguous_variant",
1054                            name.span,
1055                            format!(
1056                                "the variant name `{}` is declared on multiple sum types — qualify it as `TypeName.{}(...)`",
1057                                name.name, name.name
1058                            ),
1059                        ));
1060                    } else if types.contains_key(&name.name) {
1061                        errors.push(CompileError::new(
1062                            "bynk.resolve.type_as_function",
1063                            name.span,
1064                            format!(
1065                                "`{}` is a type, not a function — use `{}.of(value)` or `{} {{ ... }}` instead",
1066                                name.name, name.name, name.name
1067                            ),
1068                        ));
1069                    } else if name_in_scope(&name.name, params, scopes) {
1070                        // v0.20a: an in-scope value being called may be a
1071                        // legal value application if its type is a function
1072                        // type. The resolver has no type information, so the
1073                        // judgment (and `bynk.resolve.param_as_function` for
1074                        // non-function-typed values) lives in the checker's
1075                        // call dispatch. Silent pass.
1076                    } else {
1077                        errors.push(
1078                            CompileError::new(
1079                                "bynk.resolve.unknown_function",
1080                                name.span,
1081                                format!("unknown function `{}`", name.name),
1082                            )
1083                            .with_note("only functions declared in this commons are callable"),
1084                        );
1085                    }
1086                }
1087            }
1088            for a in args {
1089                check_expr_references(
1090                    a,
1091                    params,
1092                    in_method,
1093                    scopes,
1094                    types,
1095                    type_params,
1096                    fns,
1097                    methods,
1098                    errors,
1099                );
1100            }
1101        }
1102        ExprKind::BinOp(_, lhs, rhs) => {
1103            check_expr_references(
1104                lhs,
1105                params,
1106                in_method,
1107                scopes,
1108                types,
1109                type_params,
1110                fns,
1111                methods,
1112                errors,
1113            );
1114            check_expr_references(
1115                rhs,
1116                params,
1117                in_method,
1118                scopes,
1119                types,
1120                type_params,
1121                fns,
1122                methods,
1123                errors,
1124            );
1125        }
1126        ExprKind::UnaryOp(_, e) => check_expr_references(
1127            e,
1128            params,
1129            in_method,
1130            scopes,
1131            types,
1132            type_params,
1133            fns,
1134            methods,
1135            errors,
1136        ),
1137        ExprKind::Paren(e) => check_expr_references(
1138            e,
1139            params,
1140            in_method,
1141            scopes,
1142            types,
1143            type_params,
1144            fns,
1145            methods,
1146            errors,
1147        ),
1148        ExprKind::Block(b) => check_block_references(
1149            b,
1150            params,
1151            in_method,
1152            scopes,
1153            types,
1154            type_params,
1155            fns,
1156            methods,
1157            errors,
1158        ),
1159        ExprKind::If {
1160            cond,
1161            then_block,
1162            else_block,
1163        } => {
1164            check_expr_references(
1165                cond,
1166                params,
1167                in_method,
1168                scopes,
1169                types,
1170                type_params,
1171                fns,
1172                methods,
1173                errors,
1174            );
1175            // `is`-pattern bindings inside the condition flow into the
1176            // then-branch's scope (v0.2 §3.9).
1177            let mut then_extra: HashMap<String, ()> = HashMap::new();
1178            collect_is_binding_names(cond, &mut then_extra);
1179            scopes.push(then_extra);
1180            check_block_references(
1181                then_block,
1182                params,
1183                in_method,
1184                scopes,
1185                types,
1186                type_params,
1187                fns,
1188                methods,
1189                errors,
1190            );
1191            scopes.pop();
1192            check_block_references(
1193                else_block,
1194                params,
1195                in_method,
1196                scopes,
1197                types,
1198                type_params,
1199                fns,
1200                methods,
1201                errors,
1202            );
1203        }
1204        ExprKind::Ok(inner) | ExprKind::Err(inner) | ExprKind::Question(inner) => {
1205            check_expr_references(
1206                inner,
1207                params,
1208                in_method,
1209                scopes,
1210                types,
1211                type_params,
1212                fns,
1213                methods,
1214                errors,
1215            );
1216        }
1217        ExprKind::Some(inner) => {
1218            check_expr_references(
1219                inner,
1220                params,
1221                in_method,
1222                scopes,
1223                types,
1224                type_params,
1225                fns,
1226                methods,
1227                errors,
1228            );
1229        }
1230        ExprKind::ConstructorCall {
1231            type_name,
1232            method,
1233            args,
1234        } => {
1235            // The expression `T.name(args)` may be:
1236            //   - a static method call (or refined-type `of`),
1237            //   - a qualified variant constructor on a sum,
1238            //   - a qualified HttpResult variant (v0.9).
1239            // The resolver only needs to ensure that *something* matches.
1240            if type_name.name == "HttpResult" {
1241                if http_variant(&method.name).is_none() {
1242                    errors.push(CompileError::new(
1243                        "bynk.resolve.unknown_static_member",
1244                        method.span,
1245                        format!("`HttpResult` has no variant named `{}`", method.name),
1246                    ));
1247                }
1248                for a in args {
1249                    check_expr_references(
1250                        a,
1251                        params,
1252                        in_method,
1253                        scopes,
1254                        types,
1255                        type_params,
1256                        fns,
1257                        methods,
1258                        errors,
1259                    );
1260                }
1261                return;
1262            }
1263            if let Some(decl) = types.get(&type_name.name) {
1264                errors
1265                    .refs
1266                    .record(type_name.span, SymbolKind::Type, &type_name.name);
1267                let table = methods.get(&type_name.name).cloned().unwrap_or_default();
1268                let is_static_method = table.statics.contains_key(&method.name);
1269                let is_of_constructor = method.name == "of"
1270                    && matches!(
1271                        decl.body,
1272                        TypeBody::Refined { .. } | TypeBody::Opaque { .. }
1273                    );
1274                let is_unsafe_constructor =
1275                    method.name == "unsafe" && matches!(decl.body, TypeBody::Opaque { .. });
1276                let is_variant = match &decl.body {
1277                    TypeBody::Sum(s) => s.variants.iter().any(|v| v.name.name == method.name),
1278                    _ => false,
1279                };
1280                if !(is_static_method || is_of_constructor || is_unsafe_constructor || is_variant) {
1281                    errors.push(
1282                        CompileError::new(
1283                            "bynk.resolve.unknown_static_member",
1284                            method.span,
1285                            format!(
1286                                "type `{}` has no static method or variant named `{}`",
1287                                type_name.name, method.name
1288                            ),
1289                        )
1290                        .with_label(decl.name.span, "type declared here"),
1291                    );
1292                }
1293            } else {
1294                errors.push(unknown_type_error(type_name));
1295            }
1296            for a in args {
1297                check_expr_references(
1298                    a,
1299                    params,
1300                    in_method,
1301                    scopes,
1302                    types,
1303                    type_params,
1304                    fns,
1305                    methods,
1306                    errors,
1307                );
1308            }
1309        }
1310        ExprKind::RecordConstruction { type_name, fields } => {
1311            match types.get(&type_name.name) {
1312                Some(decl) => {
1313                    errors
1314                        .refs
1315                        .record(type_name.span, SymbolKind::Type, &type_name.name);
1316                    match &decl.body {
1317                        TypeBody::Record(r) => {
1318                            let declared: HashMap<&str, &RecordField> =
1319                                r.fields.iter().map(|f| (f.name.name.as_str(), f)).collect();
1320                            let mut provided: HashMap<&str, &Ident> = HashMap::new();
1321                            for f in fields {
1322                                if !declared.contains_key(f.name.name.as_str()) {
1323                                    errors.push(
1324                                        CompileError::new(
1325                                            "bynk.resolve.unknown_field",
1326                                            f.name.span,
1327                                            format!(
1328                                                "record type `{}` has no field `{}`",
1329                                                type_name.name, f.name.name
1330                                            ),
1331                                        )
1332                                        .with_label(decl.name.span, "type declared here"),
1333                                    );
1334                                }
1335                                if let Some(prev) = provided.get(f.name.name.as_str()) {
1336                                    errors.push(
1337                                        CompileError::new(
1338                                            "bynk.resolve.duplicate_field_init",
1339                                            f.name.span,
1340                                            format!(
1341                                                "field `{}` is initialised more than once",
1342                                                f.name.name
1343                                            ),
1344                                        )
1345                                        .with_label(prev.span, "previously initialised here"),
1346                                    );
1347                                } else {
1348                                    provided.insert(f.name.name.as_str(), &f.name);
1349                                }
1350                                // Shorthand `name` — must be in scope.
1351                                match &f.value {
1352                                    Some(v) => check_expr_references(
1353                                        v,
1354                                        params,
1355                                        in_method,
1356                                        scopes,
1357                                        types,
1358                                        type_params,
1359                                        fns,
1360                                        methods,
1361                                        errors,
1362                                    ),
1363                                    None => {
1364                                        if !name_in_scope(&f.name.name, params, scopes) {
1365                                            errors.push(
1366                                            CompileError::new(
1367                                                "bynk.resolve.unknown_name",
1368                                                f.name.span,
1369                                                format!(
1370                                                    "shorthand field initialiser `{}` requires a binding of that name in scope",
1371                                                    f.name.name
1372                                                ),
1373                                            )
1374                                            .with_note(
1375                                                "either bring `{name}` into scope or use the full `field: value` form",
1376                                            ),
1377                                        );
1378                                        }
1379                                    }
1380                                }
1381                            }
1382                            for decl_field in &r.fields {
1383                                if !provided.contains_key(decl_field.name.name.as_str()) {
1384                                    errors.push(
1385                                        CompileError::new(
1386                                            "bynk.resolve.missing_field",
1387                                            type_name.span,
1388                                            format!(
1389                                                "missing required field `{}` for record `{}`",
1390                                                decl_field.name.name, type_name.name
1391                                            ),
1392                                        )
1393                                        .with_label(decl_field.name.span, "field declared here"),
1394                                    );
1395                                }
1396                            }
1397                        }
1398                        TypeBody::Opaque { .. } => {
1399                            errors.push(
1400                            CompileError::new(
1401                                "bynk.resolve.opaque_record_construction",
1402                                type_name.span,
1403                                format!(
1404                                    "opaque type `{}` cannot be constructed with record-literal syntax",
1405                                    type_name.name
1406                                ),
1407                            )
1408                            .with_label(decl.name.span, "type declared here")
1409                            .with_note(
1410                                "construct opaque values via `T.of(value)` (validated) or `T.unsafe(value)` (inside the defining commons)",
1411                            ),
1412                        );
1413                        }
1414                        _ => {
1415                            errors.push(
1416                            CompileError::new(
1417                                "bynk.resolve.not_a_record_type",
1418                                type_name.span,
1419                                format!(
1420                                    "`{}` is not a record type — only record types can be constructed with `{{ ... }}`",
1421                                    type_name.name
1422                                ),
1423                            )
1424                            .with_label(decl.name.span, "type declared here"),
1425                        );
1426                        }
1427                    }
1428                }
1429                None => errors.push(unknown_type_error(type_name)),
1430            }
1431        }
1432        ExprKind::FieldAccess { receiver, field } => {
1433            // v0.9: `HttpResult.Variant` qualified nullary variant.
1434            if let ExprKind::Ident(id) = &receiver.kind
1435                && !name_in_scope(&id.name, params, scopes)
1436                && id.name == "HttpResult"
1437            {
1438                if http_variant(&field.name).is_none() {
1439                    errors.push(CompileError::new(
1440                        "bynk.resolve.unknown_static_member",
1441                        field.span,
1442                        format!("`HttpResult` has no variant named `{}`", field.name),
1443                    ));
1444                }
1445                return;
1446            }
1447            // `TypeName.Variant` — qualified nullary variant reference.
1448            if let ExprKind::Ident(id) = &receiver.kind
1449                && !name_in_scope(&id.name, params, scopes)
1450                && let Some(decl) = types.get(&id.name)
1451            {
1452                errors.refs.record(id.span, SymbolKind::Type, &id.name);
1453                let known_variant = match &decl.body {
1454                    TypeBody::Sum(s) => s.variants.iter().any(|v| v.name.name == field.name),
1455                    _ => false,
1456                };
1457                if !known_variant {
1458                    errors.push(
1459                        CompileError::new(
1460                            "bynk.resolve.unknown_static_member",
1461                            field.span,
1462                            format!(
1463                                "type `{}` has no static method or variant named `{}`",
1464                                id.name, field.name
1465                            ),
1466                        )
1467                        .with_label(decl.name.span, "type declared here"),
1468                    );
1469                }
1470            } else {
1471                check_expr_references(
1472                    receiver,
1473                    params,
1474                    in_method,
1475                    scopes,
1476                    types,
1477                    type_params,
1478                    fns,
1479                    methods,
1480                    errors,
1481                );
1482            }
1483        }
1484        ExprKind::MethodCall {
1485            receiver,
1486            method,
1487            args,
1488            ..
1489        } => {
1490            // v0.9: `HttpResult.Variant(args)` — qualified HttpResult constructor.
1491            if let ExprKind::Ident(id) = &receiver.kind
1492                && !name_in_scope(&id.name, params, scopes)
1493                && id.name == "HttpResult"
1494            {
1495                if http_variant(&method.name).is_none() {
1496                    errors.push(CompileError::new(
1497                        "bynk.resolve.unknown_static_member",
1498                        method.span,
1499                        format!("`HttpResult` has no variant named `{}`", method.name),
1500                    ));
1501                }
1502                for a in args {
1503                    check_expr_references(
1504                        a,
1505                        params,
1506                        in_method,
1507                        scopes,
1508                        types,
1509                        type_params,
1510                        fns,
1511                        methods,
1512                        errors,
1513                    );
1514                }
1515                return;
1516            }
1517            // v0.20b: `List.empty()` / `Map.empty()` — qualified statics on
1518            // the built-in collection types (no user declaration to resolve
1519            // against; the checker owns their typing). v0.22a adds the
1520            // numeric parse statics, `Int.parse(…)` / `Float.parse(…)`.
1521            if let ExprKind::Ident(id) = &receiver.kind
1522                && !name_in_scope(&id.name, params, scopes)
1523                && matches!(
1524                    id.name.as_str(),
1525                    "List"
1526                        | "Map"
1527                        | "Int"
1528                        | "Float"
1529                        | "Json"
1530                        | "Duration"
1531                        | "Instant"
1532                        | "Stream"
1533                        | "Bytes"
1534                )
1535                && !types.contains_key(&id.name)
1536            {
1537                let allowed: &[&str] = match id.name.as_str() {
1538                    "List" | "Map" => &["empty"],
1539                    "Json" => &["encode", "decode"],
1540                    // v0.86 (ADR 0112): `Duration.millis(n)`.
1541                    "Duration" => &["millis"],
1542                    // v0.90 (ADR 0114): `Instant.fromEpochMillis(n)`.
1543                    "Instant" => &["fromEpochMillis"],
1544                    // v0.100: `Stream.of(xs)`.
1545                    "Stream" => &["of"],
1546                    // v0.110 (ADR 0142): `Bytes.fromUtf8(s)`/`fromBase64(s)`/`empty()`.
1547                    "Bytes" => &["fromUtf8", "fromBase64", "empty"],
1548                    _ => &["parse"],
1549                };
1550                let only = allowed.join("`/`");
1551                if !allowed.contains(&method.name.as_str()) {
1552                    errors.push(CompileError::new(
1553                        "bynk.resolve.unknown_static_member",
1554                        method.span,
1555                        format!(
1556                            "the built-in `{}` type has no static method named `{}` — the statics are `{only}`",
1557                            id.name, method.name
1558                        ),
1559                    ));
1560                }
1561                for a in args {
1562                    check_expr_references(
1563                        a,
1564                        params,
1565                        in_method,
1566                        scopes,
1567                        types,
1568                        type_params,
1569                        fns,
1570                        methods,
1571                        errors,
1572                    );
1573                }
1574                return;
1575            }
1576            // If the receiver is a bare ident of a declared type (and not a
1577            // local binding), this is a static call: `T.method(args)`.
1578            // Validate the type/method/variant resolution here, mirroring
1579            // ConstructorCall's resolver path. Otherwise recurse into the
1580            // receiver as a value expression.
1581            if let ExprKind::Ident(id) = &receiver.kind
1582                && !name_in_scope(&id.name, params, scopes)
1583                && let Some(decl) = types.get(&id.name)
1584            {
1585                errors.refs.record(id.span, SymbolKind::Type, &id.name);
1586                let table = methods.get(&id.name).cloned().unwrap_or_default();
1587                let is_static_method = table.statics.contains_key(&method.name);
1588                let is_of_constructor = method.name == "of"
1589                    && matches!(
1590                        decl.body,
1591                        TypeBody::Refined { .. } | TypeBody::Opaque { .. }
1592                    );
1593                let is_unsafe_constructor =
1594                    method.name == "unsafe" && matches!(decl.body, TypeBody::Opaque { .. });
1595                let is_variant = match &decl.body {
1596                    TypeBody::Sum(s) => s.variants.iter().any(|v| v.name.name == method.name),
1597                    _ => false,
1598                };
1599                if !(is_static_method || is_of_constructor || is_unsafe_constructor || is_variant) {
1600                    errors.push(
1601                        CompileError::new(
1602                            "bynk.resolve.unknown_static_member",
1603                            method.span,
1604                            format!(
1605                                "type `{}` has no static method or variant named `{}`",
1606                                id.name, method.name
1607                            ),
1608                        )
1609                        .with_label(decl.name.span, "type declared here"),
1610                    );
1611                }
1612            } else {
1613                check_expr_references(
1614                    receiver,
1615                    params,
1616                    in_method,
1617                    scopes,
1618                    types,
1619                    type_params,
1620                    fns,
1621                    methods,
1622                    errors,
1623                );
1624            }
1625            for a in args {
1626                check_expr_references(
1627                    a,
1628                    params,
1629                    in_method,
1630                    scopes,
1631                    types,
1632                    type_params,
1633                    fns,
1634                    methods,
1635                    errors,
1636                );
1637            }
1638        }
1639        ExprKind::Match { discriminant, arms } => {
1640            check_expr_references(
1641                discriminant,
1642                params,
1643                in_method,
1644                scopes,
1645                types,
1646                type_params,
1647                fns,
1648                methods,
1649                errors,
1650            );
1651            for arm in arms {
1652                // Pattern bindings introduce names in the arm body. The
1653                // type checker validates the pattern against the discriminant
1654                // type. Resolver pushes a scope with those binding names so
1655                // body references resolve.
1656                let mut arm_scope = HashMap::new();
1657                collect_pattern_bindings(&arm.pattern, &mut arm_scope);
1658                scopes.push(arm_scope);
1659                match &arm.body {
1660                    MatchBody::Expr(e) => check_expr_references(
1661                        e,
1662                        params,
1663                        in_method,
1664                        scopes,
1665                        types,
1666                        type_params,
1667                        fns,
1668                        methods,
1669                        errors,
1670                    ),
1671                    MatchBody::Block(b) => check_block_references(
1672                        b,
1673                        params,
1674                        in_method,
1675                        scopes,
1676                        types,
1677                        type_params,
1678                        fns,
1679                        methods,
1680                        errors,
1681                    ),
1682                }
1683                scopes.pop();
1684            }
1685        }
1686        ExprKind::Is { value, pattern } => {
1687            check_expr_references(
1688                value,
1689                params,
1690                in_method,
1691                scopes,
1692                types,
1693                type_params,
1694                fns,
1695                methods,
1696                errors,
1697            );
1698            // `is` pattern bindings flow through to the truthy branch of
1699            // an enclosing context; binding scope is handled by the type
1700            // checker. Resolver doesn't introduce anything here.
1701            let _ = pattern;
1702        }
1703    }
1704}
1705
1706/// Walk an expression collecting names introduced by `is` patterns inside
1707/// it, when applied as a Boolean test. Mirrors the binding-flow rule from
1708/// v0.2 §3.9 — bindings from `expr is Pat`, `lhs && (expr is Pat)`, or
1709/// `(expr is Pat)` flow into the surrounding truthy branch.
1710fn collect_is_binding_names(expr: &Expr, into: &mut HashMap<String, ()>) {
1711    match &expr.kind {
1712        ExprKind::Is {
1713            pattern: Pattern::Variant { bindings, .. },
1714            ..
1715        } => {
1716            for b in bindings {
1717                if !b.is_wildcard() {
1718                    into.insert(b.local_name().name.clone(), ());
1719                }
1720            }
1721        }
1722        ExprKind::BinOp(BinOp::And, l, r) => {
1723            collect_is_binding_names(l, into);
1724            collect_is_binding_names(r, into);
1725        }
1726        ExprKind::Paren(inner) => collect_is_binding_names(inner, into),
1727        _ => {}
1728    }
1729}
1730
1731/// Walk a pattern collecting the names it would bind.
1732fn collect_pattern_bindings(pattern: &Pattern, into: &mut HashMap<String, ()>) {
1733    match pattern {
1734        Pattern::Wildcard(_) => {}
1735        Pattern::Variant { bindings, .. } => {
1736            for b in bindings {
1737                if !b.is_wildcard() {
1738                    into.insert(b.local_name().name.clone(), ());
1739                }
1740            }
1741        }
1742    }
1743}
1744
1745/// Find the unique sum type that owns a given variant name. Returns None
1746/// if no type owns it; ignores cases of multiple owners (those are
1747/// reported via `find_ambiguous_variant_owners`).
1748fn find_unique_variant_owner<'a>(
1749    name: &str,
1750    types: &'a HashMap<String, TypeDecl>,
1751) -> Option<&'a TypeDecl> {
1752    let owners = find_ambiguous_variant_owners(name, types);
1753    if owners.len() == 1 {
1754        Some(owners[0])
1755    } else {
1756        None
1757    }
1758}
1759
1760fn find_ambiguous_variant_owners<'a>(
1761    name: &str,
1762    types: &'a HashMap<String, TypeDecl>,
1763) -> Vec<&'a TypeDecl> {
1764    let mut out = Vec::new();
1765    for t in types.values() {
1766        if let TypeBody::Sum(s) = &t.body
1767            && s.variants.iter().any(|v| v.name.name == name)
1768        {
1769            out.push(t);
1770        }
1771    }
1772    out
1773}