Skip to main content

bynk_check/
symbols.rs

1use std::collections::{BTreeMap, HashMap};
2use std::path::PathBuf;
3use std::sync::Arc;
4
5use crate::checker::CapabilityInfo;
6use crate::index::{IndexBuilder, ProjectIndex, RefSink, SiteRef, SymbolKind};
7use crate::project_model::UnitInfo;
8use crate::resolver::{self, MethodTable as ResolverMethodTable};
9use bynk_project::{ParsedFile, UnitKind};
10use bynk_syntax::ast::{
11    ActorDecl, AgentDecl, AssetDecl, BaseType, Block, CapRef, CapabilityDecl, CommonsItem,
12    EmbedDecl, EventDecl, ExportKind, Expr, ExprId, ExprKind, FnDecl, FnName, HandlerKind, Ident,
13    Param, ProviderDecl, ServiceDecl, ServiceProtocol, Trivia, TypeBody, TypeDecl, TypeRef,
14    Visibility,
15};
16use bynk_syntax::error::CompileError;
17use bynk_syntax::span::Span;
18
19/// v0.25 (ADR 0053): walk every parsed file's top-level declarations into
20/// the def table (synthetic first-party units and test files excluded —
21/// neither declares user-editable symbols), then qualify and attach the
22/// recorded edges. Methods register as owners only (attribution), not as
23/// symbols — they are deferred along with fields and op names.
24pub fn assemble_index(
25    parsed: &[ParsedFile],
26    unit_uses: &HashMap<String, Vec<String>>,
27    unit_consumes: &HashMap<String, Vec<String>>,
28    refs: RefSink,
29) -> ProjectIndex {
30    let mut builder = IndexBuilder::default();
31    let mut uses = unit_uses.clone();
32    uses.extend(refs.extra_uses);
33    builder.set_uses(uses);
34    builder.set_consumes(unit_consumes.clone());
35    for pf in parsed {
36        if matches!(pf.kind(), UnitKind::Test | UnitKind::Integration) {
37            continue;
38        }
39        let unit = pf.unit().name().joined();
40        // v0.28 (ADR 0057): synthetic first-party units stay out of
41        // `symbols` (their defs point at files not on disk — the v0.25
42        // rule), but their declarations register for the second
43        // qualification pass so references to them colour as tokens.
44        if pf.is_synthetic() {
45            for item in pf.items() {
46                let (kind, name, modifiers) = match item {
47                    CommonsItem::Type(t) => (
48                        SymbolKind::Type,
49                        &t.name.name,
50                        symbol_modifiers(&unit, Some(t)),
51                    ),
52                    // Events track, slice 0 (spine #936): an `event` indexes
53                    // as an ordinary `Type` symbol — it *is* one, a record,
54                    // registered via `EventDecl::as_type_decl`. A dedicated
55                    // `SymbolKind::Event` (its own hover/completion icon) is
56                    // a follow-on, not a slice-0 blocker.
57                    CommonsItem::Event(e) => (
58                        SymbolKind::Type,
59                        &e.name.name,
60                        symbol_modifiers(&unit, None),
61                    ),
62                    CommonsItem::Fn(f) => match &f.name {
63                        FnName::Free(id) => {
64                            (SymbolKind::Fn, &id.name, symbol_modifiers(&unit, None))
65                        }
66                        FnName::Method { .. } => continue,
67                    },
68                    CommonsItem::Capability(c) => (
69                        SymbolKind::Capability,
70                        &c.name.name,
71                        symbol_modifiers(&unit, None),
72                    ),
73                    CommonsItem::Service(s) => (
74                        SymbolKind::Service,
75                        &s.name.name,
76                        symbol_modifiers(&unit, None),
77                    ),
78                    CommonsItem::Agent(a) => (
79                        SymbolKind::Agent,
80                        &a.name.name,
81                        symbol_modifiers(&unit, None),
82                    ),
83                    CommonsItem::Provider(p) => (
84                        SymbolKind::Provider,
85                        &p.provider_name.name,
86                        symbol_modifiers(&unit, None),
87                    ),
88                    CommonsItem::Actor(a) => (
89                        SymbolKind::Actor,
90                        &a.name.name,
91                        symbol_modifiers(&unit, None),
92                    ),
93                    CommonsItem::Messages(m) => {
94                        (SymbolKind::Messages, &m.tag, symbol_modifiers(&unit, None))
95                    }
96                    // #1883: an asset indexes under its own kind, keyed by
97                    // name in the declaring unit.
98                    CommonsItem::Asset(a) => (
99                        SymbolKind::Asset,
100                        &a.name.name,
101                        symbol_modifiers(&unit, None),
102                    ),
103                    // #2387: an embed indexes under its own kind, keyed by
104                    // name in the declaring unit.
105                    CommonsItem::Embed(e) => (
106                        SymbolKind::Embed,
107                        &e.name.name,
108                        symbol_modifiers(&unit, None),
109                    ),
110                };
111                builder.add_first_party_def(&unit, kind, name, modifiers);
112            }
113            continue;
114        }
115        let site = |id: &Ident| SiteRef {
116            path: pf.identity_path(),
117            span: id.span,
118        };
119        for item in pf.items() {
120            match item {
121                CommonsItem::Type(t) => {
122                    builder.add_def(
123                        &unit,
124                        SymbolKind::Type,
125                        &t.name.name,
126                        site(&t.name),
127                        symbol_modifiers(&unit, Some(t)),
128                    );
129                    // v0.129 (#259): record a refined/opaque type's builtin base
130                    // for the refinement-family codelens. A plain alias
131                    // (`type Age = Int`) counts — it parses as `Refined { …, base }`
132                    // with no `where`, still declared over the base.
133                    if let TypeBody::Refined { base, .. } | TypeBody::Opaque { base, .. } = &t.body
134                    {
135                        builder.add_refinement(&unit, &t.name.name, *base);
136                    }
137                    // v0.36 (ADR 0069, slice 2): record fields are first-class
138                    // symbols keyed by the compound `"Type.field"` name.
139                    if let TypeBody::Record(r) = &t.body {
140                        for field in &r.fields {
141                            builder.add_def(
142                                &unit,
143                                SymbolKind::Field,
144                                &format!("{}.{}", t.name.name, field.name.name),
145                                site(&field.name),
146                                symbol_modifiers(&unit, None),
147                            );
148                        }
149                    }
150                }
151                // Events track, slice 0 (spine #936): an `event` indexes
152                // exactly like a `Type` whose body is a record — same
153                // `SymbolKind::Type`/`SymbolKind::Field` reuse as the
154                // synthetic-unit arm above, so hover/go-to-def/rename work
155                // on an event and its fields without a new symbol kind.
156                CommonsItem::Event(e) => {
157                    builder.add_def(
158                        &unit,
159                        SymbolKind::Type,
160                        &e.name.name,
161                        site(&e.name),
162                        symbol_modifiers(&unit, None),
163                    );
164                    for field in &e.body.fields {
165                        builder.add_def(
166                            &unit,
167                            SymbolKind::Field,
168                            &format!("{}.{}", e.name.name, field.name.name),
169                            site(&field.name),
170                            symbol_modifiers(&unit, None),
171                        );
172                    }
173                }
174                CommonsItem::Fn(f) => match &f.name {
175                    FnName::Free(id) => {
176                        builder.add_def(
177                            &unit,
178                            SymbolKind::Fn,
179                            &id.name,
180                            site(id),
181                            symbol_modifiers(&unit, None),
182                        );
183                    }
184                    FnName::Method { .. } => {
185                        // v0.36 (ADR 0069): a method is a first-class symbol
186                        // keyed by the compound `"Type.method"` name, and (as
187                        // before) an attribution owner for call-hierarchy.
188                        builder.add_owner(&unit, &f.name.display(), &pf.identity_path());
189                        builder.add_def(
190                            &unit,
191                            SymbolKind::Method,
192                            &f.name.display(),
193                            site(f.name.ident()),
194                            symbol_modifiers(&unit, None),
195                        );
196                    }
197                },
198                CommonsItem::Capability(c) => {
199                    builder.add_def(
200                        &unit,
201                        SymbolKind::Capability,
202                        &c.name.name,
203                        site(&c.name),
204                        symbol_modifiers(&unit, None),
205                    );
206                    // v0.36 (ADR 0069, slice 2): capability operations are
207                    // first-class symbols keyed by the compound `"Cap.op"` name.
208                    for op in &c.ops {
209                        builder.add_def(
210                            &unit,
211                            SymbolKind::CapabilityOp,
212                            &format!("{}.{}", c.name.name, op.name.name),
213                            site(&op.name),
214                            symbol_modifiers(&unit, None),
215                        );
216                    }
217                }
218                CommonsItem::Service(s) => {
219                    builder.add_def(
220                        &unit,
221                        SymbolKind::Service,
222                        &s.name.name,
223                        site(&s.name),
224                        symbol_modifiers(&unit, None),
225                    );
226                }
227                CommonsItem::Agent(a) => {
228                    builder.add_def(
229                        &unit,
230                        SymbolKind::Agent,
231                        &a.name.name,
232                        site(&a.name),
233                        symbol_modifiers(&unit, None),
234                    );
235                    // #304: an agent handler is a first-class symbol keyed by
236                    // the compound `"Agent.handler"` name, mirroring the
237                    // v0.36 (ADR 0069) method/field/op convention. Service
238                    // handlers have no per-handler name (`method_name` is
239                    // always `None`), so this is naturally agent-only.
240                    for h in &a.handlers {
241                        if let Some(name) = &h.method_name {
242                            builder.add_def(
243                                &unit,
244                                SymbolKind::Handler,
245                                &format!("{}.{}", a.name.name, name.name),
246                                site(name),
247                                symbol_modifiers(&unit, None),
248                            );
249                        }
250                    }
251                }
252                CommonsItem::Provider(p) => {
253                    builder.add_def(
254                        &unit,
255                        SymbolKind::Provider,
256                        &p.provider_name.name,
257                        site(&p.provider_name),
258                        symbol_modifiers(&unit, None),
259                    );
260                }
261                CommonsItem::Actor(a) => {
262                    builder.add_def(
263                        &unit,
264                        SymbolKind::Actor,
265                        &a.name.name,
266                        site(&a.name),
267                        symbol_modifiers(&unit, None),
268                    );
269                }
270                CommonsItem::Messages(m) => {
271                    // The tag is a string literal, not an `Ident`, so build the
272                    // `SiteRef` from its span directly rather than via `site`.
273                    builder.add_def(
274                        &unit,
275                        SymbolKind::Messages,
276                        &m.tag,
277                        SiteRef {
278                            path: pf.identity_path(),
279                            span: m.tag_span,
280                        },
281                        symbol_modifiers(&unit, None),
282                    );
283                }
284                // #1883: an asset's declaration site, for hover, go-to-
285                // definition, references, rename and semantic tokens. A bare
286                // use in a unit that `uses` this one resolves here through the
287                // qualification pass, as a function's does. A leaf def: an
288                // asset encloses no call site, so it registers no owner (one
289                // would clash with a same-named service's — review of #1924).
290                CommonsItem::Asset(a) => {
291                    builder.add_leaf_def(
292                        &unit,
293                        SymbolKind::Asset,
294                        &a.name.name,
295                        site(&a.name),
296                        symbol_modifiers(&unit, None),
297                    );
298                }
299                // #2387: an embed's declaration site, for hover, go-to-
300                // definition, references, rename and semantic tokens — a leaf
301                // def, like an asset, so it takes over no owner entry.
302                CommonsItem::Embed(e) => {
303                    builder.add_leaf_def(
304                        &unit,
305                        SymbolKind::Embed,
306                        &e.name.name,
307                        site(&e.name),
308                        symbol_modifiers(&unit, None),
309                    );
310                }
311            }
312        }
313    }
314    builder.build(refs.edges)
315}
316
317/// v0.28 (ADR 0057): a symbol's semantic-token modifiers from its
318/// declaration. `refined` only when a refinement is present — `type X = Int`
319/// is `Refined { refinement: None }`, a plain alias, and carries neither;
320/// `opaque` is orthogonal (an `opaque … where` type carries both).
321/// `platform_native` when the declaring unit is a platform adapter.
322fn symbol_modifiers(unit: &str, type_decl: Option<&TypeDecl>) -> crate::index::SymbolModifiers {
323    let (refined, opaque) = match type_decl.map(|t| &t.body) {
324        Some(TypeBody::Refined { refinement, .. }) => (refinement.is_some(), false),
325        Some(TypeBody::Opaque { refinement, .. }) => (refinement.is_some(), true),
326        _ => (false, false),
327    };
328    crate::index::SymbolModifiers {
329        refined,
330        opaque,
331        platform_native: crate::firstparty::platform_of(unit).is_some(),
332    }
333}
334
335/// Combined symbol tables for a single logical commons or context.
336#[derive(Clone, Default)]
337pub struct UnitTable {
338    pub kind: Option<UnitKind>,
339    pub types: HashMap<String, Arc<TypeDecl>>,
340    pub fns: HashMap<String, Arc<FnDecl>>,
341    pub methods: HashMap<String, ResolverMethodTable>,
342    /// Per-context capabilities (v0.5). Empty for commons.
343    pub capabilities: HashMap<String, CapabilityDecl>,
344    /// Per-context providers (v0.5). One provider per capability in v0.5.
345    /// Key: capability name. Value: provider declaration.
346    pub providers: HashMap<String, ProviderDecl>,
347    /// Per-context services (v0.5). Empty for commons.
348    pub services: HashMap<String, ServiceDecl>,
349    /// Per-context agents (v0.5). Empty for commons.
350    pub agents: HashMap<String, AgentDecl>,
351    /// State-migrations S3 (#2248): the context's `removed agent`
352    /// declarations, across its files.
353    pub removed_agents: Vec<bynk_syntax::ast::RemovedAgentDecl>,
354    /// v0.45: actors — boundary contracts consumed by handler `by` clauses.
355    pub actors: HashMap<String, ActorDecl>,
356    /// v0.15: capability names this context offers to consumers via
357    /// `exports capability { … }`. Empty for commons.
358    pub exported_capabilities: std::collections::HashSet<String>,
359    /// Events track, slice 0 (spine #936): `event` declarations. Each also
360    /// registers into `types` (via `EventDecl::as_type_decl`) so ordinary
361    /// type-reference/exports/consumes/construction machinery treats it like
362    /// any other record type; this table is the separate "is `name`
363    /// specifically an event" answer — owner-only emission and the
364    /// `from Events(E)`/`Events.emit[E]` "must name a declared event, not
365    /// just any type" checks key off it. Empty for commons/adapters
366    /// (`bynk.event.outside_context` rejects it there).
367    pub events: HashMap<String, EventDecl>,
368    /// #1883, slice 1a: `asset` declarations, by name — values of the
369    /// built-in type `Asset`, local to this unit. A context or commons only
370    /// (`bynk.asset.outside_context_or_commons`); a unit's visible assets
371    /// add those of the commons it `uses` (`assets::compose_unit_assets`).
372    pub assets: HashMap<String, AssetDecl>,
373    /// #2387, slice 1: `embed` declarations, by name — `String` constants
374    /// holding a project file's text, local to this unit. A context or commons
375    /// only (`bynk.embed.outside_context_or_commons`); a unit's visible embeds
376    /// add those of the commons it `uses` (`embeds::compose_unit_embeds`).
377    pub embeds: HashMap<String, EmbedDecl>,
378    /// #291: the capabilities this unit flattens in with `consumes U { Cap }`,
379    /// each mapped to the unit providing it — `phase_resolve_consumes`'s
380    /// per-unit answer, recorded by [`record_flattened_caps`] so a test body's
381    /// privileged view sees the target's platform seams.
382    pub flattened_caps: HashMap<String, String>,
383    /// S4a (#1966): the derived capabilities (`capability Sessions = Kv`)
384    /// this context declares, by name, once resolved to their base by
385    /// [`crate::project_model::phase_resolve_derived`]. Each is also in
386    /// [`UnitTable::capabilities`], with its base's operations, so `given`,
387    /// calls and test views treat it as an ordinary capability.
388    pub derived: HashMap<String, DerivedInstance>,
389}
390
391/// S4a (#1966): a resolved derived capability: a distinct instance of a
392/// platform capability, provided by that platform's own provider bound to this
393/// instance's Worker binding.
394#[derive(Debug, Clone, PartialEq, Eq)]
395pub struct DerivedInstance {
396    /// The unit providing the base capability (`bynk.cloudflare`).
397    pub base_unit: String,
398    /// The base capability (`Kv`).
399    pub base_cap: String,
400    /// The instance's Worker binding name (`KV_SESSIONS`).
401    pub binding: String,
402    /// Its creation options, each with its literal value, as written
403    /// (`[("worker", Str("legacy-billing"))]`, interop I5 #1972; `[("limit",
404    /// Int(100)), ("period", Seconds(60))]`, P3 #2342).
405    pub options: Vec<(String, OptionValue)>,
406    /// The `capability … = …` declaration.
407    pub span: bynk_syntax::span::Span,
408}
409
410/// Whether `unit.capability` is `bynk.cloudflare.EdgeCache` (L7, #2414),
411/// whose instances turn Workers Cache on and have no `env` binding.
412pub fn is_edge_cache_base(unit: &str, capability: &str) -> bool {
413    unit == crate::firstparty::CLOUDFLARE_UNIT && capability == "EdgeCache"
414}
415
416/// The value of one instance option (standard-library P3, #2342).
417#[derive(Debug, Clone, PartialEq, Eq)]
418pub enum OptionValue {
419    /// A string literal (`@worker("legacy-billing")`).
420    Str(String),
421    /// A positive `Int` literal (`@limit(100)`).
422    Int(i64),
423    /// A `Duration` literal, in whole seconds (`@period(60.seconds)`).
424    Seconds(i64),
425}
426
427impl DerivedInstance {
428    /// The value of string option `name`, if the instance carries it.
429    pub fn option(&self, name: &str) -> Option<&str> {
430        self.options
431            .iter()
432            .find(|(o, _)| o == name)
433            .and_then(|(_, v)| match v {
434                OptionValue::Str(s) => Some(s.as_str()),
435                _ => None,
436            })
437    }
438
439    /// The value of numeric option `name` (an `Int`, or a `Duration` in
440    /// seconds), if the instance carries it.
441    pub fn number(&self, name: &str) -> Option<i64> {
442        self.options
443            .iter()
444            .find(|(o, _)| o == name)
445            .and_then(|(_, v)| match v {
446                OptionValue::Int(n) | OptionValue::Seconds(n) => Some(*n),
447                OptionValue::Str(_) => None,
448            })
449    }
450
451    /// Whether this is a `RateLimit` instance (P3, #2342).
452    pub fn is_ratelimit(&self) -> bool {
453        self.base_unit == crate::firstparty::CLOUDFLARE_UNIT && self.base_cap == "RateLimit"
454    }
455
456    /// Whether this is an `EdgeCache` instance (L7, #2414).
457    pub fn is_edge_cache(&self) -> bool {
458        is_edge_cache_base(&self.base_unit, &self.base_cap)
459    }
460
461    /// A `RateLimit` instance's `(limit, period in seconds)`, or `None` for
462    /// any other kind.
463    pub fn rate_limit(&self) -> Option<(i64, i64)> {
464        if !self.is_ratelimit() {
465            return None;
466        }
467        Some((
468            self.number(crate::firstparty::RATELIMIT_LIMIT_OPTION)?,
469            self.number(crate::firstparty::RATELIMIT_PERIOD_OPTION)?,
470        ))
471    }
472
473    /// The jurisdiction an `R2` instance's bucket is pinned to (P5, #2323), or
474    /// `None` for an unpinned one or any other kind.
475    pub fn jurisdiction(&self) -> Option<&str> {
476        self.is_r2()
477            .then(|| self.option(crate::firstparty::R2_JURISDICTION_OPTION))
478            .flatten()
479    }
480
481    /// Whether this is an `R2` instance.
482    pub fn is_r2(&self) -> bool {
483        self.base_unit == crate::firstparty::CLOUDFLARE_UNIT && self.base_cap == "R2"
484    }
485
486    /// The Worker a `Service` instance reaches (interop I5, #1972), or `None`
487    /// for any other kind.
488    pub fn worker(&self) -> Option<&str> {
489        (self.base_unit == crate::firstparty::CLOUDFLARE_UNIT && self.base_cap == "Service")
490            .then(|| self.option(crate::firstparty::SERVICE_WORKER_OPTION))
491            .flatten()
492    }
493}
494
495/// #291: record each unit's flattened capabilities (`consumes U { Cap }`, from
496/// `phase_resolve_consumes`) on its table.
497pub fn record_flattened_caps(
498    unit_tables: &mut HashMap<String, UnitTable>,
499    unit_flattened: &HashMap<String, HashMap<String, String>>,
500) {
501    for (name, flattened) in unit_flattened {
502        if let Some(t) = unit_tables.get_mut(name) {
503            t.flattened_caps = flattened.clone();
504        }
505    }
506}
507
508/// #696: each table-construction diagnostic is attributed to the project-relative
509/// `identity_path` of the file whose item produced it. Every error-producing loop
510/// below iterates `for &i in indices`, so it shadows a local `errors` vec and
511/// drains it into `out`, tagged with `parsed[i].identity_path()`, at the end of each
512/// file's pass — leaving the many inner `errors.push(…)` sites untouched.
513pub fn build_unit_table(
514    _name: &str,
515    kind: UnitKind,
516    indices: &[usize],
517    parsed: &[ParsedFile],
518    out: &mut Vec<(PathBuf, CompileError)>,
519) -> UnitTable {
520    let mut table = UnitTable {
521        kind: Some(kind),
522        ..UnitTable::default()
523    };
524    for &i in indices {
525        let mut errors: Vec<CompileError> = Vec::new();
526        table
527            .removed_agents
528            .extend(parsed[i].removed_agents().iter().cloned());
529        for item in parsed[i].items() {
530            // Events track, slice 0 (spine #936): an `event` registers into
531            // `types` exactly like a `type` (via `EventDecl::as_type_decl`,
532            // so name-conflict detection against ordinary types is a single
533            // check regardless of declaration order within the file) and
534            // additionally into `events`, the separate "is this specifically
535            // an event" table.
536            if let CommonsItem::Event(e) = item
537                && kind != UnitKind::Context
538            {
539                errors.push(CompileError::new(
540                    "bynk.event.outside_context",
541                    e.span,
542                    "`event` declarations are only allowed inside a context",
543                ));
544                continue;
545            }
546            let as_type: Option<(&Ident, TypeDecl, bool)> = match item {
547                CommonsItem::Type(t) => Some((&t.name, t.clone(), false)),
548                CommonsItem::Event(e) => Some((&e.name, e.as_type_decl(), true)),
549                _ => None,
550            };
551            if let Some((name, decl, is_event)) = as_type {
552                if let Some(prev) = table.types.get(&name.name) {
553                    errors.push(
554                        CompileError::new(
555                            "bynk.resolve.duplicate_type",
556                            name.span,
557                            format!("type `{}` is already declared", name.name),
558                        )
559                        .with_label(prev.name.span, "previously declared here"),
560                    );
561                } else {
562                    table.methods.entry(name.name.clone()).or_default();
563                    if is_event {
564                        let CommonsItem::Event(e) = item else {
565                            unreachable!("is_event only set for CommonsItem::Event")
566                        };
567                        table.events.insert(name.name.clone(), e.clone());
568                    }
569                    table.types.insert(name.name.clone(), Arc::new(decl));
570                }
571            }
572        }
573        out.extend(errors.into_iter().map(|e| (parsed[i].identity_path(), e)));
574    }
575    // v0.15: collect the names a context exports as capabilities.
576    // v0.17: adapters export capabilities too.
577    for &i in indices {
578        {
579            for clause in parsed[i].exports() {
580                if matches!(clause.kind, ExportKind::Capability) {
581                    for n in clause.names.iter().map(|e| &e.name) {
582                        table.exported_capabilities.insert(n.name.clone());
583                    }
584                }
585            }
586        }
587    }
588    // v0.5: collect capabilities, providers, services, agents.
589    for &i in indices {
590        let mut errors: Vec<CompileError> = Vec::new();
591        for item in parsed[i].items() {
592            match item {
593                CommonsItem::Capability(c) => {
594                    if kind != UnitKind::Context && kind != UnitKind::Adapter {
595                        errors.push(CompileError::new(
596                            "bynk.capability.outside_context",
597                            c.span,
598                            "`capability` declarations are only allowed inside a context or adapter",
599                        ));
600                        continue;
601                    }
602                    if let Some(prev) = table.capabilities.get(&c.name.name) {
603                        errors.push(
604                            CompileError::new(
605                                "bynk.resolve.duplicate_capability",
606                                c.name.span,
607                                format!("capability `{}` is already declared", c.name.name),
608                            )
609                            .with_label(prev.name.span, "previously declared here"),
610                        );
611                    } else {
612                        table.capabilities.insert(c.name.name.clone(), c.clone());
613                    }
614                }
615                CommonsItem::Provider(p) => {
616                    match kind {
617                        UnitKind::Context => {
618                            // v0.17: a bodiless (external) provider is only legal
619                            // inside an adapter.
620                            if p.external {
621                                errors.push(CompileError::new(
622                                    "bynk.context.external_provider",
623                                    p.span,
624                                    "an external (bodiless) provider is only allowed inside an `adapter` — a context provider must have a Bynk body",
625                                ));
626                                continue;
627                            }
628                        }
629                        UnitKind::Adapter => {
630                            // v0.17: an adapter provider must be external — its
631                            // implementation comes from the binding.
632                            if !p.external {
633                                errors.push(CompileError::new(
634                                    "bynk.adapter.provider_has_body",
635                                    p.span,
636                                    "a provider inside an `adapter` must be external (no body) — its implementation is supplied by the binding",
637                                ));
638                                continue;
639                            }
640                        }
641                        // The parser rejects `provides` anywhere else
642                        // (`bynk.provider.outside_context`) before a symbol
643                        // table is built (#1662).
644                        _ => continue,
645                    }
646                    if let Some(prev) = table.providers.get(&p.capability.name) {
647                        errors.push(
648                            CompileError::new(
649                                "bynk.resolve.duplicate_provider",
650                                p.span,
651                                format!(
652                                    "capability `{}` already has a provider in this context",
653                                    p.capability.name
654                                ),
655                            )
656                            .with_label(prev.span, "previously provided here"),
657                        );
658                    } else {
659                        table.providers.insert(p.capability.name.clone(), p.clone());
660                    }
661                }
662                CommonsItem::Service(s) => {
663                    if kind == UnitKind::Adapter {
664                        errors.push(CompileError::new(
665                            "bynk.adapter.disallowed_item",
666                            s.span,
667                            "an `adapter` may not declare a `service` — adapters contain only capabilities, boundary types, external providers, and helpers",
668                        ));
669                        continue;
670                    }
671                    // An adapter's is rejected just above
672                    // (`bynk.adapter.disallowed_item`), and the parser rejects
673                    // a `service` in a commons (`bynk.service.outside_context`)
674                    // before a symbol table is built, so only a context reaches
675                    // here (#1662).
676                    if kind != UnitKind::Context {
677                        continue;
678                    }
679                    if let Some(prev) = table.services.get(&s.name.name) {
680                        errors.push(
681                            CompileError::new(
682                                "bynk.resolve.duplicate_service",
683                                s.name.span,
684                                format!("service `{}` is already declared", s.name.name),
685                            )
686                            .with_label(prev.name.span, "previously declared here"),
687                        );
688                    } else {
689                        table.services.insert(s.name.name.clone(), s.clone());
690                    }
691                }
692                CommonsItem::Agent(a) => {
693                    if kind == UnitKind::Adapter {
694                        errors.push(CompileError::new(
695                            "bynk.adapter.disallowed_item",
696                            a.span,
697                            "an `adapter` may not declare an `agent` — adapters contain only capabilities, boundary types, external providers, and helpers",
698                        ));
699                        continue;
700                    }
701                    // An adapter's is rejected just above
702                    // (`bynk.adapter.disallowed_item`), and the parser rejects
703                    // an `agent` in a commons (`bynk.agent.outside_context`)
704                    // before a symbol table is built, so only a context reaches
705                    // here (#1662).
706                    if kind != UnitKind::Context {
707                        continue;
708                    }
709                    if let Some(prev) = table.agents.get(&a.name.name) {
710                        errors.push(
711                            CompileError::new(
712                                "bynk.resolve.duplicate_agent",
713                                a.name.span,
714                                format!("agent `{}` is already declared", a.name.name),
715                            )
716                            .with_label(prev.name.span, "previously declared here"),
717                        );
718                    } else {
719                        table.agents.insert(a.name.name.clone(), a.clone());
720                    }
721                }
722                CommonsItem::Actor(a) => {
723                    if kind == UnitKind::Adapter {
724                        errors.push(CompileError::new(
725                            "bynk.adapter.disallowed_item",
726                            a.span,
727                            "an `adapter` may not declare an `actor` — adapters contain only capabilities, boundary types, external providers, and helpers",
728                        ));
729                        continue;
730                    }
731                    if let Some(prev) = table.actors.get(&a.name.name) {
732                        errors.push(
733                            CompileError::new(
734                                "bynk.resolve.duplicate_actor",
735                                a.name.span,
736                                format!("actor `{}` is already declared", a.name.name),
737                            )
738                            .with_label(prev.name.span, "previously declared here"),
739                        );
740                    } else {
741                        table.actors.insert(a.name.name.clone(), a.clone());
742                    }
743                }
744                _ => {}
745            }
746        }
747        out.extend(errors.into_iter().map(|e| (parsed[i].identity_path(), e)));
748    }
749    for &i in indices {
750        let mut errors: Vec<CompileError> = Vec::new();
751        for item in parsed[i].items() {
752            let CommonsItem::Fn(f) = item else { continue };
753            match &f.name {
754                FnName::Free(id) => {
755                    if let Some(prev) = table.fns.get(&id.name) {
756                        errors.push(
757                            CompileError::new(
758                                "bynk.resolve.duplicate_fn",
759                                id.span,
760                                format!("function `{}` is already declared", id.name),
761                            )
762                            .with_label(prev.name.ident().span, "previously declared here"),
763                        );
764                    } else if let Some(prev) = table.types.get(&id.name) {
765                        errors.push(
766                            CompileError::new(
767                                "bynk.resolve.name_conflict",
768                                id.span,
769                                format!(
770                                    "function `{}` conflicts with a type of the same name",
771                                    id.name
772                                ),
773                            )
774                            .with_label(prev.name.span, "type declared here"),
775                        );
776                    } else {
777                        table.fns.insert(id.name.clone(), Arc::new(f.clone()));
778                    }
779                }
780                FnName::Method {
781                    type_name,
782                    method_name,
783                } => {
784                    if !table.types.contains_key(&type_name.name) {
785                        errors.push(
786                            CompileError::new(
787                                "bynk.resolve.method_unknown_type",
788                                type_name.span,
789                                format!(
790                                    "method `{}.{}` attached to an unknown type `{}`",
791                                    type_name.name, method_name.name, type_name.name
792                                ),
793                            )
794                            .with_note(
795                                "methods can only be declared on types defined in the same commons or context (across all of its files)",
796                            ),
797                        );
798                        continue;
799                    }
800                    let mt = table.methods.entry(type_name.name.clone()).or_default();
801                    let bucket = if f.has_self {
802                        &mut mt.instance
803                    } else {
804                        &mut mt.statics
805                    };
806                    if let Some(prev) = bucket.get(&method_name.name) {
807                        errors.push(
808                            CompileError::new(
809                                "bynk.resolve.duplicate_method",
810                                method_name.span,
811                                format!(
812                                    "method `{}.{}` is already declared",
813                                    type_name.name, method_name.name
814                                ),
815                            )
816                            .with_label(prev.name.ident().span, "previously declared here"),
817                        );
818                    } else {
819                        bucket.insert(method_name.name.clone(), Arc::new(f.clone()));
820                    }
821                }
822            }
823        }
824        out.extend(errors.into_iter().map(|e| (parsed[i].identity_path(), e)));
825    }
826    // #1883, slice 1a: `asset` declarations. After the function pass, so an
827    // asset that shares a function's or type's name is reported whichever
828    // was declared first: an asset is a value, in the same namespace as a
829    // free function, and a type name in expression position is an error.
830    for &i in indices {
831        let mut errors: Vec<CompileError> = Vec::new();
832        for item in parsed[i].items() {
833            let CommonsItem::Asset(a) = item else {
834                continue;
835            };
836            if !matches!(kind, UnitKind::Context | UnitKind::Commons) {
837                errors.push(
838                    CompileError::new(
839                        "bynk.asset.outside_context_or_commons",
840                        a.span,
841                        "`asset` declarations are only allowed inside a context or a commons",
842                    )
843                    .with_note(
844                        "an asset ships with the Worker of each context that declares it or `uses` a commons that does",
845                    ),
846                );
847                continue;
848            }
849            errors.extend(crate::assets::at_path_error(a));
850            if let Some(err) = crate::assets::path_errors(a) {
851                errors.push(err);
852                // Still registered below: the declaration is otherwise
853                // well-formed, so its uses should not also report
854                // `unknown_name`.
855            }
856            let key = &a.name.name;
857            match crate::assets::name_error(
858                a,
859                table.assets.get(key).map(|p| &p.name),
860                table.fns.get(key).map(|f| f.name.ident()),
861                table.types.get(key).map(|t| &t.name),
862            ) {
863                Some(err) => errors.push(err),
864                None => {
865                    table.assets.insert(key.clone(), a.clone());
866                }
867            }
868        }
869        out.extend(errors.into_iter().map(|e| (parsed[i].identity_path(), e)));
870    }
871    // #2387, slice 1: `embed` declarations, after the asset pass so an embed
872    // that shares an asset's, function's or type's name is reported on the
873    // embed whichever was declared first: an embed is a value, in the same
874    // namespace as a free function.
875    for &i in indices {
876        let mut errors: Vec<CompileError> = Vec::new();
877        for item in parsed[i].items() {
878            let CommonsItem::Embed(e) = item else {
879                continue;
880            };
881            if !matches!(kind, UnitKind::Context | UnitKind::Commons) {
882                errors.push(crate::embeds::placement_error(e));
883                continue;
884            }
885            if let Some(err) = crate::embeds::path_errors(e) {
886                errors.push(err);
887                // Still registered below, as for an asset: the declaration is
888                // otherwise well-formed, so its uses should not also report
889                // `unknown_name`.
890            }
891            let key = &e.name.name;
892            match crate::embeds::name_error(
893                e,
894                table.embeds.get(key).map(|p| &p.name),
895                table.assets.get(key).map(|a| &a.name),
896                table.fns.get(key).map(|f| f.name.ident()),
897                table.types.get(key).map(|t| &t.name),
898            ) {
899                Some(err) => errors.push(err),
900                None => {
901                    table.embeds.insert(key.clone(), e.clone());
902                }
903            }
904        }
905        out.extend(errors.into_iter().map(|e| (parsed[i].identity_path(), e)));
906    }
907    // #1925: a service, agent or provider shares one namespace with the
908    // unit's functions, assets, types and events (and with each other) — they
909    // are all one scope of the emitted TypeScript module, so a shared name
910    // would be declared twice. After every other pass, so each table is
911    // complete whatever the file and declaration order; reported at the
912    // service, agent or provider, the later kinds in this order. An external
913    // (bodiless) provider emits no declaration, so it takes no name.
914    let mut providers_seen: HashMap<&str, Span> = HashMap::new();
915    for &i in indices {
916        let mut errors: Vec<CompileError> = Vec::new();
917        for item in parsed[i].items() {
918            // Only the declaration registered: a duplicate has already
919            // reported `duplicate_service` / `duplicate_agent` /
920            // `duplicate_provider`.
921            let (what, name) = match item {
922                CommonsItem::Service(s)
923                    if table.services.get(&s.name.name).map(|p| p.name.span)
924                        == Some(s.name.span) =>
925                {
926                    (ValueKind::Service, &s.name)
927                }
928                CommonsItem::Agent(a)
929                    if table.agents.get(&a.name.name).map(|p| p.name.span) == Some(a.name.span) =>
930                {
931                    (ValueKind::Agent, &a.name)
932                }
933                CommonsItem::Provider(p)
934                    if !p.external
935                        && table.providers.get(&p.capability.name).map(|q| q.span)
936                            == Some(p.span) =>
937                {
938                    (ValueKind::Provider, &p.provider_name)
939                }
940                _ => continue,
941            };
942            // Provider against provider only. A provider meets a service or
943            // agent through their tables whichever comes first, so the pair
944            // is reported once, at the provider.
945            let earlier_provider = (what == ValueKind::Provider)
946                .then(|| providers_seen.get(name.name.as_str()).copied())
947                .flatten();
948            if let Some(err) = value_namespace_error(&table, what, name, earlier_provider) {
949                errors.push(err);
950            }
951            if what == ValueKind::Provider {
952                providers_seen.entry(&name.name).or_insert(name.span);
953            }
954        }
955        out.extend(errors.into_iter().map(|e| (parsed[i].identity_path(), e)));
956    }
957    // message-bundles slice 1 (#859): a commons declaring at least one
958    // `messages` block also gets a synthetic `render(tag: LocaleTag, msg:
959    // Message) -> String` in its own local function table — not just emitted
960    // TS. Without this, a Bynk-source `render(...)` call has no local
961    // declaration to resolve to and silently falls through to `bynk.locale`'s
962    // *imported* `render` (same signature, wrong — bundle-free — behaviour):
963    // resolution would "type-check" while quietly calling the wrong function.
964    // Registering it here, in the same local `table.fns` a real `CommonsItem::Fn`
965    // would populate, makes ordinary lexical precedence (local beats
966    // `uses`-imported, `compose_unit_symbols`) and call-site type-checking
967    // (`fns.get(name)`, never touching `.body`) work with no changes anywhere
968    // else. The body is a placeholder — nothing ever type-checks it, since
969    // body-checking walks `commons.items` (real AST items) directly, and this
970    // entry is never added there.
971    if kind == UnitKind::Commons
972        && let Some(m) = indices.iter().find_map(|&i| {
973            parsed[i].items().iter().find_map(|item| match item {
974                CommonsItem::Messages(m) => Some(m),
975                _ => None,
976            })
977        })
978    {
979        if let Some(prev) = table.fns.get("render") {
980            out.push((
981                parsed[indices[0]].identity_path(),
982                CompileError::new(
983                    "bynk.resolve.duplicate_fn",
984                    m.span,
985                    "function `render` is already declared",
986                )
987                .with_label(prev.name.ident().span, "previously declared here")
988                .with_note(
989                    "a `messages` block in this commons implicitly declares its own \
990                     `render(tag, msg) -> String` — name it something else",
991                ),
992            ));
993        } else {
994            table
995                .fns
996                .insert("render".to_string(), Arc::new(synthetic_render_fn()));
997        }
998    }
999    table
1000}
1001
1002/// #1925: what `name` is in `table`'s own value namespace — its kind, as a
1003/// diagnostic names it, and its declaration's name span — or `None` when the
1004/// unit declares no value of that name. The predicate both compositions use
1005/// to let a local declaration of any kind shadow a `uses`-imported function or
1006/// asset (`project_model::compose_unit_symbols`,
1007/// `assets::compose_unit_assets`), as the emitted module does.
1008pub fn local_value_kind(table: &UnitTable, name: &str) -> Option<(&'static str, Span)> {
1009    if let Some(f) = table.fns.get(name) {
1010        Some(("function", f.name.ident().span))
1011    } else if let Some(a) = table.assets.get(name) {
1012        Some(("asset", a.name.span))
1013    } else if let Some(e) = table.embeds.get(name) {
1014        Some(("embed", e.name.span))
1015    } else if let Some(e) = table.events.get(name) {
1016        Some(("event", e.name.span))
1017    } else if let Some(t) = table.types.get(name) {
1018        Some(("type", t.name.span))
1019    } else if let Some(s) = table.services.get(name) {
1020        Some(("service", s.name.span))
1021    } else if let Some(a) = table.agents.get(name) {
1022        Some(("agent", a.name.span))
1023    } else {
1024        table
1025            .providers
1026            .values()
1027            .find(|p| !p.external && p.provider_name.name == name)
1028            .map(|p| ("provider", p.provider_name.span))
1029    }
1030}
1031
1032/// #1925: the kinds [`value_namespace_error`] checks, in the order the
1033/// conflict pass meets them.
1034#[derive(Clone, Copy, PartialEq, Eq)]
1035enum ValueKind {
1036    Service,
1037    Agent,
1038    Provider,
1039}
1040
1041/// #1925: `bynk.resolve.name_conflict` for a service, agent or provider
1042/// (`what`) whose `name` the unit also gives a function, an asset, a type or
1043/// event, or a kind `what` follows in the pass: an agent conflicts with a
1044/// service, a provider with a service, an agent or `earlier_provider`. `None`
1045/// when the name is free.
1046fn value_namespace_error(
1047    table: &UnitTable,
1048    what: ValueKind,
1049    name: &Ident,
1050    earlier_provider: Option<Span>,
1051) -> Option<CompileError> {
1052    let key = &name.name;
1053    let services = || table.services.get(key).map(|s| s.name.span);
1054    let agents = || table.agents.get(key).map(|a| a.name.span);
1055    let (other, prev) = if let Some(f) = table.fns.get(key) {
1056        ("function", f.name.ident().span)
1057    } else if let Some(a) = table.assets.get(key) {
1058        ("asset", a.name.span)
1059    } else if let Some(e) = table.embeds.get(key) {
1060        ("embed", e.name.span)
1061    } else if let Some(e) = table.events.get(key) {
1062        ("event", e.name.span)
1063    } else if let Some(t) = table.types.get(key) {
1064        ("type", t.name.span)
1065    } else if let Some(span) = (what != ValueKind::Service).then(services).flatten() {
1066        ("service", span)
1067    } else if let Some(span) = (what == ValueKind::Provider).then(agents).flatten() {
1068        ("agent", span)
1069    } else if let Some(span) = earlier_provider {
1070        ("provider", span)
1071    } else {
1072        return None;
1073    };
1074    let what = match what {
1075        ValueKind::Service => "service",
1076        ValueKind::Agent => "agent",
1077        ValueKind::Provider => "provider",
1078    };
1079    let other_article = if matches!(other, "asset" | "embed" | "event" | "agent") {
1080        "an"
1081    } else {
1082        "a"
1083    };
1084    Some(
1085        CompileError::new(
1086            "bynk.resolve.name_conflict",
1087            name.span,
1088            format!("{what} `{key}` conflicts with {other_article} {other} of the same name"),
1089        )
1090        .with_label(prev, format!("{other} declared here"))
1091        .with_note(
1092            "a unit's services, agents, providers, functions, assets, types and events share one namespace",
1093        ),
1094    )
1095}
1096
1097/// The synthetic `FnDecl` [`build_unit_table`] registers for a messages-bearing
1098/// commons. Its body is never checked (see the call site's comment) — it
1099/// exists only so `Param`/`TypeRef`/`FnDecl` construction has somewhere to put
1100/// a syntactically valid placeholder.
1101fn synthetic_render_fn() -> FnDecl {
1102    let span = Span::default();
1103    FnDecl {
1104        type_params: Vec::new(),
1105        name: FnName::Free(Ident {
1106            name: "render".to_string(),
1107            span,
1108        }),
1109        params: vec![
1110            Param {
1111                name: Ident {
1112                    name: "tag".to_string(),
1113                    span,
1114                },
1115                type_ref: TypeRef::Named(Ident {
1116                    name: "LocaleTag".to_string(),
1117                    span,
1118                }),
1119                span,
1120            },
1121            Param {
1122                name: Ident {
1123                    name: "msg".to_string(),
1124                    span,
1125                },
1126                type_ref: TypeRef::Named(Ident {
1127                    name: "Message".to_string(),
1128                    span,
1129                }),
1130                span,
1131            },
1132        ],
1133        return_type: TypeRef::Base(BaseType::String, span),
1134        requires: Vec::new(),
1135        ensures: Vec::new(),
1136        body: Block {
1137            statements: Vec::new(),
1138            tail: Box::new(Expr {
1139                id: ExprId::SYNTHETIC,
1140                kind: ExprKind::StrLit(String::new()),
1141                span,
1142            }),
1143            span,
1144            tail_leading_comments: Vec::new(),
1145            implicit_tail: false,
1146        },
1147        has_self: false,
1148        documentation: None,
1149        span,
1150        trivia: Trivia::default(),
1151    }
1152}
1153
1154/// #1824: whether `f` is the synthetic `render` [`build_unit_table`] registers
1155/// for a messages-bearing commons. It stands in for `bynk.locale`'s `render`,
1156/// and its `LocaleTag`/`Message` mean `bynk.locale`'s, whatever the commons
1157/// itself declares under those names: the emitted bundle names the stdlib
1158/// types (#1697).
1159pub fn is_synthetic_render(f: &FnDecl) -> bool {
1160    matches!(&f.name, FnName::Free(n) if n.name == "render")
1161        && f.span == Span::default()
1162        && f.body.tail.id == ExprId::SYNTHETIC
1163}
1164
1165/// For each name declared in the unit (type, fn, method, agent), record which
1166/// source file declared it. Used by the emitter to render relative imports.
1167#[derive(Clone)]
1168pub struct FileDeclIndex {
1169    pub types: HashMap<String, PathBuf>,
1170    pub fns: HashMap<String, PathBuf>,
1171    pub methods: HashMap<String, HashMap<String, PathBuf>>,
1172    /// #1820: an agent's declaring file. A sibling file that constructs it
1173    /// calls its factory, `__make<Agent>`, which that file exports.
1174    pub agents: HashMap<String, PathBuf>,
1175    /// #1820: a capability's declaring file. A sibling file names the
1176    /// capability's interface in a handler's `deps` and a provider's
1177    /// `implements`, so it imports the type from there.
1178    pub capabilities: HashMap<String, PathBuf>,
1179}
1180
1181/// **Tree-relative, deliberately.** This is an *emit* structure, not an index:
1182/// `record_name_ref` compares these paths against `ctx.source_path`
1183/// (`emitter.rs`), which is the file's `include`-root-relative path. Keying it
1184/// by `identity_path` (ADR 0198) makes `path != &ctx.source_path` always true
1185/// for a split project, so a name declared in the *same* file is emitted as a
1186/// sibling import of itself — the module then cannot load, and a workers
1187/// runtime test hangs rather than fails. See ADR 0201 (E).
1188pub fn build_file_decl_index(indices: &[usize], parsed: &[ParsedFile]) -> FileDeclIndex {
1189    let mut idx = FileDeclIndex {
1190        types: HashMap::new(),
1191        fns: HashMap::new(),
1192        methods: HashMap::new(),
1193        agents: HashMap::new(),
1194        capabilities: HashMap::new(),
1195    };
1196    for &i in indices {
1197        let path = parsed[i].source_path();
1198        for item in parsed[i].items() {
1199            match item {
1200                CommonsItem::Type(t) => {
1201                    idx.types
1202                        .entry(t.name.name.clone())
1203                        .or_insert_with(|| path.clone());
1204                }
1205                // Events track, slice 0 (spine #936): an `event` name shares
1206                // the `types` file index — it registers into the same
1207                // `types` symbol table as an ordinary `type` everywhere else
1208                // in this module.
1209                CommonsItem::Event(e) => {
1210                    idx.types
1211                        .entry(e.name.name.clone())
1212                        .or_insert_with(|| path.clone());
1213                }
1214                CommonsItem::Fn(f) => match &f.name {
1215                    FnName::Free(id) => {
1216                        idx.fns
1217                            .entry(id.name.clone())
1218                            .or_insert_with(|| path.clone());
1219                    }
1220                    FnName::Method {
1221                        type_name,
1222                        method_name,
1223                    } => {
1224                        idx.methods
1225                            .entry(type_name.name.clone())
1226                            .or_default()
1227                            .entry(method_name.name.clone())
1228                            .or_insert_with(|| path.clone());
1229                    }
1230                },
1231                CommonsItem::Agent(a) => {
1232                    idx.agents
1233                        .entry(a.name.name.clone())
1234                        .or_insert_with(|| path.clone());
1235                }
1236                CommonsItem::Capability(c) => {
1237                    idx.capabilities
1238                        .entry(c.name.name.clone())
1239                        .or_insert_with(|| path.clone());
1240                }
1241                CommonsItem::Provider(_)
1242                | CommonsItem::Service(_)
1243                | CommonsItem::Actor(_)
1244                // `messages` bundles aren't cross-file-imported by name in
1245                // slice 1 (no multi-file bundle merge yet).
1246                | CommonsItem::Messages(_) => {}
1247                // #1883, slice 1b: an asset is an exported constant of its
1248                // file, in the function namespace, so a sibling file or a
1249                // `uses`-ing unit imports it the way it imports a function.
1250                CommonsItem::Asset(a) => {
1251                    idx.fns
1252                        .entry(a.name.name.clone())
1253                        .or_insert_with(|| path.clone());
1254                }
1255                // #2387, slice 2: an embed is an exported constant of its
1256                // file, in the function namespace, imported like an asset.
1257                CommonsItem::Embed(e) => {
1258                    idx.fns
1259                        .entry(e.name.name.clone())
1260                        .or_insert_with(|| path.clone());
1261                }
1262            }
1263        }
1264    }
1265    idx
1266}
1267
1268/// #696: returns the `parsed` index of the owning file alongside the `uses`
1269/// clause span, so the caller can attribute the diagnostic to that file.
1270pub fn uses_span_of(
1271    parsed: &[ParsedFile],
1272    indices: &[usize],
1273    target: &str,
1274) -> Option<(usize, Span)> {
1275    for &i in indices {
1276        for u in parsed[i].uses() {
1277            if u.target.joined() == target {
1278                return Some((i, u.span));
1279            }
1280        }
1281    }
1282    None
1283}
1284
1285/// Build the [`resolver::CrossContextInfo`] for a given consuming context.
1286/// Used by both the resolver/checker (per-file processing) and the emitter
1287/// (composition root + boundary casts).
1288pub fn build_cross_context_info(
1289    name: &str,
1290    unit_consumes: &HashMap<String, Vec<String>>,
1291    unit_consumes_aliases: &HashMap<String, HashMap<String, String>>,
1292    unit_uses: &HashMap<String, Vec<String>>,
1293    unit_tables: &HashMap<String, UnitTable>,
1294) -> resolver::CrossContextInfo {
1295    let consumed_contexts: Vec<String> = unit_consumes.get(name).cloned().unwrap_or_default();
1296    let aliases: HashMap<String, String> =
1297        unit_consumes_aliases.get(name).cloned().unwrap_or_default();
1298    let mut consumed_services: HashMap<String, HashMap<String, resolver::CrossContextService>> =
1299        HashMap::new();
1300    let mut consumed_types: HashMap<String, HashMap<String, Arc<TypeDecl>>> = HashMap::new();
1301    let mut consumed_capabilities: HashMap<
1302        String,
1303        HashMap<String, resolver::CrossContextCapability>,
1304    > = HashMap::new();
1305    // Events track, slice 0 (spine #936): each consumed context's own event
1306    // names, so a subscriber's `from Events(E)` can be checked against a
1307    // foreign owner too — mirrors `discover_event_subscribers` below, which
1308    // already resolves ownership this same way for wiring.
1309    let mut consumed_event_names: HashMap<String, std::collections::HashSet<String>> =
1310        HashMap::new();
1311    let mut consumed_type_owners: HashMap<String, HashMap<String, String>> = HashMap::new();
1312    for t in &consumed_contexts {
1313        let (other_types_combined, owners) =
1314            contract_types_for(t, unit_tables, unit_uses, unit_consumes);
1315        consumed_types.insert(t.clone(), other_types_combined);
1316        if !owners.is_empty() {
1317            consumed_type_owners.insert(t.clone(), owners);
1318        }
1319        let Some(other_table) = unit_tables.get(t) else {
1320            continue;
1321        };
1322        consumed_event_names.insert(t.clone(), other_table.events.keys().cloned().collect());
1323        let mut svcs: HashMap<String, resolver::CrossContextService> = HashMap::new();
1324        for (sname, sdecl) in &other_table.services {
1325            if let Some(mut svc) = resolver::cross_context_service_for(sname, sdecl) {
1326                svc.foreign_callers = sdecl
1327                    .handlers
1328                    .iter()
1329                    .find_map(|h| crate::actors::call_admission_for(h, &other_table.actors))
1330                    .map(|a| a.foreign.into_iter().map(|f| f.actor).collect())
1331                    .unwrap_or_default();
1332                svcs.insert(sname.clone(), svc);
1333            }
1334        }
1335        consumed_services.insert(t.clone(), svcs);
1336
1337        // v0.15: gather the consumed context's exported capabilities, each
1338        // paired with the provider that implements it.
1339        let mut caps: HashMap<String, resolver::CrossContextCapability> = HashMap::new();
1340        for cap_name in &other_table.exported_capabilities {
1341            let Some(decl) = other_table.capabilities.get(cap_name) else {
1342                continue;
1343            };
1344            let Some(provider) = other_table.providers.get(cap_name) else {
1345                continue;
1346            };
1347            let ops = decl
1348                .ops
1349                .iter()
1350                .map(|op| resolver::CrossContextCapabilityOp {
1351                    name: op.name.name.clone(),
1352                    type_params: op.type_params.iter().map(|p| p.name.name.clone()).collect(),
1353                    params: op
1354                        .params
1355                        .iter()
1356                        .map(|p| (p.name.name.clone(), p.type_ref.clone()))
1357                        .collect(),
1358                    return_type: op.return_type.clone(),
1359                })
1360                .collect();
1361            caps.insert(
1362                cap_name.clone(),
1363                resolver::CrossContextCapability {
1364                    name: cap_name.clone(),
1365                    ops,
1366                    provider_name: provider.provider_name.name.clone(),
1367                    provider_given: provider
1368                        .given
1369                        .iter()
1370                        .filter(|c| !c.is_cross_context())
1371                        .map(|c| c.key().to_string())
1372                        .collect(),
1373                    span: decl.span,
1374                },
1375            );
1376        }
1377        consumed_capabilities.insert(t.clone(), caps);
1378    }
1379    resolver::CrossContextInfo {
1380        self_context: Some(name.to_string()),
1381        consumed_contexts,
1382        aliases,
1383        consumed_services,
1384        consumed_types,
1385        // Set by the emitter (#1932).
1386        consumed_type_renames: HashMap::new(),
1387        consumed_capabilities,
1388        // Set by the caller from the unit's `consumes U { … }` clauses.
1389        flattened_caps: unit_tables
1390            .get(name)
1391            .map(|t| t.flattened_caps.clone())
1392            .unwrap_or_default(),
1393        consumed_event_names,
1394        consumed_type_owners,
1395    }
1396}
1397
1398/// Events track, slice 0 (spine #936): project-wide "who subscribes to what"
1399/// — for every `service ... from Events(E) { on event ... }` anywhere in the
1400/// project, resolve `E` to its *owning* context (the one whose `events` table
1401/// actually declares it — bare-name resolution, since `TypeRef` has no dotted
1402/// form) and group subscribers under `(owning_context, event_type_name)`. No
1403/// prior art to reuse: cross-context wiring elsewhere is driven by an
1404/// explicit author-written `consumes` clause, resolved eagerly in
1405/// `phase_resolve_consumes`; this is the first wiring driven by an *implicit*
1406/// relationship (a bare event-type name shared between a publisher's `event`
1407/// declaration and a subscriber's `from Events(E)` header) — the same
1408/// ownership resolution [`build_cross_context_info`]'s own `consumed_event_names`
1409/// performs above, for the same reason (#936, events track slice 0).
1410pub fn discover_event_subscribers(
1411    unit_tables: &HashMap<String, UnitTable>,
1412    unit_consumes: &HashMap<String, Vec<String>>,
1413) -> BTreeMap<(String, String), Vec<(String, String)>> {
1414    let mut out: BTreeMap<(String, String), Vec<(String, String)>> = BTreeMap::new();
1415    for (ctx_name, table) in unit_tables {
1416        for (svc_name, svc) in &table.services {
1417            let ServiceProtocol::Events { event_type, .. } = &svc.protocol else {
1418                continue;
1419            };
1420            let TypeRef::Named(id) = event_type else {
1421                continue;
1422            };
1423            let name = &id.name;
1424            let owner = if table.events.contains_key(name) {
1425                Some(ctx_name.clone())
1426            } else {
1427                unit_consumes.get(ctx_name).and_then(|consumed| {
1428                    consumed
1429                        .iter()
1430                        .find(|c| {
1431                            unit_tables
1432                                .get(c.as_str())
1433                                .is_some_and(|t| t.events.contains_key(name))
1434                        })
1435                        .cloned()
1436                })
1437            };
1438            if let Some(owner) = owner {
1439                out.entry((owner, name.clone()))
1440                    .or_default()
1441                    .push((ctx_name.clone(), svc_name.clone()));
1442            }
1443        }
1444    }
1445    // `unit_tables`/`table.services` are `HashMap`s, so the pushes above race
1446    // across builds — a multi-subscriber event's dispatch order (and so the
1447    // emitted `__eventsDispatch` closure's `await sub1; await sub2;`
1448    // sequence) would otherwise vary build to build with no source change.
1449    for subs in out.values_mut() {
1450        subs.sort();
1451    }
1452    out
1453}
1454
1455/// P6.x (#1137): one context's own cron expressions (`on cron "expr"`) and
1456/// queue names (`from queue("name")`), sorted and deduped — the two `wrangler.toml`
1457/// binding lists a context's own compose entry needs. `v0.44`: one queue binding
1458/// per service, on the `from queue(...)` header.
1459pub fn cron_and_queue_triggers(table: &UnitTable) -> (Vec<String>, Vec<String>) {
1460    let mut crons: Vec<String> = Vec::new();
1461    let mut queues: Vec<String> = Vec::new();
1462    for service in table.services.values() {
1463        for handler in &service.handlers {
1464            if let HandlerKind::Cron { expr } = &handler.kind {
1465                crons.push(expr.clone());
1466            }
1467        }
1468        if let ServiceProtocol::Queue { name } = &service.protocol {
1469            queues.push(name.clone());
1470        }
1471    }
1472    crons.sort();
1473    crons.dedup();
1474    queues.sort();
1475    queues.dedup();
1476    (crons, queues)
1477}
1478
1479/// v0.15: validate one `given` capability reference. A bare reference must name
1480/// a capability declared in this context; a cross-context reference (`given
1481/// B.Cap`) must name a capability the consumed context exports. Returns the
1482/// local [`CapabilityInfo`] to add to the in-scope map for bare references;
1483/// cross-context references return `None` (their calls are type-checked via
1484/// `consumed_capabilities` at the call site) but are still validated here.
1485/// v0.25: record a clause-position capability reference (`provides Cap`,
1486/// bare `given Cap`), qualifying a flattened bare name to its providing
1487/// unit. The span is the name segment only.
1488pub fn record_capability_clause_ref(
1489    name: &Ident,
1490    cross_context: &resolver::CrossContextInfo,
1491    refs: &mut RefSink,
1492) {
1493    record_capability_clause_ref_inner(name, cross_context, refs, false);
1494}
1495
1496/// v0.35 (ADR 0068): the `Cap` of a `provides Cap = Provider` clause — a
1497/// capability reference *and* an implementation edge (the ambient owner is the
1498/// provider). Flagged so assembly can tell it apart from the provider's own
1499/// `given` deps, which are capability refs owned by the same provider.
1500pub fn record_provides_clause_ref(
1501    name: &Ident,
1502    cross_context: &resolver::CrossContextInfo,
1503    refs: &mut RefSink,
1504) {
1505    record_capability_clause_ref_inner(name, cross_context, refs, true);
1506}
1507
1508fn record_capability_clause_ref_inner(
1509    name: &Ident,
1510    cross_context: &resolver::CrossContextInfo,
1511    refs: &mut RefSink,
1512    provides: bool,
1513) {
1514    let unit = cross_context.flattened_caps.get(&name.name);
1515    if provides {
1516        refs.record_provides(name.span, &name.name, unit.map(String::as_str));
1517    } else if let Some(unit) = unit {
1518        refs.record_in_unit(name.span, SymbolKind::Capability, &name.name, unit);
1519    } else {
1520        refs.record(name.span, SymbolKind::Capability, &name.name);
1521    }
1522}
1523
1524pub fn resolve_given_cap_ref(
1525    cap_ref: &CapRef,
1526    capability_info_map: &HashMap<String, CapabilityInfo>,
1527    cross_context: &resolver::CrossContextInfo,
1528    errors: &mut Vec<CompileError>,
1529    refs: &mut RefSink,
1530) -> Option<CapabilityInfo> {
1531    let Some(prefix) = cap_ref.prefix() else {
1532        // Local capability.
1533        match capability_info_map.get(cap_ref.key()) {
1534            Some(info) => {
1535                record_capability_clause_ref(&cap_ref.name, cross_context, refs);
1536                // Interop I5 (#1972): a kind every use of which needs an
1537                // option (a `Service` names its Worker) is reached only
1538                // through a named instance. Still resolved, so the handler's
1539                // calls are not refused a second time as undeclared.
1540                if let Some(unit) = cross_context.flattened_caps.get(cap_ref.key())
1541                    && let Some(kind) = crate::firstparty::instantiable(unit, cap_ref.key())
1542                    && !kind.bare
1543                {
1544                    let example = crate::project_model::instance_example(kind);
1545                    errors.push(
1546                        CompileError::new(
1547                            "bynk.capability.instance_only",
1548                            cap_ref.span,
1549                            format!(
1550                                "`{unit}.{}` is used only through a named instance",
1551                                cap_ref.key()
1552                            ),
1553                        )
1554                        .with_note(format!("declare one and use it instead: `{example}`")),
1555                    );
1556                }
1557                return Some(info.clone());
1558            }
1559            None => {
1560                errors.push(CompileError::new(
1561                    "bynk.given.unknown_capability",
1562                    cap_ref.span,
1563                    format!(
1564                        "capability `{}` is not declared in this context",
1565                        cap_ref.key()
1566                    ),
1567                ));
1568                return None;
1569            }
1570        }
1571    };
1572    // Cross-context capability (`given B.Cap` / `given Alias.Cap`).
1573    let Some(ctx_name) = cross_context.resolve_prefix(&prefix) else {
1574        errors.push(
1575            CompileError::new(
1576                "bynk.resolve.unconsumed_context",
1577                cap_ref.span,
1578                format!(
1579                    "`given {}.{}` refers to a context that this context does not `consumes`",
1580                    prefix,
1581                    cap_ref.key()
1582                ),
1583            )
1584            .with_note(
1585                "add a `consumes` clause for the providing context (optionally with an alias) at the top of this context",
1586            ),
1587        );
1588        return None;
1589    };
1590    let exports_it = cross_context
1591        .consumed_capabilities
1592        .get(&ctx_name)
1593        .is_some_and(|m| m.contains_key(cap_ref.key()));
1594    if exports_it {
1595        // v0.25: dotted `given B.Cap` — the name segment, in the consumed
1596        // unit's namespace.
1597        refs.record_in_unit(
1598            cap_ref.name.span,
1599            SymbolKind::Capability,
1600            cap_ref.key(),
1601            &ctx_name,
1602        );
1603    }
1604    if !exports_it {
1605        errors.push(
1606            CompileError::new(
1607                "bynk.given.cross_context_unknown_capability",
1608                cap_ref.span,
1609                format!(
1610                    "context `{}` does not export a capability named `{}`",
1611                    ctx_name,
1612                    cap_ref.key()
1613                ),
1614            )
1615            .with_note(
1616                "the providing context must list the capability in an `exports capability { … }` clause",
1617            ),
1618        );
1619    }
1620    None
1621}
1622
1623/// Build the combined type table for `unit`: its own types merged with the
1624/// types of every commons it `uses`. Used by cross-context resolution so we
1625/// can resolve a consumed context's service signatures against that context's
1626/// own view of types (v0.6 §4.5).
1627/// v0.177 (#643): the callee's own type namespace — its local declarations plus
1628/// the commons types it `uses`.
1629///
1630/// Shared deliberately. The **caller** reaches this table through
1631/// `consumed_types[callee]` and the **callee** builds it for itself; both must
1632/// canonicalise the callee's contract from the *same* table or their hashes
1633/// diverge and every call 409s. Routing both through one function makes that
1634/// agreement structural rather than a thing to keep in step by hand.
1635/// #2073: the type table a context's service signatures resolve in, as a
1636/// caller and the context itself both see it: [`combined_types_for`] (its own
1637/// types and its `uses` commons'), plus the types of the contexts it consumes,
1638/// which a signature may name (`on call(o: Order)` in `shop.ledger`, `Order`
1639/// from `shop.order`). Its own types win a name clash. The second map names
1640/// the declaring context of each type that came from a consumed context.
1641///
1642/// A borrowed type's body resolves in its declaring context's view, not this
1643/// one's: `shop.order`'s `Order { region: Region }` names `shop.order`'s
1644/// `Region` even when `shop.ledger` declares a `Region` of its own. Where the
1645/// two would differ, the reference is renamed to a key qualified by the
1646/// declaring context (`shop_order__Region`) under which that context's
1647/// declaration is added, so canonicalising (the contract hash) and walking
1648/// the table both see the right shape. A name two consumed contexts both
1649/// declare is added bare for neither; a signature cannot name it bare
1650/// anyway (an exported clash is `bynk.consumes.name_conflict`), and a body
1651/// that reaches it does so through its own context's qualified key.
1652///
1653/// Both sides of a cross-context call build the contract hash from this one
1654/// table, so they agree by construction.
1655pub fn contract_types_for(
1656    unit: &str,
1657    unit_tables: &HashMap<String, UnitTable>,
1658    unit_uses: &HashMap<String, Vec<String>>,
1659    unit_consumes: &HashMap<String, Vec<String>>,
1660) -> (HashMap<String, Arc<TypeDecl>>, HashMap<String, String>) {
1661    let mut walk = ContractWalk::default();
1662    let (table, owners, _) =
1663        contract_types_inner(unit, unit_tables, unit_uses, unit_consumes, &mut walk);
1664    (table, owners)
1665}
1666
1667/// #2087: the name a user reads for a type `contract_types_for` may have
1668/// filed under a qualified key. A key is the declaring context's namespace,
1669/// `__`, and the declaration's own name (`shop_order__Code`); the declaration
1670/// carries the key as its name too, so every path that prints a type name to a
1671/// user goes through this, and reads `Code`. Any other name is returned as is:
1672/// a key's prefix is a lowercase namespace and its last segment a capitalised
1673/// type name, which a declared type name never is.
1674pub fn display_type_name(name: &str) -> &str {
1675    match name.rsplit_once("__") {
1676        Some((prefix, last))
1677            if prefix.starts_with(|c: char| c.is_ascii_lowercase())
1678                && prefix
1679                    .chars()
1680                    .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
1681                && last.starts_with(|c: char| c.is_ascii_uppercase()) =>
1682        {
1683            last
1684        }
1685        _ => name,
1686    }
1687}
1688
1689/// #2087: `text` (a contract's normal form, say) with every qualified key in
1690/// it shown by its declaration's own name ([`display_type_name`]).
1691pub fn display_type_names_in(text: &str) -> String {
1692    let mut out = String::with_capacity(text.len());
1693    let mut word = String::new();
1694    let flush = |word: &mut String, out: &mut String| {
1695        out.push_str(display_type_name(word));
1696        word.clear();
1697    };
1698    for c in text.chars() {
1699        if c.is_ascii_alphanumeric() || c == '_' {
1700            word.push(c);
1701        } else {
1702            flush(&mut word, &mut out);
1703            out.push(c);
1704        }
1705    }
1706    flush(&mut word, &mut out);
1707    out
1708}
1709
1710/// A context's contract table and the declaring context of each borrowed type.
1711type ContractView = (HashMap<String, Arc<TypeDecl>>, HashMap<String, String>);
1712
1713/// #2073 review: one `contract_types_for` walk. `visiting` breaks a `consumes`
1714/// cycle; `memo` holds each context's view once it is computed, so a layered
1715/// `consumes` DAG costs one walk per context, not one per path. A view that
1716/// met a cycle depends on where the walk entered it, so it is not memoised.
1717#[derive(Default)]
1718struct ContractWalk {
1719    visiting: std::collections::HashSet<String>,
1720    memo: HashMap<String, ContractView>,
1721}
1722
1723/// The view, its owners, and whether computing it met a `consumes` cycle.
1724fn contract_types_inner(
1725    unit: &str,
1726    unit_tables: &HashMap<String, UnitTable>,
1727    unit_uses: &HashMap<String, Vec<String>>,
1728    unit_consumes: &HashMap<String, Vec<String>>,
1729    walk: &mut ContractWalk,
1730) -> (
1731    HashMap<String, Arc<TypeDecl>>,
1732    HashMap<String, String>,
1733    bool,
1734) {
1735    if let Some((table, owners)) = walk.memo.get(unit) {
1736        return (table.clone(), owners.clone(), false);
1737    }
1738    let mut out = combined_types_for(unit, unit_tables, unit_uses);
1739    let mut owners: HashMap<String, String> = HashMap::new();
1740    if !walk.visiting.insert(unit.to_string()) {
1741        // A `consumes` cycle: stop at the unit's own view.
1742        return (out, owners, true);
1743    }
1744    let mut consumed: Vec<&String> = unit_consumes
1745        .get(unit)
1746        .into_iter()
1747        .flatten()
1748        .filter(|u| {
1749            unit_tables
1750                .get(*u)
1751                .is_some_and(|t| t.kind == Some(UnitKind::Context))
1752        })
1753        .collect();
1754    consumed.sort();
1755    consumed.dedup();
1756    // Each consumed context's own view, in which its types' bodies resolve.
1757    let mut cyclic = false;
1758    let views: HashMap<String, ContractView> = consumed
1759        .iter()
1760        .map(|u| {
1761            let (table, owners, met) =
1762                contract_types_inner(u, unit_tables, unit_uses, unit_consumes, walk);
1763            cyclic |= met;
1764            ((*u).clone(), (table, owners))
1765        })
1766        .collect();
1767    // The context that declares `r` as `owner`'s view resolves it: `owner`
1768    // itself, a context `owner` consumes, or none (a commons type, whose
1769    // codecs and namespace are the commons').
1770    let declarer = |owner: &str, r: &str, target: &Arc<TypeDecl>| -> Option<String> {
1771        if unit_tables
1772            .get(owner)
1773            .and_then(|t| t.types.get(r))
1774            .is_some_and(|d| Arc::ptr_eq(d, target) || d.span == target.span)
1775        {
1776            return Some(owner.to_string());
1777        }
1778        views.get(owner).and_then(|(_, o)| o.get(r).cloned())
1779    };
1780    let mut by_name: std::collections::BTreeMap<&String, Vec<&String>> = Default::default();
1781    for u in &consumed {
1782        for n in unit_tables[*u].types.keys() {
1783            if !out.contains_key(n) {
1784                by_name.entry(n).or_default().push(u);
1785            }
1786        }
1787    }
1788    let mut queue: Vec<(String, String)> = Vec::new();
1789    for (n, us) in by_name {
1790        if let [u] = us.as_slice() {
1791            out.insert(n.clone(), unit_tables[*u].types[n].clone());
1792            owners.insert(n.clone(), (*u).clone());
1793            queue.push((n.clone(), (*u).clone()));
1794        }
1795    }
1796    queue.sort();
1797    let same = |a: &Arc<TypeDecl>, b: &Arc<TypeDecl>| Arc::ptr_eq(a, b) || a.span == b.span;
1798    // `via`: the consumed context whose view the key came from, in which its
1799    // body's references resolve (the declarer, for `owners`, may be a context
1800    // `via` itself consumes).
1801    while let Some((key, via)) = queue.pop() {
1802        let owner = via;
1803        let Some((view, _)) = views.get(&owner) else {
1804            continue;
1805        };
1806        let decl = out[&key].clone();
1807        let mut subst: HashMap<String, String> = HashMap::new();
1808        for r in crate::project_model::type_decl_ref_names(&decl) {
1809            let Some(target) = view.get(&r) else {
1810                continue;
1811            };
1812            match out.get(&r) {
1813                Some(here) if same(here, target) => {}
1814                None => {
1815                    out.insert(r.clone(), target.clone());
1816                    if let Some(d) = declarer(&owner, &r, target) {
1817                        owners.insert(r.clone(), d);
1818                    }
1819                    queue.push((r, owner.clone()));
1820                }
1821                Some(_) => {
1822                    let d = declarer(&owner, &r, target);
1823                    let qualified = format!(
1824                        "{}__{r}",
1825                        d.as_deref().unwrap_or(owner.as_str()).replace('.', "_")
1826                    );
1827                    if !out.contains_key(&qualified) {
1828                        // #2087: the declaration takes its key as its name, so a
1829                        // type resolved through the key (`Ty::Named`'s name is
1830                        // the declaration's) looks up this declaration, not the
1831                        // unit's same-named one.
1832                        let mut renamed = (**target).clone();
1833                        renamed.name.name = qualified.clone();
1834                        out.insert(qualified.clone(), Arc::new(renamed));
1835                        if let Some(d) = d {
1836                            owners.insert(qualified.clone(), d);
1837                        }
1838                        queue.push((qualified.clone(), owner.clone()));
1839                    }
1840                    subst.insert(r, qualified);
1841                }
1842            }
1843        }
1844        if !subst.is_empty() {
1845            out.insert(key, Arc::new(rename_decl_refs(&decl, &subst)));
1846        }
1847    }
1848    walk.visiting.remove(unit);
1849    if !cyclic {
1850        walk.memo
1851            .insert(unit.to_string(), (out.clone(), owners.clone()));
1852    }
1853    (out, owners, cyclic)
1854}
1855
1856/// #2073: `decl` with every type it names renamed by `subst` (its own name
1857/// kept).
1858fn rename_decl_refs(decl: &TypeDecl, subst: &HashMap<String, String>) -> TypeDecl {
1859    let mut out = decl.clone();
1860    match &mut out.body {
1861        TypeBody::Record(r) => {
1862            for f in &mut r.fields {
1863                rename_type_ref(&mut f.type_ref, subst);
1864            }
1865        }
1866        TypeBody::Sum(sum) => {
1867            for v in &mut sum.variants {
1868                for f in &mut v.payload {
1869                    rename_type_ref(&mut f.type_ref, subst);
1870                }
1871            }
1872        }
1873        TypeBody::Refined { .. } | TypeBody::Opaque { .. } => {}
1874    }
1875    out
1876}
1877
1878/// #2073: rename every user type `r` names, in place. Exhaustive over
1879/// `TypeRef`, like [`crate::project_model::type_ref_names`].
1880fn rename_type_ref(r: &mut TypeRef, subst: &HashMap<String, String>) {
1881    match r {
1882        TypeRef::Named(id) => {
1883            if let Some(to) = subst.get(&id.name) {
1884                id.name = to.clone();
1885            }
1886        }
1887        TypeRef::App { name, args, .. } => {
1888            if let Some(to) = subst.get(&name.name) {
1889                name.name = to.clone();
1890            }
1891            for a in args {
1892                rename_type_ref(a, subst);
1893            }
1894        }
1895        TypeRef::Result(a, b, _) | TypeRef::Map(a, b, _) => {
1896            rename_type_ref(a, subst);
1897            rename_type_ref(b, subst);
1898        }
1899        TypeRef::Option(t, _)
1900        | TypeRef::Effect(t, _)
1901        | TypeRef::HttpResult(t, _)
1902        | TypeRef::List(t, _)
1903        | TypeRef::Query(t, _)
1904        | TypeRef::Stream(t, _)
1905        | TypeRef::Connection(t, _)
1906        | TypeRef::History(t, _) => rename_type_ref(t, subst),
1907        TypeRef::Fn(params, ret, _) => {
1908            for p in params {
1909                rename_type_ref(p, subst);
1910            }
1911            rename_type_ref(ret, subst);
1912        }
1913        TypeRef::Base(..)
1914        | TypeRef::QueueResult(_)
1915        | TypeRef::Asset(_)
1916        | TypeRef::RequestHeaders(_)
1917        | TypeRef::TokenClaims(_)
1918        | TypeRef::BoundaryRejection(_)
1919        | TypeRef::ValidationError(_)
1920        | TypeRef::JsonError(_)
1921        | TypeRef::Unit(_) => {}
1922    }
1923}
1924
1925pub fn combined_types_for(
1926    unit: &str,
1927    unit_tables: &HashMap<String, UnitTable>,
1928    unit_uses: &HashMap<String, Vec<String>>,
1929) -> HashMap<String, Arc<TypeDecl>> {
1930    let mut out: HashMap<String, Arc<TypeDecl>> = HashMap::new();
1931    if let Some(table) = unit_tables.get(unit) {
1932        for (n, d) in &table.types {
1933            out.insert(n.clone(), d.clone());
1934        }
1935    }
1936    if let Some(targets) = unit_uses.get(unit) {
1937        for t in targets {
1938            if let Some(used) = unit_tables.get(t) {
1939                for (n, d) in &used.types {
1940                    out.entry(n.clone()).or_insert_with(|| d.clone());
1941                }
1942            }
1943        }
1944    }
1945    out
1946}
1947
1948/// P6.18: a `uses`-imported type's own combined visible types (one level,
1949/// matching [`combined_types_for`]'s identical shape) — the narrow resolution
1950/// scope a signature-lowering pass needs for that unit's own attached
1951/// methods. Reimplemented against [`UnitInfo`] rather than calling
1952/// `combined_types_for` directly: that function takes the flat, project-wide
1953/// `unit_tables`/`unit_uses` maps a per-unit emission prologue doesn't thread
1954/// this deep (only the coarser, already-merged `unit_info` reaches there) —
1955/// rebuilding those two maps from `unit_info` on every call would be needless
1956/// O(units) cloning for a per-unit prologue already called once per emitted
1957/// unit.
1958pub fn combined_types_for_unit_info(
1959    unit: &str,
1960    unit_info: &BTreeMap<String, UnitInfo>,
1961) -> HashMap<String, Arc<TypeDecl>> {
1962    let mut out: HashMap<String, Arc<TypeDecl>> = HashMap::new();
1963    let Some(info) = unit_info.get(unit) else {
1964        return out;
1965    };
1966    for (n, d) in &info.table.types {
1967        out.insert(n.clone(), d.clone());
1968    }
1969    for t in &info.uses {
1970        if let Some(used) = unit_info.get(t) {
1971            for (n, d) in &used.table.types {
1972                out.entry(n.clone()).or_insert_with(|| d.clone());
1973            }
1974        }
1975    }
1976    out
1977}
1978
1979/// Locale capability track, slice 2 (#882): the message bundle a context's
1980/// `Locale.current()` negotiates against, auto-detected from the context's
1981/// *direct* `uses` (one level, not transitive — see [`combined_types_for`]
1982/// just above, the precedent for this rule). `None`/`One`/`Many` drive three
1983/// different behaviours: unchanged fixed-default `Locale`, real negotiation
1984/// wiring, or (when the context also consumes `Locale`)
1985/// `bynk.messages.multiple_message_bundles` — see `check_locale_bundle_ambiguity`
1986/// (`bynk-emit/src/project/validate.rs`) and the per-Worker composition loop
1987/// (`bynk-emit/src/project.rs`).
1988// `pub`, not `pub(crate)`: `MessageBundleInfo` appears in `emit_worker_compose`'s
1989// public signature (`bynk-emit/src/emitter/workers.rs`), which must expose
1990// types at least as visible as itself (matching `UnitTable`'s own `pub`).
1991pub enum ContextMessageBundle {
1992    /// No directly-`uses`d commons declares a `messages` block.
1993    None,
1994    /// Exactly one — the negotiable case.
1995    One(MessageBundleInfo),
1996    /// Two or more (each commons's own qualified name, for the diagnostic).
1997    Many(Vec<String>),
1998}
1999
2000pub struct MessageBundleInfo {
2001    /// The commons's qualified unit name (e.g. `"app.msgs"`).
2002    pub commons: String,
2003    /// Project-relative path of the file carrying the `@reference` block —
2004    /// the import target for `__messagesLocales`/`__messagesReferenceLocale`.
2005    /// (A bundle genuinely split across multiple files, per the track doc's
2006    /// own §4.1, is not correctly merged by `emit_messages_bundle` today —
2007    /// each file emits independently, `bynk-emit/src/project.rs`'s per-file
2008    /// `emit_items` loop — this detection mirrors that same file-scoped
2009    /// reality rather than a wider, currently-unimplemented merge.)
2010    pub source_path: PathBuf,
2011}
2012
2013/// Walks `ctx`'s own direct `uses` list for commons declaring a `messages`
2014/// bundle with exactly one `@reference` block (a bundle missing or
2015/// duplicating its own reference is already diagnosed by
2016/// `check_messages_bundles` — this function simply doesn't count it as
2017/// "found", rather than compounding an already-reported error).
2018pub fn detect_context_message_bundle(
2019    ctx: &str,
2020    unit_uses: &HashMap<String, Vec<String>>,
2021    groups: &BTreeMap<String, Vec<usize>>,
2022    kinds: &BTreeMap<String, UnitKind>,
2023    parsed: &[ParsedFile],
2024) -> ContextMessageBundle {
2025    let mut found: Vec<MessageBundleInfo> = Vec::new();
2026    for target in unit_uses.get(ctx).into_iter().flatten() {
2027        if kinds.get(target) != Some(&UnitKind::Commons) {
2028            continue;
2029        }
2030        let Some(indices) = groups.get(target) else {
2031            continue;
2032        };
2033        for &i in indices {
2034            let has_reference = parsed[i].items().iter().any(|item| {
2035                matches!(item, CommonsItem::Messages(m) if m.annotations.iter().any(|a| a.name.name == "reference"))
2036            });
2037            if has_reference {
2038                found.push(MessageBundleInfo {
2039                    commons: target.clone(),
2040                    source_path: parsed[i].source_path(),
2041                });
2042                break;
2043            }
2044        }
2045    }
2046    match found.len() {
2047        0 => ContextMessageBundle::None,
2048        1 => ContextMessageBundle::One(found.pop().expect("len == 1")),
2049        _ => ContextMessageBundle::Many(found.into_iter().map(|b| b.commons).collect()),
2050    }
2051}
2052
2053#[cfg(test)]
2054mod detect_context_message_bundle_tests {
2055    use super::*;
2056    use bynk_syntax::ast::{
2057        Annotation, Commons, CommonsForm, Context, MessagesDecl, QualifiedName, SourceUnit,
2058        UsesDecl,
2059    };
2060
2061    fn ident(name: &str) -> Ident {
2062        Ident {
2063            name: name.to_string(),
2064            span: Span::default(),
2065        }
2066    }
2067
2068    fn qualified(name: &str) -> QualifiedName {
2069        QualifiedName {
2070            parts: name.split('.').map(ident).collect(),
2071            span: Span::default(),
2072        }
2073    }
2074
2075    /// A commons `ParsedFile` declaring one `messages <tag>` block, its
2076    /// `@reference` annotation present or not. `source_path` is derived from
2077    /// `name` so each test bundle gets a distinct, recognisable import
2078    /// target — real content doesn't matter, only that a path exists.
2079    fn commons_with_messages(name: &str, tag: &str, is_reference: bool) -> ParsedFile {
2080        let annotations = if is_reference {
2081            vec![Annotation {
2082                name: ident("reference"),
2083                args: Vec::new(),
2084                span: Span::default(),
2085            }]
2086        } else {
2087            Vec::new()
2088        };
2089        let messages = MessagesDecl {
2090            tag: tag.to_string(),
2091            tag_span: Span::default(),
2092            annotations,
2093            entries: Vec::new(),
2094            documentation: None,
2095            span: Span::default(),
2096            trivia: Trivia::default(),
2097        };
2098        ParsedFile::new(
2099            PathBuf::from(format!("{}.bynk", name.replace('.', "/"))),
2100            PathBuf::from(format!("src/{}.bynk", name.replace('.', "/"))),
2101            None,
2102            String::new(),
2103            SourceUnit::Commons(Commons {
2104                name: qualified(name),
2105                items: vec![CommonsItem::Messages(messages)],
2106                uses: Vec::new(),
2107                documentation: None,
2108                form: CommonsForm::Brace,
2109                span: Span::default(),
2110                trivia: Trivia::default(),
2111                trailing_comments: Vec::new(),
2112            }),
2113            UnitKind::Commons,
2114            false,
2115        )
2116    }
2117
2118    /// A minimal context `ParsedFile` with no items of its own — only its
2119    /// `uses` list matters for this function.
2120    fn context_using(name: &str, targets: &[&str]) -> ParsedFile {
2121        ParsedFile::new(
2122            PathBuf::from(format!("{}.bynk", name.replace('.', "/"))),
2123            PathBuf::from(format!("src/{}.bynk", name.replace('.', "/"))),
2124            None,
2125            String::new(),
2126            SourceUnit::Context(Context {
2127                removed_agents: Vec::new(),
2128                name: qualified(name),
2129                uses: targets
2130                    .iter()
2131                    .map(|t| UsesDecl {
2132                        target: qualified(t),
2133                        span: Span::default(),
2134                        trivia: Trivia::default(),
2135                    })
2136                    .collect(),
2137                consumes: Vec::new(),
2138                exports: Vec::new(),
2139                items: Vec::new(),
2140                documentation: None,
2141                form: CommonsForm::Brace,
2142                span: Span::default(),
2143                trivia: Trivia::default(),
2144                trailing_comments: Vec::new(),
2145            }),
2146            UnitKind::Context,
2147            false,
2148        )
2149    }
2150
2151    /// The four tables `detect_context_message_bundle`'s real callers
2152    /// already build — bundled here so [`scenario`] doesn't need a
2153    /// clippy-unfriendly four-tuple return type.
2154    struct Scenario {
2155        parsed: Vec<ParsedFile>,
2156        groups: BTreeMap<String, Vec<usize>>,
2157        kinds: BTreeMap<String, UnitKind>,
2158        unit_uses: HashMap<String, Vec<String>>,
2159    }
2160
2161    /// Assembles a [`Scenario`] from a context plus its bundle files.
2162    fn scenario(ctx_name: &str, ctx_uses: &[&str], bundles: Vec<(&str, ParsedFile)>) -> Scenario {
2163        let mut parsed = vec![context_using(ctx_name, ctx_uses)];
2164        let mut groups: BTreeMap<String, Vec<usize>> = BTreeMap::new();
2165        let mut kinds: BTreeMap<String, UnitKind> = BTreeMap::new();
2166        groups.insert(ctx_name.to_string(), vec![0]);
2167        kinds.insert(ctx_name.to_string(), UnitKind::Context);
2168        for (name, pf) in bundles {
2169            let idx = parsed.len();
2170            parsed.push(pf);
2171            groups.entry(name.to_string()).or_default().push(idx);
2172            kinds.insert(name.to_string(), UnitKind::Commons);
2173        }
2174        let mut unit_uses: HashMap<String, Vec<String>> = HashMap::new();
2175        unit_uses.insert(
2176            ctx_name.to_string(),
2177            ctx_uses.iter().map(|s| s.to_string()).collect(),
2178        );
2179        Scenario {
2180            parsed,
2181            groups,
2182            kinds,
2183            unit_uses,
2184        }
2185    }
2186
2187    #[test]
2188    fn zero_bundles_when_uses_reaches_no_messages_commons() {
2189        let Scenario {
2190            parsed,
2191            groups,
2192            kinds,
2193            unit_uses,
2194        } = scenario("app.web", &["app.other"], vec![]);
2195        assert!(matches!(
2196            detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed),
2197            ContextMessageBundle::None
2198        ));
2199    }
2200
2201    #[test]
2202    fn zero_bundles_when_uses_is_empty() {
2203        let Scenario {
2204            parsed,
2205            groups,
2206            kinds,
2207            unit_uses,
2208        } = scenario("app.web", &[], vec![]);
2209        assert!(matches!(
2210            detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed),
2211            ContextMessageBundle::None
2212        ));
2213    }
2214
2215    #[test]
2216    fn one_bundle_is_found_by_its_reference_block() {
2217        let bundle = commons_with_messages("app.msgs", "en", true);
2218        let Scenario {
2219            parsed,
2220            groups,
2221            kinds,
2222            unit_uses,
2223        } = scenario("app.web", &["app.msgs"], vec![("app.msgs", bundle)]);
2224        let found = detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed);
2225        let ContextMessageBundle::One(info) = found else {
2226            panic!("expected exactly one bundle");
2227        };
2228        assert_eq!(info.commons, "app.msgs");
2229        assert_eq!(info.source_path, PathBuf::from("app/msgs.bynk"));
2230    }
2231
2232    #[test]
2233    fn a_bundle_missing_its_reference_block_is_not_counted() {
2234        // Not a `@reference` block — already diagnosed elsewhere
2235        // (`bynk.messages.missing_reference`); this function simply doesn't
2236        // count it, rather than compounding an already-reported error.
2237        let bundle = commons_with_messages("app.msgs", "en", false);
2238        let Scenario {
2239            parsed,
2240            groups,
2241            kinds,
2242            unit_uses,
2243        } = scenario("app.web", &["app.msgs"], vec![("app.msgs", bundle)]);
2244        assert!(matches!(
2245            detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed),
2246            ContextMessageBundle::None
2247        ));
2248    }
2249
2250    #[test]
2251    fn two_bundles_report_both_commons_names() {
2252        let a = commons_with_messages("app.msgs_a", "en", true);
2253        let b = commons_with_messages("app.msgs_b", "en", true);
2254        let Scenario {
2255            parsed,
2256            groups,
2257            kinds,
2258            unit_uses,
2259        } = scenario(
2260            "app.web",
2261            &["app.msgs_a", "app.msgs_b"],
2262            vec![("app.msgs_a", a), ("app.msgs_b", b)],
2263        );
2264        let found = detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed);
2265        let ContextMessageBundle::Many(names) = found else {
2266            panic!("expected two bundles");
2267        };
2268        let mut names = names;
2269        names.sort();
2270        assert_eq!(
2271            names,
2272            vec!["app.msgs_a".to_string(), "app.msgs_b".to_string()]
2273        );
2274    }
2275
2276    #[test]
2277    fn a_commons_reached_only_transitively_is_not_counted() {
2278        // `app.web` uses `app.mid`, which itself uses the bundle — `uses` is
2279        // one level, not transitive (message-bundles' own established rule),
2280        // so this must still report `None`.
2281        let bundle = commons_with_messages("app.msgs", "en", true);
2282        let mut mid = context_using("app.mid", &["app.msgs"]);
2283        // `app.mid` needs to be a commons for this scenario to be legal, but
2284        // `context_using` builds a Context — for this narrow test only the
2285        // `uses` *resolution* (does app.web's own direct list reach the
2286        // bundle) matters, and app.web's own list never names `app.msgs`
2287        // directly, so the unit kind of the intermediate is irrelevant.
2288        mid.set_kind(UnitKind::Commons);
2289        let Scenario {
2290            mut parsed,
2291            mut groups,
2292            mut kinds,
2293            unit_uses,
2294        } = scenario("app.web", &["app.mid"], vec![("app.msgs", bundle)]);
2295        let mid_idx = parsed.len();
2296        parsed.push(mid);
2297        groups.insert("app.mid".to_string(), vec![mid_idx]);
2298        kinds.insert("app.mid".to_string(), UnitKind::Commons);
2299        assert!(matches!(
2300            detect_context_message_bundle("app.web", &unit_uses, &groups, &kinds, &parsed),
2301            ContextMessageBundle::None
2302        ));
2303    }
2304}
2305
2306/// #696: returns the `parsed` index of the owning file alongside the `consumes`
2307/// clause span, so the caller can attribute the diagnostic to that file.
2308pub fn consumes_span_of(
2309    parsed: &[ParsedFile],
2310    indices: &[usize],
2311    target: &str,
2312) -> Option<(usize, Span)> {
2313    for &i in indices {
2314        for c in parsed[i].consumes() {
2315            if c.target.joined() == target {
2316                return Some((i, c.span));
2317            }
2318        }
2319    }
2320    None
2321}
2322
2323/// #696: returns the `parsed` index of the owning file alongside the alias span,
2324/// so the caller can attribute the diagnostic to that file.
2325pub fn parsed_alias_span(
2326    parsed: &[ParsedFile],
2327    indices: &[usize],
2328    alias: &str,
2329) -> Option<(usize, Span)> {
2330    for &i in indices {
2331        for c in parsed[i].consumes() {
2332            if let Some(a) = &c.alias
2333                && a.name == alias
2334            {
2335                return Some((i, a.span));
2336            }
2337        }
2338    }
2339    None
2340}
2341
2342/// A type imported into a context via `consumes`. Carries enough metadata for
2343/// the checker and emitter to enforce / express visibility.
2344#[derive(Debug, Clone)]
2345pub struct ConsumedType {
2346    pub owning_context: String,
2347    pub visibility: Visibility,
2348}
2349
2350/// #1925: every pair of top-level declarations sharing one name, in one
2351/// context, against the rule that a unit's services, agents, providers,
2352/// functions, assets, types and events share one namespace — the scope of the
2353/// emitted TypeScript module. Capabilities and actors keep their own.
2354#[cfg(test)]
2355mod value_namespace_tests {
2356    use super::*;
2357    use crate::testkit::analyse;
2358
2359    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
2360    enum Kind {
2361        Fn,
2362        Type,
2363        Event,
2364        Asset,
2365        Embed,
2366        Service,
2367        Agent,
2368        Provider,
2369        Capability,
2370        Actor,
2371    }
2372
2373    const ALL: [Kind; 10] = [
2374        Kind::Fn,
2375        Kind::Type,
2376        Kind::Event,
2377        Kind::Asset,
2378        Kind::Embed,
2379        Kind::Service,
2380        Kind::Agent,
2381        Kind::Provider,
2382        Kind::Capability,
2383        Kind::Actor,
2384    ];
2385
2386    /// The kind a declaration names, and so the side of the rule it takes.
2387    /// Exhaustive on purpose: a new declaration kind does not compile here
2388    /// until it is placed in or out of the value namespace (and in [`ALL`]).
2389    fn kind_of(item: &CommonsItem) -> Option<Kind> {
2390        match item {
2391            CommonsItem::Fn(_) => Some(Kind::Fn),
2392            CommonsItem::Type(_) => Some(Kind::Type),
2393            CommonsItem::Event(_) => Some(Kind::Event),
2394            CommonsItem::Asset(_) => Some(Kind::Asset),
2395            CommonsItem::Embed(_) => Some(Kind::Embed),
2396            CommonsItem::Service(_) => Some(Kind::Service),
2397            CommonsItem::Agent(_) => Some(Kind::Agent),
2398            CommonsItem::Provider(_) => Some(Kind::Provider),
2399            CommonsItem::Capability(_) => Some(Kind::Capability),
2400            CommonsItem::Actor(_) => Some(Kind::Actor),
2401            // Keyed by locale tag, in a commons only: it names nothing.
2402            CommonsItem::Messages(_) => None,
2403        }
2404    }
2405
2406    fn shares_the_value_namespace(k: Kind) -> bool {
2407        !matches!(k, Kind::Capability | Kind::Actor)
2408    }
2409
2410    /// A declaration of `kind` named `n`. A provider brings the capability it
2411    /// provides, under a name no other snippet uses.
2412    fn decl(kind: Kind, n: &str) -> String {
2413        match kind {
2414            Kind::Fn => format!("fn {n}() -> Int {{\n  1\n}}\n"),
2415            Kind::Type => format!("type {n} = {{ n: Int }}\n"),
2416            Kind::Event => format!("event {n} = {{ n: Int }}\n"),
2417            Kind::Asset => format!("asset {n} = \"assets/x.json\"\n"),
2418            Kind::Embed => format!("embed {n} = \"embeds/x.txt\"\n"),
2419            Kind::Service => {
2420                format!(
2421                    "service {n} {{\n  on call() -> Effect[Int] {{\n    Effect.pure(1)\n  }}\n}}\n"
2422                )
2423            }
2424            Kind::Agent => format!(
2425                "agent {n} {{\n  key id: String\n\n  store hits: Cell[Int]\n\n  on call ping() -> Effect[Int] {{\n    1\n  }}\n}}\n"
2426            ),
2427            Kind::Provider => format!(
2428                "capability ProbeCap {{\n  fn go() -> Effect[Int]\n}}\n\nprovides ProbeCap = {n} {{\n  fn go() -> Effect[Int] {{\n    Effect.pure(1)\n  }}\n}}\n"
2429            ),
2430            Kind::Capability => format!("capability {n} {{\n  fn go() -> Effect[Int]\n}}\n"),
2431            Kind::Actor => {
2432                format!("actor {n} {{ auth = Bearer(secret = \"S\"), identity = ProbeId }}\n")
2433            }
2434        }
2435    }
2436
2437    const HEADER: &str = "context app\n\ntype ProbeId = opaque String\n\n";
2438
2439    #[test]
2440    fn each_snippet_declares_its_kind() {
2441        for k in ALL {
2442            let src = format!("{HEADER}{}", decl(k, "Thing"));
2443            let tokens = bynk_syntax::lexer::tokenize(&src).expect("lexes");
2444            let unit = bynk_syntax::parser::parse_unit(&tokens, &src).expect("parses");
2445            let bynk_syntax::ast::SourceUnit::Context(c) = unit else {
2446                panic!("a context")
2447            };
2448            assert!(
2449                c.items.iter().any(|i| kind_of(i) == Some(k)),
2450                "the {k:?} snippet declares no {k:?}"
2451            );
2452        }
2453    }
2454
2455    #[test]
2456    fn every_pair_of_kinds_against_the_value_namespace() {
2457        for (x, &a) in ALL.iter().enumerate() {
2458            for &b in &ALL[x + 1..] {
2459                // Both declaration orders, under both casings.
2460                for (first, second) in [(a, b), (b, a)] {
2461                    for n in ["thing", "Thing"] {
2462                        let src = format!("{HEADER}{}\n{}", decl(first, n), decl(second, n));
2463                        let r = analyse(&[
2464                            ("app/main.bynk", &src),
2465                            ("assets/x.json", "{}"),
2466                            ("embeds/x.txt", "x"),
2467                        ]);
2468                        let cats = r.error_categories();
2469                        if shares_the_value_namespace(a) && shares_the_value_namespace(b) {
2470                            let want = if matches!((a, b), (Kind::Type, Kind::Event)) {
2471                                "bynk.resolve.duplicate_type"
2472                            } else {
2473                                "bynk.resolve.name_conflict"
2474                            };
2475                            assert_eq!(cats, vec![want], "{first:?} then {second:?} `{n}`");
2476                        } else {
2477                            assert!(
2478                                cats.is_empty(),
2479                                "{first:?} then {second:?} `{n}` should check clean: {cats:?}"
2480                            );
2481                        }
2482                    }
2483                }
2484            }
2485        }
2486    }
2487
2488    /// A context split across files is one namespace: the conflict is found
2489    /// whichever file declares which, and reported in the service's file.
2490    #[test]
2491    fn a_split_context_is_one_namespace() {
2492        for (svc_file, asset_file) in [("a.bynk", "z.bynk"), ("z.bynk", "a.bynk")] {
2493            let svc = format!("context app\n\n{}", decl(Kind::Service, "api"));
2494            let asset = format!("context app\n\n{}", decl(Kind::Asset, "api"));
2495            let svc_path = format!("app/{svc_file}");
2496            let asset_path = format!("app/{asset_file}");
2497            let r = analyse(&[
2498                (svc_path.as_str(), svc.as_str()),
2499                (asset_path.as_str(), asset.as_str()),
2500                ("assets/x.json", "{}"),
2501            ]);
2502            assert_eq!(r.error_categories(), vec!["bynk.resolve.name_conflict"]);
2503            let d = &r.analysis.errors[0];
2504            assert!(
2505                d.source_path
2506                    .as_deref()
2507                    .is_some_and(|p| p.ends_with(svc_file)),
2508                "reported in the service's file: {:?}",
2509                d.source_path
2510            );
2511        }
2512    }
2513
2514    /// Two bodied providers of different capabilities under one name each
2515    /// emit a class of that name.
2516    #[test]
2517    fn two_providers_under_one_name_conflict() {
2518        let src = "context app\n\ncapability A {\n  fn go() -> Effect[Int]\n}\n\ncapability B {\n  fn go() -> Effect[Int]\n}\n\nprovides A = Impl {\n  fn go() -> Effect[Int] {\n    Effect.pure(1)\n  }\n}\n\nprovides B = Impl {\n  fn go() -> Effect[Int] {\n    Effect.pure(2)\n  }\n}\n";
2519        let r = analyse(&[("app/main.bynk", src)]);
2520        assert_eq!(r.error_categories(), vec!["bynk.resolve.name_conflict"]);
2521    }
2522}
2523
2524/// #2073 review: a borrowed type's body resolves in its declaring context's
2525/// view, so the contract table hashes the shape the wire carries.
2526#[cfg(test)]
2527mod contract_types_tests {
2528    use crate::testkit::analyse;
2529
2530    const ORDER: &str = "context shop.order\n\nexports transparent { Order }\n\ntype Region = enum { Domestic, International }\n\ntype Order = { id: String, region: Region }\n";
2531    const POST: &str = "context shop.post\n\nexports transparent { Parcel }\n\ntype Region = enum { North, South }\n\ntype Parcel = { ref: String, region: Region }\n";
2532    const LEDGER: &str = "context shop.ledger\n\nconsumes shop.order\nconsumes shop.post\n\ntype Region = enum { Here, There }\n\nservice orderId {\n  on call(o: Order) -> Effect[String] {\n    o.id\n  }\n}\n\nservice parcelRef {\n  on call(p: Parcel) -> Effect[String] {\n    p.ref\n  }\n}\n";
2533
2534    fn normal_form(service: &str) -> String {
2535        let a = analyse(&[
2536            ("shop/order.bynk", ORDER),
2537            ("shop/post.bynk", POST),
2538            ("shop/ledger.bynk", LEDGER),
2539        ]);
2540        a.assert_clean();
2541        let info = &a.analysis.boundary_info["shop.ledger"];
2542        let svc = crate::resolver::cross_context_service_for(service, &info.services[service])
2543            .expect("an `on call` service");
2544        crate::contract::service_normal_form(&svc, &info.types)
2545    }
2546
2547    /// #2087: a qualified key reads as its declaration's own name wherever a
2548    /// type is shown to a user. `Ty::display` is the renderer behind every
2549    /// checker diagnostic that names a type.
2550    #[test]
2551    fn a_qualified_key_displays_as_its_declarations_name() {
2552        use super::{display_type_name, display_type_names_in};
2553        assert_eq!(display_type_name("shop_order__Code"), "Code");
2554        assert_eq!(display_type_name("shop_ledger__shop_order__Code"), "Code");
2555        assert_eq!(display_type_name("Code"), "Code");
2556        assert_eq!(display_type_name("My__Thing"), "My__Thing");
2557        assert_eq!(display_type_name("shop_order__lower"), "shop_order__lower");
2558        assert_eq!(
2559            display_type_names_in("orderId(o: Order{code: shop_order__Code(String)})"),
2560            "orderId(o: Order{code: Code(String)})"
2561        );
2562        let tys = crate::checker::Types::default();
2563        let key = tys.intern(crate::checker::Ty::Named {
2564            name: "shop_order__Code".to_string(),
2565            kind: crate::checker::NamedKind::Opaque(bynk_syntax::ast::BaseType::String),
2566            args: Vec::new(),
2567        });
2568        assert_eq!(tys.display(key), "Code");
2569        let listed = tys.intern(crate::checker::Ty::List(key));
2570        assert_eq!(tys.display(listed), "List[Code]");
2571    }
2572
2573    #[test]
2574    fn a_borrowed_record_names_its_own_contexts_field_type() {
2575        let order = normal_form("orderId");
2576        assert!(
2577            order.contains("Domestic") && !order.contains("Here") && !order.contains("North"),
2578            "{order}"
2579        );
2580        let parcel = normal_form("parcelRef");
2581        assert!(
2582            parcel.contains("North") && !parcel.contains("Here") && !parcel.contains("Domestic"),
2583            "{parcel}"
2584        );
2585    }
2586}