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. By the end, you will understand how values flow through the operand stack and how to express common patterns.
Your first program
The simplest SBBE program pushes a constant and returns it:
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, 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.
To compute (3 + 4) * 2:
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
When you need to reuse a value or your expression doesn’t fit a straight-line stack sequence, use local variables. Declare them with var at the top of the function, then use ld/str to access them by name:
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(i32, i32) -> i32 {
entry:
ldl 0 // first parameter
ldl 1 // second parameter
add.s i32
ret
}
Control flow with blocks and jumps
SBBE uses labeled blocks and explicit jumps, like assembly. There is no if/else or while — you build those from jmp (unconditional) and jmp.if (conditional, pops an i32 and jumps if nonzero).
Here is a loop that computes 1 + 2 + ... + 10:
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
Push arguments in declaration order, then call:
func $square(i32) -> i32 {
entry:
ldl 0
ldl 0
mul.s i32
ret
}
func $main() -> i32 {
entry:
ldi 7
call $square // pushes 49
ret
}
The return value (if any) is left on the caller’s stack after the call.
Calling external functions
Use extern func to declare a function provided by the host environment:
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. The VM’s extern binding system (or a native linker) connects $puts to the actual implementation.
Branchless conditionals with sel
For simple conditionals where both sides are cheap to compute, sel avoids a branch entirely:
func $min(i32, i32) -> i32 {
entry:
ldl 0 // a
ldl 1 // b
ldl 0 // a (candidate if true)
ldl 1 // b (candidate if false)
lt.s i32 // a < b ?
sel // push(cond ? a : b)
ret
}
sel pops three values: condition, then the two candidates. If the condition is nonzero, it pushes the first candidate; otherwise the second.
Next steps
Now that you understand the basics, explore the other guides:
- Structs and Fields — How to represent structured data in flat memory
- Closures and Callbacks — Function pointers and captured environments
- Methods and Vtables — Object-oriented dispatch patterns
Or dive into the Instruction Set reference for the complete list of operations.