Syntax Reference

The full Resilient grammar in one page.

Table of contents

Functions

Parameters carry explicit types; zero-parameter functions use empty parentheses.

fn main() {
    println("Hello, world!");
}

fn add(int a, int b) {
    return a + b;
}

Functions can be defined in any order — forward references work:

fn caller() { return callee(); }
fn callee() { return 42; }

Return types

A -> TYPE annotation is optional. When omitted the return type is inferred from the body. Writing it out is still checked against the body:

fn square(int x) -> int { return x * x; }
fn square(int x)        { return x * x; }  // identical, inferred

fn log_once(string msg) { println(msg); }  // void, inferred

Parameter types are always required — inferring them from call-site usage produces errors at callers instead of the definition, which is worse DX.

Generic functions

Type parameters are declared with fn<T, U> name(...):

fn identity<T>(T x) -> T { return x; }
fn swap<A, B>(A a, B b) { return [b, a]; }

Type parameters are currently parse-and-AST only; the typechecker uses the HM scaffolding from RES-122 and monomorphizes at the call site. Constraints (fn<T: Trait>) are a future extension.

Contracts

requires clauses are checked on entry; ensures clauses on exit. Inside ensures, the special identifier result refers to the return value.

fn safe_div(int a, int b)
    requires b != 0
    ensures  result * b == a
{
    return a / b;
}

With --features z3, the verifier tries to discharge each clause at compile time. What it can’t prove becomes a runtime check. See Philosophy → Verifiability for the certificate story.


Variables

let x = 42;
let name = "Resilient";
x = x + 1;        // reassignment requires the name to be declared

Static variables

static let bindings persist across function calls. They’re the MVP stand-in for global state:

fn tick() {
    static let n = 0;
    n = n + 1;
    return n;
}
// tick() → 1, then 2, then 3

Live blocks

The headline feature. Code inside a live block re-executes when a recoverable error fires inside, restoring the block’s local state to its last-known-good snapshot.

live {
    let sensor_value = read_sensor();
    assert(is_valid_reading(sensor_value), "Invalid reading");
    process_data(sensor_value, threshold);
}

Optional invariants: clauses are checked after every iteration; a failed invariant triggers the same retry path as a body-level error.


Assertions

Assertions halt with a diagnostic. For comparison conditions both operand values appear in the error message:

assert(fuel >= 0, "Fuel must be non-negative");
// ASSERTION ERROR: Fuel must be non-negative
//   - condition -5 >= 0 was false

Runtime assumptions

assume(expr) is like assert but communicates intent to the verifier rather than enforcing a check on every execution path. At runtime it traps if expr is false, just like assert. When built with --features z3, the SMT verifier treats the expression as an axiom — it is assumed to hold rather than proved (verifier integration planned in RES-235).

assume(x > 0);               // asserts x is positive; verifier treats as axiom
assume(x > 0, "must be positive");  // optional message shown on trap

Data types

Type Notes
int 64-bit signed. Decimal (42), hex (0xFF), binary (0b1010). Underscore separators allowed: 0xDEAD_BEEF.
float 64-bit IEEE-754
string UTF-8 text; len(s) returns scalar count
bytes Raw byte sequence; b"\x00\x01abc" literal
bool true / false

Numeric coercion

Resilient does not implicitly coerce between numeric types. Mixing int and float in arithmetic or comparisons is a type error. Use the explicit converters:

let a = 1 + 2.0;              // ERROR: Cannot apply '+' to int and float
let b = to_float(1) + 2.0;    // ok → float 3.0
let c = 1 + to_int(2.0);      // ok → int 3
Signature Semantics
to_float(int) -> float Exact widening (for abs(x) < 2^53)
to_int(float) -> int Truncate toward zero; NaN / ±∞ / out-of-range are runtime errors

Operators

Category Operators
Arithmetic + - * / %
Comparison == != < > <= >=
Logical && \|\| ! (prefix)
Bitwise & \| ^ << >>
Prefix !x (logical-not), -x (negate)
String + (concat); int/float/bool coerce when concatenated

String comparison is lexicographic ("apple" < "banana").


Control flow

if condition {
    ...
} else {
    ...
}

while condition {
    ...
}

Parentheses around conditions are optional. while has a built-in 1,000,000-iteration runaway guard so an infinite loop terminates with a clean error rather than hanging the process.


Match expressions

match picks the first arm whose pattern matches the scrutinee.

match n {
    0 => "zero",
    x => "non-zero: " + x,
}

Arm guards

An optional if <bool-expr> after the pattern gates the arm on a runtime condition:

match n {
    x if x < 0 => "negative",
    0          => "zero",
    x if x > 100 => "big",
    _          => "small-positive",
}

A guarded arm does not count toward exhaustiveness — a match with only guarded arms still needs an unguarded catch-all.

Or-patterns

Alternatives can share an arm — <p1> | <p2> | ...:

match d {
    0 | 6             => "weekend",
    1 | 2 | 3 | 4 | 5 => "weekday",
    _                 => "invalid",
}

default keyword

default is a reserved alias for _:

match n {
    0       => "zero",
    default => "other",   // same as `_ => "other"`
}

Bind patterns (name @ inner)

A name @ inner pattern simultaneously binds the matched value to name and tests it against inner. This is useful when the arm body needs both the whole value and the confirmation that it matched:

match n {
    val @ 1..=10 => println("in range: " + val),
    _            => println("out of range"),
}

The inner pattern can be a literal, a range, a wildcard, or any other valid sub-pattern.


Type aliases

type <Name> = <Target>; at top level declares a structural alias:

type Meters = int;
fn step(Meters m) -> Meters { return m + 1; }

Aliases are structural, not nominal — Meters unifies with int at every use site. For a nominal type that doesn’t flow into int parameters, wrap in a one-field struct instead.


Structs

struct Point {
    int x,
    int y,
}

Struct literals

let p = new Point { x: 3, y: 4 };

// Shorthand: when the value expression is just the field name,
// the `: name` is optional.
let p = new Point { x, y };

// Mix explicit and shorthand in any order:
let q = new Point { x, y: y + 1 };

Destructuring

let <StructName> { ... } = expr; pulls fields into locals:

let Point { x, y } = p;          // bind both fields

let Point { x: a, y: b } = p;    // rename fields

struct Foo { int a, int b, int c, }
let f = new Foo { a: 1, b: 2, c: 3 };
let Foo { a, .. } = f;            // `..` ignores remaining fields

Without .., every declared field must appear in the pattern.


Arrays

Dynamic arrays are constructed with bracket literals and indexed with arr[i]. Indexing is bounds-checked — an out-of-range access raises E0009.

let arr = [1, 2, 3];
let first = arr[0];    // → 1
arr[1] = 42;           // in-place update
println(arr[1]);       // → 42

Fixed-size array type

Use [T; N] as a parameter or return type to declare a fixed-length array of element type T and length N:

fn sum(int a, [int; 3] v) -> int {
    return v[0] + v[1] + v[2];
}

[T; N] is a type annotation only — the length is carried in the type, not at runtime. The interpreter and VM accept both dynamic and fixed-size arrays; the JIT uses this annotation for layout decisions.


Comments

// line comment

/* block comment, can
   span multiple lines */

Built-in functions

Name Signature Notes
println(x) any → void prints, trailing newline
print(x) any → void no trailing newline; flushed
len(s) string → int Unicode scalar count
abs(x) number → number int or float
min(a, b) two numbers → number int↔float coercion
max(a, b) two numbers → number int↔float coercion
sqrt(x) number → float  
pow(a, b) two numbers → float a^b
floor(x) number → float toward -∞
ceil(x) number → float toward +∞
to_float(x) int → float Exact widening
to_int(x) float → int Truncate; errors on NaN / overflow

Diagnostics

All errors carry <file>:<line>:<col>: prefixes — editor-clickable in any tool that recognizes the format (most do). Neither the parser nor the lexer panic on any input — every error surfaces as a recoverable diagnostic. A program that fails to parse or evaluate exits non-zero so CI and shell pipelines can branch on success.

For a full index of compiler error codes see the Error Reference.


Compiling and running

# Run the interpreter
resilient examples/hello.rz

# With static type checking
resilient --typecheck examples/hello.rz

# With the verification audit
resilient --audit examples/hello.rz

# Bytecode VM
resilient --vm examples/hello.rz

# Cranelift JIT (requires --features jit at build time)
resilient --jit examples/hello.rz

# Interactive REPL
resilient

File extensions

Resilient source files use the .rz extension. The language is unrelated to Rust — no unsafe, no lifetimes, no ownership/borrow checker — and carries its own extension for clarity.