Skip to main content

brink_analyzer/
lib.rs

1//! Cross-file semantic analysis for inkle's ink narrative scripting language.
2//!
3//! The analyzer merges per-file `SymbolManifest`s from `brink-ir` into a
4//! unified `SymbolIndex`, then runs validation passes (name resolution,
5//! duplicate detection, type checking). Both `brink-compiler` and `brink-lsp`
6//! consume the analysis result.
7
8mod external_check;
9mod manifest;
10mod resolve;
11mod validate;
12
13use std::collections::BTreeMap;
14
15pub use brink_ir::FileId;
16pub use brink_ir::ResolutionMap;
17pub use external_check::{
18    ExternalCheckSeverity, InferredType, ResolvedParam, ResolvedType, SymbolMeta, ValueMeta,
19};
20
21use brink_format::DefinitionId;
22use brink_ir::{
23    Diagnostic, DocBlock, HirFile, HostManifest, ManifestExternal, SemanticTypeDef, SymbolIndex,
24    SymbolKind, SymbolManifest,
25};
26
27/// Tooling options for analysis: the registered host manifest and the
28/// severity policy for its external checks. Defaults to no manifest.
29#[derive(Debug, Clone, Default)]
30pub struct AnalysisOptions {
31    /// The registered host-capability manifest, if any.
32    pub host_manifest: Option<HostManifest>,
33    /// Severity policy for manifest-driven external diagnostics.
34    pub external_check: ExternalCheckSeverity,
35}
36
37/// The output of cross-file semantic analysis.
38#[derive(Debug, Clone)]
39pub struct AnalysisResult {
40    /// The unified symbol index.
41    pub index: SymbolIndex,
42    /// Resolved references: maps source range → definition id.
43    pub resolutions: ResolutionMap,
44    /// Diagnostics produced during analysis (duplicate definitions, unresolved refs, etc.).
45    pub diagnostics: Vec<Diagnostic>,
46    /// Per-symbol metadata enrichment (docs, resolved types, initializer
47    /// values), keyed by `DefinitionId`. Empty when no host manifest is
48    /// registered and no inline `///` docs are present.
49    pub symbol_meta: BTreeMap<DefinitionId, SymbolMeta>,
50}
51
52/// Run cross-file semantic analysis with default options (no host manifest).
53///
54/// Each entry is a `(FileId, HirFile, SymbolManifest)` tuple produced by
55/// per-file HIR lowering.
56pub fn analyze(files: &[(FileId, &HirFile, &SymbolManifest)]) -> AnalysisResult {
57    analyze_with_options(files, &AnalysisOptions::default())
58}
59
60/// Run cross-file semantic analysis with explicit tooling options, including
61/// an optional host-capability manifest and its external-check severity.
62pub fn analyze_with_options(
63    files: &[(FileId, &HirFile, &SymbolManifest)],
64    opts: &AnalysisOptions,
65) -> AnalysisResult {
66    let manifest_inputs: Vec<(FileId, &SymbolManifest)> = files
67        .iter()
68        .map(|&(id, _hir, manifest)| (id, manifest))
69        .collect();
70
71    let hir_inputs: Vec<(FileId, &HirFile)> = files.iter().map(|&(id, hir, _)| (id, hir)).collect();
72
73    let (index, mut diagnostics) = manifest::merge_manifests(&manifest_inputs);
74    let (resolutions, resolve_diags) = resolve::resolve_refs(&index, &manifest_inputs);
75    diagnostics.extend(resolve_diags);
76    diagnostics.extend(validate::validate(&hir_inputs));
77
78    // Host-manifest enrichment + checks (tooling/author-time only).
79    let inline_docs = collect_inline_docs(&manifest_inputs);
80    let (types, registered) = manifest_maps(opts.host_manifest.as_ref());
81    let (mut symbol_meta, ext_diags) = external_check::analyze_externals(
82        &index,
83        &inline_docs,
84        &types,
85        &registered,
86        opts.external_check,
87    );
88    diagnostics.extend(ext_diags);
89
90    // Knot/stitch doc enrichment (presentational; shares the semantic-type
91    // vocabulary, so unknown types still diagnose).
92    let (callable_meta, callable_diags) =
93        external_check::enrich_callables(&index, &inline_docs, &types, opts.external_check);
94    diagnostics.extend(callable_diags);
95    symbol_meta.extend(callable_meta);
96
97    // VAR/CONST initializer info + LIST docs (presentational, no diagnostics).
98    symbol_meta.extend(external_check::infer_value_meta(
99        &hir_inputs,
100        &index,
101        &inline_docs,
102    ));
103
104    // Call-site literal checks (type mismatch, closed domain) over the HIR.
105    // Externals only — knot/stitch metadata is presentational, not binding.
106    if opts.external_check != ExternalCheckSeverity::Off {
107        let name_to_meta: BTreeMap<&str, &SymbolMeta> = symbol_meta
108            .iter()
109            .filter_map(|(id, meta)| {
110                index.symbols.get(id).and_then(|s| {
111                    (s.kind == SymbolKind::External).then_some((s.name.as_str(), meta))
112                })
113            })
114            .collect();
115        diagnostics.extend(external_check::check_call_sites(&hir_inputs, &name_to_meta));
116    }
117
118    AnalysisResult {
119        index,
120        resolutions,
121        diagnostics,
122        symbol_meta,
123    }
124}
125
126/// Collect inline `///` docs across all files, keyed by `(kind, declared name)`.
127fn collect_inline_docs(
128    files: &[(FileId, &SymbolManifest)],
129) -> BTreeMap<(SymbolKind, String), DocBlock> {
130    let mut out = BTreeMap::new();
131    for &(_id, manifest) in files {
132        for (key, doc) in &manifest.docs {
133            out.insert(key.clone(), doc.clone());
134        }
135    }
136    out
137}
138
139/// Build lookup maps from the registered manifest: semantic types by name and
140/// registered externals by name.
141fn manifest_maps(
142    manifest: Option<&HostManifest>,
143) -> (
144    BTreeMap<String, SemanticTypeDef>,
145    BTreeMap<String, &ManifestExternal>,
146) {
147    let mut types = BTreeMap::new();
148    let mut registered = BTreeMap::new();
149    if let Some(manifest) = manifest {
150        for ty in &manifest.types {
151            types.insert(ty.name.clone(), ty.clone());
152        }
153        for ext in &manifest.externals {
154            registered.insert(ext.name.clone(), ext);
155        }
156    }
157    (types, registered)
158}