Move source files to root dir

This commit is contained in:
lash
2023-04-12 18:09:37 +01:00
parent e340210d8f
commit df9b30287c
42 changed files with 0 additions and 0 deletions

404
asm/asm.go Normal file
View File

@@ -0,0 +1,404 @@
package asm
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"log"
"math"
"strconv"
"strings"
"github.com/alecthomas/participle/v2"
"github.com/alecthomas/participle/v2/lexer"
"git.defalsify.org/festive/vm"
)
// Asm assembles bytecode from the festive assembly mini-language.
type Asm struct {
Instructions []*Instruction `@@*`
}
// Arg holds all parsed argument elements of a single line of assembly code.
type Arg struct {
Sym *string `(@Sym Whitespace?)?`
Size *uint32 `(@Size Whitespace?)?`
Flag *uint8 `(@Size Whitespace?)?`
Selector *string `(@Sym Whitespace?)?`
Desc *string `(Quote ((@Sym | @Size) @Whitespace?)+ Quote Whitespace?)?`
}
func flush(b *bytes.Buffer, w io.Writer) (int, error) {
if w != nil {
return w.Write(b.Bytes())
}
return 0, nil
}
func parseDescType(b *bytes.Buffer, arg Arg) (int, error) {
var rn int
var err error
var selector string
if arg.Flag != nil {
selector = strconv.FormatUint(uint64(*arg.Flag), 10)
} else if arg.Selector != nil {
selector = *arg.Selector
}
var size string
if arg.Size != nil {
size = strconv.FormatUint(uint64(*arg.Size), 10)
n, err := writeSym(b, size)
rn += n
if err != nil {
return rn, err
}
}
if arg.Sym != nil {
n, err := writeSym(b, *arg.Sym)
rn += n
if err != nil {
return rn, err
}
}
if selector != "" {
n, err := writeSym(b, *arg.Selector)
rn += n
if err != nil {
return rn, err
}
}
n, err := writeSym(b, *arg.Desc)
rn += n
if err != nil {
return rn, err
}
return rn, nil
}
func parseTwoSym(b *bytes.Buffer, arg Arg) (int, error) {
var rn int
var selector string
var sym string
if arg.Size != nil {
selector = strconv.FormatUint(uint64(*arg.Size), 10)
sym = *arg.Selector
} else if arg.Selector != nil {
sym = *arg.Sym
selector = *arg.Selector
}
n, err := writeSym(b, selector)
rn += n
if err != nil {
return rn, err
}
n, err = writeSym(b, sym)
rn += n
if err != nil {
return rn, err
}
return rn, nil
}
func parseSig(b *bytes.Buffer, arg Arg) (int, error) {
var rn int
n, err := writeSym(b, *arg.Sym)
rn += n
if err != nil {
return rn, err
}
n, err = writeSize(b, *arg.Size)
rn += n
if err != nil {
return rn, err
}
n, err = b.Write([]byte{uint8(*arg.Flag)})
rn += n
if err != nil {
return rn, err
}
return rn, nil
}
func parseSized(b *bytes.Buffer, arg Arg) (int, error) {
var rn int
n, err := writeSym(b, *arg.Sym)
rn += n
if err != nil {
return rn, err
}
n, err = writeSize(b, *arg.Size)
rn += n
if err != nil {
return rn, err
}
return rn, nil
}
func parseOne(op vm.Opcode, instruction *Instruction, w io.Writer) (int, error) {
a := instruction.OpArg
var n_buf int
var n_out int
b := bytes.NewBuffer(nil)
n, err := writeOpcode(b, op)
n_buf += n
if err != nil {
return n_out, err
}
// Catch MOUT
if a.Desc != nil {
n, err := parseDescType(b, a)
n_buf += n
if err != nil {
return n_out, err
}
return flush(b, w)
}
// Catch
if a.Selector != nil {
log.Printf("entering twosym for %v", op)
n, err := parseTwoSym(b, a)
n_buf += n
if err != nil {
return n_out, err
}
return flush(b, w)
}
// Catch CATCH, LOAD
if a.Size != nil {
if a.Flag != nil {
n, err := parseSig(b, a)
n_buf += n
if err != nil {
return n_out, err
}
} else {
n, err := parseSized(b, a)
n_buf += n
if err != nil {
return n_out, err
}
}
return flush(b, w)
}
// Catch HALT
if a.Sym == nil {
return flush(b, w)
}
n, err = writeSym(b, *a.Sym)
n_buf += n
return flush(b, w)
}
// String implements the String interface.
func (a Arg) String() string {
s := "[Arg]"
if a.Sym != nil {
s += " Sym: " + *a.Sym
}
if a.Size != nil {
s += fmt.Sprintf(" Size: %v", *a.Size)
}
if a.Flag != nil {
s += fmt.Sprintf(" Flag: %v", *a.Flag)
}
if a.Selector != nil {
s += " Selector: " + *a.Selector
}
if a.Desc != nil {
s += " Description: " + *a.Desc
}
return fmt.Sprintf(s)
}
// Instruction represents one full line of assembly code.
type Instruction struct {
OpCode string `@Ident`
OpArg Arg `(Whitespace @@)?`
Comment string `Comment? EOL`
}
// String implement the String interface.
func (i Instruction) String() string {
return fmt.Sprintf("%s %s", i.OpCode, i.OpArg)
}
var (
asmLexer = lexer.MustSimple([]lexer.SimpleRule{
{"Comment", `(?:#)[^\n]*`},
{"Ident", `^[A-Z]+`},
{"Size", `[0-9]+`},
{"Sym", `[a-zA-Z_][a-zA-Z0-9_]*`},
{"Whitespace", `[ \t]+`},
{"EOL", `[\n\r]+`},
{"Quote", `["']`},
})
asmParser = participle.MustBuild[Asm](
participle.Lexer(asmLexer),
participle.Elide("Comment", "Whitespace"),
)
)
func numSize(n uint32) int {
v := math.Log2(float64(n))
return int((v / 8) + 1)
}
func writeOpcode(w *bytes.Buffer, op vm.Opcode) (int, error) {
bn := [2]byte{}
binary.BigEndian.PutUint16(bn[:], uint16(op))
n, err := w.Write(bn[:])
return n, err
}
func writeSym(w *bytes.Buffer, s string) (int, error) {
sz := len(s)
if sz > 255 {
return 0, fmt.Errorf("string size %v too big", sz)
}
w.Write([]byte{byte(sz)})
return w.WriteString(s)
}
func writeDisplay(w *bytes.Buffer, s string) (int, error) {
s = strings.Trim(s, "\"'")
sz := len(s)
if sz > 255 {
return 0, fmt.Errorf("string size %v too big", sz)
}
w.Write([]byte{byte(sz)})
return w.WriteString(s)
}
func writeSize(w *bytes.Buffer, n uint32) (int, error) {
bn := [4]byte{}
sz := numSize(n)
if sz > 4 {
return 0, fmt.Errorf("number size %v too big", sz)
}
w.Write([]byte{byte(sz)})
binary.BigEndian.PutUint32(bn[:], n)
c := 4-sz
return w.Write(bn[c:])
}
// Batcher handles assembly commands that generates multiple instructions, such as menu navigation commands.
type Batcher struct {
menuProcessor MenuProcessor
inMenu bool
}
// NewBatcher creates a new Batcher objcet.
func NewBatcher(mp MenuProcessor) Batcher {
return Batcher{
menuProcessor: NewMenuProcessor(),
}
}
// MenuExit generates the instructions for the batch and writes them to the given io.Writer.
func(bt *Batcher) MenuExit(w io.Writer) (int, error) {
if !bt.inMenu {
return 0, nil
}
bt.inMenu = false
b := bt.menuProcessor.ToLines()
return w.Write(b)
}
// MenuAdd adds a new menu instruction to the batcher.
func(bt *Batcher) MenuAdd(w io.Writer, code string, arg Arg) (int, error) {
bt.inMenu = true
var selector string
var sym string
if arg.Size != nil {
selector = strconv.FormatUint(uint64(*arg.Size), 10)
} else if arg.Selector != nil {
selector = *arg.Selector
}
if selector == "" {
selector = *arg.Sym
} else if arg.Sym != nil {
sym = *arg.Sym
}
log.Printf("menu processor add %v '%v' '%v' '%v'", code, selector, *arg.Desc, sym)
err := bt.menuProcessor.Add(code, selector, *arg.Desc, sym)
return 0, err
}
// Exit is a synonym for MenuExit
func(bt *Batcher) Exit(w io.Writer) (int, error) {
return bt.MenuExit(w)
}
// Parse one or more lines of assembly code, and write assembled bytecode to the provided writer.
func Parse(s string, w io.Writer) (int, error) {
rd := strings.NewReader(s)
ast, err := asmParser.Parse("file", rd)
if err != nil {
return 0, err
}
batch := Batcher{
}
var rn int
for _, v := range ast.Instructions {
log.Printf("parsing line %v: %v", v.OpCode, v.OpArg)
op, ok := vm.OpcodeIndex[v.OpCode]
if !ok {
n, err := batch.MenuAdd(w, v.OpCode, v.OpArg)
rn += n
if err != nil {
return rn, err
}
} else {
n, err := batch.MenuExit(w)
if err != nil {
return rn, err
}
rn += n
n, err = parseOne(op, v, w)
rn += n
if err != nil {
return rn, err
}
log.Printf("wrote %v bytes for %v", n, v.OpArg)
}
}
n, err := batch.Exit(w)
rn += n
if err != nil {
return rn, err
}
rn += n
return rn, err
}

249
asm/asm_test.go Normal file
View File

@@ -0,0 +1,249 @@
package asm
import (
"bytes"
"encoding/hex"
"fmt"
"log"
"testing"
"git.defalsify.org/festive/vm"
)
func TestParserInit(t *testing.T) {
var b []byte
b = vm.NewLine(b, vm.HALT, nil, nil, nil)
b = vm.NewLine(b, vm.CATCH, []string{"xyzzy"}, []byte{0x02, 0x9a}, []uint8{1})
b = vm.NewLine(b, vm.INCMP, []string{"inky", "pinky"}, nil, nil)
b = vm.NewLine(b, vm.LOAD, []string{"foo"}, []byte{42}, nil)
b = vm.NewLine(b, vm.MOUT, []string{"bar", "barbarbaz"}, nil, nil)
s, err := vm.ToString(b)
log.Printf("parsing:\n%s\n", s)
n, err := Parse(s, nil)
if err != nil {
t.Fatal(err)
}
if n != 0 {
t.Fatalf("expected 0 byte write count, got %v", n)
}
}
func TestParserSized(t *testing.T) {
var b []byte
b = vm.NewLine(b, vm.LOAD, []string{"foo"}, []byte{42}, nil)
s, err := vm.ToString(b)
log.Printf("parsing:\n%s\n", s)
r := bytes.NewBuffer(nil)
n, err := Parse(s, r)
if err != nil {
t.Fatal(err)
}
if n != 8 {
t.Fatalf("expected 8 byte write count, got %v", n)
}
rb := r.Bytes()
if !bytes.Equal(rb, []byte{0x00, vm.LOAD, 0x03, 0x66, 0x6f, 0x6f, 0x01, 0x2a}) {
t.Fatalf("expected 0x00%x012a, got %v", vm.LOAD, rb)
}
}
func TestParseDisplay(t *testing.T) {
var b []byte
b = vm.NewLine(b, vm.MOUT, []string{"foo", "baz ba zbaz"}, nil, nil)
s, err := vm.ToString(b)
log.Printf("parsing:\n%s\n", s)
r := bytes.NewBuffer(nil)
n, err := Parse(s, r)
if err != nil {
t.Fatal(err)
}
if n != 18 {
t.Fatalf("expected 18 byte write count, got %v", n)
}
rb := r.Bytes()
expect := []byte{0x00, vm.MOUT, 0x03, 0x66, 0x6f, 0x6f, 0x0b, 0x62, 0x61, 0x7a, 0x20, 0x62, 0x61, 0x20, 0x7a, 0x62, 0x61, 0x7a}
if !bytes.Equal(rb, expect) {
t.Fatalf("expected %x, got %x", expect, rb)
}
}
func TestParseDouble(t *testing.T) {
var b []byte
b = vm.NewLine(b, vm.INCMP, []string{"bar", "foo"}, nil, nil)
s, err := vm.ToString(b)
log.Printf("parsing:\n%s\n", s)
r := bytes.NewBuffer(nil)
n, err := Parse(s, r)
if err != nil {
t.Fatal(err)
}
if n != 10 {
t.Fatalf("expected 18 byte write count, got %v", n)
}
rb := r.Bytes()
expect := []byte{0x00, vm.INCMP, 0x03, 0x66, 0x6f, 0x6f, 0x03, 0x62, 0x61, 0x72}
if !bytes.Equal(rb, expect) {
t.Fatalf("expected %x, got %x", expect, rb)
}
}
func TestParseMenu(t *testing.T) {
s := `DOWN foobar 00 "inky pinky"
UP s1 "tinkywinky"
`
r := bytes.NewBuffer(nil)
n, err := Parse(s, r)
if err != nil {
t.Fatal(err)
}
log.Printf("wrote %v bytes", n)
s = `MOUT foobar 00 "inky pinky"
MOUT bazbar s1 "tinky winky"
HALT
INCMP 00 foobar
INCMP s1 bazbar
`
r_check := bytes.NewBuffer(nil)
n, err = Parse(s, r)
if err != nil {
t.Fatal(err)
}
log.Printf("wrote %v bytes", n)
if !bytes.Equal(r_check.Bytes(), r.Bytes()) {
fmt.Errorf("expected:\n\t%xgot:\n\t%x\n", r_check, r)
}
}
func TestParseSingle(t *testing.T) {
var b []byte
b = vm.NewLine(b, vm.MAP, []string{"xyzzy"}, nil, nil)
s, err := vm.ToString(b)
log.Printf("parsing:\n%s\n", s)
r := bytes.NewBuffer(nil)
n, err := Parse(s, r)
if err != nil {
t.Fatal(err)
}
if n != 8 {
t.Fatalf("expected 8 byte write count, got %v", n)
}
rb := r.Bytes()
expect := []byte{0x00, vm.MAP, 0x05, 0x78, 0x79, 0x7a, 0x7a, 0x79}
if !bytes.Equal(rb, expect) {
t.Fatalf("expected %x, got %x", expect, rb)
}
}
func TestParseSig(t *testing.T) {
b := vm.NewLine(nil, vm.CATCH, []string{"plugh"}, []byte{0x02, 0x9a}, []uint8{0x2a})
s, err := vm.ToString(b)
log.Printf("parsing:\n%s\n", s)
r := bytes.NewBuffer(nil)
n, err := Parse(s, r)
if err != nil {
t.Fatal(err)
}
if n != 12 {
t.Fatalf("expected 12 byte write count, got %v", n)
}
rb := r.Bytes()
expect_hex := "000105706c75676802029a01"
expect, err := hex.DecodeString(expect_hex)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(rb, expect) {
t.Fatalf("expected %v, got %x", expect_hex, rb)
}
b = vm.NewLine(nil, vm.CATCH, []string{"plugh"}, []byte{0x01}, []uint8{0x0})
s, err = vm.ToString(b)
log.Printf("parsing:\n%s\n", s)
r = bytes.NewBuffer(nil)
n, err = Parse(s, r)
if err != nil {
t.Fatal(err)
}
if n != 11 {
t.Fatalf("expected 11 byte write count, got %v", n)
}
rb = r.Bytes()
expect_hex = "000105706c756768010100"
expect, err = hex.DecodeString(expect_hex)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(rb, expect) {
t.Fatalf("expected %v, got %x", expect_hex, rb)
}
}
func TestParseNoarg(t *testing.T) {
var b []byte
b = vm.NewLine(b, vm.HALT, nil, nil, nil)
s, err := vm.ToString(b)
log.Printf("parsing:\n%s\n", s)
r := bytes.NewBuffer(nil)
n, err := Parse(s, r)
if err != nil {
t.Fatal(err)
}
if n != 2 {
t.Fatalf("expected 8 byte write count, got %v", n)
}
rb := r.Bytes()
expect := []byte{0x00, vm.HALT}
if !bytes.Equal(rb, expect) {
t.Fatalf("expected %x, got %x", expect, rb)
}
}
func TestParserWriteMultiple(t *testing.T) {
var b []byte
b = vm.NewLine(b, vm.HALT, nil, nil, nil)
b = vm.NewLine(b, vm.CATCH, []string{"xyzzy"}, []byte{0x02, 0x9a}, []uint8{1})
b = vm.NewLine(b, vm.INCMP, []string{"pinky", "inky"}, nil, nil)
b = vm.NewLine(b, vm.LOAD, []string{"foo"}, []byte{42}, nil)
b = vm.NewLine(b, vm.MOUT, []string{"bar", "bar barb az"}, nil, nil)
s, err := vm.ToString(b)
log.Printf("parsing:\n%s\n", s)
r := bytes.NewBuffer(nil)
n, err := Parse(s, r)
if err != nil {
t.Fatal(err)
}
log.Printf("result %x", r.Bytes())
r_expect_hex := "000700010578797a7a7902029a01000804696e6b790570696e6b79000303666f6f012a000a036261720b626172206261726220617a"
r_expect, err := hex.DecodeString(r_expect_hex)
if err != nil {
t.Fatal(err)
}
n_expect := len(r_expect)
if n != n_expect {
t.Fatalf("expected total %v bytes output, got %v", n_expect, n)
}
rb := r.Bytes()
if !bytes.Equal(rb, r_expect) {
t.Fatalf("expected result:\n\t%v, got:\n\t%x", r_expect_hex, rb)
}
_, err = vm.ParseAll(rb, nil)
if err != nil {
t.Fatal(err)
}
}

94
asm/menu.go Normal file
View File

@@ -0,0 +1,94 @@
package asm
import (
"fmt"
"git.defalsify.org/festive/vm"
)
// BatchCode defines quasi-opcodes that expand to mulitple individual vm instructions.
type BatchCode uint16
const (
_MENU_OFFSET = 256
MENU_DOWN = _MENU_OFFSET
MENU_UP = _MENU_OFFSET + 1
MENU_NEXT = _MENU_OFFSET + 2
MENU_PREVIOUS = _MENU_OFFSET + 3
)
var (
batchCode = map[string]BatchCode{
"DOWN": MENU_DOWN,
"UP": MENU_UP,
"NEXT": MENU_NEXT,
"PREVIOUS": MENU_PREVIOUS,
}
)
type menuItem struct {
code BatchCode
choice string
display string
target string
}
// MenuProcessor handles code lines with BatchCode quasi-opcodes that control menu generation.
//
// It creates vm instructions for display of menu and handling of input on either size of a vm.HALT instruction.
type MenuProcessor struct {
items []menuItem
size uint32
}
// NewMenuProcessor creates a new MenuProcessor object.
func NewMenuProcessor() MenuProcessor {
return MenuProcessor{}
}
// Add a menu batch instruction to be processed.
//
// Instructions will be rendered in the order in which they have been added.
func(mp *MenuProcessor) Add(bop string, choice string, display string, target string) error {
bopCode := batchCode[bop]
if bopCode == 0 {
return fmt.Errorf("unknown menu instruction: %v", bop)
}
if len(target) > 0 && bopCode != MENU_DOWN {
return fmt.Errorf("target is only valid for DOWN")
}
m := menuItem{
code: bopCode,
choice: choice,
display: display,
target: target,
}
mp.items = append(mp.items, m)
return nil
}
// ToLines returns the generated bytecode from the added menu batch instructions.
func (mp *MenuProcessor) ToLines() []byte {
preLines := []byte{}
postLines := []byte{}
for _, v := range mp.items {
switch v.code {
case MENU_UP:
preLines = vm.NewLine(preLines, vm.MOUT, []string{v.choice, v.display}, nil, nil)
postLines = vm.NewLine(postLines, vm.INCMP, []string{v.choice, "_"}, nil, nil)
case MENU_NEXT:
preLines = vm.NewLine(preLines, vm.MNEXT, []string{v.choice, v.display}, nil, nil)
postLines = vm.NewLine(postLines, vm.INCMP, []string{v.choice, ">"}, nil, nil)
case MENU_PREVIOUS:
preLines = vm.NewLine(preLines, vm.MPREV, []string{v.choice, v.display}, nil, nil)
postLines = vm.NewLine(postLines, vm.INCMP, []string{v.choice, "<"}, nil, nil)
default:
preLines = vm.NewLine(preLines, vm.MOUT, []string{v.choice, v.display}, nil, nil)
postLines = vm.NewLine(postLines, vm.INCMP, []string{v.choice, v.target}, nil, nil)
}
}
preLines = vm.NewLine(preLines, vm.HALT, nil, nil, nil)
return append(preLines, postLines...)
}

50
asm/menu_test.go Normal file
View File

@@ -0,0 +1,50 @@
package asm
import (
"testing"
"git.defalsify.org/festive/vm"
)
func TestMenuInterpreter(t *testing.T) {
m := NewMenuProcessor()
err := m.Add("DOWN", "0", "inky", "foo")
if err != nil {
t.Fatal(err)
}
err = m.Add("NEXT", "1", "pinky", "")
if err != nil {
t.Fatal(err)
}
err = m.Add("PREVIOUS", "2", "blinky clyde", "")
if err != nil {
t.Fatal(err)
}
err = m.Add("UP", "99", "tinky-winky", "")
if err != nil {
t.Fatal(err)
}
err = m.Add("BOGUS", "42", "lala poo", "plugh")
if err == nil {
t.Errorf("expected error on invalid menu item 'BOGUS'")
}
b := m.ToLines()
r, err := vm.ToString(b)
if err != nil {
t.Fatal(err)
}
expect := `MOUT 0 "inky"
MNEXT 1 "pinky"
MPREV 2 "blinky clyde"
MOUT 99 "tinky-winky"
HALT
INCMP 0 foo
INCMP 1 >
INCMP 2 <
INCMP 99 _
`
if r != expect {
t.Errorf("expected:\n\t%v\ngot:\n\t%v\n", expect, r)
}
}