-
Notifications
You must be signed in to change notification settings - Fork 35
Versioning #263
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Versioning #263
Changes from all commits
5a4e6d9
3519939
8ca6e43
775a3f7
1ac6efd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| simc ">=0.6.0"; | ||
|
|
||
| fn main() { | ||
| let ab: u16 = <(u8, u8)>::into((0x10, 0x01)); | ||
| let c: u16 = 0x1001; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| simc ">=0.6.0"; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this not just be Can we guarantee it will work on
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I explained the reason here: #263 (comment) |
||
| use crate::math::add; | ||
|
|
||
| fn main() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| simc ">=0.6.0"; | ||
| mod math { | ||
| pub mod ops { | ||
| pub fn double(x: u32) -> u32 { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| simc ">=0.6.0"; | ||
|
|
||
| pub fn hash(x: u32, y: u32) -> u32 { | ||
| jet::xor_32(x, y) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,14 @@ | ||
| simc ">=0.6.0"; | ||
|
|
||
| /* | ||
| * PAY TO PUBLIC KEY | ||
| * | ||
| * The coins move if the person with the given public key signs the transaction. | ||
| * | ||
| * 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) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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))); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,5 @@ | ||
| simc ">=0.6.0"; | ||
|
|
||
| use math::arithmetic::add; | ||
| use crypto::hashes::sha256; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is incredibly limiting to new developers to enforce this. Surely this should be opt-in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not fully understand how it will work. If we load a file without a version, then how are we supposed to determine it? Do we assume that it is always correct? What if the functionality in that file becomes deprecated? What if we have some vulnerabilities in the file? Thus, I think we need to make it mandatory for users to use versions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me walk through the two concrete scenarios:
Case 1: File uses newer syntax (e.g., pub or use, introduced in simc 0.6.0)
If no version is specified, the file simply fails to compile on older compilers with a syntax error. The simc field doesn't change this outcome - it just gives a much better error message ("this file requires simc >= 0.6.0") instead of a confusing parser error.
Case 2: File uses deprecated or removed functionality
Same story: without a version constraint, the file fails to compile once that feature is removed. The simc field lets authors signal simc >=0.5.0, <0.6.0 to make that explicit, but it's still an error either way.
On vulnerabilities: the simc version field controls compilation compatibility, not runtime security. Vulnerabilities in Simplicity programs are addressed at the consensus/jet layer, independently of the compiler version.
So omitting the simc field is never silently wrong - the compiler still catches incompatibilities. Making it mandatory adds friction for new developers who just want to write a simple program, and requires them to understand version semantics before writing their first line of code. Keeping it as may means it's there when you need better error messages, but doesn't block the happy path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it, okay