From e78e3b9ffdd5dee89f87d2f683567de436948093 Mon Sep 17 00:00:00 2001 From: Breno Brito Date: Wed, 4 Nov 2026 23:36:16 -0800 Subject: [PATCH 1/2] fix(build): propagate source discovery parse errors Fixes #175. Written by Codex. --- CHANGELOG.md | 4 ++ crates/build/src/error.rs | 3 + crates/build/src/resolver.rs | 7 +- crates/cli/tests/build_discovery.rs | 103 ++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 crates/cli/tests/build_discovery.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index dfa0d98c..431c82e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [Unreleased] + +- Made `simplex build` fail with the source path and parser diagnostic when a configured source cannot be parsed, instead of producing incomplete artifacts. + ## [0.2.0] - Added `simplex fmt` to format SimplicityHL sources with `simfmt`, which `simplexup` now installs. diff --git a/crates/build/src/error.rs b/crates/build/src/error.rs index b85e132e..97bd010a 100644 --- a/crates/build/src/error.rs +++ b/crates/build/src/error.rs @@ -95,6 +95,9 @@ pub enum BuildError { #[error("Failed to extract content from path, err: '{0}'")] FailedToExtractContent(io::Error), + #[error("Failed to parse source '{path}': {error}")] + SourceParse { path: PathBuf, error: String }, + #[error("Failed to generate file: {0}")] GenerationFailed(String), diff --git a/crates/build/src/resolver.rs b/crates/build/src/resolver.rs index c3665425..b8e787e9 100644 --- a/crates/build/src/resolver.rs +++ b/crates/build/src/resolver.rs @@ -44,7 +44,12 @@ impl ArtifactsResolver { match parse::Program::parse_from_str(&content) { Ok(program) if Self::rec_main_checker(program.items(), &FunctionName::main()) => paths.push(path), Ok(_) => {} - Err(error) => eprintln!("warning: skipping '{}' as it doesn't parse: {error}", path.display()), + Err(error) => { + return Err(BuildError::SourceParse { + path, + error: error.to_string(), + }); + } } } diff --git a/crates/cli/tests/build_discovery.rs b/crates/cli/tests/build_discovery.rs new file mode 100644 index 00000000..1a06dafb --- /dev/null +++ b/crates/cli/tests/build_discovery.rs @@ -0,0 +1,103 @@ +use std::fs; +use std::path::PathBuf; +use std::process::{Command, Output}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use smplx_cli::config::CONFIG_FILENAME; + +const MALFORMED_SOURCE: &str = + "fn main() {\n match false { false => (), true => panic!(), }\n let x: u8 = 1;\n}\n"; + +struct Project(PathBuf); + +impl Project { + fn new(name: &str, sources: &[(&str, &str)]) -> Result> { + let suffix = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos(); + let path = std::env::temp_dir().join(format!("smplx-build-{name}-{}-{suffix}", std::process::id())); + fs::create_dir_all(path.join("simf"))?; + fs::create_dir_all(path.join("src/artifacts"))?; + fs::write(path.join(CONFIG_FILENAME), "")?; + for (name, source) in sources { + fs::write(path.join("simf").join(name), source)?; + } + Ok(Self(path)) + } + + fn build(&self) -> std::io::Result { + Command::new(env!("CARGO_BIN_EXE_simplex")) + .arg("build") + .current_dir(&self.0) + .output() + } + + fn metadata(&self) -> Result> { + Ok(serde_json::from_slice(&fs::read( + self.0.join("src/artifacts/metadata.json"), + )?)?) + } +} + +impl Drop for Project { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +#[test] +fn malformed_entrypoint_fails_with_a_source_diagnostic() -> Result<(), Box> { + let project = Project::new("malformed", &[("contract.simf", MALFORMED_SOURCE)])?; + let output = project.build()?; + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!(!output.status.success(), "build incorrectly succeeded: {stderr}"); + assert!(stderr.contains("contract.simf"), "{stderr}"); + assert!(stderr.contains("Expected `;` after statement"), "{stderr}"); + assert!(!project.0.join("src/artifacts/metadata.json").exists()); + Ok(()) +} + +#[test] +fn malformed_source_prevents_a_partial_build() -> Result<(), Box> { + let project = Project::new( + "mixed", + &[("contract.simf", MALFORMED_SOURCE), ("good.simf", "fn main() {}")], + )?; + let metadata = project.0.join("src/artifacts/metadata.json"); + fs::write(&metadata, "previous build metadata\n")?; + let output = project.build()?; + + assert!( + !output.status.success(), + "build incorrectly succeeded: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!(fs::read_to_string(metadata)?, "previous build metadata\n"); + assert!(!project.0.join("src/artifacts/good.rs").exists()); + Ok(()) +} + +#[test] +fn repaired_entrypoint_builds() -> Result<(), Box> { + let repaired = MALFORMED_SOURCE.replace("}\n let", "};\n let"); + let project = Project::new("repaired", &[("contract.simf", &repaired)])?; + let output = project.build()?; + + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + assert!(project.metadata()?["sources"]["contract.simf"].is_object()); + Ok(()) +} + +#[test] +fn valid_helpers_without_main_are_skipped() -> Result<(), Box> { + let project = Project::new( + "helper", + &[("good.simf", "fn main() {}"), ("helper.simf", "pub fn helper() {}")], + )?; + let output = project.build()?; + + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + let metadata = project.metadata()?; + assert!(metadata["sources"]["good.simf"].is_object()); + assert_eq!(metadata["sources"].as_object().unwrap().len(), 1); + Ok(()) +} From 2230b302a22217eb3b2fd1ec11fb4290f206ffb0 Mon Sep 17 00:00:00 2001 From: Artem Chystiakov Date: Tue, 10 Nov 2026 17:27:49 +0200 Subject: [PATCH 2/2] clean up --- CHANGELOG.md | 4 -- crates/cli/tests/build_discovery.rs | 103 ---------------------------- 2 files changed, 107 deletions(-) delete mode 100644 crates/cli/tests/build_discovery.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 431c82e7..dfa0d98c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,5 @@ # Changelog -## [Unreleased] - -- Made `simplex build` fail with the source path and parser diagnostic when a configured source cannot be parsed, instead of producing incomplete artifacts. - ## [0.2.0] - Added `simplex fmt` to format SimplicityHL sources with `simfmt`, which `simplexup` now installs. diff --git a/crates/cli/tests/build_discovery.rs b/crates/cli/tests/build_discovery.rs deleted file mode 100644 index 1a06dafb..00000000 --- a/crates/cli/tests/build_discovery.rs +++ /dev/null @@ -1,103 +0,0 @@ -use std::fs; -use std::path::PathBuf; -use std::process::{Command, Output}; -use std::time::{SystemTime, UNIX_EPOCH}; - -use smplx_cli::config::CONFIG_FILENAME; - -const MALFORMED_SOURCE: &str = - "fn main() {\n match false { false => (), true => panic!(), }\n let x: u8 = 1;\n}\n"; - -struct Project(PathBuf); - -impl Project { - fn new(name: &str, sources: &[(&str, &str)]) -> Result> { - let suffix = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos(); - let path = std::env::temp_dir().join(format!("smplx-build-{name}-{}-{suffix}", std::process::id())); - fs::create_dir_all(path.join("simf"))?; - fs::create_dir_all(path.join("src/artifacts"))?; - fs::write(path.join(CONFIG_FILENAME), "")?; - for (name, source) in sources { - fs::write(path.join("simf").join(name), source)?; - } - Ok(Self(path)) - } - - fn build(&self) -> std::io::Result { - Command::new(env!("CARGO_BIN_EXE_simplex")) - .arg("build") - .current_dir(&self.0) - .output() - } - - fn metadata(&self) -> Result> { - Ok(serde_json::from_slice(&fs::read( - self.0.join("src/artifacts/metadata.json"), - )?)?) - } -} - -impl Drop for Project { - fn drop(&mut self) { - let _ = fs::remove_dir_all(&self.0); - } -} - -#[test] -fn malformed_entrypoint_fails_with_a_source_diagnostic() -> Result<(), Box> { - let project = Project::new("malformed", &[("contract.simf", MALFORMED_SOURCE)])?; - let output = project.build()?; - let stderr = String::from_utf8_lossy(&output.stderr); - - assert!(!output.status.success(), "build incorrectly succeeded: {stderr}"); - assert!(stderr.contains("contract.simf"), "{stderr}"); - assert!(stderr.contains("Expected `;` after statement"), "{stderr}"); - assert!(!project.0.join("src/artifacts/metadata.json").exists()); - Ok(()) -} - -#[test] -fn malformed_source_prevents_a_partial_build() -> Result<(), Box> { - let project = Project::new( - "mixed", - &[("contract.simf", MALFORMED_SOURCE), ("good.simf", "fn main() {}")], - )?; - let metadata = project.0.join("src/artifacts/metadata.json"); - fs::write(&metadata, "previous build metadata\n")?; - let output = project.build()?; - - assert!( - !output.status.success(), - "build incorrectly succeeded: {}", - String::from_utf8_lossy(&output.stderr) - ); - assert_eq!(fs::read_to_string(metadata)?, "previous build metadata\n"); - assert!(!project.0.join("src/artifacts/good.rs").exists()); - Ok(()) -} - -#[test] -fn repaired_entrypoint_builds() -> Result<(), Box> { - let repaired = MALFORMED_SOURCE.replace("}\n let", "};\n let"); - let project = Project::new("repaired", &[("contract.simf", &repaired)])?; - let output = project.build()?; - - assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); - assert!(project.metadata()?["sources"]["contract.simf"].is_object()); - Ok(()) -} - -#[test] -fn valid_helpers_without_main_are_skipped() -> Result<(), Box> { - let project = Project::new( - "helper", - &[("good.simf", "fn main() {}"), ("helper.simf", "pub fn helper() {}")], - )?; - let output = project.build()?; - - assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); - let metadata = project.metadata()?; - assert!(metadata["sources"]["good.simf"].is_object()); - assert_eq!(metadata["sources"].as_object().unwrap().len(), 1); - Ok(()) -}