init
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var mnemonics = map[string]Op{
|
||||
"nop": NOP, "yield": YIELD, "halt": HALT, "ldi": LDI, "lui": LUI, "mov": MOV,
|
||||
"add": ADD, "sub": SUB, "mul": MUL, "div": DIV, "mod": MOD, "and": AND,
|
||||
"or": OR, "xor": XOR, "shl": SHL, "shr": SHR, "sar": SAR, "addi": ADDI,
|
||||
"jmp": JMP, "beq": BEQ, "bne": BNE, "blt": BLT, "bge": BGE, "call": CALL,
|
||||
"ret": RET, "push": PUSH, "pop": POP, "ldb": LDB, "ldh": LDH, "ldw": LDW,
|
||||
"stb": STB, "sth": STH, "stw": STW, "in": IN, "out": OUT,
|
||||
}
|
||||
|
||||
// Assemble converts assembly text into a program. Syntax:
|
||||
//
|
||||
// label: define a label
|
||||
// .equ NAME value define a constant
|
||||
// li rd, value pseudo-op: load a 32-bit constant (always 2 instructions)
|
||||
// ldw rd, [rb+off] memory access (also ldb/ldh/stb/sth/stw)
|
||||
// in rd, port | out port, rs
|
||||
// beq ra, rb, label branches and jmp/call take labels
|
||||
//
|
||||
// Registers are r0..r15 (sp = r15). Comments start with ';' or '#'.
|
||||
func Assemble(src string) ([]byte, error) {
|
||||
type line struct {
|
||||
no int
|
||||
text string
|
||||
}
|
||||
var lines []line
|
||||
consts := map[string]int64{}
|
||||
labels := map[string]int{}
|
||||
n := 0 // instruction count
|
||||
for i, raw := range strings.Split(src, "\n") {
|
||||
t := raw
|
||||
if j := strings.IndexAny(t, ";#"); j >= 0 {
|
||||
t = t[:j]
|
||||
}
|
||||
t = strings.TrimSpace(t)
|
||||
for {
|
||||
j := strings.Index(t, ":")
|
||||
if j < 0 || strings.ContainsAny(t[:j], " \t,[") {
|
||||
break
|
||||
}
|
||||
labels[t[:j]] = n
|
||||
t = strings.TrimSpace(t[j+1:])
|
||||
}
|
||||
if t == "" {
|
||||
continue
|
||||
}
|
||||
f := strings.Fields(t)
|
||||
f[0] = strings.ToLower(f[0])
|
||||
if f[0] == ".equ" {
|
||||
if len(f) != 3 {
|
||||
return nil, fmt.Errorf("line %d: .equ NAME value", i+1)
|
||||
}
|
||||
v, err := parseNum(f[2], consts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("line %d: %v", i+1, err)
|
||||
}
|
||||
consts[f[1]] = v
|
||||
continue
|
||||
}
|
||||
if f[0] == "li" {
|
||||
n += 2
|
||||
} else {
|
||||
n++
|
||||
}
|
||||
lines = append(lines, line{i + 1, t})
|
||||
}
|
||||
|
||||
var out []byte
|
||||
emit := func(op Op, ra, rb int, imm int32) {
|
||||
out = binary.LittleEndian.AppendUint32(out, Encode(op, ra, rb, imm))
|
||||
}
|
||||
for _, l := range lines {
|
||||
name, rest, _ := strings.Cut(l.text, " ")
|
||||
name = strings.ToLower(name)
|
||||
args := splitArgs(rest)
|
||||
err := func() error {
|
||||
pc := len(out) / 4
|
||||
if name == "li" {
|
||||
if len(args) != 2 {
|
||||
return fmt.Errorf("li rd, value")
|
||||
}
|
||||
rd, err := parseReg(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v, err := parseNum(args[1], consts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
emit(LDI, rd, 0, int32(int16(uint32(v))))
|
||||
emit(LUI, rd, 0, int32(int16(uint32(v)>>16)))
|
||||
return nil
|
||||
}
|
||||
op, ok := mnemonics[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown mnemonic %q", name)
|
||||
}
|
||||
target := func(s string) (int32, error) {
|
||||
if a, ok := labels[s]; ok {
|
||||
return int32(a - (pc + 1)), nil
|
||||
}
|
||||
v, err := parseNum(s, consts)
|
||||
return int32(v), err
|
||||
}
|
||||
need := func(k int) error {
|
||||
if len(args) != k {
|
||||
return fmt.Errorf("%s takes %d operands", name, k)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
switch op {
|
||||
case NOP, YIELD, HALT, RET:
|
||||
if err := need(0); err != nil {
|
||||
return err
|
||||
}
|
||||
emit(op, 0, 0, 0)
|
||||
case LDI, LUI, ADDI:
|
||||
if err := need(2); err != nil {
|
||||
return err
|
||||
}
|
||||
ra, err := parseReg(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v, err := parseNum(args[1], consts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if v < -32768 || v > 65535 {
|
||||
return fmt.Errorf("immediate %d out of 16-bit range (use li)", v)
|
||||
}
|
||||
emit(op, ra, 0, int32(int16(v)))
|
||||
case MOV, ADD, SUB, MUL, DIV, MOD, AND, OR, XOR, SHL, SHR, SAR:
|
||||
if err := need(2); err != nil {
|
||||
return err
|
||||
}
|
||||
ra, err := parseReg(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rb, err := parseReg(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
emit(op, ra, rb, 0)
|
||||
case JMP, CALL:
|
||||
if err := need(1); err != nil {
|
||||
return err
|
||||
}
|
||||
t, err := target(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
emit(op, 0, 0, t)
|
||||
case BEQ, BNE, BLT, BGE:
|
||||
if err := need(3); err != nil {
|
||||
return err
|
||||
}
|
||||
ra, err := parseReg(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rb, err := parseReg(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t, err := target(args[2])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
emit(op, ra, rb, t)
|
||||
case PUSH, POP:
|
||||
if err := need(1); err != nil {
|
||||
return err
|
||||
}
|
||||
ra, err := parseReg(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
emit(op, ra, 0, 0)
|
||||
case LDB, LDH, LDW, STB, STH, STW:
|
||||
if err := need(2); err != nil {
|
||||
return err
|
||||
}
|
||||
ra, err := parseReg(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rb, off, err := parseMem(args[1], consts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
emit(op, ra, rb, off)
|
||||
case IN:
|
||||
if err := need(2); err != nil {
|
||||
return err
|
||||
}
|
||||
ra, err := parseReg(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := parseNum(args[1], consts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
emit(op, ra, 0, int32(int16(p)))
|
||||
case OUT:
|
||||
if err := need(2); err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := parseNum(args[0], consts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ra, err := parseReg(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
emit(op, ra, 0, int32(int16(p)))
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("line %d: %v", l.no, err)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func splitArgs(s string) []string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(s, ",")
|
||||
for i := range parts {
|
||||
parts[i] = strings.TrimSpace(parts[i])
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
func parseReg(s string) (int, error) {
|
||||
s = strings.ToLower(s)
|
||||
if s == "sp" {
|
||||
return SP, nil
|
||||
}
|
||||
if strings.HasPrefix(s, "r") {
|
||||
if n, err := strconv.Atoi(s[1:]); err == nil && n >= 0 && n < NumRegs {
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("bad register %q", s)
|
||||
}
|
||||
|
||||
func parseNum(s string, consts map[string]int64) (int64, error) {
|
||||
if v, ok := consts[s]; ok {
|
||||
return v, nil
|
||||
}
|
||||
v, err := strconv.ParseInt(s, 0, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("bad number or unknown name %q", s)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// parseMem parses "[rb]" or "[rb+off]" / "[rb-off]".
|
||||
func parseMem(s string, consts map[string]int64) (int, int32, error) {
|
||||
if !strings.HasPrefix(s, "[") || !strings.HasSuffix(s, "]") {
|
||||
return 0, 0, fmt.Errorf("bad memory operand %q", s)
|
||||
}
|
||||
s = strings.TrimSpace(s[1 : len(s)-1])
|
||||
regPart, offPart := s, ""
|
||||
if i := strings.IndexAny(s, "+-"); i >= 0 {
|
||||
regPart, offPart = strings.TrimSpace(s[:i]), strings.TrimSpace(s[i:])
|
||||
offPart = strings.ReplaceAll(offPart, " ", "")
|
||||
}
|
||||
rb, err := parseReg(regPart)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
var off int64
|
||||
if offPart != "" {
|
||||
sign := int64(1)
|
||||
if offPart[0] == '-' {
|
||||
sign = -1
|
||||
}
|
||||
v, err := parseNum(offPart[1:], consts)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
off = sign * v
|
||||
}
|
||||
if off < -32768 || off > 32767 {
|
||||
return 0, 0, fmt.Errorf("offset %d out of range", off)
|
||||
}
|
||||
return rb, int32(off), nil
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package vm
|
||||
|
||||
import "testing"
|
||||
|
||||
type testBus struct {
|
||||
in map[uint16]int32
|
||||
out map[uint16]int32
|
||||
}
|
||||
|
||||
func (b *testBus) In(p uint16) int32 { return b.in[p] }
|
||||
func (b *testBus) Out(p uint16, v int32) { b.out[p] = v }
|
||||
|
||||
func run(t *testing.T, src string, budget int) (*CPU, *testBus) {
|
||||
t.Helper()
|
||||
prog, err := Assemble(src)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, err := New(prog, 256)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := &testBus{in: map[uint16]int32{5: 42}, out: map[uint16]int32{}}
|
||||
c.Run(b, budget)
|
||||
return c, b
|
||||
}
|
||||
|
||||
func TestSumLoop(t *testing.T) {
|
||||
c, b := run(t, `
|
||||
ldi r0, 0 ; sum
|
||||
ldi r1, 1 ; i
|
||||
ldi r2, 11
|
||||
loop:
|
||||
add r0, r1
|
||||
addi r1, 1
|
||||
blt r1, r2, loop
|
||||
out 7, r0
|
||||
halt
|
||||
`, 1000)
|
||||
if c.Status != Halted || b.out[7] != 55 {
|
||||
t.Fatalf("status=%v out=%d", c.Status, b.out[7])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLiCallStackMemory(t *testing.T) {
|
||||
c, b := run(t, `
|
||||
li r0, 0x12345678
|
||||
call double
|
||||
stw r0, [sp-8] ; below current sp
|
||||
ldw r3, [sp-8]
|
||||
in r4, 5
|
||||
add r3, r4
|
||||
out 1, r3
|
||||
halt
|
||||
double:
|
||||
add r0, r0
|
||||
ret
|
||||
`, 1000)
|
||||
want := int32(0x12345678)*2 + 42
|
||||
if c.Status != Halted || b.out[1] != want {
|
||||
t.Fatalf("status=%v fault=%q out=%x want %x", c.Status, c.Fault, b.out[1], want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestYieldAndBudget(t *testing.T) {
|
||||
prog, _ := Assemble("l: addi r0, 1\n jmp l")
|
||||
c, _ := New(prog, 64)
|
||||
b := &testBus{}
|
||||
if used := c.Run(b, 100); used != 100 || c.Status != Running {
|
||||
t.Fatalf("used=%d status=%v", used, c.Status)
|
||||
}
|
||||
r0 := c.R[0]
|
||||
c.Run(b, 100)
|
||||
if c.R[0] != r0+50 {
|
||||
t.Fatalf("did not resume: %d -> %d", r0, c.R[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestFaults(t *testing.T) {
|
||||
c, _ := run(t, "ldi r0, 1\n ldi r1, 0\n div r0, r1", 100)
|
||||
if c.Status != Faulted {
|
||||
t.Fatal("expected div fault")
|
||||
}
|
||||
c, _ = run(t, "ldi r1, 300\n ldw r0, [r1]", 100)
|
||||
if c.Status != Faulted {
|
||||
t.Fatal("expected memory fault")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateRoundTrip(t *testing.T) {
|
||||
c, _ := run(t, "ldi r0, 9\n stb r0, [r1+3]\n yield\n ldi r0, 1", 100)
|
||||
d, _ := New(c.Prog, 256)
|
||||
if err := d.UnmarshalState(c.MarshalState()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if d.R != c.R || d.PC != c.PC || d.RAM[3] != 9 || d.Status != Yielded {
|
||||
t.Fatal("state mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssemblerCaseInsensitiveDirectives(t *testing.T) {
|
||||
// LI expands to two instructions; label distances must agree in any case.
|
||||
lower, err := Assemble(".equ K 5\n li r1, 0x12345\n jmp end\n nop\nend: halt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
upper, err := Assemble(".EQU K 5\n LI r1, 0x12345\n JMP end\n NOP\nend: HALT")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(lower) != string(upper) {
|
||||
t.Fatal("upper- and lower-case source assembled differently")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user