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