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