use std::collections::BTreeMap;
use brink_format::DefinitionId;
use brink_ir::hir::visit::{self, HirVisitor};
use brink_ir::{
Diagnostic, DiagnosticCode, Expr, FileId, HirFile, Knot, PrefixOp, ResolutionMap, Stitch,
SymbolIndex, SymbolKind,
};
use crate::annotations;
use crate::infer::{InferenceResult, InferredSig, Ty};
use crate::structs::{self, MistypeCtx};
use rowan::TextRange;
#[must_use]
pub fn check(
files: &[(FileId, &HirFile)],
index: &SymbolIndex,
inference: &InferenceResult,
resolutions: &ResolutionMap,
) -> Vec<Diagnostic> {
let globals = crate::infer::collect_globals(files, index, None);
let mut const_ints: BTreeMap<(FileId, String), i64> = BTreeMap::new();
for &(file, hir) in files {
for c in &hir.constants {
if let Some(v) = fold_literal_bound(&c.value) {
const_ints.insert((file, c.name.text.clone()), v);
}
}
}
let mut out = Vec::new();
for &(file, hir) in files {
let resolution_by_range = resolution_index(resolutions, file);
let mut v = RefinementVisitor {
file,
index,
globals: &globals,
signatures: &inference.signatures,
bodies: &inference.bodies,
resolution_by_range: &resolution_by_range,
const_ints: &const_ints,
current_knot_name: None,
knot_locals: None,
stitch_locals: None,
lambda_locals: Vec::new(),
diagnostics: &mut out,
};
visit::visit_with_decl_initializers(hir, &mut v);
}
out
}
struct FoldCtx<'a> {
index: &'a SymbolIndex,
resolution_by_range: &'a BTreeMap<(u32, u32), DefinitionId>,
const_ints: &'a BTreeMap<(FileId, String), i64>,
}
struct RefinementVisitor<'a> {
file: FileId,
index: &'a SymbolIndex,
globals: &'a BTreeMap<DefinitionId, Ty>,
signatures: &'a BTreeMap<DefinitionId, InferredSig>,
bodies: &'a BTreeMap<DefinitionId, crate::infer::BodyTypes>,
resolution_by_range: &'a BTreeMap<(u32, u32), DefinitionId>,
const_ints: &'a BTreeMap<(FileId, String), i64>,
current_knot_name: Option<String>,
knot_locals: Option<&'a BTreeMap<String, Ty>>,
stitch_locals: Option<&'a BTreeMap<String, Ty>>,
lambda_locals: Vec<BTreeMap<String, Ty>>,
diagnostics: &'a mut Vec<Diagnostic>,
}
impl<'a> RefinementVisitor<'a> {
fn current_locals(&self) -> Option<&BTreeMap<String, Ty>> {
self.lambda_locals
.last()
.or_else(|| self.stitch_locals.or(self.knot_locals))
}
fn fold_ctx(&self) -> FoldCtx<'a> {
FoldCtx {
index: self.index,
resolution_by_range: self.resolution_by_range,
const_ints: self.const_ints,
}
}
fn knot_def_id(&self, knot: &Knot) -> Option<DefinitionId> {
let kind = knot.symbol_kind();
annotations::def_id_for(self.index, self.file, kind, &knot.name.text)
}
}
impl HirVisitor for RefinementVisitor<'_> {
fn visit_exprs(&self) -> bool {
true
}
fn enter_knot(&mut self, knot: &Knot) {
self.current_knot_name = Some(knot.name.text.clone());
self.knot_locals = self
.knot_def_id(knot)
.and_then(|id| self.bodies.get(&id))
.map(|b| &b.locals);
}
fn exit_knot(&mut self, _knot: &Knot) {
self.current_knot_name = None;
self.knot_locals = None;
}
fn enter_stitch(&mut self, stitch: &Stitch) {
self.stitch_locals = self.current_knot_name.as_ref().and_then(|knot_name| {
let qualified = format!("{knot_name}.{}", stitch.name.text);
annotations::def_id_for(self.index, self.file, SymbolKind::Stitch, &qualified)
.and_then(|id| self.bodies.get(&id))
.map(|b| &b.locals)
});
}
fn exit_stitch(&mut self, _stitch: &Stitch) {
self.stitch_locals = None;
}
fn enter_expr(&mut self, expr: &Expr) {
let ctx = MistypeCtx {
index: self.index,
globals: self.globals,
signatures: self.signatures,
resolution_by_range: self.resolution_by_range,
locals: self
.lambda_locals
.last()
.or_else(|| self.stitch_locals.or(self.knot_locals)),
};
let fold = self.fold_ctx();
check_call(expr, self.file, &ctx, &fold, self.diagnostics);
}
fn enter_lambda(&mut self, l: &brink_ir::LambdaExpr) {
let pruned = structs::pruned_locals_for_lambda(l, self.index, self.current_locals());
self.lambda_locals.push(pruned);
}
fn exit_lambda(&mut self, _l: &brink_ir::LambdaExpr) {
self.lambda_locals.pop();
}
}
fn check_call(
expr: &Expr,
file: FileId,
ctx: &MistypeCtx<'_>,
fold: &FoldCtx<'_>,
out: &mut Vec<Diagnostic>,
) {
let Expr::Call(path, args) = expr else {
return;
};
let [seg] = path.segments.as_slice() else {
return;
};
if seg.text != "int" {
return;
}
if ctx.resolution_by_range.contains_key(&range_key(path.range)) {
return; }
let [arg] = args.as_slice() else {
return; };
if let Expr::Range(r) = arg {
match (fold_bound(&r.start, fold), fold_bound(&r.end, fold)) {
(Some(start), Some(end)) => {
let inhabited = if r.inclusive {
start <= end
} else {
start < end
};
if !inhabited {
out.push(diag(
file,
path.range,
format!(
"{}: this range is provably empty — `int` draws one element, \
and there is nothing to draw",
DiagnosticCode::E117.title(),
),
));
}
}
_ => {
out.push(diag(
file,
path.range,
format!(
"{}: these bounds are not statically provable — validate with \
`non_empty(a..b)` and draw from its `some` payload",
DiagnosticCode::E117.title(),
),
));
}
}
return;
}
if let Some(Ty::Range { non_empty: false }) = structs::classify_expr_ty(arg, ctx) {
out.push(diag(
file,
path.range,
format!(
"{}: this range is possibly empty — validate with `non_empty(r)` \
(the evidence is minted once; every later draw is free)",
DiagnosticCode::E117.title(),
),
));
}
}
fn range_key(range: TextRange) -> (u32, u32) {
(range.start().into(), range.end().into())
}
fn resolution_index(
resolutions: &ResolutionMap,
file: FileId,
) -> BTreeMap<(u32, u32), DefinitionId> {
resolutions
.iter()
.filter(|r| r.file == file)
.map(|r| (range_key(r.range), r.target))
.collect()
}
fn diag(file: FileId, range: TextRange, message: String) -> Diagnostic {
Diagnostic {
file,
range,
message,
code: DiagnosticCode::E117,
}
}
fn fold_bound(expr: &Expr, fold: &FoldCtx<'_>) -> Option<i64> {
match expr {
Expr::Path(p) => {
let def = fold.resolution_by_range.get(&range_key(p.range))?;
let info = fold.index.symbols.get(def)?;
if info.kind != SymbolKind::Constant {
return None;
}
fold.const_ints
.get(&(info.file, info.name.clone()))
.copied()
}
_ => fold_literal_bound(expr),
}
}
fn fold_literal_bound(expr: &Expr) -> Option<i64> {
match expr {
Expr::Int(n) => Some(i64::from(*n)),
Expr::Prefix(PrefixOp::Negate, inner) => fold_literal_bound(inner).map(|n| -n),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use brink_ir::hir::lower;
fn check_all(src: &str) -> Vec<Diagnostic> {
let parsed = brink_syntax::parse(src);
let (hir, manifest, _diag) = lower(FileId(0), &parsed.tree());
let (index, _diag) = crate::symbol_index(&[(FileId(0), &manifest)]);
let (resolutions, _diag) =
crate::resolve(FileId(0), &manifest, &index, &crate::ImportScope::default());
let inference = crate::infer_project(
&[(FileId(0), &hir)],
&index,
&resolutions,
None,
&BTreeMap::new(),
);
check(&[(FileId(0), &hir)], &index, &inference, &resolutions)
}
#[test]
fn annotated_fn_param_non_range_type_never_reaches_e117() {
let diags = check_all("=== main(r: int) ===\n~ x = int(r)\n-> DONE\n");
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn empty_range_literal_argument_is_still_e117() {
let diags = check_all("=== main ===\n~ x = int(0..0)\n-> DONE\n");
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E117);
}
}