The T32 Book
A small, welcoming machine for learning assembly one clear step at a time.
Welcome. This book is a gentle introduction to T32, a tiny assembly language and virtual machine with exactly 32 instructions.
If assembly feels unfamiliar, that is completely fine. T32 is a nice place to start because the machine is small enough to hold in your head, but still rich enough to do real work. You get a single register, a stack, a data pointer, and a 64 KiB memory tape. That is not much, and that is the point.
This book is written in the same spirit as a good tutorial language book:
- we start with intuition before details
- we prefer small examples over dense theory
- we explain what each instruction is for
- we keep the reference close at hand when you are ready for it
By the end, you should feel comfortable reading T32 programs, writing your own, and understanding how a restricted instruction set can still be expressive.
Who this is for
This book is for:
- people who are curious about assembly programming
- people who like tiny machines and elegant constraints
- people who want to learn by building and reading examples
You do not need prior assembly experience. A little patience and curiosity are enough.
What T32 gives you
T32 combines three useful ideas:
- a single 8-bit register called
A - a stack for temporary storage and subroutine calls
- a memory tape addressed through a 16-bit data pointer
DP
That combination makes T32 feel smaller than a typical CPU, but more expressive than an accumulator-only toy machine.
How to read this book
If you are new to T32, read from the beginning. If you already know the basics, skip ahead to the instruction reference or the examples.
Getting Started
Let us get a working T32 toolchain on your machine first.
What the toolchain does
T32 is a fictitious assembly language. There is no physical T32 processor you can buy or boot, so running a T32 program always involves software that understands the instruction set.
In this repository, that software is the t32 executable. It is an all-in-one
tool that combines the two things you need:
- an assembler, which turns
.s32source code into T32 machine code - a virtual machine, which emulates the T32 processor and runs that code
That means the usual workflow looks like this:
- write a T32 assembly program in a
.s32file - assemble it into a
.o32object file - run that object file in the virtual machine
This is a very friendly setup for learning, because you do not need separate tools, a custom runtime environment, or any real hardware. Everything is bundled into one executable.
Build the project
From the repository root:
mkdir build
cd build
cmake ..
make -j
This builds:
- the
t32executable - the test suite
To run the tests:
make test
The three commands
The t32 executable currently supports three subcommands, though most users
will mainly care about the first two:
assemble: turn a.s32source file into a.o32object filerun: execute a.o32program in the virtual machineparse: inspect the parsed token stream for a.s32file, mainly useful for debugging the assembler or source syntax
In other words:
assembleis how you build a T32 programrunis how you execute itparseis mostly a developer aid
Here is the normal assemble-and-run flow:
APP="hello"
./t32 assemble ../examples/${APP}.s32 ../examples/${APP}.o32
./t32 run ../examples/${APP}.o32
You can think of assemble as producing the bytes that would live in T32
memory, and run as starting the fictional processor inside the emulator.
Where to go next
If you want the big picture first, continue to The Machine. If you would rather see code right away, jump to Your First Program.
The Machine
T32 is small enough that we can describe the whole machine on one page. That is a lovely property for learning.
Registers and pointers
T32 has four pieces of execution state you will think about most often:
PC: the program counter, which points at the next instructionA: a single 8-bit general-purpose registerDP: a 16-bit data pointer used to access memorySP: a 16-bit stack pointer used by stack operations and subroutines
Memory
T32 has a 64 KiB memory tape. Programs are loaded at the start of memory,
and execution begins at address $0000.
This means code and data live in the same address space. In practice, many T32
programs place instructions first and data later in the file, often after a
HLT or at the bottom of the source.
Flags
The virtual machine maintains two status flags:
Z: set when the most recent result is zeroN: set when the most recent result is negative in signed 8-bit terms
You mainly use these with conditional jumps such as JEQ and JNG.
The machine model in one sentence
Most T32 programs follow a simple rhythm:
- point
DPat some memory - move bytes between memory and
A - compute in
A - branch using flags
- use the stack when a value must survive across calls or temporary work
That is the whole dance.
Why this design is interesting
T32 is deliberately constrained:
- only one general-purpose register
- only 32 instructions
- 8-bit arithmetic
- explicit memory movement
These constraints make data flow visible. You can often look at a short T32 program and see exactly where every value comes from and where it goes.
The Language
T32 source files use the .s32 extension. The language is intentionally small
and easy to scan.
Comments
Comments start with ; and continue to the end of the line.
; this is a comment
LDI 65 ; this is also a comment
Mnemonics
Instruction names are written as mnemonics such as LDI, LDA, ADD, and
HLT.
LDI 72
PRT
HLT
Numbers
You can write numeric operands in decimal or hexadecimal.
- decimal:
10 - hexadecimal:
$0A
Examples:
LDI 65
LDI $41
LDP $1234
Labels
Labels mark addresses in the program. They are useful for jumps, subroutines, and data locations.
start:
LDI 65
PRT
HLT
You can then refer to that label as an operand:
JMP start
Sublabels
T32 also supports local-looking sublabels that begin with @. A sublabel is
scoped under the most recent top-level label.
printstr:
@loop:
LDA
CMI $00
JEQ @done
PRT
IDP
JMP @loop
@done:
RET
Inside printstr, the assembler treats @loop and @done as labels under
that section.
Data directives
T32 supports two simple directives for embedding data directly in the program.
.data
Use .data for raw bytes:
bytes:
.data $48, $69, $21, $00
.ascii
Use .ascii for a quoted string:
message:
.ascii "Hello"
.data $00
Common escapes supported by the parser include \n, \r, \t, \\, and \".
Your First Program
Let us begin with the smallest pleasant thing: printing a character.
LDI 72
PRT
HLT
What happens here
LDI 72loads the decimal value72into registerAPRTwrites the byte inAto outputHLTstops the machine
Since ASCII character 72 is H, the program prints H.
The same program written in hexadecimal looks like this:
LDI $48
PRT
HLT
A slightly friendlier example
The repository includes a full string-printing example in
examples/hello.s32.
Here is the heart of it:
LDP text
JSR printstr
HLT
printstr:
@loop:
LDA
CMI $00
JEQ @done
PRT
IDP
JMP @loop
@done:
RET
This shows a very typical T32 pattern:
DPpoints at some dataLDApulls the current byte intoA- a compare sets flags
- a conditional jump decides what to do next
Why this example matters
Even this tiny string printer demonstrates several important ideas:
- memory is read through
DP Ais the value you compute with and print- flags let you make decisions
- subroutines let you reuse code without adding more registers
Working With Data
In T32, data movement is half the story. Since there is only one general-purpose
register, it helps to get comfortable with the flow between A, DP, memory,
and the stack.
Reading and writing memory
The pair LDA and STA are your basic memory tools.
LDP value
LDA
ADI 1
STA
This sequence:
- points
DPatvalue - loads
mem[DP]intoA - adds
1 - stores the result back into memory
Walking through memory
Use IDP and DDP to move the data pointer.
LDP buffer
LDA
IDP
LDA
This reads two adjacent bytes from memory.
Immediate values
Use immediate instructions when the value is known directly in the code:
LDI imm8ADI imm8SBI imm8CMI imm8
These are often the simplest way to bring constants into a program.
Splitting and rebuilding DP
T32 lets you inspect or modify the low and high bytes of DP:
LDL: copyAinto the low byte ofDPLDH: copyAinto the high byte ofDPSDL: copy the low byte ofDPintoASDH: copy the high byte ofDPintoA
This is useful when you need to compute an address in pieces.
Bitwise operations
T32 includes a compact set of bitwise tools:
ANDORRXORSHLSHR
These all operate through A, usually using mem[DP] as the second operand for
the binary operations.
Control Flow
T32 keeps control flow simple and explicit.
Unconditional jumps
Use JMP to continue execution somewhere else.
start:
JMP again
again:
HLT
Conditional jumps
T32 currently provides two conditionals:
JEQ: jump if the zero flagZis setJNG: jump if the negative flagNis set
These instructions do not compare values by themselves. They rely on flags set
by an earlier operation such as CMP, CMI, ADD, SUB, ADI, or SBI.
LDA
CMI 10
JEQ equal_to_ten
JNG less_than_ten
In practice:
JEQis how you test for equalityJNGis how you detect a negative result after subtraction or comparison
A common loop pattern
loop:
LDA
CMI 0
JEQ done
IDP
JMP loop
done:
HLT
That style appears often in T32 because loops are built from a compare plus one or two jumps.
Subroutines and the Stack
T32 has just enough stack machinery to make structured programs pleasant.
Calling and returning
Use:
JSR addrto call a subroutineRETto return
JSR saves the current program position on the stack before jumping to the
target address. RET restores that position.
Saving values across calls
Because T32 has only one general-purpose register, PSH and POP are very
important.
printnl:
PSH
LDI $0A
PRT
POP
RET
This pattern saves A, does some work, and restores A before returning.
That is a very T32 way to write helper routines: preserve the caller’s value when it matters, and be explicit about what your routine clobbers.
A practical rule of thumb
When writing a subroutine, decide three things:
- what input it expects
- which state it may change
- whether it should preserve
A
Writing those expectations in comments makes larger T32 programs much easier to read.
Instruction Reference
This chapter is the compact reference. Come here when you know what you want to do and just need the exact instruction.
Data movement
| Mnemonic | Bytes | Meaning | Effect |
|---|---|---|---|
LDA | 1 | Load from memory | A = mem[DP] |
STA | 1 | Store to memory | mem[DP] = A |
LDI imm8 | 2 | Load immediate into A | A = imm |
LDP imm16/label | 3 | Load immediate into DP | DP = addr |
LDL | 1 | Set low byte of DP from A | DP[7:0] = A |
LDH | 1 | Set high byte of DP from A | DP[15:8] = A |
SDL | 1 | Read low byte of DP into A | A = DP[7:0] |
SDH | 1 | Read high byte of DP into A | A = DP[15:8] |
IDP | 1 | Increment DP | DP = DP + 1 |
DDP | 1 | Decrement DP | DP = DP - 1 |
Arithmetic and comparison
| Mnemonic | Bytes | Meaning | Effect |
|---|---|---|---|
ADD | 1 | Add memory to A | A = A + mem[DP] |
SUB | 1 | Subtract memory from A | A = A - mem[DP] |
CMP | 1 | Compare A with memory | flags from A - mem[DP] |
ADI imm8 | 2 | Add immediate | A = A + imm |
SBI imm8 | 2 | Subtract immediate | A = A - imm |
CMI imm8 | 2 | Compare immediate | flags from A - imm |
These instructions update flags except for store-like operations.
Logic and shifts
| Mnemonic | Bytes | Meaning | Effect |
|---|---|---|---|
AND | 1 | Bitwise AND | A = A & mem[DP] |
ORR | 1 | Bitwise OR | A = A | mem[DP] |
XOR | 1 | Bitwise XOR | A = A ^ mem[DP] |
SHL | 1 | Shift left | A = A << 1 |
SHR | 1 | Shift right | A = A >> 1 |
Stack and subroutines
| Mnemonic | Bytes | Meaning | Effect |
|---|---|---|---|
PSH | 1 | Push A | push(A) |
POP | 1 | Pop into A | A = pop() |
JSR addr | 3 | Jump to subroutine | push(PC); PC = addr |
RET | 1 | Return | PC = pop() |
Control flow
| Mnemonic | Bytes | Meaning | Effect |
|---|---|---|---|
JMP addr | 3 | Unconditional jump | PC = addr |
JEQ addr | 3 | Jump if zero | if Z, jump |
JNG addr | 3 | Jump if negative | if N, jump |
NOP | 1 | Do nothing | no state change |
HLT | 1 | Halt execution | stop VM |
Input and output
| Mnemonic | Bytes | Meaning | Effect |
|---|---|---|---|
PRT | 1 | Print byte in A | output A |
RTR | 1 | Read byte into A | A = read() |
Notes on flags
T32 maintains:
Zfor zero resultsNfor negative results
In practice, this means you often compute or compare first, then branch.
Examples
The repository already contains a nice set of real examples:
examples/hello.s32examples/fibo.s32examples/int2dec.s32examples/int2hex.s32examples/prime_sieve_u8.s32
Suggested reading order
If you are new to T32, this order works well:
hello.s32int2dec.s32fibo.s32int2hex.s32prime_sieve_u8.s32
That progression starts with simple output, then moves through data handling, subroutines, loops, and larger algorithmic programs.
A note about style
The examples in this repository already follow a helpful style:
- labels are descriptive
- sublabels keep loops local
- comments explain intent
- data is grouped near the bottom