const CELL: i32 = 4;
pub const MAX_DEPTH: usize = 128;
#[must_use]
pub fn walk(
top_cip: u32,
top_frm: i32,
stp: i32,
read_cell: impl Fn(i32) -> Option<i32>,
) -> Vec<(u32, i32)> {
let mut frames = vec![(top_cip, top_frm)];
let mut frm = top_frm;
for _ in 0..MAX_DEPTH {
if frm <= 0 || frm + CELL >= stp {
break;
}
let (Some(ret), Some(prev)) = (read_cell(frm + CELL), read_cell(frm)) else {
break;
};
if ret <= 0 {
break;
}
frames.push((ret.cast_unsigned(), prev));
if prev <= frm || prev >= stp {
break;
}
frm = prev;
}
frames
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
fn mem(pairs: &[(i32, i32)]) -> impl Fn(i32) -> Option<i32> {
let map: HashMap<i32, i32> = pairs.iter().copied().collect();
move |addr| map.get(&addr).copied()
}
#[test]
fn single_frame_when_return_is_zero() {
let read = mem(&[(1000, 0), (1004, 0)]);
assert_eq!(walk(40, 1000, 2000, read), vec![(40, 1000)]);
}
#[test]
fn walks_two_levels() {
let read = mem(&[
(1000, 1100), (1004, 40), (1100, 0),
(1104, 0), ]);
assert_eq!(walk(8, 1000, 2000, read), vec![(8, 1000), (40, 1100)]);
}
#[test]
fn stops_when_frame_leaves_the_stack() {
let read = mem(&[(1000, 9000), (1004, 40)]);
assert_eq!(walk(8, 1000, 2000, read), vec![(8, 1000), (40, 9000)]);
}
#[test]
fn stops_when_the_chain_does_not_ascend() {
let read = mem(&[(1000, 1000), (1004, 40)]);
assert_eq!(walk(8, 1000, 2000, read), vec![(8, 1000), (40, 1000)]);
}
#[test]
fn stops_when_memory_is_unreadable() {
let read = mem(&[]);
assert_eq!(walk(8, 1000, 2000, read), vec![(8, 1000)]);
}
#[test]
fn depth_is_capped() {
let read = |addr: i32| {
if addr % 8 == 0 {
Some(addr + 8) } else {
Some(40) }
};
assert_eq!(walk(8, 1000, i32::MAX, read).len(), MAX_DEPTH + 1);
}
}