Walkthrough: building a desk calculator
The dc example in examples/dc/ is a reverse-polish notation calculator
modeled on the Unix dc tool. It's a uniquely fitting example for a
stack-based language: dc itself is a stack machine, so we're building
a stack machine inside a stack machine.
This walkthrough explains how it works, section by section. Reading the full
source in examples/dc/dc.qd alongside this page is recommended.
What dc does
dc reads tokens from its input. Numbers are pushed onto a stack; letters
are operators that pop operands and push results.
$ dc
3 4 + p # 3+4, print: 7
5 d * p # 5 squared (d duplicates): 25
16 v p # square root: 4
q # quit
With -e, you can pass an expression directly:
$ dc -e "2 3 * 4 + p"
10
The stack
The program's fundamental data structure is a Stack of f64 values:
pub struct Stack {
data: ptr
size: i64
capacity: i64
}
datais a raw memory buffersizeis how many values are currently storedcapacityis how many the buffer can hold
The buffer is allocated once with a fixed capacity (1024 slots) and never grows. If it fills up, we print an error and drop the push.
Allocation and basic operations
stack_new allocates the buffer and constructs the struct:
fn stack_new( -- s:ptr) {
1024 -> cap
cap 8 * mem::alloc! -> data
Stack {
data = data
size = 0
capacity = cap
}
}
Read left to right:
- Bind
1024to the localcap - Allocate
cap * 8bytes (each f64 is 8 bytes) intodata - Construct a
Stackwith those fields
stack_push writes a value at the current top, then increments size:
fn stack_push(s:ptr val:f64 -- ) {
s <<size -> sz
s <<capacity -> cap
sz cap >= if {
"dc: stack overflow" print nl
} else {
val s <<data sz 8 * mem::set_f64
s sz 1 + >>size!
}
}
Key syntax:
s <<sizereads thesizefield from structss sz 1 + >>size!setssizetosz + 1. The!suffix means "consume the struct after the assignment" — we don't need it back on the stack.val s <<data sz 8 * mem::set_f64writesvalat offsetsz * 8indata
stack_pop is the mirror image — decrement size, read the value:
fn stack_pop(s:ptr -- val:f64) {
s <<size -> sz
sz 0 <= if {
"dc: stack empty" print nl
0.0
} else {
sz 1 - -> sz
s sz >>size!
s <<data sz 8 * mem::get_f64
}
}
Parsing a token
is_number decides whether a token is a number or an operator. dc uses
underscore _ as the negative sign (since - is subtraction):
fn is_number(tok:str -- result:i64) {
0 -> result
tok strings::len 0 > if {
tok 0 strings::char_at! -> first
first unicode::is_digit if {
1 -> result
} else {
first unicode::underscore == first unicode::minus == or if {
tok strings::len 1 > if {
tok 1 strings::char_at! -> second
second unicode::is_digit -> result
}
}
}
}
result
}
The pattern is:
- If first char is a digit, it's a number
- If first char is
_or-and second char is a digit, it's a negative number - Otherwise, it's an operator
process_token dispatches based on this:
fn process_token(s:ptr tok:str -- ) {
tok is_number -> is_num
is_num if {
tok 0 strings::char_at! -> first
first unicode::underscore == if {
// dc convention: `_5` means -5
tok 1 tok strings::len 1 - strings::substring! cast<f64> -> val
s 0.0 val - stack_push
} else {
tok cast<f64> -> val
s val stack_push
}
}
is_num 0 == if {
tok strings::len 0 > if {
s tok process_operator
}
}
}
Notice how numbers with an _ prefix are handled by extracting the digits
after the underscore and negating.
The operator table
process_operator is where the actual calculator lives. It's a big switch
on the first character of the operator token:
fn process_operator(s:ptr op:str -- ) {
op 0 strings::char_at! switch {
unicode::plus {
s 2 stack_check if {
s stack_pop -> b
s stack_pop -> a
s a b + stack_push
}
}
unicode::minus {
// ...
}
// ... many more cases
}
}
Each case:
- Checks the stack has enough operands (
stack_check) - Pops operands — note the order: b pops first because it was pushed
last, so
a breflects the source orderathenb - Performs the operation
- Pushes the result
The operators implemented:
| Op | Description |
|---|---|
+, -, *, / |
Arithmetic |
% |
Remainder (float modulo) |
^ |
Exponentiation |
v |
Square root |
~ |
Divmod (pushes both quotient and remainder) |
p |
Print top without popping |
n |
Pop and print |
f |
Print the entire stack |
d |
Duplicate top |
r |
Reverse (swap) top two |
c |
Clear stack |
z |
Push stack depth |
q |
Quit |
Edge cases
Division and remainder check for zero denominators:
unicode::slash {
s 2 stack_check if {
s stack_peek "dc: divide by zero" check_nonzero if {
s stack_pop -> b
s stack_pop -> a
s a b / stack_push
}
}
}
stack_peek reads the top without popping — that way we can check the
divisor before committing to pop both operands. check_nonzero prints an
error and returns 0 if the value is zero; the if only proceeds on a
non-zero check.
Printing the whole stack (f)
unicode::f {
s stack_depth -> n
0 n 1 for it {
s it stack_pick print nl
}
}
stack_pick(s, n) returns the value at position n from the top. Iterating
it from 0 to n-1 walks the stack from top to bottom.
Processing a line
process_line tokenizes an input line and processes each token:
fn process_line(s:ptr input:str -- ) {
// Strip anything after `#` (comments)
input "#" strings::index_of -> comment_pos
comment_pos 0 >= if {
input 0 comment_pos strings::substring! -> input
}
input strings::trim -> trimmed
loop {
trimmed strings::len 0 <= if { break }
trimmed " " strings::index_of -> space_pos
space_pos 0 < if {
s trimmed process_token
break
}
trimmed 0 space_pos strings::substring! -> token
token strings::len 0 > if {
s token process_token
}
trimmed space_pos 1 + trimmed strings::len space_pos - 1 - strings::substring! -> trimmed
}
}
The loop repeatedly:
- Finds the next space
- Extracts the token before it
- Processes it
- Advances past the space
If there's no space, the remaining string is the last token.
The REPL
The interactive loop just reads lines from stdin until EOF:
fn repl(s:ptr -- ) {
loop {
io::readline switch {
Ok { -> input s input process_line input drop }
_ { 0 os::exit }
}
}
}
io::readline returns a Result. Ok binds the line and processes it; any
error (including EOF) exits.
Main and flags
main parses command-line flags with the flag module:
fn main() {
stack_new -> s
defer { s stack_free }
read flag::parse -> f
defer { f flag::destroy }
// ... check each flag
}
Two defer blocks ensure the stack's buffer and the flag parser are freed
when main exits, even if an earlier branch calls os::exit.
The flag logic is straightforward:
-h/--help→ print help and exit-V/--version→ print version and exit-e EXPR/--expression EXPR→ evaluate EXPR and exit-f FILE/--file FILE→ evaluate FILE and exit- No flags → start the REPL
What this example teaches
- Struct definition with fields and the
<<field/>>field!access syntax - Raw memory operations via the
memmodule for implementing data structures - Dispatch on character using
switchandunicode::constants - Named parameters as locals — the
sandtokparameters are referenced by name inside the function, not pulled off the stack deferfor cleanup that runs no matter how the function exitsio::readline switchfor result-based error handling- Stack-based thinking at its purest: the program IS a stack machine, and the implementation mirrors that directly
This is a complete, working CLI tool in about 470 lines. The stack-based style made the operator dispatch especially natural — in most languages, each operator's implementation would be wrapped in function-call syntax; here, operators look like operators.