diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e80edd7..440c76e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -108,3 +108,24 @@ jobs: - name: Build each integration test run: | nix develop .#ci --command bash -c "just build_integration" + + build-wasm: + name: Build - WASM + runs-on: ubuntu-latest + needs: test-stable + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install nix + uses: cachix/install-nix-action@v24 + with: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Enable Rust cache + uses: Swatinem/rust-cache@v2 + + - name: Build WASM library + run: | + nix develop .#wasm --command bash -c "just build_wasm" diff --git a/Cargo.lock b/Cargo.lock index 36e3c5a..e198b5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,8 +32,8 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" dependencies = [ - "bitcoin-internals 0.3.0", - "bitcoin_hashes 0.14.0", + "bitcoin-internals", + "bitcoin_hashes", ] [[package]] @@ -56,21 +56,15 @@ checksum = "0032b0e8ead7074cda7fc4f034409607e3f03a6f71d66ade8a307f79b4d99e73" dependencies = [ "base58ck", "bech32", - "bitcoin-internals 0.3.0", + "bitcoin-internals", "bitcoin-io", "bitcoin-units", - "bitcoin_hashes 0.14.0", + "bitcoin_hashes", "hex-conservative 0.2.1", "hex_lit", "secp256k1", ] -[[package]] -name = "bitcoin-internals" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" - [[package]] name = "bitcoin-internals" version = "0.3.0" @@ -95,17 +89,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" dependencies = [ - "bitcoin-internals 0.3.0", -] - -[[package]] -name = "bitcoin_hashes" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" -dependencies = [ - "bitcoin-internals 0.2.0", - "hex-conservative 0.1.1", + "bitcoin-internals", ] [[package]] @@ -469,7 +453,7 @@ version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "bitcoin_hashes 0.13.0", + "bitcoin_hashes", "rand", "secp256k1-sys", ] @@ -497,9 +481,9 @@ dependencies = [ [[package]] name = "secp256k1-zkp-sys" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6eea7919e0cab992510edfbf40bd9342c0a3c2bb910f2c51355c2cb2d69839" +checksum = "57f08b2d0b143a22e07f798ae4f0ab20d5590d7c68e0d090f2088a48a21d1654" dependencies = [ "cc", "secp256k1-sys", @@ -571,7 +555,7 @@ version = "0.3.0" source = "git+https://github.com/BlockstreamResearch/rust-simplicity?rev=fbe88d6f703c2f3b74f52eb3eebf60411606ed42#fbe88d6f703c2f3b74f52eb3eebf60411606ed42" dependencies = [ "bitcoin", - "bitcoin_hashes 0.14.0", + "bitcoin_hashes", "byteorder", "elements", "getrandom", @@ -586,7 +570,7 @@ name = "simplicity-sys" version = "0.3.0" source = "git+https://github.com/BlockstreamResearch/rust-simplicity?rev=fbe88d6f703c2f3b74f52eb3eebf60411606ed42#fbe88d6f703c2f3b74f52eb3eebf60411606ed42" dependencies = [ - "bitcoin_hashes 0.14.0", + "bitcoin_hashes", "cc", ] diff --git a/flake.nix b/flake.nix index 8840745..e7a23e5 100644 --- a/flake.nix +++ b/flake.nix @@ -26,10 +26,11 @@ pkgs = import nixpkgs { inherit system overlays; }; - mkRust = stable: version: profile: extensions: pkgs.rust-bin.${stable}.${version}.${profile}.override { + mkRust = stable: version: profile: targets: extensions: pkgs.rust-bin.${stable}.${version}.${profile}.override { + inherit targets; inherit extensions; }; - defaultRust = mkRust "stable" "latest" "default" ["rust-src"]; + defaultRust = mkRust "stable" "latest" "default" ["wasm32-unknown-unknown"] ["rust-src"]; elementsd-simplicity = pkgs.callPackage ./bitcoind-tests/elementsd-simplicity.nix {}; CC_wasm32_unknown_unknown = "${pkgs.llvmPackages_16.clang-unwrapped}/bin/clang-16"; AR_wasm32_unknown_unknown = "${pkgs.llvmPackages_16.libllvm}/bin/llvm-ar"; @@ -43,6 +44,9 @@ ] ++ ( if with_elements then [ elementsd-simplicity ] else [] ); + inherit CC_wasm32_unknown_unknown; + inherit AR_wasm32_unknown_unknown; + inherit CFLAGS_wasm32_unknown_unknown; # Constants for IDE RUST_TOOLCHAIN = "${defaultRust}/bin"; RUST_STDLIB = "${defaultRust}/lib/rustlib/src/rust"; @@ -56,14 +60,14 @@ # Temporary shells until CI has its nix derivations ci = pkgs.mkShell { buildInputs = [ - (mkRust "stable" "latest" "default" []) + (mkRust "stable" "latest" "default" [] []) pkgs.just pkgs.cargo-hack ]; }; msrv = pkgs.mkShell { buildInputs = [ - (mkRust "stable" "1.63.0" "minimal" []) + (mkRust "stable" "1.63.0" "minimal" [] []) pkgs.just ]; }; @@ -71,13 +75,18 @@ stdenv = pkgs.clang16Stdenv; } { buildInputs = [ - (mkRust "nightly" "2024-07-01" "minimal" ["llvm-tools-preview"]) + (mkRust "nightly" "2024-07-01" "minimal" [] ["llvm-tools-preview"]) pkgs.just pkgs.cargo-fuzz pkgs.cargo-binutils pkgs.rustfilt ]; - # Constants for compiler + }; + wasm = pkgs.mkShell { + buildInputs = [ + (mkRust "stable" "latest" "default" ["wasm32-unknown-unknown"] []) + pkgs.just + ]; inherit CC_wasm32_unknown_unknown; inherit AR_wasm32_unknown_unknown; inherit CFLAGS_wasm32_unknown_unknown; diff --git a/justfile b/justfile index 890c1d3..be54fd7 100644 --- a/justfile +++ b/justfile @@ -49,6 +49,10 @@ build_fuzz: build_integration: cargo test --no-run --manifest-path ./bitcoind-tests/Cargo.toml +# Build code for the WASM target +build_wasm: + cargo check --target wasm32-unknown-unknown + # Remove all temporary files clean: rm -rf target diff --git a/src/ast.rs b/src/ast.rs index d9927c3..a36a918 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::{DebugSymbols, TrackedCallName}; +use crate::debug::{CallTracker, DebugSymbols, TrackedCallName}; use crate::error::{Error, RichError, Span, WithSpan}; use crate::num::{NonZeroPow2Usize, Pow2Usize}; use crate::parse::MatchPattern; @@ -39,7 +39,7 @@ impl WitnessTypes { pub struct Program { main: Expression, witness_types: WitnessTypes, - tracked_calls: Arc<[(Span, TrackedCallName)]>, + call_tracker: Arc, } impl Program { @@ -57,11 +57,12 @@ impl Program { /// 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.iter() { - debug_symbols.insert(*span, name.clone(), file); - } - debug_symbols + self.call_tracker.with_file(file) + } + + /// Access the tracker of function calls. + pub(crate) fn call_tracker(&self) -> &Arc { + &self.call_tracker } } @@ -485,7 +486,7 @@ struct Scope { witnesses: HashMap, functions: HashMap, is_main: bool, - tracked_calls: Vec<(Span, TrackedCallName)>, + call_tracker: CallTracker, } impl Scope { @@ -602,9 +603,9 @@ impl Scope { /// Consume the scope and return its contents: /// /// 1. The map that assigns witness names to their expected type. - /// 2. The list of tracked function calls. - pub fn destruct(self) -> (WitnessTypes, Vec<(Span, TrackedCallName)>) { - (WitnessTypes(self.witnesses), self.tracked_calls) + /// 2. The function call tracker. + pub fn destruct(self) -> (WitnessTypes, CallTracker) { + (WitnessTypes(self.witnesses), self.call_tracker) } /// Insert a custom function into the global map. @@ -633,7 +634,7 @@ impl Scope { /// 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)); + self.call_tracker.track_call(*span.as_ref(), name); } } @@ -660,7 +661,7 @@ impl Program { .map(|s| Item::analyze(s, &unit, &mut scope)) .collect::, RichError>>()?; debug_assert!(scope.is_topmost()); - let (witness_types, tracked_calls) = scope.destruct(); + let (witness_types, call_tracker) = scope.destruct(); let mut iter = items.into_iter().filter_map(|item| match item { Item::Function(Function::Main(expr)) => Some(expr), _ => None, @@ -672,7 +673,7 @@ impl Program { Ok(Self { main, witness_types, - tracked_calls: tracked_calls.into_iter().collect(), + call_tracker: Arc::new(call_tracker), }) } } @@ -1110,7 +1111,7 @@ impl AbstractSyntaxTree for Call { 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)); + scope.track_call(from, TrackedCallName::Debug(arg_ty)); args } CallName::TypeCast(source) => { diff --git a/src/compile.rs b/src/compile.rs index e42d4d2..93f7c09 100644 --- a/src/compile.rs +++ b/src/compile.rs @@ -12,6 +12,7 @@ use crate::ast::{ Call, CallName, Expression, ExpressionInner, Match, Program, SingleExpression, SingleExpressionInner, Statement, }; +use crate::debug::CallTracker; use crate::error::{Error, RichError, Span, WithSpan}; use crate::named::{CoreExt, PairBuilder}; use crate::num::{NonZeroPow2Usize, Pow2Usize}; @@ -59,16 +60,19 @@ struct Scope { /// ``` variables: Vec>, ctx: simplicity::types::Context, + /// Tracker of function calls. + call_tracker: Arc, } impl Scope { /// Create the main scope. /// /// _This function should be called at the start of the compilation and then never again._ - pub fn new() -> Self { + pub fn new(call_tracker: Arc) -> Self { Self { variables: vec![vec![Pattern::Ignore]], ctx: simplicity::types::Context::new(), + call_tracker, } } @@ -77,6 +81,7 @@ impl Scope { Self { variables: vec![vec![input]], ctx: self.ctx.shallow_clone(), + call_tracker: Arc::clone(&self.call_tracker), } } @@ -163,6 +168,28 @@ impl Scope { pub fn ctx(&self) -> &simplicity::types::Context { &self.ctx } + + /// 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. + pub fn with_debug_symbol>( + &mut self, + args: PairBuilder, + body: &ProgNode, + span: &S, + ) -> Result, RichError> { + match self.call_tracker.get_cmr(span.as_ref()) { + Some(cmr) => { + let false_and_args = ProgNode::bit(self.ctx(), false).pair(args); + let nop_assert = ProgNode::assertl_drop(body, cmr); + false_and_args.comp(&nop_assert).with_span(span) + } + None => args.comp(body).with_span(span), + } + } } fn compile_blk( @@ -197,7 +224,7 @@ fn compile_blk( impl Program { pub fn compile(&self) -> Result { - let mut scope = Scope::new(); + let mut scope = Scope::new(Arc::clone(self.call_tracker())); self.main().compile(&mut scope).map(PairBuilder::build) } } @@ -285,27 +312,10 @@ 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); - with_debug_symbol(args, &jet, scope, self) + scope.with_debug_symbol(args, &jet, self) } CallName::UnwrapLeft(..) => { let input_and_unit = @@ -315,7 +325,7 @@ impl Call { Cmr::fail(FailEntropy::ZERO), ); let body = input_and_unit.comp(&extract_inner).with_span(self)?; - with_debug_symbol(args, body.as_ref(), scope, self) + scope.with_debug_symbol(args, body.as_ref(), self) } CallName::UnwrapRight(..) | CallName::Unwrap => { let input_and_unit = @@ -325,7 +335,7 @@ impl Call { &ProgNode::iden(scope.ctx()), ); let body = input_and_unit.comp(&extract_inner).with_span(self)?; - with_debug_symbol(args, body.as_ref(), scope, self) + scope.with_debug_symbol(args, body.as_ref(), self) } CallName::IsNone(..) => { let input_and_unit = @@ -336,17 +346,17 @@ impl Call { } CallName::Assert => { let jet = ProgNode::jet(scope.ctx(), Elements::Verify); - with_debug_symbol(args, &jet, scope, self) + scope.with_debug_symbol(args, &jet, self) } CallName::Panic => { // panic! ignores its arguments let fail = ProgNode::fail(scope.ctx(), FailEntropy::ZERO); - with_debug_symbol(args, &fail, scope, self) + scope.with_debug_symbol(args, &fail, self) } CallName::Debug => { // dbg! computes the identity function let iden = ProgNode::iden(scope.ctx()); - with_debug_symbol(args, &iden, scope, self.args().first().unwrap()) + scope.with_debug_symbol(args, &iden, self) } CallName::TypeCast(..) => { // A cast converts between two structurally equal types. diff --git a/src/debug.rs b/src/debug.rs index 2919cbc..a5529dd 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -2,7 +2,8 @@ use std::collections::HashMap; use std::sync::Arc; use either::Either; -use simplicity::Cmr; +use hashes::{sha256, Hash, HashEngine}; +use simplicity::{hashes, Cmr}; use crate::error::Span; use crate::types::ResolvedType; @@ -14,6 +15,16 @@ use crate::value::{StructuralValue, Value}; #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct DebugSymbols(HashMap); +/// Intermediate representation of tracked Simfony call expressions +/// that is mutable and that lacks information about the source file. +/// +/// The struct can be converted to [`DebugSymbols`] by providing the source file. +#[derive(Debug, Clone, Eq, PartialEq, Default)] +pub(crate) struct CallTracker { + next_id: u32, + map: HashMap, +} + /// Call expression with a debug symbol. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct TrackedCall { @@ -61,9 +72,13 @@ pub struct DebugValue { 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(); + pub(crate) fn insert(&mut self, span: Span, cmr: Cmr, name: TrackedCallName, file: &str) { let text = remove_excess_whitespace(span.to_slice(file).unwrap_or("")); + let text = text + .strip_prefix("dbg!(") + .and_then(|s| s.strip_suffix(")")) + .unwrap_or(&text); + self.0.insert( cmr, TrackedCall { @@ -97,6 +112,45 @@ fn remove_excess_whitespace(s: &str) -> String { s.replace(is_excess_whitespace, "") } +impl CallTracker { + /// Track a new function call with the given `span`. + /// + /// ## Precondition + /// + /// Different function calls have different spans. + /// + /// This holds true when the method is called on a real source file. + /// The precondition might be broken when this method is called on random input. + pub fn track_call(&mut self, span: Span, name: TrackedCallName) { + let cmr = self.next_id_cmr(); + let _replaced = self.map.insert(span, (cmr, name)); + self.next_id += 1; + } + + /// Get the CMR of the tracked function call with the given `span`. + pub fn get_cmr(&self, span: &Span) -> Option { + self.map.get(span).map(|x| x.0) + } + + fn next_id_cmr(&self) -> Cmr { + let tag_hash = sha256::Hash::hash(b"simfony\x1fdebug\x1f"); + let mut engine = sha256::Hash::engine(); + engine.input(tag_hash.as_ref()); + engine.input(tag_hash.as_ref()); + engine.input(self.next_id.to_be_bytes().as_ref()); + Cmr::from_byte_array(sha256::Hash::from_engine(engine).to_byte_array()) + } + + /// Create debug symbols by attaching information from the source `file`. + pub fn with_file(&self, file: &str) -> DebugSymbols { + let mut debug_symbols = DebugSymbols::default(); + for (span, (cmr, name)) in &self.map { + debug_symbols.insert(*span, *cmr, name.clone(), file); + } + debug_symbols + } +} + impl TrackedCall { /// Access the text of the Simfony call expression. pub fn text(&self) -> &str {