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    /// #1883, slice 1a: every `asset` visible in this unit — its own and its
201    /// `uses`d commons' ([`crate::assets::compose_unit_assets`]). An asset
202    /// name is a value of type `Asset`. Empty unless set with
203    /// [`ResolvedCommons::with_assets`].
204    pub assets: HashMap<String, AssetDecl>,
205    /// #2387, slice 1: every `embed` visible in this unit — its own and its
206    /// `uses`d commons' ([`crate::embeds::compose_unit_embeds`]). An embed
207    /// name is a `String` value. Empty unless set with
208    /// [`ResolvedCommons::with_embeds`].
209    pub embeds: HashMap<String, EmbedDecl>,
210    /// #1925: the `uses`-imported values this unit's own declarations of
211    /// another kind shadow, by name. Empty unless set with
212    /// [`ResolvedCommons::with_shadowed_imports`]; read through
213    /// [`ResolvedCommons::shadowed_import_error`].
214    pub shadowed_imports: HashMap<String, ShadowedImport>,
215    /// Standard-library S3 (#1963): the names in scope that are a first-party
216    /// unit's extensible enums (`FetchError`, `Method`), whose `match` needs a
217    /// wildcard. Types carry no owning unit, so this is decided once per unit
218    /// from its provenance (`check_pipeline`) and carried to every check built
219    /// from it. Empty unless set with [`ResolvedCommons::with_extensible_enums`].
220    pub extensible_enums: HashSet<String>,
221    /// Standard-library S3 (#1963): the unit's stability facts — the project's
222    /// `[unstable]` policy and the imports it blocks. Set with
223    /// [`ResolvedCommons::with_stability`]; the default blocks nothing.
224    pub stability: crate::stability::UnitStability,
225}
226
227/// Static information about the consuming context: the set of contexts it
228/// `consumes`, and any aliases introduced via `as Alias` clauses. Used by
229/// the resolver to recognise cross-context service calls and by the checker
230/// to type them (v0.6 §4.2).
231#[derive(Debug, Default, Clone)]
232pub struct CrossContextInfo {
233    /// The qualified name of the consuming context, if this unit is a context.
234    pub self_context: Option<String>,
235    /// Qualified names of every consumed context.
236    pub consumed_contexts: Vec<String>,
237    /// alias → consumed-context qualified name.
238    pub aliases: HashMap<String, String>,
239    /// For each consumed context, its service surface plus the structural
240    /// shapes of each service handler's params and return type (as seen
241    /// from the consumed context's own namespace). Populated by the project
242    /// driver; empty in single-file mode.
243    pub consumed_services: HashMap<String, HashMap<String, CrossContextService>>,
244    /// For each consumed context, its full type table (the consumed
245    /// context's local types, plus the types it brings in via `uses`).
246    /// Used by the checker for structural shape comparisons across the
247    /// boundary (v0.6 §4.3).
248    pub consumed_types: HashMap<String, HashMap<String, Arc<TypeDecl>>>,
249    /// #1932: for each consumed context, the renames (`Box` → `shop__Box`)
250    /// under which the consumer generates codecs for the consumed context's
251    /// types it neither imports nor names (#1846). Set by the emitter, which
252    /// computes them once (`emitter::unexported_type_renames`) so a call site
253    /// names the same codecs the consumer generates. Empty in the checker.
254    pub consumed_type_renames: HashMap<String, HashMap<String, String>>,
255    /// v0.15: for each consumed context, the capabilities it `exports
256    /// capability { … }` — keyed by capability name. Used to resolve and
257    /// type-check `given B.Cap` references and `B.Cap.op(…)` calls, and by
258    /// the emitter to instantiate the provider locally.
259    pub consumed_capabilities: HashMap<String, HashMap<String, CrossContextCapability>>,
260    /// v0.17: `consumes U { Cap, … }` flattens selected capabilities into the
261    /// consumer's local namespace under their bare names (§3.3). Maps each bare
262    /// capability name to the consumed unit (context or adapter) providing it,
263    /// so bare `given Cap` / `Cap.op(…)` resolve, the deps type imports from the
264    /// right module, and compose instantiates the provider.
265    pub flattened_caps: HashMap<String, String>,
266    /// Events track, slice 0 (spine #936): for each consumed context, the
267    /// names of its own `event` declarations. Lets a subscriber's `from
268    /// Events(E)` header be checked against a foreign owner too — `E` is
269    /// legitimate if it's a local event *or* a declared event of some
270    /// consumed context, mirroring how `discover_event_subscribers`
271    /// (`bynk-emit/src/project.rs`) already resolves ownership for wiring.
272    pub consumed_event_names: HashMap<String, HashSet<String>>,
273    /// #2073: for each consumed context, the types in its `consumed_types`
274    /// table that it does not declare but consumes from another context,
275    /// mapped to that declaring context (`Order` → `shop.order` for a consumed
276    /// `shop.ledger`). A boundary cast or codec names such a type through its
277    /// declaring context, not the consumed one.
278    pub consumed_type_owners: HashMap<String, HashMap<String, String>>,
279}
280
281/// Snapshot of one exported capability in a consumed context, as needed for
282/// v0.15 cross-context capability resolution. Operation signatures are
283/// expressed in the consumed context's own namespace (resolved against
284/// `consumed_types` at the call site, mirroring [`CrossContextService`]).
285#[derive(Debug, Clone)]
286pub struct CrossContextCapability {
287    pub name: String,
288    /// Each operation's parameter type-refs and return type-ref.
289    pub ops: Vec<CrossContextCapabilityOp>,
290    /// The provider that implements this capability in the providing context
291    /// (its generated class name), so the consumer can instantiate it.
292    pub provider_name: String,
293    /// The provider's own `given` capabilities (intra-providing-context),
294    /// needed to wire the provider's constructor when instantiated locally.
295    pub provider_given: Vec<String>,
296    pub span: bynk_syntax::span::Span,
297}
298
299#[derive(Debug, Clone)]
300pub struct CrossContextCapabilityOp {
301    pub name: String,
302    /// #926: the op's own type parameters (empty for a non-generic op),
303    /// spelled the same as the consumed context's own declaration. A cross-
304    /// context call resolves these from an explicit call-site type argument,
305    /// same as the local-capability path.
306    pub type_params: Vec<String>,
307    pub params: Vec<(String, TypeRef)>,
308    pub return_type: TypeRef,
309}
310
311/// Snapshot of one service in a consumed context, as needed for v0.6
312/// cross-context type checking. The params and return type are expressed
313/// in the consumed context's own namespace.
314#[derive(Debug, Clone)]
315pub struct CrossContextService {
316    pub name: String,
317    /// Surface (parsed) type-refs of the `on call` handler's parameters.
318    pub params: Vec<(String, TypeRef)>,
319    pub return_type: TypeRef,
320    pub span: bynk_syntax::span::Span,
321    /// I3 (#1971): the foreign callers the handler admits, by actor name. A
322    /// system test that calls as one of them signs as that actor. Not part
323    /// of the contract.
324    pub foreign_callers: Vec<String>,
325}
326
327/// Project one local `on call` handler into the [`CrossContextService`] shape
328/// both sides of a cross-context contract check need — a caller resolving a
329/// *consumed* service ([`crate::symbols::build_cross_context_info`]) and a
330/// callee stamping its *own* `X-Bynk-Contract` constant
331/// ([`crate::contract::own_contract_hashes`]). Sharing this one projection is
332/// the whole correctness argument for that symmetry: if the two sides ever
333/// diverged, a working deployment would 409 on every call instead of only on
334/// real skew. `None` when `sdecl` has no `on call` handler (e.g. an
335/// events-only or queue-only service).
336pub fn cross_context_service_for(name: &str, sdecl: &ServiceDecl) -> Option<CrossContextService> {
337    let handler = sdecl
338        .handlers
339        .iter()
340        .find(|h| matches!(h.kind, HandlerKind::Call))?;
341    Some(CrossContextService {
342        name: name.to_string(),
343        params: handler
344            .params
345            .iter()
346            .map(|p| (p.name.name.clone(), p.type_ref.clone()))
347            .collect(),
348        return_type: handler.return_type.clone(),
349        span: sdecl.span,
350        foreign_callers: Vec::new(),
351    })
352}
353
354impl CrossContextInfo {
355    /// Returns the qualified name of the consumed context this prefix refers
356    /// to, treating `prefix` as either an alias or a full qualified name.
357    pub fn resolve_prefix(&self, prefix: &str) -> Option<String> {
358        if let Some(q) = self.aliases.get(prefix) {
359            return Some(q.clone());
360        }
361        if self.consumed_contexts.iter().any(|c| c == prefix) {
362            return Some(prefix.to_string());
363        }
364        None
365    }
366
367    /// v0.15: resolve a dotted receiver chain like `platform.time.Clock` or
368    /// `Time.Clock` to `(consumed_context, capability)` when the leading
369    /// segments name a consumed context (or alias) that exports the trailing
370    /// capability. Returns `None` if the chain is not a cross-context
371    /// capability reference.
372    pub fn resolve_cross_capability(&self, chain: &str) -> Option<(String, String)> {
373        let (prefix, cap) = chain.rsplit_once('.')?;
374        let ctx = self.resolve_prefix(prefix)?;
375        let caps = self.consumed_capabilities.get(&ctx)?;
376        if caps.contains_key(cap) {
377            Some((ctx, cap.to_string()))
378        } else {
379            None
380        }
381    }
382}
383
384impl ResolvedCommons {
385    /// Returns true if `name` is a type declared in the current commons
386    /// (rather than imported via `uses`). Local types alone may reach into
387    /// their opaque representation (`.raw`) or call `.unsafe(value)`.
388    pub fn is_local_type(&self, name: &str) -> bool {
389        self.local_type_names.contains(name)
390    }
391
392    /// Events track, slice 0: is `name` a declared `event` in this commons —
393    /// not merely any local type?
394    pub fn is_local_event(&self, name: &str) -> bool {
395        self.event_type_names.contains(name)
396    }
397
398    /// Is `name` in scope via `uses` of a *commons* specifically? See
399    /// `uses_commons_type_names`'s field doc for the exact predicate.
400    pub fn is_uses_commons_type(&self, name: &str) -> bool {
401        self.uses_commons_type_names.contains(name)
402    }
403
404    /// Build a `ResolvedCommons` from a merged (local + `uses`/`consumes`)
405    /// symbol table, deriving `local_type_names`/`event_type_names` from
406    /// `local_types`/`local_events` — the *pre-merge* tables — rather than
407    /// from `types`/`agents` (already merged). This is the one thing every
408    /// hand-rolled construction outside this crate got a chance to disagree
409    /// on: the pre-merge/merged distinction is exactly what backs
410    /// `.raw`/`.unsafe()`/owner-only-event-emission gating, and reusing the
411    /// merged table there silently widens all three to any consumed/used
412    /// type or event (found during the events track, slice 0, spine #936).
413    #[allow(clippy::too_many_arguments)]
414    pub fn new(
415        commons: Commons,
416        types: HashMap<String, Arc<TypeDecl>>,
417        local_types: &HashMap<String, Arc<TypeDecl>>,
418        fns: HashMap<String, Arc<FnDecl>>,
419        methods: HashMap<String, MethodTable>,
420        agents: HashMap<String, AgentDecl>,
421        local_events: &HashMap<String, EventDecl>,
422        cross_context: CrossContextInfo,
423        imported_from: HashMap<String, String>,
424        is_context: bool,
425        uses_commons_type_names: HashSet<String>,
426    ) -> Self {
427        Self {
428            commons,
429            local_type_names: local_types.keys().cloned().collect(),
430            event_type_names: local_events.keys().cloned().collect(),
431            types,
432            fns,
433            methods,
434            cross_context,
435            agents,
436            imported_from,
437            is_context,
438            uses_commons_type_names,
439            assets: HashMap::new(),
440            embeds: HashMap::new(),
441            shadowed_imports: HashMap::new(),
442            extensible_enums: HashSet::new(),
443            stability: Default::default(),
444        }
445    }
446
447    /// #1883, slice 1a: the assets visible in this unit. A builder rather
448    /// than a [`ResolvedCommons::new`] parameter, so the constructions that
449    /// can never see an asset (a synthetic test scaffold) need not mention
450    /// them.
451    pub fn with_assets(mut self, assets: HashMap<String, AssetDecl>) -> Self {
452        self.assets = assets;
453        self
454    }
455
456    /// #2387, slice 1: the embeds visible in this unit, a builder like
457    /// [`ResolvedCommons::with_assets`].
458    pub fn with_embeds(mut self, embeds: HashMap<String, EmbedDecl>) -> Self {
459        self.embeds = embeds;
460        self
461    }
462
463    /// #1925: the `uses`-imported values this unit's own declarations of
464    /// another kind shadow ([`crate::project_model::shadowed_imports`]).
465    pub fn with_shadowed_imports(mut self, shadowed: HashMap<String, ShadowedImport>) -> Self {
466        self.shadowed_imports = shadowed;
467        self
468    }
469
470    /// S3 (#1963): set [`ResolvedCommons::extensible_enums`].
471    pub fn with_extensible_enums(mut self, names: HashSet<String>) -> Self {
472        self.extensible_enums = names;
473        self
474    }
475
476    /// S3 (#1963): set [`ResolvedCommons::stability`].
477    pub fn with_stability(mut self, stability: crate::stability::UnitStability) -> Self {
478        self.stability = stability;
479        self
480    }
481
482    /// #1925: the conflict to report for a reference to `name` that the
483    /// unit's own declaration could not answer, when `name` is a shadowed
484    /// import — or `None`, and the caller reports its usual diagnostic.
485    pub fn shadowed_import_error(&self, name: &str, span: Span) -> Option<CompileError> {
486        self.shadowed_imports
487            .get(name)
488            .map(|s| shadowed_import_error(name, span, s))
489    }
490}
491
492/// #1925: a `uses`-imported function or asset that the unit hides behind its
493/// own declaration of the same name and another kind. A local declaration
494/// wins, as it does in the emitted module, so the import is out of scope; a
495/// reference the local declaration cannot answer (a call to a type, a bare
496/// service name) is reported as this conflict, not as an unknown name.
497#[derive(Debug, Clone)]
498pub struct ShadowedImport {
499    /// The commons that declares the import.
500    pub unit: String,
501    /// The import's kind: `"function"`, `"asset"`, or `"type"` (#1927).
502    pub imported: &'static str,
503    /// The local declaration's kind, as `symbols::local_value_kind` names it.
504    pub local: &'static str,
505    /// The local declaration's name.
506    pub local_span: Span,
507}
508
509/// `bynk.uses.name_conflict` for a reference to `name`, a shadowed import.
510pub(crate) fn shadowed_import_error(name: &str, span: Span, s: &ShadowedImport) -> CompileError {
511    CompileError::new(
512        "bynk.uses.name_conflict",
513        span,
514        format!(
515            "`{name}` names this unit's {}, which hides the {} `{name}` from `{}`",
516            s.local, s.imported, s.unit
517        ),
518    )
519    .with_label(s.local_span, format!("{} declared here", s.local))
520    .with_note(format!(
521        "a unit's own declaration shadows a `uses`-imported one of the same name; rename one to use the {}",
522        s.imported
523    ))
524}
525
526/// Resolve names in a single-file (or already-merged) commons. Use this
527/// entry point only for self-contained Bynk programs. For multi-file
528/// projects and `uses`-resolving commons, use [`resolve_file`] against a
529/// pre-built combined symbol table.
530pub fn resolve(commons: Commons) -> Result<ResolvedCommons, Vec<CompileError>> {
531    let (resolved, errors) = resolve_recovering(commons);
532    if errors.is_empty() {
533        Ok(resolved)
534    } else {
535        Err(errors)
536    }
537}
538
539/// [`resolve`], returning the symbol table *with* every resolve error rather
540/// than instead of it (#1663, Decision A). A resolve error is local to the
541/// declaration it is in, so the checker can still check every declaration —
542/// an unknown name in `b` no longer hides a type error in `a`. The table is
543/// complete apart from what the errors name (a duplicate keeps its first
544/// declaration). A caller that emits must still refuse on any error.
545pub fn resolve_recovering(commons: Commons) -> (ResolvedCommons, Vec<CompileError>) {
546    let mut errors = Vec::new();
547    let mut types: HashMap<String, Arc<TypeDecl>> = HashMap::new();
548    let mut fns: HashMap<String, Arc<FnDecl>> = HashMap::new();
549    let mut methods: HashMap<String, MethodTable> = HashMap::new();
550
551    // First pass: collect declarations and detect duplicates / name overlap.
552    for item in &commons.items {
553        match item {
554            // v0.5 declaration kinds — these don't introduce types/fns into
555            // the symbol space. They go through the context-level v0.5 path
556            // in project.rs. Skip them at the per-commons level.
557            CommonsItem::Capability(_)
558            | CommonsItem::Provider(_)
559            | CommonsItem::Service(_)
560            | CommonsItem::Agent(_)
561            | CommonsItem::Actor(_)
562            // `messages` entries are plain string literals with no type refs
563            // to resolve here; commons-only legality and the reference/
564            // duplicate-code checks live in bynk-emit's project validation.
565            | CommonsItem::Messages(_)
566            // #1883, slice 1a: an asset's placement, path and name checks
567            // live in `symbols::build_unit_table`; it has no type refs.
568            | CommonsItem::Asset(_)
569            // #2387, slice 1: likewise an embed (`embeds::path_errors`).
570            | CommonsItem::Embed(_) => {}
571            CommonsItem::Type(t) => {
572                if let Some(prev) = types.get(&t.name.name) {
573                    errors.push(
574                        CompileError::new(
575                            "bynk.resolve.duplicate_type",
576                            t.name.span,
577                            format!("type `{}` is already declared", t.name.name),
578                        )
579                        .with_label(prev.name.span, "previously declared here"),
580                    );
581                } else if let Some(prev) = fns.get(&t.name.name) {
582                    errors.push(
583                        CompileError::new(
584                            "bynk.resolve.name_conflict",
585                            t.name.span,
586                            format!(
587                                "type `{}` conflicts with a function of the same name",
588                                t.name.name
589                            ),
590                        )
591                        .with_label(prev.name.ident().span, "function declared here"),
592                    );
593                } else {
594                    types.insert(t.name.name.clone(), Arc::new(t.clone()));
595                    methods.insert(t.name.name.clone(), MethodTable::default());
596                }
597            }
598            // Events track, slice 0 (spine #936): an `event` registers into
599            // the same `types` table as an ordinary `type` — via the
600            // synthetic `TypeDecl` `EventDecl::as_type_decl` builds — so it
601            // reuses every existing type-reference/construction check.
602            // Context-only legality (`bynk.event.outside_context`) and
603            // event-vs-plain-type distinctions live in bynk-emit's project
604            // validation, the same split `messages` already uses.
605            CommonsItem::Event(e) => {
606                let t = e.as_type_decl();
607                if let Some(prev) = types.get(&t.name.name) {
608                    errors.push(
609                        CompileError::new(
610                            "bynk.resolve.duplicate_type",
611                            t.name.span,
612                            format!("type `{}` is already declared", t.name.name),
613                        )
614                        .with_label(prev.name.span, "previously declared here"),
615                    );
616                } else if let Some(prev) = fns.get(&t.name.name) {
617                    errors.push(
618                        CompileError::new(
619                            "bynk.resolve.name_conflict",
620                            t.name.span,
621                            format!(
622                                "type `{}` conflicts with a function of the same name",
623                                t.name.name
624                            ),
625                        )
626                        .with_label(prev.name.ident().span, "function declared here"),
627                    );
628                } else {
629                    methods.insert(t.name.name.clone(), MethodTable::default());
630                    types.insert(t.name.name.clone(), Arc::new(t));
631                }
632            }
633            CommonsItem::Fn(f) => match &f.name {
634                FnName::Free(id) => {
635                    if let Some(prev) = fns.get(&id.name) {
636                        errors.push(
637                            CompileError::new(
638                                "bynk.resolve.duplicate_fn",
639                                id.span,
640                                format!("function `{}` is already declared", id.name),
641                            )
642                            .with_label(prev.name.ident().span, "previously declared here"),
643                        );
644                    } else if let Some(prev) = types.get(&id.name) {
645                        errors.push(
646                            CompileError::new(
647                                "bynk.resolve.name_conflict",
648                                id.span,
649                                format!(
650                                    "function `{}` conflicts with a type of the same name",
651                                    id.name
652                                ),
653                            )
654                            .with_label(prev.name.span, "type declared here"),
655                        );
656                    } else {
657                        fns.insert(id.name.clone(), Arc::new(f.clone()));
658                    }
659                }
660                FnName::Method {
661                    type_name,
662                    method_name,
663                } => {
664                    // The type the method is attached to must be declared.
665                    if !types.contains_key(&type_name.name) {
666                        errors.push(
667                            CompileError::new(
668                                "bynk.resolve.method_unknown_type",
669                                type_name.span,
670                                format!(
671                                    "method `{}.{}` attached to an unknown type `{}`",
672                                    type_name.name, method_name.name, type_name.name
673                                ),
674                            )
675                            .with_note(
676                                "methods can only be declared on types defined in the same commons",
677                            ),
678                        );
679                        continue;
680                    }
681                    // #594: an *instance* method on a generic type is a generic
682                    // method — the receiver's type arguments supply the type's
683                    // parameters (`self: Box[A]`), so it resolves and emits as an
684                    // erased TS generic method. A *static* method has no receiver
685                    // to supply those parameters, so it stays deferred (it would
686                    // need free-function-style inference of the type's params);
687                    // reject it rather than emit an under-applied `Box` signature.
688                    if !f.has_self
689                        && types
690                            .get(&type_name.name)
691                            .is_some_and(|d| !d.type_params.is_empty())
692                    {
693                        errors.push(
694                            CompileError::new(
695                                "bynk.generics.method_on_generic_type",
696                                type_name.span,
697                                format!(
698                                    "static method `{}.{}` is attached to generic type `{}` — static methods on generic types are deferred (instance methods are supported)",
699                                    type_name.name, method_name.name, type_name.name
700                                ),
701                            )
702                            .with_note(
703                                "give the method a `self` receiver, or use a free function taking the generic value as a parameter instead",
704                            ),
705                        );
706                        continue;
707                    }
708                    let table = methods.entry(type_name.name.clone()).or_default();
709                    let bucket = if f.has_self {
710                        &mut table.instance
711                    } else {
712                        &mut table.statics
713                    };
714                    if let Some(prev) = bucket.get(&method_name.name) {
715                        errors.push(
716                            CompileError::new(
717                                "bynk.resolve.duplicate_method",
718                                method_name.span,
719                                format!(
720                                    "method `{}.{}` is already declared",
721                                    type_name.name, method_name.name
722                                ),
723                            )
724                            .with_label(prev.name.ident().span, "previously declared here"),
725                        );
726                    } else {
727                        bucket.insert(method_name.name.clone(), Arc::new(f.clone()));
728                    }
729                }
730            },
731        }
732    }
733
734    // #1883, slice 1a: single-file mode has no `uses`, so the visible assets
735    // are exactly the ones declared here.
736    // The first declaration of a name wins, as in the unit table; a second is
737    // `bynk.asset.duplicate` (`assets::single_file_asset_errors`).
738    let mut local_assets: HashMap<String, AssetDecl> = HashMap::new();
739    for item in &commons.items {
740        if let CommonsItem::Asset(a) = item {
741            local_assets
742                .entry(a.name.name.clone())
743                .or_insert_with(|| a.clone());
744        }
745    }
746
747    // #2387, slice 1: likewise the embeds, first declaration winning
748    // (`embeds::single_file_embed_errors` reports a second).
749    let mut local_embeds: HashMap<String, EmbedDecl> = HashMap::new();
750    for item in &commons.items {
751        if let CommonsItem::Embed(e) = item {
752            local_embeds
753                .entry(e.name.name.clone())
754                .or_insert_with(|| e.clone());
755        }
756    }
757
758    // Second pass: validate references inside type-refs and function bodies.
759    let mut refs = RefSink::new(); // single-file mode: no recording context.
760    let mut sinks = Sinks {
761        errs: &mut errors,
762        refs: &mut refs,
763    };
764    for item in &commons.items {
765        check_reserved_host_name(item, &mut sinks);
766        match item {
767            CommonsItem::Type(t) => {
768                check_type_decl_refs(t, &types, &mut sinks);
769            }
770            CommonsItem::Event(e) => {
771                check_type_decl_refs(&e.as_type_decl(), &types, &mut sinks);
772            }
773            CommonsItem::Fn(f) => {
774                check_fn_refs(
775                    f,
776                    &types,
777                    &fns,
778                    &methods,
779                    &local_assets,
780                    &local_embeds,
781                    // A single file has no `uses` imports to shadow.
782                    &HashMap::new(),
783                    &mut sinks,
784                );
785            }
786            // v0.5 items' bodies are resolved via a separate context-level
787            // pass; their signatures here (#1679).
788            CommonsItem::Capability(_)
789            | CommonsItem::Service(_)
790            | CommonsItem::Agent(_)
791            | CommonsItem::Provider(_)
792            | CommonsItem::Actor(_) => {
793                check_signature_refs(item, &types, &mut sinks);
794            }
795            // `messages` entries are plain string literals with no type refs
796            // to resolve here; commons-only legality and the reference/
797            // duplicate-code checks live in bynk-emit's project validation.
798            // #1883, slice 1a: likewise an asset (a string-literal path).
799            CommonsItem::Messages(_) | CommonsItem::Asset(_) | CommonsItem::Embed(_) => {}
800        }
801    }
802
803    {
804        let local_type_names = types.keys().cloned().collect();
805        let event_type_names = commons
806            .items
807            .iter()
808            .filter_map(|item| match item {
809                CommonsItem::Event(e) => Some(e.name.name.clone()),
810                _ => None,
811            })
812            .collect();
813        let resolved = ResolvedCommons {
814            commons,
815            types,
816            fns,
817            methods,
818            local_type_names,
819            cross_context: CrossContextInfo::default(),
820            agents: HashMap::new(),
821            // Single-file mode has no `uses`-imported functions.
822            imported_from: HashMap::new(),
823            // Single-file mode has no `uses` at all — the rebrand this flag
824            // gates is unreachable here.
825            is_context: false,
826            uses_commons_type_names: HashSet::new(),
827            event_type_names,
828            assets: local_assets,
829            embeds: local_embeds,
830            shadowed_imports: HashMap::new(),
831            extensible_enums: HashSet::new(),
832            stability: Default::default(),
833        };
834        (resolved, errors)
835    }
836}
837
838/// #1663 (Decision B): split resolve errors into those to report and those
839/// that only echo a declaration the parser had to skip.
840///
841/// A declaration that fails to parse is dropped from the AST, so every
842/// reference to its name — `Money` in a signature, `Money.zero` as a method's
843/// owner, a call to a skipped `fn` — resolves as unknown, and one missing comma
844/// becomes twenty diagnostics. Its name is known (the parser recorded it in
845/// `broken_decl_names`); only its declaration is broken, and that is already
846/// reported. So an unknown-name diagnostic naming one is hidden.
847///
848/// The hidden errors still count as resolve errors: pass *both* halves to
849/// [`without_resolve_echoes`], so the declarations they are in reject the
850/// checker's follow-on diagnostics too.
851pub fn split_broken_decl_echoes(
852    resolve_errors: Vec<CompileError>,
853    broken_decl_names: &[String],
854) -> (Vec<CompileError>, Vec<CompileError>) {
855    const ECHO_CODES: &[&str] = &[
856        "bynk.resolve.unknown_type",
857        "bynk.resolve.method_unknown_type",
858        "bynk.resolve.unknown_name",
859        "bynk.resolve.unknown_function",
860        "bynk.resolve.unknown_static_member",
861        // #1710: echoes the checker reports (so the project path splits the
862        // checker's diagnostics too), each naming a declaration recovery may
863        // skip: a consumed context's service, a method, a capability (as a
864        // provider's target, in `given`, or unused for want of one), an actor.
865        "bynk.consumes.unknown_service",
866        "bynk.types.method_not_found",
867        "bynk.provider.unknown_capability",
868        "bynk.given.unknown_capability",
869        "bynk.given.unused_capability",
870        "bynk.actor.unknown_actor",
871    ];
872    // The *subject* each of these diagnostics is about, spelled as the parser
873    // records a broken declaration (`T`, `f`, or a method as `T.m`):
874    // - `unknown type `T``, `unknown name `x``, `unknown function `f``: the
875    //   one backticked name;
876    // - `method `T.m` attached to an unknown type `T``: the type, its last;
877    // - `type `T` has no static method or variant named `m``: the member,
878    //   qualified by its type (`T.m`), its first and last;
879    // - `context `c` has no service named `s``: the service, its last;
880    // - `type `T` has no instance method named `m``: the method, as `T.m`;
881    // - `capability `C` is declared in `given` but never used`: its first;
882    // - any other capability or actor diagnostic: its one backticked name.
883    // Matching only the subject keeps a broken `fn m` from hiding an unrelated
884    // `Other.m` that merely shares the name.
885    let names = |message: &str| -> Vec<String> {
886        message
887            .split('`')
888            .skip(1)
889            .step_by(2)
890            .map(str::to_string)
891            .collect()
892    };
893    let subject = |e: &CompileError| -> Option<String> {
894        let ns = names(&e.message);
895        match e.category {
896            "bynk.resolve.unknown_static_member" | "bynk.types.method_not_found" => {
897                Some(format!("{}.{}", ns.first()?, ns.last()?))
898            }
899            // `capability `C` is declared in `given` but never used`: the
900            // capability comes first (`given` is the keyword, quoted).
901            "bynk.given.unused_capability" => ns.first().cloned(),
902            _ => ns.last().cloned(),
903        }
904    };
905    resolve_errors.into_iter().partition(|e| {
906        !(ECHO_CODES.contains(&e.category)
907            && subject(e).is_some_and(|s| broken_decl_names.contains(&s)))
908    })
909}
910
911/// #1663 (Decision A): the checker's diagnostics after a resolve that reported
912/// errors, keeping only those in declarations the resolver found clean.
913///
914/// Resolve-then-check is per *declaration*: an unknown name in `b` must not
915/// hide a type error in `a`, so the checker runs over every declaration. Inside
916/// a declaration the resolver rejected, the checker would only meet the same
917/// fault again — under the same code (`unknown_function`), under its own twin
918/// code (`types.unknown_static_member`), or as a follow-on (`unknown_name` for a
919/// misplaced `self`, `type_in_expr` beside an unknown variant). The resolver's
920/// report is the one that names the fault, so that declaration keeps only it.
921/// `item_spans` are the unit's top-level declarations' spans
922/// ([`CommonsItem::span`]). A diagnostic outside every declaration is kept
923/// unless it overlaps a resolve error.
924pub fn without_resolve_echoes(
925    checked: Vec<CompileError>,
926    resolve_errors: &[CompileError],
927    item_spans: &[Span],
928) -> Vec<CompileError> {
929    let within = |outer: Span, at: Span| {
930        outer.file == at.file
931            && outer.start <= at.start
932            && at.start < outer.end.max(outer.start + 1)
933    };
934    let rejected: Vec<Span> = item_spans
935        .iter()
936        .copied()
937        .filter(|item| resolve_errors.iter().any(|r| within(*item, r.span)))
938        .collect();
939    checked
940        .into_iter()
941        .filter(|c| {
942            !rejected.iter().any(|item| within(*item, c.span))
943                && !resolve_errors.iter().any(|r| within(r.span, c.span))
944        })
945        .collect()
946}
947
948/// Validate name references inside a single file's items against an
949/// already-built symbol table (`resolved.types`, `resolved.fns`,
950/// `resolved.methods`). Used by the project-level driver after combining
951/// declarations from every file in a multi-file commons and from every
952/// commons brought in by `uses`.
953pub fn resolve_file(resolved: &ResolvedCommons) -> Result<(), Vec<CompileError>> {
954    resolve_file_record(resolved, &mut RefSink::new())
955}
956
957/// [`resolve_file`], recording binding edges into `refs` as the walk
958/// resolves them (v0.25). The project pass sets the sink's per-file context;
959/// a fresh sink records nothing.
960pub fn resolve_file_record(
961    resolved: &ResolvedCommons,
962    refs: &mut RefSink,
963) -> Result<(), Vec<CompileError>> {
964    let mut errors = Vec::new();
965    let mut sinks = Sinks {
966        errs: &mut errors,
967        refs,
968    };
969    for item in &resolved.commons.items {
970        check_reserved_host_name(item, &mut sinks);
971        match item {
972            CommonsItem::Type(t) => {
973                sinks.refs.set_owner(&t.name.name);
974                check_type_decl_refs(t, &resolved.types, &mut sinks);
975            }
976            CommonsItem::Event(e) => {
977                sinks.refs.set_owner(&e.name.name);
978                check_type_decl_refs(&e.as_type_decl(), &resolved.types, &mut sinks);
979            }
980            CommonsItem::Fn(f) => {
981                sinks.refs.set_owner(f.name.display());
982                check_fn_refs(
983                    f,
984                    &resolved.types,
985                    &resolved.fns,
986                    &resolved.methods,
987                    &resolved.assets,
988                    &resolved.embeds,
989                    &resolved.shadowed_imports,
990                    &mut sinks,
991                );
992            }
993            CommonsItem::Capability(_)
994            | CommonsItem::Service(_)
995            | CommonsItem::Agent(_)
996            | CommonsItem::Provider(_)
997            | CommonsItem::Actor(_) => {
998                check_signature_refs(item, &resolved.types, &mut sinks);
999            }
1000            // `messages` entries are plain string literals with no type refs
1001            // to resolve here; commons-only legality and the reference/
1002            // duplicate-code checks live in bynk-emit's project validation.
1003            CommonsItem::Messages(_) | CommonsItem::Asset(_) | CommonsItem::Embed(_) => {}
1004        }
1005        sinks.refs.clear_owner();
1006    }
1007    if errors.is_empty() {
1008        Ok(())
1009    } else {
1010        Err(errors)
1011    }
1012}
1013
1014/// #1679 (runtime-semantics track S12): every type named in a handler or
1015/// capability **signature** must resolve, exactly as in a `fn` signature
1016/// (`bynk.resolve.unknown_type`). Their bodies are resolved by the
1017/// context-level pass, but nothing walked the signatures, so `on call(v:
1018/// Bogus)` and `Effect[Unit]` (Bynk's unit is `()`) were accepted and the
1019/// emitter wrote a `/* unknown */` placeholder. Covered:
1020/// - service and agent handler parameters and return types;
1021/// - an agent's key type and its `store` fields' kind arguments
1022///   (`Cell[T]`, `Map[K, V]`, …);
1023/// - capability operation parameters and return types, with the operation's
1024///   own type parameters in scope;
1025/// - a service header's types: a `from websocket(in: …, out: …)` frame pair
1026///   and a `from events(…)` event type;
1027/// - provider operation parameters and return types;
1028/// - an actor's `identity` type.
1029fn check_signature_refs(
1030    item: &CommonsItem,
1031    types: &HashMap<String, Arc<TypeDecl>>,
1032    sinks: &mut Sinks,
1033) {
1034    let handler = |h: &Handler, sinks: &mut Sinks| {
1035        for p in &h.params {
1036            check_type_ref_resolves(&p.type_ref, types, sinks);
1037        }
1038        check_type_ref_resolves(&h.return_type, types, sinks);
1039    };
1040    match item {
1041        CommonsItem::Service(s) => {
1042            match &s.protocol {
1043                ServiceProtocol::WebSocket { in_type, out_type } => {
1044                    check_type_ref_resolves(in_type, types, sinks);
1045                    check_type_ref_resolves(out_type, types, sinks);
1046                }
1047                ServiceProtocol::Events { event_type, .. } => {
1048                    check_type_ref_resolves(event_type, types, sinks);
1049                }
1050                ServiceProtocol::Call
1051                | ServiceProtocol::Http
1052                | ServiceProtocol::Cron
1053                | ServiceProtocol::Queue { .. } => {}
1054            }
1055            for h in &s.handlers {
1056                handler(h, sinks);
1057            }
1058        }
1059        CommonsItem::Provider(p) => {
1060            for op in &p.ops {
1061                for param in &op.params {
1062                    check_type_ref_resolves(&param.type_ref, types, sinks);
1063                }
1064                check_type_ref_resolves(&op.return_type, types, sinks);
1065            }
1066        }
1067        CommonsItem::Actor(a) => {
1068            if let Some(identity) = &a.identity {
1069                check_type_ref_resolves(identity, types, sinks);
1070            }
1071        }
1072        CommonsItem::Agent(a) => {
1073            check_type_ref_resolves(&a.key_type, types, sinks);
1074            for f in &a.store_fields {
1075                for arg in &f.kind.args {
1076                    check_type_ref_resolves(arg, types, sinks);
1077                }
1078            }
1079            for h in &a.handlers {
1080                handler(h, sinks);
1081            }
1082        }
1083        CommonsItem::Capability(c) => {
1084            for op in &c.ops {
1085                let type_params: HashSet<String> = op
1086                    .type_params
1087                    .iter()
1088                    .map(|tp| tp.name.name.clone())
1089                    .collect();
1090                for p in &op.params {
1091                    check_type_ref_resolves_in(&p.type_ref, types, &type_params, sinks);
1092                }
1093                check_type_ref_resolves_in(&op.return_type, types, &type_params, sinks);
1094            }
1095        }
1096        CommonsItem::Type(_)
1097        | CommonsItem::Event(_)
1098        | CommonsItem::Fn(_)
1099        | CommonsItem::Messages(_)
1100        | CommonsItem::Asset(_)
1101        | CommonsItem::Embed(_) => {}
1102    }
1103}
1104
1105/// v0.157 (ADR 0183): the name a record field *directly contains* — a top-level
1106/// `Named` (`f: A`) or a generic application (`f: A[T]`). Both are direct
1107/// containment edges for the cycle guards; a `List[…]`/`Option[…]` wrapper is
1108/// not (its empty/`None` inhabitant breaks the cycle).
1109fn direct_record_head(tr: &TypeRef) -> Option<&str> {
1110    match tr {
1111        TypeRef::Named(id) => Some(&id.name),
1112        TypeRef::App { name, .. } => Some(&name.name),
1113        _ => None,
1114    }
1115}
1116
1117/// Whether `target` is reachable from `start` over direct record-field edges
1118/// (bare `Named` or generic `App` heads) — the record-containment graph. Used
1119/// to reject indirect record cycles (`A = { b: B }`, `B = { a: A }`); a
1120/// `visited` set bounds the walk on graphs that already contain cycles
1121/// elsewhere.
1122fn record_field_reaches(start: &str, target: &str, types: &HashMap<String, Arc<TypeDecl>>) -> bool {
1123    let mut visited: HashSet<String> = HashSet::new();
1124    let mut stack = vec![start.to_string()];
1125    while let Some(name) = stack.pop() {
1126        if name == target {
1127            return true;
1128        }
1129        if !visited.insert(name.clone()) {
1130            continue;
1131        }
1132        if let Some(decl) = types.get(&name)
1133            && let TypeBody::Record(r) = &decl.body
1134        {
1135            for f in &r.fields {
1136                if let Some(head) = direct_record_head(&f.type_ref) {
1137                    stack.push(head.to_string());
1138                }
1139            }
1140        }
1141    }
1142    false
1143}
1144
1145/// v0.157 (ADR 0183): reject a repeated type-parameter name — a duplicate would
1146/// collapse silently in the substitution map (the later argument winning), so a
1147/// `Pair[T, T]` mis-checks its fields. Shared by `type` and `fn` declarations.
1148fn check_duplicate_type_params(params: &[TypeParam], owner: &str, errors: &mut Sinks) {
1149    let mut seen: HashMap<&str, bynk_syntax::span::Span> = HashMap::new();
1150    for tp in params {
1151        if let Some(prev) = seen.get(tp.name.name.as_str()) {
1152            errors.push(
1153                CompileError::new(
1154                    "bynk.generics.duplicate_type_param",
1155                    tp.span,
1156                    format!(
1157                        "type parameter `{}` is declared more than once on {owner}",
1158                        tp.name.name
1159                    ),
1160                )
1161                .with_label(*prev, "previously declared here"),
1162            );
1163        } else {
1164            seen.insert(tp.name.name.as_str(), tp.span);
1165        }
1166    }
1167}
1168
1169/// #1653: the generated TypeScript reaches host globals as `globalThis.<name>`,
1170/// so a module-scope declaration of that name (a type, function, agent,
1171/// provider, …) would hide every one of them in its module. Parameters and
1172/// locals are renamed by the emitter instead; a declaration is rejected.
1173fn check_reserved_host_name(item: &CommonsItem, errors: &mut Sinks) {
1174    if let Some(name) = item.name()
1175        && name.name == "globalThis"
1176    {
1177        errors.push(
1178            CompileError::new(
1179                "bynk.resolve.reserved_host_name",
1180                name.span,
1181                "`globalThis` cannot be used as a declaration name",
1182            )
1183            .with_note(
1184                "the generated TypeScript uses `globalThis` to reach the host's built-in objects; rename the declaration",
1185            ),
1186        );
1187    }
1188}
1189
1190/// Recursively walk a type declaration to check that every type reference
1191/// inside it resolves.
1192fn check_type_decl_refs(t: &TypeDecl, types: &HashMap<String, Arc<TypeDecl>>, errors: &mut Sinks) {
1193    // A `type` declaration may not reuse a compiler-known built-in type name
1194    // (`List`, `Map`, `Query`, …). Those names are dispatched on by the type
1195    // parser (`parser/types.rs`), so any *reference* to the alias would be
1196    // intercepted as the built-in — the declaration would be silently shadowed
1197    // (`QueueResult`) or fail with an incoherent message at the use site. Reject
1198    // it here, at the declaration, with a message the user can act on. Base
1199    // types and other reserved *keywords* (`Int`, `Result`, …) are already
1200    // rejected earlier, by `expect_ident` at parse time.
1201    if bynk_syntax::keywords::is_builtin_type_name(&t.name.name) {
1202        errors.push(
1203            CompileError::new(
1204                "bynk.resolve.reserved_builtin_type",
1205                t.name.span,
1206                format!(
1207                    "`{}` is a built-in type name and cannot be redeclared",
1208                    t.name.name
1209                ),
1210            )
1211            .with_note("rename the type — built-in type names are reserved in type position"),
1212        );
1213    }
1214    // v0.157 (ADR 0183): a record body may be generic. #593: a sum body may too
1215    // — its variant payloads resolve the parameters as rigid vars, exactly as
1216    // record fields do. Type parameters on a refined / opaque body are still
1217    // rejected; a parameter shadowing a declared type is diagnosed (mirrors the
1218    // function-generics rule).
1219    let type_params: HashSet<String> = t.type_params.iter().map(|p| p.name.name.clone()).collect();
1220    if !t.type_params.is_empty() {
1221        check_duplicate_type_params(&t.type_params, &format!("type `{}`", t.name.name), errors);
1222        if !matches!(t.body, TypeBody::Record(_) | TypeBody::Sum(_)) {
1223            errors.push(
1224                CompileError::new(
1225                    "bynk.generics.generic_non_record",
1226                    t.type_params[0].span,
1227                    format!(
1228                        "type `{}` declares type parameters, but only a record (`{{ … }}`) or sum (`| … | …`) type may be generic",
1229                        t.name.name
1230                    ),
1231                )
1232                .with_note("refined and opaque types cannot be generic — their base is a fixed primitive"),
1233            );
1234        }
1235        // #593: a generic sum may not carry an `embeds` clause. Embedding folds
1236        // another sum's variants in by name; composing that with per-parameter
1237        // substitution (the embedded source could itself be generic, or mention
1238        // the host's parameters) is out of scope for this increment.
1239        if let TypeBody::Sum(s) = &t.body
1240            && let Some(clause) = s.embeds.first()
1241        {
1242            errors.push(
1243                CompileError::new(
1244                    "bynk.generics.generic_sum_embeds",
1245                    clause.span,
1246                    format!("generic sum `{}` cannot use an `embeds` clause", t.name.name),
1247                )
1248                .with_note("embedding into a generic sum is not supported — declare the variants directly, or make the sum non-generic"),
1249            );
1250        }
1251        for tp in &t.type_params {
1252            if types.contains_key(&tp.name.name) {
1253                errors.push(
1254                    CompileError::new(
1255                        "bynk.generics.type_arg_mismatch",
1256                        tp.span,
1257                        format!(
1258                            "type parameter `{}` shadows the declared type of the same name",
1259                            tp.name.name
1260                        ),
1261                    )
1262                    .with_note("rename the type parameter"),
1263                );
1264            }
1265        }
1266    }
1267    match &t.body {
1268        TypeBody::Refined { .. } => {
1269            // Refined-type bodies only reference base types directly.
1270        }
1271        TypeBody::Opaque { .. } => {
1272            // Opaque-type bodies only reference base types directly.
1273        }
1274        TypeBody::Record(r) => {
1275            let mut seen = HashMap::new();
1276            for f in &r.fields {
1277                if let Some(prev_span) = seen.get(&f.name.name) {
1278                    errors.push(
1279                        CompileError::new(
1280                            "bynk.resolve.duplicate_field",
1281                            f.name.span,
1282                            format!("field `{}` is declared more than once", f.name.name),
1283                        )
1284                        .with_label(*prev_span, "previously declared here"),
1285                    );
1286                } else {
1287                    seen.insert(f.name.name.clone(), f.name.span);
1288                }
1289                // Detect containment cycles: a direct `type A = { f: A }`,
1290                // and indirect cycles through direct record fields
1291                // (`A = { b: B }`, `B = { a: A }`). Such a cycle admits no finite
1292                // value, and defeats every structural walk downstream (zero-value
1293                // emission, codecs). A `List[...]`/`Option[...]` wrapper (whose
1294                // empty/`None` inhabitant breaks the cycle) is not a direct edge.
1295                // v0.157 (ADR 0183): a generic self-reference `f: A[T]` is a
1296                // `TypeRef::App` direct edge — caught here in the checker (and so
1297                // in the standalone LSP), not only by the emit-side boundary pass.
1298                if let Some(head) = direct_record_head(&f.type_ref) {
1299                    if head == t.name.name {
1300                        errors.push(
1301                            CompileError::new(
1302                                "bynk.resolve.recursive_record_field",
1303                                f.name.span,
1304                                format!(
1305                                    "record `{}` cannot directly contain a field of its own type",
1306                                    t.name.name
1307                                ),
1308                            )
1309                            .with_label(t.name.span, "type declared here")
1310                            .with_note(
1311                                "wrap the recursive reference in `Option[...]` to break the cycle",
1312                            ),
1313                        );
1314                    } else if record_field_reaches(head, &t.name.name, types) {
1315                        errors.push(
1316                            CompileError::new(
1317                                "bynk.resolve.recursive_record_field",
1318                                f.name.span,
1319                                format!(
1320                                    "record `{}` contains itself through this field — `{}` leads back to `{}`",
1321                                    t.name.name, head, t.name.name
1322                                ),
1323                            )
1324                            .with_label(t.name.span, "type declared here")
1325                            .with_note(
1326                                "wrap one field in the cycle in `Option[...]` to break it",
1327                            ),
1328                        );
1329                    }
1330                }
1331                check_type_ref_resolves_in(&f.type_ref, types, &type_params, errors);
1332            }
1333        }
1334        TypeBody::Sum(s) => {
1335            let mut seen = HashMap::new();
1336            for v in &s.variants {
1337                if let Some(prev_span) = seen.get(&v.name.name) {
1338                    errors.push(
1339                        CompileError::new(
1340                            "bynk.resolve.duplicate_variant",
1341                            v.name.span,
1342                            format!("variant `{}` is declared more than once", v.name.name),
1343                        )
1344                        .with_label(*prev_span, "previously declared here"),
1345                    );
1346                } else {
1347                    seen.insert(v.name.name.clone(), v.name.span);
1348                }
1349                let mut payload_seen = HashMap::new();
1350                for f in &v.payload {
1351                    if let Some(prev) = payload_seen.get(&f.name.name) {
1352                        errors.push(
1353                            CompileError::new(
1354                                "bynk.resolve.duplicate_field",
1355                                f.name.span,
1356                                format!(
1357                                    "payload field `{}` is declared more than once in variant `{}`",
1358                                    f.name.name, v.name.name
1359                                ),
1360                            )
1361                            .with_label(*prev, "previously declared here"),
1362                        );
1363                    } else {
1364                        payload_seen.insert(f.name.name.clone(), f.name.span);
1365                    }
1366                    // #1653: a variant is a flat `{ "kind": "<Variant>", ... }`
1367                    // object on the wire, so a payload field named `kind` would
1368                    // collide with the discriminant itself.
1369                    if f.name.name == "kind" {
1370                        errors.push(
1371                            CompileError::new(
1372                                "bynk.resolve.reserved_payload_field",
1373                                f.name.span,
1374                                format!(
1375                                    "variant `{}` cannot have a payload field named `kind`",
1376                                    v.name.name
1377                                ),
1378                            )
1379                            .with_note(
1380                                "`kind` carries the variant's name when a sum is encoded as JSON; rename the field (e.g. `category`)",
1381                            ),
1382                        );
1383                    }
1384                    // #593: a generic sum's declared type parameters are in scope
1385                    // in its variant payloads, resolving as rigid vars (empty set
1386                    // for a non-generic sum — the same reference walk as before).
1387                    check_type_ref_resolves_in(&f.type_ref, types, &type_params, errors);
1388                }
1389            }
1390            // v0.154 (ADR 0178): the `embeds E as V` clauses' source types must
1391            // resolve (the target variant is checked in `check_embeds`).
1392            for clause in &s.embeds {
1393                check_type_ref_resolves(&clause.source_type, types, errors);
1394            }
1395        }
1396    }
1397}
1398
1399#[allow(clippy::too_many_arguments)]
1400fn check_fn_refs(
1401    f: &FnDecl,
1402    types: &HashMap<String, Arc<TypeDecl>>,
1403    fns: &HashMap<String, Arc<FnDecl>>,
1404    methods: &HashMap<String, MethodTable>,
1405    assets: &HashMap<String, AssetDecl>,
1406    embeds: &HashMap<String, EmbedDecl>,
1407    shadowed: &HashMap<String, ShadowedImport>,
1408    errors: &mut Sinks,
1409) {
1410    // Parameter types resolve.
1411    // v0.20a: the fn's type parameters are legal named references in its
1412    // own signature and body annotations.
1413    let mut type_params: HashSet<String> = f
1414        .type_params
1415        .iter()
1416        .map(|tp| tp.name.name.clone())
1417        .collect();
1418    check_duplicate_type_params(
1419        &f.type_params,
1420        &format!("function `{}`", f.name.display()),
1421        errors,
1422    );
1423    // #594: an instance method on a generic type inherits the receiver type's
1424    // parameters into scope, so `fn Box.map[U](self, f: A -> U) -> Box[U]` may
1425    // name the type's own parameter `A` alongside the method's `U`. A method
1426    // parameter that reuses one of the type's parameter names would shadow it
1427    // ambiguously in the substitution — diagnose the collision.
1428    if let FnName::Method { type_name, .. } = &f.name
1429        && let Some(recv) = types.get(&type_name.name)
1430    {
1431        for tp in &recv.type_params {
1432            if type_params.contains(&tp.name.name) {
1433                errors.push(
1434                    CompileError::new(
1435                        "bynk.generics.duplicate_type_param",
1436                        f.type_params
1437                            .iter()
1438                            .find(|mp| mp.name.name == tp.name.name)
1439                            .map_or(tp.span, |mp| mp.span),
1440                        format!(
1441                            "type parameter `{}` is already a parameter of the receiver type `{}`",
1442                            tp.name.name, type_name.name
1443                        ),
1444                    )
1445                    .with_label(tp.span, "declared on the type here"),
1446                );
1447            }
1448            type_params.insert(tp.name.name.clone());
1449        }
1450    }
1451    let mut seen_params: HashMap<&str, &Ident> = HashMap::new();
1452    for p in &f.params {
1453        check_type_ref_resolves_in(&p.type_ref, types, &type_params, errors);
1454        if let Some(prev) = seen_params.get(p.name.name.as_str()) {
1455            errors.push(
1456                CompileError::new(
1457                    "bynk.resolve.duplicate_param",
1458                    p.name.span,
1459                    format!("parameter `{}` is declared more than once", p.name.name),
1460                )
1461                .with_label(prev.span, "previously declared here"),
1462            );
1463        } else {
1464            seen_params.insert(p.name.name.as_str(), &p.name);
1465        }
1466    }
1467    check_type_ref_resolves_in(&f.return_type, types, &type_params, errors);
1468
1469    // Build the initial scope: parameters plus `self` (for instance methods).
1470    let mut params: HashMap<String, ()> =
1471        f.params.iter().map(|p| (p.name.name.clone(), ())).collect();
1472    if f.has_self {
1473        params.insert("self".to_string(), ());
1474    }
1475    let in_method = matches!(f.name, FnName::Method { .. });
1476    let mut cx = RefCheckCtx {
1477        params: &params,
1478        in_method,
1479        types,
1480        type_params: &type_params,
1481        fns,
1482        methods,
1483        assets,
1484        embeds,
1485        shadowed,
1486        scopes: Vec::new(),
1487        errors,
1488    };
1489    check_block_references(&f.body, &mut cx);
1490}
1491
1492/// #1807, #1814: the walk the naming gates share: every place source writes
1493/// a type name, reporting each one that names a hidden type. See
1494/// [`check_hidden_type_names`] for what is walked.
1495struct Gate<'a> {
1496    /// Each gated type name, with its owning unit.
1497    hidden: &'a std::collections::BTreeMap<String, String>,
1498    /// #1814: the error for a gated name written at `id`, given its owner. A
1499    /// unit and a suite name the `uses` that would admit a hidden type
1500    /// differently; a shadowed import is a conflict instead (#1927).
1501    report: &'a dyn Fn(&Ident, &str) -> CompileError,
1502    /// #1927: each bare variant whose construction is gated, with its sum's
1503    /// name. Empty for #1807's hidden types, whose bare variants are values
1504    /// typed by their position; a shadowed sum's construction emits its type's
1505    /// name, which the local declaration has taken.
1506    variants: std::collections::BTreeMap<String, String>,
1507    /// #1927: for a shadowed type, the local declaration of each name, which
1508    /// answers a receiver it can serve (`svc.GET(…)` in a suite, `logo.url`).
1509    /// `None` for #1807's hidden types, which nothing local shares a name with.
1510    locals: Option<&'a HashMap<String, ShadowedImport>>,
1511    errors: Vec<CompileError>,
1512}
1513impl Gate<'_> {
1514    /// #1927: whether the receiver `id` of a method call (`method`) or a
1515    /// field read (`field`) is the local declaration that hides the type:
1516    /// a service addressed in a suite, or an asset's `url`.
1517    fn local_receiver(&self, id: &Ident, method: Option<&Ident>, field: Option<&Ident>) -> bool {
1518        let Some(s) = self.locals.and_then(|l| l.get(&id.name)) else {
1519            return false;
1520        };
1521        match s.local {
1522            "service" => method.is_some(),
1523            "asset" => field.is_some_and(|f| f.name == "url"),
1524            _ => false,
1525        }
1526    }
1527    fn name(&mut self, id: &Ident) {
1528        if let Some(owner) = self.hidden.get(&id.name) {
1529            self.errors.push((self.report)(id, owner));
1530        }
1531    }
1532    /// #1927: a bare variant constructed (`Done`, `Done(x)`) of a gated sum.
1533    /// It names the sum in the emitted module (`Status.Done`), so it is the
1534    /// conflict; the error says so, since matching the same variant is legal.
1535    fn bare_variant(&mut self, id: &Ident) {
1536        let Some(sum) = self.variants.get(&id.name) else {
1537            return;
1538        };
1539        let Some(s) = self.locals.and_then(|l| l.get(sum)) else {
1540            return;
1541        };
1542        self.errors.push(
1543            CompileError::new(
1544                "bynk.uses.name_conflict",
1545                id.span,
1546                format!(
1547                    "`{}` constructs a variant of `{sum}` from `{}`, which this unit's {} `{sum}` hides",
1548                    id.name, s.unit, s.local
1549                ),
1550            )
1551            .with_label(s.local_span, format!("{} declared here", s.local))
1552            .with_note(format!(
1553                "constructing a variant bare still names its type (`{sum}.{}`); match on it instead, or rename one of the two",
1554                id.name
1555            )),
1556        );
1557    }
1558    fn type_ref(&mut self, r: &TypeRef) {
1559        match r {
1560            TypeRef::Named(id) => self.name(id),
1561            TypeRef::App { name, args, .. } => {
1562                self.name(name);
1563                for a in args {
1564                    self.type_ref(a);
1565                }
1566            }
1567            TypeRef::Result(a, b, _) | TypeRef::Map(a, b, _) => {
1568                self.type_ref(a);
1569                self.type_ref(b);
1570            }
1571            TypeRef::Option(t, _)
1572            | TypeRef::Effect(t, _)
1573            | TypeRef::HttpResult(t, _)
1574            | TypeRef::List(t, _)
1575            | TypeRef::Query(t, _)
1576            | TypeRef::Stream(t, _)
1577            | TypeRef::Connection(t, _)
1578            | TypeRef::History(t, _) => self.type_ref(t),
1579            TypeRef::Fn(params, ret, _) => {
1580                for p in params {
1581                    self.type_ref(p);
1582                }
1583                self.type_ref(ret);
1584            }
1585            TypeRef::Base(..)
1586            | TypeRef::QueueResult(_)
1587            | TypeRef::Asset(_)
1588            | TypeRef::RequestHeaders(_)
1589            | TypeRef::TokenClaims(_)
1590            | TypeRef::BoundaryRejection(_)
1591            | TypeRef::ValidationError(_)
1592            | TypeRef::JsonError(_)
1593            | TypeRef::Unit(_) => {}
1594        }
1595    }
1596    fn type_decl(&mut self, t: &TypeDecl) {
1597        match &t.body {
1598            TypeBody::Record(r) => {
1599                for f in &r.fields {
1600                    self.type_ref(&f.type_ref);
1601                    // Review of #1813: an `event` field default.
1602                    if let Some(init) = &f.init {
1603                        self.expr(init);
1604                    }
1605                }
1606            }
1607            TypeBody::Sum(s) => {
1608                for v in &s.variants {
1609                    for p in &v.payload {
1610                        self.type_ref(&p.type_ref);
1611                    }
1612                }
1613            }
1614            TypeBody::Refined { .. } | TypeBody::Opaque { .. } => {}
1615        }
1616    }
1617    fn signature(&mut self, params: &[Param], ret: &TypeRef) {
1618        for p in params {
1619            self.type_ref(&p.type_ref);
1620        }
1621        self.type_ref(ret);
1622    }
1623    fn pattern(&mut self, p: &Pattern) {
1624        match p {
1625            Pattern::Variant {
1626                type_name,
1627                variant,
1628                bindings,
1629                ..
1630            } => {
1631                if let Some(tn) = type_name {
1632                    self.name(tn);
1633                } else if bindings.is_empty() {
1634                    // A bare, payload-less pattern may name a refined type
1635                    // rather than a variant: `s is Repo` tests a refinement.
1636                    self.name(variant);
1637                }
1638                for b in bindings {
1639                    match &b.kind {
1640                        PatternBindingKind::Positional { pattern }
1641                        | PatternBindingKind::Named { pattern, .. } => self.pattern(pattern),
1642                    }
1643                }
1644            }
1645            Pattern::Refined { inner, .. } => self.pattern(inner),
1646            Pattern::Or(alts, _) => {
1647                for a in alts {
1648                    self.pattern(a);
1649                }
1650            }
1651            Pattern::Wildcard(_) | Pattern::Binding(_) | Pattern::Literal { .. } => {}
1652        }
1653    }
1654    fn block(&mut self, b: &Block) {
1655        for s in &b.statements {
1656            if let Statement::Let(l) | Statement::EffectLet(l) = s
1657                && let Some(t) = &l.type_annot
1658            {
1659                self.type_ref(t);
1660            }
1661        }
1662        // `expr_children` of a block expression is exactly its statements'
1663        // expressions and its tail.
1664        for e in &b.statements {
1665            let mut exprs = Vec::new();
1666            statement_exprs(e, &mut exprs);
1667            for x in exprs {
1668                self.expr(x);
1669            }
1670        }
1671        self.expr(&b.tail);
1672    }
1673    fn expr(&mut self, e: &Expr) {
1674        match &e.kind {
1675            ExprKind::Block(b) => return self.block(b),
1676            ExprKind::If {
1677                cond,
1678                then_block,
1679                else_block,
1680            } => {
1681                self.expr(cond);
1682                self.block(then_block);
1683                return self.block(else_block);
1684            }
1685            ExprKind::Match { discriminant, arms } => {
1686                self.expr(discriminant);
1687                for arm in arms {
1688                    self.pattern(&arm.pattern);
1689                    if let Some(g) = &arm.guard {
1690                        self.expr(g);
1691                    }
1692                    match &arm.body {
1693                        MatchBody::Expr(b) => self.expr(b),
1694                        MatchBody::Block(b) => self.block(b),
1695                    }
1696                }
1697                return;
1698            }
1699            ExprKind::Is { pattern, .. } => self.pattern(pattern),
1700            ExprKind::RecordConstruction { type_name, .. } => self.name(type_name),
1701            ExprKind::RecordSpread {
1702                type_name: Some(tn),
1703                ..
1704            } => self.name(tn),
1705            ExprKind::FieldAccess { receiver, field } => {
1706                if let ExprKind::Ident(id) = &receiver.kind
1707                    && !self.local_receiver(id, None, Some(field))
1708                {
1709                    self.name(id);
1710                }
1711            }
1712            ExprKind::MethodCall {
1713                receiver,
1714                method,
1715                type_args,
1716                ..
1717            } => {
1718                if let ExprKind::Ident(id) = &receiver.kind
1719                    && !self.local_receiver(id, Some(method), None)
1720                {
1721                    self.name(id);
1722                }
1723                for t in type_args {
1724                    self.type_ref(t);
1725                }
1726            }
1727            ExprKind::Call {
1728                name, type_args, ..
1729            } => {
1730                self.bare_variant(name);
1731                for t in type_args {
1732                    self.type_ref(t);
1733                }
1734            }
1735            ExprKind::Ident(id) => self.bare_variant(id),
1736            ExprKind::Val { type_ref, .. } => self.type_ref(type_ref),
1737            ExprKind::Lambda(l) => {
1738                for p in &l.params {
1739                    if let Some(t) = &p.type_ref {
1740                        self.type_ref(t);
1741                    }
1742                }
1743            }
1744            _ => {}
1745        }
1746        for c in expr_children(e) {
1747            self.expr(c);
1748        }
1749    }
1750}
1751
1752/// #1807: the naming gate for *hidden* types. A hidden type is one this unit's
1753/// composed table carries only because an imported declaration reaches it
1754/// (`crate::project_model::close_reachable_types`): resolvable here, never
1755/// nameable, since `uses` is one level (ADR 0278). Every place the unit's
1756/// source writes a type name is walked: type references in declarations,
1757/// signatures, `let` annotations, lambda parameters and type arguments; a
1758/// record, constructor or spread's type; a type used as a receiver
1759/// (`Repo.of(…)`, `Status.Pending`); a method declared on a type; a
1760/// pattern's qualifier; and a refinement test (`s is Repo`). Bodies are
1761/// walked, and so are an agent's invariants, transitions and `store`
1762/// initialisers, and an `event` field's default. A hidden name in any of them is
1763/// `bynk.resolve.unknown_type`, the error the name gave before the closure
1764/// existed, with a note naming the commons to `uses`.
1765///
1766/// A bare variant (`Pending`) is a value, not a type name, so it is not gated:
1767/// like a literal admitted as a hidden refined type, it is typed by the
1768/// position it fills. (A context still cannot construct one: the rebrand
1769/// construction check covers a hidden commons sum like a `uses`d one.)
1770pub fn check_hidden_type_names(
1771    items: &[CommonsItem],
1772    hidden: &std::collections::BTreeMap<String, String>,
1773) -> Vec<CompileError> {
1774    let report = |id: &Ident, owner: &str| {
1775        hidden_type_error(
1776            id,
1777            format!(
1778                "`{}` reaches this unit only through an imported declaration. It is \
1779                 declared in `{owner}`, which this unit does not `uses`; add `uses \
1780                 {owner}` to name it",
1781                id.name
1782            ),
1783        )
1784    };
1785    let mut g = Gate {
1786        hidden,
1787        report: &report,
1788        variants: Default::default(),
1789        locals: None,
1790        errors: Vec::new(),
1791    };
1792    gate_items(&mut g, items);
1793    g.errors
1794}
1795
1796/// `bynk.resolve.unknown_type` for a hidden type written at `id`. Not
1797/// `unknown_type_error`: its note lists what is in scope, which a hidden
1798/// type, reached but unnameable, is not.
1799fn hidden_type_error(id: &Ident, note: String) -> CompileError {
1800    CompileError::new(
1801        "bynk.resolve.unknown_type",
1802        id.span,
1803        format!("unknown type `{}`", id.name),
1804    )
1805    .with_note(note)
1806}
1807
1808/// #1927: the naming gate for a `uses`-imported type the unit hides behind
1809/// its own declaration of another kind (a function, asset, service, agent or
1810/// provider: `crate::project_model::shadowed_imports`). The type stays in the
1811/// unit's table, so an imported declaration that uses it still checks, and a
1812/// value of it still flows through the unit's code. But the emitted module
1813/// binds the name to the local declaration, so the unit may not write it: every
1814/// position [`check_hidden_type_names`] gates is `bynk.uses.name_conflict`
1815/// here, and so is constructing one of the sum's variants bare (`Done`), which
1816/// emits the type's name too. Matching a variant does not.
1817pub fn check_shadowed_type_names(
1818    items: &[CommonsItem],
1819    shadowed: &HashMap<String, ShadowedImport>,
1820    types: &HashMap<String, Arc<TypeDecl>>,
1821) -> Vec<CompileError> {
1822    let (gated, variants) = shadowed_type_gate(shadowed, types);
1823    let report = |id: &Ident, _owner: &str| {
1824        let s = &shadowed[&id.name];
1825        shadowed_import_error(&id.name, id.span, s)
1826    };
1827    let mut g = Gate {
1828        hidden: &gated,
1829        report: &report,
1830        variants,
1831        locals: Some(shadowed),
1832        errors: Vec::new(),
1833    };
1834    gate_items(&mut g, items);
1835    g.errors
1836}
1837
1838/// #1927: the shadowed imported types, by name with their owning unit, and
1839/// each bare variant of a shadowed sum, with the sum's name.
1840fn shadowed_type_gate(
1841    shadowed: &HashMap<String, ShadowedImport>,
1842    types: &HashMap<String, Arc<TypeDecl>>,
1843) -> (
1844    std::collections::BTreeMap<String, String>,
1845    std::collections::BTreeMap<String, String>,
1846) {
1847    let mut gated = std::collections::BTreeMap::new();
1848    let mut variants = std::collections::BTreeMap::new();
1849    for (name, s) in shadowed.iter().filter(|(_, s)| s.imported == "type") {
1850        gated.insert(name.clone(), s.unit.clone());
1851        if let Some(TypeBody::Sum(sum)) = types.get(name).map(|t| &t.body) {
1852            for v in &sum.variants {
1853                variants.insert(v.name.name.clone(), name.clone());
1854            }
1855        }
1856    }
1857    (gated, variants)
1858}
1859
1860/// Walk every item of a unit through `g`.
1861fn gate_items(g: &mut Gate<'_>, items: &[CommonsItem]) {
1862    if g.hidden.is_empty() {
1863        return;
1864    }
1865    for item in items {
1866        match item {
1867            CommonsItem::Type(t) => g.type_decl(t),
1868            CommonsItem::Event(e) => g.type_decl(&e.as_type_decl()),
1869            CommonsItem::Fn(f) => {
1870                if let FnName::Method { type_name, .. } = &f.name {
1871                    g.name(type_name);
1872                }
1873                g.signature(&f.params, &f.return_type);
1874                g.block(&f.body);
1875            }
1876            CommonsItem::Capability(c) => {
1877                for op in &c.ops {
1878                    g.signature(&op.params, &op.return_type);
1879                }
1880            }
1881            CommonsItem::Provider(p) => {
1882                for op in &p.ops {
1883                    g.signature(&op.params, &op.return_type);
1884                    g.block(&op.body);
1885                }
1886            }
1887            CommonsItem::Service(s) => {
1888                match &s.protocol {
1889                    ServiceProtocol::WebSocket { in_type, out_type } => {
1890                        g.type_ref(in_type);
1891                        g.type_ref(out_type);
1892                    }
1893                    ServiceProtocol::Events { event_type, .. } => g.type_ref(event_type),
1894                    ServiceProtocol::Call
1895                    | ServiceProtocol::Http
1896                    | ServiceProtocol::Cron
1897                    | ServiceProtocol::Queue { .. } => {}
1898                }
1899                for h in &s.handlers {
1900                    g.signature(&h.params, &h.return_type);
1901                    g.block(&h.body);
1902                }
1903            }
1904            CommonsItem::Agent(a) => {
1905                g.type_ref(&a.key_type);
1906                for f in &a.store_fields {
1907                    for arg in &f.kind.args {
1908                        g.type_ref(arg);
1909                    }
1910                    if let Some(init) = &f.init {
1911                        g.expr(init);
1912                    }
1913                }
1914                for h in &a.handlers {
1915                    g.signature(&h.params, &h.return_type);
1916                    g.block(&h.body);
1917                }
1918                for inv in &a.invariants {
1919                    g.expr(&inv.predicate);
1920                }
1921                for t in &a.transitions {
1922                    g.expr(&t.predicate);
1923                }
1924            }
1925            CommonsItem::Actor(a) => {
1926                if let Some(id) = &a.identity {
1927                    g.type_ref(id);
1928                }
1929            }
1930            CommonsItem::Messages(_) | CommonsItem::Asset(_) | CommonsItem::Embed(_) => {}
1931        }
1932    }
1933}
1934
1935/// #1814: the naming gate for a test suite's view. A suite resolves names in
1936/// its target's privileged view (a unit suite) or its harness table (a
1937/// `system` suite), each closed over the types its imported declarations reach
1938/// (`crate::project_model::close_view_types`). A hidden type is resolvable
1939/// there, so a case can build and read a reached field, but a suite may no
1940/// more name it than its target may. Walks every `stub` clause's argument
1941/// patterns and values, every case body, and every property's `for all`
1942/// bindings, `where` filter and predicate, as [`check_hidden_type_names`]
1943/// walks a unit's items. `target` is the unit whose `uses` would admit a
1944/// hidden name: the target of a unit suite, or the suite itself (`None`) for a
1945/// `system` suite, which declares its own `uses`.
1946pub fn check_hidden_type_names_in_suite(
1947    suite: &SuiteDecl,
1948    target: Option<&str>,
1949    hidden: &std::collections::BTreeMap<String, String>,
1950) -> Vec<CompileError> {
1951    let report = |id: &Ident, owner: &str| {
1952        let name = &id.name;
1953        hidden_type_error(
1954            id,
1955            match target {
1956                Some(target) => format!(
1957                    "`{name}` reaches this suite only through an imported declaration. It is \
1958                     declared in `{owner}`, which `{target}` does not `uses`; add `uses {owner}` \
1959                     to `{target}` to name it"
1960                ),
1961                None => format!(
1962                    "`{name}` reaches this suite only through an imported declaration. It is \
1963                     declared in `{owner}`, which this suite does not `uses`; add `uses {owner}` \
1964                     to name it"
1965                ),
1966            },
1967        )
1968    };
1969    let mut g = Gate {
1970        hidden,
1971        report: &report,
1972        variants: Default::default(),
1973        locals: None,
1974        errors: Vec::new(),
1975    };
1976    gate_suite(&mut g, suite);
1977    g.errors
1978}
1979
1980/// #1927: [`check_shadowed_type_names`] for a unit suite: a case may no more
1981/// write a type its target hides than the target may.
1982pub fn check_shadowed_type_names_in_suite(
1983    suite: &SuiteDecl,
1984    shadowed: &HashMap<String, ShadowedImport>,
1985    types: &HashMap<String, Arc<TypeDecl>>,
1986) -> Vec<CompileError> {
1987    let (gated, variants) = shadowed_type_gate(shadowed, types);
1988    let report =
1989        |id: &Ident, _owner: &str| shadowed_import_error(&id.name, id.span, &shadowed[&id.name]);
1990    let mut g = Gate {
1991        hidden: &gated,
1992        report: &report,
1993        variants,
1994        locals: Some(shadowed),
1995        errors: Vec::new(),
1996    };
1997    gate_suite(&mut g, suite);
1998    g.errors
1999}
2000
2001/// Walk every stub, case and property of `suite` through `g`.
2002fn gate_suite(g: &mut Gate<'_>, suite: &SuiteDecl) {
2003    if g.hidden.is_empty() {
2004        return;
2005    }
2006    let stub = |g: &mut Gate<'_>, s: &StubClause| {
2007        for a in &s.args {
2008            if let ArgPattern::Value(e) = a {
2009                g.expr(e);
2010            }
2011        }
2012        match &s.rhs {
2013            StubRhs::Returns(e) => g.expr(e),
2014            StubRhs::ReturnsEach(outcomes, _) => {
2015                for o in outcomes {
2016                    if let SeqOutcome::Value(e) = o {
2017                        g.expr(e);
2018                    }
2019                }
2020            }
2021            StubRhs::Fails(_) => {}
2022        }
2023    };
2024    for s in &suite.stubs {
2025        stub(g, s);
2026    }
2027    for case in &suite.cases {
2028        for s in &case.stubs {
2029            stub(g, s);
2030        }
2031        g.block(&case.body);
2032    }
2033    for prop in &suite.properties {
2034        for b in &prop.forall.bindings {
2035            g.type_ref(&b.type_ref);
2036        }
2037        if let Some(w) = &prop.forall.where_pred {
2038            g.expr(w);
2039        }
2040        g.block(&prop.forall.body);
2041    }
2042}
2043
2044fn unknown_type_error(id: &Ident) -> CompileError {
2045    CompileError::new(
2046        "bynk.resolve.unknown_type",
2047        id.span,
2048        format!("unknown type `{}`", id.name),
2049    )
2050    .with_note(
2051        "in scope are the base types (`Int`, `Float`, `String`, `Bool`, `Duration`, \
2052         `Instant`, `Bytes`), the built-in generics (`List`, `Map`, `Option`, `Result`, …), \
2053         `ValidationError`, and the types this unit declares or imports",
2054    )
2055}
2056
2057/// v0.157 (ADR 0183): a generic type named without its `[…]` arguments.
2058fn bare_generic_type_error(id: &Ident, arity: usize) -> CompileError {
2059    CompileError::new(
2060        "bynk.generics.type_arg_count",
2061        id.span,
2062        format!(
2063            "generic type `{}` must be applied to {} type argument{} — write `{}[…]`",
2064            id.name,
2065            arity,
2066            if arity == 1 { "" } else { "s" },
2067            id.name
2068        ),
2069    )
2070    .with_note("a generic type is used only through a concrete instantiation")
2071}
2072
2073/// Recursively check that every type reference resolves.
2074fn check_type_ref_resolves(
2075    r: &TypeRef,
2076    types: &HashMap<String, Arc<TypeDecl>>,
2077    errors: &mut Sinks,
2078) {
2079    check_type_ref_resolves_in(r, types, &HashSet::new(), errors)
2080}
2081
2082/// v0.20a: like [`check_type_ref_resolves`], with the enclosing function's
2083/// type parameters in scope — a `Named` reference matching one is a type
2084/// variable, not an unknown type.
2085fn check_type_ref_resolves_in(
2086    r: &TypeRef,
2087    types: &HashMap<String, Arc<TypeDecl>>,
2088    type_params: &HashSet<String>,
2089    errors: &mut Sinks,
2090) {
2091    match r {
2092        TypeRef::Base(_, _) => {}
2093        // v0.20a: a function type's components must each resolve.
2094        TypeRef::Fn(params, ret, _) => {
2095            for p in params {
2096                check_type_ref_resolves_in(p, types, type_params, errors);
2097            }
2098            check_type_ref_resolves_in(ret, types, type_params, errors);
2099        }
2100        TypeRef::Named(id) => {
2101            if let Some(decl) = types.get(&id.name) {
2102                errors.refs.record(id.span, SymbolKind::Type, &id.name);
2103                // v0.157 (ADR 0183): a generic type must be applied to its type
2104                // arguments — a bare `Paginated` (declared `Paginated[T]`) is an
2105                // under-application.
2106                if !decl.type_params.is_empty() {
2107                    errors.push(bare_generic_type_error(id, decl.type_params.len()));
2108                }
2109            } else if !type_params.contains(&id.name) {
2110                errors.push(unknown_type_error(id));
2111            }
2112        }
2113        // v0.157 (ADR 0183): `Name[Arg, …]` — a user generic-type application.
2114        // Validate existence, that the target is generic, and arity; then walk
2115        // the arguments.
2116        TypeRef::App { name, args, span } => {
2117            match types.get(&name.name) {
2118                None if type_params.contains(&name.name) => {
2119                    // A type parameter applied to arguments (`T[Int]`) — a type
2120                    // parameter is not itself generic (no higher-kinded types).
2121                    errors.push(
2122                        CompileError::new(
2123                            "bynk.generics.type_arg_count",
2124                            *span,
2125                            format!(
2126                                "type parameter `{}` cannot take type arguments — it is not a generic type",
2127                                name.name
2128                            ),
2129                        )
2130                        .with_note("higher-kinded type parameters are not supported"),
2131                    );
2132                }
2133                None => errors.push(unknown_type_error(name)),
2134                Some(decl) => {
2135                    errors.refs.record(name.span, SymbolKind::Type, &name.name);
2136                    let expected = decl.type_params.len();
2137                    // Finding #46: `decl` comes from the combined cross-file
2138                    // symbol table (`uses`/multi-file siblings), so its span
2139                    // may belong to a different file than `name` — a label
2140                    // can't express that without per-label file identity (a
2141                    // Wave 8 follow-up). A note keeps the same conservative
2142                    // choice `bynk-emit/src/project/consistency.rs` already
2143                    // makes for its own always-cross-file diagnostics,
2144                    // rather than risk underlining unrelated text.
2145                    if expected == 0 {
2146                        errors.push(
2147                            CompileError::new(
2148                                "bynk.generics.type_arg_count",
2149                                *span,
2150                                format!(
2151                                    "type `{}` is not generic — it takes no type arguments",
2152                                    name.name
2153                                ),
2154                            )
2155                            .with_note("type declared here"),
2156                        );
2157                    } else if expected != args.len() {
2158                        errors.push(
2159                            CompileError::new(
2160                                "bynk.generics.type_arg_count",
2161                                *span,
2162                                format!(
2163                                    "type `{}` expects {} type argument{}, but {} {} given",
2164                                    name.name,
2165                                    expected,
2166                                    if expected == 1 { "" } else { "s" },
2167                                    args.len(),
2168                                    if args.len() == 1 { "was" } else { "were" },
2169                                ),
2170                            )
2171                            .with_note("type declared here"),
2172                        );
2173                    }
2174                }
2175            }
2176            for a in args {
2177                check_type_ref_resolves_in(a, types, type_params, errors);
2178            }
2179        }
2180        TypeRef::Result(t, e, _) => {
2181            check_type_ref_resolves_in(t, types, type_params, errors);
2182            check_type_ref_resolves_in(e, types, type_params, errors);
2183        }
2184        TypeRef::Option(t, _) => {
2185            check_type_ref_resolves_in(t, types, type_params, errors);
2186        }
2187        TypeRef::Effect(t, _) => {
2188            check_type_ref_resolves_in(t, types, type_params, errors);
2189        }
2190        TypeRef::HttpResult(t, _) => {
2191            check_type_ref_resolves_in(t, types, type_params, errors);
2192        }
2193        TypeRef::QueueResult(_)
2194        | TypeRef::Asset(_)
2195        | TypeRef::RequestHeaders(_)
2196        | TypeRef::TokenClaims(_)
2197        | TypeRef::BoundaryRejection(_) => {}
2198        TypeRef::List(t, _) => {
2199            check_type_ref_resolves_in(t, types, type_params, errors);
2200        }
2201        TypeRef::Query(t, _) => {
2202            check_type_ref_resolves_in(t, types, type_params, errors);
2203        }
2204        TypeRef::Stream(t, _) => {
2205            check_type_ref_resolves_in(t, types, type_params, errors);
2206        }
2207        TypeRef::Connection(t, _) => {
2208            check_type_ref_resolves_in(t, types, type_params, errors);
2209        }
2210        // v0.119 (ADR 0155): `History[Agent]` is a test-only generator, legal only
2211        // as a `for all` binding inside a `property` (validated in
2212        // `check_property_body`). A `History[…]` reaching this declared-type walk —
2213        // a field, parameter, return, or local annotation — is out of place.
2214        TypeRef::History(_, span) => {
2215            errors.push(
2216                CompileError::new(
2217                    "bynk.history.outside_property",
2218                    *span,
2219                    "`History[…]` is only valid as a `for all` generator inside a `property`",
2220                )
2221                .with_note(
2222                    "bind a driven call-history with `for all run: History[Agent]` in a `property`",
2223                ),
2224            );
2225        }
2226        TypeRef::Map(k, v, _) => {
2227            check_type_ref_resolves_in(k, types, type_params, errors);
2228            check_type_ref_resolves_in(v, types, type_params, errors);
2229            check_map_key_keyable(k, types, type_params, errors);
2230        }
2231        TypeRef::ValidationError(_) | TypeRef::JsonError(_) => {}
2232        TypeRef::Unit(_) => {}
2233    }
2234}
2235
2236/// v0.20b: `Map` keys are confined to value-keyable types — `String`, `Int`,
2237/// and refined/opaque types over them — so the emitted `ReadonlyMap` keeps
2238/// value equality (object keys would compare by reference). A type parameter
2239/// is admitted in key position: it can only ever be instantiated through a
2240/// concrete `Map[K, V]` reference elsewhere, and that site is checked.
2241fn check_map_key_keyable(
2242    k: &TypeRef,
2243    types: &HashMap<String, Arc<TypeDecl>>,
2244    type_params: &HashSet<String>,
2245    errors: &mut Sinks,
2246) {
2247    let keyable = match k {
2248        TypeRef::Base(BaseType::String | BaseType::Int, _) => true,
2249        TypeRef::Named(id) => {
2250            // A type parameter is admitted (see above). An unknown name has
2251            // already been reported by the resolution walk; don't pile a
2252            // keyability error on top of it.
2253            if type_params.contains(&id.name) || !types.contains_key(&id.name) {
2254                return;
2255            }
2256            matches!(
2257                types.get(&id.name).map(|t| &t.body),
2258                Some(TypeBody::Refined { base, .. } | TypeBody::Opaque { base, .. })
2259                    if matches!(base, BaseType::String | BaseType::Int)
2260            )
2261        }
2262        _ => false,
2263    };
2264    if !keyable {
2265        errors.push(
2266            CompileError::new(
2267                "bynk.types.unkeyable_map_key",
2268                k.span(),
2269                "a `Map` key must be value-keyable — `String`, `Int`, or a refined/opaque type over them",
2270            )
2271            .with_note(
2272                "record, sum, collection, and function keys are rejected in v0.20b; value-equality keys need bounded generics",
2273            ),
2274        );
2275    }
2276}
2277
2278/// Lookup a name across scopes. Returns true if it's bound somewhere
2279/// (param, self, or any let-scope).
2280fn name_in_scope(name: &str, params: &HashMap<String, ()>, scopes: &[HashMap<String, ()>]) -> bool {
2281    if params.contains_key(name) {
2282        return true;
2283    }
2284    scopes.iter().rev().any(|s| s.contains_key(name))
2285}
2286
2287/// Validate a record construction's *field set* — every required field present,
2288/// no undeclared extra field, no field initialised twice, and every shorthand
2289/// `{ name }` bound in scope. Pure over the declaration and the provided fields;
2290/// the caller supplies its own scope predicate (the resolver's lexical scope via
2291/// [`name_in_scope`], the checker's binding table via `Ctx::lookup`) and its own
2292/// diagnostic sink.
2293///
2294/// #711: this walk skips `Service`/`Agent`/`Actor` items, so their handler
2295/// bodies never pass through it — the checker's `check_record_construction` is
2296/// their only backstop and calls this same function. A single implementation is
2297/// the point: an earlier fix copied three of these four checks into the checker
2298/// and dropped the shorthand one, re-opening the gap for shorthand fields. Both
2299/// callers now share this, so the two cannot re-diverge.
2300pub(crate) fn check_record_field_set(
2301    type_name: &Ident,
2302    fields: &[FieldInit],
2303    record: &RecordBody,
2304    // #852: the span of the whole `TypeName { … }` literal, so the missing-field
2305    // quick-fix knows where to insert a new field (before the closing brace when
2306    // the literal is empty).
2307    construction_span: Span,
2308    in_scope: impl Fn(&str) -> bool,
2309    errors: &mut Vec<CompileError>,
2310) {
2311    let declared: HashMap<&str, &RecordField> = record
2312        .fields
2313        .iter()
2314        .map(|f| (f.name.name.as_str(), f))
2315        .collect();
2316    let mut provided: HashMap<&str, bynk_syntax::span::Span> = HashMap::new();
2317    for f in fields {
2318        if !declared.contains_key(f.name.name.as_str()) {
2319            errors.push(
2320                CompileError::new(
2321                    "bynk.resolve.unknown_field",
2322                    f.name.span,
2323                    format!(
2324                        "record type `{}` has no field `{}`",
2325                        type_name.name, f.name.name
2326                    ),
2327                )
2328                // Finding #46: `decl_name_span` may name a declaration in a
2329                // different file than this construction site (both callers
2330                // resolve against the combined cross-file symbol table) — a
2331                // note instead of a label, matching the same conservative
2332                // choice made elsewhere for cross-file provenance without
2333                // per-label file identity (a Wave 8 follow-up).
2334                .with_note("type declared here"),
2335            );
2336        }
2337        if let Some(prev) = provided.get(f.name.name.as_str()) {
2338            errors.push(
2339                CompileError::new(
2340                    "bynk.resolve.duplicate_field_init",
2341                    f.name.span,
2342                    format!("field `{}` is initialised more than once", f.name.name),
2343                )
2344                .with_label(*prev, "previously initialised here"),
2345            );
2346        } else {
2347            provided.insert(f.name.name.as_str(), f.name.span);
2348        }
2349        // A shorthand `{ name }` (no `: value`) reads the binding `name` from
2350        // scope — it must exist. The full `field: value` form is checked by the
2351        // caller (the resolver recurses into the value, the checker types it).
2352        if f.value.is_none() && !in_scope(&f.name.name) {
2353            errors.push(
2354                CompileError::new(
2355                    "bynk.resolve.unknown_name",
2356                    f.name.span,
2357                    format!(
2358                        "shorthand field initialiser `{}` requires a binding of that name in scope",
2359                        f.name.name
2360                    ),
2361                )
2362                .with_note("either bring `{name}` into scope or use the full `field: value` form"),
2363            );
2364        }
2365    }
2366    // Missing required fields. Each is a diagnostic anchored at the type name;
2367    // a field whose type has a safe default additionally carries a
2368    // machine-applicable "add field `x`" quick-fix (#852, DECISIONS B/C) that
2369    // inserts `x: <default>` at a fmt-stable position, and — when more than one
2370    // field is missing and every missing field is defaultable — the first such
2371    // diagnostic also carries an "add all missing fields" convenience.
2372    let missing: Vec<&RecordField> = record
2373        .fields
2374        .iter()
2375        .filter(|f| !provided.contains_key(f.name.name.as_str()))
2376        .collect();
2377    // The edit for a `body` of one or more `name: default` entries. With
2378    // existing fields it appends `, body` right after the last one. With an
2379    // *empty* literal there is no field span to anchor to and the interior
2380    // spacing/trailing punctuation is unknown, so instead the whole ` { … }`
2381    // tail (from the end of the type name through the closing brace) is
2382    // **replaced** with a canonical ` { body }` — fmt-stable regardless of how
2383    // the empty braces were originally spelled (`{}`, `{ }`, `{  }`).
2384    let field_edit = |body: &str| -> (Span, String) {
2385        match fields.iter().map(|f| f.span.end).max() {
2386            Some(end) => (Span::new(end, end), format!(", {body}")),
2387            None => (
2388                Span::new(type_name.span.end, construction_span.end),
2389                format!(" {{ {body} }}"),
2390            ),
2391        }
2392    };
2393    // Defaultable missing fields, in declaration order, as `name: default`.
2394    let defaultable: Vec<String> = missing
2395        .iter()
2396        .filter_map(|f| field_default_init(f))
2397        .collect();
2398    let all_defaultable = defaultable.len() == missing.len();
2399
2400    for (i, decl_field) in missing.iter().enumerate() {
2401        let mut err = CompileError::new(
2402            "bynk.resolve.missing_field",
2403            type_name.span,
2404            format!(
2405                "missing required field `{}` for record `{}`",
2406                decl_field.name.name, type_name.name
2407            ),
2408        )
2409        .with_label(decl_field.name.span, "field declared here");
2410        if let Some(piece) = field_default_init(decl_field) {
2411            err = err.with_suggestion(
2412                format!("add field `{}`", decl_field.name.name),
2413                vec![field_edit(&piece)],
2414                Applicability::MachineApplicable,
2415            );
2416        }
2417        // The "add all missing fields" convenience rides on the first missing
2418        // diagnostic (they all share `type_name.span`, so it surfaces together
2419        // with the single-field fixes), and only when the whole set is
2420        // defaultable and there is more than one to add.
2421        if i == 0 && missing.len() > 1 && all_defaultable {
2422            err = err.with_suggestion(
2423                "add all missing fields",
2424                vec![field_edit(&defaultable.join(", "))],
2425                Applicability::MachineApplicable,
2426            );
2427        }
2428        errors.push(err);
2429    }
2430}
2431
2432/// The `name: <default>` initialiser for a missing record field, or `None` when
2433/// the field's type has no value that is guaranteed to re-check clean (#852,
2434/// DECISION B). Deliberately conservative: an inline-refined field or a
2435/// user-named type (which may itself be refined, a sum, or opaque) has no
2436/// synthesised default — only the unrefined built-in scalars, `Option` (`None`),
2437/// and `List` (`[]`) do, so the inserted value always type-checks.
2438fn field_default_init(field: &RecordField) -> Option<String> {
2439    if field.refinement.is_some() {
2440        return None;
2441    }
2442    let default = match &field.type_ref {
2443        TypeRef::Base(BaseType::Int, _) => "0",
2444        TypeRef::Base(BaseType::Float, _) => "0.0",
2445        TypeRef::Base(BaseType::String, _) => "\"\"",
2446        TypeRef::Base(BaseType::Bool, _) => "false",
2447        TypeRef::Option(..) => "None",
2448        TypeRef::List(..) => "[]",
2449        _ => return None,
2450    };
2451    Some(format!("{}: {}", field.name.name, default))
2452}
2453
2454#[allow(clippy::too_many_arguments)]
2455/// Bundles the reference-walk's read-only lookup tables and mutable
2456/// traversal state (finding #37): threading nine positional parameters
2457/// through a ~900-line walk meant 313 of resolver.rs's 2,346 lines were
2458/// argument names at recursive call sites.
2459struct RefCheckCtx<'a, 'b> {
2460    params: &'a HashMap<String, ()>,
2461    in_method: bool,
2462    types: &'a HashMap<String, Arc<TypeDecl>>,
2463    type_params: &'a HashSet<String>,
2464    fns: &'a HashMap<String, Arc<FnDecl>>,
2465    methods: &'a HashMap<String, MethodTable>,
2466    /// #1883, slice 1a: the asset names in scope, values of type `Asset`.
2467    assets: &'a HashMap<String, AssetDecl>,
2468    /// #2387, slice 1: the embed names in scope, `String` values.
2469    embeds: &'a HashMap<String, EmbedDecl>,
2470    /// #1925: imports a local declaration of another kind hides.
2471    shadowed: &'a HashMap<String, ShadowedImport>,
2472    scopes: Vec<HashMap<String, ()>>,
2473    errors: &'a mut Sinks<'b>,
2474}
2475
2476fn check_block_references(block: &Block, cx: &mut RefCheckCtx) {
2477    cx.scopes.push(HashMap::new());
2478    for stmt in &block.statements {
2479        match stmt {
2480            Statement::Let(l) | Statement::EffectLet(l) => {
2481                check_expr_references(&l.value, cx);
2482                if let Some(annot) = &l.type_annot {
2483                    check_type_ref_resolves_in(annot, cx.types, cx.type_params, cx.errors);
2484                }
2485                if let Some(prev) = cx.types.get(&l.name.name) {
2486                    cx.errors.push(
2487                        CompileError::new(
2488                            "bynk.resolve.let_shadows_type",
2489                            l.name.span,
2490                            format!(
2491                                "`let {}` shadows the declared type `{}`",
2492                                l.name.name, l.name.name
2493                            ),
2494                        )
2495                        .with_label(prev.name.span, "type declared here")
2496                        .with_note("choose a different name for the let binding"),
2497                    );
2498                } else if let Some(prev) = cx.fns.get(&l.name.name) {
2499                    cx.errors.push(
2500                        CompileError::new(
2501                            "bynk.resolve.let_shadows_fn",
2502                            l.name.span,
2503                            format!(
2504                                "`let {}` shadows the declared function `{}`",
2505                                l.name.name, l.name.name
2506                            ),
2507                        )
2508                        .with_label(prev.name.ident().span, "function declared here")
2509                        .with_note("choose a different name for the let binding"),
2510                    );
2511                } else if l.name.name != "_" {
2512                    cx.scopes
2513                        .last_mut()
2514                        .unwrap()
2515                        .insert(l.name.name.clone(), ());
2516                }
2517            }
2518            Statement::Expect(a) => {
2519                check_expr_references(&a.value, cx);
2520            }
2521            Statement::Send(s) => {
2522                check_expr_references(&s.value, cx);
2523            }
2524            Statement::Do(d) => {
2525                check_expr_references(&d.value, cx);
2526            }
2527            Statement::Assign(a) => {
2528                // v0.81: walk the RHS for references; the target resolves to a
2529                // `store` field, handled in the storage-track checker slice.
2530                check_expr_references(&a.value, cx);
2531            }
2532            Statement::Advance(a) => {
2533                check_expr_references(&a.amount, cx);
2534            }
2535        }
2536    }
2537    check_expr_references(&block.tail, cx);
2538    cx.scopes.pop();
2539}
2540
2541#[allow(clippy::too_many_lines)]
2542fn check_expr_references(expr: &Expr, cx: &mut RefCheckCtx) {
2543    match &expr.kind {
2544        // v0.43: resolve names referenced inside each interpolation hole.
2545        ExprKind::InterpStr(parts) => {
2546            for part in parts {
2547                if let InterpPart::Hole(hole) = part {
2548                    check_expr_references(hole, cx);
2549                }
2550            }
2551        }
2552        ExprKind::IntLit { .. }
2553        | ExprKind::FloatLit { .. }
2554        | ExprKind::DurationLit { .. }
2555        | ExprKind::StrLit(_)
2556        | ExprKind::BoolLit(_)
2557        | ExprKind::None
2558        | ExprKind::UnitLit => {}
2559        // v0.20b: a list literal — each element resolves as a value.
2560        ExprKind::ListLit(elems) => {
2561            for el in elems {
2562                check_expr_references(el, cx);
2563            }
2564        }
2565        // Slice C: `Wire(<String>)` — the raw inner expression resolves as an
2566        // ordinary value (a string literal in practice).
2567        ExprKind::Wire(inner) => {
2568            check_expr_references(inner, cx);
2569        }
2570        // v0.20a: a lambda introduces a scope frame holding its params; the
2571        // body walks with the frame in place. Annotated param types resolve
2572        // through the ordinary type-ref check.
2573        ExprKind::Lambda(lambda) => {
2574            for p in &lambda.params {
2575                if let Some(tr) = &p.type_ref {
2576                    check_type_ref_resolves_in(tr, cx.types, cx.type_params, cx.errors);
2577                }
2578            }
2579            let mut frame: HashMap<String, ()> = HashMap::new();
2580            for p in &lambda.params {
2581                frame.insert(p.name.name.clone(), ());
2582            }
2583            cx.scopes.push(frame);
2584            check_expr_references(&lambda.body, cx);
2585            cx.scopes.pop();
2586        }
2587        ExprKind::EffectPure(inner) => {
2588            check_expr_references(inner, cx);
2589        }
2590        ExprKind::Expect(inner) => {
2591            check_expr_references(inner, cx);
2592        }
2593        ExprKind::Faults(f) => {
2594            // #1812: a call-site principal's identity (`by User(who)`) is an
2595            // ordinary expression, resolved like the call's arguments.
2596            if let Some(identity) = f.principal.as_ref().and_then(|p| p.identity.as_deref()) {
2597                check_expr_references(identity, cx);
2598            }
2599            check_expr_references(&f.call, cx);
2600        }
2601        ExprKind::Val { args, .. } => {
2602            // v0.9.4: the mocked type is validated by the checker; resolve any
2603            // pin-argument references here.
2604            for a in args {
2605                check_expr_references(a, cx);
2606            }
2607        }
2608        ExprKind::Observation(_) => {
2609            // v0.117: a `with` predicate's free names are the operation's
2610            // parameters, bound during type checking and not visible to name
2611            // resolution; a count is a literal. Nothing to resolve here.
2612        }
2613        ExprKind::Trace { .. } => {
2614            // v0.117: `Cap.op` names a capability seam, not value references.
2615        }
2616        ExprKind::RecordSpread {
2617            type_name,
2618            base,
2619            overrides,
2620        } => {
2621            if let Some(tn) = type_name
2622                && !cx.types.contains_key(&tn.name)
2623            {
2624                cx.errors.push(unknown_type_error(tn));
2625            }
2626            check_expr_references(base, cx);
2627            for f in overrides {
2628                if let Some(v) = &f.value {
2629                    check_expr_references(v, cx);
2630                }
2631            }
2632        }
2633        ExprKind::Ident(id) => {
2634            if id.name == "self" {
2635                if !cx.in_method {
2636                    cx.errors.push(
2637                        CompileError::new(
2638                            "bynk.resolve.self_outside_method",
2639                            id.span,
2640                            "`self` can only be used inside a method body",
2641                        )
2642                        .with_note(
2643                            "declare the function as `fn TypeName.method(self, ...)` if you intended a method",
2644                        ),
2645                    );
2646                }
2647                return;
2648            }
2649            if name_in_scope(&id.name, cx.params, &cx.scopes) {
2650                // OK.
2651            } else if http_variant(&id.name).is_some() {
2652                // v0.9: predeclared HttpResult variant (e.g. `NoContent`,
2653                // `Unauthorized`). The checker validates payload arity and
2654                // expected-type disambiguation.
2655            } else if let Some(sum_owner) = find_unique_variant_owner(&id.name, cx.types) {
2656                // It's a bare variant reference. We treat it as a valid
2657                // expression in resolver — the type checker will assign
2658                // the correct sum type. Mark with no error.
2659                let _ = sum_owner;
2660            } else if cx.fns.contains_key(&id.name)
2661                && cx
2662                    .shadowed
2663                    .get(&id.name)
2664                    .is_some_and(|s| s.imported == "type")
2665            {
2666                // Review of #1933: a local function hiding an imported type of
2667                // its name answers a bare reference (a function value), as in
2668                // the emitted module; the checker judges the position
2669                // (`check_ident`'s function arm, met before types).
2670                cx.errors.refs.record(id.span, SymbolKind::Fn, &id.name);
2671            } else if (cx.assets.contains_key(&id.name) || cx.embeds.contains_key(&id.name))
2672                && cx
2673                    .shadowed
2674                    .get(&id.name)
2675                    .is_some_and(|s| s.imported == "type")
2676            {
2677                // #1927: a local asset (or #2387 embed) hiding an imported
2678                // type of its name is the asset here, as in the emitted
2679                // module; the checker types it (`check_ident` meets assets
2680                // and embeds before types).
2681            } else if let Some(s) = cx.shadowed.get(&id.name)
2682                && !matches!(s.local, "function" | "asset" | "embed")
2683            {
2684                // #1925: the local declaration (a type, event, service,
2685                // agent or provider) is no value here, and it hides the import.
2686                cx.errors.push(shadowed_import_error(&id.name, id.span, s));
2687            } else if cx.types.contains_key(&id.name) {
2688                cx.errors.push(
2689                    CompileError::new(
2690                        "bynk.resolve.type_in_expr",
2691                        id.span,
2692                        format!("`{}` is a type, not a value", id.name),
2693                    )
2694                    .with_note(
2695                        "types cannot appear in expression position; \
2696                         use `TypeName.of(value)` or `TypeName { ... }` to construct values",
2697                    ),
2698                );
2699            } else if cx.fns.contains_key(&id.name) {
2700                // v0.20a: a bare named-function reference may be a function
2701                // VALUE where a function type is expected. The resolver has
2702                // no type information, so the judgment (and the
2703                // `bynk.resolve.fn_without_call` diagnostic for non-function
2704                // positions) now lives in the checker's ident rule. Silent
2705                // pass here keeps `unknown_name` from misfiring.
2706                cx.errors.refs.record(id.span, SymbolKind::Fn, &id.name);
2707            } else if cx.assets.contains_key(&id.name) {
2708                // #1883, slice 1a: an asset name is a value of type `Asset`;
2709                // the checker types it (`check_ident`).
2710            } else if cx.embeds.contains_key(&id.name) {
2711                // #2387, slice 1: an embed name is a `String` value; the
2712                // checker types it (`check_ident`).
2713            } else if find_ambiguous_variant_owners(&id.name, cx.types).len() > 1 {
2714                cx.errors.push(
2715                    CompileError::new(
2716                        "bynk.resolve.ambiguous_variant",
2717                        id.span,
2718                        format!(
2719                            "the variant name `{}` is declared on multiple sum types — qualify it as `TypeName.{}`",
2720                            id.name, id.name
2721                        ),
2722                    ),
2723                );
2724            } else {
2725                cx.errors.push(
2726                    CompileError::new(
2727                        "bynk.resolve.unknown_name",
2728                        id.span,
2729                        format!("unknown name `{}`", id.name),
2730                    )
2731                    .with_note(
2732                        "only parameters, `let` bindings, and functions declared \
2733                         in this commons are in scope",
2734                    ),
2735                );
2736            }
2737        }
2738        ExprKind::Call {
2739            name,
2740            type_args,
2741            args,
2742        } => {
2743            // #712: explicit type arguments (`identity[T](…)`) are type
2744            // references and must resolve — the checker's `check_generic_call`
2745            // otherwise dropped an unknown one silently. Validated here so
2746            // `fn`/method bodies are covered; the checker backstops handler
2747            // bodies (which never reach this walk).
2748            for ta in type_args {
2749                check_type_ref_resolves_in(ta, cx.types, cx.type_params, cx.errors);
2750            }
2751            match cx.fns.get(&name.name) {
2752                Some(decl) => {
2753                    cx.errors.refs.record(name.span, SymbolKind::Fn, &name.name);
2754                    if decl.params.len() != args.len() {
2755                        cx.errors.push(
2756                            CompileError::new(
2757                                "bynk.resolve.arity_mismatch",
2758                                name.span,
2759                                format!(
2760                                    "function `{}` expects {} argument(s), but {} were given",
2761                                    name.name,
2762                                    decl.params.len(),
2763                                    args.len()
2764                                ),
2765                            )
2766                            // Finding #46: `decl` is looked up in the
2767                            // combined cross-file symbol table, so its span
2768                            // may belong to a different file than this call
2769                            // — see the same note at the type-arity checks
2770                            // above.
2771                            .with_note("function declared here"),
2772                        );
2773                    }
2774                }
2775                None => {
2776                    // Maybe it's a variant constructor with a payload (e.g., `Placed(at, total)`).
2777                    let owners = find_ambiguous_variant_owners(&name.name, cx.types);
2778                    if http_variant(&name.name).is_some() {
2779                        // v0.9: predeclared HttpResult variant constructor.
2780                    } else if owners.len() == 1 {
2781                        // Single owner — treat as variant construction. Type
2782                        // checker validates arg count and types.
2783                    } else if owners.len() > 1 {
2784                        cx.errors.push(CompileError::new(
2785                            "bynk.resolve.ambiguous_variant",
2786                            name.span,
2787                            format!(
2788                                "the variant name `{}` is declared on multiple sum types — qualify it as `TypeName.{}(...)`",
2789                                name.name, name.name
2790                            ),
2791                        ));
2792                    } else if let Some(s) = cx.shadowed.get(&name.name) {
2793                        // #1925: no local function answers the call (one would
2794                        // have matched above), and the local declaration of
2795                        // another kind hides the import.
2796                        cx.errors
2797                            .push(shadowed_import_error(&name.name, name.span, s));
2798                    } else if cx.types.contains_key(&name.name) {
2799                        cx.errors.push(CompileError::new(
2800                            "bynk.resolve.type_as_function",
2801                            name.span,
2802                            format!(
2803                                "`{}` is a type, not a function — use `{}.of(value)` or `{} {{ ... }}` instead",
2804                                name.name, name.name, name.name
2805                            ),
2806                        ));
2807                    } else if name_in_scope(&name.name, cx.params, &cx.scopes) {
2808                        // v0.20a: an in-scope value being called may be a
2809                        // legal value application if its type is a function
2810                        // type. The resolver has no type information, so the
2811                        // judgment (and `bynk.resolve.param_as_function` for
2812                        // non-function-typed values) lives in the checker's
2813                        // call dispatch. Silent pass.
2814                    } else {
2815                        cx.errors.push(
2816                            CompileError::new(
2817                                "bynk.resolve.unknown_function",
2818                                name.span,
2819                                format!("unknown function `{}`", name.name),
2820                            )
2821                            .with_note("only functions declared in this commons are callable"),
2822                        );
2823                    }
2824                }
2825            }
2826            for a in args {
2827                check_expr_references(a, cx);
2828            }
2829        }
2830        // #1654: the right operand of `&&`/`implies` is evaluated only when the
2831        // left holds, so the left's `is` bindings are in scope there.
2832        ExprKind::BinOp(BinOp::And | BinOp::Implies, lhs, rhs) => {
2833            check_expr_references(lhs, cx);
2834            let mut extra: HashMap<String, ()> = HashMap::new();
2835            collect_is_binding_names(lhs, true, &mut extra);
2836            cx.scopes.push(extra);
2837            check_expr_references(rhs, cx);
2838            cx.scopes.pop();
2839        }
2840        ExprKind::BinOp(_, lhs, rhs) => {
2841            check_expr_references(lhs, cx);
2842            check_expr_references(rhs, cx);
2843        }
2844        ExprKind::UnaryOp(_, e) => check_expr_references(e, cx),
2845        ExprKind::Paren(e) => check_expr_references(e, cx),
2846        ExprKind::Block(b) => check_block_references(b, cx),
2847        ExprKind::If {
2848            cond,
2849            then_block,
2850            else_block,
2851        } => {
2852            check_expr_references(cond, cx);
2853            // `is`-pattern bindings the condition proves flow into the
2854            // then-branch's scope (v0.2 §3.9), and those its *falsity* proves
2855            // into the else-branch's (#1654: `if !(o is Some(v)) { … } else
2856            // { v }`).
2857            let mut then_extra: HashMap<String, ()> = HashMap::new();
2858            collect_is_binding_names(cond, true, &mut then_extra);
2859            cx.scopes.push(then_extra);
2860            check_block_references(then_block, cx);
2861            cx.scopes.pop();
2862            let mut else_extra: HashMap<String, ()> = HashMap::new();
2863            collect_is_binding_names(cond, false, &mut else_extra);
2864            cx.scopes.push(else_extra);
2865            check_block_references(else_block, cx);
2866            cx.scopes.pop();
2867        }
2868        ExprKind::Ok(inner) | ExprKind::Err(inner) | ExprKind::Question(inner) => {
2869            check_expr_references(inner, cx);
2870        }
2871        ExprKind::Some(inner) => {
2872            check_expr_references(inner, cx);
2873        }
2874        ExprKind::RecordConstruction { type_name, fields } => {
2875            match cx.types.get(&type_name.name) {
2876                Some(decl) => {
2877                    cx.errors
2878                        .refs
2879                        .record(type_name.span, SymbolKind::Type, &type_name.name);
2880                    match &decl.body {
2881                        TypeBody::Record(r) => {
2882                            // Field-set validation (missing / unknown / duplicate
2883                            // / shorthand-in-scope) is shared with the checker's
2884                            // `check_record_construction` so the two cannot
2885                            // re-diverge (#711). The value recursion below stays
2886                            // here — it is the resolver's reference walk.
2887                            check_record_field_set(
2888                                type_name,
2889                                fields,
2890                                r,
2891                                expr.span,
2892                                |n| name_in_scope(n, cx.params, &cx.scopes),
2893                                cx.errors.errs,
2894                            );
2895                            for f in fields {
2896                                if let Some(v) = &f.value {
2897                                    check_expr_references(v, cx);
2898                                }
2899                            }
2900                        }
2901                        TypeBody::Opaque { .. } => {
2902                            cx.errors.push(
2903                            CompileError::new(
2904                                "bynk.resolve.opaque_record_construction",
2905                                type_name.span,
2906                                format!(
2907                                    "opaque type `{}` cannot be constructed with record-literal syntax",
2908                                    type_name.name
2909                                ),
2910                            )
2911                            // Finding #46: cross-file table lookup — see resolver.rs:1029.
2912                            .with_note("type declared here")
2913                            .with_note(
2914                                "construct opaque values via `T.of(value)` (validated) or `T.unsafe(value)` (inside the defining commons)",
2915                            ),
2916                        );
2917                        }
2918                        _ => {
2919                            cx.errors.push(
2920                            CompileError::new(
2921                                "bynk.resolve.not_a_record_type",
2922                                type_name.span,
2923                                format!(
2924                                    "`{}` is not a record type — only record types can be constructed with `{{ ... }}`",
2925                                    type_name.name
2926                                ),
2927                            )
2928                            // Finding #46: cross-file table lookup — see resolver.rs:1029.
2929                            .with_note("type declared here"),
2930                        );
2931                        }
2932                    }
2933                }
2934                None => cx.errors.push(unknown_type_error(type_name)),
2935            }
2936        }
2937        ExprKind::FieldAccess { receiver, field } => {
2938            // v0.9: `HttpResult.Variant` qualified nullary variant.
2939            if let ExprKind::Ident(id) = &receiver.kind
2940                && !name_in_scope(&id.name, cx.params, &cx.scopes)
2941                && id.name == "HttpResult"
2942            {
2943                if http_variant(&field.name).is_none() {
2944                    cx.errors.push(CompileError::new(
2945                        "bynk.resolve.unknown_static_member",
2946                        field.span,
2947                        format!("`HttpResult` has no variant named `{}`", field.name),
2948                    ));
2949                }
2950                return;
2951            }
2952            // #2341 (L4): `ErrorStatus.Forbidden` — a `RawError` status, qualified.
2953            // The checker admits it only as `RawError`'s first argument.
2954            if let ExprKind::Ident(id) = &receiver.kind
2955                && !name_in_scope(&id.name, cx.params, &cx.scopes)
2956                && id.name == "ErrorStatus"
2957                && !cx.types.contains_key(&id.name)
2958            {
2959                if bynk_syntax::ast::error_status(&field.name).is_none() {
2960                    cx.errors.push(CompileError::new(
2961                        "bynk.resolve.unknown_static_member",
2962                        field.span,
2963                        format!(
2964                            "`ErrorStatus` has no status named `{}` — the error statuses are the 4xx and 5xx `HttpResult` variants",
2965                            field.name
2966                        ),
2967                    ));
2968                }
2969                return;
2970            }
2971            // `TypeName.Variant` — qualified nullary variant reference.
2972            // #1927: not for a local asset that hides an imported type of its
2973            // name: `logo.url` reads the asset, as the emitted module does.
2974            if let ExprKind::Ident(id) = &receiver.kind
2975                && !name_in_scope(&id.name, cx.params, &cx.scopes)
2976                && !matches!(cx.shadowed.get(&id.name), Some(s) if s.imported == "type" && s.local == "asset")
2977                && let Some(decl) = cx.types.get(&id.name)
2978            {
2979                cx.errors.refs.record(id.span, SymbolKind::Type, &id.name);
2980                let known_variant = match &decl.body {
2981                    TypeBody::Sum(s) => s.variants.iter().any(|v| v.name.name == field.name),
2982                    _ => false,
2983                };
2984                if !known_variant {
2985                    cx.errors.push(
2986                        CompileError::new(
2987                            "bynk.resolve.unknown_static_member",
2988                            field.span,
2989                            format!(
2990                                "type `{}` has no static method or variant named `{}`",
2991                                id.name, field.name
2992                            ),
2993                        )
2994                        // Finding #46: cross-file table lookup — see resolver.rs:1029.
2995                        .with_note("type declared here"),
2996                    );
2997                }
2998            } else {
2999                check_expr_references(receiver, cx);
3000            }
3001        }
3002        ExprKind::MethodCall {
3003            receiver,
3004            method,
3005            args,
3006            ..
3007        } => {
3008            // v0.9: `HttpResult.Variant(args)` — qualified HttpResult constructor.
3009            if let ExprKind::Ident(id) = &receiver.kind
3010                && !name_in_scope(&id.name, cx.params, &cx.scopes)
3011                && id.name == "HttpResult"
3012            {
3013                if http_variant(&method.name).is_none() {
3014                    cx.errors.push(CompileError::new(
3015                        "bynk.resolve.unknown_static_member",
3016                        method.span,
3017                        format!("`HttpResult` has no variant named `{}`", method.name),
3018                    ));
3019                }
3020                for a in args {
3021                    check_expr_references(a, cx);
3022                }
3023                return;
3024            }
3025            // v0.20b: `List.empty()` / `Map.empty()` — qualified statics on
3026            // the built-in collection types (no user declaration to resolve
3027            // against; the checker owns their typing). v0.22a adds the
3028            // numeric parse statics, `Int.parse(…)` / `Float.parse(…)`.
3029            if let ExprKind::Ident(id) = &receiver.kind
3030                && !name_in_scope(&id.name, cx.params, &cx.scopes)
3031                && matches!(
3032                    id.name.as_str(),
3033                    "List"
3034                        | "Map"
3035                        | "Int"
3036                        | "Float"
3037                        | "Json"
3038                        | "Duration"
3039                        | "Instant"
3040                        | "Stream"
3041                        | "Bytes"
3042                        | "RequestHeaders"
3043                        | "TokenClaims"
3044                )
3045                && !cx.types.contains_key(&id.name)
3046            {
3047                let allowed: &[&str] = match id.name.as_str() {
3048                    "List" | "Map" => &["empty"],
3049                    "Json" => &["encode", "decode"],
3050                    // v0.86 (ADR 0112): `Duration.millis(n)`.
3051                    "Duration" => &["millis"],
3052                    // v0.90 (ADR 0114): `Instant.fromEpochMillis(n)`.
3053                    "Instant" => &["fromEpochMillis"],
3054                    // v0.100: `Stream.of(xs)`.
3055                    "Stream" => &["of"],
3056                    // v0.110 (ADR 0142): `Bytes.fromUtf8(s)`/`fromBase64(s)`/`empty()`;
3057                    // #2016: `fromHex(s)`/`fromBase64Url(s)`.
3058                    "Bytes" => &[
3059                        "fromUtf8",
3060                        "fromBase64",
3061                        "fromHex",
3062                        "fromBase64Url",
3063                        "empty",
3064                    ],
3065                    // #2341 (L4): the test-side constructors.
3066                    "RequestHeaders" => &["of", "empty"],
3067                    // #2380 (L5): the test-side constructor.
3068                    "TokenClaims" => &["of"],
3069                    _ => &["parse"],
3070                };
3071                let only = allowed.join("`/`");
3072                if !allowed.contains(&method.name.as_str()) {
3073                    cx.errors.push(CompileError::new(
3074                        "bynk.resolve.unknown_static_member",
3075                        method.span,
3076                        format!(
3077                            "the built-in `{}` type has no static method named `{}` — the statics are `{only}`",
3078                            id.name, method.name
3079                        ),
3080                    ));
3081                }
3082                for a in args {
3083                    check_expr_references(a, cx);
3084                }
3085                return;
3086            }
3087            // If the receiver is a bare ident of a declared type (and not a
3088            // local binding), this is a static call: `T.method(args)`.
3089            // Validate the type/method/variant resolution here. Otherwise
3090            // recurse into the receiver as a value expression.
3091            if let ExprKind::Ident(id) = &receiver.kind
3092                && !name_in_scope(&id.name, cx.params, &cx.scopes)
3093                && let Some(decl) = cx.types.get(&id.name)
3094            {
3095                cx.errors.refs.record(id.span, SymbolKind::Type, &id.name);
3096                let table = cx.methods.get(&id.name).cloned().unwrap_or_default();
3097                let is_static_method = table.statics.contains_key(&method.name);
3098                let is_of_constructor = method.name == "of"
3099                    && matches!(
3100                        decl.body,
3101                        TypeBody::Refined { .. } | TypeBody::Opaque { .. }
3102                    );
3103                let is_unsafe_constructor =
3104                    method.name == "unsafe" && matches!(decl.body, TypeBody::Opaque { .. });
3105                let is_variant = match &decl.body {
3106                    TypeBody::Sum(s) => s.variants.iter().any(|v| v.name.name == method.name),
3107                    _ => false,
3108                };
3109                if !(is_static_method || is_of_constructor || is_unsafe_constructor || is_variant) {
3110                    cx.errors.push(
3111                        CompileError::new(
3112                            "bynk.resolve.unknown_static_member",
3113                            method.span,
3114                            format!(
3115                                "type `{}` has no static method or variant named `{}`",
3116                                id.name, method.name
3117                            ),
3118                        )
3119                        // Finding #46: cross-file table lookup — see resolver.rs:1029.
3120                        .with_note("type declared here"),
3121                    );
3122                }
3123            } else {
3124                check_expr_references(receiver, cx);
3125            }
3126            for a in args {
3127                check_expr_references(a, cx);
3128            }
3129        }
3130        ExprKind::Match { discriminant, arms } => {
3131            check_expr_references(discriminant, cx);
3132            for arm in arms {
3133                // Pattern bindings introduce names in the arm body. The
3134                // type checker validates the pattern against the discriminant
3135                // type. Resolver pushes a scope with those binding names so
3136                // body references resolve.
3137                let mut arm_scope = HashMap::new();
3138                collect_pattern_bindings(&arm.pattern, &mut arm_scope);
3139                cx.scopes.push(arm_scope);
3140                // #1848: the guard, in the arm's pattern scope and before the
3141                // body, as the checker types it (ADR 0169). Its `is` bindings
3142                // stay in the guard; they do not flow into the body.
3143                if let Some(guard) = &arm.guard {
3144                    check_expr_references(guard, cx);
3145                }
3146                match &arm.body {
3147                    MatchBody::Expr(e) => check_expr_references(e, cx),
3148                    MatchBody::Block(b) => check_block_references(b, cx),
3149                }
3150                cx.scopes.pop();
3151            }
3152        }
3153        ExprKind::Is { value, pattern } => {
3154            check_expr_references(value, cx);
3155            // `is` pattern bindings flow through to the truthy branch of
3156            // an enclosing context; binding scope is handled by the type
3157            // checker. Resolver doesn't introduce anything here.
3158            let _ = pattern;
3159        }
3160    }
3161}
3162
3163/// The names introduced by the `is` tests `expr` proves matched when it
3164/// evaluates to `when_true` (v0.2 §3.9). Which tests those are is the shared
3165/// rule in [`crate::narrowing`] (#1654), the one the checker and emitter use.
3166fn collect_is_binding_names(expr: &Expr, when_true: bool, into: &mut HashMap<String, ()>) {
3167    for test in crate::narrowing::matched_is_tests(expr, when_true) {
3168        if let ExprKind::Is { pattern, .. } = &test.kind {
3169            collect_is_pattern_binding_names(pattern, into);
3170        }
3171    }
3172}
3173
3174/// The depth-1 names an `is` pattern introduces — a `Variant`'s own flat
3175/// bindings (`is` supports only flat, depth-1 name bindings, ADR 0169 keeps
3176/// nesting/guards match-only, matching `gather_pattern_bindings`), or — #474
3177/// — for an or-pattern, the first alternative's (Rule 2 guarantees every
3178/// alternative gives a shared name the same type, so any one alternative's
3179/// names are representative of them all).
3180fn collect_is_pattern_binding_names(pattern: &Pattern, into: &mut HashMap<String, ()>) {
3181    match pattern {
3182        Pattern::Variant { bindings, .. } => {
3183            for b in bindings {
3184                if let Pattern::Binding(name) = b.pattern() {
3185                    into.insert(name.name.clone(), ());
3186                }
3187            }
3188        }
3189        Pattern::Or(alts, _) => {
3190            if let Some(first) = alts.first() {
3191                collect_is_pattern_binding_names(first, into);
3192            }
3193        }
3194        _ => {}
3195    }
3196}
3197
3198/// Walk a pattern collecting the names it would bind, recursively through
3199/// nested payload patterns (ADR 0169) — `Some(Ok(x))` binds `x`.
3200fn collect_pattern_bindings(pattern: &Pattern, into: &mut HashMap<String, ()>) {
3201    for id in pattern.bound_names() {
3202        into.insert(id.name.clone(), ());
3203    }
3204}
3205
3206/// Find the unique sum type that owns a given variant name. Returns None
3207/// if no type owns it; ignores cases of multiple owners (those are
3208/// reported via `find_ambiguous_variant_owners`).
3209fn find_unique_variant_owner<'a>(
3210    name: &str,
3211    types: &'a HashMap<String, Arc<TypeDecl>>,
3212) -> Option<&'a TypeDecl> {
3213    let owners = find_ambiguous_variant_owners(name, types);
3214    if owners.len() == 1 {
3215        Some(owners[0])
3216    } else {
3217        None
3218    }
3219}
3220
3221fn find_ambiguous_variant_owners<'a>(
3222    name: &str,
3223    types: &'a HashMap<String, Arc<TypeDecl>>,
3224) -> Vec<&'a TypeDecl> {
3225    let mut out = Vec::new();
3226    for t in types.values() {
3227        if let TypeBody::Sum(s) = &t.body
3228            && s.variants.iter().any(|v| v.name.name == name)
3229        {
3230            out.push(t.as_ref());
3231        }
3232    }
3233    out
3234}
3235
3236/// #1848: a match-arm guard is resolved like every other expression, in its
3237/// arm's pattern scope.
3238#[cfg(test)]
3239mod guard_resolution_tests {
3240    use crate::testkit::analyse;
3241
3242    #[test]
3243    fn an_unknown_variant_in_a_guard_is_reported_by_the_resolver() {
3244        analyse(&[(
3245            "d.bynk",
3246            "commons d\n\ntype T = enum { A, B }\n\nfn g(n: Int, t: T) -> Int {\n  match n {\n    x if t == T.Zzz => 1\n    _ => 0\n  }\n}\n",
3247        )])
3248        .assert_reports("bynk.resolve.unknown_static_member");
3249    }
3250
3251    /// True before #1848 too (the checker records it). Pinned so resolving
3252    /// guards cannot drop or double the reference.
3253    #[test]
3254    fn a_function_named_only_in_a_guard_is_a_recorded_reference() {
3255        let a = analyse(&[(
3256            "d.bynk",
3257            "commons d\n\nfn big(n: Int) -> Bool { n > 9 }\n\nfn g(n: Int) -> Int {\n  match n {\n    x if big(x) => 1\n    _ => 0\n  }\n}\n",
3258        )]);
3259        a.assert_clean();
3260        let key = a.resolves_to("d.bynk", "big(x)");
3261        assert_eq!((key.unit.as_str(), key.name.as_str()), ("d", "big"));
3262    }
3263
3264    #[test]
3265    fn a_guard_reads_its_arm_s_pattern_bindings() {
3266        analyse(&[(
3267            "d.bynk",
3268            "commons d\n\nfn g(o: Option[Int]) -> Int {\n  match o {\n    Some(v) if v > 0 => v\n    _ => 0\n  }\n}\n",
3269        )])
3270        .assert_clean();
3271    }
3272}