use crate::ir::{IrNode, Shape};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Arm {
conditional: u32,
index: u32,
believed: bool,
reachable: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ArmPath {
arms: Vec<Arm>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StaticCondition {
False,
True,
Unknown,
}
#[derive(Debug, Default)]
pub struct ArmTracker {
path: ArmPath,
definitely_taken: Vec<bool>,
next: u32,
}
impl ArmTracker {
pub fn begin(&mut self, condition: StaticCondition) {
let reachable = condition != StaticCondition::False;
self.path.arms.push(Arm {
conditional: self.next,
index: 0,
believed: true,
reachable,
});
self.next = self.next.wrapping_add(1);
self.definitely_taken
.push(condition == StaticCondition::True);
}
pub fn next_arm(&mut self, condition: StaticCondition) {
let (Some(arm), Some(taken)) =
(self.path.arms.last_mut(), self.definitely_taken.last_mut())
else {
return;
};
arm.index = arm.index.saturating_add(1);
arm.reachable = !*taken && condition != StaticCondition::False;
if condition == StaticCondition::True {
*taken = true;
}
}
pub fn end(&mut self) {
let _ = self.path.arms.pop();
let _ = self.definitely_taken.pop();
}
#[must_use]
pub fn current(&self) -> ArmPath {
self.path.clone()
}
}
impl ArmPath {
#[must_use]
pub fn descend(&self, node: &IrNode, next: &mut u32) -> Option<Self> {
let Shape::Native(kind) = &node.shape else {
return None;
};
let mut arms = self.arms.clone();
match &**kind {
"preproc_if" | "preproc_ifdef" => {
arms.push(Arm {
conditional: *next,
index: 0,
believed: !stumbled_inside(node),
reachable: true,
});
*next = next.wrapping_add(1);
}
"preproc_elif" | "preproc_elifdef" | "preproc_elifndef" | "preproc_else" => {
let arm = arms.last_mut()?;
if !arm.believed {
return None;
}
arm.index += 1;
}
_ => return None,
}
Some(Self { arms })
}
#[must_use]
pub fn excludes(&self, other: &Self) -> bool {
self.arms
.iter()
.zip(&other.arms)
.find(|(a, b)| a != b)
.is_some_and(|(a, b)| a.believed && b.believed && a.conditional == b.conditional)
}
#[must_use]
pub fn is_unreachable(&self) -> bool {
self.arms.iter().any(|arm| !arm.reachable)
}
}
fn stumbled_inside(node: &IrNode) -> bool {
let mut stumbled = false;
node.walk(&mut |inner| stumbled |= matches!(inner.shape, Shape::Error));
stumbled
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::frontend::Lexeme;
use crate::ir::ByteRange;
fn node(shape: Shape, children: Vec<IrNode>) -> IrNode {
IrNode {
shape,
name: None,
token_start: 0,
token_end: 0,
range: ByteRange { start: 0, end: 0 },
children,
}
}
fn native(kind: &str) -> IrNode {
node(Shape::Native(Lexeme::from(kind)), Vec::new())
}
fn broken(kind: &str) -> IrNode {
node(
Shape::Native(Lexeme::from(kind)),
vec![node(Shape::Error, Vec::new())],
)
}
fn path(kinds: &[&str]) -> ArmPath {
let mut next = 0;
let mut here = ArmPath::default();
for kind in kinds {
if let Some(descended) = here.descend(&native(kind), &mut next) {
here = descended;
}
}
here
}
#[test]
fn a_shape_that_is_not_a_conditional_changes_nothing() {
let mut next = 0;
assert_eq!(
ArmPath::default().descend(&node(Shape::Function, Vec::new()), &mut next),
None
);
assert_eq!(
ArmPath::default().descend(&native("goto_statement"), &mut next),
None
);
assert_eq!(next, 0, "no identifier is spent on an ordinary shape");
}
#[test]
fn the_two_arms_of_one_conditional_exclude_each_other() {
let mut next = 0;
let root = ArmPath::default();
let taken = root.descend(&native("preproc_ifdef"), &mut next).unwrap();
let otherwise = taken.descend(&native("preproc_else"), &mut next).unwrap();
assert!(taken.excludes(&otherwise));
assert!(otherwise.excludes(&taken));
}
#[test]
fn every_arm_of_a_chain_excludes_every_other() {
let mut next = 0;
let root = ArmPath::default();
let first = root.descend(&native("preproc_if"), &mut next).unwrap();
let second = first.descend(&native("preproc_elif"), &mut next).unwrap();
let third = second.descend(&native("preproc_else"), &mut next).unwrap();
for (a, b) in [(&first, &second), (&first, &third), (&second, &third)] {
assert!(a.excludes(b));
assert!(b.excludes(a));
}
}
#[test]
fn a_unit_outside_every_conditional_excludes_nothing() {
let outside = ArmPath::default();
let guarded = path(&["preproc_ifdef"]);
assert!(!outside.excludes(&guarded));
assert!(!guarded.excludes(&outside));
assert!(!outside.excludes(&ArmPath::default()));
}
#[test]
fn two_separate_conditionals_do_not_exclude_each_other() {
let mut next = 0;
let root = ArmPath::default();
let here = root.descend(&native("preproc_ifdef"), &mut next).unwrap();
let there = root.descend(&native("preproc_ifdef"), &mut next).unwrap();
assert!(!here.excludes(&there));
assert!(!there.excludes(&here));
}
#[test]
fn exclusion_survives_further_nesting() {
let mut next = 0;
let root = ArmPath::default();
let taken = root.descend(&native("preproc_if"), &mut next).unwrap();
let deep = taken.descend(&native("preproc_ifdef"), &mut next).unwrap();
let otherwise = taken.descend(&native("preproc_else"), &mut next).unwrap();
assert!(deep.excludes(&otherwise));
assert!(otherwise.excludes(&deep));
assert!(!deep.excludes(&taken));
}
#[test]
fn a_branch_keyword_with_no_conditional_open_is_survivable() {
let mut next = 0;
assert_eq!(
ArmPath::default().descend(&native("preproc_else"), &mut next),
None
);
}
#[test]
fn a_conditional_the_parser_stumbled_inside_relates_nothing() {
let mut next = 0;
let root = ArmPath::default();
let taken = root.descend(&broken("preproc_if"), &mut next).unwrap();
let otherwise = taken.descend(&native("preproc_else"), &mut next);
assert_eq!(otherwise, None);
assert!(!taken.excludes(&root));
assert!(!root.excludes(&taken));
}
#[test]
fn a_sound_conditional_inside_a_broken_one_still_relates_its_own_arms() {
let mut next = 0;
let outer = ArmPath::default()
.descend(&broken("preproc_if"), &mut next)
.unwrap();
let inner = outer.descend(&native("preproc_ifdef"), &mut next).unwrap();
let otherwise = inner.descend(&native("preproc_else"), &mut next).unwrap();
assert!(inner.excludes(&otherwise));
assert!(!inner.excludes(&outer));
}
#[test]
fn an_else_under_a_broken_conditional_leaves_the_sound_one_above_alone() {
let mut next = 0;
let outer = ArmPath::default()
.descend(&native("preproc_if"), &mut next)
.unwrap();
let broken_inner = outer.descend(&broken("preproc_if"), &mut next).unwrap();
assert_eq!(
broken_inner.descend(&native("preproc_else"), &mut next),
None
);
assert!(!broken_inner.excludes(&outer));
}
#[test]
fn an_error_beside_a_conditional_does_not_touch_it() {
let mut next = 0;
let file = node(
Shape::Impl,
vec![node(Shape::Error, Vec::new()), native("preproc_if")],
);
let opener = file.children.last().unwrap();
let taken = ArmPath::default().descend(opener, &mut next).unwrap();
let otherwise = taken.descend(&native("preproc_else"), &mut next).unwrap();
assert!(taken.excludes(&otherwise));
}
}