From d37e16b3168dfc1bbececed22ef916fed7dc1ab1 Mon Sep 17 00:00:00 2001 From: jenska Date: Thu, 3 Sep 2026 22:02:52 +0200 Subject: [PATCH] Modernize to Go 1.27 and split the parser - Bump the module and CI matrix to Go 1.27; update README badge/instructions. - Adopt current standard-library idioms across the codebase: maps.Copy, slices.Contains, slices.Clone, min(), strings.Cut, errors.AsType[*Error], and range-over-int in tests. Verified clean with gopls modernize. - Merge the duplicated size-validation helpers into a single sizeAllowed(allowed, sz) with an empty-list guard. - Split the 1.4k-line internal/asm/parser.go into focused files (parser_labels, parser_stmt, parser_macro, parser_operand, parser_ea); pure code movement, no behavior change. - gofmt the previously unformatted additional_api_test.go. Public API is unchanged. go build/vet/test (incl. e2e), benchmark compilation, and a bin/srec/elf CLI smoke test all pass. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 2 +- CHANGELOG.md | 8 + additional_api_test.go | 8 +- canonical.go | 6 +- go.mod | 2 +- internal/asm/asm_bench_test.go | 2 +- internal/asm/assemble.go | 14 +- internal/asm/elf.go | 9 +- internal/asm/elf_test.go | 2 +- internal/asm/instructions/table.go | 9 +- internal/asm/parser.go | 1143 +--------------------------- internal/asm/parser_ea.go | 385 ++++++++++ internal/asm/parser_labels.go | 110 +++ internal/asm/parser_macro.go | 125 +++ internal/asm/parser_operand.go | 311 ++++++++ internal/asm/parser_stmt.go | 225 ++++++ internal/asm/srec.go | 5 +- metadata_api.go | 10 +- readme.md | 4 +- 19 files changed, 1206 insertions(+), 1174 deletions(-) create mode 100644 internal/asm/parser_ea.go create mode 100644 internal/asm/parser_labels.go create mode 100644 internal/asm/parser_macro.go create mode 100644 internal/asm/parser_operand.go create mode 100644 internal/asm/parser_stmt.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e42b89..8c794be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: [ '1.26.x' ] + go-version: [ '1.27.x' ] steps: - name: Checkout uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6af4e35..bc4baa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed + +- Raised the minimum supported Go version to 1.27 and aligned CI with the new toolchain target +- Adopted Go 1.21+/1.27 standard-library idioms across the codebase (`slices`, `maps`, `min`, `strings.Cut`, `errors.AsType`, range-over-int) +- Split the monolithic parser into focused files (labels, statements, macros, operands, effective addresses) and de-duplicated the size-validation helper + ## [1.3.2] - 2026-06-13 ### Fixed diff --git a/additional_api_test.go b/additional_api_test.go index 5736141..0ca8b9a 100644 --- a/additional_api_test.go +++ b/additional_api_test.go @@ -134,13 +134,13 @@ func TestFileAndFormatWrappers(t *testing.T) { elfSrc := ".org 0x1000\nstart:\n.byte 0xAA\nMOVEQ #1,D0\n" for name, fn := range map[string]func() ([]byte, error){ - "AssembleELF": func() ([]byte, error) { return AssembleELF(strings.NewReader(elfSrc)) }, - "AssembleBytesELF": func() ([]byte, error) { return AssembleBytesELF([]byte(elfSrc)) }, + "AssembleELF": func() ([]byte, error) { return AssembleELF(strings.NewReader(elfSrc)) }, + "AssembleBytesELF": func() ([]byte, error) { return AssembleBytesELF([]byte(elfSrc)) }, "AssembleBytesELFWithOptions": func() ([]byte, error) { return AssembleBytesELFWithOptions([]byte(".org 0x1000\n.byte FOO\n"), opts) }, - "AssembleStringELF": func() ([]byte, error) { return AssembleStringELF(elfSrc) }, - "AssembleFileELF": func() ([]byte, error) { return AssembleFileELF(writeTempSource(t, elfSrc)) }, + "AssembleStringELF": func() ([]byte, error) { return AssembleStringELF(elfSrc) }, + "AssembleFileELF": func() ([]byte, error) { return AssembleFileELF(writeTempSource(t, elfSrc)) }, "AssembleFileELFWithOptions": func() ([]byte, error) { return AssembleFileELFWithOptions(writeTempSource(t, ".org 0x1000\n.byte FOO\n"), opts) }, diff --git a/canonical.go b/canonical.go index f07380e..b0dc094 100644 --- a/canonical.go +++ b/canonical.go @@ -196,7 +196,11 @@ func formatIndexRegister(ix instructions.EAIndex) string { if ix.IsA { reg = formatAddrRegister(ix.Reg) } - out := reg + "." + map[bool]string{false: "W", true: "L"}[ix.Long] + width := "W" + if ix.Long { + width = "L" + } + out := reg + "." + width if ix.Scale > 1 { out += fmt.Sprintf("*%d", ix.Scale) } diff --git a/go.mod b/go.mod index df544b2..9ff4a63 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/jenska/m68kasm -go 1.26 +go 1.27 diff --git a/internal/asm/asm_bench_test.go b/internal/asm/asm_bench_test.go index 1fb9f7e..e4967a0 100644 --- a/internal/asm/asm_bench_test.go +++ b/internal/asm/asm_bench_test.go @@ -92,7 +92,7 @@ func buildBenchmarkSource(blocks int) string { sb.Grow(blocks * 64) sb.WriteString(".org 0\n") - for i := 0; i < blocks; i++ { + for i := range blocks { sb.WriteString(fmt.Sprintf("label%d: moveq #%d,d0\n", i, i%8)) sb.WriteString("nop\n") diff --git a/internal/asm/assemble.go b/internal/asm/assemble.go index 70910e8..8f3b604 100644 --- a/internal/asm/assemble.go +++ b/internal/asm/assemble.go @@ -3,6 +3,7 @@ package asm import ( "fmt" "io" + "slices" "github.com/jenska/m68kasm/internal/asm/instructions" ) @@ -169,14 +170,11 @@ func selectForm(def *instructions.InstrDef, ins *Instr, actual []instructions.Op return nil, fmt.Errorf("no form matches operands/size for %s", def.Mnemonic) } -func sizeAllowed(list []instructions.Size, sz instructions.Size) bool { - // Most forms allow 1-3 sizes, so linear search is efficient here - for _, v := range list { - if v == sz { - return true - } - } - return false +// sizeAllowed reports whether sz is permitted by allowed. An empty allowed list +// means the form places no constraint on the size. Forms allow 1-3 sizes, so a +// linear scan is the right call here. +func sizeAllowed(allowed []instructions.Size, sz instructions.Size) bool { + return len(allowed) == 0 || slices.Contains(allowed, sz) } func operandKinds(a *instructions.Args) []instructions.OperandKind { diff --git a/internal/asm/elf.go b/internal/asm/elf.go index ad8c033..57ac63a 100644 --- a/internal/asm/elf.go +++ b/internal/asm/elf.go @@ -3,6 +3,7 @@ package asm import ( "encoding/binary" "fmt" + "slices" ) const ( @@ -93,9 +94,9 @@ func FormatELFWithLabels(code []byte, origin uint32, labels []DefinedLabel) []by entry: origin, segmentAddr: origin, textAddr: origin, - textBytes: append([]byte(nil), code...), + textBytes: slices.Clone(code), textPresent: len(code) > 0, - definedLabels: append([]DefinedLabel(nil), labels...), + definedLabels: slices.Clone(labels), } for i := range layout.definedLabels { layout.definedLabels[i].Section = SectionText @@ -123,7 +124,7 @@ func assembleELFLayout(p *Program) (elfLayout, error) { layout := elfLayout{ entry: p.Origin, segmentAddr: p.Origin, - definedLabels: append([]DefinedLabel(nil), p.DefinedLabels...), + definedLabels: slices.Clone(p.DefinedLabels), } var initializedEnd uint32 = p.Origin @@ -436,7 +437,7 @@ func (t *elfStringTable) add(name string) uint32 { } func (t *elfStringTable) bytes() []byte { - return append([]byte(nil), t.buf...) + return slices.Clone(t.buf) } func elfStInfo(bind, typ byte) byte { diff --git a/internal/asm/elf_test.go b/internal/asm/elf_test.go index 6cadce4..0dae6c3 100644 --- a/internal/asm/elf_test.go +++ b/internal/asm/elf_test.go @@ -281,7 +281,7 @@ func readSectionHeaders(t *testing.T, elf []byte) []elfSectionHeader { shoff := int(binary.BigEndian.Uint32(elf[32:36])) shnum := int(binary.BigEndian.Uint16(elf[48:50])) headers := make([]elfSectionHeader, shnum) - for i := 0; i < shnum; i++ { + for i := range shnum { base := shoff + i*sectionHeaderSize headers[i] = elfSectionHeader{ name: binary.BigEndian.Uint32(elf[base:]), diff --git a/internal/asm/instructions/table.go b/internal/asm/instructions/table.go index f469a6f..de2cd8f 100644 --- a/internal/asm/instructions/table.go +++ b/internal/asm/instructions/table.go @@ -1,6 +1,9 @@ package instructions -import "sync" +import ( + "maps" + "sync" +) // Table is a read-only lookup structure for instruction definitions. // @@ -37,8 +40,6 @@ func (t *Table) Lookup(mnemonic string) *InstrDef { func cloneDefs(src map[string]*InstrDef) map[string]*InstrDef { dst := make(map[string]*InstrDef, len(src)) - for k, v := range src { - dst[k] = v - } + maps.Copy(dst, src) return dst } diff --git a/internal/asm/parser.go b/internal/asm/parser.go index 8dc4573..2539565 100644 --- a/internal/asm/parser.go +++ b/internal/asm/parser.go @@ -4,8 +4,9 @@ import ( "bytes" "fmt" "io" - "math" + "maps" "os" + "slices" "strings" "github.com/jenska/m68kasm/internal/asm/instructions" @@ -65,54 +66,10 @@ func copySymbols(src map[string]uint32) map[string]uint32 { return map[string]uint32{} } dst := make(map[string]uint32, len(src)) - for k, v := range src { - dst[k] = v - } + maps.Copy(dst, src) return dst } -func localLabelName(num, idx int) string { - return fmt.Sprintf("__local_%d_%d", num, idx) -} - -func (p *Parser) defineLocalLabel(tok Token) error { - num := int(tok.Val) - if num < 0 { - return parserError(tok, "local label must be non-negative") - } - idx := p.locals[num] + 1 - name := localLabelName(num, idx) - p.locals[num] = idx - p.labels[name] = p.pc - return nil -} - -func (p *Parser) resolveLocalLabel(num int, forward bool) (string, error) { - if num < 0 { - return "", fmt.Errorf("local label must be non-negative") - } - if forward { - idx := p.locals[num] + 1 - if p.localForwards[num] < idx { - p.localForwards[num] = idx - } - return localLabelName(num, idx), nil - } - if p.locals[num] == 0 { - return "", fmt.Errorf("no previous local label %d", num) - } - return localLabelName(num, p.locals[num]), nil -} - -func (p *Parser) ensureLocalForwardsResolved() error { - for num, needed := range p.localForwards { - if p.locals[num] < needed { - return fmt.Errorf("no forward definition for local label %d", num) - } - } - return nil -} - type ParseOptions struct { Symbols map[string]uint32 InstrTable *instructions.Table @@ -143,7 +100,7 @@ func ParseWithOptions(r io.Reader, opts ParseOptions) (*Program, error) { if err != nil { return nil, withSourceLines(err, lines) } - prog.SourceLines = append([]string(nil), lines...) + prog.SourceLines = slices.Clone(lines) return prog, nil } @@ -212,7 +169,7 @@ func parseWithLexer(lx lexer, table *instructions.Table, symbols map[string]uint origin = 0 } - definedLabels := append([]DefinedLabel(nil), p.definedLabels...) + definedLabels := slices.Clone(p.definedLabels) return &Program{Items: p.items, Labels: p.labels, DefinedLabels: definedLabels, Origin: origin}, nil } @@ -230,1096 +187,6 @@ func ParseFileWithOptions(path string, opts ParseOptions) (*Program, error) { return ParseWithOptions(f, opts) } -func parserError(t Token, msg string) error { - return errorAtToken(t, fmt.Errorf("%s", msg)) -} - -// parseLabelDefinition checks for and consumes a label definition (IDENT: or NUMBER:). -// Returns true if a label was parsed. -func (p *Parser) parseLabelDefinition() (bool, error) { - t := p.peek() - if (t.Kind == IDENT || t.Kind == NUMBER) && p.peekN(2).Kind == COLON { - lbl := p.next() - p.next() // consume ':' - if lbl.Kind == IDENT { - p.labels[lbl.Text] = p.pc - p.recordDefinedLabel(lbl.Text, p.pc, lbl.Line) - } else { - if err := p.defineLocalLabel(lbl); err != nil { - return true, err - } - } - return true, nil - } - return false, nil -} - -func (p *Parser) recordDefinedLabel(name string, addr uint32, line int) { - if idx, ok := p.definedLabelPos[name]; ok { - p.definedLabels[idx].Addr = addr - p.definedLabels[idx].Line = line - p.definedLabels[idx].Section = p.section - return - } - p.definedLabelPos[name] = len(p.definedLabels) - p.definedLabels = append(p.definedLabels, DefinedLabel{Name: name, Addr: addr, Line: line, Section: p.section}) -} - -func (p *Parser) consumeLocalLabelRef() (string, bool, error) { - numTok := p.peek() - dirTok := p.peekN(2) - if numTok.Kind != NUMBER || dirTok.Kind != IDENT { - return "", false, nil - } - dir := strings.ToLower(dirTok.Text) - if dir != "f" && dir != "b" { - return "", false, nil - } - p.next() // number - p.next() // direction - name, err := p.resolveLocalLabel(int(numTok.Val), dir == "f") - return name, true, err -} - -func (p *Parser) parseLabelReference() (string, error) { - if p.peek().Kind == IDENT { - return p.next().Text, nil - } - if name, ok, err := p.consumeLocalLabelRef(); ok { - return name, err - } - t := p.next() - return "", parserError(t, "expected label") -} - -func (p *Parser) parseStmt() (bool, error) { - t := p.peek() - - if t.Kind == IDENT { - if def, ok := p.macros[t.Text]; ok { - return p.invokeMacro(def) - } - if p.isConstDefinitionStart() { - return false, p.parseConstDefinition(t) - } - base, suffix := splitMnemonic(t.Text) - if instrDef := p.instrs.Lookup(base); instrDef != nil { - return false, p.parseInstruction(instrDef) - } - - if base == "DC" && suffix != "" { - _ = p.next() - return false, parseDC(p, suffix) - } - - if pseudo, ok := lookupPseudo(t.Text); ok { - _ = p.next() - return false, pseudo(p) - } - return false, parserError(t, "unknown mnemonic") - } - - if t.Kind == DOT { - p.next() - id, err := p.want(IDENT) - if err != nil { - return false, err - } - name := "." + strings.ToUpper(id.Text) - if pseudo, ok := pseudoMap[name]; ok { - return false, pseudo(p) - } - return false, parserError(t, "unknown pseudo op") - } - return false, parserError(t, "unexpected token") -} - -func (p *Parser) isConstDefinitionStart() bool { - if p.peekN(2).Kind == EQUAL { - return true - } - return p.peekN(2).Kind == DOT && isEquToken(p.peekN(3)) -} - -func splitMnemonic(text string) (base, suffix string) { - upper := strings.ToUpper(text) - if idx := strings.IndexByte(upper, '.'); idx >= 0 { - return upper[:idx], upper[idx+1:] - } - return upper, "" -} - -func lookupPseudo(text string) (func(*Parser) error, bool) { - pseudo, ok := pseudoMap["."+strings.ToUpper(text)] - return pseudo, ok -} - -func isEquToken(tok Token) bool { - return tok.Kind == IDENT && strings.EqualFold(tok.Text, "equ") -} - -func (p *Parser) parseConstDefinition(nameTok Token) error { - _ = p.next() // consume name - - if p.peek().Kind == DOT { - p.next() - eqTok, err := p.want(IDENT) - if err != nil { - return err - } - if !isEquToken(eqTok) { - return parserError(eqTok, "expected EQU") - } - } else { - if _, err := p.want(EQUAL); err != nil { - return err - } - } - - val, err := p.parseExpr() - if err != nil { - return err - } - if val < 0 || val > math.MaxUint32 { - return errorAtLine(nameTok.Line, fmt.Errorf("constant out of 32-bit range: %d", val)) - } - - p.labels[nameTok.Text] = uint32(val) - return nil -} - -func (p *Parser) parseInstruction(instrDef *instructions.InstrDef) error { - if p.section == SectionBSS { - return errorAtLine(p.line, fmt.Errorf("instructions are not allowed in %s", p.section.Name())) - } - - mn, err := p.want(IDENT) - if err != nil { - return err - } - - operandTokens := p.consumeUntilEOL() - defer p.releaseTokens(operandTokens) - var lastErr error - for i := range instrDef.Forms { - form := &instrDef.Forms[i] - args, err := p.tryParseForm(mn, form, operandTokens) - if err != nil { - lastErr = err - continue - } - ins := &Instr{Def: instrDef, Form: form, Args: args, PC: p.pc, Line: mn.Line, Col: mn.Col, Section: p.section} - p.items = append(p.items, ins) - words, err := instructionWords(form, args) - if err != nil { - return err - } - p.pc += uint32(words * 2) - return nil - } - - if lastErr != nil { - return contextualizeAt(mn.Line, mn.Col, lastErr) - } - return &Error{Line: mn.Line, Col: mn.Col, Err: fmt.Errorf("no form matches operands")} -} - -func instructionWords(form *instructions.FormDef, args instructions.Args) (int, error) { - words := 0 - - var srcEA, dstEA instructions.EAEncoded - var err error - - if args.Src.Kind != instructions.EAkNone { - srcEA, err = instructions.EncodeEA(args.Src, 0) - if err != nil { - return 0, err - } - } - if args.Dst.Kind != instructions.EAkNone { - dstEA, err = instructions.EncodeEA(args.Dst, 0) - if err != nil { - return 0, err - } - } - - for _, step := range form.Steps { - haveWord := (step.WordBits != 0) || (len(step.Fields) > 0) - if haveWord { - words++ - } - for _, tr := range step.Trailer { - switch tr { - case instructions.TSrcEAExt: - words += len(srcEA.Ext) - case instructions.TDstEAExt: - words += len(dstEA.Ext) - case instructions.TImmSized: - words++ - case instructions.TSrcImm: - if args.Src.Kind == instructions.EAkImm { - switch args.Size { - case instructions.LongSize: - words += 2 - default: - words++ - } - } - case instructions.TBranchWordIfNeeded: - if args.Size == instructions.WordSize { - words++ - } - case instructions.TSrcRegMask, instructions.TDstRegMask: - words++ - } - } - } - - return words, nil -} - -func (p *Parser) consumeUntilEOL() []Token { - tokens := p.tokenScratch[:0] - if cap(tokens) == 0 { - tokens = make([]Token, 0, 8) - } - for { - t := p.peek() - if t.Kind == NEWLINE || t.Kind == EOF { - break - } - tokens = append(tokens, p.next()) - } - return tokens -} - -func (p *Parser) releaseTokens(tokens []Token) { - p.tokenScratch = tokens[:0] -} - -func (p *Parser) prependTokens(tokens []Token) { - if len(tokens) == 0 { - return - } - buf := make([]Token, len(tokens)+len(p.buf)) - copy(buf, tokens) - copy(buf[len(tokens):], p.buf) - p.buf = buf -} - -func (p *Parser) invokeMacro(def macroDef) (bool, error) { - nameTok := p.next() - rawArgs := p.consumeUntilEOL() - defer p.releaseTokens(rawArgs) - - args, err := splitMacroArgs(rawArgs) - if err != nil { - return false, errorAtLine(nameTok.Line, err) - } - if len(args) != len(def.params) { - return false, errorAtLine(nameTok.Line, fmt.Errorf("macro %s expects %d args, got %d", nameTok.Text, len(def.params), len(args))) - } - - if p.macroDepth > 64 { - return false, errorAtLine(nameTok.Line, fmt.Errorf("macro expansion depth exceeded")) - } - p.macroDepth++ - defer func() { p.macroDepth-- }() - - expanded := expandMacroBody(def, args, nameTok) - p.prependTokens(expanded) - return true, nil -} - -func expandMacroBody(def macroDef, args [][]Token, origin Token) []Token { - expanded := make([]Token, 0, len(def.body)+len(args)) - for _, t := range def.body { - if repl, ok := macroArgumentTokens(def.params, args, t.Text); ok && t.Kind == IDENT { - for _, at := range repl { - expanded = append(expanded, relocatedToken(at, origin)) - } - continue - } - expanded = append(expanded, relocatedToken(t, origin)) - } - return expanded -} - -func macroArgumentTokens(params []string, args [][]Token, name string) ([]Token, bool) { - for i, param := range params { - if name == param { - return args[i], true - } - } - return nil, false -} - -func relocatedToken(tok Token, origin Token) Token { - tok.Line = origin.Line - tok.Col = origin.Col - return tok -} - -func splitMacroArgs(tokens []Token) ([][]Token, error) { - if len(tokens) == 0 { - return [][]Token{}, nil - } - args := [][]Token{} - current := []Token{} - depth := 0 - for _, t := range tokens { - switch t.Kind { - case LPAREN: - depth++ - case RPAREN: - if depth == 0 { - return nil, fmt.Errorf("unmatched ')'") - } - depth-- - case COMMA: - if depth == 0 { - args = append(args, append([]Token{}, current...)) - current = current[:0] - continue - } - } - current = append(current, t) - } - if depth != 0 { - return nil, fmt.Errorf("unmatched '('") - } - args = append(args, append([]Token{}, current...)) - return args, nil -} - -func withSliceLexer(tokens []Token, line int, scratch []Token) (*sliceLexer, []Token) { - tmp := append(scratch[:0], tokens...) - tmp = append(tmp, Token{Kind: EOF, Line: line}) - return &sliceLexer{tokens: tmp}, tmp -} - -func (p *Parser) tryParseForm(mn Token, form *instructions.FormDef, tokens []Token) (instructions.Args, error) { - args := instructions.Args{} - origLX, origBuf, origLine, origCol := p.lx, p.buf, p.line, p.col - defer func() { - p.lx, p.buf, p.line, p.col = origLX, origBuf, origLine, origCol - p.formScratch = p.formScratch[:0] - }() - - // isolate parsing to the captured tokens - var lx *sliceLexer - lx, p.formScratch = withSliceLexer(tokens, mn.Line, p.formScratch) - p.lx = lx - p.buf = nil - - sz, err := p.parseSizeSpec(mn, form.DefaultSize, form.Sizes) - if err != nil { - return args, err - } - args.Size = sz - - for i, operandKind := range form.OperKinds { - if i == 1 { - if _, err := p.want(COMMA); err != nil { - return args, err - } - } - - eaExpr, err := p.parseOperand(operandKind, mn, &args, i) - if err != nil { - return args, err - } - - if i == 0 { - args.Src = eaExpr - } else { - args.Dst = eaExpr - } - } - - if trailing := p.peek(); trailing.Kind != EOF { - return args, errorAtToken(trailing, fmt.Errorf("unexpected token %s", trailing.Text)) - } - - return args, nil -} - -func (p *Parser) parseOperand(kind instructions.OperandKind, mn Token, args *instructions.Args, position int) (instructions.EAExpr, error) { - var eaExpr instructions.EAExpr - - switch kind { - case instructions.OpkImm: - if _, err := p.want(HASH); err != nil { - return eaExpr, err - } - imm, err := p.parseExpr() - if err != nil { - return eaExpr, err - } - eaExpr.Kind = instructions.EAkImm - eaExpr.Imm = imm - - case instructions.OpkImmQuick: - if _, err := p.want(HASH); err != nil { - return eaExpr, err - } - imm, err := p.parseExpr() - if err != nil { - return eaExpr, err - } - eaExpr.Kind = instructions.EAkNone - eaExpr.Imm = imm - args.HasImmQuick = true - - case instructions.OpkDn: - dreg, err := p.want(IDENT) - if err != nil { - return eaExpr, err - } - ok, dn := isRegDn(dreg.Text) - if !ok { - return eaExpr, errorAtToken(dreg, fmt.Errorf("expected Dn, got %s", dreg.Text)) - } - eaExpr.Kind = instructions.EAkDn - eaExpr.Reg = dn - - case instructions.OpkAn: - areg, err := p.want(IDENT) - if err != nil { - return eaExpr, err - } - ok, an := isRegAn(areg.Text) - if !ok { - return eaExpr, errorAtToken(areg, fmt.Errorf("expected An, got %s", areg.Text)) - } - eaExpr.Kind = instructions.EAkAn - eaExpr.Reg = an - - case instructions.OpkSR: - special, err := p.parseExpectedSpecialRegister("SR", instructions.EAkSR) - if err != nil { - return eaExpr, err - } - eaExpr = special - - case instructions.OpkCCR: - special, err := p.parseExpectedSpecialRegister("CCR", instructions.EAkCCR) - if err != nil { - return eaExpr, err - } - eaExpr = special - - case instructions.OpkUSP: - special, err := p.parseExpectedSpecialRegister("USP", instructions.EAkUSP) - if err != nil { - return eaExpr, err - } - eaExpr = special - - case instructions.OpkEA: - ea, err := p.parseEA() - if err != nil { - return eaExpr, err - } - eaExpr = ea - - case instructions.OpkPredecAn: - ea, err := p.parseEA() - if err != nil { - return eaExpr, err - } - if ea.Kind != instructions.EAkAddrPredec { - return eaExpr, errorAtLine(mn.Line, fmt.Errorf("expected -(An)")) - } - eaExpr = ea - - case instructions.OpkRegList: - mask, err := p.parseRegList() - if err != nil { - return eaExpr, err - } - eaExpr.Kind = instructions.EAkNone - if position == 0 { - args.RegMaskSrc = mask - } else { - args.RegMaskDst = mask - } - - case instructions.OpkDispRel: - if name, ok, err := p.consumeLocalLabelRef(); err != nil { - return eaExpr, err - } else if ok { - args.Target = name - return eaExpr, nil - } - if p.peek().Kind == IDENT && (p.peekN(2).Kind == EOF || p.peekN(2).Kind == NEWLINE) { - args.Target = p.next().Text - return eaExpr, nil - } - target, err := p.parseExpr() - if err != nil { - return eaExpr, err - } - args.TargetAddr = target - args.HasTargetAddr = true - - default: - return eaExpr, errorAtLine(mn.Line, fmt.Errorf("unknown identifier %s", mn.Text)) - } - - return eaExpr, nil -} - -// emitPaddingBytes centralizes emitting zero/filled padding while keeping the -// generated output within a safe upper bound. Without this guard, a crafted -// .org/.align could request gigabytes of padding and exhaust memory. -func (p *Parser) emitPaddingBytes(count uint32, fill byte) error { - if count == 0 { - return nil - } - if count > maxProgramSize || p.pc > maxProgramSize-count { - return errorAtLine(p.line, fmt.Errorf("padding would exceed maximum program size of %d bytes", maxProgramSize)) - } - - buf := make([]byte, int(count)) - if fill != 0 { - for i := range buf { - buf[i] = fill - } - } - p.items = append(p.items, &DataBytes{Bytes: buf, PC: p.pc, Line: p.line, Col: p.col, Section: p.section}) - p.pc += count - return nil -} - -func (p *Parser) setSection(section SectionKind) error { - if section < p.section { - return errorAtLine(p.line, fmt.Errorf("sections must stay in .text -> .data -> .bss order")) - } - p.section = section - return nil -} - -func sizeAllowedList(sz instructions.Size, allowed []instructions.Size) bool { - if len(allowed) == 0 { - return true - } - for _, a := range allowed { - if a == sz { - return true - } - } - return false -} - -func (p *Parser) parseSizeSpec(mn Token, def instructions.Size, allowed []instructions.Size) (instructions.Size, error) { - // DBcc instructions (like DBRA) always use a word-sized displacement, - // but assemblers don't require a ".W" suffix. To ensure the instruction - // size is calculated correctly, we explicitly set the size to WordSize. - if len(mn.Text) >= 2 && strings.ToUpper(mn.Text[:2]) == "DB" { - return instructions.WordSize, nil - } - - if idx := strings.IndexRune(mn.Text, '.'); idx > 0 { - suf := mn.Text[idx+1:] - if suf == "" { - return 0, parserError(mn, "unknown size suffix") - } - sz, ok := sizeFromIdent(suf) - if !ok { - return 0, parserError(mn, "unknown size suffix "+suf) - } - if !sizeAllowedList(sz, allowed) { - return 0, parserError(mn, "illegal size for instruction") - } - return sz, nil - } - sz, err := p.parseSizeSuffix(def, allowed) - if err != nil { - return 0, err - } - return sz, nil -} - -func (p *Parser) parseSizeSuffix(def instructions.Size, allowed []instructions.Size) (instructions.Size, error) { - sz := def - if p.accept(DOT) { - id, err := p.want(IDENT) - if err != nil { - return 0, err - } - val, ok := sizeFromIdent(id.Text) - if !ok { - return 0, parserError(id, "unknown size suffix") - } - sz = val - } - if !sizeAllowedList(sz, allowed) { - return 0, contextualizeAt(p.line, p.col, fmt.Errorf("illegal size for instruction")) - } - return sz, nil -} - -func sizeFromIdent(s string) (instructions.Size, bool) { - switch strings.ToLower(s) { - case "b": - return instructions.ByteSize, true - case "s": - return instructions.ByteSize, true - case "w": - return instructions.WordSize, true - case "l": - return instructions.LongSize, true - default: - return 0, false - } -} - -func (p *Parser) parseRegList() (uint16, error) { - mask := uint16(0) - for { - regTok, err := p.want(IDENT) - if err != nil { - return 0, err - } - isA, reg, err := parseRegName(regTok) - if err != nil { - return 0, err - } - endIsA, endReg := isA, reg - if p.accept(MINUS) { - toTok, err := p.want(IDENT) - if err != nil { - return 0, err - } - endIsA, endReg, err = parseRegName(toTok) - if err != nil { - return 0, err - } - if endIsA != isA { - return 0, errorAtToken(toTok, fmt.Errorf("register ranges must stay within D or A registers")) - } - if endReg < reg { - return 0, errorAtToken(toTok, fmt.Errorf("descending ranges are not allowed")) - } - } - for r := reg; r <= endReg; r++ { - bit := uint16(1 << r) - if isA { - bit <<= 8 - } - mask |= bit - } - if p.peek().Kind == SLASH { - p.next() - continue - } - if p.peek().Kind == COMMA { - nxt := p.peekN(2) - if nxt.Kind == IDENT { - if ok, _ := isRegDn(nxt.Text); ok { - p.next() - continue - } - if ok, _ := isRegAn(nxt.Text); ok { - p.next() - continue - } - } - return mask, nil - } - break - } - return mask, nil -} - -func parseRegName(tok Token) (bool, int, error) { - if ok, dn := isRegDn(tok.Text); ok { - return false, dn, nil - } - if ok, an := isRegAn(tok.Text); ok { - return true, an, nil - } - return false, 0, errorAtToken(tok, fmt.Errorf("expected register in list")) -} - -func parseDirectEAFromIdent(tok Token) (instructions.EAExpr, bool) { - if ok, dn := isRegDn(tok.Text); ok { - return instructions.EAExpr{Kind: instructions.EAkDn, Reg: dn}, true - } - if ok, an := isRegAn(tok.Text); ok { - return instructions.EAExpr{Kind: instructions.EAkAn, Reg: an}, true - } - return parseSpecialRegisterEA(tok) -} - -// parseEA parses an effective address operand. It acts as a dispatcher, -// delegating to more specific parsing functions based on the initial token. -func (p *Parser) parseEA() (instructions.EAExpr, error) { - t := p.peek() - - // -(An) - if t.Kind == MINUS && p.peekN(2).Kind == LPAREN { - return p.parseEAPreDecrement() - } - - // #imm - if t.Kind == HASH { - return p.parseEAImmediate() - } - - // (An), (An)+, (d,An), (d,PC), etc. - if t.Kind == LPAREN { - return p.parseEAIndirect() - } - - // Dn, An, SR, CCR, USP - if t.Kind == IDENT { - if ea, ok := parseDirectEAFromIdent(t); ok { - p.next() - return ea, nil - } - } - - // If we're here, it must be one of the forms that can start with an - // expression (a label or a number): - // - d(An) / d(PC) / d(An,ix) / d(PC,ix) - // - addr.W / addr.L - return p.parseEADisplacementOrAbsolute() -} - -func parseSpecialRegisterEA(tok Token) (instructions.EAExpr, bool) { - switch strings.ToUpper(tok.Text) { - case "SR": - return instructions.EAExpr{Kind: instructions.EAkSR}, true - case "CCR": - return instructions.EAExpr{Kind: instructions.EAkCCR}, true - case "USP": - return instructions.EAExpr{Kind: instructions.EAkUSP}, true - default: - return instructions.EAExpr{}, false - } -} - -func (p *Parser) parseExpectedSpecialRegister(name string, kind instructions.EAExprKind) (instructions.EAExpr, error) { - tok, err := p.want(IDENT) - if err != nil { - return instructions.EAExpr{}, err - } - if !strings.EqualFold(tok.Text, name) { - return instructions.EAExpr{}, errorAtToken(tok, fmt.Errorf("expected %s", name)) - } - return instructions.EAExpr{Kind: kind}, nil -} - -func parseAbsoluteEA(kind instructions.EAExprKind, value int64) instructions.EAExpr { - if kind == instructions.EAkAbsW { - return instructions.EAExpr{Kind: kind, Abs16: uint16(value)} - } - return instructions.EAExpr{Kind: kind, Abs32: uint32(value)} -} - -func (p *Parser) parseAbsoluteSuffix(defaultKind instructions.EAExprKind, invalidMsg string) (instructions.EAExprKind, error) { - kind := defaultKind - if !p.accept(DOT) { - return kind, nil - } - suf, err := p.want(IDENT) - if err != nil { - return 0, err - } - switch strings.ToUpper(suf.Text) { - case "W": - return instructions.EAkAbsW, nil - case "L": - return instructions.EAkAbsL, nil - default: - return 0, errorAtToken(suf, fmt.Errorf(invalidMsg, suf.Text)) - } -} - -func parseEABaseRegister(text string) (reg int, pcRelative bool, ok bool) { - if ok, an := isRegAn(text); ok { - return an, false, true - } - if isPC(text) { - return 0, true, true - } - return 0, false, false -} - -func displacementEA(base Token, disp int64) (instructions.EAExpr, error) { - if reg, isPC, ok := parseEABaseRegister(base.Text); ok { - if isPC { - return instructions.EAExpr{Kind: instructions.EAkPCDisp16, Disp16: int32(disp)}, nil - } - return instructions.EAExpr{Kind: instructions.EAkAddrDisp16, Reg: reg, Disp16: int32(disp)}, nil - } - return instructions.EAExpr{}, parserError(base, "base must be An or PC for displacement addressing") -} - -func indexedEA(base Token, disp int64, ix instructions.EAIndex) (instructions.EAExpr, error) { - ix.Disp8 = int8(disp) - if reg, isPC, ok := parseEABaseRegister(base.Text); ok { - if isPC { - return instructions.EAExpr{Kind: instructions.EAkIdxPCBrief, Index: ix}, nil - } - return instructions.EAExpr{Kind: instructions.EAkIdxAnBrief, Reg: reg, Index: ix}, nil - } - return instructions.EAExpr{}, parserError(base, "base must be An or PC for indexed addressing") -} - -func (p *Parser) pcRelativeDisp(expr exprInfo, min, max int64) (int64, error) { - disp := expr.Value - if expr.HasSymbol { - // 68000 PC-relative addressing uses the extension-word address as the base. - disp -= int64(p.pc) + 2 - } - if disp < min || disp > max { - return 0, errorAtLine(p.line, fmt.Errorf("PC-relative displacement out of range: %d", disp)) - } - return disp, nil -} - -// ---------- EA parsing helpers ---------- - -func (p *Parser) parseEAPreDecrement() (instructions.EAExpr, error) { - p.next() // '-' - p.next() // '(' - areg, err := p.want(IDENT) - if err != nil { - return instructions.EAExpr{}, err - } - ok, an := isRegAn(areg.Text) - if !ok { - return instructions.EAExpr{}, parserError(areg, "expected address register") - } - if _, err := p.want(RPAREN); err != nil { - return instructions.EAExpr{}, err - } - return instructions.EAExpr{Kind: instructions.EAkAddrPredec, Reg: an}, nil -} - -func (p *Parser) parseEAImmediate() (instructions.EAExpr, error) { - p.next() // '#' - v, err := p.parseExpr() - if err != nil { - return instructions.EAExpr{}, err - } - return instructions.EAExpr{Kind: instructions.EAkImm, Imm: v}, nil -} - -func (p *Parser) parseEADisplacementOrAbsolute() (instructions.EAExpr, error) { - // This handles two cases that can start with an expression: - // 1. Displacement modes: d(An), d(PC), d(An,ix), d(PC,ix) - // 2. Absolute modes: addr.W, addr.L - expr, err := p.parseExprInfoUntil(LPAREN, DOT, COMMA, NEWLINE, EOF) - if err != nil { - return instructions.EAExpr{}, err - } - - // Case 1: Displacement modes, identified by a following '('. - if p.accept(LPAREN) { - return p.parseEADisplacementBody(expr) - } - - kind, err := p.parseAbsoluteSuffix(instructions.EAkAbsL, "unknown size suffix .%s") - if err != nil { - return instructions.EAExpr{}, err - } - return parseAbsoluteEA(kind, expr.Value), nil -} - -func (p *Parser) parseEAIndirect() (instructions.EAExpr, error) { - p.next() // consume '(' - - // Case 1: (An) or (An)+ - if id := p.peek(); id.Kind == IDENT && p.peekN(2).Kind == RPAREN { - if ok, an := isRegAn(id.Text); ok { - p.next() // id - p.next() // ')' - if p.accept(PLUS) { - return instructions.EAExpr{Kind: instructions.EAkAddrPostinc, Reg: an}, nil - } - return instructions.EAExpr{Kind: instructions.EAkAddrInd, Reg: an}, nil - } else if ok, _ := isRegDn(id.Text); ok { - return instructions.EAExpr{}, parserError(id, "data register not allowed in indirect addressing (expected An)") - } - } - - // Case 2: (An, ix) or (PC, ix) -- no outer displacement - if base := p.peek(); base.Kind == IDENT && p.peekN(2).Kind == COMMA { - if reg, isPC, ok := parseEABaseRegister(base.Text); ok { - p.next() // base - p.next() // ',' - ix, err := p.parseEAIndex() - if err != nil { - return instructions.EAExpr{}, err - } - if _, err := p.want(RPAREN); err != nil { - return instructions.EAExpr{}, err - } - if isPC { - return instructions.EAExpr{Kind: instructions.EAkIdxPCBrief, Index: ix}, nil - } - return instructions.EAExpr{Kind: instructions.EAkIdxAnBrief, Reg: reg, Index: ix}, nil - } - } - - // Case 3: (disp, ...), (abs).W, or (abs).L - expr, err := p.parseExprInfoUntil(COMMA, RPAREN) - if err != nil { - return instructions.EAExpr{}, err - } - - // Subcase 3a: (disp, An/PC) or (disp, An/PC, ix) - if p.accept(COMMA) { - return p.parseEADisplacementBody(expr) - } - - // Subcase 3b: (abs).W or (abs).L - if _, err := p.want(RPAREN); err != nil { - return instructions.EAExpr{}, err - } - kind, err := p.parseAbsoluteSuffix(0, "expected .W or .L after (absolute address)") - if err == nil && kind != 0 { - return parseAbsoluteEA(kind, expr.Value), nil - } - return instructions.EAExpr{}, errorAtLine(p.line, fmt.Errorf("invalid effective address form, expected (abs).W or (abs).L")) -} - -func (p *Parser) parseEADisplacementBody(expr exprInfo) (instructions.EAExpr, error) { - // We are inside the parentheses of d(...) or (d,...) - base, err := p.want(IDENT) - if err != nil { - return instructions.EAExpr{}, err - } - disp := expr.Value - if isPC(base.Text) { - if p.accept(COMMA) { - ix, err := p.parseEAIndex() - if err != nil { - return instructions.EAExpr{}, err - } - if _, err := p.want(RPAREN); err != nil { - return instructions.EAExpr{}, err - } - disp, err = p.pcRelativeDisp(expr, -128, 127) - if err != nil { - return instructions.EAExpr{}, err - } - return indexedEA(base, disp, ix) - } - if _, err := p.want(RPAREN); err != nil { - return instructions.EAExpr{}, err - } - disp, err = p.pcRelativeDisp(expr, -32768, 32767) - if err != nil { - return instructions.EAExpr{}, err - } - return displacementEA(base, disp) - } - - // Is it an indexed mode, d(An,ix) or d(PC,ix)? - if p.accept(COMMA) { - ix, err := p.parseEAIndex() - if err != nil { - return instructions.EAExpr{}, err - } - if _, err := p.want(RPAREN); err != nil { - return instructions.EAExpr{}, err - } - return indexedEA(base, disp, ix) - } - - // It's a simple displacement mode: d(An) or d(PC) - if _, err := p.want(RPAREN); err != nil { - return instructions.EAExpr{}, err - } - return displacementEA(base, disp) -} - -func (p *Parser) parseEAIndex() (instructions.EAIndex, error) { - idxTok, err := p.want(IDENT) - if err != nil { - return instructions.EAIndex{}, err - } - - // Handle embedded size suffix (e.g. D0.L) which the lexer consumes as one IDENT - name := idxTok.Text - var suffix string - if idx := strings.IndexByte(name, '.'); idx >= 0 { - suffix = name[idx+1:] - name = name[:idx] - } - - ix, err := parseIndexRegister(name) - if err != nil { - return ix, parserError(idxTok, err.Error()) - } - - if suffix != "" { - long, err := parseIndexSizeSuffix(suffix) - if err != nil { - return ix, parserError(idxTok, err.Error()) - } - ix.Long = long - } else if p.accept(DOT) { - szTok, err := p.want(IDENT) - if err != nil { - return ix, err - } - long, err := parseIndexSizeSuffix(szTok.Text) - if err != nil { - return ix, parserError(szTok, err.Error()) - } - ix.Long = long - } - - // Optional scale factor *1, *2, *4, *8 - if p.accept(STAR) { - sc, err := p.parseExprUntil(COMMA, RPAREN) - if err != nil { - return ix, err - } - switch sc { - case 1, 2, 4, 8: - ix.Scale = uint8(sc) - default: - return ix, fmt.Errorf("invalid scale factor: %d", sc) - } - } else { - ix.Scale = 1 // Default scale - } - - return ix, nil -} - -func parseIndexRegister(name string) (instructions.EAIndex, error) { - var ix instructions.EAIndex - if ok, dn := isRegDn(name); ok { - ix.Reg = dn - return ix, nil - } - if ok, an := isRegAn(name); ok { - ix.Reg = an - ix.IsA = true - return ix, nil - } - return ix, fmt.Errorf("expected Dn or An as index register") -} - -func parseIndexSizeSuffix(suffix string) (bool, error) { - switch strings.ToUpper(suffix) { - case "W": - return false, nil - case "L": - return true, nil - default: - return false, fmt.Errorf("expected .W or .L for index register size") - } -} - // ----------------------------------------------------------------------------- // Token Reader / Lexer Integration func (p *Parser) fill(n int) { diff --git a/internal/asm/parser_ea.go b/internal/asm/parser_ea.go new file mode 100644 index 0000000..84db58f --- /dev/null +++ b/internal/asm/parser_ea.go @@ -0,0 +1,385 @@ +package asm + +import ( + "fmt" + "strings" + + "github.com/jenska/m68kasm/internal/asm/instructions" +) + +func parseDirectEAFromIdent(tok Token) (instructions.EAExpr, bool) { + if ok, dn := isRegDn(tok.Text); ok { + return instructions.EAExpr{Kind: instructions.EAkDn, Reg: dn}, true + } + if ok, an := isRegAn(tok.Text); ok { + return instructions.EAExpr{Kind: instructions.EAkAn, Reg: an}, true + } + return parseSpecialRegisterEA(tok) +} + +// parseEA parses an effective address operand. It acts as a dispatcher, +// delegating to more specific parsing functions based on the initial token. +func (p *Parser) parseEA() (instructions.EAExpr, error) { + t := p.peek() + + // -(An) + if t.Kind == MINUS && p.peekN(2).Kind == LPAREN { + return p.parseEAPreDecrement() + } + + // #imm + if t.Kind == HASH { + return p.parseEAImmediate() + } + + // (An), (An)+, (d,An), (d,PC), etc. + if t.Kind == LPAREN { + return p.parseEAIndirect() + } + + // Dn, An, SR, CCR, USP + if t.Kind == IDENT { + if ea, ok := parseDirectEAFromIdent(t); ok { + p.next() + return ea, nil + } + } + + // If we're here, it must be one of the forms that can start with an + // expression (a label or a number): + // - d(An) / d(PC) / d(An,ix) / d(PC,ix) + // - addr.W / addr.L + return p.parseEADisplacementOrAbsolute() +} + +func parseSpecialRegisterEA(tok Token) (instructions.EAExpr, bool) { + switch strings.ToUpper(tok.Text) { + case "SR": + return instructions.EAExpr{Kind: instructions.EAkSR}, true + case "CCR": + return instructions.EAExpr{Kind: instructions.EAkCCR}, true + case "USP": + return instructions.EAExpr{Kind: instructions.EAkUSP}, true + default: + return instructions.EAExpr{}, false + } +} + +func (p *Parser) parseExpectedSpecialRegister(name string, kind instructions.EAExprKind) (instructions.EAExpr, error) { + tok, err := p.want(IDENT) + if err != nil { + return instructions.EAExpr{}, err + } + if !strings.EqualFold(tok.Text, name) { + return instructions.EAExpr{}, errorAtToken(tok, fmt.Errorf("expected %s", name)) + } + return instructions.EAExpr{Kind: kind}, nil +} + +func parseAbsoluteEA(kind instructions.EAExprKind, value int64) instructions.EAExpr { + if kind == instructions.EAkAbsW { + return instructions.EAExpr{Kind: kind, Abs16: uint16(value)} + } + return instructions.EAExpr{Kind: kind, Abs32: uint32(value)} +} + +func (p *Parser) parseAbsoluteSuffix(defaultKind instructions.EAExprKind, invalidMsg string) (instructions.EAExprKind, error) { + kind := defaultKind + if !p.accept(DOT) { + return kind, nil + } + suf, err := p.want(IDENT) + if err != nil { + return 0, err + } + switch strings.ToUpper(suf.Text) { + case "W": + return instructions.EAkAbsW, nil + case "L": + return instructions.EAkAbsL, nil + default: + return 0, errorAtToken(suf, fmt.Errorf(invalidMsg, suf.Text)) + } +} + +func parseEABaseRegister(text string) (reg int, pcRelative bool, ok bool) { + if ok, an := isRegAn(text); ok { + return an, false, true + } + if isPC(text) { + return 0, true, true + } + return 0, false, false +} + +func displacementEA(base Token, disp int64) (instructions.EAExpr, error) { + if reg, isPC, ok := parseEABaseRegister(base.Text); ok { + if isPC { + return instructions.EAExpr{Kind: instructions.EAkPCDisp16, Disp16: int32(disp)}, nil + } + return instructions.EAExpr{Kind: instructions.EAkAddrDisp16, Reg: reg, Disp16: int32(disp)}, nil + } + return instructions.EAExpr{}, parserError(base, "base must be An or PC for displacement addressing") +} + +func indexedEA(base Token, disp int64, ix instructions.EAIndex) (instructions.EAExpr, error) { + ix.Disp8 = int8(disp) + if reg, isPC, ok := parseEABaseRegister(base.Text); ok { + if isPC { + return instructions.EAExpr{Kind: instructions.EAkIdxPCBrief, Index: ix}, nil + } + return instructions.EAExpr{Kind: instructions.EAkIdxAnBrief, Reg: reg, Index: ix}, nil + } + return instructions.EAExpr{}, parserError(base, "base must be An or PC for indexed addressing") +} + +func (p *Parser) pcRelativeDisp(expr exprInfo, min, max int64) (int64, error) { + disp := expr.Value + if expr.HasSymbol { + // 68000 PC-relative addressing uses the extension-word address as the base. + disp -= int64(p.pc) + 2 + } + if disp < min || disp > max { + return 0, errorAtLine(p.line, fmt.Errorf("PC-relative displacement out of range: %d", disp)) + } + return disp, nil +} + +// ---------- EA parsing helpers ---------- + +func (p *Parser) parseEAPreDecrement() (instructions.EAExpr, error) { + p.next() // '-' + p.next() // '(' + areg, err := p.want(IDENT) + if err != nil { + return instructions.EAExpr{}, err + } + ok, an := isRegAn(areg.Text) + if !ok { + return instructions.EAExpr{}, parserError(areg, "expected address register") + } + if _, err := p.want(RPAREN); err != nil { + return instructions.EAExpr{}, err + } + return instructions.EAExpr{Kind: instructions.EAkAddrPredec, Reg: an}, nil +} + +func (p *Parser) parseEAImmediate() (instructions.EAExpr, error) { + p.next() // '#' + v, err := p.parseExpr() + if err != nil { + return instructions.EAExpr{}, err + } + return instructions.EAExpr{Kind: instructions.EAkImm, Imm: v}, nil +} + +func (p *Parser) parseEADisplacementOrAbsolute() (instructions.EAExpr, error) { + // This handles two cases that can start with an expression: + // 1. Displacement modes: d(An), d(PC), d(An,ix), d(PC,ix) + // 2. Absolute modes: addr.W, addr.L + expr, err := p.parseExprInfoUntil(LPAREN, DOT, COMMA, NEWLINE, EOF) + if err != nil { + return instructions.EAExpr{}, err + } + + // Case 1: Displacement modes, identified by a following '('. + if p.accept(LPAREN) { + return p.parseEADisplacementBody(expr) + } + + kind, err := p.parseAbsoluteSuffix(instructions.EAkAbsL, "unknown size suffix .%s") + if err != nil { + return instructions.EAExpr{}, err + } + return parseAbsoluteEA(kind, expr.Value), nil +} + +func (p *Parser) parseEAIndirect() (instructions.EAExpr, error) { + p.next() // consume '(' + + // Case 1: (An) or (An)+ + if id := p.peek(); id.Kind == IDENT && p.peekN(2).Kind == RPAREN { + if ok, an := isRegAn(id.Text); ok { + p.next() // id + p.next() // ')' + if p.accept(PLUS) { + return instructions.EAExpr{Kind: instructions.EAkAddrPostinc, Reg: an}, nil + } + return instructions.EAExpr{Kind: instructions.EAkAddrInd, Reg: an}, nil + } else if ok, _ := isRegDn(id.Text); ok { + return instructions.EAExpr{}, parserError(id, "data register not allowed in indirect addressing (expected An)") + } + } + + // Case 2: (An, ix) or (PC, ix) -- no outer displacement + if base := p.peek(); base.Kind == IDENT && p.peekN(2).Kind == COMMA { + if reg, isPC, ok := parseEABaseRegister(base.Text); ok { + p.next() // base + p.next() // ',' + ix, err := p.parseEAIndex() + if err != nil { + return instructions.EAExpr{}, err + } + if _, err := p.want(RPAREN); err != nil { + return instructions.EAExpr{}, err + } + if isPC { + return instructions.EAExpr{Kind: instructions.EAkIdxPCBrief, Index: ix}, nil + } + return instructions.EAExpr{Kind: instructions.EAkIdxAnBrief, Reg: reg, Index: ix}, nil + } + } + + // Case 3: (disp, ...), (abs).W, or (abs).L + expr, err := p.parseExprInfoUntil(COMMA, RPAREN) + if err != nil { + return instructions.EAExpr{}, err + } + + // Subcase 3a: (disp, An/PC) or (disp, An/PC, ix) + if p.accept(COMMA) { + return p.parseEADisplacementBody(expr) + } + + // Subcase 3b: (abs).W or (abs).L + if _, err := p.want(RPAREN); err != nil { + return instructions.EAExpr{}, err + } + kind, err := p.parseAbsoluteSuffix(0, "expected .W or .L after (absolute address)") + if err == nil && kind != 0 { + return parseAbsoluteEA(kind, expr.Value), nil + } + return instructions.EAExpr{}, errorAtLine(p.line, fmt.Errorf("invalid effective address form, expected (abs).W or (abs).L")) +} + +func (p *Parser) parseEADisplacementBody(expr exprInfo) (instructions.EAExpr, error) { + // We are inside the parentheses of d(...) or (d,...) + base, err := p.want(IDENT) + if err != nil { + return instructions.EAExpr{}, err + } + disp := expr.Value + if isPC(base.Text) { + if p.accept(COMMA) { + ix, err := p.parseEAIndex() + if err != nil { + return instructions.EAExpr{}, err + } + if _, err := p.want(RPAREN); err != nil { + return instructions.EAExpr{}, err + } + disp, err = p.pcRelativeDisp(expr, -128, 127) + if err != nil { + return instructions.EAExpr{}, err + } + return indexedEA(base, disp, ix) + } + if _, err := p.want(RPAREN); err != nil { + return instructions.EAExpr{}, err + } + disp, err = p.pcRelativeDisp(expr, -32768, 32767) + if err != nil { + return instructions.EAExpr{}, err + } + return displacementEA(base, disp) + } + + // Is it an indexed mode, d(An,ix) or d(PC,ix)? + if p.accept(COMMA) { + ix, err := p.parseEAIndex() + if err != nil { + return instructions.EAExpr{}, err + } + if _, err := p.want(RPAREN); err != nil { + return instructions.EAExpr{}, err + } + return indexedEA(base, disp, ix) + } + + // It's a simple displacement mode: d(An) or d(PC) + if _, err := p.want(RPAREN); err != nil { + return instructions.EAExpr{}, err + } + return displacementEA(base, disp) +} + +func (p *Parser) parseEAIndex() (instructions.EAIndex, error) { + idxTok, err := p.want(IDENT) + if err != nil { + return instructions.EAIndex{}, err + } + + // Handle embedded size suffix (e.g. D0.L) which the lexer consumes as one IDENT + name := idxTok.Text + var suffix string + if idx := strings.IndexByte(name, '.'); idx >= 0 { + suffix = name[idx+1:] + name = name[:idx] + } + + ix, err := parseIndexRegister(name) + if err != nil { + return ix, parserError(idxTok, err.Error()) + } + + if suffix != "" { + long, err := parseIndexSizeSuffix(suffix) + if err != nil { + return ix, parserError(idxTok, err.Error()) + } + ix.Long = long + } else if p.accept(DOT) { + szTok, err := p.want(IDENT) + if err != nil { + return ix, err + } + long, err := parseIndexSizeSuffix(szTok.Text) + if err != nil { + return ix, parserError(szTok, err.Error()) + } + ix.Long = long + } + + // Optional scale factor *1, *2, *4, *8 + if p.accept(STAR) { + sc, err := p.parseExprUntil(COMMA, RPAREN) + if err != nil { + return ix, err + } + switch sc { + case 1, 2, 4, 8: + ix.Scale = uint8(sc) + default: + return ix, fmt.Errorf("invalid scale factor: %d", sc) + } + } else { + ix.Scale = 1 // Default scale + } + + return ix, nil +} + +func parseIndexRegister(name string) (instructions.EAIndex, error) { + var ix instructions.EAIndex + if ok, dn := isRegDn(name); ok { + ix.Reg = dn + return ix, nil + } + if ok, an := isRegAn(name); ok { + ix.Reg = an + ix.IsA = true + return ix, nil + } + return ix, fmt.Errorf("expected Dn or An as index register") +} + +func parseIndexSizeSuffix(suffix string) (bool, error) { + switch strings.ToUpper(suffix) { + case "W": + return false, nil + case "L": + return true, nil + default: + return false, fmt.Errorf("expected .W or .L for index register size") + } +} diff --git a/internal/asm/parser_labels.go b/internal/asm/parser_labels.go new file mode 100644 index 0000000..df68c63 --- /dev/null +++ b/internal/asm/parser_labels.go @@ -0,0 +1,110 @@ +package asm + +import ( + "fmt" + "strings" +) + +func localLabelName(num, idx int) string { + return fmt.Sprintf("__local_%d_%d", num, idx) +} + +func (p *Parser) defineLocalLabel(tok Token) error { + num := int(tok.Val) + if num < 0 { + return parserError(tok, "local label must be non-negative") + } + idx := p.locals[num] + 1 + name := localLabelName(num, idx) + p.locals[num] = idx + p.labels[name] = p.pc + return nil +} + +func (p *Parser) resolveLocalLabel(num int, forward bool) (string, error) { + if num < 0 { + return "", fmt.Errorf("local label must be non-negative") + } + if forward { + idx := p.locals[num] + 1 + if p.localForwards[num] < idx { + p.localForwards[num] = idx + } + return localLabelName(num, idx), nil + } + if p.locals[num] == 0 { + return "", fmt.Errorf("no previous local label %d", num) + } + return localLabelName(num, p.locals[num]), nil +} + +func (p *Parser) ensureLocalForwardsResolved() error { + for num, needed := range p.localForwards { + if p.locals[num] < needed { + return fmt.Errorf("no forward definition for local label %d", num) + } + } + return nil +} + +func parserError(t Token, msg string) error { + return errorAtToken(t, fmt.Errorf("%s", msg)) +} + +// parseLabelDefinition checks for and consumes a label definition (IDENT: or NUMBER:). +// Returns true if a label was parsed. +func (p *Parser) parseLabelDefinition() (bool, error) { + t := p.peek() + if (t.Kind == IDENT || t.Kind == NUMBER) && p.peekN(2).Kind == COLON { + lbl := p.next() + p.next() // consume ':' + if lbl.Kind == IDENT { + p.labels[lbl.Text] = p.pc + p.recordDefinedLabel(lbl.Text, p.pc, lbl.Line) + } else { + if err := p.defineLocalLabel(lbl); err != nil { + return true, err + } + } + return true, nil + } + return false, nil +} + +func (p *Parser) recordDefinedLabel(name string, addr uint32, line int) { + if idx, ok := p.definedLabelPos[name]; ok { + p.definedLabels[idx].Addr = addr + p.definedLabels[idx].Line = line + p.definedLabels[idx].Section = p.section + return + } + p.definedLabelPos[name] = len(p.definedLabels) + p.definedLabels = append(p.definedLabels, DefinedLabel{Name: name, Addr: addr, Line: line, Section: p.section}) +} + +func (p *Parser) consumeLocalLabelRef() (string, bool, error) { + numTok := p.peek() + dirTok := p.peekN(2) + if numTok.Kind != NUMBER || dirTok.Kind != IDENT { + return "", false, nil + } + dir := strings.ToLower(dirTok.Text) + if dir != "f" && dir != "b" { + return "", false, nil + } + p.next() // number + p.next() // direction + name, err := p.resolveLocalLabel(int(numTok.Val), dir == "f") + return name, true, err +} + +func (p *Parser) parseLabelReference() (string, error) { + if p.peek().Kind == IDENT { + return p.next().Text, nil + } + if name, ok, err := p.consumeLocalLabelRef(); ok { + return name, err + } + t := p.next() + return "", parserError(t, "expected label") +} diff --git a/internal/asm/parser_macro.go b/internal/asm/parser_macro.go new file mode 100644 index 0000000..612f9b3 --- /dev/null +++ b/internal/asm/parser_macro.go @@ -0,0 +1,125 @@ +package asm + +import ( + "fmt" +) + +func (p *Parser) consumeUntilEOL() []Token { + tokens := p.tokenScratch[:0] + if cap(tokens) == 0 { + tokens = make([]Token, 0, 8) + } + for { + t := p.peek() + if t.Kind == NEWLINE || t.Kind == EOF { + break + } + tokens = append(tokens, p.next()) + } + return tokens +} + +func (p *Parser) releaseTokens(tokens []Token) { + p.tokenScratch = tokens[:0] +} + +func (p *Parser) prependTokens(tokens []Token) { + if len(tokens) == 0 { + return + } + buf := make([]Token, len(tokens)+len(p.buf)) + copy(buf, tokens) + copy(buf[len(tokens):], p.buf) + p.buf = buf +} + +func (p *Parser) invokeMacro(def macroDef) (bool, error) { + nameTok := p.next() + rawArgs := p.consumeUntilEOL() + defer p.releaseTokens(rawArgs) + + args, err := splitMacroArgs(rawArgs) + if err != nil { + return false, errorAtLine(nameTok.Line, err) + } + if len(args) != len(def.params) { + return false, errorAtLine(nameTok.Line, fmt.Errorf("macro %s expects %d args, got %d", nameTok.Text, len(def.params), len(args))) + } + + if p.macroDepth > 64 { + return false, errorAtLine(nameTok.Line, fmt.Errorf("macro expansion depth exceeded")) + } + p.macroDepth++ + defer func() { p.macroDepth-- }() + + expanded := expandMacroBody(def, args, nameTok) + p.prependTokens(expanded) + return true, nil +} + +func expandMacroBody(def macroDef, args [][]Token, origin Token) []Token { + expanded := make([]Token, 0, len(def.body)+len(args)) + for _, t := range def.body { + if repl, ok := macroArgumentTokens(def.params, args, t.Text); ok && t.Kind == IDENT { + for _, at := range repl { + expanded = append(expanded, relocatedToken(at, origin)) + } + continue + } + expanded = append(expanded, relocatedToken(t, origin)) + } + return expanded +} + +func macroArgumentTokens(params []string, args [][]Token, name string) ([]Token, bool) { + for i, param := range params { + if name == param { + return args[i], true + } + } + return nil, false +} + +func relocatedToken(tok Token, origin Token) Token { + tok.Line = origin.Line + tok.Col = origin.Col + return tok +} + +func splitMacroArgs(tokens []Token) ([][]Token, error) { + if len(tokens) == 0 { + return [][]Token{}, nil + } + args := [][]Token{} + current := []Token{} + depth := 0 + for _, t := range tokens { + switch t.Kind { + case LPAREN: + depth++ + case RPAREN: + if depth == 0 { + return nil, fmt.Errorf("unmatched ')'") + } + depth-- + case COMMA: + if depth == 0 { + args = append(args, append([]Token{}, current...)) + current = current[:0] + continue + } + } + current = append(current, t) + } + if depth != 0 { + return nil, fmt.Errorf("unmatched '('") + } + args = append(args, append([]Token{}, current...)) + return args, nil +} + +func withSliceLexer(tokens []Token, line int, scratch []Token) (*sliceLexer, []Token) { + tmp := append(scratch[:0], tokens...) + tmp = append(tmp, Token{Kind: EOF, Line: line}) + return &sliceLexer{tokens: tmp}, tmp +} diff --git a/internal/asm/parser_operand.go b/internal/asm/parser_operand.go new file mode 100644 index 0000000..6f2b147 --- /dev/null +++ b/internal/asm/parser_operand.go @@ -0,0 +1,311 @@ +package asm + +import ( + "fmt" + "strings" + + "github.com/jenska/m68kasm/internal/asm/instructions" +) + +func (p *Parser) tryParseForm(mn Token, form *instructions.FormDef, tokens []Token) (instructions.Args, error) { + args := instructions.Args{} + origLX, origBuf, origLine, origCol := p.lx, p.buf, p.line, p.col + defer func() { + p.lx, p.buf, p.line, p.col = origLX, origBuf, origLine, origCol + p.formScratch = p.formScratch[:0] + }() + + // isolate parsing to the captured tokens + var lx *sliceLexer + lx, p.formScratch = withSliceLexer(tokens, mn.Line, p.formScratch) + p.lx = lx + p.buf = nil + + sz, err := p.parseSizeSpec(mn, form.DefaultSize, form.Sizes) + if err != nil { + return args, err + } + args.Size = sz + + for i, operandKind := range form.OperKinds { + if i == 1 { + if _, err := p.want(COMMA); err != nil { + return args, err + } + } + + eaExpr, err := p.parseOperand(operandKind, mn, &args, i) + if err != nil { + return args, err + } + + if i == 0 { + args.Src = eaExpr + } else { + args.Dst = eaExpr + } + } + + if trailing := p.peek(); trailing.Kind != EOF { + return args, errorAtToken(trailing, fmt.Errorf("unexpected token %s", trailing.Text)) + } + + return args, nil +} + +func (p *Parser) parseOperand(kind instructions.OperandKind, mn Token, args *instructions.Args, position int) (instructions.EAExpr, error) { + var eaExpr instructions.EAExpr + + switch kind { + case instructions.OpkImm: + if _, err := p.want(HASH); err != nil { + return eaExpr, err + } + imm, err := p.parseExpr() + if err != nil { + return eaExpr, err + } + eaExpr.Kind = instructions.EAkImm + eaExpr.Imm = imm + + case instructions.OpkImmQuick: + if _, err := p.want(HASH); err != nil { + return eaExpr, err + } + imm, err := p.parseExpr() + if err != nil { + return eaExpr, err + } + eaExpr.Kind = instructions.EAkNone + eaExpr.Imm = imm + args.HasImmQuick = true + + case instructions.OpkDn: + dreg, err := p.want(IDENT) + if err != nil { + return eaExpr, err + } + ok, dn := isRegDn(dreg.Text) + if !ok { + return eaExpr, errorAtToken(dreg, fmt.Errorf("expected Dn, got %s", dreg.Text)) + } + eaExpr.Kind = instructions.EAkDn + eaExpr.Reg = dn + + case instructions.OpkAn: + areg, err := p.want(IDENT) + if err != nil { + return eaExpr, err + } + ok, an := isRegAn(areg.Text) + if !ok { + return eaExpr, errorAtToken(areg, fmt.Errorf("expected An, got %s", areg.Text)) + } + eaExpr.Kind = instructions.EAkAn + eaExpr.Reg = an + + case instructions.OpkSR: + special, err := p.parseExpectedSpecialRegister("SR", instructions.EAkSR) + if err != nil { + return eaExpr, err + } + eaExpr = special + + case instructions.OpkCCR: + special, err := p.parseExpectedSpecialRegister("CCR", instructions.EAkCCR) + if err != nil { + return eaExpr, err + } + eaExpr = special + + case instructions.OpkUSP: + special, err := p.parseExpectedSpecialRegister("USP", instructions.EAkUSP) + if err != nil { + return eaExpr, err + } + eaExpr = special + + case instructions.OpkEA: + ea, err := p.parseEA() + if err != nil { + return eaExpr, err + } + eaExpr = ea + + case instructions.OpkPredecAn: + ea, err := p.parseEA() + if err != nil { + return eaExpr, err + } + if ea.Kind != instructions.EAkAddrPredec { + return eaExpr, errorAtLine(mn.Line, fmt.Errorf("expected -(An)")) + } + eaExpr = ea + + case instructions.OpkRegList: + mask, err := p.parseRegList() + if err != nil { + return eaExpr, err + } + eaExpr.Kind = instructions.EAkNone + if position == 0 { + args.RegMaskSrc = mask + } else { + args.RegMaskDst = mask + } + + case instructions.OpkDispRel: + if name, ok, err := p.consumeLocalLabelRef(); err != nil { + return eaExpr, err + } else if ok { + args.Target = name + return eaExpr, nil + } + if p.peek().Kind == IDENT && (p.peekN(2).Kind == EOF || p.peekN(2).Kind == NEWLINE) { + args.Target = p.next().Text + return eaExpr, nil + } + target, err := p.parseExpr() + if err != nil { + return eaExpr, err + } + args.TargetAddr = target + args.HasTargetAddr = true + + default: + return eaExpr, errorAtLine(mn.Line, fmt.Errorf("unknown identifier %s", mn.Text)) + } + + return eaExpr, nil +} + +func (p *Parser) parseSizeSpec(mn Token, def instructions.Size, allowed []instructions.Size) (instructions.Size, error) { + // DBcc instructions (like DBRA) always use a word-sized displacement, + // but assemblers don't require a ".W" suffix. To ensure the instruction + // size is calculated correctly, we explicitly set the size to WordSize. + if len(mn.Text) >= 2 && strings.ToUpper(mn.Text[:2]) == "DB" { + return instructions.WordSize, nil + } + + if idx := strings.IndexRune(mn.Text, '.'); idx > 0 { + suf := mn.Text[idx+1:] + if suf == "" { + return 0, parserError(mn, "unknown size suffix") + } + sz, ok := sizeFromIdent(suf) + if !ok { + return 0, parserError(mn, "unknown size suffix "+suf) + } + if !sizeAllowed(allowed, sz) { + return 0, parserError(mn, "illegal size for instruction") + } + return sz, nil + } + sz, err := p.parseSizeSuffix(def, allowed) + if err != nil { + return 0, err + } + return sz, nil +} + +func (p *Parser) parseSizeSuffix(def instructions.Size, allowed []instructions.Size) (instructions.Size, error) { + sz := def + if p.accept(DOT) { + id, err := p.want(IDENT) + if err != nil { + return 0, err + } + val, ok := sizeFromIdent(id.Text) + if !ok { + return 0, parserError(id, "unknown size suffix") + } + sz = val + } + if !sizeAllowed(allowed, sz) { + return 0, contextualizeAt(p.line, p.col, fmt.Errorf("illegal size for instruction")) + } + return sz, nil +} + +func sizeFromIdent(s string) (instructions.Size, bool) { + switch strings.ToLower(s) { + case "b": + return instructions.ByteSize, true + case "s": + return instructions.ByteSize, true + case "w": + return instructions.WordSize, true + case "l": + return instructions.LongSize, true + default: + return 0, false + } +} + +func (p *Parser) parseRegList() (uint16, error) { + mask := uint16(0) + for { + regTok, err := p.want(IDENT) + if err != nil { + return 0, err + } + isA, reg, err := parseRegName(regTok) + if err != nil { + return 0, err + } + endIsA, endReg := isA, reg + if p.accept(MINUS) { + toTok, err := p.want(IDENT) + if err != nil { + return 0, err + } + endIsA, endReg, err = parseRegName(toTok) + if err != nil { + return 0, err + } + if endIsA != isA { + return 0, errorAtToken(toTok, fmt.Errorf("register ranges must stay within D or A registers")) + } + if endReg < reg { + return 0, errorAtToken(toTok, fmt.Errorf("descending ranges are not allowed")) + } + } + for r := reg; r <= endReg; r++ { + bit := uint16(1 << r) + if isA { + bit <<= 8 + } + mask |= bit + } + if p.peek().Kind == SLASH { + p.next() + continue + } + if p.peek().Kind == COMMA { + nxt := p.peekN(2) + if nxt.Kind == IDENT { + if ok, _ := isRegDn(nxt.Text); ok { + p.next() + continue + } + if ok, _ := isRegAn(nxt.Text); ok { + p.next() + continue + } + } + return mask, nil + } + break + } + return mask, nil +} + +func parseRegName(tok Token) (bool, int, error) { + if ok, dn := isRegDn(tok.Text); ok { + return false, dn, nil + } + if ok, an := isRegAn(tok.Text); ok { + return true, an, nil + } + return false, 0, errorAtToken(tok, fmt.Errorf("expected register in list")) +} diff --git a/internal/asm/parser_stmt.go b/internal/asm/parser_stmt.go new file mode 100644 index 0000000..b243f06 --- /dev/null +++ b/internal/asm/parser_stmt.go @@ -0,0 +1,225 @@ +package asm + +import ( + "fmt" + "math" + "strings" + + "github.com/jenska/m68kasm/internal/asm/instructions" +) + +func (p *Parser) parseStmt() (bool, error) { + t := p.peek() + + if t.Kind == IDENT { + if def, ok := p.macros[t.Text]; ok { + return p.invokeMacro(def) + } + if p.isConstDefinitionStart() { + return false, p.parseConstDefinition(t) + } + base, suffix := splitMnemonic(t.Text) + if instrDef := p.instrs.Lookup(base); instrDef != nil { + return false, p.parseInstruction(instrDef) + } + + if base == "DC" && suffix != "" { + _ = p.next() + return false, parseDC(p, suffix) + } + + if pseudo, ok := lookupPseudo(t.Text); ok { + _ = p.next() + return false, pseudo(p) + } + return false, parserError(t, "unknown mnemonic") + } + + if t.Kind == DOT { + p.next() + id, err := p.want(IDENT) + if err != nil { + return false, err + } + name := "." + strings.ToUpper(id.Text) + if pseudo, ok := pseudoMap[name]; ok { + return false, pseudo(p) + } + return false, parserError(t, "unknown pseudo op") + } + return false, parserError(t, "unexpected token") +} + +func (p *Parser) isConstDefinitionStart() bool { + if p.peekN(2).Kind == EQUAL { + return true + } + return p.peekN(2).Kind == DOT && isEquToken(p.peekN(3)) +} + +func splitMnemonic(text string) (base, suffix string) { + upper := strings.ToUpper(text) + if before, after, ok := strings.Cut(upper, "."); ok { + return before, after + } + return upper, "" +} + +func lookupPseudo(text string) (func(*Parser) error, bool) { + pseudo, ok := pseudoMap["."+strings.ToUpper(text)] + return pseudo, ok +} + +func isEquToken(tok Token) bool { + return tok.Kind == IDENT && strings.EqualFold(tok.Text, "equ") +} + +func (p *Parser) parseConstDefinition(nameTok Token) error { + _ = p.next() // consume name + + if p.peek().Kind == DOT { + p.next() + eqTok, err := p.want(IDENT) + if err != nil { + return err + } + if !isEquToken(eqTok) { + return parserError(eqTok, "expected EQU") + } + } else { + if _, err := p.want(EQUAL); err != nil { + return err + } + } + + val, err := p.parseExpr() + if err != nil { + return err + } + if val < 0 || val > math.MaxUint32 { + return errorAtLine(nameTok.Line, fmt.Errorf("constant out of 32-bit range: %d", val)) + } + + p.labels[nameTok.Text] = uint32(val) + return nil +} + +func (p *Parser) parseInstruction(instrDef *instructions.InstrDef) error { + if p.section == SectionBSS { + return errorAtLine(p.line, fmt.Errorf("instructions are not allowed in %s", p.section.Name())) + } + + mn, err := p.want(IDENT) + if err != nil { + return err + } + + operandTokens := p.consumeUntilEOL() + defer p.releaseTokens(operandTokens) + var lastErr error + for i := range instrDef.Forms { + form := &instrDef.Forms[i] + args, err := p.tryParseForm(mn, form, operandTokens) + if err != nil { + lastErr = err + continue + } + ins := &Instr{Def: instrDef, Form: form, Args: args, PC: p.pc, Line: mn.Line, Col: mn.Col, Section: p.section} + p.items = append(p.items, ins) + words, err := instructionWords(form, args) + if err != nil { + return err + } + p.pc += uint32(words * 2) + return nil + } + + if lastErr != nil { + return contextualizeAt(mn.Line, mn.Col, lastErr) + } + return &Error{Line: mn.Line, Col: mn.Col, Err: fmt.Errorf("no form matches operands")} +} + +func instructionWords(form *instructions.FormDef, args instructions.Args) (int, error) { + words := 0 + + var srcEA, dstEA instructions.EAEncoded + var err error + + if args.Src.Kind != instructions.EAkNone { + srcEA, err = instructions.EncodeEA(args.Src, 0) + if err != nil { + return 0, err + } + } + if args.Dst.Kind != instructions.EAkNone { + dstEA, err = instructions.EncodeEA(args.Dst, 0) + if err != nil { + return 0, err + } + } + + for _, step := range form.Steps { + haveWord := (step.WordBits != 0) || (len(step.Fields) > 0) + if haveWord { + words++ + } + for _, tr := range step.Trailer { + switch tr { + case instructions.TSrcEAExt: + words += len(srcEA.Ext) + case instructions.TDstEAExt: + words += len(dstEA.Ext) + case instructions.TImmSized: + words++ + case instructions.TSrcImm: + if args.Src.Kind == instructions.EAkImm { + switch args.Size { + case instructions.LongSize: + words += 2 + default: + words++ + } + } + case instructions.TBranchWordIfNeeded: + if args.Size == instructions.WordSize { + words++ + } + case instructions.TSrcRegMask, instructions.TDstRegMask: + words++ + } + } + } + + return words, nil +} + +// emitPaddingBytes centralizes emitting zero/filled padding while keeping the +// generated output within a safe upper bound. Without this guard, a crafted +// .org/.align could request gigabytes of padding and exhaust memory. +func (p *Parser) emitPaddingBytes(count uint32, fill byte) error { + if count == 0 { + return nil + } + if count > maxProgramSize || p.pc > maxProgramSize-count { + return errorAtLine(p.line, fmt.Errorf("padding would exceed maximum program size of %d bytes", maxProgramSize)) + } + + buf := make([]byte, int(count)) + if fill != 0 { + for i := range buf { + buf[i] = fill + } + } + p.items = append(p.items, &DataBytes{Bytes: buf, PC: p.pc, Line: p.line, Col: p.col, Section: p.section}) + p.pc += count + return nil +} + +func (p *Parser) setSection(section SectionKind) error { + if section < p.section { + return errorAtLine(p.line, fmt.Errorf("sections must stay in .text -> .data -> .bss order")) + } + p.section = section + return nil +} diff --git a/internal/asm/srec.go b/internal/asm/srec.go index 4be3a8c..afeedac 100644 --- a/internal/asm/srec.go +++ b/internal/asm/srec.go @@ -31,10 +31,7 @@ func FormatSRecords(entries []ListingEntry, origin uint32, header string) []byte lines = append(lines, srecHeader(header)) for _, seg := range segments { for offset := 0; offset < len(seg.Data); offset += srecDataBytesPerRecord { - end := offset + srecDataBytesPerRecord - if end > len(seg.Data) { - end = len(seg.Data) - } + end := min(offset+srecDataBytesPerRecord, len(seg.Data)) lines = append(lines, s3Record(seg.Addr+uint32(offset), seg.Data[offset:end])) } } diff --git a/metadata_api.go b/metadata_api.go index a4d1486..ca97855 100644 --- a/metadata_api.go +++ b/metadata_api.go @@ -5,6 +5,7 @@ import ( "errors" "io" "os" + "slices" internal "github.com/jenska/m68kasm/internal/asm" ) @@ -57,8 +58,7 @@ func NormalizeError(err error) string { if err == nil { return "" } - var asmErr *Error - if errors.As(err, &asmErr) { + if asmErr, ok := errors.AsType[*Error](err); ok { return asmErr.Message() } return err.Error() @@ -124,7 +124,7 @@ func buildAssemblyResult(prog *internal.Program) (*AssemblyResult, error) { Bytes: out, Origin: prog.Origin, Labels: make(map[string]uint32, len(prog.DefinedLabels)), - DefinedLabels: append([]DefinedLabel(nil), prog.DefinedLabels...), + DefinedLabels: slices.Clone(prog.DefinedLabels), Listing: cloneListing(listing), LineAddresses: make(map[int]uint32, len(listing)+len(prog.DefinedLabels)), } @@ -155,7 +155,7 @@ func cloneListing(listing []ListingEntry) []ListingEntry { out[i] = ListingEntry{ Line: entry.Line, PC: entry.PC, - Bytes: append([]byte(nil), entry.Bytes...), + Bytes: slices.Clone(entry.Bytes), } } return out @@ -183,7 +183,7 @@ func collectInstructionMetadata(items []any, listing []ListingEntry) []Instructi md := InstructionMetadata{ Line: ins.Line, PC: ins.PC, - Bytes: append([]byte(nil), entry.Bytes...), + Bytes: slices.Clone(entry.Bytes), Size: len(entry.Bytes), Words: len(entry.Bytes) / 2, Canonical: canonicalInstruction(ins), diff --git a/readme.md b/readme.md index 617c91f..d788502 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,7 @@ # m68kasm [![CI](https://github.com/jenska/m68kasm/actions/workflows/ci.yml/badge.svg)](../../actions) -![Go Version](https://img.shields.io/badge/Go-1.26+-00ADD8?logo=go) +![Go Version](https://img.shields.io/badge/Go-1.27+-00ADD8?logo=go) ![License](https://img.shields.io/badge/license-MIT-informational) A compact, **table-driven Motorola 68000 assembler** written in Go. @@ -58,7 +58,7 @@ The assembler implements core principles of assembler construction: ## 🛠️ Installation -You can install the CLI directly from GitHub using Go 1.26+: +You can install the CLI directly from GitHub using Go 1.27+: ```bash go install github.com/jenska/m68kasm/cmd/m68kasm@latest