Getting Started
Write your first SBBE programs and learn the fundamentals of stack-based IR.
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:
- Push
3(left operand of+) - Push
4(right operand of+) add.s i32pops both, pushes7- Push
2(right operand of*) mul.s i32pops both, pushes14
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:
- Declares locals with
varat the top of the function (before any blocks) - Uses
str $nameto pop a value from the stack into the local - Uses
ld $nameto 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:
entry: Initialize variables, jump to the loop condition check.check: Test ifn == 0. If so, jump todone. Otherwise, fall through tobody.body: Computeacc += nandn -= 1, then jump back tocheck.done: Loadaccand 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
- Callee:
$squareloads its parameter twice (forx * x) usingldl 0. Sincemul.sconsumes both stack values, we need two copies of the parameter. - 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
- Declaration:
extern functells the assembler that$putsis not defined in this unit. The VM’s extern binding system (or a native linker) connects it at load time. - Call: Same as any other call. Push arguments, then
call. - Discard return value:
putsreturns ani32that we don’t need, sodroppops 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:
- Push the two candidates (
aandb) - Push the condition (
a < b) selpops 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:
- Structs and Fields — How to represent structured data in flat memory
- Unions and Tagged Variants — Discriminated unions and option types
- Closures and Callbacks — Function pointers and captured environments
- Methods and Vtables — Object-oriented dispatch patterns
- Strings and Byte Buffers — Working with string data in linear memory
- Error Handling — Result types, return codes, and setjmp/longjmp
- Coroutines and Async — State machine lowering for generators and async
Or dive into the Instruction Set reference for the complete list of operations.