wip: macros and associated fallout

This commit is contained in:
Zellyn Hunter 2014-05-07 17:44:03 -07:00
parent a241c48657
commit fd1253fc4f
12 changed files with 249 additions and 55 deletions

View File

@ -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)

View File

@ -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

View File

@ -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.")
}

View File

@ -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
}

View File

@ -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.")
}

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
View 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
}