diff --git a/Cargo.lock b/Cargo.lock index 770c6790..ad591afa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1128,15 +1128,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "santiago" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de36022292bc2086eb8f55bffa460fef3475e4459b478820711f4c421feb87ec" -dependencies = [ - "regex", -] - [[package]] name = "sct" version = "0.7.1" @@ -1268,9 +1259,9 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "simplicity-lang" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e57bd4d84853974a212eab24ed89da54f49fbccf5e33e93bcd29f0a6591cd5" +checksum = "13ed081e3046d66c146d7201bcbf3b655ca3436cb83f6efc26d7895bd2b79d06" dependencies = [ "bitcoin", "bitcoin_hashes", @@ -1280,15 +1271,14 @@ dependencies = [ "ghost-cell", "hex-conservative", "miniscript", - "santiago", "simplicity-sys", ] [[package]] name = "simplicity-sys" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3401ee7331f183a5458c0f5a4b3d5d00bde0fd12e2e03728c537df34efae289" +checksum = "96d1ec5477c7650b8ef511aa56dccb28f2e8cdb6e87f260ecffdaf0ebfef2d3b" dependencies = [ "bitcoin_hashes", "cc", @@ -1296,9 +1286,8 @@ dependencies = [ [[package]] name = "simplicityhl" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25de8990174fe3e1a843df138cacc4265d05839ebd2550c18b9196f567d55e81" +version = "0.6.0-rc.0" +source = "git+https://github.com/LesterEvSe/SimplicityHL.git?branch=feature%2Fdependency-builder#710a6315f4fa2dfcb1c26542a533bc2c3bf0e2b8" dependencies = [ "base64 0.21.7", "chumsky", @@ -1328,6 +1317,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", + "regex", "serde", "simplicityhl", "syn", diff --git a/Cargo.toml b/Cargo.toml index 481c8f99..1b54dea3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,8 @@ toml = { version = "0.9.8" } minreq = { version = "2.14.1", features = ["https", "json-using-serde"] } electrsd = { version = "0.29.0", features = ["legacy"] } -simplicityhl = { version = "0.5.0" } +# simplicityhl = { version = "0.5.0" } +simplicityhl = { git = "https://github.com/LesterEvSe/SimplicityHL.git", branch = "feature/dependency-builder" } [workspace.lints.rust] rust_2018_idioms = "warn" diff --git a/crates/build/Cargo.toml b/crates/build/Cargo.toml index 8487fbfe..fc389b1a 100644 --- a/crates/build/Cargo.toml +++ b/crates/build/Cargo.toml @@ -14,6 +14,7 @@ toml = { workspace = true } serde = { workspace = true } simplicityhl = { workspace = true } +regex = { version = "1" } syn = { version = "2.0.114", default-features = false, features = ["proc-macro", "full", "parsing", "derive", "clone-impls", "extra-traits", "printing"] } proc-macro2 = { version = "1.0.106", features = ["span-locations"] } quote = { version = "1.0.44" } diff --git a/crates/build/src/config.rs b/crates/build/src/config.rs index 3395e75e..7422284a 100644 --- a/crates/build/src/config.rs +++ b/crates/build/src/config.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::fs::OpenOptions; use std::io::Read; use std::path::Path; @@ -18,6 +19,23 @@ pub struct BuildConfig { pub out_dir: String, } +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(default)] +pub struct DependencyConfig { + #[serde(flatten)] + pub inner: HashMap, +} + +#[derive(Debug, Default, Clone, Deserialize)] +#[serde(default)] +pub struct Dependency { + /// Exact path to dir, where `Simplex.toml` file was located + pub path: Option, + + /// Link to git repo + pub git: Option, +} + impl BuildConfig { pub fn from_file(path: impl AsRef) -> Result { let mut content = String::new(); @@ -38,3 +56,34 @@ impl Default for BuildConfig { } } } + +impl DependencyConfig { + pub fn from_file(path: impl AsRef) -> Result { + let mut content = String::new(); + let mut file = OpenOptions::new().read(true).open(path)?; + file.read_to_string(&mut content)?; + + let table: toml::Table = toml::from_str(&content)?; + let res = if let Some(deps_section) = table.get("dependencies") { + deps_section.clone().try_into()? + } else { + DependencyConfig::default() + }; + + if let Err(err) = res.validate() { + Err(BuildError::InvalidDependency(err)) + } else { + Ok(res) + } + } + + /// When error occured, returned, return drp_name of the invalid dependency + pub fn validate(&self) -> Result<(), String> { + for (drp_name, dep) in &self.inner { + if dep.path.is_none() && dep.git.is_none() { + return Err(drp_name.clone()); + } + } + Ok(()) + } +} diff --git a/crates/build/src/error.rs b/crates/build/src/error.rs index 140b89a0..ce74f12d 100644 --- a/crates/build/src/error.rs +++ b/crates/build/src/error.rs @@ -30,4 +30,19 @@ pub enum BuildError { #[error("Failed to find prefix for a file: {0}")] NoBasePathForGeneration(#[from] std::path::StripPrefixError), + + #[error("Invalid dependency '{0}': you must specify either a 'path' or a 'git' repository")] + InvalidDependency(String), + + #[error("Dependency '{dep_name}' is missing its configuration manifest at: {expected_path}")] + MissingDependencyConfig { dep_name: String, expected_path: PathBuf }, + + #[error("{0}")] + PathCanonicalization(String), + + #[error("Failed to build dependency map: {0}")] + DependencyMap(String), + + #[error("Failed to flatten program: {0}")] + Flattening(String), } diff --git a/crates/build/src/generator.rs b/crates/build/src/generator.rs index 670bbff7..d84291c5 100644 --- a/crates/build/src/generator.rs +++ b/crates/build/src/generator.rs @@ -3,10 +3,17 @@ use std::env; use std::fs; use std::io::Write; use std::path::{Component, Path, PathBuf}; +use std::sync::Arc; use proc_macro2::TokenStream; use quote::{format_ident, quote}; +use simplicityhl::TemplateProgram; +use simplicityhl::resolution::DependencyMap; +use simplicityhl::resolution::ValidatedDeps; +use simplicityhl::source::CanonPath; +use simplicityhl::source::CanonSourceFile; + use crate::macros::codegen::{ convert_contract_name_to_contract_module, convert_contract_name_to_contract_source_const, convert_contract_name_to_struct_name, @@ -17,9 +24,24 @@ use super::error::BuildError; pub struct ArtifactsGenerator {} +/// A single processed `.simf` file with all metadata needed for binding generation. +/// +/// Created once per source file and carries everything downstream — no recomputation +/// of paths or contract names in later stages. +struct SimfArtifact { + /// Path relative to `base_dir` (e.g. `hash/func/sha256.simf`). + /// Used to mirror the source structure under `out_dir/simf/`. + relative_path: PathBuf, + /// Full path to the file written under `out_dir/simf/`. + /// Passed directly to `include_simf!` — no path reconstruction needed. + mirrored_path: PathBuf, + /// Contract name extracted from the `.simf` source file. + contract_name: String, +} + #[derive(Default)] struct TreeNode { - files: Vec, + files: Vec, dirs: HashMap, } @@ -28,108 +50,177 @@ impl ArtifactsGenerator { out_dir: impl AsRef, base_dir: impl AsRef, simfs: &[impl AsRef], + validated_deps: &ValidatedDeps, ) -> Result<(), BuildError> { - let tree = Self::build_directory_tree(&base_dir, simfs)?; + let cwd = env::current_dir()?; + let out_dir = out_dir.as_ref(); + let base_dir = base_dir.as_ref(); + + let pathdiff = pathdiff::diff_paths(base_dir, &cwd).ok_or(BuildError::FailedToFindCorrectRelativePath { + cwd, + simf_file: base_dir.to_path_buf(), + })?; + + let simf_out_dir = out_dir.join(pathdiff); - Self::generate_bindings(out_dir.as_ref(), tree)?; + let artifacts = simfs + .iter() + .map(|s| Self::process_simf(s.as_ref(), base_dir, validated_deps, &simf_out_dir)) + .collect::, _>>()?; + + let tree = Self::build_tree(artifacts)?; + + Self::generate_bindings(out_dir, tree)?; Ok(()) } - fn build_directory_tree(base_dir: impl AsRef, paths: &[impl AsRef]) -> Result { - let mut root = TreeNode::default(); + pub fn build_dependency_map( + validated_deps: &ValidatedDeps, + entry_root_dir: impl AsRef, + ) -> Result { + let canon_entry_root = + CanonPath::canonicalize(entry_root_dir.as_ref()).map_err(BuildError::PathCanonicalization)?; + + validated_deps + .with_root(canon_entry_root) + .map_err(|e| BuildError::DependencyMap(e.to_string())) + } - for path in paths { - let path = path.as_ref(); + /// Processes a single `.simf` source file: + /// - Writes its content to the mirrored path under `simf_out_dir` + /// - Extracts the contract name + /// + /// All path and name information needed for downstream stages is captured + /// here so later steps never need to re-derive anything. + fn process_simf( + source: &Path, + base_dir: &Path, + validated_deps: &ValidatedDeps, + simf_out_dir: &Path, + ) -> Result { + let relative_path = source + .strip_prefix(base_dir) + .map_err(BuildError::NoBasePathForGeneration)? + .to_path_buf(); + + let mirrored_path = simf_out_dir.join(&relative_path); + + if let Some(parent) = mirrored_path.parent() { + fs::create_dir_all(parent)?; + } + + let content = Self::process_content(source, validated_deps)?; + fs::write(&mirrored_path, &content)?; + + let contract_name = SimfContent::extract_content_from_path(&source.to_path_buf()) + .map_err(BuildError::FailedToExtractContent)? + .contract_name; + + Ok(SimfArtifact { + relative_path, + mirrored_path, + contract_name, + }) + } + + /// Reads and processes the content of a `.simf` file. + fn process_content(source: &Path, validated_deps: &ValidatedDeps) -> Result { + let parent_dir = source.parent().ok_or_else(|| { + BuildError::GenerationFailed(format!("Path '{}' has no parent directory", source.display())) + })?; - let relative_path = path - .strip_prefix(base_dir.as_ref()) - .map_err(BuildError::NoBasePathForGeneration)?; + let canon_source = CanonPath::canonicalize(source).map_err(BuildError::PathCanonicalization)?; + let content = fs::read_to_string(source)?; + let canon_source_file = CanonSourceFile::new(canon_source, Arc::from(content)); + let dependency_map = Self::build_dependency_map(validated_deps, parent_dir)?; - let components: Vec<_> = relative_path + TemplateProgram::flatten(canon_source_file, &dependency_map).map_err(BuildError::Flattening) + } + + /// Arranges a flat list of artifacts into a tree mirroring the source directory layout. + fn build_tree(artifacts: Vec) -> Result { + let mut root = TreeNode::default(); + + for artifact in artifacts { + let components: Vec<_> = artifact + .relative_path .components() .filter_map(|c| { if let Component::Normal(name) = c { - Some(name) + Some(name.to_string_lossy().into_owned()) } else { None } }) .collect(); - let mut current_node = &mut root; - let components_len = components.len(); - - for (i, name) in components.into_iter().enumerate() { - let is_file = i == components_len - 1; - - if is_file { - current_node.files.push(path.to_path_buf()); - } else { - let dir_name = name.to_string_lossy().into_owned(); - - current_node = current_node.dirs.entry(dir_name).or_default(); - } + // All components except the last are directories; the last is the file itself + let mut current = &mut root; + for dir in &components[..components.len().saturating_sub(1)] { + current = current.dirs.entry(dir.clone()).or_default(); } + + current.files.push(artifact); } Ok(root) } - fn generate_bindings(out_dir: &Path, path_tree: TreeNode) -> Result, BuildError> { + /// Recursively generates bindings for every node in the tree. + fn generate_bindings(out_dir: &Path, tree: TreeNode) -> Result<(), BuildError> { fs::create_dir_all(out_dir)?; - let mut mod_filenames = Self::generate_simfs(out_dir, &path_tree.files)?; + let mut mod_names = Vec::new(); - for (dir_name, tree_node) in path_tree.dirs.into_iter() { - Self::generate_bindings(&out_dir.join(&dir_name), tree_node)?; - mod_filenames.push(dir_name); + for artifact in tree.files { + let mod_name = Self::generate_simf_binding(out_dir, artifact)?; + mod_names.push(mod_name); } - Self::generate_mod_rs(out_dir, &mod_filenames)?; - - Ok(mod_filenames) - } - - fn generate_simfs(out_dir: impl AsRef, simfs: &[impl AsRef]) -> Result, BuildError> { - let mut module_files = Vec::with_capacity(simfs.len()); - - for simf_file_path in simfs { - let mod_name = Self::generate_simf_file(out_dir.as_ref(), simf_file_path)?; - module_files.push(mod_name); + for (dir_name, subtree) in tree.dirs { + Self::generate_bindings(&out_dir.join(&dir_name), subtree)?; + mod_names.push(dir_name); } - Ok(module_files) - } + Self::generate_mod_rs(out_dir, &mod_names)?; - fn generate_simf_file(out_dir: impl AsRef, simf_file_path: impl AsRef) -> Result { - let simf_file_buf = PathBuf::from(simf_file_path.as_ref()); - let simf_content = - SimfContent::extract_content_from_path(&simf_file_buf).map_err(BuildError::FailedToExtractContent)?; + Ok(()) + } - let contract_name = simf_content.contract_name.clone(); - let output_file = out_dir.as_ref().join(format!("{}.rs", &contract_name)); + /// Generates a single `.rs` binding file for one simf artifact. + fn generate_simf_binding(out_dir: &Path, artifact: SimfArtifact) -> Result { + let output_file = out_dir.join(format!("{}.rs", &artifact.contract_name)); let mut file = fs::OpenOptions::new() .create(true) .write(true) .truncate(true) .open(&output_file)?; - let code = Self::generate_simf_binding_code(simf_content, simf_file_buf)?; + + let cwd = env::current_dir()?; + let pathdiff = + pathdiff::diff_paths(&artifact.mirrored_path, &cwd).ok_or(BuildError::FailedToFindCorrectRelativePath { + cwd, + simf_file: artifact.mirrored_path.clone(), + })?; + + let code = Self::generate_simf_binding_code(&artifact.contract_name, &pathdiff)?; Self::expand_file(code, &mut file)?; - Ok(contract_name) + Ok(artifact.contract_name) } - fn generate_mod_rs(out_dir: impl AsRef, simfs_mod_names: &[String]) -> Result<(), BuildError> { + fn generate_mod_rs(out_dir: impl AsRef, mod_names: &[String]) -> Result<(), BuildError> { let output_file = out_dir.as_ref().join("mod.rs"); let mut file = fs::OpenOptions::new() .create(true) .write(true) .truncate(true) .open(&output_file)?; - let code = Self::generate_mod_binding_code(simfs_mod_names)?; + + let code = Self::generate_mod_binding_code(mod_names)?; Self::expand_file(code, &mut file)?; @@ -147,19 +238,15 @@ impl ArtifactsGenerator { Ok(()) } - fn generate_simf_binding_code(simf_content: SimfContent, simf_file: PathBuf) -> Result { - let cwd = env::current_dir()?; - let contract_name = &simf_content.contract_name; + fn generate_simf_binding_code(contract_name: &str, target_simf: &Path) -> Result { let program_name = { let base_name = convert_contract_name_to_struct_name(contract_name); format_ident!("{base_name}Program") }; + let include_simf_source_const = convert_contract_name_to_contract_source_const(contract_name); let include_simf_module = convert_contract_name_to_contract_module(contract_name); - - let pathdiff = pathdiff::diff_paths(&simf_file, &cwd) - .ok_or(BuildError::FailedToFindCorrectRelativePath { cwd, simf_file })?; - let pathdiff = pathdiff.to_string_lossy().into_owned(); + let target_simf_str = target_simf.to_string_lossy().into_owned(); let code = quote! { use simplex::include_simf; @@ -185,14 +272,12 @@ impl ArtifactsGenerator { #[must_use] pub fn with_taproot_pubkey(mut self, pub_key: XOnlyPublicKey) -> Self { self.program = self.program.with_taproot_pubkey(pub_key); - self } #[must_use] pub fn with_storage_capacity(mut self, capacity: usize) -> Self { self.program = self.program.with_storage_capacity(capacity); - self } @@ -239,21 +324,20 @@ impl ArtifactsGenerator { } } - include_simf!(#pathdiff); + include_simf!(#target_simf_str); }; Ok(code) } fn generate_mod_binding_code(mod_names: &[String]) -> Result { - let mod_names = mod_names.iter().map(|x| format_ident!("{x}")).collect::>(); + let mod_idents = mod_names.iter().map(|x| format_ident!("{x}")).collect::>(); let code = quote! { #![allow(clippy::all)] - #( #[rustfmt::skip] - pub mod #mod_names; + pub mod #mod_idents; )* }; diff --git a/crates/build/src/lib.rs b/crates/build/src/lib.rs index 06ccd253..09d01aea 100644 --- a/crates/build/src/lib.rs +++ b/crates/build/src/lib.rs @@ -4,6 +4,6 @@ pub mod generator; pub mod macros; pub mod resolver; -pub use config::BuildConfig; +pub use config::{BuildConfig, DependencyConfig}; pub use generator::ArtifactsGenerator; pub use resolver::ArtifactsResolver; diff --git a/crates/build/src/macros/core.rs b/crates/build/src/macros/core.rs index d27d1982..969801e4 100644 --- a/crates/build/src/macros/core.rs +++ b/crates/build/src/macros/core.rs @@ -3,6 +3,7 @@ use std::error::Error; use proc_macro2::Span; use quote::quote; +use simplicityhl::ast::ElementsJetHinter; use simplicityhl::{AbiMeta, TemplateProgram}; use super::codegen::{ @@ -88,5 +89,5 @@ fn construct_argument_helpers(derived_meta: &SimfContractMeta) -> syn::Result Result> { let program = content.content.as_str(); - Ok(TemplateProgram::new(program)?.generate_abi_meta()?) + Ok(TemplateProgram::new(program, Box::new(ElementsJetHinter))?.generate_abi_meta()?) } diff --git a/crates/build/src/resolver.rs b/crates/build/src/resolver.rs index 18c9286c..682da8d6 100644 --- a/crates/build/src/resolver.rs +++ b/crates/build/src/resolver.rs @@ -1,7 +1,15 @@ +use std::collections::HashSet; use std::env; use std::path::{Path, PathBuf}; +use std::sync::OnceLock; use globwalk::FileType; +use regex::Regex; + +use simplicityhl::resolution::{DependencyMapBuilder, ValidatedDeps}; +use simplicityhl::source::CanonPath; + +use crate::{BuildConfig, DependencyConfig}; use super::error::BuildError; @@ -21,7 +29,12 @@ impl ArtifactsResolver { .filter_map(Result::ok); for img in walker { - paths.push(img.path().to_path_buf().canonicalize()?); + let path = img.path().to_path_buf().canonicalize()?; + let content = std::fs::read_to_string(&path)?; + + if Self::contains_main(&content) { + paths.push(path); + } } Ok(paths) @@ -56,4 +69,116 @@ impl ArtifactsResolver { // TODO: canonicalize? but this path may not exist Ok(path_outer) } + + /// Builds a [`ValidatedDeps`] by recursively walking the dependency tree + /// starting from the current working directory. + /// + /// Each dependency may have its own config file declaring further dependencies. + /// Those are registered with their own directory as the context, so that + /// `crate::` and sibling imports resolve correctly relative to each package root. + pub fn resolve_remappings( + deps_config: &DependencyConfig, + config_filename: &str, + ) -> Result { + let root_dir = env::current_dir()?; + let canon_root = CanonPath::canonicalize(&root_dir).map_err(BuildError::PathCanonicalization)?; + + let root_build_cfg = BuildConfig::from_file(canon_root.as_path().join(config_filename))?; + let root_simf_dir = CanonPath::canonicalize(&canon_root.as_path().join(&root_build_cfg.src_dir)) + .map_err(BuildError::PathCanonicalization)?; + + let mut collector = DepCollector { + builder: DependencyMapBuilder::new(), + visited: HashSet::new(), + config_filename: config_filename.to_string(), + }; + collector.visited.insert(canon_root.clone()); + + collector.rec_collect(deps_config, &root_simf_dir, &canon_root)?; + + collector + .builder + .validate_deps() + .map_err(|e| BuildError::DependencyMap(e.to_string())) + } + + /// Checks whether the source contains a `fn main(...)` declaration, + /// regardless of visibility (`pub fn main` or `fn main`) or nesting + /// inside `mod { ... }` blocks; The regex matches anywhere in the text. + fn contains_main(source: &str) -> bool { + static RE: OnceLock = OnceLock::new(); + let re = RE.get_or_init(|| Regex::new(r"(?m)^\s*(pub\s+)?fn\s+main\s*\(").unwrap()); + + re.is_match(source) + } +} + +/// A temporary context struct to hold global state during recursion. +/// This eliminates the need to pass `builder`, `visited`, and `config_filename` +/// into every single recursive call. +struct DepCollector { + builder: DependencyMapBuilder, + visited: HashSet, + config_filename: String, +} + +impl DepCollector { + /// Recursively registers each dependency's `simf` directory under its parent context, + /// then recurses into the dependency's own config to discover transitive dependencies. + /// + /// # Example + /// + /// Given the dependency graph: + /// ```text + /// root -> A -> B + /// root -> B + /// ``` + /// + /// Processing proceeds as follows: + /// + /// 1. Starting at `root`, register `A` as a dependency under `root`'s context, + /// then mark `root` as visited and recurse into `A`. + /// 2. Inside `A`, register `B` as a dependency under `A`'s context, mark `A` as + /// visited, and recurse into `B`. + /// 3. Inside `B`, `deps_config` is empty, so nothing is registered — recursion + /// simply returns. + /// 4. Back at `root`, register `B` as a dependency under `root`'s context as well. + /// Note that the dependency is registered *before* checking `visited` — `root` + /// must record its own link to `B`, even though `B` itself was already visited + /// and does not need to be recursed into again. + fn rec_collect( + &mut self, + deps_config: &DependencyConfig, + simf_dir: &CanonPath, + context: &CanonPath, + ) -> Result<(), BuildError> { + for (dep_name, dep) in &deps_config.inner { + let Some(dep_path_str) = dep.path.as_ref() else { + // TODO: git support + continue; + }; + + let loaded_context = CanonPath::canonicalize(&context.as_path().join(dep_path_str)) + .map_err(BuildError::PathCanonicalization)?; + + // TODO: This code could be further optimized by using a `from_str` method inside `BuildConfig` and `DependencyConfig` + let config_path = loaded_context.as_path().join(&self.config_filename); + + let loaded_src_dir = BuildConfig::from_file(&config_path)?.src_dir; + let loaded_simf_dir = CanonPath::canonicalize(&loaded_context.as_path().join(loaded_src_dir)) + .map_err(BuildError::PathCanonicalization)?; + + self.builder + .add_dependency(simf_dir.clone(), dep_name.clone(), loaded_simf_dir.clone()); + + if !self.visited.insert(loaded_context.clone()) { + continue; + } + + let nested_deps = DependencyConfig::from_file(&config_path)?; + self.rec_collect(&nested_deps, &loaded_simf_dir, &loaded_context)?; + } + + Ok(()) + } } diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 69edbbba..9f4be5ca 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -70,7 +70,7 @@ impl Cli { let config_path = Config::get_default_path()?; let loaded_config = Config::load(config_path)?; - Ok(Build::run(&loaded_config.build)?) + Ok(Build::run(&loaded_config.build, &loaded_config.dependencies)?) } Command::Clean => { let config_path = Config::get_default_path()?; diff --git a/crates/cli/src/commands/build.rs b/crates/cli/src/commands/build.rs index d6d69775..95d72511 100644 --- a/crates/cli/src/commands/build.rs +++ b/crates/cli/src/commands/build.rs @@ -1,4 +1,6 @@ -use smplx_build::{ArtifactsGenerator, ArtifactsResolver, BuildConfig}; +use smplx_build::{ArtifactsGenerator, ArtifactsResolver, BuildConfig, DependencyConfig}; + +use crate::config::CONFIG_FILENAME; use super::error::CommandError; @@ -9,15 +11,21 @@ impl Build { /// /// # Errors /// Returns a `CommandError` if it fails to resolve directories or files, or if artifact generation encounters an error. - pub fn run(config: &BuildConfig) -> Result<(), CommandError> { + pub fn run(config: &BuildConfig, deps: &DependencyConfig) -> Result<(), CommandError> { let output_dir = ArtifactsResolver::resolve_local_dir(&config.out_dir)?; let src_dir = ArtifactsResolver::resolve_local_dir(&config.src_dir)?; + + // NOTE: Assume that remappings already install + let dependency_builder = ArtifactsResolver::resolve_remappings(deps, CONFIG_FILENAME)?; + + // TODO: For all remappings need to check `files_to_build` and concatenate it to `Vec` let files_to_build = ArtifactsResolver::resolve_files_to_build(&config.src_dir, &config.simf_files)?; Ok(ArtifactsGenerator::generate_artifacts( &output_dir, &src_dir, &files_to_build, + &dependency_builder, )?) } } diff --git a/crates/cli/src/config/core.rs b/crates/cli/src/config/core.rs index 882c8dfe..19d6aa39 100644 --- a/crates/cli/src/config/core.rs +++ b/crates/cli/src/config/core.rs @@ -1,7 +1,7 @@ use serde::Deserialize; use std::path::{Path, PathBuf}; -use smplx_build::BuildConfig; +use smplx_build::{BuildConfig, DependencyConfig}; use smplx_regtest::RegtestConfig; use smplx_test::TestConfig; @@ -14,6 +14,9 @@ pub const CONFIG_FILENAME: &str = "Simplex.toml"; #[serde(default)] pub struct Config { pub build: BuildConfig, + + /// Key is dependency root path name + pub dependencies: DependencyConfig, pub regtest: RegtestConfig, pub test: TestConfig, } @@ -66,18 +69,15 @@ impl Config { } fn validate(config: &Config) -> Result<(), ConfigError> { - match config.test.esplora.clone() { - Some(esplora_config) => { - Self::validate_network(&esplora_config.network)?; - - if config.test.rpc.is_some() && esplora_config.network != "ElementsRegtest" { - return Err(ConfigError::NetworkNameUnmatched(esplora_config.network.clone())); - } + if let Some(esplora_config) = config.test.esplora.clone() { + Self::validate_network(&esplora_config.network)?; - Ok(()) + if config.test.rpc.is_some() && esplora_config.network != "ElementsRegtest" { + return Err(ConfigError::NetworkNameUnmatched(esplora_config.network.clone())); } - None => Ok(()), } + + config.dependencies.validate().map_err(ConfigError::InvalidDependency) } fn validate_network(network: &String) -> Result<(), ConfigError> { diff --git a/crates/cli/src/config/error.rs b/crates/cli/src/config/error.rs index d76ca7b9..1105a648 100644 --- a/crates/cli/src/config/error.rs +++ b/crates/cli/src/config/error.rs @@ -8,6 +8,9 @@ pub enum ConfigError { #[error("TOML parse error: {0}")] TomlParse(#[from] toml::de::Error), + #[error("Invalid dependency '{0}': you must specify either a 'path' or a 'git' repository")] + InvalidDependency(String), + #[error("Network name should either be `Liquid`, `LiquidTestnet` or `ElementsRegtest`, got: {0}")] BadNetworkName(String), diff --git a/crates/sdk/src/program/core.rs b/crates/sdk/src/program/core.rs index 8c16de42..703d3955 100644 --- a/crates/sdk/src/program/core.rs +++ b/crates/sdk/src/program/core.rs @@ -4,10 +4,10 @@ use std::sync::Arc; use dyn_clone::DynClone; use simplicityhl::CompiledProgram; +use simplicityhl::ast::ElementsJetHinter; use simplicityhl::elements::pset::PartiallySignedTransaction; use simplicityhl::elements::{Address, Script, Transaction, TxOut, taproot}; use simplicityhl::simplicity::bitcoin::{XOnlyPublicKey, secp256k1}; -use simplicityhl::simplicity::jet::Elements; use simplicityhl::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; use simplicityhl::simplicity::{BitMachine, RedeemNode, Value, leaf_version}; use simplicityhl::{Parameters, WitnessTypes, WitnessValues}; @@ -62,7 +62,7 @@ pub trait ProgramTrait: DynClone { witness: &WitnessValues, input_index: usize, network: &SimplicityNetwork, - ) -> Result<(Arc>, Value), ProgramError>; + ) -> Result<(Arc, Value), ProgramError>; /// Finalizes and returns `pruned_witness` as output after executing the program on certain parameters. /// @@ -150,7 +150,7 @@ impl ProgramTrait for Program { witness: &WitnessValues, input_index: usize, network: &SimplicityNetwork, - ) -> Result<(Arc>, Value), ProgramError> { + ) -> Result<(Arc, Value), ProgramError> { let satisfied = self .load()? .satisfy(witness.clone()) @@ -312,6 +312,7 @@ impl Program { self.source, self.arguments.build_arguments(), GlobalConfig::get_include_debug_symbols(), + Box::new(ElementsJetHinter), ) .map_err(ProgramError::Compilation)?; diff --git a/crates/sdk/src/program/logger.rs b/crates/sdk/src/program/logger.rs index 5d1f1b46..1e6fe1a6 100644 --- a/crates/sdk/src/program/logger.rs +++ b/crates/sdk/src/program/logger.rs @@ -2,10 +2,7 @@ use std::{cell::RefCell, fmt::Write}; use simplicityhl::{ debug::DebugSymbols, - simplicity::{ - jet::Jet, - node::{Node, Redeem}, - }, + simplicity::node::{Node, Redeem}, tracker::{DefaultTracker, TrackerLogLevel}, }; @@ -102,7 +99,7 @@ impl ProgramLogger { /// # Safety /// Uses `transmute` to extract the inner `u32` from [`Cost`] since no public /// accessor exists. Remove once `as_milliweight()` is upstreamed to rust-simplicity. - pub fn buffer_cost_log(node: &Node>) { + pub fn buffer_cost_log(node: &Node) { let bounds = node.bounds(); // FIXME: Cost has no public accessor; remove once as_milliweight() is upstreamed let mw: u32 = unsafe { std::mem::transmute(bounds.cost) }; diff --git a/fixtures/Cargo.lock b/fixtures/Cargo.lock index e12d8d8b..7684466e 100644 --- a/fixtures/Cargo.lock +++ b/fixtures/Cargo.lock @@ -1044,15 +1044,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "santiago" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de36022292bc2086eb8f55bffa460fef3475e4459b478820711f4c421feb87ec" -dependencies = [ - "regex", -] - [[package]] name = "sct" version = "0.7.1" @@ -1192,9 +1183,9 @@ dependencies = [ [[package]] name = "simplicity-lang" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e57bd4d84853974a212eab24ed89da54f49fbccf5e33e93bcd29f0a6591cd5" +checksum = "13ed081e3046d66c146d7201bcbf3b655ca3436cb83f6efc26d7895bd2b79d06" dependencies = [ "bitcoin", "bitcoin_hashes", @@ -1204,15 +1195,14 @@ dependencies = [ "ghost-cell", "hex-conservative", "miniscript", - "santiago", "simplicity-sys", ] [[package]] name = "simplicity-sys" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3401ee7331f183a5458c0f5a4b3d5d00bde0fd12e2e03728c537df34efae289" +checksum = "96d1ec5477c7650b8ef511aa56dccb28f2e8cdb6e87f260ecffdaf0ebfef2d3b" dependencies = [ "bitcoin_hashes", "cc", @@ -1220,9 +1210,8 @@ dependencies = [ [[package]] name = "simplicityhl" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25de8990174fe3e1a843df138cacc4265d05839ebd2550c18b9196f567d55e81" +version = "0.6.0-rc.0" +source = "git+https://github.com/LesterEvSe/SimplicityHL.git?branch=feature%2Fdependency-builder#710a6315f4fa2dfcb1c26542a533bc2c3bf0e2b8" dependencies = [ "base64 0.21.7", "chumsky", @@ -1252,6 +1241,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", + "regex", "serde", "simplicityhl", "syn", diff --git a/fixtures/Simplex.toml b/fixtures/Simplex.toml index e8eff361..7a272b7b 100644 --- a/fixtures/Simplex.toml +++ b/fixtures/Simplex.toml @@ -5,6 +5,10 @@ # simf_files = ["*.simf"] # out_dir = "./src/artifacts" +[dependencies] +merkle = { path = "simf/deps/merkle" } +base_math = { path = "simf/deps/math" } + # [regtest] # mnemonic = "exist carry drive collect lend cereal occur much tiger just involve mean" # bitcoins = 10_000_000 diff --git a/fixtures/simf/deps/math/Simplex.toml b/fixtures/simf/deps/math/Simplex.toml new file mode 100644 index 00000000..e8eff361 --- /dev/null +++ b/fixtures/simf/deps/math/Simplex.toml @@ -0,0 +1,28 @@ +# DEFAULT CONFIG + +# [build] +# src_dir = "./simf" +# simf_files = ["*.simf"] +# out_dir = "./src/artifacts" + +# [regtest] +# mnemonic = "exist carry drive collect lend cereal occur much tiger just involve mean" +# bitcoins = 10_000_000 +# rpc_port = 18443 +# esplora_port = 3000 +# rpc_user = "user" +# rpc_password = "password" + +# [test] +# mnemonic = "exist carry drive collect lend cereal occur much tiger just involve mean" +# bitcoins = 10_000_000 +# verbosity = 0 + +# [test.esplora] +# url = "" +# network = "" + +# [test.rpc] +# url = "" +# username = "" +# password = "" diff --git a/fixtures/simf/deps/math/simf/simple_op.simf b/fixtures/simf/deps/math/simf/simple_op.simf new file mode 100644 index 00000000..28e3e2c3 --- /dev/null +++ b/fixtures/simf/deps/math/simf/simple_op.simf @@ -0,0 +1,3 @@ +pub fn hash(x: u32, y: u32) -> u32 { + jet::xor_32(x, y) +} diff --git a/fixtures/simf/deps/merkle/Simplex.toml b/fixtures/simf/deps/merkle/Simplex.toml new file mode 100644 index 00000000..dcd08ec7 --- /dev/null +++ b/fixtures/simf/deps/merkle/Simplex.toml @@ -0,0 +1,31 @@ +# DEFAULT CONFIG + +#[build] +# src_dir = "./simf" +# simf_files = ["*.simf"] +# out_dir = "./src/artifacts" + +[dependencies] +math = { path = "../math" } + +# [regtest] +# mnemonic = "exist carry drive collect lend cereal occur much tiger just involve mean" +# bitcoins = 10_000_000 +# rpc_port = 18443 +# esplora_port = 3000 +# rpc_user = "user" +# rpc_password = "password" + +# [test] +# mnemonic = "exist carry drive collect lend cereal occur much tiger just involve mean" +# bitcoins = 10_000_000 +# verbosity = 0 + +# [test.esplora] +# url = "" +# network = "" + +# [test.rpc] +# url = "" +# username = "" +# password = "" diff --git a/fixtures/simf/deps/merkle/simf/build_root.simf b/fixtures/simf/deps/merkle/simf/build_root.simf new file mode 100644 index 00000000..4075d31e --- /dev/null +++ b/fixtures/simf/deps/merkle/simf/build_root.simf @@ -0,0 +1,13 @@ +use math::simple_op::hash as temp_hash; + +//pub mod wrapper { + pub fn get_root(tx1: u32, tx2: u32) -> u32 { + temp_hash(tx1, tx2) + } + + pub fn hash(tx1: u32, tx2: u32) -> u32 { + jet::and_32(tx1, tx2) + } +//} + +//pub use crate::wrapper::{get_root, hash}; diff --git a/fixtures/simf/imports/multidep.simf b/fixtures/simf/imports/multidep.simf new file mode 100644 index 00000000..b13d74c5 --- /dev/null +++ b/fixtures/simf/imports/multidep.simf @@ -0,0 +1,16 @@ +use merkle::build_root::{get_root, hash as and_hash}; +use base_math::simple_op::hash as or_hash; + +pub fn get_block_value_hash(prev_hash: u32, tx1: u32, tx2: u32) -> u32 { + let root: u32 = get_root(tx1, tx2); + or_hash(prev_hash, root) +} + +fn main() { + let block_val_hash: u32 = get_block_value_hash(param::PREV_HASH, 10, 20); + assert!(jet::eq_32(block_val_hash, 27)); + + let first_value: u32 = witness::TX1; + let second_value: u32 = 22; + assert!(jet::eq_32(and_hash(first_value, second_value), 6)); +} diff --git a/fixtures/tests/multidep_test.rs b/fixtures/tests/multidep_test.rs new file mode 100644 index 00000000..52b86684 --- /dev/null +++ b/fixtures/tests/multidep_test.rs @@ -0,0 +1,58 @@ +use simplex::simplicityhl::elements::Script; + +use simplex::transaction::{FinalTransaction, PartialInput, ProgramInput, RequiredSignature}; + +use simplex_fixtures::artifacts::imports::multidep::MultidepProgram; +use simplex_fixtures::artifacts::imports::multidep::derived_multidep::{MultidepArguments, MultidepWitness}; + +fn get_multidep(context: &simplex::TestContext) -> (MultidepProgram, Script) { + let arguments = MultidepArguments { prev_hash: 5 }; + + let p2pk = MultidepProgram::new(arguments); + let p2pk_script = p2pk.get_script_pubkey(context.get_network()); + + (p2pk, p2pk_script) +} + +fn spend_p2wpkh(context: &simplex::TestContext) -> anyhow::Result<()> { + let signer = context.get_default_signer(); + + let (_, script) = get_multidep(context); + + let tx_receipt = signer.send(script.clone(), 50)?; + println!("Broadcast: {}", tx_receipt); + + Ok(()) +} + +fn spend_p2pk(context: &simplex::TestContext) -> anyhow::Result<()> { + let signer = context.get_default_signer(); + let provider = context.get_default_provider(); + + let (program, script) = get_multidep(context); + + let utxos = provider.fetch_scripthash_utxos(&script)?; + + let mut ft = FinalTransaction::new(); + + let witness = MultidepWitness { tx1: 15 }; + + ft.add_program_input( + PartialInput::new(utxos[0].clone()), + ProgramInput::new(Box::new(program.as_ref().clone()), Box::new(witness.clone())), + RequiredSignature::None, + ); + + let tx_receipt = signer.broadcast(&ft)?; + println!("Broadcast: {}", tx_receipt); + + Ok(()) +} + +#[simplex::test] +fn multidep_test(context: simplex::TestContext) -> anyhow::Result<()> { + spend_p2wpkh(&context)?; + spend_p2pk(&context)?; + + Ok(()) +}