mirror of
https://github.com/zellyn/go6502.git
synced 2024-12-27 04:29:27 +00:00
wip: macros and associated fallout
This commit is contained in:
parent
a241c48657
commit
fd1253fc4f
60
asm/asm.go
60
asm/asm.go
@ -3,31 +3,31 @@ package asm
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/zellyn/go6502/asm/context"
|
||||
"github.com/zellyn/go6502/asm/flavors"
|
||||
"github.com/zellyn/go6502/asm/inst"
|
||||
"github.com/zellyn/go6502/asm/lines"
|
||||
"github.com/zellyn/go6502/asm/macros"
|
||||
)
|
||||
|
||||
type Flavor interface {
|
||||
ParseInstr(Line lines.Line) (inst.I, error)
|
||||
DefaultOrigin() (uint16, error)
|
||||
SetWidthsOnFirstPass() bool
|
||||
context.Context
|
||||
}
|
||||
|
||||
type Assembler struct {
|
||||
Flavor Flavor
|
||||
Flavor flavors.F
|
||||
Opener lines.Opener
|
||||
Insts []*inst.I
|
||||
LastLabel string
|
||||
Macros map[string]macros.M
|
||||
}
|
||||
|
||||
func NewAssembler(flavor Flavor, opener lines.Opener) *Assembler {
|
||||
return &Assembler{Flavor: flavor, Opener: opener}
|
||||
func NewAssembler(flavor flavors.F, opener lines.Opener) *Assembler {
|
||||
return &Assembler{
|
||||
Flavor: flavor,
|
||||
Opener: opener,
|
||||
Macros: make(map[string]macros.M),
|
||||
}
|
||||
}
|
||||
|
||||
// Load loads a new assembler file, deleting any previous data.
|
||||
func (a *Assembler) Load(filename string) error {
|
||||
a.initPass()
|
||||
context := lines.Context{Filename: filename}
|
||||
ls, err := lines.NewFileLineSource(filename, context, a.Opener)
|
||||
if err != nil {
|
||||
@ -35,6 +35,7 @@ func (a *Assembler) Load(filename string) error {
|
||||
}
|
||||
lineSources := []lines.LineSource{ls}
|
||||
ifdefs := []bool{}
|
||||
macroCall := 0
|
||||
for len(lineSources) > 0 {
|
||||
line, done, err := lineSources[0].Next()
|
||||
if err != nil {
|
||||
@ -65,9 +66,22 @@ func (a *Assembler) Load(filename string) error {
|
||||
case inst.TypeUnknown:
|
||||
return in.Errorf("unknown instruction: %s", line)
|
||||
case inst.TypeMacroStart:
|
||||
if err := a.readMacro(in, lineSources[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
continue // no need to append
|
||||
return in.Errorf("macro start not (yet) implemented: %s", line)
|
||||
case inst.TypeMacroCall:
|
||||
return in.Errorf("macro call not (yet) implemented: %s", line)
|
||||
m, ok := a.Macros[in.Command]
|
||||
if !ok {
|
||||
return in.Errorf(`unknown macro: "%s"`, in.Command)
|
||||
}
|
||||
subLs, err := m.LineSource(a.Flavor, in)
|
||||
if err != nil {
|
||||
return in.Errorf(`error calling macro "%s": %v`, m.Name, err)
|
||||
}
|
||||
lineSources = append([]lines.LineSource{subLs}, lineSources...)
|
||||
continue // no need to append
|
||||
case inst.TypeIfdef:
|
||||
if len(in.Exprs) == 0 {
|
||||
panic(fmt.Sprintf("Ifdef got parsed with no expression: %s", line))
|
||||
@ -109,6 +123,28 @@ func (a *Assembler) Load(filename string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Assembler) readMacro(in inst.I, ls lines.LineSource) error {
|
||||
m := macros.M{
|
||||
Name: in.TextArg,
|
||||
Args: in.MacroArgs,
|
||||
}
|
||||
for {
|
||||
line, done, err := ls.Next()
|
||||
if err != nil {
|
||||
return in.Errorf("error while reading macro %s: %v", m.Name)
|
||||
}
|
||||
if done {
|
||||
return in.Errorf("end of file while reading macro %s", m.Name)
|
||||
}
|
||||
in2, err := a.Flavor.ParseInstr(line)
|
||||
if err == nil && in2.Type == inst.TypeMacroEnd {
|
||||
a.Macros[m.Name] = m
|
||||
return nil
|
||||
}
|
||||
m.Lines = append(m.Lines, line.Parse.Text())
|
||||
}
|
||||
}
|
||||
|
||||
// Clear out stuff that may be hanging around from the previous pass, set origin to default, etc.
|
||||
func (a *Assembler) initPass() {
|
||||
a.Flavor.SetLastLabel("") // No last label (yet)
|
||||
|
@ -10,6 +10,7 @@ TODO(zellyn): make errors return line and character position.
|
||||
TODO(zellyn): scma requires .EQ and .BS to have known values. Is this universal?
|
||||
TODO(zellyn): make lineparse have a line, rather than the reverse.
|
||||
TODO(zellyn): implement ca65 and compile ehbasic
|
||||
TODO(zellyn): implement named macro args
|
||||
|
||||
*/
|
||||
package asm
|
||||
|
@ -35,3 +35,7 @@ func (a *AS65) DefaultOrigin() (uint16, error) {
|
||||
func (a *AS65) SetWidthsOnFirstPass() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *AS65) ReplaceMacroArgs(line string, args []string, kwargs map[string]string) (string, error) {
|
||||
panic("AS65.ReplaceMacroArgs not implemented yet.")
|
||||
}
|
||||
|
@ -1 +1,15 @@
|
||||
package flavors
|
||||
|
||||
import (
|
||||
"github.com/zellyn/go6502/asm/context"
|
||||
"github.com/zellyn/go6502/asm/inst"
|
||||
"github.com/zellyn/go6502/asm/lines"
|
||||
)
|
||||
|
||||
type F interface {
|
||||
ParseInstr(Line lines.Line) (inst.I, error)
|
||||
DefaultOrigin() (uint16, error)
|
||||
SetWidthsOnFirstPass() bool
|
||||
ReplaceMacroArgs(line string, args []string, kwargs map[string]string) (string, error)
|
||||
context.Context
|
||||
}
|
||||
|
@ -36,3 +36,7 @@ func (a *Merlin) DefaultOrigin() (uint16, error) {
|
||||
func (a *Merlin) SetWidthsOnFirstPass() bool {
|
||||
panic("don't know yet")
|
||||
}
|
||||
|
||||
func (a *Merlin) ReplaceMacroArgs(line string, args []string, kwargs map[string]string) (string, error) {
|
||||
panic("Merlin.ReplaceMacroArgs not implemented yet.")
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package scma
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -20,6 +21,7 @@ const hexdigits = digits + "abcdefABCDEF"
|
||||
const labelChars = letters + digits + ".:"
|
||||
const whitespace = " \t"
|
||||
const cmdChars = letters + "."
|
||||
const macroNameChars = letters + digits + "._"
|
||||
const fileChars = letters + digits + "."
|
||||
const operatorChars = "+-*/<>="
|
||||
|
||||
@ -59,7 +61,7 @@ func New() *SCMA {
|
||||
".DO": {inst.TypeIfdef, s.parseDo},
|
||||
".ELSE": {inst.TypeIfdefElse, s.parseNoArgDir},
|
||||
".FIN": {inst.TypeIfdefEnd, s.parseNoArgDir},
|
||||
".MA": {inst.TypeMacroStart, s.parseNoArgDir},
|
||||
".MA": {inst.TypeMacroStart, s.parseMacroStart},
|
||||
".EM": {inst.TypeMacroEnd, s.parseNoArgDir},
|
||||
".US": {inst.TypeNone, s.parseNotImplemented},
|
||||
}
|
||||
@ -160,14 +162,14 @@ func (a *SCMA) parseCmd(in inst.I, lp *lines.Parse) (inst.I, error) {
|
||||
in.Type = inst.TypeOp
|
||||
return a.parseOpArgs(in, lp, summary)
|
||||
}
|
||||
return inst.I{}, in.Errorf(`not implemented: "%s": `, in.Command, in.Line)
|
||||
return inst.I{}, in.Errorf(`unknown command/instruction: "%s": `, in.Command, in.Line)
|
||||
}
|
||||
|
||||
// parseMacroCall parses a macro call. We expect to be looking at a the
|
||||
// first character of the macro name.
|
||||
func (a *SCMA) parseMacroCall(in inst.I, lp *lines.Parse) (inst.I, error) {
|
||||
in.Type = inst.TypeMacroCall
|
||||
if !lp.AcceptRun(cmdChars) {
|
||||
if !lp.AcceptRun(macroNameChars) {
|
||||
c := lp.Next()
|
||||
return inst.I{}, in.Errorf("expecting macro name, found '%c' (%d)", c, c)
|
||||
}
|
||||
@ -431,6 +433,19 @@ func (a *SCMA) parseInclude(in inst.I, lp *lines.Parse) (inst.I, error) {
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func (a *SCMA) parseMacroStart(in inst.I, lp *lines.Parse) (inst.I, error) {
|
||||
lp.IgnoreRun(whitespace)
|
||||
if !lp.AcceptRun(macroNameChars) {
|
||||
return inst.I{}, in.Errorf("Expecting valid macro name, found '%c'", lp.Next())
|
||||
}
|
||||
in.TextArg = lp.Emit()
|
||||
in.WidthKnown = true
|
||||
in.MinWidth = 0
|
||||
in.MaxWidth = 0
|
||||
in.Final = true
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func (a *SCMA) parseNoArgDir(in inst.I, lp *lines.Parse) (inst.I, error) {
|
||||
in.WidthKnown = true
|
||||
in.MinWidth = 0
|
||||
@ -541,3 +556,17 @@ func (a *SCMA) parseTerm(in inst.I, lp *lines.Parse) (*expr.E, error) {
|
||||
ex.Text = lp.Emit()
|
||||
return top, nil
|
||||
}
|
||||
|
||||
var macroArgRe = regexp.MustCompile("][0-9]+")
|
||||
|
||||
func (a *SCMA) ReplaceMacroArgs(line string, args []string, kwargs map[string]string) (string, error) {
|
||||
line = strings.Replace(line, "]#", strconv.Itoa(len(args)), -1)
|
||||
line = string(macroArgRe.ReplaceAllFunc([]byte(line), func(in []byte) []byte {
|
||||
n, _ := strconv.Atoi(string(in[1:]))
|
||||
if n > 0 && n <= len(args) {
|
||||
return []byte(args[n-1])
|
||||
}
|
||||
return []byte{}
|
||||
}))
|
||||
return line, nil
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ func TestMultiline(t *testing.T) {
|
||||
{ss, "Unknown label: wide", []string{
|
||||
"L1 LDA L2-L1",
|
||||
"L2 NOP",
|
||||
}, nil, "ad0300ea", false},
|
||||
}, nil, "ad0300ea", true},
|
||||
|
||||
// sc-asm sets instruction widths on the first pass
|
||||
{ss, "Later label: wide", []string{
|
||||
@ -46,7 +46,7 @@ func TestMultiline(t *testing.T) {
|
||||
"L2 BEQ .2",
|
||||
".1 NOP",
|
||||
".2 NOP",
|
||||
}, nil, "f000eaf001eaea", false},
|
||||
}, nil, "f000eaf001eaea", true},
|
||||
|
||||
// Includes: one level deep
|
||||
{ss, "Include A", []string{
|
||||
@ -57,7 +57,7 @@ func TestMultiline(t *testing.T) {
|
||||
"SUBFILE1": {
|
||||
" LDA #$2a",
|
||||
},
|
||||
}, "f002a92aea", false},
|
||||
}, "f002a92aea", true},
|
||||
|
||||
// Ifdefs: simple at first
|
||||
{ss, "Ifdef A", []string{
|
||||
@ -134,6 +134,63 @@ func TestMultiline(t *testing.T) {
|
||||
" .FIN",
|
||||
" NOP",
|
||||
}, nil, "a901a903ea", true},
|
||||
|
||||
// Macro, simple
|
||||
{ss, "Macro, simple", []string{
|
||||
" .MA INCD MACRO NAME",
|
||||
" INC ]1 CALL PARAMETER",
|
||||
" BNE :1 PRIVATE LABEL",
|
||||
" INC ]1+l",
|
||||
":1",
|
||||
" .EM END OF DEFINITION",
|
||||
" >INCD PTR",
|
||||
"PTR .HS 0000",
|
||||
"ZPTR .EQ $42",
|
||||
" >INCD ZPTR",
|
||||
}, nil, "", true},
|
||||
|
||||
// Macro, conditional assembly
|
||||
{ss, "Macro, conditional assembly", []string{
|
||||
" *--------------------------------",
|
||||
" * DEMONSTRATE CONDITIONAL ASSEMBLY IN",
|
||||
" *-------------------------------- MACRO",
|
||||
" .MA INCD",
|
||||
" .DO ]#=2",
|
||||
" INC ]1,]2",
|
||||
" BNE :3",
|
||||
" INC ]1+1,]2",
|
||||
":3",
|
||||
" .ELSE",
|
||||
" INC ]1",
|
||||
" BNE :3",
|
||||
" INC ]1+1",
|
||||
":3",
|
||||
" .FIN",
|
||||
" .EM",
|
||||
" *--------------------------------",
|
||||
" >INCD $12",
|
||||
" >INCD $1234",
|
||||
" >INCD $12,X",
|
||||
" >INCD $1234,X",
|
||||
}, nil, "e612d002e613ee3412d003ee3512f612d002f613fe3412d003fe3512", true},
|
||||
|
||||
// Macros, nested
|
||||
{ss, "Macros, nested", []string{
|
||||
" .MA CALL",
|
||||
" JSR ]1",
|
||||
" .DO ]#>1",
|
||||
" JSR ]2",
|
||||
" .FIN",
|
||||
" .DO ]#>2",
|
||||
" JSR ]3",
|
||||
" .FIN",
|
||||
" .EM",
|
||||
" >CALL SAM,TOM,JOE",
|
||||
" >CALL SAM,TOM",
|
||||
"SAM RTS",
|
||||
"JOE RTS",
|
||||
"TOM RTS",
|
||||
}, nil, "200f08201108201008200f08201108606060", true},
|
||||
}
|
||||
|
||||
for i, tt := range tests {
|
||||
@ -150,9 +207,11 @@ func TestMultiline(t *testing.T) {
|
||||
t.Errorf(`%d("%s"): tt.a.Load("TESTFILE") failed: %s`, i, tt.name, err)
|
||||
continue
|
||||
}
|
||||
if _, err := tt.a.Pass(true, false); err != nil {
|
||||
t.Errorf(`%d("%s"): tt.a.Pass(true, false) failed: %s`, i, tt.name, err)
|
||||
continue
|
||||
if !tt.a.Flavor.SetWidthsOnFirstPass() {
|
||||
if _, err := tt.a.Pass(true, false); err != nil {
|
||||
t.Errorf(`%d("%s"): tt.a.Pass(true, false) failed: %s`, i, tt.name, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
isFinal, err := tt.a.Pass(true, true)
|
||||
if err != nil {
|
||||
@ -177,7 +236,7 @@ func TestMultiline(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApplesoftBasic(t *testing.T) {
|
||||
if err := os.Chdir("../../../goapple2/source/applesoft"); err != nil {
|
||||
if err := os.Chdir("../../../../goapple2/source/applesoft"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var o lines.OsOpener
|
@ -4,7 +4,7 @@ import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/zellyn/go6502/asm"
|
||||
"github.com/zellyn/go6502/asm/flavors"
|
||||
"github.com/zellyn/go6502/asm/flavors/as65"
|
||||
"github.com/zellyn/go6502/asm/flavors/merlin"
|
||||
"github.com/zellyn/go6502/asm/flavors/scma"
|
||||
@ -17,10 +17,10 @@ func TestSimpleCommonFunctions(t *testing.T) {
|
||||
mm := merlin.New()
|
||||
|
||||
tests := []struct {
|
||||
a asm.Flavor // assembler flavor
|
||||
i string // input string
|
||||
p string // printed instruction, expected
|
||||
b string // bytes, expected
|
||||
a flavors.F // assembler flavor
|
||||
i string // input string
|
||||
p string // printed instruction, expected
|
||||
b string // bytes, expected
|
||||
}{
|
||||
{ss, "* Comment", "{-}", ""},
|
||||
{ss, "* Comment", "{-}", ""},
|
||||
@ -114,7 +114,7 @@ func TestSimpleCommonFunctions(t *testing.T) {
|
||||
{ss, " .DO A<$3", "{if (< A $0003)}", ""},
|
||||
{ss, " .ELSE", "{else}", ""},
|
||||
{ss, " .FIN", "{endif}", ""},
|
||||
{ss, "Label .MA", "{macro 'Label'}", ""},
|
||||
{ss, " .MA MacroName", "{macro 'MacroName'}", ""},
|
||||
{ss, " .EM", "{endm}", ""},
|
||||
{ss, " .EN", "{end}", ""},
|
||||
{ss, `>SAM AB,$12,"A B","A, B, "" C"`,
|
||||
@ -172,8 +172,8 @@ func TestSimpleErrors(t *testing.T) {
|
||||
mm := merlin.New()
|
||||
|
||||
tests := []struct {
|
||||
a asm.Flavor // assembler flavor
|
||||
i string // input string
|
||||
a flavors.F // assembler flavor
|
||||
i string // input string
|
||||
}{
|
||||
|
||||
{ss, " LDA"}, // missing arg
|
@ -135,6 +135,9 @@ func (i I) String() string {
|
||||
if i.Label != "" {
|
||||
s += fmt.Sprintf(" '%s'", i.Label)
|
||||
}
|
||||
if i.TextArg != "" {
|
||||
s += fmt.Sprintf(" '%s'", i.TextArg)
|
||||
}
|
||||
if len(i.MacroArgs) > 0 {
|
||||
ma := fmt.Sprintf("%#v", i.MacroArgs)[8:]
|
||||
s += " " + ma
|
||||
|
@ -19,27 +19,8 @@ func (o OsOpener) Open(filename string) (io.ReadCloser, error) {
|
||||
return os.Open(filename)
|
||||
}
|
||||
|
||||
type FileLineSource struct {
|
||||
context Context
|
||||
lines []string
|
||||
size int
|
||||
curr int
|
||||
}
|
||||
|
||||
func (fls *FileLineSource) Next() (line Line, done bool, err error) {
|
||||
if fls.curr >= fls.size {
|
||||
return Line{}, true, nil
|
||||
}
|
||||
fls.curr++
|
||||
return NewLine(fls.lines[fls.curr-1], fls.curr, &fls.context), false, nil
|
||||
}
|
||||
|
||||
func (fls FileLineSource) Context() Context {
|
||||
return fls.context
|
||||
}
|
||||
|
||||
func NewFileLineSource(filename string, context Context, opener Opener) (LineSource, error) {
|
||||
fls := &FileLineSource{context: context}
|
||||
var ls []string
|
||||
file, err := opener.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -47,15 +28,14 @@ func NewFileLineSource(filename string, context Context, opener Opener) (LineSou
|
||||
defer file.Close()
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
fls.lines = append(fls.lines, scanner.Text())
|
||||
ls = append(ls, scanner.Text())
|
||||
}
|
||||
|
||||
if err = scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fls.size = len(fls.lines)
|
||||
return fls, nil
|
||||
return NewSimpleLineSource(context, ls), nil
|
||||
}
|
||||
|
||||
type TestOpener map[string]string
|
||||
|
@ -3,8 +3,9 @@ package lines
|
||||
import "fmt"
|
||||
|
||||
type Context struct {
|
||||
Filename string // Pointer to the filename
|
||||
Parent *Line // Pointer to parent line (eg. include, macro)
|
||||
Filename string // Pointer to the filename
|
||||
Parent *Line // Pointer to parent line (eg. include, macro)
|
||||
MacroCall int
|
||||
}
|
||||
|
||||
type Line struct {
|
||||
@ -34,6 +35,16 @@ func NewSimple(s string) Line {
|
||||
return NewLine(s, 0, &Context{Filename: testFilename})
|
||||
}
|
||||
|
||||
func (c Context) GetMacroCall() int {
|
||||
if c.MacroCall > 0 {
|
||||
return c.MacroCall
|
||||
}
|
||||
if c.Parent == nil || c.Parent.Context == nil {
|
||||
return 0
|
||||
}
|
||||
return c.Parent.Context.GetMacroCall()
|
||||
}
|
||||
|
||||
func (l Line) Text() string {
|
||||
return l.Parse.Text()
|
||||
}
|
||||
@ -57,3 +68,29 @@ func (l Line) Sprintf(format string, a ...interface{}) string {
|
||||
}
|
||||
return fmt.Sprintf(fmt.Sprintf("%s:%d: %s", filename, l.LineNo, format), a...)
|
||||
}
|
||||
|
||||
type SimpleLineSource struct {
|
||||
context Context
|
||||
lines []string
|
||||
size int
|
||||
curr int
|
||||
}
|
||||
|
||||
func (sls *SimpleLineSource) Next() (line Line, done bool, err error) {
|
||||
if sls.curr >= sls.size {
|
||||
return Line{}, true, nil
|
||||
}
|
||||
sls.curr++
|
||||
return NewLine(sls.lines[sls.curr-1], sls.curr, &sls.context), false, nil
|
||||
}
|
||||
|
||||
func (sls SimpleLineSource) Context() Context {
|
||||
return sls.context
|
||||
}
|
||||
func NewSimpleLineSource(context Context, ls []string) LineSource {
|
||||
return &SimpleLineSource{
|
||||
context: context,
|
||||
lines: ls,
|
||||
size: len(ls),
|
||||
}
|
||||
}
|
||||
|
27
asm/macros/macros.go
Normal file
27
asm/macros/macros.go
Normal file
@ -0,0 +1,27 @@
|
||||
package macros
|
||||
|
||||
import (
|
||||
"github.com/zellyn/go6502/asm/flavors"
|
||||
"github.com/zellyn/go6502/asm/inst"
|
||||
"github.com/zellyn/go6502/asm/lines"
|
||||
)
|
||||
|
||||
type M struct {
|
||||
Name string
|
||||
Args []string
|
||||
Lines []string
|
||||
}
|
||||
|
||||
func (m M) LineSource(flavor flavors.F, in inst.I) (lines.LineSource, error) {
|
||||
var ls []string
|
||||
context := lines.Context{Filename: "macro:" + m.Name, Parent: in.Line}
|
||||
for _, line := range m.Lines {
|
||||
// TODO(zellyn): implement named macro args
|
||||
subbed, err := flavor.ReplaceMacroArgs(line, in.MacroArgs, nil)
|
||||
if err != nil {
|
||||
return nil, in.Errorf("error in macro %s: %v", m.Name, err)
|
||||
}
|
||||
ls = append(ls, subbed)
|
||||
}
|
||||
return lines.NewSimpleLineSource(context, ls), nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user