Skip to main content

simplicityhl/
tracker.rs

1use simplicity::bit_machine::{ExecTracker, FrameIter, NodeOutput, PruneTracker, SetTracker};
2use simplicity::node::Inner;
3use simplicity::{Ihr, RedeemNode, Value as SimValue};
4
5use crate::array::Unfolder;
6use crate::ast::{ElementsJetHinter, JetHinter};
7use crate::debug::{DebugSymbols, TrackedCallName};
8use crate::either::Either;
9use crate::jet::{source_type, target_type, JetHL};
10use crate::str::AliasName;
11use crate::types::AliasedType;
12use crate::value::StructuralValue;
13use crate::{ResolvedType, Value};
14
15/// Callback signature for receiving debug output.
16///
17/// The first argument is the label (variable name or expression), and the second
18/// is the formatted value.
19type DebugSink<'a> = Box<dyn FnMut(&str, &Value) + 'a>;
20
21/// Callback signature for receiving jet execution traces.
22///
23/// Arguments are: the jet that was executed, its input arguments (if successfully parsed),
24/// and the result (`None` if the jet failed).
25type JetTraceSink<'a> = Box<dyn FnMut(&dyn JetHL, Option<&[Value]>, Option<Value>) + 'a>;
26
27/// Callback signature for receiving warnings during execution.
28type WarningSink<'a> = Box<dyn Fn(&str) + 'a>;
29
30/// Controls the verbosity of program execution logging.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
32pub enum TrackerLogLevel {
33    #[default]
34    None,
35    Debug,
36    Warning,
37    Trace,
38}
39
40/// Default debug sink that prints labeled values to stderr.
41fn default_debug_sink(label: &str, value: &Value) {
42    println!("DBG: {label} = {value}");
43}
44
45/// Default jet trace sink that prints jet calls to stderr.
46fn default_jet_trace_sink(jet: &dyn JetHL, args: Option<&[Value]>, result: Option<Value>) {
47    print!("{jet:?}(");
48    if let Some(args) = args {
49        for (i, arg) in args.iter().enumerate() {
50            if i > 0 {
51                print!(", ");
52            }
53            print!("{arg}");
54        }
55    } else {
56        print!("...");
57    }
58
59    match result {
60        Some(value) => println!(") = {value}"),
61        None => println!(") -> [failed]"),
62    }
63}
64
65/// Default warning sink that prints warnings to stderr.
66fn default_warning_sink(message: &str) {
67    println!("WARN: {message}");
68}
69
70/// Tracker for introspecting SimplicityHL program execution.
71///
72/// This tracker extends [`SetTracker`] with SimplicityHL-specific functionality:
73///
74/// - Decodes and forwards `dbg!()` calls to a configurable sink, using
75///   the provided [`DebugSymbols`] to resolve CMRs to debug information.
76/// - Optionally traces jet invocations with decoded arguments and return values.
77///
78/// # Example
79///
80/// ```rust,ignore
81/// let tracker = DefaultTracker::new(&debug_symbols)
82///     .with_log_level(TrackerLogLevel::Debug);
83///
84/// let pruned = program.prune_with_tracker(&env, &mut tracker)?;
85/// ```
86pub struct DefaultTracker<'a> {
87    debug_symbols: &'a DebugSymbols,
88    jet_hinter: Box<dyn JetHinter>,
89    debug_sink: Option<DebugSink<'a>>,
90    jet_trace_sink: Option<JetTraceSink<'a>>,
91    warning_sink: Option<WarningSink<'a>>,
92    inner: SetTracker,
93}
94
95impl<'a> DefaultTracker<'a> {
96    /// Creates a new tracker bound to the given debug symbol table.
97    ///
98    /// This constructor is deprecated in favor of more flexible tracker setup.
99    /// The deprecation is necessary to show the direction in which the SimplicityHL is moving
100    /// (i.e. different targets and support for possible custom Jets)  
101    #[deprecated(since = "0.6.0", note = "Please use `build` instead")]
102    pub fn new(debug_symbols: &'a DebugSymbols) -> Self {
103        Self::build(debug_symbols, Box::new(ElementsJetHinter::new()))
104    }
105
106    /// Creates a new tracker bound to a custom jet family.
107    pub fn build(debug_symbols: &'a DebugSymbols, jet_hinter: Box<dyn JetHinter>) -> Self {
108        Self {
109            debug_symbols,
110            jet_hinter,
111            debug_sink: None,
112            jet_trace_sink: None,
113            warning_sink: None,
114            inner: SetTracker::default(),
115        }
116    }
117
118    /// Enables forwarding of `debug!()` calls to the provided sink.
119    pub fn with_debug_sink<F>(mut self, sink: F) -> Self
120    where
121        F: FnMut(&str, &Value) + 'a,
122    {
123        self.debug_sink = Some(Box::new(sink));
124        self
125    }
126
127    /// Enables the default debug sink that prints to stderr.
128    pub fn with_default_debug_sink(self) -> Self {
129        self.with_debug_sink(default_debug_sink)
130    }
131
132    /// Enables forwarding of jet call traces to the provided sink.
133    pub fn with_jet_trace_sink<F>(mut self, sink: F) -> Self
134    where
135        F: FnMut(&dyn JetHL, Option<&[Value]>, Option<Value>) + 'a,
136    {
137        self.jet_trace_sink = Some(Box::new(sink));
138        self
139    }
140
141    /// Enables the default jet trace sink that prints to stderr.
142    pub fn with_default_jet_trace_sink(self) -> Self {
143        self.with_jet_trace_sink(default_jet_trace_sink)
144    }
145
146    /// Enables forwarding of warnings to the provided sink.
147    pub fn with_warning_sink<F>(mut self, sink: F) -> Self
148    where
149        F: Fn(&str) + 'a,
150    {
151        self.warning_sink = Some(Box::new(sink));
152        self
153    }
154
155    /// Enables the default warning sink that prints to stderr.
156    pub fn with_default_warning_sink(self) -> Self {
157        self.with_warning_sink(default_warning_sink)
158    }
159
160    /// Configures the tracker based on the specified log level.
161    ///
162    /// - [`TrackerLogLevel::None`]: No sinks enabled.
163    /// - [`TrackerLogLevel::Debug`]: Default debug sink enabled.
164    /// - [`TrackerLogLevel::Warning`]: Default debug and warning sinks enabled.
165    /// - [`TrackerLogLevel::Trace`]: Default debug, warning, and jet trace sinks enabled.
166    pub fn with_log_level(self, log_level: TrackerLogLevel) -> Self {
167        let tracker = if log_level >= TrackerLogLevel::Debug {
168            self.with_default_debug_sink()
169        } else {
170            self
171        };
172
173        let tracker = if log_level >= TrackerLogLevel::Warning {
174            tracker.with_default_warning_sink()
175        } else {
176            tracker
177        };
178
179        if log_level >= TrackerLogLevel::Trace {
180            tracker.with_default_jet_trace_sink()
181        } else {
182            tracker
183        }
184    }
185
186    /// Handles jet node execution by decoding arguments and results.
187    fn handle_jet(
188        &mut self,
189        node: &RedeemNode,
190        jet: &dyn JetHL,
191        input: &FrameIter,
192        output: &NodeOutput,
193    ) {
194        if self.jet_trace_sink.is_none() {
195            return;
196        }
197
198        let mut input_frame = input.clone();
199
200        let args = match parse_jet_arguments(jet, &mut input_frame) {
201            Ok(args) => args,
202            Err(e) => {
203                self.warn(&format!("Failed to parse arguments for jet {jet:?}: {e}"));
204
205                // Still call the sink to report the jet execution, but without arguments.
206                let result = Self::parse_jet_result(node, jet, output);
207                if let Some(sink) = self.jet_trace_sink.as_mut() {
208                    sink(jet, None, result);
209                }
210
211                return;
212            }
213        };
214
215        let result = Self::parse_jet_result(node, jet, output);
216
217        if let Some(sink) = self.jet_trace_sink.as_mut() {
218            sink(jet, Some(&args), result);
219        }
220    }
221
222    /// Parses the result of a jet execution from the output frame.
223    fn parse_jet_result(node: &RedeemNode, jet: &dyn JetHL, output: &NodeOutput) -> Option<Value> {
224        match output.clone() {
225            NodeOutput::Success(mut output_frame) => {
226                let target_ty = &node.arrow().target;
227                let jet_target_ty = resolve_jet_type(&target_type(jet));
228
229                let output_value = SimValue::from_padded_bits(&mut output_frame, target_ty)
230                    .expect("output from bit machine is always well-formed");
231
232                Value::reconstruct(&StructuralValue::from(output_value), &jet_target_ty)
233            }
234            _ => None,
235        }
236    }
237
238    /// Sends a warning to the warning sink if configured.
239    fn warn(&self, message: &str) {
240        if let Some(sink) = self.warning_sink.as_ref() {
241            sink(message);
242        }
243    }
244
245    /// Handles debug node execution by resolving symbols and decoding values.
246    fn handle_debug(&mut self, node: &RedeemNode, input: &FrameIter, cmr: &simplicity::Cmr) {
247        if self.debug_sink.is_none() {
248            return;
249        }
250
251        let Some(tracked_call) = self.debug_symbols.get(cmr) else {
252            self.warn(&format!("Unknown debug symbol: CMR {cmr}"));
253            return;
254        };
255
256        let TrackedCallName::Debug(_) = tracked_call.name() else {
257            return;
258        };
259
260        let mut input_frame = input.clone();
261
262        // Skip the Case combinator's branch selection bit
263        // The reason we need to advance by a bit is that the AssertL combinator is actually a Case combinator,
264        // which takes a bit of input to decide which branch to take. But this bit is "meaningless" and
265        // is always 0 because it's an assertion.
266        let _ = input_frame.next();
267
268        // The debug call has signature `dbg!(T) -> T`, so the target type
269        // matches the value being debugged
270        let Ok(input_val) = SimValue::from_padded_bits(&mut input_frame, &node.arrow().target)
271        else {
272            self.warn(&format!("Failed to decode debug value for CMR {cmr}"));
273            return;
274        };
275
276        let Some(Either::Right(debug_value)) =
277            tracked_call.map_value(&StructuralValue::from(input_val))
278        else {
279            return;
280        };
281
282        if let Some(sink) = self.debug_sink.as_mut() {
283            sink(debug_value.text(), debug_value.value());
284        }
285    }
286}
287
288impl PruneTracker for DefaultTracker<'_> {
289    fn contains_left(&self, ihr: Ihr) -> bool {
290        if PruneTracker::contains_left(&self.inner, ihr) {
291            return true;
292        }
293
294        if let Some(sink) = self.warning_sink.as_ref() {
295            sink(&format!("Pruning unexecuted left child of IHR {ihr}"));
296        }
297
298        false
299    }
300
301    fn contains_right(&self, ihr: Ihr) -> bool {
302        if PruneTracker::contains_right(&self.inner, ihr) {
303            return true;
304        }
305
306        if let Some(sink) = self.warning_sink.as_ref() {
307            sink(&format!("Pruning unexecuted right child of IHR {ihr}"));
308        }
309
310        false
311    }
312}
313
314impl ExecTracker for DefaultTracker<'_> {
315    fn visit_node(&mut self, node: &RedeemNode, input: FrameIter, output: NodeOutput) {
316        match node.inner() {
317            Inner::Jet(jet) => {
318                if let Some(jet) = self.jet_hinter.conjure(jet.as_ref()) {
319                    self.handle_jet(node, jet.as_ref(), &input, &output);
320                } else {
321                    self.warn(&format!("Unexpected jet type for tracker: {jet:?}"));
322                }
323            }
324            Inner::AssertL(_, cmr) => self.handle_debug(node, &input, cmr),
325            _ => {}
326        }
327
328        self.inner.visit_node(node, input, output);
329    }
330}
331
332/// Parses jet input arguments from the bit machine's read frame.
333fn parse_jet_arguments(jet: &dyn JetHL, input_frame: &mut FrameIter) -> Result<Vec<Value>, String> {
334    let source_types = source_type(jet);
335    if source_types.is_empty() {
336        return Ok(vec![]);
337    }
338
339    let arguments_blob = SimValue::from_padded_bits(input_frame, &jet.source_ty().to_final())
340        .expect("input from bit machine is always well-formed");
341
342    let args = Unfolder::new(arguments_blob.as_ref(), source_types.len())
343        .unfold(|v| v.as_product())
344        .ok_or("expected product type while collecting arguments")?;
345
346    Ok(args
347        .into_iter()
348        .zip(source_types.iter())
349        .map(|(arg, aliased_type)| {
350            Value::reconstruct(&arg.to_value().into(), &resolve_jet_type(aliased_type))
351                .expect("compiled program produces correctly structured values")
352        })
353        .collect())
354}
355
356/// Resolves an aliased type to its concrete form.
357fn resolve_jet_type(aliased_type: &AliasedType) -> ResolvedType {
358    aliased_type
359        .resolve(|name: &AliasName| Err(name.clone()))
360        .expect("jet types always resolve without aliases")
361}
362
363#[cfg(test)]
364mod tests {
365    use std::cell::RefCell;
366    use std::collections::HashMap;
367    use std::rc::Rc;
368    use std::sync::Arc;
369
370    use simplicity::elements::taproot::ControlBlock;
371    use simplicity::elements::BlockHash;
372    use simplicity::elements::{self, pset::PartiallySignedTransaction};
373    use simplicity::jet::elements::{ElementsEnv, ElementsUtxo};
374    use simplicity::Cmr;
375
376    use crate::ast::ElementsJetHinter;
377    use crate::elements::confidential::Asset;
378    use crate::elements::hashes::Hash;
379    use crate::elements::pset::Input;
380    use crate::elements::{AssetId, OutPoint, Script, Txid};
381    use crate::{Arguments, TemplateProgram, WitnessValues};
382
383    use super::*;
384
385    const TEST_PROGRAM: &str = r#"
386        fn get_input_explicit_asset_amount(index: u32) -> (u256, u64) {
387            let pair: (Asset1, Amount1) = unwrap(jet::input_amount(index));
388            let (asset, amount): (Asset1, Amount1) = dbg!(pair);
389            let asset_bits: u256 = unwrap_right::<(u1, u256)>(asset);
390            let amount: u64 = unwrap_right::<(u1, u256)>(amount);
391            (asset_bits, amount)
392        }
393
394        fn main() {
395            let a: u32 = jet::num_inputs();
396            let b: bool = dbg!(jet::eq_32(20, 21));
397            let c: (u256, u64) = dbg!(get_input_explicit_asset_amount(0));
398        }
399    "#;
400
401    type DebugStore = Rc<RefCell<HashMap<String, String>>>;
402    type JetStore = Rc<RefCell<HashMap<String, (Option<Vec<String>>, Option<String>)>>>;
403
404    fn create_test_elements_tracker(
405        debug_symbols: &DebugSymbols,
406    ) -> (DefaultTracker<'_>, DebugStore, JetStore) {
407        let debug_store: DebugStore = Rc::default();
408        let jet_store: JetStore = Rc::default();
409
410        let debug_clone = debug_store.clone();
411        let jet_clone = jet_store.clone();
412
413        let tracker = DefaultTracker::build(debug_symbols, Box::new(ElementsJetHinter::new()))
414            .with_debug_sink(move |label, value| {
415                debug_clone
416                    .borrow_mut()
417                    .insert(label.to_string(), value.to_string());
418            })
419            .with_jet_trace_sink(move |jet, args, result| {
420                jet_clone.borrow_mut().insert(
421                    jet.to_string(),
422                    (
423                        args.map(|a| a.iter().map(|v| v.to_string()).collect()),
424                        result.map(|r| r.to_string()),
425                    ),
426                );
427            });
428
429        (tracker, debug_store, jet_store)
430    }
431
432    fn create_test_env() -> ElementsEnv<Arc<elements::Transaction>> {
433        let mut tx = PartiallySignedTransaction::new_v2();
434        let outpoint = OutPoint::new(Txid::from_slice(&[2; 32]).unwrap(), 33);
435        tx.add_input(Input::from_prevout(outpoint));
436
437        ElementsEnv::new(
438            Arc::new(tx.extract_tx().unwrap()),
439            vec![ElementsUtxo {
440                script_pubkey: Script::new(),
441                asset: Asset::Explicit(AssetId::LIQUID_BTC),
442                value: elements::confidential::Value::Explicit(1000),
443            }],
444            0,
445            Cmr::from_byte_array([0; 32]),
446            ControlBlock::from_slice(&[0xc0; 33]).unwrap(),
447            None,
448            BlockHash::all_zeros(),
449        )
450    }
451
452    #[test]
453    fn test_debug_and_jet_tracing() {
454        let program =
455            TemplateProgram::new(TEST_PROGRAM, Box::new(ElementsJetHinter::new())).unwrap();
456        let program = program.instantiate(Arguments::default(), true).unwrap();
457        let satisfied = program.satisfy(WitnessValues::default()).unwrap();
458
459        let (mut tracker, debug_store, jet_store) =
460            create_test_elements_tracker(&satisfied.debug_symbols);
461        let env = create_test_env();
462
463        let _ = satisfied
464            .redeem()
465            .prune_with_tracker(&env, &mut tracker)
466            .unwrap();
467
468        let debug = debug_store.borrow();
469        assert_eq!(
470            debug.get("get_input_explicit_asset_amount(0)"),
471            Some(
472                &"(0x6d521c38ec1ea15734ae22b7c46064412829c0d0579f0a713d1c04ede979026f, 1000)"
473                    .to_string()
474            ),
475        );
476        assert_eq!(
477            debug.get("pair"),
478            Some(
479                &"(Right(0x6d521c38ec1ea15734ae22b7c46064412829c0d0579f0a713d1c04ede979026f), Right(1000))"
480                    .to_string()
481            ),
482        );
483        assert_eq!(debug.get("jet::eq_32(20, 21)"), Some(&"false".to_string()));
484
485        let jets = jet_store.borrow();
486
487        assert_eq!(
488            jets.get("num_inputs").unwrap().0.as_deref(),
489            Some([].as_slice())
490        );
491        assert_eq!(jets.get("num_inputs").unwrap().1.as_deref(), Some("1"));
492
493        assert_eq!(
494            jets.get("eq_32").unwrap().0,
495            Some(vec!["20".to_string(), "21".to_string()])
496        );
497        assert_eq!(jets.get("eq_32").unwrap().1.as_deref(), Some("false"));
498
499        assert_eq!(
500            jets.get("input_amount").unwrap().0,
501            Some(vec!["0".to_string()])
502        );
503        assert_eq!(
504            jets.get("input_amount").unwrap().1.as_deref(),
505            Some("Some((Right(0x6d521c38ec1ea15734ae22b7c46064412829c0d0579f0a713d1c04ede979026f), Right(1000)))")
506        );
507    }
508    const TEST_ARITHMETIC_JETS: &str = r#"
509        fn main() {
510
511            let x: u32 = 5;
512            let y: u32 = 4;
513
514            let sum: (bool, u32) = jet::add_32(x, y);
515            let prod: u64 = jet::multiply_32(x, y);
516
517            assert!(jet::eq_64(prod, 20));
518        }
519    "#;
520
521    #[test]
522    fn test_arith_jet_trace_regression() {
523        let env = create_test_env();
524
525        let program =
526            TemplateProgram::new(TEST_ARITHMETIC_JETS, Box::new(ElementsJetHinter::new())).unwrap();
527        let program = program.instantiate(Arguments::default(), true).unwrap();
528        let satisfied = program.satisfy(WitnessValues::default()).unwrap();
529
530        let (mut tracker, _, jet_store) = create_test_elements_tracker(&satisfied.debug_symbols);
531
532        let _ = satisfied.redeem().prune_with_tracker(&env, &mut tracker);
533
534        let jets = jet_store.borrow();
535
536        assert_eq!(
537            jets.get("add_32").unwrap().0,
538            Some(vec!["5".to_string(), "4".to_string()])
539        );
540        assert_eq!(
541            jets.get("add_32").unwrap().1,
542            Some("(false, 9)".to_string())
543        );
544
545        assert_eq!(
546            jets.get("multiply_32").unwrap().0,
547            Some(vec!["5".to_string(), "4".to_string()])
548        );
549        assert_eq!(jets.get("multiply_32").unwrap().1, Some("20".to_string()));
550
551        assert_eq!(
552            jets.get("eq_64").unwrap().0,
553            Some(vec!["20".to_string(), "20".to_string()])
554        );
555        assert_eq!(jets.get("eq_64").unwrap().1, Some("true".to_string()));
556    }
557
558    const TEST_FULL_MULTIPLY_JETS: &str = r#"
559    fn main() {
560        let r8: u16 = jet::full_multiply_8(200, 201, 202, 203);
561        let r16: u32 = jet::full_multiply_16(20000, 20001, 20002, 20003);
562        let r32: u64 = jet::full_multiply_32(2000000000, 2000000001, 2000000002, 2000000003);
563        let r64: u128 = jet::full_multiply_64(2000000000, 2000000001, 2000000002, 2000000003);
564
565        assert!(jet::eq_16(r8, 40605));
566        assert!(jet::eq_32(r16, 400060005));
567        assert!(jet::eq_64(r32, 4000000006000000005));
568
569        // TODO: Currently no eq_128 jet, this must be revised in future. Placeholder to match on 'unwrap().1`.
570        let _keep: u128 = r64;
571    }
572    "#;
573
574    #[test]
575    fn test_full_multiply_jet_trace_regression() {
576        // FullMultiply -> (a * b + c + d)
577
578        let env = create_test_env();
579
580        let program =
581            TemplateProgram::new(TEST_FULL_MULTIPLY_JETS, Box::new(ElementsJetHinter::new()))
582                .unwrap();
583        let program = program.instantiate(Arguments::default(), true).unwrap();
584        let satisfied = program.satisfy(WitnessValues::default()).unwrap();
585
586        let (mut tracker, _, jet_store) = create_test_elements_tracker(&satisfied.debug_symbols);
587
588        let _ = satisfied.redeem().prune_with_tracker(&env, &mut tracker);
589
590        let jets = jet_store.borrow();
591
592        assert_eq!(
593            jets.get("full_multiply_8").unwrap().0,
594            Some(vec![
595                "200".to_string(),
596                "201".to_string(),
597                "202".to_string(),
598                "203".to_string(),
599            ])
600        );
601        assert_eq!(
602            jets.get("full_multiply_8").unwrap().1,
603            Some("40605".to_string())
604        );
605
606        assert_eq!(
607            jets.get("full_multiply_16").unwrap().0,
608            Some(vec![
609                "20000".to_string(),
610                "20001".to_string(),
611                "20002".to_string(),
612                "20003".to_string(),
613            ])
614        );
615        assert_eq!(
616            jets.get("full_multiply_16").unwrap().1,
617            Some("400060005".to_string())
618        );
619
620        assert_eq!(
621            jets.get("full_multiply_32").unwrap().0,
622            Some(vec![
623                "2000000000".to_string(),
624                "2000000001".to_string(),
625                "2000000002".to_string(),
626                "2000000003".to_string(),
627            ])
628        );
629        assert_eq!(
630            jets.get("full_multiply_32").unwrap().1,
631            Some("4000000006000000005".to_string())
632        );
633
634        assert_eq!(
635            jets.get("full_multiply_64").unwrap().0,
636            Some(vec![
637                "2000000000".to_string(),
638                "2000000001".to_string(),
639                "2000000002".to_string(),
640                "2000000003".to_string(),
641            ])
642        );
643        assert_eq!(
644            jets.get("full_multiply_64").unwrap().1,
645            // Check: u128 defaults to hex in fmt::Display for UIntValue
646            Some("0x00000000000000003782dad00330bc05".to_string()) // u128 => 4000000006000000005
647        );
648    }
649}