Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cookbook

Copy-paste recipes. After this page you can do the common jobs: print text, loop, match, handle errors, and run the everyday project commands. Language recipes are complete modules: save one as app.spx and semaprax run app.spx. A program that exits 0 prints 0 after its own output.

module app.greet;

permit { process.stdout.write }

@id("app.main")
fn main() -> i64
    uses { process.stdout.write }
{
    let greeting = string_concat("hello, ", "world");
    let view = string_as_str(greeting);
    let written = stdout_write(str_as_bytes(view));
    if written == 12usize { 0 } else { 1 }
}

stdout_write returns the byte count. Assert it so a truncated write fails loudly. The module needs permit { process.stdout.write } and uses on main exactly as above (SPX-E101, SPX-E102 otherwise).

Sum digits with a loop

module app.sum;

@id("flow.digit_sum")
fn digit_sum(value: i64) -> i64
    requires value >= 0
    ensures result >= 0
{
    let mut remaining = value;
    let mut total = 0;
    while remaining > 0 {
        total = total + remaining % 10;
        remaining = remaining / 10;
        remaining > 0
    }
    total
}

@id("app.main")
fn main() -> i64
{
    if digit_sum(98765) == 35 { 0 } else { 1 }
}

The loop pattern: let mut state, while with scalar updates, continuation condition as the body’s last line, pure scalar result.

Classify with match

module app.sign;

@id("flow.classify")
fn classify(value: i64) -> i64
{
    match value { 0 => 0, -1 | -2 => -9, n if n < 0 => -1, _ => 1, }
}

@id("app.main")
fn main() -> i64
{
    if classify(-2) == -9 { 0 } else { 1 }
}

Order arms from specific to general; the final _ catch-all is mandatory.

Safe division with Result

module app.divide;

@id("data.checked_div")
fn checked_div(left: i64, right: i64) -> Result<i64, i64>
{
    if right == 0 { Result<i64, i64>::Err { error: 1 } } else { Result<i64, i64>::Ok { value: left / right } }
}

@id("app.main")
fn main() -> i64
{
    let ok = match checked_div(8, 2) { Result::Ok { value: v } => v, Result::Err { error: code } => code, };
    let err = match checked_div(8, 0) { Result::Ok { value: v } => v, Result::Err { error: code } => code, };
    if ok == 4 && err == 1 { 0 } else { 1 }
}

Construct with type arguments (Result<i64, i64>::Ok), match without (Result::Ok). Callers handle both arms; the compiler enforces it. The interpreter does not admit this program (SPX-F102), so run it with semaprax run app.spx --native.

Update a record immutably

module app.move_point;

@id("data.point")
record Point {
    @id("data.point.x")
    x: i64,
    @id("data.point.y")
    y: i64,
}

@id("app.main")
fn main() -> i64
{
    let origin = Point { x: 1, y: 2 };
    let moved = origin with { y: 10 };
    if moved.x == 1 && moved.y == 10 { 0 } else { 1 }
}

with builds a new value; the original is untouched. Construction must name every field. Run it with --native (SPX-F102 in the interpreter).

Count bytes in a string

module app.count;

@id("bytes.count_a")
fn count_a(text: borrow str) -> usize
{
    let view = str_as_bytes(text);
    let length = byte_len(view);
    let mut index = 0usize;
    let mut hits = 0usize;
    while index < length {
        hits = match byte_get(view, index) { Option::Some { value: byte } => if byte == 97u8 { hits + 1usize } else { hits }, Option::None {} => hits, };
        index = index + 1usize;
        index < length
    }
    hits
}

@id("app.main")
fn main() -> i64
{
    let word = "banana";
    if count_a(string_as_str(word)) == 3usize { 0 } else { 1 }
}

The byte pattern: borrow the string, view it as bytes, walk with usize indices, destructure byte_get’s Option<u8>. Byte 97u8 is 'a'.

Start a project and run its checks

semaprax new my-app && cd my-app
semaprax fmt . && semaprax check . && semaprax test . && semaprax run .

fmt first, check second. Stop at the first failure.

Add a test

Add a function to your test module and list that module under tests:

@id("my_app.tests.test_add")
fn test_add() -> i64
{
    if add(2, 2) == 4 { 0 } else { 1 }
}

semaprax test . runs every zero-argument test_* function and names failures.

Add a dependency

semaprax add . std.num "^0.1.0"
semaprax help library std.num      # exact signatures and stable IDs

Find who calls a function

semaprax query . --calls my-app.add          # callers
semaprax context . my-app.add --direction both --depth 1 --max-bytes 4096

Rename safely

semaprax change preview . rename-display-name my-app.add sum

Read the preview. It writes nothing. See Shipping.

Ship a web package

semaprax build . --target web -o dist/web
semaprax lock . --write

Gate CI on interface breaks

semaprax fmt . --check && semaprax check . && semaprax test .
semaprax lock . --compare base.lock      # exits 1 when breaking

Run a network command offline

semaprax network-run . --fixture http.fixture.json --arg https://example.test/

This uses a recorded fixture; no real connection opens. See Profiles.