From bb1359d2f1d7968567c66bfc08d86a972abf38f5 Mon Sep 17 00:00:00 2001 From: Christian Lewe Date: Mon, 16 Sep 2024 18:05:20 +0200 Subject: [PATCH 1/8] chore: Update simplicity in integration tests --- bitcoind-tests/Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bitcoind-tests/Cargo.lock b/bitcoind-tests/Cargo.lock index 2cc7d47..61ab274 100644 --- a/bitcoind-tests/Cargo.lock +++ b/bitcoind-tests/Cargo.lock @@ -769,7 +769,7 @@ dependencies = [ [[package]] name = "simplicity-lang" version = "0.2.0" -source = "git+https://github.com/BlockstreamResearch/rust-simplicity?rev=794918783291465a109bdaf2ef694f86467c477e#794918783291465a109bdaf2ef694f86467c477e" +source = "git+https://github.com/BlockstreamResearch/rust-simplicity?rev=713842937ab0b5e0b1a343e19fdee6634b7a6add#713842937ab0b5e0b1a343e19fdee6634b7a6add" dependencies = [ "bitcoin 0.31.2", "bitcoin_hashes 0.13.0", @@ -785,7 +785,7 @@ dependencies = [ [[package]] name = "simplicity-sys" version = "0.2.0" -source = "git+https://github.com/BlockstreamResearch/rust-simplicity?rev=794918783291465a109bdaf2ef694f86467c477e#794918783291465a109bdaf2ef694f86467c477e" +source = "git+https://github.com/BlockstreamResearch/rust-simplicity?rev=713842937ab0b5e0b1a343e19fdee6634b7a6add#713842937ab0b5e0b1a343e19fdee6634b7a6add" dependencies = [ "bitcoin_hashes 0.13.0", "cc", From 5ef47f944c7f30b0fff02b659c4ffc2b0d7ae03e Mon Sep 17 00:00:00 2001 From: Christian Lewe Date: Thu, 12 Sep 2024 23:25:37 +0200 Subject: [PATCH 2/8] refactor: Call expression analysis Introduce a helper method for each step of the analysis. Analyze each call variant in terms of the steps. This refactor makes the code easier to read. --- src/ast.rs | 247 +++++++++++++++++++++++------------------------------ 1 file changed, 109 insertions(+), 138 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index 5f6fd27..83ec496 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -945,193 +945,164 @@ impl AbstractSyntaxTree for Call { type From = parse::Call; fn analyze(from: &Self::From, ty: &ResolvedType, scope: &mut Scope) -> Result { - let name = CallName::analyze(from, ty, scope)?; + fn check_argument_types( + parse_args: &[parse::Expression], + expected_tys: &[ResolvedType], + ) -> Result<(), Error> { + if parse_args.len() != expected_tys.len() { + Err(Error::InvalidNumberOfArguments( + expected_tys.len(), + parse_args.len(), + )) + } else { + Ok(()) + } + } + + fn check_output_type( + observed_ty: &ResolvedType, + expected_ty: &ResolvedType, + ) -> Result<(), Error> { + if observed_ty != expected_ty { + Err(Error::ExpressionTypeMismatch( + expected_ty.clone(), + observed_ty.clone(), + )) + } else { + Ok(()) + } + } + fn analyze_arguments( + parse_args: &[parse::Expression], + args_tys: &[ResolvedType], + scope: &mut Scope, + ) -> Result, RichError> { + let args = parse_args + .iter() + .zip(args_tys.iter()) + .map(|(arg_parse, arg_ty)| Expression::analyze(arg_parse, arg_ty, scope)) + .collect::, RichError>>()?; + Ok(args) + } + + let name = CallName::analyze(from, ty, scope)?; let args = match name.clone() { CallName::Jet(jet) => { - let args_ty = crate::jet::source_type(jet) + let args_tys = crate::jet::source_type(jet) .iter() .map(AliasedType::resolve_builtin) .collect::, Identifier>>() .map_err(Error::UndefinedAlias) .with_span(from)?; - if from.args().len() != args_ty.len() { - return Err(Error::InvalidNumberOfArguments( - args_ty.len(), - from.args().len(), - )) - .with_span(from); - } + check_argument_types(from.args(), &args_tys).with_span(from)?; let out_ty = crate::jet::target_type(jet) .resolve_builtin() .map_err(Error::UndefinedAlias) .with_span(from)?; - if ty != &out_ty { - return Err(Error::ExpressionTypeMismatch(ty.clone(), out_ty)).with_span(from); - } - from.args() - .iter() - .zip(args_ty.iter()) - .map(|(arg_parse, arg_ty)| Expression::analyze(arg_parse, arg_ty, scope)) - .collect::, RichError>>()? + check_output_type(&out_ty, ty).with_span(from)?; + analyze_arguments(from.args(), &args_tys, scope)? } CallName::UnwrapLeft(right_ty) => { - let args_ty = ResolvedType::either(ty.clone(), right_ty); - if from.args().len() != 1 { - return Err(Error::InvalidNumberOfArguments(1, from.args().len())) - .with_span(from); - } - Arc::from([Expression::analyze( - from.args().first().unwrap(), - &args_ty, - scope, - )?]) + let args_tys = [ResolvedType::either(ty.clone(), right_ty)]; + check_argument_types(from.args(), &args_tys).with_span(from)?; + analyze_arguments(from.args(), &args_tys, scope)? } CallName::UnwrapRight(left_ty) => { - let args_ty = ResolvedType::either(left_ty, ty.clone()); - if from.args().len() != 1 { - return Err(Error::InvalidNumberOfArguments(1, from.args().len())) - .with_span(from); - } - Arc::from([Expression::analyze( - from.args().first().unwrap(), - &args_ty, - scope, - )?]) + let args_tys = [ResolvedType::either(left_ty, ty.clone())]; + check_argument_types(from.args(), &args_tys).with_span(from)?; + analyze_arguments(from.args(), &args_tys, scope)? } CallName::IsNone(some_ty) => { - if from.args().len() != 1 { - return Err(Error::InvalidNumberOfArguments(1, from.args().len())) - .with_span(from); - } + let args_tys = [ResolvedType::option(some_ty)]; + check_argument_types(from.args(), &args_tys).with_span(from)?; let out_ty = ResolvedType::boolean(); - if ty != &out_ty { - return Err(Error::ExpressionTypeMismatch(ty.clone(), out_ty)).with_span(from); - } - let arg_ty = ResolvedType::option(some_ty); - Arc::from([Expression::analyze( - from.args().first().unwrap(), - &arg_ty, - scope, - )?]) + check_output_type(&out_ty, ty).with_span(from)?; + analyze_arguments(from.args(), &args_tys, scope)? } CallName::Unwrap => { - let args_ty = ResolvedType::option(ty.clone()); - if from.args().len() != 1 { - return Err(Error::InvalidNumberOfArguments(1, from.args().len())) - .with_span(from); - } - Arc::from([Expression::analyze( - from.args().first().unwrap(), - &args_ty, - scope, - )?]) + let args_tys = [ResolvedType::option(ty.clone())]; + check_argument_types(from.args(), &args_tys).with_span(from)?; + analyze_arguments(from.args(), &args_tys, scope)? } CallName::Assert => { - if from.args().len() != 1 { - return Err(Error::InvalidNumberOfArguments(1, from.args().len())) - .with_span(from); - } - if !ty.is_unit() { - return Err(Error::ExpressionTypeMismatch( - ty.clone(), - ResolvedType::unit(), - )) - .with_span(from); - } - let arg_type = ResolvedType::boolean(); - Arc::from([Expression::analyze( - from.args().first().unwrap(), - &arg_type, - scope, - )?]) + let args_tys = [ResolvedType::boolean()]; + check_argument_types(from.args(), &args_tys).with_span(from)?; + let out_ty = ResolvedType::unit(); + check_output_type(&out_ty, ty).with_span(from)?; + analyze_arguments(from.args(), &args_tys, scope)? } CallName::Panic => { - if !from.args().is_empty() { - return Err(Error::InvalidNumberOfArguments(0, from.args().len())) - .with_span(from); - } + let args_tys = []; + check_argument_types(from.args(), &args_tys).with_span(from)?; // panic! allows every output type because it will never return anything - Arc::from([]) + analyze_arguments(from.args(), &args_tys, scope)? } CallName::TypeCast(source) => { - if from.args().len() != 1 { - return Err(Error::InvalidNumberOfArguments(1, from.args().len())) - .with_span(from); - } if StructuralType::from(&source) != StructuralType::from(ty) { return Err(Error::InvalidCast(source, ty.clone())).with_span(from); } - Arc::from([Expression::analyze( - from.args().first().unwrap(), - &source, - scope, - )?]) + + let args_tys = [source]; + check_argument_types(from.args(), &args_tys).with_span(from)?; + analyze_arguments(from.args(), &args_tys, scope)? } CallName::Custom(function) => { - if from.args().len() != function.params().len() { - return Err(Error::InvalidNumberOfArguments( - function.params().len(), - from.args().len(), - )) - .with_span(from); - } - let out_ty = function.body().ty(); - if ty != out_ty { - return Err(Error::ExpressionTypeMismatch(ty.clone(), out_ty.clone())) - .with_span(from); - } - from.args() + let args_ty = function + .params() .iter() - .zip(function.params.iter().map(FunctionParam::ty)) - .map(|(arg_parse, arg_ty)| Expression::analyze(arg_parse, arg_ty, scope)) - .collect::, RichError>>()? + .map(FunctionParam::ty) + .cloned() + .collect::>(); + check_argument_types(from.args(), &args_ty).with_span(from)?; + let out_ty = function.body().ty(); + check_output_type(out_ty, ty).with_span(from)?; + analyze_arguments(from.args(), &args_ty, scope)? } CallName::Fold(function, bound) => { - if from.args().len() != 2 { - return Err(Error::InvalidNumberOfArguments(2, from.args().len())) - .with_span(from); - } - let out_ty = function.body().ty(); - if ty != out_ty { - return Err(Error::ExpressionTypeMismatch(ty.clone(), out_ty.clone())) - .with_span(from); - } // A list fold has the signature: // fold::(list: List, initial_accumulator: A) -> A // where // fn f(element: E, accumulator: A) -> A let element_ty = function.params().first().expect("foldable function").ty(); let list_ty = ResolvedType::list(element_ty.clone(), bound); - let accumulator_ty = function.params().get(1).expect("foldable function").ty(); - from.args() - .iter() - .zip([&list_ty, accumulator_ty]) - .map(|(arg_parse, arg_ty)| Expression::analyze(arg_parse, arg_ty, scope)) - .collect::, RichError>>()? + let accumulator_ty = function + .params() + .get(1) + .expect("foldable function") + .ty() + .clone(); + let args_ty = [list_ty, accumulator_ty]; + + check_argument_types(from.args(), &args_ty).with_span(from)?; + let out_ty = function.body().ty(); + check_output_type(out_ty, ty).with_span(from)?; + analyze_arguments(from.args(), &args_ty, scope)? } CallName::ForWhile(function, _bit_width) => { - if from.args().len() != 2 { - return Err(Error::InvalidNumberOfArguments(2, from.args().len())) - .with_span(from); - } - let out_ty = function.body().ty(); - if ty != out_ty { - return Err(Error::ExpressionTypeMismatch(ty.clone(), out_ty.clone())) - .with_span(from); - } // A for-while loop has the signature: // for_while::(initial_accumulator: A, readonly_context: C) -> Either // where // fn f(accumulator: A, readonly_context: C, counter: u{N}) -> Either // N is a power of two - let accumulator_ty = function.params().first().expect("loopable function").ty(); - let context_ty = function.params().get(1).expect("loopable function").ty(); - from.args() - .iter() - .zip([accumulator_ty, context_ty]) - .map(|(arg_parse, arg_ty)| Expression::analyze(arg_parse, arg_ty, scope)) - .collect::, RichError>>()? + let accumulator_ty = function + .params() + .first() + .expect("loopable function") + .ty() + .clone(); + let context_ty = function + .params() + .get(1) + .expect("loopable function") + .ty() + .clone(); + let args_ty = [accumulator_ty, context_ty]; + + check_argument_types(from.args(), &args_ty).with_span(from)?; + let out_ty = function.body().ty(); + check_output_type(out_ty, ty).with_span(from)?; + analyze_arguments(from.args(), &args_ty, scope)? } }; From c749a7901322c6590b93bd7bf9d4f41a657354cc Mon Sep 17 00:00:00 2001 From: Christian Lewe Date: Mon, 16 Sep 2024 17:28:53 +0200 Subject: [PATCH 3/8] feat: Display u128 and u256 as hex I find it much easier to read hex arrays than long decimal strings. --- src/value.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/value.rs b/src/value.rs index 5e8f3b3..5a778dc 100644 --- a/src/value.rs +++ b/src/value.rs @@ -47,6 +47,8 @@ impl fmt::Debug for UIntValue { impl fmt::Display for UIntValue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use hex_conservative::DisplayHex; + match self { UIntValue::U1(n) => ::fmt(n, f), UIntValue::U2(n) => ::fmt(n, f), @@ -55,8 +57,8 @@ impl fmt::Display for UIntValue { UIntValue::U16(n) => ::fmt(n, f), UIntValue::U32(n) => ::fmt(n, f), UIntValue::U64(n) => ::fmt(n, f), - UIntValue::U128(n) => ::fmt(n, f), - UIntValue::U256(n) => ::fmt(n, f), + UIntValue::U128(n) => write!(f, "{}", n.to_be_bytes().as_hex()), + UIntValue::U256(n) => write!(f, "{}", n.as_ref().as_hex()), } } } From be4bc84a4947d670c6b449d2743dc42016975bbe Mon Sep 17 00:00:00 2001 From: Christian Lewe Date: Fri, 13 Sep 2024 16:02:19 +0200 Subject: [PATCH 4/8] feat: Convert Simplicity -> Simfony --- src/types.rs | 6 ++++++ src/value.rs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/types.rs b/src/types.rs index 6f25993..b3c1135 100644 --- a/src/types.rs +++ b/src/types.rs @@ -923,6 +923,12 @@ impl From for Arc { } } +impl From> for StructuralType { + fn from(value: Arc) -> Self { + Self(value) + } +} + impl TreeLike for StructuralType { fn as_node(&self) -> Tree { match self.0.bound() { diff --git a/src/value.rs b/src/value.rs index 5a778dc..f6aee14 100644 --- a/src/value.rs +++ b/src/value.rs @@ -824,6 +824,12 @@ impl From for SimValue { } } +impl From for StructuralValue { + fn from(value: SimValue) -> Self { + Self(value) + } +} + impl TreeLike for StructuralValue { fn as_node(&self) -> Tree { use simplicity::dag::{Dag, DagLike}; From 438c5c7ef8a650097380044028de7f8d49f6813d Mon Sep 17 00:00:00 2001 From: Christian Lewe Date: Mon, 16 Sep 2024 17:16:51 +0200 Subject: [PATCH 5/8] feat: Add debug symbols A Simfony function call f(x) is usually compiled as the following Simplicity expression, where [x] is the compilation of x, and so on. comp [x] [f] However, we can write a different Simplicity target expression that has the same semantics. The input is paired with a false bit. This is piped into an assertion that checks if the bit is false. The assertion always succeeds. comp (pair false [x]) (assertl (drop [f]) CMR) The lower expression is a more convoluted way of writing the upper expression. The benefit of the lower expression is that it includes a CMR that we can use to inject arbitrary data. We use the hash of the position of the call inside the original Simfony source code as CMR. This is the debug symbol. For now, I insert debug symbols into every call that is fallible. Any failure on the Simplicity Bit Machine can be associated with a Simfony call expression via the CMR. Currently, debug symbols are inserted into every compiled program. We don't want programs bloated with debug symbols on the blockchain. It is easy to add a "release" mode next to a "debug" mode to the Simfony compiler. For brevity, I chose to leave this for later. Note that I had to compile unwraps with slightly more combinators in order to fit the shape comp [function args] [function body]. I plan to revert this change in the "release" mod of the Simfony compiler. --- src/ast.rs | 39 +++++++++++++++++++++++++++-------- src/compile.rs | 56 +++++++++++++++++++++++++++++++++++++------------- src/debug.rs | 12 +++++++++++ src/error.rs | 14 ++++++++++++- src/lib.rs | 1 + 5 files changed, 99 insertions(+), 23 deletions(-) create mode 100644 src/debug.rs diff --git a/src/ast.rs b/src/ast.rs index 83ec496..ec43b3c 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -7,6 +7,7 @@ use either::Either; use miniscript::iter::{Tree, TreeLike}; use simplicity::jet::Elements; +use crate::debug::TrackedCallName; use crate::error::{Error, RichError, Span, WithSpan}; use crate::num::{NonZeroPow2Usize, Pow2Usize}; use crate::parse::MatchPattern; @@ -37,6 +38,7 @@ impl DeclaredWitnesses { pub struct Program { main: Expression, witnesses: DeclaredWitnesses, + tracked_calls: Vec<(Span, TrackedCallName)>, } impl Program { @@ -431,6 +433,7 @@ struct Scope { witnesses: HashMap, functions: HashMap, is_main: bool, + tracked_calls: Vec<(Span, TrackedCallName)>, } impl Scope { @@ -544,11 +547,12 @@ impl Scope { } } - /// Consume the scope and return the map of witness names to their expected type. + /// Consume the scope and return its contents: /// - /// Use this map to finalize the Simfony program with witness values of the same type. - pub fn into_witnesses(self) -> HashMap { - self.witnesses + /// 1. The map that assigns witness names to their expected type. + /// 2. The list of tracked function calls. + pub fn destruct(self) -> (DeclaredWitnesses, Vec<(Span, TrackedCallName)>) { + (DeclaredWitnesses(self.witnesses), self.tracked_calls) } /// Insert a custom function into the global map. @@ -574,6 +578,11 @@ impl Scope { pub fn get_function(&self, name: &FunctionName) -> Option<&CustomFunction> { self.functions.get(name) } + + /// Track a call expression with its span. + pub fn track_call>(&mut self, span: &S, name: TrackedCallName) { + self.tracked_calls.push((*span.as_ref(), name)); + } } /// Part of the abstract syntax tree that can be generated from a precursor in the parse tree. @@ -599,7 +608,7 @@ impl Program { .map(|s| Item::analyze(s, &unit, &mut scope)) .collect::, RichError>>()?; debug_assert!(scope.is_topmost()); - let witnesses = DeclaredWitnesses(scope.into_witnesses()); + let (witnesses, tracked_calls) = scope.destruct(); let mut mains = items .into_iter() @@ -617,7 +626,11 @@ impl Program { } }; - Ok(Self { main, witnesses }) + Ok(Self { + main, + witnesses, + tracked_calls, + }) } } @@ -1001,17 +1014,24 @@ impl AbstractSyntaxTree for Call { .map_err(Error::UndefinedAlias) .with_span(from)?; check_output_type(&out_ty, ty).with_span(from)?; + scope.track_call(from, TrackedCallName::Jet); analyze_arguments(from.args(), &args_tys, scope)? } CallName::UnwrapLeft(right_ty) => { let args_tys = [ResolvedType::either(ty.clone(), right_ty)]; check_argument_types(from.args(), &args_tys).with_span(from)?; - analyze_arguments(from.args(), &args_tys, scope)? + let args = analyze_arguments(from.args(), &args_tys, scope)?; + let [arg_ty] = args_tys; + scope.track_call(from, TrackedCallName::UnwrapLeft(arg_ty)); + args } CallName::UnwrapRight(left_ty) => { let args_tys = [ResolvedType::either(left_ty, ty.clone())]; check_argument_types(from.args(), &args_tys).with_span(from)?; - analyze_arguments(from.args(), &args_tys, scope)? + let args = analyze_arguments(from.args(), &args_tys, scope)?; + let [arg_ty] = args_tys; + scope.track_call(from, TrackedCallName::UnwrapRight(arg_ty)); + args } CallName::IsNone(some_ty) => { let args_tys = [ResolvedType::option(some_ty)]; @@ -1023,6 +1043,7 @@ impl AbstractSyntaxTree for Call { CallName::Unwrap => { let args_tys = [ResolvedType::option(ty.clone())]; check_argument_types(from.args(), &args_tys).with_span(from)?; + scope.track_call(from, TrackedCallName::Unwrap); analyze_arguments(from.args(), &args_tys, scope)? } CallName::Assert => { @@ -1030,12 +1051,14 @@ impl AbstractSyntaxTree for Call { check_argument_types(from.args(), &args_tys).with_span(from)?; let out_ty = ResolvedType::unit(); check_output_type(&out_ty, ty).with_span(from)?; + scope.track_call(from, TrackedCallName::Assert); analyze_arguments(from.args(), &args_tys, scope)? } CallName::Panic => { let args_tys = []; check_argument_types(from.args(), &args_tys).with_span(from)?; // panic! allows every output type because it will never return anything + scope.track_call(from, TrackedCallName::Panic); analyze_arguments(from.args(), &args_tys, scope)? } CallName::TypeCast(source) => { diff --git a/src/compile.rs b/src/compile.rs index 199b665..269490d 100644 --- a/src/compile.rs +++ b/src/compile.rs @@ -12,7 +12,7 @@ use crate::ast::{ Call, CallName, Expression, ExpressionInner, Match, Program, SingleExpression, SingleExpressionInner, Statement, }; -use crate::error::{Error, RichError, WithSpan}; +use crate::error::{Error, RichError, Span, WithSpan}; use crate::named::{CoreExt, PairBuilder}; use crate::num::{NonZeroPow2Usize, Pow2Usize}; use crate::pattern::{BasePattern, Pattern}; @@ -286,35 +286,63 @@ impl Call { let args_ast = SingleExpression::tuple(self.args().clone(), *self.as_ref()); let args = args_ast.compile(scope)?; + // Attach a debug symbol to the function body. + // This debug symbol can be used by the Simplicity runtime to print the call arguments + // during execution. + // + // The debug symbol is attached in such a way that a Simplicity runtime without support + // for debug symbols will simply ignore it. The semantics of the program remain unchanged. + fn with_debug_symbol>( + args: PairBuilder, + body: &ProgNode, + scope: &mut Scope, + span: &S, + ) -> Result, RichError> { + let false_and_args = ProgNode::bit(scope.ctx(), false).pair(args); + let nop_assert = ProgNode::assertl_drop(body, span.as_ref().cmr()); + false_and_args.comp(&nop_assert).with_span(span) + } + match self.name() { CallName::Jet(name) => { let jet = ProgNode::jet(scope.ctx(), *name); - args.comp(&jet).with_span(self) + with_debug_symbol(args, &jet, scope, self) } CallName::UnwrapLeft(..) => { - let left_and_unit = args.pair(PairBuilder::unit(scope.ctx())); - let fail_cmr = Cmr::fail(FailEntropy::ZERO); - let get_inner = ProgNode::assertl_take(&ProgNode::iden(scope.ctx()), fail_cmr); - left_and_unit.comp(&get_inner).with_span(self) + let input_and_unit = + PairBuilder::iden(scope.ctx()).pair(PairBuilder::unit(scope.ctx())); + let extract_inner = ProgNode::assertl_take( + &ProgNode::iden(scope.ctx()), + Cmr::fail(FailEntropy::ZERO), + ); + let body = input_and_unit.comp(&extract_inner).with_span(self)?; + with_debug_symbol(args, body.as_ref(), scope, self) } CallName::UnwrapRight(..) | CallName::Unwrap => { - let right_and_unit = args.pair(PairBuilder::unit(scope.ctx())); - let fail_cmr = Cmr::fail(FailEntropy::ZERO); - let get_inner = ProgNode::assertr_take(fail_cmr, &ProgNode::iden(scope.ctx())); - right_and_unit.comp(&get_inner).with_span(self) + let input_and_unit = + PairBuilder::iden(scope.ctx()).pair(PairBuilder::unit(scope.ctx())); + let extract_inner = ProgNode::assertr_take( + Cmr::fail(FailEntropy::ZERO), + &ProgNode::iden(scope.ctx()), + ); + let body = input_and_unit.comp(&extract_inner).with_span(self)?; + with_debug_symbol(args, body.as_ref(), scope, self) } CallName::IsNone(..) => { - let sum_and_unit = args.pair(PairBuilder::unit(scope.ctx())); + let input_and_unit = + PairBuilder::iden(scope.ctx()).pair(PairBuilder::unit(scope.ctx())); let is_right = ProgNode::case_true_false(scope.ctx()); - sum_and_unit.comp(&is_right).with_span(self) + let body = input_and_unit.comp(&is_right).with_span(self)?; + args.comp(&body).with_span(self) } CallName::Assert => { let jet = ProgNode::jet(scope.ctx(), Elements::Verify); - args.comp(&jet).with_span(self) + with_debug_symbol(args, &jet, scope, self) } CallName::Panic => { // panic! ignores its arguments - Ok(PairBuilder::fail(scope.ctx(), FailEntropy::ZERO)) + let fail = ProgNode::fail(scope.ctx(), FailEntropy::ZERO); + with_debug_symbol(args, &fail, scope, self) } CallName::TypeCast(..) => { // A cast converts between two structurally equal types. diff --git a/src/debug.rs b/src/debug.rs new file mode 100644 index 0000000..3eb8dea --- /dev/null +++ b/src/debug.rs @@ -0,0 +1,12 @@ +use crate::types::ResolvedType; + +/// Name of a call expression with a debug symbol. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum TrackedCallName { + Assert, + Panic, + Jet, + UnwrapLeft(ResolvedType), + UnwrapRight(ResolvedType), + Unwrap, +} diff --git a/src/error.rs b/src/error.rs index aaec590..dd09e16 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,7 +2,8 @@ use std::fmt; use std::num::NonZeroUsize; use std::sync::Arc; -use simplicity::elements; +use simplicity::hashes::{sha256, Hash, HashEngine}; +use simplicity::{elements, Cmr}; use crate::parse::{MatchPattern, Rule}; use crate::str::{FunctionName, Identifier, JetName, WitnessName}; @@ -89,6 +90,17 @@ impl Span { pub const fn is_multiline(&self) -> bool { self.start.line.get() < self.end.line.get() } + + /// Return the CMR of the span. + pub fn cmr(&self) -> Cmr { + let mut hasher = sha256::HashEngine::default(); + hasher.input(&self.start.line.get().to_be_bytes()); + hasher.input(&self.start.col.get().to_be_bytes()); + hasher.input(&self.end.line.get().to_be_bytes()); + hasher.input(&self.end.col.get().to_be_bytes()); + let hash = sha256::Hash::from_engine(hasher); + Cmr::from_byte_array(hash.to_byte_array()) + } } impl<'a> From<&'a pest::iterators::Pair<'_, Rule>> for Span { diff --git a/src/lib.rs b/src/lib.rs index 3d6a97b..0d33c24 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ pub type ProgNode = Arc; pub mod array; pub mod ast; pub mod compile; +pub mod debug; pub mod dummy_env; pub mod error; pub mod jet; From 0ff3f2c091f71603dc243f1a988ebcdc1583575b Mon Sep 17 00:00:00 2001 From: Christian Lewe Date: Mon, 16 Sep 2024 17:17:06 +0200 Subject: [PATCH 6/8] feat: Add public API for debug symbols We want to use debug symbols to produce helpful error messages on the Bit Machine. The debug symbols cover all cases where the Simplicity target code may fail (assertl, assertr, fail). We want to produce error messages of the following kind: Assertion failed: false Called `unwrap()` on a `None` value Called `unwrap_left()` on a `Right` value: 1 Called `unwrap_right()` on a `Left` value: 1 Some errors display the input value (unwrap_left, unwrap_right); some errors are always the same (assert, unwrap). TrackedCallName includes type information to reconstruct the input value at runtime from the Bit Machine. FallibleCallName includes the reconstructed value. TrackedCall and FallibleCall include the Simfony text of the call expression, so we can print it alongside the error. We might choose to include the span, too, in the future, depending on where we want to go with this design. --- src/ast.rs | 11 ++++- src/debug.rs | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 28 ++++++++++++ 3 files changed, 162 insertions(+), 1 deletion(-) diff --git a/src/ast.rs b/src/ast.rs index ec43b3c..ee9d9cb 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -7,7 +7,7 @@ use either::Either; use miniscript::iter::{Tree, TreeLike}; use simplicity::jet::Elements; -use crate::debug::TrackedCallName; +use crate::debug::{DebugSymbols, TrackedCallName}; use crate::error::{Error, RichError, Span, WithSpan}; use crate::num::{NonZeroPow2Usize, Pow2Usize}; use crate::parse::MatchPattern; @@ -53,6 +53,15 @@ impl Program { pub fn witnesses(&self) -> &DeclaredWitnesses { &self.witnesses } + + /// Access the debug symbols of the program. + pub fn debug_symbols(&self, file: &str) -> DebugSymbols { + let mut debug_symbols = DebugSymbols::default(); + for (span, name) in self.tracked_calls.clone() { + debug_symbols.insert(span, name, file); + } + debug_symbols + } } /// An item is a component of a program. diff --git a/src/debug.rs b/src/debug.rs index 3eb8dea..d6fde5d 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -1,4 +1,24 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use simplicity::Cmr; + +use crate::error::Span; use crate::types::ResolvedType; +use crate::value::{StructuralValue, Value}; + +/// Tracker of Simfony call expressions inside Simplicity target code. +/// +/// Tracking happens via CMRs that are inserted into the Simplicity target code. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct DebugSymbols(HashMap); + +/// Call expression with a debug symbol. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct TrackedCall { + text: Arc, + name: TrackedCallName, +} /// Name of a call expression with a debug symbol. #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -10,3 +30,107 @@ pub enum TrackedCallName { UnwrapRight(ResolvedType), Unwrap, } + +/// Fallible call expression with runtime input value. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct FallibleCall { + text: Arc, + name: FallibleCallName, +} + +/// Name of a fallible call expression with runtime input value. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum FallibleCallName { + Assert, + Panic, + Jet, + UnwrapLeft(Value), + UnwrapRight(Value), + Unwrap, +} + +impl DebugSymbols { + /// Insert a tracked call expression. + /// Use the Simfony source `file` to extract the Simfony text of the expression. + pub(crate) fn insert(&mut self, span: Span, name: TrackedCallName, file: &str) { + let cmr = span.cmr(); + let text = remove_excess_whitespace(span.to_slice(file).unwrap_or("")); + self.0.insert( + cmr, + TrackedCall { + text: Arc::from(text), + name, + }, + ); + } + + /// Check if the given CMR tracks any call expressions. + pub fn contains_key(&self, cmr: &Cmr) -> bool { + self.0.contains_key(cmr) + } + + /// Get the call expression that is tracked by the given CMR. + pub fn get(&self, cmr: &Cmr) -> Option<&TrackedCall> { + self.0.get(cmr) + } +} + +fn remove_excess_whitespace(s: &str) -> String { + let mut last_was_space = true; + let is_excess_whitespace = move |c: char| match c { + ' ' => std::mem::replace(&mut last_was_space, true), + '\n' => true, + _ => { + last_was_space = false; + false + } + }; + s.replace(is_excess_whitespace, "") +} + +impl TrackedCall { + /// Access the text of the Simfony call expression. + pub fn text(&self) -> &str { + &self.text + } + + /// Access the name of the call. + pub fn name(&self) -> &TrackedCallName { + &self.name + } + + /// Supply the Simplicity input value of the call expression at runtime. + /// + /// Return `None` if the Simplicity input value is of the wrong type, + /// according to the debug symbol. + pub fn map_value(&self, value: &StructuralValue) -> Option { + let name = match self.name() { + TrackedCallName::Assert => FallibleCallName::Assert, + TrackedCallName::Panic => FallibleCallName::Panic, + TrackedCallName::Jet => FallibleCallName::Jet, + TrackedCallName::UnwrapLeft(ty) => { + Value::reconstruct(value, ty).map(FallibleCallName::UnwrapLeft)? + } + TrackedCallName::UnwrapRight(ty) => { + Value::reconstruct(value, ty).map(FallibleCallName::UnwrapRight)? + } + TrackedCallName::Unwrap => FallibleCallName::Unwrap, + }; + Some(FallibleCall { + text: Arc::clone(&self.text), + name, + }) + } +} + +impl FallibleCall { + /// Access the Simfony text of the call expression. + pub fn text(&self) -> &str { + &self.text + } + + /// Access the name of the call. + pub fn name(&self) -> &FallibleCallName { + &self.name + } +} diff --git a/src/error.rs b/src/error.rs index dd09e16..e884740 100644 --- a/src/error.rs +++ b/src/error.rs @@ -101,6 +101,34 @@ impl Span { let hash = sha256::Hash::from_engine(hasher); Cmr::from_byte_array(hash.to_byte_array()) } + + /// Return a slice from the given `file` that corresponds to the span. + /// + /// Return `None` if the span runs out of bounds. + pub fn to_slice<'a>(&self, file: &'a str) -> Option<&'a str> { + let mut current_line = 1; + let mut current_col = 1; + let mut start_index = None; + + for (i, c) in file.char_indices() { + if current_line == self.start.line.get() && current_col == self.start.col.get() { + start_index = Some(i); + } + if current_line == self.end.line.get() && current_col == self.end.col.get() { + let start_index = start_index.expect("start comes before end"); + let end_index = i; + return Some(&file[start_index..end_index]); + } + if c == '\n' { + current_line += 1; + current_col = 1; + } else { + current_col += 1; + } + } + + None + } } impl<'a> From<&'a pest::iterators::Pair<'_, Rule>> for Span { From bfb6a5478a59402ba3d75c2aeaa975d8afe6bd1a Mon Sep 17 00:00:00 2001 From: Christian Lewe Date: Mon, 16 Sep 2024 17:18:57 +0200 Subject: [PATCH 7/8] feat: Expose debug symbols of satisfied program Return a plain-old-data type that wraps the Simplicity target code and its debug symbols. When we add a "release" mode to the Simfony compiler, we won't return debug symbols. --- bitcoind-tests/tests/test_arith.rs | 2 +- src/lib.rs | 26 +++++++++++++++++++------- src/main.rs | 2 +- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/bitcoind-tests/tests/test_arith.rs b/bitcoind-tests/tests/test_arith.rs index c12bdc9..6f452b3 100644 --- a/bitcoind-tests/tests/test_arith.rs +++ b/bitcoind-tests/tests/test_arith.rs @@ -43,7 +43,7 @@ pub fn test_simplicity(cl: &ElementsD, program_file: &str, witness_file: &str) { let program_text = std::fs::read_to_string(program_path).unwrap(); let witness_text = std::fs::read_to_string(witness_path).unwrap(); let witness_values = serde_json::from_str::(&witness_text).unwrap(); - let program = simfony::satisfy(&program_text, &witness_values).unwrap(); + let program = simfony::satisfy(&program_text, &witness_values).unwrap().simplicity; let secp = secp256k1::Secp256k1::new(); let internal_key = XOnlyPublicKey::from_str("f5919fa64ce45f8306849072b26c1bfdd2937e6b81774796ff372bd1eb5362d2").unwrap(); diff --git a/src/lib.rs b/src/lib.rs index 0d33c24..84d0be6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,6 +25,7 @@ use simplicity::{jet::Elements, CommitNode, RedeemNode}; pub extern crate simplicity; pub use simplicity::elements; +use crate::debug::DebugSymbols; use crate::error::WithFile; use crate::parse::ParseFromStr; use crate::witness::WitnessValues; @@ -38,10 +39,16 @@ pub fn compile(prog_text: &str) -> Result>, String> { Ok(simplicity_commit) } -pub fn satisfy( - prog_text: &str, - witness: &WitnessValues, -) -> Result>, String> { +/// A satisfied Simfony program, compiled to Simplicity. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SatisfiedProgram { + /// Simplicity target code, including witness data. + pub simplicity: Arc>, + /// Debug symbols for the Simplicity target code. + pub debug_symbols: DebugSymbols, +} + +pub fn satisfy(prog_text: &str, witness: &WitnessValues) -> Result { let parse_program = parse::Program::parse_from_str(prog_text)?; let ast_program = ast::Program::analyze(&parse_program).with_file(prog_text)?; let simplicity_named_construct = ast_program.compile().with_file(prog_text)?; @@ -50,7 +57,12 @@ pub fn satisfy( .map_err(|e| e.to_string())?; let simplicity_witness = named::to_witness_node(&simplicity_named_construct, witness); - simplicity_witness.finalize().map_err(|e| e.to_string()) + let simplicity_redeem = simplicity_witness.finalize().map_err(|e| e.to_string())?; + + Ok(SatisfiedProgram { + simplicity: simplicity_redeem, + debug_symbols: ast_program.debug_symbols(prog_text), + }) } /// Recursively implement [`PartialEq`], [`Eq`] and [`std::hash::Hash`] @@ -159,12 +171,12 @@ mod tests { } pub fn with_witness_values(self, witness_values: &WitnessValues) -> TestCase { - let redeem_program = match satisfy(self.program.0.as_ref(), witness_values) { + let program = match satisfy(self.program.0.as_ref(), witness_values) { Ok(x) => x, Err(error) => panic!("{error}"), }; TestCase { - program: Compiled(redeem_program), + program: Compiled(program.simplicity), lock_time: self.lock_time, sequence: self.sequence, } diff --git a/src/main.rs b/src/main.rs index be702c7..28f9805 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,7 +38,7 @@ fn run() -> Result<(), String> { let witness = serde_json::from_str::(&wit_text).unwrap(); let program = satisfy(&prog_text, &witness)?; - let (program_bytes, witness_bytes) = program.encode_to_vec(); + let (program_bytes, witness_bytes) = program.simplicity.encode_to_vec(); println!( "Program:\n{}", Base64Display::new(&program_bytes, &STANDARD) From 0a677193c2497cf1d0c4ed5f2645331cef83ba73 Mon Sep 17 00:00:00 2001 From: Christian Lewe Date: Mon, 16 Sep 2024 17:04:46 +0200 Subject: [PATCH 8/8] feat: Add debug expression dbg!(x) behaves in Simfony like in Rust: It is a NOP that prints its input value as a side effect. The Simfony compiler adds a debug symbol for this purpose. I split FallibleCall from DebugValue in the debug symbol API because FallibleCall is supposed to be used in error messages, while DebugValue is used for logging on the side. I don't want to handle dbg! as a case in an error message, even though dbg! is infallible. --- src/ast.rs | 15 +++++++++++++-- src/compile.rs | 5 +++++ src/debug.rs | 37 ++++++++++++++++++++++++++++++++++--- src/minimal.pest | 5 +++-- src/parse.rs | 8 ++++++-- 5 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/ast.rs b/src/ast.rs index ee9d9cb..32a1aec 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -259,10 +259,12 @@ pub enum CallName { IsNone(ResolvedType), /// [`Option::unwrap`]. Unwrap, - /// [`assert`]. + /// [`assert!`]. Assert, - /// [`panic`] without error message. + /// [`panic!`] without error message. Panic, + /// [`dbg!`]. + Debug, /// Cast from the given source type. TypeCast(ResolvedType), /// A custom function that was defined previously. @@ -1070,6 +1072,14 @@ impl AbstractSyntaxTree for Call { scope.track_call(from, TrackedCallName::Panic); analyze_arguments(from.args(), &args_tys, scope)? } + CallName::Debug => { + let args_tys = [ty.clone()]; + check_argument_types(from.args(), &args_tys).with_span(from)?; + let args = analyze_arguments(from.args(), &args_tys, scope)?; + let [arg_ty] = args_tys; + scope.track_call(from.args().first().unwrap(), TrackedCallName::Debug(arg_ty)); + args + } CallName::TypeCast(source) => { if StructuralType::from(&source) != StructuralType::from(ty) { return Err(Error::InvalidCast(source, ty.clone())).with_span(from); @@ -1176,6 +1186,7 @@ impl AbstractSyntaxTree for CallName { parse::CallName::Unwrap => Ok(Self::Unwrap), parse::CallName::Assert => Ok(Self::Assert), parse::CallName::Panic => Ok(Self::Panic), + parse::CallName::Debug => Ok(Self::Debug), parse::CallName::TypeCast(target) => { scope.resolve(target).map(Self::TypeCast).with_span(from) } diff --git a/src/compile.rs b/src/compile.rs index 269490d..ba31163 100644 --- a/src/compile.rs +++ b/src/compile.rs @@ -344,6 +344,11 @@ impl Call { let fail = ProgNode::fail(scope.ctx(), FailEntropy::ZERO); with_debug_symbol(args, &fail, scope, self) } + CallName::Debug => { + // dbg! computes the identity function + let iden = ProgNode::iden(scope.ctx()); + with_debug_symbol(args, &iden, scope, self.args().first().unwrap()) + } CallName::TypeCast(..) => { // A cast converts between two structurally equal types. // Structural equality of Simfony types A and B means diff --git a/src/debug.rs b/src/debug.rs index d6fde5d..2919cbc 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use std::sync::Arc; +use either::Either; use simplicity::Cmr; use crate::error::Span; @@ -29,6 +30,7 @@ pub enum TrackedCallName { UnwrapLeft(ResolvedType), UnwrapRight(ResolvedType), Unwrap, + Debug(ResolvedType), } /// Fallible call expression with runtime input value. @@ -49,6 +51,13 @@ pub enum FallibleCallName { Unwrap, } +/// Debug expression with runtime input value. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct DebugValue { + text: Arc, + value: Value, +} + impl DebugSymbols { /// Insert a tracked call expression. /// Use the Simfony source `file` to extract the Simfony text of the expression. @@ -100,10 +109,12 @@ impl TrackedCall { } /// Supply the Simplicity input value of the call expression at runtime. + /// Convert the debug call into a fallible call or into a debug value, + /// depending on the kind of debug symbol. /// /// Return `None` if the Simplicity input value is of the wrong type, /// according to the debug symbol. - pub fn map_value(&self, value: &StructuralValue) -> Option { + pub fn map_value(&self, value: &StructuralValue) -> Option> { let name = match self.name() { TrackedCallName::Assert => FallibleCallName::Assert, TrackedCallName::Panic => FallibleCallName::Panic, @@ -115,11 +126,19 @@ impl TrackedCall { Value::reconstruct(value, ty).map(FallibleCallName::UnwrapRight)? } TrackedCallName::Unwrap => FallibleCallName::Unwrap, + TrackedCallName::Debug(ty) => { + return Value::reconstruct(value, ty) + .map(|value| DebugValue { + text: Arc::clone(&self.text), + value, + }) + .map(Either::Right) + } }; - Some(FallibleCall { + Some(Either::Left(FallibleCall { text: Arc::clone(&self.text), name, - }) + })) } } @@ -134,3 +153,15 @@ impl FallibleCall { &self.name } } + +impl DebugValue { + /// Access the Simfony text of the debug expression. + pub fn text(&self) -> &str { + &self.text + } + + /// Access the runtime input value. + pub fn value(&self) -> &Value { + &self.value + } +} diff --git a/src/minimal.pest b/src/minimal.pest index bab5b38..04855fb 100644 --- a/src/minimal.pest +++ b/src/minimal.pest @@ -12,7 +12,7 @@ jet = @{ "jet::" ~ (ASCII_ALPHANUMERIC | "_")+ } witness_name = @{ (ASCII_ALPHANUMERIC | "_")+ } builtin_type = @{ "Either" | "Option" | "bool" | "List" | unsigned_type } -builtin_function = @{ jet | "unwrap_left" | "unwrap_right" | "for_while" | "is_none" | "unwrap" | "assert" | "panic" | "match" | "into" | "fold" } +builtin_function = @{ jet | "unwrap_left" | "unwrap_right" | "for_while" | "is_none" | "unwrap" | "assert" | "panic" | "match" | "into" | "fold" | "dbg" } function_name = { !builtin_function ~ identifier } typed_identifier = { identifier ~ ":" ~ ty } function_params = { "(" ~ (typed_identifier ~ ("," ~ typed_identifier)*)? ~ ")" } @@ -64,9 +64,10 @@ unwrap = @{ "unwrap" } assert = @{ "assert!" } panic = @{ "panic!" } type_cast = { "<" ~ ty ~ ">::into" } +debug = @{ "dbg!" } fold = { "fold::<" ~ function_name ~ "," ~ list_bound ~ ">" } for_while = { "for_while::<" ~ function_name ~ ">" } -call_name = { jet | unwrap_left | unwrap_right | is_none | unwrap | assert | panic | type_cast | fold | for_while | function_name } +call_name = { jet | unwrap_left | unwrap_right | is_none | unwrap | assert | panic | type_cast | debug | fold | for_while | function_name } call_args = { "(" ~ (expression ~ ("," ~ expression)*)? ~ ")" } call_expr = { call_name ~ call_args } dec_literal = @{ (ASCII_DIGIT | "_")+ } diff --git a/src/parse.rs b/src/parse.rs index f2c52d7..e56f14c 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -177,10 +177,12 @@ pub enum CallName { Unwrap, /// [`Option::is_none`]. IsNone(AliasedType), - /// [`assert`]. + /// [`assert!`]. Assert, - /// [`panic`] without error message. + /// [`panic!`] without error message. Panic, + /// [`dbg!`]. + Debug, /// Cast from the given source type. TypeCast(AliasedType), /// Name of a custom function. @@ -671,6 +673,7 @@ impl fmt::Display for CallName { CallName::IsNone(ty) => write!(f, "is_none::<{ty}>"), CallName::Assert => write!(f, "assert!"), CallName::Panic => write!(f, "panic!"), + CallName::Debug => write!(f, "dbg!"), CallName::TypeCast(ty) => write!(f, "<{ty}>::into"), CallName::Custom(name) => write!(f, "{name}"), CallName::Fold(name, bound) => write!(f, "fold::<{name}, {bound}>"), @@ -943,6 +946,7 @@ impl PestParse for CallName { Rule::unwrap => Ok(Self::Unwrap), Rule::assert => Ok(Self::Assert), Rule::panic => Ok(Self::Panic), + Rule::debug => Ok(Self::Debug), Rule::type_cast => { let inner = pair.into_inner().next().unwrap(); AliasedType::parse(inner).map(Self::TypeCast)