Skip to content

Commit a4cbd56

Browse files
committed
fix(bit_machine): use cursor position instead of frame start for tracker read iterator
The tracker was receiving a bit iterator anchored at the frame's start rather than the current cursor position. When a `case` node executes inside another `case` or `drop`, the read frame cursor has already advanced past the start, so the tracker read stale bits and recorded the wrong branch. This caused `assertl`/`assertr` pruning to remove the wrong branch after execution. Adds a regression test covering the specific `comp (pair (injl (injr unit)) unit) (case (case unit unit) unit)` pattern that triggered the bug.
1 parent fe1f888 commit a4cbd56

2 files changed

Lines changed: 46 additions & 5 deletions

File tree

src/bit_machine/frame.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ impl Frame {
123123
) -> BitIter<core::iter::Copied<core::slice::Iter<'a, u8>>> {
124124
BitIter::byte_slice_window(data, self.start, self.start + self.len)
125125
}
126+
127+
/// Like [`as_bit_iter`] but starts from the current cursor position rather than the frame start.
128+
pub(super) fn as_bit_iter_from_cursor<'a>(
129+
&self,
130+
data: &'a [u8],
131+
) -> BitIter<core::iter::Copied<core::slice::Iter<'a, u8>>> {
132+
BitIter::byte_slice_window(data, self.cursor, self.start + self.len)
133+
}
126134
}
127135

128136
fn get_indices(cursor: usize) -> (usize, usize) {

src/bit_machine/mod.rs

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,11 @@ impl BitMachine {
277277
}
278278

279279
'main_loop: loop {
280-
// Make a copy of the input frame to give to the tracker.
280+
// Capture read and write frames before the node action, to give to the tracker.
281+
// The read frame cursor is where the node's input begins.
282+
// The write frame cursor is where the node's output begins.
281283
let input_frame = self.read.last().map(Frame::shallow_copy);
284+
let output_frame = self.write.last().map(Frame::shallow_copy);
282285
let mut jet_result = Ok(());
283286

284287
match ip.inner() {
@@ -391,16 +394,15 @@ impl BitMachine {
391394
// describes the Bit Machine "input" to the current node,
392395
// no matter the node.
393396
let read_iter = input_frame
394-
.map(|frame| frame.as_bit_iter(&self.data))
397+
.map(|frame| frame.as_bit_iter_from_cursor(&self.data))
395398
.unwrap_or(crate::BitIter::from([].iter().copied()));
396399
// See the docs on `tracker::NodeOutput` for more information about
397400
// this match.
398401
let output = match (ip.inner(), &jet_result) {
399402
(node::Inner::Unit | node::Inner::Iden | node::Inner::Witness(_), _)
400403
| (node::Inner::Jet(_), Ok(_)) => NodeOutput::Success(
401-
self.write
402-
.last()
403-
.map(|r| r.as_bit_iter(&self.data))
404+
output_frame
405+
.map(|frame| frame.as_bit_iter_from_cursor(&self.data))
404406
.unwrap_or(crate::BitIter::from([].iter().copied())),
405407
),
406408
(node::Inner::Jet(_), Err(_)) => NodeOutput::JetFailed,
@@ -669,6 +671,37 @@ mod tests {
669671
.exec(&prog, &env)
670672
}
671673

674+
#[test]
675+
#[cfg(feature = "human_encoding")]
676+
fn set_tracker_cursor_regression() {
677+
// When a `case` node executes inside another `case` or `drop`, the read frame cursor
678+
// is no longer at the frame's start. Before this fix, `exec_with_tracker` passed an
679+
// iterator starting at frame.start to the tracker, so `SetTracker` read the wrong bit
680+
// and recorded the wrong branch, leading to an assertl/assertr mismatch after pruning.
681+
//
682+
// Program: comp (pair (injl (injr unit)) unit) (case (case unit unit) unit)
683+
// Intermediate frame bits: [0=L-tag of outer, 1=R-tag of inner]
684+
// Outer case: peek bit 0 = 0 (LEFT), fwd(1), cursor moves to bit 1.
685+
// Inner case: should read bit 1 = 1 (RIGHT).
686+
// Bug: tracker received iterator from bit 0, read 0 (LEFT) → pruned wrong branch.
687+
use crate::human_encoding::Forest;
688+
use crate::types;
689+
use std::collections::HashMap;
690+
691+
types::Context::with_context(|ctx| {
692+
let s = "main := comp (pair (injl (injr unit)) unit) (case (case unit unit) unit)";
693+
let program = Forest::parse::<crate::jet::Core>(s)
694+
.expect("parse")
695+
.to_witness_node(&ctx, &HashMap::new())
696+
.expect("main root")
697+
.finalize_pruned(&CoreEnv::new())
698+
.expect("finalize and prune");
699+
let mut mac = BitMachine::for_program(&program).expect("for_program");
700+
mac.exec(&program, &CoreEnv::new())
701+
.expect("pruned execution should succeed");
702+
});
703+
}
704+
672705
#[test]
673706
#[cfg(feature = "elements")]
674707
fn crash_regression1() {

0 commit comments

Comments
 (0)