Initial commit

This commit is contained in:
sloum 2023-12-29 23:01:38 -08:00
commit 40cd1d855a
10 changed files with 942 additions and 0 deletions

90
design.md Normal file
View File

@ -0,0 +1,90 @@
# Op Layout
The first three bits are the category, the next four are the op, the last one is whether or not the op should operate on two bytes (1 for 2byte/16bit, 0 for 1byte/8bit)
```
[ 000 0000 0 ]
| \/ |
Cat Op 2byte
```
1. memory - 000
1. set - 000
2. get - 001
3. sei - 010 - set indirect (use a pointer stores elswhere)
4. gei - 011
5. .
6. .
7. .
2. stack - 001
1. psh - 000
2. pop - 001
3. ovr - 010
4. swp - 011
5. rot - 100
6. dup - 101
7. .
3. math - 010
1. add - 000
2. sub - 001
3. mul - 010
4. div - 011
5. inc - 100
6. dec - 101
7. .
4. bits - 011
1. and - 000
2. bor - 001 (bitwise or)
3. xor - 010
4. not - 011
5. shr - 100
6. shl - 101
7. eql - 111
5. jump - 100
1. jmp - 000
2. sub - 001
3. jcd - 010
4. ret - 011
5. .
6. .
7. .
7. sys - 101
1. gch - 000
2. gst - 001
3. pch - 010
4. pst - 011
5. hlt - 100
6. .
7. .
8. .
9. .
10. .
11. .
12. .
13. .
14. .
15. .
8. dev - 111
1. .
2. .
3. .
4. .
5. .
6. .
7. .
8. .
9. .
10. .
11. .
12. .
13. .
14. .
15. .
## Memory Layout
```
var memory = [MAX_uint16]byte{}
```
## Stack

7
go.mod Normal file
View File

@ -0,0 +1,7 @@
module tildegit.org/sloum/bird-vm
go 1.21.5
require golang.org/x/term v0.15.0
require golang.org/x/sys v0.15.0 // indirect

4
go.sum Normal file
View File

@ -0,0 +1,4 @@
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=

66
helpers.go Normal file
View File

@ -0,0 +1,66 @@
package main
import (
"fmt"
"os"
)
const MaxUint16 uint16 = uint16(0xFFFF)
const MaxByte byte = byte(0xFF)
func IsLongOp(b byte) bool {
if b>>7 == 1 {
return true
}
return false
}
func IsRetStackOp(b byte) bool {
if b&0x40 == 0x40 {
return true
}
return false
}
func PopAddress() uint16 {
return To16(vm.Data.Pop2())
}
func ReadAddress(a uint16) uint16 {
return To16(vm.Mem.Read2(a))
}
func IsTrue(b byte) bool {
if b == 0 {
return false
}
return true
}
func To16(high, low byte) uint16 {
return (uint16(high)<<8)|uint16(low)
}
func To8s(n uint16) (byte, byte) {
return byte(n>>8), byte(n & uint16(0xFF))
}
func LoadMemory(b []byte) {
for i := range b {
vm.Mem[i] = b[i]
}
vm.HP = uint16(len(b))
vm.MemOffset = uint16(len(b))
}
func DumpVm() {
fmt.Fprintf(
os.Stderr,
"\n<%03d>Data : %v\n<%03d>Return: %v\nPC: %d\nHP: %d\n",
vm.Data.pointer,
vm.Data.data[:vm.Data.pointer],
vm.Return.pointer,
vm.Return.data[:vm.Return.pointer],
vm.PC,
vm.HP-vm.MemOffset)
}

74
main.go Normal file
View File

@ -0,0 +1,74 @@
package main
import (
"flag"
"os"
)
// This has room for up to 255 ops, but just uses these right now.
// Once a screen device is enable there will likely be more. Screen
// and sound, really.
var ops = []func(bool, *Stack){
opNop, opSet, opGet, opSei, opGei, opAlo, opNop,
opPsh, opPop, opOvr, opSwp, opRot, opDup, opSsw, // <-13
opAdd, opSub, opMul, opDiv, opInc, opDec, opNop,
opAnd, opBor, opXor, opShr, opShl, opNop, opNop, // <-27
opJmp, opCal, opJcd, opRet, opNop, opNop, opNop,
opGch, opGst, opPch, opPst, opHlt, opPhx, opNop, //<-41
opGrt, opLst, opGte, opLte, opEql, opNeq, opNop,
opNop, opNop, opNop, opNop, opNop, opNop, opNop,
opNop, opNop, opNop, opNop, opNop, opNop, opNop,
opNop, opNop, opNop, opNop, opNop, opNop, opNop,
opNop, opNop, opNop, opNop, opNop, opNop, opNop,
opNop, opNop, opNop, opNop, opNop, opNop, opNop,
opNop, opNop, opNop, opNop, opNop, opNop, opNop,
opNop, opNop, opNop, opNop, opNop, opNop, opNop,
opNop, opNop, opNop, opNop, opNop, opNop, opNop,
opNop, opNop, opNop, opNop, opNop, opNop, opNop,
opNop, opNop, opNop, opNop, opNop, opNop, opNop,
opNop, opNop, opNop, opNop, opNop, opNop, opNop,
opNop, opNop,
opNop, opSet, opGet, opSei, opGei, opAlo, opNop,
opPsh, opPop, opOvr, opSwp, opRot, opDup, opNop,
opAdd, opSub, opMul, opDiv, opInc, opDec, opNop,
opAnd, opBor, opXor, opShr, opShl, opNop, opNop,
opJmp, opCal, opJcd, opRet, opNop, opNop, opNop,
opNop, opNop, opNop, opNop, opNop, opPhx, opNop,
opGrt, opLst, opGte, opLte, opEql, opNeq, opNop,
}
var vm Vm = Vm{0, 0, 0, [65535]byte{0}, Stack{0, [255]byte{0}}, Stack{0, [255]byte{0}}}
var dumpVmData bool
var halt = -1
func main() {
flag.BoolVar(&dumpVmData, "dump", false, "Dump stacks on exit")
flag.Parse()
test := []byte{
0x87, 0x02, 0x08, // Push 0x02 and 0x08 to the stack
0x0e, 0x28, // add them together and print the hex value
0x07, 0x02, 0x27, // Then exit with 0x02 as the exit code
}
LoadMemory(test)
// Loop
for ;vm.PC < MaxUint16 && halt < 0;vm.PC++{
o := vm.Mem.Read(vm.PC)
if IsRetStackOp(o) {
ops[o](IsLongOp(o), &(vm.Return))
} else {
ops[o](IsLongOp(o), &(vm.Data))
}
}
if halt < 0 {
halt = 9
}
if dumpVmData {
DumpVm()
}
os.Exit(halt)
}

99
main.go.bak Normal file
View File

@ -0,0 +1,99 @@
package main
// https://www.andreinc.net/2021/12/01/writing-a-simple-vm-in-less-than-125-lines-of-c
type Memory [65536]uint16
func (m *Memory) Read(address uint16) uint16 {
return m[address]
}
func (m *Memory) Write(address, value uint16) {
m[address] = value
}
const (
FlagFp uint16 = 1 << 0
FlagFz uint16 = 1 << 1
FlagFn uint16 = 1 << 2
R0 uint16 = 0
R2 = iota
R3
R4
R5
R6
R7
RPC
RCND
RCNT
)
var (
PcStart uint16 = 0x3000
Reg [RCNT]uint16
OpMap [16]func(uint16){opBr,opAdd,opLd,opSt,opJsr,opAnd,opLdr,opStr,opRti,opNot,opLdi,opSti,opJump,opNop,opLea,opTrap}
)
func Uf(r uint16) {
if (Reg[r] == 0) {
reg[RCND] = FlagFz
} else if (Reg[r]>>15 == 1) {
reg[RCND] = FlagFn
} else {
reg[RCND] = FlagFp
}
}
func FIMM(n uint16) uint16 {
return (n>>5)&1
}
func DR(n uint16) uint16 {
return (n>>9)&0x7
}
func SR1(n uint16) uint16 {
return (i>>6)&0x7
}
func SR2(n uint16) uint16 {
return n&0x7
}
func IMM(n uint16) uint16 {
return n&0x1F
}
func SextImm(n uint16) uint16 {
return Sext(IMM(n), 5)
}
func Sext(n uint16, b int) uint16 {
if ((n>>(b-1))&1 == 1) { // if the bth bit of n is 1 (number is negative):
return (n|(0xFFFF << b)) // fill up with 1s the remaining bits (15)
} else { // else:
return n // return the number as is
}
}
func opAdd(i uint16) {
reg[DR(i)] = reg[SR1(i)]
if FIMM(i) == 1 { // If the 5th bit is 1:
reg[DR[i]] += SextImm(i) // we sign extend IMM5 and we add it to SR1 (add2)
} else { // else:
reg[DR[i]] += reg[SR2(i)] // add the value of SR2 to SR1 (add1)
}
Uf(DR(i)); // !! Update the conditional register depending on the value of DR1
}
func opAnd(i uint16) {
if FIMM(i) == 1 { // If the 5th bit is 1
reg[DR(i)] = reg[SR1(i)] & SextImm(i) // We sign extend IMM5 and we & it to SR1 (and1)
} else {
reg[DR(i)] = reg[SR1(i)] & reg[SR2(i)] // Otherwise we & the value of SR2 to SR1
}
Uf(DR(i)); // Update the conditional register
}

386
ops.go Normal file
View File

@ -0,0 +1,386 @@
package main
import (
"fmt"
"os"
"golang.org/x/term"
)
func opNop(longOp bool, s *Stack){}
// ( [val] val high low -- )
func opSet(longOp bool, s *Stack) {
addr := PopAddress()
if longOp {
h, l := vm.Data.Pop2()
vm.Mem.Write2(addr, h, l)
return
}
vm.Mem.Write(addr, vm.Data.Pop())
}
// ( high low -- val [val] )
func opGet(longOp bool, s *Stack) {
addr := PopAddress()
if longOp {
vm.Data.Push2(vm.Mem.Read2(addr))
return
}
vm.Data.Push(vm.Mem.Read(addr))
}
// Set indirect
// ( [val] val high low -- )
func opSei(longOp bool, s *Stack) {
ptr := PopAddress()
vm.Data.Push2(vm.Mem.Read2(ptr))
opSet(longOp, s)
}
// ( high low -- val [val] )
func opGei(longOp bool, s *Stack) {
ptr := PopAddress()
vm.Data.Push2(vm.Mem.Read2(ptr))
opGet(longOp, s)
}
// ( -- [val] val )
func opPsh(longOp bool, s *Stack) {
vm.PC++
if longOp {
s.Push2(vm.Mem.Read2(vm.PC))
vm.PC++
return
}
s.Push(vm.Mem.Read(vm.PC))
}
// ( v1 -- )
// ( v1 v2 -- )
func opPop(longOp bool, s *Stack) {
s.Pop()
if longOp {
s.Pop()
}
}
// ( v1 v2 -- v1 v2 v1 )
// ( v1 v2 v3 v4 -- v1 v2 v3 v4 v1 v2 )
func opOvr(longOp bool, s *Stack) {
hTop, lTop := vm.Data.Pop2()
if longOp {
hOvr, lOvr := vm.Data.Pop2()
vm.Data.Push2(hOvr, lOvr)
vm.Data.Push2(hTop, lTop)
vm.Data.Push2(hOvr, lOvr)
return
}
vm.Data.Push2(hTop, lTop)
vm.Data.Push(hTop)
}
// ( v1 v2 -- v2 v1 )
// ( v1 v2 v3 v4 -- v3 v4 v1 v2 )
func opSwp(longOp bool, s *Stack) {
hTop, lTop := vm.Data.Pop2()
if longOp {
hSwp, lSwp := vm.Data.Pop2()
vm.Data.Push2(hTop, lTop)
vm.Data.Push2(hSwp, lSwp)
return
}
vm.Data.Push2(lTop, hTop)
}
func opRot(longOp bool, s *Stack) {
// TODO
}
// ( v -- v v )
// ( v1 v2 -- v1 v2 v1 v2 )
func opDup(longOp bool, s *Stack) {
if longOp {
vm.Data.Push2(vm.Data.Peek2())
return
}
vm.Data.Push(vm.Data.Peek())
}
// Swaps data from one stack to another
// In return mode goes from ret->data
// normally goes data->ret
func opSsw(longOp bool, s *Stack) {
var from, to *Stack
if s == &(vm.Data) {
from = s
to = &(vm.Return)
} else {
to = s
from = &(vm.Return)
}
if longOp {
to.PushLong(from.PopLong())
} else {
to.Push(from.Pop())
}
}
// ( v1 v2 -- v1+v2 )
// ( v1 v2 v3 v4 -- v1v2+v3v4 )
func opAdd(longOp bool, s *Stack) {
if longOp {
vm.Data.PushLong(PopAddress()+PopAddress())
return
}
vm.Data.Push(vm.Data.Pop()+vm.Data.Pop())
}
// ( v1 v2 -- v1-v2 )
// ( v1 v2 v3 v4 -- v1v2-v3v4 )
func opSub(longOp bool, s *Stack) {
if longOp {
l := PopAddress()
h := PopAddress()
vm.Data.PushLong(h-l)
return
}
h, l := vm.Data.Pop2()
vm.Data.Push(h-l)
}
// ( v1 v2 == v1*v2 )
// ( v1 v2 v3 v4 == v1v2*v3v4 )
func opMul(longOp bool, s *Stack) {
if longOp {
vm.Data.PushLong(PopAddress()*PopAddress())
return
}
vm.Data.Push(vm.Data.Pop()*vm.Data.Pop())
}
// ( v1 v2 -- v1/v2 )
// ( v1 v2 v3 v4 -- v1v2/v3v4 )
func opDiv(longOp bool, s *Stack) {
if longOp {
h := PopAddress()
l := PopAddress()
if l == 0 {
vm.Data.Push(byte(0))
return
}
vm.Data.PushLong(h/l)
return
}
h := vm.Data.Pop()
l := vm.Data.Pop()
if l == 0 {
vm.Data.Push(byte(0))
return
}
vm.Data.Push(h/l)
}
func opInc(longOp bool, s *Stack){
addr := PopAddress()
if longOp {
v := vm.Mem.ReadLong(addr)
if v == MaxUint16 {
v = 0
} else {
v++
}
vm.Mem.WriteLong(addr, v)
return
}
v := vm.Mem.Read(addr)
if v == MaxByte {
v = 0
} else {
v++
}
vm.Mem.Write(addr, v)
}
func opDec(longOp bool, s *Stack){
addr := PopAddress()
if longOp {
v := vm.Mem.ReadLong(addr)
if v == 0 {
v = MaxUint16
} else {
v--
}
vm.Mem.WriteLong(addr, v)
return
}
v := vm.Mem.Read(addr)
if v == 0 {
v = MaxByte
} else {
v--
}
vm.Mem.Write(addr, v)
}
func opAnd(longOp bool, s *Stack){
if longOp {
l16, h16 := PopAddress(), PopAddress()
h16 = h16&l16
vm.Data.PushLong(h16)
return
}
n1, n2 := vm.Data.Pop2()
vm.Data.Push(n1&n2)
}
func opBor(longOp bool, s *Stack){
if longOp {
l16, h16 := PopAddress(), PopAddress()
h16 = h16|l16
vm.Data.PushLong(h16)
return
}
n1, n2 := vm.Data.Pop2()
vm.Data.Push(n1|n2)
}
func opXor(longOp bool, s *Stack){
if longOp {
l16, h16 := PopAddress(), PopAddress()
h16 = h16^l16
vm.Data.PushLong(h16)
return
}
n1, n2 := vm.Data.Pop2()
vm.Data.Push(n1^n2)
}
func opShr(longOp bool, s *Stack){
if longOp {
l16, h16 := PopAddress(), PopAddress()
h16 = h16>>l16
vm.Data.PushLong(h16)
return
}
n1, n2 := vm.Data.Pop2()
vm.Data.Push(n1>>n2)
}
func opShl(longOp bool, s *Stack){
if longOp {
l16, h16 := PopAddress(), PopAddress()
h16 = h16>>l16
vm.Data.PushLong(h16)
return
}
n1, n2 := vm.Data.Pop2()
vm.Data.Push(n1>>n2)
}
func opJmp(longOp bool, s *Stack){
if longOp {
vm.PC = vm.Data.PopLong()
} else {
vm.PC = uint16(vm.Data.Pop())
}
}
func opCal(longOp bool, s *Stack){
// No short version
vm.Return.PushLong(vm.PC)
opJmp(longOp, s)
}
func opJcd(longOp bool, s *Stack){
if longOp {
if vm.Data.PopLong() == 0 {
vm.Data.PopLong()
} else {
opJmp(true, s)
}
} else {
if vm.Data.Pop() == 0 {
vm.Data.PopLong()
} else {
opJmp(false, s)
}
}
}
func opRet(longOp bool, s *Stack){
// No short version
vm.Data.PushLong(vm.Return.PopLong())
opJmp(true, s)
}
// ( -- ch )
func opGch(longOp bool, s *Stack) {
oldTerm, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
vm.Data.Push(byte(0))
return
}
defer term.Restore(int(os.Stdin.Fd()), oldTerm)
var count = 1
if longOp {
count = 2
}
b := make([]byte, count)
os.Stdin.Read(b)
if longOp {
vm.Data.Push2(b[0], b[1])
return
}
vm.Data.Push(b[0])
}
// Get string placeholder
// Maybe use scanline instead?
func opGst(longOp bool, s *Stack){
b := make([]byte, 1)
_, err := os.Stdin.Read(b)
if err != nil {
vm.Data.Push(0)
return
}
vm.Data.Push(b[0])
}
func opPch(longOp bool, s *Stack){}
func opPst(longOp bool, s *Stack){}
// Halt execution
// ( exitCode -- )
func opHlt(longOp bool, s *Stack){
halt = int(vm.Data.Pop())
}
// Print hex
func opPhx(longOp bool, s *Stack){
if longOp {
fmt.Printf("%04x", PopAddress())
return
}
fmt.Printf("%02x", vm.Data.Pop())
}
func opGrt(longOp bool, s *Stack){}
func opLst(longOp bool, s *Stack){}
func opGte(longOp bool, s *Stack){}
func opLte(longOp bool, s *Stack){}
func opEql(longOp bool, s *Stack){}
func opNeq(longOp bool, s *Stack){}
// ( [count] count -- addrHigh addrLow )
func opAlo(longOp bool, s *Stack){
if longOp {
count := PopAddress()
vm.Data.PushLong(vm.HP)
vm.HP += count
return
}
count := uint16(vm.Data.Pop())
vm.Data.PushLong(vm.HP)
vm.HP += count
}

83
scratchpad.txt Normal file
View File

@ -0,0 +1,83 @@
/*
0000 0000 set
0000 0001 get
0000 0010 sei
0000 0011 gei
0000 0100 nop
0000 0101 nop
0000 0111 nop
0000 1000 psh
0000 1001 pop
0000 1010 ovr
0000 1011 swp
0000 1100 rot
0000 1101 dup
0000 1111 nop
0001 0000 add
0001 0001 sub
0001 0010 mul
0001 0011 div
0001 0100 inc
0001 0101 dec
0001 0111 nop
0001 1000 and
0001 1001 bor
0001 1010 xor
0001 1011 not
0001 1100 shr
0001 1101 shl
0001 1111 eql
0010 0000 jmp
0010 0001 cal
0010 0010 jcd
0010 0011 ret
0010 0100 nop
0010 0101 nop
0010 0111 nop
0010 1000 gch
0010 1001 gst
0010 1010 pch
0010 1011 pst
0010 1100 hlt
0010 1101 nop
0010 1111 nop
0011 0000 grt
0011 0001 lst
0011 0010 gte
0011 0011 lte
0011 0100 eql
0011 0101 neq
...
1000 0000 set2
1000 0001 get2
1000 0010 sei2
1000 0011 gei2
1000 1000 psh2
1000 1001 pop2
1000 1010 ovr2
1000 1011 swp2
1000 1100 rot2
1000 1101 dup2
1001 0000 add2
1001 0001 sub2
1001 0010 mul2
1001 0011 div2
1001 0100 inc2
1001 0101 dec2
1001 1000 and2
1001 1001 bor2
1001 1010 xor2
1001 1011 not2
1001 1100 shr2
1001 1101 shl2
1001 1111 eql2
1010 0000 jmp2
1010 0001 cal2
1010 0010 jcd2
1010 0011 ret2
1010 0100 nop
1010 0101 nop
1010 0111 nop
...
*/

2
stack.go Normal file
View File

@ -0,0 +1,2 @@
package main

131
types.go Normal file
View File

@ -0,0 +1,131 @@
package main
type Memory [65535]byte
func (m *Memory) Read(address uint16) byte {
return m[address]
}
func (m *Memory) ReadLong(address uint16) uint16 {
return To16(m[address], m[address+1])
}
func (m *Memory) Read2(address uint16) (byte, byte) {
return m[address], m[address+1] // May error if called at max index for the low
}
func (m *Memory) Write(address uint16, value byte) {
m[address] = value
}
func (m *Memory) WriteLong(address uint16, value uint16) {
h, l := To8s(value)
m.Write(address, h)
address++
if address >= MaxUint16-1 {
address = 0
}
m.Write(address, l)
}
func (m *Memory) Write2(address uint16, high, low byte) {
m.Write(address, high)
address++
if address >= MaxUint16-1 {
address = 0
}
m.Write(address, low)
}
type Stack struct {
pointer byte
data [255]byte
}
func (s Stack) Peek() byte {
if s.pointer == 0 {
return s.data[len(s.data)-1]
}
return s.data[s.pointer-1]
}
func (s Stack) Peek2() (byte, byte) {
low := s.Peek()
var high byte
if s.pointer == 1 {
high = s.data[len(s.data)-1]
} else if s.pointer == 0 {
high = s.data[len(s.data)-2]
} else {
high = s.data[s.pointer-2]
}
return high, low
}
func (s *Stack) Push(b byte) {
s.data[s.pointer] = b
if s.pointer == MaxByte-1 {
s.pointer = 0
} else {
s.pointer++
}
}
func (s *Stack) Push2(h, l byte) {
s.Push(h)
s.Push(l)
}
func (s *Stack) PushLong(n uint16) {
h, l := To8s(n)
s.Push2(h, l)
}
func (s *Stack) Pop() byte {
if s.pointer == 0 {
s.pointer = 254
} else {
s.pointer--
}
return s.data[s.pointer]
}
func (s *Stack) Pop2() (byte, byte) {
low := s.Pop()
return s.Pop(), low
}
func (s *Stack) PopLong() (uint16) {
h, l := s.Pop2()
return To16(h,l)
}
/* The idea of the heap pointer is that after all
* data and ops are written to the memory space the
* heap space will start to get used up. However,
* when a subroutine is called with `cal`, the current
* HP will be added to the return stack along with the
* return address. When `ret` is called, it will change
* the PC to the return address and HP to its previous
* position. This allows for temporary variable usage
* within the heap space and clear things up as needed.
* The only issue is how to do something like dynamically
* read in a string to a memory space. Since the length is
* not known in advance, the space cannot be reserved in
* advance. As a result, there is a case to be made for
* not doing this at all, and essentially just fillling
* up space without freeing it, especially since many of
* the references in question will have their sizes known
* prior to runtime. Doing all sized references in a "data"
* section makes sense. That would leave the heap section
* for large dynamic runtime things...
*/
type Vm struct {
PC uint16 // Current Position
HP uint16 // Current start of heap
MemOffset uint16 // Start of user memory
Mem Memory
Data Stack
Return Stack
}