tlm-jxcl-forge / docs /ISA_SPEC.md
SNAPKITTYWEST's picture
October 2026 main drop: mirror from GitHub (part 2)
ae44e8c verified
|
Raw History Blame Contribute Delete
11.9 kB

TLM JXCL β€” Instruction Set Architecture Specification

This is the authoritative architectural specification for TLM JXCL ("Pure Raw Dense ISA Forge"): a deterministic, byte-addressable, 64-bit instruction set, and the reference implementation of it in the jxcl crate. Every bare file path in this document (src/..., alu.rs, execution.rs, etc.) is relative to crates/jxcl/.

The implementation (isa::opcodes::ALL/all_defs()) is the single source of truth for opcode numbers, formats and flag effects; this document is a human-readable rendering of that same table plus the architectural decisions the code makes (this file and isa/opcodes.rs must never disagree β€” if they do, the registry is correct and this file has a bug).

1. Machine model

Component Width Notes
General-purpose registers R0-R31 64-bit R0 is hardwired to zero: reads yield 0, writes are discarded.
PC (program counter) 64-bit Holds the address of the instruction currently being fetched.
SP (stack pointer) 64-bit Grows downward; initialized to memory.len().
FP (frame pointer) 64-bit Architectural register; the base ISA never writes it itself (available for calling-convention use by software).
FLAGS 4 used bits Z (bit 0), N (bit 1), C (bit 2), V (bit 3).
Address space 64-bit Backed by a bounded flat buffer in the reference machine.
Endianness Little-endian All multi-byte memory and immediate fields.

2. Encoding

Every register operand occupies one whole byte (values 0..32 are legal; anything else is INVALID_REGISTER). This is a deliberate trade-off: JXCL is dense relative to fixed-width RISC encodings (the shortest instructions are 1-2 bytes; nothing is padded to a uniform word), but every field boundary is byte-aligned, so decoding is never ambiguous β€” no bit-packing puzzle, no variable-length integer scheme. Immediates and displacements are fixed-width per opcode (never chosen by a runtime flag) and little-endian.

Format Layout Length
None opcode 1
R opcode, rd 2
RR opcode, rd, rs 3
RRR opcode, rd, rs1, rs2 4
RImm64 opcode, rd, imm64 10
RMem opcode, rd, base, disp32 7
MemR opcode, base, disp32, rs 7
Cas opcode, rd, base, rs_new, disp32 8
BranchImm32 opcode, disp32 5
Imm16 opcode, imm16 3

Minimum instruction length: 1 byte. Maximum: 10 bytes. Opcode space: one byte (256 possible opcodes; 50 are assigned).

Effective address for every memory operand (RMem/MemR/Cas) is R[base] + sign_extend(disp32) β€” there is no scaled-index (base + index*scale + disp) addressing mode in this version; that is a deliberate scope decision (see spec discussion Β§8), not an oversight.

3. Opcode map

Opcode Mnemonic Format Flags Category
0x00 NOP None β€” System
0x01 MOV RR β€” Data
0x02 MOVI RImm64 β€” Data
0x03 LOAD RMem β€” Data
0x04 STORE MemR β€” Data
0x05 PUSH R β€” Data
0x06 POP R β€” Data
0x07 LEA RMem β€” Data
0x10 ADD RR Z,N,C,V Arithmetic
0x11 SUB RR Z,N,C,V Arithmetic
0x12 ADC RR Z,N,C,V Arithmetic
0x13 SBC RR Z,N,C,V Arithmetic
0x14 MUL RR Z,N,C,V Arithmetic
0x15 MULH RR Z,N Arithmetic
0x16 DIV RR Z,N Arithmetic
0x17 REM RR Z,N Arithmetic
0x18 NEG R Z,N,C,V Arithmetic
0x19 INC R Z,N,V Arithmetic
0x1A DEC R Z,N,V Arithmetic
0x20 AND RR Z,N Logical
0x21 OR RR Z,N Logical
0x22 XOR RR Z,N Logical
0x23 NOT R Z,N Logical
0x24 NAND RR Z,N Logical
0x25 NOR RR Z,N Logical
0x26 XOR3 RRR Z,N Logical
0x30 SHL RR Z,N,C Shift
0x31 SHR RR Z,N,C Shift
0x32 SAR RR Z,N,C Shift
0x33 ROL RR Z,N,C Shift
0x34 ROR RR Z,N,C Shift
0x40 CMP RR Z,N,C,V Comparison
0x41 TEST RR Z,N Comparison
0x50 JMP BranchImm32 β€” Control flow
0x51 CALL BranchImm32 β€” Control flow
0x52 RET None β€” Control flow
0x53 JZ BranchImm32 β€” Control flow
0x54 JNZ BranchImm32 β€” Control flow
0x55 JC BranchImm32 β€” Control flow
0x56 JNC BranchImm32 β€” Control flow
0x57 JL BranchImm32 β€” Control flow
0x58 JLE BranchImm32 β€” Control flow
0x59 JG BranchImm32 β€” Control flow
0x5A JGE BranchImm32 β€” Control flow
0x60 HALT None β€” System
0x61 TRAP Imm16 β€” System
0x62 SYS Imm16 β€” System
0x70 CAS Cas Z,N (custom, see Β§7) Atomic
0x71 XCHG RMem β€” Atomic
0x72 FENCE None β€” Atomic

isa::opcodes::all_defs() is machine-readable proof this table is exhaustive and collision-free (no_duplicate_opcodes / every_mnemonic_has_exactly_one_entry tests enforce it on every build).

4. Flag semantics

  • Carry (C) follows the x86 convention: for ADD/ADC it is the unsigned carry-out of bit 63; for SUB/SBC/CMP/NEG it is the unsigned borrow (1 iff the true difference is negative).
  • Overflow (V) is always the signed overflow of the operation.
  • INC/DEC leave C unchanged (only Z, N, V) β€” a documented choice distinguishing them from full ADD/SUB of 1.
  • MUL sets C and V together when the discarded high 64 bits of the 128-bit product are nonzero; it does not distinguish signed vs. unsigned overflow.
  • Shift/rotate C is the last bit shifted out (or, for shift-by-zero, false). Shift/rotate counts are masked to the low 6 bits of the count operand (0..64), never faulting on a large count.
  • CMP/TEST compute like SUB/AND respectively but discard the result, keeping only flags.

5. Memory model

The reference machine is a single flat, bounded address space split into two regions:

  • Code region [0, code_size): readable + executable, not writable.
  • Everything else: readable + writable, not executable.

An access that only partially overlaps the code region is denied in full (not just the overlapping part) β€” matching how a real MMU denies an access that touches any byte of a protected page.

16/32/64-bit accesses must be naturally aligned or raise AlignmentFault; 8-bit accesses are always aligned. Faults: INVALID_ADDRESS, ALIGNMENT_FAULT, READ_VIOLATION, WRITE_VIOLATION, EXECUTE_VIOLATION (memory.rs), plus INVALID_OPCODE, INVALID_REGISTER, DIVIDE_BY_ZERO, STACK_FAULT, ILLEGAL_OPERAND at the execution layer (execution.rs).

LOAD/STORE/XCHG/CAS all operate on 64-bit words only in this version (no byte/half-word/word memory ops) β€” the enumerated instruction list never asked for width-suffixed memory ops, so this is a scope decision, not an omission.

6. Control flow

PC holds the address of the instruction currently being fetched. Every branch/CALL displacement is relative to the address of the next sequential instruction:

pc_after_fetch = pc_of_instruction + instruction_length
target = pc_after_fetch + displacement

Signed conditions after a CMP/SUB use the standard two's-complement flag algebra: JL is N != V, JGE is N == V, JG is !Z && (N == V), JLE is Z || (N != V).

CALL pushes pc_after_fetch (the return address) and jumps to the target; RET pops that address back into PC. The stack grows downward: PUSH decrements SP by 8 before writing; POP reads then increments SP by 8. SP is initialized to memory.len().

7. Atomics (single-threaded reference semantics)

CAS rd, [base+disp], rs_new: reads the memory word at the effective address; if it equals R[rd] ("expected"), writes R[rs_new] there and leaves R[rd] unchanged, setting Z. Otherwise R[rd] is overwritten with the actual memory value and Z is cleared. N reflects the sign bit of whatever ends up in R[rd]. This is a custom flag convention (not the generic ALU Z/N), documented here as required by spec Β§25.

XCHG rd, [base+disp]: atomically swaps R[rd] with the memory word.

FENCE: a real, decodable no-op in this single-threaded reference model β€” there is no second agent whose memory-visibility order a fence could affect. It exists so instruction streams written for a concurrent JXCL backend still assemble and execute here unchanged (spec Β§25 explicitly disclaims any hardware cache-coherency claim beyond what is actually implemented).

8. Binary format

See src/binary.rs for the exact 56-byte header layout (MAGIC, VERSION, ARCHITECTURE, ENTRY_POINT, CODE_OFFSET, CODE_SIZE, DATA_OFFSET, DATA_SIZE, FLAGS, reserved). When loaded, code is placed at virtual address 0 and data immediately follows at virtual address code_size β€” a fixed, deterministic layout.

9. Assembly syntax

start:
    MOVI R1, 10
    MOVI R2, 20
loop:
    ADD  R1, R2
    DEC  R2
    CMP  R2, R0
    JG   loop
    HALT

.data
buffer:
    .qword 1234
    .byte 1, 2, 3
  • Labels (name:), instructions, and directives are one per line; ; starts a line comment.
  • Registers: R0-R31 (case-insensitive).
  • Numeric literals: decimal, 0x hex, 0b binary; a leading +/- is accepted before a literal.
  • Memory operands: [Rbase], [Rbase+disp], [Rbase-disp].
  • Directives: .code/.text, .data (switch section), .const NAME value, .entry LABEL_OR_NUMBER, .byte/.word/.dword/.qword v, v, ... (emit data).
  • A label or .const reference is used wherever an immediate, displacement, or branch target is expected; label addresses are absolute virtual addresses (code labels equal their code offset; data labels equal code_size + their data offset).

The disassembler's canonical form never invents label names β€” it prints every resolved address as a plain number, which the assembler accepts anywhere it accepts a label. This is what makes assemble(disassemble(program)) == program hold without needing a symbolication pass in the disassembler (see src/disassembler.rs).

10. Execution engine

execution::step performs, in order: fetch (opcode byte, then the rest of the instruction once its format-determined length is known) β†’ decode (encoding::decoder::decode_one, which also validates every register field) β†’ execute (the giant match in execution.rs, dispatching to alu.rs for arithmetic/logical/shift operations) β†’ update state β†’ next PC. Every architectural fault path sets state.fault and state.halted and returns StepResult::Faulted rather than panicking. The handful of .unwrap()/.expect() calls outside test code (execution.rs's register read/write, main.rs's post-validation binary re-parse, fixed-width slice-to-array conversions in binary.rs/encoding/decoder.rs after a length check earlier in the same function) are all on invariants the immediately preceding code in that same function already established β€” none of them is reachable on malformed input reaching that point, but they are not architectural fault handling either. All architectural error/fault reporting β€” bad opcodes, bad registers, memory violations, divide-by-zero, stack under/overflow, malformed assembly/binaries β€” goes through the structured error types in errors.rs, never a panic.

SYS/TRAP never touch the host filesystem/network/process/environment themselves β€” they only set state.pending_signal, which the CLI or a future host integration observes after step() returns.