Skip to content

Tracker can spuriously report "Unknown debug symbol" #421

Description

@schoen

Project version

0.7.2

Project

core

What happened?

In the runnable-snippets logic for the web site, some programs (but not all) that run successfully would display an inexplicable warning Unknown debug symbol, while still executing correctly.

I've worked around this in runnable-snippets by simply hiding this message wherever it occurs, but an AI concluded that this is a tracker bug, having to do with DefaultTracker::visit_node in src/tracker.rs.

Minimal reproduction steps

//! Standalone, narrated reproduction of a bug in `DefaultTracker`
//! (src/tracker.rs): it reports "Unknown debug symbol: CMR <hex>" for
//! programs that contain no `dbg!()` call at all, and does so
//! inconsistently -- for one of two structurally-symmetric operations, but
//! not the other -- purely depending on internal branch-canonicalization
//! details the program author has no visibility into or control over.
//!
//! Root cause: `DefaultTracker::visit_node`'s `ExecTracker` impl only
//! inspects `Inner::AssertL` nodes when deciding whether to run
//! `handle_debug`; there is no matching arm for `Inner::AssertR`. Two
//! unrelated Simplicity constructs produce untracked assert nodes of
//! exactly this shape:
//!
//!   1. `unwrap_left::<T>()` compiles via `assertl_take` into an `AssertL`
//!      carrying a fixed, deliberately-untracked sentinel CMR
//!      (`Cmr::fail(FailEntropy::ZERO)`). `unwrap()`/`unwrap_right::<T>()`
//!      compile via `assertr_take` into the very same kind of sentinel,
//!      but as an `AssertR`. (src/compile/mod.rs:406-425)
//!
//!   2. Pruning a redeemed program (`RedeemNode::prune`/
//!      `prune_with_tracker`, in the simplicity-lang dependency) collapses
//!      any single-resolution `match` into `Inner::AssertL` or
//!      `Inner::AssertR`, depending only on which side survived -- again,
//!      neither CMR is ever a tracked call.
//!
//! In both cases, the *identical kind* of untracked node is diagnosed for
//! one side and silently ignored for the other.
//!
//! No dbg!() appears anywhere in the four small programs below. Run with:
//!
//!     cargo run --example debug_symbol_warning_repro

use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;

use simplicityhl::ast::ElementsJetHinter;
use simplicityhl::elements;
use simplicityhl::elements::confidential::{Asset, Value};
use simplicityhl::elements::hashes::Hash;
use simplicityhl::elements::pset::{Input, PartiallySignedTransaction};
use simplicityhl::elements::taproot::ControlBlock;
use simplicityhl::elements::{AssetId, BlockHash, LockTime, OutPoint, Script, Sequence, Txid};
use simplicityhl::tracker::DefaultTracker;
use simplicityhl::{Arguments, TemplateAst, WitnessValues};

type ElementsEnv = simplicity::jet::elements::ElementsEnv<Arc<elements::Transaction>>;

fn env_with_lock_time(lock_time: LockTime) -> ElementsEnv {
    simplicityhl::dummy_env::dummy_with(lock_time, Sequence::MAX, false)
}

/// A UTXO with an explicit (non-null) asset and value, so that
/// `jet::input_amount(0)` deterministically returns `Some(..)`.
fn env_with_explicit_input_amount() -> ElementsEnv {
    let mut tx = PartiallySignedTransaction::new_v2();
    let outpoint = OutPoint::new(Txid::from_slice(&[2; 32]).unwrap(), 33);
    tx.add_input(Input::from_prevout(outpoint));

    simplicity::jet::elements::ElementsEnv::new(
        Arc::new(tx.extract_tx().unwrap()),
        vec![simplicity::jet::elements::ElementsUtxo {
            script_pubkey: Script::new(),
            asset: Asset::Explicit(AssetId::LIQUID_BTC),
            value: Value::Explicit(1000),
        }],
        0,
        simplicity::Cmr::from_byte_array([0; 32]),
        ControlBlock::from_slice(&[0xc0; 33]).unwrap(),
        None,
        BlockHash::all_zeros(),
    )
}

fn run_unpruned_and_collect_warnings(source: &str, env: &ElementsEnv) -> Vec<String> {
    let program = TemplateAst::new(source, Box::new(ElementsJetHinter::new())).unwrap();
    let compiled = program.instantiate(Arguments::default(), true).unwrap();
    let satisfied = compiled.satisfy(WitnessValues::default()).unwrap();

    let warnings: Rc<RefCell<Vec<String>>> = Rc::default();
    let warnings_clone = warnings.clone();
    let mut tracker = DefaultTracker::build(
        satisfied.debug_symbols(),
        Box::new(ElementsJetHinter::new()),
    )
    .with_debug_sink(|_, _| {})
    .with_warning_sink(move |msg| warnings_clone.borrow_mut().push(msg.to_string()));

    satisfied
        .redeem()
        .prune_with_tracker(env, &mut tracker)
        .unwrap();

    let result = warnings.borrow().clone();
    result
}

fn run_pruned_and_collect_warnings(source: &str, env: &ElementsEnv) -> Vec<String> {
    let program = TemplateAst::new(source, Box::new(ElementsJetHinter::new())).unwrap();
    let compiled = program.instantiate(Arguments::default(), true).unwrap();

    // Satisfy *with* an environment: this prunes the tree, collapsing the
    // single-resolution match into an AssertL/AssertR node up front.
    let satisfied = compiled
        .satisfy_with_env(WitnessValues::default(), Some(env))
        .unwrap();

    let warnings: Rc<RefCell<Vec<String>>> = Rc::default();
    let warnings_clone = warnings.clone();
    let mut tracker = DefaultTracker::build(
        satisfied.debug_symbols(),
        Box::new(ElementsJetHinter::new()),
    )
    .with_debug_sink(|_, _| {})
    .with_warning_sink(move |msg| warnings_clone.borrow_mut().push(msg.to_string()));

    // Interpret the already-pruned tree again, as a harness would to
    // collect an execution trace for display.
    satisfied
        .redeem()
        .prune_with_tracker(env, &mut tracker)
        .unwrap();

    let result = warnings.borrow().clone();
    result
}

fn report(label: &str, source: &str, warnings: &[String]) {
    println!("--- {label} ---");
    println!("{}", source.trim());
    if warnings.is_empty() {
        println!(">> No warnings. (clean)");
    } else {
        println!(">> Warnings emitted:");
        for w in warnings {
            println!("   {w}");
        }
    }
    println!();
}

fn main() {
    println!(
        "Reproducing a DefaultTracker bug: spurious \"Unknown debug symbol\" \
         warnings with no dbg!() anywhere, appearing for one of two \
         structurally-symmetric operations but not the other.\n"
    );

    // --- Mechanism 1: unwrap_left vs. unwrap_right --------------------
    println!("=== Mechanism 1: unwrap_left vs. unwrap_right ===\n");

    const UNWRAP_LEFT_ONLY: &str = r#"
        fn main() {
            let x: Either<Height, Time> = jet::parse_lock(jet::lock_time());
            let _h: Height = unwrap_left::<Time>(x);
        }
    "#;
    const UNWRAP_RIGHT_ONLY: &str = r#"
        fn main() {
            let x: Either<Height, Time> = jet::parse_lock(jet::lock_time());
            let _t: Time = unwrap_right::<Height>(x);
        }
    "#;

    // lock_time = 0 -> Left(Height(0)), so `unwrap_left` succeeds.
    let left_warnings =
        run_unpruned_and_collect_warnings(UNWRAP_LEFT_ONLY, &env_with_lock_time(LockTime::ZERO));
    report("unwrap_left::<Time>(x)", UNWRAP_LEFT_ONLY, &left_warnings);

    // lock_time = 600_000_000 -> Right(Time(600000000)), so `unwrap_right` succeeds.
    let right_warnings = run_unpruned_and_collect_warnings(
        UNWRAP_RIGHT_ONLY,
        &env_with_lock_time(LockTime::from_consensus(600_000_000)),
    );
    report(
        "unwrap_right::<Height>(x)  (same operation, opposite side)",
        UNWRAP_RIGHT_ONLY,
        &right_warnings,
    );

    // --- Mechanism 2: pruned match arms --------------------------------
    println!("=== Mechanism 2: which side of a pruned match survives ===\n");

    const BOOL_MATCH_TAKES_LEFT_ARM: &str = r#"
        fn main() {
            let bit_flip: bool = match false {
                false => true,
                true => false,
            };
            assert!(bit_flip);
        }
    "#;
    const OPTION_MATCH_TAKES_RIGHT_ARM: &str = r#"
        fn main() {
            match jet::input_amount(0) {
                None => {},
                Some(_pair: (Asset1, Amount1)) => {},
            }
        }
    "#;

    let input_amount_env = env_with_explicit_input_amount();

    let bool_warnings =
        run_pruned_and_collect_warnings(BOOL_MATCH_TAKES_LEFT_ARM, &input_amount_env);
    report(
        "match false { .. }  (resolves to its canonically-left arm)",
        BOOL_MATCH_TAKES_LEFT_ARM,
        &bool_warnings,
    );

    let option_warnings =
        run_pruned_and_collect_warnings(OPTION_MATCH_TAKES_RIGHT_ARM, &input_amount_env);
    report(
        "match jet::input_amount(0) { .. }  (resolves to its canonically-right arm)",
        OPTION_MATCH_TAKES_RIGHT_ARM,
        &option_warnings,
    );

    println!(
        "Conclusion: none of the four programs above contain dbg!(). Yet \
         \"unwrap_left\" and the bool match both spuriously warn, while their \
         structural twins (\"unwrap_right\" and the Option match) stay silent. \
         See DefaultTracker::visit_node in src/tracker.rs: it only inspects \
         Inner::AssertL, never Inner::AssertR."
    );
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions