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