Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Control Flow

Instructions for branching, looping, and program termination.

CodeMnemonicArgumentsStack EffectRegister EffectDescription
0x00NOP\([\ldots] \to [\ldots]\)No operation.
0x01TARGET\([\ldots] \to [\ldots]\)Mark a valid jump destination. Required at every label that JUMP/JUMPI may target; treated as NOP at runtime. The assembler emits this automatically wherever a label is placed, either via the .N: shorthand or the explicit TARGET .N directive.
0x02JUMP2label: u16\([\ldots] \to [\ldots]\)Seek the instruction stream to jump_table[label].start. Unconditional. Wide form: takes a u16 label index.
0x03JUMPI2label: u16\([\ldots, c] \to [\ldots]\)Pop \(c\). If \(c \neq 0\), seek to jump_table[label].start; otherwise fall through. Wide form: takes a u16 label index.
0x80JUMP1label: u8\([\ldots] \to [\ldots]\)Same as JUMP2 but with a single-byte u8 label index. Used by the assembler when the label id fits in u8 to save one byte per call site.
0x81JUMPI1label: u8\([\ldots, c] \to [\ldots]\)Same as JUMPI2 but with a single-byte u8 label index.
0x04NEXT\([\ldots] \to [\ldots]\)Advance the active loop frame. For Range: increment current; if \(\text{current} < \text{end}\), seek to body start, else pop frame. For Iter: increment index; if \(\text{index} < \text{len}\), seek to body start, else pop frame. Errors if no loop frame is active.
0x05LVALreg: Register\([\ldots] \to [\ldots]\)writeCopy the current loop value into reg. For Range: \(\text{reg} \leftarrow \text{Int}(\text{current})\). For Iter: \(\text{reg} \leftarrow \text{vec}[\text{index}]\).
0x06RANGE\([\ldots, s, n] \to [\ldots]\)Pop \(n\) (count), then \(s\) (start). Push a Range loop frame with \(\text{current} = s,; \text{end} = s + n\).
0x07ITERreg: Register\([\ldots, s, e] \to [\ldots]\)readPop \(e\) (end_idx), then \(s\) (start_idx). Validate that reg holds VecInt or VecXqmx, copy vec[s..e] into a new Iter loop frame with \(\text{start\_offset} = s\) and \(\text{index} = 0\). The slice is copied, so mutations to the source vec inside the loop body do not affect what LVAL sees. Errors with IndexOutOfBounds if either index is negative, exceeds vec.len(), or if s > e.
0x08LIDXreg: Register\([\ldots] \to [\ldots]\)writeCopy the current loop index into reg as Int. For Range: \(\text{reg} \leftarrow \text{Int}(\text{current})\) (equivalent to LVAL because Range values are already indices). For Iter: \(\text{reg} \leftarrow \text{Int}(\text{start\_offset} + \text{index})\), i.e. the absolute position inside the source vec, not the 0-based slice position. Errors with NoActiveLoop if no loop frame is active.
0x09HALT\([\ldots] \to [\ldots]\)Stop execution immediately.

Branching

JUMP and JUMPI use label indices, not raw byte offsets. The label index maps to a byte range via the program’s jump table. At the assembly level, labels are written as .N (e.g. .0, .1); the assembler resolves them to indices automatically and picks the narrowest encoding:

  • JUMP1 / JUMPI1 (0x80 / 0x81) use a single-byte u8 label index. The assembler emits these whenever the label id is < 256, so most programs will use them exclusively (each call site saves one byte).
  • JUMP2 / JUMPI2 (0x02 / 0x03) use a two-byte u16 label index. The assembler falls back to these only for labels with id >= 256.

The assembly source still spells these as JUMP .N and JUMPI .N; the narrow-vs-wide selection happens at assembly time and is transparent to authors. Disassembled output, on the other hand, shows the explicit form (JUMP1 .N, JUMP2 .N, etc.) so the round-tripped source preserves the exact wire encoding.

TARGET must appear at every label destination. It is a no-op at runtime but serves as a validation marker – the VM verifies that jump targets land on TARGET instructions. The assembler inserts a TARGET automatically wherever a label is placed, so authors do not normally type it by hand. Two equivalent spellings produce the same bytecode:

; Shorthand: label form
.0: HALT

; Explicit form: TARGET directive bound to a label
TARGET .0
HALT

Both compile to [TARGET, HALT]. Use whichever is clearer in context. A bare TARGET (with no operand) emits a raw Target opcode without binding any label; that is only useful for direct bytecode construction and most user programs should prefer one of the label-bearing forms.

Looping

XQVM provides two loop primitives:

Range Loops

RANGE pops \(n\) and \(s\) from the stack and creates a loop frame that iterates current from \(s\) to \(s + n - 1\). Use LVAL inside the loop body to copy the current value into a register, and NEXT to advance:

PUSH 0       ; start
PUSH 10      ; count
RANGE
  LVAL r0    ; r0 = current iteration value (0, 1, ..., 9)
  ; ... loop body ...
NEXT

Iterator Loops

ITER takes a register holding a VecInt or VecXqmx plus two stack operands start_idx and end_idx (with end_idx on top), and iterates over the half-open slice vec[start_idx..end_idx]:

PUSH 0       ; start_idx
PUSH 4       ; end_idx
ITER r1      ; r1 must hold a VecInt or VecXqmx
  LVAL r2    ; r2 = current element (from the slice copy)
  LIDX r3    ; r3 = absolute position in r1 (start_idx + index)
  ; ... loop body ...
NEXT

Both indices must satisfy \(0 \le \text{start} \le \text{end} \le \text{vec.len()}\); otherwise ITER raises IndexOutOfBounds. To iterate the entire vec, push \(\text{start} = 0\) and \(\text{end} = \text{vec.len()}\) (use VECLEN for the latter).

ITER copies the slice into the loop frame at the time it runs, so subsequent in-loop mutations of the source vec via VECSET/VECPUSH are not visible to LVAL or LIDX. This makes loop bodies safe to mutate the register they iterate over.

Loops can be nested. Each RANGE or ITER pushes a frame onto the loop stack; NEXT pops the frame when the loop completes.

Loop Index vs. Loop Value

LVAL reads the current loop value: the integer being iterated for RANGE loops, or the actual vec element for ITER loops. LIDX reads the current loop index into the iteration source instead. The two opcodes have overlapping but distinct semantics:

Loop kindLVALLIDX
RANGEInt(current)Int(current) – identical to LVAL, because the values are indices
ITERthe slice element at the current index (Int or Model)Int(start_offset + index) – the absolute position in the source vec

Use LIDX inside an ITER loop when you need to know where the current element lives in the source vec – typically for index-based lookups or constraint generation. With slicing, LIDX reports the absolute index in the source vec, not the 0-based position within the slice:

PUSH 2       ; iterate r1[2..5]
PUSH 5
ITER r1
  LIDX r2    ; r2 = 2, 3, 4 (absolute position in r1)
  LVAL r3    ; r3 = element value at that position
  ; ... loop body uses both r2 and r3 ...
NEXT

Calling either LIDX or LVAL outside any active loop produces a NoActiveLoop runtime error.