use std::collections::{BTreeMap, BTreeSet};
use brink_format::DefinitionId;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[expect(
clippy::struct_excessive_bools,
reason = "opaque + the NS-A2 emits/tags/faults dimensions are independent \
lattice components of one row, not a state machine"
)]
pub struct EffectRow {
pub reads: BTreeSet<DefinitionId>,
pub writes: BTreeSet<DefinitionId>,
pub calls: BTreeSet<String>,
pub opaque: bool,
pub emits: bool,
pub tags: bool,
pub faults: bool,
pub faults_refined: bool,
pub holes: BTreeSet<u32>,
}
impl EffectRow {
#[must_use]
pub fn pessimal() -> Self {
Self {
opaque: true,
faults_refined: true,
..Self::default()
}
}
#[must_use]
pub fn is_pessimal(&self) -> bool {
self.opaque || !self.holes.is_empty()
}
#[must_use]
pub fn is_empty(&self) -> bool {
!self.is_pessimal()
&& self.reads.is_empty()
&& self.writes.is_empty()
&& self.calls.is_empty()
&& !self.emits
&& !self.tags
&& !self.faults
}
pub fn join(&mut self, other: &EffectRow) {
self.join_atoms(other);
self.holes.extend(other.holes.iter().copied());
}
pub fn join_atoms(&mut self, other: &EffectRow) {
self.reads.extend(other.reads.iter().copied());
self.writes.extend(other.writes.iter().copied());
self.calls.extend(other.calls.iter().cloned());
self.opaque |= other.opaque;
self.emits |= other.emits;
self.tags |= other.tags;
self.faults |= other.faults;
self.faults_refined |= other.faults_refined;
}
#[must_use]
pub fn covers(&self, other: &EffectRow) -> bool {
if self.is_pessimal() {
return true;
}
if other.is_pessimal() {
return false;
}
other.reads.is_subset(&self.reads)
&& other.writes.is_subset(&self.writes)
&& other.calls.is_subset(&self.calls)
&& (self.emits || !other.emits)
&& (self.tags || !other.tags)
&& (self.faults || !other.faults)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[expect(
clippy::struct_excessive_bools,
reason = "mirrors EffectRow's independent dimension flags"
)]
pub struct EffectAtoms {
pub reads: BTreeSet<DefinitionId>,
pub writes: BTreeSet<DefinitionId>,
pub calls: BTreeSet<String>,
pub direct_calls: BTreeSet<DefinitionId>,
pub creates_fn_values: BTreeSet<DefinitionId>,
pub opaque: bool,
pub emits: bool,
pub tags: bool,
pub faults: bool,
pub faults_refined: bool,
pub param_holes: BTreeSet<u32>,
pub call_fn_args: BTreeMap<(DefinitionId, u32), FnArgOrigins>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct FnArgOrigins {
pub targets: BTreeSet<DefinitionId>,
pub untraced: bool,
}
impl FnArgOrigins {
#[must_use]
pub fn is_fillable(&self) -> bool {
!self.untraced && !self.targets.is_empty()
}
}
impl EffectAtoms {
#[must_use]
pub fn base_row(&self) -> EffectRow {
EffectRow {
reads: self.reads.clone(),
writes: self.writes.clone(),
calls: self.calls.clone(),
opaque: self.opaque,
emits: self.emits,
tags: self.tags,
faults: self.faults,
faults_refined: self.faults_refined,
holes: self.param_holes.clone(),
}
}
}
#[must_use]
pub fn solve_scc_effects(
batch: &BTreeSet<DefinitionId>,
atoms: &BTreeMap<DefinitionId, EffectAtoms>,
known_rows: &BTreeMap<DefinitionId, EffectRow>,
) -> BTreeMap<DefinitionId, EffectRow> {
let mut rows: BTreeMap<DefinitionId, EffectRow> = batch
.iter()
.map(|&id| {
let base = atoms
.get(&id)
.map(EffectAtoms::base_row)
.unwrap_or_default();
(id, base)
})
.collect();
let cap = batch.len().saturating_add(1);
for _round in 0..cap {
let mut changed = false;
for &id in batch {
let Some(member_atoms) = atoms.get(&id) else {
continue;
};
let mut next = member_atoms.base_row();
for &callee in &member_atoms.direct_calls {
let Some(row) = rows.get(&callee).or_else(|| known_rows.get(&callee)) else {
next.opaque = true;
continue;
};
next.join_atoms(row);
for &hole in &row.holes {
instantiate_hole(
&mut next,
member_atoms.call_fn_args.get(&(callee, hole)),
&rows,
known_rows,
);
}
}
if rows.get(&id) != Some(&next) {
changed = true;
rows.insert(id, next);
}
}
if !changed {
break;
}
}
rows
}
pub(crate) fn instantiate_hole(
next: &mut EffectRow,
origins: Option<&FnArgOrigins>,
rows: &BTreeMap<DefinitionId, EffectRow>,
known_rows: &BTreeMap<DefinitionId, EffectRow>,
) {
let Some(origins) = origins.filter(|o| o.is_fillable()) else {
next.opaque = true;
return;
};
for target in &origins.targets {
match rows.get(target).or_else(|| known_rows.get(target)) {
Some(row) if row.holes.is_empty() => next.join_atoms(row),
Some(row) => {
next.join_atoms(row);
next.opaque = true;
}
None => next.opaque = true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use brink_format::{DefinitionId, DefinitionTag};
fn cell(n: u64) -> DefinitionId {
DefinitionId::new(DefinitionTag::GlobalVar, n)
}
#[test]
fn join_is_set_union_and_opaque_is_sticky() {
let mut a = EffectRow {
reads: [cell(1)].into_iter().collect(),
calls: ["Play".to_string()].into_iter().collect(),
..Default::default()
};
let b = EffectRow {
reads: [cell(2)].into_iter().collect(),
writes: [cell(3)].into_iter().collect(),
opaque: true,
..Default::default()
};
a.join(&b);
assert_eq!(a.reads, [cell(1), cell(2)].into_iter().collect());
assert_eq!(a.writes, [cell(3)].into_iter().collect());
assert_eq!(a.calls, ["Play".to_string()].into_iter().collect());
assert!(a.opaque, "opaque must be sticky under join");
}
#[test]
fn covers_is_superset_and_opaque_tops_the_lattice() {
let big = EffectRow {
reads: [cell(1), cell(2)].into_iter().collect(),
..Default::default()
};
let small = EffectRow {
reads: [cell(1)].into_iter().collect(),
..Default::default()
};
assert!(big.covers(&small));
assert!(!small.covers(&big));
let pess = EffectRow::pessimal();
assert!(pess.covers(&big), "pessimal covers everything");
assert!(!big.covers(&pess), "no concrete row covers pessimal");
assert!(pess.covers(&pess));
}
#[test]
fn solve_scc_effects_propagates_a_callee_row_to_its_caller() {
let up = cell(1);
let leaf = cell(2);
let batch: BTreeSet<DefinitionId> = [up].into_iter().collect();
let atoms: BTreeMap<DefinitionId, EffectAtoms> = [(
up,
EffectAtoms {
writes: [cell(20)].into_iter().collect(),
direct_calls: [leaf].into_iter().collect(),
..Default::default()
},
)]
.into_iter()
.collect();
let known_rows: BTreeMap<DefinitionId, EffectRow> = [(
leaf,
EffectRow {
reads: [cell(10)].into_iter().collect(),
calls: ["Play".to_string()].into_iter().collect(),
..Default::default()
},
)]
.into_iter()
.collect();
let rows = solve_scc_effects(&batch, &atoms, &known_rows);
let row = &rows[&up];
assert_eq!(row.reads, [cell(10)].into_iter().collect());
assert_eq!(row.writes, [cell(20)].into_iter().collect());
assert_eq!(row.calls, ["Play".to_string()].into_iter().collect());
assert!(!row.opaque);
}
#[test]
fn solve_scc_effects_reaches_a_mutual_recursion_fixpoint() {
let a = cell(1);
let b = cell(2);
let batch: BTreeSet<DefinitionId> = [a, b].into_iter().collect();
let atoms: BTreeMap<DefinitionId, EffectAtoms> = [
(
a,
EffectAtoms {
reads: [cell(10)].into_iter().collect(),
direct_calls: [b].into_iter().collect(),
..Default::default()
},
),
(
b,
EffectAtoms {
writes: [cell(20)].into_iter().collect(),
direct_calls: [a].into_iter().collect(),
..Default::default()
},
),
]
.into_iter()
.collect();
let rows = solve_scc_effects(&batch, &atoms, &BTreeMap::new());
for id in [a, b] {
let row = &rows[&id];
assert_eq!(
row.reads,
[cell(10)].into_iter().collect(),
"both SCC members see a's read"
);
assert_eq!(
row.writes,
[cell(20)].into_iter().collect(),
"both SCC members see b's write"
);
}
}
#[test]
fn join_carries_emits_tags_faults_stickily() {
let mut a = EffectRow::default();
let b = EffectRow {
emits: true,
..Default::default()
};
let c = EffectRow {
tags: true,
faults: true,
..Default::default()
};
a.join(&b);
a.join(&c);
assert!(a.emits && a.tags && a.faults);
a.join(&EffectRow::default());
assert!(a.emits && a.tags && a.faults);
}
#[test]
fn covers_is_per_dimension_for_emits_tags_faults() {
let silent = EffectRow::default();
let emitting = EffectRow {
emits: true,
..Default::default()
};
let tagging = EffectRow {
tags: true,
..Default::default()
};
let faulting = EffectRow {
faults: true,
..Default::default()
};
assert!(!silent.covers(&emitting));
assert!(!silent.covers(&tagging));
assert!(!silent.covers(&faulting));
assert!(
emitting.covers(&silent),
"asserting less than reality is legal"
);
assert!(!emitting.covers(&tagging));
assert!(!emitting.covers(&faulting));
assert!(!tagging.covers(&emitting));
let pess = EffectRow::pessimal();
assert!(pess.covers(&emitting));
assert!(pess.covers(&tagging));
assert!(pess.covers(&faulting));
assert!(!faulting.covers(&pess));
}
#[test]
fn solve_scc_effects_propagates_emitter_tagger_faulter_status_transitively() {
let up = cell(1);
let leaf = cell(2);
let batch: BTreeSet<DefinitionId> = [up].into_iter().collect();
let atoms: BTreeMap<DefinitionId, EffectAtoms> = [(
up,
EffectAtoms {
direct_calls: [leaf].into_iter().collect(),
..Default::default()
},
)]
.into_iter()
.collect();
let known_rows: BTreeMap<DefinitionId, EffectRow> = [(
leaf,
EffectRow {
emits: true,
tags: true,
faults: true,
..Default::default()
},
)]
.into_iter()
.collect();
let rows = solve_scc_effects(&batch, &atoms, &known_rows);
let row = &rows[&up];
assert!(row.emits, "glue-only caller of an emitter still emits");
assert!(row.tags);
assert!(row.faults);
assert!(!row.opaque);
}
#[test]
fn an_opaque_atom_makes_the_whole_row_pessimal() {
let a = cell(1);
let batch: BTreeSet<DefinitionId> = [a].into_iter().collect();
let atoms: BTreeMap<DefinitionId, EffectAtoms> = [(
a,
EffectAtoms {
reads: [cell(10)].into_iter().collect(),
opaque: true,
..Default::default()
},
)]
.into_iter()
.collect();
let rows = solve_scc_effects(&batch, &atoms, &BTreeMap::new());
assert!(rows[&a].opaque);
}
#[test]
fn a_hole_tops_the_lattice_exactly_like_opaque() {
let holed = EffectRow {
holes: [0].into_iter().collect(),
..Default::default()
};
let concrete = EffectRow {
reads: [cell(1)].into_iter().collect(),
..Default::default()
};
assert!(holed.is_pessimal());
assert!(!holed.opaque, "the hole is not the intrinsic opaque bit");
assert!(!holed.is_empty(), "a parametric row is never 'empty'");
assert!(holed.covers(&concrete));
assert!(!concrete.covers(&holed));
}
#[test]
fn join_atoms_leaves_the_callees_holes_behind() {
let mut up = EffectRow::default();
let down = EffectRow {
reads: [cell(1)].into_iter().collect(),
holes: [2].into_iter().collect(),
..Default::default()
};
up.join_atoms(&down);
assert_eq!(up.reads, [cell(1)].into_iter().collect());
assert!(up.holes.is_empty());
let mut same_def = EffectRow::default();
same_def.join(&down);
assert_eq!(same_def.holes, [2].into_iter().collect::<BTreeSet<u32>>());
}
#[test]
fn a_traced_argument_instantiates_the_callees_row_variable() {
let caller = cell(1);
let higher_order = cell(2);
let target = cell(3);
let batch: BTreeSet<DefinitionId> = [caller].into_iter().collect();
let atoms: BTreeMap<DefinitionId, EffectAtoms> = [(
caller,
EffectAtoms {
direct_calls: [higher_order, target].into_iter().collect(),
call_fn_args: [(
(higher_order, 0),
FnArgOrigins {
targets: [target].into_iter().collect(),
untraced: false,
},
)]
.into_iter()
.collect(),
..Default::default()
},
)]
.into_iter()
.collect();
let known_rows: BTreeMap<DefinitionId, EffectRow> = [
(
higher_order,
EffectRow {
holes: [0].into_iter().collect(),
..Default::default()
},
),
(
target,
EffectRow {
writes: [cell(20)].into_iter().collect(),
..Default::default()
},
),
]
.into_iter()
.collect();
let rows = solve_scc_effects(&batch, &atoms, &known_rows);
let row = &rows[&caller];
assert!(!row.is_pessimal(), "a filled hole is not a floor");
assert!(row.holes.is_empty(), "the callee's hole is not inherited");
assert!(
row.writes.contains(&cell(20)),
"the instantiated row carries the argument target's own writes"
);
}
#[test]
fn an_untraced_or_missing_argument_leaves_the_hole_pessimal() {
let caller = cell(1);
let higher_order = cell(2);
let target = cell(3);
let holed: BTreeMap<DefinitionId, EffectRow> = [(
higher_order,
EffectRow {
holes: [0].into_iter().collect(),
..Default::default()
},
)]
.into_iter()
.collect();
let batch: BTreeSet<DefinitionId> = [caller].into_iter().collect();
for (label, origins) in [
("no entry at all", None),
(
"an untraced write in the position",
Some(FnArgOrigins {
targets: [target].into_iter().collect(),
untraced: true,
}),
),
("an entry with no targets", Some(FnArgOrigins::default())),
] {
let atoms: BTreeMap<DefinitionId, EffectAtoms> = [(
caller,
EffectAtoms {
direct_calls: [higher_order].into_iter().collect(),
call_fn_args: origins
.into_iter()
.map(|o| ((higher_order, 0), o))
.collect(),
..Default::default()
},
)]
.into_iter()
.collect();
let rows = solve_scc_effects(&batch, &atoms, &holed);
assert!(rows[&caller].opaque, "{label} must keep the pessimal floor");
}
}
#[test]
fn a_still_parametric_fill_target_keeps_the_floor() {
let caller = cell(1);
let higher_order = cell(2);
let target = cell(3);
let batch: BTreeSet<DefinitionId> = [caller].into_iter().collect();
let atoms: BTreeMap<DefinitionId, EffectAtoms> = [(
caller,
EffectAtoms {
direct_calls: [higher_order].into_iter().collect(),
call_fn_args: [(
(higher_order, 0),
FnArgOrigins {
targets: [target].into_iter().collect(),
untraced: false,
},
)]
.into_iter()
.collect(),
..Default::default()
},
)]
.into_iter()
.collect();
let known_rows: BTreeMap<DefinitionId, EffectRow> = [
(
higher_order,
EffectRow {
holes: [0].into_iter().collect(),
..Default::default()
},
),
(
target,
EffectRow {
writes: [cell(20)].into_iter().collect(),
holes: [0].into_iter().collect(),
..Default::default()
},
),
]
.into_iter()
.collect();
let rows = solve_scc_effects(&batch, &atoms, &known_rows);
assert!(rows[&caller].opaque);
assert!(
rows[&caller].writes.contains(&cell(20)),
"degrading to the floor still absorbs everything the target listed"
);
}
#[test]
fn a_fill_target_with_no_row_forces_pessimal() {
let caller = cell(1);
let higher_order = cell(2);
let ghost = cell(99);
let batch: BTreeSet<DefinitionId> = [caller].into_iter().collect();
let atoms: BTreeMap<DefinitionId, EffectAtoms> = [(
caller,
EffectAtoms {
direct_calls: [higher_order].into_iter().collect(),
call_fn_args: [(
(higher_order, 0),
FnArgOrigins {
targets: [ghost].into_iter().collect(),
untraced: false,
},
)]
.into_iter()
.collect(),
..Default::default()
},
)]
.into_iter()
.collect();
let known_rows: BTreeMap<DefinitionId, EffectRow> = [(
higher_order,
EffectRow {
holes: [0].into_iter().collect(),
..Default::default()
},
)]
.into_iter()
.collect();
let rows = solve_scc_effects(&batch, &atoms, &known_rows);
assert!(rows[&caller].opaque);
}
#[test]
fn an_unknown_callee_forces_pessimal() {
let a = cell(1);
let ghost = cell(99);
let batch: BTreeSet<DefinitionId> = [a].into_iter().collect();
let atoms: BTreeMap<DefinitionId, EffectAtoms> = [(
a,
EffectAtoms {
direct_calls: [ghost].into_iter().collect(),
..Default::default()
},
)]
.into_iter()
.collect();
let rows = solve_scc_effects(&batch, &atoms, &BTreeMap::new());
assert!(rows[&a].opaque);
}
}