Resilient Language Reference
A formal specification of Resilient’s lexical grammar, type system, and evaluation semantics — written for tool authors, static analysis developers, and safety auditors. For a tutorial-oriented syntax guide with worked examples, see Syntax Reference.
Table of contents
Scope and conventions
This document specifies the surface language as implemented by the
reference compiler in resilient/src/. Where the informal
SYNTAX.md
presents usage-oriented descriptions and examples, this reference
presents the grammar in EBNF, the type system as inference rules, and
runtime behaviour in terms of the operational error variants the VM
surfaces. The two documents are complementary; this one is
authoritative for questions of the form “is X syntactically legal?”,
“what type does expression E have in context Γ?”, and “what
runtime errors can operation O produce?”.
EBNF notation
Grammar rules use a standard EBNF dialect:
::=— production|— alternation[ x ]— optional (zero or one){ x }— repetition (zero or more)( x )— grouping"x"— terminal literal (source text)x*— shorthand for{ x }x+— shorthand forx { x }- Identifiers in
UpperCamelname non-terminals; lowercase names name character classes.
Source encoding
Source files are UTF-8. The lexer scans by Unicode scalar value
(char), but identifier bodies are constrained to ASCII (see
Identifiers). Newlines are \n (LF); \r\n sequences
appear as whitespace. A leading #!...\n shebang line is silently
skipped so programs can be made executable.
1. Lexical grammar
Whitespace and comments
Whitespace ::= (" " | "\t" | "\n" | "\r")+
LineComment ::= "//" { any-char-except-newline } ("\n" | eof)
BlockComment ::= "/*" { any-char | BlockComment-body } "*/"
Block comments do not nest; the first */ terminates the comment.
An unterminated block comment is a lexical error.
Identifiers
Identifier ::= (ascii-letter | "_") (ascii-letter | ascii-digit | "_")*
ascii-letter ::= "A" ... "Z" | "a" ... "z"
ascii-digit ::= "0" ... "9"
Identifiers are ASCII-only by design. Non-ASCII letters (Cyrillic, Greek, accented Latin, CJK, etc.) in identifier position are a lexical error:
1:1: identifier contains non-ASCII character 'ф' — Resilient
identifiers are ASCII-only (see SYNTAX.md)
The restriction is a homoglyph safety property: two identifiers that render identically must also compare identical by code point. String literals, comments, and file contents retain full UTF-8 — only identifier scanning is tightened.
Keywords and reserved words
Keywords cannot appear as identifiers. The complete set:
fn let live assert assume if else
return static while for in requires
ensures invariant struct new match use
impl type default true false _
default is a reserved alias for _ in match-arm position
(see §3, Match expressions); it is otherwise
illegal where an identifier is expected.
Integer literals
IntLit ::= DecIntLit | HexIntLit | BinIntLit
DecIntLit ::= ascii-digit (ascii-digit | "_")*
HexIntLit ::= "0" ("x" | "X") hex-digit (hex-digit | "_")*
BinIntLit ::= "0" ("b" | "B") bin-digit (bin-digit | "_")*
hex-digit ::= ascii-digit | "a" ... "f" | "A" ... "F"
bin-digit ::= "0" | "1"
- The underscore separator is purely visual;
1_000_000and1000000tokenize identically. - A bare
0xor0bwith no following digit tokenizes to the integer0; callers that care about well-formedness should forbid empty radix bodies at the grammar level. - All integer literals have static type
int(i64). Values outside thei64range overflow at lex time to0; real programs should assume this is a follow-up and not rely on the fallback.
Float literals
FloatLit ::= ascii-digit+ "." ascii-digit+
A float literal must have at least one digit on both sides of the
decimal point. 1. and .5 are not valid float literals: 1. scans
as the integer 1 followed by a . (field-access token), and .5
scans as a . followed by 5. There is no exponent syntax
(1.5e3), no hex-float syntax, and no suffix form. All floats have
static type float (f64, IEEE-754 binary64).
String literals
StringLit ::= "\"" { StringChar } "\""
StringChar ::= any-char-except-quote-and-backslash
| "\\" EscapeChar
EscapeChar ::= "n" | "t" | "r" | "\\" | "\""
Supported escapes: \n, \t, \r, \\, \". Unknown escapes pass
through as the literal two characters \ + the following character
(lenient recovery — the lexer never halts on a bad escape). There
are no \x, \u{...}, octal, or continuation escapes in string
literals; use a bytes literal for binary data.
Bytes literals
BytesLit ::= "b\"" { BytesByte } "\""
BytesByte ::= any-ascii-byte-except-quote-and-backslash
| "\\" BytesEscape
BytesEscape ::= "n" | "t" | "r" | "0" | "\\" | "\""
| "x" hex-digit hex-digit
Bytes literals produce a raw bytes value. Non-ASCII source bytes
inside a b"..." literal are encoded as their UTF-8 bytes (the
lexer does not reject them, but the idiomatic form is \xNN for
anything non-printable). Unicode escapes (\u{...}) are deliberately
not honoured at the bytes level.
Boolean literals
BoolLit ::= "true" | "false"
Duration literals
Duration literals appear only inside the within clause of a
live block. They are not a general time-library surface.
DurationLit ::= DecIntLit DurationUnit
DurationUnit ::= "ns" | "us" | "ms" | "s"
Operators and punctuation
Complete terminal set:
Arithmetic + - * / %
Comparison == != < > <= >=
Logical && || !
Bitwise & | ^ << >>
Assignment =
Arrow -> =>
Delimiters ( ) { } [ ] #{
Separators , ; : .
Attribute @
Other ? _
The composite token #{ opens a set literal; the closing brace is an
ordinary }. The attribute prefix @ introduces function
annotations (currently only @pure).
Operator precedence and associativity
Precedence levels, from lowest (1) to highest (10). All binary operators are left-associative; prefix operators are right-associative.
| Level | Operator(s) | Associativity |
|---|---|---|
| 1 | \|\| |
left |
| 2 | && |
left |
| 3 | \| (bitwise-or) |
left |
| 4 | ^ (bitwise-xor) |
left |
| 5 | & (bitwise-and) |
left |
| 6 | == != |
left |
| 7 | < > <= >= |
left |
| 8 | << >> |
left |
| 9 | + - |
left |
| 10 | * / % |
left |
| 11 | unary - ! |
right |
| 12 | call f(...), index a[i], field s.f |
left |
Call, index, and field-access operators are postfix; they bind tighter than any prefix operator.
2. Type system
Type syntax
Type ::= "int"
| "float"
| "string"
| "bool"
| "bytes"
| "void"
| "any"
| ArrayType
| FixedArrayType
| FunctionType
| "Result" [ "<" Type ">" ]
| Identifier (* struct name or alias *)
ArrayType ::= "[" Type "]"
FixedArrayType ::= "[" Type ";" IntLit "]"
FunctionType ::= "fn" "(" [ Type { "," Type } ] ")" "->" Type
Type universe
T ::= int | float | string | bool | bytes | void | any
| [T] -- dynamic array, element T
| [T; N] -- fixed-length array, element T, length N
| fn(T1,...,Tn) -> T -- function type
| Result<T> -- fallible computation carrying T
| struct Name -- nominal record
| ?α -- inference variable (internal)
Semantics:
intis 64-bit two’s-complement signed (i64); overflow in arithmetic traps via Rust’s checked-arithmetic layer in the verifier and saturates / wraps in the interpreter depending on the operation.floatis IEEE-754 binary64 (f64). NaN and infinities are representable;to_intrejects them.stringis an owned UTF-8 sequence.len(s)returns the Unicode scalar count, not the byte length.bytesis a raw byte sequence, distinct fromstring. No implicit conversion between the two.boolistrueorfalse.voidis the type of expressions with no value (function bodies that omitreturn,printlncalls, etc.). It has no literal form and cannot appear as a value in user code.anyis a dynamic-type escape hatch used by the typechecker during inference and by built-in signatures that accept heterogeneous arguments. User code never writesanydirectly; thecompatiblerelation treats it as unifiable with every type. Values typedanydefer all enforcement to runtime.
Compatibility relation
Two types T1 and T2 are compatible (written T1 ~ T2) iff:
T1 ~ T2 ⟺ T1 = T2 ∨ T1 = any ∨ T2 = any
Compatibility is used at argument passing, assignment, and
return-type checking. It is not transitive when any is
involved (it is strictly not an equivalence relation); the
typechecker uses it as a “don’t reject, defer” signal.
Numeric coercion rules
Resilient performs no implicit numeric coercion. Every arithmetic and comparison operator `⊕ ∈ {+, -, *, /, %, ==, !=, <,
, <=, >=}` requires both operands to share a numeric type:
Γ ⊢ e1 : int Γ ⊢ e2 : int
──────────────────────────────── (T-ArithInt)
Γ ⊢ e1 ⊕ e2 : int
Γ ⊢ e1 : float Γ ⊢ e2 : float
──────────────────────────────── (T-ArithFloat)
Γ ⊢ e1 ⊕ e2 : float
Mixing int and float is a static error. Users bridge explicitly:
| Signature | Semantics |
|---|---|
to_float(int) -> float |
exact widening (faithful for |x| < 253) |
to_int(float) -> int |
truncate toward zero; NaN / ±∞ / out-of-range → runtime error |
Comparison and equality operators share the same same-numeric-type
rule and produce bool.
String concatenation coercion
The + operator on strings is overloaded: if either operand is
string, the other operand may be int, float, bool, or
string. The non-string operand is rendered via its Display form
and concatenated. This is the only implicit conversion in the
language.
Γ ⊢ e1 : string Γ ⊢ e2 : T T ∈ {string, int, float, bool}
──────────────────────────────────────────────────────────────── (T-Concat-L)
Γ ⊢ e1 + e2 : string
Symmetric rule T-Concat-R with sides swapped.
Type aliases
TypeAliasDecl ::= "type" Identifier "=" Type ";"
Aliases are structural, not nominal. type Meters = int; makes
Meters and int interchangeable at every use site — assignment,
argument passing, return types, arithmetic. A value of type Meters
flows into an int parameter without a cast.
Cycles (type A = B; type B = A;) are a static error:
type alias cycle: A -> B -> A
For a nominal distinct-from-int type, wrap in a one-field struct:
struct Meters { int val, }
Alias declarations hoist within a file — forward references work because the typechecker collects aliases in its first pass.
Struct types
StructDecl ::= "struct" Identifier "{" { StructField } "}"
StructField ::= Type Identifier ","
Structs are nominal: two structs with identical field shape but
different names do not unify. Fields are stored in declaration order
(preserved across Display and equality).
any and safety
The any type is an escape hatch, not a user-facing construct. It
arises only in:
- Built-in signatures where argument types are heterogeneous
(
println,abs,min,max, themap_*family). - Inference intermediate states before a concrete type is known.
- Array element types (MVP — typed arrays are a planned follow-up).
Safety implications: any expression typed any bypasses static
type checking at that position. Runtime errors on mistyped any
values surface through VmError::TypeMismatch. Static analysis tools
should treat any as an unknown-type node and, if precise type
information is required, reject the program or fall back to runtime
instrumentation.
Function types
FunctionType ::= "fn" "(" [ Type { "," Type } ] ")" "->" Type
A function with an omitted return-type annotation is inferred from
the body. An omitted-return body infers void. Parameter types are
always required.
3. Expression grammar
Expression productions are stratified by precedence level. Each level delegates to the next-higher level for sub-expressions.
Expression ::= OrExpr
OrExpr ::= AndExpr { "||" AndExpr }
AndExpr ::= BitOrExpr { "&&" BitOrExpr }
BitOrExpr ::= BitXorExpr { "|" BitXorExpr }
BitXorExpr ::= BitAndExpr { "^" BitAndExpr }
BitAndExpr ::= EqExpr { "&" EqExpr }
EqExpr ::= CmpExpr { ("==" | "!=") CmpExpr }
CmpExpr ::= ShiftExpr { ("<" | ">" | "<=" | ">=") ShiftExpr }
ShiftExpr ::= AddExpr { ("<<" | ">>") AddExpr }
AddExpr ::= MulExpr { ("+" | "-") MulExpr }
MulExpr ::= UnaryExpr { ("*" | "/" | "%") UnaryExpr }
UnaryExpr ::= ("-" | "!") UnaryExpr
| PostfixExpr
PostfixExpr ::= PrimaryExpr { PostfixOp }
PostfixOp ::= "(" [ ArgList ] ")" -- call
| "[" Expression [ ".." Expression ] "]" -- index / slice
| "." Identifier -- field access
ArgList ::= Expression { "," Expression }
PrimaryExpr ::= IntLit | FloatLit | StringLit | BytesLit | BoolLit
| Identifier
| "(" Expression ")"
| ArrayLit
| SetLit
| StructLit
| IfExpr
| MatchExpr
| LiveBlock
| Block
ArrayLit ::= "[" [ Expression { "," Expression } [","] ] "]"
SetLit ::= "#{" [ Expression { "," Expression } [","] ] "}"
StructLit ::= "new" Identifier "{" [ FieldInit { "," FieldInit } [","] ] "}"
FieldInit ::= Identifier [ ":" Expression ] -- shorthand allowed
Block ::= "{" { Statement } [ Expression ] "}"
IfExpr ::= "if" Expression Block [ "else" (IfExpr | Block) ]
MatchExpr ::= "match" Expression "{" MatchArm { "," MatchArm } [","] "}"
MatchArm ::= Pattern [ "if" Expression ] "=>" Expression
Pattern ::= OrPattern
OrPattern ::= SubPattern { "|" SubPattern }
SubPattern ::= BindPattern | Literal | Identifier | "_" | "default"
BindPattern ::= Identifier "@" SubPattern
if as expression
if is an expression when all branches produce a value of the same
type; otherwise it is a statement producing void. The type rule:
Γ ⊢ c : bool Γ ⊢ e1 : T Γ ⊢ e2 : T
───────────────────────────────────────── (T-If-Expr)
Γ ⊢ (if c { e1 } else { e2 }) : T
An if without an else branch, or with mismatched branch types,
has type void.
match expressions
Type rule (simplified):
Γ ⊢ e : S
for each arm (pᵢ ⇒ bodyᵢ) with optional guard gᵢ:
Γ, bindings(pᵢ) ⊢ gᵢ : bool (if present)
Γ, bindings(pᵢ) ⊢ bodyᵢ : T
arms are exhaustive over S
────────────────────────────────────────────────── (T-Match)
Γ ⊢ match e { ... } : T
Exhaustiveness: guarded arms do not count toward exhaustiveness.
A match with only guarded arms requires an unguarded catch-all (_,
default, or a bare identifier pattern) or full literal coverage of
a finite type (e.g. both true and false for bool).
Or-patterns: every branch of an or-pattern must bind the same
set of names; otherwise or-pattern branches bind different names
is reported at typecheck.
Live blocks
LiveBlock ::= "live" [ BackoffClause ] [ WithinClause ]
[ BackoffClause ] Block
BackoffClause ::= "backoff" "(" BackoffKwarg { "," BackoffKwarg } ")"
BackoffKwarg ::= ("base_ms" | "factor" | "max_ms") "=" IntLit
WithinClause ::= "within" DurationLit
A live block is an expression of type void. The order of
backoff and within clauses is free; each clause may appear at
most once. Semantics are specified in
§7, Error model.
Array indexing and slicing
IndexOp ::= "[" Expression "]"
SliceOp ::= "[" Expression ".." Expression "]"
Indexing: a[i] produces the element type when a : [T]. Runtime
bounds check: out-of-range indices raise ArrayIndexOutOfBounds
(E0009).
Slicing: a[i..j] produces [T]. The range is half-open (i
inclusive, j exclusive); an inverted or out-of-bounds range raises
a runtime error.
Field access
s.f requires s : struct Name and f declared in Name. The
result type is the field’s declared type. Field assignment is a
statement (see §4).
4. Statement grammar
Statement ::= LetStmt
| StaticLetStmt
| AssignStmt
| ReturnStmt
| WhileStmt
| ForStmt
| ExprStmt
| FnDecl
| StructDecl
| TypeAliasDecl
| ImplDecl
| UseDecl
| AssertStmt
| ";" -- empty
LetStmt ::= "let" (LetPattern | Identifier [":" Type]) "=" Expression ";"
LetPattern ::= Identifier "{" FieldPat { "," FieldPat } [ "," ".." ] "}"
FieldPat ::= Identifier [ ":" Identifier ]
StaticLetStmt ::= "static" "let" Identifier "=" Expression ";"
AssignStmt ::= LValue "=" Expression ";"
LValue ::= Identifier
| LValue "." Identifier
| LValue "[" Expression "]"
ReturnStmt ::= "return" [ Expression ] ";"
WhileStmt ::= "while" Expression Block
ForStmt ::= "for" Identifier "in" Expression Block
ExprStmt ::= Expression ";"
AssertStmt ::= "assert" "(" Expression [ "," Expression ] ")" ";"
UseDecl ::= "use" StringLit ";"
FnDecl ::= [ "@" Identifier ] "fn" [ TypeParams ]
Identifier "(" [ ParamList ] ")"
[ "->" Type ]
{ Contract }
Block
TypeParams ::= "<" Identifier { "," Identifier } ">"
ParamList ::= Param { "," Param }
Param ::= Type Identifier
Contract ::= "requires" Expression
| "ensures" Expression
| "invariant" Expression
ImplDecl ::= "impl" Identifier "{" { FnDecl } "}"
let semantics
Γ ⊢ e : T x ∉ dom(Γ_current_scope)
───────────────────────────────────────── (T-Let)
Γ, x:T ⊢ let x = e; : void
letcreates a new binding in the current scope. Shadowing a name in an enclosing scope is permitted; re-binding within the same scope is a type error.- Subsequent
x = expr;in the same or a child scope reassigns the binding. The assigned expression must be compatible with the binding’s declared or inferred type. - The optional
:Tannotation is structurally checked against the RHS.
static let semantics
A static let binding inside a function body persists across
invocations of that function. Its initializer is evaluated exactly
once — on the first call that reaches the declaration. Subsequent
calls observe the value left by the previous call.
use semantics
use "path/to/file.rs"; is a textual splice performed by the
importer before parsing of the importing file completes — the
imported file’s top-level declarations become part of the importing
file. Imports are resolved relative to the importing file’s
directory. There is no module namespace, no re-export control, and
no symbol visibility modifier; a use makes everything in the
imported file available by its original name.
for loops
Γ ⊢ e : [T]
Γ, x:T ⊢ body : void
──────────────────────────────────── (T-For-Array)
Γ ⊢ for x in e { body } : void
Iteration over a Value::Array binds x to successive elements in
index order. Iteration over a set or map is a runtime concern; the
typechecker accepts any at the collection position.
while loops
Γ ⊢ c : bool Γ ⊢ body : void
─────────────────────────────────── (T-While)
Γ ⊢ while c { body } : void
While-loops have a built-in runaway guard: after 1,000,000
iterations of a single while instance, the interpreter raises:
while loop exceeded 1000000 iterations (runaway?)
This guard exists to preserve the progress property on safety- critical targets; it is not a user-tunable.
5. Contract clauses
Contract ::= "requires" Expression
| "ensures" Expression
| "invariant" Expression
requires — preconditions
A requires e; clause attached to a function is checked before
the function body executes. The clause is an arbitrary bool
expression in the function’s parameter scope.
for fn f(T1 p1, ..., Tn pn) requires r1 ... requires rk :
at each call site f(a1, ..., an):
evaluate ri[a1/p1, ..., an/pn] in the caller's scope
if any ri = false, raise a contract violation
ensures — postconditions
An ensures e; clause is checked after the function returns.
Inside e, the special identifier result is bound to the return
value. The clause may also reference parameters (their values at
function entry).
at function exit with return value v:
evaluate ei with { pᵢ = caller's argᵢ, result = v }
if any ei = false, raise a contract violation
invariant — live-block invariants
An invariant e; clause attached to a live { } block is checked
after every iteration of the block’s body. A failed invariant
triggers the same retry path as a body-level error (see
§7).
Static discharge (Z3)
When built with --features z3 and invoked with --audit, the
typechecker passes each requires clause with known argument values
to the Z3 SMT solver. Outcomes:
- Unsat — negation of the clause is unsatisfiable ⇒ the clause is proven and the runtime check is elided.
- Sat — a counter-example exists ⇒ the clause is reported as a static contract violation with the counter-example attached.
- Unknown (timeout) — the clause is deferred to runtime.
The per-query timeout is --verifier-timeout-ms N (default 5000).
Without --features z3, every clause is deferred to runtime.
6. Built-in functions
All built-ins are bound in the global environment at startup. User functions may shadow built-in names in an inner scope but cannot redefine a built-in at the top level without a name collision error.
I/O
| Name | Signature | Errors | Notes |
|---|---|---|---|
println(x) |
any -> void |
none | prints x + "\n"; strings print unquoted |
print(x) |
any -> void |
none | no trailing newline; stdout flushed |
input(s) |
string -> string |
I/O error → halt | prompts with s, reads one line from stdin (std-only) |
Math
| Name | Signature | Errors | Notes |
|---|---|---|---|
abs(x) |
int -> int / float -> float |
overflow on i64::MIN |
single-arg |
min(a,b) |
(T,T) -> T for T ∈ {int,float} |
type mismatch | |
max(a,b) |
(T,T) -> T for T ∈ {int,float} |
type mismatch | |
sqrt(x) |
float -> float / int -> float |
NaN on negative input |
|
pow(a,b) |
(int,int) -> int / (float,float) -> float |
int overflow → saturate |
|
floor(x) |
float -> float |
— | toward −∞ |
ceil(x) |
float -> float |
— | toward +∞ |
sin(x) |
float -> float |
— | radians |
cos(x) |
float -> float |
— | radians |
tan(x) |
float -> float |
— | radians |
ln(x) |
float -> float |
NaN on x ≤ 0 |
natural log |
log(b,x) |
(float,float) -> float |
NaN on ill-defined inputs |
base-b log |
exp(x) |
float -> float |
— | eˣ |
Numeric conversion
| Name | Signature | Errors |
|---|---|---|
to_float(x) |
int -> float |
— (exact for |x| < 253) |
to_int(x) |
float -> int |
runtime error on NaN, ±∞, or out-of-i64-range |
Arrays
| Name | Signature | Errors |
|---|---|---|
len(a) |
any -> int |
— (Unicode scalar count for strings, element count for arrays / bytes / maps / sets) |
push(a, x) |
([T], T) -> [T] |
— (returns new array) |
pop(a) |
[T] -> [T] |
— on empty, returns empty array |
slice(a,i,j) |
([T], int, int) -> [T] |
bounds → runtime error |
Strings
| Name | Signature | Errors |
|---|---|---|
split(s, sep) |
(string, string) -> [string] |
— |
trim(s) |
string -> string |
— |
contains(s, sub) |
(string, string) -> bool |
— |
to_upper(s) |
string -> string |
— |
to_lower(s) |
string -> string |
— |
replace(s,a,b) |
(string, string, string) -> string |
— |
format(tpl, args) |
(string, [any]) -> string |
arity / index mismatch → runtime error |
Bytes
| Name | Signature | Errors |
|---|---|---|
bytes_len(b) |
bytes -> int |
— |
bytes_slice(b,i,j) |
(bytes, int, int) -> bytes |
bounds → runtime error |
byte_at(b, i) |
(bytes, int) -> int |
bounds → runtime error |
Result
| Name | Signature | Errors |
|---|---|---|
Ok(x) |
T -> Result<T> |
— |
Err(x) |
T -> Result<T> |
— |
is_ok(r) |
Result -> bool |
— |
is_err(r) |
Result -> bool |
— |
unwrap(r) |
Result -> any |
halts on Err |
unwrap_err(r) |
Result -> any |
halts on Ok |
Randomness
| Name | Signature | Errors | Notes |
|---|---|---|---|
random_int(lo, hi) |
(int, int) -> int |
lo >= hi → runtime error |
uniform on [lo, hi) |
random_float() |
() -> float |
— | uniform on [0, 1) |
Both draw from a global SplitMix64 stream. The seed is either
--seed <u64> from the CLI (deterministic) or derived from the
wall clock (reported to stderr so the user can pin the next run).
Time / clock
| Name | Signature | Errors | Notes |
|---|---|---|---|
clock_ms() |
() -> int |
— | monotonic ms since an unspecified epoch; std-only |
Files (std-only)
| Name | Signature | Errors |
|---|---|---|
file_read(path) |
string -> string |
I/O error → halt |
file_write(path,c) |
(string, string) -> void |
I/O error → halt |
env(name) |
string -> Result<string> |
— (absence is Err) |
Maps and sets
| Name | Signature | Errors |
|---|---|---|
map_new() |
() -> map |
— |
map_insert(m,k,v) |
(map, K, V) -> map |
K must be int/string/bool |
map_get(m,k) |
(map, K) -> Result<V> |
— (absence is Err) |
map_remove(m,k) |
(map, K) -> map |
— |
map_keys(m) |
map -> [K] |
— |
map_len(m) |
map -> int |
— |
set_new() |
() -> set |
— |
set_insert(s,x) |
(set, T) -> set |
T must be int/string/bool |
set_remove(s,x) |
(set, T) -> set |
— |
set_has(s,x) |
(set, T) -> bool |
— |
set_len(s) |
set -> int |
— |
set_items(s) |
set -> [T] |
— (order unspecified) |
Diagnostics
| Name | Signature | Errors | Notes |
|---|---|---|---|
assert(cond, msg?) |
(bool, string?) -> void |
halts on cond = false |
second arg optional |
assume(cond, msg?) |
(bool, string?) -> void |
halts on cond = false |
verifier treats as axiom; see Syntax → Runtime assumptions |
live_retries() |
() -> int |
— | current retry count of innermost live block |
live_total_retries() |
() -> int |
— | process-wide live retry counter |
live_total_exhaustions() |
() -> int |
— | process-wide live exhaustion counter |
7. Error model
Runtime error variants
The VM surfaces runtime faults as VmError variants. The
interpreter reports an equivalent set through its diagnostics
pipeline. Variants (source: resilient/src/vm.rs):
| Variant | Trigger |
|---|---|
EmptyStack |
operand stack underflow (compiler bug, not user-reachable) |
DivideByZero |
/ or % with RHS = 0 (int); emitted as E0008 |
TypeMismatch(what) |
operator applied to wrong value types at runtime |
LocalOutOfBounds(i) |
local-slot index invalid (compiler bug) |
ConstantOutOfBounds(i) |
constant-pool index invalid (compiler bug) |
FunctionOutOfBounds(i) |
call target invalid (compiler bug) |
CallStackUnderflow |
return at top level (compiler bug) |
CallStackOverflow |
call depth exceeds 1024 frames (runaway recursion guard) |
JumpOutOfBounds |
control-flow target invalid (compiler bug) |
Unsupported(opcode) |
opcode reserved but not yet implemented |
ArrayIndexOutOfBounds |
index out of range; emitted as E0009 |
AtLine { line, kind } |
wrapper carrying source line of the failing op |
Compiler-bug variants are defensive checks against malformed
bytecode; user programs cannot cause them without a compiler
defect. The user-visible variants are DivideByZero,
ArrayIndexOutOfBounds, TypeMismatch, and CallStackOverflow.
Additionally, contract violations, assertion failures, and
unwrap/unwrap_err on the wrong variant halt execution with a
diagnostic. These surface through the interpreter rather than
VmError.
Interaction with live { } blocks
A live block supervises its body. A fault raised inside the body
is classified as recoverable or fatal:
Recoverable (trigger retry):
assert(false, ...)— assertion failure- contract violations (
requires/ensures) - failed
invariantclauses - runtime errors from built-ins (I/O failures, etc.)
DivideByZero,ArrayIndexOutOfBounds,TypeMismatchunwrapon anErr,unwrap_erron anOk
Fatal (escape the block, terminate the program):
CallStackOverflow(runaway recursion)- exhausting the
while-loop 1,000,000-iteration guard - the live block itself exceeds
MAX_RETRIES = 3
On a recoverable fault, the runtime:
- Rolls back the block’s local state to the last-known-good snapshot (captured at block entry and after each successful iteration of the body).
- Increments the retry counter.
- If
backoff(...)is set, sleepsmin(max_ms, base_ms * factor^retries)ms. - If
within <duration>is set, checks wall-clock budget; if exceeded, escalates as a timeout (distinct prefix in diagnostics). - Re-executes the body from the top.
When a live block exhausts its own budget (3 attempts by default),
it raises a Live block failed after N attempts error. If that
block is nested inside another live, the outer block treats the
failure as one recoverable fault and may itself retry. Retry
budgets at each nesting level are independent — with defaults, two
nested live blocks run the inner body up to 3 × 3 = 9 times.
Propagation without a live block
A fault raised outside any live block terminates the program with
a formatted diagnostic of the form:
<file>:<line>:<col>: <category>: <message>
and a non-zero exit code. No default retry, no unwind past main.
Result type
Result<T> is the explicit error-propagation alternative to
supervised retries. Ok(x) and Err(x) construct the two variants;
is_ok, is_err, unwrap, unwrap_err inspect them. A program
that wants to model recoverable domain errors without the live-block
machinery uses Result end-to-end.
8. Evaluation order and guarantees
Argument evaluation order
Function arguments are evaluated left to right, fully, before the callee is entered. There is no argument lazy evaluation and no compile-time argument reordering.
Short-circuit evaluation
&& and || short-circuit:
e1 && e2evaluatese1; ife1isfalse,e2is not evaluated and the result isfalse.e1 || e2evaluatese1; ife1istrue,e2is not evaluated and the result istrue.
All other operators (including &, |, ^) evaluate both operands.
Evaluation of struct literals and array literals
Fields and elements are evaluated in source order, left to right.
Determinism
Given the same input source, the same command-line flags, and the
same --seed <N> value, program output is bit-identical across
runs. Specifically:
random_int/random_floatare deterministic under--seed.- Map and set iteration orders are unspecified and may vary across
runs unless the user sorts explicitly; on std builds,
HashMapandHashSetprovide no order guarantee. (The no_std runtime uses sorted containers; programs that need ordered iteration across targets should sort at the API boundary.) clock_msis a non-determinism source; programs requiring reproducibility must avoid reading it or must record it into a trace.
Without --seed, the RNG seed is derived from the wall clock and
reported to stderr on first use so the user can pin it on the next
run.
Runaway guards
Two hard caps on pathological execution:
| Guard | Limit | On trip |
|---|---|---|
while-loop iterations |
1,000,000 per loop | while loop exceeded ... (runaway?) (fatal) |
| VM call-stack depth | 1,024 frames | CallStackOverflow (fatal) |
Both are compile-time constants in the reference implementation. They are not tunable via CLI flags.
Appendix A: Cross-reference
- Syntax Reference — tutorial-oriented walkthrough of the same features, with worked examples.
- Philosophy — design rationale for the type system,
live { }blocks, and the verifier. - Error Reference — stable error codes (E0001+) corresponding to each user-visible diagnostic.
- Memory Model — evaluation model, aliasing, and
the state-snapshot mechanism used by
liveblocks. no_stdruntime — subset of the specification that survives the embedded profile.