diff --git a/asm/asm.go b/asm/asm.go new file mode 100644 index 0000000..61d477b --- /dev/null +++ b/asm/asm.go @@ -0,0 +1,190 @@ +package asm + +import ( + "fmt" + + "github.com/zellyn/go6502/asm/context" + "github.com/zellyn/go6502/asm/inst" + "github.com/zellyn/go6502/asm/lines" +) + +type Flavor interface { + ParseInstr(Line lines.Line) (inst.I, error) + DefaultOrigin() (uint16, error) + context.Context +} + +type Assembler struct { + Flavor Flavor + Opener lines.Opener + Insts []*inst.I + LastLabel string +} + +func NewAssembler(flavor Flavor, opener lines.Opener) *Assembler { + return &Assembler{Flavor: flavor, Opener: opener} +} + +// Load loads a new assembler file, deleting any previous data. +func (a *Assembler) Load(filename string) error { + context := lines.Context{Filename: filename} + ls, err := lines.NewFileLineSource(filename, context, a.Opener) + if err != nil { + return err + } + lineSources := []lines.LineSource{ls} + ifdefs := []bool{} + for len(lineSources) > 0 { + line, done, err := lineSources[0].Next() + if err != nil { + return err + } + if done { + lineSources = lineSources[1:] + continue + } + in, err := a.Flavor.ParseInstr(line) + if len(ifdefs) > 0 && !ifdefs[0] && in.Type != inst.TypeIfdefElse && in.Type != inst.TypeIfdefEnd { + // we're in an inactive ifdef branch + continue + } + if in.Label != "" && in.Label[0] != '.' { + a.Flavor.SetLastLabel(in.Label) + } + in.FixLabels(a.Flavor.GetLastLabel()) + if err != nil { + return err + } + + if _, err := a.passInst(&in, false, false); err != nil { + return err + } + + switch in.Type { + case inst.TypeUnknown: + return fmt.Errorf("Unknown instruction: %s", line) + case inst.TypeMacroStart: + return fmt.Errorf("Macro start not (yet) implemented: %s", line) + case inst.TypeMacroCall: + return fmt.Errorf("Macro call not (yet) implemented: %s", line) + case inst.TypeIfdef: + if len(in.Exprs) == 0 { + panic(fmt.Sprintf("Ifdef got parsed with no expression: %s", line)) + } + val, err := in.Exprs[0].Eval(a.Flavor) + if err != nil { + return fmt.Errorf("Cannot eval ifdef condition: %v", err) + } + ifdefs = append([]bool{val != 0}, ifdefs...) + + case inst.TypeIfdefElse: + if len(ifdefs) == 0 { + return fmt.Errorf("Ifdef else branch encountered outside ifdef: %s", line) + } + ifdefs[0] = !ifdefs[0] + case inst.TypeIfdefEnd: + if len(ifdefs) == 0 { + return fmt.Errorf("Ifdef end encountered outside ifdef: %s", line) + } + ifdefs = ifdefs[1:] + case inst.TypeInclude: + subContext := lines.Context{Filename: in.TextArg, Parent: in.Line} + subLs, err := lines.NewFileLineSource(in.TextArg, subContext, a.Opener) + if err != nil { + return fmt.Errorf("error including file: %v", err) + } + lineSources = append([]lines.LineSource{subLs}, lineSources...) + continue // no need to append + case inst.TypeTarget: + return fmt.Errorf("Target not (yet) implemented: %s", line) + case inst.TypeSegment: + return fmt.Errorf("Segment not (yet) implemented: %s", line) + case inst.TypeEnd: + return nil + default: + } + a.Insts = append(a.Insts, &in) + } + return nil +} + +// 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) + a.Flavor.RemoveChanged() // Remove any variables whose value ever changed. + if org, err := a.Flavor.DefaultOrigin(); err == nil { + a.Flavor.SetAddr(org) + } else { + a.Flavor.ClearAddr() + } +} + +// passInst performs a pass on a single instruction. Depending on +// whether the instruction width can be determined, it updates or +// clears the current address. If setWidth is true, it forces the +// instruction to decide its final width. If final is true, and the +// instruction cannot be finalized, it returns an error. +func (a *Assembler) passInst(in *inst.I, setWidth, final bool) (isFinal bool, err error) { + fmt.Printf("PLUGH: in.Compute(a.Flavor, true, true) on %s\n", in) + isFinal, err = in.Compute(a.Flavor, setWidth, final) + fmt.Printf("PLUGH: isFinal=%v, in.Final=%v, in.WidthKnown=%v, in.MinWidth=%v\n", isFinal, in.Final, in.WidthKnown, in.MinWidth) + if err != nil { + return false, err + } + + if in.WidthKnown && in.MinWidth != in.MaxWidth { + panic(fmt.Sprintf("inst.I %s: WidthKnown=true, but MinWidth=%d, MaxWidth=%d", in, in.MinWidth, in.MaxWidth)) + } + + if in.WidthKnown && a.Flavor.AddrKnown() { + addr, _ := a.Flavor.GetAddr() + a.Flavor.SetAddr(addr + in.MinWidth) + } else { + a.Flavor.ClearAddr() + } + + return isFinal, nil +} + +// Pass performs an assembly pass. If setWidth is true, it causes all +// instructions to set their final width. If final is true, it returns +// an error for any instruction that cannot be finalized. +func (a *Assembler) Pass(setWidth, final bool) (isFinal bool, err error) { + fmt.Printf("PLUGH: Pass(%v, %v): %d instructions\n", setWidth, final, len(a.Insts)) + setWidth = setWidth || final // final ⊢ setWidth + + a.initPass() + + isFinal = true + for _, in := range a.Insts { + instFinal, err := a.passInst(in, setWidth, final) + if err != nil { + return false, err + } + if final && !instFinal { + return false, fmt.Errorf("Cannot finalize instruction: %s", in) + } + fmt.Printf("PLUGH: instFinal=%v, in.Final=%v, in.WidthKnown=%v, in.MinWidth=%v\n", instFinal, in.Final, in.WidthKnown, in.MinWidth) + isFinal = isFinal && instFinal + } + + return isFinal, nil +} + +// RawBytes returns the raw bytes, sequentially. Intended for testing, +// the return value gives no indication of address changes. +func (a *Assembler) RawBytes() ([]byte, error) { + result := []byte{} + for _, in := range a.Insts { + if !in.Final { + return []byte{}, fmt.Errorf("cannot finalize value: %s", in) + } + result = append(result, in.Data...) + } + return result, nil +} + +func (a *Assembler) Reset() { + a.Insts = nil + a.LastLabel = "" +} diff --git a/asm/asm.org b/asm/asm.org new file mode 100644 index 0000000..5923168 --- /dev/null +++ b/asm/asm.org @@ -0,0 +1,63 @@ + + +| Concept | SCSC | AS65 | Merlin | +|-------------------------------+------------------+------------------+--------| +| origin | .OR | org | | +| | | | | +| target address | .TA | | | +| | | | | +| include | .IN | | | +| | | | | +| comment | * | ; | | +| | >1 space | | | +| | | | | +| label | at left | | | +| | | | | +| equ | .EQ | = | | +| | | equ | | +| | | | | +| binary | | %00000001 | | +| | | | | +| macro | .MA macro_name | macro_name macro | | +| | .EM | endm | | +| | | | | +| if | .DO | if foo = 1 | | +| | .ELSE | endif | | +| | [.ELSE] (toggle) | | | +| | .FIN | | | +| data | | | | +| - word (LE) | .DA expr | | | +| - byte (LSB) | .DA #expr | | | +| - byte (MSB) | .DA /expr | | | +| | | | | +| ascii string | | | | +| - normal use | .AS "foo" | | | +| - arb. delimeter | .AS dfood | | | +| - high bit set | .AS -"foo" | | | +| - last high bit opposite | .AT "foo" | | | +| | | | | +| hex string | .HS | | | +| | | | | +| reserve space (block storage) | .BS | | | +| | | | | + +Things to ignore +| Concept | SCSC | AS65 | +|---------+------------------------+------| +| title | .TI ,title | | +| file | .TF filename | | + + + +* AS65 notes +https://github.com/Klaus2m5/6502_65C02_functional_tests/blob/master/6502_functional_test.a65 + + +* SCSC notes + + .DO -- Conditional Assembly 5-9 + .ELSE -- Conditional Assembly 5-9 + .FIN -- Conditional Assembly 5-9 + .MA -- Macro Definition 5-11 + .EM -- End of Macro 5-11 + diff --git a/asm/context/context.go b/asm/context/context.go new file mode 100644 index 0000000..034cfb2 --- /dev/null +++ b/asm/context/context.go @@ -0,0 +1,97 @@ +package context + +import "fmt" + +type Context interface { + Set(name string, value uint16) + Get(name string) (uint16, bool) + SetAddr(uint16) + ClearAddr() + GetAddr() (uint16, bool) + Zero() (uint16, error) // type ZeroFunc + RemoveChanged() + AddrKnown() bool + GetLastLabel() string + SetLastLabel(string) + Clear() +} + +type SimpleContext struct { + symbols map[string]symbolValue + addr int32 + lastLabel string +} + +type symbolValue struct { + v uint16 + changed bool // Did the value ever change? +} + +func (sc *SimpleContext) fix() { + if sc.symbols == nil { + sc.symbols = make(map[string]symbolValue) + } +} + +func (sc *SimpleContext) Zero() (uint16, error) { + return 0, fmt.Errorf("Not implemented: context.SimpleContext.Zero()") +} + +func (sc *SimpleContext) Get(name string) (uint16, bool) { + if name == "*" { + return sc.GetAddr() + } + sc.fix() + s, found := sc.symbols[name] + return s.v, found +} + +func (sc *SimpleContext) ClearAddr() { + sc.addr = -1 +} + +func (sc *SimpleContext) SetAddr(addr uint16) { + sc.addr = int32(addr) +} + +func (sc *SimpleContext) GetAddr() (uint16, bool) { + if sc.addr == -1 { + return 0, false + } + return uint16(sc.addr), true +} + +func (sc *SimpleContext) AddrKnown() bool { + return sc.addr != -1 +} + +func (sc *SimpleContext) Set(name string, value uint16) { + sc.fix() + s, found := sc.symbols[name] + if found && s.v != value { + s.changed = true + } + s.v = value + sc.symbols[name] = s +} + +func (sc *SimpleContext) RemoveChanged() { + sc.fix() + for n, s := range sc.symbols { + if s.changed { + delete(sc.symbols, n) + } + } +} + +func (sc *SimpleContext) GetLastLabel() string { + return sc.lastLabel +} + +func (sc *SimpleContext) SetLastLabel(l string) { + sc.lastLabel = l +} + +func (sc *SimpleContext) Clear() { + sc.symbols = make(map[string]symbolValue) +} diff --git a/asm/doc.go b/asm/doc.go new file mode 100644 index 0000000..a5c2f11 --- /dev/null +++ b/asm/doc.go @@ -0,0 +1,13 @@ +/* +Package asm provides routines for assembling 6502 code. It currently +emulates the S-C Macro Assembler. The goal is to support (at least) +as65 and Merlin assembly files too. + +Once those three (two ancient, one modern) are complete, adding +additional flavors should be straightforward. + +TODO(zellyn): make errors return line and character position. +TODO(zellyn): scma requires .EQ and .BS to have known values. Is this universal? + +*/ +package asm diff --git a/asm/expr/expression.go b/asm/expr/expression.go new file mode 100644 index 0000000..9c14fa2 --- /dev/null +++ b/asm/expr/expression.go @@ -0,0 +1,210 @@ +package expr + +import ( + "errors" + "fmt" + + "github.com/zellyn/go6502/asm/context" +) + +type UnknownLabelError struct { + Label string +} + +func (e UnknownLabelError) Error() string { + return fmt.Sprintf(`unknown label: "%s"`, e.Label) +} + +type Operator int + +const ( + OpUnknown Operator = iota + OpLeaf // No op - this is a leaf node + OpPlus // add + OpMinus // subtract / negate (with Right==nil) + OpMul // multiply + OpDiv // divide + OpLsb // Low byte + OpMsb // High byte + OpLt // Less than + OpGt // Greater than + OpEq // Equal to + OpByte // A single byte of storage +) + +var OpStrings = map[Operator]string{ + OpPlus: "+", + OpMinus: "-", + OpMul: "*", + OpDiv: "/", + OpLsb: "lsb", + OpMsb: "msb", + OpByte: "byte", + OpLt: "<", + OpGt: ">", + OpEq: "=", +} + +type E struct { + Left *E + Right *E + Op Operator + Text string + Val uint16 +} + +func (e E) String() string { + switch e.Op { + case OpLeaf: + if e.Text != "" { + return e.Text + } + return fmt.Sprintf("$%04x", e.Val) + + case OpPlus, OpMinus, OpMul, OpDiv, OpLsb, OpMsb, OpByte, OpLt, OpGt, OpEq: + if e.Right != nil { + return fmt.Sprintf("(%s %s %s)", OpStrings[e.Op], *e.Left, *e.Right) + } + return fmt.Sprintf("(%s %s)", OpStrings[e.Op], *e.Left) + case OpUnknown: + return "?" + default: + panic(fmt.Sprintf("Unprintable op type: %v", e.Op)) + } +} + +func (e *E) Equal(o *E) bool { + if e == nil || o == nil { + return e == nil && o == nil + } + if e.Op != o.Op { + return false + } + if e.Text != o.Text { + return false + } + if e.Val != o.Val { + return false + } + return e.Left.Equal(o.Left) && e.Right.Equal(o.Right) +} + +// Width returns the width in bytes of an expression. It'll be two for anything except +// expressions that start with Lsb or Msb operators. +func (e *E) Width() uint16 { + switch e.Op { + case OpLsb, OpMsb, OpByte: + return 1 + } + return 2 +} + +func (e *E) Eval(ctx context.Context) (uint16, error) { + if e == nil { + return 0, errors.New("cannot Eval() nil expression") + } + switch e.Op { + case OpLeaf: + if e.Text == "" { + return e.Val, nil + } + if val, ok := ctx.Get(e.Text); ok { + return val, nil + } + return 0, UnknownLabelError{Label: e.Text} + case OpMinus: + l, err := e.Left.Eval(ctx) + if err != nil { + return 0, err + } + if e.Right == nil { + return -l, nil + } + r, err := e.Right.Eval(ctx) + if err != nil { + return 0, err + } + return l - r, nil + case OpMsb, OpLsb: + l, err := e.Left.Eval(ctx) + if err != nil { + return 0, err + } + if e.Op == OpMsb { + return l >> 8, nil + } + return l & 0xff, nil + case OpByte: + return e.Val, nil + case OpPlus, OpMul, OpDiv, OpLt, OpGt, OpEq: + l, err := e.Left.Eval(ctx) + if err != nil { + return 0, err + } + r, err := e.Right.Eval(ctx) + if err != nil { + return 0, err + } + switch e.Op { + case OpPlus: + return l + r, nil + case OpMul: + return l * r, nil + case OpLt: + if l < r { + return 1, nil + } + return 0, nil + case OpGt: + if l > r { + return 1, nil + } + return 0, nil + case OpEq: + if l == r { + return 1, nil + } + return 0, nil + case OpDiv: + if r == 0 { + return ctx.Zero() + } + return l / r, nil + } + panic(fmt.Sprintf("bad code - missing switch case: %d", e.Op)) + } + panic(fmt.Sprintf("unknown operator type: %d", e.Op)) +} + +// CheckedEval calls Eval, but also turns UnknownLabelErrors into labelMissing booleans. +func (e *E) CheckedEval(ctx context.Context) (val uint16, labelMissing bool, err error) { + val, err = e.Eval(ctx) + switch err.(type) { + case nil: + return val, false, nil + case UnknownLabelError: + return val, true, err + } + return val, false, err +} + +// FixLabels attempts to turn .1 into LAST_LABEL.1 +func (e *E) FixLabels(last string) error { + if e.Text != "" && e.Text[0] == '.' { + if last == "" { + return fmt.Errorf("Reference to sub-label '%s' before full label.", e.Text) + } + e.Text = last + e.Text + } + + if e.Left != nil { + if err := e.Left.FixLabels(last); err != nil { + return err + } + } + if e.Right != nil { + return e.Right.FixLabels(last) + } + + return nil +} diff --git a/asm/expr/expression_test.go b/asm/expr/expression_test.go new file mode 100644 index 0000000..3f23459 --- /dev/null +++ b/asm/expr/expression_test.go @@ -0,0 +1,50 @@ +package expr + +import ( + "testing" +) + +func TestExpressionString(t *testing.T) { + tests := []struct { + expr E + want string + }{ + { + E{}, + "?", + }, + { + E{Op: OpLeaf, Text: "*"}, + "*", + }, + { + E{ + Op: OpPlus, + Left: &E{Op: OpLeaf, Text: "Label"}, + Right: &E{Op: OpLeaf, Val: 42}, + }, + "(+ Label $002a)", + }, + { + E{ + Op: OpMinus, + Left: &E{Op: OpLeaf, Text: "Label"}, + Right: &E{Op: OpLeaf, Val: 42}, + }, + "(- Label $002a)", + }, + { + E{ + Op: OpMinus, + Left: &E{Op: OpLeaf, Val: 42}, + }, + "(- $002a)", + }, + } + for i, tt := range tests { + got := tt.expr.String() + if got != tt.want { + t.Errorf(`%d: want String(expr)="%s"; got "%s"`, i, tt.want, got) + } + } +} diff --git a/asm/flavors/as65/as65.go b/asm/flavors/as65/as65.go new file mode 100644 index 0000000..eab5200 --- /dev/null +++ b/asm/flavors/as65/as65.go @@ -0,0 +1,33 @@ +package as65 + +import ( + "errors" + + "github.com/zellyn/go6502/asm/context" + "github.com/zellyn/go6502/asm/inst" + "github.com/zellyn/go6502/asm/lines" +) + +// AS65 implements the AS65-compatible assembler flavor. +// See http://www.kingswood-consulting.co.uk/assemblers/ + +type AS65 struct { + context.SimpleContext +} + +func New() *AS65 { + return &AS65{} +} + +// Parse an entire instruction, or return an appropriate error. +func (a *AS65) ParseInstr(line lines.Line) (inst.I, error) { + return inst.I{}, nil +} + +func (a *AS65) Zero() (uint16, error) { + return 0, errors.New("Division by zero.") +} + +func (a *AS65) DefaultOrigin() (uint16, error) { + return 0, nil +} diff --git a/asm/flavors/assemble_test.go b/asm/flavors/assemble_test.go new file mode 100644 index 0000000..0714f8c --- /dev/null +++ b/asm/flavors/assemble_test.go @@ -0,0 +1,199 @@ +package flavors + +import ( + "encoding/hex" + "os" + "strings" + "testing" + + "github.com/zellyn/go6502/asm" + "github.com/zellyn/go6502/asm/flavors/scma" + "github.com/zellyn/go6502/asm/lines" +) + +func TestMultiline(t *testing.T) { + o := lines.NewTestOpener() + + ss := asm.NewAssembler(scma.New(), o) + // aa := asm.NewAssembler(as65.New(), o) + // mm := asm.NewAssembler(merlin.New(), o) + + tests := []struct { + a *asm.Assembler // assembler + name string // name + i []string // main file: lines + ii map[string][]string // other files: lines + b string // bytes, expected + active bool + }{ + // We cannot determine L2, so we go wide. + {ss, "Unknown label: wide", []string{ + "L1 LDA L2-L1", + "L2 NOP", + }, nil, "ad0300ea", false}, + + // Sub-labels + {ss, "Sublabels", []string{ + "L1 BEQ .1", + ".1 NOP", + "L2 BEQ .2", + ".1 NOP", + ".2 NOP", + }, nil, "f000eaf001eaea", false}, + + // Includes: one level deep + {ss, "Include A", []string{ + " BEQ OVER", + " .IN SUBFILE1", + "OVER NOP", + }, map[string][]string{ + "SUBFILE1": { + " LDA #$2a", + }, + }, "f002a92aea", false}, + + // Ifdefs: simple at first + {ss, "Ifdef A", []string{ + "L1 .EQ $17", + " .DO L1>$16", + " LDA #$01", + " .ELSE", + " LDA #$02", + " .FIN", + " NOP", + }, nil, "a901ea", true}, + + // Ifdefs: else part + {ss, "Ifdef B", []string{ + "L1 .EQ $16", + " .DO L1>$16", + " LDA #$01", + " .ELSE", + " LDA #$02", + " .FIN", + " NOP", + }, nil, "a902ea", true}, + + // Ifdefs: multiple else, true + {ss, "Ifdef C", []string{ + "L1 .EQ $17", + " .DO L1>$16", + " LDA #$01", + " .ELSE", + " LDA #$02", + " .ELSE", + " LDA #$03", + " .ELSE", + " LDA #$04", + " .FIN", + " NOP", + }, nil, "a901a903ea", true}, + + // Ifdefs: multiple else, false + {ss, "Ifdef D", []string{ + "L1 .EQ $16", + " .DO L1>$16", + " LDA #$01", + " .ELSE", + " LDA #$02", + " .ELSE", + " LDA #$03", + " .ELSE", + " LDA #$04", + " .FIN", + " NOP", + }, nil, "a902a904ea", true}, + + // Ifdef based on org, true + {ss, "Ifdef/org A", []string{ + " .OR $1000", + " LDA #$01", + " .DO *=$1002", + " LDA #$02", + " .ELSE", + " LDA #$03", + " .FIN", + " NOP", + }, nil, "a901a902ea", true}, + + // Ifdef based on org, false + {ss, "Ifdef/org B", []string{ + " .OR $1000", + " LDA #$01", + " .DO *=$1003", + " LDA #$02", + " .ELSE", + " LDA #$03", + " .FIN", + " NOP", + }, nil, "a901a903ea", true}, + } + + for i, tt := range tests { + if !tt.active { + continue + } + tt.a.Reset() + o.Clear() + o["TESTFILE"] = strings.Join(tt.i, "\n") + for k, v := range tt.ii { + o[k] = strings.Join(v, "\n") + } + if err := tt.a.Load("TESTFILE"); err != nil { + 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 + } + isFinal, err := tt.a.Pass(true, true) + if err != nil { + t.Errorf(`%d("%s"): tt.a.Pass(true, true) failed: %s`, i, tt.name, err) + continue + } + if !isFinal { + t.Errorf(`%d("%s"): tt.a.Pass(true, true) couldn't finalize`, i, tt.name) + continue + } + bb, err := tt.a.RawBytes() + if err != nil { + t.Errorf(`%d("%s"): tt.a.RawBytes() failed: %s`, i, tt.name, err) + continue + } + hx := hex.EncodeToString(bb) + if hx != tt.b { + t.Errorf(`%d("%s"): tt.a.RawBytes()=[%s]; want [%s]`, i, tt.name, hx, tt.b) + continue + } + } +} + +func TestApplesoftBasic(t *testing.T) { + if err := os.Chdir("../../../goapple2/source/applesoft"); err != nil { + t.Fatal(err) + } + var o lines.OsOpener + + ss := asm.NewAssembler(scma.New(), o) + ss.Reset() + + if err := ss.Load("S.ACF"); err != nil { + t.Fatalf(`ss.Load("S.ACF") failed: %s`, err) + } + if _, err := ss.Pass(true, false); err != nil { + t.Fatalf(`ss.Pass(true, false) failed: %s`, err) + } + isFinal, err := ss.Pass(true, true) + if err != nil { + t.Fatalf(`ss.Pass(true, true) failed: %s`, err) + } + if !isFinal { + t.Fatalf(`ss.Pass(true, true) couldn't finalize`) + } + bb, err := ss.RawBytes() + if err != nil { + t.Fatalf(`ss.RawBytes() failed: %s`, err) + } + _ = bb +} diff --git a/asm/flavors/common/common.go b/asm/flavors/common/common.go new file mode 100644 index 0000000..c22abd7 --- /dev/null +++ b/asm/flavors/common/common.go @@ -0,0 +1,142 @@ +package common + +import ( + "fmt" + + "github.com/zellyn/go6502/asm/context" + "github.com/zellyn/go6502/asm/inst" + "github.com/zellyn/go6502/opcodes" +) + +// DecodeOp contains the common code that decodes an Opcode, once we +// have fully parsed it. +func DecodeOp(c context.Context, in inst.I, summary opcodes.OpSummary, indirect bool, xy rune) (inst.I, error) { + i := inst.I{} + ex := in.Exprs[0] + + // At this point we've parsed the Opcode. Let's see if it makes sense. + if indirect { + switch xy { + case 'x': + op, ok := summary.OpForMode(opcodes.MODE_INDIRECT_X) + if !ok { + return i, fmt.Errorf("%s doesn't support indexed indirect (addr,X) mode", in.Command) + } + in.Op = op.Byte + in.WidthKnown = true + in.MinWidth = 2 + in.MaxWidth = 2 + in.Mode = opcodes.MODE_INDIRECT_X + return in, nil + case 'y': + op, ok := summary.OpForMode(opcodes.MODE_INDIRECT_Y) + if !ok { + return i, fmt.Errorf("%s doesn't support indirect indexed (addr),Y mode", in.Command) + } + in.WidthKnown = true + in.MinWidth = 2 + in.MaxWidth = 2 + in.Mode = opcodes.MODE_INDIRECT_Y + in.Op = op.Byte + return in, nil + default: + op, ok := summary.OpForMode(opcodes.MODE_INDIRECT) + if !ok { + return i, fmt.Errorf("%s doesn't support indirect (addr) mode", in.Command) + } + in.Op = op.Byte + in.WidthKnown = true + in.MinWidth = 3 + in.MaxWidth = 3 + in.Mode = opcodes.MODE_INDIRECT + return in, nil + } + } + + // Branch + if summary.Modes == opcodes.MODE_RELATIVE { + op, ok := summary.OpForMode(opcodes.MODE_RELATIVE) + if !ok { + panic(fmt.Sprintf("opcode error: %s has no MODE_RELATIVE opcode", in.Command)) + } + in.Op = op.Byte + in.WidthKnown = true + in.MinWidth = 2 + in.MaxWidth = 2 + in.Mode = opcodes.MODE_RELATIVE + return in, nil + } + + // No ,X or ,Y, and width is forced to 1-byte: immediate mode. + if xy == '-' && ex.Width() == 1 && summary.AnyModes(opcodes.MODE_IMMEDIATE) { + op, ok := summary.OpForMode(opcodes.MODE_IMMEDIATE) + if !ok { + panic(fmt.Sprintf("opcode error: %s has no MODE_IMMEDIATE opcode", in.Command)) + } + in.Op = op.Byte + in.WidthKnown = true + in.MinWidth = 2 + in.MaxWidth = 2 + in.Mode = opcodes.MODE_IMMEDIATE + return in, nil + } + + var zp, wide opcodes.AddressingMode + var zpS, wideS string + switch xy { + case 'x': + zp, wide = opcodes.MODE_ZP_X, opcodes.MODE_ABS_X + zpS, wideS = "ZeroPage,X", "Absolute,X" + case 'y': + zp, wide = opcodes.MODE_ZP_Y, opcodes.MODE_ABS_Y + zpS, wideS = "ZeroPage,Y", "Absolute,Y" + default: + zp, wide = opcodes.MODE_ZP, opcodes.MODE_ABSOLUTE + zpS, wideS = "ZeroPage", "Absolute" + } + + opWide, wideOk := summary.OpForMode(wide) + opZp, zpOk := summary.OpForMode(zp) + + if !summary.AnyModes(zp | wide) { + return i, fmt.Errorf("%s opcode doesn't support %s or %s modes.", zpS, wideS) + } + + if !summary.AnyModes(zp) { + if !wideOk { + panic(fmt.Sprintf("opcode error: %s has no %s opcode", in.Command, wideS)) + } + in.Op = opWide.Byte + in.WidthKnown = true + in.MinWidth = 3 + in.MaxWidth = 3 + in.Mode = wide + return in, nil + } + if !summary.AnyModes(wide) { + if !zpOk { + panic(fmt.Sprintf("opcode error: %s has no %s opcode", in.Command, zpS)) + } + in.Op = opZp.Byte + in.WidthKnown = true + in.MinWidth = 2 + in.MaxWidth = 2 + in.Mode = zp + return in, nil + } + + // Okay, we don't know whether it's wide or narrow: store enough info for either. + if !zpOk { + panic(fmt.Sprintf("opcode error: %s has no %s opcode", in.Command, zpS)) + } + if !wideOk { + panic(fmt.Sprintf("opcode error: %s has no %s opcode", in.Command, wideS)) + } + in.Op = opWide.Byte + in.ZeroOp = opZp.Byte + in.Mode = wide + in.ZeroMode = zp + in.MinWidth = 2 + in.MaxWidth = 3 + return in, nil +} diff --git a/asm/flavors/flavors.go b/asm/flavors/flavors.go new file mode 100644 index 0000000..f6e0d7d --- /dev/null +++ b/asm/flavors/flavors.go @@ -0,0 +1 @@ +package flavors diff --git a/asm/flavors/merlin/merlin.go b/asm/flavors/merlin/merlin.go new file mode 100644 index 0000000..1f35d24 --- /dev/null +++ b/asm/flavors/merlin/merlin.go @@ -0,0 +1,34 @@ +package merlin + +import ( + "errors" + + "github.com/zellyn/go6502/asm/context" + "github.com/zellyn/go6502/asm/inst" + "github.com/zellyn/go6502/asm/lines" +) + +// Merlin implements the Merlin-compatible assembler flavor. +// See http://en.wikipedia.org/wiki/Merlin_(assembler) and +// http://www.apple-iigs.info/doc/fichiers/merlin816.pdf‎ + +type Merlin struct { + context.SimpleContext +} + +func New() *Merlin { + return &Merlin{} +} + +// Parse an entire instruction, or return an appropriate error. +func (a *Merlin) ParseInstr(line lines.Line) (inst.I, error) { + return inst.I{}, nil +} + +func (a *Merlin) Zero() (uint16, error) { + return 0, errors.New("Division by zero.") +} + +func (a *Merlin) DefaultOrigin() (uint16, error) { + return 0x8000, nil +} diff --git a/asm/flavors/scma/scma.go b/asm/flavors/scma/scma.go new file mode 100644 index 0000000..6a29851 --- /dev/null +++ b/asm/flavors/scma/scma.go @@ -0,0 +1,518 @@ +package scma + +import ( + "encoding/hex" + "fmt" + "strconv" + "strings" + + "github.com/zellyn/go6502/asm/context" + "github.com/zellyn/go6502/asm/expr" + "github.com/zellyn/go6502/asm/flavors/common" + "github.com/zellyn/go6502/asm/inst" + "github.com/zellyn/go6502/asm/lines" + "github.com/zellyn/go6502/opcodes" +) + +const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +const digits = "0123456789" +const hexdigits = digits + "abcdefABCDEF" +const labelChars = letters + digits + ".:" +const whitespace = " \t" +const cmdChars = letters + "." +const fileChars = letters + digits + "." +const operatorChars = "+-*/<>=" + +type directiveInfo struct { + Type inst.Type + Func func(inst.I, *lines.Parse) (inst.I, error) +} + +// SCMA implements the S-C Macro Assembler-compatible assembler flavor. +// See http://www.txbobsc.com/scsc/ and http://stjarnhimlen.se/apple2/ +type SCMA struct { + directives map[string]directiveInfo + operators map[string]expr.Operator + context.SimpleContext +} + +func New() *SCMA { + s := &SCMA{} + s.directives = map[string]directiveInfo{ + ".IN": {inst.TypeInclude, s.parseInclude}, + ".OR": {inst.TypeOrg, s.parseAddress}, + ".TA": {inst.TypeTarget, s.parseNotImplemented}, + ".TF": {inst.TypeNone, nil}, + ".EN": {inst.TypeEnd, s.parseNoArgDir}, + ".EQ": {inst.TypeEqu, s.parseEquate}, + ".DA": {inst.TypeData, s.parseData}, + ".HS": {inst.TypeData, s.parseHexString}, + ".AS": {inst.TypeData, s.parseAscii}, + ".AT": {inst.TypeData, s.parseAscii}, + ".BS": {inst.TypeBlock, s.parseBlockStorage}, + ".TI": {inst.TypeNone, nil}, + ".LIST": {inst.TypeNone, nil}, + ".PG": {inst.TypeNone, nil}, + ".DO": {inst.TypeIfdef, s.parseDo}, + ".ELSE": {inst.TypeIfdefElse, s.parseNoArgDir}, + ".FIN": {inst.TypeIfdefEnd, s.parseNoArgDir}, + ".MA": {inst.TypeMacroStart, s.parseNoArgDir}, + ".EM": {inst.TypeMacroEnd, s.parseNoArgDir}, + ".US": {inst.TypeNone, s.parseNotImplemented}, + } + s.operators = map[string]expr.Operator{ + "*": expr.OpMul, + "/": expr.OpDiv, + "+": expr.OpPlus, + "-": expr.OpMinus, + "<": expr.OpLt, + ">": expr.OpGt, + "=": expr.OpEq, + } + + return s +} + +// Parse an entire instruction, or return an appropriate error. +func (a *SCMA) ParseInstr(line lines.Line) (inst.I, error) { + lp := line.Parse + in := inst.I{Line: &line} + + // Lines that start with a digit are considered to have a declared line number. + if lp.AcceptRun(digits) { + s := lp.Emit() + if len(s) != 4 { + return inst.I{}, fmt.Errorf("Line number must be exactly 4 digits: %s", s) + } + if !lp.Consume(" ") && lp.Peek() != lines.Eol { + return inst.I{}, fmt.Errorf("Line number (%s) followed by non-space", s) + } + i, err := strconv.ParseUint(s, 10, 16) + if err != nil { + return inst.I{}, fmt.Errorf("Invalid line number: %s: %s", s, err) + } + in.DeclaredLine = uint16(i) + } + + // Empty line or comment + trimmed := strings.TrimSpace(lp.Rest()) + if trimmed == "" || trimmed[0] == '*' { + in.Type = inst.TypeNone + return in, nil + } + + // See if we have a label at the start + if lp.AcceptRun(labelChars) { + in.Label = lp.Emit() + } + + // Ignore whitespace at the start or after the label. + lp.IgnoreRun(whitespace) + + if lp.Peek() == lines.Eol { + in.Type = inst.TypeNone + return in, nil + } + return a.parseCmd(in, lp) +} + +func (a *SCMA) Zero() (uint16, error) { + return uint16(0xffff), nil +} + +func (a *SCMA) DefaultOrigin() (uint16, error) { + return 0x0800, nil +} + +// parseCmd parses the "command" part of an instruction: we expect to be +// looking at a non-whitespace character. +func (a *SCMA) parseCmd(in inst.I, lp *lines.Parse) (inst.I, error) { + if lp.Consume(">") { + return a.parseMacroCall(in, lp) + } + if !lp.AcceptRun(cmdChars) { + c := lp.Next() + return inst.I{}, fmt.Errorf("Expecting instruction, found '%c' (%d)", c, c) + } + in.Command = lp.Emit() + if dir, ok := a.directives[in.Command]; ok { + in.Type = dir.Type + if dir.Func == nil { + return in, nil + } + return dir.Func(in, lp) + } + + if summary, ok := opcodes.ByName[in.Command]; ok { + in.Type = inst.TypeOp + return a.parseOpArgs(in, lp, summary) + } + return inst.I{}, fmt.Errorf(`Not implemented: "%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) { + c := lp.Next() + return inst.I{}, fmt.Errorf("Expecting macro name, found '%c' (%d)", c, c) + } + in.Command = lp.Emit() + + lp.Consume(whitespace) + + for { + s, err := a.parseMacroArg(lp) + if err != nil { + return inst.I{}, err + } + in.MacroArgs = append(in.MacroArgs, s) + if !lp.Consume(",") { + break + } + } + + return in, nil +} + +// parseMacroArg parses a single macro argument. We expect to be looking at the first +// character of a macro argument. +func (a *SCMA) parseMacroArg(lp *lines.Parse) (string, error) { + if lp.Peek() == '"' { + return a.parseQuoted(lp) + } + lp.AcceptUntil(whitespace + ",") + return lp.Emit(), nil +} + +// parseQuoted parses a single quoted string macro argument. We expect +// to be looking at the first quote. +func (a *SCMA) parseQuoted(lp *lines.Parse) (string, error) { + if !lp.Consume(`"`) { + panic(fmt.Sprintf("parseQuoted called not looking at a quote")) + } + for { + lp.AcceptUntil(`"`) + // We're done, unless there's an escaped quote + if !lp.AcceptString(`""`) { + break + } + } + s := lp.Emit() + if !lp.Consume(`"`) { + c := lp.Peek() + return "", fmt.Errorf("Expected closing quote; got %s", c) + } + + c := lp.Peek() + if c != ',' && c != ' ' && c != lines.Eol && c != '\t' { + return "", fmt.Errorf("Unexpected char after quoted string: '%s'", c) + } + + return strings.Replace(s, `""`, `"`, -1), nil +} + +// parseOpArgs parses the arguments to an assembly op. We expect to be looking at the first +// non-op character (probably whitespace) +func (a *SCMA) parseOpArgs(in inst.I, lp *lines.Parse, summary opcodes.OpSummary) (inst.I, error) { + i := inst.I{} + + // MODE_IMPLIED: we don't really care what comes next: it's a comment. + if summary.Modes == opcodes.MODE_IMPLIED { + op := summary.Ops[0] + in.Data = []byte{op.Byte} + in.WidthKnown = true + in.MinWidth = 1 + in.MaxWidth = 1 + in.Final = true + in.Mode = opcodes.MODE_IMPLIED + return in, nil + } + + // Nothing else on the line? Must be MODE_A + lp.Consume(whitespace) + if lp.Consume(whitespace) || lp.Peek() == lines.Eol { + if !summary.AnyModes(opcodes.MODE_A) { + return i, fmt.Errorf("%s with no arguments", in.Command) + } + op, ok := summary.OpForMode(opcodes.MODE_A) + if !ok { + panic(fmt.Sprintf("%s doesn't support accumulator mode", in.Command)) + } + in.Data = []byte{op.Byte} + in.WidthKnown = true + in.MinWidth = 1 + in.MaxWidth = 1 + in.Final = true + in.Mode = opcodes.MODE_A + return in, nil + } + + indirect := lp.Consume("(") + if indirect && !summary.AnyModes(opcodes.MODE_INDIRECT_ANY) { + return i, fmt.Errorf("%s doesn't support any indirect modes", in.Command) + } + xy := '-' + expr, err := a.parseExpression(lp) + if err != nil { + return i, err + } + in.Exprs = append(in.Exprs, expr) + comma := lp.Consume(",") + if comma { + if lp.Consume("xX") { + xy = 'x' + } else if lp.Consume("yY") { + if indirect { + return i, fmt.Errorf(",Y unexpected inside parens") + } + xy = 'y' + } else { + return i, fmt.Errorf("X or Y expected after comma") + } + } + comma2 := false + if indirect { + if !lp.Consume(")") { + return i, fmt.Errorf("Expected closing paren") + } + comma2 = lp.Consume(",") + if comma2 { + if comma { + return i, fmt.Errorf("Cannot have ,X or ,Y twice.") + } + if !lp.Consume("yY") { + return i, fmt.Errorf("Only ,Y can follow parens.") + } + xy = 'y' + } + } + + return common.DecodeOp(a, in, summary, indirect, xy) +} + +func (a *SCMA) parseAddress(in inst.I, lp *lines.Parse) (inst.I, error) { + lp.IgnoreRun(whitespace) + expr, err := a.parseExpression(lp) + if err != nil { + return inst.I{}, err + } + in.Exprs = append(in.Exprs, expr) + in.WidthKnown = true + in.MinWidth = 0 + in.MaxWidth = 0 + in.Final = true + return in, nil +} + +func (a *SCMA) parseAscii(in inst.I, lp *lines.Parse) (inst.I, error) { + lp.IgnoreRun(whitespace) + var invert, invertLast byte + if lp.Consume("-") { + invert = 0x80 + } + if in.Command == ".AT" { + invertLast = 0x80 + } + delim := lp.Next() + if delim == lines.Eol || strings.IndexRune(whitespace, delim) >= 0 { + return inst.I{}, fmt.Errorf("%s expects delimeter, found '%s'", in.Command, delim) + } + lp.Ignore() + lp.AcceptUntil(string(delim)) + delim2 := lp.Next() + if delim != delim2 { + return inst.I{}, fmt.Errorf("%s: expected closing delimeter '%s'; got '%s'", in.Command, delim, delim2) + } + lp.Backup() + in.Data = []byte(lp.Emit()) + for i := range in.Data { + in.Data[i] ^= invert + if i == len(in.Data)-1 { + in.Data[i] ^= invertLast + } + } + return in, nil +} + +func (a *SCMA) parseBlockStorage(in inst.I, lp *lines.Parse) (inst.I, error) { + lp.IgnoreRun(whitespace) + ex, err := a.parseExpression(lp) + if err != nil { + return inst.I{}, err + } + in.Exprs = append(in.Exprs, ex) + return in, nil +} + +func (a *SCMA) parseData(in inst.I, lp *lines.Parse) (inst.I, error) { + lp.IgnoreRun(whitespace) + for { + ex, err := a.parseExpression(lp) + if err != nil { + return inst.I{}, err + } + in.Exprs = append(in.Exprs, ex) + if !lp.Consume(",") { + break + } + } + return in, nil +} + +func (a *SCMA) parseDo(in inst.I, lp *lines.Parse) (inst.I, error) { + lp.IgnoreRun(whitespace) + expr, err := a.parseExpression(lp) + if err != nil { + return inst.I{}, err + } + in.Exprs = append(in.Exprs, expr) + in.WidthKnown = true + in.MinWidth = 0 + in.MaxWidth = 0 + in.Final = true + return in, nil +} + +func (a *SCMA) parseEquate(in inst.I, lp *lines.Parse) (inst.I, error) { + lp.IgnoreRun(whitespace) + expr, err := a.parseExpression(lp) + if err != nil { + return inst.I{}, err + } + in.Exprs = append(in.Exprs, expr) + in.WidthKnown = true + in.MinWidth = 0 + in.MaxWidth = 0 + in.Final = true + return in, nil +} + +func (a *SCMA) parseHexString(in inst.I, lp *lines.Parse) (inst.I, error) { + lp.IgnoreRun(whitespace) + if !lp.AcceptRun(hexdigits) { + return inst.I{}, fmt.Errorf("%s expects hex digits; got '%s'", in.Command, lp.Next()) + } + hs := lp.Emit() + if len(hs)%2 != 0 { + return inst.I{}, fmt.Errorf("%s expects pairs of hex digits; got %d", in.Command, len(hs)) + } + var err error + if in.Data, err = hex.DecodeString(hs); err != nil { + return inst.I{}, fmt.Errorf("%s: error decoding hex string: %s", in.Command, err) + } + return in, nil +} + +func (a *SCMA) parseInclude(in inst.I, lp *lines.Parse) (inst.I, error) { + lp.IgnoreRun(whitespace) + if !lp.AcceptRun(fileChars) { + return inst.I{}, fmt.Errorf("Expecting filename, 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 + in.MaxWidth = 0 + in.Final = true + return in, nil +} + +func (a *SCMA) parseNotImplemented(in inst.I, lp *lines.Parse) (inst.I, error) { + return inst.I{}, fmt.Errorf("Not implemented (yet?): %s", in.Command) +} + +func (a *SCMA) parseExpression(lp *lines.Parse) (*expr.E, error) { + var outer *expr.E + if lp.Accept("#/") { + switch lp.Emit() { + case "#": + outer = &expr.E{Op: expr.OpLsb} + case "/": + outer = &expr.E{Op: expr.OpMsb} + } + } + + tree, err := a.parseTerm(lp) + if err != nil { + return &expr.E{}, err + } + + for lp.Accept(operatorChars) { + c := lp.Emit() + right, err := a.parseTerm(lp) + if err != nil { + return &expr.E{}, err + } + tree = &expr.E{Op: a.operators[c], Left: tree, Right: right} + } + + if outer != nil { + outer.Left = tree + return outer, nil + } + return tree, nil +} + +func (a *SCMA) parseTerm(lp *lines.Parse) (*expr.E, error) { + ex := &expr.E{} + top := ex + + // Unary minus: just wrap the current expression + if lp.Consume("-") { + top = &expr.E{Op: expr.OpMinus, Left: ex} + } + + // Current location + if lp.Consume("*") { + ex.Op = expr.OpLeaf + ex.Text = "*" + return top, nil + } + + // Hex + if lp.Consume("$") { + if !lp.AcceptRun(hexdigits) { + c := lp.Next() + return &expr.E{}, fmt.Errorf("Expecting hex number, found '%c' (%d)", c, c) + } + s := lp.Emit() + i, err := strconv.ParseUint(s, 16, 16) + if err != nil { + return &expr.E{}, fmt.Errorf("Invalid hex number: %s: %s", s, err) + } + ex.Op = expr.OpLeaf + ex.Val = uint16(i) + return top, nil + } + + // Decimal + if lp.AcceptRun(digits) { + s := lp.Emit() + i, err := strconv.ParseUint(s, 10, 16) + if err != nil { + return &expr.E{}, fmt.Errorf("Invalid number: %s: %s", s, err) + } + ex.Op = expr.OpLeaf + ex.Val = uint16(i) + return top, nil + } + + // Label + if !lp.AcceptRun(labelChars) { + c := lp.Next() + return &expr.E{}, fmt.Errorf("Expecting *, (hex) number, or label; found '%c' (%d)", c, c) + } + + ex.Op = expr.OpLeaf + ex.Text = lp.Emit() + return top, nil +} diff --git a/asm/flavors/simple_parse_test.go b/asm/flavors/simple_parse_test.go new file mode 100644 index 0000000..13ea953 --- /dev/null +++ b/asm/flavors/simple_parse_test.go @@ -0,0 +1,197 @@ +package flavors + +import ( + "encoding/hex" + "testing" + + "github.com/zellyn/go6502/asm" + "github.com/zellyn/go6502/asm/flavors/as65" + "github.com/zellyn/go6502/asm/flavors/merlin" + "github.com/zellyn/go6502/asm/flavors/scma" + "github.com/zellyn/go6502/asm/lines" +) + +func TestSimpleCommonFunctions(t *testing.T) { + ss := scma.New() + aa := as65.New() + mm := merlin.New() + + tests := []struct { + a asm.Flavor // assembler flavor + i string // input string + p string // printed instruction, expected + b string // bytes, expected + }{ + {ss, "* Comment", "{-}", ""}, + {ss, "* Comment", "{-}", ""}, + {aa, "; Comment", "{-}", ""}, + {mm, "* Comment", "{-}", ""}, + {ss, "Label", "{- 'Label'}", ""}, + {aa, "Label", "{- 'Label'}", ""}, + {mm, "Label", "{- 'Label'}", ""}, + {ss, " .IN FILE.NAME", "{inc 'FILE.NAME'}", ""}, + {ss, " .IN S.DEFS", "{inc 'S.DEFS'}", ""}, + {aa, ` include "FILE.NAME"`, "{inc 'FILE.NAME'}", ""}, + {mm, " PUT !FILE.NAME", "{inc 'FILE.NAME'}", ""}, + {ss, " .TI 76,Title here", "{-}", ""}, + {aa, ` title "Title here"`, "{-}", ""}, + {mm, ` TTL "Title here"`, "{-}", ""}, + {ss, " .TF OUT.BIN", "{-}", ""}, + {mm, " DSK OUTFILE", "{-}", ""}, + {mm, " SAV OUTFILE", "{-}", ""}, + {ss, " .OR $D000", "{org $d000}", ""}, + {aa, " org $D000", "{org $d000}", ""}, + {mm, " ORG $D000", "{org $d000}", ""}, + // {ss, " .TA *-1234", "{target (- * $04d2)}", ""}, + {ss, " .DA $1234", "{data $1234}", "3412"}, + {aa, " dw $1234", "{data $1234}", "3412"}, + {mm, " DW $1234", "{data $1234}", "3412"}, + {ss, " .DA/$1234,#$1234,$1234", "{data (msb $1234),(lsb $1234),$1234}", "12343412"}, + {ss, " ROL", "{ROL/a}", "2a"}, + {aa, " rol a", "{ROL/a}", "2a"}, + {mm, " ROL", "{ROL/a}", "2a"}, + {ss, " ROL Comment after two spaces", "{ROL/a}", "2a"}, + {ss, " ROL $1234", "{ROL/abs $1234}", "2e3412"}, + {aa, " rol $1234", "{ROL/abs $1234}", "2e3412"}, + {mm, " ROL $1234", "{ROL/abs $1234}", "2e3412"}, + {ss, " ROL $12", "{ROL/zp $0012}", "2612"}, + {aa, " rol $12", "{ROL/zp $0012}", "2612"}, + {mm, " ROL $12", "{ROL/zp $0012}", "2612"}, + {ss, " LDA #$12", "{LDA/imm (lsb $0012)}", "a912"}, + {aa, " lda #$12", "{LDA/imm (lsb $0012)}", "a912"}, + {mm, " LDA #$12", "{LDA/imm (lsb $0012)}", "a912"}, + {ss, " JMP $1234", "{JMP/abs $1234}", "4c3412"}, + {aa, " jmp $1234", "{JMP/abs $1234}", "4c3412"}, + {mm, " JMP $1234", "{JMP/abs $1234}", "4c3412"}, + {ss, " JMP ($1234)", "{JMP/ind $1234}", "6c3412"}, + {aa, " jmp ($1234)", "{JMP/ind $1234}", "6c3412"}, + {mm, " JMP ($1234)", "{JMP/ind $1234}", "6c3412"}, + {ss, " BEQ $2345", "{BEQ/rel $2345}", "f0fe"}, + {aa, " beq $2345", "{BEQ/rel $2345}", "f0fe"}, + {mm, " BEQ $2345", "{BEQ/rel $2345}", "f0fe"}, + {ss, " BEQ $2347", "{BEQ/rel $2347}", "f000"}, + {aa, " beq $2347", "{BEQ/rel $2347}", "f000"}, + {mm, " BEQ $2347", "{BEQ/rel $2347}", "f000"}, + {ss, " BEQ $2343", "{BEQ/rel $2343}", "f0fc"}, + {aa, " beq $2343", "{BEQ/rel $2343}", "f0fc"}, + {mm, " BEQ $2343", "{BEQ/rel $2343}", "f0fc"}, + {ss, " LDA $1234", "{LDA/abs $1234}", "ad3412"}, + {aa, " lda $1234", "{LDA/abs $1234}", "ad3412"}, + {mm, " LDA $1234", "{LDA/abs $1234}", "ad3412"}, + {ss, " LDA $1234,X", "{LDA/absX $1234}", "bd3412"}, + {aa, " lda $1234,x", "{LDA/absX $1234}", "bd3412"}, + {mm, " LDA $1234,X", "{LDA/absX $1234}", "bd3412"}, + {ss, " STA $1234,Y", "{STA/absY $1234}", "993412"}, + {aa, " sta $1234,y", "{STA/absY $1234}", "993412"}, + {mm, " STA $1234,Y", "{STA/absY $1234}", "993412"}, + {ss, " LDA $12", "{LDA/zp $0012}", "a512"}, + {aa, " lda $12", "{LDA/zp $0012}", "a512"}, + {mm, " LDA $12", "{LDA/zp $0012}", "a512"}, + {ss, " LDA $12,X", "{LDA/zpX $0012}", "b512"}, + {aa, " lda $12,x", "{LDA/zpX $0012}", "b512"}, + {mm, " LDA $12,X", "{LDA/zpX $0012}", "b512"}, + {ss, " LDX $12,Y", "{LDX/zpY $0012}", "b612"}, + {aa, " ldx $12,y", "{LDX/zpY $0012}", "b612"}, + {mm, " LDX $12,Y", "{LDX/zpY $0012}", "b612"}, + {ss, " LDA ($12),Y", "{LDA/indY $0012}", "b112"}, + {aa, " lda ($12),y", "{LDA/indY $0012}", "b112"}, + {mm, " LDA ($12),Y", "{LDA/indY $0012}", "b112"}, + {ss, " LDA ($12,X)", "{LDA/indX $0012}", "a112"}, + {aa, " lda ($12,x)", "{LDA/indX $0012}", "a112"}, + {mm, " LDA ($12,X)", "{LDA/indX $0012}", "a112"}, + {ss, ` .AS "ABC"`, "{data}", "414243"}, + {ss, ` .AT "ABC"`, "{data}", "4142c3"}, + {ss, ` .AS /ABC/`, "{data}", "414243"}, + {ss, ` .AT /ABC/`, "{data}", "4142c3"}, + {ss, ` .AS -"ABC"`, "{data}", "c1c2c3"}, + {ss, ` .AT -"ABC"`, "{data}", "c1c243"}, + {ss, ` .AS -dABCd`, "{data}", "c1c2c3"}, + {ss, ` .AT -dABCd`, "{data}", "c1c243"}, + {ss, " .HS 0001ffAb", "{data}", "0001ffab"}, + {ss, "A.B .EQ *-C.D", "{= 'A.B' (- * C.D)}", ""}, + {ss, " .BS $8", "{block $0008}", ""}, + {ss, " .DO A<$3", "{if (< A $0003)}", ""}, + {ss, " .ELSE", "{else}", ""}, + {ss, " .FIN", "{endif}", ""}, + {ss, "Label .MA", "{macro 'Label'}", ""}, + {ss, " .EM", "{endm}", ""}, + {ss, " .EN", "{end}", ""}, + {ss, `>SAM AB,$12,"A B","A, B, "" C"`, + `{call SAM {"AB", "$12", "A B", "A, B, \" C"}}`, ""}, + } + + // TODO(zellyn): Add tests for finalization of four SCMA directives: + // "Labels used in operand expressions after .OR, TA, .BS, + // and .EQ directives must be defined prior to use (to prevent an + // undefined or ambiguous location counter)." + + for i, tt := range tests { + // TODO(zellyn): Test AS65 and Merlin too. + if tt.a != ss { + continue + } + + // Initialize to a known state for testing. + tt.a.Clear() + tt.a.SetAddr(0x2345) + tt.a.Set("A.B", 0x6789) + tt.a.Set("C.D", 0x789a) + + inst, err := tt.a.ParseInstr(lines.NewSimple(tt.i)) + if err != nil { + t.Errorf(`%d. %T.ParseInstr("%s") => error: %s`, i, tt.a, tt.i, err) + continue + } + if inst.Line.Parse == nil { + t.Errorf("Got empty inst.Line.Parse on input '%s'", tt.i) + } + _, err = inst.Compute(tt.a, true, true) + if err != nil { + t.Errorf(`%d. %s.Compute(tt.a, true, true) => error: %s`, i, inst, err) + continue + } + if inst.String() != tt.p { + t.Errorf(`%d. %T.ParseInstr("%s") = %s; want %s`, i, tt.a, tt.i, inst.String(), tt.p) + continue + } + + if tt.b != "?" { + hx := hex.EncodeToString(inst.Data) + if hx != tt.b { + t.Fatalf(`%d. %T.ParseInstr("%s").Data = [%s]; want [%s]`, i, tt.a, tt.i, hx, tt.b) + } + } + } +} + +func TestSimpleErrors(t *testing.T) { + ss := scma.New() + aa := as65.New() + mm := merlin.New() + + tests := []struct { + a asm.Flavor // assembler flavor + i string // input string + }{ + + {ss, " LDA"}, // missing arg + {aa, " lda"}, // + {mm, " LDA"}, // + {aa, " rol"}, // missing arg (for assemblers that need "A") + {ss, " .DA $1234,"}, // data: trailing comma + {ss, `>MACRO "ABC`}, // macro: unclosed quote on arg + {ss, `>MACRO "ABC"$12`}, // macro: stuff after closing quote + } + + for i, tt := range tests { + // TODO(zellyn): Test AS65 and Merlin too. + if tt.a != ss { + continue + } + inst, err := tt.a.ParseInstr(lines.NewSimple(tt.i)) + if err == nil { + t.Errorf(`%d. %T.ParseInstr("%s") want err; got %s`, i, tt.a, tt.i, inst) + continue + } + } +} diff --git a/asm/flavors/todos.org b/asm/flavors/todos.org new file mode 100644 index 0000000..ec7008b --- /dev/null +++ b/asm/flavors/todos.org @@ -0,0 +1,3 @@ +* TODO only figure out what the opcode *looks* like in the assembler: more DRY +* TODO implement byte value check in simple instruction test +* TODO add line/character reporting to parse errors diff --git a/asm/inst/instruction.go b/asm/inst/instruction.go new file mode 100644 index 0000000..906a72f --- /dev/null +++ b/asm/inst/instruction.go @@ -0,0 +1,405 @@ +package inst + +import ( + "errors" + "fmt" + "strings" + + "github.com/zellyn/go6502/asm/context" + "github.com/zellyn/go6502/asm/expr" + "github.com/zellyn/go6502/asm/lines" + "github.com/zellyn/go6502/opcodes" +) + +type Type int + +const ( + TypeUnknown Type = iota + + TypeNone // No type (eg. just a label, just a comment, empty, ignored directive) + TypeMacroStart // Start of macro definition + TypeMacroEnd // End of macro definition + TypeMacroCall // Macro invocation + TypeMacroExit // Macro early exit + TypeIfdef // Ifdef start + TypeIfdefElse // Ifdef else block + TypeIfdefEnd // Ifdef end + TypeInclude // Include a file + TypeData // Data: hex, ascii, etc., etc. + TypeBlock // Block storage + TypeOrg // Where to store assembled code + TypeTarget // Target address to use for jumps, labels, etc. + TypeSegment // Segments: bss/code/data + TypeEqu // Equate + TypeOp // An actual asm opcode + TypeEnd // End assembly +) + +type I struct { + Type Type // Type of instruction + Label string // Text of label part + MacroArgs []string // Macro args + Command string // Text of command part + TextArg string // Text of argument part + Exprs []*expr.E // Expression(s) + Data []byte // Actual bytes + WidthKnown bool // Do we know how many bytes this instruction takes yet? + MinWidth uint16 // minimum width in bytes + MaxWidth uint16 // maximum width in bytes + Final bool // Do we know the actual bytes yet? + Op byte // Opcode + Mode opcodes.AddressingMode // Opcode mode + ZeroMode opcodes.AddressingMode // Possible ZP-option mode + ZeroOp byte // Possible ZP-option Opcode + Value uint16 // For Equates, the value + DeclaredLine uint16 + Line *lines.Line +} + +func (i I) TypeString() string { + switch i.Type { + case TypeNone: + return "-" + case TypeMacroStart: + return "macro" + case TypeMacroEnd: + return "endm" + case TypeMacroCall: + return "call " + i.Command + case TypeMacroExit: + return "exitm" + case TypeIfdef: + return "if" + case TypeIfdefElse: + return "else" + case TypeIfdefEnd: + return "endif" + case TypeInclude: + return "inc" + case TypeData: + return "data" + case TypeBlock: + return "block" + case TypeOrg: + return "org" + case TypeTarget: + return "target" + case TypeSegment: + return "seg" + case TypeEqu: + return "=" + case TypeEnd: + return "end" + case TypeOp: + modeStr := "?" + switch i.Mode { + case opcodes.MODE_IMPLIED: + modeStr = "imp" + case opcodes.MODE_ABSOLUTE: + modeStr = "abs" + case opcodes.MODE_INDIRECT: + modeStr = "ind" + case opcodes.MODE_RELATIVE: + modeStr = "rel" + case opcodes.MODE_IMMEDIATE: + modeStr = "imm" + case opcodes.MODE_ABS_X: + modeStr = "absX" + case opcodes.MODE_ABS_Y: + modeStr = "absY" + case opcodes.MODE_ZP: + modeStr = "zp" + case opcodes.MODE_ZP_X: + modeStr = "zpX" + case opcodes.MODE_ZP_Y: + modeStr = "zpY" + case opcodes.MODE_INDIRECT_Y: + modeStr = "indY" + case opcodes.MODE_INDIRECT_X: + modeStr = "indX" + case opcodes.MODE_A: + modeStr = "a" + + } + return fmt.Sprintf("%s/%s", i.Command, modeStr) + } + return "?" +} + +func (i I) String() string { + switch i.Type { + case TypeInclude: + return fmt.Sprintf("{inc '%s'}", i.TextArg) + } + s := "{" + i.TypeString() + if i.Label != "" { + s += fmt.Sprintf(" '%s'", i.Label) + } + if len(i.MacroArgs) > 0 { + ma := fmt.Sprintf("%#v", i.MacroArgs)[8:] + s += " " + ma + } + if len(i.Exprs) > 0 { + exprs := []string{} + for _, expr := range i.Exprs { + exprs = append(exprs, expr.String()) + } + s += " " + strings.Join(exprs, ",") + } + return s + "}" +} + +// Compute attempts to finalize the instruction. +func (i *I) Compute(c context.Context, setWidth bool, final bool) (bool, error) { + if i.Type == TypeEqu || i.Type == TypeTarget || i.Type == TypeOrg { + return i.computeMustKnow(c, setWidth, final) + } + if err := i.computeLabel(c, setWidth, final); err != nil { + return false, err + } + if i.Final { + return true, nil + } + switch i.Type { + case TypeOp: + return i.computeOp(c, setWidth, final) + case TypeData: + return i.computeData(c, setWidth, final) + case TypeBlock: + return i.computeBlock(c, setWidth, final) + } + + // Everything else is zero-width + i.WidthKnown = true + i.MinWidth = 0 + i.MaxWidth = 0 + i.Final = true + + return true, nil +} + +// FixLabels attempts to turn .1 into LAST_LABEL.1 +func (i *I) FixLabels(last string) error { + if i.Label != "" && i.Label[0] == '.' { + if last == "" { + return fmt.Errorf("Reference to sub-label '%s' before full label.", i.Label) + } + i.Label = last + i.Label + } + + for _, e := range i.Exprs { + if err := e.FixLabels(last); err != nil { + return err + } + } + + return nil +} + +// computeLabel attempts to compute equates and label values. +func (i *I) computeLabel(c context.Context, setWidth bool, final bool) error { + if i.Label == "" { + return nil + } + + if i.Type == TypeEqu { + panic("computeLabel should not be called for equates") + } + + addr, aok := c.GetAddr() + if !aok { + return nil + } + + lval, lok := c.Get(i.Label) + if lok && addr != lval { + return fmt.Errorf("Trying to set label '%s' to $%04x, but it already has value $%04x", i.Label, addr, lval) + } + c.Set(i.Label, addr) + return nil +} + +func (i *I) computeData(c context.Context, setWidth bool, final bool) (bool, error) { + if len(i.Data) > 0 { + i.WidthKnown = true + i.MinWidth = uint16(len(i.Data)) + i.MaxWidth = i.MinWidth + i.Final = true + return true, nil + } + + allFinal := true + data := []byte{} + var width uint16 + for _, e := range i.Exprs { + w := e.Width() + width += w + val, labelMissing, err := e.CheckedEval(c) + if err != nil && !labelMissing { + return false, err + } + if labelMissing { + allFinal = false + if final { + return false, err + } + } + switch w { + case 1: + data = append(data, byte(val)) + case 2: + data = append(data, byte(val), byte(val>>8)) + } + } + i.MinWidth = width + i.MaxWidth = width + i.WidthKnown = true + if allFinal { + i.Data = data + i.Final = true + } + + return i.Final, nil +} + +func (i *I) computeBlock(c context.Context, setWidth bool, final bool) (bool, error) { + val, err := i.Exprs[0].Eval(c) + if err == nil { + i.Value = val + i.WidthKnown = true + i.Final = true + i.MinWidth = val + i.MaxWidth = val + } else { + if setWidth || final { + return false, fmt.Errorf("block storage with unknown size") + } + } + return i.Final, nil +} + +func (i *I) computeMustKnow(c context.Context, setWidth bool, final bool) (bool, error) { + i.WidthKnown = true + i.MinWidth = 0 + i.MaxWidth = 0 + i.Final = true + val, err := i.Exprs[0].Eval(c) + if err != nil { + return false, err + } + i.Value = val + switch i.Type { + case TypeTarget: + return false, errors.New("Target not implemented yet.") + case TypeOrg: + c.SetAddr(val) + case TypeEqu: + c.Set(i.Label, val) + // Don't handle labels. + return true, nil + } + if err := i.computeLabel(c, setWidth, final); err != nil { + return false, err + } + return true, nil +} + +func (i *I) computeOp(c context.Context, setWidth bool, final bool) (bool, error) { + // If the width is not known, we better have a ZeroPage alternative. + if !i.WidthKnown && (i.ZeroOp == 0 || i.ZeroMode == 0) { + panic(fmt.Sprintf("Reached computeOp for '%s' with no ZeroPage alternative, i.Command")) + } + // An op with no args would be final already, so we must have an Expression. + if len(i.Exprs) == 0 { + panic(fmt.Sprintf("Reached computeOp for '%s' with no expressions", i.Command)) + } + val, labelMissing, err := i.Exprs[0].CheckedEval(c) + // A real error + if !labelMissing && err != nil { + return false, err + } + + if labelMissing { + // Don't know, do care. + if final { + return false, err + } + + // Already know enough. + if i.WidthKnown { + return false, nil + } + + // Do we know the width, even though the value is unknown? + if i.Exprs[0].Width() == 1 { + i.WidthKnown = true + i.MinWidth, i.MaxWidth = 2, 2 + i.Op, i.Mode = i.ZeroOp, i.ZeroMode + i.ZeroOp, i.ZeroMode = 0, 0 + return false, nil + } + + // Don't know the width, but don't care on this pass. + if !setWidth { + return false, nil + } + + // Okay, we have to set the width: since we don't know, go wide. + i.WidthKnown = true + i.MinWidth, i.MaxWidth = 3, 3 + i.ZeroOp, i.ZeroMode = 0, 0 + return false, nil + } + + // If we got here, we got an actual value. + + // We need to figure out the width + if !i.WidthKnown { + if val < 0x100 { + i.MinWidth = 2 + i.MaxWidth = 2 + i.Op = i.ZeroOp + i.Mode = i.ZeroMode + } else { + i.MinWidth = 3 + i.MaxWidth = 3 + + } + } + + // It's a branch + if i.Mode == opcodes.MODE_RELATIVE { + curr, ok := c.GetAddr() + if !ok { + if final { + return false, fmt.Errorf("Cannot determine current address for '%s'", i.Command) + } + return false, nil + } + // Found both current and target addresses + offset := int32(val) - (int32(curr) + 2) + if offset > 127 { + return false, fmt.Errorf("%s cannot jump forward %d (max 127)", i.Command, offset) + } + if offset < -128 { + return false, fmt.Errorf("%s cannot jump back %d (max -128)", i.Command, offset) + } + val = uint16(offset) + } + + i.WidthKnown = true + i.Final = true + i.ZeroOp = 0 + i.ZeroMode = 0 + + switch i.MinWidth { + case 2: + // TODO(zellyn): Warn if > 0xff + i.Data = []byte{i.Op, byte(val)} + case 3: + i.Data = []byte{i.Op, byte(val), byte(val >> 8)} + default: + panic(fmt.Sprintf("computeOp reached erroneously for '%s'", i.Command)) + } + return true, nil +} diff --git a/asm/inst/instruction_test.go b/asm/inst/instruction_test.go new file mode 100644 index 0000000..46ab51e --- /dev/null +++ b/asm/inst/instruction_test.go @@ -0,0 +1,82 @@ +package inst + +import ( + "testing" + + "github.com/zellyn/go6502/asm/context" + "github.com/zellyn/go6502/asm/expr" +) + +func TestComputeLabel(t *testing.T) { + i := I{ + Label: "L1", + } + c := &context.SimpleContext{} + i.computeLabel(c, false, false) +} + +func TestWidthDoesNotChange(t *testing.T) { + i := I{ + Type: TypeOp, + Command: "LDA", + Exprs: []*expr.E{ + &expr.E{ + Op: expr.OpMinus, + Left: &expr.E{Op: expr.OpLeaf, Text: "L1"}, + Right: &expr.E{Op: expr.OpLeaf, Text: "L2"}, + }, + }, + MinWidth: 0x2, + MaxWidth: 0x3, + Final: false, + Op: 0xad, + Mode: 0x2, + ZeroMode: 0x80, + ZeroOp: 0xa5, + } + c := &context.SimpleContext{} + c.Set("L1", 0x102) + final, err := i.Compute(c, false, false) + if err != nil { + t.Fatal(err) + } + if final { + t.Fatal("First pass shouldn't be able to finalize expression with unknown width") + } + + final, err = i.Compute(c, true, false) + if err != nil { + t.Fatal(err) + } + if final { + t.Fatal("Second pass shouldn't be able to finalize expression with unknown variable.") + } + if !i.WidthKnown { + t.Fatal("Second pass should have set width.") + } + if i.MinWidth != i.MaxWidth { + t.Fatalf("i.WidthKnown, but i.MinWidth(%d) != i.MaxWidth(%d)", i.MinWidth, i.MaxWidth) + } + if i.MinWidth != 3 { + t.Fatalf("i.MinWidth should be 3; got %d", i.MinWidth) + } + + c.Set("L2", 0x101) + + final, err = i.Compute(c, true, true) + if err != nil { + t.Fatal(err) + } + if !final { + t.Fatal("Third pass should be able to finalize expression.") + } + if !i.WidthKnown { + t.Fatal("Third pass should left width unchanged.") + } + if i.MinWidth != i.MaxWidth { + t.Fatalf("i.WidthKnown, but i.MinWidth(%d) != i.MaxWidth(%d)", i.MinWidth, i.MaxWidth) + } + if i.MinWidth != 3 { + t.Fatalf("i.MinWidth should still be 3; got %d", i.MinWidth) + } +} diff --git a/asm/lines/files.go b/asm/lines/files.go new file mode 100644 index 0000000..5d1299b --- /dev/null +++ b/asm/lines/files.go @@ -0,0 +1,80 @@ +package lines + +import ( + "bufio" + "fmt" + "io" + "io/ioutil" + "os" + "strings" +) + +type Opener interface { + Open(filename string) (io.ReadCloser, error) +} + +type OsOpener struct{} + +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} + file, err := opener.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + fls.lines = append(fls.lines, scanner.Text()) + } + + if err = scanner.Err(); err != nil { + return nil, err + } + + fls.size = len(fls.lines) + return fls, nil +} + +type TestOpener map[string]string + +func (o TestOpener) Open(filename string) (io.ReadCloser, error) { + contents, ok := o[filename] + if ok { + return ioutil.NopCloser(strings.NewReader(contents)), nil + } else { + return nil, fmt.Errorf("File not found: %s", filename) + } +} + +func (o TestOpener) Clear() { + for k := range o { + delete(o, k) + } +} + +func NewTestOpener() TestOpener { + return make(TestOpener) +} diff --git a/asm/lines/lineparse.go b/asm/lines/lineparse.go new file mode 100644 index 0000000..4a3cdc6 --- /dev/null +++ b/asm/lines/lineparse.go @@ -0,0 +1,118 @@ +package lines + +import ( + "strings" + "unicode" + "unicode/utf8" +) + +const Eol rune = '\n' + +/* +Parse is a struct representing the parse/lex of a single line. It's +based on the lexer presented in Rob Pike's "Lexical Scannign in Go" +talk: http://cuddle.googlecode.com/hg/talk/lex.html, but it's simpler, +because assembly files are entirely line-based. +*/ +type Parse struct { + line string + start int + pos int + width int +} + +func NewParse(text string) *Parse { + text = strings.TrimRightFunc(text, unicode.IsSpace) + return &Parse{line: text} +} + +func (lp *Parse) Emit() string { + emitted := lp.line[lp.start:lp.pos] + lp.start = lp.pos + return emitted +} + +func (lp *Parse) Ignore() { + lp.start = lp.pos +} + +func (lp *Parse) Next() (c rune) { + if lp.pos >= len(lp.line) { + lp.width = 0 + return Eol + } + c, lp.width = utf8.DecodeRuneInString(lp.line[lp.pos:]) + lp.pos += lp.width + return c +} + +func (lp *Parse) Backup() { + lp.pos -= lp.width +} + +func (lp *Parse) Peek() rune { + c := lp.Next() + lp.Backup() + return c +} + +func (lp *Parse) Accept(valid string) bool { + if strings.IndexRune(valid, lp.Next()) >= 0 { + return true + } + lp.Backup() + return false +} + +// Consume accepts and ignores a single character of input. +func (lp *Parse) Consume(valid string) bool { + if strings.IndexRune(valid, lp.Next()) >= 0 { + lp.start = lp.pos + return true + } + lp.Backup() + return false +} + +func (lp *Parse) AcceptRun(valid string) bool { + some := false + for strings.IndexRune(valid, lp.Next()) >= 0 { + some = true + } + lp.Backup() + return some +} + +func (lp *Parse) AcceptUntil(until string) bool { + until += "\n" + some := false + for strings.IndexRune(until, lp.Next()) < 0 { + some = true + } + lp.Backup() + return some +} + +func (lp *Parse) IgnoreRun(valid string) bool { + if lp.AcceptRun(valid) { + lp.Ignore() + return true + } + return false +} + +func (lp *Parse) AcceptString(prefix string) bool { + if strings.HasPrefix(lp.line[lp.pos:], prefix) { + lp.pos += len(prefix) + return true + } + return false +} + +func (lp *Parse) Rest() string { + return lp.line[lp.pos:] +} + +func (lp *Parse) Text() string { + return lp.line +} diff --git a/asm/lines/lines.go b/asm/lines/lines.go new file mode 100644 index 0000000..2f44741 --- /dev/null +++ b/asm/lines/lines.go @@ -0,0 +1,43 @@ +package lines + +import "fmt" + +type Context struct { + Filename string // Pointer to the filename + Parent *Line // Pointer to parent line (eg. include, macro) +} + +type Line struct { + LineNo int // Actual line number in file + DeclaredLineNo int // Declared line number in file, or 0 + Context *Context // Pointer to the file/include/macro context + Parse *Parse // The lineparser +} + +type LineSource interface { + Next() (line Line, done bool, err error) + Context() Context +} + +func NewLine(s string, lineNo int, context *Context) Line { + l := Line{ + LineNo: lineNo, + Context: context, + Parse: NewParse(s), + } + return l +} + +var testFilename = "(test)" + +func NewSimple(s string) Line { + return NewLine(s, 0, &Context{Filename: testFilename}) +} + +func (l Line) Text() string { + return l.Parse.Text() +} + +func (l Line) String() string { + return fmt.Sprintf("%s:%d: %s", l.Context.Filename, l.LineNo, l.Parse.Text()) +} diff --git a/cpu/cpu.go b/cpu/cpu.go index c83eddc..3705bd5 100644 --- a/cpu/cpu.go +++ b/cpu/cpu.go @@ -42,9 +42,7 @@ type Memory interface { } // Ticker interface, for keeping track of cycles. -type Ticker interface { - Tick() -} +type Ticker func() // Interrupt vectors. const ( @@ -146,7 +144,7 @@ func (c *cpu) Step() error { c.oldPC = c.r.PC i := c.m.Read(c.r.PC) c.r.PC++ - c.t.Tick() + c.t() if f, ok := Opcodes[i]; ok { f(c) diff --git a/cpu/opcodeinstructions.go b/cpu/opcodeinstructions.go index 6d3adda..c7551d4 100644 --- a/cpu/opcodeinstructions.go +++ b/cpu/opcodeinstructions.go @@ -24,7 +24,7 @@ func clearFlag(flag byte) func(*cpu) { return func(c *cpu) { c.r.P &^= flag c.m.Read(c.r.PC) - c.t.Tick() + c.t() } } @@ -34,7 +34,7 @@ func setFlag(flag byte) func(*cpu) { return func(c *cpu) { c.r.P |= flag c.m.Read(c.r.PC) - c.t.Tick() + c.t() } } @@ -45,12 +45,12 @@ func branch(mask, value byte) func(*cpu) { // T1 offset := c.m.Read(c.r.PC) c.r.PC++ - c.t.Tick() + c.t() // T2 oldPC := c.r.PC if c.r.P&mask == value { c.m.Read(oldPC) - c.t.Tick() + c.t() // T3 c.r.PC = c.r.PC + uint16(offset) if offset >= 128 { @@ -58,7 +58,7 @@ func branch(mask, value byte) func(*cpu) { } if !samePage(c.r.PC, oldPC) { c.m.Read((oldPC & 0xFF00) | (c.r.PC & 0x00FF)) - c.t.Tick() + c.t() } } } @@ -121,7 +121,7 @@ func adc_d(c *cpu, value byte) { c.r.P |= FLAG_Z } case VERSION_65C02: - c.t.Tick() + c.t() c.setNZ(byte(a & 0xFF)) default: panic("Unknown chip version") @@ -155,27 +155,27 @@ func brk(c *cpu) { // T1 c.m.Read(c.r.PC) c.r.PC++ - c.t.Tick() + c.t() // T2 c.m.Write(0x100+uint16(c.r.SP), byte(c.r.PC>>8)) c.r.SP-- - c.t.Tick() + c.t() // T3 c.m.Write(0x100+uint16(c.r.SP), byte(c.r.PC&0xff)) c.r.SP-- - c.t.Tick() + c.t() // T4 c.m.Write(0x100+uint16(c.r.SP), c.r.P|FLAG_B) // Set B flag c.r.SP-- c.r.P |= FLAG_I // Disable interrupts - c.t.Tick() + c.t() // T5 addr := uint16(c.m.Read(IRQ_VECTOR)) - c.t.Tick() + c.t() // T6 addr |= (uint16(c.m.Read(IRQ_VECTOR+1)) << 8) c.r.PC = addr - c.t.Tick() + c.t() } func cmp(c *cpu, value byte) { @@ -215,14 +215,14 @@ func dex(c *cpu) { c.r.X-- c.setNZ(c.r.X) c.m.Read(c.r.PC) - c.t.Tick() + c.t() } func dey(c *cpu) { c.r.Y-- c.setNZ(c.r.Y) c.m.Read(c.r.PC) - c.t.Tick() + c.t() } func eor(c *cpu, value byte) { @@ -240,40 +240,40 @@ func inx(c *cpu) { c.r.X++ c.setNZ(c.r.X) c.m.Read(c.r.PC) - c.t.Tick() + c.t() } func iny(c *cpu) { c.r.Y++ c.setNZ(c.r.Y) c.m.Read(c.r.PC) - c.t.Tick() + c.t() } func jmpAbsolute(c *cpu) { // T1 addr := uint16(c.m.Read(c.r.PC)) c.r.PC++ - c.t.Tick() + c.t() // T2 addr |= (uint16(c.m.Read(c.r.PC)) << 8) c.r.PC++ c.r.PC = addr - c.t.Tick() + c.t() } func jmpIndirect(c *cpu) { // T1 iAddr := uint16(c.m.Read(c.r.PC)) c.r.PC++ - c.t.Tick() + c.t() // T2 iAddr |= (uint16(c.m.Read(c.r.PC)) << 8) c.r.PC++ - c.t.Tick() + c.t() // T3 addr := uint16(c.m.Read(iAddr)) - c.t.Tick() + c.t() // T4 // 6502 jumps to (xxFF,xx00) instead of (xxFF,xxFF+1). // See http://en.wikipedia.org/wiki/MOS_Technology_6502#Bugs_and_quirks @@ -290,29 +290,29 @@ func jmpIndirect(c *cpu) { panic("Unknown chip version") } c.r.PC = addr - c.t.Tick() + c.t() } func jsr(c *cpu) { // T1 addr := uint16(c.m.Read(c.r.PC)) // We actually push PC(next) - 1 c.r.PC++ - c.t.Tick() + c.t() // T2 c.m.Read(0x100 + uint16(c.r.SP)) // Ignored read on stack - c.t.Tick() + c.t() // T3 c.m.Write(0x100+uint16(c.r.SP), byte(c.r.PC>>8)) // Write PC|hi to stack c.r.SP-- - c.t.Tick() + c.t() // T4 c.m.Write(0x100+uint16(c.r.SP), byte(c.r.PC&0xff)) // Write PC|lo to stack c.r.SP-- - c.t.Tick() + c.t() // T5 addr |= (uint16(c.m.Read(c.r.PC)) << 8) c.r.PC = addr - c.t.Tick() + c.t() } func lda(c *cpu, value byte) { @@ -344,44 +344,44 @@ func ora(c *cpu, value byte) { func nop(c *cpu) { c.m.Read(c.r.PC) - c.t.Tick() + c.t() } func pha(c *cpu) { c.m.Read(c.r.PC) - c.t.Tick() + c.t() c.m.Write(0x100+uint16(c.r.SP), c.r.A) c.r.SP-- - c.t.Tick() + c.t() } func pla(c *cpu) { c.m.Read(c.r.PC) - c.t.Tick() + c.t() c.m.Read(0x100 + uint16(c.r.SP)) c.r.SP++ - c.t.Tick() + c.t() c.r.A = c.m.Read(0x100 + uint16(c.r.SP)) c.setNZ(c.r.A) - c.t.Tick() + c.t() } func php(c *cpu) { c.m.Read(c.r.PC) - c.t.Tick() + c.t() c.m.Write(0x100+uint16(c.r.SP), c.r.P) c.r.SP-- - c.t.Tick() + c.t() } func plp(c *cpu) { c.m.Read(c.r.PC) - c.t.Tick() + c.t() c.m.Read(0x100 + uint16(c.r.SP)) c.r.SP++ - c.t.Tick() + c.t() c.r.P = c.m.Read(0x100+uint16(c.r.SP)) | FLAG_UNUSED | FLAG_B - c.t.Tick() + c.t() } func rol(c *cpu, value byte) byte { @@ -401,43 +401,43 @@ func ror(c *cpu, value byte) byte { func rts(c *cpu) { // T1 c.m.Read(c.r.PC) - c.t.Tick() + c.t() // T2 c.m.Read(0x100 + uint16(c.r.SP)) c.r.SP++ - c.t.Tick() + c.t() // T3 addr := uint16(c.m.Read(0x100 + uint16(c.r.SP))) c.r.SP++ - c.t.Tick() + c.t() // T4 addr |= (uint16(c.m.Read(0x100+uint16(c.r.SP))) << 8) - c.t.Tick() + c.t() // T5 c.m.Read(addr) c.r.PC = addr + 1 // Since we pushed PC(next) - 1 - c.t.Tick() + c.t() } func rti(c *cpu) { // T1 c.m.Read(c.r.PC) - c.t.Tick() + c.t() // T2 c.m.Read(0x100 + uint16(c.r.SP)) c.r.SP++ - c.t.Tick() + c.t() // T3 c.r.P = c.m.Read(0x100+uint16(c.r.SP)) | FLAG_UNUSED c.r.SP++ // T4 addr := uint16(c.m.Read(0x100 + uint16(c.r.SP))) c.r.SP++ - c.t.Tick() + c.t() // T5 addr |= (uint16(c.m.Read(0x100+uint16(c.r.SP))) << 8) c.r.PC = addr - c.t.Tick() + c.t() } func sbc(c *cpu, value byte) { @@ -502,7 +502,7 @@ func sbc_d(c *cpu, value byte) { } // fmt.Printf(" a=$%04X ($%02X)\n", a, byte(a)) c.r.A = byte(a) - c.t.Tick() + c.t() c.setNZ(c.r.A) default: panic("Unknown chip version") @@ -525,39 +525,39 @@ func tax(c *cpu) { c.r.X = c.r.A c.setNZ(c.r.X) c.m.Read(c.r.PC) - c.t.Tick() + c.t() } func tay(c *cpu) { c.r.Y = c.r.A c.setNZ(c.r.Y) c.m.Read(c.r.PC) - c.t.Tick() + c.t() } func tsx(c *cpu) { c.r.X = c.r.SP c.setNZ(c.r.X) c.m.Read(c.r.PC) - c.t.Tick() + c.t() } func txa(c *cpu) { c.r.A = c.r.X c.setNZ(c.r.A) c.m.Read(c.r.PC) - c.t.Tick() + c.t() } func txs(c *cpu) { c.r.SP = c.r.X c.m.Read(c.r.PC) - c.t.Tick() + c.t() } func tya(c *cpu) { c.r.A = c.r.Y c.setNZ(c.r.A) c.m.Read(c.r.PC) - c.t.Tick() + c.t() } diff --git a/cpu/opcodemodes.go b/cpu/opcodemodes.go index d657a05..5eacda9 100644 --- a/cpu/opcodemodes.go +++ b/cpu/opcodemodes.go @@ -19,7 +19,7 @@ func immediate2(f func(*cpu, byte)) func(*cpu) { value := c.m.Read(c.r.PC) c.r.PC++ f(c, value) - c.t.Tick() + c.t() } } @@ -29,15 +29,15 @@ func absolute4r(f func(*cpu, byte)) func(*cpu) { // T1 addr := uint16(c.m.Read(c.r.PC)) c.r.PC++ - c.t.Tick() + c.t() // T2 addr |= (uint16(c.m.Read(c.r.PC)) << 8) c.r.PC++ - c.t.Tick() + c.t() // T3 value := c.m.Read(addr) f(c, value) - c.t.Tick() + c.t() } } @@ -47,14 +47,14 @@ func absolute4w(f func(*cpu) byte) func(*cpu) { // T1 addr := uint16(c.m.Read(c.r.PC)) c.r.PC++ - c.t.Tick() + c.t() // T2 addr |= (uint16(c.m.Read(c.r.PC)) << 8) c.r.PC++ - c.t.Tick() + c.t() // T3 c.m.Write(addr, f(c)) - c.t.Tick() + c.t() } } @@ -64,11 +64,11 @@ func zp3r(f func(*cpu, byte)) func(*cpu) { // T1 addr := uint16(c.m.Read(c.r.PC)) c.r.PC++ - c.t.Tick() + c.t() // T2 value := c.m.Read(addr) f(c, value) - c.t.Tick() + c.t() } } @@ -78,10 +78,10 @@ func zp3w(f func(*cpu) byte) func(*cpu) { // T1 addr := uint16(c.m.Read(c.r.PC)) c.r.PC++ - c.t.Tick() + c.t() // T2 c.m.Write(addr, f(c)) - c.t.Tick() + c.t() } } @@ -91,21 +91,21 @@ func absx4r(f func(*cpu, byte)) func(*cpu) { // T1 addr := uint16(c.m.Read(c.r.PC)) c.r.PC++ - c.t.Tick() + c.t() // T2 addr |= (uint16(c.m.Read(c.r.PC)) << 8) addrX := addr + uint16(c.r.X) c.r.PC++ - c.t.Tick() + c.t() // T3 if !samePage(addr, addrX) { c.m.Read(addrX - 0x100) - c.t.Tick() + c.t() } // T3(cotd.) or T4 value := c.m.Read(addrX) f(c, value) - c.t.Tick() + c.t() } } @@ -115,21 +115,21 @@ func absy4r(f func(*cpu, byte)) func(*cpu) { // T1 addr := uint16(c.m.Read(c.r.PC)) c.r.PC++ - c.t.Tick() + c.t() // T2 addr |= (uint16(c.m.Read(c.r.PC)) << 8) addrY := addr + uint16(c.r.Y) c.r.PC++ - c.t.Tick() + c.t() // T3 if !samePage(addr, addrY) { c.m.Read(addrY - 0x100) - c.t.Tick() + c.t() } // T3(cotd.) or T4 value := c.m.Read(addrY) f(c, value) - c.t.Tick() + c.t() } } @@ -139,18 +139,18 @@ func absx5w(f func(*cpu) byte) func(*cpu) { // T1 addr := uint16(c.m.Read(c.r.PC)) c.r.PC++ - c.t.Tick() + c.t() // T2 addr |= (uint16(c.m.Read(c.r.PC)) << 8) addrX := addr + uint16(c.r.X) c.r.PC++ - c.t.Tick() + c.t() // T3 c.m.Read((addr & 0xFF00) | (addrX & 0x00FF)) - c.t.Tick() + c.t() // T4 c.m.Write(addrX, f(c)) - c.t.Tick() + c.t() } } @@ -160,18 +160,18 @@ func absy5w(f func(*cpu) byte) func(*cpu) { // T1 addr := uint16(c.m.Read(c.r.PC)) c.r.PC++ - c.t.Tick() + c.t() // T2 addr |= (uint16(c.m.Read(c.r.PC)) << 8) addrY := addr + uint16(c.r.Y) c.r.PC++ - c.t.Tick() + c.t() // T3 c.m.Read((addr & 0xFF00) | (addrY & 0x00FF)) - c.t.Tick() + c.t() // T4 c.m.Write(addrY, f(c)) - c.t.Tick() + c.t() } } @@ -182,14 +182,14 @@ func zpx4r(f func(*cpu, byte)) func(*cpu) { addr := c.m.Read(c.r.PC) addrX := uint16(addr + c.r.X) c.r.PC++ - c.t.Tick() + c.t() // T2 c.m.Read(uint16(addr)) - c.t.Tick() + c.t() // T3 value := c.m.Read(addrX) f(c, value) - c.t.Tick() + c.t() } } @@ -200,13 +200,13 @@ func zpx4w(f func(*cpu) byte) func(*cpu) { addr := c.m.Read(c.r.PC) addrX := uint16(addr + c.r.X) c.r.PC++ - c.t.Tick() + c.t() // T2 c.m.Read(uint16(addr)) - c.t.Tick() + c.t() // T3 c.m.Write(uint16(addrX), f(c)) - c.t.Tick() + c.t() } } @@ -217,14 +217,14 @@ func zpy4r(f func(*cpu, byte)) func(*cpu) { addr := c.m.Read(c.r.PC) addrY := uint16(addr + c.r.Y) c.r.PC++ - c.t.Tick() + c.t() // T2 c.m.Read(uint16(addr)) - c.t.Tick() + c.t() // T3 value := c.m.Read(uint16(addrY)) f(c, value) - c.t.Tick() + c.t() } } @@ -235,13 +235,13 @@ func zpy4w(f func(*cpu) byte) func(*cpu) { addr := c.m.Read(c.r.PC) addrY := uint16(addr + c.r.Y) c.r.PC++ - c.t.Tick() + c.t() // T2 c.m.Read(uint16(addr)) - c.t.Tick() + c.t() // T3 c.m.Write(addrY, f(c)) - c.t.Tick() + c.t() } } @@ -251,23 +251,23 @@ func zpiy5r(f func(*cpu, byte)) func(*cpu) { // T1 iAddr := c.m.Read(c.r.PC) c.r.PC++ - c.t.Tick() + c.t() // T2 addr := uint16(c.m.Read(uint16(iAddr))) - c.t.Tick() + c.t() // T3 addr |= (uint16(c.m.Read(uint16(iAddr+1))) << 8) addrY := addr + uint16(c.r.Y) - c.t.Tick() + c.t() // T4 if !samePage(addr, addrY) { c.m.Read((addr & 0xFF00) | (addrY & 0x00FF)) - c.t.Tick() + c.t() } // T4(cotd.) or T5 value := c.m.Read(addr + uint16(c.r.Y)) f(c, value) - c.t.Tick() + c.t() } } @@ -277,20 +277,20 @@ func zpiy6w(f func(*cpu) byte) func(*cpu) { // T1 iAddr := c.m.Read(c.r.PC) c.r.PC++ - c.t.Tick() + c.t() // T2 addr := uint16(uint16(c.m.Read(uint16(iAddr)))) - c.t.Tick() + c.t() // T3 addr |= (uint16(c.m.Read(uint16(iAddr+1))) << 8) addrY := addr + uint16(c.r.Y) - c.t.Tick() + c.t() // T4 c.m.Read((addr & 0xFF00) | (addrY & 0x00FF)) - c.t.Tick() + c.t() // T5 c.m.Write(addr+uint16(c.r.Y), f(c)) - c.t.Tick() + c.t() } } @@ -300,20 +300,20 @@ func zpxi6r(f func(*cpu, byte)) func(*cpu) { // T1 iAddr := c.m.Read(c.r.PC) c.r.PC++ - c.t.Tick() + c.t() // T2 c.m.Read(uint16(iAddr)) - c.t.Tick() + c.t() // T3 addr := uint16(uint16(c.m.Read(uint16(iAddr + c.r.X)))) - c.t.Tick() + c.t() // T4 addr |= (uint16(c.m.Read(uint16(iAddr+c.r.X+1))) << 8) - c.t.Tick() + c.t() // T5 value := c.m.Read(addr) f(c, value) - c.t.Tick() + c.t() } } @@ -323,19 +323,19 @@ func zpxi6w(f func(*cpu) byte) func(*cpu) { // T1 iAddr := c.m.Read(c.r.PC) c.r.PC++ - c.t.Tick() + c.t() // T2 c.m.Read(uint16(iAddr)) - c.t.Tick() + c.t() // T3 addr := uint16(uint16(c.m.Read(uint16(iAddr + c.r.X)))) - c.t.Tick() + c.t() // T4 addr |= (uint16(c.m.Read(uint16(iAddr+c.r.X+1))) << 8) - c.t.Tick() + c.t() // T5 c.m.Write(addr, f(c)) - c.t.Tick() + c.t() } } @@ -345,7 +345,7 @@ func acc2rmw(f func(*cpu, byte) byte) func(*cpu) { // T1 c.m.Read(c.r.PC) c.r.A = f(c, c.r.A) - c.t.Tick() + c.t() } } @@ -355,16 +355,16 @@ func zp5rmw(f func(*cpu, byte) byte) func(*cpu) { // T1 addr := uint16(c.m.Read(c.r.PC)) c.r.PC++ - c.t.Tick() + c.t() // T2 value := c.m.Read(addr) - c.t.Tick() + c.t() // T3 c.m.Write(addr, value) - c.t.Tick() + c.t() // T4 c.m.Write(addr, f(c, value)) - c.t.Tick() + c.t() } } @@ -374,20 +374,20 @@ func abs6rmw(f func(*cpu, byte) byte) func(*cpu) { // T1 addr := uint16(c.m.Read(c.r.PC)) c.r.PC++ - c.t.Tick() + c.t() // T2 addr |= (uint16(c.m.Read(c.r.PC)) << 8) c.r.PC++ - c.t.Tick() + c.t() // T3 value := c.m.Read(addr) - c.t.Tick() + c.t() // T4 c.m.Write(addr, value) // Spurious write... - c.t.Tick() + c.t() // T5 c.m.Write(addr, f(c, value)) - c.t.Tick() + c.t() } } @@ -397,20 +397,20 @@ func zpx6rmw(f func(*cpu, byte) byte) func(*cpu) { // T1 addr8 := c.m.Read(c.r.PC) c.r.PC++ - c.t.Tick() + c.t() // T2 c.m.Read(uint16(addr8)) - c.t.Tick() + c.t() // T3 addr := uint16(addr8 + c.r.X) value := c.m.Read(addr) - c.t.Tick() + c.t() // T4 c.m.Write(addr, value) - c.t.Tick() + c.t() // T5 c.m.Write(addr, f(c, value)) - c.t.Tick() + c.t() } } @@ -420,23 +420,23 @@ func absx7rmw(f func(*cpu, byte) byte) func(*cpu) { // T1 addr := uint16(c.m.Read(c.r.PC)) c.r.PC++ - c.t.Tick() + c.t() // T2 addr |= (uint16(c.m.Read(c.r.PC)) << 8) addr += uint16(c.r.X) c.r.PC++ - c.t.Tick() + c.t() // T3 c.m.Read(addr) - c.t.Tick() + c.t() // T4 value := c.m.Read(addr) - c.t.Tick() + c.t() // T5 c.m.Write(addr, value) // Spurious write - c.t.Tick() + c.t() // T6 c.m.Write(addr, f(c, value)) - c.t.Tick() + c.t() } } diff --git a/opcodes/opcodes.go b/opcodes/opcodes.go index f2cd1ab..cff9fdc 100644 --- a/opcodes/opcodes.go +++ b/opcodes/opcodes.go @@ -6,10 +6,12 @@ function tables. package opcodes // Opcode addressing modes. -type AddressingMode int +type AddressingMode uint + +const MODE_UNKNOWN = 0 const ( - MODE_IMPLIED AddressingMode = iota + MODE_IMPLIED AddressingMode = 1 << iota MODE_ABSOLUTE MODE_INDIRECT MODE_RELATIVE @@ -24,6 +26,9 @@ const ( MODE_A ) +// Logical OR of the three indirect modes +const MODE_INDIRECT_ANY AddressingMode = MODE_INDIRECT | MODE_INDIRECT_X | MODE_INDIRECT_Y + // Opcode read/write semantics: does the opcode read, write, or // rmw. Useful to distinguish between instructions further than just // addressing mode. @@ -162,20 +167,20 @@ var Opcodes = map[byte]Opcode{ // 3-opcode, 4*-cycle abs,X/Y 0x1D: {"ORA", MODE_ABS_X, RW_R}, - 0x19: {"ORA", MODE_ABS_X, RW_R}, - 0x39: {"AND", MODE_ABS_X, RW_R}, + 0x19: {"ORA", MODE_ABS_Y, RW_R}, + 0x39: {"AND", MODE_ABS_Y, RW_R}, 0x3D: {"AND", MODE_ABS_X, RW_R}, - 0x59: {"EOR", MODE_ABS_X, RW_R}, + 0x59: {"EOR", MODE_ABS_Y, RW_R}, 0x5D: {"EOR", MODE_ABS_X, RW_R}, - 0x79: {"ADC", MODE_ABS_X, RW_R}, + 0x79: {"ADC", MODE_ABS_Y, RW_R}, 0x7D: {"ADC", MODE_ABS_X, RW_R}, + 0xB9: {"LDA", MODE_ABS_Y, RW_R}, 0xBD: {"LDA", MODE_ABS_X, RW_R}, - 0xB9: {"LDA", MODE_ABS_X, RW_R}, - 0xD9: {"CMP", MODE_ABS_X, RW_R}, + 0xD9: {"CMP", MODE_ABS_Y, RW_R}, 0xDD: {"CMP", MODE_ABS_X, RW_R}, - 0xF9: {"SBC", MODE_ABS_X, RW_R}, + 0xF9: {"SBC", MODE_ABS_Y, RW_R}, 0xFD: {"SBC", MODE_ABS_X, RW_R}, - 0xBE: {"LDX", MODE_ABS_X, RW_R}, + 0xBE: {"LDX", MODE_ABS_Y, RW_R}, 0xBC: {"LDY", MODE_ABS_X, RW_R}, // 3-opcode, 5-cycle abs,X/Y @@ -254,3 +259,42 @@ var Opcodes = map[byte]Opcode{ 0xDE: {"DEC", MODE_ABS_X, RW_RMW}, 0xFE: {"INC", MODE_ABS_X, RW_RMW}, } + +// Information for lookup of opcodes by name and mode ------------------------- + +type OpInfo struct { + Mode AddressingMode + Length int + Byte byte +} + +type OpSummary struct { + Modes AddressingMode // bitmask of supported modes + Ops []OpInfo +} + +var ByName map[string]OpSummary + +func init() { + ByName = make(map[string]OpSummary) + for b, oc := range Opcodes { + info := OpInfo{oc.Mode, ModeLengths[oc.Mode], b} + summary := ByName[oc.Name] + summary.Modes |= oc.Mode + summary.Ops = append(summary.Ops, info) + ByName[oc.Name] = summary + } +} + +func (s OpSummary) AnyModes(modes AddressingMode) bool { + return modes&s.Modes != 0 +} + +func (s OpSummary) OpForMode(mode AddressingMode) (OpInfo, bool) { + for _, o := range s.Ops { + if o.Mode == mode { + return o, true + } + } + return OpInfo{}, false +} diff --git a/tests/compare_test.go b/tests/compare_test.go index acb7b52..c41347f 100644 --- a/tests/compare_test.go +++ b/tests/compare_test.go @@ -51,7 +51,7 @@ func TestFunctionalTestCompare(t *testing.T) { } var cc CycleCount - c := cpu.NewCPU(&m, &cc, cpu.VERSION_6502) + c := cpu.NewCPU(&m, cc.Tick, cpu.VERSION_6502) c.Reset() m.Reset(MODE_RECORD) diff --git a/tests/functional_test.go b/tests/functional_test.go index 4cb9a8d..b35c2d4 100644 --- a/tests/functional_test.go +++ b/tests/functional_test.go @@ -24,7 +24,7 @@ func (m *K64) Write(address uint16, value byte) { m[address] = value } -// Cycle counter for the tests. Satisfies the cpu.Ticker interface. +// Cycle counter for the tests. type CycleCount uint64 func (c *CycleCount) Tick() { @@ -60,7 +60,7 @@ func TestFunctionalTestInstructions(t *testing.T) { var cc CycleCount OFFSET := 0xa copy(m[OFFSET:len(bytes)+OFFSET], bytes) - c := cpu.NewCPU(&m, &cc, cpu.VERSION_6502) + c := cpu.NewCPU(&m, cc.Tick, cpu.VERSION_6502) c.Reset() c.SetPC(0x1000) for { @@ -150,7 +150,7 @@ func TestDecimalMode6502(t *testing.T) { OFFSET := 0x1000 copy(m[OFFSET:len(bytes)+OFFSET], bytes) m[1] = 0 // 6502 - c := cpu.NewCPU(&m, &cc, cpu.VERSION_6502) + c := cpu.NewCPU(&m, cc.Tick, cpu.VERSION_6502) c.Reset() c.SetPC(0x1000) for { @@ -186,7 +186,7 @@ func TestDecimalMode65C02(t *testing.T) { OFFSET := 0x1000 copy(m[OFFSET:len(bytes)+OFFSET], bytes) m[1] = 1 // 65C02 - c := cpu.NewCPU(&m, &cc, cpu.VERSION_65C02) + c := cpu.NewCPU(&m, cc.Tick, cpu.VERSION_65C02) c.Reset() c.SetPC(0x1000) for {