use std::collections::BTreeMap;
use std::collections::BTreeSet;
use brink_format::DefinitionId;
use brink_ir::{
Diagnostic, DiagnosticCode, EffectsAssertion, FileId, HirFile, SymbolIndex, SymbolKind,
};
use rowan::TextRange;
use crate::infer::EffectRow;
use crate::resolve::{ImportScope, lookup_by_name};
struct Ctx<'a> {
index: &'a SymbolIndex,
scope: &'a ImportScope,
}
#[must_use]
pub fn check(
file: FileId,
hir: &HirFile,
index: &SymbolIndex,
scope: &ImportScope,
rows: &BTreeMap<DefinitionId, EffectRow>,
) -> Vec<Diagnostic> {
let ctx = Ctx { index, scope };
let mut out = Vec::new();
for knot in &hir.knots {
let kind = knot.symbol_kind();
check_one(
file,
knot.effects_assertion.as_ref(),
kind,
&knot.name.text,
&ctx,
rows,
&mut out,
);
for stitch in &knot.stitches {
let qualified = format!("{}.{}", knot.name.text, stitch.name.text);
check_one(
file,
stitch.effects_assertion.as_ref(),
SymbolKind::Stitch,
&qualified,
&ctx,
rows,
&mut out,
);
}
}
out
}
#[must_use]
pub fn assertion_defs(hir: &HirFile, index: &SymbolIndex, file: FileId) -> Vec<DefinitionId> {
let mut out = Vec::new();
for knot in &hir.knots {
let kind = knot.symbol_kind();
if knot.effects_assertion.is_some()
&& let Some(id) = find_def_id(index, file, kind, &knot.name.text)
{
out.push(id);
}
for stitch in &knot.stitches {
if stitch.effects_assertion.is_some() {
let qualified = format!("{}.{}", knot.name.text, stitch.name.text);
if let Some(id) = find_def_id(index, file, SymbolKind::Stitch, &qualified) {
out.push(id);
}
}
}
}
out
}
fn check_one(
file: FileId,
assertion: Option<&EffectsAssertion>,
kind: SymbolKind,
name: &str,
ctx: &Ctx<'_>,
rows: &BTreeMap<DefinitionId, EffectRow>,
out: &mut Vec<Diagnostic>,
) {
let Some(assertion) = assertion else {
return;
};
let Some(def_id) = find_def_id(ctx.index, file, kind, name) else {
return;
};
let Some(inferred) = rows.get(&def_id) else {
return;
};
if assertion.silent && (inferred.emits || inferred.is_pessimal()) {
out.push(Diagnostic {
file,
range: assertion.range,
code: DiagnosticCode::E108,
message: if inferred.is_pessimal() {
"inferred effects are unbounded (a call through a function value, or an unresolved callee) — the `silent` assertion cannot cover this definition"
.to_string()
} else {
"inferred effects exceed the `silent` assertion: the definition can produce content (a content line, or a transitive call to an emitter)"
.to_string()
},
});
}
if assertion.total && (inferred.faults || inferred.is_pessimal()) {
out.push(Diagnostic {
file,
range: assertion.range,
code: DiagnosticCode::E109,
message: if inferred.is_pessimal() {
"inferred effects are unbounded (a call through a function value, or an unresolved callee) — the `total` assertion cannot cover this definition"
.to_string()
} else {
"inferred effects exceed the `total` assertion: the definition can raise a turn-terminating fault"
.to_string()
},
});
}
if !assertion.pure
&& assertion.reads.is_empty()
&& assertion.writes.is_empty()
&& assertion.calls.is_empty()
{
return;
}
let mut well_formed = true;
let mut declared_reads = BTreeSet::new();
for n in &assertion.reads {
if let Some(id) = resolve_cell(ctx, n) {
declared_reads.insert(id);
} else {
out.push(unknown_name_diagnostic(file, assertion.range, n));
well_formed = false;
}
}
let mut declared_writes = BTreeSet::new();
for n in &assertion.writes {
if let Some(id) = resolve_cell(ctx, n) {
declared_writes.insert(id);
} else {
out.push(unknown_name_diagnostic(file, assertion.range, n));
well_formed = false;
}
}
let mut declared_calls = BTreeSet::new();
for n in &assertion.calls {
if external_declared(ctx, n) {
declared_calls.insert(n.clone());
} else {
out.push(unknown_name_diagnostic(file, assertion.range, n));
well_formed = false;
}
}
if !well_formed {
return;
}
let declared_row = EffectRow {
reads: declared_reads,
writes: declared_writes,
calls: declared_calls,
opaque: false,
emits: inferred.emits,
tags: inferred.tags,
faults: inferred.faults,
faults_refined: inferred.faults_refined,
holes: BTreeSet::new(),
};
if !declared_row.covers(inferred) {
out.push(Diagnostic {
file,
range: assertion.range,
code: DiagnosticCode::E103,
message: exceedance_message(&declared_row, inferred, ctx.index),
});
}
}
fn find_def_id(
index: &SymbolIndex,
file: FileId,
kind: SymbolKind,
name: &str,
) -> Option<DefinitionId> {
index.by_name.get(name)?.iter().copied().find(|id| {
index
.symbols
.get(id)
.is_some_and(|info| info.file == file && info.kind == kind)
})
}
fn resolve_cell(ctx: &Ctx<'_>, name: &str) -> Option<DefinitionId> {
let resolved = lookup_by_name(
ctx.index,
ctx.scope,
name,
&[SymbolKind::Variable, SymbolKind::Constant],
);
if resolved.is_some() {
return resolved;
}
if name == "rng" {
return Some(DefinitionId::RNG_CELL);
}
None
}
fn external_declared(ctx: &Ctx<'_>, name: &str) -> bool {
lookup_by_name(ctx.index, ctx.scope, name, &[SymbolKind::External]).is_some()
}
fn unknown_name_diagnostic(file: FileId, range: TextRange, name: &str) -> Diagnostic {
Diagnostic {
file,
range,
code: DiagnosticCode::E102,
message: format!(
"the effects assertion names `{name}`, which isn't a declared global VAR/CONST or EXTERNAL anywhere in the project"
),
}
}
fn exceedance_message(declared: &EffectRow, inferred: &EffectRow, index: &SymbolIndex) -> String {
if inferred.is_pessimal() {
return "inferred effects are unbounded (a call through a function value, or an \
unresolved callee) — no effects assertion can cover this definition"
.to_string();
}
let name_of = |id: &DefinitionId| {
if *id == DefinitionId::RNG_CELL {
return "rng (the std::rand RNG state cell)".to_string();
}
index
.symbols
.get(id)
.map_or_else(|| format!("{id:?}"), |info| info.name.clone())
};
let mut parts = Vec::new();
let extra_reads: Vec<String> = inferred
.reads
.difference(&declared.reads)
.map(name_of)
.collect();
if !extra_reads.is_empty() {
parts.push(format!("reads {}", extra_reads.join(", ")));
}
let extra_writes: Vec<String> = inferred
.writes
.difference(&declared.writes)
.map(name_of)
.collect();
if !extra_writes.is_empty() {
parts.push(format!("writes {}", extra_writes.join(", ")));
}
let extra_calls: Vec<String> = inferred
.calls
.difference(&declared.calls)
.cloned()
.collect();
if !extra_calls.is_empty() {
parts.push(format!("calls {}", extra_calls.join(", ")));
}
format!(
"inferred effects exceed the effects assertion's declared bound: {}",
parts.join("; ")
)
}