use alloc::vec::Vec;
use miden_air::logup::{BlockHashMsg, BlockStackMsg, OpGroupMsg};
use miden_core::{
Felt, ONE, ZERO,
mast::{
BasicBlockNodeBuilder, CallNodeBuilder, JoinNodeBuilder, LoopNodeBuilder, MastForest,
MastForestContributor, MastNodeExt, SplitNodeBuilder,
},
operations::{Operation, opcodes},
program::Program,
};
use super::{
ExecutionTrace, build_trace_from_ops, build_trace_from_program,
build_trace_from_program_with_stack,
lookup_harness::{Expectations, InteractionLog},
};
use crate::{RowIndex, StackInputs, trace::MainTrace};
fn next_op_first_child_flag(main: &MainTrace, next: RowIndex) -> Felt {
let op_next = main.get_op_code(next);
let is = |code: u8| if op_next == Felt::from_u8(code) { ONE } else { ZERO };
ONE - is(opcodes::END) - is(opcodes::REPEAT) - is(opcodes::RESPAN) - is(opcodes::HALT)
}
fn for_each_op<F>(trace: &ExecutionTrace, mut f: F)
where
F: FnMut(usize, Felt),
{
let main = trace.main_trace();
let num_rows = main.num_rows();
for row in 0..num_rows - 1 {
let idx = RowIndex::from(row);
f(row, main.get_op_code(idx));
}
}
#[test]
fn block_stack_span_push_pop() {
let ops = vec![Operation::Add, Operation::Mul];
let trace = build_trace_from_ops(ops, &[]);
let log = InteractionLog::new(&trace);
let main = trace.main_trace();
let mut exp = Expectations::new(&log);
for_each_op(&trace, |row, op| {
let idx = RowIndex::from(row);
let addr = main.addr(idx);
let addr_next = main.addr(RowIndex::from(row + 1));
if op == Felt::from_u8(opcodes::SPAN) {
exp.add(
row,
&BlockStackMsg::Simple {
block_id: addr_next,
parent_id: addr,
is_loop: ZERO,
},
);
} else if op == Felt::from_u8(opcodes::END) {
exp.remove(
row,
&BlockStackMsg::Simple {
block_id: addr,
parent_id: addr_next,
is_loop: ZERO,
},
);
}
});
assert_eq!(exp.count_adds(), 1, "expected exactly one SPAN push");
assert_eq!(exp.count_removes(), 1, "expected exactly one matching END pop");
log.assert_contains(&exp);
}
#[test]
fn block_stack_call_full_push_pop() {
let program = {
let mut forest = MastForest::new();
let callee = BasicBlockNodeBuilder::new(vec![Operation::Noop], Vec::new())
.add_to_forest(&mut forest)
.unwrap();
let call_id = CallNodeBuilder::new(callee).add_to_forest(&mut forest).unwrap();
forest.make_root(call_id);
Program::new(forest.into(), call_id)
};
let trace = build_trace_from_program(&program, &[]);
let log = InteractionLog::new(&trace);
let main = trace.main_trace();
let mut exp = Expectations::new(&log);
for_each_op(&trace, |row, op| {
let idx = RowIndex::from(row);
let next = RowIndex::from(row + 1);
if op == Felt::from_u8(opcodes::CALL) {
exp.add(
row,
&BlockStackMsg::Full {
block_id: main.addr(next),
parent_id: main.addr(idx),
is_loop: ZERO,
ctx: main.ctx(idx),
fmp: main.stack_depth(idx),
depth: main.parent_overflow_address(idx),
fn_hash: main.fn_hash(idx),
},
);
}
if op == Felt::from_u8(opcodes::END) && main.is_call_flag(idx) == ONE {
exp.remove(
row,
&BlockStackMsg::Full {
block_id: main.addr(idx),
parent_id: main.addr(next),
is_loop: ZERO,
ctx: main.ctx(next),
fmp: main.stack_depth(next),
depth: main.parent_overflow_address(next),
fn_hash: main.fn_hash(next),
},
);
}
});
assert_eq!(exp.count_adds(), 1, "expected exactly one CALL push");
assert_eq!(exp.count_removes(), 1, "expected exactly one matching END pop");
log.assert_contains(&exp);
}
#[rstest::rstest]
#[case::taken(1)]
#[case::not_taken(0)]
fn block_stack_split_push_pop(#[case] cond: u64) {
let program = {
let mut f = MastForest::new();
let t = BasicBlockNodeBuilder::new(vec![Operation::Add], Vec::new())
.add_to_forest(&mut f)
.unwrap();
let e = BasicBlockNodeBuilder::new(vec![Operation::Mul], Vec::new())
.add_to_forest(&mut f)
.unwrap();
let s = SplitNodeBuilder::new([t, e]).add_to_forest(&mut f).unwrap();
f.make_root(s);
Program::new(f.into(), s)
};
let trace = build_trace_from_program(&program, &[cond]);
let log = InteractionLog::new(&trace);
let main = trace.main_trace();
let mut split_adds = 0usize;
let mut exp = Expectations::new(&log);
for_each_op(&trace, |row, op| {
let idx = RowIndex::from(row);
let addr = main.addr(idx);
let addr_next = main.addr(RowIndex::from(row + 1));
if op == Felt::from_u8(opcodes::SPLIT) {
exp.add(
row,
&BlockStackMsg::Simple {
block_id: addr_next,
parent_id: addr,
is_loop: ZERO,
},
);
split_adds += 1;
} else if op == Felt::from_u8(opcodes::END) && main.is_call_flag(idx) == ZERO {
let is_loop = main.is_loop_flag(idx);
exp.remove(
row,
&BlockStackMsg::Simple {
block_id: addr,
parent_id: addr_next,
is_loop,
},
);
}
});
assert_eq!(split_adds, 1, "expected exactly one SPLIT push");
assert_eq!(exp.count_removes(), 2, "expected two Simple pops (child END + SPLIT END)");
log.assert_contains(&exp);
}
#[rstest::rstest]
#[case::enters(1, ONE)]
#[case::skips(0, ZERO)]
fn block_stack_loop_is_loop_flag(#[case] cond: u64, #[case] expected_is_loop: Felt) {
let program = {
let mut f = MastForest::new();
let body = BasicBlockNodeBuilder::new(vec![Operation::Pad, Operation::Drop], Vec::new())
.add_to_forest(&mut f)
.unwrap();
let loop_id = LoopNodeBuilder::new(body).add_to_forest(&mut f).unwrap();
f.make_root(loop_id);
Program::new(f.into(), loop_id)
};
let trace = build_trace_from_program(&program, &[cond, 0]);
let log = InteractionLog::new(&trace);
let main = trace.main_trace();
let mut loop_pushes = 0usize;
let mut loop_pops = 0usize;
let mut exp = Expectations::new(&log);
for_each_op(&trace, |row, op| {
let idx = RowIndex::from(row);
let addr = main.addr(idx);
let addr_next = main.addr(RowIndex::from(row + 1));
if op == Felt::from_u8(opcodes::LOOP) {
let is_loop = main.stack_element(0, idx);
assert_eq!(is_loop, expected_is_loop, "s0 sanity at LOOP row");
exp.add(
row,
&BlockStackMsg::Simple {
block_id: addr_next,
parent_id: addr,
is_loop,
},
);
loop_pushes += 1;
} else if op == Felt::from_u8(opcodes::END)
&& main.is_call_flag(idx) == ZERO
&& main.is_loop_flag(idx) == expected_is_loop
&& main.is_loop_body_flag(idx) == ZERO
{
exp.remove(
row,
&BlockStackMsg::Simple {
block_id: addr,
parent_id: addr_next,
is_loop: expected_is_loop,
},
);
loop_pops += 1;
}
});
assert_eq!(loop_pushes, 1, "expected one LOOP push");
assert_eq!(
loop_pops, 1,
"expected one matching LOOP END pop (is_loop={expected_is_loop:?})"
);
log.assert_contains(&exp);
}
#[test]
fn block_stack_respan_add_and_remove() {
let ops: Vec<Operation> = (0..80).map(|_| Operation::Noop).collect();
let trace = build_trace_from_ops(ops, &[]);
let log = InteractionLog::new(&trace);
let main = trace.main_trace();
let mut respan_rows = 0usize;
let mut exp = Expectations::new(&log);
for_each_op(&trace, |row, op| {
if op != Felt::from_u8(opcodes::RESPAN) {
return;
}
let idx = RowIndex::from(row);
let next = RowIndex::from(row + 1);
let addr = main.addr(idx);
let addr_next = main.addr(next);
let parent = main.decoder_hasher_state_element(1, next);
exp.add(
row,
&BlockStackMsg::Simple {
block_id: addr_next,
parent_id: parent,
is_loop: ZERO,
},
);
exp.remove(
row,
&BlockStackMsg::Simple {
block_id: addr,
parent_id: parent,
is_loop: ZERO,
},
);
respan_rows += 1;
});
assert!(respan_rows >= 1, "program did not produce a RESPAN row");
assert_eq!(exp.count_adds(), respan_rows);
assert_eq!(exp.count_removes(), respan_rows);
log.assert_contains(&exp);
}
#[test]
fn block_hash_join_enqueue_dequeue() {
let program = {
let mut mast_forest = MastForest::new();
let bb1 = BasicBlockNodeBuilder::new(vec![Operation::Mul], Vec::new())
.add_to_forest(&mut mast_forest)
.unwrap();
let bb2 = BasicBlockNodeBuilder::new(vec![Operation::Add], Vec::new())
.add_to_forest(&mut mast_forest)
.unwrap();
let join_id = JoinNodeBuilder::new([bb1, bb2]).add_to_forest(&mut mast_forest).unwrap();
mast_forest.make_root(join_id);
Program::new(mast_forest.into(), join_id)
};
let trace = build_trace_from_program(&program, &[]);
let log = InteractionLog::new(&trace);
let main = trace.main_trace();
let mut exp = Expectations::new(&log);
for_each_op(&trace, |row, op| {
let idx = RowIndex::from(row);
let next = RowIndex::from(row + 1);
let addr_next = main.addr(next);
let first = main.decoder_hasher_state_first_half(idx);
let h0: [Felt; 4] = [first[0], first[1], first[2], first[3]];
let second = main.decoder_hasher_state_second_half(idx);
let h1: [Felt; 4] = [second[0], second[1], second[2], second[3]];
if op == Felt::from_u8(opcodes::JOIN) {
exp.add(row, &BlockHashMsg::FirstChild { parent: addr_next, child_hash: h0 });
exp.add(row, &BlockHashMsg::Child { parent: addr_next, child_hash: h1 });
}
if op == Felt::from_u8(opcodes::END) {
let is_first_child = next_op_first_child_flag(main, next);
let is_loop_body = main.is_loop_body_flag(idx);
exp.remove(
row,
&BlockHashMsg::End {
parent: addr_next,
child_hash: h0,
is_first_child,
is_loop_body,
},
);
}
});
assert_eq!(exp.count_adds(), 2, "expected JOIN to enqueue FirstChild + Child");
assert_eq!(exp.count_removes(), 3, "expected an END dequeue for bb1, bb2, and JOIN");
log.assert_contains(&exp);
}
#[test]
fn block_hash_loop_body_with_repeat() {
let program = {
let mut mast_forest = MastForest::new();
let bb1 = BasicBlockNodeBuilder::new(vec![Operation::Pad], Vec::new())
.add_to_forest(&mut mast_forest)
.unwrap();
let bb2 = BasicBlockNodeBuilder::new(vec![Operation::Drop], Vec::new())
.add_to_forest(&mut mast_forest)
.unwrap();
let join_id = JoinNodeBuilder::new([bb1, bb2]).add_to_forest(&mut mast_forest).unwrap();
let loop_id = LoopNodeBuilder::new(join_id).add_to_forest(&mut mast_forest).unwrap();
mast_forest.make_root(loop_id);
Program::new(mast_forest.into(), loop_id)
};
let trace = build_trace_from_program(&program, &[1, 1, 0]);
let log = InteractionLog::new(&trace);
let main = trace.main_trace();
let mut fired_loop_body = 0usize;
let mut fired_loop_body_end = 0usize;
let mut exp = Expectations::new(&log);
for_each_op(&trace, |row, op| {
let idx = RowIndex::from(row);
let next = RowIndex::from(row + 1);
let first = main.decoder_hasher_state_first_half(idx);
let h0: [Felt; 4] = [first[0], first[1], first[2], first[3]];
let addr_next = main.addr(next);
let is_loop_entering =
op == Felt::from_u8(opcodes::LOOP) && main.stack_element(0, idx) == ONE;
let is_repeat = op == Felt::from_u8(opcodes::REPEAT);
if is_loop_entering || is_repeat {
exp.add(row, &BlockHashMsg::LoopBody { parent: addr_next, child_hash: h0 });
fired_loop_body += 1;
}
if op == Felt::from_u8(opcodes::END) && main.is_loop_body_flag(idx) == ONE {
let is_first_child = next_op_first_child_flag(main, next);
exp.remove(
row,
&BlockHashMsg::End {
parent: addr_next,
child_hash: h0,
is_first_child,
is_loop_body: ONE,
},
);
fired_loop_body_end += 1;
}
});
assert_eq!(fired_loop_body, 2, "expected LOOP + REPEAT to each fire a LoopBody enqueue");
assert_eq!(fired_loop_body_end, 2, "expected one END-of-loop-body remove per iteration");
log.assert_contains(&exp);
}
#[rstest::rstest]
#[case::taken(1)]
#[case::not_taken(0)]
fn block_hash_split_enqueue_dequeue(#[case] cond: u64) {
let program = {
let mut f = MastForest::new();
let t = BasicBlockNodeBuilder::new(vec![Operation::Add], Vec::new())
.add_to_forest(&mut f)
.unwrap();
let e = BasicBlockNodeBuilder::new(vec![Operation::Mul], Vec::new())
.add_to_forest(&mut f)
.unwrap();
let s = SplitNodeBuilder::new([t, e]).add_to_forest(&mut f).unwrap();
f.make_root(s);
Program::new(f.into(), s)
};
let trace = build_trace_from_program(&program, &[cond]);
let log = InteractionLog::new(&trace);
let main = trace.main_trace();
let mut split_rows = 0usize;
let mut exp = Expectations::new(&log);
for_each_op(&trace, |row, op| {
let idx = RowIndex::from(row);
let next = RowIndex::from(row + 1);
let addr_next = main.addr(next);
let first = main.decoder_hasher_state_first_half(idx);
let second = main.decoder_hasher_state_second_half(idx);
if op == Felt::from_u8(opcodes::SPLIT) {
let s0 = main.stack_element(0, idx);
let one_minus_s0 = ONE - s0;
let child_hash: [Felt; 4] =
std::array::from_fn(|i| s0 * first[i] + one_minus_s0 * second[i]);
exp.add(row, &BlockHashMsg::Child { parent: addr_next, child_hash });
split_rows += 1;
}
if op == Felt::from_u8(opcodes::END) {
let is_loop_body = main.is_loop_body_flag(idx);
let h0: [Felt; 4] = [first[0], first[1], first[2], first[3]];
let is_first_child = next_op_first_child_flag(main, next);
exp.remove(
row,
&BlockHashMsg::End {
parent: addr_next,
child_hash: h0,
is_first_child,
is_loop_body,
},
);
}
});
assert_eq!(split_rows, 1, "expected exactly one SPLIT enqueue");
assert_eq!(exp.count_removes(), 2, "expected END pops for child + SPLIT: cond={cond}");
log.assert_contains(&exp);
}
#[test]
fn op_group_span_8_groups_inserts() {
let pattern = [Operation::Noop, Operation::Incr, Operation::Neg, Operation::Eqz];
let ops: Vec<Operation> = (0..64).map(|i| pattern[i % pattern.len()]).collect();
let trace = build_trace_from_ops(ops, &[]);
let log = InteractionLog::new(&trace);
let main = trace.main_trace();
let mut g8_rows_seen = 0usize;
let mut exp = Expectations::new(&log);
for_each_op(&trace, |row, op| {
let idx = RowIndex::from(row);
if op != Felt::from_u8(opcodes::SPAN) && op != Felt::from_u8(opcodes::RESPAN) {
return;
}
let batch_flags = main.op_batch_flag(idx);
if batch_flags[0] != ONE {
return;
}
g8_rows_seen += 1;
let addr_next = main.addr(RowIndex::from(row + 1));
let gc = main.group_count(idx);
let first = main.decoder_hasher_state_first_half(idx);
let second = main.decoder_hasher_state_second_half(idx);
for i in 1u16..=3 {
let group_value = first[i as usize];
exp.add(row, &OpGroupMsg::new(&addr_next, gc, i, group_value));
}
for i in 4u16..=7 {
let group_value = second[(i - 4) as usize];
exp.add(row, &OpGroupMsg::new(&addr_next, gc, i, group_value));
}
});
assert!(g8_rows_seen > 0, "program did not produce a g8 SPAN/RESPAN batch");
assert_eq!(
exp.count_adds(),
7 * g8_rows_seen,
"expected 7 g8 inserts per SPAN/RESPAN row (positions 1..=7)"
);
assert_eq!(exp.count_removes(), 0, "op_group_span_8_groups_inserts only checks inserts");
log.assert_contains(&exp);
}
#[test]
fn op_group_span_removal_covers_decode_rows() {
let mut ops: Vec<Operation> = (0..9).map(|_| Operation::Noop).collect();
ops.push(Operation::Push(Felt::new_unchecked(42)));
ops.extend(vec![Operation::Add, Operation::Mul, Operation::Drop]);
let trace = build_trace_from_ops(ops, &[]);
let log = InteractionLog::new(&trace);
let main = trace.main_trace();
let mut fired_push_branch = false;
let mut fired_nonpush_branch = false;
let mut exp = Expectations::new(&log);
for_each_op(&trace, |row, op| {
let idx = RowIndex::from(row);
let next = RowIndex::from(row + 1);
if main.is_in_span(idx) != ONE {
return;
}
let gc = main.group_count(idx);
let gc_next = main.group_count(next);
if gc == gc_next {
return;
}
let addr = main.addr(idx);
let group_value = if op == Felt::from_u8(opcodes::PUSH) {
fired_push_branch = true;
main.stack_element(0, next)
} else {
fired_nonpush_branch = true;
let h0_next = main.decoder_hasher_state_element(0, next);
let opcode_next = main.get_op_code(next);
h0_next * Felt::from_u16(128) + opcode_next
};
exp.remove(
row,
&OpGroupMsg {
batch_id: addr,
group_pos: gc,
group_value,
},
);
});
assert!(
fired_push_branch,
"test did not cover the PUSH-mux branch of the op-group remove"
);
assert!(
fired_nonpush_branch,
"test did not cover the non-PUSH branch of the op-group remove"
);
log.assert_contains(&exp);
}
#[rstest::rstest]
#[case::g8_plus_g1(80, 1, 0, 0, 1)]
#[case::g8_plus_g4(100, 1, 1, 0, 0)]
fn op_group_span_two_batch_transition_inserts(
#[case] noop_count: usize,
#[case] expected_g8_rows: usize,
#[case] expected_g4_rows: usize,
#[case] expected_g2_rows: usize,
#[case] expected_g1_rows: usize,
) {
let ops: Vec<Operation> = (0..noop_count).map(|_| Operation::Noop).collect();
let trace = build_trace_from_ops(ops, &[]);
let log = InteractionLog::new(&trace);
let main = trace.main_trace();
let mut g8_rows = 0usize;
let mut g4_rows = 0usize;
let mut g2_rows = 0usize;
let mut g1_rows = 0usize;
let mut respan_observed = false;
let mut exp = Expectations::new(&log);
for_each_op(&trace, |row, op| {
let idx = RowIndex::from(row);
if op != Felt::from_u8(opcodes::SPAN) && op != Felt::from_u8(opcodes::RESPAN) {
return;
}
if op == Felt::from_u8(opcodes::RESPAN) {
respan_observed = true;
}
let batch_flags = main.op_batch_flag(idx);
let (c0, c1, c2) = (batch_flags[0], batch_flags[1], batch_flags[2]);
let addr_next = main.addr(RowIndex::from(row + 1));
let gc = main.group_count(idx);
let first = main.decoder_hasher_state_first_half(idx);
let second = main.decoder_hasher_state_second_half(idx);
if c0 == ONE && c1 == ZERO && c2 == ZERO {
g8_rows += 1;
for i in 1u16..=3 {
exp.add(row, &OpGroupMsg::new(&addr_next, gc, i, first[i as usize]));
}
for i in 4u16..=7 {
exp.add(row, &OpGroupMsg::new(&addr_next, gc, i, second[(i - 4) as usize]));
}
} else if c0 == ZERO && c1 == ONE && c2 == ZERO {
g4_rows += 1;
for i in 1u16..=3 {
exp.add(row, &OpGroupMsg::new(&addr_next, gc, i, first[i as usize]));
}
} else if c0 == ZERO && c1 == ZERO && c2 == ONE {
g2_rows += 1;
exp.add(row, &OpGroupMsg::new(&addr_next, gc, 1, first[1]));
} else if c0 == ZERO && c1 == ONE && c2 == ONE {
g1_rows += 1;
} else {
panic!("unexpected batch_flags on SPAN/RESPAN row: ({c0:?}, {c1:?}, {c2:?})");
}
});
assert!(respan_observed, "program did not produce a RESPAN row");
assert_eq!(g8_rows, expected_g8_rows);
assert_eq!(g4_rows, expected_g4_rows);
assert_eq!(g2_rows, expected_g2_rows);
assert_eq!(g1_rows, expected_g1_rows);
assert_eq!(exp.count_adds(), 7 * g8_rows + 3 * g4_rows + g2_rows);
assert_eq!(exp.count_removes(), 0);
log.assert_contains(&exp);
}
#[test]
fn decoder_dyncall_at_min_stack_depth_records_post_drop_ctx_info() {
use std::sync::Arc;
use crate::{
MIN_STACK_DEPTH,
mast::{DynNodeBuilder, MastForestContributor},
operation::opcodes,
};
const HASH_ADDR: Felt = Felt::new_unchecked(40);
let mut forest = MastForest::new();
let root = {
let preamble = BasicBlockNodeBuilder::new(
vec![
Operation::Push(HASH_ADDR),
Operation::MStoreW,
Operation::Drop,
Operation::Drop,
Operation::Drop,
Operation::Drop,
Operation::Push(HASH_ADDR),
],
Vec::new(),
)
.add_to_forest(&mut forest)
.unwrap();
let dyncall = DynNodeBuilder::new_dyncall().add_to_forest(&mut forest).unwrap();
JoinNodeBuilder::new([preamble, dyncall]).add_to_forest(&mut forest).unwrap()
};
forest.make_root(root);
let target = BasicBlockNodeBuilder::new(vec![Operation::Swap], Vec::new())
.add_to_forest(&mut forest)
.unwrap();
forest.make_root(target);
let target_hash: Vec<Felt> =
forest.get_node_by_id(target).unwrap().digest().iter().copied().collect();
let program = Program::new(Arc::new(forest), root);
let trace =
build_trace_from_program_with_stack(&program, StackInputs::new(&target_hash).unwrap());
let main = trace.main_trace();
let dyncall_opcode = Felt::from_u8(opcodes::DYNCALL);
let row = main
.row_iter()
.find(|&i| main.get_op_code(i) == dyncall_opcode)
.expect("DYNCALL row not found in trace");
assert_eq!(
main.decoder_hasher_state_element(4, row),
Felt::new_unchecked(MIN_STACK_DEPTH as u64),
"parent_stack_depth should equal MIN_STACK_DEPTH"
);
assert_eq!(
main.decoder_hasher_state_element(5, row),
ZERO,
"parent_next_overflow_addr should be ZERO when stack is at MIN_STACK_DEPTH"
);
}
#[test]
fn decoder_dyncall_with_multiple_overflow_entries_records_correct_overflow_addr() {
use std::sync::Arc;
use crate::{
mast::{DynNodeBuilder, MastForestContributor},
operation::opcodes,
};
const HASH_ADDR: Felt = Felt::new_unchecked(40);
let mut forest = MastForest::new();
let target = BasicBlockNodeBuilder::new(vec![Operation::Swap], Vec::new())
.add_to_forest(&mut forest)
.unwrap();
forest.make_root(target);
let target_hash: Vec<Felt> =
forest.get_node_by_id(target).unwrap().digest().iter().copied().collect();
let root = {
let preamble = BasicBlockNodeBuilder::new(
vec![
Operation::Push(HASH_ADDR),
Operation::MStoreW,
Operation::Drop,
Operation::Drop,
Operation::Drop,
Operation::Drop,
Operation::Push(ZERO), Operation::Push(HASH_ADDR), ],
Vec::new(),
)
.add_to_forest(&mut forest)
.unwrap();
let dyncall = DynNodeBuilder::new_dyncall().add_to_forest(&mut forest).unwrap();
let inner_join =
JoinNodeBuilder::new([preamble, dyncall]).add_to_forest(&mut forest).unwrap();
let cleanup = BasicBlockNodeBuilder::new(vec![Operation::Drop], Vec::new())
.add_to_forest(&mut forest)
.unwrap();
JoinNodeBuilder::new([inner_join, cleanup]).add_to_forest(&mut forest).unwrap()
};
forest.make_root(root);
let program = Program::new(Arc::new(forest), root);
let trace =
build_trace_from_program_with_stack(&program, StackInputs::new(&target_hash).unwrap());
let main = trace.main_trace();
let dyncall_opcode = Felt::from_u8(opcodes::DYNCALL);
let dyncall_row = main
.row_iter()
.find(|&i| main.get_op_code(i) == dyncall_opcode)
.expect("DYNCALL row not found in trace");
let recorded_depth = main.decoder_hasher_state_element(4, dyncall_row);
let recorded_overflow_addr = main.decoder_hasher_state_element(5, dyncall_row);
assert_eq!(
recorded_depth,
Felt::new_unchecked(17),
"parent_stack_depth should be 17 (= pre-DYNCALL depth 18 minus 1)"
);
let push_opcode = Felt::from_u8(opcodes::PUSH);
let push_rows_before_dyncall: Vec<_> = main
.row_iter()
.filter(|&i| i < dyncall_row && main.get_op_code(i) == push_opcode)
.collect();
let n = push_rows_before_dyncall.len();
assert!(n >= 2, "expected at least 2 PUSH rows before DYNCALL, found {n}");
let t1_row = push_rows_before_dyncall[n - 2]; let t2_row = push_rows_before_dyncall[n - 1]; let t1 = main.clk(t1_row);
let t2 = main.clk(t2_row);
assert_eq!(t2, t1 + ONE, "push(0) and push(HASH_ADDR) must be at consecutive clocks");
assert_eq!(
recorded_overflow_addr, t1,
"parent_next_overflow_addr must equal T1 (second-to-last overflow clock = {t1}); \
T2 (top overflow clock = {t2}) would indicate the buggy path"
);
}