commit 380acbdc5fe6d904f1ffc66bccea06ad1c3ef12d Author: ipc Date: Sun May 28 17:31:59 2023 +0200 initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..395a2b4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022, 2023 ipc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2ec32a7 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +all: hypo hypoc + +hypo: $(wildcard cmd/hypo/*.go) $(wildcard cpu/*.go) + go build ./cmd/hypo + +hypoc: $(wildcard cmd/hypoc/*.go) $(wildcard asm/*.go) + go build ./cmd/hypoc + +clean: + rm -f hypo hypoc diff --git a/README.md b/README.md new file mode 100644 index 0000000..b33bb7d --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# hypo + +hypo is an interpreter for the hypo architecture. + +# hypoc + +hypoc is the hypo assembler. Samples of assembler code are +included in the sample directory. + +# Install + +To compile, type in: + +`make` + +To install the binaries: + +`make install` diff --git a/asm/asm.go b/asm/asm.go new file mode 100644 index 0000000..b5b97cf --- /dev/null +++ b/asm/asm.go @@ -0,0 +1,368 @@ +package asm + +import ( + "bufio" + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "strconv" + "strings" + "unicode" +) + +const ( + Id = iota + Label + Reg + Addr + Eof +) + +const ErrThreshold = 8 + +type Symbol struct { + Type int + Val string + Line int +} + +type Instruction struct { + Op byte + Params []int +} + +type Reader struct { + sym []Symbol + nsym int +} + +type Writer struct { + buf bytes.Buffer + pc uint32 + lab map[string]uint32 + addr map[uint32]string + f io.Writer +} + +var Hdr = []byte{0x48, 0x59, 0x50, 0x00} + +var syms = map[byte]int{ + '%': Reg, + '$': Addr, +} + +var inst = map[string]Instruction{ + "nop": {OpNop, []int{}}, + "ld": {OpLd, []int{Reg, Reg}}, + "lr": {OpLr, []int{Addr, Reg}}, + "st": {OpSt, []int{Reg, Reg}}, + "add": {OpAdd, []int{Reg, Reg, Reg}}, + "sub": {OpSub, []int{Reg, Reg, Reg}}, + "addi": {OpAddi, []int{Reg, Addr, Reg}}, + "subi": {OpSubi, []int{Reg, Addr, Reg}}, + "p": {OpP, []int{Reg}}, + "beq": {OpBeq, []int{Reg, Reg, Addr}}, + "bne": {OpBne, []int{Reg, Reg, Addr}}, + "bgt": {OpBgt, []int{Reg, Reg, Addr}}, + "blt": {OpBlt, []int{Reg, Reg, Addr}}, + "j": {OpJ, []int{Addr}}, + "jr": {OpJr, []int{Reg}}, + "call": {OpCall, []int{Addr}}, + "exit": {OpExit, []int{}}, +} + +var lc int + +func NewReader(s []Symbol) *Reader { + r := new(Reader) + r.sym = s + return r +} + +func NewWriter(w io.Writer) *Writer { + r := new(Writer) + r.lab = make(map[string]uint32) + r.addr = make(map[uint32]string) + r.f = w + return r +} + +func (s *Reader) Read() (Symbol, error) { + if s.nsym == len(s.sym) { + return Symbol{}, errors.New("bad argument count") + } + + sym := s.sym[s.nsym] + s.nsym++ + return sym, nil +} + +func (s *Reader) Expect(t int) (Symbol, error) { + sym, err := s.Read() + + if err != nil { + return sym, err + } + + switch t { + case Id: + if sym.Type != t && sym.Type != Label { + return sym, fmt.Errorf("expected identifier got '%s'", sym.Val) + } + default: + if sym.Type != t && sym.Type != Id { + tval := "register" + if t == Addr { + tval = "immediate" + } + + return sym, fmt.Errorf("expected %s got '%s'", tval, sym.Val) + } + } + + return sym, nil +} + +func (w *Writer) WriteAddr(addr uint32) { + binary.Write(&w.buf, binary.LittleEndian, addr) + w.pc += 4 +} + +func (w *Writer) WriteSymbol(sym Symbol) error { + switch sym.Type { + case Id: + if f, ok := inst[sym.Val]; ok { + w.buf.WriteByte(f.Op) + w.pc++ + } else { + w.addr[w.pc] = sym.Val + w.WriteAddr(0) + } + case Label: + _, ok := w.lab[sym.Val] + + if ok { + return fmt.Errorf("redefining label '%s'", sym.Val) + } + + w.lab[sym.Val] = w.pc + case Reg: + r, err := strconv.Atoi(sym.Val) + + if err != nil { + return fmt.Errorf("bad register '%s'", sym.Val) + } + + w.buf.WriteByte(byte(r)) + w.pc++ + case Addr: + addr, err := strconv.ParseInt(sym.Val, 16, 32) + + if err != nil { + return fmt.Errorf("bad address '%s'", sym.Val) + } + + w.WriteAddr(uint32(addr)) + } + + return nil +} + +func (w *Writer) Write() (int, error) { + b := w.buf.Bytes() + + for i, j := range w.addr { + l, ok := w.lab[j] + + if !ok { + return -1, fmt.Errorf("%s: no such label", j) + } + + b[i] = byte(l) + b[i+1] = byte(l >> 8) + b[i+2] = byte(l >> 16) + b[i+3] = byte(l >> 24) + } + + if n, err := w.f.Write(Hdr); err != nil { + return n, err + } + + return w.f.Write(b) +} + +func ReadToken(r *bufio.Reader) (string, error) { + b := new(bytes.Buffer) + + for { + c, err := r.ReadByte() + + if err != nil { + return "", err + } + + if c == '\n' { + lc++ + } + + if unicode.IsSpace(rune(c)) { + break + } + + b.WriteByte(c) + } + + return b.String(), nil +} + +func Read(r *bufio.Reader) (sym Symbol, err error) { + sym.Type = -1 + + for { + c, err := r.ReadByte() + + if err != nil { + sym.Type = Eof + break + } + + switch c { + case '\n': + lc++ + case '#': + r.ReadBytes('\n') + lc++ + return sym, nil + } + + if unicode.IsSpace(rune(c)) { + continue + } + + if !unicode.IsGraphic(rune(c)) { + return sym, fmt.Errorf("invalid character '%02x'", c) + } + + if t, ok := syms[c]; ok { + s, err := ReadToken(r) + + if err != nil { + sym.Type = Eof + } else { + sym = Symbol{t, s, lc} + } + + break + } + + if sym.Type == -1 && unicode.IsLetter(rune(c)) { + r.UnreadByte() + s, err := ReadToken(r) + + if err != nil { + sym.Type = Eof + } else { + if s[len(s)-1] == ':' { + sym = Symbol{Label, strings.TrimSuffix(s, ":"), lc} + lc++ + } else { + sym = Symbol{Id, s, lc} + } + } + + break + } + } + + return sym, nil +} + +// Gen takes the code from r and writes a machine code representation +// to w. Any errors are outputted to e. +func Gen(r io.Reader, w io.Writer, e io.Writer) (sym []Symbol, err error) { + b := bufio.NewReader(r) + errc := 0 + + werr := func(s Symbol, err error) { + if errc <= ErrThreshold { + fmt.Fprintf(e, "%d: %s\n", s.Line, err) + } + errc++ + } + + for { + s, err := Read(b) + + if err != nil { + werr(s, err) + } + + if errc > ErrThreshold { + return sym, errors.New("invalid file") + } + + if s.Type != -1 { + sym = append(sym, s) + } + + if s.Type == Eof { + break + } + } + + reader := NewReader(sym) + writer := NewWriter(w) + + for { + s, err := reader.Expect(Id) + + if s.Type == Eof { + break + } + + if err != nil { + werr(s, err) + continue + } + + if s.Type != Label { + f, ok := inst[s.Val] + + if !ok { + werr(s, fmt.Errorf("bad instruction '%s'", s.Val)) + continue + } + + writer.WriteSymbol(s) + for _, t := range f.Params { + s, err = reader.Expect(t) + + if err != nil { + werr(s, err) + } else if err = writer.WriteSymbol(s); err != nil { + werr(s, err) + } + } + + continue + } + + if err = writer.WriteSymbol(s); err != nil { + werr(s, err) + } + } + + if errc > ErrThreshold { + return sym, fmt.Errorf("%d errors (%d shown)", errc, ErrThreshold) + } else if errc > 0 { + return sym, fmt.Errorf("%d errors", errc) + } + + if _, err := writer.Write(); err != nil { + fmt.Fprintf(e, "%s\n", err) + } + + return sym, nil +} diff --git a/asm/op.go b/asm/op.go new file mode 100644 index 0000000..a8fc97f --- /dev/null +++ b/asm/op.go @@ -0,0 +1,21 @@ +package asm + +const ( + OpNop = iota + OpLd + OpLr + OpSt + OpAdd + OpSub + OpAddi + OpSubi + OpP + OpBeq + OpBne + OpBgt + OpBlt + OpJ + OpJr + OpCall + OpExit +) diff --git a/cmd/hypo/main.go b/cmd/hypo/main.go new file mode 100644 index 0000000..4dc11cc --- /dev/null +++ b/cmd/hypo/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "os" + + "github.com/rtcall/hypo/cpu" +) + +func main() { + if len(os.Args) < 2 { + fmt.Printf("usage: %s file\n", os.Args[0]) + return + } + + buf, err := os.ReadFile(os.Args[1]) + if err != nil { + fmt.Printf("error: %s\n", err) + os.Exit(1) + } + + c, err := cpu.New(buf) + if err != nil { + fmt.Printf("error: %s\n", err) + os.Exit(1) + } + + for c.State() { + if err := c.Step(); err != nil { + fmt.Printf("fatal: %s\n\n", err) + c.WriteTrace(os.Stdout) + break + } + } +} diff --git a/cmd/hypoc/main.go b/cmd/hypoc/main.go new file mode 100644 index 0000000..ed8d687 --- /dev/null +++ b/cmd/hypoc/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/rtcall/hypo/asm" +) + +func main() { + outPath := flag.String("o", "out", "output path") + flag.Parse() + + if len(flag.Args()) == 0 { + fmt.Printf("usage: %s [-o path] file\n", os.Args[0]) + os.Exit(1) + } + + inPath := flag.Arg(0) + + f, err := os.OpenFile(*outPath, os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + panic(err) + } + + defer f.Close() + f.Truncate(0) + + in, err := os.Open(inPath) + if err != nil { + f.Close() + os.Remove(*outPath) + panic(err) + } + + defer in.Close() + + _, err = asm.Gen(in, f, os.Stderr) + if err != nil { + f.Close() + os.Remove(*outPath) + fmt.Printf("%s: %s\n", inPath, err) + os.Exit(1) + } +} diff --git a/cpu/cpu.go b/cpu/cpu.go new file mode 100644 index 0000000..d65afcd --- /dev/null +++ b/cpu/cpu.go @@ -0,0 +1,351 @@ +package cpu + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + + "github.com/rtcall/hypo/asm" +) + +type Cpu struct { + reg [8]uint32 + mem [8192]byte + pc uint32 + flags uint32 + err error + buf *bytes.Reader +} + +func New(buf []byte) (c Cpu, err error) { + c.buf = bytes.NewReader(buf) + + hdr := make([]byte, len(asm.Hdr)) + if _, err = c.buf.Read(hdr); err != nil { + return c, errors.New("could not read header") + } else if !bytes.Equal(hdr, asm.Hdr) { + return c, errors.New("bad header") + } + + return c, nil +} + +func (c *Cpu) read(ins any) { + if err := binary.Read(c.buf, binary.LittleEndian, ins); err != nil { + c.err = errors.New("bad read") + } +} + +func (c *Cpu) checkReg(r byte) error { + if r > byte(len(c.reg)) { + c.err = fmt.Errorf("invalid register %02x", r) + c.flags |= 1 + } + + return c.err +} + +func (c *Cpu) readReg(r byte) uint32 { + if c.checkReg(r) == nil { + return c.reg[r] + } + + return 0 +} + +func (c *Cpu) writeReg(r byte, i uint32) { + if c.checkReg(r) == nil { + c.reg[r] = i + } +} + +func (c *Cpu) readImm(addr uint32) (uint32, error) { + if addr > uint32(len(c.mem)) { + return 0, fmt.Errorf("illegal read %08x", addr) + } + + i := c.mem[addr:4] + return uint32(i[3])<<24 | uint32(i[2])<<16 | uint32(i[1])<<8 | uint32(i[0]), nil +} + +func (c *Cpu) writeImm(addr, imm uint32) error { + if addr > uint32(len(c.mem)) { + return fmt.Errorf("illegal write %08x (at %08x)", imm, addr) + } + + c.mem[addr] = byte(imm) + c.mem[addr+1] = byte(imm >> 8) + c.mem[addr+2] = byte(imm >> 16) + c.mem[addr+3] = byte(imm >> 24) + return nil +} + +func (c *Cpu) jump(pc uint32) error { + if _, err := c.buf.Seek(int64(pc+uint32(len(asm.Hdr))), io.SeekStart); err != nil { + return err + } + + c.pc = pc + return nil +} + +var ops = map[byte]func(*Cpu) int{ + asm.OpNop: func(*Cpu) int { + return 0 + }, + asm.OpLd: func(c *Cpu) int { + var ins struct { + R1, R2 byte + } + + if c.read(&ins); c.err != nil { + return 0 + } + + i, err := c.readImm(c.readReg(ins.R2)) + c.err = err + + if err != nil { + c.writeReg(ins.R1, i) + } + + return 2 + }, + asm.OpLr: func(c *Cpu) int { + var ins struct { + I uint32 + R byte + } + + if c.read(&ins); c.err != nil { + return 0 + } + + c.writeReg(ins.R, ins.I) + return 5 + }, + asm.OpSt: func(c *Cpu) int { + var ins struct { + R1, R2 byte + } + + if c.read(&ins); c.err != nil { + return 0 + } + + c.err = c.writeImm(c.readReg(ins.R1), c.readReg(ins.R2)) + return 2 + }, + asm.OpAdd: func(c *Cpu) int { + var ins struct { + R1, R2, R3 byte + } + + if c.read(&ins); c.err != nil { + return 0 + } + + c.writeReg(ins.R3, c.readReg(ins.R1)+c.readReg(ins.R2)) + return 3 + }, + asm.OpSub: func(c *Cpu) int { + var ins struct { + R1, R2, R3 byte + } + + if c.read(&ins); c.err != nil { + return 0 + } + + c.writeReg(ins.R3, c.readReg(ins.R1)-c.readReg(ins.R2)) + return 3 + }, + asm.OpAddi: func(c *Cpu) int { + var ins struct { + R1 byte + I uint32 + R2 byte + } + + if c.read(&ins); c.err != nil { + return 0 + } + + c.writeReg(ins.R2, c.readReg(ins.R1)+ins.I) + return 6 + }, + asm.OpSubi: func(c *Cpu) int { + var ins struct { + R1 byte + I uint32 + R2 byte + } + + if c.read(&ins); c.err != nil { + return 0 + } + + c.writeReg(ins.R2, c.readReg(ins.R1)-ins.I) + return 6 + }, + asm.OpP: func(c *Cpu) int { + var R byte + + if c.read(&R); c.err != nil { + return 0 + } + + fmt.Print(string(rune(c.reg[R]))) + return 1 + }, + asm.OpBeq: func(c *Cpu) int { + var ins struct { + R1, R2 byte + I uint32 + } + + if c.read(&ins); c.err != nil { + return 0 + } + + if c.readReg(ins.R1) == c.readReg(ins.R2) { + c.jump(ins.I) + return 0 + } + + return 6 + }, + asm.OpBne: func(c *Cpu) int { + var ins struct { + R1, R2 byte + I uint32 + } + + if c.read(&ins); c.err != nil { + return 0 + } + + if c.readReg(ins.R1) != c.readReg(ins.R2) { + c.jump(ins.I) + return 0 + } + + return 6 + }, + asm.OpBgt: func(c *Cpu) int { + var ins struct { + R1, R2 byte + I uint32 + } + + if c.read(&ins); c.err != nil { + return 0 + } + + if c.readReg(ins.R1) > c.readReg(ins.R2) { + c.jump(ins.I) + return 0 + } + + return 6 + }, + asm.OpBlt: func(c *Cpu) int { + var ins struct { + R1, R2 byte + I uint32 + } + + if c.read(&ins); c.err != nil { + return 0 + } + + if c.readReg(ins.R1) < c.readReg(ins.R2) { + c.jump(ins.I) + return 0 + } + + return 6 + }, + asm.OpJ: func(c *Cpu) int { + var I uint32 + + if c.read(&I); c.err != nil { + return 0 + } + + c.jump(I) + return 0 + }, + asm.OpJr: func(c *Cpu) int { + var R byte + + if c.read(&R); c.err != nil { + return 0 + } + + c.jump(c.readReg(R)) + return 0 + }, + asm.OpCall: func(c *Cpu) int { + var I uint32 + + if c.read(&I); c.err != nil { + return 0 + } + + c.writeReg(3, c.pc+4) + c.jump(I) + return 0 + }, + asm.OpExit: func(c *Cpu) int { + c.flags |= 1 + return 0 + }, +} + +func (c *Cpu) State() bool { + return c.flags != 1 +} + +func (c *Cpu) Step() error { + var op byte + + if c.read(&op); c.err != nil { + return c.err + } + + c.pc++ + + f, ok := ops[op] + if !ok { + c.pc-- + return fmt.Errorf("invalid opcode: %02x", op) + } + + pc := f(c) + c.pc += uint32(pc) + return c.err +} + +func (c *Cpu) WriteTrace(w io.Writer) { + fmt.Fprintln(w, "register trace:") + for i, j := range c.reg { + fmt.Fprintf(w, "%02x: %08x\n", i, j) + } + + fmt.Fprintf(w, "pc: %08x\n", c.pc) + fmt.Fprintln(w, "memory trace:") + for i, j := range c.mem { + if i > 0xff { + break + } + if i > 0 && i%16 == 0 { + fmt.Fprintln(w, "") + } + fmt.Fprintf(w, "%02x ", j) + } + + fmt.Fprintln(w, "") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8d9c624 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/rtcall/hypo + +go 1.19 diff --git a/sample/l.s b/sample/l.s new file mode 100644 index 0000000..bc97a6f --- /dev/null +++ b/sample/l.s @@ -0,0 +1,23 @@ +main: + lr $61 %0 # a + lr $7b %1 # z + 1 + + loop: + p %0 + addi %0 $1 %0 + bne %0 %1 loop + + nop + lr $0a %0 # \n + p %0 + + lr $40 %0 + call func + exit + +func: + lr $0 %1 +func_loop: + addi %1 $2 %1 + blt %1 %0 func_loop + jr %3