brink_analyzer/signature.rs
1//! `signature(def)` — per-declaration signature summary.
2//!
3//! Phase-0 **stub** of the `signature(DefId)` query (scripting-substrate
4//! spec §4, layer 2): it carries only what is already derivable from the
5//! declaration itself — name, kind, declared params, the initializer-inferred
6//! type (the same inference as [`crate::external_check::infer_value_meta`]),
7//! and the `#@local` flow-private bit. No checking, no body analysis. The
8//! future type checker reads this as its firewall unit.
9
10use std::sync::Arc;
11
12use brink_format::DefinitionId;
13use brink_ir::hir::{Expr, HirFile};
14use brink_ir::{FileId, HostManifest, ParamInfo, SymbolIndex, SymbolKind};
15
16use crate::annotations::resolve as resolve_annotation;
17use crate::external_check::{InferredType, infer_literal_type};
18use crate::infer::Ty;
19use crate::resolve::{BareItemResult, lookup_by_name, lookup_list_item_bare};
20
21/// Per-declaration signature summary (phase-0 stub).
22///
23/// Everything here is declaration-derived: a body edit that doesn't touch
24/// the declaration line(s) must not change a `Sig`.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Sig {
27 /// Canonical/qualified name, as indexed (e.g. `knot.stitch`).
28 pub name: String,
29 /// Declaration kind.
30 pub kind: SymbolKind,
31 /// Declared parameters (knots, stitches, externals).
32 pub params: Vec<ParamInfo>,
33 /// Type inferred from the initializer literal (VAR/CONST only), if the
34 /// initializer is one. TM-2 (docs/typed-mode-spec.md §3): when the
35 /// `VAR`/`CONST` also carries a `: type` annotation and that annotation
36 /// resolves to a representable [`InferredType`], the annotation
37 /// *replaces* this value — annotation wins over inference (the
38 /// firewall rule) — so every existing consumer of `value_type`
39 /// (notably `infer::collect_globals`) picks up the annotated type
40 /// automatically, with no seam change.
41 ///
42 /// [`local_signature`]'s `Param`/`Temp` locals populate this field too
43 /// (issue #530) — there it is never initializer-inferred (a local has
44 /// no initializer-literal path here), only the downcast of the local's
45 /// own TM-2 `: type` annotation, `None` when unannotated or unresolved.
46 pub value_type: Option<InferredType>,
47 /// A VAR/CONST's declaration-derived type at **full [`Ty`] fidelity**
48 /// (issue #1540) — `None` for every other `signature`-produced symbol
49 /// kind, and for a VAR/CONST whose declaration determines no type at
50 /// all. [`local_signature`] (issue #530) is the one other producer that
51 /// populates this field: for a `Param`/`Temp`, it is the local's own
52 /// TM-2 `: type` annotation resolved the same way, `None` when the
53 /// local is unannotated — this is precisely the field
54 /// `brink-ide::hover::inferred_local_type_str` reads via
55 /// `db.local_signature`.
56 ///
57 /// This is the field every *typed* consumer reads (`collect_globals` in
58 /// both this crate and `brink-db`'s narrowed mirror); `value_type` above
59 /// stays exactly as narrow as it was, because it is the wire-adjacent
60 /// domain `infer_value_meta`/hover share and widening it would leak
61 /// `Array`/`Map`/`Struct`/`Fn`/`Option`/`Range` into that schema.
62 ///
63 /// Populated in TM-2 firewall order — annotation first, then the
64 /// initializer:
65 /// - an explicit `: type` annotation on the VAR/CONST, resolved by
66 /// [`crate::annotations::resolve`] with **no downcast** (so
67 /// `Array<int>`, `Map<string, int>`, a declared `STRUCT` name,
68 /// `fn(T…): R`, `Handle<K>` and (since issue #1552) `Option<T>`/
69 /// `Weighted<T>` all survive — this is the `ty_to_inferred_type` gap
70 /// issue #1540 closes. `range` still has **no annotation grammar at
71 /// all** ([`crate::annotations::resolve`] has no arm for it — deferred
72 /// pending demonstrated demand, `docs/decision-log.md` 2026-07-27), so
73 /// a `Ty::Range` value can never actually reach this field yet;
74 /// - else the initializer literal, at the same fidelity
75 /// ([`literal_ty`]): `#[…]` → `Ty::Array`, `#{…}` → `Ty::Map`,
76 /// `Name#{…}` → `Ty::Struct`, plus every scalar/`List<L>` form
77 /// `infer_literal_type` already covered;
78 /// - else a fn-value initializer ([`declared_fn_type`]) — a bare
79 /// `#fn(target, args…)` literal (T1c follow-up, issue #712,
80 /// docs/t1c-spec.md §4), or, on the **native** surface only, a bare
81 /// name resolving to a function definition (docs/t1c-spec.md §2a,
82 /// issue #1895; zero bound args, since the binding form has no native
83 /// spelling). Either way the type is the bound prefix consumed from
84 /// `target`'s *own* declaration-derived signature
85 /// (`param_annotations`/`return_annotation` — a second, single-level
86 /// `signature()` call, never the target's body). An unannotated target
87 /// param/return reads as `Ty::Unknown` in the row, the same
88 /// conservative fallback declaration-derived typing always uses.
89 pub value_ty: Option<Ty>,
90 /// Marked flow-private via a `#@local` directive (knots, stitches, VARs).
91 pub is_local: bool,
92 /// TM-2 (docs/typed-mode-spec.md §3): declared param type annotations,
93 /// positional (parallel to `params`) — `None` per-slot for an
94 /// unannotated param, or one whose annotation doesn't resolve to a `Ty`
95 /// (`void`, `fn(...)`, an unrecognized name — `crate::check_annotations`
96 /// reports those separately). Knots/stitches only; empty for other
97 /// symbol kinds.
98 pub param_annotations: Vec<Option<Ty>>,
99 /// TM-2: the function-header/stitch-header return type annotation
100 /// (`): type ===` on a knot, `: type` on a stitch — #1509 widened the
101 /// grammar to stitches), resolved. `None` when absent, `void`, or
102 /// unresolved. Knots and stitches only.
103 pub return_annotation: Option<Ty>,
104}
105
106/// Downcast a resolved [`Ty`] to [`InferredType`] where the two universes
107/// overlap (`Ty`'s scalar leaves plus nominal `List<L>`) — the annotation-
108/// wins substitution for `Sig::value_type`, which predates TM-2 and only
109/// has room for `InferredType`'s narrower domain (hover + the `@brink-lang/web`
110/// program model read it; widening it would change that schema).
111///
112/// Every `Ty` outside that overlap — `Array<T>`, `Map<K, V>`, a nominal
113/// `STRUCT`, `fn(T…): R`, `Handle<K>`, `Option<T>`, `range`, `Weighted<T>`,
114/// a tower kind — is **not** dropped: since issue #1540 it is carried at
115/// full fidelity on [`Sig::value_ty`], which is what `infer::collect_globals`
116/// (and `brink-db`'s narrowed mirror of it) reads to give a global its static
117/// type. `param_annotations`/`return_annotation`/`crate::resolve_annotation`
118/// remain the equivalent full-fidelity surfaces for knots/stitches and for
119/// any bare `TypeExpr`.
120fn ty_to_inferred_type(ty: &Ty) -> Option<InferredType> {
121 match ty {
122 Ty::Int => Some(InferredType::Int),
123 Ty::Float => Some(InferredType::Float),
124 Ty::Bool => Some(InferredType::Bool),
125 Ty::String => Some(InferredType::String),
126 Ty::Divert => Some(InferredType::Divert),
127 Ty::List(name) => Some(InferredType::List(name.clone())),
128 // `Conflicted` (#627): a genuine type conflict has no representable
129 // `InferredType` any more than `Unknown` does — this stub is a
130 // gradual/advisory consumer, so it reads both the same way.
131 // Everything below has no `InferredType` variant to downcast to —
132 // `Array`/`Map` (T1b), `Struct` (TM-4b), `Fn` (T1c), `Handle`
133 // (T1d-2), `Option` (NS-A1), `Range` (NS-A5), `Weighted` (NS-A7),
134 // `Tower` (NS-A8), `Content` (issue #1846). None of them is a silent
135 // drop: since issue #1540 each is carried whole on `Sig::value_ty`
136 // (see this function's doc), which is the field every typed
137 // consumer reads. Only the narrow, wire-adjacent `Sig::value_type`
138 // stops here.
139 Ty::Weighted(_)
140 | Ty::Tower(_)
141 | Ty::Array(_)
142 | Ty::Map(_, _)
143 | Ty::Struct(_)
144 | Ty::Fn(..)
145 | Ty::Handle(_)
146 | Ty::Option(_)
147 | Ty::Range { .. }
148 | Ty::Content
149 | Ty::Unknown
150 | Ty::Conflicted => None,
151 }
152}
153
154/// TM-2 firewall rule, shared by `VAR` and `CONST`: if `annotation` resolves
155/// to a representable [`InferredType`], it replaces `literal_type`; otherwise
156/// the literal-inferred type stands.
157fn value_type_with_annotation_override(
158 literal_type: Option<InferredType>,
159 annotation: Option<&brink_ir::TypeExpr>,
160 names: &crate::annotations::TypeNames,
161) -> Option<InferredType> {
162 annotation
163 .and_then(|ann| resolve_annotation(ann, names))
164 .and_then(|ty| ty_to_inferred_type(&ty))
165 .or(literal_type)
166}
167
168/// A VAR/CONST initializer literal's type at full [`Ty`] fidelity (issue
169/// #1540) — the collection-aware sibling of
170/// [`infer_literal_type`](crate::external_check::infer_literal_type), which
171/// stops at [`InferredType`]'s scalar/`List<L>` domain.
172///
173/// Policy parity with `infer::body`'s own per-body literal arms is
174/// deliberate and load-bearing: this declaration-derived stub and the real
175/// HM inference must never disagree about what `#[1, 2, 3]` is, or a
176/// global and a `temp` holding the same literal would type differently.
177/// So the element/key/value joins go through the same
178/// [`unify_all`](crate::infer::unify_all) the body arms use (an empty or
179/// mixed literal lands on `Ty::Unknown`/`Ty::Conflicted` identically), and
180/// a construction literal's type is its written shape name, exactly as
181/// `Expr::StructLiteral`'s arm has it — construction *validity* is
182/// `crate::structs`' job, not this function's.
183///
184/// Nested elements recurse through this same function rather than through
185/// body inference: a declaration default is constant-folded, so the only
186/// element forms that can carry a type here are literals (a call/index/
187/// field access in that position is already `E077`).
188///
189/// Deliberately **not** handled: `Expr::Range`. A range literal never
190/// constant-folds into a declaration default at all (`brink-ir`'s
191/// `lir::lower::decls::is_const_foldable_decl_default` returns `false` for
192/// it), so typing one here would mint `NonEmptyRange` evidence — or its
193/// absence — for a declaration that cannot compile. Moot in practice: a
194/// `range` *annotation* has no grammar either
195/// ([`crate::annotations::resolve`] has no `"range"` arm), so
196/// `declared_value_ty`'s annotation branch can't produce `Ty::Range` any
197/// more than this literal branch can.
198///
199/// `pub(crate)` (issue #1877) so `strict::check_one_global_initializer` can
200/// independently type a VAR/CONST initializer literal and compare it
201/// against the declaration's own explicit annotation — the exact TM-2
202/// firewall comparison `declared_value_ty` below never makes, since there
203/// the annotation *replaces* this value outright rather than being checked
204/// against it.
205pub(crate) fn literal_ty(expr: &Expr, index: &SymbolIndex) -> Option<Ty> {
206 match expr {
207 Expr::ArrayLiteral(a) => Some(Ty::Array(Box::new(crate::infer::unify_all(
208 a.elements
209 .iter()
210 .map(|e| literal_ty(e, index).unwrap_or(Ty::Unknown)),
211 )))),
212 Expr::MapLiteral(m) => {
213 let keys = m
214 .entries
215 .iter()
216 .map(|(k, _)| literal_ty(k, index).unwrap_or(Ty::Unknown));
217 let vals: Vec<Ty> = m
218 .entries
219 .iter()
220 .map(|(_, v)| literal_ty(v, index).unwrap_or(Ty::Unknown))
221 .collect();
222 Some(Ty::Map(
223 Box::new(crate::infer::unify_all(keys)),
224 Box::new(crate::infer::unify_all(vals)),
225 ))
226 }
227 Expr::StructLiteral(sl) => Some(Ty::Struct(sl.shape.text.clone())),
228 _ => infer_literal_type(expr, index).map(Ty::from),
229 }
230}
231
232/// A VAR/CONST's declaration-derived type at full [`Ty`] fidelity — the
233/// value behind [`Sig::value_ty`] (issue #1540). Resolution order is the
234/// TM-2 firewall's: an explicit annotation wins outright, then the
235/// initializer literal, then a fn-value initializer.
236///
237/// `native` is the declaring file's frontend flag ([`HirFile::native`]) —
238/// the one gate the native bare-name spelling needs (issue #1895); see
239/// [`declared_fn_type`].
240fn declared_value_ty(
241 value: &Expr,
242 annotation: Option<&brink_ir::TypeExpr>,
243 index: &SymbolIndex,
244 files: &[(FileId, &HirFile)],
245 names: &crate::annotations::TypeNames,
246 manifest: Option<&HostManifest>,
247 native: bool,
248) -> Option<Ty> {
249 if let Some(ty) = annotation.and_then(|ann| resolve_annotation(ann, names)) {
250 return Some(ty);
251 }
252 literal_ty(value, index).or_else(|| declared_fn_type(value, index, files, manifest, native))
253}
254
255/// A VAR/CONST's declaration-derived fn-value initializer type (T1c
256/// follow-up, issue #712) — `None` when the initializer is neither an ink
257/// `#fn(target, args…)` literal nor, on the native surface, a bare name
258/// resolving to a function definition. Feeds [`Sig::value_ty`]'s last
259/// fallback, after the annotation and the initializer-literal branches in
260/// [`declared_value_ty`] have both already come up empty — an `fn(...)`
261/// *annotation* is resolved and returned there directly (that call site's
262/// own `if let Some(ty) = annotation.and_then(...)` is strictly earlier in
263/// the firewall order and already covers it), so this function only ever
264/// needs to look at the initializer.
265///
266/// **Two spellings, one type** (`docs/t1c-spec.md` §2a). Issue #1895 closed
267/// the declaration-initializer half of the native bare-name rule: lowering
268/// has always minted the fn value here (`lir::lower::decls::fold_path_ref`
269/// → `ConstValue::FnRef`, gated on `native && is_function_definition`) and
270/// `fn_values::check_native_bare_refs` has always walked decl initializers
271/// for `E080`, but typing had no native arm at all — so `Sig::value_ty`
272/// stayed `None`, the global never reached `infer::collect_globals`,
273/// `ty_of_def` answered `Ty::Unknown`, and a later `f(3)` misfired `E065`
274/// on correct code. The bare-name arm below is gated on the *same* two
275/// conjuncts lowering uses, so the two can never disagree about which
276/// initializers are fn values *when the target resolves to an actual
277/// knot* — the lookup below is restricted to `&[SymbolKind::Knot]`,
278/// narrower than lowering's `is_function_definition` (`Knot | Stitch`),
279/// so a top-level stitch promoted to knot status is a known, still-open
280/// one-sided gap (`docs/t1c-spec.md` §2a). A bare name always binds
281/// **zero** arguments — the binding form `#fn(f, a)` has no native
282/// spelling (§2a) — so the value's parameter row is the target's whole
283/// signature.
284fn declared_fn_type(
285 value: &Expr,
286 index: &SymbolIndex,
287 files: &[(FileId, &HirFile)],
288 manifest: Option<&HostManifest>,
289 native: bool,
290) -> Option<Ty> {
291 // Targets are always a single, statically-named function knot — never
292 // a stitch (T1c-1's own note: "every stitch target rejects until
293 // stitch-functions exist"), never a local (a global initializer has no
294 // enclosing body to scope a temp/param against). This is exactly
295 // `resolve::resolve_function`'s own first-tried bucket for a bare
296 // name, so it agrees with the real resolver for every target that
297 // isn't already an E079 error; an E079 target (wrong kind, or no
298 // "function" detail) falls through to `None` — the pre-existing
299 // Unknown-escape fallback, not a new failure mode.
300 let (target_path, bound_args) = match value {
301 Expr::FnLiteral(fl) => (&fl.target, fl.args.len()),
302 // Issue #1895: the sigil-free native spelling. Zero bound args,
303 // and only on the native surface — in ink the same bare name is a
304 // knot's visit count, never a fn value (§2a, "Ink is unchanged").
305 Expr::Path(path) if native => (path, 0),
306 _ => return None,
307 };
308 let target_name = target_path
309 .segments
310 .iter()
311 .map(|s| s.text.as_str())
312 .collect::<Vec<_>>()
313 .join(".");
314 // A bare `Path` initializer is the one form whose *own* resolution is
315 // not decided here: `lower_path`/`fold_path_ref` consult the real
316 // resolution map, which reaches a same-named VAR/CONST/list item
317 // before it reaches a function knot. `signature()` has no resolution
318 // map (declaration-derived only), so rather than guess the wrong
319 // interpretation for a shadowed name, decline — `None` is the honest
320 // "can't determine" and leaves the pre-existing `Ty::Unknown`
321 // behaviour exactly as it was. The `#fn` literal form is unaffected:
322 // its target grammar admits nothing but a function name.
323 //
324 // This check is **project index-wide**, not scoped to what this
325 // declaration can actually see — issue #1901 asked whether that could
326 // disagree with `fold_path_ref`'s real, `ImportScope`-aware
327 // resolution in a way that is user-visible: a same-named global in an
328 // unrelated, non-importing file suppressing this typing even though
329 // lowering would still resolve the bare name to the local knot. For
330 // the cross-file case it cannot: `lower_native::decl::{lower_var_decl,
331 // lower_const_decl, lower_flags_decl}` hard-code `visibility: None`
332 // for every native `VAR`/`CONST`/`LIST` (no annotation grammar
333 // overrides it, unlike ink's `#@public`/`#@private` tags), and a
334 // native file is unconditionally its own module
335 // (`brink_db::modules::native_module_path`), so a same-named
336 // `VAR`/`CONST`/`ListItem` declared in a *different* `.brink` file can
337 // never be legitimately referenced at all — `modules::check`'s `E087`
338 // fires unconditionally the moment resolution reaches it (confirmed
339 // by `native_cross_file_global_shadow_of_a_fn_value_reference_fails_to_compile`,
340 // `crates/brink-compiler/tests/driver.rs`), failing the whole compile
341 // before this decision could ever matter.
342 //
343 // The **same-file** case is different, and matters: `ListItem`s are
344 // indexed under their *qualified* name (`List.Item`, `manifest.rs`'s
345 // `index.by_name.entry(sym.name.clone())`), never their bare item
346 // name, so a direct `index.by_name.get(target_name)` lookup can never
347 // find a bare-name list item — the `SymbolKind::ListItem` arm below
348 // only ever fires when `target_name` is already the qualified
349 // `List.Item` spelling. A bare reference to a list item's own name
350 // (`flags Palette = double, other` then `var alias = double`) needs
351 // the same suffix scan `lookup_variable` itself uses
352 // (`lookup_list_item_bare`, priority 3, strictly ahead of the knot
353 // lookup at priority 6) — without it this guard silently disagreed
354 // with `fold_path_ref` in exactly the same file, no cross-module
355 // privacy gate to save it: `alias` typed `Ty::Fn` from the knot
356 // `double` while lowering bound it to the list item, producing a
357 // bogus `E063` the moment a caller's declared parameter type didn't
358 // happen to admit `fn`. `native_bare_name_shadowed_by_a_same_named_list_item_is_not_typed_as_a_fn_value`
359 // (`crates/brink-compiler/tests/driver.rs`) is the regression test;
360 // reverting the `lookup_list_item_bare` call below reproduces the
361 // bogus `E063` it guards against.
362 if matches!(value, Expr::Path(_))
363 && (index.by_name.get(target_name.as_str()).is_some_and(|ids| {
364 ids.iter().any(|id| {
365 index.symbols.get(id).is_some_and(|i| {
366 matches!(
367 i.kind,
368 SymbolKind::Variable | SymbolKind::Constant | SymbolKind::ListItem
369 )
370 })
371 })
372 }) || !matches!(
373 lookup_list_item_bare(index, target_name.as_str()),
374 BareItemResult::NotFound
375 ))
376 {
377 return None;
378 }
379 // Signature typing of a global `#fn` initializer has no per-file import
380 // context (no enclosing body), so it looks the target up flatly (M-2d
381 // import scoping — issue #790 — is inert here: a single-candidate knot
382 // hits `lookup_by_name`'s fast path and resolves identically; only the
383 // new multi-declared-module homonym case, not reachable from a typed
384 // `#fn` target today, would differ).
385 let target_def = lookup_by_name(
386 index,
387 &crate::resolve::ImportScope::default(),
388 &target_name,
389 &[SymbolKind::Knot],
390 )?;
391 let target_info = index.symbols.get(&target_def)?;
392 if target_info.detail.as_deref() != Some("function") {
393 return None;
394 }
395 // `signature()` is only ever handed a *narrowed* `files` slice by
396 // `brink-db`'s per-def `signature_query` (FG-2, single declaring file
397 // only — never a whole-project scan, by design). A cross-file `#fn`
398 // target's HIR simply isn't in that slice; calling `signature()`
399 // anyway would silently return a Sig with empty `param_annotations`
400 // (indistinguishable from "declares zero params"), fabricating a wrong
401 // arity instead of the honest "can't determine" `None`. The
402 // whole-project caller (`infer::collect_globals`, which always passes
403 // every file) never hits this guard.
404 if !files.iter().any(|&(id, _)| id == target_info.file) {
405 return None;
406 }
407 // Single-level recursion into the *target's* declaration-derived
408 // signature only (never its body) — `#fn` targets are always knots, so
409 // this never re-enters the `Variable`/`Constant` arms that call
410 // `declared_fn_type` in the first place.
411 let target_sig = signature(target_def, index, files, manifest)?;
412 let remaining: Vec<Ty> = target_sig
413 .param_annotations
414 .iter()
415 .skip(bound_args)
416 .map(|a| a.clone().unwrap_or(Ty::Unknown))
417 .collect();
418 let ret = target_sig.return_annotation.clone().unwrap_or(Ty::Unknown);
419 // The declaration-derived effect row (issue #1680,
420 // `docs/effects-spec.md` §5): this cell's initializer *is* a `#fn`
421 // creation site, and `target_def` is the target it names — syntactic
422 // evidence, resolved above by name lookup, never an inferred row
423 // (§6.1a). This is the row `collect_globals` inserts once and
424 // `BodyCtx::globals` only ever reads (`infer/mod.rs`'s
425 // `collect_globals`, `infer/body.rs`'s `ty_of_def`) — a later
426 // `~ cell = #fn(other)` write is folded into the effect walk's write
427 // set, never back into this cell's type, so the row stays fixed at
428 // this declaration's target and under-approximates any cell reassigned
429 // to a different fn value (`docs/effects-spec.md` §6.1c, filed against
430 // #1753).
431 Some(Ty::Fn(
432 remaining,
433 Box::new(ret),
434 crate::infer::FnRow::of_target(target_def),
435 ))
436}
437
438/// Compute the signature stub for one definition.
439///
440/// Reads the declaration only: the indexed `SymbolInfo` plus the declaring
441/// file's HIR for the initializer expression and the `#@local` bit. Returns
442/// `None` for an unknown definition id.
443#[must_use]
444#[expect(
445 clippy::too_many_lines,
446 reason = "each per-kind annotation-resolution branch below (Variable/Constant/ \
447 Knot/Stitch) threads the same TypeNames bundle (lists/structs/handles, \
448 TM-4b + T1d-2) through its own param/return/value-type resolution — \
449 a wide but flat shape, not a new structural concern worth splitting out"
450)]
451pub fn signature(
452 def: DefinitionId,
453 index: &SymbolIndex,
454 files: &[(FileId, &HirFile)],
455 manifest: Option<&HostManifest>,
456) -> Option<Arc<Sig>> {
457 let info = index.symbols.get(&def)?;
458 let hir = files
459 .iter()
460 .find(|&&(id, _)| id == info.file)
461 .map(|&(_, hir)| hir);
462
463 let mut value_type = None;
464 let mut value_ty = None;
465 let mut is_local = false;
466 let mut param_annotations = Vec::new();
467 let mut return_annotation = None;
468 if let Some(hir) = hir {
469 // `List<L>`/declared-struct annotations resolve nominally against
470 // every declared `LIST`/`STRUCT` in the project (spec §2/§3, TM-4b
471 // §6) — computed lazily, only when this def actually has a
472 // knot/stitch/var to annotate below. T1d-2b (issue #774): the
473 // registered `HostManifest` now reaches `signature()` too (threaded
474 // by `brink-db`'s per-def `signature_query`, the same coarse
475 // project-wide dependency shape `per_file_diagnostics_query` already
476 // reads `host_manifest` at), so `Handle<K>` annotations resolve
477 // against the manifest's declared kind vocabulary here exactly like
478 // `List<L>`/`STRUCT` resolve against the project's declared names —
479 // `None` (no manifest registered) still degrades to an empty
480 // handle-kind set, same "unresolved -> silent" contract every other
481 // unrecognized name already gets.
482 let names = || crate::annotations::TypeNames::new(index, manifest);
483 match info.kind {
484 SymbolKind::Variable => {
485 if let Some(v) = hir.variables.iter().find(|v| v.name.text == info.name) {
486 is_local = v.is_local;
487 // TM-2: annotation wins over the literal-inferred type.
488 value_type = value_type_with_annotation_override(
489 infer_literal_type(&v.value, index),
490 v.annotation.as_ref(),
491 &names(),
492 );
493 // Issue #1540: the full-fidelity type every typed
494 // consumer reads — `Ty::Array`/`Map`/`Struct`/`Fn`/
495 // `Option`/`Range` have no `InferredType` home, so they
496 // ride here. See `Sig::value_ty`.
497 value_ty = declared_value_ty(
498 &v.value,
499 v.annotation.as_ref(),
500 index,
501 files,
502 &names(),
503 manifest,
504 hir.native,
505 );
506 }
507 }
508 SymbolKind::Constant => {
509 if let Some(c) = hir.constants.iter().find(|c| c.name.text == info.name) {
510 // TM-2: annotation wins over the literal-inferred type
511 // (same firewall rule as VAR — see the `Variable` arm).
512 value_type = value_type_with_annotation_override(
513 infer_literal_type(&c.value, index),
514 c.annotation.as_ref(),
515 &names(),
516 );
517 // Issue #1540 — see the `Variable` arm.
518 value_ty = declared_value_ty(
519 &c.value,
520 c.annotation.as_ref(),
521 index,
522 files,
523 &names(),
524 manifest,
525 hir.native,
526 );
527 }
528 }
529 SymbolKind::Knot => {
530 if let Some(k) = hir.knots.iter().find(|k| k.name.text == info.name) {
531 is_local = k.is_local;
532 let names = names();
533 param_annotations = k
534 .params
535 .iter()
536 .map(|p| {
537 p.annotation
538 .as_ref()
539 .and_then(|a| resolve_annotation(a, &names))
540 })
541 .collect();
542 return_annotation = k
543 .return_type
544 .as_ref()
545 .and_then(|rt| resolve_annotation(rt, &names));
546 }
547 }
548 SymbolKind::Stitch => {
549 // Indexed stitch names are qualified (`knot.stitch`); HIR
550 // nests stitches under their knot by bare name. A top-level
551 // stitch is promoted to knot status during HIR lowering.
552 if let Some((knot_name, stitch_name)) = info.name.split_once('.') {
553 if let Some(s) = hir
554 .knots
555 .iter()
556 .find(|k| k.name.text == knot_name)
557 .and_then(|k| k.stitches.iter().find(|s| s.name.text == stitch_name))
558 {
559 is_local = s.is_local;
560 let names = names();
561 param_annotations = s
562 .params
563 .iter()
564 .map(|p| {
565 p.annotation
566 .as_ref()
567 .and_then(|a| resolve_annotation(a, &names))
568 })
569 .collect();
570 return_annotation = s
571 .return_type
572 .as_ref()
573 .and_then(|rt| resolve_annotation(rt, &names));
574 }
575 } else if let Some(k) = hir.knots.iter().find(|k| k.name.text == info.name) {
576 is_local = k.is_local;
577 let names = names();
578 param_annotations = k
579 .params
580 .iter()
581 .map(|p| {
582 p.annotation
583 .as_ref()
584 .and_then(|a| resolve_annotation(a, &names))
585 })
586 .collect();
587 return_annotation = k
588 .return_type
589 .as_ref()
590 .and_then(|rt| resolve_annotation(rt, &names));
591 }
592 }
593 _ => {}
594 }
595 }
596
597 Some(Arc::new(Sig {
598 name: info.name.clone(),
599 kind: info.kind,
600 params: info.params.clone(),
601 value_type,
602 value_ty,
603 is_local,
604 param_annotations,
605 return_annotation,
606 }))
607}
608
609/// The per-file locals path [`signature`] itself cannot take (issue #530).
610///
611/// A local (`Param`/`Temp`) `DefinitionId` is a content hash of
612/// `(scope, name, kind)` alone (`manifest::local_definition_id`) — it has
613/// no file component, unlike a declaration's id (recoverable from the
614/// project-wide index's `SymbolInfo.file`). So `signature`'s own
615/// `index.symbols.get(&def)` lookup can never find one once the caller
616/// passes the decls-only [`resolution_index_query`](../../brink_db)
617/// projection ([`resolution_index_query`]'s own doc: locals are dropped
618/// entirely, issue #517) — and widening that lookup to the full,
619/// range-carrying index would reintroduce exactly the whole-project
620/// invalidation #517's cutoff exists to kill. A local's body lives in
621/// exactly one file (the same fact [`crate::resolve::lookup_local_in_scope`]
622/// already leans on), so a caller that already knows which file declares
623/// the local — hover, go-to-def, resolved from a reference inside that
624/// same file — can hand this function that file's own `manifest` directly,
625/// keeping the lookup a narrow per-file scan instead of a project-wide one.
626///
627/// Declaration-derived only, matching [`signature`]'s own contract: a
628/// `Param`'s or `~ temp`'s TM-2 (docs/typed-mode-spec.md §3) `: type`
629/// annotation, if any — no body inference (that's `infer_body`'s job, read
630/// separately by a caller that wants an inferred fallback). `is_local` is
631/// always `false` — the `#@local` flow-private directive only exists on
632/// knot/stitch/VAR declarations, never on a param or temp. `params` and
633/// `param_annotations` stay empty and `return_annotation` stays `None` —
634/// a local isn't callable, so those `Sig` fields (knot/stitch-only) don't
635/// apply. Returns `None` when `def` doesn't match any local declared in
636/// `manifest`.
637#[must_use]
638pub fn local_signature(
639 def: DefinitionId,
640 manifest: &brink_ir::SymbolManifest,
641 index: &SymbolIndex,
642 host: Option<&HostManifest>,
643) -> Option<Arc<Sig>> {
644 let local = manifest
645 .locals
646 .iter()
647 .find(|l| crate::manifest::local_definition_id(&l.scope, &l.name, l.kind) == def)?;
648
649 let names = crate::annotations::TypeNames::new(index, host);
650 let value_ty = local
651 .annotation
652 .as_ref()
653 .and_then(|a| resolve_annotation(a, &names));
654 let value_type = value_ty.as_ref().and_then(ty_to_inferred_type);
655
656 Some(Arc::new(Sig {
657 name: local.name.clone(),
658 kind: local.kind,
659 params: Vec::new(),
660 value_type,
661 value_ty,
662 is_local: false,
663 param_annotations: Vec::new(),
664 return_annotation: None,
665 }))
666}