Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
48 changes: 48 additions & 0 deletions doc/versioning.md
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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Every `.simf` file must begin with a compiler version directive:
Every `.simf` file may begin with a compiler version directive:

I think it is incredibly limiting to new developers to enforce this. Surely this should be opt-in.

Copy link
Copy Markdown
Collaborator

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

Copy link
Copy Markdown
Contributor

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, okay

```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.
2 changes: 2 additions & 0 deletions examples/array_fold.simf
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
Expand Down
4 changes: 4 additions & 0 deletions examples/array_fold_2n.simf
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions examples/cat.simf
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;
Expand Down
4 changes: 4 additions & 0 deletions examples/ctv.simf
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
simc ">=0.6.0";

/*
* This program is an emulation of CTV using simplicity
*
* Instead of specifying the template hash as in BIP CTV,
* 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());
Expand Down
4 changes: 4 additions & 0 deletions examples/escrow_with_delay.simf
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
simc ">=0.6.0";

/*
* ESCROW WITH DELAY
*
Expand All @@ -7,6 +9,8 @@
*
* https://docs.ivylang.org/bitcoin/language/ExampleContracts.html#escrowwithdelay
*/


fn not(bit: bool) -> bool {
<u1>::into(jet::complement_1(<bool>::into(bit)))
}
Expand Down
2 changes: 2 additions & 0 deletions examples/hash_loop.simf
Original file line number Diff line number Diff line change
@@ -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<u256, Ctx8> {
let new_ctx: Ctx8 = jet::sha_256_ctx_8_add_1(ctx, byte);
Expand Down
4 changes: 4 additions & 0 deletions examples/hodl_vault.simf
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
simc ">=0.6.0";

/*
* HODL VAULT
*
Expand All @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions examples/htlc.simf
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
simc ">=0.6.0";

/*
* HTLC (Hash Time-Locked Contract)
*
Expand All @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions examples/last_will.simf
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
simc ">=0.6.0";

/*
* LAST WILL
*
* The inheritor can spend the coins if the owner doesn't move the them for 180
* 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);
Expand Down
1 change: 1 addition & 0 deletions examples/local_crate/main.simf
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
simc ">=0.6.0";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this not just be simc "0.6.0"?

Can we guarantee it will work on 0.7.0?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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() {
Expand Down
2 changes: 2 additions & 0 deletions examples/local_crate/math.simf
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
Expand Down
1 change: 1 addition & 0 deletions examples/modules.simf
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 {
Expand Down
2 changes: 2 additions & 0 deletions examples/multiple_deps/main.simf
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
2 changes: 2 additions & 0 deletions examples/multiple_deps/math/simple_op.simf
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)
}
2 changes: 2 additions & 0 deletions examples/multiple_deps/merkle/build_root.simf
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 {
Expand Down
3 changes: 3 additions & 0 deletions examples/non_interactive_fee_bump.simf
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
simc ">=0.6.0";

/*
* NON-INTERACTIVE FEE BUMPING
*
Expand All @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions examples/p2ms.simf
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
simc ">=0.6.0";

/*
* PAY TO MULTISIG
*
Expand All @@ -6,6 +8,8 @@
*
* https://docs.ivylang.org/bitcoin/language/ExampleContracts.html#lockwithmultisig
*/


fn not(bit: bool) -> bool {
<u1>::into(jet::complement_1(<bool>::into(bit)))
}
Expand Down
4 changes: 4 additions & 0 deletions examples/p2pk.simf
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)
}
4 changes: 4 additions & 0 deletions examples/p2pkh.simf
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
simc ">=0.6.0";

/*
* PAY TO PUBLIC KEY HASH
*
Expand All @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions examples/pattern_matching.simf
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)));

Expand Down
4 changes: 4 additions & 0 deletions examples/presigned_vault.simf
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
simc ">=0.6.0";

/*
* PRESIGNED VAULT
*
Expand All @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions examples/reveal_collision.simf
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
simc ">=0.6.0";

/*
* REVEAL COLLISION
*
Expand All @@ -8,6 +10,8 @@
*
* https://docs.ivylang.org/bitcoin/language/ExampleContracts.html#revealcollision
*/


fn not(bit: bool) -> bool {
<u1>::into(jet::complement_1(<bool>::into(bit)))
}
Expand Down
4 changes: 4 additions & 0 deletions examples/reveal_fix_point.simf
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
simc ">=0.6.0";

/*
* REVEAL FIX POINT
*
Expand All @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions examples/sighash_all_anyonecanpay.simf
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 4 additions & 0 deletions examples/sighash_all_anyprevout.simf
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 4 additions & 0 deletions examples/sighash_all_anyprevoutanyscript.simf
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 4 additions & 0 deletions examples/sighash_none.simf
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 4 additions & 0 deletions examples/sighash_single.simf
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions examples/simple_multidep/crypto/hashes.simf
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
2 changes: 2 additions & 0 deletions examples/simple_multidep/main.simf
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;

Expand Down
Loading
Loading