Introduction
Simfony is a high-level language for writing Bitcoin smart contracts. In other words, Simfony is a language for expressing spending conditions of UTXOs on the Bitcoin blockchain.
Simfony looks and feels like Rust. Developers write Simfony, Bitcoin full nodes run Simplicity.
Types and Values
Simfony mostly uses a subset of Rust's types. It extends Rust in some ways to make it work better with Simplicity and with the blockchain.
Boolean Type
Type | Description | Values |
---|---|---|
bool | Boolean | false , true |
Values of type bool
are truth values, which are either true
or false
.
Integer Types
Type | Description | Values |
---|---|---|
u1 | 1-bit integer | 0 , 1 |
u2 | 2-bit integer | 0 , 1 , 2 , 3 |
u4 | 4-bit integer | 0 , 1 , …, 15 |
u8 | 8-bit integer | 0 , 1 , …, 255 |
u16 | 16-bit integer | 0 , 1 , …, 65535 |
u32 | 32-bit integer | 0 , 1 , …, 4294967295 |
u64 | 64-bit integer | 0 , 1 , …, 18446744073709551615 |
u128 | 128-bit integer | 0 , 1 , …, 340282366920938463463374607431768211455 |
u256 | 256-bit integer | 0 , 1 , …, 2256 - 11 |
Unsigned integers range from 1 bit to 256 bits.
u8
to u128
are also supported in Rust.
u1
, u2
, u4
and u256
are new to Simfony.
Integer values can be written in decimal notation 123456
, binary notation2 0b10101010
or hexadecimal notation3 0xdeadbeef
.
There are no signed integers.
The maximal value of type u256
is 115792089237316195423570985008687907853269984665640564039457584007913129639935
.
The number of bits must be equal to the bit width of the type.
The number of hex digits must correspond to the bit width of the type.
Tuple Types
Type | Description | Values |
---|---|---|
() | 0-tuple | () |
(A) | 1-tuple | (a0,) , (a1,) , … |
(A, B) | 2-tuple | (a0, b0) , (a1, b1) , (a2, b2) , (a3, b3) , … |
… | … | … |
(A, B, …) | n-tuple | (a0, b0, …) , … |
Tuples work just like in Rust.
The empty tuple ()
contains no information.
It is also called the "unit".
It is mostly used as the return type of functions that don't return anything.
Singletons (a0,)
must be written with an extra comma ,
to differentiate them from function calls.
Bigger tuples (a0, b0, …)
work like in pretty much any other programming language.
Each tuple type (A1, A2, …, AN)
defines a sequence A1
, A2
, …, AN
of types.
Values of that type must mirror the sequence of types:
A tuple value (a1, a2, …, aN)
consists of a sequence a1
, a2
, …, aN
of values, where a1
is of type A1
, a2
is of type A2
, and so on.
Tuples are always finite in length.
Tuples are different from arrays: Each element of a tuple can have a different type. Each element of an array must have the same type.
Array Types
Type | Description | Values |
---|---|---|
[A; 0] | 0-array | [] |
[A; 1] | 1-array | [a0] , [a1] , … |
[A; 2] | 2-array | [a0, a1] , [a2, a3] , [a4, a5] , [a6, a7] , … |
… | … | … |
[A; N] | n-array | [a0, …, aN] , … |
Arrays work just like in Rust.
The empty array []
is basically useless, but I included it for completeness.
Arrays [a0, …, aN]
work like in pretty much any other programming language.
Each array type [A; N]
defines an element type A
and a length N
.
An array value [a0, …, aN]
of that type consists of N
many elements a0
, …, aN
that are each of type A
.
Arrays are always of finite length.
Arrays are different from tuples: Each element of an array must have the same type. Each element of a tuple can have a different type.
List Types
Type | Description | Values |
---|---|---|
List<A, 2> | <2-list | list![] , list![a1] |
List<A, 4> | <4-list | list![] , …, list![a1, a2, a3] |
List<A, 8> | <8-list | list![] , …, list![a1, …, a7] |
List<A, 16> | <16-list | list![] , …, list![a1, …, a15] |
List<A, 32> | <32-list | list![] , …, list![a1, …, a31] |
List<A, 64> | <64-list | list![] , …, list![a1, …, a62] |
List<A, 128> | <128-list | list![] , …, list![a1, …, a127] |
List<A, 256> | <256-list | list![] , …, list![a1, …, a255] |
List<A, 512> | <512-list | list![] , …, list![a1, …, a511] |
… | … | … |
List<A, 2N> | <2N-list | list![] , …, list![a1, …, a (2N - 1)] |
Lists hold a variable number of elements of the same type. This is similar to Rust vectors, but Simfony doesn't have a heap. In Simfony, lists exists on the stack, which is why the maximum list length is bounded.
<2-lists hold fewer than 2 elements, so zero or one element. <4-lists hold fewer than 4 elements, so zero to three elements. <8-lists hold fewer than 8 elements, so zero to seven elements. And so on. For technical reasons, the list bound is always a power of two. The bound 1 is not supported, because it would only allow empty lists, which is useless.
Lists are different from arrays: List values hold a variable number of elements. Array values hold a fixed number of elements.
On the blockchain, you pay for every byte that you use.
If you use an array, then you pay for every single element.
For example, values of type [u8; 512]
cost roughly as much as 512 many u8
values.
However, if you use a list, then you only pay for the elements that you actually use.
For example, the type List<u8, 512>
allows for up to 511 elements.
If you only use three elements list![1, 2, 3]
, then you pay for exactly three elements.
You don't pay for the remaining 508 unused elements.
Option Types
Type | Values |
---|---|
Option<A> | None , Some(a0) , Some(a1) , … |
Options represent values that might not be present. They work just like in Rust.
An option type is generic over a type A
.
The value None
is empty.
The value Some(a)
contains an inner value a
of type A
.
In Rust, we implement options as follows.
#![allow(unused)] fn main() { enum Option<A> { None, Some(A), } }
Either Types
Type | Values |
---|---|
Either<A, B> | Left(a0) , Left(a1) , …, Right(b0) , Right(b1) , … |
Sum types represent values that are of some "left" type in some cases and that are of another "right" type in other cases. They work just like in the either crate. The Result type from Rust is very similar, too.
A sum type type is generic over two types, A
and B
.
The value Left(a)
contains an inner value a
of type A
.
The value Right(b)
contains an inner value b
of type B
.
In Rust, we implement sum types as follows.
#![allow(unused)] fn main() { enum Either<A, B> { Left(A), Right(B), } }
Type Aliases
Simfony currently doesn't support Rust-like struct
s for organizing data.
#![allow(unused)] fn main() { struct User { active: bool, id: u256, sign_in_count: u64, } }
Simfony programmers have to handle long tuples of unlabeled data, which can get messy.
#![allow(unused)] fn main() { (bool, u256, u64) }
To help with the situation, programmers can define custom type aliases.
Aliases define a new name for an existing type.
In contrast, struct
s define an entirely new type, so aliases are different from struct
s.
However, aliases still help us to make the code more readable.
#![allow(unused)] fn main() { type User = (bool, u256, u64); }
There is also a list of builtin type aliases. These aliases can be used without defining them.
Builtin Alias | Definition |
---|---|
Amount1 | Either<(u1, u256), u64> |
Asset1 | Either<(u1, u256), u256> |
Confidential1 | (u1, u256) |
Ctx8 | (List<u8, 64>, (u64, u256)) |
Distance | u16 |
Duration | u16 |
ExplicitAmount | u256 |
ExplicitAsset | u256 |
ExplicitNonce | u256 |
Fe | u256 |
Ge | (u256, u256) |
Gej | ((u256, u256), u256) |
Height | u32 |
Lock | u32 |
Message | u256 |
Message64 | [u8; 64] |
Nonce | Either<(u1, u256), u256> |
Outpoint | (u256, u32) |
Point | (u1, u256) |
Pubkey | u256 |
Scalar | u256 |
Signature | [u8; 64] |
Time | u32 |
TokenAmount1 | Either<(u1, u256), u64> |
Casting
A Simfony type can be cast into another Simfony type if both types share the same structure. The structure of a type has to do with how the type is implemented on the Simplicity "processor". I will spare you the boring details.
Below is a table of types that can be cast into each other.
Type | Casts To (And Back) |
---|---|
bool | Either<(), ()> |
Option<A> | Either<(), A> |
u1 | bool |
u2 | (u1, u1) |
u4 | (u2, u2) |
u8 | (u4, u4) |
u16 | (u8, u8) |
u32 | (u16, u16) |
u64 | (u32, u32) |
u128 | (u64, u64) |
u256 | (u128, u128) |
(A) | A |
(A, B, C) | (A, (B, C)) |
(A, B, C, D) | ((A, B), (C, D)) |
… | … |
[A; 0] | () |
[A; 1] | A |
[A; 2] | (A, A) |
[A; 3] | (A, (A, A)) |
[A; 4] | ((A, A), (A, A)) |
… | … |
List<A, 2> | Option<A> |
List<A, 4> | (Option<[A; 2]>, List<A, 2>) |
List<A, 8> | (Option<[A; 4]>, List<A, 4>) |
List<A, 16> | (Option<[A; 8]>, List<A, 8>) |
List<A, 32> | (Option<[A; 16]>, List<A, 16>) |
List<A, 64> | (Option<[A; 32]>, List<A, 32>) |
List<A, 128> | (Option<[A; 64]>, List<A, 64>) |
List<A, 256> | (Option<[A; 128]>, List<A, 128>) |
List<A, 512> | (Option<[A; 256]>, List<A, 256>) |
… | … |
Casting Rules
Type A
can be cast into itself (reflexivity).
If type A
can be cast into type B
, then type B
can be cast into type A
(symmetry).
If type A
can be cast into type B
and type B
can be cast into type C
, then type A
can be cast into type C
(transitivity).
Casting Expression
All casting in Simfony happens explicitly through a casting expression.
#![allow(unused)] fn main() { <Input>::into(input) }
The above expression casts the value input
of type Input
into some output type.
The input type of the cast is explict while the output type is implicit.
In Simfony, the output type of every expression is known.
#![allow(unused)] fn main() { let x: u32 = 1; }
In the above example, the meaning of the expression 1
is clear because of the type u32
of variable x
.
Here, 1
means a string of 31 zeroes and 1 one.
In other contexts, 1
could mean something different, like a string of 255 zeroes and 1 one.
The Simfony compiler knows the type of the outermost expression, and it tries to infer the types of inner expressions based on that. When it comes to casting expressions, the compiler has no idea about the input type of the cast. The programmer needs to supply this information by annotating the cast with its input type.
#![allow(unused)] fn main() { let x: u32 = <(u16, u16)>::into((0, 1)); }
In the above example, we cast the tuple (0, 1)
of type (u16, u16)
into type u32
.
Feel free to consult the table above to verify that this is valid cast.
Let Statement
Variables are defined in let statements, just like in Rust.
#![allow(unused)] fn main() { let x: u32 = 1; }
The above let statement defines a variable called x
.
The variable is of type u32
and it is assigned the value 1
.
#![allow(unused)] fn main() { let x: u32 = f(1337); }
Variables can be assigned to the output value of any expression, such as function calls.
Explicit typing
In Simfony, the type of a defined variable always has to be written. This is different from Rust, which has better type inference.
Immutability
Simfony variables are always immutable. There are no mutable variables.
Redefinition and scoping
The same variable can be defined twice in the same scope. The later definition overrides the earlier definition.
#![allow(unused)] fn main() { let x: u32 = 1; let x: u32 = 2; assert!(jet::eq_32(x, 2)); // x == 2 }
Normal scoping rules apply: Variables from outer scopes are available inside inner scopes. A variable defined in an inner scope shadows a variable of the same name from an outer scope.
#![allow(unused)] fn main() { let x: u32 = 1; let y: u32 = 2; let z: u32 = { let x: u32 = 3; assert!(jet::eq_32(y, 2)); // y == 2 x }; assert!(jet::eq_32(x, 3)); // z == 3 }
Pattern matching
There is limited pattern matching support inside let statements.
#![allow(unused)] fn main() { let (x, y, _): (u8, u16, u32) = (1, 2, 3); let [x, _, z]: [u32; 3] = [1, 2, 3]; }
In the first line, the tuple (1, 2, 3)
is deconstructed into the values 1
, 2
and 3
.
These values are assigned to the variable names x
, y
and _
.
The variable name _
is a special name that ignores its value.
In the end, two variables are created: x: u32 = 1
and y: u16 = 2
.
Similarly, arrays can be deconstructed element by element and assigned to a variable each.
Match Expression
A match expression conditionally executes code branches. Which branch is executed depends on the input to the match expression.
#![allow(unused)] fn main() { let result: u32 = match f(42) { Left(x: u32) => x, Right(x: u16) => jet::left_pad_low_16_32(x), }; }
In the above example, the output of the function call f(42)
is matched.
f
returns an output of type Either<u32, u16>
.
If f(42)
returns a value that matches the pattern Left(x: u32)
, then the first match arm is executed.
This arm simply returns the value x
.
Alternatively, if f(42)
returns a value that matches the pattern Right(x: u16)
, then the second match arm is executed.
This arm extends the 16-bit number x
to a 32-bit number by padding its left with zeroes.
Because of type constraints, the output of f
must match one of these two patterns.
The whole match expression returns a value of type u32
, from one of the two arms.
Explicit typing
In Simfony, the type of variables inside match arms must always be written. This is different from Rust, which has better type inference.
Pattern matching
There is limited support for pattern matching inside match expressions.
Boolean values can be matched. The Boolean match expression is the replacement for an "if-then-else" in Simfony.
#![allow(unused)] fn main() { let bit_flip: bool = match false { false => true, true => false, }; }
Optional values can be matched.
The Some
arm introduces a variable which must be explicitly typed.
#![allow(unused)] fn main() { let unwrap_or_default: u32 = match Some(42) { None => 0, Some(x: u32) => x, }; }
Finally, Either
values can be matched.
Again, variables that are introduced in match arms must be explicitly typed.
#![allow(unused)] fn main() { let map_either: u32 = match Left(1337) { Left(x: u32) => f(x), Right(y: u32) => f(y), }; }
Match expressions don't support further pattern matching, in contrast to Rust.
#![allow(unused)] fn main() { let unwrap_or_default: u32 = match Some((4, 2)) { None => 0, // this doesn't compile Some((y, z): (u16, u16)) => <(u16, u16)>::into((y, z)), }; }
However, the match arm can contain code that performs the deconstruction.
For example, the tuple x
of type (u16, u16)
can be deconstructed into two integers y
and z
of type u16
.
#![allow(unused)] fn main() { let unwrap_or_default: u32 = match Some((4, 2)) { None => 0, Some(x: (u16, u16)) => { let (y, z): (u16, u16) = x; <(u16, u16)>::into((y, z)) } }; }
The match arm can also contain match expressions for further deconstruction.
For example, the sum value x
of type Either<u32, u32>
can be matched as either Left(y: u32)
or Right(z: u32)
.
#![allow(unused)] fn main() { let unwrap_or_default: u32 = match Some(Left(42)) { None => 0, Some(x: Either<u32, u32>) => match x { Left(y: u32) => y, Right(z: u32) => z, }, }; }
Functions
Functions are defined and called just like in Rust.
#![allow(unused)] fn main() { fn add(x: u32, y: u32) -> u32 { let (carry, sum): (bool, u32) = jet::add_32(x, y); match carry { true => panic!(), // overflow false => {}, // ok }; sum } }
The above example defines a function called add
that takes two parameters: variable x
of type u32
and variable y
of type u32
. The function returns a value of type u32
.
The body of the function is a block expression { ... }
that is executed from top to bottom.
The function returns on the final line (note the missing semicolon ;
).
In the above example, x
and y
are added via the add_32
jet.
The function then checks if the carry is true, signaling an overflow, in which case it panics.
On the last line, the value of sum
is returned.
The above function is called by writing its name add
followed by a list of arguments (40, 2)
.
Each parameter needs an argument, so the list of arguments is as long as the list of arguments.
Here, x
is assigned the value 40
and y
is assigned the value 2
.
#![allow(unused)] fn main() { let z: u32 = add(40, 2); }
No early returns
Simfony has no support for an early return via a "return" keyword. The only branching that is available is via match expressions.
No recursion
Simfony has no support for recursive function calls.
A function can be called inside a function body if it has been defined before.
This means that a function cannot call itself.
Loops, where f
calls g
and g
calls f
, are also impossible.
What is possible are stratified function definitions, where level-0 functions depend on nothing, level-1 functions depend on level-0 functions, and so on.
#![allow(unused)] fn main() { fn level_0() -> u32 { 0 } fn level_1() -> u32 { let (_, next) = jet::increment_32(level_0()); next } fn level_2() -> u32 { let (_, next) = jet::increment_32(level_1)); next } }
Order matters
If function g
calls function f
, then f
must be defined before g
.
#![allow(unused)] fn main() { fn f() -> u32 { 42 } fn g() -> u32 { f() } }
Main function
The main
function is the entry point of each Simfony program.
Running a program means running its main
function.
Other functions are called from the main
function.
fn main() { // ... }
The main
function is a reserved name and must exist in every program.
Simfony programs are always "binaries".
There is no support for "libraries".
Jets
Jets are predefined and optimized functions for common usecases.
#![allow(unused)] fn main() { jet::add_32(40, 2) }
Jets live inside the namespace jet
, which is why they are prefixed with jet::
.
They can be called without defining them manually.
It is usually more efficient to call a jet than to manually compute a value.
The jet documentation lists each jet and explains what it does.
Programs
A Simfony program consists of a main
function.
A program may also have type aliases or custom function definitions.
The main
function comes last in the program, because everything it calls must be defined before it.
type Furlong = u32; type Mile = u32; fn to_miles(distance: Either<Furlong, Mile>) -> Mile { match distance { Left(furlongs: Furlong) => jet::divide_32(furlongs, 8), Right(miles: Mile) => miles, } } fn main() { let eight_furlongs: Either<Furlong, Mile> = Left(8); let one_mile: Either<Furlong, Mile> = Right(1); assert!(jet::eq_32(1, to_miles(eight_furlongs))); assert!(jet::eq_32(1, to_miles(one_mile))); }