use std::collections::BTreeMap;
use brink_format::DefinitionId;
use brink_ir::{
Block, BlockStmt, Content, ContentPart, Diagnostic, DiagnosticCode, Expr, FileId, HirFile,
ResolutionMap, Stmt, SymbolIndex,
};
use rowan::TextRange;
use crate::infer::EffectRow;
fn callback_arg_index(name: &str) -> Option<usize> {
match name {
"sort_by" | "sorted_by" | "map" | "filter" | "filter_map" => Some(1),
"fold" => Some(2),
_ => None,
}
}
fn verb_name(name: &str) -> Option<&'static str> {
match name {
"sort_by" => Some("sort_by"),
"sorted_by" => Some("sorted_by"),
"map" => Some("map"),
"filter" => Some("filter"),
"fold" => Some("fold"),
"filter_map" => Some("filter_map"),
_ => None,
}
}
#[must_use]
pub fn check(
file: FileId,
hir: &HirFile,
index: &SymbolIndex,
resolutions: &ResolutionMap,
rows: &BTreeMap<DefinitionId, EffectRow>,
) -> Vec<Diagnostic> {
let by_range = range_map(file, resolutions);
let is_fn_target = |range: TextRange| is_fn_target_ref(range, &by_range, index);
let ctx = CollectCtx {
native: hir.native,
is_fn_target: &is_fn_target,
};
let mut sites = Vec::new();
collect_sites(hir, &ctx, &mut sites);
let mut out = Vec::new();
for site in sites {
let key = (
site.target_range.start().into(),
site.target_range.end().into(),
);
let Some(def) = by_range.get(&key) else {
continue; };
let Some(row) = rows.get(def) else {
continue; };
if let Some(exceedance) = contract_exceedance(row, index) {
let (role, requirement) = callback_role(site.verb);
out.push(Diagnostic {
file,
range: site.call_range,
message: format!(
"{}: `{}`'s {role} `{}` {} — {requirement}",
DiagnosticCode::E119.title(),
site.verb,
site.target_name,
exceedance,
),
code: DiagnosticCode::E119,
});
}
}
out
}
#[must_use]
pub fn hir_has_comparator_site(hir: &HirFile) -> bool {
let is_fn_target = |_: TextRange| true;
let ctx = CollectCtx {
native: hir.native,
is_fn_target: &is_fn_target,
};
let mut sites = Vec::new();
collect_sites(hir, &ctx, &mut sites);
!sites.is_empty()
}
#[must_use]
pub fn comparator_callees(
file: FileId,
hir: &HirFile,
index: &SymbolIndex,
resolutions: &ResolutionMap,
) -> std::collections::BTreeSet<DefinitionId> {
let by_range = range_map(file, resolutions);
let is_fn_target = |range: TextRange| is_fn_target_ref(range, &by_range, index);
let ctx = CollectCtx {
native: hir.native,
is_fn_target: &is_fn_target,
};
let mut sites = Vec::new();
collect_sites(hir, &ctx, &mut sites);
let mut out = std::collections::BTreeSet::new();
for site in sites {
let key = (
site.target_range.start().into(),
site.target_range.end().into(),
);
if let Some(&def) = by_range.get(&key) {
out.insert(def);
}
}
out
}
fn callback_role(verb: &str) -> (&'static str, &'static str) {
match verb {
"sort_by" | "sorted_by" => (
"comparator",
"a comparator must be a pure, silent `fn(T, T): int` (stdlib-spec §4b: the order \
must depend only on the two comparands)",
),
_ => (
"callback",
"the callback must be pure and silent (stdlib-spec §4: the quartet is \
pure-required, which is what makes iteration order unobservable) — make it pure, \
or say `each`/`map_each`",
),
}
}
struct CollectCtx<'a> {
native: bool,
is_fn_target: &'a dyn Fn(TextRange) -> bool,
}
fn range_key(range: TextRange) -> (u32, u32) {
(range.start().into(), range.end().into())
}
fn is_fn_target_ref(
range: TextRange,
by_range: &BTreeMap<(u32, u32), DefinitionId>,
index: &SymbolIndex,
) -> bool {
by_range.get(&range_key(range)).is_some_and(|def| {
index
.symbols
.get(def)
.is_some_and(brink_ir::SymbolInfo::is_function_definition)
})
}
fn range_map(file: FileId, resolutions: &ResolutionMap) -> BTreeMap<(u32, u32), DefinitionId> {
resolutions
.iter()
.filter(|r| r.file == file)
.map(|r| ((r.range.start().into(), r.range.end().into()), r.target))
.collect()
}
fn contract_exceedance(row: &EffectRow, index: &SymbolIndex) -> Option<String> {
let name_of = |id: &DefinitionId| {
index
.symbols
.get(id)
.map_or_else(|| format!("{id:?}"), |info| info.name.clone())
};
let mut parts = Vec::new();
if !row.reads.is_empty() {
let names: Vec<String> = row.reads.iter().map(name_of).collect();
parts.push(format!("reads {}", names.join(", ")));
}
if !row.writes.is_empty() {
let names: Vec<String> = row.writes.iter().map(name_of).collect();
parts.push(format!("writes {}", names.join(", ")));
}
if !row.calls.is_empty() {
let names: Vec<String> = row.calls.iter().cloned().collect();
parts.push(format!("calls {}", names.join(", ")));
}
if row.emits {
parts.push("emits content".to_string());
}
if row.tags {
parts.push("touches the tag channel".to_string());
}
if parts.is_empty() {
None
} else {
Some(parts.join("; "))
}
}
struct ComparatorSite {
call_range: TextRange,
verb: &'static str,
target_range: TextRange,
target_name: String,
}
fn collect_sites(hir: &HirFile, ctx: &CollectCtx<'_>, out: &mut Vec<ComparatorSite>) {
collect_block(&hir.root_content, ctx, out);
for knot in &hir.knots {
collect_block(&knot.body, ctx, out);
for stitch in &knot.stitches {
collect_block(&stitch.body, ctx, out);
}
}
for var in &hir.variables {
collect_expr(&var.value, ctx, out);
}
for c in &hir.constants {
collect_expr(&c.value, ctx, out);
}
}
fn collect_block(block: &Block, ctx: &CollectCtx<'_>, out: &mut Vec<ComparatorSite>) {
for stmt in &block.stmts {
collect_stmt(stmt, ctx, out);
}
}
fn collect_stmt(stmt: &Stmt, ctx: &CollectCtx<'_>, out: &mut Vec<ComparatorSite>) {
match stmt {
Stmt::TempDecl(t) => {
if let Some(v) = &t.value {
collect_expr(v, ctx, out);
}
}
Stmt::Assignment(a) => {
collect_expr(&a.target, ctx, out);
collect_expr(&a.value, ctx, out);
}
Stmt::Return(r) => {
if let Some(v) = &r.value {
collect_expr(v, ctx, out);
}
for a in &r.onwards_args {
collect_expr(a, ctx, out);
}
}
Stmt::ExprStmt(e) | Stmt::AttachElement(e) => collect_expr(e, ctx, out),
Stmt::Await(a) => {
if let Some(cond) = &a.condition {
collect_expr(cond, ctx, out);
}
}
Stmt::LogicBlock(lb) => {
for bs in &lb.stmts {
collect_block_stmt(bs, ctx, out);
}
}
Stmt::ChoiceSet(cs) => {
for choice in &cs.choices {
if let Some(cond) = &choice.condition {
collect_expr(cond, ctx, out);
}
collect_block(&choice.body, ctx, out);
}
collect_block(&cs.continuation, ctx, out);
}
Stmt::LabeledBlock(b) => collect_block(b, ctx, out),
Stmt::Conditional(c) => collect_conditional(c, ctx, out),
Stmt::Sequence(s) => {
for branch in &s.branches {
collect_block(&branch.body, ctx, out);
}
}
Stmt::Content(content) => collect_content(content, ctx, out),
Stmt::Divert(_)
| Stmt::TunnelCall(_)
| Stmt::ThreadStart(_)
| Stmt::EndOfLine
| Stmt::EndElementRun => {}
}
}
fn collect_conditional(
c: &brink_ir::Conditional,
ctx: &CollectCtx<'_>,
out: &mut Vec<ComparatorSite>,
) {
for branch in &c.branches {
if let Some(cond) = &branch.condition {
collect_expr(cond, ctx, out);
}
collect_block(&branch.body, ctx, out);
}
}
fn collect_content(content: &Content, ctx: &CollectCtx<'_>, out: &mut Vec<ComparatorSite>) {
for part in &content.parts {
collect_content_part(part, ctx, out);
}
}
fn collect_content_part(part: &ContentPart, ctx: &CollectCtx<'_>, out: &mut Vec<ComparatorSite>) {
match part {
ContentPart::Interpolation(e) => collect_expr(e, ctx, out),
ContentPart::InlineConditional(c) => collect_conditional(c, ctx, out),
ContentPart::InlineSequence(s) => {
for branch in &s.branches {
collect_block(&branch.body, ctx, out);
}
}
ContentPart::Span(span) => {
for child in &span.children {
collect_content_part(child, ctx, out);
}
}
ContentPart::Text(_) | ContentPart::Glue | ContentPart::Spring => {}
}
}
fn collect_block_stmt(bs: &BlockStmt, ctx: &CollectCtx<'_>, out: &mut Vec<ComparatorSite>) {
match bs {
BlockStmt::TempDecl(t) => {
if let Some(v) = &t.value {
collect_expr(v, ctx, out);
}
}
BlockStmt::Assignment(a) => {
collect_expr(&a.target, ctx, out);
collect_expr(&a.value, ctx, out);
}
BlockStmt::Return(r) => {
if let Some(v) = &r.value {
collect_expr(v, ctx, out);
}
for a in &r.onwards_args {
collect_expr(a, ctx, out);
}
}
BlockStmt::ExprStmt(e) => collect_expr(e, ctx, out),
BlockStmt::Await(a) => {
if let Some(cond) = &a.condition {
collect_expr(cond, ctx, out);
}
}
BlockStmt::While(w) => {
collect_expr(&w.condition, ctx, out);
for s in &w.body {
collect_block_stmt(s, ctx, out);
}
}
BlockStmt::If(i) => collect_if(i, ctx, out),
BlockStmt::For(f) => {
collect_expr(&f.iterable, ctx, out);
for s in &f.body {
collect_block_stmt(s, ctx, out);
}
}
BlockStmt::Break(_) | BlockStmt::Continue(_) => {}
}
}
fn collect_if(i: &brink_ir::IfStmt, ctx: &CollectCtx<'_>, out: &mut Vec<ComparatorSite>) {
collect_expr(&i.condition, ctx, out);
for s in &i.body {
collect_block_stmt(s, ctx, out);
}
match &i.else_branch {
Some(brink_ir::ElseBranch::Else(stmts)) => {
for s in stmts {
collect_block_stmt(s, ctx, out);
}
}
Some(brink_ir::ElseBranch::ElseIf(nested)) => collect_if(nested, ctx, out),
None => {}
}
}
#[expect(
clippy::too_many_lines,
reason = "one match arm per Expr variant; splitting would obscure the dispatch"
)]
fn collect_expr(expr: &Expr, ctx: &CollectCtx<'_>, out: &mut Vec<ComparatorSite>) {
match expr {
Expr::Call(path, args) => {
let name = path.segments.last().map_or("", |seg| seg.text.as_str());
if path.segments.len() == 1
&& let Some(idx) = callback_arg_index(name)
&& let Some(verb) = verb_name(name)
&& let Some(arg) = args.get(idx)
{
match arg {
Expr::FnLiteral(fnl) => {
out.push(ComparatorSite {
call_range: path.range,
verb,
target_range: fnl.target.range,
target_name: fnl
.target
.segments
.iter()
.map(|s| s.text.as_str())
.collect::<Vec<_>>()
.join("."),
});
}
Expr::Path(p) if ctx.native && (ctx.is_fn_target)(p.range) => {
out.push(ComparatorSite {
call_range: path.range,
verb,
target_range: p.range,
target_name: p
.segments
.iter()
.map(|s| s.text.as_str())
.collect::<Vec<_>>()
.join("::"),
});
}
_ => {}
}
}
for a in args {
collect_expr(a, ctx, out);
}
}
Expr::Prefix(_, inner) | Expr::Postfix(inner, _) => collect_expr(inner, ctx, out),
Expr::Infix(ie) => {
collect_expr(&ie.lhs, ctx, out);
collect_expr(&ie.rhs, ctx, out);
}
Expr::Index(idx) => {
collect_expr(&idx.base, ctx, out);
collect_expr(&idx.index, ctx, out);
}
Expr::FieldAccess(fa) => collect_expr(&fa.base, ctx, out),
Expr::Range(r) => {
collect_expr(&r.start, ctx, out);
collect_expr(&r.end, ctx, out);
}
Expr::ArrayLiteral(a) => {
for e in &a.elements {
collect_expr(e, ctx, out);
}
}
Expr::MapLiteral(m) => {
for (k, v) in &m.entries {
collect_expr(k, ctx, out);
collect_expr(v, ctx, out);
}
}
Expr::StructLiteral(sl) => {
for (_, v) in &sl.fields {
collect_expr(v, ctx, out);
}
}
Expr::FnLiteral(fnl) => {
for a in &fnl.args {
collect_expr(a, ctx, out);
}
}
Expr::RefArg(r) => collect_expr(&r.operand, ctx, out),
Expr::Lambda(l) => match &l.body {
brink_ir::LambdaBody::Expr(e) => collect_expr(e, ctx, out),
brink_ir::LambdaBody::Block { stmts, tail } => {
for s in stmts {
collect_block_stmt(s, ctx, out);
}
if let Some(t) = tail {
collect_expr(t, ctx, out);
}
}
},
Expr::Int(_)
| Expr::Float(_)
| Expr::Bool(_)
| Expr::String(_)
| Expr::Null
| Expr::Path(_)
| Expr::DivertTarget(_)
| Expr::ListLiteral(_) => {}
Expr::Fragment(stmts) => {
for s in stmts {
collect_stmt(s, ctx, out);
}
}
}
}