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