Skip to content

Latest commit

 

History

History
521 lines (495 loc) · 24.1 KB

instruction-encoding.md

File metadata and controls

521 lines (495 loc) · 24.1 KB

VM Bytecode Specification

Instructions are 4 bytes wide: [Opcode: 8] [A: 8] [B: 8] [C: 8].

  • Registers are split into per-type banks (int_regs, str_regs, por_regs, bool_regs); an operand indexes the bank implied by the opcode. There is no monetary bank: a monetary is a (str_regs asset, int_regs amount) pair, so the instructions that deal in monetaries take or return the two halves separately.
  • 0xFF in a register slot means nil (absent optional operand).
  • Bx = a u16 formed by slots B,C (little-endian); used for pool indices and jump targets. sBx is its signed form.
  • Most instructions are one word. A few extend into continuation words (shown as ↳ cont.); an instruction's length is fixed by its opcode.
  • There is no HALT: programs terminate by design (jumps are forward-only).
  • Opcodes are grouped by category with gaps, so new instructions slot into a category without renumbering. Unused values are reserved (users can't emit them, so we stay free to define them later).

Opcode values match the constants in internal/vm/instruction.go; operand layouts match decodeInstr in internal/vm/verify.go.


1. State & Assertions

OpcodeHexName ABC Description
00x00SET_CURRENT_ASSET asset-- Sets the current asset (used by PULL_ACCOUNT / SEND_TO_ACCOUNT) from str_regs[A]
10x01ASSERT_SAME_ASSET xy- Traps unless str_regs[A] and str_regs[B] are the same asset
20x02ASSERT_VALID_ACCOUNT acc-- Traps if the account name in str_regs[A] is malformed
30x03ASSERT_NON_NEGATIVE_BALANCE amtacc- Traps if int_regs[A] is negative; B = account (for the error)
40x04ASSERT_LEFTOVER porexact- Traps if por_regs[A] is negative; when B == 1 (no remaining) also traps if non-zero
50x05CHECK_ENOUGH_FUNDS pulledtarget- Traps (missing funds) unless int_regs[A] == int_regs[B]: the pulled amount must match the target exactly
60x06ASSERT_VALID_COLOR color-- Traps if the color in str_regs[A] is malformed (only uppercase letters; the empty string is valid)
70x07ASSERT_NON_NEGATIVE_AMOUNT amt-- Traps if int_regs[A] (a sent/saved amount) is negative
80x08ASSERT_VALID_SCOPE scope-- Traps if the scope in str_regs[A] is malformed
90x09ASSERT_NON_NEGATIVE_PORTION por-- Traps if por_regs[A] (an allotment clause portion) is negative
100x0AASSERT_UNSCOPED scopeacc- Traps (cannot cast a scoped account to string) if str_regs[A] is not empty; B = account (for the error). Emitted before a scoped account is interpolated into an account name
0x0B..0x0F reserved

2. Constants & Variables

OpcodeHexName ABC Description
160x10LOAD_INT destBx (const idx) int_regs[A] = int_pool[Bx]
170x11LOAD_STR destBx (const idx) str_regs[A] = str_pool[Bx]
180x12LOAD_VAR_INT destBx (var idx) int_regs[A] = vars.int_pool[Bx]
190x13LOAD_VAR_STR destBx (var idx) str_regs[A] = vars.str_pool[Bx]
200x14LOAD_INT_IMMEDIATE destsBx (i16 value) int_regs[A] = (big.Int)sBx — small literals inline, no pool entry. Reserved; not implemented
210x15CONST_TRUE dest-- bool_regs[A] = true — the value is in the opcode, so there is nothing to decode and no pool entry
220x16CONST_FALSE dest-- bool_regs[A] = false
0x17..0x1F reserved

3. Metadata

OpcodeHexName ABC Description
320x20SET_TX_META keyval- Sets transaction metadata str_regs[A] = str_regs[B]
330x21SET_ACCOUNT_META acckeyval Sets account metadata: account A, key B, value C. 2 words:
​​↳ cont. scope-- Scope reg (0xFF = unscoped)
340x22META_STR destacckey str_regs[A] = meta(account B, key C). 2 words:
​​↳ cont. scope-- Scope reg (0xFF = unscoped)
350x23META_INT destacckey as META_STR, typed int, same continuation word
360x24META_PORTION destacckey as META_STR, typed portion, same continuation word
370x25META_MONETARY dest assetacckey Parses the value as a monetary. One store read yields both halves, so this is the only two-destination read: str_regs[A] = asset
​​↳ cont. dest amtscope- int_regs[A] = amount; scope reg in B (0xFF = unscoped)
0x26..0x2F reserved

4. Arithmetic & Constructors (binary)

OpcodeHexName ABC Description
480x30ADD_INT destleftright int_regs[A] = int_regs[B] + int_regs[C]
490x31SUB_INT destleftright int_regs[A] = int_regs[B] - int_regs[C]
0x32 reserved (was MIN_INT: a min is a comparison and a copy, so it is LT_INT plus a branch)
510x33SUB_PORTION destleftright por_regs[A] = por_regs[B] - por_regs[C]. Its counterpart ADD_PORTION is at 0x38, not adjacent, because 0x32 is burned and 0x34..0x37 were taken
520x34MK_PORTION destnumden por_regs[A] = int_regs[B] / int_regs[C]
0x35 reserved (was MK_MONETARY: a monetary is a register pair, nothing to construct)
540x36ADD_STRING destleftright str_regs[A] = str_regs[B] + str_regs[C]
0x37 reserved (was STR_EQ: moved to the comparison group, §7, now 0x62)
560x38ADD_PORTION destleftright por_regs[A] = por_regs[B] + por_regs[C]. A rational sum, so unequal denominators combine correctly and the result is normalised; it may exceed 1
570x39MUL_PORTION destleftright por_regs[A] = por_regs[B] * por_regs[C]. With PORTION_TO_INT, this is how an allotment share is computed
0x3A..0x3F reserved

5. Unary & Conversions

OpcodeHexName ABC Description
0x40..0x41 reserved (were GET_AMOUNT / GET_ASSET: projecting a monetary is naming one of its two registers, so it costs no instruction)
660x42INT_COPY destsrc- int_regs[A] = int_regs[B] (fresh copy). One copy per bank, none crossing banks; the family is split across 0x42..0x43 and 0x4A..0x4B because 0x44..0x49 were already spoken for. No monetary copy: a monetary is a (str, int) pair, so copy the halves
670x43PORTION_COPY destsrc- por_regs[A] = por_regs[B] (fresh copy)
680x44NEG_INT destsrc- int_regs[A] = -int_regs[B]
690x45INT_TO_STRING destsrc- str_regs[A] = str(int_regs[B])
700x46PORTION_TO_STRING destsrc- str_regs[A] = str(por_regs[B])
710x47MONETARY_TO_STRING destassetamt str_regs[A] = str_regs[B] + " " + str(int_regs[C]) — takes both halves, so it is ternary despite living in this section
0x48 reserved (was IS_ZERO: moved to the comparison group, §7, now 0x63)
0x49 reserved (was NOT: moved to the bool-ops group, §8, now 0x70)
740x4ASTR_COPY destsrc- str_regs[A] = str_regs[B]
750x4BBOOL_COPY destsrc- bool_regs[A] = bool_regs[B]
760x4CINT_TO_PORTION destsrc- por_regs[A] = int_regs[B], exact
770x4DPORTION_TO_INT destsrc- int_regs[A] = floor(por_regs[B])
0x4E..0x4F reserved

6. Funds & Postings

OpcodeHexName ABC Description
800x50PULL_ACCOUNT destacccap Pulls the current asset from account str_regs[B] into the source queue, capped by int_regs[C] (0xFF = uncapped); pulled amount → int_regs[A]. 2 words:
​​↳ cont. overdraftcolorscope Overdraft bound int_regs[A] (0xFF = unbounded), color str_regs[B] and scope str_regs[C] (0xFF = none). Cap and overdraft both 0xFF traps (unbounded source)
810x51SEND_TO_ACCOUNT acccapscope Drains the source queue in the current asset to account str_regs[A], up to int_regs[B] (0xFF = everything queued), scope str_regs[C] (0xFF = unscoped). A = 0xFF is kept: the funds go back to their sources and no posting is emitted. Traps while a mark is open
820x52SAVE accassetamount Reduces the balance of account str_regs[A] for asset str_regs[B] by int_regs[C] (C = 0xFF ⇒ save all), floored at 0. Traps while a mark is open. 2 words:
​​↳ cont. scope-- Scope reg (0xFF = unscoped)
0x53 reserved (was MK_ALLOTMENT: an allotment share is built from MUL_PORTION, PORTION_TO_INT and the leftover fixup, so there is no variadic instruction)
840x54BALANCE dest amtaccasset int_regs[A] = balance(account B, asset C) from the run-state. Only the amount: the resulting monetary's asset is operand C, which the caller already holds. 2 words:
​​↳ cont. scope-- Scope reg (0xFF = unscoped)
850x55MARK_PUSH --- Opens a region at the current source-queue depth and posting count, for oneof backtracking. The run-state keeps the stack of open regions; no register is involved
860x56MARK_END rewind-- Closes the innermost region. A = 1 rewinds it: repays everything pulled and reverses everything posted since the matching MARK_PUSH. A = 0 commits it. Any other value is rejected by the verifier; traps if no region is open. Textual IR: mark_rewind() / mark_commit()
0x57..0x5F reserved (e.g. PULL_ACCOUNT specializations). This block used to run to 0x8F; §7 and §8 took 0x60..0x7F out of it, leaving nine slots for the four specializations sketched in instruction.go

7. Comparisons

Every bool producer lives here. A = dest (a bool_regs index) for all of them; the operand banks are what the opcode implies. IS_ZERO is unary and the rest are binary — they are one group because they are one category, not one arity.

Only < and == exist, per type. The other four surface operators are normalised by the front end:

surface lowering
a < b Lt(a, b)
a > b Lt(b, a) — operands swapped
a <= b Not(Lt(b, a))
a >= b Not(Lt(a, b))
a == b Eq(a, b)
a != b Not(Eq(a, b))

12 surface operators, 5 opcodes. Every extra predicate is another case in the SMT encoder and in any formal model of the VM, so the cost would be paid three times over. LLVM does the same, canonicalising sgt to slt with swapped operands in InstCombine so downstream passes only ever see one form.

OpcodeHexName ABC Description
960x60LT_INT destleftright bool_regs[A] = int_regs[B] < int_regs[C]. Strict
970x61EQ_INT destleftright bool_regs[A] = int_regs[B] == int_regs[C]
980x62STR_EQ destleftright bool_regs[A] = str_regs[B] == str_regs[C]. The only string comparison that yields a value rather than trapping (cf. ASSERT_SAME_ASSET). Was 0x37
990x63IS_ZERO destsrc- bool_regs[A] = int_regs[B].Sign() == 0 — the projection from a quantity to a condition, since the jumps take a bool. Tests the sign, so a negative amount is not zero. Kept alongside EQ_INT because it needs no materialised zero and it is on every quantity branch. Was 0x48
1000x64LT_PORTION destleftright bool_regs[A] = por_regs[B] < por_regs[C]. Strict, and by value — see EQ_PORTION
1010x65EQ_PORTION destleftright bool_regs[A] = por_regs[B] == por_regs[C]. Value equality: 1/2 == 2/4 is true. big.Rat normalises on construction, so the rationals are compared — comparing numerator/denominator pairs separately would give the wrong answer
0x66..0x6F reserved for < and == on types that don't exist yet. Str gets equality only, never ordering. Bool equality, and structural comparison of tuples/arrays, are front-end expansions rather than opcodes. No named-but-unimplemented constants live here on purpose: a live opcode with no emitter invites a second lowering path that no test exercises

8. Bool ops

OpcodeHexName ABC Description
1120x70NOT destsrc- bool_regs[A] = !bool_regs[B] — the only operation whose operand and result are both bools, and what the four derived operators above are built from. Was 0x49
0x71..0x7F reserved for and/or, if they ever pay for themselves — both are expressible as branches, so neither is needed for completeness
0x80..0x8F reserved

9. Control Flow

OpcodeHexName ABC Description
1440x90JMP_IF_FALSE condBx (forward delta) If bool_regs[A] is false, skip Bx instructions: pc += Bx, where pc already points at the next instruction. Being an unsigned delta, the jump is forward-only (guarantees termination). A quantity is not a condition — project it with IS_ZERO first
1450x91JMP —Bx (forward delta) Unconditional: pc += Bx. Forward-only, as above
1460x92JMP_IF_TRUE condBx (forward delta) The dual of JMP_IF_FALSE, so either edge of a condition is one instruction and no negation opcode is needed
0x93..0xFF reserved