Acheron VM - Instruction Set

Contents


Naming Conventions

r0-r15The 16 registers currently visible in the register window. r0 is always the head of the register stack.
rDThe primary data register that will be affected in the instruction.
rAAuxiliary/address register for 2-reg instructions. This register tends to be read-only.
rPThe prior register, which is rD from the last instruction that used one. Determines branch tests and is the target of some implied-mode instructions.
rHThe register immediately after rD, usually used as the high word of a 32-bit value starting in rD. For example, if an instruction is using r3 as rD, then rH is r4. There is no wraparound, so if r15 is used, rH will be the currently unaddressable effective r16.
rD:rH32-bit effective register, with rD being the low word and rH being the high word.
imm44-bit immediate value, usually 0-15, though some instructions disallow 0.
imm4p14 bit immediate value interpreted as 1-16.
imm88-bit immediate value, usually 0-255, or -128 to +127 signed.
imm1616-bit immediate value, usually 0-65535, or -32768 to +32767 signed.
rel8Used in branches, a target address which is within an 8-bit signed range of bytes from the first byte of the instruction. A branch to a rel8-encoded $00 loops back to the branch instruction itself.
:=Assignment
*Embedded instruction, taking 16 opcodes.

The following was auto-generated by AcheronVM build tools


Register Operations

clr
*rD
rD := 0
clrmbp
 
memory(rP) := 0 byte
clrmp
 
memory(rP) := 0
copy
 rD, rA
rD := rA
grow4
*imm4p1
Grow the register stack by imm4 (1-16) slots. The resulting r0 becomes the new rP.
grow8
 imm8
Grow the register stack by imm8 (signed) slots. The resulting r0 becomes the new rP.
ldm
 rD, rA
rD := memory(rA)
ldmb
 rD, rA
rD := memory(rA), unsigned byte
ldmi
 rD, rA, imm8
rD := memory(rA + imm8)
ldrptr
 rD, rA
rD := current address of rA. Note that task switching or otherwise swapping out zp can leave this pointer dangling.
popregs
 rD, imm4
Pop imm4 (1-15) registers starting at rD from the CPU stack.
pushregs
 rD, imm4
Push imm4 (1-15) registers starting at rD to the CPU stack.
set16
*rD, imm16
rD := imm16
set8
*rD, imm8
rD := imm8
stm
 rD, rA
memory(rA) := rD
stmb
 rD, rA
memory(rA) := low byte of rD
stmi
 rD, rA, imm8
memory(rA + imm8) := rD
stpm
 rD
memory(rD) := rP
stpmb
 rD
memory(rD) := low byte of rP
with
*rD
Explicitly assign rP.

Pseudo Instructions
growimmBecomes grow4 or grow8
setreg, immBecomes clr, set8, or set16.

Native Routines
jsr clear_rstack
Initializes an empty register stack. This is the minimum initialization needed for basic test code, but note that other features may not be initialized (gptr in particular).

Zeropage Locations
zpTop:
 .res 0
Highest zeropage memory location in use, for copying out process context.
rptr:
 .res 1
Pointer to the head of the register stack, which is also r0, and the lowest used byte in zp.
pptr:
 .res 1
Pointer to the prior register.
rstackTop:
 .res 0
The memory location right after the register stack. This is the value of rptr when the register stack is empty.


Flow Control

ba
 rel8
Always branch.
bc
 rel8
Pop a carry bit, branch if it is set.
bnc
 rel8
Pop a carry bit, branch if it is clear.
bneg
 rel8
Branch if rP is negative.
bnz
 rel8
Branch if rP is non-zero.
bpos
 rel8
Branch if rP is non-negative.
bz
 rel8
Branch if rP is zero.
call
 imm16
Call subroutine at imm16.
call2
 rD, rA, imm16
r0 := rD, r1 := rA, call subroutine at imm16.
callp
 
Call subroutine at the address held in rP.
callp2
 rD, rA
r0 := rD, r1 = rA, call subroutine at the address held in rP.
case16
 imm16, rel8
Branch if rP = imm16.
case8
 imm8, rel8
Branch if rP = imm8.
decloop
 rD, imm4p1, rel8
rD := rD - imm4 (1-16), branch until it wraps past zero. Does not affect carry stack.
jump
 imm16
Jump to imm16.
jumpp
 
Jump to address held in rP.
native
 
Shifts to 6502 mode starting at the byte after this instruction. .X is preloaded with rptr for convenience.
noop
 
No operation.
ret
 
Return from subroutine.
rets4
*imm4p1
Return from subroutine, and shrink register stack by imm4 (1-16) slots. The resulting r0 becomes the new rP.
rets8
 imm8
Return from subroutine, and shrink register stack by imm8 slots. The resulting r0 becomes the new rP.

Pseudo Instructions
retsimmBecomes ret, rets4 or rets8.
caseimm, rel8Becomes case8 or case16

Native Routines
jsr acheron
Enter Acheron mode, interpreting bytecodes immediately after the JSR instruction.

Zeropage Locations
iptr:
 .res 2
Instruction pointer. Always points to the beginning of an instruction, with (zp),y addressing reading the parameters.


Arithmetic

add
 rD, rA
rD := rD + rA
addc
 rD, rA
rD := rD + rA + carry
addi4
 rD, imm4p1
rD := rD + imm4 (1-16)
addi4c
 rD, imm4
rD := rD + imm4 + carry
addi8
 rD, imm8
rD := rD + imm8 (1-256)
addp16
 imm16
rP := rP + imm16
addp16c
 imm16
rP := rP + imm16 + carry
addp8
 imm8
rP := rP + imm8
addp8c
 imm8
rP := rP + imm8 + carry
decp
 
rP := rP - 1
decp2
 
rP := rP - 2
div
 rD, rA
rD := quotient, rH := remainder, of rD/rA.
incp
 
rP := rP + 1
incp2
 
rP := rP + 2
ldiv
 rD, rA
rD := quotient, rH := remainder, of rD:rH/rA.
mac
 rD, rA
rD:rH := rD * rA + rH
mul
 rD, rA
rD:rH := rD * rA
negatep
 
rP := -rP
sub
 rD, rA
rD := rD - rA
subc
 rD, rA
rD := rD - rA - borrow
subi
 rD, imm4p1
rD := rD - imm4 (1-16)
subic
 rD, imm4
rD := rD - imm4 - borrow
subp8
 imm8
rP := rP - imm8
subp8c
 imm8
rP := rP - imm8 - borrow
test
 rD, rA
rH := rD - rA

Pseudo Instructions
addpimmBecomes addp8, addp16, or subp8.
addpcimmBecomes addp8c, addp16c, or subp8c.
subpimmBecomes subp8, addp16, or addp8.
subpcimmBecomes subp8c, addp16c, or addp8c.

Zeropage Locations
cstack:
 .res 1
Carry stack. MSB is the current carry bit.

All add and sub variations push a carry bit, inc and dec do not.


Bitwise Operations

andp
 imm16
rP := rP & imm16
andr
 rD, rA
rD := rD & rA
bswap
 
Swap high and low bytes of rP.
dropc
 
Discard the most recent carry bit.
flipc
 
Flip the state of the most recent carry bit, leaving it on the carry stack.
hibyte
 
rP := upper byte of rP.
lobyte
 
rP := lower byte of rP.
notp
 
rP := rP ^ $ffff
nswap
 
Swap nybbles of the lowest byte of rP.
orp
 imm16
rP := rP | imm16
orr
 rD, rA
rD := rD | rA
roll
 rD, imm4
rD := (rD << imm4) | (rD >> (16 - imm4)), imm4 is nonzero.
shl
 rD, imm4
rD := rD << imm4 (nonzero), bits shift into the carry stack.
shr
 rD, imm4
rD := rD >> imm4 (nonzero), bits shift into the carry stack.
sshr
 rD, imm4
rD := rD >>> imm4 (nonzero), bits shift into the carry stack.
xorp
 imm16
rP := rP ^ imm16
xorr
 rD, rA
rD := rD ^ rA

Global Variables (FEATURE__GLOBALS)

Similar to the 6502's zeropage, 256 bytes of global storage (as opposed to register stack storage) are addressable in short form.

addgptr
 
rP := pointer to global(rP)
ldg
*rD, imm8
rD := global(imm8)
stgp
 imm8
global(imm8) := rP

Zeropage Locations
gptr:
 .res 2
Pointer to the globals area. Must be directly initialized before use.

Dereferencing through gptr allows faster context switching and reusable code between processes with different global tables.


Exception Handling (FEATURE__EXCEPTIONS)

These are non-local returns that can also be used for error handling.

catch
 imm16
Register an exception handler routine at imm16.
popcatch
 
Discard the most recent exception handler.
throw
 rD, rA
If rD is nonzero, throw an exception with tag rD and parameter rA. Can be used to rethrow from inside a catch handler.

Zeropage Locations
currentCatch:
 .res 1
CPU stack position describing the currently registered exception handler.

When a throw is triggered, the CPU and register stacks are restored to the state at the time of the catch, the register stack grows by 2 slots, and r0 & r1 become the exception tag and parameter, respectively, with rP pointing to r0. Usually a handler will use 'case' instructions to branch on desired tags, rethrowing the exception otherwise.

For handling 'finally' situations, normal code flow and exception rethrowing need to be handled in the same code:

  catch finally
  ...
  popcatch
  grow 2
  set r0, 0
finally:
  ...
  throw r0,r1  ; ignored if no exception was thrown and r0 is still zero

Each catch context takes 5 bytes on the CPU stack.


Monitor Traps (FEATURE__TRAPS)

The trap system allows breaking into native code before each instruction is executed, or when exceptions are thrown. Native code can then check the iptr for breakpoint matches, resume, single-step, swap tasks, or do whatever it wants.

Native Routines
jsr enableInstructionTrap
Enable the instruction trap to call <.A >.X.
jsr disableInstructionTrap
Disable the break functionality.
jmp continueFromInstructionTrap
Continue running the VM after a break was triggered. If the break is still enabled, this effectively single-steps.
jsr enableExceptionTrap
Enable the exception trap to call <.A >.X
jsr disableExceptionTrap
Disable exception trap.
jmp continueFromExceptionTrap
Continue processing the exception thrown.

When the trap runs, the iptr points to the beginning of the instruction yet to be executed.

These handlers self-modify the main loop, so there is no runtime overhead when this feature is included and disabled, besides the memory footprint. Since the selfmod happens only on native instruction boundaries, it is safe to enable/disable traps from interrupt handlers. Preemptive task switchers should use this sort of approach.