This guide walks through your first SBBE programs, from a single return value to loops and function calls. Each example starts with the high-level code we want to express, then shows how it lowers to SBBE IR and explains why.

Your first program

What we’re lowering

func main() -> Int32 {
    return 42
}

SBBE lowering

func $main() -> i32 {
entry:
    ldi 42
    ret
}

Every SBBE program needs at least one function. $main is the entry point (by convention). The -> i32 means it returns a 32-bit integer. The body has one block (entry:) with two instructions: ldi 42 pushes the integer 42 onto the operand stack, and ret pops it as the return value.

Understanding the stack

SBBE is a stack machine. There are no named registers. Every instruction either pushes a value onto an implicit operand stack, pops values from it, or both.

What we’re lowering

let result = (3 + 4) * 2  // = 14

Step-by-step lowering

The expression tree (3 + 4) * 2 is evaluated bottom-up. The compiler walks the tree in post-order, emitting a push for each leaf and an operation for each inner node:

  1. Push 3 (left operand of +)
  2. Push 4 (right operand of +)
  3. add.s i32 pops both, pushes 7
  4. Push 2 (right operand of *)
  5. mul.s i32 pops both, pushes 14

SBBE lowering

func $main() -> i32 {
entry:
    ldi 3        // stack: [3]
    ldi 4        // stack: [3, 4]
    add.s i32    // stack: [7]        — pops 4 and 3, pushes 7
    ldi 2        // stack: [7, 2]
    mul.s i32    // stack: [14]       — pops 2 and 7, pushes 14
    ret          //                   — pops 14 as the return value
}

Binary operations always pop the right operand first (b = pop(); a = pop()), then push the result. This means you push operands in the natural left-to-right order.

Local variables

What we’re lowering

func main() -> Int32 {
    let x = 10
    let y = 20
    return x + y  // = 30
}

Step-by-step lowering

When you need to reuse a value or your expression doesn’t fit a straight-line stack sequence, use local variables. The compiler:

  1. Declares locals with var at the top of the function (before any blocks)
  2. Uses str $name to pop a value from the stack into the local
  3. Uses ld $name to push a local’s value back onto the stack

SBBE lowering

func $main() -> i32 {
    var $x i32
    var $y i32

entry:
    ldi 10
    str $x         // x = 10
    ldi 20
    str $y         // y = 20
    ld $x
    ld $y
    add.s i32      // x + y = 30
    ret
}

Function parameters are also locals, accessed by index with ldl:

func add(a: Int32, b: Int32) -> Int32 {
    return a + b
}
func $add(i32, i32) -> i32 {
entry:
    ldl 0          // first parameter
    ldl 1          // second parameter
    add.s i32
    ret
}

Control flow with blocks and jumps

What we’re lowering

func main() -> Int32 {
    var n = 10
    var acc = 0
    while n > 0 {
        acc += n
        n -= 1
    }
    return acc  // 1 + 2 + ... + 10 = 55
}

Step-by-step lowering

SBBE uses labeled blocks and explicit jumps, like assembly. There is no if/else or while. The compiler breaks the control flow into basic blocks:

  1. entry: Initialize variables, jump to the loop condition check.
  2. check: Test if n == 0. If so, jump to done. Otherwise, fall through to body.
  3. body: Compute acc += n and n -= 1, then jump back to check.
  4. done: Load acc and return.

Each block is a straight-line sequence of instructions with a jump at the end. This flat structure maps directly to the basic blocks that all backends work with.

SBBE lowering

func $main() -> i32 {
    var $n i32
    var $acc i32

entry:
    ldi 10
    str $n
    ldi 0
    str $acc
    jmp check

check:
    ld $n
    eqz i32        // push 1 if n == 0, else 0
    jmp.if done    // jump to done if n == 0
    jmp body

body:
    ld $acc
    ld $n
    add.s i32
    str $acc        // acc += n
    ld $n
    ldi 1
    sub.s i32
    str $n          // n -= 1
    jmp check

done:
    ld $acc
    ret             // return 55
}

Calling functions

What we’re lowering

func square(x: Int32) -> Int32 {
    return x * x
}

func main() -> Int32 {
    return square(7)  // = 49
}

Step-by-step lowering

  1. Callee: $square loads its parameter twice (for x * x) using ldl 0. Since mul.s consumes both stack values, we need two copies of the parameter.
  2. Caller: Push arguments in declaration order, then call $name. The return value (if any) is left on the caller’s stack.

SBBE lowering

func $square(i32) -> i32 {
entry:
    ldl 0
    ldl 0
    mul.s i32
    ret
}

func $main() -> i32 {
entry:
    ldi 7
    call $square   // pushes 49
    ret
}

Calling external functions

What we’re lowering

// Provided by the host environment
extern func puts(s: *UInt8) -> Int32

func main() -> Int32 {
    puts("hello")
    return 0
}

Step-by-step lowering

  1. Declaration: extern func tells the assembler that $puts is not defined in this unit. The VM’s extern binding system (or a native linker) connects it at load time.
  2. Call: Same as any other call. Push arguments, then call.
  3. Discard return value: puts returns an i32 that we don’t need, so drop pops and discards it.

SBBE lowering

extern func $puts(ptr) -> i32

func $main() -> i32 {
entry:
    ldi 0          // address of string in memory
    call $puts
    drop           // discard puts return value
    ldi 0
    ret
}

The host sets up the string data in linear memory before execution.

Branchless conditionals with sel

What we’re lowering

func min(a: Int32, b: Int32) -> Int32 {
    return a < b ? a : b
}

Step-by-step lowering

For simple conditionals where both sides are cheap to compute, sel avoids a branch entirely:

  1. Push the two candidates (a and b)
  2. Push the condition (a < b)
  3. sel pops all three: if the condition is nonzero, it keeps the first candidate; otherwise the second

This compiles to a single cmov on x86 or csel on ARM, which is faster than a branch for small conditional expressions.

SBBE lowering

func $min(i32, i32) -> i32 {
entry:
    ldl 0          // a (candidate if true)
    ldl 1          // b (candidate if false)
    ldl 0          // a
    ldl 1          // b
    lt.s i32       // a < b ?
    sel            // push(cond ? a : b)
    ret
}

Next steps

Now that you understand the basics, explore the other guides:

Or dive into the Instruction Set reference for the complete list of operations.