• English
  • The Hack Instruction Set Architecture

    ← Tutorial · Hack overview · 中文 · Assembler internals →

    This document focuses on the Hack ISA itself: the state visible to software, machine-instruction encodings, and the state transition caused by each instruction. The detailed bit diagrams use the default hack16 profile; profile differences are called out explicitly. See the Hack package reference for commands, assertions, and annotated .hack metadata.

    What an ISA is

    An instruction set architecture (ISA) is the contract between software and a processor implementation. It defines:

    • registers and memory visible to programs;
    • how machine words decode;
    • what every instruction reads, computes, and writes;
    • how the program counter advances or branches.

    An ISA does not specify how many NAND gates form an adder, how long signals take to settle, or whether an implementation uses a pipeline or cache. Those are circuit or microarchitecture concerns. Two processors implement the same ISA when they produce the same architectural result from the same machine state and instruction.

    The layers used by this project are distinct:

    LayerExamplePart of the Hack ISA?
    Profile machine instructionhack16: 16 bits; hack32: 32 bitsYes
    Standard Hack assembly@2, M=D, D;JGTTextual representation of machine instructions
    Labels and symbols(LOOP), @R0, @variableAssembler syntax, not CPU instructions
    Hack+ pseudoinstructionSET, JEQ target,label, HALTProject-specific assembler convenience, not ISA
    Test directive.assert, .max_stepsExecution metadata; never enters ROM
    Sail modelshared model/*.sail plus profile files/projectsExecutable description of the ISA semantics

    Only A or C instructions for the selected profile ultimately reach the modeled CPU. Commands default to hack16; --profile hack32 selects the extension.

    Architecture overview

    Both profiles use separate instruction and data storage: programs are fetched from ROM while data is read and written in RAM, commonly described as a Harvard architecture. They share a 15-bit PC/address space but differ in word width.

    Architectural state

    Statehack16hack32Purpose
    A16 bits32 bitsAddress/data register; supplies low 15 address bits for M and jumps
    D16 bits32 bitsGeneral data register and fixed ALU input
    PC15 bits15 bitsAddress of the next ROM instruction
    RAM32768 × 16 bits32768 × 32 bitsData address space
    ROM32768 × 16 bits32768 × 32 bitsProgram machine words; loaded by the driver, fetched by the model

    Assembly M is not a separate register. It denotes memory at the current A address:

    M ≡ RAM[A[14:0]]

    The standard assembler symbols R0..R15 are aliases for RAM[0]..RAM[15]; they are not additional architectural registers. For example, @R3 loads address 3 into A, and a following D=M reads RAM[3].

    In hack16, A is 16 bits and an A instruction carries a 15-bit immediate. In hack32, A is 32 bits and an A instruction carries a 31-bit immediate. In both profiles, RAM addresses and jump targets use only A[14:0]; hack32's upper A data bits still participate in the 32-bit ALU.

    Hack platform memory map

    The complete nand2tetris Hack platform normally interprets data addresses as:

    AddressPlatform meaning
    0..16383General RAM
    16384..24575Screen bitmap (SCREEN = 16384)
    24576Keyboard register (KBD = 24576)

    The Sail model treats every address from 0 through 32767 as plain RAM. Instruction addressing rules are modeled; screen refresh and keyboard input are outside its scope.

    Machine-instruction formats

    Each profile has only A and C instruction forms. The following diagrams show the canonical 16-bit hack16 layout; hack32 wraps the same C fields in a wider encoding.

    A instruction

    15              0
    ┌─┬───────────────┐
    │0│ vvvvvvvvvvvvvvv│
    └─┴───────────────┘

    Standard assembly syntax:

    @value

    Semantics:

    A  := zero_extend_16(value)
    PC := (PC + 1) mod 32768

    For example, @42 encodes as:

    0000000000101010

    The operand may be a decimal value or a symbol. Symbol resolution is an assembler operation. hack16 encodes 0 plus 15 immediate bits; hack32 encodes 0 plus 31 immediate bits.

    C instruction

    15  13 12  6 5  3 2  0
    ┌─────┬───────┬────┬────┐
    │ 111 │a cccccc│ddd │jjj │
    └─────┴───────┴────┴────┘

    Standard assembly syntax:

    [dest=]comp[;jump]

    The fields select:

    • a + comp: the ALU computation;
    • dest: any combination of A, D, and M to receive the result;
    • jump: whether PC receives the address from the old value of A.

    dest and jump are optional; comp is required. In hack32, the 16-bit C layout shown above is the low half of the word and the high half is 0xFFFF, so the complete encoding is 1111111111111111 111accccccdddjjj.

    comp: ALU operations

    The fixed ALU input x is D. When a=0, y=A; when a=1, y=M=RAM[A].

    This table lists the canonical mnemonics accepted by the assembler. — means that no canonical assembly form names that combination. The gate-control ALU itself is total for all 64 values of cccccc; a legal C word may therefore decode and execute a control value that the assembler never emits.

    cccccca=0a=1
    1010100—
    1111111—
    111010-1—
    001100D—
    110000AM
    001101!D—
    110001!A!M
    001111-D—
    110011-A-M
    011111D+1—
    110111A+1M+1
    001110D-1—
    110010A-1M-1
    000010D+AD+M
    010011D-AD-M
    000111A-DM-D
    000000D&AD&M
    010101`DA`

    Bitwise and arithmetic results are truncated to the selected word width, so overflow wraps modulo 2^16 in hack16 and modulo 2^32 in hack32. Hack has no separate flags register. Jump conditions inspect whether the current ALU result is zero and whether its most significant bit is one.

    dest: write-back targets

    From high to low, the ddd bits are write enables for A, D, and M:

    dddAssemblyWritten locations
    000omittedNone
    001MRAM[old_A]
    010DD
    011MDM, D
    100AA
    101AMA, M
    110ADA, D
    111AMDA, M, D

    Multiple destinations are architecturally simultaneous. In particular, for AM=... or AMD=..., A receives the new result while M still writes RAM at the address from old_A.

    jump: conditional control flow

    A jump interprets the ALU result out as a two's-complement value of the selected profile width:

    jjjAssemblyCondition
    000omittedNever jump
    001JGTout > 0
    010JEQout == 0
    011JGEout >= 0
    100JLTout < 0
    101JNEout != 0
    110JLEout <= 0
    111JMPAlways jump

    When the condition holds, the target is the low 15 bits of A from the start of the instruction—not the ALU result or a newly written value of A.

    Executing one instruction

    Sail's execute function can be summarized as the following architectural pseudocode.

    A instruction

    A  = zero_extend_to_word(value)
    PC = PC + 1

    C instruction

    old_A    = A
    y        = (a == 0) ? A : RAM[old_A[14:0]]
    out      = ALU(comp, D, y)
    next_PC  = (PC + 1) mod 32768
    
    if dest.A: A = out
    if dest.D: D = out
    if dest.M: RAM[old_A[14:0]] = out
    
    if jump_condition(jump, out):
        PC = old_A[14:0]
    else:
        PC = next_PC

    Saving old_A is the crucial detail. For example:

    AM=D+1;JGT

    If the condition holds, this one instruction:

    1. computes D+1;
    2. writes the result to A;
    3. writes the result to RAM addressed by old A;
    4. jumps to the ROM address from old A.

    That is why shared model/core.sail begins the C-instruction path by preserving the old value of A.

    From standard assembly to machine code

    The assembler uses two passes:

    1. Parse source and expand Hack+ into standard A/C instructions.
    2. In pass one, record the ROM address of every (LABEL); labels occupy no ROM word.
    3. In pass two, resolve A-instruction symbols and encode every machine instruction.
    4. Allocate previously unknown variable symbols consecutively from RAM address 16.

    Predefined symbols include:

    • R0..R15;
    • SP=0, LCL=1, ARG=2, THIS=3, and THAT=4;
    • SCREEN=16384 and KBD=24576.

    For hack16, an A instruction is 0 followed by its 15-bit value, and a C instruction is concatenated as:

    111 + COMP[comp] + DEST[dest] + JUMP[jump]

    For hack32, an A instruction is 0 followed by its 31-bit value. A C instruction is 0xFFFF followed by the same canonical 16-bit C encoding.

    For example:

    M=D

    uses comp=D, dest=M, and no jump:

    111 0001100 001 000
    1110001100001000

    How Hack+ lowers to real instructions

    Hack+ expansion happens before label resolution and machine-code encoding. Every expanded line uses canonical nand2tetris A/C assembly syntax; final machine words use the selected profile's encoding.

    Hack+ sourceStandard Hack expansionMain side effects
    SET target, value@value / D=A / @target / M=DChanges A, D, and RAM[target]
    MOV target, source@source / D=M / @target / M=DChanges A, D, and RAM[target]
    CLR target@target / M=0Changes A and RAM[target]
    INC target@target / M=M+1Changes A and RAM[target]
    DEC target@target / M=M-1Changes A and RAM[target]
    ADD target, source@source / D=M / @target / M=D+MRAM[target] += RAM[source]; changes A and D
    SUB target, source@source / D=M / @target / M=M-DRAM[target] -= RAM[source]; changes A and D
    AND target, source@source / D=M / @target / M=D&MBitwise update of RAM[target]; changes A and D
    OR target, source@source / D=M / @target / M=D|MBitwise update of RAM[target]; changes A and D
    NEG target@target / M=-MChanges A and RAM[target]
    NOT target@target / M=!MChanges A and RAM[target]
    NOP0Advances PC without changing data state
    GOTO label@label / 0;JMPChanges A and jumps
    JNZ/JNE target, label@target / D=M / @label / D;JNEChanges A and D; jumps if nonzero
    JGT target, label@target / D=M / @label / D;JGTChanges A and D; jumps if positive
    JEQ target, label@target / D=M / @label / D;JEQChanges A and D; jumps if zero
    JGE target, label@target / D=M / @label / D;JGEChanges A and D; jumps if nonnegative
    JLT target, label@target / D=M / @label / D;JLTChanges A and D; jumps if negative
    JLE target, label@target / D=M / @label / D;JLEChanges A and D; jumps if nonpositive
    HALTPrivate label + @private-label / 0;JMPForms a self-loop in the selected profile

    Subtleties worth making explicit:

    • SET R0, R1 stores the symbol R1's address value 1 in R0; use MOV R0, R1 to copy RAM[R1].
    • Binary memory operations use destination-first order: SUB R0, R1 means RAM[R0] -= RAM[R1].
    • MOV, binary memory operations, and conditional pseudoinstructions overwrite D.
    • JNZ and JNE are equivalent; JNE matches the standard Hack jump mnemonic.
    • HALT is not a Hack instruction. The assembler creates a unique __HACKPLUS_HALT_n label and a two-instruction self-loop, then records that ROM address as execution metadata. This project's executor stops when it reaches the address; an implementation of the selected profile would remain in the loop.

    Complete lowering example

    Source:

    SET R0, 6
    JEQ R0, DONE
    (DONE)
    HALT

    Conceptual standard Hack assembly after expansion:

    @6
    D=A
    @R0
    M=D
    
    @R0
    D=M
    @DONE
    D;JEQ
    
    (DONE)
    (__HACKPLUS_HALT_0)
    @__HACKPLUS_HALT_0
    0;JMP

    Only after this expansion does pass one compute the ROM addresses of DONE and the private HALT label; pass two then emits words for the selected profile. Expanded instructions therefore consume real ROM addresses, and all label addresses refer to the expanded program.

    Mapping this ISA to Sail

    The current model separates shared semantics from profile layout:

    Sail pathResponsibility
    model/core.sailShared instruction/exception, total 64-control ALU, architectural state, fetch, legality-checked decode, execute, step, and the scattered encdec declaration used directly for encoding
    model/profiles/hack16.sail / hack32.sailSingle profile entries defining widths and legality, including core.sail, then supplying their bidirectional A/C mapping clauses
    projects/hack16.sail_project / hack32.sail_projectOne-file source closure for each buildable profile

    The C-instruction fields use named bit-vector types rather than anonymous bit/bits(n) positions. This documents their roles throughout decoded instructions, alu, and should_jump without changing the raw encoding domain.

    decode_hack checks legality before using the profile mapping. In hack16, words with bit 15 clear are A instructions and only the 111 prefix is C; 100, 101, and 110 are illegal. In hack32, words with bit 31 clear are A instructions and C requires high 16 bits 0xFFFF plus a low-half 111 prefix; every other word with bit 31 set is illegal. An illegal word throws HackIllegalInstruction(word) before execute begins, preserving the raw word without committing state.

    Once the C prefix is legal, all six-bit ALU controls, both a values, and every dest/jump mask are in the machine encoding domain. Shared alu defines all 64 control results at the selected word width. The assembler deliberately emits only the canonical mnemonic subset shown above; assembler vocabulary must not be confused with machine-code legality.

    Compatibility boundary

    hack16 is the canonical nand2tetris baseline. hack32 is not an ordinary nand2tetris .hack format: its words are 32 bits, A immediates are 31 bits, and C words carry the additional 0xFFFF high half. Matching assembly mnemonics do not make the binaries interchangeable; use an explicit hack32 profile and a profile-aware manifest loader.

    Model boundaries

    Keep the complete Hack platform separate from this executable model when interpreting results:

    • ROM is declared by the selected profile composition; the generated driver initializes raw profile-width words, while fetch_hack and hack_step own instruction fetch and dispatch.
    • RAM has no Screen or Keyboard device side effects.
    • HALT, Hack+, .assert, and .max_steps belong to the tool layer, not the Hack ISA.
    • The model specifies architectural transitions, not gate delays, clock-edge details, or nand2tetris HDL.
    • Execution uses Sail's C backend and makes no claim of a completed formal equivalence proof.

    Study the model in code

    1. Read the A/C encodings and execution pseudocode in this document. Open one file under model/profiles/, follow its include into model/core.sail, then return to the profile's mapping clauses; use encdec(instruction) directly for encoding and trace fetch_hack → decode_hack → encdec(word) → execute → hack_step for execution.
    2. Compare standard assembly and Hack+ in programs/basic_alu.asm.
    3. Run pixi run just hack assemble basic_alu and inspect isa/hack/.build/hack16/asm/basic_alu/basic_alu.hack; add --profile hack32 to inspect .build/hack32/asm/basic_alu/basic_alu.hack.
    4. Read tests/sail/hack16/conformance.sail and tests/sail/hack32/conformance.sail to see profile rules turned directly into tests.

    Specification sources


    Next: choose an extension and evolve Hack yourself.