use brink_ir::hir::visit::{self, HirVisitor};
use brink_ir::{
AssignOp, Block, Diagnostic, DiagnosticCode, Expr, FileId, HirFile, Knot, LambdaExpr, Stmt,
};
use rowan::TextRange;
use crate::determinism::{LookupMap, LookupSet};
pub(crate) struct DeclSite {
pub(crate) range: TextRange,
pub(crate) owner: String,
}
pub(crate) struct Read {
pub(crate) name: String,
pub(crate) range: TextRange,
}
pub fn check(file: FileId, hir: &HirFile, is_native: bool) -> Vec<Diagnostic> {
let mut out = Vec::new();
check_frame(
file,
&hir.root_content,
"the file's root content",
&[],
is_native,
&mut out,
);
for knot in &hir.knots {
check_knot(file, knot, is_native, &mut out);
}
out
}
fn check_knot(file: FileId, knot: &Knot, is_native: bool, out: &mut Vec<Diagnostic>) {
let knot_noun = if is_native { "flow" } else { "knot" };
let knot_owner = format!("{knot_noun} `{}`", knot.name.text);
let knot_params: Vec<&str> = knot.params.iter().map(|p| p.name.text.as_str()).collect();
check_frame(file, &knot.body, &knot_owner, &knot_params, is_native, out);
for stitch in &knot.stitches {
let stitch_owner = format!("stitch `{}.{}`", knot.name.text, stitch.name.text);
let stitch_params: Vec<&str> = stitch.params.iter().map(|p| p.name.text.as_str()).collect();
check_frame(
file,
&stitch.body,
&stitch_owner,
&stitch_params,
is_native,
out,
);
}
}
fn check_frame(
file: FileId,
block: &Block,
owner: &str,
params: &[&str],
is_native: bool,
out: &mut Vec<Diagnostic>,
) {
let decl_keyword = if is_native { "let" } else { "temp" };
let mut decls: LookupMap<String, DeclSite> = LookupMap::new();
collect_decls(block, owner, &mut decls);
if decls.is_empty() {
return;
}
let mut dominated: LookupSet<TextRange> = LookupSet::new();
mark_dominated(block, &mut dominated);
let mut reads = Vec::new();
let mut skipped = LookupSet::new();
let mut v = ReadCollector {
reads: &mut reads,
skipped: &mut skipped,
lambda_depth: 0,
};
visit::walk_block(block, &mut v);
for read in &reads {
if dominated.contains(&read.range) || skipped.contains(&read.range) {
continue;
}
if params.contains(&read.name.as_str()) {
continue;
}
let Some(decl) = decls.get(&read.name) else {
continue;
};
let name = &read.name;
let owner = &decl.owner;
let when = if decl.range.start() >= read.range.end() {
"is written further down"
} else {
"runs on a path this read does not pass through"
};
out.push(Diagnostic {
file,
range: read.range,
message: format!(
"{}: `{name}` is read here, but the `~ {decl_keyword} {name}` that declares \
it (in {owner}) {when} — so the slot may still be unset, and an unset \
temp reads as `0`",
DiagnosticCode::E193.title(),
),
code: DiagnosticCode::E193,
});
}
}
pub(crate) fn collect_decls(block: &Block, owner: &str, out: &mut LookupMap<String, DeclSite>) {
for stmt in &block.stmts {
match stmt {
Stmt::TempDecl(decl) => {
if !out.contains_key(&decl.name.text) {
out.insert(
decl.name.text.clone(),
DeclSite {
range: decl.ptr.text_range(),
owner: owner.to_owned(),
},
);
}
}
Stmt::ChoiceSet(cs) => {
for choice in &cs.choices {
collect_decls(&choice.body, owner, out);
}
collect_decls(&cs.continuation, owner, out);
}
Stmt::Conditional(cond) => {
for branch in &cond.branches {
collect_decls(&branch.body, owner, out);
}
}
Stmt::Sequence(seq) => {
for branch in &seq.branches {
collect_decls(&branch.body, owner, out);
}
}
Stmt::LabeledBlock(inner) => collect_decls(inner, owner, out),
_ => {}
}
}
}
fn mark_dominated(block: &Block, out: &mut LookupSet<TextRange>) {
let mut subtree: Option<Vec<Read>> = None;
for stmt in &block.stmts {
if let Stmt::TempDecl(decl) = stmt {
let reads = subtree.get_or_insert_with(|| collect_subtree_reads(block));
let decl_end = decl.ptr.text_range().end();
for read in reads.iter() {
if read.name == decl.name.text && read.range.start() >= decl_end {
out.insert(read.range);
}
}
}
}
for child in child_blocks(block) {
mark_dominated(child, out);
}
}
fn collect_subtree_reads(block: &Block) -> Vec<Read> {
let mut reads = Vec::new();
let mut skipped = LookupSet::new();
let mut v = ReadCollector {
reads: &mut reads,
skipped: &mut skipped,
lambda_depth: 0,
};
visit::walk_block(block, &mut v);
reads
}
fn child_blocks(block: &Block) -> Vec<&Block> {
let mut out = Vec::new();
for stmt in &block.stmts {
match stmt {
Stmt::ChoiceSet(cs) => {
for choice in &cs.choices {
out.push(&choice.body);
}
out.push(&cs.continuation);
}
Stmt::Conditional(cond) => out.extend(cond.branches.iter().map(|b| &b.body)),
Stmt::Sequence(seq) => out.extend(seq.branches.iter().map(|b| &b.body)),
Stmt::LabeledBlock(inner) => out.push(inner),
_ => {}
}
}
out
}
pub(crate) struct ReadCollector<'a> {
pub(crate) reads: &'a mut Vec<Read>,
pub(crate) skipped: &'a mut LookupSet<TextRange>,
pub(crate) lambda_depth: u32,
}
impl HirVisitor for ReadCollector<'_> {
fn visit_exprs(&self) -> bool {
true
}
fn enter_stmt(&mut self, stmt: &Stmt) {
if let Stmt::Assignment(a) = stmt
&& a.op == AssignOp::Set
&& let Expr::Path(p) = &a.target
{
self.skipped.insert(p.range);
}
}
fn enter_lambda(&mut self, _lambda: &LambdaExpr) {
self.lambda_depth += 1;
}
fn exit_lambda(&mut self, _lambda: &LambdaExpr) {
self.lambda_depth = self.lambda_depth.saturating_sub(1);
}
fn enter_expr(&mut self, expr: &Expr) {
if self.lambda_depth > 0 {
return;
}
let Expr::Path(p) = expr else { return };
let [seg] = p.segments.as_slice() else {
return;
};
self.reads.push(Read {
name: seg.text.clone(),
range: p.range,
});
}
}