use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use brink_analyzer::{
AnalysisOptions, CallGraph, HarvestIndex, HarvestNames, ImportScope, InferenceResult, SccGraph,
Sig, TypePolicy,
};
use brink_format::{
CallAtom, CapabilityParam, DefinitionId, DirectEffects, EffectRowEntry, NameId, StoryData,
};
use brink_ir::suppressions::{Suppressions, apply_suppressions, parse_suppressions};
use brink_ir::symbols::project_manifest;
use brink_ir::{
Diagnostic, DiagnosticCode, FileId, HirFile, ResolutionMap, Severity, SymbolIndex, SymbolKind,
SymbolManifest, lower_declarations, lower_single_knot, lower_top_level,
};
use brink_syntax::Parse;
use brink_syntax_native::Parse as NativeParse;
use crate::db::resolve_include_path;
use crate::determinism::{LookupMap, LookupSet};
use crate::include_graph::IncludeGraph;
mod analysis;
mod heap_size;
pub(crate) mod segments;
pub(crate) fn is_ink_file(db: &BrinkDatabase, file: SourceFile) -> bool {
file_language(file.path(db)) == Language::Ink
}
pub(crate) use segments::{
FileSegment, file_segments_query, line_contexts_query, projection_query, semantic_tokens_query,
};
pub use analysis::ResolvedProject;
pub(crate) use analysis::{
MemberSet, analysis_diagnostics_query, analysis_query, await_purity_diagnostics_query,
call_site_diagnostics_query, call_site_metas_query, coalesce_types_query,
comparator_contract_diagnostics_query, contributor_diagnostics_query,
conventions_confinement_diagnostics_query, conventions_projection_query, diagnostics_query,
effects_assertion_diagnostics_query, external_claim_handlers_query, external_meta_query,
has_errors_in_closure_query, has_errors_query, import_closure_query, inline_docs_query,
per_file_diagnostics_query, resolutions_index_query, subset_analysis_query,
ufcs_resolution_query, value_meta_query, whole_project_diagnostics_query,
};
#[salsa::db]
#[derive(Clone)]
pub(crate) struct BrinkDatabase {
storage: salsa::Storage<Self>,
}
#[salsa::db]
impl salsa::Database for BrinkDatabase {}
impl Default for BrinkDatabase {
fn default() -> Self {
Self {
storage: salsa::Storage::builder()
.ingredient::<SourceFile>()
.ingredient::<ProjectInput>()
.ingredient::<DefKey<'_>>()
.ingredient::<MemberSet<'_>>()
.ingredient::<FileSegment<'_>>()
.ingredient::<file_segments_query>()
.ingredient::<segments::segment_lowered_query>()
.ingredient::<resolved_dialect_query>()
.ingredient::<segments::segment_projection_query>()
.ingredient::<segments::projection_query>()
.ingredient::<segments::segment_line_contexts_query>()
.ingredient::<segments::line_contexts_query>()
.ingredient::<segments::file_resolution_kinds_query>()
.ingredient::<segments::segment_resolution_kinds_query>()
.ingredient::<segments::segment_semantic_tokens_query>()
.ingredient::<segments::segment_semantic_tokens_classifier_query>()
.ingredient::<segments::semantic_tokens_query>()
.ingredient::<parse_query>()
.ingredient::<parse_native_query>()
.ingredient::<raw_lowered_query>()
.ingredient::<lowered_query>()
.ingredient::<suppressions_query>()
.ingredient::<include_graph_query>()
.ingredient::<module_map_query>()
.ingredient::<symbol_index_query>()
.ingredient::<harvest_index_query>()
.ingredient::<harvest_completion_index_query>()
.ingredient::<resolution_index_query>()
.ingredient::<file_import_scope_query>()
.ingredient::<resolve_query>()
.ingredient::<signature_query>()
.ingredient::<local_signature_query>()
.ingredient::<resolutions_index_query>()
.ingredient::<per_file_diagnostics_query>()
.ingredient::<contributor_diagnostics_query>()
.ingredient::<inline_docs_query>()
.ingredient::<external_meta_query>()
.ingredient::<call_site_metas_query>()
.ingredient::<value_meta_query>()
.ingredient::<call_site_diagnostics_query>()
.ingredient::<whole_project_diagnostics_query>()
.ingredient::<ufcs_resolution_query>()
.ingredient::<coalesce_types_query>()
.ingredient::<analysis_diagnostics_query>()
.ingredient::<analysis_query>()
.ingredient::<subset_analysis_query>()
.ingredient::<diagnostics_query>()
.ingredient::<has_errors_query>()
.ingredient::<has_errors_in_closure_query>()
.ingredient::<type_policy_query>()
.ingredient::<lint_policy_query>()
.ingredient::<debug_info_policy_query>()
.ingredient::<struct_shape_data_query>()
.ingredient::<normalized_stamped_query>()
.ingredient::<decl_hir_query>()
.ingredient::<lir_prelude_decls_query>()
.ingredient::<KnotChunkKey<'_>>()
.ingredient::<chunk_lowering_ctx_query>()
.ingredient::<lir_knot_chunk_query>()
.ingredient::<lir_lowering_query>()
.ingredient::<inference_index_query>()
.ingredient::<inferable_defs_query>()
.ingredient::<def_body_query>()
.ingredient::<referenced_globals_query>()
.ingredient::<call_edges_query>()
.ingredient::<call_graph_query>()
.ingredient::<scc_membership_query>()
.ingredient::<solve_scc_query>()
.ingredient::<inferred_signature_query>()
.ingredient::<external_signatures_query>()
.ingredient::<type_inference_query>()
.ingredient::<infer_body_query>()
.ingredient::<type_diagnostics_query>()
.ingredient::<def_effect_atoms_query>()
.ingredient::<effects_scc_query>()
.ingredient::<effects_query>()
.ingredient::<effects_assertion_diagnostics_query>()
.ingredient::<await_purity_diagnostics_query>()
.ingredient::<comparator_contract_diagnostics_query>()
.ingredient::<conventions_confinement_diagnostics_query>()
.ingredient::<external_claim_handlers_query>()
.ingredient::<import_closure_query>()
.ingredient::<conventions_projection_query>()
.ingredient::<lir_query>()
.ingredient::<lir_in_closure_query>()
.ingredient::<story_data_query>()
.build(),
}
}
}
#[salsa::input]
pub(crate) struct SourceFile {
pub file_id: FileId,
#[returns(ref)]
pub path: String,
#[returns(ref)]
pub text: String,
}
#[salsa::input]
pub(crate) struct ProjectInput {
#[returns(ref)]
pub files: Vec<SourceFile>,
pub entry: Option<FileId>,
#[returns(ref)]
pub analysis_options: AnalysisOptions,
#[returns(ref)]
pub dialect: Option<brink_ir::DialogueDialect>,
#[returns(ref)]
pub native_root: Option<String>,
#[returns(ref)]
pub ink_root: Option<String>,
}
#[salsa::tracked(returns(ref), no_eq)]
pub(crate) fn resolved_dialect_query(
db: &dyn salsa::Database,
project: ProjectInput,
) -> ResolvedDialectHandle {
ResolvedDialectHandle(
project
.dialect(db)
.as_ref()
.and_then(|config| brink_ir::ResolvedDialect::compile(config).ok())
.map(Arc::new),
)
}
#[derive(Clone)]
pub(crate) struct NoEqArc<T>(pub Arc<T>);
impl<T> std::fmt::Debug for NoEqArc<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("NoEqArc")
.field(&std::any::type_name::<T>())
.finish()
}
}
#[expect(
unsafe_code,
reason = "salsa::Update is an unsafe trait by design; this is the \
always-replace impl a derive would emit for a local type. \
The body is a plain pointer write per the trait's documented \
contract."
)]
unsafe impl<T: 'static> salsa::Update for NoEqArc<T> {
unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
unsafe { *old_pointer = new_value };
true
}
}
#[derive(Clone)]
pub(crate) struct ResolvedDialectHandle(pub Option<Arc<brink_ir::ResolvedDialect>>);
impl std::fmt::Debug for ResolvedDialectHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("ResolvedDialectHandle")
.field(&self.0.is_some())
.finish()
}
}
#[expect(
unsafe_code,
reason = "salsa::Update is an unsafe trait by design; this is the \
always-replace impl `#[derive(salsa::Update)]` would emit if \
`ResolvedDialect` were local (it lives in brink-ir, which \
deliberately has no salsa dependency). The body is a plain \
pointer write per the trait's documented contract."
)]
unsafe impl salsa::Update for ResolvedDialectHandle {
unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
unsafe { *old_pointer = new_value };
true
}
}
#[salsa::tracked(returns(ref), lru = 4096)]
pub(crate) fn parse_query(db: &dyn salsa::Database, file: SourceFile) -> Parse {
brink_syntax::parse(file.text(db))
}
#[salsa::tracked(returns(ref), lru = 4096)]
pub(crate) fn parse_native_query(db: &dyn salsa::Database, file: SourceFile) -> NativeParse {
brink_syntax_native::parse(file.text(db))
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct LoweredFile {
pub hir: HirFile,
pub manifest: SymbolManifest,
pub diagnostics: Vec<Diagnostic>,
pub admission: Vec<Diagnostic>,
}
#[salsa::tracked(returns(ref), lru = 4096, heap_size = heap_size::lowered_file_heap_size)]
pub(crate) fn raw_lowered_query(db: &dyn salsa::Database, file: SourceFile) -> Arc<LoweredFile> {
let file_id = file.file_id(db);
Arc::new(match file_language(file.path(db)) {
Language::Ink => segments::assemble_lowered_file(db, file),
Language::Native => lower_native_file(file_id, parse_native_query(db, file), None),
})
}
#[salsa::tracked(returns(ref), lru = 4096, heap_size = heap_size::lowered_file_heap_size)]
pub(crate) fn lowered_query(
db: &dyn salsa::Database,
project: ProjectInput,
file: SourceFile,
) -> Arc<LoweredFile> {
let file_id = file.file_id(db);
if file_language(file.path(db)) != Language::Native {
return Arc::clone(raw_lowered_query(db, file));
}
let external = external_claim_handlers_query(db, project);
match &**external {
Some((conventions_file_id, decls)) if *conventions_file_id != file_id => {
Arc::new(lower_native_file(
file_id,
parse_native_query(db, file),
Some(decls.as_slice()),
))
}
_ => Arc::clone(raw_lowered_query(db, file)),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Language {
Ink,
Native,
}
pub(crate) fn file_language(path: &str) -> Language {
if std::path::Path::new(path)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("brink"))
{
Language::Native
} else {
Language::Ink
}
}
#[salsa::tracked(returns(ref), lru = 4096)]
pub(crate) fn suppressions_query(db: &dyn salsa::Database, file: SourceFile) -> Suppressions {
let mut out = parse_suppressions(file.text(db));
out.allow_scopes
.clone_from(&raw_lowered_query(db, file).hir.allow_scopes);
out
}
#[salsa::tracked(returns(ref))]
pub(crate) fn include_graph_query(db: &dyn salsa::Database, project: ProjectInput) -> IncludeGraph {
let files = project.files(db);
let path_to_id: LookupMap<&str, FileId> = files
.iter()
.map(|f| (f.path(db).as_str(), f.file_id(db)))
.collect();
let mut graph = IncludeGraph::new();
for file in files {
if !is_source_file(file.path(db)) {
continue;
}
let hir = &raw_lowered_query(db, *file).hir;
let include_ids: Vec<FileId> = hir
.includes
.iter()
.filter_map(|inc| {
let resolved = resolve_include_path(file.path(db), &inc.file_path);
path_to_id.get(resolved.as_str()).copied()
})
.collect();
graph.update(file.file_id(db), include_ids);
}
graph
}
pub(crate) fn compilation_closure_files(
db: &dyn salsa::Database,
project: ProjectInput,
) -> Vec<FileId> {
let Some(entry) = project.entry(db) else {
return Vec::new();
};
let files = project.files(db);
let entry_is_native = files
.iter()
.find(|f| f.file_id(db) == entry)
.is_some_and(|f| file_language(f.path(db)) == Language::Native);
if entry_is_native {
let mut ids: Vec<FileId> = files
.iter()
.filter(|f| file_language(f.path(db)) == Language::Native)
.map(|f| f.file_id(db))
.collect();
ids.sort_unstable_by_key(|id| id.0);
ids
} else {
include_graph_query(db, project).topological_order(entry)
}
}
pub(crate) fn is_source_file(path: &str) -> bool {
match std::path::Path::new(path).extension() {
None => true,
Some(ext) => ext.eq_ignore_ascii_case("ink") || ext.eq_ignore_ascii_case("brink"),
}
}
pub fn has_recognized_source_extension(path: &str) -> bool {
std::path::Path::new(path)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("brink") || ext.eq_ignore_ascii_case("ink"))
}
pub fn is_native_source_path(path: &str) -> bool {
file_language(path) == Language::Native
}
pub(crate) fn project_is_all_native(db: &dyn salsa::Database, project: ProjectInput) -> bool {
let files = project.files(db);
let mut saw_source = false;
for f in files {
let path = f.path(db);
if !has_recognized_source_extension(path) {
continue;
}
saw_source = true;
if file_language(path) != Language::Native {
return false;
}
}
saw_source
}
#[salsa::tracked(returns(ref))]
pub(crate) fn module_map_query(
db: &dyn salsa::Database,
project: ProjectInput,
) -> (brink_analyzer::ModuleMap, Vec<Diagnostic>) {
let files = project.files(db);
let ink_inputs: Vec<crate::modules::FileModuleInput> = files
.iter()
.filter(|f| is_source_file(f.path(db)) && file_language(f.path(db)) == Language::Ink)
.map(|f| {
let hir_module = raw_lowered_query(db, *f).hir.module.as_ref();
crate::modules::FileModuleInput {
file: f.file_id(db),
stem: crate::modules::file_stem(f.path(db)).to_string(),
declared: hir_module.map(|m| m.name.clone()),
was: hir_module.and_then(|m| m.was.as_ref().map(|(old, _)| old.clone())),
}
})
.collect();
let (mut map, diags) =
crate::modules::resolve_modules(&ink_inputs, include_graph_query(db, project));
let native_root = project.native_root(db).as_deref();
for f in files {
if file_language(f.path(db)) == Language::Native {
let was = raw_lowered_query(db, *f)
.hir
.module
.as_ref()
.and_then(|m| m.was.as_ref().map(|(old, _)| old.clone()));
let key = crate::modules::root_relative_key(native_root, f.path(db));
map.insert(
f.file_id(db),
brink_analyzer::ResolvedModule {
name: crate::modules::native_module_path(&key),
declared: true,
was,
},
);
}
}
(map, diags)
}
#[salsa::tracked(returns(ref))]
pub(crate) fn symbol_index_query(
db: &dyn salsa::Database,
project: ProjectInput,
) -> (Arc<SymbolIndex>, Vec<Diagnostic>) {
let files = project.files(db);
let manifest_refs: Vec<(FileId, &SymbolManifest)> = files
.iter()
.filter(|f| is_source_file(f.path(db)))
.map(|f| (f.file_id(db), &lowered_query(db, project, *f).manifest))
.collect();
let (module_map, module_diags) = module_map_query(db, project);
let dialect = project.analysis_options(db).dialect;
let is_native = project_is_all_native(db, project);
let (index, mut diagnostics) =
brink_analyzer::symbol_index_with_modules(&manifest_refs, module_map, dialect, is_native);
diagnostics.extend(module_diags.clone());
(index, diagnostics)
}
#[salsa::tracked(returns(ref))]
pub(crate) fn harvest_index_query(
db: &dyn salsa::Database,
project: ProjectInput,
) -> Arc<HarvestIndex> {
let files = project.files(db);
let hir_refs: Vec<(FileId, &HirFile)> = files
.iter()
.filter(|f| is_source_file(f.path(db)))
.map(|f| (f.file_id(db), &lowered_query(db, project, *f).hir))
.collect();
let manifest = project.analysis_options(db).host_manifest.as_ref();
Arc::new(brink_analyzer::harvest(&hir_refs, manifest))
}
#[salsa::tracked(returns(ref))]
pub(crate) fn harvest_completion_index_query(
db: &dyn salsa::Database,
project: ProjectInput,
) -> Arc<HarvestNames> {
let index = harvest_index_query(db, project);
Arc::new(index.names())
}
#[salsa::tracked(returns(ref))]
pub(crate) fn resolution_index_query(
db: &dyn salsa::Database,
project: ProjectInput,
) -> Arc<SymbolIndex> {
let (index, _diags) = symbol_index_query(db, project);
let mut stripped: SymbolIndex = (**index).clone();
stripped
.symbols
.retain(|_, info| !matches!(info.kind, SymbolKind::Param | SymbolKind::Temp));
let live_ids: LookupSet<DefinitionId> = stripped.symbols.keys().copied().collect();
stripped.by_name.retain(|_, ids| {
ids.retain(|id| live_ids.contains(id));
!ids.is_empty()
});
for info in stripped.symbols.values_mut() {
info.range = rowan::TextRange::default();
}
Arc::new(stripped)
}
#[salsa::tracked(returns(ref), lru = 4096)]
pub(crate) fn file_import_scope_query(
db: &dyn salsa::Database,
project: ProjectInput,
file: SourceFile,
) -> ImportScope {
let (module_map, _module_diags) = module_map_query(db, project);
let file_module = module_map
.get(&file.file_id(db))
.filter(|m| m.declared)
.map(|m| m.name.clone());
let hir = &lowered_query(db, project, file).hir;
ImportScope::new(file_module, &hir.imports)
}
#[salsa::tracked(returns(ref), lru = 4096)]
pub(crate) fn resolve_query(
db: &dyn salsa::Database,
project: ProjectInput,
file: SourceFile,
) -> (Arc<ResolutionMap>, Vec<Diagnostic>) {
let index = resolution_index_query(db, project);
let lowered = lowered_query(db, project, file);
let scope = file_import_scope_query(db, project, file);
brink_analyzer::resolve(file.file_id(db), &lowered.manifest, index, scope)
}
#[salsa::interned]
pub(crate) struct DefKey<'db> {
pub def: DefinitionId,
}
#[salsa::tracked(lru = 16384, heap_size = heap_size::signature_heap_size)]
pub(crate) fn signature_query<'db>(
db: &'db dyn salsa::Database,
project: ProjectInput,
def: DefKey<'db>,
) -> Option<Arc<Sig>> {
let index = resolution_index_query(db, project);
let def_id = def.def(db);
let declaring_file = index.symbols.get(&def_id)?.file;
let hir_refs: Vec<(FileId, &HirFile)> = project
.files(db)
.iter()
.filter(|f| f.file_id(db) == declaring_file)
.map(|f| (f.file_id(db), &lowered_query(db, project, *f).hir))
.collect();
let opts = project.analysis_options(db);
brink_analyzer::signature(def_id, index, &hir_refs, opts.host_manifest.as_ref())
}
#[salsa::tracked(lru = 4096, heap_size = heap_size::signature_heap_size)]
pub(crate) fn local_signature_query<'db>(
db: &'db dyn salsa::Database,
project: ProjectInput,
file: SourceFile,
def: DefKey<'db>,
) -> Option<Arc<Sig>> {
let index = resolution_index_query(db, project);
let manifest = &lowered_query(db, project, file).manifest;
let opts = project.analysis_options(db);
brink_analyzer::local_signature(def.def(db), manifest, index, opts.host_manifest.as_ref())
}
#[salsa::tracked(returns(ref))]
pub(crate) fn inference_index_query(
db: &dyn salsa::Database,
project: ProjectInput,
) -> Arc<SymbolIndex> {
let (index, _diags) = symbol_index_query(db, project);
let mut stripped: SymbolIndex = (**index).clone();
for info in stripped.symbols.values_mut() {
info.range = rowan::TextRange::default();
}
Arc::new(stripped)
}
pub(crate) type SccId = DefinitionId;
#[salsa::tracked(returns(ref))]
pub(crate) fn inferable_defs_query(
db: &dyn salsa::Database,
project: ProjectInput,
) -> BTreeSet<DefinitionId> {
let index = inference_index_query(db, project);
brink_analyzer::inferable_defs_from_index(index)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DefBody {
pub file: FileId,
pub params: Vec<brink_ir::Param>,
pub return_annotation: Option<brink_ir::TypeExpr>,
pub body: brink_ir::Block,
pub native: bool,
}
#[salsa::tracked(lru = 16384, heap_size = heap_size::def_body_heap_size)]
pub(crate) fn def_body_query<'db>(
db: &'db dyn salsa::Database,
project: ProjectInput,
def: DefKey<'db>,
) -> Option<Arc<DefBody>> {
let index = inference_index_query(db, project);
let def_id = def.def(db);
let declaring_file = index.symbols.get(&def_id)?.file;
let file = project
.files(db)
.iter()
.find(|f| f.file_id(db) == declaring_file)?;
let hir = &lowered_query(db, project, *file).hir;
let (params, return_annotation, body) =
brink_analyzer::def_body(def_id, &[(declaring_file, hir)], index)?;
Some(Arc::new(DefBody {
file: declaring_file,
params,
return_annotation,
body,
native: hir.native,
}))
}
#[salsa::tracked(lru = 16384)]
pub(crate) fn referenced_globals_query<'db>(
db: &'db dyn salsa::Database,
project: ProjectInput,
def: DefKey<'db>,
) -> Arc<BTreeSet<DefinitionId>> {
let index = inference_index_query(db, project);
let def_id = def.def(db);
let Some(declaring_file) = index.symbols.get(&def_id).map(|info| info.file) else {
return Arc::new(BTreeSet::new());
};
let Some(file) = project
.files(db)
.iter()
.find(|f| f.file_id(db) == declaring_file)
else {
return Arc::new(BTreeSet::new());
};
let hir = &lowered_query(db, project, *file).hir;
let (resolutions, _diags) = resolve_query(db, project, *file);
Arc::new(brink_analyzer::referenced_globals(
def_id,
&[(declaring_file, hir)],
index,
resolutions,
None,
))
}
#[salsa::tracked(lru = 16384)]
pub(crate) fn call_edges_query<'db>(
db: &'db dyn salsa::Database,
project: ProjectInput,
def: DefKey<'db>,
) -> Arc<BTreeSet<DefinitionId>> {
let index = inference_index_query(db, project);
let def_id = def.def(db);
let Some(declaring_file) = index.symbols.get(&def_id).map(|info| info.file) else {
return Arc::new(BTreeSet::new());
};
let Some(file) = project
.files(db)
.iter()
.find(|f| f.file_id(db) == declaring_file)
else {
return Arc::new(BTreeSet::new());
};
let hir = &lowered_query(db, project, *file).hir;
let (resolutions, _diags) = resolve_query(db, project, *file);
let inferable = inferable_defs_query(db, project);
Arc::new(brink_analyzer::call_edges(
def_id,
&[(declaring_file, hir)],
index,
resolutions,
inferable,
None,
))
}
#[salsa::tracked(returns(ref))]
pub(crate) fn call_graph_query(db: &dyn salsa::Database, project: ProjectInput) -> CallGraph {
let defs = inferable_defs_query(db, project);
let mut graph = CallGraph::new();
for &def in defs {
graph.add_node(def);
let edges = call_edges_query(db, project, DefKey::new(db, def));
for &callee in edges.iter() {
graph.add_edge(def, callee);
}
}
graph
}
#[salsa::tracked(returns(ref))]
pub(crate) fn scc_membership_query(db: &dyn salsa::Database, project: ProjectInput) -> SccGraph {
let graph = call_graph_query(db, project);
brink_analyzer::scc_graph(graph)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct SolvedScc {
pub signatures: BTreeMap<DefinitionId, brink_analyzer::InferredSig>,
pub bodies: BTreeMap<DefinitionId, brink_analyzer::BodyTypes>,
}
#[salsa::tracked(lru = 16384, heap_size = heap_size::solve_scc_heap_size)]
pub(crate) fn solve_scc_query<'db>(
db: &'db dyn salsa::Database,
project: ProjectInput,
scc: DefKey<'db>,
) -> Arc<SolvedScc> {
let scc_id: SccId = scc.def(db);
let membership = scc_membership_query(db, project);
let Some(batch) = membership
.order
.iter()
.find(|comp| comp.iter().next().copied() == Some(scc_id))
else {
return Arc::new(SolvedScc::default());
};
let mut known_sigs: BTreeMap<DefinitionId, brink_analyzer::InferredSig> = BTreeMap::new();
if let Some(deps) = membership.depends_on.get(&scc_id) {
for &dep in deps {
let solved = solve_scc_query(db, project, DefKey::new(db, dep));
known_sigs.extend(solved.signatures.iter().map(|(k, v)| (*k, v.clone())));
}
}
let index = inference_index_query(db, project);
let inferable = inferable_defs_query(db, project);
let member_bodies: BTreeMap<DefinitionId, Arc<DefBody>> = batch
.iter()
.filter_map(|&id| def_body_query(db, project, DefKey::new(db, id)).map(|b| (id, b)))
.collect();
let defs: Vec<brink_analyzer::Def<'_>> = member_bodies
.iter()
.map(|(&id, b)| brink_analyzer::Def {
id,
file: b.file,
params: &b.params,
body: &b.body,
return_annotation: b.return_annotation.as_ref(),
native: b.native,
})
.collect();
let mut global_ids: BTreeSet<DefinitionId> = BTreeSet::new();
for &member in batch {
global_ids.extend(referenced_globals_query(db, project, DefKey::new(db, member)).iter());
}
let mut globals: BTreeMap<DefinitionId, brink_analyzer::Ty> = BTreeMap::new();
for gid in global_ids {
if let Some(sig) = signature_query(db, project, DefKey::new(db, gid))
&& let Some(ty) = sig.value_ty.clone()
{
globals.insert(gid, ty);
}
}
let mut resolutions = ResolutionMap::new();
let member_files: BTreeSet<FileId> = member_bodies.values().map(|b| b.file).collect();
for file_id in member_files {
if let Some(file) = project.files(db).iter().find(|f| f.file_id(db) == file_id) {
let (file_map, _diags) = resolve_query(db, project, *file);
resolutions.extend(file_map.iter().cloned());
}
}
let opts = project.analysis_options(db);
let inline_docs = inline_docs_query(db, project);
let (signatures, bodies) = brink_analyzer::solve_scc(
batch,
&defs,
index,
&resolutions,
&globals,
inferable,
known_sigs,
opts.host_manifest.as_ref(),
inline_docs,
);
Arc::new(SolvedScc { signatures, bodies })
}
#[salsa::tracked(lru = 16384)]
pub(crate) fn inferred_signature_query<'db>(
db: &'db dyn salsa::Database,
project: ProjectInput,
def: DefKey<'db>,
) -> Option<Arc<brink_analyzer::InferredSig>> {
let def_id = def.def(db);
let membership = scc_membership_query(db, project);
let scc_id = *membership.member_of.get(&def_id)?;
let solved = solve_scc_query(db, project, DefKey::new(db, scc_id));
solved.signatures.get(&def_id).cloned().map(Arc::new)
}
#[salsa::tracked(lru = 16384)]
pub(crate) fn def_effect_atoms_query<'db>(
db: &'db dyn salsa::Database,
project: ProjectInput,
def: DefKey<'db>,
) -> Arc<brink_analyzer::EffectAtoms> {
let index = inference_index_query(db, project);
let def_id = def.def(db);
let Some(declaring_file) = index.symbols.get(&def_id).map(|info| info.file) else {
return Arc::new(brink_analyzer::EffectAtoms::default());
};
let Some(file) = project
.files(db)
.iter()
.find(|f| f.file_id(db) == declaring_file)
else {
return Arc::new(brink_analyzer::EffectAtoms::default());
};
let hir = &lowered_query(db, project, *file).hir;
let (resolutions, _diags) = resolve_query(db, project, *file);
let inferable = inferable_defs_query(db, project);
Arc::new(brink_analyzer::def_effect_atoms(
def_id,
&[(declaring_file, hir)],
index,
resolutions,
inferable,
None,
))
}
#[salsa::tracked(lru = 16384)]
pub(crate) fn effects_scc_query<'db>(
db: &'db dyn salsa::Database,
project: ProjectInput,
scc: DefKey<'db>,
) -> Arc<BTreeMap<DefinitionId, brink_analyzer::EffectRow>> {
let scc_id: SccId = scc.def(db);
let membership = scc_membership_query(db, project);
let Some(batch) = membership
.order
.iter()
.find(|comp| comp.iter().next().copied() == Some(scc_id))
else {
return Arc::new(BTreeMap::new());
};
let mut known_rows: BTreeMap<DefinitionId, brink_analyzer::EffectRow> = BTreeMap::new();
if let Some(deps) = membership.depends_on.get(&scc_id) {
for &dep in deps {
let solved = effects_scc_query(db, project, DefKey::new(db, dep));
known_rows.extend(solved.iter().map(|(k, v)| (*k, v.clone())));
}
}
let atoms: BTreeMap<DefinitionId, brink_analyzer::EffectAtoms> = batch
.iter()
.map(|&id| {
(
id,
(*def_effect_atoms_query(db, project, DefKey::new(db, id))).clone(),
)
})
.collect();
Arc::new(brink_analyzer::solve_scc_effects(
batch,
&atoms,
&known_rows,
))
}
#[salsa::tracked(lru = 16384)]
pub(crate) fn effects_query<'db>(
db: &'db dyn salsa::Database,
project: ProjectInput,
def: DefKey<'db>,
) -> Option<Arc<brink_analyzer::EffectRow>> {
let def_id = def.def(db);
let membership = scc_membership_query(db, project);
let scc_id = *membership.member_of.get(&def_id)?;
let solved = effects_scc_query(db, project, DefKey::new(db, scc_id));
solved.get(&def_id).cloned().map(Arc::new)
}
#[salsa::tracked(returns(ref))]
pub(crate) fn external_signatures_query(
db: &dyn salsa::Database,
project: ProjectInput,
) -> Arc<BTreeMap<DefinitionId, brink_analyzer::InferredSig>> {
let index = inference_index_query(db, project);
let inline_docs = inline_docs_query(db, project);
let opts = project.analysis_options(db);
Arc::new(brink_analyzer::collect_external_sigs(
index,
opts.host_manifest.as_ref(),
inline_docs,
))
}
#[salsa::tracked(returns(ref))]
pub(crate) fn type_inference_query(
db: &dyn salsa::Database,
project: ProjectInput,
) -> Arc<InferenceResult> {
let membership = scc_membership_query(db, project);
let mut signatures = BTreeMap::new();
let mut bodies = BTreeMap::new();
let mut seen: BTreeSet<SccId> = BTreeSet::new();
for comp in &membership.order {
let Some(scc_id) = comp.iter().next().copied() else {
continue;
};
if !seen.insert(scc_id) {
continue;
}
let solved = solve_scc_query(db, project, DefKey::new(db, scc_id));
signatures.extend(solved.signatures.iter().map(|(k, v)| (*k, v.clone())));
bodies.extend(solved.bodies.iter().map(|(k, v)| (*k, v.clone())));
}
signatures.extend(
external_signatures_query(db, project)
.iter()
.map(|(k, v)| (*k, v.clone())),
);
Arc::new(InferenceResult { signatures, bodies })
}
#[salsa::tracked(lru = 16384, heap_size = heap_size::infer_body_heap_size)]
pub(crate) fn infer_body_query<'db>(
db: &'db dyn salsa::Database,
project: ProjectInput,
def: DefKey<'db>,
) -> Option<Arc<brink_analyzer::BodyTypes>> {
let def_id = def.def(db);
let membership = scc_membership_query(db, project);
let scc_id = *membership.member_of.get(&def_id)?;
let solved = solve_scc_query(db, project, DefKey::new(db, scc_id));
solved.bodies.get(&def_id).cloned().map(Arc::new)
}
#[salsa::tracked(returns(ref))]
pub(crate) fn type_diagnostics_query(
db: &dyn salsa::Database,
project: ProjectInput,
file: SourceFile,
) -> Vec<Diagnostic> {
let _ = (db, project, file);
Vec::new()
}
#[derive(Clone, Default)]
pub struct LirProduct {
pub program: Option<Arc<brink_ir::lir::Program>>,
pub errors: Vec<Diagnostic>,
pub warnings: Vec<Diagnostic>,
}
impl PartialEq for LirProduct {
fn eq(&self, other: &Self) -> bool {
let program_eq = match (&self.program, &other.program) {
(Some(a), Some(b)) => Arc::ptr_eq(a, b),
(None, None) => true,
_ => false,
};
program_eq && self.errors == other.errors && self.warnings == other.warnings
}
}
#[derive(Clone, Default)]
pub(crate) struct LirLowering {
pub program: Option<Arc<brink_ir::lir::Program>>,
pub errors: Vec<Diagnostic>,
pub warnings: Vec<Diagnostic>,
}
impl PartialEq for LirLowering {
fn eq(&self, other: &Self) -> bool {
let program_eq = match (&self.program, &other.program) {
(Some(a), Some(b)) => Arc::ptr_eq(a, b),
(None, None) => true,
_ => false,
};
program_eq && self.errors == other.errors && self.warnings == other.warnings
}
}
#[salsa::tracked(returns(ref))]
pub(crate) fn struct_shape_data_query(
db: &dyn salsa::Database,
project: ProjectInput,
) -> brink_ir::lir::StructShapeData {
if project.entry(db).is_none() {
return brink_ir::lir::StructShapeData::default();
}
let files = project.files(db);
let by_id: LookupMap<FileId, SourceFile> = files.iter().map(|f| (f.file_id(db), *f)).collect();
let topo = compilation_closure_files(db, project);
let hir_refs: Vec<(FileId, &HirFile)> = topo
.iter()
.filter_map(|id| {
by_id
.get(id)
.map(|f| (*id, &lowered_query(db, project, *f).hir))
})
.collect();
let resolved = resolutions_index_query(db, project);
brink_ir::lir::build_struct_shape_data(&hir_refs, &resolved.index, &resolved.resolutions)
}
#[salsa::tracked(returns(ref))]
pub(crate) fn decl_hir_query(
db: &dyn salsa::Database,
project: ProjectInput,
file: SourceFile,
) -> HirFile {
let hir = &lowered_query(db, project, file).hir;
HirFile {
root_content: brink_ir::hir::Block::default(),
knots: Vec::new(),
..hir.clone()
}
}
#[salsa::tracked(returns(ref))]
pub(crate) fn normalized_stamped_query(
db: &dyn salsa::Database,
project: ProjectInput,
file: SourceFile,
) -> Arc<HirFile> {
let resolved = resolutions_index_query(db, project);
let hir = lowered_query(db, project, file).hir.clone();
let mut slice = [(file.file_id(db), hir)];
let ink_root = project.ink_root(db).as_deref();
let file_paths: LookupMap<FileId, String> = std::iter::once((
file.file_id(db),
crate::modules::root_relative_key(ink_root, file.path(db)).into_owned(),
))
.collect();
brink_ir::stamp_container_ids(&mut slice, &resolved.index, &file_paths);
brink_ir::normalize_file(&mut slice[0].1);
let [(_, stamped)] = slice;
Arc::new(stamped)
}
#[derive(Clone)]
pub(crate) struct PreludeDeclsResult {
pub decls: Arc<brink_ir::lir::PreludeDecls>,
}
impl PartialEq for PreludeDeclsResult {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.decls, &other.decls)
}
}
#[salsa::tracked(no_eq)]
pub(crate) fn lir_prelude_decls_query(
db: &dyn salsa::Database,
project: ProjectInput,
) -> PreludeDeclsResult {
let type_mode = match type_policy_query(db, project) {
TypePolicy::Strict => brink_ir::lir::TypeMode::Strict,
TypePolicy::Gradual => brink_ir::lir::TypeMode::Gradual,
};
if project.entry(db).is_none() {
return PreludeDeclsResult {
decls: Arc::new(brink_ir::lir::PreludeDecls::empty(type_mode)),
};
}
let files = project.files(db);
let by_id: LookupMap<FileId, SourceFile> = files.iter().map(|f| (f.file_id(db), *f)).collect();
let topo = compilation_closure_files(db, project);
let decl_refs: Vec<(FileId, &HirFile)> = topo
.iter()
.filter_map(|id| {
by_id
.get(id)
.map(|f| (*id, decl_hir_query(db, project, *f)))
})
.collect();
let resolved = resolutions_index_query(db, project);
let ink_root = project.ink_root(db).as_deref();
let file_paths: LookupMap<FileId, String> = files
.iter()
.map(|f| {
(
f.file_id(db),
crate::modules::root_relative_key(ink_root, f.path(db)).into_owned(),
)
})
.collect();
let ufcs = &ufcs_resolution_query(db, project).table;
let coalesce = coalesce_types_query(db, project);
let tables = brink_ir::lir::AnalyzerTables { ufcs, coalesce };
let decls = brink_ir::lir::build_prelude_decls(
&decl_refs,
&resolved.index,
&resolved.resolutions,
&file_paths,
type_mode,
tables,
);
PreludeDeclsResult {
decls: Arc::new(decls),
}
}
#[salsa::interned]
pub(crate) struct KnotChunkKey<'db> {
pub file: FileId,
pub knot_index: u32,
}
#[derive(Clone, Default)]
pub(crate) struct LoweredChunk {
pub chunk: Arc<brink_ir::lir::ScopeChunk>,
pub diagnostics: Vec<Diagnostic>,
}
impl PartialEq for LoweredChunk {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.chunk, &other.chunk) && self.diagnostics == other.diagnostics
}
}
#[derive(Clone)]
pub(crate) struct ChunkLoweringCtxResult {
pub ctx: Arc<brink_ir::lir::ChunkLoweringCtx>,
}
impl PartialEq for ChunkLoweringCtxResult {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.ctx, &other.ctx)
}
}
#[salsa::tracked(no_eq)]
pub(crate) fn chunk_lowering_ctx_query(
db: &dyn salsa::Database,
project: ProjectInput,
) -> ChunkLoweringCtxResult {
let resolved = resolutions_index_query(db, project);
let shape_data = struct_shape_data_query(db, project);
let type_mode = match type_policy_query(db, project) {
TypePolicy::Strict => brink_ir::lir::TypeMode::Strict,
TypePolicy::Gradual => brink_ir::lir::TypeMode::Gradual,
};
let ink_root = project.ink_root(db).as_deref();
let file_paths: LookupMap<FileId, String> = project
.files(db)
.iter()
.map(|f| {
(
f.file_id(db),
crate::modules::root_relative_key(ink_root, f.path(db)).into_owned(),
)
})
.collect();
ChunkLoweringCtxResult {
ctx: Arc::new(brink_ir::lir::ChunkLoweringCtx::new(
&resolved.resolutions,
shape_data,
file_paths,
type_mode,
)),
}
}
#[salsa::tracked(no_eq)]
pub(crate) fn lir_knot_chunk_query(
db: &dyn salsa::Database,
project: ProjectInput,
key: KnotChunkKey<'_>,
) -> LoweredChunk {
let file_id = key.file(db);
let knot_index = key.knot_index(db) as usize;
let Some(source) = project
.files(db)
.iter()
.copied()
.find(|f| f.file_id(db) == file_id)
else {
return LoweredChunk::default();
};
let resolved = resolutions_index_query(db, project);
let ctx = &chunk_lowering_ctx_query(db, project).ctx;
let hir_file = normalized_stamped_query(db, project, source);
let Some(knot) = hir_file.knots.get(knot_index) else {
return LoweredChunk::default();
};
let ufcs = &ufcs_resolution_query(db, project).table;
let coalesce = coalesce_types_query(db, project);
let tables = brink_ir::lir::AnalyzerTables { ufcs, coalesce };
let (chunk, diagnostics) = brink_ir::lir::lower_knot_chunk_incremental(
hir_file,
knot,
&resolved.index,
ctx,
file_id,
tables,
);
LoweredChunk {
chunk: Arc::new(chunk),
diagnostics,
}
}
#[salsa::tracked]
pub(crate) fn type_policy_query(db: &dyn salsa::Database, project: ProjectInput) -> TypePolicy {
project.analysis_options(db).type_policy()
}
#[salsa::tracked]
pub(crate) fn lint_policy_query(
db: &dyn salsa::Database,
project: ProjectInput,
) -> brink_analyzer::LintPolicy {
project.analysis_options(db).lints.clone()
}
#[salsa::tracked]
pub(crate) fn debug_info_policy_query(db: &dyn salsa::Database, project: ProjectInput) -> bool {
project.analysis_options(db).emit_debug_info
}
#[salsa::tracked(no_eq)]
pub(crate) fn lir_lowering_query(db: &dyn salsa::Database, project: ProjectInput) -> LirLowering {
if project.entry(db).is_none() {
return LirLowering::default();
}
if has_errors_in_closure_query(db, project) {
return LirLowering::default();
}
let files = project.files(db);
let resolved = resolutions_index_query(db, project);
let by_id: LookupMap<FileId, SourceFile> = files.iter().map(|f| (f.file_id(db), *f)).collect();
let topo = compilation_closure_files(db, project);
let ink_root = project.ink_root(db).as_deref();
let paths: LookupMap<FileId, String> = topo
.iter()
.filter_map(|id| {
by_id.get(id).map(|f| {
(
*id,
crate::modules::root_relative_key(ink_root, f.path(db)).into_owned(),
)
})
})
.collect();
let types = type_policy_query(db, project);
let lints = lint_policy_query(db, project);
let prelude_decls = lir_prelude_decls_query(db, project);
let normalized: Vec<(FileId, HirFile)> = topo
.iter()
.filter_map(|id| {
by_id
.get(id)
.map(|f| (*id, (**normalized_stamped_query(db, project, *f)).clone()))
})
.collect();
let prelude = brink_ir::lir::assemble_prelude((*prelude_decls.decls).clone(), normalized);
let ufcs = &ufcs_resolution_query(db, project).table;
let coalesce = coalesce_types_query(db, project);
let tables = brink_ir::lir::AnalyzerTables { ufcs, coalesce };
let (root_chunks, root_temp_slots) = brink_ir::lir::lower_root_content_for_prelude(
&prelude,
&resolved.index,
&resolved.resolutions,
&paths,
tables,
);
let mut lir_diagnostics = prelude.decl_diagnostics.clone();
let mut ordered_chunks: Vec<brink_ir::lir::ScopeChunk> = Vec::new();
let prelude_files = prelude.files();
let mut root_iter = root_chunks.into_iter();
for (file_id, hir_file) in &prelude_files {
if let Some((chunk, diags)) = root_iter.next() {
ordered_chunks.push(chunk);
lir_diagnostics.extend(diags);
}
for knot_index in 0..hir_file.knots.len() {
#[expect(
clippy::cast_possible_truncation,
reason = "a file won't declare anywhere near u32::MAX knots"
)]
let key = KnotChunkKey::new(db, *file_id, knot_index as u32);
let lowered = lir_knot_chunk_query(db, project, key);
ordered_chunks.push((*lowered.chunk).clone());
lir_diagnostics.extend(lowered.diagnostics.clone());
}
}
let program = brink_ir::lir::assemble_program(
&prelude,
ordered_chunks,
root_temp_slots,
&resolved.index,
&paths,
);
let mut lir_errors: Vec<Diagnostic> = Vec::new();
let mut lir_warnings: Vec<Diagnostic> = Vec::new();
for d in lir_diagnostics {
match brink_analyzer::effective_severity(d.code, types, &lints) {
None => {}
Some(Severity::Error) => lir_errors.push(d),
Some(_) => lir_warnings.push(d),
}
}
if lir_errors.is_empty() {
LirLowering {
program: Some(Arc::new(program)),
errors: lir_errors,
warnings: lir_warnings,
}
} else {
LirLowering {
program: None,
errors: lir_errors,
warnings: lir_warnings,
}
}
}
#[salsa::tracked(returns(ref), no_eq)]
pub(crate) fn lir_query(db: &dyn salsa::Database, project: ProjectInput) -> LirProduct {
let files = project.files(db);
let Some(entry) = project.entry(db) else {
return LirProduct::default();
};
let diagnostics = analysis_diagnostics_query(db, project);
let disable_all = files
.iter()
.find(|f| f.file_id(db) == entry)
.is_some_and(|f| suppressions_query(db, *f).disable_all);
let inputs: Vec<FileDiagnostics<'_>> = files
.iter()
.filter(|f| is_source_file(f.path(db)))
.map(|f| FileDiagnostics {
file: f.file_id(db),
source: f.text(db),
suppressions: suppressions_query(db, *f),
lowering: &lowered_query(db, project, *f).diagnostics,
})
.collect();
let opts = project.analysis_options(db);
let types = opts.type_policy();
let (mut errors, mut warnings) =
partition_diagnostics(&inputs, diagnostics, disable_all, types, &opts.lints);
if has_errors_query(db, project) {
return LirProduct {
program: None,
errors,
warnings,
};
}
let lowering = lir_lowering_query(db, project);
errors.extend(lowering.errors);
warnings.extend(lowering.warnings);
LirProduct {
program: lowering.program,
errors,
warnings,
}
}
#[salsa::tracked(returns(ref), no_eq)]
pub(crate) fn lir_in_closure_query(db: &dyn salsa::Database, project: ProjectInput) -> LirProduct {
let files = project.files(db);
let Some(entry) = project.entry(db) else {
return LirProduct::default();
};
let closure: LookupSet<FileId> = compilation_closure_files(db, project).into_iter().collect();
let diagnostics: Vec<Diagnostic> = analysis_diagnostics_query(db, project)
.iter()
.filter(|d| closure.contains(&d.file))
.cloned()
.collect();
let disable_all = files
.iter()
.find(|f| f.file_id(db) == entry)
.is_some_and(|f| suppressions_query(db, *f).disable_all);
let inputs: Vec<FileDiagnostics<'_>> = files
.iter()
.filter(|f| closure.contains(&f.file_id(db)))
.map(|f| FileDiagnostics {
file: f.file_id(db),
source: f.text(db),
suppressions: suppressions_query(db, *f),
lowering: &lowered_query(db, project, *f).diagnostics,
})
.collect();
let opts = project.analysis_options(db);
let types = opts.type_policy();
let (mut errors, mut warnings) =
partition_diagnostics(&inputs, &diagnostics, disable_all, types, &opts.lints);
if has_errors_in_closure_query(db, project) {
return LirProduct {
program: None,
errors,
warnings,
};
}
let lowering = lir_lowering_query(db, project);
errors.extend(lowering.errors);
warnings.extend(lowering.warnings);
LirProduct {
program: lowering.program,
errors,
warnings,
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct CompileProduct {
pub story: Option<Arc<StoryData>>,
pub errors: Vec<Diagnostic>,
pub warnings: Vec<Diagnostic>,
}
#[expect(clippy::cast_possible_truncation)]
fn populate_effect_rows(db: &dyn salsa::Database, project: ProjectInput, story: &mut StoryData) {
let inferable = inferable_defs_query(db, project);
if inferable.is_empty() {
return;
}
let mut name_lookup: BTreeMap<String, u16> = story
.name_table
.iter()
.enumerate()
.map(|(i, s)| (s.clone(), i as u16))
.collect();
let private_defs = story.private_defs.clone();
let is_private = |def: DefinitionId| {
private_defs
.binary_search_by_key(&def.to_raw(), |d| d.to_raw())
.is_ok()
};
let mut rows: Vec<EffectRowEntry> = Vec::with_capacity(inferable.len());
for &def in inferable {
let Some(row) = effects_query(db, project, DefKey::new(db, def)) else {
continue;
};
let mut calls: Vec<CallAtom> = Vec::with_capacity(row.calls.len());
for name in &row.calls {
let id = if let Some(&id) = name_lookup.get(name) {
id
} else {
let id = story.name_table.len() as u16;
story.name_table.push(name.clone());
name_lookup.insert(name.clone(), id);
id
};
calls.push(CallAtom {
name: NameId(id),
capability: CapabilityParam::Any,
handle_param: None,
});
}
rows.push(EffectRowEntry {
def,
is_entry: !is_private(def),
direct: DirectEffects {
reads: row.reads.iter().copied().collect(),
writes: row.writes.iter().copied().collect(),
calls,
opaque: row.is_pessimal(),
emits: row.emits,
tags: row.tags,
faults: row.faults,
},
dispatches: Vec::new(),
});
}
story.effect_rows = rows;
}
#[salsa::tracked(returns(ref))]
pub(crate) fn story_data_query(db: &dyn salsa::Database, project: ProjectInput) -> CompileProduct {
let lir = lir_in_closure_query(db, project);
let Some(program) = lir.program.as_ref() else {
return CompileProduct {
story: None,
errors: lir.errors.clone(),
warnings: lir.warnings.clone(),
};
};
let emit_debug_info = debug_info_policy_query(db, project);
let debug_sources: Option<std::collections::BTreeMap<FileId, String>> =
emit_debug_info.then(|| {
project
.files(db)
.iter()
.filter(|f| program.file_paths.contains_key(&f.file_id(db)))
.map(|f| (f.file_id(db), f.text(db).to_owned()))
.collect()
});
let debug_options = brink_codegen_inkb::EmitOptions {
emit_debug_info,
debug_sources: debug_sources.as_ref(),
};
match brink_codegen_inkb::emit_with_options(program, debug_options) {
Ok(mut story) => {
populate_effect_rows(db, project, &mut story);
CompileProduct {
story: Some(Arc::new(story)),
errors: lir.errors.clone(),
warnings: lir.warnings.clone(),
}
}
Err(err) => {
let mut errors = lir.errors.clone();
errors.push(Diagnostic {
file: project.entry(db).unwrap_or(FileId(0)),
range: rowan::TextRange::default(),
message: format!("{}: {err}", DiagnosticCode::E060.title()),
code: DiagnosticCode::E060,
});
CompileProduct {
story: None,
errors,
warnings: lir.warnings.clone(),
}
}
}
}
pub struct FileDiagnostics<'a> {
pub file: FileId,
pub source: &'a str,
pub suppressions: &'a Suppressions,
pub lowering: &'a [Diagnostic],
}
static NO_SUPPRESSIONS: Suppressions = Suppressions {
disable_all: false,
disable_file: false,
file_codes: Vec::new(),
malformed: Vec::new(),
line_directives: std::collections::BTreeMap::new(),
allow_scopes: Vec::new(),
};
#[must_use]
pub fn partition_diagnostics(
files: &[FileDiagnostics<'_>],
analysis_diagnostics: &[Diagnostic],
disable_all: bool,
types: brink_analyzer::TypePolicy,
lints: &brink_analyzer::LintPolicy,
) -> (Vec<Diagnostic>, Vec<Diagnostic>) {
let mut errors = Vec::new();
let mut warnings = Vec::new();
let mut partition = |d: Diagnostic| {
match brink_analyzer::effective_severity(d.code, types, lints) {
None => {}
Some(Severity::Error) => errors.push(d),
Some(_) => warnings.push(d),
}
};
for input in files {
let filtered = apply_suppressions(
input.file,
input.source,
input.lowering.to_vec(),
input.suppressions,
);
for d in filtered {
partition(d);
}
}
if !disable_all {
let mut by_file: LookupMap<FileId, Vec<Diagnostic>> = LookupMap::new();
for d in analysis_diagnostics {
by_file.entry(d.file).or_default().push(d.clone());
}
let mut file_ids: Vec<_> = by_file.keys().copied().collect();
file_ids.sort_by_key(|id| id.0);
for fid in file_ids {
let diags = by_file.remove(&fid).unwrap_or_default();
let (source, suppressions) = files
.iter()
.find(|input| input.file == fid)
.map_or(("", &NO_SUPPRESSIONS), |input| {
(input.source, input.suppressions)
});
let filtered = apply_suppressions(fid, source, diags, suppressions);
for d in filtered {
partition(d);
}
}
}
(errors, warnings)
}
fn lower_file(file_id: FileId, parse: &Parse) -> LoweredFile {
let tree = parse.tree();
let knot_entries: Vec<_> = tree
.knots()
.map(|knot_ast| lower_single_knot(file_id, &knot_ast))
.collect();
let (root_content, top_level_knots, top_diagnostics) = lower_top_level(file_id, &tree);
let (mut hir, decl_diagnostics) = lower_declarations(file_id, &tree);
hir.knots = knot_entries
.iter()
.filter_map(|(knot, _)| knot.clone())
.collect();
hir.knots.extend(top_level_knots);
hir.root_content = root_content;
let manifest = project_manifest(&hir);
let mut diagnostics = decl_diagnostics;
diagnostics.extend(top_diagnostics);
for (_, knot_diags) in &knot_entries {
diagnostics.extend(knot_diags.iter().cloned());
}
diagnostics.extend(parse.errors().iter().map(|e| Diagnostic {
file: file_id,
range: e.range,
message: e.message.clone(),
code: DiagnosticCode::E037,
}));
diagnostics.extend(brink_analyzer::check_anonymous_stateful(file_id, &hir));
let file_len = parse.syntax().text_range().end();
let admission = brink_analyzer::validate_admission(file_id, &hir, &manifest, file_len);
LoweredFile {
hir,
manifest,
diagnostics,
admission,
}
}
fn lower_native_file(
file_id: FileId,
parse: &NativeParse,
external: Option<&[brink_ir::ClaimHandlerDecl]>,
) -> LoweredFile {
let tree = parse.tree();
let (hir, manifest, mut diagnostics) =
brink_ir::hir::lower_native::lower_with_conventions(file_id, &tree, external);
diagnostics.extend(parse.errors().iter().map(|e| Diagnostic {
file: file_id,
range: e.range,
message: e.message.clone(),
code: match e.severity {
brink_syntax_native::ParseSeverity::Error => DiagnosticCode::E037,
brink_syntax_native::ParseSeverity::Warning => DiagnosticCode::E131,
},
}));
diagnostics.extend(brink_analyzer::check_native_choice_dead_end(file_id, &hir));
diagnostics.extend(brink_analyzer::check_anonymous_stateful(file_id, &hir));
let file_len = parse.syntax().text_range().end();
let mut admission = brink_analyzer::validate_admission(file_id, &hir, &manifest, file_len);
admission.extend(brink_analyzer::validate_native_accept_list(file_id, &hir));
LoweredFile {
hir,
manifest,
diagnostics,
admission,
}
}
#[cfg(test)]
mod tests {
use super::{DefKey, call_graph_query, def_effect_atoms_query, inferable_defs_query};
use crate::db::ProjectDb;
#[test]
fn call_graph_covers_direct_calls_and_creates_fn_values() {
let mut db = ProjectDb::new();
db.set_file(
"main.ink",
"VAR total = 0\nVAR extra = 0\n\
=== function bar(): int ===\n~ total = total + 1\n~ return total\n\
=== function baz(): int ===\n~ extra = extra + 100\n~ return extra\n\
=== function user(cond: int): int ===\n\
~ temp f = #fn(bar)\n{cond:\n ~ f = #fn(baz)\n}\n~ return f()\n"
.to_owned(),
);
let (salsa, project) = db.salsa_and_project();
let graph = call_graph_query(salsa, project);
for &def in inferable_defs_query(salsa, project) {
let atoms = def_effect_atoms_query(salsa, project, DefKey::new(salsa, def));
let outgoing = graph.edges.get(&def).cloned().unwrap_or_default();
for &callee in atoms
.direct_calls
.iter()
.chain(atoms.creates_fn_values.iter())
{
assert!(
outgoing.contains(&callee),
"call_graph_query's edges for {def:?} do not cover \
direct_calls ∪ creates_fn_values: missing edge to \
{callee:?} (direct_calls={:?}, creates_fn_values={:?}, \
graph edges={outgoing:?})",
atoms.direct_calls,
atoms.creates_fn_values,
);
}
}
}
#[test]
fn public_extension_predicates_are_case_insensitive() {
use super::{has_recognized_source_extension, is_native_source_path};
assert!(has_recognized_source_extension("story.ink"));
assert!(has_recognized_source_extension("story.INK"));
assert!(has_recognized_source_extension("main.brink"));
assert!(has_recognized_source_extension("main.BRINK"));
assert!(!has_recognized_source_extension("brink.toml"));
assert!(!has_recognized_source_extension("notes.txt"));
assert!(is_native_source_path("main.brink"));
assert!(is_native_source_path("main.BRINK"));
assert!(!is_native_source_path("story.ink"));
assert!(!is_native_source_path("story.INK"));
assert!(!is_native_source_path("no_extension"));
}
}