The Maize Instruction Set Architecture, Version 1.0
This directory is the normative specification of the Maize virtual machine's instruction set architecture (ISA). Version 1.0 is the behavioral freeze point: the register model, the instruction encoding, every instruction's operation and flag effects, the memory model, the trap model, the device-facing surface, and the floating-point contract are fixed here and do not change within the v1.x series except by the additive, reserved-space rules of Chapter 12 and Chapter 15.
Freeze status
v1.0 freezes behavior, not timing. Every observable result of every instruction is pinned. The cycle-cost / performance model is specified separately (Chapter 14 is a pointer to it) and is explicitly out of the behavioral freeze. Conformance (Chapter 13) is defined against behavior alone.
How to read this specification
Each chapter is a single Markdown file. This index assigns the chapter numbers and the reading order; the chapter files themselves are the normative text. The three chapters marked EXISTS are incorporated by reference: their files are canonical and are not restated here.
Normative vs informative. Unless a passage is explicitly marked informative (or sits under a heading named "Rationale", "Note", or "Positioning"), it is normative: a conforming implementation must behave as it says. Informative passages explain intent and never add a requirement.
The reference implementation is normative ground truth. Maize is spec-as-product:
the reference VM (src/cpu.cpp, src/maize_cpu.h) is the definition of correct
behavior, and every normative claim in these chapters is grounded in the shipped
dispatch, with the repository README.md opcode and flag tables as the human-readable
cross-check. Where a chapter states what the machine does, it states what the code does.
Each newly authored chapter carries a Sourcing section naming the code and tables its
claims are keyed to.
No undefined behavior. Every condition that is undefined behavior on a conventional machine is, on Maize, either a named trap (Chapter 10) or an explicitly enumerated defined, non-trapping result. There is no third category. This is the single property that makes the machine conformance-testable and two conforming VMs bit-for-bit equivalent on every input.
Notation
Numeric radix prefixes, used throughout Maize source and documentation:
%precedes a binary value:%0100_0001.#precedes a decimal value:#123.$precedes a hexadecimal value:$FEDC.
The separators ` (back-tick), _ (underscore), and , (comma) are all legal
inside any numeric literal and carry no value; group digits however reads best
($FEDC`BA98, %0100_0001, #1,000,000). A bare token with no prefix is decimal.
An operand written with a leading @ is a memory address that gets dereferenced;
without @ it is a plain value (Chapter 5).
Table of contents
| Ch | Title | File | Status |
|---|---|---|---|
| 1 | Overview and positioning | overview.md | NEW |
| 2 | Register model | register-model.md | NEW |
| 3 | Subregister model | subregister-model.md | NEW |
| 4 | Memory model | memory-model.md | NEW |
| 5 | Addressing modes and operand encoding | addressing-modes.md | NEW |
| 6 | Instruction encoding | instruction-encoding.md | NEW |
| 7 | Instruction reference | instruction-reference.md | NEW |
| 8 | Floating-point | floating-point.md | NEW |
| 9 | Execution model | execution-model.md | NEW |
| 10 | Trap model | trap-model.md | EXISTS |
| 11 | Device-facing surface | device-surface.md | EXISTS |
| 12 | Forward compatibility and reserved space | reservations.md | EXISTS |
| 13 | Conformance | conformance.md | NEW (thin) |
| 14 | Cycle-cost model | cycle-cost.md | NEW (deferral pointer) |
| 15 | Versioning and freeze policy | versioning.md | NEW (thin) |
| A | Opcode map (numeric) | appendix-a-opcode-map.md | NEW |
| B | Encoding quick reference | appendix-b-encoding-quickref.md | NEW |
| C | Syscall surface (informative) | appendix-c-syscall-surface.md | NEW |
| D | Glossary | appendix-d-glossary.md | NEW |
What v1.0 is, in one paragraph
Maize is a flat 64-bit CISC byte-code machine with sixteen operand-addressable 64-bit registers, each sliced into byte / quarter-word / half-word / word subregisters; variable-length instructions built from a one-byte opcode (two mode bits plus a six-bit base slot) and one operand byte per operand; a sparse, no-fault, little-endian memory; five arithmetic/logic flags (C, N, V, P, Z) plus four privileged status bits; a precise, fully-defined trap model; a Zfinx floating-point extension (IEEE-754 binary32 and binary64 in the integer registers with a separate FCSR); a port-mapped device surface with no MMIO; and a reserved-space schedule that lets paging, segments, atomics, and SMP arrive later without breaking a single v1.0 binary.
Chapter 1: Overview and Positioning
This chapter is informative framing. The normative content of the specification begins in Chapter 2. Nothing here adds a requirement; it states the design goals a reader needs in order to read the rest of the document correctly.
1.1 What Maize is
Maize is a 64-bit virtual machine that executes a custom byte code. It is a "fantasy
computer": a machine designed for study and enjoyment rather than to model any physical
part, but designed to the same standard of rigor as a real ISA. The reference VM is
implemented in C++ and runs on Windows and Linux; a complete toolchain (the mazm
assembler, the mzld linker, the mzdis disassembler, and the mzcc C compiler)
targets it.
Maize is a flat 64-bit machine. There is one linear address space of 2^64 bytes. Pointers, the program counter, and the stack pointer are all full 64-bit values; there is no segmentation of the address space in v1.0 (segments, where the reservations chapter mentions them, are a future privilege/protection concept, not an addressing concept).
Maize is intentionally CISC. Arithmetic and logic instructions may take a memory
operand directly (for example ADD @R1 R0). The machine does not go load-store. The
three data-movement instructions CP / LD / ST, plus the zero-extending copy CPZ, are the
enforced memory boundary: CP/CPZ never touch memory, LD only reads memory, ST only writes
memory, and the assembler rejects any spelling that crosses those lines. This keeps the
mnemonic honest about whether an instruction touches memory while leaving the ALU free to
fold a load into an operation.
1.2 Design goals
- Spec-as-product. The specification is the deliverable. The reference VM is the authority for what every instruction does, and this document exists so that a second implementation can be written from the prose alone.
- No undefined behavior, ever. Every condition that would be undefined behavior on a conventional machine is a defined outcome on Maize: either a named trap with a stable numeric cause (Chapter 10) or an explicitly enumerated defined, non-trapping result. Out-of-range shifts, sparse-memory reads, misaligned accesses, decoded-but-undefined operand fields, and floating-point arithmetic exceptions all have defined results. This is what makes a Maize binary behave identically under analysis and in production and makes two conforming VMs indistinguishable.
- Determinism. A run is a pure function of the image and its declared inputs (argv, envp, mounts). The VM never inherits ambient host state.
- Forward compatibility by reservation. A paging MMU, base-and-bounds segments, atomics and SMP, and a future nommu-Linux port are all planned. v1.0 holds the encoding space and states the contracts (Chapter 12) so that each arrives as a v1.x extension from reserved space, with no v1.0 binary affected.
1.3 The reference-implementation principle
Where this document and the reference VM could in principle disagree, the VM is
normative and the document has a defect. Every normative claim in Chapters 2 through 9 is
grounded in the shipped dispatch (src/cpu.cpp) and the opcode/register model
(src/maize_cpu.h), with the repository README.md tables as the human cross-check.
Each chapter names its sources in a Sourcing section.
A few narrow points where the shipped code does not yet match the frozen contract are
called out explicitly in the relevant chapters. They are: the reference VM's throw-and-exit
trap delivery in place of in-guest vector delivery (Chapter 10); the $94 OUT form writing
the raw source register rather than the loaded value (Chapter 11); the unpopulated-port
dereference in place of the read-0 / write-discard outcome (Chapter 11); the not-yet-enforced
privilege gate on the port-I/O instructions (Chapter 11); and the undefined subregister
selector $F, which the reference VM currently reads as zero rather than raising the
illegal-operand trap the contract requires (Chapter 3). Each of these is a known deferred
code fix against a frozen contract, tracked separately as reference-VM work; none is a spec
defect, and the conformance suite tests the contract, not the current code in those spots.
1.4 Scope of v1.0
v1.0 freezes behavior. In scope: the register and subregister model; the addressing modes and instruction encoding; every instruction's operation, operand forms, flag effects, and trap behavior; the memory model; the execution and reset model; the trap and interrupt taxonomy; the device-facing port-I/O surface; and the IEEE-754 floating-point contract.
Out of scope for the behavioral freeze: the cycle-cost / performance model (Chapter 14, specified separately), and every reserved future extension enumerated in Chapter 12 (paging, segments, SMP/atomics ordering primitives beyond CMPXCHG, the control-register mechanism, the escape-prefix second opcode plane). Those occupy reserved encoding and contract space but define no v1.0 behavior.
1.5 Document conventions
Radix prefixes (% binary, # decimal, $ hex) and the numeric separators
(` _ ,) are described in the index. Opcodes are written as $xx hex bytes.
A reg operand is any of the sixteen registers (Chapter 2), optionally suffixed with a
subregister (Chapter 3); an operand written @X is a memory dereference (Chapter 5).
"Word" means 64 bits, "half-word" 32, "quarter-word" 16, "byte" 8.
Chapter 2: Register Model
This chapter is normative. It fixes the register set, the operand register encoding, the special-purpose register roles, and the flag-bit layout.
2.1 The register file
Maize has sixteen operand-addressable 64-bit registers. Every register is a full word (64 bits) and is sliced into subregisters by Chapter 3. The 4-bit operand register field (Chapter 5) encodes all sixteen; there is no unallocated register encoding.
| Field | Register | Role |
|---|---|---|
$0 | R0 | General purpose |
$1 | R1 | General purpose |
$2 | R2 | General purpose |
$3 | R3 | General purpose |
$4 | R4 | General purpose |
$5 | R5 | General purpose |
$6 | R6 | General purpose |
$7 | R7 | General purpose |
$8 | R8 | General purpose |
$9 | R9 | General purpose (thread pointer by ABI convention, section 2.5) |
$A | RT | Temporary register |
$B | RV | Return-value register |
$C | RF | Flag register |
$D | RB | Base-pointer register (alias BP) |
$E | RP | Program-counter register (alias PC) |
$F | RS | Stack-pointer register (alias SP) |
R0 through R9 are the ten general-purpose registers. RT, RV, RF, RB, RP, and RS are special-purpose (section 2.3). The operand register field value is the low nibble of the operand byte; the value in the table is that nibble.
2.2 The instruction register RI (not operand-addressable)
There is a seventeenth register, RI, the instruction register. The decoder writes RI as it reads each opcode byte and its operand bytes from memory. RI is decoder-internal: it has no operand-field encoding and cannot be named as an instruction operand. It is listed here only so the count is complete; software cannot read or write it.
2.3 Special-purpose registers
- RT (temporary). A general scratch register. The C calling convention reserves it as back-end scratch (not register-allocatable); see Appendix C. It is otherwise an ordinary 64-bit register.
- RV (return value). Function and syscall results are placed here by convention. Ordinary register in every other respect.
- RF (flags).
RF.H0(the low 32 bits) is aliased FL and holds the arithmetic/logic status flags (section 2.4).RF.H1(the high 32 bits) holds the privileged status flags and may only be written in privileged mode. The arithmetic/logic instructions never touch RF.H1. - RB / BP (base pointer). The frame (base) pointer for the current stack frame, a full
64-bit address.
BPis an assembler alias forRB. - RP / PC (program counter). The full 64-bit address of the next instruction to be
decoded.
PCis an alias forRP. JMP always targets the full 64-bit width and ignores any subregister selection on its operand; CALL, by contrast, honors the operand subregister and zero-extends the selected field into PC (Chapter 7 sections 7.7). - RS / SP (stack pointer). The full 64-bit address of the top of the stack.
SPis an alias forRS. The stack is full-descending: PUSH and CALL pre-decrement RS before writing, so RS always points at the last value pushed. See section 2.6 and Chapter 9 for the reset value and the process-start block.
An earlier register enum in the reference header (reg_enum) carries the legacy internal
names fl/in/pc/sp at nibbles $C..$F; that enum is a decoder-internal artifact.
The operand-addressable mapping that the ISA freezes is the table in section 2.1
(opflag_reg_* in src/maize_cpu.h, cross-checked against the README "Register bit
field"): $C=RF, $D=RB/BP, $E=RP/PC, $F=RS/SP.
2.4 The arithmetic/logic flags (FL = RF.H0)
RF.H0, aliased FL, holds five live status flags and two reserved bits. The bit
positions are frozen ISA contract:
| Bit | Symbol | Name | Meaning |
|---|---|---|---|
| 0 | C | Carry | Unsigned carry-out (ADD/ADC/INC) or borrow (SUB/SBB/CMP/CMPIND/DEC/NEG). For shifts, the last bit shifted out. Set by SETCRY, cleared by CLRCRY. |
| 1 | N | Negative | The sign bit of the result. |
| 2 | V | Overflow | Signed overflow: the signed result does not fit the operand width. |
| 3 | P | Parity / unordered | Set by FCMP when a floating-point compare is unordered (either operand is NaN). Read by the JP and SETP predicates. No integer instruction computes P. |
| 4 | Z | Zero | The result is zero. |
| 5 | - | reserved | Reserved. (The reference VM declares an unused sign-flag bit here that is never read or written.) |
| 6 | - | reserved | Reserved. |
P (bit 3) is a live, allocated flag, not spare bits. It occupies allocated encoding space, is set only by the FCMP instruction on an unordered (NaN) compare, and is consumed only by the JP (jump-if-parity) and SETP (set-if-parity) predicates. Every non-FCMP instruction leaves P unaffected. FCMP itself computes C, Z, and P together and forces N = V = 0 (Chapter 7 section 7.5 and Chapter 8). The two actually reserved flag bits are bit 5 and bit 6.
C and V are distinct. C is the unsigned carry/borrow flag and uses the x86 borrow convention: after SUB or CMP, C is set exactly when the destination was unsigned-less-than the source. This is what makes JB ("below") and JA ("above") the correct unsigned branches directly off a compare. V is the signed-overflow flag and drives the signed branches JLT and JGT. Each has a complement: JGE/JLE (signed) and JAE/JBE (unsigned).
Data movement and address computation (CP, CPZ, LD, ST, CLR, LEA) leave C/N/V/Z (and P) unchanged, matching x86 MOV / ARM / RISC-V, so a compare and its dependent branch may be separated by register shuffling. The exact per-instruction flag effects, by operand width and including the explicit statement of P for every entry, are in Chapter 7.
2.5 The privileged status flags (RF.H1)
RF.H1 holds four status bits that may be written only in privileged mode and are
unaffected by arithmetic/logic instructions. In the reference VM they occupy the low bits
of the high half (bit positions within the full 64-bit RF word given for grounding):
| RF word bit | Symbol | Meaning |
|---|---|---|
| 32 | privilege | Set in privileged (supervisor) mode; cleared in user mode. |
| 33 | interrupt-enabled | Maskable external interrupts are enabled. Toggled by SETINT / CLRINT. |
| 34 | interrupt-set | An external interrupt is pending (the raise latch signalling the run loop). |
| 35 | running | Set once execution begins; cleared by HALT. |
The privilege bit gates the privileged instructions (Chapter 7 section 7.9; the candidate privileged set is finalized with the interrupt and segment work). The interrupt-enable bit governs only maskable external interrupts; synchronous traps are unmaskable (Chapter 10).
2.6 Reset and process-start register state
At process start the register and stack state is a guaranteed contract, not incidental defaults (crt0 and the C calling convention depend on it from the first instruction):
- RP / PC = the program entry: the recorded entry point for a
.mzxexecutable, or address$0for a flat.mzbimage. - RS / SP = the base of the process-start block, so RS points at argc (Chapter 4 section
4.5). The block occupies the top of the address space and ends at
$FFFF_FFFF_FFFF_FFF8(the top of the block, not RS). The stack grows downward; the first guest push pre-decrements RS into the free region just below the block. - RB / BP = 0.
- R0..R9, RT, RV = 0.
- RF: the arithmetic/logic flags (RF.H0) are clear; the privilege bit is set (execution starts privileged); interrupts are disabled; the running bit is set once execution begins.
Full reset semantics, including paging-off and the process-start block layout, are in Chapter 9 (execution model) and Chapter 4 (memory model).
2.7 The thread pointer (R9, by convention)
Because all sixteen operand-register encodings are allocated, a thread pointer cannot be a
new operand-addressable register. v1.0 designates R9 as the thread pointer by C-ABI
convention (callee-saved, never an argument, the highest general register), the same way
RISC-V designates tp by convention. This costs no encoding and is an ABI agreement, not
an instruction; the machine, mazm, and mzdis are unchanged. See Chapter 12 section 4
and Appendix C.
Sourcing
- Register set and operand encoding:
src/maize_cpu.hopflag_reg_*constants (lines ~31-47) and theregsextern declarations (~846-866); README "Registers", "Special-purpose Registers", and "Register bit field". - Flag-bit layout:
src/cpu.cppflag-bit constantsbit_carryout/bit_negative/bit_overflow/bit_parity/bit_zero/bit_sign/bit_reserved(lines 18-24) and the RF.H1 constantsbit_privilege/bit_interrupt_enabled/bit_interrupt_set/bit_running(lines 25-28); README "Flags" table. P is set bydo_fcmp(cpu.cpp ~1001-1019) and read byeval_conditioncase 10 (cpu.cpp ~793). - Reset / process-start contract: README "Process start" and "Execution"; Chapter 9.
Chapter 3: Subregister Model
This chapter is normative. It fixes how each 64-bit register is sliced into subregisters, the subregister selector encoding, and the write-merge semantics.
3.1 The subregister layout
Every register is 64 bits and is addressable as smaller fields: bytes (B, 8 bits),
quarter-words (Q, 16 bits), half-words (H, 32 bits), and the full word (W, 64 bits). The
full register Rn is a synonym for Rn.W0. The fields tile the register from the low end:
byte offset 7 6 5 4 3 2 1 0
[B7][B6][B5][B4][B3][B2][B1][B0]
[ Q3 ][ Q2 ][ Q1 ][ Q0 ]
[ H1 ][ H0 ]
[ W0 ]
For the value $FEDC_BA98_7654_3210 in R0:
| Field | Value | Field | Value | |
|---|---|---|---|---|
| R0 / R0.W0 | $FEDCBA9876543210 | R0.Q0 | $3210 | |
| R0.H1 | $FEDCBA98 | R0.B7 | $FE | |
| R0.H0 | $76543210 | R0.B3 | $76 | |
| R0.Q3 | $FEDC | R0.B1 | $32 | |
| R0.Q2 | $BA98 | R0.B0 | $10 | |
| R0.Q1 | $7654 |
B0 is the least-significant byte, B7 the most-significant; H0 is the low half-word, H1 the high half-word; Q0..Q3 run low to high. The layout is a view over a single backing word, so it is host-endianness-independent: the numbering above is the architectural truth regardless of the host.
3.2 The subregister selector encoding
An operand byte's low nibble is the subregister selector (the high nibble is the register; Chapter 5). The fifteen defined selectors:
| Selector | Field | Width |
|---|---|---|
$0 | B0 | 1 byte |
$1 | B1 | 1 byte |
$2 | B2 | 1 byte |
$3 | B3 | 1 byte |
$4 | B4 | 1 byte |
$5 | B5 | 1 byte |
$6 | B6 | 1 byte |
$7 | B7 | 1 byte |
$8 | Q0 | 2 bytes |
$9 | Q1 | 2 bytes |
$A | Q2 | 2 bytes |
$B | Q3 | 2 bytes |
$C | H0 | 4 bytes |
$D | H1 | 4 bytes |
$E | W0 | 8 bytes |
3.3 The $F selector: illegal operand
Selector $F is an undefined subregister encoding. It is a deterministic illegal-operand
trap (cause 0; Chapter 10): an operand naming subregister $F raises the
illegal-instruction / illegal-operand fault rather than accessing any field. An assembler
will not emit $F; a hand-crafted or corrupt image that carries it traps.
The reference VM does not yet trap this encoding (it currently reads zero through an out-of-range table access); a VM correction to raise the trap is tracked separately. The frozen contract is the trap, and a conforming implementation must raise it.
3.4 Write-merge semantics
Writing a named subregister preserves the rest of the register. CP $01 R0.B0 writes
only the low byte of R0 and leaves B1..B7 untouched. Writing the full register (.W0,
or a bare Rn) replaces all 64 bits.
This merge rule is uniform across the machine:
- A load takes its width from the destination subregister and lands the bytes in exactly
that field, preserving the rest:
LD @addr R0.B0reads one byte,LD @addr R0.H0reads four,LD @addr R0reads eight (Chapter 7 section 7.1). - A copy that is narrower than its destination extends to the destination's full width: CP sign-extends, CPZ zero-extends (Chapter 5 section 5.6, Chapter 7 section 7.1). Naming a narrower destination subregister instead writes only that field and preserves the rest.
- SETcc and CLR follow the same rule: a bare register destination writes the full W0 (a clean value with no stale upper bits); an explicit subregister writes only that field.
3.5 Subregisters and floating-point
Under the Zfinx floating-point model (Chapter 8), the subregister field also selects the floating-point format of an FP operand: H0 or H1 selects binary32 (single), and W0 selects binary64 (double). A B* or Q* subregister on a floating-point operand is illegal and traps (cause 0, illegal operand). This is the only place where the subregister field carries a type rather than merely a width; see Chapter 8.
Sourcing
- Field layout and numbering: README "Sub-registers" and the graphical map;
src/maize_cpu.hsubreg_enum(lines 112-128),subreg_mask_enum(lines 131-147), and thesubword_refview (lines 155-181). - Selector encoding: README "Sub-register bit field"; the reference decode maps in
src/cpu.cpp(subreg_*_maptables). The undefined$Fselector is a frozen illegal-operand trap (Chapter 10); the reference VM's current out-of-range read is a known divergence corrected separately. - Write-merge:
src/cpu.cppwrite_subreg_bits/copy_regval_reg/copy_regval_reg_zext(lines ~615-688); README "Copy width". - FP width selection:
src/cpu.cppfp_width_from_subreg(lines ~979-989); Chapter 8.
Chapter 4: Memory Model
This chapter is normative. It fixes the address space, byte order, the sparse no-fault memory semantics, alignment behavior, and the process-start block.
4.1 Flat 64-bit address space
Maize has a single linear address space of 2^64 bytes, addressed $0 through
$FFFF_FFFF_FFFF_FFFF. There is no segmentation in v1.0: pointers, PC, and SP are all full
64-bit addresses. The machine is unbounded: there is no fixed RAM ceiling, so the top
of the stack is a chosen top-of-space constant (section 4.5), not a derived RAM limit.
Devices are not in this space. There is no memory-mapped I/O: no device register is reachable through LD / ST / CP, and a port access never touches memory. The device surface is a disjoint 16-bit port space (Chapter 11).
4.2 Byte order
Memory is little-endian. A multi-byte value is stored with its least-significant byte
at the lowest address. Immediate operands in the instruction stream are likewise
little-endian (Chapter 6), and the object/executable file formats match. Reading an 8-byte
slot back with LD @addr Rn reconstructs the value that a ST Rn @addr wrote, and the
process-start block slots (section 4.5) are read back this way.
4.3 Sparse, no-fault memory (allocate-on-write, read-zero)
Memory is sparse and lazily zero-filled:
- A read of never-written memory returns 0. There is no fault, no EFAULT, no page fault in the v1.0 flat model.
- A write to never-written memory allocates backing storage for that region on first touch, zero-filled, and stores the value.
This is a defined, non-trapping outcome (Chapter 10), not a gap in the trap taxonomy. The absence of a memory-access fault here is deliberate. Segment / bounds enforcement (trap cause 5) is a separate, reserved future path (Chapter 12); in the v1.0 flat model no access is ever out of bounds.
Implementation note (informative). The reference VM backs memory with 256-byte allocation blocks in a map keyed by block address, allocating a block on first write and returning zero for any block never allocated. The 256-byte block is an implementation detail, not an architectural boundary; software must not rely on the block size, only on the read-zero / allocate-on-write semantics above.
4.4 Alignment: defined-allow
Multi-byte accesses have no alignment requirement. A load or store of any width may sit at any address; it is stitched byte-wise across allocation blocks with no trap and no performance-visible architectural penalty in the behavioral model. Misalignment is a defined-allow outcome, not undefined behavior and not a trap. No trap vector is spent on alignment.
A load reads exactly as many bytes as its destination subregister holds and no more, so it never over-reads past its source address (Chapter 7 section 7.1).
4.5 Process start and the process-start block
At a fresh VM invocation Maize builds a System V-style process-start block at the top of
the address space and points RS (SP) at its base. There is no ELF auxiliary vector. This
makes a C main(int argc, char **argv, char **envp) callable from the first instruction.
The block occupies the top of the address space, ending at $FFFF_FFFF_FFFF_FFF8; the
guest image loads at address 0, so the two never overlap. From RS upward, every slot is 8
bytes, little-endian (read back through LD @):
RS + 0 argc
RS + 8 argv[0] -> address of argv[0]'s string
... argv[argc-1]
RS + 8 + argc*8 0 (argv NULL terminator)
next 8 envp[0] -> address of envp[0]'s string
... envp[envc-1]
... 0 (envp NULL terminator)
[higher addresses] the NUL-terminated argument and environment strings, packed
in order and ending at the top of the address space
Each argv[i] and envp[j] holds the absolute address of its NUL-terminated string.
envp is always present and NULL-terminated even with zero environment entries, so
main(argc, argv, envp) is always well-formed; a main(void) simply ignores it. The
initial register contract that accompanies this block (RS, RP, RB, the general registers,
and RF) is in Chapter 2 section 2.6 and Chapter 9.
The stack is full-descending: PUSH and CALL pre-decrement RS before writing, so at process start RS points one slot past the top usable stack slot (it points at argc), and the first guest push moves RS down into the free region just below the block. No stack-pointer wraparound is relied upon.
Sourcing
- Flat-64 and no-MMIO: README "Special-purpose Registers" flat-64 note and "Devices and
port I/O"; Chapter 11 (
docs/spec/device-surface.md). - Sparse allocate-on-touch, read-zero, 256-byte block:
src/maize_cpu.hmemory_module(block_size$100, thememory_map, lines ~373-408) andsrc/cpu.cppmemory read/write paths;docs/spec/trap-model.md"Defined non-trapping behavior". - Little-endian and misaligned defined-allow: README "Instruction Description" and the
byte-wise stitching in
memory_module;docs/spec/trap-model.md. - Process-start block and the reset register contract: README "Process start" and "Process-start block"; Chapter 9.
Chapter 5: Addressing Modes and Operand Encoding
This chapter is normative. It fixes the two opcode mode bits, the four addressing-mode
forms they select, the register-operand byte, the immediate-size field, the @ memory
marker, and the sign/zero-extension rules.
5.1 The two mode bits
The opcode byte is two flag bits (bits 7 and 6) plus a six-bit base opcode (bits 5..0; Chapter 6). For an instruction whose source operand may vary, the two mode bits select the addressing-mode form of the source operand:
%BBxx`xxxx bits 7,6 = mode bits
%x0xx`xxxx bit 6 = 0: source is a register
%x1xx`xxxx bit 6 = 1: source is an immediate value
%0xxx`xxxx bit 7 = 0: source is a value
%1xxx`xxxx bit 7 = 1: source is a memory address (dereferenced)
Bit 6 chooses register vs immediate; bit 7 chooses value vs memory address. The destination operand's shape is fixed by the instruction, not by these bits.
5.2 The four addressing-mode forms
The two bits give four forms. Using base opcode $03 (ADD) as the worked example:
| Bits 7,6 | Hex form | Source shape | Meaning (ADD) |
|---|---|---|---|
| 0 0 | $03 | regVal | Source is a register value: ADD R1 R0 |
| 0 1 | $43 | immVal | Source is an immediate: ADD $05 R0 |
| 1 0 | $83 | regAddr | Source is the value at the address in a register: ADD @R1 R0 |
| 1 1 | $C3 | immAddr | Source is the value at an immediate address: ADD @$1000 R0 |
Not every instruction defines all four forms. Data-movement instructions are held to the strict CP/LD/ST split (section 5.5): CP and CPZ define only the two value forms (regVal, immVal), because a copy never touches memory; LD defines only the two address forms, because a load always reads memory; ST is the store side. Control-transfer, stack, and zero-operand instructions define the subset that makes sense for them (Chapter 7). The opcode map (Appendix A) lists exactly which forms exist for each instruction; a form not in the map is a reserved encoding.
5.3 The register-operand byte
Each operand that names a register is one byte: the high nibble is the register (Chapter 2 section 2.1) and the low nibble is the subregister selector (Chapter 3 section 3.2).
%RRRR`SSSS RRRR = register field ($0..$F) SSSS = subregister selector ($0..$F)
For example the operand byte $3E names R3.W0 (register $3 = R3, subregister $E =
W0), and $0C names R0.H0.
5.4 The immediate-size field
When the source is an immediate (bit 6 = 1), the source operand byte carries the immediate's width in its low three bits, and the immediate bytes follow in the instruction stream, little-endian:
%xxxx`x000 $x0 1-byte immediate (8 bits)
%xxxx`x001 $x1 2-byte immediate (16 bits)
%xxxx`x010 $x2 4-byte immediate (32 bits)
%xxxx`x011 $x3 8-byte immediate (64 bits)
Bits 0..2 select the width. Bit 3 and bits 4..7 are reserved (must be zero) and carry no operation. An immediate-size encoding of 4..7 is a decoded-but-undefined operand field and decodes to the value-initialized default (Chapter 10), not a trap.
5.5 The @ memory-access marker and the CP/LD/ST boundary
In assembly, a leading @ on an operand marks a memory access: @R1 is "the value at
the address in R1", @$1000 is "the value at address $1000", and @R1.H0 dereferences
the address held in R1.H0. An operand without @ is a plain value. @ corresponds to
bit 7 = 1 (the address mode bit) on the encoding side.
The three data-movement instructions name the memory boundary explicitly, and the assembler enforces the split:
- CP (and CPZ) move a value into a register (register-to-register or
immediate-to-register). They never touch memory, so their source is never an address;
CP @...is rejected. - LD reads from a memory address into a register. Its source is always an address
(
@-prefixed);LD value(a non-address source) is rejected. - ST writes a register or immediate to a memory address. Its destination is always an address.
The ALU instructions are separate and, Maize being CISC, may take a memory-address operand
directly (ADD @R1 R0); only CP / LD / ST / CPZ are held to the strict split.
An ALU/CMP/TEST @ memory-source operand reads exactly the destination register's
declared subregister width, the same rule LD uses (section 5.6), and performs the operation
at that width with no further extension: ADD @R1 R0.B0 reads one byte at the address in R1
and adds at byte width; ADD @R1 R0 (no destination suffix) reads and adds at the full
eight-byte width. The interpreter reads only those bytes from memory, never a fixed eight, so
a sub-width @ source never over-reads past the intended address. Note that a subregister
suffix on the address operand (@R1.H0) selects which portion of R1 holds the pointer being
dereferenced; it does not affect the read width, which is governed solely by the destination.
5.6 Extension rules: sign vs zero
A source narrower than its destination is extended to the destination's full width:
- CP and the ALU immediates sign-extend.
CP $FF R0leaves R0 =$FFFF_FFFF_FFFF_FFFF(the byte$FFread as signed -1);CP $01 R0leaves R0 = 1.ADD $01 R0adds exactly 1 rather than carrying stale upper bytes, because the immediate is sign-extended to the operation width. - CPZ zero-extends.
CPZ $FF R0leaves R0 =$0000_0000_0000_00FF.
To write only part of a register and preserve the rest, name the destination subregister
explicitly (CP $01 R0.B0 writes just the low byte; Chapter 3 section 3.4). A load (LD) takes
its width from the destination subregister and preserves the surrounding bits; the
zero-extending load (LDZ) reads the same narrow width but replaces the full register with
the zero-extended value (Chapter 7 section 7.1).
5.7 Worked encoding example
CP $FFCC4411 R3 copies the 32-bit immediate $FFCC4411 into R3 (full register, so it
sign-extends):
$41 $02 $3E $11 $44 $CC $FF
$41= opcode: base$01(CP/LD family) with bit 6 set (immediate source): "CP immVal reg".$02= source operand byte: immediate-size$2(4-byte immediate).$3E= destination operand byte: register$3= R3, subregister$E= W0.$11 $44 $CC $FF= the 32-bit immediate$FFCC4411, little-endian.
The full encoding structure (opcode byte, operand bytes, immediate placement) is Chapter 6.
Sourcing
- Mode bits and the four forms: README "Opcode Bytes";
src/maize_cpu.hopcode_flag_srcReg/srcImm/srcAddr(lines 26-29) and the per-instruction form constants. - Register/subregister operand byte: README "Register bit field" and "Sub-register bit field"; Chapters 2 and 3.
- Immediate-size field: README "Immediate Parameter";
src/maize_cpu.hopflag_imm_size*(lines 74-78); operand decode insrc/cpu.cpp(op1_imm_sizeetc., lines ~507-587). @marker and CP/LD/ST split: README "CP, LD, and ST: the memory boundary".- Sign/zero-extension: README "Copy width";
src/cpu.cppcopy_regval_reg(sign-extend) andcopy_regval_reg_zext(zero-extend), lines ~659-688.
Chapter 6: Instruction Encoding
This chapter is normative. It fixes the byte-level structure of an instruction: the opcode byte, the operand bytes, and the placement of immediate values.
6.1 Instruction shape
Instructions are variable-length. An instruction is:
- one opcode byte, then
- zero, one, two, or three operand bytes (one per operand), then
- for an immediate source operand, the immediate bytes, little-endian.
There are no other instruction components. The decoder reads the opcode byte, uses it to determine how many operand bytes and immediate bytes follow, and advances the program counter (RP) past the whole instruction. When an instruction has two parameters, the first is the source and the second is the destination. Three-operand instructions (LEA, CMPXCHG, MULW, UMULW, FMADD, FMSUB) name their operands in the order the instruction's entry in Chapter 7 gives.
6.2 The opcode byte
The opcode byte is two flag (mode) bits plus a six-bit base opcode:
%BBxx`xxxx bits 7,6 mode bits (Chapter 5 section 5.1)
%xxBB`BBBB bits 5..0 base opcode (0..63)
The six base-opcode bits give 64 base slots. Most instructions occupy one base slot and
use the two mode bits to select the addressing-mode form (regVal $0x, immVal $4x,
regAddr $8x, immAddr $Cx). Some families instead pack multiple register-only
micro-ops into one base slot using the two high bits as a "condition row" selector rather
than as addressing-mode bits:
- Condition families (Jcc, SETcc): the two high bits select the condition row and the
base slot selects the column; together
(row * 3 + col)indexes one shared predicate table (Chapter 7 section 7.6/7.7). - Row-packed unary families: for example base
$31packs INC ($31), DEC ($71), NOT ($B1), NEG ($F1); base$29packs SETINT / CLRINT / SETCRY / CLRCRY; base$27packs RET ($27), IRET ($67), NOP ($A7), with row 3 ($E7) reserved. (BRK is the standalone byte$FF, not a member of base$27.) The FP unary/min-max/convert families are row-packed the same way (Chapter 8).
Which interpretation applies is fixed per base slot by the instruction map (Appendix A); the decoder does not choose.
6.3 Operand bytes
Each operand is one byte (Chapter 5 sections 5.3, 5.4):
- A register operand byte is register nibble + subregister nibble (
$3E= R3.W0). - An immediate source operand byte carries the immediate width in its low three bits
(
$02= a 4-byte immediate follows).
An instruction with N operands has N operand bytes, in source-then-destination order (with the three-operand order per the instruction's entry).
6.4 Immediate placement
Immediate bytes follow the operand bytes, in little-endian order, in the width the source operand byte selected (1, 2, 4, or 8 bytes). Only a source operand can be an immediate; a destination is always a register or a memory address named by a register or immediate.
6.5 Worked example
CP $FFCC4411 R3 encodes as seven bytes:
$41 $02 $3E $11 $44 $CC $FF
| | | |
| | | +-- immediate $FFCC4411, little-endian (low byte first)
| | +-------- destination operand byte: R3 ($3) . W0 ($E)
| +-------------- source operand byte: immediate size $2 = 4 bytes
+-------------------- opcode: base $01, mode bit 6 set (immediate source) = "CP immVal reg"
The decoder reads $41, sees an immVal-form CP, reads the source operand byte $02 (a
4-byte immediate), reads the destination operand byte $3E, then reads the four immediate
bytes, and advances RP by seven. Because CP is narrower (32-bit immediate) than its
destination (R3.W0, 64-bit), the value is sign-extended (Chapter 5 section 5.6).
6.6 Decoded-but-undefined fields
Operand-field encodings split into two cases. The undefined immediate-size encoding
4..7 decodes to the value-initialized default and never traps (Chapter 5 section 5.4). The
undefined subregister selector $F, by contrast, is a deterministic illegal-operand
trap (cause 0; Chapter 3 section 3.3, Chapter 10). An undefined opcode byte is likewise
the illegal-instruction trap (cause 0). So an undefined opcode and an undefined subregister
both trap; only the undefined immediate-size field has a defined non-trapping default.
Sourcing
- Instruction shape and the source-then-destination order: README "Instruction Description".
- Opcode byte structure and the 64 base slots: README "Opcode Bytes";
src/maize_cpu.hopcode_flag*(lines 26-29) and the base-opcode constants;docs/spec/reservations.mdsection 1. - Row-packed / condition families:
src/maize_cpu.hcond_row_*and the Jcc/SETcc/unary constants (lines ~638-745);src/cpu.cppdecode_condition/eval_condition(lines ~775-800). - Immediate placement: README "Instruction Description" and "Immediate Parameter".
Chapter 7: Instruction Reference
This chapter is normative and is the core of the specification. It documents every instruction in the opcode map, grouped by function. Floating-point instructions are documented in Chapter 8; section 7.10 cross-references them.
7.0 How to read an entry
Every entry pins the same five items, and none is left implementation-defined:
- Operation: the one-line semantics.
- Forms: each addressing-mode opcode (
$xx) and its operand shape. The mode-bit scheme is Chapter 5 section 5.1;regVal/immVal/regAddr/immAddrare the four source forms. Forms not listed for an instruction are reserved encodings. - Flags: the effect on each of C, N, V, Z, and P, by operand width where it differs.
- Encoding: the opcode byte plus operand-byte layout (Chapter 6).
- Traps: which trap cause the instruction can raise, or "none" (Chapter 10).
Flag notation. The five arithmetic/logic flags are C (carry/borrow), N (negative), V (signed overflow), Z (zero), and P (parity/unordered). The flag layout is Chapter 2 section 2.4. P is written by exactly one instruction, FCMP (Chapter 8); every other instruction in this chapter leaves P unaffected, and each entry states so explicitly. "unaffected" means the bit keeps its prior value. Unless an entry's Flags line says otherwise, the flags are computed over the destination operand's selected subregister width (byte / qword / hword / word), and the sign bit N, the overflow test V, and the zero test Z are taken at that width.
Common trap facts. An undefined opcode byte is the illegal-instruction trap (cause 0),
as is the undefined subregister selector $F on any operand (illegal-operand trap, cause 0;
Chapter 3 section 3.3). The undefined immediate-size field 4..7 does not trap; it decodes to
the value-initialized default (Chapter 6 section 6.6). Where an entry says "Traps: none", the
instruction raises no synchronous trap for any defined operand encoding or value.
7.1 Data movement
These instructions move data without arithmetic. None of them affects any flag (C/N/V/Z/P all unaffected), matching x86 MOV / ARM / RISC-V, so a compare and its dependent branch may be separated by data moves. CP / CPZ / LD / ST are the enforced memory boundary (Chapter 5 section 5.5).
RF as a destination in user mode. RF is operand-addressable, so any of these (CP / CPZ / LD / CLR / POP) may name RF as its destination. When such a write executes in user mode (privilege bit clear), the privileged RF.H1 bits (privilege, interrupt-enabled, interrupt-set, running, syscall-guest) are masked: they keep their current values while only the non-privileged RF.H0 condition flags take the written value. A user instruction thus cannot set the privilege bit by writing RF directly; the mask raises no fault (a benign flag write still lands). Supervisor writes are unmasked. See section 7.9 for the full privilege model.
CP (copy)
- Operation: copy the source value into the destination register, sign-extended to the destination subregister width (Chapter 5 section 5.6). Never touches memory.
- Forms:
$01CP regVal reg;$41CP immVal reg. (No address forms: a copy never reads memory. Base slot$01is shared with LD, which owns the address forms.) - Flags: C/N/V/Z/P unaffected.
- Encoding: opcode byte, source operand byte, destination operand byte, then immediate bytes for the immVal form.
- Traps: none.
CPZ (copy, zero-extended)
- Operation: copy the source value into the destination register, zero-extended to the destination width. Never touches memory.
- Forms:
$13CPZ regVal reg;$53CPZ immVal reg. (The address forms$93/$D3are LDZ, the zero-extending load; see the LDZ entry below.) - Flags: C/N/V/Z/P unaffected.
- Encoding: as CP.
- Traps: none.
LDZ (load, zero-extended)
- Operation: read from a memory address into the destination register, exactly like LD
(the number of bytes read is fixed by the destination subregister width), then
zero-extend the result into the full destination register, clearing the bytes above
the loaded value rather than preserving them. Equivalent to
LD dst.<width>followed byCPZ dst.<width> dst, in one instruction (card maize-204). - Forms:
$93LDZ regAddr reg (address in a register);$D3LDZ immAddr reg (immediate address). Shares CPZ's base slot$13the way LD shares CP's$01; only the two address forms are defined. - Flags: C/N/V/Z/P unaffected.
- Encoding: as LD.
- Traps: none. A read of never-written memory returns 0; misaligned reads are defined-allow.
LD (load)
- Operation: read from a memory address into the destination register. The number of bytes read is fixed by the destination subregister width, landing in that field and preserving the rest of the register (Chapter 3 section 3.4). A load never over-reads past its source address, and LD itself never zero-extends (the surrounding bits are preserved); the zero-extending load is a separate instruction, LDZ (see above).
- Forms:
$81LD regAddr reg (address in a register);$C1LD immAddr reg (immediate address). - Flags: C/N/V/Z/P unaffected.
- Encoding: opcode byte, source operand byte (the address register, or immediate-size for the immAddr form), destination operand byte, then immediate address bytes for immAddr.
- Traps: none. A read of never-written memory returns 0 (Chapter 4 section 4.3); misaligned reads are defined-allow (section 4.4).
ST (store)
- Operation: write a register value or immediate to a memory address. The number of bytes written is the source's selected width.
- Forms:
$02ST regVal regAddr (register value to address in a register);$42ST immVal regAddr (immediate value to address in a register). - Flags: C/N/V/Z/P unaffected.
- Encoding: opcode byte, source operand byte, destination (address) operand byte, then immediate bytes for immVal.
- Traps: none. A store to never-written memory allocates a zero-filled block (section 4.3); misaligned stores are defined-allow.
CLR (clear)
- Operation: set the destination to zero. A bare register destination writes the full W0 (a clean 0 with no stale upper bits); an explicit subregister writes only that field and preserves the rest.
- Forms:
$32CLR regVal (register-only; row 0 of base$32). - Flags: C/N/V/Z/P unaffected.
- Encoding: opcode byte, one register operand byte.
- Traps: none.
LEA (load effective address)
- Operation: compute operand1 + operand2 and store the sum in operand3. Address
arithmetic; no memory is dereferenced for the value forms.
LEA $-08 BP RTputsBP - 8in RT. - Forms (three operands):
$12LEA regVal reg reg;$52LEA immVal reg reg;$92LEA regAddr reg reg;$D2LEA immAddr reg reg. Operand1 is the addend (value / immediate / value-at-address), operand2 the base register, operand3 the destination. - Flags: C/N/V/Z/P unaffected (address computation never sets flags).
- Encoding: opcode byte, operand1 byte, operand2 byte, operand3 byte, then immediate bytes for the immediate forms.
- Traps: none.
XCHG (exchange)
- Operation: atomically exchange the values of two registers.
- Forms:
$E0XCHG reg reg (register-only; single encoding). - Flags: C/N/V/Z/P unaffected.
- Encoding: opcode byte, two register operand bytes.
- Traps: none.
PUSH
- Operation: push the source onto the stack. The stack is full-descending: RS (SP) is pre-decremented by the operand's size, then the value is written at RS.
- Forms:
$20PUSH regVal;$60PUSH immVal. - Flags: C/N/V/Z/P unaffected.
- Encoding: opcode byte, source operand byte, then immediate bytes for immVal.
- Traps: none. (See section 7.8 for the stack model.)
POP
- Operation: pop the top of the stack into the destination register: copy the value at RS into the register, then increment RS by the register's size.
- Forms:
$72POP regVal (register-only; row 1 of base$32). - Flags: C/N/V/Z/P unaffected.
- Encoding: opcode byte, one register operand byte.
- Traps: none.
7.2 Integer arithmetic
Unless an entry says otherwise, these set C/N/V/Z over the destination width and leave P
unaffected. Integer overflow wraps two's-complement and is observed through C and V, never
a trap (Chapter 10). All four addressing-mode source forms exist for the binary ALU ops
(regVal $0x, immVal $4x, regAddr $8x, immAddr $Cx) unless noted.
ADD
- Operation: destination = destination + source.
- Forms:
$03regVal reg,$43immVal reg,$83regAddr reg,$C3immAddr reg. - Flags: C = unsigned carry-out; N = sign of result; V = signed overflow; Z = result is zero; P unaffected.
- Encoding: opcode byte, source operand byte, destination register byte, immediate bytes for immediate forms.
- Traps: none.
SUB
- Operation: destination = destination - source.
- Forms:
$04regVal reg,$44immVal reg,$84regAddr reg,$C4immAddr reg. - Flags: C = unsigned borrow (set when destination was unsigned-less-than source); N; V = signed overflow; Z; P unaffected.
- Traps: none.
MUL
- Operation: destination = low half of (destination * source). Keeps only the low half of the product; for the full double-width product use MULW / UMULW.
- Forms:
$05regVal reg,$45immVal reg,$85regAddr reg,$C5immAddr reg. - Flags: V = signed overflow (product does not fit the destination width); C = V (C mirrors the overflow flag); N = sign of the low-half result; Z = result is zero; P unaffected.
- Traps: none.
MULW (signed wide multiply)
- Operation: full double-width signed product of operand2 by the source; low half to operand2, high half to operand3.
- Forms (three operands):
$3DregVal reg reg,$7DimmVal reg reg,$BDregAddr reg reg,$FDimmAddr reg reg. - Flags: C = high half nonzero (the product spilled out of the low register); N = sign bit of the whole double-width product; V = true signed overflow (the product does not fit the low register's signed range); Z = the entire product is zero; P unaffected.
- Traps: none.
UMULW (unsigned wide multiply)
- Operation: full double-width unsigned product of operand2 by the source; low half to operand2, high half to operand3.
- Forms:
$3EregVal reg reg,$7EimmVal reg reg,$BEregAddr reg reg,$FEimmAddr reg reg. - Flags: C = high half nonzero; V = C; N = sign bit of the whole product; Z = entire product is zero; P unaffected.
- Traps: none.
DIV (signed divide)
- Operation: destination = destination / source, signed, truncated toward zero.
- Forms:
$06regVal reg,$46immVal reg,$86regAddr reg,$C6immAddr reg. - Flags: C = 0 (cleared); V = 0 (cleared); N = sign of result; Z = result is zero; P unaffected.
- Traps: divide error (cause 2) on a zero divisor (subcode 0) or the signed
INT_MIN / -1quotient overflow (subcode 1). Fault-class; captures the DIV instruction's PC. Unhandled, the machine halts deterministically with cause 2 surfaced.
MOD (signed remainder)
- Operation: destination = destination mod source, signed; the remainder takes the sign of the dividend.
- Forms:
$07regVal reg,$47immVal reg,$87regAddr reg,$C7immAddr reg. - Flags: C = 0; V = 0; N = sign of result; Z = result is zero; P unaffected.
- Traps: divide error (cause 2), as DIV.
UDIV (unsigned divide)
- Operation: destination = destination / source, unsigned.
- Forms:
$35regVal reg,$75immVal reg,$B5regAddr reg,$F5immAddr reg. - Flags: C = 0; V = 0; N = sign of result (the high bit of the unsigned quotient at the destination width); Z = result is zero; P unaffected.
- Traps: divide error (cause 2) on a zero divisor (subcode 0). Unsigned division has no quotient-overflow case.
UMOD (unsigned remainder)
- Operation: destination = destination mod source, unsigned.
- Forms:
$36regVal reg,$76immVal reg,$B6regAddr reg,$F6immAddr reg. - Flags: C = 0; V = 0; N; Z; P unaffected.
- Traps: divide error (cause 2) on a zero divisor.
INC
- Operation: destination = destination + 1.
- Forms:
$31regVal (register-only; row 0 of base$31). - Flags: C = unsigned carry-out; N; V = signed overflow; Z; P unaffected. (Add-family flags.)
- Traps: none.
DEC
- Operation: destination = destination - 1.
- Forms:
$71regVal (row 1 of base$31). - Flags: C = unsigned borrow; N; V = signed overflow; Z; P unaffected. (Sub-family flags.)
- Traps: none.
NEG (two's-complement negate)
- Operation: destination = 0 - destination.
- Forms:
$F1regVal (row 3 of base$31). - Flags: sub-family flags computed as
0 - value: C is set when the value is non-zero (a borrow occurs for any non-zero operand); N = sign of the result; V is set only for the width's most-negative value (whose negation overflows); Z = result is zero; P unaffected. - Traps: none.
ADC (add with carry)
- Operation: destination = destination + source + C. The low word of a multi-word add runs ADD (which sets C); each higher word runs ADC.
- Forms:
$3BregVal reg,$7BimmVal reg,$BBregAddr reg,$FBimmAddr reg. - Flags: C = unsigned carry-out of the full
dst + src + C; N; V = signed overflow; Z reflects only the current word (a full-width zero test ANDs the per-word Z results across the chain); P unaffected. - Traps: none.
SBB (subtract with borrow)
- Operation: destination = destination - source - C. The low word runs SUB; each higher word runs SBB.
- Forms:
$3CregVal reg,$7CimmVal reg,$BCregAddr reg,$FCimmAddr reg. - Flags: C = unsigned borrow of the full
dst - src - C; N; V = signed overflow; Z reflects only the current word; P unaffected. - Traps: none.
7.3 Logic
Bitwise operations. All set C = 0 and V = 0 (cleared), compute N and Z over the destination width, and leave P unaffected. Full four source forms for the binary ops.
AND
- Operation: destination = destination AND source.
- Forms:
$08regVal reg,$48immVal reg,$88regAddr reg,$C8immAddr reg. - Flags: C = 0; V = 0; N = sign of result; Z = result is zero; P unaffected.
- Traps: none.
OR
- Operation: destination = destination OR source.
- Forms:
$09regVal reg,$49immVal reg,$89regAddr reg,$C9immAddr reg. - Flags: C = 0; V = 0; N; Z; P unaffected.
- Traps: none.
XOR
- Operation: destination = destination XOR source.
- Forms:
$0CregVal reg,$4CimmVal reg,$8CregAddr reg,$CCimmAddr reg. - Flags: C = 0; V = 0; N; Z; P unaffected.
- Traps: none.
NAND
- Operation: destination = NOT (destination AND source).
- Forms:
$0BregVal reg,$4BimmVal reg,$8BregAddr reg,$CBimmAddr reg. - Flags: C = 0; V = 0; N; Z; P unaffected.
- Traps: none.
NOR
- Operation: destination = NOT (destination OR source).
- Forms:
$0AregVal reg,$4AimmVal reg,$8AregAddr reg,$CAimmAddr reg. - Flags: C = 0; V = 0; N; Z; P unaffected.
- Traps: none.
NOT (one's complement)
- Operation: destination = NOT destination (bitwise one's complement).
- Forms:
$B1regVal (register-only; row 2 of base$31). - Flags: C = 0; V = 0; N; Z; P unaffected.
- Traps: none.
7.4 Shift
Shift counts are defined for every value (Chapter 10): a count of 0 leaves all flags unaffected; the over-width behavior is stated per instruction. An out-of-range count never invokes host undefined behavior. All shifts leave P unaffected. Full four source forms.
SHL (shift left)
- Operation: destination = destination << source.
- Forms:
$0DregVal reg,$4DimmVal reg,$8DregAddr reg,$CDimmAddr reg. - Flags: for count
nover operand widthbits:n == 0leaves all flags unaffected;1 <= n <= bitsshifts normally with C = the last bit shifted out, N = sign of result, Z, and V defined only forn == 1(V = the sign bit changed);n > bitsyields result 0 with C = 0, N = 0, V = 0, Z = 1. P unaffected throughout. - Traps: none.
SHR (logical shift right)
- Operation: destination = destination >> source (zero-fill).
- Forms:
$0EregVal reg,$4EimmVal reg,$8EregAddr reg,$CEimmAddr reg. - Flags:
n == 0leaves flags unaffected;1 <= n <= bitsshifts with C = last bit shifted out, N = sign of result, Z, and V defined only forn == 1(V = the prior sign bit);n > bitsyields result 0 with C = N = V = 0, Z = 1. P unaffected. - Traps: none.
SAR (arithmetic shift right)
- Operation: destination = destination >> source with sign replication (the sign bit fills the vacated high bits).
- Forms:
$2EregVal reg,$6EimmVal reg,$AEregAddr reg,$EEimmAddr reg. - Flags:
n == 0leaves flags unaffected;1 <= n <= bits-1shifts right with sign replicated, C = the last bit shifted out (bitn-1of the operand), V = 0 always (an arithmetic shift replicates the sign and can never flip it), N = sign, Z;n >= bitssaturates to the sign fill: all-ones (the width's -1) for a negative operand or 0 for a non-negative one, with C = the operand's sign bit, N = the operand's sign, V = 0, and Z set only for a non-negative operand. P unaffected. This over-width behavior diverges from SHR, whose over-width count returns 0. - Traps: none.
7.5 Compare and test
These compute flags without writing a general register. Full source forms as noted.
CMP
- Operation: set flags by computing destination - source, discarding the difference.
- Forms:
$0FregVal reg,$4FimmVal reg,$8FregAddr reg,$CFimmAddr reg. - Flags: identical to SUB: C = unsigned borrow, N, V = signed overflow, Z; P unaffected. The C borrow convention is what makes JB/JA the correct unsigned branches and V the signed ones (Chapter 2 section 2.4).
- Traps: none.
CMPIND (compare indirect)
- Operation: set flags by computing (value at the address in the destination register) minus the source. Compares against a value in memory.
- Forms:
$2FregVal regAddr;$6FimmVal regAddr. - Flags: SUB-family: C = unsigned borrow, N, V, Z; P unaffected.
- Traps: none.
TEST
- Operation: set flags by computing destination AND source, discarding the result.
- Forms:
$10regVal reg,$50immVal reg,$90regAddr reg,$D0immAddr reg. - Flags: logic-family: C = 0, V = 0, N = sign of the AND, Z = the AND is zero; P unaffected.
- Traps: none.
TSTIND (test indirect)
- Operation: set flags by ANDing the source with the value at the address in the destination register.
- Forms:
$30regVal regAddr;$70immVal regAddr. - Flags: logic-family: C = 0, V = 0, N, Z; P unaffected.
- Traps: none.
CMPXCHG (compare-and-swap)
- Operation: the frozen compare-and-swap primitive. Three operands
CMPXCHG new target expected(operand1 = new value, operand2 = target, operand3 = expected). Compare the target (operand2) against the expected value (operand3) over the selected subregister width. On equality (success): set Z = 1 and copy the new value (operand1) into the target (operand2). On inequality (failure): set Z = 0 and copy the current target (operand2) into the expected register (operand3), handing the caller the observed value for a retry loop. - Forms:
$11regVal reg reg;$51immVal reg reg;$91regAddr reg reg;$D1immAddr reg reg (operand1 varies; target and expected are always registers). - Flags: Z is the sole and authoritative success indicator (1 = swapped, 0 = not swapped). C, N, V, and P are unaffected: the equality test writes no flag and the copy is flag-neutral. A caller tests success with JZ / JNZ.
- Encoding: opcode byte, operand1 byte, operand2 byte, operand3 byte, then immediate bytes for the immediate forms.
- Traps: none. Under single-hart v1.0 the operation is trivially atomic; a conforming multi-hart v1.x implementation must keep it an atomic CAS with exactly these effects (Chapter 12 section 2).
FCMP (floating-point compare) -- the only writer of P
- Operation: compare the destination floating-point value against the source and set the integer flags per the IEEE-754 outcome. Documented fully in Chapter 8; summarized here because it is the sole instruction that writes the P flag.
- Forms:
$2AregVal reg,$6AimmVal reg,$AAregAddr reg,$EAimmAddr reg. - Flags: FCMP computes C, Z, and P together and forces N = 0 and V = 0. The mapping
(x86 UCOMISD convention):
a > src-> C=0 Z=0 P=0;a < src-> C=1 Z=0 P=0;a == src-> C=0 Z=1 P=0; unordered (either operand NaN) -> C=1 Z=1 P=1. P is the unordered / NaN indicator and is written only here; JP and SETP consume it. After FCMP the ordered branch idioms JB/JBE/JA/JAE/JZ work directly, and JP/SETP test the unordered case (Chapter 8). - Traps: none for arithmetic (FP exceptions are sticky, not trapping); illegal FP operand encodings trap cause 0. See Chapter 8.
7.6 Conditional set (SETcc)
SETcc materializes a condition as 0 or 1 in a destination register. It reads the flags and is flag-neutral: it writes no flag (C/N/V/Z/P all unaffected). A bare register destination writes the full W0 (a clean 0/1, no stale upper bits); an explicit subregister writes only that field and preserves the rest, exactly like CLR. Each predicate is identical to the matching Jcc branch (section 7.7), so a SETcc and its branch can never disagree for the same flag state. Each is a register-only instruction, one register operand.
| Mnemonic | Opcode | Condition | Predicate |
|---|---|---|---|
| SETZ | $2B | equal / zero | Z == 1 |
| SETNZ | $2C | not-equal / nonzero | Z == 0 |
| SETLT | $2D | signed < | N != V |
| SETGE | $AB | signed >= | N == V |
| SETGT | $6C | signed > | Z == 0 and N == V |
| SETLE | $AC | signed <= | Z == 1 or N != V |
| SETB | $6B | unsigned < | C == 1 |
| SETAE | $EB | unsigned >= | C == 0 |
| SETA | $6D | unsigned > | C == 0 and Z == 0 |
| SETBE | $AD | unsigned <= | C == 1 or Z == 1 |
| SETP | $EC | unordered (FCMP NaN) | P == 1 |
- Traps: none for the defined encodings. An unallocated condition encoding in the SETcc
family (for example the reserved
$ED) is the illegal-instruction trap (cause 0, subcode 1). SETP ($EC) is defined (it claims one of the reserved spare condition encodings and reads P); the remaining spare$EDstays reserved.
mazm also accepts assembler-only synonyms that emit the identical opcode as their
canonical form (byte-identical after assembly): SETE=SETZ, SETNE=SETNZ, SETL/SETNGE=SETLT,
SETNL=SETGE, SETG/SETNLE=SETGT, SETNG=SETLE, SETC/SETNAE=SETB, SETNC/SETNB=SETAE,
SETNBE=SETA, SETNA=SETBE.
7.7 Control flow
Control-transfer targets are always full 64-bit addresses. Jumps and calls do not affect any flag (C/N/V/Z/P unaffected); the conditional branches read flags but write none.
JMP (unconditional jump)
- Operation: set PC to the target and continue. JMP targets the full 64-bit width
regardless of any operand subregister selection;
mazmrejects a JMP operand carrying a subregister suffix. - Forms:
$16regVal (address in a register),$56immVal (immediate address),$96regAddr (address pointed to by a register),$D6immAddr (address pointed to by an immediate). - Flags: C/N/V/Z/P unaffected.
- Traps: none.
Jcc (conditional branches)
- Operation: if the condition holds, set PC to the immediate target; else fall through. Immediate target only (the register/indirect conditional forms are synthesized as an inverted Jcc over JMP). Each shares its predicate with the matching SETcc (section 7.6).
- Forms and conditions (each
immVal):
| Mnemonic | Opcode | Branch taken when |
|---|---|---|
| JZ | $17 | Z == 1 (equal / zero) |
| JNZ | $18 | Z == 0 |
| JLT | $19 | N != V (signed <) |
| JGE | $97 | N == V (signed >=) |
| JGT | $58 | Z == 0 and N == V (signed >) |
| JLE | $98 | Z == 1 or N != V (signed <=) |
| JB | $57 | C == 1 (unsigned <) |
| JAE | $D7 | C == 0 (unsigned >=) |
| JA | $59 | C == 0 and Z == 0 (unsigned >) |
| JBE | $99 | C == 1 or Z == 1 (unsigned <=) |
| JP | $D8 | P == 1 (FCMP unordered / NaN) |
- Flags: read, none written. C/N/V/Z/P unaffected.
- Encoding: opcode byte, immediate-size source operand byte, immediate target bytes.
- Traps: none for the defined encodings. An unallocated condition encoding in the Jcc
family (for example the reserved
$D9) is the illegal-instruction trap (cause 0, subcode 1). JP ($D8) is defined and reads P;$D9stays reserved.
CALL
- Operation: push the return address (the address of the following instruction, a full 64-bit value) onto the stack (pre-decrementing RS), then jump to the target. Execution continues until a RET. Unlike JMP (which always reads the whole target register), CALL honors the operand subregister: the register form reads the operand's selected subregister and zero-extends it into the full-width PC, and the immediate form zero-extends the immediate at its encoded width. The address forms dereference a full 64-bit target from memory.
- Forms:
$1DregVal,$5DimmVal,$9DregAddr,$DDimmAddr. - Flags: C/N/V/Z/P unaffected.
- Traps: none.
RET
- Operation: pop the return address from the stack into PC and continue there (incrementing RS). Returns from CALL.
- Forms:
$27(zero-operand; row 0 of base$27). - Flags: C/N/V/Z/P unaffected.
- Traps: none.
JP / SETP
JP ($D8) is listed in the Jcc table above and SETP ($EC) in the SETcc table; both read
the P flag set by FCMP (Chapter 8). JNP / SETNP are synthesized by branch inversion (no
dedicated opcode).
7.8 Stack
The stack is full-descending with pre-decrement on push. RS (SP) always points at the last value pushed. The stack instructions are PUSH and POP (section 7.1) and CALL and RET (section 7.7):
- PUSH src: RS -= size(src); write src at RS.
- POP dst: read dst from RS; RS += size(dst).
- CALL target: RS -= 8; write the return address at RS; PC = target.
- RET: PC = value at RS; RS += 8.
Return addresses are 8-byte (full 64-bit). At process start RS points at the process-start block's argc slot; the first push moves RS just below the block (Chapter 4 section 4.5). There is no stack-limit check in the v1.0 flat model; a stack-fault trap (cause 6) is reserved for the future segment/bounds work (Chapter 12).
7.9 System and privileged instructions
Instructions marked (privileged) require the RF privilege bit set. Executed in user mode (privilege bit clear) they raise the privileged-operation fault (cause 4). Enforcement is live (card maize-180): a head-of-dispatch privilege gate is applied at every privileged instruction below, and a trap or interrupt entry raises privilege to supervisor for the handler, so the handler's own IRET runs privileged and drops back to user by restoring a saved RF whose privilege bit is clear. The enforced privileged set is IN / OUT / OUTR, MOVTCR / MOVFCR, TLBINV / TLBINVA, SETINT / CLRINT, SETSYSG / CLRSYSG, IRET, and HALT. INT is privileged but has no active dispatch in v1.0, so its fault applies once its dispatch lands; future segment-write and other control instructions join the set as they land.
The privilege boundary also covers the direct-write vector. RF is an operand-addressable destination register, so the gate above is not enough on its own: a user-mode guest write that names RF as its destination (CP / LD / POP / CPZ / CLR, or an ALU write-back) has its privileged RF.H1 bits (privilege, interrupt-enabled, interrupt-set, running, syscall-guest) masked, they keep their current values while only the non-privileged condition flags (RF.H0: C / N / V / Z / P) take the written value. This is the x86 POPF-in-user model: a user instruction cannot set the privilege bit by writing RF directly, and the masking raises no fault, so a benign user-mode flag write still succeeds. Supervisor-mode RF writes are unmasked (this is what lets a handler's IRET restore the full saved RF).
SYS (system call)
- Operation: enter the kernel syscall dispatcher with the syscall index in the operand. A deliberate synchronous software trap; trap-class (captures the following-instruction PC, so IRET resumes after SYS). SYS is not privileged: it is the deliberate user-to-supervisor entry, so user-mode code calls it to request kernel service. The syscall number and arguments travel in registers per the syscall ABI (Appendix C). Today the reference VM dispatches SYS directly to the BIOS/syscall surface; routing it through the shared trap table at vector 7 is the future path (Chapter 10).
- Forms:
$34regVal (index in a register),$74immVal (immediate index). The index is a single byte (operand.b0). - Flags: the instruction itself sets no arithmetic/logic flag (a syscall's effects on RV and errno are the ABI's, not a flag effect). C/N/V/Z/P unaffected by the dispatch.
- Traps: reserved as cause 7 (SYS / syscall entry). Not a privileged instruction, so it never raises the cause-4 privileged-operation fault.
INT (software interrupt)
- Operation: raise a software interrupt at the given vector index, entering through the shared trap frame (Chapter 10): the entry sequence pushes PC, then the full RF word, then the cause and aux words. Privileged. The dispatch is deferred until the vector-table format exists; INT has no active dispatch case in v1.0.
- Forms:
$24regVal (index in a register),$64immVal (immediate index). - Flags: C/N/V/Z/P unaffected.
- Traps: privileged-operation fault (cause 4) in user mode. INT has no active dispatch in v1.0, so this fault applies once its dispatch lands.
IRET (interrupt return)
- Operation: the shared return path for both traps and interrupts (there is no TRET).
IRET pops the full RF word (
RF.w0, including the privileged RF.H1 bits) and then PC, the two low words of the shared aux/cause/RF/PC trap frame (the handler having already removed aux and cause). Restoring the full RF, including the interrupt-enable bit, re-enables interrupts on a normal handler return. Privileged. - Forms:
$67(zero-operand; row 1 of base$27). - Flags: RF (and thus C/N/V/Z/P) is restored from the saved frame, not computed.
- Traps: privileged-operation fault (cause 4) in user mode. Making IRET privileged closes the forged-RF escalation: only supervisor code (which every trap/interrupt handler is, since entry raises privilege) can execute it, so user code cannot forge a privileged RF word on its stack and IRET into supervisor mode (card maize-180).
SETINT / CLRINT
- Operation: SETINT sets the RF interrupt-enable bit (enabling maskable external interrupts); CLRINT clears it. Privileged.
- Forms: SETINT
$29(row 0 of base$29); CLRINT$69(row 1). Zero-operand. - Flags: the arithmetic/logic flags C/N/V/Z/P are unaffected (these write RF.H1).
- Traps: privileged-operation fault (cause 4) in user mode.
SETCRY / CLRCRY
- Operation: SETCRY sets the Carry flag to 1; CLRCRY clears it to 0. The direct software controls over C used to seed a multi-word ADC/SBB chain.
- Forms: SETCRY
$A9(row 2 of base$29); CLRCRY$E9(row 3). Zero-operand. - Flags: C = 1 (SETCRY) or C = 0 (CLRCRY); N, V, Z, P unaffected.
- Traps: none. (Not privileged; C is a user-mode arithmetic flag.)
SETSYSG / CLRSYSG (syscall-provider select, privileged)
- Operation: SETSYSG sets the RF syscall-guest bit; CLRSYSG clears it (card maize-24). The
bit selects which provider SYS dispatches to: clear (boot default) calls the native
sys::callprovider byte-identical to v1.0; set routes SYS through the shared trap table at cause 7 into a guest-installed handler (the syscall number rides the frame's aux word; args and result use the R0/R1/R2/RV ABI). A resident guest kernel toggles the bit around a re-issued SYS to pass calls through to the native provider. Privileged. - Forms: SETSYSG
$A4(row 2 of base$24); CLRSYSG$E4(row 3). Zero-operand. - Flags: the arithmetic/logic flags C/N/V/Z/P are unaffected (these write RF.H1).
- Traps: privileged-operation fault (cause 4) in user mode.
IN (port input, privileged)
- Operation: read a value from the device on the selected port into the destination
register. The port id is the low 16 bits (
.q0) of the port operand. An IN from an unpopulated port yields 0 (Chapter 11). - Forms:
$1FregVal reg,$5FimmVal reg,$9FregAddr reg,$DFimmAddr reg (the first operand names the port; the destination is always a register). - Flags: C/N/V/Z/P unaffected.
- Traps: privileged-operation fault (cause 4) in user mode.
OUT (port output, immediate port, privileged)
- Operation: write the source value (register / immediate / value-at-address) to the device on the port named by the trailing immediate. Writing to an unpopulated port is discarded (Chapter 11).
- Forms:
$14regVal imm,$54immVal imm,$94regAddr imm,$D4immAddr imm. - Flags: C/N/V/Z/P unaffected.
- Traps: privileged-operation fault (cause 4) in user mode.
OUTR (port output, register port, privileged)
- Operation: OUT with the port named by a register operand rather than an immediate. The
port id is the port register's
.q0. - Forms:
$1EregVal reg,$5EimmVal reg,$9EregAddr reg,$DEimmAddr reg. - Flags: C/N/V/Z/P unaffected.
- Traps: privileged-operation fault (cause 4) in user mode.
MOVTCR (move to control register, privileged)
- Operation: write a value into the control register named by the immediate CR index
crn. The control-register file is a small, privileged, flat-indexed bank reached only through MOVTCR / MOVFCR, distinct from device ports and the syscall surface: CR0SATP(address-translation control), CR1FAULT_VA, CR2FAULT_ERR(Chapter 12). A write to an undefined CR index (> 2) is discarded, mirroring the unpopulated-port convention. CR0SATPis live (card maize-194): writing MODE = 1 (Sv48) into bits [3:0] with a root page-table PPN in bits [63:12] activates the 4-level Sv48 walk on every subsequent guest memory access; MODE = 0 (Bare, the reset default) is the identity passthrough. A CR0 write also flushes the whole software TLB. Reserved SATP bits [11:4] are forced to 0 on write. - Forms:
$26regVal-imm (MOVTCR Rsrc, crn, source in a register, trailing immediate CR index),$66immVal-imm (MOVTCR imm, crn, immediate source).$E6reserved. - Flags: C/N/V/Z/P unaffected.
- Traps: privileged-operation fault (cause 4) in user mode.
MOVFCR (move from control register, privileged)
- Operation: read the control register named by the leading immediate CR index
crninto the destination register. A read from an undefined CR index (> 2) yields 0, mirroring the unpopulated-port read-0 convention. - Forms:
$A6immVal-reg (MOVFCR crn, Rdst, leading immediate CR index, trailing destination register). The opcode is fixed: unlike IN, the$26row bit is a slot selector, not an addressing-mode tag. - Flags: C/N/V/Z/P unaffected.
- Traps: privileged-operation fault (cause 4) in user mode.
TLBINV (invalidate all TLB entries, privileged)
- Operation: invalidate every software-TLB entry (card maize-194). The 64-entry direct-mapped software TLB caches successful level-0 (4 KiB) Sv48 leaf translations; this instruction clears every entry, forcing a fresh walk on the next access to any VA. (A CR0 SATP write flushes the whole TLB implicitly; TLBINV is the explicit same-address-space flush after a kernel edits a live page table.)
- Forms:
$28(zero-operand). - Flags: C/N/V/Z/P unaffected.
- Traps: privileged-operation fault (cause 4) in user mode.
TLBINVA (invalidate one TLB entry, privileged)
- Operation: invalidate the single software-TLB entry whose tag is the VA in the register operand, shifted right by the page size (card maize-194). Used after a kernel edits one page's mapping, to drop just that VA's cached translation without flushing the whole TLB.
- Forms:
$68regVal (one register operand naming the VA).$A8/$E8reserved. - Flags: C/N/V/Z/P unaffected.
- Traps: privileged-operation fault (cause 4) in user mode.
HALT
- Operation: halt the clock, stopping execution pending an interrupt. With no interrupt
source a halted core has nothing to wake it, so the run loop returns and the host process
exits 0 with no recorded status (distinct from the status-carrying SYS $3C exit; Appendix
C). Pinned at opcode
$00so a run of zeroed memory halts. Privileged. - Forms:
$00(zero-operand). - Flags: C/N/V/Z/P unaffected (the running bit in RF.H1 clears).
- Traps: privileged-operation fault (cause 4) in user mode.
BRK (breakpoint)
- Operation: a defined breakpoint trap. Trap-class: captures the following-instruction
PC, so a debugger that resumes lands after the BRK. Pinned at
$FF, the value that fills erased/uninitialized memory, so a run of$FFbytes reached as code raises a breakpoint rather than wandering, mirroring HALT at$00. - Forms:
$FF(zero-operand). - Flags: C/N/V/Z/P unaffected.
- Traps: breakpoint (cause 3). Unhandled, the machine halts deterministically with cause 3 surfaced and the following instruction does not execute.
NOP
- Operation: no operation; an instruction placeholder.
- Forms:
$A7(zero-operand; row 2 of base$27). - Flags: C/N/V/Z/P unaffected.
- Traps: none.
7.10 Floating-point (cross-reference to Chapter 8)
The floating-point instructions are documented in Chapter 8 (Zfinx, FCSR, NaN/rounding, the FCMP flag mapping): the arithmetic ops FADD / FSUB / FMUL / FDIV; the unary ops FSQRT / FNEG / FABS; the fused multiply ops FMADD / FMSUB; the min/max ops FMIN / FMAX; the compare FCMP (the sole writer of the P flag, summarized in section 7.5); the conversions FCVTFF / FCVTFS / FCVTFU / FCVTSF / FCVTUF; the FCSR-access ops FGETCSR / FSETCSR; and the unordered predicates JP / SETP (listed in sections 7.7 and 7.6). FP arithmetic exceptions are sticky in the FCSR FFLAGS and never trap; only illegal FP encodings/operands trap (cause 0). The integer flags C/N/V/Z are untouched by every FP instruction except FCMP, and P is written only by FCMP.
Sourcing
- Opcode map and forms: README "Instructions" and "Opcodes Sorted Numerically"; Appendix A;
src/maize_cpu.hinstrnamespace (lines ~442-843). - Flag effects: README "Per-instruction flag effects" table and its Notes;
src/cpu.cpprun_aluand its width-case blocks (ADD/SUB/ADC/SBB/MUL/MULW/UMULW/DIV/MOD/shift/logic, lines ~1022 onward),do_fcmpfor P (lines ~1001-1019). The zero-flag-only CMPXCHG contract isdocs/spec/reservations.mdsection 2 and the reference dispatch. - Condition predicates:
src/cpu.cppeval_condition(lines ~781-800), shared by Jcc and SETcc; README SETcc/Jcc tables. - Trap behavior:
docs/spec/trap-model.md(Chapter 10); README "Trap Model". Divide error:raise_divide_error(cpu.cpp ~921). Breakpoint:raise_breakpoint(~935). Privileged and port I/O:docs/spec/device-surface.md(Chapter 11).
Chapter 8: Floating-Point
This chapter is normative. It fixes the floating-point model: the Zfinx placement of FP values in the integer registers, the formats, the FCSR, the operation set, the FCMP flag mapping consumed by JP / SETP, conversions, NaN and rounding behavior, and the sticky (never-trapping) exception model.
Maize implements IEEE-754-2019 binary32 (single) and binary64 (double): both widths, all five rounding modes, all five sticky exception flags, subnormals (gradual underflow, no flush-to-zero), and a fused multiply-add. Semantics track the RISC-V F/D extensions closely, with one deliberate divergence named in section 8.7 (a NaN float-to-integer conversion yields 0 rather than the RISC-V maximum value).
8.1 Zfinx: floats live in the integer registers
Floating-point instructions read operands from and write results to the existing sixteen integer registers. There is no separate FP register bank and no FP load/store/move (LD / ST / CP already move the bits). Format width comes from the per-operand subregister field, exactly as for integer ops:
- binary32 occupies a 32-bit subregister view:
H0orH1. - binary64 occupies the full 64-bit register:
W0.
A B* or Q* subregister on a floating-point operand is illegal: mazm rejects it at
assemble time, and the VM raises a deterministic illegal-operand trap (cause 0). There is
no NaN-boxing: a binary32 value simply occupies a 32-bit subregister like any 32-bit
integer, and the upper bits follow the ordinary subregister merge semantics (Chapter 3).
The float operands of a same-format op (FADD / FSUB / FMUL / FDIV / FCMP, FSQRT / FNEG /
FABS, FMIN / FMAX, and FMADD / FMSUB) must all be the same width: mixing a binary32 and
a binary64 operand (for example FADD R0.H0 R1.W0) is a deterministic illegal-operand trap,
and mazm rejects it at assemble time. An FP immediate source must likewise be exactly the
destination float width (4 bytes for binary32, 8 for binary64). The conversion ops (FCVTFF
and the integer/float FCVT* family) are the sole exception, since a conversion names two
operands of intentionally different width.
8.2 FCSR: rounding mode and sticky exception flags
A dedicated architectural FP control/status register (FCSR) holds the rounding mode and
the sticky exception flags, keeping the hot per-instruction integer flags C/N/V/Z/P
uncontaminated. It is not one of the sixteen operand-addressable registers; software
reads and writes it with FGETCSR reg ($15) and FSETCSR reg ($55). The byte layout is
RISC-V's fcsr verbatim:
Bits 7-5 FRM rounding mode (3 bits)
Bits 4-0 FFLAGS sticky exception flags (5 bits)
FSETCSR writes the low 8 bits and thereby also clears the sticky flags (flags are set by
hardware, cleared only by software). Reset default is $00 (RNE, no flags). The upper bits
are reserved for a future trap-enable extension and are not implemented.
FRM (rounding mode), RISC-V encoding
000 RNE round to nearest, ties to even (default)
001 RTZ round toward zero
010 RDN round toward -infinity
011 RUP round toward +infinity
100 RMM round to nearest, ties to max magnitude
101, 110 reserved
111 DYN not supported (Maize has no per-instruction rounding field)
Every rounding op consults the current FRM; there is no per-instruction rounding field. A
rounding op executed while FRM holds a reserved encoding (101 / 110) or the unsupported
DYN (111) is a deterministic illegal-operand trap (cause 0), not a silent
round-to-nearest, matching RISC-V's treatment of a reserved static rounding mode. The
non-rounding ops (FNEG, FABS, FMIN, FMAX, FCMP) never consult FRM and are unaffected.
FFLAGS (sticky exception flags), RISC-V bit order
bit 4 NV invalid operation (sNaN operand, 0*inf, inf-inf, 0/0, inf/inf,
sqrt of a negative, invalid float->int conversion)
bit 3 DZ divide by zero (finite nonzero / 0)
bit 2 OF overflow (rounded result exceeds the largest finite value)
bit 1 UF underflow (a tiny nonzero result)
bit 0 NX inexact (the result is not exactly representable)
These are the 754 arithmetic exceptions and are distinct from the integer RF flags; in particular FFLAGS.OF (binary overflow to infinity) is not RF.V (integer signed overflow).
8.3 Exceptions are sticky, not trapping
FP arithmetic exceptions do not trap. A divide by zero yields the correctly signed infinity and sets DZ; an invalid operation yields the canonical qNaN and sets NV; the operation always produces its 754-defined result. Only illegal encodings trap (a B* / Q* subregister on an FP operand, an operand-width mismatch, a reserved/unsupported FCSR rounding mode on a rounding op, or a reserved/unallocated FP opcode form), as part of the illegal-instruction / illegal-operand taxonomy (cause 0; see the Trap Model chapter). Trapping FP (754 alternate exception handling) is a reserved future extension in the unused FCSR bits.
8.4 NaN handling
A signaling NaN has the significand MSB clear; a quiet NaN has it set. A signaling-NaN
operand to any arithmetic op, to FMIN / FMAX, or to FCMP raises NV; a quiet NaN does not.
Every NaN-producing arithmetic op returns the canonical qNaN (binary32 $7FC00000,
binary64 $7FF8000000000000; positive, quiet, zero payload) rather than preserving an input
payload, matching RISC-V. FNEG and FABS are the sole exceptions: they manipulate the sign bit
only, raise no flags, do not round, and pass NaN payloads through unchanged.
8.5 FCMP and the float branch idioms
FCMP src, a compares the destination register a against the source src and writes the
integer flags, mapping the four 754 outcomes onto the x86 UCOMISD convention. It is the
only instruction that writes the P (parity / unordered) flag, and it forces N = V = 0:
Outcome C (bit0) Z (bit4) P (bit3) N,V
a > src (ordered) 0 0 0 0,0
a < src (ordered) 1 0 0 0,0
a == src 0 1 0 0,0
unordered (a NaN) 1 1 1 0,0
FCMP is the quiet compare: a quiet-NaN operand yields unordered without signaling; only a signaling NaN raises NV. After FCMP the ordered branch idioms work directly off the reused predicate table (Chapter 7 sections 7.6 and 7.7):
JB / SETB a < src JA / SETA a > src
JBE a <= src JAE a >= src
JZ / SETZ a == src JP / SETP unordered (either operand NaN)
JP / SETP is the unordered predicate, reading the P flag; JNP / SETNP are synthesized
by branch inversion (there is no separate opcode). No integer instruction writes P, so the
P flag survives across integer work between an FCMP and its JP / SETP exactly as C/N/V/Z do.
8.6 Operations
- Arithmetic (
FADD$1A,FSUB$1B,FMUL$1C,FDIV$21): correctly rounded under the current FRM, full four addressing-mode source forms like the integer ALU.FADD src dstcomputesdst = dst + src. - Unary (
FSQRT$22,FNEG$62,FABS$A2): register-only,MNEMONIC src dstcomputingdst = f(src). FSQRT is correctly rounded; FNEG / FABS are exact sign-bit ops that raise no flags and do not round. - Fused multiply-add (
FMADD,FMSUB): three operands, single-roundeddst = a*b (+/-) cwhere the third operand is both the addendcand the destination. Like the wide-multiply family, operand 1 has all four addressing-mode source forms: FMADD$23regVal,$63immVal,$A3regAddr,$E3immAddr; FMSUB$25regVal,$65immVal,$A5regAddr,$E5immAddr (operands 2 and 3 are always registers).FNMADD=FNEG(FMADD(...))andFNMSUB=FNEG(FMSUB(...))are synthesized via the exact FNEG (no dedicated opcode), so they remain single-rounded. - Min / max (
FMIN$33,FMAX$73): RISC-V semantics. A quiet-NaN operand returns the non-NaN operand; both-NaN returns the canonical qNaN; a signaling NaN raises NV;-0 < +0. - Compare (
FCMP$2A): the quiet compare of section 8.5. - Conversions:
FCVTFF$39(float to float, widths from the two subregisters);FCVTFS$79/FCVTFU$B9(float to signed / unsigned integer, saturating);FCVTSF$3A/FCVTUF$7A(signed / unsigned integer to float). All round per FRM. - FCSR access:
FGETCSR$15copies FCSR into the operand register;FSETCSR$55copies the operand register's low 8 bits into FCSR and clears the sticky FFLAGS.
8.7 Conversions in detail
The float-to-integer conversions (FCVTFS, FCVTFU) are saturating: overflow yields
the maximum representable integer, underflow the minimum, both setting NV. Note one
deliberate divergence from RISC-V: a NaN input to a float-to-integer conversion
yields 0 here (matching the common C-cast convention), whereas RISC-V yields the
maximum-magnitude value; both set NV. This is the only point where Maize's float-to-int
conversion departs from the RISC-V F/D behavior it otherwise mirrors. The integer-to-float
conversions (FCVTSF, FCVTUF) round the integer to the target float width per FRM.
8.8 Synthesizing FCLASS / copysign (no dedicated opcode)
Maize spends no opcode on FCLASS or FSGNJ (copysign); under Zfinx these are integer bit-operations on the register that already holds the float:
- isnan(x):
FCMP x, xthenJP(a value compares unordered with itself iff it is NaN). - copysign(x, y): clear x's sign with
ANDagainst$7FFF..., extract y's sign withANDagainst$8000..., thenORthe two. - isinf / isfinite / fpclassify: mask the exponent and mantissa fields with
ANDand compare the bit patterns; the exponent-all-ones / mantissa-zero test distinguishes infinity from NaN.
8.9 Rounding and portability notes (informative)
The reference VM computes on the host FPU (IEEE-754 conformant), setting the hardware
rounding direction from FRM and reading the hardware exception flags into FFLAGS, with
std::fma for the fused multiply-add. The four directed rounding modes (RNE / RTZ / RDN /
RUP) map onto hardware directions and are correctly rounded by the host FPU. RMM
(ties-to-max-magnitude), which no hardware direction provides, is synthesized by computing
the operation in a wider host type and rounding to the target width with an explicit
ties-away rule; its flags come from the equivalent nearest evaluation, which shares RMM's
exception conditions and overflow threshold.
RMM carries a small set of documented limitations, all confined to exact-tie behavior and none affecting the four hardware rounding modes:
- The wider-type path is exact for binary32 add/sub/mul (double is exact) and for binary64
add/sub/mul on a host with an 80-bit
long double. The RMM tie decision for div, sqrt, and FMA inherits a benign double-rounding edge (the wider result is itself a rounding), which a focused conformance fixture does not exercise. - A binary64 FMA under RMM at an exact tie falls back to the nearest-even value, because
re-rounding a 106-bit product exceeds the 80-bit host
long double.
RMM's directed sibling modes and every mode of the other ops are unaffected. Because RMM's
tie edge depends on the host long double width, a portability-sensitive program that
requires bit-identical RMM ties across hosts should avoid RMM for div / sqrt / FMA; the four
directed modes and nearest-even are host-independent.
8.10 Floating-point and the C ABI
Under Zfinx, floating-point arguments and results share the integer registers: a float
occupies the low 32 bits (H0) of its register and a double the full 64 bits (W0).
There is no separate FP argument class. See Appendix C and
toolchain/qbe-maize/CALLING-CONVENTION.md.
Sourcing
- Zfinx placement, formats, and the same-width rule: the reference VM FP dispatch and
fp_width_from_subreginsrc/cpu.cpp;src/maize_cpu.hFP opcode constants; README "Floating-Point (IEEE-754)". - FCSR layout, FRM check, and sticky FFLAGS:
src/cpu.cppfcsr_frm/fp_checked_frm/fcsr_raise; README "FCSR" section. - FCMP flag mapping (C/Z/P, N=V=0) and the P flag:
src/cpu.cppdo_fcmpandeval_conditioncase 10 (JP/SETP); README "FCMP and the float branch idioms". - NaN, rounding, conversions, and the RMM notes:
src/fpu.*and README "NaN handling", "Operations", "Implementation note (RMM)"; illegal-FP encodings trap viaraise_illegal_fp(Trap Model chapter, cause 0).
Chapter 9: Execution Model
This chapter is normative. It fixes the fetch/decode/execute cycle, the reset state, the process-start contract, and the stop conditions.
9.1 The fetch/decode/execute cycle
Maize is a strictly in-order interpreter. It fully retires one instruction, with all of its architectural effects, before it decodes the next. There is no speculation, no out-of-order retirement, and no pipeline visible to the program. Each cycle:
- Fetch/decode. Read the opcode byte at PC (RP) into the decoder-internal instruction register RI, determine the addressing-mode form and how many operand bytes and immediate bytes follow (Chapters 5 and 6), read them, and advance RP past the whole instruction.
- Execute. Perform the instruction's operation: register and memory effects, flag updates, and any control transfer (which overwrites RP). An ALU instruction routes through the shared arithmetic/logic unit, which computes the result and the flags for the destination width.
- Retire. All effects are committed before the next fetch. A synchronous trap raised mid-instruction is delivered precisely: prior instructions are fully retired and no later instruction takes effect (Chapter 10).
Because retirement is complete before the next decode, PC always names the next instruction to run, a fault captures the faulting instruction's entry PC, and a trap (BRK, SYS) captures the following instruction's PC (Chapter 10).
9.2 The instruction register RI
RI is the decoder-internal register that holds the instruction bytes as they are read. It drives the addressing-mode and condition decode (the two high opcode bits select the addressing-mode form or the condition row; Chapter 6). RI is not operand-addressable: software cannot read or write it.
9.3 Reset state
At power-on / reset the machine comes up in a fully defined state:
- Privileged. The RF privilege bit is set; execution starts in supervisor mode.
- Interrupts disabled. The RF interrupt-enable bit is clear; maskable external interrupts are masked until SETINT.
- Flags clear. The arithmetic/logic flags (RF.H0 = FL): C, N, V, P, Z are all 0.
- Paging off. No MMU is armed; the machine runs in the flat 64-bit model. Paging-off is the permanent reset state and a first-class mode: every future MMU extension is disabled at reset, so a v1.0 image sees the v1.0 machine on any conforming VM (Chapter 12).
- Stack pointer at the process-start block. RS (SP) = the base of the process-start
block, so RS points at argc (Chapter 4 section 4.5). The block ends at
$FFFF_FFFF_FFFF_FFF8, the highest 8-byte-aligned address; that is the top of the block, not RS. The stack grows downward from there. - Entry point. RP (PC) = the recorded entry point of a
.mzxexecutable, or$0for a flat.mzbimage. - Registers zero. RB (BP) = 0; R0..R9, RT, RV = 0.
- Running. The running bit (RF.H1) is set once execution begins.
These values are a guaranteed contract, not incidental defaults; crt0 and the C calling convention depend on a usable stack pointer and a well-formed process-start block from the first instruction (Chapter 2 section 2.6, Chapter 4 section 4.5).
9.4 Process start
A fresh VM invocation builds the System V-style process-start block at the top of the
address space, points RS at its base (argc), and enters at the program entry. The block
carries argc, the argv pointer array (NULL-terminated), the envp pointer array (always
present and NULL-terminated), and the packed argument and environment strings; each slot is
8 bytes, little-endian. The full layout is Chapter 4 section 4.5. The C runtime's crt0 reads
argc / argv / envp off this block into the argument registers and calls main; a
main(void) ignores them.
A program's environment is built only from what the invocation passes on the command line; the VM never inherits the host shell's environment, so a run is deterministic. Command-line arguments after the image become argv; explicitly passed environment entries become envp.
9.5 Stop conditions
Execution stops in one of these defined ways:
- HALT (
$00). Halts the core pending an interrupt. With no interrupt source a halted core has nothing to wake it, so the run loop returns and the host process exits 0 with no recorded status. Because$00is HALT, a run of zeroed memory reached as code halts cleanly rather than executing garbage. - sys_exit (
SYS $3C). The status-carrying termination path: it records the low 8 bits of R0 as the process exit status and stops the VM, so the host process returns that value. Values wrap to the 0..255 range (the host truncates the process status to 8 bits). This is distinct from HALT, which records no status. See Appendix C. - Unhandled synchronous trap. With no handler installed, a fired trap halts the VM deterministically with the cause surfaced (Chapter 10). No instruction after the trapping instruction takes effect.
The running bit (RF.H1) reflects the executing state and clears when the core halts.
9.6 Determinism
Given the same image and the same declared inputs (command-line arguments, environment entries, and directory mounts), a Maize run is a pure function of those inputs. There is no ambient host state, no uninitialized-memory nondeterminism (unwritten memory reads as 0), and no undefined behavior (Chapter 10). Two conforming VMs produce identical observable behavior on the same inputs.
Sourcing
- Fetch/decode/execute and RI: the reference VM
run()/ decode loop andrun_aluinsrc/cpu.cpp; README "Execution". - Reset and process-start contract: README "Process start" and "Execution"; Chapters 2 and 4. Paging-off reset state: the Reserved Space chapter.
- Stop conditions: README "Hello, World!" HALT note and the
sys_exit(SYS $3C) description; the deterministic-halt trap behavior in the Trap Model chapter.
Chapter 10: Trap Model
This chapter is a normative part of the Maize ISA specification. It fixes the trap taxonomy, the cause/vector numbering, the state a trap captures, and how traps and interrupts share one delivery path. A third-party implementation that follows this chapter behaves identically to the reference VM on every condition described here.
The governing rule: no undefined behavior
Every condition that is undefined behavior on a conventional machine is, on Maize, a defined outcome. There are exactly two kinds of outcome and no third category:
- A named trap with a stable numeric cause, delivered precisely (below), or
- An explicitly enumerated defined, non-trapping result (see "Defined non-trapping behavior").
Nothing is left implementation-defined. A binary therefore behaves identically under analysis and in production, and two conforming VMs cannot diverge on any input.
Precise delivery
Trap delivery is precise, and this is the frozen v1.0 contract. The core is a strictly in-order interpreter: it fully retires one instruction before decoding the next. When a synchronous trap fires, every prior instruction has completed and no later instruction has taken any architectural effect. Precise delivery is what makes a trap conformance-testable and lets a corrective handler resume.
Faults versus traps
Maize distinguishes two synchronous-trap classes by the program counter they capture. The distinction follows the model most readers already know from x86, and is stated explicitly so it is never folded away:
- A fault captures the address of the faulting instruction. A handler that corrects the condition can return and re-execute that instruction. Illegal-instruction, divide-error, privileged-operation, segment/bounds, and stack are faults.
- A trap (in the narrow sense) captures the address of the following instruction, because there is nothing to retry. Breakpoint is a trap.
Implementation note (reference VM): the decoder reads the opcode at RP and then
advances RP past it before dispatch, and an ALU decode advances RP further still.
The captured faulting-instruction PC is therefore the value of RP at the entry to
the instruction's decode, not the partially advanced value. A conforming VM must stash
each instruction's entry PC so that a mid-instruction fault reports the instruction's
own address. A breakpoint, by contrast, wants the already-advanced RP (the following
instruction), which the reference dispatch holds naturally at the point BRK executes.
The trap taxonomy
Each synchronous trap has a stable numeric cause. The cause number doubles as the trap's index into the shared vector table (see "Vector table and delivery"). Synchronous traps occupy the low range 0..31; external and device interrupts occupy 32 and above. Vector 1 is intentionally left unassigned, reserving a slot for a future debug / single-step trap, matching the x86 lineage a reader will expect.
| Cause | Name | Class | Trigger | Captured aux |
|---|---|---|---|---|
| 0 | Illegal instruction / illegal operand | Fault | An unknown / undefined opcode; an unallocated condition encoding on a Jcc / SETcc; or an illegal floating-point encoding (a B* / Q* subregister on an FP operand, an FP operand-width mismatch, a reserved / unsupported FCSR rounding mode on a rounding op, or a reserved FP opcode form) | Offending instruction byte |
| 1 | (reserved) | n/a | Unassigned; reserved for a future debug / single-step trap | n/a |
| 2 | Divide error | Fault | Divide-by-zero (signed or unsigned), or signed INT_MIN / -1 quotient overflow | Subcode: 0 = divide-by-zero, 1 = quotient overflow |
| 3 | Breakpoint | Trap | The BRK ($FF) instruction executes | 0 |
| 4 | Privileged operation in user mode | Fault | A privileged instruction executes with the RF privilege bit clear | Offending instruction byte |
| 5 | Segment / bounds violation | Fault | An access falls outside a segment window, once segments exist | Faulting address |
| 6 | Stack fault | Fault | A stack access violates the stack limit, once a stack bound exists | Faulting address |
| 7 | SYS / syscall entry | Trap | The SYS instruction executes (a deliberate synchronous software trap into the kernel syscall dispatcher) | 0 (syscall number and arguments travel in registers per the syscall ABI) |
| 8 | Page fault | Fault | An Sv48 address translation (CR0 SATP.MODE = 1) finds no valid mapping (a PTE with V=0, a non-leaf PTE at walk level 0, a reserved W=1/R=0 leaf, or a misaligned superpage) or a mapping that violates the requested access (X for a fetch, R for a load, W for a store, or the U bit for a user-mode access) | Faulting VA in CR1 FAULT_VA; a packed error code in CR2 FAULT_ERR |
| 9..31 | (reserved) | n/a | Future synchronous traps | n/a |
| 32.. | External / device interrupts | Interrupt | Device / timer sources; the timer is the first source | Source-defined |
Cause subcodes: where one cause number multiplexes distinct conditions, the cause word carries a subcode so a handler can tell them apart without re-deriving the condition. Divide error uses subcode 0 for divide-by-zero and 1 for signed quotient overflow. Illegal instruction uses subcode 0 for an unknown opcode, 1 for an unallocated condition encoding, and 2 for an illegal floating-point encoding / operand.
Per-entry detail
Cause 0, Illegal instruction / illegal operand (fault). Fires when the decoder meets
an opcode byte with no defined meaning, when a Jcc / SETcc carries a condition selector
that is not an allocated encoding, or when a floating-point instruction carries an
illegal encoding or operand. The floating-point cases are: a B* or Q* subregister on an
FP operand (FP operands must select H0 / H1 for binary32 or W0 for binary64), an FP
operand-width mismatch (mixing binary32 and binary64 on a same-format op), a reserved or
unsupported FCSR rounding mode on a rounding op, or a reserved FP opcode form. All of
these are illegal-encoding / illegal-operand conditions, distinct from FP arithmetic
exceptions, which never trap (see "Defined non-trapping behavior"). The undefined
sub-register selector $F on any operand is also an illegal-operand case here (it is the
one operand field that traps; the undefined immediate-size field, by contrast, has a defined
default). The trap captures the faulting instruction's PC (a handler may rewrite the
encoding and retry) and the offending instruction byte as aux; the cause subcode is 0 for an
unknown opcode, 1 for an unallocated condition encoding, and 2 for an illegal FP or operand
encoding (the FP encodings and the undefined $F selector).
Cause 2, Divide error (fault). Fires on signed or unsigned divide-by-zero and on the
one signed quotient overflow (INT_MIN / -1, whose true quotient is not representable),
across all widths of DIV, MOD, UDIV, and UMOD. It captures the faulting instruction's PC
and a subcode distinguishing divide-by-zero (0) from quotient overflow (1). This is the
only arithmetic trap; every other arithmetic condition is defined non-trapping (below).
Cause 3, Breakpoint (trap). Fires when BRK ($FF) executes. It is a trap, not a
fault: it captures the address of the following instruction, so a debugger that
resumes lands on the instruction after the breakpoint. $FF is also the value that
fills erased or uninitialized memory, so a run of $FF bytes reached as code raises a
breakpoint rather than wandering, mirroring HALT ($00) at the other end.
Cause 4, Privileged operation in user mode (fault). It fires when a privileged instruction executes with the RF privilege bit clear. Enforcement is live: card maize-21 gated IN / OUT / OUTR, and card maize-180 extended the head-of-dispatch privilege gate to the control-register and TLB instructions (MOVTCR / MOVFCR, TLBINV / TLBINVA) and to the previously-ungated HALT, SETINT / CLRINT and SETSYSG / CLRSYSG, and made IRET privileged (closing the forged-RF escalation: user code cannot forge a privileged RF word and IRET into supervisor mode). The privilege boundary also covers the direct-write vector: RF is an operand-addressable destination register, so a user-mode guest write that names RF as its destination (CP / LD / POP / CPZ / CLR, or an ALU write-back) has its privileged RF.H1 bits masked, they retain their current values while only the non-privileged condition flags take the written value (the x86 POPF-in-user model). A user instruction therefore cannot set the privilege bit by writing RF directly, and this write-masking raises no fault (a benign user-mode flag write still succeeds). A trap or interrupt entry raises privilege to supervisor for the handler, so the handler's IRET runs privileged and drops back to user by restoring a saved RF whose privilege bit is clear (a supervisor RF write is unmasked). INT is privileged but has no active dispatch in v1.0, so its fault applies once its dispatch lands; future segment-register writes join the set as they land.
Cause 5, Segment / bounds violation (fault, reserved). The cause number is frozen; the mechanism ships with the segment / base-bounds registers as a future extension. In the v1.0 flat sparse memory model no access is out of bounds, so this never fires yet.
Cause 6, Stack fault (fault, reserved). The cause number is frozen; there is no stack bound in the flat sparse model, so it never fires until bounds exist.
Cause 7, SYS / syscall entry (trap, reserved). The cause number is frozen; the
trap-vector delivery mechanism and the syscall ABI are specified separately. SYS is a
deliberate synchronous software trap: unlike the fault causes, it is requested by the
program, so it vectors through the shared trap table at index 7 into the kernel syscall
dispatcher, uses the corrected capture layout below, and returns via the shared IRET. Its
model is that SYS ($34) is syscall entry with a saved-state contract and IRET return.
Like every synchronous trap it is unmaskable. It is trap-class: it captures the address
of the following instruction, so IRET resumes the program at the instruction after
SYS. The syscall number and arguments travel in registers per the syscall ABI; this
chapter only reserves the vector and names SYS as its source. In the reference VM SYS
today dispatches directly to the BIOS / syscall surface (src/sys.cpp) rather than
through the trap table; routing it through the trap table is a future path.
Cause 8, Page fault (fault, live under Sv48). Raised by the Sv48 address-translation
layer (card maize-194) when CR0 SATP.MODE = 1 (Sv48) and a guest memory access cannot be
translated. Two failure classes: no valid mapping and a permission violation. No valid
mapping covers a PTE with V=0, a non-leaf PTE reached at walk level 0, and two structurally
invalid PTE encodings that are rejected rather than honored: a leaf with W=1 and R=0 (the
RISC-V-reserved write-without-read encoding) and a misaligned superpage (a leaf above walk
level 0 whose PPN has non-zero bits below its level's page boundary). A permission violation
is a leaf that lacks the bit the access requires: X for a fetch, R for a load, W for a
store; or a U-clear leaf accessed from user mode, since supervisor bypasses the U check. The
A and D bits are software-managed and never cause a fault. Unlike the reserved causes above,
page fault is delivered through the real trap table so a kernel handler can run:
raise_page_fault latches the faulting VA into CR1 FAULT_VA and a packed error code into
CR2 FAULT_ERR, then vectors through entry[8]. It is FAULT-class (captures the faulting
instruction's own PC, so a handler that repairs the mapping can IRET and re-execute it), not
trap-class like SYS. A stack-pushing instruction (PUSH, CALL) makes its faultable stack and
target accesses atomic with respect to the stack pointer: a page fault leaves RS unmutated,
so an IRET-and-re-execute decrements RS exactly once (the demand-paging stack-grow
contract). With no handler installed (entry[8] = 0) it is a deterministic halt with the
cause surfaced. A page fault raised from inside the trap-frame push itself (an unmapped or
read-only kernel stack) is a double fault: it halts deterministically rather than recursing.
CR2 FAULT_ERR bit layout: bit 0 PRESENT (0 = no valid mapping, 1 = a mapping was found but violates the requested permission); bits 2:1 ACCESS_KIND (0 = fetch, 1 = load, 2 = store); bit 3 USER (1 if the faulting access ran in user mode); bits 63:4 reserved, written 0. Bare mode (MODE = 0, the reset default) never page-faults: translation is the identity and every access passes through unchanged.
Defined non-trapping behavior
The following conditions look like undefined behavior on a conventional machine but are first-class defined outcomes on Maize. None of them traps, and none of them is host undefined behavior. They are listed here so no reader mistakes a defined result for a gap in the taxonomy.
- Integer overflow wraps. ADD, SUB, MUL, INC, DEC, NEG, and shift results wrap two's-complement and set the C and V flags per the flags model (see the Register Model chapter). There is no trap-on-overflow mode in v1.0. The JO / JNO and SETO / SETNO condition encodings stay reserved; overflow is observed through flags, never through a trap.
- Out-of-range shift count is defined. For a shift of width
bits:n == 0leaves the flags unaffected;1 <= n <= bitsshifts normally;n > bitsyields a result of 0 with C, V, and N cleared and Z set (Z = 1, since the result is zero). Never a trap, never host undefined behavior. - Unmapped / sparse memory access is defined. Memory is sparse: a read of never-written memory returns 0, and a write allocates a zero-filled block on first touch. There is no EFAULT and no page fault in the v1.0 flat model. The absence of a memory-access trap here is deliberate, not a missing case; segment / bounds enforcement (cause 5) is the separate, reserved path a future extension adds.
- Misaligned multi-byte access is defined-allow. A multi-byte load or store may sit at any address; it is stitched byte-wise across the 256-byte allocation blocks with no alignment requirement and no trap. No vector is spent on alignment.
- An undefined immediate-size field decodes to a defined default. An undefined
immediate-size encoding (4..7) decodes to the value-initialized default and does not
trap; it is an operand field, not an opcode. (The undefined sub-register selector
$F, by contrast, is an illegal-operand trap, cause 0, enumerated above; it is the one operand field that does trap.) - Floating-point arithmetic exceptions are sticky, never trapping. An FP invalid operation, divide-by-zero, overflow, underflow, or inexact result does not trap: the operation produces its IEEE-754 defined result (a quiet NaN, a signed infinity, the correctly rounded value) and sets the corresponding sticky FFLAGS bit in the FCSR. Software clears those flags explicitly; the machine never raises a trap on an FP arithmetic exception. Only illegal FP encodings / operands trap, and those are cause 0 (illegal instruction / illegal operand), not an arithmetic exception. See the Floating-Point chapter.
Captured state
On a synchronous trap the machine captures, at a location the handler reads at known offsets, three items:
- Faulting PC, per the fault-versus-trap class above (faulting instruction for a fault, following instruction for a trap).
- Cause word: the vector index, plus the subcode where a cause multiplexes distinct
conditions (divide-by-zero versus quotient overflow; unknown-opcode versus
unallocated-condition versus illegal-FP-encoding). The two pack into the 64-bit cause
word as: the cause number in the low byte (bits 7:0), the subcode in the next byte
(bits 15:8), and the remaining bits (63:16) reserved and written zero. A handler reads
the cause with
AND $FFand the subcode with a shift-and-mask; the reserved-zero high bits leave room for the vector-table format to widen the field without breaking existing handlers. This packing is frozen here. - Aux info: the faulting address for memory-class faults; the offending instruction byte for illegal-instruction; zero otherwise.
Saved-state stack layout
The capture reuses the shared IRET return path unchanged rather than inventing a separate trap-return instruction. IRET pops RF first and then the PC. For that to be correct, the top of the saved frame (the word at SP on handler entry, once the handler has removed the two extra words) must be RF, with the saved PC just below it.
The trap-entry sequence therefore pushes, in order: PC, then RF, then cause, then aux. Because the stack grows downward (each push pre-decrements SP), the resulting frame reads, from SP upward toward higher addresses (top to bottom):
SP + 0 aux <- SP points here on handler entry
SP + 8 cause
SP + 16 RF
SP + 24 PC (RF and PC are the IRET frame)
The handler pops the two trap-only words itself (aux, then cause), which leaves SP pointing at the saved RF. It then returns with IRET, which pops RF and then PC, restoring the interrupted context. This is the shared return path for both traps and interrupts; there is no TRET.
The vector-table format is fixed. The table has a fixed low base address of
0x1000, one 4 KiB page above the null/zero location, so the "a null pointer reads 0"
convention stays clean and an uninstalled entry reads as an unambiguous zero. Each entry
is 8 bytes wide and holds a full 64-bit handler address; the table has 256 entries
(2 KiB), indexed 0..255 by cause number (synchronous traps 0..31, external interrupts
32..255). The index is a cause byte, so it is always within the table. An out-of-range
vector or a zero (uninstalled) entry is a deterministic halt with the cause surfaced,
never an out-of-bounds read or a stray dereference. A relocatable base, held in the
reserved privileged control register, is a deferred extension; v1.0 fixes the base. This
chapter freezes the taxonomy, the cause / subcode numbering, the capture layout above,
and this table format.
Vector table and delivery
Traps and interrupts share one vector table, one saved-state layout, and one return instruction (IRET). This single-table design is the coherence requirement with the interrupt model.
- Indexing. The table is indexed by cause number: entry[cause] holds the handler.
It lives at the fixed base
0x1000with 8-byte entries, so entry[cause] is at0x1000 + cause * 8. Synchronous traps occupy indices 0..31; external interrupts occupy 32 and above, through 255 (256 entries, 2 KiB). - Handler entry. On a fired trap the machine looks up entry[cause], captures the state described above onto the stack, and loads the handler address into PC. The handler address is a full 64-bit value. The interrupt-enable state on handler entry is governed by the maskability rule below.
- Handler return. The handler removes its trap-only words (aux, cause) and executes IRET, which pops RF and then PC. This is shared with the interrupt return path.
- No handler installed (pre-OS / bare metal). If no handler is installed for a fired trap, the machine halts deterministically with the cause surfaced. A divide-by-zero, an unknown opcode, or a breakpoint stops the VM with an observable cause rather than wandering or invoking host undefined behavior. A third-party VM with no OS loaded must behave identically.
Maskability and the interrupt model
Synchronous traps (the faults and the breakpoint) are unmaskable: a masked divide-by-zero cannot be silently dropped, because that would reintroduce undefined behavior through the back door. External and device interrupts are maskable through the RF interrupt-enable bit, toggled by the SETINT and CLRINT instructions. The interrupt-enable bit governs only the maskable external sources; it has no effect on a synchronous trap.
Nesting, priority, and interrupt acknowledge are the interrupt model's to define, but
they are built on the saved-state layout this chapter fixes. The reference VM already
carries the seam: RF holds the interrupt-enable and interrupt-set bits, SETINT / CLRINT
toggle the enable bit, IRET pops RF then PC, and run() holds a commented delivery
skeleton (push PC, push RF, load the handler address) filled against this layout.
Conformance
The trap contract is observable and testable. A conforming VM must pass, at minimum, these checks for the divide-by-zero and illegal-opcode cases; the same shape generalizes to every entry in the taxonomy.
Divide-by-zero (cause 2). Execute a signed DIV with a zero divisor.
- With a handler installed at entry[2]: the handler is entered with cause 2, subcode 0 (divide-by-zero), and a faulting PC equal to the address of the DIV instruction (a fault captures the faulting instruction, so an IRET without correcting the divisor would re-execute and trap again).
- With no handler installed: the VM halts deterministically with cause 2 surfaced, and no instruction after the DIV takes effect.
Illegal opcode (cause 0). Execute an undefined opcode byte.
- With a handler installed at entry[0]: the handler is entered with cause 0, subcode 0 (unknown opcode), aux equal to the offending byte, and a faulting PC equal to the address of that byte.
- With no handler installed: the VM halts deterministically with cause 0 surfaced.
Breakpoint (cause 3). Execute BRK ($FF). Because a breakpoint is a trap, the
captured PC is the address of the instruction after BRK. With no handler installed
the VM halts deterministically with cause 3 surfaced, and the instruction after BRK
does not execute. The reference VM ships a regression test asserting exactly this
(asm/test_brk.mazm plus the breakpoint-trap runner in scripts/run-tests.sh).
Reference implementation
The reference VM (src/cpu.cpp, src/maize_cpu.h) grounds this chapter:
- Illegal instruction / illegal operand (cause 0) is the
default:case of the opcode dispatch, thedefault:case of the condition evaluator, and theraise_illegal_fpcall sites (a B* / Q* subregister on an FP operand, an FP operand-width mismatch, a reserved / unsupported FCSR rounding mode, and a reserved FP opcode form). - Divide error (cause 2) is
raise_divide_error, which guards signed and unsigned divide-by-zero and the signedINT_MIN / -1quotient overflow across all four DIV / MOD / UDIV / UMOD widths rather than letting the host divide fault. - Breakpoint (cause 3) is
raise_breakpoint:BRK($FF) is a defined breakpoint trap, not a no-op. - Where no in-guest handler is installed, these synchronous-trap paths halt the VM deterministically with the cause surfaced. In-guest vector-table delivery (vector lookup, four-word capture, handler entry) is realized for external interrupts, which vector through the table at 32..255; the synchronous faults keep the throw-and-exit no-handler behavior until an OS handler-install path exists.
- Privileged operation (cause 4): the RF privilege bit is set on power-up. IN / OUT / OUTR enforce it (card maize-21), and card maize-180 extends the gate to MOVTCR / MOVFCR, TLBINV / TLBINVA, HALT, SETINT / CLRINT, SETSYSG / CLRSYSG, and IRET: executed with the bit clear they raise cause 4. A trap or interrupt entry raises privilege to supervisor for the handler; user mode is reached only by an IRET (now privileged, so executable only from supervisor) that restores an RF word with the privilege bit clear.
- Segment / bounds (cause 5) and stack fault (cause 6): reserved numbers; the flat sparse memory model has no out-of-bounds access and no stack bound, so neither fires.
- SYS / syscall entry (cause 7): reserved number;
SYS($34) dispatches directly to the BIOS / syscall surface today, with trap-vector delivery reserved. - Page fault (cause 8): live under Sv48 (card maize-194). When CR0 SATP.MODE = 1 the
memory-access path translates every guest access through a 64-entry software TLB backed
by a 4-level Sv48 walk; a not-present or permission failure calls
raise_page_fault, which latches CR1 FAULT_VA and CR2 FAULT_ERR and vectors through entry[8] so a kernel handler can run (FAULT-class: the saved PC is the faulting instruction's own). CR0 writes and TLBINV flush the whole TLB; TLBINVA flushes one entry. Bare mode (MODE = 0, reset default) is the identity passthrough and never faults. - Interrupt delivery substrate: live for external interrupts. RF carries the
interrupt-enable and interrupt-set bits; SETINT / CLRINT toggle the enable bit; IRET
pops RF then PC. At each instruction boundary the run loop delivers a pending, enabled
IRQ: it acknowledges (clears the pending latch), pushes the four-word aux / cause / RF /
PC frame, masks interrupts, and loads the handler from entry[vector]. The timer is the
first interrupt source. INT (
$24/$64) has no dispatch case yet and is deferred as a guest-requested software-trap path.
The defined non-trapping behaviors are all shipped: overflow wrap and flags, out-of-range shift result-0, sparse allocate-on-touch memory, byte-wise misaligned access, the value-initialized default for undefined sub-register and immediate-size encodings, and sticky (never-trapping) FP arithmetic exceptions via the FCSR FFLAGS.
Chapter 11: Device-facing Surface
This chapter is a normative part of the Maize ISA specification. It fixes the contract by which a Maize program reaches devices: the port-I/O model, external-interrupt vectoring, and the standard device set. A third-party VM that follows this chapter accepts the same device programs and delivers the same interrupts as the reference VM, so the conformance suite can exercise a device surface without reference to any host backend.
This chapter freezes a contract, not an implementation. The surface it describes is latent in the reference VM as reserved encoding and partial mechanism; the chapter's job is to nail that surface into conformance-testable prose so third-party VMs match this one. The interrupt-controller implementation, the device plugin API, and the host-backed devices are out of scope and are built against this frozen contract by later work. Where this chapter says a mechanism is "deferred to the implementing extension", it means the contract is frozen here and the code that satisfies it lands later, and must not deviate from what this chapter fixes.
Ground truth for every encoding and register below is the reference VM (src/maize_cpu.h
for the opcode map, the device shape, and the RF flag bits; src/cpu.cpp for the IN /
OUT / OUTR dispatch, the devices port table, SETINT / CLRINT, IRET, and the run()
delivery seam), cross-checked against the repository README opcode tables and the Trap
Model chapter, which owns the shared vector table, the saved-state layout, and the IRET
return this chapter reuses.
Coherence with the trap model
External and device interrupts do not define a parallel entry story. They vector through the same table, capture the same saved state, and return through the same IRET that the trap model freezes. This single-table, single-frame, single-return design is the one load-bearing decision of this chapter, and it is the coherence requirement stated from the interrupt side that the trap model states from the trap side. Concretely, this chapter inherits from the Trap Model chapter:
- One shared vector table indexed by cause number: synchronous traps occupy the low range 0..31; external and device interrupts occupy the high range 32 and above. The vector index of an interrupt is its cause code.
- One saved-state frame. The entry sequence pushes PC, then RF, then the cause word, then aux, so the frame reads, from SP upward toward higher addresses (top to bottom): aux, cause, RF, PC. The handler pops the two trap-only words (aux, cause) itself, leaving SP at the saved RF.
- One return instruction, the shipped IRET, which pops RF and then PC. There is no TRET and no IRET variant.
- The privileged-operation fault is cause 4 ("privileged operation in user mode"), whose candidate privileged set includes IN / OUT. This chapter pins that membership for port I/O; the trap model owns the vector number and the enforcement mechanism.
- The vector-table format is pinned with the interrupt delivery mechanism: a fixed base
at
0x1000, 8-byte entries holding a full 64-bit handler address, 256 entries (2 KiB) indexed 0..255 by cause number. This chapter fixes that interrupt vectors live in the high range 32..255; the trap model owns the full format. An out-of-range or uninstalled (zero) entry is a deterministic halt with the cause surfaced.
Surface 1: the port-I/O model
Separate device address space, no MMIO
Devices are reached only through a dedicated port space, never through the memory
address space. No device register is mapped into memory. A conformant VM MUST NOT expose
any device state through LD, ST, or CP memory access: a port access reads or writes
device state, not ordinary memory, and a memory access never touches a device register.
Keeping the two spaces disjoint is what leaves the flat 64-bit memory model, and the future
paging sketch in the Reserved Space chapter, free of device carve-outs: memory is uniformly
sparse RAM, and devices are uniformly ports.
This no-MMIO rule governs device registers, not a device's own use of guest memory as a
bulk data buffer. A device MAY, on an explicit port command, perform a bounded transfer
between the device and a region of ordinary guest RAM whose base address the guest
registered through a port, DMA-style. This is distinct from MMIO: no device register is
mapped into memory, the buffer is ordinary sparse RAM with no device side effects on a
LD / ST / CP to it, and the transfer happens only when the guest writes the command
port, never implicitly on a memory access. The memory-backed framebuffer's present command
(Surface 3) is the first such transfer; the control plane stays in ports while the bulk data
plane lives in RAM.
In the reference VM the two spaces are structurally distinct: memory is the sparse
allocate-on-touch block store, and the port space is a separate table (std::map<u_qword, device*> devices in src/cpu.cpp) keyed by port id and reached only by the IN / OUT / OUTR
dispatch. There is no path from a memory address to a device or from a port to memory.
Port space is 16-bit
The port space is a flat numeric namespace of 65,536 ports, $0000 through $FFFF,
disjoint from memory addresses. A port id is a 16-bit value. This freezes the shipped
convention: the port table's key type is a 64-bit word, but every dispatch site truncates
the port operand to its low 16 bits (the .q0 field) regardless of the encoded operand
width, so the effective port id is 16-bit. An implementation MUST mask the port operand to
16 bits; the high 48 bits of a register-named port are ignored, not an error.
Instruction set
Port I/O is performed by three instructions, whose already-shipped encodings this chapter freezes. Each has the four addressing-mode forms the family reserves, selected by the two high opcode bits (value / immediate and value / memory-address), exactly as the rest of the ISA. In every form the port operand names the port and the data operand names the CPU side of the transfer.
- OUT (
$14) transfers a value from the CPU to the device selected by an immediate port operand. Forms:$14regVal,$54immVal,$94regAddr,$D4immAddr (the data source is a register value, an immediate, a value at a register address, or a value at an immediate address, respectively; the port is the trailing immediate). - OUTR (
$1E) is OUT with the port named by a register operand rather than an immediate. Forms:$1EregVal,$5EimmVal,$9EregAddr,$DEimmAddr. The port id is the port register's.q0. - IN (
$1F) transfers a value from the selected device into a register; the four forms vary how the port is named, and the data destination is always a register. Forms:$1FregVal (the port id is a register value),$5FimmVal (the port id is an immediate),$9FregAddr (the port id is fetched from memory at the address in a register),$DFimmAddr (the port id is fetched from memory at an immediate address). In the two address forms the port id is the value read from memory, not the address itself. The transfer direction is device-to-register, the mirror of OUT.
In all twelve forms the port id is the low 16 bits (.q0) of the port operand. This
12-form surface (four forms each of OUT, OUTR, and IN) is the frozen v1.0 port-I/O
instruction set; no new port-I/O encoding is introduced.
Transfer semantics and the device register model
Every standard device is expressed in an abstract (address, data) register pair, which
is the shipped device shape (class device : public reg { reg address_reg; } in
src/maize_cpu.h): a device is a data register (the reg backing) plus an address_reg
that selects an internal offset or sub-function.
- A data transfer moves a value between the CPU operand and the device's data register.
OUT and OUTR write the CPU-side value into the device data register; IN reads the device
data register into the destination register. The device side of the transfer is the full
register width (
w0) per the shipped dispatch; the CPU side is the operand's selected sub-register. - An address / sub-function select is expressed by writing the device's
address_regthrough a port write, then reading or writing the data register. Whether a device needs theaddress_regindirection (a windowed device with many internal registers) or presents a single flat data register (a byte-at-a-time device) is a per-device property, fixed in Surface 3.
A device is free to give a port read and a port write to the same port id different meanings (for example, reading a status register where writing sends a command); the per-device register model in Surface 3 states the read and write meaning of each port.
Reference-VM note. The $94 OUT form (out_regAddr_imm, "value at the address in the
source register") writes the value loaded from the source address, matching the transfer
semantics above and its sibling address forms ($D4 OUT immAddr, and $9E / $DE OUTR
regAddr / immAddr). The conformance suite tests this transfer against the contract.
Privilege
IN, OUT, and OUTR are privileged instructions. Executed with the RF privilege bit
(bit_privilege) clear, that is in user mode, each raises the cause-4 "privileged
operation in user mode" fault. This chapter fixes only the membership: port I/O is not
available to unprivileged code, so a user-mode program cannot touch a device directly and
must go through the kernel. The trap model owns the cause-4 vector number and the general
enforcement mechanism. The reference VM enforces the gate on all three instructions: the
machine starts privileged, and user mode is reached only by an IRET that restores an RF
word with the privilege bit clear.
Unpopulated port
An access to a port with no attached device is a defined outcome, never host undefined behavior. This mirrors the trap model's governing rule that every condition is either a named trap or an explicitly enumerated defined, non-trapping result, with no third category, and it mirrors the sparse-memory model where a read of never-written memory returns 0 and a write is absorbed.
The frozen outcome is read-0 / write-discard:
- An IN from an unpopulated port yields 0.
- An OUT or OUTR to an unpopulated port is discarded (a quiet no-op).
Neither traps. This is a conformance-visible defined-behavior guarantee: a third-party VM MUST return 0 and discard writes on an unpopulated port, so a program that probes the port space behaves identically on every conforming VM.
The reference VM satisfies this at all twelve dispatch sites (the four forms each of OUT, OUTR, and IN) through a single shared port-table lookup helper: a map miss returns a null device and the caller applies read-0 (IN) or write-discard (OUT / OUTR), so no form retains the earlier value-initialize-null-then-dereference path that crashed on an unpopulated port.
Surface 2: external-interrupt vectoring
One shared table, high vector range
External and device interrupts vector through the trap model's shared table, occupying the
high vector range 32 and above; synchronous traps hold the low range 0..31. v1.0
reserves vectors 32 through 255 as IRQ vectors: 224 interrupt vectors in a 256-entry
table. Interrupts start at exactly 32 (the first index above the reserved synchronous-trap
range), settled jointly with the trap model's vector-table format. The vector index of an
interrupt is its cause code: an IRQ delivered at vector v enters the handler at
entry[v] with cause v (where 32 <= v <= 255).
Shared saved state and return
External-interrupt entry reuses the trap model's saved-state frame unchanged. On delivery the machine pushes PC, then RF, then the cause word, then aux, so the frame reads top to bottom aux, cause, RF, PC. For an external interrupt:
- cause is the delivered vector index (a value 32 or above), packed in the low byte of the cause word exactly as a trap cause, with the subcode and high bits reserved-zero unless the source defines a subcode.
- aux is 0 unless a source defines an IRQ-specific subcode word.
The handler pops its two trap-only words (aux, cause) and returns with the shipped IRET, which pops RF and then PC. Traps and interrupts share one return path; there is no separate interrupt-return instruction.
Maskability
External interrupts are maskable through the shipped RF interrupt-enable bit
(bit_interrupt_enabled), toggled by SETINT ($29) and CLRINT ($69). Delivery is
gated at an instruction boundary: a pending IRQ is delivered only while the interrupt-enable
bit is set. Synchronous traps remain unmaskable per the trap model; the interrupt-enable bit
governs only the maskable external sources and has no effect on a fault or breakpoint.
On delivery the machine clears the interrupt-enable bit, so the handler runs with interrupts masked. Because the pre-interrupt RF, including the enable bit, is saved on the frame and restored when IRET pops RF, a normal handler return re-enables interrupts automatically. A handler that wants to accept a further interrupt before it returns does so explicitly with SETINT.
Minimal acknowledge contract
This chapter pins the minimal interrupt-acknowledge contract the ISA must promise; the richer per-line controller is a downstream implementation detail:
- A device or controller raises an IRQ by making a vector pending. The reference VM
carries the seam as the RF
bit_interrupt_setlatch, which signals therun()loop. - The CPU, at the next instruction boundary while interrupts are enabled, acknowledges by clearing the pending latch before entering the handler, so the same IRQ is not re-delivered on return. Acknowledge-on-delivery is the single deterministic ack this chapter fixes.
- Any device-level end-of-interrupt or re-arm (writing an ack register, or reading a status port to clear the source) is expressed through the device's own port registers (Surface 3) and is settled with the controller.
The reference VM realizes this delivery seam at the instruction boundary in run(),
against the four-word aux / cause / RF / PC push order this chapter and the trap model fix.
A pending, enabled IRQ is acknowledged before the frame is built, so the same IRQ is not
re-delivered on IRET; delivery clears the interrupt-enable bit, and the saved RF restores
it on return.
Interrupt model: flat
v1.0 promises a flat interrupt model: a single pending source, no preemptive nesting, and a handler that runs to IRET with interrupts masked unless it explicitly SETINTs. This matches the shipped single-latch shape. The ISA contract does not promise priority levels: a conforming third-party VM implements flat delivery, and a program written to the v1.0 contract does not assume that one IRQ can preempt another. A downstream controller may build a prioritized model with per-line pending and mask and priority registers as an implementation detail, but that is not an ISA-visible promise, and a program that relies on priority levels is not portable across conforming VMs.
Surface 3: the standard device set
v1.0 fixes a conformance baseline of exactly five mandatory devices, PLUS a reserved device-class and port-range convention so future optional devices attach without an ISA revision. The floor is fixed; the surface is extensible.
Each device is defined abstractly: its port-access pattern, its minimal register skeleton, and its IRQ condition and acknowledge behavior, at the level the conformance suite needs a third-party VM to implement. The full per-device register maps, including concrete port-number assignments and the block device's fixed logical block size, are settled with the device-plugin work and land as the device chapters of the ISA document; this chapter freezes the skeleton, not the pinout. No device definition here references the host plugin API, native terminal internals, or any concrete host backend.
Console and framebuffer are required interrupt-capable in v1.0. The block device, timer, and keyboard are interrupt sources as described below. A device that is not interrupt-driven is still pollable through its status register.
1. Console / terminal
The reference device, built in and default. A native terminal remains built in and default.
- Register skeleton: a byte-wide data register and a status register. The status register carries at least an input-available bit, an output-ready bit, and an end-of-input (EOF) bit. The EOF bit latches once the host input stream is exhausted (a read returned no byte); it stays set thereafter, so a reader that sees it delivers a zero-length (EOF) read to the program rather than a synthesized data byte.
- Port access: flat data register (no
address_regindirection). OUT writes one byte to the output stream; IN reads one byte from the input stream. A program polls the status register for readiness. - IRQ: input-available raises an IRQ (the console is required interrupt-capable). The handler reads the data register to consume the byte, which clears the input-available condition.
2. Block device
Random-access storage.
- Register skeleton: a block-number register (selecting the logical block, via the
address_reg), a data register or data port for the block payload, and a control / status register (start-read, start-write, busy, error). The device has a fixed logical block size, stated in the contract; the concrete value is settled with the device-plugin work. - Port access:
address_reg-indirected. A program writes the block number, issues a read or write through the control register, and transfers the payload through the data port. - IRQ: transfer-complete raises an IRQ. The handler reads the status register to observe completion and clear the condition.
3. Timer
The first interrupt source and the end-to-end proof of the interrupt mechanism.
- Register skeleton: a period / reload register, a control register (enable, one-shot versus periodic), and a status / acknowledge register.
- Port access: small windowed register set. A program programs the period, sets the mode and enable, and services and acknowledges ticks through the status / ack register.
- IRQ: fires on each tick (periodic mode) or once (one-shot mode). The handler acknowledges through the status / ack register to re-arm or clear the source. This is the device whose IRQ semantics the conformance suite exercises end to end.
4. Framebuffer
Display output.
- Register skeleton: dimension and format registers (width, height, pixel format), a base-address register, and a present command register.
- Port access: the framebuffer is memory-backed, not register-per-pixel. The pixel
buffer lives in a region of ordinary guest RAM: the program writes pixels with normal
ST/CPstores at full speed, with no per-pixel port traffic. The control plane is ports. The program reads the host-configured width, height, and pixel format (read-only per-run host configuration), writes the guest base address of its pixel buffer to the base register, fills the buffer, and writes the present register to signal a completed frame. On present the device reads the buffer,[base, base + width * height * bytes_per_pixel), from guest memory and displays it. This is not MMIO: the pixel memory has no device side effects (a store there is an ordinary store), and the device reads it only on the explicit present command (Surface 1's DMA carve-out). A present with an unregistered or out-of-range base is a defined, non-trapping invalid present. The buffer size is fixed by the host resolution, never guest-controlled. - IRQ: an optional vsync / frame IRQ (the framebuffer is required interrupt-capable, so the vsync path exists; a program that does not use it simply leaves it masked).
5. Keyboard
Key input.
- Register skeleton: a scancode / data register and a status register (key-available).
- Port access: flat data register with a status register. A program polls key-available and reads the scancode.
- IRQ: key-available raises an IRQ; the handler reads the scancode register, which clears the condition.
Reserved device-class and port-range convention
v1.0 reserves a device-class and port-range convention so an optional device (a mouse, a network interface, a real-time clock, and so on) attaches without an ISA revision. The five mandatory devices occupy reserved low-port blocks; the remainder of the 16-bit port space is held for future device classes assigned by later spec work. Reserving the convention now, and defining only the five-device floor, is what lets the device set grow within v1.x without a binary-compatibility break: a v1.0 program that uses only the five mandatory devices is unaffected by any later device class, because those classes attach in reserved port ranges the program never touches. The concrete port-range assignment and per-device register maps are given below.
Concrete pinout and per-device register maps (v1.0)
The five mandatory devices occupy a reserved low-port block below $0080, so a natural
8-bit immediate port operand reaches them without the immediate sign-extension that would
push the low-16-bit port id (the .q0 field) into the high range. Ports at or above
$0080 remain reachable only via a 16-bit immediate or a register-named port. The ratified
pinout:
Port Device / register R / W meaning
---- ------------------------- ------------------------------------------
$00 console data R: next input byte W: output byte
$01 console status R: bit0 input-available, bit1 output-ready, bit2 end-of-input
$10 keyboard data R: scancode (read clears key-available)
$11 keyboard status R: bit0 key-available
$20 - $22 block device reserved (no backend in this revision)
$40 timer period W: reload value (instruction ticks)
$41 timer control W: bit0 enable, bit1 periodic
$42 timer status / ack R: bit0 tick-pending; W: ack
$50 framebuffer width R: pixels (host config)
$51 framebuffer height R: pixels (host config)
$52 framebuffer format R: format id (1 = XRGB8888)
$53 framebuffer base R/W: guest address of the SELECTED slot's pixel buffer
$54 framebuffer present W: present the selected slot's frame; R: bit0 last-present-valid
$55 framebuffer status R: bit0 vsync-pending, bit2 register-rejected; W: vsync-IRQ-enable / ack
$56 framebuffer slot R/W: select the target slot (0..7) for $53/$54/$55; reset 0
$57 framebuffer activate R/W: W switch the active (scanned-out) surface; R the active slot / console sentinel
IRQ vectors: timer 32, console input-available 33, keyboard key-available 34,
block transfer-complete 35 (reserved), framebuffer vsync/refresh 36.
Console. OUT $00 emits a byte to the output stream; IN $00 reads a byte from the
input stream; $01 reports output-ready and input-available; input-available raises IRQ
33, and reading $00 clears it.
Keyboard. The scancode register carries raw PC hardware scancodes: the Set-1 (XT)
code set. A key press delivers the key's make code; a key release delivers the same code
with bit 7 set (make | $80), the break code. A key event latches a scancode at $10,
sets $11 bit0, and raises IRQ 34; reading $10 consumes the scancode and clears
key-available.
Block device. Ports $20-$22 and IRQ 35 are reserved as the block-device range; no
storage backend, logical block size, or filesystem is defined in this revision. A
reachable-but-unbacked access is a defined, non-trapping outcome.
Timer. The period, control, and status/ack registers at $40-$42 with IRQ 32, as
already specified: a program programs the period, sets enable and periodic in the control
register, and services and acknowledges ticks through the status/ack register.
Framebuffer. Memory-backed (see Surface 3, device 4): $50/$51/$52 are the
read-only host-configured width, height, and pixel format (format id 1 is XRGB8888,
0x00RRGGBB, 4 bytes per pixel). The program writes the guest base address of its pixel
buffer to $53, writes the pixels into that buffer with ordinary stores, and writes $54
to present a completed frame; on present the device reads [base, base + width * height * 4) from guest memory. Reading $54 returns whether the last present was valid (bit0). The
resolution is host configuration for the run; the vsync/refresh IRQ (vector 36) exists at
$55 but generation is disabled by default.
Framebuffer registration table. The framebuffer holds a fixed-size registration table
(8 slots), generalizing a single takeover latch so several guest processes can each register
their own pixel buffer at once (each brings its own guest-RAM buffer; there is no added copy).
Ports $53/$54/$55 are slot-relative: they act on whichever slot $56 currently
selects (reset 0, so a program that never touches $56 sees the single-slot behavior
unchanged). A nonzero base written to $53 claims the selected slot; a zero base
releases it. Only one surface is scanned out at a time; $57 chooses it: writing a
claimed slot's index makes that slot active, and writing the console sentinel ($FFFFFFFF)
returns to the text console. Reading $57 returns the active slot index, or the sentinel
when the console is active. Activation keeps a small history, so releasing the active slot
reverts scanout to the previous surface (a virtual-terminal-style switch-back) rather than
going blank. Width, height, and format are global and apply to every slot identically (no
per-slot resolution). On a display-less view a claim is rejected: the slot stays
unclaimed and $55 bit2 (register-rejected) is set, which lets an operating system return a
per-process error to just that caller instead of the whole machine stopping. Which slot maps
to which host window, and which registration receives input, is host presentation policy
above the device; the device itself knows only slots, never processes.
Explicitly out of scope
The following stay in downstream implementation work, post-freeze, and are built against this frozen contract. Stating them here keeps the contract's boundary sharp.
- A richer interrupt controller: per-line pending, mask, and priority registers and concrete multi-line IRQ wiring. The flat single-latch controller, instruction-boundary delivery against the four-word frame, and the timer as the first live source are realized; a prioritized per-line controller stays a downstream implementation detail that is not an ISA-visible promise.
- The device plugin API: native-code (dynamic-load) plugin loading and the associated
native-code trust boundary. The compile-time, statically-linked host device-model API
(the port-access hooks, port-range registration, and the shim / passthrough shape) and
the concrete port-number and IRQ-vector pinout are settled and published above; the timer
keeps its
$40-$42/ vector 32 assignment. Dynamic native-plugin loading stays out of scope. - The host-backed devices: the real framebuffer, keyboard, timer, and block backends and the built-in native terminal. The instruction-tick timer is deterministic; a real host-time backend is part of this work.
- Closing INT (
$24) dispatch and any privileged control-register encoding: the trap model and the Reserved Space chapter (the reserved control-register mechanism at base slot$26) own that surface. Port-I/O privilege enforcement is landed; the INT software-trap dispatch and any privileged control-register encoding remain reserved.
Binary-compatibility statement
This contract introduces no new ISA-visible encoding. It freezes already-reserved and
already-dispatched opcodes ($14 / $1E / $1F for OUT / OUTR / IN), the shipped RF.H1
flag bits (privilege, interrupt-enabled, interrupt-set, running), the shipped IRET / SETINT
/ CLRINT semantics, the shipped device (address_reg, data) shape, and the shared vector
table the trap model defines. It is a specification of existing and reserved surface, not a
compatibility break. The three code changes this surface implies are realized in the
reference VM against this frozen contract, and none of them alters an encoding:
- Gating IN / OUT / OUTR on the RF privilege bit, so executing them in user mode raises the cause-4 privileged-operation fault.
- Applying the read-0 / write-discard unpopulated-port outcome at all twelve dispatch
sites, closing the
devices[id]value-initialize-null-then-dereference crash. - Correcting the
$94OUT form (out_regAddr_imm) so it writes the value loaded from the source address rather than the raw source register, matching the transfer semantics and its sibling address forms.
Because all three are contract-conforming fixes to behavior a v1.0 binary cannot rely on being different, none is a compatibility break.
Any change that would alter an encoding, for example a different port-space width, would be a binary-compatibility break and would have to be flagged as such when the decision lands. No such change is made here.
Cross-references
- Trap Model chapter: owns the shared vector table, the saved-state frame, the IRET return, and the cause-4 privileged-operation fault this chapter reuses. The two chapters share one table, one frame, and one return path.
- Reserved Space chapter: owns the reserved control-register mechanism and the forward-compatibility guarantee; the privileged port-I/O and interrupt-control instructions named here are consistent with its privilege-and-syscall reservations.
- Conformance chapter: the conformance suite exercises the port-I/O, external-interrupt, and standard-device surface this chapter fixes.
Chapter 12: Forward Compatibility and Reserved Space
This chapter is a normative part of the Maize ISA specification. It fixes what encoding space and what contracts the v1.0 freeze holds open so that a paging MMU, atomics and threads, base-and-bounds segments, and eventually a nommu-Linux (uClinux) port can arrive as v1.x extensions without breaking a single v1.0 binary.
This chapter adds nothing to v1.0. It defines no new instruction, no new register, and no
new semantic. Every encoding it names as reserved already decodes as reserved in the
reference VM and in mzdis, and already appears as a reserved row in the README opcode
tables. The chapter is a reservation-and-contract schedule over state that is already
latent in the machine, so it introduces no binary-compatibility break and requires no
change to src/, mazm, or mzdis.
Ground truth for every number and name below is the reference VM (src/maize_cpu.h for the
opcode map and the register model), cross-checked against the repository README (the
Special-purpose Registers, Flags, Execution, and the two opcode tables) and the Trap Model
chapter.
The forward-compatibility guarantee
A conforming v1.x Maize implementation runs every v1.0 binary unchanged. This guarantee rests on two pillars fixed here:
- Reset state is the v1.0 world. Every extension this chapter reserves is disabled at reset. Paging is off, no segment limit is armed, and the machine comes up privileged in the flat 64-bit model that v1.0 defines. A v1.0 image therefore sees exactly the v1.0 machine on any conforming v1.x VM.
- Extensions arrive only from reserved space. New instructions come from the reserved opcode encodings named below; new architectural state comes through the reserved control-register mechanism, never by widening the 4-bit operand register field or by repurposing a defined encoding. Because a v1.0 binary uses none of the reserved encodings, no extension can collide with it.
Nothing in this chapter is optional for a v1.x implementation that claims backward compatibility.
1. Encoding-space reservation (the opcode map)
The opcode byte is two mode bits plus a six-bit base opcode, giving 64 base slots. The current map includes the full floating-point set plus FGETCSR / FSETCSR and JP / SETP; the residual free-slot count below is pinned by inspecting that map, not from an earlier estimate.
Floating-point base-slot accounting
Floating point occupies twelve base slots: eleven arithmetic, compare, and conversion slots plus one FCSR-access slot. The twelve are:
$1AFADD,$1BFSUB,$1CFMUL,$21FDIV (four arithmetic).$22FSQRT / FNEG / FABS,$23FMADD,$25FMSUB (fused and unary).$2AFCMP (compare).$33FMIN / FMAX.$39FCVTFF / FCVTFS / FCVTFU,$3AFCVTSF / FCVTUF (conversions).$15FGETCSR / FSETCSR (FCSR access; the one slot outside the eleven-op arithmetic set).
JP ($D8) and SETP ($EC) do not consume a base slot; they claim one of the two reserved
spare condition encodings that the Jcc / SETcc families left open, and the other spare pair
($D9 / $ED) stays reserved.
Residual free base slots at freeze
After the floating-point claim, four base slots were earmarked to a known v1.x claimant
class each, forming a labelled reservation band. Earmarking records intent only; it defines
no encoding. Card maize-180 (the paging MMU foundation) has since landed the $26 and $28
claimants, so two fully-free base slots remain ($37, $38); the other two still decode
as reserved until their owning extension lands.
| Base slot | Reserved for | Status |
|---|---|---|
$26 | Privileged control-register access mechanism (move-to / move-from control register) | Landed (card maize-180): MOVTCR $26/$66, MOVFCR $A6 ($E6 reserved) |
$28 | Paging / MMU control (paging-enable, TLB-invalidate) | Landed (card maize-180): TLBINV $28, TLBINVA $68 ($A8/$E8 reserved). Paging-enable is a MOVTCR write to CR0.MODE, no dedicated opcode; the Sv48 walk stays reserved for maize-194 |
$37 | SMP and memory-ordering primitives (fences, acquire-release, LL-SC) | Reserved |
$38 | Versioning and capability query hook | Reserved |
Each class fits comfortably in a single base slot: a base slot carries four addressing-mode
forms, or up to four row-packed register-only micro-ops in the condition-row style the
machine already uses (INC / DEC / NOT / NEG at $31; SETINT / CLRINT / SETCRY / CLRCRY at
$29). For example, the control-register slot needs only a move-to and a move-from form,
and the atomics slot can row-pack a fence with an acquire-release or LL-SC pair.
Full-byte-dispatch reserve and the escape prefix
Three encodings are held for future operations that dispatch on the whole opcode byte
rather than masking to a base slot: $3F, $7F, and $BF. Their sibling $FF is the BRK
breakpoint sentinel and is not available; a run of erased ($FF) memory reached as code
must trap as a breakpoint, so a base-$3F mask-to-base operation (whose immAddr form would
be $FF) can never be defined. These three encodings are the natural home for an escape
prefix.
v1.0 formally reserves an escape-prefix page: the concept of a single opcode byte that,
when formalized, opens a second 256-entry opcode plane and multiplies the machine's ultimate
encoding headroom. v1.0 reserves this page but defines nothing about it. It does not name
which byte is the escape prefix, and it does not define the second plane's contents; both
are deliberately left to a future extension. The $3F / $7F / $BF full-byte-dispatch
band is the reserved candidate carrier. Reserving the page now guarantees that even if every
base slot is one day spent, a whole second plane remains available without any v1.0 binary
being affected.
Micro-op headroom in existing families
Beyond the four free base slots and the escape page, several reserved rows inside
already-defined row-packed families provide headroom for extensions that fit an existing
instruction's shape, at no cost to the free-slot count. These stay reserved and are listed
so the inventory is complete: the spare condition encodings $D9 (Jcc) and $ED (SETcc),
reserved for a future integer-overflow JO / JNO and SETO / SETNO; the reserved zero-operand
row $E7; the FGETCSR / FSETCSR upper
rows $95 and $D5; and the reserved FP rows $E2, $F9, $BA, $FA, $B3, $F3.
The two spare rows of the $24 INT slot, $A4 and $E4, were allocated as the v1.x
zero-operand pair SETSYSG / CLRSYSG (card maize-24): they set / clear the RF syscall-guest
bit that selects whether SYS dispatches to the native provider (clear, the boot default)
or traps through cause 7 to a guest-installed handler (set). This is a v1.x-compatible
extension (default-clear preserves every v1.0 binary); it spends no fully-free base slot
(the $26 / $28 control-register and MMU slots later landed via card maize-180, and
$37 / $38 stay reserved for their v1.x claimants).
The reserved CPZ address-form rows $93 and $D3 were spent as LDZ, the zero-extending
load (card maize-204): LDZ reads N bytes (N = the destination subregister width) and
zero-extends into the full register, sharing CPZ's base slot $13 the way LD shares CP's
$01. Unlike SETSYSG it is default-available (no opt-in bit) and is v1.x-compatible
because no v1.0 binary can contain $93 / $D3: both decoded as reserved /
illegal-instruction from the removal of the original LDZ (card maize-29) until this spend.
2. Memory-ordering and atomics contract
v1.0 states a memory-ordering model rather than leaving it implementation-defined, so that a future multi-hart extension cannot retroactively change what a v1.0 binary means.
v1.0 baseline: single-hart sequential consistency
Maize v1.0 is a single execution context (one hart). All memory operations take effect in program order, and each instruction is indivisible with respect to traps and interrupts: trap delivery is precise (see the Trap Model chapter), so a memory operation either completes fully before a trap is taken or has not begun. Under a single hart every ordinary load and store is therefore already sequentially consistent, and every read-modify-write instruction is already atomic with respect to the only observer that exists.
CMPXCHG is the frozen compare-and-swap primitive
CMPXCHG ($11) exists and is dispatched in the reference VM. v1.0 fully specifies its
contract, so the atomic path has its compare-and-swap at freeze and no second freeze event
is needed for atomics. The instruction takes three operands in the shared three-operand
shape (the same shape as LEA and MULW): CMPXCHG new target expected, where operand 1 is
the new value, operand 2 is the target cell, and operand 3 is the expected (comparand)
value.
The frozen semantics:
- The instruction compares the target (operand 2) against the expected value (operand 3). The comparison is a pure equality test over the selected sub-register width and touches no flag.
- On equality (success): the Zero flag is set to 1, and the new value (operand 1) is copied into the target (operand 2). This is the swap.
- On inequality (failure): the Zero flag is set to 0, and the current target value (operand 2) is copied into the expected register (operand 3), handing the caller the observed value for a retry loop.
- Flag effect: the Zero flag is the sole and authoritative success indicator (1 =
swapped, 0 = not swapped). The copy is flag-neutral and the equality test writes no flag,
so
CMPXCHGleaves the Carry, Negative, and Overflow flags unchanged and reports its outcome only in Zero. A caller tests success with aJZ/JNZon the Zero flag.
The addressing-mode forms follow the family: operand 1 may be a register value ($11), an
immediate ($51), a value at a register address ($91), or a value at an immediate address
($D1); the target and expected operands are always registers.
Because v1.0 is single-hart, this contract is trivially atomic. A conforming v1.x multi-hart
implementation must preserve it: CMPXCHG must remain an atomic compare-and-swap with
exactly these register and Zero-flag effects, so v1.0 binaries that use it as their atomic
keep working.
Reserved SMP ordering path
A future multi-hart extension needs explicit ordering primitives that single-hart v1.0 does
not: memory fences, and either acquire-release annotations or a load-linked / store-
conditional pair. v1.0 reserves base slot $37 and its encoding space for that primitive
set (section 1). The exact primitives are v1.x work; v1.0 only holds the door, so SMP can
arrive from reserved space without invalidating any single-hart v1.0 binary.
3. Privilege and trap hooks for syscall entry and return
The machine already carries the hooks a kernel syscall boundary needs. v1.0 pins them as contract and reserves the surrounding trap-number headroom; it defines no new enforcement.
Privilege mode
RF.H1 holds the privilege, interrupt-enabled, interrupt-set, and running flags, which may
only be set in privileged mode (see the README Flags and Execution sections and the Register
Model chapter). The CPU starts privileged at reset. v1.0 pins user versus supervisor mode on
the privilege bit and pins the rule that privileged flags, registers, and instructions are
inaccessible when the bit is clear. The candidate privileged instruction set (IN / OUT,
SETINT / CLRINT, IRET, HALT, and future control-register and segment writes) is settled
jointly with the port-I/O and segment work; the enforcement mechanism itself is reserved,
and its trap is cause 4 in the trap taxonomy.
Syscall entry and return
SYS ($34) is the syscall-entry primitive. The interrupt path back to privileged mode is
INT / IRET ($24 / $67) with SETINT / CLRINT toggling the interrupt-enable bit.
v1.0 pins the user-to-supervisor transition on SYS and the saved-state and return
contract, which is consistent with the trap model's reserved cause 7 (SYS / syscall
entry):
SYSis a deliberate synchronous software trap. It is trap-class, not fault-class, so it captures the address of the following instruction;IRETtherefore resumes the program at the instruction afterSYS.- The saved-state contract reuses the shared trap frame the trap model freezes: the entry
sequence pushes PC, then RF, then the cause word, then aux, and the handler pops its two
trap-only words (aux, cause) and returns with the shared
IRET, which pops RF and then PC. There is no separate trap-return instruction. - The syscall number and arguments travel in registers per the syscall ABI, not on the
frame. The reference VM today dispatches
SYSdirectly to the BIOS and syscall surface; routing it through the shared trap table at vector 7 is a future path. This chapter reserves the vector and namesSYSas its source; it defines no syscall numbering.
The save and restore contract above matches the trap model's trap-time state contract exactly; the two chapters share one frame layout and one return instruction so a syscall and a trap are indistinguishable to the return path.
Reserved trap classes
The trap model reserves, in the shared cause / vector taxonomy, the numbers that future protection extensions need, so they slot in without renumbering any v1.0 trap:
- Cause 4, privileged operation in user mode (fault): reserved number, enforcement mechanism deferred.
- Cause 5, segment / bounds violation (fault): reserved for the segment extension; reports the faulting address; never fires in the flat model.
- Cause 6, stack fault (fault): reserved for the segment extension; reports the faulting address; never fires until a stack bound exists.
- Cause 7, SYS / syscall entry (trap): reserved as above.
- A future page-fault class joins the same taxonomy from the reserved cause range (8 through 31), with faulting-address and error-code reporting, when paging lands (section 7). Reserving the range now means the page-fault number is assigned without disturbing any v1.0 trap.
4. Thread-pointer convention
The operand register field is fully allocated. The 4-bit operand register field encodes sixteen registers, and all sixteen are assigned: R0 through R9, RT, RV, RF, RB, RP, RS. There is no free operand-register encoding for a new architectural register. A thread pointer therefore cannot be a new operand-addressable register. It is necessarily one of two things, and v1.0 pins the first and reserves the second:
Pinned: R9 is the thread-pointer register by C-ABI convention
v1.0 designates R9 as the thread pointer by calling-convention agreement. This costs
zero encoding at freeze and keeps the threads door open, exactly as RISC-V designates tp
= x4 by convention rather than by a dedicated opcode. The choice is grounded in the Maize
C calling convention (toolchain/qbe-maize/CALLING-CONVENTION.md):
- R9 is a callee-saved general register (R6 through R9 are callee-saved), so a thread pointer held in R9 survives across calls, which a thread pointer must.
- R9 never carries an argument (arguments use R0 through R5) and is not the return register (RV), so pinning it does not perturb argument or return lowering.
- It is not one of the fixed-role special registers (RT scratch, RB frame pointer, RS stack pointer, RP program counter, RF flags), all of which already have a job.
- R9 is the highest general allocatable register. Removing the single highest register from the allocatable pool is the least-disruptive choice for the register allocator, which fills the general pool from the low end; the QBE Maize back-end would drop R9 from its callee-saved allocatable set and treat it as globally reserved, the same treatment RT already receives.
Under this convention a kernel or threading runtime loads the current thread's control block
pointer into R9 on context switch, and thread-local-storage access reads through R9. Because
this is an ABI convention and not an instruction, the reference VM, mazm, and mzdis need
no change; the convention lives in the C calling convention and binds only code that opts
into threading.
Reserved: the thread-pointer system-register path
v1.0 also reserves the option of a future dedicated thread-pointer system register, set by the kernel and read through the reserved control-register mechanism (section 5) rather than through the operand field. If a v1.x extension wants a thread pointer that does not consume a general register, it allocates one control-register number for it. Reserving this path now means the machine can move the thread pointer off R9 later without a new opcode.
5. Control-register mechanism reservation (the linchpin)
Every new privileged register the protection ladder needs (the segment base and limit registers, the page-table-base register, the paging-enable bit, and the optional thread- pointer system register) is architectural state that cannot live in the full 4-bit operand field. v1.0 therefore reserves a single privileged control-register access mechanism and its register-numbering space, defining nothing yet:
- The access mechanism is a privileged move-to-control-register and move-from-control-
register pair, MOVTCR / MOVFCR at base slot
$26(section 1). Landed by card maize-180: executing either with the privilege bit clear raises the cause-4 privileged-operation fault. - The numbering space is a flat control-register index. Card maize-180 assigns the first
three: CR0
SATP(address-translation control), CR1FAULT_VA, CR2FAULT_ERR. Indices above 2 stay reserved for privileged control registers defined by later v1.x extensions (segment base/limit, thread pointer); a write to an unassigned index is discarded and a read yields 0, mirroring the unpopulated-port convention.
This mechanism is the linchpin shared by base-and-bounds segments, paging, and the thread- pointer-as-system-register option: none of them fits the operand field, and all of them reach their state through this one reserved door. Reserving it now, cheaply and defining nothing, is what lets each downstream extension add state without a new access path and without touching v1.0.
6. Base-and-bounds segment reservation
The protection ladder's second rung is base-and-bounds segmentation: a privileged base and limit register pair (with a considered upgrade to two pairs, code and data, for execute protection), an effective address computed as segment base plus offset and limit-checked, and a bounds trap on violation. v1.0 reserves the space for this rung regardless of when it ships:
- The segment base and limit registers are reserved control-register numbers in the section 5 namespace.
- The segment / bounds violation trap is reserved as cause 5 and the stack fault trap as cause 6 in the trap taxonomy, both reporting the faulting address.
Whether the rung-2 registers and traps land inside the v1.0 freeze or arrive as the first v1.x extension is the segment extension's decision; this chapter does not pre-empt that timing. It reserves the register-numbering and trap-number space either way, so the segment extension can define the rung inside or outside v1.0 without any renumbering.
7. Paging and MMU headroom, and the versioning hook
So a future Sv48-style paging extension can arrive without touching v1.0 binaries, v1.0 reserved the following. Card maize-180 (the paging MMU foundation) landed the opcode and control-register pieces; the Sv48 translation itself (the four-level walk, the software TLB behaviour, the page-fault trap) has since landed via card maize-194 on that foundation:
- Opcode headroom for a paging-enable control and a TLB-invalidate instruction, drawn
from base slot
$28(section 1). Landed (card maize-180 opcodes; card maize-194 behaviour): TLBINV$28(flush all) / TLBINVA$68(flush one), live under Sv48. Paging-enable needs no dedicated opcode; it is a MOVTCR write to CR0.MODE. - A privileged control-register namespace for the page-table-base register and the
paging-enable bit, allocated from the section 5 control-register numbering space alongside
the segment base and limit registers. Landed (card maize-180): CR0
SATPcarries both the MODE (paging-enable) field and the root page-table PPN. - A page-fault trap class, reserved from the trap taxonomy's future range (causes 8 through 31), reporting the faulting address and an error code, joining the reserved bounds (cause 5) and stack (cause 6) classes. Landed (card maize-194): cause 8, with the faulting VA in CR1 FAULT_VA and a PRESENT / ACCESS_KIND / USER error code in CR2 FAULT_ERR.
- A versioning and capability hook. v1.0 reserves a read-only way for software to detect
the machine's extension level: an ISA major and minor version plus a supported-extension
bitmap covering floating point, atomics and SMP, segments, and paging, analogous to
RISC-V
misaor x86 CPUID. The carrier is not fixed here (it may be a read-only capability control register in the section 5 namespace, aSYSquery function, or anINport); base slot$38is held as the opcode carrier if the hook is realized as an instruction rather than through the control-register namespace. The versioning policy chapter defines what a version number promises and what may change across a v1.x bump; this chapter only mandates that the hook be reserved. - Paging-off is the reset state and a permanent first-class mode. The entire v1.0 world (flat images, hosted mode) runs unchanged with any future MMU disabled at reset, mirroring the real-hardware boot arc. Stating it in the freeze is what makes the forward- compatibility guarantee concrete: a v1.0 binary is guaranteed to run on every conforming v1.x VM, whatever extensions that VM adds, because they are all off until privileged code turns them on.
The paging address model (48-bit canonical virtual addresses, 4 KB pages, four-level tables, a PTE carrying valid / R / W / X / user-supervisor / accessed / dirty and a physical frame number) is v1.x design work owned by the paging extension, not v1.0 content. v1.0 reserves only the opcode, control-register, and trap-number space that extension will consume.
8. Cross-references and the no-break statement
- The ISA specification document consumes this chapter as its forward-compatibility and reserved-space chapter.
- The versioning and freeze policy chapter consumes the reservation inventory in sections 1 through 7 as the definition of what may be added from reserved space in a v1.x bump versus what is frozen.
- The Trap Model chapter owns the trap taxonomy and the trap-time saved state. The syscall save and restore contract (section 3) and the reserved trap classes (causes 4, 5, 6, 7, and the future page-fault class) are consistent with it; the two chapters share one vector table, one frame layout, and one return instruction.
- The base-and-bounds segment extension owns whether the rung-2 registers and traps land inside v1.0 or as the first v1.x extension (section 6). This chapter reserves the space either way.
- The floating-point set fixes the residual free base-slot count reconciled in section 1.
No v1.0 binary-compatibility break, and no code change. This chapter adds no v1.0
instruction, register, or semantic. Every encoding it names as reserved already decodes as
reserved in the VM and in mzdis and already appears as reserved in the README opcode
tables. The only surfaces it fixes are this freeze document and the README reserved-slot and
privilege notes. No change to src/, mazm, or mzdis is required.
Chapter 13: Conformance
This chapter is normative in stating what conformance means; the concrete conformance test suite is a separate deliverable, referenced at the end.
13.1 The conformance contract
A conforming Maize v1.0 implementation reproduces the observable behavior this specification fixes, for every instruction and every input, bit for bit with the reference VM. Conformance is defined against behavior, not timing (the cycle-cost model, Chapter 14, is out of the behavioral freeze).
The property that makes conformance decidable is the no-undefined-behavior rule (Chapter 10): every condition is either a named trap with a stable cause or an explicitly enumerated defined, non-trapping result, with no third category. Because nothing is implementation-defined, two conforming implementations cannot diverge on any input, and a divergence is always a defect in one of them, never a permitted latitude.
13.2 What a conforming implementation must reproduce
- Instruction semantics. Every instruction's operation, operand forms, and result, per Chapter 7 (integer) and Chapter 8 (floating-point), over every subregister width.
- Flag effects. The C / N / V / Z / P effects of every instruction, exactly as Chapter 7 and Chapter 8 pin them, including the FCMP-only production of P and the explicit "unaffected" for every other instruction.
- Encoding. The opcode and operand-byte decode, the immediate placement, the defined
default for the undefined immediate-size field (4..7 to the value-initialized default),
and the illegal-operand trap on the undefined subregister selector
$F(cause 0), per Chapters 5 and 6. - Memory model. Flat 64-bit, little-endian, sparse read-zero / allocate-on-write, misaligned defined-allow, per Chapter 4.
- Traps. The trap taxonomy, cause / subcode numbering, precise delivery, the fault vs trap PC capture, the saved-state frame, and the deterministic-halt-when-unhandled behavior, per Chapter 10.
- Devices and interrupts. The port-I/O model, the unpopulated-port read-0 / write-discard outcome, and the shared-table external-interrupt vectoring, per Chapter 11.
- Reset and process start. The reset register/flag state and the process-start block, per Chapters 2, 4, and 9.
13.3 Testability shape
Each contract in this specification is stated so it is directly testable from outside the implementation. For example (Chapter 10): a signed DIV by zero enters cause 2 subcode 0 with the faulting-instruction PC, or halts deterministically with cause 2 surfaced when no handler is installed; an undefined opcode enters cause 0 subcode 0 with the offending byte as aux; BRK enters cause 3 with the following-instruction PC. The same observable shape generalizes to every entry in the taxonomy and to every instruction's documented result and flags.
13.4 The conformance suite
The concrete, executable conformance suite (the corpus of programs and expected results a
third-party VM runs to demonstrate conformance) is a separate deliverable and is not
reproduced here. The reference VM's own regression corpus under asm/, driven by
scripts/run-tests.sh, exercises a substantial subset today (including the breakpoint-trap
regression named in Chapter 10). This chapter defines the conformance contract; the suite
provides the vectors.
Chapter 14: Cycle-Cost Model
The cycle-cost / performance model is specified separately and is not part of the v1.0 behavioral freeze.
v1.0 freezes the behavior of the machine: every observable result of every instruction. It deliberately does not fix a timing or cycle-cost model. A per-instruction cost model (how many cycles an instruction takes, and how costs compose) is a separate specification with its own deliverable and its own lifecycle, so that the performance model can evolve without touching the behavioral contract, and so that a behavioral conformance claim (Chapter 13) never depends on timing.
Consequently:
- No program's result depends on cycle cost. Two conforming implementations produce identical observable behavior regardless of how they cost instructions.
- Timing is not observable through the v1.0 ISA. There is no architectural cycle counter in the v1.0 instruction set; any such facility, if added, arrives as a reserved-space extension (Chapter 12) with its own contract.
When the cycle-cost model is published it will be linked here. Until then, this chapter is a named deferral: the absence of a timing model in v1.0 is deliberate, not an omission.
Chapter 15: Versioning and Freeze Policy
This chapter states the versioning intent for the Maize ISA. The full versioning and freeze policy (what a version number promises and precisely what may change across a bump) is a separate deliverable; this chapter fixes the shape it commits to and points to it.
15.1 v1.0 is the behavioral freeze point
Version 1.0 freezes the behavior of the machine: the register and subregister model, the instruction encoding, every instruction's operation / flags / traps, the memory model, the execution and reset model, the trap and interrupt taxonomy, the device-facing port surface, and the floating-point contract. A conforming v1.0 implementation reproduces all of it (see the Conformance chapter). Once frozen, none of these change within the v1.x series except by the additive, reserved-space rules below.
15.2 What a v1.x bump may and may not do
The forward-compatibility guarantee (Chapter 12) is the substance of the versioning policy:
- A v1.x bump may add capability only from reserved space. New instructions come from reserved opcode encodings; new architectural state comes through the reserved control-register mechanism. Every reserved extension is disabled at reset, so a v1.0 binary sees the v1.0 machine on any conforming v1.x VM. This covers paging, base-and-bounds segments, atomics/SMP ordering primitives beyond CMPXCHG, the thread-pointer system register, the escape-prefix second opcode plane, and the versioning/capability hook.
- A v1.x bump may not break a v1.0 binary. It may not repurpose a defined encoding, narrow a defined behavior, widen the operand register field, or change any observable result of a v1.0 instruction. Any change that would alter an encoding or a defined result is a major-version break, not a v1.x bump, and must be flagged as such.
15.3 Capability detection
So software can detect the extension level of the machine it runs on, v1.0 reserves a
read-only versioning and capability hook: an ISA major/minor version plus a
supported-extension bitmap (floating point, atomics/SMP, segments, paging), analogous to
RISC-V misa or x86 CPUID. The carrier (a capability control register, a SYS query, or an
IN port) is reserved but not fixed in v1.0; see Chapter 12 section 7.
15.4 The full policy
The complete versioning and freeze policy, including exactly what a version number promises across a bump and the process for ratifying a reserved-space extension into a v1.x release, is a separate deliverable and will be linked here when published. This chapter fixes the guarantee (sections 15.1 and 15.2) that policy must uphold.
Appendix A: Opcode Map (Numeric)
This appendix lists every base-opcode byte $00..$FF with its mnemonic and operand
shape, in numeric order. It is the encoding-first companion to the function-grouped
Chapter 7. The mode bits (bits 7,6) select the addressing-mode form: $0x regVal, $4x
immVal, $8x regAddr, $Cx immAddr, except in the row-packed condition and unary families,
where the two high bits select a row (Chapter 6). reserved rows decode as the
illegal-instruction trap when reached as an opcode (cause 0); $3F/$7F/$BF are the
reserved full-byte-dispatch / escape-prefix band and $FF is BRK.
The definitive per-byte table is maintained in the repository README.md under "Opcodes
Sorted Numerically" and mirrors src/maize_cpu.h. The summary below groups the map by base
slot; consult Chapter 7 or Chapter 8 for each instruction's full entry.
A.1 Base slots $00..$3F (mode bits select the form)
| Base | Mnemonic | Shape | Forms ($0x/$4x/$8x/$Cx) |
|---|---|---|---|
$00 | HALT | zero-op | $00 (privileged) |
$01 | CP / LD | reg/mem | CP $01 regVal, $41 immVal; LD $81 regAddr, $C1 immAddr |
$02 | ST | store | $02 regVal regAddr, $42 immVal regAddr |
$03 | ADD | ALU | $03 $43 $83 $C3 |
$04 | SUB | ALU | $04 $44 $84 $C4 |
$05 | MUL | ALU | $05 $45 $85 $C5 |
$06 | DIV | ALU | $06 $46 $86 $C6 |
$07 | MOD | ALU | $07 $47 $87 $C7 |
$08 | AND | ALU | $08 $48 $88 $C8 |
$09 | OR | ALU | $09 $49 $89 $C9 |
$0A | NOR | ALU | $0A $4A $8A $CA |
$0B | NAND | ALU | $0B $4B $8B $CB |
$0C | XOR | ALU | $0C $4C $8C $CC |
$0D | SHL | ALU | $0D $4D $8D $CD |
$0E | SHR | ALU | $0E $4E $8E $CE |
$0F | CMP | ALU | $0F $4F $8F $CF |
$10 | TEST | ALU | $10 $50 $90 $D0 |
$11 | CMPXCHG | 3-op | $11 $51 $91 $D1 |
$12 | LEA | 3-op | $12 $52 $92 $D2 |
$13 | CPZ / LDZ | copy / load | $13 CPZ regVal, $53 CPZ immVal; $93 LDZ regAddr, $D3 LDZ immAddr (zero-extending load, maize-204) |
$14 | OUT | port | $14 $54 $94 $D4 (privileged) |
$15 | FGETCSR / FSETCSR | FP reg | FGETCSR $15, FSETCSR $55 ($95/$D5 reserved) |
$16 | JMP | jump | $16 $56 $96 $D6 |
$17 | Jcc column 0 | branch | JZ $17, JB $57, JGE $97, JAE $D7 |
$18 | Jcc column 1 | branch | JNZ $18, JGT $58, JLE $98, JP $D8 |
$19 | Jcc column 2 | branch | JLT $19, JA $59, JBE $99 ($D9 reserved) |
$1A | FADD | FP ALU | $1A $5A $9A $DA |
$1B | FSUB | FP ALU | $1B $5B $9B $DB |
$1C | FMUL | FP ALU | $1C $5C $9C $DC |
$1D | CALL | call | $1D $5D $9D $DD |
$1E | OUTR | port | $1E $5E $9E $DE (privileged) |
$1F | IN | port | $1F $5F $9F $DF (privileged) |
$20 | PUSH | stack | $20 regVal, $60 immVal |
$21 | FDIV | FP ALU | $21 $61 $A1 $E1 |
$22 | FSQRT / FNEG / FABS | FP unary | $22 / $62 / $A2 ($E2 reserved) |
$23 | FMADD | FP 3-op | $23 $63 $A3 $E3 |
$24 | INT / SETSYSG / CLRSYSG | interrupt / zero-op | INT $24 regVal, $64 immVal (privileged; dispatch deferred); SETSYSG $A4, CLRSYSG $E4 (v1.x syscall-provider select, card maize-24) |
$25 | FMSUB | FP 3-op | $25 $65 $A5 $E5 |
$26 | MOVTCR / MOVFCR | control-register | MOVTCR $26 regVal-imm, $66 immVal-imm; MOVFCR $A6 immVal-reg ($E6 reserved) (privileged; control-register access, card maize-180) |
$27 | RET / IRET / NOP | zero-op | RET $27, IRET $67, NOP $A7 ($E7 reserved) |
$28 | TLBINV / TLBINVA | paging / MMU | TLBINV $28 zero-op (flush all), TLBINVA $68 regVal (flush one, VA in the register) ($A8/$E8 reserved) (privileged; live under Sv48, card maize-194) |
$29 | SETINT / CLRINT / SETCRY / CLRCRY | zero-op | $29 / $69 / $A9 / $E9 |
$2A | FCMP | FP compare | $2A $6A $AA $EA |
$2B | SETcc column 0 | set | SETZ $2B, SETB $6B, SETGE $AB, SETAE $EB |
$2C | SETcc column 1 | set | SETNZ $2C, SETGT $6C, SETLE $AC, SETP $EC |
$2D | SETcc column 2 | set | SETLT $2D, SETA $6D, SETBE $AD ($ED reserved) |
$2E | SAR | ALU | $2E $6E $AE $EE |
$2F | CMPIND | compare-ind | $2F regVal regAddr, $6F immVal regAddr |
$30 | TSTIND | test-ind | $30 regVal regAddr, $70 immVal regAddr |
$31 | INC / DEC / NOT / NEG | unary | INC $31, DEC $71, NOT $B1, NEG $F1 |
$32 | CLR / POP | unary | CLR $32, POP $72 ($B2/$F2 reserved) |
$33 | FMIN / FMAX | FP min/max | FMIN $33, FMAX $73 ($B3/$F3 reserved) |
$34 | SYS | syscall | $34 regVal, $74 immVal (user-callable; trap cause 7) |
$35 | UDIV | ALU | $35 $75 $B5 $F5 |
$36 | UMOD | ALU | $36 $76 $B6 $F6 |
$37 | reserved | - | SMP / memory-ordering primitives (reserved) |
$38 | reserved | - | versioning / capability query (reserved) |
$39 | FCVTFF / FCVTFS / FCVTFU | FP convert | $39 / $79 / $B9 ($F9 reserved) |
$3A | FCVTSF / FCVTUF | FP convert | $3A / $7A ($BA/$FA reserved) |
$3B | ADC | ALU | $3B $7B $BB $FB |
$3C | SBB | ALU | $3C $7C $BC $FC |
$3D | MULW | 3-op | $3D $7D $BD $FD |
$3E | UMULW | 3-op | $3E $7E $BE $FE |
$3F | reserved | - | full-byte-dispatch / escape-prefix band ($3F/$7F/$BF) |
A.2 The standalone high-byte encodings
A few instructions occupy a single fixed byte rather than a base-slot family:
| Byte | Mnemonic | Shape |
|---|---|---|
$E0 | XCHG | reg reg |
$E4 | CLRSYSG | zero-op (v1.x syscall-provider select, row 3 of the $24 slot; card maize-24) |
$FF | BRK | zero-op (breakpoint trap, cause 3) |
A.3 Reserved bytes
Every byte not assigned above decodes as reserved. Reached as an opcode, a reserved byte
raises the illegal-instruction trap (cause 0, subcode 0); an unallocated condition encoding
in the Jcc / SETcc families raises cause 0 subcode 1. The full byte-by-byte enumeration,
including each reserved row's earmark, is the repository README "Opcodes Sorted Numerically"
table and the Reserved Space chapter.
Appendix B: Encoding Quick Reference
A one-page summary of the bit fields. Full detail is Chapters 5 and 6.
B.1 The opcode byte
%BBAA`AAAA
|| +----- bits 5..0: base opcode (0..63), 64 base slots
++-------- bits 7,6: mode bits
Mode bits, for an instruction whose source form varies:
bit 6 = 0 source is a register
bit 6 = 1 source is an immediate
bit 7 = 0 source is a value
bit 7 = 1 source is a memory address (@)
The four forms of a full-form instruction at base $xx:
$0x regVal (register value)
$4x immVal (immediate value)
$8x regAddr (value at address in register, @Rn)
$Cx immAddr (value at immediate address, @$nnnn)
In the row-packed condition families (Jcc, SETcc) and register-only unary families, the two high bits instead select a row; see Appendix A and Chapter 6.
B.2 The register operand byte
%RRRR`SSSS
| +----- bits 3..0: subregister selector
+---------- bits 7..4: register field
Register field (high nibble):
$0..$9 R0..R9 $C RF (flags)
$A RT $D RB / BP (base pointer)
$B RV $E RP / PC (program counter)
$F RS / SP (stack pointer)
Subregister selector (low nibble):
$0..$7 B0..B7 (1 byte each)
$8..$B Q0..Q3 (2 bytes each)
$C H0 (4 bytes, low half)
$D H1 (4 bytes, high half)
$E W0 (8 bytes, full register; a bare Rn means Rn.W0)
$F illegal (undefined selector; deterministic illegal-operand trap, cause 0)
Byte/field positions in the 64-bit register (low to high):
offset 7 6 5 4 3 2 1 0
[B7][B6][B5][B4][B3][B2][B1][B0]
[ Q3 ][ Q2 ][ Q1 ][ Q0 ]
[ H1 ][ H0 ]
[ W0 ]
B.3 The immediate source operand byte
%xxxx`x000 $x0 1-byte immediate (8 bits)
%xxxx`x001 $x1 2-byte immediate (16 bits)
%xxxx`x010 $x2 4-byte immediate (32 bits)
%xxxx`x011 $x3 8-byte immediate (64 bits)
Bits 0..2 select the width; bit 3 and bits 4..7 are reserved (must be zero). An immediate-size encoding of 4..7 decodes to the value-initialized default (not a trap). Immediate bytes follow the operand bytes in little-endian order.
B.4 The flag register FL (RF.H0)
bit 0 C carry / borrow
bit 1 N negative (sign of result)
bit 2 V signed overflow
bit 3 P parity / unordered (written only by FCMP; read by JP / SETP)
bit 4 Z zero
bit 5 - reserved
bit 6 - reserved
RF.H1 (privileged, written only in supervisor mode): privilege, interrupt-enabled, interrupt-set, running.
B.5 Worked example
CP $FFCC4411 R3:
$41 $02 $3E $11 $44 $CC $FF
opcode: CP immVal reg (base $01, bit 6 set)
$02: 4-byte immediate follows
$3E: destination R3 ($3) . W0 ($E)
immediate $FFCC4411, little-endian
CP sign-extends the 32-bit immediate to the 64-bit destination, so R3 = $FFFFFFFFFFCC4411.
Appendix C: Syscall Surface (Informative)
This appendix is informative. The syscall ABI is a separate contract; this appendix
summarizes the currently-implemented surface for orientation. The authoritative documents
are toolchain/qbe-maize/CALLING-CONVENTION.md (the C calling convention) and
toolchain/rt/SYSCALL-ABI.md (the C-to-syscall binding).
C.1 The C calling convention
- The first six integer/pointer arguments pass in R0..R5, left to right. Under the
Zfinx floating-point model a
float/doubleargument uses the same integer argument registers (afloatin H0, adoublein W0); there is no separate FP argument class. - Arguments past R5 are pushed on the stack right-to-left in 8-byte slots.
- Results are returned in RV (a
floatin RV.H0, adoublein RV.W0). RV is distinct from R0. - R0..R5 and RV are caller-saved; R6..R9 are callee-saved. RT is back-end scratch (not register-allocatable). RB/BP is the frame pointer (callee-saved, established by the prologue); RS/SP is the stack pointer; RP/PC and RF are fixed-role.
- R9 is the thread pointer by convention (callee-saved, never an argument; Chapter 12).
- The stack is full-descending, 8-byte slots, 8-byte alignment at call boundaries. CALL
pushes an 8-byte return address. The standard prologue is
PUSH BP; CP SP BP; SUB framesize SP; the epilogue isCP BP SP; POP BP; RET.
C.2 The SYS instruction and the syscall boundary
SYS executes a system call with the syscall index in its operand (SYS $01 or SYS Rn).
The index is a single byte (operand.b0), so the id space is $00..$FF. Syscall arguments
follow the same register convention (R0, R1, R2, ...), and the result is placed in RV.
SYS is not privileged: it is the deliberate user-to-supervisor entry, user-callable
and trap-class (Chapter 10 reserves it as cause 7). Today the reference VM dispatches it
directly to the BIOS / syscall surface.
An alternative INT $80 path (syscall number in R9, raise the interrupt) is the planned
OS-level surface and is not implemented yet; the implemented path is SYS.
C.3 The implemented syscall numbers
Maize mirrors Linux x86-64 numbers where an analog exists:
| SYS | Name | Args | Returns |
|---|---|---|---|
$00 | sys_read | R0=fd, R1=buf, R2=count | RV = bytes read, -errno on error |
$01 | sys_write | R0=fd, R1=buf, R2=count | RV = bytes written, -errno on error |
$02 | sys_open | R0=path, R1=flags, R2=mode | RV = fd, -errno on error |
$03 | sys_close | R0=fd | RV = 0, -errno on error |
$05 | sys_fstat | R0=fd, R1=statbuf | RV = 0, -errno on error |
$08 | sys_lseek | R0=fd, R1=offset, R2=whence | RV = new offset, -errno on error |
$0C | sys_brk | R0=requested break (0 queries) | RV = the current break (never -errno) |
$3C | sys_exit | R0=code | does not return; low 8 bits become the exit status |
$A9 | sys_reboot | (none) | (reserved) |
$D9 | sys_getdents64 | R0=fd, R1=dirp, R2=count | RV = bytes read, 0 at end of directory, -errno on error |
For sys_read / sys_write the count in R2 is read as a full 64-bit value. The file
syscalls ($02/$03/$05/$08/$D9, and read/write on fds >= 3) are guest-visible only
when the program was started with a --mount / --mount-home grant; without a grant the
guest filesystem is empty and only the stdio fds exist.
C.4 The error convention
Errors follow the Linux/musl convention: a result in [-4095, -1] encodes -errno, and
everything else is a valid result. The C runtime's wrapper layer translates that into the
familiar errno + -1 return.
C.5 sys_exit versus HALT
sys_exit (SYS $3C) is the status-carrying termination path: it records the low 8 bits of
R0 as the process exit status and stops the VM, so the host process returns that value (codes
wrap to 0..255). HALT ($00) halts the core pending an interrupt and records no status,
so a program that ends via HALT exits 0. See Chapter 9.
Appendix D: Glossary
Terms as used in this specification.
- Base slot / base opcode. The low six bits of the opcode byte (0..63), selecting the instruction; the two high bits are the mode bits or a condition/unary row. (Chapter 6.)
- binary32 / binary64. IEEE-754 single (32-bit) and double (64-bit) floating-point formats. Under Zfinx, binary32 occupies an H0/H1 subregister and binary64 the full W0. (Chapter 8.)
- BP. Alias for RB, the base (frame) pointer. (Chapter 2.)
- byte / quarter-word / half-word / word. 8 / 16 / 32 / 64 bits. A register is one word.
- C, N, V, P, Z. The five arithmetic/logic flags: carry/borrow, negative, signed overflow, parity/unordered, zero. (Chapter 2 section 2.4.)
- CAS. Compare-and-swap; the CMPXCHG instruction. (Chapter 7 section 7.5.)
- CISC. Complex-instruction-set style: ALU instructions may take a memory operand directly. Maize is CISC; only CP/LD/ST/CPZ are held to the strict memory boundary.
- Fault. A synchronous trap that captures the faulting instruction's PC (retryable). (Chapter 10.)
- FCSR. The floating-point control/status register: FRM (rounding mode) + FFLAGS (sticky exception flags). Not operand-addressable. (Chapter 8.)
- FL. Alias for RF.H0, the arithmetic/logic flags. (Chapter 2.)
- Flat-64. A single linear 64-bit address space with no segmentation. (Chapter 4.)
- FRM / FFLAGS. The rounding-mode field and the sticky exception-flag field of the FCSR.
- Full-descending stack. The stack grows toward lower addresses; PUSH/CALL pre-decrement SP before writing. (Chapter 7 section 7.8.)
- Hart. A hardware thread / execution context. v1.0 is single-hart. (Chapter 12.)
- Immediate. A constant encoded in the instruction stream, little-endian, in a power-of-two width selected by the source operand byte. (Chapter 5.)
- MMIO. Memory-mapped I/O. Maize has none: devices are a disjoint port space. (Chapter 11.)
- Mode bits. The two high bits of the opcode byte, selecting register/immediate and value/address for the source operand. (Chapter 5 section 5.1.)
- PC. Alias for RP, the program counter. (Chapter 2.)
- Port space. The 16-bit device address space (65,536 ports), reached only by IN / OUT / OUTR, disjoint from memory. (Chapter 11.)
- Precise trap. A trap delivered after full retirement of all prior instructions and before any later instruction takes effect. (Chapter 10.)
- RF.H0 / RF.H1. The low half of the flag register (FL, arithmetic/logic flags) and the high half (privileged status flags). (Chapter 2.)
- RI. The decoder-internal instruction register; not operand-addressable. (Chapter 2, Chapter 9.)
- Sparse memory. Memory allocated on first write, reading zero where never written; no fault on unmapped access. (Chapter 4.)
- SP. Alias for RS, the stack pointer. (Chapter 2.)
- Subregister. A byte / quarter-word / half-word / word view of a register, selected by the operand byte's low nibble. (Chapter 3.)
- Trap (narrow sense). A synchronous trap that captures the following instruction's PC (BRK, SYS). (Chapter 10.)
- W+X. Writable-and-executable; rejected by the linker's hygiene pass.
- Zfinx. The floating-point model in which FP values live in the integer registers, with no separate FP register bank. (Chapter 8.)
@(at). The memory-access marker:@Xdereferences the address X. (Chapter 5.)