Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 .s32 source 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:

  1. write a T32 assembly program in a .s32 file
  2. assemble it into a .o32 object file
  3. 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 t32 executable
  • 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 .s32 source file into a .o32 object file
  • run: execute a .o32 program in the virtual machine
  • parse: inspect the parsed token stream for a .s32 file, mainly useful for debugging the assembler or source syntax

In other words:

  • assemble is how you build a T32 program
  • run is how you execute it
  • parse is 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 instruction
  • A: a single 8-bit general-purpose register
  • DP: a 16-bit data pointer used to access memory
  • SP: 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 zero
  • N: 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:

  1. point DP at some memory
  2. move bytes between memory and A
  3. compute in A
  4. branch using flags
  5. 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 72 loads the decimal value 72 into register A
  • PRT writes the byte in A to output
  • HLT stops 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:

  • DP points at some data
  • LDA pulls the current byte into A
  • 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
  • A is 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:

  1. points DP at value
  2. loads mem[DP] into A
  3. adds 1
  4. 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 imm8
  • ADI imm8
  • SBI imm8
  • CMI 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: copy A into the low byte of DP
  • LDH: copy A into the high byte of DP
  • SDL: copy the low byte of DP into A
  • SDH: copy the high byte of DP into A

This is useful when you need to compute an address in pieces.

Bitwise operations

T32 includes a compact set of bitwise tools:

  • AND
  • ORR
  • XOR
  • SHL
  • SHR

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 flag Z is set
  • JNG: jump if the negative flag N is 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:

  • JEQ is how you test for equality
  • JNG is 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 addr to call a subroutine
  • RET to 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

MnemonicBytesMeaningEffect
LDA1Load from memoryA = mem[DP]
STA1Store to memorymem[DP] = A
LDI imm82Load immediate into AA = imm
LDP imm16/label3Load immediate into DPDP = addr
LDL1Set low byte of DP from ADP[7:0] = A
LDH1Set high byte of DP from ADP[15:8] = A
SDL1Read low byte of DP into AA = DP[7:0]
SDH1Read high byte of DP into AA = DP[15:8]
IDP1Increment DPDP = DP + 1
DDP1Decrement DPDP = DP - 1

Arithmetic and comparison

MnemonicBytesMeaningEffect
ADD1Add memory to AA = A + mem[DP]
SUB1Subtract memory from AA = A - mem[DP]
CMP1Compare A with memoryflags from A - mem[DP]
ADI imm82Add immediateA = A + imm
SBI imm82Subtract immediateA = A - imm
CMI imm82Compare immediateflags from A - imm

These instructions update flags except for store-like operations.

Logic and shifts

MnemonicBytesMeaningEffect
AND1Bitwise ANDA = A & mem[DP]
ORR1Bitwise ORA = A | mem[DP]
XOR1Bitwise XORA = A ^ mem[DP]
SHL1Shift leftA = A << 1
SHR1Shift rightA = A >> 1

Stack and subroutines

MnemonicBytesMeaningEffect
PSH1Push Apush(A)
POP1Pop into AA = pop()
JSR addr3Jump to subroutinepush(PC); PC = addr
RET1ReturnPC = pop()

Control flow

MnemonicBytesMeaningEffect
JMP addr3Unconditional jumpPC = addr
JEQ addr3Jump if zeroif Z, jump
JNG addr3Jump if negativeif N, jump
NOP1Do nothingno state change
HLT1Halt executionstop VM

Input and output

MnemonicBytesMeaningEffect
PRT1Print byte in Aoutput A
RTR1Read byte into AA = read()

Notes on flags

T32 maintains:

  • Z for zero results
  • N for 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:

Suggested reading order

If you are new to T32, this order works well:

  1. hello.s32
  2. int2dec.s32
  3. fibo.s32
  4. int2hex.s32
  5. prime_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