From 5a4e6d9002ea928706ab76ed7e4b3bf76312dde2 Mon Sep 17 00:00:00 2001 From: Sdoba16 Date: Mon, 29 Jun 2026 11:38:32 +0200 Subject: [PATCH 1/5] Add version checking engine --- Cargo.lock | 7 + Cargo.toml | 1 + src/error.rs | 75 +++++++++ src/version.rs | 436 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 519 insertions(+) create mode 100644 src/version.rs diff --git a/Cargo.lock b/Cargo.lock index f923f7e9..e4c04378 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -579,6 +579,12 @@ dependencies = [ "secp256k1-sys", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.188" @@ -655,6 +661,7 @@ dependencies = [ "getrandom", "itertools", "miniscript", + "semver", "serde", "serde_json", "simplicity-lang", diff --git a/Cargo.toml b/Cargo.toml index 0d43699e..45f167b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ itertools = "0.13.0" arbitrary = { version = "1", optional = true, features = ["derive"] } clap = "4.5.37" chumsky = "0.11.2" +semver = "1.0.27" [target.wasm32-unknown-unknown.dependencies] getrandom = { version = "0.2", features = ["js"] } diff --git a/src/error.rs b/src/error.rs index dc63c10c..1393e3c0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -482,6 +482,16 @@ pub enum Error { UnstableFeature { feature: UnstableFeature, }, + MissingSimcVersion { + compiler: String, + }, + InvalidSimcVersionSyntax { + err: String, + }, + SimcVersionMismatch { + required: String, + current: String, + }, DependencyPathNotFound { path: PathBuf, }, @@ -657,6 +667,19 @@ impl fmt::Display for Error { f, "The '{feature}' feature is not enabled.\nEnable it with: -Z {feature}" ), + Error::MissingSimcVersion { compiler } => write!( + f, + "Missing compiler version: Contract must declare a version, e.g., `{}{}{}`", crate::version::DIRECTIVE_PREFIX, compiler, crate::version::DIRECTIVE_SUFFIX + ), + Error::InvalidSimcVersionSyntax { err } => write!( + f, + "Invalid version syntax: {}", err + ), + Error::SimcVersionMismatch { required, current } => write!( + f, + "Incompatible compiler version: file requires `{}`, but the compiler is `{}`. Update the compiler or the `simc` directive.", + required, current + ), Error::DependencyPathNotFound { path } => write!( f, "Path not found: {}", path.display() @@ -1099,6 +1122,58 @@ let x: u32 = Left( assert_eq!(&expected[1..], &error.to_string()); } + #[test] + fn display_compiler_version_missing() { + let file = "fn main() {}"; + let error = Error::MissingSimcVersion { + compiler: "0.5.0".to_string(), + } + .with_span(Span::new(0, 0)) + .with_content(Arc::from(file)); + + let expected = r#" + | +1 | fn main() {} + | ^ Missing compiler version: Contract must declare a version, e.g., `simc "0.5.0";`"#; + + assert_eq!(&expected[1..], &error.to_string()); + } + + #[test] + fn display_compiler_version_invalid_syntax() { + let file = "simc \"abc\";\nfn main() {}"; + let error = Error::InvalidSimcVersionSyntax { + err: "unexpected character 'a'".to_string(), + } + .with_span(Span::new(0, 11)) + .with_content(Arc::from(file)); + + let expected = r#" + | +1 | simc "abc"; + | ^^^^^^^^^^^ Invalid version syntax: unexpected character 'a'"#; + + assert_eq!(&expected[1..], &error.to_string()); + } + + #[test] + fn display_compiler_version_mismatch() { + let file = "simc \">= 0.6.0\";\nfn main() {}"; + let error = Error::SimcVersionMismatch { + required: ">= 0.6.0".to_string(), + current: "0.5.0".to_string(), + } + .with_span(Span::new(0, 16)) + .with_content(Arc::from(file)); + + let expected = r#" + | +1 | simc ">= 0.6.0"; + | ^^^^^^^^^^^^^^^^ Incompatible compiler version: file requires `>= 0.6.0`, but the compiler is `0.5.0`. Update the compiler or the `simc` directive."#; + + assert_eq!(&expected[1..], &error.to_string()); + } + // --- Tests with filename --- #[test] fn display_single_line_with_file() { diff --git a/src/version.rs b/src/version.rs new file mode 100644 index 00000000..d1fa9db0 --- /dev/null +++ b/src/version.rs @@ -0,0 +1,436 @@ +//! Compiler-version directives (`simc "...";`). User-facing documentation lives +//! in `doc/versioning.md`; the rustdoc here is the developer's view of how the +//! directive is parsed and enforced. +//! +//! Reading order mirrors the data flow: the compiler-side check first, then the +//! tooling-side reader, then the [`VersionRequirement`] semver wrapper, and finally +//! the raw-text scanning the rest of the module is built on. + +use std::borrow::Cow; + +use semver::{Version, VersionReq}; + +use crate::error::{Error, ErrorCollector, RichError, Span}; +use crate::source::SourceFile; + +pub const DIRECTIVE_PREFIX: &str = "simc \""; +pub const DIRECTIVE_SUFFIX: &str = "\";"; + +/// When `true`, every file must declare a `simc "...";` directive (missing → error). +/// Flip to `false` to make it opt-in: undeclared files skip the check, declared ones +/// are still enforced. +/// TODO: delete before merging +pub const REQUIRE_VERSION_DIRECTIVE: bool = true; + +/// The running compiler's version (`CARGO_PKG_VERSION`). +pub fn current_version() -> &'static str { + env!("CARGO_PKG_VERSION") +} + +/// Validate the leading `simc "...";` directive against the running compiler, +/// returning the diagnostic and its span on failure. Works on raw content, +/// independent of parsing the body, so directive errors surface clearly. +pub fn check(content: &str) -> Result<(), (Error, Span)> { + let (req_str, span) = match extract_version_directive(content) { + Ok(Some(found)) => found, + Ok(None) => { + // A malformed `simc` directive is a syntax error, not an absent one. + if let Some(span) = malformed_leading_directive(content) { + let err = Error::InvalidSimcVersionSyntax { + err: malformed_directive_message(), + }; + return Err((err, span)); + } + if REQUIRE_VERSION_DIRECTIVE { + let err = Error::MissingSimcVersion { + compiler: current_version().to_string(), + }; + return Err((err, Span::new(0, 0))); + } + return Ok(()); + } + Err(dup_span) => { + return Err(( + Error::Syntax { + expected: vec!["Exactly one compiler version directive".to_string()], + label: None, + found: Some("Multiple directives".to_string()), + }, + dup_span, + )); + } + }; + + let required = req_str.trim(); + let req = VersionRequirement::parse(required) + .map_err(|e| (Error::InvalidSimcVersionSyntax { err: e }, span))?; + + let current = current_version(); + let current_ver = Version::parse(current).expect("CARGO_PKG_VERSION is valid semver"); + if !req.matches(¤t_ver) { + let err = Error::SimcVersionMismatch { + required: required.to_string(), + current: current.to_string(), + }; + return Err((err, span)); + } + + Ok(()) +} + +/// Run [`check`] on a source file and record any diagnostic in `handler`. The +/// per-file entry point used by the driver and `TemplateProgram`. +pub fn check_source + Clone>(source: &S, handler: &mut ErrorCollector) { + let source_file: SourceFile = source.clone().into(); + if let Err((err, span)) = check(&source_file.content()) { + handler.push(RichError::new(err, span).with_source(source_file)); + } +} + +/// Error from reading a file's version directive with [`requirement_of`]. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum DirectiveError { + Duplicate, + InvalidSyntax(String), +} + +/// Cheaply read a file's declared version requirement (leading directive only, no +/// lexing), for external tooling such as Simplex. `Ok(None)` when absent. A malformed +/// directive is reported as [`DirectiveError::InvalidSyntax`], the same as [`check`], +/// so a typo is never silently treated as "no directive". +pub fn requirement_of(content: &str) -> Result, DirectiveError> { + match extract_version_directive(content) { + Ok(Some((req_str, _span))) => VersionRequirement::parse(req_str.trim()) + .map(Some) + .map_err(DirectiveError::InvalidSyntax), + Ok(None) if malformed_leading_directive(content).is_some() => { + Err(DirectiveError::InvalidSyntax(malformed_directive_message())) + } + Ok(None) => Ok(None), + Err(_dup_span) => Err(DirectiveError::Duplicate), + } +} + +/// A parsed directive requirement (the text inside the quotes). Wraps +/// [`semver::VersionReq`] to add the compiler-aware pre-release handling in +/// [`Self::matches`]. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VersionRequirement { + req: VersionReq, +} + +impl VersionRequirement { + /// Parse a requirement string such as `>=0.6.0` or `=0.6.0`. + pub fn parse(s: &str) -> Result { + VersionReq::parse(s) + .map(|req| VersionRequirement { req }) + .map_err(|e| e.to_string()) + } + + /// The underlying requirement, for external tooling that + /// intersects ranges across a project's files. + pub fn req(&self) -> &VersionReq { + &self.req + } + + /// Whether `version` satisfies the requirement, after pre-release + /// normalization (see [`Self::effective_version`]). + pub fn matches(&self, version: &Version) -> bool { + self.req.matches(&self.effective_version(version)) + } + + /// Strip the compiler's pre-release tag (`0.6.0-rc.0` → `0.6.0`) when the + /// requirement names no pre-release, so a release range still accepts a matching + /// pre-release compiler. Without this, semver would reject `0.6.0-rc.0` for a + /// plain `>=0.6.0`. + fn effective_version(&self, version: &Version) -> Version { + let req_allows_pre = self.req.comparators.iter().any(|c| !c.pre.is_empty()); + if req_allows_pre || version.pre.is_empty() { + version.clone() + } else { + Version { + pre: semver::Prerelease::EMPTY, + ..version.clone() + } + } + } +} + +/// Prepend a directive for the running compiler. Used by tests and tooling that +/// emit `.simf` source under the required-directive policy. +pub fn inject_version_header(content: &str) -> String { + let current = current_version(); + format!("{DIRECTIVE_PREFIX}{current}{DIRECTIVE_SUFFIX}\n{content}") +} + +/// Remove the directive and its trailing newline — the inverse of +/// [`inject_version_header`]. +pub fn strip_version_header(content: &str) -> String { + let Ok(Some((_, span))) = extract_version_directive(content) else { + return content.to_string(); + }; + let rest = &content[span.end..]; + let rest = rest + .strip_prefix("\r\n") + .or_else(|| rest.strip_prefix('\n')) + .unwrap_or(rest); + format!("{}{rest}", &content[..span.start]) +} + +/// Whether the file declares a directive. +pub fn has_version_header(content: &str) -> bool { + matches!(extract_version_directive(content), Ok(Some(_))) +} + +/// Replace the directive with equal-length spaces so the parser never sees it +/// while later byte offsets (and thus error spans) stay correct. +pub(crate) fn blank_version_directive(content: &str) -> Cow<'_, str> { + let directives: Vec<_> = leading_directives(content).collect(); + if directives.is_empty() { + return Cow::Borrowed(content); + } + + let mut buf = content.to_string(); + // Blank duplicates too, so `check` reports them instead of a confusing parse error. + for (_, span) in &directives { + buf.replace_range(span.start..span.end, &" ".repeat(span.end - span.start)); + } + // A directive-only file blanks to whitespace; an empty program is correct. + Cow::Owned(if buf.trim().is_empty() { + String::new() + } else { + buf + }) +} + +/// The leading lines that carry code or directives, as `(full line, trimmed line, +/// byte offset of the line)`. Comments and blank lines are skipped; offsets keep +/// counting through them so spans stay correct. +fn meaningful_lines(content: &str) -> impl Iterator { + let mut in_block_comment = false; + let mut offset = 0; + content.split_inclusive('\n').filter_map(move |line| { + let start = offset; + offset += line.len(); + let trimmed = skip_block_comments(line.trim_start(), &mut in_block_comment); + let skippable = in_block_comment || trimmed.is_empty() || trimmed.starts_with("//"); + (!skippable).then_some((line, trimmed, start)) + }) +} + +/// The leading `simc "...";` directives in order, stopping at the first line of +/// program code. More than one item means the file declared duplicates. +fn leading_directives(content: &str) -> impl Iterator { + meaningful_lines(content) + .map_while(|(line, trimmed, offset)| extract_directive_from_line(line, trimmed, offset)) +} + +/// The file's single declared directive (requirement string and span), `Ok(None)` +/// if absent, or the span of the offending second directive when more than one. +fn extract_version_directive(content: &str) -> Result, Span> { + let mut dirs = leading_directives(content); + match (dirs.next(), dirs.next()) { + (None, _) => Ok(None), + (Some(first), None) => Ok(Some(first)), + // More than one directive: report the second one's span as the offender. + (Some(_), Some(second)) => Err(second.1), + } +} + +/// Parse one line as a `simc "...";` directive, returning the requirement string +/// and the span covering `simc "...";`. +fn extract_directive_from_line<'a>( + line: &str, + trimmed: &'a str, + current_offset: usize, +) -> Option<(&'a str, Span)> { + if !trimmed.starts_with("simc") { + return None; + } + let after_simc = &trimmed[4..]; + if !after_simc.is_empty() && !after_simc.starts_with(|c: char| c.is_whitespace() || c == '"') { + return None; + } + + let rest = after_simc.trim_start(); + let rest = rest.strip_prefix('"')?; + let end_quote_idx = rest.find('"')?; + let req_str = &rest[..end_quote_idx]; + + let after_quote = rest[end_quote_idx + 1..].trim_start(); + if !after_quote.starts_with(';') { + return None; + } + + // Both `trimmed` and `after_quote` are suffixes of `line`, so their lengths + // give the byte offsets directly — no pointer arithmetic needed. + let span_start = current_offset + (line.len() - trimmed.len()); + let span_end = span_start + (trimmed.len() - after_quote.len()) + 1; + + Some((req_str, Span::new(span_start, span_end))) +} + +/// Skip past `/* ... */` so a directive may follow a leading comment block. +fn skip_block_comments<'a>(mut trimmed: &'a str, in_block_comment: &mut bool) -> &'a str { + loop { + if *in_block_comment { + if let Some(end_idx) = trimmed.find("*/") { + trimmed = trimmed[end_idx + 2..].trim_start(); + *in_block_comment = false; + } else { + break; + } + } else if let Some(rest) = trimmed.strip_prefix("/*") { + *in_block_comment = true; + trimmed = rest; + } else { + break; + } + } + trimmed +} + +/// Span of a leading line that attempts a `simc` directive but is malformed (e.g. +/// missing the closing quote or semicolon), or `None` for ordinary program code. +fn malformed_leading_directive(content: &str) -> Option { + // Only the first meaningful line can be a leading directive. + let (line, trimmed, offset) = meaningful_lines(content).next()?; + let after = trimmed.strip_prefix("simc")?; + let looks_like_directive = + after.is_empty() || after.starts_with(|c: char| c.is_whitespace() || c == '"'); + if !looks_like_directive || extract_directive_from_line(line, trimmed, offset).is_some() { + return None; + } + let start = offset + (line.len() - trimmed.len()); + Some(Span::new(start, start + trimmed.trim_end().len())) +} + +/// The message shared by [`check`] and [`requirement_of`] for a leading line that +/// looks like a directive but is malformed, so the two agree on the wording. +fn malformed_directive_message() -> String { + format!("malformed compiler version directive; expected `{DIRECTIVE_PREFIX}{DIRECTIVE_SUFFIX}`") +} + +#[cfg(test)] +mod tests { + use super::*; + + /// `matches` accepts or rejects requirements against the running compiler, + /// exercising the pre-release normalization in `effective_version`: a release + /// range still accepts the pre-release compiler, but semver pre-release gating + /// (and reordered compound ranges) still bite. `0.6.0-rc.0` stands in for the + /// compiler. + #[test] + fn matches_respects_operators_and_prerelease() { + let cur = Version::parse("0.6.0-rc.0").unwrap(); + let accepted = [ + "*", + "0.6.0", + "^0.6.0", + "~0.6.0", + ">=0.6.0", + ">0.1.0", + "=0.6.0-rc.0", + "^0.6.0-rc.0", + ]; + let rejected = [ + "=0.5.0", + ">99.0.0", + "<0.0.1", + "<0.6.0", // -rc tag stripped, so 0.6.0 is not < 0.6.0 + ">=0.1.0-alpha.1", // pre-release gating: different base, so no match + ">=0.7.0, =0.6.0", // the `=0.6.0` must not rescue the failing `>=0.7.0` + ]; + for req in accepted { + let req = VersionRequirement::parse(req).unwrap(); + assert!(req.matches(&cur), "`{req:?}` should match {cur}"); + } + for req in rejected { + let parsed = VersionRequirement::parse(req).unwrap(); + assert!(!parsed.matches(&cur), "`{req}` should not match {cur}"); + } + } + + #[test] + fn malformed_leading_directive_is_syntax_error_not_missing() { + // A directive attempt with a missing semicolon or closing quote is a syntax + // error, not an absent directive. + for src in [ + "simc \"=0.5.0\"\nfn main() {}", + "simc \"=0.5.0\nfn main() {}", + ] { + let (err, _span) = check(src).unwrap_err(); + assert!( + matches!(err, Error::InvalidSimcVersionSyntax { .. }), + "{src:?}" + ); + } + } + + #[test] + fn directive_scanned_through_leading_comments() { + // The directive is the first *meaningful* line: leading `//` lines, blank + // lines, and `/* */` blocks (multi-line or inline) are skipped, and the span + // still lands exactly on `simc "...";` because offsets keep counting through + // the skipped text. `*` matches any compiler, so this stays version-bump proof. + for src in [ + "// header\n// notes\n\n/* a block\n comment */\nsimc \"*\";\nfn main() {}", + "/* lead */ simc \"*\";\nfn main() {}", + ] { + assert!( + check(src).is_ok(), + "should accept directive after comments: {src:?}" + ); + let (req, span) = extract_version_directive(src) + .unwrap() + .expect("directive found past the comments"); + assert_eq!(req, "*", "{src:?}"); + assert_eq!( + &src[span.start..span.end], + "simc \"*\";", + "span must cover the directive in {src:?}" + ); + } + + // A commented-out directive does not count; the real one after it does. + let commented = "// simc \"=99.0.0\";\nsimc \"*\";\nfn main() {}"; + let (req, _) = extract_version_directive(commented).unwrap().unwrap(); + assert_eq!(req, "*"); + } + + #[test] + fn version_header_roundtrip() { + let body = "fn main() {}"; + let injected = inject_version_header(body); + assert!(injected.starts_with(DIRECTIVE_PREFIX)); + assert!(has_version_header(&injected)); + assert!(!has_version_header(body)); + assert_eq!(strip_version_header(&injected), body); + } + + #[test] + fn requirement_of_reads_or_rejects_directive() { + // `req()` exposes the underlying semver requirement for tooling that + // intersects ranges across a project's files (e.g. Simplex). + let parsed = requirement_of("simc \">=0.1.0\";\nfn main() {}") + .unwrap() + .expect("directive present"); + assert_eq!(parsed.req(), &VersionReq::parse(">=0.1.0").unwrap()); + assert_eq!(requirement_of("fn main() {}"), Ok(None)); + assert!(matches!( + requirement_of("simc \"not-a-version\";\nfn main() {}"), + Err(DirectiveError::InvalidSyntax(_)) + )); + // A malformed directive is a syntax error here too, not silently "absent", + // so tooling and the compiler agree (the missing semicolon below). + assert!(matches!( + requirement_of("simc \"=0.1.0\"\nfn main() {}"), + Err(DirectiveError::InvalidSyntax(_)) + )); + // Duplicates are only reachable through this external entry point. + assert_eq!( + requirement_of("simc \"=0.1.0\";\nsimc \"=0.2.0\";\nfn main() {}"), + Err(DirectiveError::Duplicate) + ); + } +} From 35199391eda04733cf923e1559ed71827fef674e Mon Sep 17 00:00:00 2001 From: Sdoba16 Date: Mon, 29 Jun 2026 11:38:46 +0200 Subject: [PATCH 2/5] Integrate version checking engine into compiler --- src/driver/mod.rs | 19 +++++- src/lexer.rs | 7 +- src/lib.rs | 147 +++++++++++++++++++++++++++++++++++------- src/parse.rs | 12 +++- src/tracker.rs | 12 +++- src/witness.rs | 21 ++++-- tests/core_tracker.rs | 3 +- 7 files changed, 179 insertions(+), 42 deletions(-) diff --git a/src/driver/mod.rs b/src/driver/mod.rs index 35a3e6d4..5a04ee4a 100644 --- a/src/driver/mod.rs +++ b/src/driver/mod.rs @@ -29,6 +29,9 @@ mod linearization; mod resolve_order; +#[cfg(test)] +mod version_tests; + use std::collections::{HashMap, HashSet, VecDeque}; use std::path::PathBuf; use std::sync::Arc; @@ -38,6 +41,7 @@ use crate::parse::{self, ParseFromStrWithErrors}; use crate::resolution::{DependencyMap, ResolvedUse}; use crate::source::{CanonPath, CanonSourceFile}; use crate::unstable::UnstableFeatures; +use crate::version::check_source; /// The reserved identifier for the program's entry point. pub(crate) const MAIN_STR: &str = "main"; @@ -268,6 +272,8 @@ impl DependencyGraph { let mut error_handler = ErrorCollector::new(); let source = CanonSourceFile::new(path.clone(), Arc::from(content)); + check_source(&source, &mut error_handler); + let ast = parse::Program::parse_from_str_with_errors( source.clone(), unstable_features, @@ -450,6 +456,7 @@ pub(crate) mod tests { use super::*; use crate::resolution::tests::{build_map, canon}; use crate::test_utils::TempWorkspace; + use crate::version::{has_version_header, inject_version_header}; /// Initializes a raw graph environment for testing, explicitly allowing for and capturing failure states. /// @@ -496,17 +503,25 @@ pub(crate) mod tests { // Create all requested files for (path, content) in files { let full_path = format!("workspace/{}", path); - let created_file = canon(&ws.create_file(&full_path, content)); + let injected_content = + if has_version_header(content) || content.contains("// NO_INJECT") { + content.to_string() + } else { + inject_version_header(content) + }; + let created_file = canon(&ws.create_file(&full_path, &injected_content)); if path == "main.simf" { root_file_path = Some(created_file); - root_content = content.to_string(); + root_content = injected_content; } } let root_p = root_file_path.expect("main.simf must be defined in file list"); let main_canon_source = CanonSourceFile::new(root_p, Arc::from(root_content)); + check_source(&main_canon_source, &mut handler); + let main_program_option = parse::Program::parse_from_str_with_errors( main_canon_source.clone(), &UnstableFeatures::all(), diff --git a/src/lexer.rs b/src/lexer.rs index 06d63adc..ca8595d6 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -366,10 +366,13 @@ mod tests { fn lexer_test() { use chumsky::prelude::*; - // Check if the lexer parses the example file without errors. + // Check if the lexer parses the example file without errors. The leading + // `simc "...";` directive is blanked before lexing in every production path + // (it is not a language token), so mirror that here. let src = include_str!("../examples/last_will.simf"); + let src = crate::version::blank_version_directive(src); - let (tokens, lex_errs) = lexer().parse(src).into_output_errors(); + let (tokens, lex_errs) = lexer().parse(src.as_ref()).into_output_errors(); let _ = tokens.unwrap(); assert!(lex_errs.is_empty()); diff --git a/src/lib.rs b/src/lib.rs index 46a9836d..a1f26c86 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,7 @@ pub mod test_utils; pub mod tracker; pub mod types; pub mod value; +pub mod version; mod witness; use std::sync::Arc; @@ -48,6 +49,7 @@ use crate::source::SourceFile; pub use crate::types::ResolvedType; pub use crate::unstable::{UnstableFeature, UnstableFeatures}; pub use crate::value::Value; +use crate::version::check_source; pub use crate::witness::{Arguments, Parameters, WitnessTypes, WitnessValues}; /// The template of a SimplicityHL program. @@ -153,6 +155,7 @@ impl TemplateProgram { let source = SourceFile::anonymous(file.clone()); let mut error_handler = ErrorCollector::new(); + check_source(&source, &mut error_handler); let parse_program = parse::Program::parse_from_str_with_errors( source, unstable_features, @@ -160,6 +163,9 @@ impl TemplateProgram { ); if let Some(program) = parse_program { + if error_handler.has_errors() { + return Err(error_handler); + } let Ok(ast_program) = ast::Program::analyze(&program, jet_hinter.clone_box()) .with_content(Arc::clone(&file)) .map_err(|e| error_handler.push(e)) @@ -185,10 +191,16 @@ impl TemplateProgram { unstable_features: &UnstableFeatures, handler: &mut ErrorCollector, ) -> Result<(Option, SourceMap), String> { + // Run the version check first, and regardless of parse success, so a + // missing or incompatible directive leads the diagnostics and explains a + // parse failure instead of being hidden behind a raw lexer/parser error. + check_source(&source, handler); let program = parse::Program::parse_from_str_with_errors(source.clone(), unstable_features, handler) .ok_or_else(|| handler.to_string())?; - + if handler.has_errors() { + return Err(handler.to_string()); + } // TODO: we should remove this errors push after refactoring errors let graph = DependencyGraph::new( source, @@ -556,6 +568,7 @@ pub(crate) mod tests { use crate::resolution::DependencyMapBuilder; use crate::source::CanonPath; use crate::test_utils::TempWorkspace; + use crate::version::{inject_version_header, strip_version_header, REQUIRE_VERSION_DIRECTIVE}; use base64::display::Base64Display; use base64::engine::general_purpose::STANDARD; use simplicity::BitMachine; @@ -673,8 +686,11 @@ pub(crate) mod tests { program_text: Cow, unstable_features: UnstableFeatures, ) -> Self { + let clean_text = strip_version_header(&program_text); + let injected_text = inject_version_header(&clean_text); + let program = match TemplateProgram::new_with_unstable( - program_text.as_ref(), + injected_text.as_str(), &unstable_features, Box::new(ElementsJetHinter::new()), ) { @@ -1038,12 +1054,14 @@ pub(crate) mod tests { let ws = TempWorkspace::new("crate_success"); let root = ws.create_dir("workspace"); ws.create_file( - format!("workspace/{MAIN}").as_str(), - "use crate::utils::add;\nfn main() { assert!(jet::eq_32(add(2, 2), 4)); }", + "workspace/main.simf", + &inject_version_header( + "use crate::utils::add;\nfn main() { assert!(jet::eq_32(add(2, 2), 4)); }", + ), ); ws.create_file( "workspace/utils.simf", - "pub fn add(a: u32, b: u32) -> u32 { let (_, sum): (bool, u32) = jet::add_32(a, b); sum }", + &inject_version_header("pub fn add(a: u32, b: u32) -> u32 { let (_, sum): (bool, u32) = jet::add_32(a, b); sum }"), ); let main_path = root.join(MAIN); @@ -1063,11 +1081,12 @@ pub(crate) mod tests { #[test] fn test_anonymous_source_compiles_without_dependencies() { - let code = "fn main() { assert!(true); }"; - let program = TemplateProgram::new(code, Box::new(ElementsJetHinter::new())); + let code = inject_version_header("fn main() { assert!(true); }"); + let program = TemplateProgram::new(code.as_str(), Box::new(ElementsJetHinter::new())); assert!( program.is_ok(), - "TemplateProgram::new should successfully compile anonymous source files without requiring canonical paths" + "TemplateProgram::new should successfully compile anonymous source files without requiring canonical paths, error: {:?}", + program.err() ); } @@ -1264,14 +1283,17 @@ pub(crate) mod tests { #[test] fn empty_function_body_nonempty_return() { - let prog_text = r#"fn my_true() -> bool { + let prog_text = inject_version_header( + r#" +fn my_true() -> bool { // function body is empty, although function must return `bool` } fn main() { assert!(my_true()); } -"#; +"#, + ); match SatisfiedProgram::new( prog_text, Arguments::default(), @@ -1329,12 +1351,14 @@ fn main() { #[test] fn test_compilation_against_different_jet_hinters() { - let code = r#"fn main() { + let code = inject_version_header( + r#"fn main() { let (_, sum): (bool, u32) = jet::add_32(10, 20); assert!(jet::eq_32(sum, 30)); let and_result: u32 = jet::and_32(0xFF00FF00, 0x0F0F0F0F); assert!(jet::eq_32(and_result, 0x0F000F00)); -}"#; +}"#, + ); let hinters: Vec> = vec![ Box::new(CoreJetHinter::new()), @@ -1342,7 +1366,7 @@ fn main() { ]; for hinter in hinters { - let program = TemplateProgram::new(code, hinter); + let program = TemplateProgram::new(code.as_str(), hinter); assert!( program.is_ok(), "TemplateProgram::new should successfully compile the same program with different jet hinters: {:?}", @@ -1354,21 +1378,24 @@ fn main() { #[test] fn test_fail_with_different_jet_hinters() { // Uses jets that exist only in Elements (not in Core). - let code = r#"fn main() { + let code = inject_version_header( + r#"fn main() { let v: u32 = jet::version(); let idx: u32 = jet::current_index(); assert!(jet::eq_32(v, v)); assert!(jet::eq_32(idx, idx)); -}"#; +}"#, + ); - let elements_result = TemplateProgram::new(code, Box::new(ElementsJetHinter::new())); + let elements_result = + TemplateProgram::new(code.as_str(), Box::new(ElementsJetHinter::new())); assert!( elements_result.is_ok(), "ElementsJetHinter should compile Elements-specific jets: {:?}", elements_result.err(), ); - let core_result = TemplateProgram::new(code, Box::new(CoreJetHinter::new())); + let core_result = TemplateProgram::new(code.as_str(), Box::new(CoreJetHinter::new())); assert!( core_result.is_err(), "CoreJetHinter should fail to compile Elements-specific jets", @@ -1524,19 +1551,86 @@ fn main() { regression_test("transfer_with_timeout"); } } + + /// Asserts a missing directive is rejected when required and allowed otherwise, + /// so the suite passes under either `version::REQUIRE_VERSION_DIRECTIVE` setting. + fn assert_directive_omitted(result: Result) { + if REQUIRE_VERSION_DIRECTIVE { + assert!(result + .unwrap_err() + .to_string() + .contains("Missing compiler version")); + } else { + assert!(result.is_ok(), "expected success, got: {:?}", result.err()); + } + } + + #[test] + fn directive_omitted() { + let missing = "fn main() {}"; + assert_directive_omitted(TemplateProgram::new( + missing, + Box::new(crate::ast::ElementsJetHinter::new()), + )); + } + + #[test] + fn directive_omitted_with_empty_dependency_map() { + let missing = "fn main() {}"; + let temp = + crate::test_utils::TempWorkspace::new("directive_omitted_with_empty_dependency_map"); + let root = crate::source::CanonPath::canonicalize(&temp.create_dir("test")).unwrap(); + let source = CanonSourceFile::new(root.clone(), Arc::from(missing)); + let empty_map = crate::resolution::DependencyMapBuilder::new() + .build(root) + .unwrap(); + assert_directive_omitted(TemplateProgram::new_with_dep( + source, + &empty_map, + &UnstableFeatures::none(), + Box::new(crate::ast::ElementsJetHinter::new()), + )); + } + + // Smoke tests that the version check is wired into `TemplateProgram::new`: one + // compatible directive compiles, one incompatible directive aborts. The semver + // matching and per-kind messages are covered exhaustively in `version`'s unit + // tests, so they are not re-asserted through the pipeline here. + #[test] + fn compatible_directive_compiles() { + let exact = inject_version_header("fn main() {}"); + assert!(TemplateProgram::new( + exact.as_str(), + Box::new(crate::ast::ElementsJetHinter::new()) + ) + .is_ok()); + } + + #[test] + fn incompatible_directive_aborts() { + let too_old = "simc \">= 99.99.99\";\nfn main() {}"; + let err = TemplateProgram::new(too_old, Box::new(crate::ast::ElementsJetHinter::new())) + .unwrap_err() + .to_string(); + assert!( + err.contains("Incompatible compiler version"), + "Expected 'Incompatible compiler version', got: {}", + err + ); + } } #[cfg(test)] mod error_tests { use std::path::Path; - use super::tests::MAIN; use super::*; use crate::ast::ElementsJetHinter; use crate::resolution::tests::{build_map, canon}; use crate::source::CanonPath; use crate::test_utils::TempWorkspace; + use crate::version::inject_version_header; fn dependency_map(root_dir: &Path, drp: &str, lib_dir: &Path) -> DependencyMap { let context = CanonPath::canonicalize(root_dir).unwrap(); @@ -1557,12 +1651,12 @@ mod error_tests { let root_dir = ws.create_dir("workspace"); let lib_dir = ws.create_dir("workspace/lib"); let main_path = ws.create_file( - format!("workspace/{MAIN}").as_str(), + "workspace/main.simf", "use lib::bad::f;\nfn main() { f(); }\n", ); let bad_path = ws.create_file( "workspace/lib/bad.simf", - "pub fn f() { let x: u32 = true; }\n", + &inject_version_header("pub fn f() { let x: u32 = true; }\n"), ); let dependencies = dependency_map(&root_dir, "lib", &lib_dir); @@ -1588,14 +1682,17 @@ mod error_tests { let root_dir = ws.create_dir("workspace"); let lib_dir = ws.create_dir("workspace/lib"); let main_path = ws.create_file( - format!("workspace/{MAIN}").as_str(), + "workspace/main.simf", "use lib::nested::two;\nfn main() { assert!(jet::eq_32(two(), 2)); }\n", ); ws.create_file( "workspace/lib/nested.simf", - "use lib::base::one;\npub fn two() -> u32 {\n let (_, out): (bool, u32) = jet::add_32(one(), 1);\n out\n}\n", + &inject_version_header("use lib::base::one;\npub fn two() -> u32 {\n let (_, out): (bool, u32) = jet::add_32(one(), 1);\n out\n}\n"), + ); + ws.create_file( + "workspace/lib/base.simf", + &inject_version_header("pub fn one() -> u32 { 1 }\n"), ); - ws.create_file("workspace/lib/base.simf", "pub fn one() -> u32 { 1 }\n"); let dependencies = dependency_map(&root_dir, "lib", &lib_dir); let _err = TemplateProgram::new_with_dep( @@ -1613,8 +1710,8 @@ mod error_tests { let root_dir = ws.create_dir("workspace"); let lib_dir = ws.create_dir("workspace/lib"); let main_path = ws.create_file( - format!("workspace/{MAIN}").as_str(), - "use lib::missing::Thing;\nfn main() {}\n", + "workspace/main.simf", + &inject_version_header("use lib::missing::Thing;\nfn main() {}\n"), ); let dependencies = dependency_map(&root_dir, "lib", &lib_dir); diff --git a/src/parse.rs b/src/parse.rs index 61eede9d..190da1a1 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -31,6 +31,7 @@ use crate::str::{ }; use crate::types::{AliasedType, BuiltinAlias, TypeConstructible, UIntType}; use crate::unstable::{impl_require_feature, UnstableFeature, UnstableFeatures}; +use crate::version::blank_version_directive; /// A program is a sequence of items. #[derive(Clone, Debug)] @@ -1239,7 +1240,8 @@ type ParseError<'src> = extra::Err; /// This implementation only returns first encountered error. impl ParseFromStr for A { fn parse_from_str(s: &str) -> Result { - let (tokens, mut lex_errs) = crate::lexer::lex(s); + let blanked = blank_version_directive(s); + let (tokens, mut lex_errs) = crate::lexer::lex(&blanked); let Some(tokens) = tokens else { return Err(lex_errs.pop().unwrap_or(RichError::parsing_error( @@ -1274,7 +1276,11 @@ impl ParseF handler: &mut ErrorCollector, ) -> Option { let source: SourceFile = source.into(); - let src = source.content().to_string(); + let original = source.content().to_string(); + // Blank the `simc "...";` directive (replacing it with equal-length spaces) + // before lexing, so the grammar never sees it while byte offsets — and thus + // error spans — stay aligned with the original source. + let src = blank_version_directive(&original); let (tokens, lex_errs) = crate::lexer::lex(&src); let lex_ok = lex_errs.is_empty(); @@ -1533,6 +1539,8 @@ impl ChumskyParse for Item { // Lazy item here let mod_parser = Module::parser_with_items(item).map(Item::Module); + // The `simc "...";` directive is removed from the source before lexing + // (see `version::blank_version_directive`), so the grammar never sees it. choice((func_parser, use_parser, type_parser, mod_parser)) }) } diff --git a/src/tracker.rs b/src/tracker.rs index bd463e04..7a064a9d 100644 --- a/src/tracker.rs +++ b/src/tracker.rs @@ -378,6 +378,7 @@ mod tests { use crate::elements::hashes::Hash; use crate::elements::pset::Input; use crate::elements::{AssetId, OutPoint, Script, Txid}; + use crate::version::inject_version_header; use crate::{Arguments, TemplateProgram, WitnessValues}; use super::*; @@ -451,8 +452,10 @@ mod tests { #[test] fn test_debug_and_jet_tracing() { + let program_text = inject_version_header(TEST_PROGRAM); let program = - TemplateProgram::new(TEST_PROGRAM, Box::new(ElementsJetHinter::new())).unwrap(); + TemplateProgram::new(program_text.as_str(), Box::new(ElementsJetHinter::new())) + .unwrap(); let program = program.instantiate(Arguments::default(), true).unwrap(); let satisfied = program.satisfy(WitnessValues::default()).unwrap(); @@ -522,8 +525,10 @@ mod tests { fn test_arith_jet_trace_regression() { let env = create_test_env(); + let program_text = inject_version_header(TEST_ARITHMETIC_JETS); let program = - TemplateProgram::new(TEST_ARITHMETIC_JETS, Box::new(ElementsJetHinter::new())).unwrap(); + TemplateProgram::new(program_text.as_str(), Box::new(ElementsJetHinter::new())) + .unwrap(); let program = program.instantiate(Arguments::default(), true).unwrap(); let satisfied = program.satisfy(WitnessValues::default()).unwrap(); @@ -577,8 +582,9 @@ mod tests { let env = create_test_env(); + let program_text = inject_version_header(TEST_FULL_MULTIPLY_JETS); let program = - TemplateProgram::new(TEST_FULL_MULTIPLY_JETS, Box::new(ElementsJetHinter::new())) + TemplateProgram::new(program_text.as_str(), Box::new(ElementsJetHinter::new())) .unwrap(); let program = program.instantiate(Arguments::default(), true).unwrap(); let satisfied = program.satisfy(WitnessValues::default()).unwrap(); diff --git a/src/witness.rs b/src/witness.rs index ae8b0581..7a4da8b6 100644 --- a/src/witness.rs +++ b/src/witness.rs @@ -216,14 +216,17 @@ mod tests { use crate::ast::ElementsJetHinter; use crate::parse::ParseFromStr; use crate::value::ValueConstructible; + use crate::version::inject_version_header; use crate::{ast, parse, CompiledProgram, SatisfiedProgram}; #[test] fn witness_reuse() { - let s = r#"fn main() { + let s = inject_version_header( + r#"fn main() { assert!(jet::eq_32(witness::A, witness::A)); -}"#; - let parse_program = parse::Program::parse_from_str(s).expect("parsing works"); +}"#, + ); + let parse_program = parse::Program::parse_from_str(&s).expect("parsing works"); match ast::Program::analyze(&parse_program, Box::new(ElementsJetHinter::new())) .map_err(Error::from) { @@ -235,9 +238,11 @@ mod tests { #[test] fn witness_type_mismatch() { - let s = r#"fn main() { + let s = inject_version_header( + r#"fn main() { assert!(jet::is_zero_32(witness::A)); -}"#; +}"#, + ); let witness = WitnessValues::from(HashMap::from([( WitnessName::from_str_unchecked("A"), @@ -260,13 +265,15 @@ mod tests { #[test] fn witness_outside_main() { - let s = r#"fn f() -> u32 { + let s = inject_version_header( + r#"fn f() -> u32 { witness::OUTPUT_OF_F } fn main() { assert!(jet::is_zero_32(f())); -}"#; +}"#, + ); match CompiledProgram::new( s, diff --git a/tests/core_tracker.rs b/tests/core_tracker.rs index 6c9b1608..3c2a3e84 100644 --- a/tests/core_tracker.rs +++ b/tests/core_tracker.rs @@ -6,7 +6,8 @@ use simplicityhl::simplicity::jet::CoreEnv; use simplicityhl::tracker::DefaultTracker; use simplicityhl::{Arguments, TemplateProgram, WitnessValues}; -const CORE_PROGRAM: &str = r#"fn main() { +const CORE_PROGRAM: &str = r#"simc ">=0.6.0"; +fn main() { let (_, sum): (bool, u32) = jet::add_32(10, 20); assert!(jet::eq_32(sum, 30)); }"#; From 8ca6e43497d9469d9be570af1d6e41dae0540fa7 Mon Sep 17 00:00:00 2001 From: Sdoba16 Date: Mon, 29 Jun 2026 11:38:59 +0200 Subject: [PATCH 3/5] Add version tests for multi-file resolution --- src/driver/version_tests.rs | 232 ++++++++++++++++++++++++++++++++++++ tests/cli.rs | 147 ++++++++++++++++++++++- 2 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 src/driver/version_tests.rs diff --git a/src/driver/version_tests.rs b/src/driver/version_tests.rs new file mode 100644 index 00000000..629da0d7 --- /dev/null +++ b/src/driver/version_tests.rs @@ -0,0 +1,232 @@ +//! Integration tests for the *multi-file* enforcement of `simc "...";` directives: +//! the entry file and every reachable dependency are checked, unreachable files +//! are not, and our directive scanner (duplicates, comments) behaves end to end. +//! +//! Semver matching and mismatch-message classification are tested directly and far +//! more cheaply in `crate::version`'s unit tests; they are deliberately not +//! re-asserted through the dependency graph here. + +use crate::driver::tests::setup_graph_raw; + +/// Builds the given files (replacing `{v}` with the current compiler version), runs +/// the dependency-graph build, and asserts the expected outcome. When `expect_success` +/// is false and `expected_err` is `Some`, the collected diagnostics must contain it. +fn check_versions(expect_success: bool, expected_err: Option<&str>, files: &[(&str, &str)]) { + let v = env!("CARGO_PKG_VERSION"); + let owned: Vec<(&str, String)> = files + .iter() + .map(|(p, c)| (*p, c.replace("{v}", v))) + .collect(); + let refs: Vec<(&str, &str)> = owned.iter().map(|(p, c)| (*p, c.as_str())).collect(); + let (graph_opt, _, _ws, handler) = setup_graph_raw(refs); + + if expect_success { + assert!( + graph_opt.is_some() && !handler.has_errors(), + "Scenario failed unexpectedly. Errors:\n{handler}" + ); + return; + } + + assert!( + graph_opt.is_none() || handler.has_errors(), + "Scenario succeeded when it should have failed." + ); + if let Some(err) = expected_err { + assert!( + handler.to_string().contains(err), + "Expected error containing '{err}' but got:\n{handler}" + ); + } +} + +/// A multi-file program whose every file declares a compatible directive compiles: +/// each directive is checked and stripped, and the bodies still parse across `use`. +#[test] +fn mixed_valid_operators() { + check_versions( + true, + None, + &[ + ( + "main.simf", + r#"simc "^{v}"; +use lib::A::foo; +fn main() {}"#, + ), + ( + "libs/lib/A.simf", + r#"simc "={v}"; +use crate::B::foo; +pub fn foo() {}"#, + ), + ( + "libs/lib/B.simf", + r#"simc ">0.1.0"; +use crate::C::foo; +pub fn foo() {}"#, + ), + ( + "libs/lib/C.simf", + r#"simc "*"; +pub fn foo() {}"#, + ), + ], + ); +} + +/// The entry file's directive is checked. +#[test] +fn main_too_old_fails() { + check_versions( + false, + Some("Incompatible compiler version"), + &[ + ( + "main.simf", + r#"simc ">99.0.0"; +use lib::A::foo; +fn main() {}"#, + ), + ( + "libs/lib/A.simf", + r#"simc "={v}"; +pub fn foo() {}"#, + ), + ], + ); +} + +/// Every reachable dependency's directive is checked, not just the entry file. +#[test] +fn lib_too_old_fails() { + check_versions( + false, + Some("Incompatible compiler version"), + &[ + ( + "main.simf", + r#"simc "={v}"; +use lib::A::foo; +fn main() {}"#, + ), + ( + "libs/lib/A.simf", + r#"simc ">99.0.0"; +pub fn foo() {}"#, + ), + ], + ); +} + +/// A file that is never imported is not checked, even with an incompatible directive. +#[test] +fn unreferenced_file_with_invalid_version_ignored() { + check_versions( + true, + None, + &[ + ( + "main.simf", + r#"simc "={v}"; +use lib::A::foo; +fn main() {}"#, + ), + ( + "libs/lib/A.simf", + r#"simc "={v}"; +pub fn foo() {}"#, + ), + ( + "libs/lib/B.simf", + r#"simc ">99.0.0"; +pub fn unused() {}"#, + ), + ], + ); +} + +/// The required-directive policy is enforced through the driver (flag-aware). +#[test] +fn directive_omitted() { + check_versions( + !crate::version::REQUIRE_VERSION_DIRECTIVE, + crate::version::REQUIRE_VERSION_DIRECTIVE.then_some("Missing compiler version"), + &[( + "main.simf", + r#"// NO_INJECT +fn main() {}"#, + )], + ); +} + +/// A file may declare at most one directive. +#[test] +fn multiple_directives_same_file_fails() { + check_versions( + false, + Some("Exactly one compiler version directive"), + &[( + "main.simf", + r#"simc "={v}"; +simc "={v}"; +fn main() {}"#, + )], + ); +} + +/// A malformed version requirement surfaces through the pipeline. +#[test] +fn invalid_syntax_main() { + check_versions( + false, + Some("Invalid version syntax"), + &[ + ( + "main.simf", + r#"simc "foo"; +use lib::A::foo; +fn main() {}"#, + ), + ( + "libs/lib/A.simf", + r#"simc "={v}"; +pub fn foo() {}"#, + ), + ], + ); +} + +/// A directive may follow a leading line comment; a commented-out directive does not +/// count. +#[test] +fn version_in_comment_ignored() { + check_versions( + true, + None, + &[( + "main.simf", + r#"// simc "=99.0.0"; +simc "={v}"; +fn main() {}"#, + )], + ); +} + +/// A directive may follow a leading block comment; one inside the comment does not +/// count. +#[test] +fn version_in_block_comment_ignored() { + check_versions( + true, + None, + &[( + "main.simf", + r#"/* +simc "=99.0.0"; +*/ +simc "={v}"; +fn main() {}"#, + )], + ); +} diff --git a/tests/cli.rs b/tests/cli.rs index aa76eef3..6668d087 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -1,10 +1,34 @@ use std::path::{Path, PathBuf}; -use std::process::Command; +use std::process::{Command, Output}; fn repo_path(path: &str) -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join(path) } +/// Write `content` to a uniquely named `.simf` file in the test temp dir and run +/// `simc` on it, returning the process output. Used by the version-directive tests +/// to exercise the real binary on standalone files. +fn run_simc_on_source(name: &str, content: &str) -> Output { + let file = Path::new(env!("CARGO_TARGET_TMPDIR")).join(format!("{name}.simf")); + std::fs::write(&file, content).expect("failed to write source file"); + Command::new(env!("CARGO_BIN_EXE_simc")) + .arg(file) + .output() + .expect("failed to run simc") +} + +/// Write each `(relative path, content)` under a unique temp project root (creating +/// parent directories) and return the root. Used to drive multi-file `--dep` builds. +fn setup_project(name: &str, files: &[(&str, &str)]) -> PathBuf { + let root = Path::new(env!("CARGO_TARGET_TMPDIR")).join(name); + for (rel, content) in files { + let path = root.join(rel); + std::fs::create_dir_all(path.parent().unwrap()).expect("failed to create project dirs"); + std::fs::write(&path, content).expect("failed to write project file"); + } + root +} + #[test] fn cli_dependency_can_use_crate_root() { let root = repo_path("functional-tests/valid-test-cases/external-library-uses-crate"); @@ -87,3 +111,124 @@ fn cli_reserved_crate_mapping_fails() { stderr ); } + +/// A compatible version directive compiles from the command line. `*` matches any +/// compiler, so this stays valid across version bumps and acts as the positive +/// control for the rejection tests below. +#[test] +fn cli_version_compatible_accepted() { + let output = run_simc_on_source("version_ok", "simc \"*\";\nfn main() {}\n"); + assert!( + output.status.success(), + "simc should accept a compatible directive\nstderr:\n{}", + String::from_utf8_lossy(&output.stderr), + ); +} + +/// A directive the running compiler cannot satisfy is rejected. `>99.0.0` is +/// permanently too new, so the build aborts with a non-zero exit and a clear message. +#[test] +fn cli_version_incompatible_rejected() { + let output = run_simc_on_source("version_incompatible", "simc \">99.0.0\";\nfn main() {}\n"); + assert!( + !output.status.success(), + "simc must reject an incompatible directive" + ); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("Incompatible compiler version"), + "Expected 'Incompatible compiler version', got:\n{stderr}" + ); +} + +/// Under the required-directive policy, a file with no directive is rejected. +#[test] +fn cli_version_missing_rejected() { + let output = run_simc_on_source("version_missing", "fn main() {}\n"); + assert!( + !output.status.success(), + "simc must reject a file with no version directive" + ); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("Missing compiler version"), + "Expected 'Missing compiler version', got:\n{stderr}" + ); +} + +/// A directive whose requirement is not valid semver is a syntax error, not a +/// version mismatch. +#[test] +fn cli_version_invalid_syntax_rejected() { + let output = run_simc_on_source( + "version_bad_syntax", + "simc \"not-a-version\";\nfn main() {}\n", + ); + assert!( + !output.status.success(), + "simc must reject a malformed version requirement" + ); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("Invalid version syntax"), + "Expected 'Invalid version syntax', got:\n{stderr}" + ); +} + +/// A directive that is structurally broken (here a missing semicolon) is rejected +/// before the requirement is even parsed — a different path than an invalid semver +/// string above. +#[test] +fn cli_version_malformed_directive_rejected() { + let output = run_simc_on_source("version_malformed", "simc \"1.0\"\nfn main() {}\n"); + assert!( + !output.status.success(), + "simc must reject a directive with a missing semicolon" + ); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("malformed compiler version directive"), + "Expected 'malformed compiler version directive', got:\n{stderr}" + ); +} + +/// An incompatible directive in a *dependency* (reached via `--dep`), not the entry +/// file, also aborts the build and the diagnostic points at the dependency. +#[test] +fn cli_dependency_version_mismatch_rejected() { + let root = setup_project( + "version_dep_mismatch", + &[ + ( + "main.simf", + "simc \"*\";\nuse lib::module::add;\nfn main() {}\n", + ), + ("lib/module.simf", "simc \">99.0.0\";\npub fn add() {}\n"), + ], + ); + let dep_arg = format!("{}:lib={}", root.display(), root.join("lib").display()); + + let output = Command::new(env!("CARGO_BIN_EXE_simc")) + .arg(root.join("main.simf")) + .arg("-Z") + .arg("imports") + .arg("--dep") + .arg(dep_arg) + .output() + .expect("failed to run simc"); + + assert!( + !output.status.success(), + "simc must reject an incompatible directive in a dependency" + ); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("Incompatible compiler version") && stderr.contains("module.simf"), + "expected an incompatible-version error pointing at the dependency, got:\n{stderr}" + ); +} From 775a3f7808d47a976d1d6b78e72dc7b478f29428 Mon Sep 17 00:00:00 2001 From: Sdoba16 Date: Mon, 29 Jun 2026 11:39:22 +0200 Subject: [PATCH 4/5] Update examples with versions --- examples/array_fold.simf | 2 ++ examples/array_fold_2n.simf | 4 ++++ examples/cat.simf | 2 ++ examples/ctv.simf | 4 ++++ examples/escrow_with_delay.simf | 4 ++++ examples/hash_loop.simf | 2 ++ examples/hodl_vault.simf | 4 ++++ examples/htlc.simf | 4 ++++ examples/last_will.simf | 4 ++++ examples/local_crate/main.simf | 1 + examples/local_crate/math.simf | 2 ++ examples/modules.simf | 1 + examples/multiple_deps/main.simf | 2 ++ examples/multiple_deps/math/simple_op.simf | 2 ++ examples/multiple_deps/merkle/build_root.simf | 2 ++ examples/non_interactive_fee_bump.simf | 3 +++ examples/p2ms.simf | 4 ++++ examples/p2pk.simf | 4 ++++ examples/p2pkh.simf | 4 ++++ examples/pattern_matching.simf | 2 ++ examples/presigned_vault.simf | 4 ++++ examples/reveal_collision.simf | 4 ++++ examples/reveal_fix_point.simf | 4 ++++ examples/sighash_all_anyonecanpay.simf | 4 ++++ examples/sighash_all_anyprevout.simf | 4 ++++ examples/sighash_all_anyprevoutanyscript.simf | 4 ++++ examples/sighash_none.simf | 4 ++++ examples/sighash_single.simf | 4 ++++ examples/simple_multidep/crypto/hashes.simf | 2 ++ examples/simple_multidep/main.simf | 2 ++ examples/simple_multidep/math/arithmetic.simf | 2 ++ examples/single_dep/main.simf | 2 ++ examples/single_dep/temp/constants/utils.simf | 2 ++ examples/single_dep/temp/funcs.simf | 2 ++ examples/transfer_with_timeout.simf | 4 ++++ external-jet-lib-example/src/main.rs | 3 ++- .../error-test-cases/crate-file-not-found/main.simf | 2 ++ .../error-test-cases/cyclic-dependency/lib/module_a.simf | 2 ++ .../error-test-cases/cyclic-dependency/lib/module_b.simf | 2 ++ functional-tests/error-test-cases/cyclic-dependency/main.simf | 2 ++ functional-tests/error-test-cases/file-not-found/main.simf | 2 ++ functional-tests/error-test-cases/lib-not-found/main.simf | 2 ++ .../error-test-cases/local-file-as-external/main.simf | 2 ++ .../error-test-cases/local-file-as-external/utils.simf | 2 ++ .../error-test-cases/name-collision/lib/groups.simf | 2 ++ .../error-test-cases/name-collision/lib/math.simf | 2 ++ functional-tests/error-test-cases/name-collision/main.simf | 2 ++ .../error-test-cases/private-visibility/lib/hidden.simf | 2 ++ .../error-test-cases/private-visibility/main.simf | 2 ++ .../type-alias-duplication/lib/duplication.simf | 2 ++ .../error-test-cases/type-alias-duplication/main.simf | 2 ++ .../valid-test-cases/deep-reexport-chain/lib/level1.simf | 2 ++ .../valid-test-cases/deep-reexport-chain/lib/level2.simf | 2 ++ .../valid-test-cases/deep-reexport-chain/lib/level3.simf | 2 ++ .../valid-test-cases/deep-reexport-chain/main.simf | 2 ++ .../diamond-dependency-resolution/lib/base.simf | 2 ++ .../diamond-dependency-resolution/lib/left.simf | 2 ++ .../diamond-dependency-resolution/lib/right.simf | 2 ++ .../valid-test-cases/diamond-dependency-resolution/main.simf | 2 ++ .../external-library-uses-crate/ext_lib/internal.simf | 2 ++ .../external-library-uses-crate/ext_lib/math.simf | 2 ++ .../valid-test-cases/external-library-uses-crate/main.simf | 2 ++ .../valid-test-cases/interleaved-waterfall/auth/verify.simf | 2 ++ .../valid-test-cases/interleaved-waterfall/db/store.simf | 2 ++ .../valid-test-cases/interleaved-waterfall/main.simf | 2 ++ .../valid-test-cases/interleaved-waterfall/orch/handler.simf | 2 ++ .../valid-test-cases/interleaved-waterfall/types/def.simf | 2 ++ .../valid-test-cases/leaky-signature/lib/internal.simf | 2 ++ functional-tests/valid-test-cases/leaky-signature/main.simf | 2 ++ functional-tests/valid-test-cases/local-crate-nested/a/b.simf | 2 ++ .../valid-test-cases/local-crate-nested/main.simf | 2 ++ functional-tests/valid-test-cases/local-crate/main.simf | 2 ++ functional-tests/valid-test-cases/local-crate/utils.simf | 2 ++ .../valid-test-cases/module-name-collision/lib/module.simf | 1 + .../valid-test-cases/module-name-collision/main.simf | 1 + .../valid-test-cases/module-simple/lib/module.simf | 2 ++ functional-tests/valid-test-cases/module-simple/main.simf | 2 ++ .../valid-test-cases/multi-lib-facade/api/api.simf | 2 ++ .../valid-test-cases/multi-lib-facade/crypto/crypto.simf | 2 ++ functional-tests/valid-test-cases/multi-lib-facade/main.simf | 2 ++ .../valid-test-cases/multi-lib-facade/math/math.simf | 2 ++ .../valid-test-cases/reexport-diamond/lib/core.simf | 2 ++ .../valid-test-cases/reexport-diamond/lib/route_a.simf | 2 ++ .../valid-test-cases/reexport-diamond/lib/route_b.simf | 2 ++ functional-tests/valid-test-cases/reexport-diamond/main.simf | 2 ++ .../valid-test-cases/use-statement-collision/A.simf | 1 + .../valid-test-cases/use-statement-collision/control.simf | 1 + .../valid-test-cases/use-statement-collision/libs/lib/A.simf | 1 + .../valid-test-cases/use-statement-collision/libs/lib/B.simf | 1 + .../valid-test-cases/use-statement-collision/main.simf | 1 + 90 files changed, 208 insertions(+), 1 deletion(-) diff --git a/examples/array_fold.simf b/examples/array_fold.simf index e671493c..f571a0bf 100644 --- a/examples/array_fold.simf +++ b/examples/array_fold.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + fn sum(elt: u32, acc: u32) -> u32 { let (_, acc): (bool, u32) = jet::add_32(elt, acc); acc diff --git a/examples/array_fold_2n.simf b/examples/array_fold_2n.simf index 9d86325c..9dad67c7 100644 --- a/examples/array_fold_2n.simf +++ b/examples/array_fold_2n.simf @@ -1,5 +1,9 @@ +simc ">=0.6.0"; + // From https://github.com/BlockstreamResearch/SimplicityHL/issues/153 + + fn sum(elt: u32, acc: u32) -> u32 { let (_, acc): (bool, u32) = jet::add_32(elt, acc); acc diff --git a/examples/cat.simf b/examples/cat.simf index f6c5e76c..a5d03267 100644 --- a/examples/cat.simf +++ b/examples/cat.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + fn main() { let ab: u16 = <(u8, u8)>::into((0x10, 0x01)); let c: u16 = 0x1001; diff --git a/examples/ctv.simf b/examples/ctv.simf index 3eadb44c..d1452be2 100644 --- a/examples/ctv.simf +++ b/examples/ctv.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + /* * This program is an emulation of CTV using simplicity * @@ -5,6 +7,8 @@ * we require the user to specify all the components of the sighash * that they want to commit. */ + + fn main() { let ctx: Ctx8 = jet::sha_256_ctx_8_init(); let ctx: Ctx8 = jet::sha_256_ctx_8_add_4(ctx, jet::version()); diff --git a/examples/escrow_with_delay.simf b/examples/escrow_with_delay.simf index 8a11a54a..99bd2bd9 100644 --- a/examples/escrow_with_delay.simf +++ b/examples/escrow_with_delay.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + /* * ESCROW WITH DELAY * @@ -7,6 +9,8 @@ * * https://docs.ivylang.org/bitcoin/language/ExampleContracts.html#escrowwithdelay */ + + fn not(bit: bool) -> bool { ::into(jet::complement_1(::into(bit))) } diff --git a/examples/hash_loop.simf b/examples/hash_loop.simf index f554d894..82ad7086 100644 --- a/examples/hash_loop.simf +++ b/examples/hash_loop.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + // Add counter to streaming hash and finalize when the loop exists fn hash_counter_8(ctx: Ctx8, unused: (), byte: u8) -> Either { let new_ctx: Ctx8 = jet::sha_256_ctx_8_add_1(ctx, byte); diff --git a/examples/hodl_vault.simf b/examples/hodl_vault.simf index fdc29ae5..15988097 100644 --- a/examples/hodl_vault.simf +++ b/examples/hodl_vault.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + /* * HODL VAULT * @@ -8,6 +10,8 @@ * the use of old data. The transaction is timelocked to the oracle height, * which means that the transaction becomes valid after the oracle height. */ + + fn checksig(pk: Pubkey, sig: Signature) { let msg: u256 = jet::sig_all_hash(); jet::bip_0340_verify((pk, msg), sig); diff --git a/examples/htlc.simf b/examples/htlc.simf index 2ee98c5e..fa467b01 100644 --- a/examples/htlc.simf +++ b/examples/htlc.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + /* * HTLC (Hash Time-Locked Contract) * @@ -9,6 +11,8 @@ * * https://docs.ivylang.org/bitcoin/language/ExampleContracts.html#htlc */ + + fn sha2(string: u256) -> u256 { let hasher: Ctx8 = jet::sha_256_ctx_8_init(); let hasher: Ctx8 = jet::sha_256_ctx_8_add_32(hasher, string); diff --git a/examples/last_will.simf b/examples/last_will.simf index 9790a1cf..a32e32ba 100644 --- a/examples/last_will.simf +++ b/examples/last_will.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + /* * LAST WILL * @@ -5,6 +7,8 @@ * days. The owner has to repeat the covenant when he moves the coins with his * hot key. The owner can break out of the covenant with his cold key. */ + + fn checksig(pk: Pubkey, sig: Signature) { let msg: u256 = jet::sig_all_hash(); jet::bip_0340_verify((pk, msg), sig); diff --git a/examples/local_crate/main.simf b/examples/local_crate/main.simf index c4df525f..7acdb9ae 100644 --- a/examples/local_crate/main.simf +++ b/examples/local_crate/main.simf @@ -1,3 +1,4 @@ +simc ">=0.6.0"; use crate::math::add; fn main() { diff --git a/examples/local_crate/math.simf b/examples/local_crate/math.simf index 25076863..f5763b04 100644 --- a/examples/local_crate/math.simf +++ b/examples/local_crate/math.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + pub fn add(a: u32, b: u32) -> u32 { let (_, sum): (bool, u32) = jet::add_32(a, b); sum diff --git a/examples/modules.simf b/examples/modules.simf index 70979db9..d0d6449c 100644 --- a/examples/modules.simf +++ b/examples/modules.simf @@ -1,3 +1,4 @@ +simc ">=0.6.0"; mod math { pub mod ops { pub fn double(x: u32) -> u32 { diff --git a/examples/multiple_deps/main.simf b/examples/multiple_deps/main.simf index cead0852..d78ff150 100644 --- a/examples/multiple_deps/main.simf +++ b/examples/multiple_deps/main.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use merkle::build_root::{get_root, hash as and_hash}; use base_math::simple_op::hash as or_hash; diff --git a/examples/multiple_deps/math/simple_op.simf b/examples/multiple_deps/math/simple_op.simf index b152a361..198d9013 100644 --- a/examples/multiple_deps/math/simple_op.simf +++ b/examples/multiple_deps/math/simple_op.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + pub fn hash(x: u32, y: u32) -> u32 { jet::xor_32(x, y) } \ No newline at end of file diff --git a/examples/multiple_deps/merkle/build_root.simf b/examples/multiple_deps/merkle/build_root.simf index f41c37e4..8e2b7bd9 100644 --- a/examples/multiple_deps/merkle/build_root.simf +++ b/examples/multiple_deps/merkle/build_root.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use math::simple_op::hash as temp_hash; pub fn get_root(tx1: u32, tx2: u32) -> u32 { diff --git a/examples/non_interactive_fee_bump.simf b/examples/non_interactive_fee_bump.simf index 5188633c..617bdb75 100644 --- a/examples/non_interactive_fee_bump.simf +++ b/examples/non_interactive_fee_bump.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + /* * NON-INTERACTIVE FEE BUMPING * @@ -12,6 +14,7 @@ * sponsors, Child-Pays-For-Parent (CPFP), or anchor outputs, simplifying fee management for transaction inclusion. */ + // This function computes a signature hash for transactions that allows non-interactive fee bumping. // It omits certain fields from the transaction that can be modified by anyone, // specifically nLockTime and change/fee outputs amounts. diff --git a/examples/p2ms.simf b/examples/p2ms.simf index d95c1a0e..3ba9e07e 100644 --- a/examples/p2ms.simf +++ b/examples/p2ms.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + /* * PAY TO MULTISIG * @@ -6,6 +8,8 @@ * * https://docs.ivylang.org/bitcoin/language/ExampleContracts.html#lockwithmultisig */ + + fn not(bit: bool) -> bool { ::into(jet::complement_1(::into(bit))) } diff --git a/examples/p2pk.simf b/examples/p2pk.simf index 7e40dd55..5666fb83 100644 --- a/examples/p2pk.simf +++ b/examples/p2pk.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + /* * PAY TO PUBLIC KEY * @@ -5,6 +7,8 @@ * * https://docs.ivylang.org/bitcoin/language/ExampleContracts.html#lockwithpublickey */ + + fn main() { jet::bip_0340_verify((param::ALICE_PUBLIC_KEY, jet::sig_all_hash()), witness::ALICE_SIGNATURE) } diff --git a/examples/p2pkh.simf b/examples/p2pkh.simf index 42f527a9..2eaa6142 100644 --- a/examples/p2pkh.simf +++ b/examples/p2pkh.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + /* * PAY TO PUBLIC KEY HASH * @@ -6,6 +8,8 @@ * * https://docs.ivylang.org/bitcoin/language/ExampleContracts.html#lockwithpublickeyhash */ + + fn sha2(string: u256) -> u256 { let hasher: Ctx8 = jet::sha_256_ctx_8_init(); let hasher: Ctx8 = jet::sha_256_ctx_8_add_32(hasher, string); diff --git a/examples/pattern_matching.simf b/examples/pattern_matching.simf index 706996fa..e57ba53a 100644 --- a/examples/pattern_matching.simf +++ b/examples/pattern_matching.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + fn main() { let complex_pattern: Either<(u32, u32, (u1, u1)), [u1; 8]> = Left((32, 3, (0, 1))); diff --git a/examples/presigned_vault.simf b/examples/presigned_vault.simf index d1a836e4..f179d7a5 100644 --- a/examples/presigned_vault.simf +++ b/examples/presigned_vault.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + /* * PRESIGNED VAULT * @@ -16,6 +18,8 @@ * * https://docs.ivylang.org/bitcoin/language/ExampleContracts.html#vaultspend */ + + fn checksig(pk: Pubkey, sig: Signature) { let msg: u256 = jet::sig_all_hash(); jet::bip_0340_verify((pk, msg), sig); diff --git a/examples/reveal_collision.simf b/examples/reveal_collision.simf index bec8b1f3..ff5fed67 100644 --- a/examples/reveal_collision.simf +++ b/examples/reveal_collision.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + /* * REVEAL COLLISION * @@ -8,6 +10,8 @@ * * https://docs.ivylang.org/bitcoin/language/ExampleContracts.html#revealcollision */ + + fn not(bit: bool) -> bool { ::into(jet::complement_1(::into(bit))) } diff --git a/examples/reveal_fix_point.simf b/examples/reveal_fix_point.simf index 02f5da8d..9a7256b2 100644 --- a/examples/reveal_fix_point.simf +++ b/examples/reveal_fix_point.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + /* * REVEAL FIX POINT * @@ -8,6 +10,8 @@ * * https://docs.ivylang.org/bitcoin/language/ExampleContracts.html#revealfixedpoint */ + + fn sha2(string: u256) -> u256 { let hasher: Ctx8 = jet::sha_256_ctx_8_init(); let hasher: Ctx8 = jet::sha_256_ctx_8_add_32(hasher, string); diff --git a/examples/sighash_all_anyonecanpay.simf b/examples/sighash_all_anyonecanpay.simf index ed1f6e2f..3827a45b 100644 --- a/examples/sighash_all_anyonecanpay.simf +++ b/examples/sighash_all_anyonecanpay.simf @@ -1,7 +1,11 @@ +simc ">=0.6.0"; + /* * This program verifies a Schnorr signature based on * SIGHASH_ALL | SIGHASH_ANYONECANPAY. */ + + fn main() { let ctx: Ctx8 = jet::sha_256_ctx_8_init(); // Blockchain diff --git a/examples/sighash_all_anyprevout.simf b/examples/sighash_all_anyprevout.simf index e5448792..f2124577 100644 --- a/examples/sighash_all_anyprevout.simf +++ b/examples/sighash_all_anyprevout.simf @@ -1,7 +1,11 @@ +simc ">=0.6.0"; + /* * This program verifies a Schnorr signature based on * SIGHASH_ALL | SIGHASH_ANYPREVOUT. */ + + fn main() { let ctx: Ctx8 = jet::sha_256_ctx_8_init(); // Blockchain diff --git a/examples/sighash_all_anyprevoutanyscript.simf b/examples/sighash_all_anyprevoutanyscript.simf index fb7ab0a7..f9075ed9 100644 --- a/examples/sighash_all_anyprevoutanyscript.simf +++ b/examples/sighash_all_anyprevoutanyscript.simf @@ -1,7 +1,11 @@ +simc ">=0.6.0"; + /* * This program verifies a Schnorr signature based on * SIGHASH_ALL | SIGHASH_ANYPREVOUTANYSCRIPT. */ + + fn main() { let ctx: Ctx8 = jet::sha_256_ctx_8_init(); // Blockchain diff --git a/examples/sighash_none.simf b/examples/sighash_none.simf index 3ae78c45..1e3e5a3b 100644 --- a/examples/sighash_none.simf +++ b/examples/sighash_none.simf @@ -1,7 +1,11 @@ +simc ">=0.6.0"; + /* * This program verifies a Schnorr signature based on * SIGHASH_NONE. */ + + fn main() { let ctx: Ctx8 = jet::sha_256_ctx_8_init(); // Blockchain diff --git a/examples/sighash_single.simf b/examples/sighash_single.simf index db06aa0a..fe4a4516 100644 --- a/examples/sighash_single.simf +++ b/examples/sighash_single.simf @@ -1,7 +1,11 @@ +simc ">=0.6.0"; + /* * This program verifies a Schnorr signature based on * SIGHASH_SINGLE. */ + + fn main() { let ctx: Ctx8 = jet::sha_256_ctx_8_init(); // Blockchain diff --git a/examples/simple_multidep/crypto/hashes.simf b/examples/simple_multidep/crypto/hashes.simf index 0e463d89..45ae1f3a 100644 --- a/examples/simple_multidep/crypto/hashes.simf +++ b/examples/simple_multidep/crypto/hashes.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + pub fn sha256(data: u32) -> u256 { let ctx: Ctx8 = jet::sha_256_ctx_8_init(); let ctx: Ctx8 = jet::sha_256_ctx_8_add_4(ctx, data); diff --git a/examples/simple_multidep/main.simf b/examples/simple_multidep/main.simf index 81031d4c..12537bd2 100644 --- a/examples/simple_multidep/main.simf +++ b/examples/simple_multidep/main.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use math::arithmetic::add; use crypto::hashes::sha256; diff --git a/examples/simple_multidep/math/arithmetic.simf b/examples/simple_multidep/math/arithmetic.simf index 2f348e0c..30266a5a 100644 --- a/examples/simple_multidep/math/arithmetic.simf +++ b/examples/simple_multidep/math/arithmetic.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + pub fn add(a: u32, b: u32) -> u32 { let (_, res): (bool, u32) = jet::add_32(a, b); res diff --git a/examples/single_dep/main.simf b/examples/single_dep/main.simf index 7897ab8a..7ce457d6 100644 --- a/examples/single_dep/main.simf +++ b/examples/single_dep/main.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + pub use temp::constants::utils::two as smth; use temp::funcs::{get_five, Smth}; diff --git a/examples/single_dep/temp/constants/utils.simf b/examples/single_dep/temp/constants/utils.simf index 4fd2102e..12aa2c09 100644 --- a/examples/single_dep/temp/constants/utils.simf +++ b/examples/single_dep/temp/constants/utils.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + pub use crate::funcs::Smth; pub fn two() -> Smth { diff --git a/examples/single_dep/temp/funcs.simf b/examples/single_dep/temp/funcs.simf index 0ff2da55..f2af8f8c 100644 --- a/examples/single_dep/temp/funcs.simf +++ b/examples/single_dep/temp/funcs.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + pub type Smth = u32; pub fn get_five() -> u32 { diff --git a/examples/transfer_with_timeout.simf b/examples/transfer_with_timeout.simf index 91f6f69e..4c998720 100644 --- a/examples/transfer_with_timeout.simf +++ b/examples/transfer_with_timeout.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + /* * TRANSFER WITH TIMEOUT * @@ -12,6 +14,8 @@ * * https://docs.ivylang.org/bitcoin/language/ExampleContracts.html#transferwithtimeout */ + + fn checksig(pk: Pubkey, sig: Signature) { let msg: u256 = jet::sig_all_hash(); jet::bip_0340_verify((pk, msg), sig); diff --git a/external-jet-lib-example/src/main.rs b/external-jet-lib-example/src/main.rs index c4389271..863ed292 100644 --- a/external-jet-lib-example/src/main.rs +++ b/external-jet-lib-example/src/main.rs @@ -37,7 +37,8 @@ fn main() { .expect("failed to initialize external jet lib"); } - let code = r#"fn main() { + let code = r#"simc ">=0.6.0"; +fn main() { assert!(true); }"#; diff --git a/functional-tests/error-test-cases/crate-file-not-found/main.simf b/functional-tests/error-test-cases/crate-file-not-found/main.simf index c07ebfbf..c780ebfa 100644 --- a/functional-tests/error-test-cases/crate-file-not-found/main.simf +++ b/functional-tests/error-test-cases/crate-file-not-found/main.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use crate::missing::foo; fn main() {} diff --git a/functional-tests/error-test-cases/cyclic-dependency/lib/module_a.simf b/functional-tests/error-test-cases/cyclic-dependency/lib/module_a.simf index 4d32267e..477f627d 100644 --- a/functional-tests/error-test-cases/cyclic-dependency/lib/module_a.simf +++ b/functional-tests/error-test-cases/cyclic-dependency/lib/module_a.simf @@ -1,2 +1,4 @@ +simc ">=0.6.0"; + pub use crate::module_b::TypeB; pub type TypeA = u32; diff --git a/functional-tests/error-test-cases/cyclic-dependency/lib/module_b.simf b/functional-tests/error-test-cases/cyclic-dependency/lib/module_b.simf index f51190c1..c33138fd 100644 --- a/functional-tests/error-test-cases/cyclic-dependency/lib/module_b.simf +++ b/functional-tests/error-test-cases/cyclic-dependency/lib/module_b.simf @@ -1,2 +1,4 @@ +simc ">=0.6.0"; + pub use crate::module_a::TypeA; pub type TypeB = u32; diff --git a/functional-tests/error-test-cases/cyclic-dependency/main.simf b/functional-tests/error-test-cases/cyclic-dependency/main.simf index 338a4030..49e4a75c 100644 --- a/functional-tests/error-test-cases/cyclic-dependency/main.simf +++ b/functional-tests/error-test-cases/cyclic-dependency/main.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use lib::module_a::TypeA; fn main() {} diff --git a/functional-tests/error-test-cases/file-not-found/main.simf b/functional-tests/error-test-cases/file-not-found/main.simf index 2d016d20..963a32aa 100644 --- a/functional-tests/error-test-cases/file-not-found/main.simf +++ b/functional-tests/error-test-cases/file-not-found/main.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use lib::module::AssetId; fn main() { diff --git a/functional-tests/error-test-cases/lib-not-found/main.simf b/functional-tests/error-test-cases/lib-not-found/main.simf index 2d016d20..963a32aa 100644 --- a/functional-tests/error-test-cases/lib-not-found/main.simf +++ b/functional-tests/error-test-cases/lib-not-found/main.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use lib::module::AssetId; fn main() { diff --git a/functional-tests/error-test-cases/local-file-as-external/main.simf b/functional-tests/error-test-cases/local-file-as-external/main.simf index a8b7e844..a69525f4 100644 --- a/functional-tests/error-test-cases/local-file-as-external/main.simf +++ b/functional-tests/error-test-cases/local-file-as-external/main.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use ext::utils::helper; fn main() { diff --git a/functional-tests/error-test-cases/local-file-as-external/utils.simf b/functional-tests/error-test-cases/local-file-as-external/utils.simf index dfaf506e..51420042 100644 --- a/functional-tests/error-test-cases/local-file-as-external/utils.simf +++ b/functional-tests/error-test-cases/local-file-as-external/utils.simf @@ -1 +1,3 @@ +simc ">=0.6.0"; + pub fn helper() {} diff --git a/functional-tests/error-test-cases/name-collision/lib/groups.simf b/functional-tests/error-test-cases/name-collision/lib/groups.simf index 7ef2eff1..dac4ddec 100644 --- a/functional-tests/error-test-cases/name-collision/lib/groups.simf +++ b/functional-tests/error-test-cases/name-collision/lib/groups.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + pub fn add(a: u32, b: u32) -> (bool, u32) { jet::add_32(a, b) } \ No newline at end of file diff --git a/functional-tests/error-test-cases/name-collision/lib/math.simf b/functional-tests/error-test-cases/name-collision/lib/math.simf index b9a83fd9..77cd253f 100644 --- a/functional-tests/error-test-cases/name-collision/lib/math.simf +++ b/functional-tests/error-test-cases/name-collision/lib/math.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + pub fn add(a: u32, b: u32) -> (bool, u32) { let (_, c): (bool, u32) = jet::add_32(a, b); jet::add_32(c, 1) diff --git a/functional-tests/error-test-cases/name-collision/main.simf b/functional-tests/error-test-cases/name-collision/main.simf index 59b5c813..f22c77a2 100644 --- a/functional-tests/error-test-cases/name-collision/main.simf +++ b/functional-tests/error-test-cases/name-collision/main.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use lib::groups::add; use lib::math::add; diff --git a/functional-tests/error-test-cases/private-visibility/lib/hidden.simf b/functional-tests/error-test-cases/private-visibility/lib/hidden.simf index 70da82d4..351f1dd7 100644 --- a/functional-tests/error-test-cases/private-visibility/lib/hidden.simf +++ b/functional-tests/error-test-cases/private-visibility/lib/hidden.simf @@ -1 +1,3 @@ +simc ">=0.6.0"; + type SecretType = u32; pub fn ok() {} \ No newline at end of file diff --git a/functional-tests/error-test-cases/private-visibility/main.simf b/functional-tests/error-test-cases/private-visibility/main.simf index e1eb9dd8..e284a5a1 100644 --- a/functional-tests/error-test-cases/private-visibility/main.simf +++ b/functional-tests/error-test-cases/private-visibility/main.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use lib::hidden::SecretType; fn main() {} \ No newline at end of file diff --git a/functional-tests/error-test-cases/type-alias-duplication/lib/duplication.simf b/functional-tests/error-test-cases/type-alias-duplication/lib/duplication.simf index c668cb7b..cc575f8e 100644 --- a/functional-tests/error-test-cases/type-alias-duplication/lib/duplication.simf +++ b/functional-tests/error-test-cases/type-alias-duplication/lib/duplication.simf @@ -1,2 +1,4 @@ +simc ">=0.6.0"; + pub type A = u32; pub type A = u64; \ No newline at end of file diff --git a/functional-tests/error-test-cases/type-alias-duplication/main.simf b/functional-tests/error-test-cases/type-alias-duplication/main.simf index da4c08be..dd813394 100644 --- a/functional-tests/error-test-cases/type-alias-duplication/main.simf +++ b/functional-tests/error-test-cases/type-alias-duplication/main.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use lib::duplication::A; fn main() { diff --git a/functional-tests/valid-test-cases/deep-reexport-chain/lib/level1.simf b/functional-tests/valid-test-cases/deep-reexport-chain/lib/level1.simf index a56bb43b..8cf1acf4 100644 --- a/functional-tests/valid-test-cases/deep-reexport-chain/lib/level1.simf +++ b/functional-tests/valid-test-cases/deep-reexport-chain/lib/level1.simf @@ -1,2 +1,4 @@ +simc ">=0.6.0"; + pub use crate::level2::CoreSmth; pub use crate::level2::core_val; \ No newline at end of file diff --git a/functional-tests/valid-test-cases/deep-reexport-chain/lib/level2.simf b/functional-tests/valid-test-cases/deep-reexport-chain/lib/level2.simf index 3c73e1bd..043235d7 100644 --- a/functional-tests/valid-test-cases/deep-reexport-chain/lib/level2.simf +++ b/functional-tests/valid-test-cases/deep-reexport-chain/lib/level2.simf @@ -1,2 +1,4 @@ +simc ">=0.6.0"; + pub use crate::level3::CoreSmth; pub use crate::level3::core_val; \ No newline at end of file diff --git a/functional-tests/valid-test-cases/deep-reexport-chain/lib/level3.simf b/functional-tests/valid-test-cases/deep-reexport-chain/lib/level3.simf index 6f0b9983..e3cb54b1 100644 --- a/functional-tests/valid-test-cases/deep-reexport-chain/lib/level3.simf +++ b/functional-tests/valid-test-cases/deep-reexport-chain/lib/level3.simf @@ -1,2 +1,4 @@ +simc ">=0.6.0"; + pub type CoreSmth = u32; pub fn core_val() -> CoreSmth { 42 } \ No newline at end of file diff --git a/functional-tests/valid-test-cases/deep-reexport-chain/main.simf b/functional-tests/valid-test-cases/deep-reexport-chain/main.simf index bd15f456..ea5bcd0d 100644 --- a/functional-tests/valid-test-cases/deep-reexport-chain/main.simf +++ b/functional-tests/valid-test-cases/deep-reexport-chain/main.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use lib::level1::CoreSmth; use lib::level1::core_val; diff --git a/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/base.simf b/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/base.simf index ad05850a..331edec1 100644 --- a/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/base.simf +++ b/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/base.simf @@ -1 +1,3 @@ +simc ">=0.6.0"; + pub type BaseType = u32; \ No newline at end of file diff --git a/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/left.simf b/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/left.simf index 7204af2f..b0b703b9 100644 --- a/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/left.simf +++ b/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/left.simf @@ -1,2 +1,4 @@ +simc ">=0.6.0"; + pub use crate::base::BaseType; pub fn get_left() -> BaseType { 1 } \ No newline at end of file diff --git a/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/right.simf b/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/right.simf index a4086971..54079642 100644 --- a/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/right.simf +++ b/functional-tests/valid-test-cases/diamond-dependency-resolution/lib/right.simf @@ -1,2 +1,4 @@ +simc ">=0.6.0"; + pub use crate::base::BaseType; pub fn get_right() -> BaseType { 2 } \ No newline at end of file diff --git a/functional-tests/valid-test-cases/diamond-dependency-resolution/main.simf b/functional-tests/valid-test-cases/diamond-dependency-resolution/main.simf index 8b2835cb..834ebf75 100644 --- a/functional-tests/valid-test-cases/diamond-dependency-resolution/main.simf +++ b/functional-tests/valid-test-cases/diamond-dependency-resolution/main.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use lib::left::get_left; use lib::right::get_right; use lib::base::BaseType; diff --git a/functional-tests/valid-test-cases/external-library-uses-crate/ext_lib/internal.simf b/functional-tests/valid-test-cases/external-library-uses-crate/ext_lib/internal.simf index 0105464e..542e28af 100644 --- a/functional-tests/valid-test-cases/external-library-uses-crate/ext_lib/internal.simf +++ b/functional-tests/valid-test-cases/external-library-uses-crate/ext_lib/internal.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + pub fn core_add(a: u32, b: u32) -> u32 { let (_, sum): (bool, u32) = jet::add_32(a, b); sum diff --git a/functional-tests/valid-test-cases/external-library-uses-crate/ext_lib/math.simf b/functional-tests/valid-test-cases/external-library-uses-crate/ext_lib/math.simf index 2d5beb65..12a44821 100644 --- a/functional-tests/valid-test-cases/external-library-uses-crate/ext_lib/math.simf +++ b/functional-tests/valid-test-cases/external-library-uses-crate/ext_lib/math.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use crate::internal::core_add; pub fn add_one(a: u32) -> u32 { diff --git a/functional-tests/valid-test-cases/external-library-uses-crate/main.simf b/functional-tests/valid-test-cases/external-library-uses-crate/main.simf index 78cc1a9c..8c0ad129 100644 --- a/functional-tests/valid-test-cases/external-library-uses-crate/main.simf +++ b/functional-tests/valid-test-cases/external-library-uses-crate/main.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use ext_lib::math::add_one; fn main() { diff --git a/functional-tests/valid-test-cases/interleaved-waterfall/auth/verify.simf b/functional-tests/valid-test-cases/interleaved-waterfall/auth/verify.simf index 5b33d6b9..a248f0c5 100644 --- a/functional-tests/valid-test-cases/interleaved-waterfall/auth/verify.simf +++ b/functional-tests/valid-test-cases/interleaved-waterfall/auth/verify.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use types::def::UserId; use db::store::get_record; diff --git a/functional-tests/valid-test-cases/interleaved-waterfall/db/store.simf b/functional-tests/valid-test-cases/interleaved-waterfall/db/store.simf index 20a6cc61..e5bd72d5 100644 --- a/functional-tests/valid-test-cases/interleaved-waterfall/db/store.simf +++ b/functional-tests/valid-test-cases/interleaved-waterfall/db/store.simf @@ -1,2 +1,4 @@ +simc ">=0.6.0"; + use types::def::UserId; pub fn get_record(id: UserId) -> UserId { id } \ No newline at end of file diff --git a/functional-tests/valid-test-cases/interleaved-waterfall/main.simf b/functional-tests/valid-test-cases/interleaved-waterfall/main.simf index 0abb7218..569e9ad0 100644 --- a/functional-tests/valid-test-cases/interleaved-waterfall/main.simf +++ b/functional-tests/valid-test-cases/interleaved-waterfall/main.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use orch::handler::run_system; fn main() { diff --git a/functional-tests/valid-test-cases/interleaved-waterfall/orch/handler.simf b/functional-tests/valid-test-cases/interleaved-waterfall/orch/handler.simf index 86d627d6..12116be1 100644 --- a/functional-tests/valid-test-cases/interleaved-waterfall/orch/handler.simf +++ b/functional-tests/valid-test-cases/interleaved-waterfall/orch/handler.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use auth::verify::is_valid; use db::store::get_record; use types::def::UserId; diff --git a/functional-tests/valid-test-cases/interleaved-waterfall/types/def.simf b/functional-tests/valid-test-cases/interleaved-waterfall/types/def.simf index a8fd5650..8d2c7185 100644 --- a/functional-tests/valid-test-cases/interleaved-waterfall/types/def.simf +++ b/functional-tests/valid-test-cases/interleaved-waterfall/types/def.simf @@ -1 +1,3 @@ +simc ">=0.6.0"; + pub type UserId = u32; \ No newline at end of file diff --git a/functional-tests/valid-test-cases/leaky-signature/lib/internal.simf b/functional-tests/valid-test-cases/leaky-signature/lib/internal.simf index 2f11c01e..9cfdccf5 100644 --- a/functional-tests/valid-test-cases/leaky-signature/lib/internal.simf +++ b/functional-tests/valid-test-cases/leaky-signature/lib/internal.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + type SecretKey = u64; pub fn unlock(key: SecretKey) -> u64 { diff --git a/functional-tests/valid-test-cases/leaky-signature/main.simf b/functional-tests/valid-test-cases/leaky-signature/main.simf index 77592ac0..92ed04c1 100644 --- a/functional-tests/valid-test-cases/leaky-signature/main.simf +++ b/functional-tests/valid-test-cases/leaky-signature/main.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use lib::internal::unlock; fn main() { diff --git a/functional-tests/valid-test-cases/local-crate-nested/a/b.simf b/functional-tests/valid-test-cases/local-crate-nested/a/b.simf index 3804e1cb..42e1105d 100644 --- a/functional-tests/valid-test-cases/local-crate-nested/a/b.simf +++ b/functional-tests/valid-test-cases/local-crate-nested/a/b.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + pub fn helper() -> bool { true } \ No newline at end of file diff --git a/functional-tests/valid-test-cases/local-crate-nested/main.simf b/functional-tests/valid-test-cases/local-crate-nested/main.simf index 3e1d833e..7865290a 100644 --- a/functional-tests/valid-test-cases/local-crate-nested/main.simf +++ b/functional-tests/valid-test-cases/local-crate-nested/main.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use crate::a::b::helper; fn main() { diff --git a/functional-tests/valid-test-cases/local-crate/main.simf b/functional-tests/valid-test-cases/local-crate/main.simf index ba2cb9bd..8ddd9774 100644 --- a/functional-tests/valid-test-cases/local-crate/main.simf +++ b/functional-tests/valid-test-cases/local-crate/main.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use crate::utils::helper; fn main() { diff --git a/functional-tests/valid-test-cases/local-crate/utils.simf b/functional-tests/valid-test-cases/local-crate/utils.simf index 3804e1cb..42e1105d 100644 --- a/functional-tests/valid-test-cases/local-crate/utils.simf +++ b/functional-tests/valid-test-cases/local-crate/utils.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + pub fn helper() -> bool { true } \ No newline at end of file diff --git a/functional-tests/valid-test-cases/module-name-collision/lib/module.simf b/functional-tests/valid-test-cases/module-name-collision/lib/module.simf index d5cfec25..0472a45e 100644 --- a/functional-tests/valid-test-cases/module-name-collision/lib/module.simf +++ b/functional-tests/valid-test-cases/module-name-collision/lib/module.simf @@ -1 +1,2 @@ +simc ">=0.6.0"; pub fn add() {} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/module-name-collision/main.simf b/functional-tests/valid-test-cases/module-name-collision/main.simf index 75102521..8862891f 100644 --- a/functional-tests/valid-test-cases/module-name-collision/main.simf +++ b/functional-tests/valid-test-cases/module-name-collision/main.simf @@ -1,3 +1,4 @@ +simc ">=0.6.0"; use lib::module::add; mod unit_1 {} fn main() {} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/module-simple/lib/module.simf b/functional-tests/valid-test-cases/module-simple/lib/module.simf index d5cfec25..0d75ceaa 100644 --- a/functional-tests/valid-test-cases/module-simple/lib/module.simf +++ b/functional-tests/valid-test-cases/module-simple/lib/module.simf @@ -1 +1,3 @@ +simc ">=0.6.0"; + pub fn add() {} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/module-simple/main.simf b/functional-tests/valid-test-cases/module-simple/main.simf index bb2705df..f63174ef 100644 --- a/functional-tests/valid-test-cases/module-simple/main.simf +++ b/functional-tests/valid-test-cases/module-simple/main.simf @@ -1,2 +1,4 @@ +simc ">=0.6.0"; + use lib::module::add; fn main() {} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/multi-lib-facade/api/api.simf b/functional-tests/valid-test-cases/multi-lib-facade/api/api.simf index d80ca18a..232a37f5 100644 --- a/functional-tests/valid-test-cases/multi-lib-facade/api/api.simf +++ b/functional-tests/valid-test-cases/multi-lib-facade/api/api.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + pub use crypto::crypto::mock_hash; pub use math::math::MathInt; pub use math::math::add_two; \ No newline at end of file diff --git a/functional-tests/valid-test-cases/multi-lib-facade/crypto/crypto.simf b/functional-tests/valid-test-cases/multi-lib-facade/crypto/crypto.simf index 51111aca..3ea41187 100644 --- a/functional-tests/valid-test-cases/multi-lib-facade/crypto/crypto.simf +++ b/functional-tests/valid-test-cases/multi-lib-facade/crypto/crypto.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use math::math::MathInt; pub fn mock_hash(x: MathInt) -> (bool, MathInt) { diff --git a/functional-tests/valid-test-cases/multi-lib-facade/main.simf b/functional-tests/valid-test-cases/multi-lib-facade/main.simf index 44ff5c3c..939447b6 100644 --- a/functional-tests/valid-test-cases/multi-lib-facade/main.simf +++ b/functional-tests/valid-test-cases/multi-lib-facade/main.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use api::api::{add_two, mock_hash, MathInt}; fn main() { diff --git a/functional-tests/valid-test-cases/multi-lib-facade/math/math.simf b/functional-tests/valid-test-cases/multi-lib-facade/math/math.simf index d8d16499..76fb182d 100644 --- a/functional-tests/valid-test-cases/multi-lib-facade/math/math.simf +++ b/functional-tests/valid-test-cases/multi-lib-facade/math/math.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + pub type MathInt = u32; pub fn add_two(x: MathInt) -> (bool, MathInt) { jet::add_32(x, 2) diff --git a/functional-tests/valid-test-cases/reexport-diamond/lib/core.simf b/functional-tests/valid-test-cases/reexport-diamond/lib/core.simf index 71526bed..37c3e96b 100644 --- a/functional-tests/valid-test-cases/reexport-diamond/lib/core.simf +++ b/functional-tests/valid-test-cases/reexport-diamond/lib/core.simf @@ -1,2 +1,4 @@ +simc ">=0.6.0"; + pub type Coin = u64; pub fn mint(val: u64) -> Coin { val } \ No newline at end of file diff --git a/functional-tests/valid-test-cases/reexport-diamond/lib/route_a.simf b/functional-tests/valid-test-cases/reexport-diamond/lib/route_a.simf index 8021599e..36abe356 100644 --- a/functional-tests/valid-test-cases/reexport-diamond/lib/route_a.simf +++ b/functional-tests/valid-test-cases/reexport-diamond/lib/route_a.simf @@ -1,2 +1,4 @@ +simc ">=0.6.0"; + pub use crate::core::Coin; pub use crate::core::mint; \ No newline at end of file diff --git a/functional-tests/valid-test-cases/reexport-diamond/lib/route_b.simf b/functional-tests/valid-test-cases/reexport-diamond/lib/route_b.simf index 32edb866..57e192e2 100644 --- a/functional-tests/valid-test-cases/reexport-diamond/lib/route_b.simf +++ b/functional-tests/valid-test-cases/reexport-diamond/lib/route_b.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + pub use crate::core::Coin; pub fn burn(c: Coin) -> (bool, Coin) { diff --git a/functional-tests/valid-test-cases/reexport-diamond/main.simf b/functional-tests/valid-test-cases/reexport-diamond/main.simf index 799b96e6..f61edfe9 100644 --- a/functional-tests/valid-test-cases/reexport-diamond/main.simf +++ b/functional-tests/valid-test-cases/reexport-diamond/main.simf @@ -1,3 +1,5 @@ +simc ">=0.6.0"; + use lib::route_a::Coin; use lib::route_a::mint; use lib::route_b::burn; diff --git a/functional-tests/valid-test-cases/use-statement-collision/A.simf b/functional-tests/valid-test-cases/use-statement-collision/A.simf index 2af1be6a..f7c8d432 100644 --- a/functional-tests/valid-test-cases/use-statement-collision/A.simf +++ b/functional-tests/valid-test-cases/use-statement-collision/A.simf @@ -1 +1,2 @@ +simc ">=0.6.0"; pub fn foo() -> u32 { 7 } \ No newline at end of file diff --git a/functional-tests/valid-test-cases/use-statement-collision/control.simf b/functional-tests/valid-test-cases/use-statement-collision/control.simf index 8360a3cb..79a34d86 100644 --- a/functional-tests/valid-test-cases/use-statement-collision/control.simf +++ b/functional-tests/valid-test-cases/use-statement-collision/control.simf @@ -1,3 +1,4 @@ +simc ">=0.6.0"; use crate::A::foo; fn main() { diff --git a/functional-tests/valid-test-cases/use-statement-collision/libs/lib/A.simf b/functional-tests/valid-test-cases/use-statement-collision/libs/lib/A.simf index 8456c23a..5b55a519 100644 --- a/functional-tests/valid-test-cases/use-statement-collision/libs/lib/A.simf +++ b/functional-tests/valid-test-cases/use-statement-collision/libs/lib/A.simf @@ -1 +1,2 @@ +simc ">=0.6.0"; pub fn foo() -> u32 { 8 } \ No newline at end of file diff --git a/functional-tests/valid-test-cases/use-statement-collision/libs/lib/B.simf b/functional-tests/valid-test-cases/use-statement-collision/libs/lib/B.simf index 041fe2a7..32e9aa69 100644 --- a/functional-tests/valid-test-cases/use-statement-collision/libs/lib/B.simf +++ b/functional-tests/valid-test-cases/use-statement-collision/libs/lib/B.simf @@ -1,3 +1,4 @@ +simc ">=0.6.0"; use crate::A::foo; pub fn bar() {} \ No newline at end of file diff --git a/functional-tests/valid-test-cases/use-statement-collision/main.simf b/functional-tests/valid-test-cases/use-statement-collision/main.simf index 2293b510..80ba3de9 100644 --- a/functional-tests/valid-test-cases/use-statement-collision/main.simf +++ b/functional-tests/valid-test-cases/use-statement-collision/main.simf @@ -1,3 +1,4 @@ +simc ">=0.6.0"; use crate::A::foo; use lib::B::bar; From 1ac6efdfc1d89d9ccd97203e209420839d8d8dde Mon Sep 17 00:00:00 2001 From: Sdoba16 Date: Mon, 29 Jun 2026 11:39:36 +0200 Subject: [PATCH 5/5] Add documentation for versioning --- doc/versioning.md | 48 ++++++++++++++++++++++++++++++ external-jet-lib-example/README.md | 2 ++ 2 files changed, 50 insertions(+) create mode 100644 doc/versioning.md diff --git a/doc/versioning.md b/doc/versioning.md new file mode 100644 index 00000000..1557e9d7 --- /dev/null +++ b/doc/versioning.md @@ -0,0 +1,48 @@ +# Compiler Versioning + +Every `.simf` file must begin with a compiler version directive: +```rust +simc ">=0.6.0"; +``` + +The directive is a **fail-fast compatibility check**: it asserts that the compiler building the file satisfies the stated version range, turning an otherwise confusing parser error (when a file uses syntax or features the running compiler does not support) into a clear, actionable message. A file that omits the directive is rejected with a `Missing compiler version` error before any further compilation. The directive must be the first non-comment item in the file, and a file may declare it at most once. + +It is *not* a guarantee that the same source always produces the same program. A version *range* (for example `^0.6.0`) can be satisfied by several compiler versions, and codegen differences between them would change the program's Commitment Merkle Root (CMR) — and therefore its address. See [Reproducibility and deployment](#reproducibility-and-deployment). + +## Semantic Versioning (SemVer) + +The compiler uses standard Semantic Versioning rules to evaluate whether a file is compatible with the currently running compiler. You can use operators to define acceptable ranges: + +* **Caret (`^`) or Bare strings:** `^0.6.0` or `0.6.0`. Allows patch-level and minor-level updates that do not modify the left-most non-zero digit. (e.g., `^0.6.0` allows `0.6.1`, but rejects `0.7.0`). +* **Tilde (`~`):** `~0.6`. Allows only patch-level updates within the given minor version. (e.g., `~0.6` allows `0.6.1` and `0.6.2`, but rejects `0.7.0`). Note that `~0.6.0` is equivalent to `^0.6.0` because both pin the minor version. +* **Exact (`=`):** `=0.6.0`. Strictly requires this exact version of the compiler. +* **Inequalities (`>`, `>=`, `<`, `<=`):** `>=0.6.0`. Allows any compiler version equal to or newer than `0.6.0`. +* **Wildcards (`*`, `x`):** `0.x.x`. Allows any version matching the specified major release. +* **Multiple Bounds:** `>=0.6.0, <1.0.0`. You can combine operators with a comma. + +### Pre-release versions +If the compiler is currently on a pre-release version (e.g., `0.6.0-rc.0`), it will only match against contracts that explicitly request that exact pre-release base, or contracts that safely encompass the base version. + +## Multi-File Enforcement + +Version checking is performed eagerly immediately after the initial syntax parsing, before dependency resolution and semantic analysis occur. When building a multi-file project, the compiler driver evaluates the version directive of the `main.simf` entry point, as well as the directives of every external library file imported via the `--dep` flag. + +If *any* file in the dependency graph requires a compiler version that is incompatible with the currently running compiler, the driver immediately halts compilation. + +This ensures that an older, stable library cannot be accidentally compiled with an incompatible compiler without the developer's explicit consent. + +## Reproducibility and deployment + +For a deployed contract the address is derived from the compiled program's CMR, so reproducible compilation matters. Because a range can be satisfied by multiple compiler versions, pin an **exact** version (`=x.y.z`) for anything you deploy, and record and verify the CMR that `simc` prints — rather than relying on the range alone. This is the same practice as verifying on-chain bytecode in other contract ecosystems. + +## Scope: what the compiler does and does not do + +The compiler's responsibility ends at **per-file enforcement**: each file's directive is checked against the *currently running* compiler, and compilation halts if any file is incompatible. The compiler does **not** select a compiler, resolve a single version across a project's differing ranges, fetch compiler binaries, or guarantee reproducible output. Choosing a compatible compiler for a project — and pinning it so a deployed CMR is reproducible — is the responsibility of higher-level tooling such as Simplex. + +## Tooling + +The requirement is machine-readable without compiling the program: tools can call `version::requirement_of` to read a file's declared range cheaply (it scans only the leading directive and returns the underlying `semver::VersionReq` via `VersionRequirement::req`, so a tool can intersect ranges across files). Simplex uses this to select a compatible compiler across a project's `.simf` files. + +## Known limitation: flattened output + +`simc` can flatten a multi-file project into a single file. The flattened source is the combined program body and does **not** carry a `simc` directive, so under the required-directive policy it must have one added before it can be recompiled. Threading a merged requirement through flattening is left to future work. diff --git a/external-jet-lib-example/README.md b/external-jet-lib-example/README.md index b2055d7e..36c4901f 100644 --- a/external-jet-lib-example/README.md +++ b/external-jet-lib-example/README.md @@ -39,6 +39,8 @@ let program = TemplateProgram::new(simf_code, Box::new(ExternalJetHinter::new()) .expect("compilation failed"); ``` +`simf_code` must begin with a compiler-version directive, e.g. `simc ">=0.6.0";` — source without one is rejected with a `Missing compiler version` error. + The compiler will call `parse_jet` whenever it encounters an unknown jet name, forwarding the lookup to your shared library. ## Building the Example