From 7ecf394ce6f2965a5dd9ec7a6ef7f91597ae38cb Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sat, 6 Dec 2025 17:12:41 +0000 Subject: [PATCH 1/8] BitIter: add ExactSizeIterator bound, size_hint and tests The tests call `iter.len()` a lot. The default implementation of this method passes through to `iter.size_hint()` and also asserts that both the low and the high estimates are equal. So these tests are pretty thorough. --- src/bit_encoding/bititer.rs | 44 +++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/bit_encoding/bititer.rs b/src/bit_encoding/bititer.rs index 1a61220e..ba85378c 100644 --- a/src/bit_encoding/bititer.rs +++ b/src/bit_encoding/bititer.rs @@ -194,11 +194,29 @@ impl> Iterator for BitIter { self.next() } } + + fn size_hint(&self) -> (usize, Option) { + let (lo, hi) = self.iter.size_hint(); + let adj = |n| 8 - self.read_bits + 8 * n; + (adj(lo), hi.map(adj)) + } +} + +impl core::iter::FusedIterator for BitIter where + I: Iterator + core::iter::FusedIterator +{ +} + +impl core::iter::ExactSizeIterator for BitIter where + I: Iterator + core::iter::ExactSizeIterator +{ } impl<'a> BitIter>> { - /// Creates a new bitwise iterator from a bytewise one. Equivalent - /// to using `From` + /// Creates a new bitwise iterator from a bytewise one. + /// + /// Takes start and end indices *in bits*. If you want to use the entire slice, + /// `BitIter::from` is equivalent and easier to call. pub fn byte_slice_window(sl: &'a [u8], start: usize, end: usize) -> Self { assert!(start <= end); assert!(end <= sl.len() * 8); @@ -423,6 +441,8 @@ mod tests { #[test] fn empty_iter() { let mut iter = BitIter::from([].iter().cloned()); + assert_eq!(iter.len(), 0); + assert!(iter.next().is_none()); assert!(iter.next().is_none()); assert_eq!(iter.read_bit(), Err(EarlyEndOfStreamError)); assert_eq!(iter.read_u2(), Err(EarlyEndOfStreamError)); @@ -434,8 +454,11 @@ mod tests { #[test] fn one_bit_iter() { let mut iter = BitIter::from([0x80].iter().cloned()); + assert_eq!(iter.len(), 8); assert_eq!(iter.read_bit(), Ok(true)); + assert_eq!(iter.len(), 7); assert_eq!(iter.read_bit(), Ok(false)); + assert_eq!(iter.len(), 6); assert_eq!(iter.read_u8(), Err(EarlyEndOfStreamError)); assert_eq!(iter.n_total_read(), 2); } @@ -443,17 +466,22 @@ mod tests { #[test] fn bit_by_bit() { let mut iter = BitIter::from([0x0f, 0xaa].iter().cloned()); + assert_eq!(iter.len(), 16); for _ in 0..4 { assert_eq!(iter.next(), Some(false)); } + assert_eq!(iter.len(), 12); for _ in 0..4 { assert_eq!(iter.next(), Some(true)); } + assert_eq!(iter.len(), 8); for _ in 0..4 { assert_eq!(iter.next(), Some(true)); assert_eq!(iter.next(), Some(false)); } + assert_eq!(iter.len(), 0); assert_eq!(iter.next(), None); + assert_eq!(iter.len(), 0); } #[test] @@ -480,7 +508,9 @@ mod tests { let data = [0x12, 0x23, 0x34]; let mut full = BitIter::byte_slice_window(&data, 0, 24); + assert_eq!(full.len(), 24); assert_eq!(full.read_u8(), Ok(0x12)); + assert_eq!(full.len(), 16); assert_eq!(full.n_total_read(), 8); assert_eq!(full.read_u8(), Ok(0x23)); assert_eq!(full.n_total_read(), 16); @@ -489,7 +519,9 @@ mod tests { assert_eq!(full.read_u8(), Err(EarlyEndOfStreamError)); let mut mid = BitIter::byte_slice_window(&data, 8, 16); + assert_eq!(mid.len(), 8); assert_eq!(mid.read_u8(), Ok(0x23)); + assert_eq!(mid.len(), 0); assert_eq!(mid.read_u8(), Err(EarlyEndOfStreamError)); let mut offs = BitIter::byte_slice_window(&data, 4, 20); @@ -498,13 +530,21 @@ mod tests { assert_eq!(offs.read_u8(), Err(EarlyEndOfStreamError)); let mut shift1 = BitIter::byte_slice_window(&data, 1, 24); + assert_eq!(shift1.len(), 23); assert_eq!(shift1.read_u8(), Ok(0x24)); + assert_eq!(shift1.len(), 15); assert_eq!(shift1.read_u8(), Ok(0x46)); + assert_eq!(shift1.len(), 7); assert_eq!(shift1.read_u8(), Err(EarlyEndOfStreamError)); + assert_eq!(shift1.len(), 7); let mut shift7 = BitIter::byte_slice_window(&data, 7, 24); + assert_eq!(shift7.len(), 17); assert_eq!(shift7.read_u8(), Ok(0x11)); + assert_eq!(shift7.len(), 9); assert_eq!(shift7.read_u8(), Ok(0x9a)); + assert_eq!(shift7.len(), 1); assert_eq!(shift7.read_u8(), Err(EarlyEndOfStreamError)); + assert_eq!(shift7.len(), 1); } } From 8622a8f8aa90dd5281fbbf172e5f911983c411ff Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sat, 6 Dec 2025 17:00:21 +0000 Subject: [PATCH 2/8] bit_machine: return concrete type from `as_bit_iter` Returning a concrete type lets us get more trait methods rather than just `Iterator`. In particular we should be able to get `ExactSizeIterator` and `Fuse` which are important for efficiency. We also want to name this type elsewhere, and even the existential one was annoying to type, so we add an alias for it. --- src/bit_machine/frame.rs | 2 +- src/bit_machine/mod.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/bit_machine/frame.rs b/src/bit_machine/frame.rs index ba20e758..8fb65ba4 100644 --- a/src/bit_machine/frame.rs +++ b/src/bit_machine/frame.rs @@ -104,7 +104,7 @@ impl Frame { /// Extend the present frame with a read-only reference the the data /// and return the resulting struct. - pub fn as_bit_iter<'a>(&self, data: &'a [u8]) -> BitIter + 'a> { + pub(super) fn as_bit_iter<'a>(&self, data: &'a [u8]) -> super::FrameIter<'a> { BitIter::byte_slice_window(data, self.start, self.start + self.len) } } diff --git a/src/bit_machine/mod.rs b/src/bit_machine/mod.rs index 05625489..ec691eac 100644 --- a/src/bit_machine/mod.rs +++ b/src/bit_machine/mod.rs @@ -24,6 +24,9 @@ use simplicity_sys::ffi::UWORD; pub use self::limits::LimitError; +/// An iterator over the contents of a read or write frame which yields bits. +pub type FrameIter<'a> = crate::BitIter>>; + /// An execution context for a Simplicity program pub struct BitMachine { /// Space for bytes that read and write frames point to. From 228a85f25b848304f6cda586ca7ec486d980db1c Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sat, 6 Dec 2025 17:19:40 +0000 Subject: [PATCH 3/8] bit_machine: put pub(super) on all public methods of Frame This whole type is pub(super). It should not have public methods. --- src/bit_machine/frame.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/bit_machine/frame.rs b/src/bit_machine/frame.rs index 8fb65ba4..9de4d8bf 100644 --- a/src/bit_machine/frame.rs +++ b/src/bit_machine/frame.rs @@ -34,12 +34,12 @@ impl Frame { } /// Return the start index of the frame inside the referenced data. - pub fn start(&self) -> usize { + pub(super) fn start(&self) -> usize { self.start } /// Return the bit width of the frame. - pub fn bit_width(&self) -> usize { + pub(super) fn bit_width(&self) -> usize { self.len } @@ -104,7 +104,10 @@ impl Frame { /// Extend the present frame with a read-only reference the the data /// and return the resulting struct. - pub(super) fn as_bit_iter<'a>(&self, data: &'a [u8]) -> super::FrameIter<'a> { + pub(super) fn as_bit_iter<'a>( + &self, + data: &'a [u8], + ) -> BitIter>> { BitIter::byte_slice_window(data, self.start, self.start + self.len) } } From eaa02d0cae21f597b9c711593a162fcb9b3ffd51 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sat, 6 Dec 2025 20:51:58 +0000 Subject: [PATCH 4/8] bit_machine: rename private new/move/drop methods and variants There are three basic operations on the frame stack in the bit machine: creating a new write frame, popping the write stack onto the read stack, and dropping a read frame. Our code uses shorthand like "new_frame" and "drop_frame" which makes it easy to forget which stacks these operations are operating on. This makes it harder to understand the code for somebody who doesn't currently have the bit machine algorithm loaded into their head. Rename to make stuff easier to follow. --- src/bit_machine/mod.rs | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/bit_machine/mod.rs b/src/bit_machine/mod.rs index ec691eac..9135d675 100644 --- a/src/bit_machine/mod.rs +++ b/src/bit_machine/mod.rs @@ -74,7 +74,7 @@ impl BitMachine { } /// Push a new frame of given size onto the write frame stack - fn new_frame(&mut self, len: usize) { + fn new_write_frame(&mut self, len: usize) { debug_assert!( self.next_frame_start + len <= self.data.len() * 8, "Data out of bounds: number of cells" @@ -89,14 +89,14 @@ impl BitMachine { } /// Move the active write frame to the read frame stack - fn move_frame(&mut self) { + fn move_write_frame_to_read(&mut self) { let mut _active_write_frame = self.write.pop().unwrap(); _active_write_frame.reset_cursor(); self.read.push(_active_write_frame); } /// Drop the active read frame - fn drop_frame(&mut self) { + fn drop_read_frame(&mut self) { let active_read_frame = self.read.pop().unwrap(); self.next_frame_start -= active_read_frame.bit_width(); assert_eq!(self.next_frame_start, active_read_frame.start()); @@ -206,9 +206,9 @@ impl BitMachine { } // Unit value doesn't need extra frame if !input.is_empty() { - self.new_frame(input.padded_len()); + self.new_write_frame(input.padded_len()); self.write_value(input); - self.move_frame(); + self.move_write_frame_to_read(); } Ok(()) } @@ -259,8 +259,8 @@ impl BitMachine { ) -> Result { enum CallStack<'a, J: Jet> { Goto(&'a RedeemNode), - MoveFrame, - DropFrame, + MoveWriteFrameToRead, + DropReadFrame, CopyFwd(usize), Back(usize), } @@ -270,8 +270,8 @@ impl BitMachine { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { CallStack::Goto(ins) => write!(f, "goto {}", ins.inner()), - CallStack::MoveFrame => f.write_str("move frame"), - CallStack::DropFrame => f.write_str("drop frame"), + CallStack::MoveWriteFrameToRead => f.write_str("move frame"), + CallStack::DropReadFrame => f.write_str("drop frame"), CallStack::CopyFwd(n) => write!(f, "copy/fwd {}", n), CallStack::Back(n) => write!(f, "back {}", n), } @@ -287,7 +287,7 @@ impl BitMachine { let output_width = ip.arrow().target.bit_width(); if output_width > 0 { - self.new_frame(output_width); + self.new_write_frame(output_width); } 'main_loop: loop { @@ -316,10 +316,10 @@ impl BitMachine { node::Inner::Comp(left, right) => { let size_b = left.arrow().target.bit_width(); - self.new_frame(size_b); - call_stack.push(CallStack::DropFrame); + self.new_write_frame(size_b); + call_stack.push(CallStack::DropReadFrame); call_stack.push(CallStack::Goto(right)); - call_stack.push(CallStack::MoveFrame); + call_stack.push(CallStack::MoveWriteFrameToRead); call_stack.push(CallStack::Goto(left)); } node::Inner::Disconnect(left, right) => { @@ -328,18 +328,18 @@ impl BitMachine { let size_prod_b_c = left.arrow().target.bit_width(); let size_b = size_prod_b_c - right.arrow().source.bit_width(); - self.new_frame(size_prod_256_a); + self.new_write_frame(size_prod_256_a); self.write_bytes(right.cmr().as_ref()); self.copy(size_a); - self.move_frame(); - self.new_frame(size_prod_b_c); + self.move_write_frame_to_read(); + self.new_write_frame(size_prod_b_c); // Remember that call stack pushes are executed in reverse order - call_stack.push(CallStack::DropFrame); - call_stack.push(CallStack::DropFrame); + call_stack.push(CallStack::DropReadFrame); + call_stack.push(CallStack::DropReadFrame); call_stack.push(CallStack::Goto(right)); call_stack.push(CallStack::CopyFwd(size_b)); - call_stack.push(CallStack::MoveFrame); + call_stack.push(CallStack::MoveWriteFrameToRead); call_stack.push(CallStack::Goto(left)); } node::Inner::Take(left) => call_stack.push(CallStack::Goto(left)), @@ -403,8 +403,8 @@ impl BitMachine { ip = loop { match call_stack.pop() { Some(CallStack::Goto(next)) => break next, - Some(CallStack::MoveFrame) => self.move_frame(), - Some(CallStack::DropFrame) => self.drop_frame(), + Some(CallStack::MoveWriteFrameToRead) => self.move_write_frame_to_read(), + Some(CallStack::DropReadFrame) => self.drop_read_frame(), Some(CallStack::CopyFwd(n)) => { self.copy(n); self.fwd(n); From ca4e497e81f99707f42cbd48ed12bb95d5d46279 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sat, 6 Dec 2025 16:22:11 +0000 Subject: [PATCH 5/8] bit_machine: move tracker stuff to its own module Re-export everything so there is no API change. This just tidies up the code a little bit. --- src/bit_machine/mod.rs | 92 ++------------------------------- src/bit_machine/tracker.rs | 101 +++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+), 89 deletions(-) create mode 100644 src/bit_machine/tracker.rs diff --git a/src/bit_machine/mod.rs b/src/bit_machine/mod.rs index 9135d675..2e0c3828 100644 --- a/src/bit_machine/mod.rs +++ b/src/bit_machine/mod.rs @@ -8,21 +8,21 @@ mod frame; mod limits; +mod tracker; -use std::collections::HashSet; use std::error; use std::fmt; use std::sync::Arc; +use crate::analysis; use crate::jet::{Jet, JetFailed}; use crate::node::{self, RedeemNode}; use crate::types::Final; -use crate::{analysis, Ihr}; use crate::{Cmr, FailEntropy, Value}; use frame::Frame; -use simplicity_sys::ffi::UWORD; pub use self::limits::LimitError; +pub use self::tracker::{ExecTracker, NoTracker, SetTracker}; /// An iterator over the contents of a read or write frame which yields bits. pub type FrameIter<'a> = crate::BitIter>>; @@ -539,92 +539,6 @@ impl BitMachine { } } -/// A type that keeps track of Bit Machine execution. -/// -/// The trait is implemented for [`SetTracker`], that tracks which case branches were executed, -/// and it is implemented for [`NoTracker`], which is a dummy tracker that is -/// optimized out by the compiler. -/// -/// The trait enables us to turn tracking on or off depending on a generic parameter. -pub trait ExecTracker { - /// Track the execution of the left branch of the case node with the given `ihr`. - fn track_left(&mut self, ihr: Ihr); - - /// Track the execution of the right branch of the case node with the given `ihr`. - fn track_right(&mut self, ihr: Ihr); - - /// Track the execution of a `jet` call with the given `input_buffer`, `output_buffer`, and call result `success`. - fn track_jet_call( - &mut self, - jet: &J, - input_buffer: &[UWORD], - output_buffer: &[UWORD], - success: bool, - ); - - /// Track the potential execution of a `dbg!` call with the given `cmr` and `value`. - fn track_dbg_call(&mut self, cmr: &Cmr, value: Value); - - /// Check if tracking debug calls is enabled. - fn is_track_debug_enabled(&self) -> bool; -} - -/// Tracker of executed left and right branches for each case node. -#[derive(Clone, Debug, Default)] -pub struct SetTracker { - left: HashSet, - right: HashSet, -} - -impl SetTracker { - /// Access the set of IHRs of case nodes whose left branch was executed. - pub fn left(&self) -> &HashSet { - &self.left - } - - /// Access the set of IHRs of case nodes whose right branch was executed. - pub fn right(&self) -> &HashSet { - &self.right - } -} - -/// Tracker that does not do anything (noop). -#[derive(Copy, Clone, Debug)] -pub struct NoTracker; - -impl ExecTracker for SetTracker { - fn track_left(&mut self, ihr: Ihr) { - self.left.insert(ihr); - } - - fn track_right(&mut self, ihr: Ihr) { - self.right.insert(ihr); - } - - fn track_jet_call(&mut self, _: &J, _: &[UWORD], _: &[UWORD], _: bool) {} - - fn track_dbg_call(&mut self, _: &Cmr, _: Value) {} - - fn is_track_debug_enabled(&self) -> bool { - false - } -} - -impl ExecTracker for NoTracker { - fn track_left(&mut self, _: Ihr) {} - - fn track_right(&mut self, _: Ihr) {} - - fn track_jet_call(&mut self, _: &J, _: &[UWORD], _: &[UWORD], _: bool) {} - - fn track_dbg_call(&mut self, _: &Cmr, _: Value) {} - - fn is_track_debug_enabled(&self) -> bool { - // Set flag to test frame decoding in unit tests - cfg!(test) - } -} - /// Errors related to simplicity Execution #[derive(Debug)] pub enum ExecutionError { diff --git a/src/bit_machine/tracker.rs b/src/bit_machine/tracker.rs new file mode 100644 index 00000000..42f5b44c --- /dev/null +++ b/src/bit_machine/tracker.rs @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Bit Machine Tracker +//! +//! This module provides traits for adding "trackers" to the bit machine execution +//! and pruning algorithms, which can provide debugging output or control which +//! branches are pruned. It also provides a couple example/utility trackers. +//! +//! It is a private module but all types and traits are re-exported above. + +use simplicity_sys::ffi::UWORD; +use std::collections::HashSet; + +use crate::jet::Jet; +use crate::{Cmr, Ihr, Value}; + +/// A type that keeps track of Bit Machine execution. +/// +/// The trait is implemented for [`SetTracker`], that tracks which case branches were executed, +/// and it is implemented for [`NoTracker`], which is a dummy tracker that is +/// optimized out by the compiler. +/// +/// The trait enables us to turn tracking on or off depending on a generic parameter. +pub trait ExecTracker { + /// Track the execution of the left branch of the case node with the given `ihr`. + fn track_left(&mut self, ihr: Ihr); + + /// Track the execution of the right branch of the case node with the given `ihr`. + fn track_right(&mut self, ihr: Ihr); + + /// Track the execution of a `jet` call with the given `input_buffer`, `output_buffer`, and call result `success`. + fn track_jet_call( + &mut self, + jet: &J, + input_buffer: &[UWORD], + output_buffer: &[UWORD], + success: bool, + ); + + /// Track the potential execution of a `dbg!` call with the given `cmr` and `value`. + fn track_dbg_call(&mut self, cmr: &Cmr, value: Value); + + /// Check if tracking debug calls is enabled. + fn is_track_debug_enabled(&self) -> bool; +} + +/// Tracker of executed left and right branches for each case node. +#[derive(Clone, Debug, Default)] +pub struct SetTracker { + left: HashSet, + right: HashSet, +} + +impl SetTracker { + /// Access the set of IHRs of case nodes whose left branch was executed. + pub fn left(&self) -> &HashSet { + &self.left + } + + /// Access the set of IHRs of case nodes whose right branch was executed. + pub fn right(&self) -> &HashSet { + &self.right + } +} + +/// Tracker that does not do anything (noop). +#[derive(Copy, Clone, Debug)] +pub struct NoTracker; + +impl ExecTracker for SetTracker { + fn track_left(&mut self, ihr: Ihr) { + self.left.insert(ihr); + } + + fn track_right(&mut self, ihr: Ihr) { + self.right.insert(ihr); + } + + fn track_jet_call(&mut self, _: &J, _: &[UWORD], _: &[UWORD], _: bool) {} + + fn track_dbg_call(&mut self, _: &Cmr, _: Value) {} + + fn is_track_debug_enabled(&self) -> bool { + false + } +} + +impl ExecTracker for NoTracker { + fn track_left(&mut self, _: Ihr) {} + + fn track_right(&mut self, _: Ihr) {} + + fn track_jet_call(&mut self, _: &J, _: &[UWORD], _: &[UWORD], _: bool) {} + + fn track_dbg_call(&mut self, _: &Cmr, _: Value) {} + + fn is_track_debug_enabled(&self) -> bool { + // Set flag to test frame decoding in unit tests + cfg!(test) + } +} From 4134976da0a0a91108600c2199cdfb4258ecc9d3 Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sat, 6 Dec 2025 17:27:29 +0000 Subject: [PATCH 6/8] bit_machine: add PruneTracker trait and RedeemNode::prune_with_tracker This eliminates the private `exec_prune` method which was weirdly-named and no longer makes sense now that we're using a generic tracker rather than a SetTracker. --- src/bit_machine/mod.rs | 22 +--------------------- src/bit_machine/tracker.rs | 38 ++++++++++++++++++++++---------------- src/node/redeem.rs | 26 +++++++++++++++++++------- 3 files changed, 42 insertions(+), 44 deletions(-) diff --git a/src/bit_machine/mod.rs b/src/bit_machine/mod.rs index 2e0c3828..8d462636 100644 --- a/src/bit_machine/mod.rs +++ b/src/bit_machine/mod.rs @@ -22,7 +22,7 @@ use crate::{Cmr, FailEntropy, Value}; use frame::Frame; pub use self::limits::LimitError; -pub use self::tracker::{ExecTracker, NoTracker, SetTracker}; +pub use self::tracker::{ExecTracker, NoTracker, PruneTracker, SetTracker}; /// An iterator over the contents of a read or write frame which yields bits. pub type FrameIter<'a> = crate::BitIter>>; @@ -226,26 +226,6 @@ impl BitMachine { self.exec_with_tracker(program, env, &mut NoTracker) } - /// Execute the given `program` on the Bit Machine and track executed case branches. - /// - /// If the program runs successfully, then two sets of IHRs are returned: - /// - /// 1) The IHRs of case nodes whose _left_ branch was executed. - /// 2) The IHRs of case nodes whose _right_ branch was executed. - /// - /// ## Precondition - /// - /// The Bit Machine is constructed via [`Self::for_program()`] to ensure enough space. - pub(crate) fn exec_prune( - &mut self, - program: &RedeemNode, - env: &J::Environment, - ) -> Result { - let mut tracker = SetTracker::default(); - self.exec_with_tracker(program, env, &mut tracker)?; - Ok(tracker) - } - /// Execute the given `program` on the Bit Machine, using the given environment and tracker. /// /// ## Precondition diff --git a/src/bit_machine/tracker.rs b/src/bit_machine/tracker.rs index 42f5b44c..57054fee 100644 --- a/src/bit_machine/tracker.rs +++ b/src/bit_machine/tracker.rs @@ -44,6 +44,14 @@ pub trait ExecTracker { fn is_track_debug_enabled(&self) -> bool; } +pub trait PruneTracker: ExecTracker { + /// Returns true if the left branch of the of the `Case` node with the IHR `ihr` was taken. + fn contains_left(&self, ihr: Ihr) -> bool; + + /// Returns true if the right branch of the of the `Case` node with the IHR `ihr` was taken. + fn contains_right(&self, ihr: Ihr) -> bool; +} + /// Tracker of executed left and right branches for each case node. #[derive(Clone, Debug, Default)] pub struct SetTracker { @@ -51,22 +59,6 @@ pub struct SetTracker { right: HashSet, } -impl SetTracker { - /// Access the set of IHRs of case nodes whose left branch was executed. - pub fn left(&self) -> &HashSet { - &self.left - } - - /// Access the set of IHRs of case nodes whose right branch was executed. - pub fn right(&self) -> &HashSet { - &self.right - } -} - -/// Tracker that does not do anything (noop). -#[derive(Copy, Clone, Debug)] -pub struct NoTracker; - impl ExecTracker for SetTracker { fn track_left(&mut self, ihr: Ihr) { self.left.insert(ihr); @@ -85,6 +77,20 @@ impl ExecTracker for SetTracker { } } +impl PruneTracker for SetTracker { + fn contains_left(&self, ihr: Ihr) -> bool { + self.left.contains(&ihr) + } + + fn contains_right(&self, ihr: Ihr) -> bool { + self.right.contains(&ihr) + } +} + +/// Tracker that does not do anything (noop). +#[derive(Copy, Clone, Debug)] +pub struct NoTracker; + impl ExecTracker for NoTracker { fn track_left(&mut self, _: Ihr) {} diff --git a/src/node/redeem.rs b/src/node/redeem.rs index df7af5a0..1a09bb12 100644 --- a/src/node/redeem.rs +++ b/src/node/redeem.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: CC0-1.0 use crate::analysis::NodeBounds; -use crate::bit_machine::{ExecutionError, SetTracker}; +use crate::bit_machine::{ExecutionError, PruneTracker, SetTracker}; use crate::dag::{DagLike, InternalSharing, MaxSharing, PostOrderIterItem}; use crate::jet::Jet; use crate::types::{self, arrow::FinalArrow}; @@ -290,13 +290,25 @@ impl RedeemNode { /// In this case, the witness data needs to be revised. /// The other pruning steps (2 & 3) never fail. pub fn prune(&self, env: &J::Environment) -> Result>, ExecutionError> { - struct Pruner<'brand, J> { + self.prune_with_tracker(env, &mut SetTracker::default()) + } + + pub fn prune_with_tracker>( + &self, + env: &J::Environment, + tracker: &mut T, + ) -> Result>, ExecutionError> { + struct Pruner<'brand, 't, J, T> { inference_context: types::Context<'brand>, - tracker: SetTracker, + tracker: &'t mut T, phantom: PhantomData, } - impl<'brand, J: Jet> Converter, Construct<'brand, J>> for Pruner<'brand, J> { + impl<'brand, 't, J, T> Converter, Construct<'brand, J>> for Pruner<'brand, 't, J, T> + where + J: Jet, + T: PruneTracker, + { type Error = std::convert::Infallible; fn convert_witness( @@ -332,8 +344,8 @@ impl RedeemNode { // but the Converter trait gives us access to the unpruned node (`data`). // The Bit Machine tracked (un)used case branches based on the unpruned IHR. match ( - self.tracker.left().contains(&data.node.ihr()), - self.tracker.right().contains(&data.node.ihr()), + self.tracker.contains_left(data.node.ihr()), + self.tracker.contains_right(data.node.ihr()), ) { (true, true) => Ok(Hide::Neither), (false, true) => Ok(Hide::Left), @@ -418,7 +430,7 @@ impl RedeemNode { // 1) Run the Bit Machine and mark (un)used branches. // This is the only fallible step in the pruning process. let mut mac = BitMachine::for_program(self)?; - let tracker = mac.exec_prune(self, env)?; + mac.exec_with_tracker(self, env, tracker)?; // 2) Prune out unused case branches. // Because the types of the pruned program may change, From 6013bd521baa054f6dee12a23f419a68430bbaaf Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Sat, 6 Dec 2025 20:29:52 +0000 Subject: [PATCH 7/8] bit_machine: replace `ExecTracker` API with a much more general one This replaces the `ExecTracker` API with one which is able to detect every node, not just cases and jets; which is able to read the input value for every node (as a bit iterator which can be converted to a value with Value::from_padded_bits) and the output value for terminal nodes; and which can do all the existing things that the tracker can do. I suspect we want to add some examples or unit tests, in particular around "debug nodes". Fixes #324 --- src/bit_machine/frame.rs | 13 ++++ src/bit_machine/mod.rs | 63 +++++++++++------- src/bit_machine/tracker.rs | 130 ++++++++++++++++++++++--------------- 3 files changed, 129 insertions(+), 77 deletions(-) diff --git a/src/bit_machine/frame.rs b/src/bit_machine/frame.rs index 9de4d8bf..9415b9ac 100644 --- a/src/bit_machine/frame.rs +++ b/src/bit_machine/frame.rs @@ -43,6 +43,19 @@ impl Frame { self.len } + /// Makes a copy of the frame. + /// + /// This copies *only the indices* and none of the underlying + /// data. It is the caller's responsibility to make sure that + /// the indices are not invalidated. + pub(super) fn shallow_copy(&self) -> Self { + Self { + cursor: self.cursor, + start: self.start, + len: self.len, + } + } + /// Reset the cursor to the start. pub(super) fn reset_cursor(&mut self) { self.cursor = self.start; diff --git a/src/bit_machine/mod.rs b/src/bit_machine/mod.rs index 8d462636..9b504752 100644 --- a/src/bit_machine/mod.rs +++ b/src/bit_machine/mod.rs @@ -22,7 +22,7 @@ use crate::{Cmr, FailEntropy, Value}; use frame::Frame; pub use self::limits::LimitError; -pub use self::tracker::{ExecTracker, NoTracker, PruneTracker, SetTracker}; +pub use self::tracker::{ExecTracker, NoTracker, NodeOutput, PruneTracker, SetTracker}; /// An iterator over the contents of a read or write frame which yields bits. pub type FrameIter<'a> = crate::BitIter>>; @@ -271,6 +271,10 @@ impl BitMachine { } 'main_loop: loop { + // Make a copy of the input frame to give to the tracker. + let input_frame = self.read.last().map(Frame::shallow_copy); + let mut jet_result = Ok(()); + match ip.inner() { node::Inner::Unit => {} node::Inner::Iden => { @@ -336,32 +340,18 @@ impl BitMachine { let (sum_a_b, _c) = ip.arrow().source.as_product().unwrap(); let (a, b) = sum_a_b.as_sum().unwrap(); - if tracker.is_track_debug_enabled() { - if let node::Inner::AssertL(_, cmr) = ip.inner() { - let mut bits = in_frame.as_bit_iter(&self.data); - // Skips 1 + max(a.bit_width, b.bit_width) - a.bit_width - bits.nth(a.pad_left(b)) - .expect("AssertL: unexpected end of frame"); - let value = Value::from_padded_bits(&mut bits, _c) - .expect("AssertL: decode `C` value"); - tracker.track_dbg_call(cmr, value); - } - } - match (ip.inner(), choice_bit) { (node::Inner::Case(_, right), true) | (node::Inner::AssertR(_, right), true) => { self.fwd(1 + a.pad_right(b)); call_stack.push(CallStack::Back(1 + a.pad_right(b))); call_stack.push(CallStack::Goto(right)); - tracker.track_right(ip.ihr()); } (node::Inner::Case(left, _), false) | (node::Inner::AssertL(left, _), false) => { self.fwd(1 + a.pad_left(b)); call_stack.push(CallStack::Back(1 + a.pad_left(b))); call_stack.push(CallStack::Goto(left)); - tracker.track_left(ip.ihr()); } (node::Inner::AssertL(_, r_cmr), true) => { return Err(ExecutionError::ReachedPrunedBranch(*r_cmr)) @@ -373,13 +363,43 @@ impl BitMachine { } } node::Inner::Witness(value) => self.write_value(value), - node::Inner::Jet(jet) => self.exec_jet(*jet, env, tracker)?, + node::Inner::Jet(jet) => { + jet_result = self.exec_jet(*jet, env); + } node::Inner::Word(value) => self.write_value(value.as_value()), node::Inner::Fail(entropy) => { return Err(ExecutionError::ReachedFailNode(*entropy)) } } + // Notify the tracker. + { + // Notice that, because the read frame stack is only ever + // shortened by `drop_read_frame`, and that method was not + // called above, this frame is still valid and correctly + // describes the Bit Machine "input" to the current node, + // no matter the node. + let read_iter = input_frame + .map(|frame| frame.as_bit_iter(&self.data)) + .unwrap_or(crate::BitIter::from([].iter().copied())); + // See the docs on `tracker::NodeOutput` for more information about + // this match. + let output = match (ip.inner(), &jet_result) { + (node::Inner::Unit | node::Inner::Iden | node::Inner::Witness(_), _) + | (node::Inner::Jet(_), Ok(_)) => NodeOutput::Success( + self.write + .last() + .map(|r| r.as_bit_iter(&self.data)) + .unwrap_or(crate::BitIter::from([].iter().copied())), + ), + (node::Inner::Jet(_), Err(_)) => NodeOutput::JetFailed, + _ => NodeOutput::NonTerminal, + }; + tracker.visit_node(ip, read_iter, output); + } + // Fail if the jet failed. + jet_result?; + ip = loop { match call_stack.pop() { Some(CallStack::Goto(next)) => break next, @@ -410,12 +430,7 @@ impl BitMachine { } } - fn exec_jet>( - &mut self, - jet: J, - env: &J::Environment, - tracker: &mut T, - ) -> Result<(), JetFailed> { + fn exec_jet(&mut self, jet: J, env: &J::Environment) -> Result<(), JetFailed> { use crate::ffi::c_jets::frame_ffi::{c_readBit, c_writeBit, CFrameItem}; use crate::ffi::c_jets::uword_width; use crate::ffi::ffi::UWORD; @@ -501,15 +516,13 @@ impl BitMachine { let output_width = jet.target_ty().to_bit_width(); // Input buffer is implicitly referenced by input read frame! // Same goes for output buffer - let (input_read_frame, input_buffer) = unsafe { get_input_frame(self, input_width) }; + let (input_read_frame, _input_buffer) = unsafe { get_input_frame(self, input_width) }; let (mut output_write_frame, output_buffer) = unsafe { get_output_frame(output_width) }; let jet_fn = jet.c_jet_ptr(); let c_env = J::c_jet_env(env); let success = jet_fn(&mut output_write_frame, input_read_frame, c_env); - tracker.track_jet_call(&jet, &input_buffer, &output_buffer, success); - if !success { Err(JetFailed) } else { diff --git a/src/bit_machine/tracker.rs b/src/bit_machine/tracker.rs index 57054fee..cccdf353 100644 --- a/src/bit_machine/tracker.rs +++ b/src/bit_machine/tracker.rs @@ -8,40 +8,60 @@ //! //! It is a private module but all types and traits are re-exported above. -use simplicity_sys::ffi::UWORD; use std::collections::HashSet; use crate::jet::Jet; -use crate::{Cmr, Ihr, Value}; +use crate::node::Inner; +use crate::{Ihr, RedeemNode, Value}; -/// A type that keeps track of Bit Machine execution. +/// Write frame of a terminal (childless) Simplicity program node. /// -/// The trait is implemented for [`SetTracker`], that tracks which case branches were executed, -/// and it is implemented for [`NoTracker`], which is a dummy tracker that is -/// optimized out by the compiler. +/// When a terminal node of a program is encountered in the Bit Machine, it +/// has a well-defined "output": the contents of the topmost write frame in +/// the machine. In particular, for `witness` nodes this will be the witness +/// data, for jets it will be the result of the jet, and so on. /// -/// The trait enables us to turn tracking on or off depending on a generic parameter. -pub trait ExecTracker { - /// Track the execution of the left branch of the case node with the given `ihr`. - fn track_left(&mut self, ihr: Ihr); - - /// Track the execution of the right branch of the case node with the given `ihr`. - fn track_right(&mut self, ihr: Ihr); - - /// Track the execution of a `jet` call with the given `input_buffer`, `output_buffer`, and call result `success`. - fn track_jet_call( - &mut self, - jet: &J, - input_buffer: &[UWORD], - output_buffer: &[UWORD], - success: bool, - ); - - /// Track the potential execution of a `dbg!` call with the given `cmr` and `value`. - fn track_dbg_call(&mut self, cmr: &Cmr, value: Value); +/// For non-terminal nodes, the Bit Machine typically does some setup, then +/// executes the nodes' children, then does some teardown. So at no point is +/// there a well-defined "output" we can provide. +#[derive(Debug, Clone)] +pub enum NodeOutput<'m> { + /// Non-terminal node, which has no output. + NonTerminal, + /// Node was a jet which failed, i.e. aborted the program, and therefore + /// has no output. + JetFailed, + /// Node succeeded. This is its output frame. + Success(super::FrameIter<'m>), +} - /// Check if tracking debug calls is enabled. - fn is_track_debug_enabled(&self) -> bool; +/// An object which can be used to introspect the execution of the Bit Machine. +/// +/// If this tracker records accesses to the left and right children of `Case` nodes, you +/// may want to also implement [`PruneTracker`] so that this data can be used by +/// [`RedeemNode::prune_with_tracker`] to prune the program. The most straightforward +/// way to do this is to embed a [`SetTracker`] in your tracker and forward all the trait +/// methods to that. +pub trait ExecTracker { + /// Called immediately after a specific node of the program is executed, but before + /// its children are executed. + /// + /// More precisely, this iterates through the through the Simplicity program tree in + /// *pre* ordering. That is, for the program `comp iden unit` the nodes will be visited + /// in the order `comp`, `iden`, `unit`. + /// + /// This method can be used for logging, to track left or right accesses of the children of a + /// `Case` node (to do this, call `input.peek_bit()`; false means left and true means right), + /// to extract debug information (which may be embedded in the hidden CMR in `AssertL` + /// and `AssertR` nodes, depending how the program was constructed), and so on. + /// + /// The provided arguments are: + /// * `node` is the node which was just visited. + /// * `input` is an iterator over the read frame when the node's execution began + /// * for terminal nodes (`witness`, `unit`, `iden` and jets), `output` is an iterator + /// the write frame after the node has executed. See [`NodeOutput`] for more information. + fn visit_node(&mut self, _node: &RedeemNode, _input: super::FrameIter, _output: NodeOutput) { + } } pub trait PruneTracker: ExecTracker { @@ -60,20 +80,21 @@ pub struct SetTracker { } impl ExecTracker for SetTracker { - fn track_left(&mut self, ihr: Ihr) { - self.left.insert(ihr); - } - - fn track_right(&mut self, ihr: Ihr) { - self.right.insert(ihr); - } - - fn track_jet_call(&mut self, _: &J, _: &[UWORD], _: &[UWORD], _: bool) {} - - fn track_dbg_call(&mut self, _: &Cmr, _: Value) {} - - fn is_track_debug_enabled(&self) -> bool { - false + fn visit_node<'d>( + &mut self, + node: &RedeemNode, + mut input: super::FrameIter, + _output: NodeOutput, + ) { + match (node.inner(), input.next()) { + (Inner::AssertL(..) | Inner::Case(..), Some(false)) => { + self.left.insert(node.ihr()); + } + (Inner::AssertR(..) | Inner::Case(..), Some(true)) => { + self.right.insert(node.ihr()); + } + _ => {} + } } } @@ -92,16 +113,21 @@ impl PruneTracker for SetTracker { pub struct NoTracker; impl ExecTracker for NoTracker { - fn track_left(&mut self, _: Ihr) {} - - fn track_right(&mut self, _: Ihr) {} - - fn track_jet_call(&mut self, _: &J, _: &[UWORD], _: &[UWORD], _: bool) {} - - fn track_dbg_call(&mut self, _: &Cmr, _: Value) {} - - fn is_track_debug_enabled(&self) -> bool { - // Set flag to test frame decoding in unit tests - cfg!(test) + fn visit_node<'d>( + &mut self, + node: &RedeemNode, + mut input: super::FrameIter, + output: NodeOutput, + ) { + if cfg!(test) { + // In unit tests, attempt to decode values from the frames, confirming that + // decoding works. + Value::from_padded_bits(&mut input, &node.arrow().source) + .expect("decoding input should work"); + if let NodeOutput::Success(mut output) = output { + Value::from_padded_bits(&mut output, &node.arrow().target) + .expect("decoding output should work"); + } + } } } From 3bf7cefdd1d4d15893f48e4a5114ecdea43d01fd Mon Sep 17 00:00:00 2001 From: Andrew Poelstra Date: Tue, 9 Dec 2025 21:54:41 +0000 Subject: [PATCH 8/8] bit_machine: add example StderrTracker which outputs stuff to stderr Outputting to stderr is not super useful but this can be used as a demo of what the tracker is able to do. --- src/bit_machine/mod.rs | 7 +++- src/bit_machine/tracker.rs | 70 ++++++++++++++++++++++++++++++++++++++ src/node/redeem.rs | 5 +++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/bit_machine/mod.rs b/src/bit_machine/mod.rs index 9b504752..39c7b35e 100644 --- a/src/bit_machine/mod.rs +++ b/src/bit_machine/mod.rs @@ -22,7 +22,9 @@ use crate::{Cmr, FailEntropy, Value}; use frame::Frame; pub use self::limits::LimitError; -pub use self::tracker::{ExecTracker, NoTracker, NodeOutput, PruneTracker, SetTracker}; +pub use self::tracker::{ + ExecTracker, NoTracker, NodeOutput, PruneTracker, SetTracker, StderrTracker, +}; /// An iterator over the contents of a read or write frame which yields bits. pub type FrameIter<'a> = crate::BitIter>>; @@ -228,6 +230,9 @@ impl BitMachine { /// Execute the given `program` on the Bit Machine, using the given environment and tracker. /// + /// See [`crate::bit_machine::StderrTracker`] as an example which outputs various debug + /// data for each node, providing a track of the bit machine's operation. + /// /// ## Precondition /// /// The Bit Machine is constructed via [`Self::for_program()`] to ensure enough space. diff --git a/src/bit_machine/tracker.rs b/src/bit_machine/tracker.rs index cccdf353..0fef2494 100644 --- a/src/bit_machine/tracker.rs +++ b/src/bit_machine/tracker.rs @@ -131,3 +131,73 @@ impl ExecTracker for NoTracker { } } } + +/// Tracker that just outputs all its activity to stderr. +#[derive(Clone, Debug, Default)] +pub struct StderrTracker { + exec_count: usize, + inner: SetTracker, +} + +impl StderrTracker { + /// Constructs a new empty [`StderrTracker`], ready for use. + pub fn new() -> Self { + Self::default() + } +} + +impl ExecTracker for StderrTracker { + fn visit_node(&mut self, node: &RedeemNode, input: super::FrameIter, output: NodeOutput) { + let input_val = Value::from_padded_bits(&mut input.clone(), &node.arrow().source) + .expect("input from bit machine will always be well-formed"); + eprintln!( + "[{:4}] exec {:10} {}", + self.exec_count, + node.inner(), + node.arrow() + ); + eprintln!(" input {input_val}"); + match output.clone() { + NodeOutput::NonTerminal => { /* don't bother describing non-terminal output */ } + NodeOutput::JetFailed => eprintln!(" JET FAILED"), + NodeOutput::Success(mut output) => { + let output_val = Value::from_padded_bits(&mut output, &node.arrow().target) + .expect("output from bit machine will always be well-formed"); + eprintln!(" output {output_val}"); + } + } + + if let crate::node::Inner::AssertL(_, cmr) = node.inner() { + // SimplicityHL, when compiling in "debug mode", tags nodes by inserting + // synthetic AssertL nodes where the "cmr" is actually a key into a lookup + // table of debug information. An implementation of ExecTracker within + // the compiler itself might do a lookup here to output more useful + // information to the user. + eprintln!(" [debug] assertL CMR {cmr}"); + } + + ExecTracker::::visit_node(&mut self.inner, node, input, output); + self.exec_count += 1; + eprintln!(); + } +} + +impl PruneTracker for StderrTracker { + fn contains_left(&self, ihr: Ihr) -> bool { + if PruneTracker::::contains_left(&self.inner, ihr) { + true + } else { + eprintln!("Pruning unexecuted left child of IHR {ihr}"); + false + } + } + + fn contains_right(&self, ihr: Ihr) -> bool { + if PruneTracker::::contains_right(&self.inner, ihr) { + true + } else { + eprintln!("Pruning unexecuted right child of IHR {ihr}"); + false + } + } +} diff --git a/src/node/redeem.rs b/src/node/redeem.rs index 1a09bb12..aa0bf45e 100644 --- a/src/node/redeem.rs +++ b/src/node/redeem.rs @@ -293,6 +293,11 @@ impl RedeemNode { self.prune_with_tracker(env, &mut SetTracker::default()) } + /// Prune the redeem program, as in [`Self::prune`], but with a custom tracker which + /// can introspect or control pruning. + /// + /// See [`crate::bit_machine::StderrTracker`] as an example which outputs the IHR of + /// each case combinator that we prune a child of. pub fn prune_with_tracker>( &self, env: &J::Environment,