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.
//! 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."
);
}
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_nodeinsrc/tracker.rs.Minimal reproduction steps