use std::collections::BTreeMap;
use brink_format::DefinitionId;
use brink_ir::{
Block, BlockStmt, Diagnostic, DiagnosticCode, Expr, FileId, HirFile, ResolutionMap, Stmt,
SymbolIndex, SymbolKind,
};
use rowan::TextRange;
use crate::infer::EffectRow;
#[must_use]
pub fn check(
file: FileId,
hir: &HirFile,
index: &SymbolIndex,
resolutions: &ResolutionMap,
rows: &BTreeMap<DefinitionId, EffectRow>,
) -> Vec<Diagnostic> {
let by_range: BTreeMap<(u32, u32), DefinitionId> = resolutions
.iter()
.filter(|r| r.file == file)
.map(|r| ((r.range.start().into(), r.range.end().into()), r.target))
.collect();
let ctx = Ctx {
index,
rows,
by_range,
};
let mut sites: Vec<AwaitSite<'_>> = Vec::new();
for knot in &hir.knots {
collect_block(&knot.body, &mut sites);
for stitch in &knot.stitches {
collect_block(&stitch.body, &mut sites);
}
}
let mut out = Vec::new();
for site in sites {
if ctx.condition_is_effectful(site.condition) {
out.push(Diagnostic {
file,
range: site.range,
message: format!(
"{}: an `await` suspension point re-evaluates its condition to decide when \
to wake, so the condition must be read-only (docs/flow-suspension-spec.md \
§5)",
DiagnosticCode::E105.title(),
),
code: DiagnosticCode::E105,
});
}
}
out
}
#[must_use]
pub fn hir_has_await(hir: &HirFile) -> bool {
let mut sites = Vec::new();
for knot in &hir.knots {
collect_block(&knot.body, &mut sites);
for stitch in &knot.stitches {
collect_block(&stitch.body, &mut sites);
}
if !sites.is_empty() {
return true;
}
}
!sites.is_empty()
}
#[must_use]
pub fn condition_callees(
file: FileId,
hir: &HirFile,
resolutions: &ResolutionMap,
) -> std::collections::BTreeSet<DefinitionId> {
let by_range: BTreeMap<(u32, u32), DefinitionId> = resolutions
.iter()
.filter(|r| r.file == file)
.map(|r| ((r.range.start().into(), r.range.end().into()), r.target))
.collect();
let mut sites: Vec<AwaitSite<'_>> = Vec::new();
for knot in &hir.knots {
collect_block(&knot.body, &mut sites);
for stitch in &knot.stitches {
collect_block(&stitch.body, &mut sites);
}
}
let mut out = std::collections::BTreeSet::new();
for site in sites {
collect_call_callees(site.condition, &by_range, &mut out);
}
out
}
fn collect_call_callees(
expr: &Expr,
by_range: &BTreeMap<(u32, u32), DefinitionId>,
out: &mut std::collections::BTreeSet<DefinitionId>,
) {
match expr {
Expr::Call(path, args) => {
let key = (path.range.start().into(), path.range.end().into());
if let Some(&def) = by_range.get(&key) {
out.insert(def);
}
for a in args {
collect_call_callees(a, by_range, out);
}
}
Expr::Prefix(_, inner) | Expr::Postfix(inner, _) => {
collect_call_callees(inner, by_range, out);
}
Expr::Infix(ie) => {
collect_call_callees(&ie.lhs, by_range, out);
collect_call_callees(&ie.rhs, by_range, out);
}
Expr::Index(idx) => {
collect_call_callees(&idx.base, by_range, out);
collect_call_callees(&idx.index, by_range, out);
}
Expr::FieldAccess(fa) => collect_call_callees(&fa.base, by_range, out),
Expr::Range(r) => {
collect_call_callees(&r.start, by_range, out);
collect_call_callees(&r.end, by_range, out);
}
Expr::ArrayLiteral(a) => {
for e in &a.elements {
collect_call_callees(e, by_range, out);
}
}
Expr::MapLiteral(m) => {
for (k, v) in &m.entries {
collect_call_callees(k, by_range, out);
collect_call_callees(v, by_range, out);
}
}
Expr::StructLiteral(sl) => {
for (_, v) in &sl.fields {
collect_call_callees(v, by_range, out);
}
}
Expr::Int(_)
| Expr::Float(_)
| Expr::Bool(_)
| Expr::String(_)
| Expr::Null
| Expr::Path(_)
| Expr::DivertTarget(_)
| Expr::ListLiteral(_)
| Expr::FnLiteral(_)
| Expr::Lambda(_)
| Expr::RefArg(_)
| Expr::Fragment(_) => {}
}
}
struct AwaitSite<'a> {
range: TextRange,
condition: &'a Expr,
}
struct Ctx<'a> {
index: &'a SymbolIndex,
rows: &'a BTreeMap<DefinitionId, EffectRow>,
by_range: BTreeMap<(u32, u32), DefinitionId>,
}
impl Ctx<'_> {
fn condition_is_effectful(&self, cond: &Expr) -> bool {
let mut effectful = false;
self.walk_expr(cond, &mut effectful);
effectful
}
fn walk_expr(&self, expr: &Expr, effectful: &mut bool) {
if *effectful {
return; }
match expr {
Expr::Call(path, args) => {
if self.call_is_effectful(path, args.len()) {
*effectful = true;
return;
}
for a in args {
self.walk_expr(a, effectful);
}
}
Expr::Prefix(_, inner) | Expr::Postfix(inner, _) => self.walk_expr(inner, effectful),
Expr::Infix(ie) => {
self.walk_expr(&ie.lhs, effectful);
self.walk_expr(&ie.rhs, effectful);
}
Expr::Index(idx) => {
self.walk_expr(&idx.base, effectful);
self.walk_expr(&idx.index, effectful);
}
Expr::FieldAccess(fa) => self.walk_expr(&fa.base, effectful),
Expr::Range(r) => {
self.walk_expr(&r.start, effectful);
self.walk_expr(&r.end, effectful);
}
Expr::ArrayLiteral(a) => {
for e in &a.elements {
self.walk_expr(e, effectful);
}
}
Expr::MapLiteral(m) => {
for (k, v) in &m.entries {
self.walk_expr(k, effectful);
self.walk_expr(v, effectful);
}
}
Expr::StructLiteral(sl) => {
for (_, v) in &sl.fields {
self.walk_expr(v, effectful);
}
}
Expr::Int(_)
| Expr::Float(_)
| Expr::Bool(_)
| Expr::String(_)
| Expr::Null
| Expr::Path(_)
| Expr::DivertTarget(_)
| Expr::ListLiteral(_)
| Expr::FnLiteral(_)
| Expr::Lambda(_)
| Expr::RefArg(_)
| Expr::Fragment(_) => {}
}
}
fn call_is_effectful(&self, path: &brink_ir::Path, arg_count: usize) -> bool {
let range = path.range;
let key = (range.start().into(), range.end().into());
let Some(&def) = self.by_range.get(&key) else {
if let [seg] = path.segments.as_slice() {
let fx = crate::infer::intrinsic_effects(&seg.text, arg_count);
return fx.rng_write || fx.faults;
}
return false;
};
if let Some(row) = self.rows.get(&def) {
return row.is_pessimal() || !row.writes.is_empty() || !row.calls.is_empty();
}
matches!(
self.index.symbols.get(&def).map(|s| s.kind),
Some(SymbolKind::External)
)
}
}
fn collect_block<'a>(block: &'a Block, out: &mut Vec<AwaitSite<'a>>) {
for stmt in &block.stmts {
collect_stmt(stmt, out);
}
}
fn collect_stmt<'a>(stmt: &'a Stmt, out: &mut Vec<AwaitSite<'a>>) {
match stmt {
Stmt::Await(a) => {
if let Some(cond) = &a.condition {
out.push(AwaitSite {
range: a.ptr.text_range(),
condition: cond,
});
}
}
Stmt::LogicBlock(lb) => {
for bs in &lb.stmts {
collect_block_stmt(bs, out);
}
}
Stmt::ChoiceSet(cs) => {
for choice in &cs.choices {
collect_block(&choice.body, out);
}
collect_block(&cs.continuation, out);
}
Stmt::LabeledBlock(b) => collect_block(b, out),
Stmt::Conditional(c) => {
for branch in &c.branches {
collect_block(&branch.body, out);
}
}
Stmt::Sequence(s) => {
for branch in &s.branches {
collect_block(&branch.body, out);
}
}
Stmt::Content(_)
| Stmt::Divert(_)
| Stmt::TunnelCall(_)
| Stmt::ThreadStart(_)
| Stmt::TempDecl(_)
| Stmt::Assignment(_)
| Stmt::Return(_)
| Stmt::ExprStmt(_)
| Stmt::EndOfLine
| Stmt::AttachElement(_)
| Stmt::EndElementRun => {}
}
}
fn collect_block_stmt<'a>(bs: &'a BlockStmt, out: &mut Vec<AwaitSite<'a>>) {
match bs {
BlockStmt::Await(a) => {
if let Some(cond) = &a.condition {
out.push(AwaitSite {
range: a.ptr.text_range(),
condition: cond,
});
}
}
BlockStmt::While(w) => {
if w.is_await {
out.push(AwaitSite {
range: w.ptr.text_range(),
condition: &w.condition,
});
}
for s in &w.body {
collect_block_stmt(s, out);
}
}
BlockStmt::If(i) => collect_if(i, out),
BlockStmt::For(f) => {
for s in &f.body {
collect_block_stmt(s, out);
}
}
BlockStmt::TempDecl(_)
| BlockStmt::Assignment(_)
| BlockStmt::Return(_)
| BlockStmt::ExprStmt(_)
| BlockStmt::Break(_)
| BlockStmt::Continue(_) => {}
}
}
fn collect_if<'a>(i: &'a brink_ir::IfStmt, out: &mut Vec<AwaitSite<'a>>) {
for s in &i.body {
collect_block_stmt(s, out);
}
match &i.else_branch {
Some(brink_ir::ElseBranch::ElseIf(inner)) => collect_if(inner, out),
Some(brink_ir::ElseBranch::Else(stmts)) => {
for s in stmts {
collect_block_stmt(s, out);
}
}
None => {}
}
}