This commit is contained in:
root
2026-09-19 20:21:47 +02:00
commit 0798933b05
62 changed files with 7658 additions and 0 deletions
+325
View File
@@ -0,0 +1,325 @@
// 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
}