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