326 lines
6.3 KiB
Go
326 lines
6.3 KiB
Go
// Package vm implements the bytecode CPU that runs ship programs.
|
|
//
|
|
// The machine has 16 32-bit registers (r15 is the stack pointer), a program
|
|
// ROM and a byte-addressable little-endian data RAM. Peripherals are reached
|
|
// through IN/OUT port instructions. Every instruction is 4 bytes:
|
|
//
|
|
// byte 0: opcode | byte 1: ra<<4 | rb | bytes 2-3: signed 16-bit immediate
|
|
//
|
|
// Branch/jump immediates are offsets in instructions relative to the next
|
|
// instruction. Each tick a CPU runs up to a cycle budget or until it executes
|
|
// YIELD; state persists between ticks so an over-long computation simply
|
|
// continues on the next tick.
|
|
package vm
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
)
|
|
|
|
type Op uint8
|
|
|
|
const (
|
|
NOP Op = iota
|
|
YIELD
|
|
HALT
|
|
LDI
|
|
LUI
|
|
MOV
|
|
ADD
|
|
SUB
|
|
MUL
|
|
DIV
|
|
MOD
|
|
AND
|
|
OR
|
|
XOR
|
|
SHL
|
|
SHR
|
|
SAR
|
|
ADDI
|
|
JMP
|
|
BEQ
|
|
BNE
|
|
BLT
|
|
BGE
|
|
CALL
|
|
RET
|
|
PUSH
|
|
POP
|
|
LDB
|
|
LDH
|
|
LDW
|
|
STB
|
|
STH
|
|
STW
|
|
IN
|
|
OUT
|
|
numOps
|
|
)
|
|
|
|
const (
|
|
NumRegs = 16
|
|
SP = 15
|
|
)
|
|
|
|
type Status uint8
|
|
|
|
const (
|
|
Running Status = iota
|
|
Yielded // finished this tick's work voluntarily
|
|
Halted
|
|
Faulted
|
|
)
|
|
|
|
func (s Status) String() string {
|
|
return [...]string{"running", "yielded", "halted", "faulted"}[s]
|
|
}
|
|
|
|
// Bus connects the CPU to peripherals.
|
|
type Bus interface {
|
|
In(port uint16) int32
|
|
Out(port uint16, v int32)
|
|
}
|
|
|
|
type CPU struct {
|
|
R [NumRegs]int32
|
|
PC uint32 // byte address into Prog
|
|
Prog []byte
|
|
RAM []byte
|
|
Status Status
|
|
Fault string
|
|
}
|
|
|
|
// New creates a CPU with the program loaded and the stack pointer at the top
|
|
// of RAM.
|
|
func New(prog []byte, ramBytes int) (*CPU, error) {
|
|
if len(prog)%4 != 0 {
|
|
return nil, fmt.Errorf("program length %d is not a multiple of 4", len(prog))
|
|
}
|
|
c := &CPU{Prog: append([]byte(nil), prog...), RAM: make([]byte, ramBytes)}
|
|
c.R[SP] = int32(ramBytes)
|
|
return c, nil
|
|
}
|
|
|
|
func (c *CPU) fault(format string, args ...any) {
|
|
c.Status = Faulted
|
|
c.Fault = fmt.Sprintf(format, args...)
|
|
}
|
|
|
|
func cost(op Op) int {
|
|
switch op {
|
|
case MUL:
|
|
return 2
|
|
case DIV, MOD:
|
|
return 8
|
|
}
|
|
return 1
|
|
}
|
|
|
|
// Run executes instructions until the budget is spent, YIELD, HALT or a
|
|
// fault, returning the cycles used. A yielded CPU resumes on the next call.
|
|
func (c *CPU) Run(bus Bus, budget int) int {
|
|
if c.Status == Halted || c.Status == Faulted {
|
|
return 0
|
|
}
|
|
c.Status = Running
|
|
used := 0
|
|
for used < budget {
|
|
if int(c.PC)+4 > len(c.Prog) {
|
|
// Falling off the end of the program halts the CPU.
|
|
c.Status = Halted
|
|
break
|
|
}
|
|
w := binary.LittleEndian.Uint32(c.Prog[c.PC:])
|
|
op := Op(w)
|
|
ra, rb := (w>>12)&0xf, (w>>8)&0xf
|
|
imm := int32(int16(w >> 16))
|
|
if op >= numOps {
|
|
c.fault("illegal opcode %d at pc=%d", op, c.PC)
|
|
break
|
|
}
|
|
used += cost(op)
|
|
next := c.PC + 4
|
|
r := &c.R
|
|
switch op {
|
|
case NOP:
|
|
case YIELD:
|
|
c.Status = Yielded
|
|
case HALT:
|
|
c.Status = Halted
|
|
case LDI:
|
|
r[ra] = imm
|
|
case LUI:
|
|
r[ra] = int32(uint32(imm)<<16 | uint32(r[ra])&0xffff)
|
|
case MOV:
|
|
r[ra] = r[rb]
|
|
case ADD:
|
|
r[ra] += r[rb]
|
|
case SUB:
|
|
r[ra] -= r[rb]
|
|
case MUL:
|
|
r[ra] *= r[rb]
|
|
case DIV, MOD:
|
|
d := r[rb]
|
|
if d == 0 {
|
|
c.fault("division by zero at pc=%d", c.PC)
|
|
break
|
|
}
|
|
switch {
|
|
case d == -1: // avoid MinInt32 / -1 overflow panic semantics
|
|
if op == DIV {
|
|
r[ra] = -r[ra]
|
|
} else {
|
|
r[ra] = 0
|
|
}
|
|
case op == DIV:
|
|
r[ra] /= d
|
|
default:
|
|
r[ra] %= d
|
|
}
|
|
case AND:
|
|
r[ra] &= r[rb]
|
|
case OR:
|
|
r[ra] |= r[rb]
|
|
case XOR:
|
|
r[ra] ^= r[rb]
|
|
case SHL:
|
|
r[ra] = int32(uint32(r[ra]) << (uint32(r[rb]) & 31))
|
|
case SHR:
|
|
r[ra] = int32(uint32(r[ra]) >> (uint32(r[rb]) & 31))
|
|
case SAR:
|
|
r[ra] >>= uint32(r[rb]) & 31
|
|
case ADDI:
|
|
r[ra] += imm
|
|
case JMP:
|
|
next = uint32(int64(next) + int64(imm)*4)
|
|
case BEQ, BNE, BLT, BGE:
|
|
var t bool
|
|
switch op {
|
|
case BEQ:
|
|
t = r[ra] == r[rb]
|
|
case BNE:
|
|
t = r[ra] != r[rb]
|
|
case BLT:
|
|
t = r[ra] < r[rb]
|
|
case BGE:
|
|
t = r[ra] >= r[rb]
|
|
}
|
|
if t {
|
|
next = uint32(int64(next) + int64(imm)*4)
|
|
}
|
|
case CALL:
|
|
if c.push(int32(next)) {
|
|
next = uint32(int64(next) + int64(imm)*4)
|
|
}
|
|
case RET:
|
|
if v, ok := c.pop(); ok {
|
|
next = uint32(v)
|
|
}
|
|
case PUSH:
|
|
c.push(r[ra])
|
|
case POP:
|
|
if v, ok := c.pop(); ok {
|
|
r[ra] = v
|
|
}
|
|
case LDB, LDH, LDW:
|
|
n := accessSize(op)
|
|
if a, ok := c.addr(r[rb]+imm, n); ok {
|
|
var v uint32
|
|
for i := n - 1; i >= 0; i-- {
|
|
v = v<<8 | uint32(c.RAM[a+i])
|
|
}
|
|
r[ra] = int32(v)
|
|
}
|
|
case STB, STH, STW:
|
|
n := accessSize(op)
|
|
if a, ok := c.addr(r[rb]+imm, n); ok {
|
|
v := uint32(r[ra])
|
|
for i := 0; i < n; i++ {
|
|
c.RAM[a+i] = byte(v >> (8 * i))
|
|
}
|
|
}
|
|
case IN:
|
|
r[ra] = bus.In(uint16(imm))
|
|
case OUT:
|
|
bus.Out(uint16(imm), r[ra])
|
|
}
|
|
if c.Status == Faulted {
|
|
break
|
|
}
|
|
c.PC = next
|
|
if c.Status != Running {
|
|
break
|
|
}
|
|
}
|
|
return used
|
|
}
|
|
|
|
func accessSize(op Op) int {
|
|
switch op {
|
|
case LDB, STB:
|
|
return 1
|
|
case LDH, STH:
|
|
return 2
|
|
}
|
|
return 4
|
|
}
|
|
|
|
func (c *CPU) addr(a int32, n int) (int, bool) {
|
|
if a < 0 || int(a)+n > len(c.RAM) {
|
|
c.fault("memory access out of range: %d", a)
|
|
return 0, false
|
|
}
|
|
return int(a), true
|
|
}
|
|
|
|
func (c *CPU) push(v int32) bool {
|
|
a, ok := c.addr(c.R[SP]-4, 4)
|
|
if !ok {
|
|
return false
|
|
}
|
|
c.R[SP] -= 4
|
|
binary.LittleEndian.PutUint32(c.RAM[a:], uint32(v))
|
|
return true
|
|
}
|
|
|
|
func (c *CPU) pop() (int32, bool) {
|
|
a, ok := c.addr(c.R[SP], 4)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
c.R[SP] += 4
|
|
return int32(binary.LittleEndian.Uint32(c.RAM[a:])), true
|
|
}
|
|
|
|
// Encode builds one instruction word.
|
|
func Encode(op Op, ra, rb int, imm int32) uint32 {
|
|
return uint32(op) | uint32(rb&0xf)<<8 | uint32(ra&0xf)<<12 | uint32(uint16(imm))<<16
|
|
}
|
|
|
|
// MarshalState serialises the mutable CPU state (not the program).
|
|
func (c *CPU) MarshalState() []byte {
|
|
b := make([]byte, 0, NumRegs*4+8+len(c.RAM))
|
|
for _, r := range c.R {
|
|
b = binary.LittleEndian.AppendUint32(b, uint32(r))
|
|
}
|
|
b = binary.LittleEndian.AppendUint32(b, c.PC)
|
|
b = append(b, byte(c.Status), 0, 0, 0)
|
|
return append(b, c.RAM...)
|
|
}
|
|
|
|
// UnmarshalState restores state produced by MarshalState.
|
|
func (c *CPU) UnmarshalState(b []byte) error {
|
|
hdr := NumRegs*4 + 8
|
|
if len(b) != hdr+len(c.RAM) {
|
|
return fmt.Errorf("state size %d does not match expected %d", len(b), hdr+len(c.RAM))
|
|
}
|
|
for i := range c.R {
|
|
c.R[i] = int32(binary.LittleEndian.Uint32(b[i*4:]))
|
|
}
|
|
c.PC = binary.LittleEndian.Uint32(b[NumRegs*4:])
|
|
c.Status = Status(b[NumRegs*4+4])
|
|
copy(c.RAM, b[hdr:])
|
|
return nil
|
|
}
|