From 88f90fe5beb50f8a33007c1d729f5ebb1d088d03 Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Sun, 2 Nov 2025 18:07:14 +0100 Subject: [PATCH] Added and, or, xor and subtract commands --- internal/commands/add.go | 66 +--- internal/commands/add_test.go | 7 +- internal/commands/and.go | 237 ++++++++++++++ internal/commands/and_test.go | 426 ++++++++++++++++++++++++++ internal/commands/or.go | 237 ++++++++++++++ internal/commands/or_test.go | 426 ++++++++++++++++++++++++++ internal/commands/subtr.go | 259 ++++++++++++++++ internal/commands/subtr_test.go | 510 +++++++++++++++++++++++++++++++ internal/commands/xor.go | 237 ++++++++++++++ internal/commands/xor_test.go | 426 ++++++++++++++++++++++++++ internal/compiler/command.go | 45 +++ internal/compiler/symboltable.go | 8 + main.go | 4 + 13 files changed, 2836 insertions(+), 52 deletions(-) create mode 100644 internal/commands/and.go create mode 100644 internal/commands/and_test.go create mode 100644 internal/commands/or.go create mode 100644 internal/commands/or_test.go create mode 100644 internal/commands/subtr.go create mode 100644 internal/commands/subtr_test.go create mode 100644 internal/commands/xor.go create mode 100644 internal/commands/xor_test.go diff --git a/internal/commands/add.go b/internal/commands/add.go index 38d0465..b3654a5 100644 --- a/internal/commands/add.go +++ b/internal/commands/add.go @@ -103,15 +103,20 @@ func (c *AddCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) return fmt.Errorf("ADD: cannot assign to constant %q", destName) } c.destVarName = destSym.FullName() - c.destVarKind = getVarKind(destSym) + c.destVarKind = destSym.GetVarKind() // Parse param1 - if err := c.parseParam(params[1], &c.param1VarName, &c.param1VarKind, &c.param1Value, &c.param1IsVar, ctx, scope, constLookup); err != nil { + var err error + c.param1VarName, c.param1VarKind, c.param1Value, c.param1IsVar, err = compiler.ParseOperandParam( + params[1], ctx.SymbolTable, scope, constLookup) + if err != nil { return fmt.Errorf("ADD: param1: %w", err) } // Parse param2 - if err := c.parseParam(params[3], &c.param2VarName, &c.param2VarKind, &c.param2Value, &c.param2IsVar, ctx, scope, constLookup); err != nil { + c.param2VarName, c.param2VarKind, c.param2Value, c.param2IsVar, err = compiler.ParseOperandParam( + params[3], ctx.SymbolTable, scope, constLookup) + if err != nil { return fmt.Errorf("ADD: param2: %w", err) } @@ -139,15 +144,20 @@ func (c *AddCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) return fmt.Errorf("ADD: cannot assign to constant %q", destName) } c.destVarName = destSym.FullName() - c.destVarKind = getVarKind(destSym) + c.destVarKind = destSym.GetVarKind() // Parse param1 - if err := c.parseParam(params[2], &c.param1VarName, &c.param1VarKind, &c.param1Value, &c.param1IsVar, ctx, scope, constLookup); err != nil { + var err error + c.param1VarName, c.param1VarKind, c.param1Value, c.param1IsVar, err = compiler.ParseOperandParam( + params[2], ctx.SymbolTable, scope, constLookup) + if err != nil { return fmt.Errorf("ADD: param1: %w", err) } // Parse param2 - if err := c.parseParam(params[4], &c.param2VarName, &c.param2VarKind, &c.param2Value, &c.param2IsVar, ctx, scope, constLookup); err != nil { + c.param2VarName, c.param2VarKind, c.param2Value, c.param2IsVar, err = compiler.ParseOperandParam( + params[4], ctx.SymbolTable, scope, constLookup) + if err != nil { return fmt.Errorf("ADD: param2: %w", err) } } @@ -155,42 +165,6 @@ func (c *AddCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) return nil } -func (c *AddCommand) parseParam( - param string, - varName *string, - varKind *compiler.VarKind, - value *uint16, - isVar *bool, - ctx *compiler.CompilerContext, - scope []string, - constLookup utils.ConstantLookup, -) error { - // Try variable lookup first - sym := ctx.SymbolTable.Lookup(param, scope) - if sym != nil { - // It's a variable or constant - *varName = sym.FullName() - *varKind = getVarKind(sym) - *value = sym.Value - *isVar = true - return nil - } - - // Not a variable, must be an expression - val, err := utils.EvaluateExpression(param, constLookup) - if err != nil { - return fmt.Errorf("not a valid variable or expression: %w", err) - } - - if val < 0 || val > 65535 { - return fmt.Errorf("value %d out of range (0-65535)", val) - } - - *value = uint16(val) - *isVar = false - return nil -} - func (c *AddCommand) Generate(_ *compiler.CompilerContext) ([]string, error) { var asm []string @@ -263,11 +237,3 @@ func (c *AddCommand) Generate(_ *compiler.CompilerContext) ([]string, error) { return asm, nil } - -// getVarKind extracts VarKind from Symbol -func getVarKind(sym *compiler.Symbol) compiler.VarKind { - if sym.IsByte() { - return compiler.KindByte - } - return compiler.KindWord -} diff --git a/internal/commands/add_test.go b/internal/commands/add_test.go index cb9c6a6..7f888ac 100644 --- a/internal/commands/add_test.go +++ b/internal/commands/add_test.go @@ -181,8 +181,11 @@ func TestAddCommand_Interpret_OldSyntax(t *testing.T) { text: "ADD MAX TO a GIVING c", wantErr: false, check: func(t *testing.T, cmd *AddCommand) { - if !cmd.param1IsVar || cmd.param1VarName != "MAX" { - t.Errorf("param1 should be constant MAX") + if cmd.param1IsVar { + t.Errorf("param1 should be literal (constant inlined), got isVar=true") + } + if cmd.param1Value != 100 { + t.Errorf("param1 value = %d, want 100", cmd.param1Value) } }, }, diff --git a/internal/commands/and.go b/internal/commands/and.go new file mode 100644 index 0000000..b338013 --- /dev/null +++ b/internal/commands/and.go @@ -0,0 +1,237 @@ +package commands + +import ( + "fmt" + "strings" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" + "c65gm/internal/utils" +) + +// AndCommand handles bitwise AND operations +// Syntax: +// +// AND WITH GIVING # old syntax with WITH/GIVING +// AND WITH -> # old syntax with WITH/-> +// = & # new syntax +type AndCommand struct { + param1VarName string + param1VarKind compiler.VarKind + param1Value uint16 + param1IsVar bool + + param2VarName string + param2VarKind compiler.VarKind + param2Value uint16 + param2IsVar bool + + destVarName string + destVarKind compiler.VarKind +} + +func (c *AndCommand) WillHandle(line preproc.Line) bool { + params, err := utils.ParseParams(line.Text) + if err != nil || len(params) == 0 { + return false + } + + // Old syntax: AND ... (must have exactly 6 params) + if strings.ToUpper(params[0]) == "AND" && len(params) == 6 { + return true + } + + // New syntax: = & + if len(params) == 5 && params[1] == "=" && params[3] == "&" { + return true + } + + return false +} + +func (c *AndCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error { + // Clear state + c.param1VarName = "" + c.param1IsVar = false + c.param1Value = 0 + c.param2VarName = "" + c.param2IsVar = false + c.param2Value = 0 + c.destVarName = "" + + params, err := utils.ParseParams(line.Text) + if err != nil { + return err + } + + paramCount := len(params) + scope := ctx.CurrentScope() + + // Create constant lookup function + constLookup := func(name string) (int64, bool) { + sym := ctx.SymbolTable.Lookup(name, scope) + if sym != nil && sym.IsConst() { + return int64(sym.Value), true + } + return 0, false + } + + // Determine syntax and parse accordingly + if strings.ToUpper(params[0]) == "AND" { + // Old syntax: AND WITH GIVING/-> + if paramCount != 6 { + return fmt.Errorf("AND: wrong number of parameters (%d), expected 6", paramCount) + } + + separator1 := strings.ToUpper(params[2]) + if separator1 != "WITH" { + return fmt.Errorf("AND: parameter #3 must be 'WITH', got %q", params[2]) + } + + separator2 := strings.ToUpper(params[4]) + if separator2 != "GIVING" && separator2 != "->" { + return fmt.Errorf("AND: parameter #5 must be 'GIVING' or '->', got %q", params[4]) + } + + // Parse destination + destName := params[5] + destSym := ctx.SymbolTable.Lookup(destName, scope) + if destSym == nil { + return fmt.Errorf("AND: unknown variable %q", destName) + } + if destSym.IsConst() { + return fmt.Errorf("AND: cannot assign to constant %q", destName) + } + c.destVarName = destSym.FullName() + c.destVarKind = destSym.GetVarKind() + + // Parse param1 + var err error + c.param1VarName, c.param1VarKind, c.param1Value, c.param1IsVar, err = compiler.ParseOperandParam( + params[1], ctx.SymbolTable, scope, constLookup) + if err != nil { + return fmt.Errorf("AND: param1: %w", err) + } + + // Parse param2 + c.param2VarName, c.param2VarKind, c.param2Value, c.param2IsVar, err = compiler.ParseOperandParam( + params[3], ctx.SymbolTable, scope, constLookup) + if err != nil { + return fmt.Errorf("AND: param2: %w", err) + } + + } else { + // New syntax: = & + if paramCount != 5 { + return fmt.Errorf("AND: wrong number of parameters (%d), expected 5", paramCount) + } + + if params[1] != "=" { + return fmt.Errorf("AND: expected '=' at position 2, got %q", params[1]) + } + + if params[3] != "&" { + return fmt.Errorf("AND: expected '&' at position 4, got %q", params[3]) + } + + // Parse destination + destName := params[0] + destSym := ctx.SymbolTable.Lookup(destName, scope) + if destSym == nil { + return fmt.Errorf("AND: unknown variable %q", destName) + } + if destSym.IsConst() { + return fmt.Errorf("AND: cannot assign to constant %q", destName) + } + c.destVarName = destSym.FullName() + c.destVarKind = destSym.GetVarKind() + + // Parse param1 + var err error + c.param1VarName, c.param1VarKind, c.param1Value, c.param1IsVar, err = compiler.ParseOperandParam( + params[2], ctx.SymbolTable, scope, constLookup) + if err != nil { + return fmt.Errorf("AND: param1: %w", err) + } + + // Parse param2 + c.param2VarName, c.param2VarKind, c.param2Value, c.param2IsVar, err = compiler.ParseOperandParam( + params[4], ctx.SymbolTable, scope, constLookup) + if err != nil { + return fmt.Errorf("AND: param2: %w", err) + } + } + + return nil +} + +func (c *AndCommand) Generate(_ *compiler.CompilerContext) ([]string, error) { + var asm []string + + // If both params are literals, fold at compile time + if !c.param1IsVar && !c.param2IsVar { + result := c.param1Value & c.param2Value + lo := uint8(result & 0xFF) + hi := uint8((result >> 8) & 0xFF) + + asm = append(asm, fmt.Sprintf("\tlda #$%02x", lo)) + asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName)) + + if c.destVarKind == compiler.KindWord { + asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi)) + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName)) + } + + return asm, nil + } + + // At least one param is a variable - generate AND code + // Load param1 + if c.param1IsVar { + asm = append(asm, fmt.Sprintf("\tlda %s", c.param1VarName)) + } else { + asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8(c.param1Value&0xFF))) + } + + // AND with param2 + if c.param2IsVar { + asm = append(asm, fmt.Sprintf("\tand %s", c.param2VarName)) + } else { + asm = append(asm, fmt.Sprintf("\tand #$%02x", uint8(c.param2Value&0xFF))) + } + + // Store low byte + asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName)) + + // If destination is word, handle high byte + if c.destVarKind == compiler.KindWord { + // Load high byte of param1 + if c.param1IsVar { + if c.param1VarKind == compiler.KindWord { + asm = append(asm, fmt.Sprintf("\tlda %s+1", c.param1VarName)) + } else { + asm = append(asm, "\tlda #0") + } + } else { + hi := uint8((c.param1Value >> 8) & 0xFF) + asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi)) + } + + // AND with high byte of param2 + if c.param2IsVar { + if c.param2VarKind == compiler.KindWord { + asm = append(asm, fmt.Sprintf("\tand %s+1", c.param2VarName)) + } else { + asm = append(asm, "\tand #0") + } + } else { + hi := uint8((c.param2Value >> 8) & 0xFF) + asm = append(asm, fmt.Sprintf("\tand #$%02x", hi)) + } + + // Store high byte + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName)) + } + + return asm, nil +} diff --git a/internal/commands/and_test.go b/internal/commands/and_test.go new file mode 100644 index 0000000..63045fc --- /dev/null +++ b/internal/commands/and_test.go @@ -0,0 +1,426 @@ +package commands + +import ( + "strings" + "testing" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" +) + +func TestAndCommand_WillHandle(t *testing.T) { + cmd := &AndCommand{} + + tests := []struct { + name string + line string + want bool + }{ + {"old syntax WITH/GIVING", "AND a WITH b GIVING c", true}, + {"old syntax WITH/arrow", "AND a WITH b -> c", true}, + {"new syntax", "result = x & y", true}, + {"not AND", "ADD a TO b GIVING c", false}, + {"wrong param count", "AND a b c", false}, + {"empty", "", false}, + {"new syntax wrong op", "result = x + y", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + line := preproc.Line{Text: tt.line, Kind: preproc.Source} + got := cmd.WillHandle(line) + if got != tt.want { + t.Errorf("WillHandle() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAndCommand_OldSyntax(t *testing.T) { + tests := []struct { + name string + line string + setupVars func(*compiler.SymbolTable) + wantAsm []string + wantErr bool + }{ + { + name: "byte AND byte -> byte (variables)", + line: "AND a WITH b GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0xFF) + st.AddVar("b", "", compiler.KindByte, 0x0F) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda a", + "\tand b", + "\tsta result", + }, + }, + { + name: "byte AND byte -> word", + line: "AND a WITH b GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0xFF) + st.AddVar("b", "", compiler.KindByte, 0x0F) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda a", + "\tand b", + "\tsta result", + "\tlda #0", + "\tand #0", + "\tsta result+1", + }, + }, + { + name: "word AND word -> word", + line: "AND x WITH y GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("x", "", compiler.KindWord, 0x1234) + st.AddVar("y", "", compiler.KindWord, 0x0F0F) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda x", + "\tand y", + "\tsta result", + "\tlda x+1", + "\tand y+1", + "\tsta result+1", + }, + }, + { + name: "byte AND literal -> byte", + line: "AND a WITH $F0 GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0xFF) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda a", + "\tand #$f0", + "\tsta result", + }, + }, + { + name: "literal AND byte -> byte", + line: "AND 255 WITH b GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("b", "", compiler.KindByte, 0x0F) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda #$ff", + "\tand b", + "\tsta result", + }, + }, + { + name: "constant folding: 255 AND 15 -> byte", + line: "AND 255 WITH 15 GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda #$0f", + "\tsta result", + }, + }, + { + name: "constant folding: $FFFF AND $0F0F -> word", + line: "AND $FFFF WITH $0F0F GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda #$0f", + "\tsta result", + "\tlda #$0f", + "\tsta result+1", + }, + }, + { + name: "arrow syntax", + line: "AND a WITH b -> result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0) + st.AddVar("b", "", compiler.KindByte, 0) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda a", + "\tand b", + "\tsta result", + }, + }, + { + name: "word AND byte -> byte", + line: "AND wval WITH bval GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("wval", "", compiler.KindWord, 0x1234) + st.AddVar("bval", "", compiler.KindByte, 0xFF) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda wval", + "\tand bval", + "\tsta result", + }, + }, + { + name: "word AND byte -> word", + line: "AND wval WITH bval GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("wval", "", compiler.KindWord, 0x1234) + st.AddVar("bval", "", compiler.KindByte, 0xFF) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda wval", + "\tand bval", + "\tsta result", + "\tlda wval+1", + "\tand #0", + "\tsta result+1", + }, + }, + { + name: "error: unknown destination variable", + line: "AND a WITH b GIVING unknown", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0) + st.AddVar("b", "", compiler.KindByte, 0) + }, + wantErr: true, + }, + { + name: "error: wrong separator", + line: "AND a TO b GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0) + st.AddVar("b", "", compiler.KindByte, 0) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantErr: true, + }, + { + name: "error: cannot assign to constant", + line: "AND a WITH b GIVING MAXVAL", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0) + st.AddVar("b", "", compiler.KindByte, 0) + st.AddConst("MAXVAL", "", compiler.KindByte, 255) + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := compiler.NewCompilerContext(&preproc.Pragma{}) + tt.setupVars(ctx.SymbolTable) + + cmd := &AndCommand{} + line := preproc.Line{Text: tt.line, Kind: preproc.Source} + + err := cmd.Interpret(line, ctx) + if tt.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("Interpret() error = %v", err) + } + + asm, err := cmd.Generate(ctx) + if err != nil { + t.Fatalf("Generate() error = %v", err) + } + + if !equalAsm(asm, tt.wantAsm) { + t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s", + strings.Join(asm, "\n"), + strings.Join(tt.wantAsm, "\n")) + } + }) + } +} + +func TestAndCommand_NewSyntax(t *testing.T) { + tests := []struct { + name string + line string + setupVars func(*compiler.SymbolTable) + wantAsm []string + wantErr bool + }{ + { + name: "byte & byte -> byte", + line: "result = a & b", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0xFF) + st.AddVar("b", "", compiler.KindByte, 0x0F) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda a", + "\tand b", + "\tsta result", + }, + }, + { + name: "byte & byte -> word", + line: "result = a & b", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0xFF) + st.AddVar("b", "", compiler.KindByte, 0x0F) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda a", + "\tand b", + "\tsta result", + "\tlda #0", + "\tand #0", + "\tsta result+1", + }, + }, + { + name: "word & word -> word", + line: "result = x & y", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("x", "", compiler.KindWord, 0x1234) + st.AddVar("y", "", compiler.KindWord, 0x0F0F) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda x", + "\tand y", + "\tsta result", + "\tlda x+1", + "\tand y+1", + "\tsta result+1", + }, + }, + { + name: "variable & literal", + line: "result = a & $F0", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0xFF) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda a", + "\tand #$f0", + "\tsta result", + }, + }, + { + name: "constant folding", + line: "result = 255 & 15", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda #$0f", + "\tsta result", + }, + }, + { + name: "constant folding word", + line: "result = $FFFF & $1234", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda #$34", + "\tsta result", + "\tlda #$12", + "\tsta result+1", + }, + }, + { + name: "using constant in expression", + line: "result = a & MASK", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0xFF) + st.AddConst("MASK", "", compiler.KindByte, 0xF0) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda a", + "\tand #$f0", + "\tsta result", + }, + }, + { + name: "error: unknown destination", + line: "unknown = a & b", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0) + st.AddVar("b", "", compiler.KindByte, 0) + }, + wantErr: true, + }, + { + name: "error: wrong operator", + line: "result = a + b", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0) + st.AddVar("b", "", compiler.KindByte, 0) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := compiler.NewCompilerContext(&preproc.Pragma{}) + tt.setupVars(ctx.SymbolTable) + + cmd := &AndCommand{} + line := preproc.Line{Text: tt.line, Kind: preproc.Source} + + err := cmd.Interpret(line, ctx) + if tt.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("Interpret() error = %v", err) + } + + asm, err := cmd.Generate(ctx) + if err != nil { + t.Fatalf("Generate() error = %v", err) + } + + if !equalAsm(asm, tt.wantAsm) { + t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s", + strings.Join(asm, "\n"), + strings.Join(tt.wantAsm, "\n")) + } + }) + } +} + +// equalAsm compares two assembly slices for equality +func equalAsm(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/internal/commands/or.go b/internal/commands/or.go new file mode 100644 index 0000000..540fa1d --- /dev/null +++ b/internal/commands/or.go @@ -0,0 +1,237 @@ +package commands + +import ( + "fmt" + "strings" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" + "c65gm/internal/utils" +) + +// OrCommand handles bitwise OR operations +// Syntax: +// +// OR WITH GIVING # old syntax with WITH/GIVING +// OR WITH -> # old syntax with WITH/-> +// = | # new syntax +type OrCommand struct { + param1VarName string + param1VarKind compiler.VarKind + param1Value uint16 + param1IsVar bool + + param2VarName string + param2VarKind compiler.VarKind + param2Value uint16 + param2IsVar bool + + destVarName string + destVarKind compiler.VarKind +} + +func (c *OrCommand) WillHandle(line preproc.Line) bool { + params, err := utils.ParseParams(line.Text) + if err != nil || len(params) == 0 { + return false + } + + // Old syntax: OR ... (must have exactly 6 params) + if strings.ToUpper(params[0]) == "OR" && len(params) == 6 { + return true + } + + // New syntax: = | + if len(params) == 5 && params[1] == "=" && params[3] == "|" { + return true + } + + return false +} + +func (c *OrCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error { + // Clear state + c.param1VarName = "" + c.param1IsVar = false + c.param1Value = 0 + c.param2VarName = "" + c.param2IsVar = false + c.param2Value = 0 + c.destVarName = "" + + params, err := utils.ParseParams(line.Text) + if err != nil { + return err + } + + paramCount := len(params) + scope := ctx.CurrentScope() + + // Create constant lookup function + constLookup := func(name string) (int64, bool) { + sym := ctx.SymbolTable.Lookup(name, scope) + if sym != nil && sym.IsConst() { + return int64(sym.Value), true + } + return 0, false + } + + // Determine syntax and parse accordingly + if strings.ToUpper(params[0]) == "OR" { + // Old syntax: OR WITH GIVING/-> + if paramCount != 6 { + return fmt.Errorf("OR: wrong number of parameters (%d), expected 6", paramCount) + } + + separator1 := strings.ToUpper(params[2]) + if separator1 != "WITH" { + return fmt.Errorf("OR: parameter #3 must be 'WITH', got %q", params[2]) + } + + separator2 := strings.ToUpper(params[4]) + if separator2 != "GIVING" && separator2 != "->" { + return fmt.Errorf("OR: parameter #5 must be 'GIVING' or '->', got %q", params[4]) + } + + // Parse destination + destName := params[5] + destSym := ctx.SymbolTable.Lookup(destName, scope) + if destSym == nil { + return fmt.Errorf("OR: unknown variable %q", destName) + } + if destSym.IsConst() { + return fmt.Errorf("OR: cannot assign to constant %q", destName) + } + c.destVarName = destSym.FullName() + c.destVarKind = destSym.GetVarKind() + + // Parse param1 + var err error + c.param1VarName, c.param1VarKind, c.param1Value, c.param1IsVar, err = compiler.ParseOperandParam( + params[1], ctx.SymbolTable, scope, constLookup) + if err != nil { + return fmt.Errorf("OR: param1: %w", err) + } + + // Parse param2 + c.param2VarName, c.param2VarKind, c.param2Value, c.param2IsVar, err = compiler.ParseOperandParam( + params[3], ctx.SymbolTable, scope, constLookup) + if err != nil { + return fmt.Errorf("OR: param2: %w", err) + } + + } else { + // New syntax: = | + if paramCount != 5 { + return fmt.Errorf("OR: wrong number of parameters (%d), expected 5", paramCount) + } + + if params[1] != "=" { + return fmt.Errorf("OR: expected '=' at position 2, got %q", params[1]) + } + + if params[3] != "|" { + return fmt.Errorf("OR: expected '|' at position 4, got %q", params[3]) + } + + // Parse destination + destName := params[0] + destSym := ctx.SymbolTable.Lookup(destName, scope) + if destSym == nil { + return fmt.Errorf("OR: unknown variable %q", destName) + } + if destSym.IsConst() { + return fmt.Errorf("OR: cannot assign to constant %q", destName) + } + c.destVarName = destSym.FullName() + c.destVarKind = destSym.GetVarKind() + + // Parse param1 + var err error + c.param1VarName, c.param1VarKind, c.param1Value, c.param1IsVar, err = compiler.ParseOperandParam( + params[2], ctx.SymbolTable, scope, constLookup) + if err != nil { + return fmt.Errorf("OR: param1: %w", err) + } + + // Parse param2 + c.param2VarName, c.param2VarKind, c.param2Value, c.param2IsVar, err = compiler.ParseOperandParam( + params[4], ctx.SymbolTable, scope, constLookup) + if err != nil { + return fmt.Errorf("OR: param2: %w", err) + } + } + + return nil +} + +func (c *OrCommand) Generate(_ *compiler.CompilerContext) ([]string, error) { + var asm []string + + // If both params are literals, fold at compile time + if !c.param1IsVar && !c.param2IsVar { + result := c.param1Value | c.param2Value + lo := uint8(result & 0xFF) + hi := uint8((result >> 8) & 0xFF) + + asm = append(asm, fmt.Sprintf("\tlda #$%02x", lo)) + asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName)) + + if c.destVarKind == compiler.KindWord { + asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi)) + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName)) + } + + return asm, nil + } + + // At least one param is a variable - generate OR code + // Load param1 + if c.param1IsVar { + asm = append(asm, fmt.Sprintf("\tlda %s", c.param1VarName)) + } else { + asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8(c.param1Value&0xFF))) + } + + // OR with param2 + if c.param2IsVar { + asm = append(asm, fmt.Sprintf("\tora %s", c.param2VarName)) + } else { + asm = append(asm, fmt.Sprintf("\tora #$%02x", uint8(c.param2Value&0xFF))) + } + + // Store low byte + asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName)) + + // If destination is word, handle high byte + if c.destVarKind == compiler.KindWord { + // Load high byte of param1 + if c.param1IsVar { + if c.param1VarKind == compiler.KindWord { + asm = append(asm, fmt.Sprintf("\tlda %s+1", c.param1VarName)) + } else { + asm = append(asm, "\tlda #0") + } + } else { + hi := uint8((c.param1Value >> 8) & 0xFF) + asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi)) + } + + // OR with high byte of param2 + if c.param2IsVar { + if c.param2VarKind == compiler.KindWord { + asm = append(asm, fmt.Sprintf("\tora %s+1", c.param2VarName)) + } else { + asm = append(asm, "\tora #0") + } + } else { + hi := uint8((c.param2Value >> 8) & 0xFF) + asm = append(asm, fmt.Sprintf("\tora #$%02x", hi)) + } + + // Store high byte + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName)) + } + + return asm, nil +} diff --git a/internal/commands/or_test.go b/internal/commands/or_test.go new file mode 100644 index 0000000..cc87186 --- /dev/null +++ b/internal/commands/or_test.go @@ -0,0 +1,426 @@ +package commands + +import ( + "strings" + "testing" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" +) + +func TestOrCommand_WillHandle(t *testing.T) { + cmd := &OrCommand{} + + tests := []struct { + name string + line string + want bool + }{ + {"old syntax WITH/GIVING", "OR a WITH b GIVING c", true}, + {"old syntax WITH/arrow", "OR a WITH b -> c", true}, + {"new syntax", "result = x | y", true}, + {"not OR", "ADD a TO b GIVING c", false}, + {"wrong param count", "OR a b c", false}, + {"empty", "", false}, + {"new syntax wrong op", "result = x + y", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + line := preproc.Line{Text: tt.line, Kind: preproc.Source} + got := cmd.WillHandle(line) + if got != tt.want { + t.Errorf("WillHandle() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestOrCommand_OldSyntax(t *testing.T) { + tests := []struct { + name string + line string + setupVars func(*compiler.SymbolTable) + wantAsm []string + wantErr bool + }{ + { + name: "byte OR byte -> byte (variables)", + line: "OR a WITH b GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0xF0) + st.AddVar("b", "", compiler.KindByte, 0x0F) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda a", + "\tora b", + "\tsta result", + }, + }, + { + name: "byte OR byte -> word", + line: "OR a WITH b GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0xF0) + st.AddVar("b", "", compiler.KindByte, 0x0F) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda a", + "\tora b", + "\tsta result", + "\tlda #0", + "\tora #0", + "\tsta result+1", + }, + }, + { + name: "word OR word -> word", + line: "OR x WITH y GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("x", "", compiler.KindWord, 0x1234) + st.AddVar("y", "", compiler.KindWord, 0x0F0F) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda x", + "\tora y", + "\tsta result", + "\tlda x+1", + "\tora y+1", + "\tsta result+1", + }, + }, + { + name: "byte OR literal -> byte", + line: "OR a WITH $0F GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0xF0) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda a", + "\tora #$0f", + "\tsta result", + }, + }, + { + name: "literal OR byte -> byte", + line: "OR 15 WITH b GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("b", "", compiler.KindByte, 0xF0) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda #$0f", + "\tora b", + "\tsta result", + }, + }, + { + name: "constant folding: 15 OR 240 -> byte", + line: "OR 15 WITH 240 GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda #$ff", + "\tsta result", + }, + }, + { + name: "constant folding: $00F0 OR $0F00 -> word", + line: "OR $00F0 WITH $0F00 GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda #$f0", + "\tsta result", + "\tlda #$0f", + "\tsta result+1", + }, + }, + { + name: "arrow syntax", + line: "OR a WITH b -> result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0) + st.AddVar("b", "", compiler.KindByte, 0) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda a", + "\tora b", + "\tsta result", + }, + }, + { + name: "word OR byte -> byte", + line: "OR wval WITH bval GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("wval", "", compiler.KindWord, 0x1234) + st.AddVar("bval", "", compiler.KindByte, 0x0F) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda wval", + "\tora bval", + "\tsta result", + }, + }, + { + name: "word OR byte -> word", + line: "OR wval WITH bval GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("wval", "", compiler.KindWord, 0x1234) + st.AddVar("bval", "", compiler.KindByte, 0x0F) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda wval", + "\tora bval", + "\tsta result", + "\tlda wval+1", + "\tora #0", + "\tsta result+1", + }, + }, + { + name: "error: unknown destination variable", + line: "OR a WITH b GIVING unknown", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0) + st.AddVar("b", "", compiler.KindByte, 0) + }, + wantErr: true, + }, + { + name: "error: wrong separator", + line: "OR a TO b GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0) + st.AddVar("b", "", compiler.KindByte, 0) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantErr: true, + }, + { + name: "error: cannot assign to constant", + line: "OR a WITH b GIVING MAXVAL", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0) + st.AddVar("b", "", compiler.KindByte, 0) + st.AddConst("MAXVAL", "", compiler.KindByte, 255) + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := compiler.NewCompilerContext(&preproc.Pragma{}) + tt.setupVars(ctx.SymbolTable) + + cmd := &OrCommand{} + line := preproc.Line{Text: tt.line, Kind: preproc.Source} + + err := cmd.Interpret(line, ctx) + if tt.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("Interpret() error = %v", err) + } + + asm, err := cmd.Generate(ctx) + if err != nil { + t.Fatalf("Generate() error = %v", err) + } + + if !equalAsmOr(asm, tt.wantAsm) { + t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s", + strings.Join(asm, "\n"), + strings.Join(tt.wantAsm, "\n")) + } + }) + } +} + +func TestOrCommand_NewSyntax(t *testing.T) { + tests := []struct { + name string + line string + setupVars func(*compiler.SymbolTable) + wantAsm []string + wantErr bool + }{ + { + name: "byte | byte -> byte", + line: "result = a | b", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0xF0) + st.AddVar("b", "", compiler.KindByte, 0x0F) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda a", + "\tora b", + "\tsta result", + }, + }, + { + name: "byte | byte -> word", + line: "result = a | b", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0xF0) + st.AddVar("b", "", compiler.KindByte, 0x0F) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda a", + "\tora b", + "\tsta result", + "\tlda #0", + "\tora #0", + "\tsta result+1", + }, + }, + { + name: "word | word -> word", + line: "result = x | y", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("x", "", compiler.KindWord, 0x1234) + st.AddVar("y", "", compiler.KindWord, 0x0F0F) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda x", + "\tora y", + "\tsta result", + "\tlda x+1", + "\tora y+1", + "\tsta result+1", + }, + }, + { + name: "variable | literal", + line: "result = a | $0F", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0xF0) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda a", + "\tora #$0f", + "\tsta result", + }, + }, + { + name: "constant folding", + line: "result = 15 | 240", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda #$ff", + "\tsta result", + }, + }, + { + name: "constant folding word", + line: "result = $00F0 | $0F00", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda #$f0", + "\tsta result", + "\tlda #$0f", + "\tsta result+1", + }, + }, + { + name: "using constant in expression", + line: "result = a | BITS", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0xF0) + st.AddConst("BITS", "", compiler.KindByte, 0x0F) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda a", + "\tora #$0f", + "\tsta result", + }, + }, + { + name: "error: unknown destination", + line: "unknown = a | b", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0) + st.AddVar("b", "", compiler.KindByte, 0) + }, + wantErr: true, + }, + { + name: "error: wrong operator", + line: "result = a + b", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0) + st.AddVar("b", "", compiler.KindByte, 0) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := compiler.NewCompilerContext(&preproc.Pragma{}) + tt.setupVars(ctx.SymbolTable) + + cmd := &OrCommand{} + line := preproc.Line{Text: tt.line, Kind: preproc.Source} + + err := cmd.Interpret(line, ctx) + if tt.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("Interpret() error = %v", err) + } + + asm, err := cmd.Generate(ctx) + if err != nil { + t.Fatalf("Generate() error = %v", err) + } + + if !equalAsmOr(asm, tt.wantAsm) { + t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s", + strings.Join(asm, "\n"), + strings.Join(tt.wantAsm, "\n")) + } + }) + } +} + +// equalAsmOr compares two assembly slices for equality +func equalAsmOr(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/internal/commands/subtr.go b/internal/commands/subtr.go new file mode 100644 index 0000000..d2aef93 --- /dev/null +++ b/internal/commands/subtr.go @@ -0,0 +1,259 @@ +package commands + +import ( + "fmt" + "strings" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" + "c65gm/internal/utils" +) + +// SubtractCommand handles SUBTRACT operations +// Syntax: +// +// SUBTRACT FROM GIVING # old syntax: param1 - param2 +// SUBT - -> # old syntax: param1 - param2 +// = - # new syntax: param1 - param2 +// +// Note: FROM syntax swaps parameter order (SUBTRACT a FROM b means b-a) +type SubtractCommand struct { + param1VarName string + param1VarKind compiler.VarKind + param1Value uint16 + param1IsVar bool + + param2VarName string + param2VarKind compiler.VarKind + param2Value uint16 + param2IsVar bool + + destVarName string + destVarKind compiler.VarKind +} + +func (c *SubtractCommand) WillHandle(line preproc.Line) bool { + params, err := utils.ParseParams(line.Text) + if err != nil || len(params) == 0 { + return false + } + + // Old syntax: SUBTRACT/SUBT ... (must have exactly 6 params) + kw := strings.ToUpper(params[0]) + if (kw == "SUBTRACT" || kw == "SUBT") && len(params) == 6 { + return true + } + + // New syntax: = - + if len(params) == 5 && params[1] == "=" && params[3] == "-" { + return true + } + + return false +} + +func (c *SubtractCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error { + // Clear state + c.param1VarName = "" + c.param1IsVar = false + c.param1Value = 0 + c.param2VarName = "" + c.param2IsVar = false + c.param2Value = 0 + c.destVarName = "" + + params, err := utils.ParseParams(line.Text) + if err != nil { + return err + } + + paramCount := len(params) + scope := ctx.CurrentScope() + + // Create constant lookup function + constLookup := func(name string) (int64, bool) { + sym := ctx.SymbolTable.Lookup(name, scope) + if sym != nil && sym.IsConst() { + return int64(sym.Value), true + } + return 0, false + } + + // Determine syntax and parse accordingly + kw := strings.ToUpper(params[0]) + if kw == "SUBTRACT" || kw == "SUBT" { + // Old syntax: SUBTRACT/SUBT FROM/- GIVING/-> + if paramCount != 6 { + return fmt.Errorf("SUBTRACT: wrong number of parameters (%d), expected 6", paramCount) + } + + separator1 := strings.ToUpper(params[2]) + if separator1 != "FROM" && separator1 != "-" { + return fmt.Errorf("SUBTRACT: parameter #3 must be 'FROM' or '-', got %q", params[2]) + } + + separator2 := strings.ToUpper(params[4]) + if separator2 != "GIVING" && separator2 != "->" { + return fmt.Errorf("SUBTRACT: parameter #5 must be 'GIVING' or '->', got %q", params[4]) + } + + // Parse destination + destName := params[5] + destSym := ctx.SymbolTable.Lookup(destName, scope) + if destSym == nil { + return fmt.Errorf("SUBTRACT: unknown variable %q", destName) + } + if destSym.IsConst() { + return fmt.Errorf("SUBTRACT: cannot assign to constant %q", destName) + } + c.destVarName = destSym.FullName() + c.destVarKind = destSym.GetVarKind() + + // Parse parameters based on separator + // FROM syntax: SUBTRACT a FROM b means b - a (swap needed) + // Minus syntax: SUBT a - b means a - b (no swap) + var minuendParam, subtrahendParam string + if separator1 == "FROM" { + // Swap: position 4 becomes minuend, position 2 becomes subtrahend + minuendParam = params[3] + subtrahendParam = params[1] + } else { + // No swap: position 2 is minuend, position 4 is subtrahend + minuendParam = params[1] + subtrahendParam = params[3] + } + + // Parse minuend (param1) + var err error + c.param1VarName, c.param1VarKind, c.param1Value, c.param1IsVar, err = compiler.ParseOperandParam( + minuendParam, ctx.SymbolTable, scope, constLookup) + if err != nil { + return fmt.Errorf("SUBTRACT: minuend: %w", err) + } + + // Parse subtrahend (param2) + c.param2VarName, c.param2VarKind, c.param2Value, c.param2IsVar, err = compiler.ParseOperandParam( + subtrahendParam, ctx.SymbolTable, scope, constLookup) + if err != nil { + return fmt.Errorf("SUBTRACT: subtrahend: %w", err) + } + + } else { + // New syntax: = - + if paramCount != 5 { + return fmt.Errorf("SUBTRACT: wrong number of parameters (%d), expected 5", paramCount) + } + + if params[1] != "=" { + return fmt.Errorf("SUBTRACT: expected '=' at position 2, got %q", params[1]) + } + + if params[3] != "-" { + return fmt.Errorf("SUBTRACT: expected '-' at position 4, got %q", params[3]) + } + + // Parse destination + destName := params[0] + destSym := ctx.SymbolTable.Lookup(destName, scope) + if destSym == nil { + return fmt.Errorf("SUBTRACT: unknown variable %q", destName) + } + if destSym.IsConst() { + return fmt.Errorf("SUBTRACT: cannot assign to constant %q", destName) + } + c.destVarName = destSym.FullName() + c.destVarKind = destSym.GetVarKind() + + // Parse param1 (minuend) + var err error + c.param1VarName, c.param1VarKind, c.param1Value, c.param1IsVar, err = compiler.ParseOperandParam( + params[2], ctx.SymbolTable, scope, constLookup) + if err != nil { + return fmt.Errorf("SUBTRACT: param1: %w", err) + } + + // Parse param2 (subtrahend) + c.param2VarName, c.param2VarKind, c.param2Value, c.param2IsVar, err = compiler.ParseOperandParam( + params[4], ctx.SymbolTable, scope, constLookup) + if err != nil { + return fmt.Errorf("SUBTRACT: param2: %w", err) + } + } + + return nil +} + +func (c *SubtractCommand) Generate(_ *compiler.CompilerContext) ([]string, error) { + var asm []string + + // If both params are literals, fold at compile time + if !c.param1IsVar && !c.param2IsVar { + // param1 - param2 (minuend - subtrahend) + result := uint16(int32(c.param1Value) - int32(c.param2Value)) + lo := uint8(result & 0xFF) + hi := uint8((result >> 8) & 0xFF) + + asm = append(asm, fmt.Sprintf("\tlda #$%02x", lo)) + asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName)) + + if c.destVarKind == compiler.KindWord { + asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi)) + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName)) + } + + return asm, nil + } + + // At least one param is a variable - generate subtract code + // SEC sets carry for borrow + asm = append(asm, "\tsec") + + // Load minuend (param1) + if c.param1IsVar { + asm = append(asm, fmt.Sprintf("\tlda %s", c.param1VarName)) + } else { + asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8(c.param1Value&0xFF))) + } + + // Subtract subtrahend (param2) + if c.param2IsVar { + asm = append(asm, fmt.Sprintf("\tsbc %s", c.param2VarName)) + } else { + asm = append(asm, fmt.Sprintf("\tsbc #$%02x", uint8(c.param2Value&0xFF))) + } + + // Store low byte + asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName)) + + // If destination is word, handle high byte + if c.destVarKind == compiler.KindWord { + // Load high byte of minuend (param1) + if c.param1IsVar { + if c.param1VarKind == compiler.KindWord { + asm = append(asm, fmt.Sprintf("\tlda %s+1", c.param1VarName)) + } else { + asm = append(asm, "\tlda #0") + } + } else { + hi := uint8((c.param1Value >> 8) & 0xFF) + asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi)) + } + + // Subtract high byte of subtrahend (param2) + if c.param2IsVar { + if c.param2VarKind == compiler.KindWord { + asm = append(asm, fmt.Sprintf("\tsbc %s+1", c.param2VarName)) + } else { + asm = append(asm, "\tsbc #0") + } + } else { + hi := uint8((c.param2Value >> 8) & 0xFF) + asm = append(asm, fmt.Sprintf("\tsbc #$%02x", hi)) + } + + // Store high byte + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName)) + } + + return asm, nil +} diff --git a/internal/commands/subtr_test.go b/internal/commands/subtr_test.go new file mode 100644 index 0000000..8c3549d --- /dev/null +++ b/internal/commands/subtr_test.go @@ -0,0 +1,510 @@ +package commands + +import ( + "strings" + "testing" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" +) + +func TestSubtractCommand_WillHandle(t *testing.T) { + cmd := &SubtractCommand{} + + tests := []struct { + name string + line string + want bool + }{ + {"old syntax SUBTRACT FROM/GIVING", "SUBTRACT a FROM b GIVING c", true}, + {"old syntax SUBT FROM/arrow", "SUBT a FROM b -> c", true}, + {"old syntax SUBT minus/arrow", "SUBT a - b -> c", true}, + {"old syntax SUBTRACT minus/GIVING", "SUBTRACT a - b GIVING c", true}, + {"new syntax", "result = x - y", true}, + {"not SUBTRACT", "ADD a TO b GIVING c", false}, + {"wrong param count", "SUBTRACT a b c", false}, + {"empty", "", false}, + {"new syntax wrong op", "result = x + y", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + line := preproc.Line{Text: tt.line, Kind: preproc.Source} + got := cmd.WillHandle(line) + if got != tt.want { + t.Errorf("WillHandle() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSubtractCommand_OldSyntax_FROM(t *testing.T) { + tests := []struct { + name string + line string + setupVars func(*compiler.SymbolTable) + wantAsm []string + wantErr bool + }{ + { + name: "SUBTRACT a FROM b (means b-a)", + line: "SUBTRACT 5 FROM 10 GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda #$05", + "\tsta result", + }, + }, + { + name: "SUBTRACT byte FROM byte -> byte (variables)", + line: "SUBTRACT a FROM b GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 10) + st.AddVar("b", "", compiler.KindByte, 20) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tsec", + "\tlda b", + "\tsbc a", + "\tsta result", + }, + }, + { + name: "SUBT a FROM b -> c with arrow", + line: "SUBT a FROM b -> result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 10) + st.AddVar("b", "", compiler.KindByte, 20) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tsec", + "\tlda b", + "\tsbc a", + "\tsta result", + }, + }, + { + name: "SUBTRACT literal FROM variable", + line: "SUBTRACT 10 FROM a GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 20) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tsec", + "\tlda a", + "\tsbc #$0a", + "\tsta result", + }, + }, + { + name: "word FROM word -> word", + line: "SUBTRACT x FROM y GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("x", "", compiler.KindWord, 0x1000) + st.AddVar("y", "", compiler.KindWord, 0x2000) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tsec", + "\tlda y", + "\tsbc x", + "\tsta result", + "\tlda y+1", + "\tsbc x+1", + "\tsta result+1", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := compiler.NewCompilerContext(&preproc.Pragma{}) + tt.setupVars(ctx.SymbolTable) + + cmd := &SubtractCommand{} + line := preproc.Line{Text: tt.line, Kind: preproc.Source} + + err := cmd.Interpret(line, ctx) + if tt.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("Interpret() error = %v", err) + } + + asm, err := cmd.Generate(ctx) + if err != nil { + t.Fatalf("Generate() error = %v", err) + } + + if !equalAsmSubtr(asm, tt.wantAsm) { + t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s", + strings.Join(asm, "\n"), + strings.Join(tt.wantAsm, "\n")) + } + }) + } +} + +func TestSubtractCommand_OldSyntax_Minus(t *testing.T) { + tests := []struct { + name string + line string + setupVars func(*compiler.SymbolTable) + wantAsm []string + wantErr bool + }{ + { + name: "SUBT a - b (means a-b, no swap)", + line: "SUBT 10 - 5 GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda #$05", + "\tsta result", + }, + }, + { + name: "SUBT byte - byte -> byte (variables)", + line: "SUBT a - b -> result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 20) + st.AddVar("b", "", compiler.KindByte, 10) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tsec", + "\tlda a", + "\tsbc b", + "\tsta result", + }, + }, + { + name: "SUBTRACT a - b GIVING c", + line: "SUBTRACT a - b GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 20) + st.AddVar("b", "", compiler.KindByte, 10) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tsec", + "\tlda a", + "\tsbc b", + "\tsta result", + }, + }, + { + name: "SUBT variable - literal", + line: "SUBT a - 10 -> result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 20) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tsec", + "\tlda a", + "\tsbc #$0a", + "\tsta result", + }, + }, + { + name: "word - word -> word", + line: "SUBT x - y -> result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("x", "", compiler.KindWord, 0x2000) + st.AddVar("y", "", compiler.KindWord, 0x1000) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tsec", + "\tlda x", + "\tsbc y", + "\tsta result", + "\tlda x+1", + "\tsbc y+1", + "\tsta result+1", + }, + }, + { + name: "byte - byte -> word", + line: "SUBT a - b GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 20) + st.AddVar("b", "", compiler.KindByte, 10) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tsec", + "\tlda a", + "\tsbc b", + "\tsta result", + "\tlda #0", + "\tsbc #0", + "\tsta result+1", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := compiler.NewCompilerContext(&preproc.Pragma{}) + tt.setupVars(ctx.SymbolTable) + + cmd := &SubtractCommand{} + line := preproc.Line{Text: tt.line, Kind: preproc.Source} + + err := cmd.Interpret(line, ctx) + if tt.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("Interpret() error = %v", err) + } + + asm, err := cmd.Generate(ctx) + if err != nil { + t.Fatalf("Generate() error = %v", err) + } + + if !equalAsmSubtr(asm, tt.wantAsm) { + t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s", + strings.Join(asm, "\n"), + strings.Join(tt.wantAsm, "\n")) + } + }) + } +} + +func TestSubtractCommand_NewSyntax(t *testing.T) { + tests := []struct { + name string + line string + setupVars func(*compiler.SymbolTable) + wantAsm []string + wantErr bool + }{ + { + name: "byte - byte -> byte", + line: "result = a - b", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 20) + st.AddVar("b", "", compiler.KindByte, 10) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tsec", + "\tlda a", + "\tsbc b", + "\tsta result", + }, + }, + { + name: "byte - byte -> word", + line: "result = a - b", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 20) + st.AddVar("b", "", compiler.KindByte, 10) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tsec", + "\tlda a", + "\tsbc b", + "\tsta result", + "\tlda #0", + "\tsbc #0", + "\tsta result+1", + }, + }, + { + name: "word - word -> word", + line: "result = x - y", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("x", "", compiler.KindWord, 0x2000) + st.AddVar("y", "", compiler.KindWord, 0x1000) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tsec", + "\tlda x", + "\tsbc y", + "\tsta result", + "\tlda x+1", + "\tsbc y+1", + "\tsta result+1", + }, + }, + { + name: "variable - literal", + line: "result = a - 10", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 20) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tsec", + "\tlda a", + "\tsbc #$0a", + "\tsta result", + }, + }, + { + name: "literal - variable", + line: "result = 20 - b", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("b", "", compiler.KindByte, 10) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tsec", + "\tlda #$14", + "\tsbc b", + "\tsta result", + }, + }, + { + name: "constant folding", + line: "result = 100 - 25", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda #$4b", + "\tsta result", + }, + }, + { + name: "constant folding word", + line: "result = $2000 - $1000", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda #$00", + "\tsta result", + "\tlda #$10", + "\tsta result+1", + }, + }, + { + name: "using constant in expression", + line: "result = a - OFFSET", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 100) + st.AddConst("OFFSET", "", compiler.KindByte, 10) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tsec", + "\tlda a", + "\tsbc #$0a", + "\tsta result", + }, + }, + { + name: "word - byte -> word", + line: "result = wval - bval", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("wval", "", compiler.KindWord, 0x1234) + st.AddVar("bval", "", compiler.KindByte, 0x10) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tsec", + "\tlda wval", + "\tsbc bval", + "\tsta result", + "\tlda wval+1", + "\tsbc #0", + "\tsta result+1", + }, + }, + { + name: "error: unknown destination", + line: "unknown = a - b", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0) + st.AddVar("b", "", compiler.KindByte, 0) + }, + wantErr: true, + }, + { + name: "error: wrong operator", + line: "result = a + b", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0) + st.AddVar("b", "", compiler.KindByte, 0) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantErr: true, + }, + { + name: "error: cannot assign to constant", + line: "MAXVAL = a - b", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0) + st.AddVar("b", "", compiler.KindByte, 0) + st.AddConst("MAXVAL", "", compiler.KindByte, 255) + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := compiler.NewCompilerContext(&preproc.Pragma{}) + tt.setupVars(ctx.SymbolTable) + + cmd := &SubtractCommand{} + line := preproc.Line{Text: tt.line, Kind: preproc.Source} + + err := cmd.Interpret(line, ctx) + if tt.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("Interpret() error = %v", err) + } + + asm, err := cmd.Generate(ctx) + if err != nil { + t.Fatalf("Generate() error = %v", err) + } + + if !equalAsmSubtr(asm, tt.wantAsm) { + t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s", + strings.Join(asm, "\n"), + strings.Join(tt.wantAsm, "\n")) + } + }) + } +} + +// equalAsmSubtr compares two assembly slices for equality +func equalAsmSubtr(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/internal/commands/xor.go b/internal/commands/xor.go new file mode 100644 index 0000000..72d3e5a --- /dev/null +++ b/internal/commands/xor.go @@ -0,0 +1,237 @@ +package commands + +import ( + "fmt" + "strings" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" + "c65gm/internal/utils" +) + +// XorCommand handles bitwise XOR operations +// Syntax: +// +// XOR WITH GIVING # old syntax with WITH/GIVING +// XOR WITH -> # old syntax with WITH/-> +// = ^ # new syntax +type XorCommand struct { + param1VarName string + param1VarKind compiler.VarKind + param1Value uint16 + param1IsVar bool + + param2VarName string + param2VarKind compiler.VarKind + param2Value uint16 + param2IsVar bool + + destVarName string + destVarKind compiler.VarKind +} + +func (c *XorCommand) WillHandle(line preproc.Line) bool { + params, err := utils.ParseParams(line.Text) + if err != nil || len(params) == 0 { + return false + } + + // Old syntax: XOR ... (must have exactly 6 params) + if strings.ToUpper(params[0]) == "XOR" && len(params) == 6 { + return true + } + + // New syntax: = ^ + if len(params) == 5 && params[1] == "=" && params[3] == "^" { + return true + } + + return false +} + +func (c *XorCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error { + // Clear state + c.param1VarName = "" + c.param1IsVar = false + c.param1Value = 0 + c.param2VarName = "" + c.param2IsVar = false + c.param2Value = 0 + c.destVarName = "" + + params, err := utils.ParseParams(line.Text) + if err != nil { + return err + } + + paramCount := len(params) + scope := ctx.CurrentScope() + + // Create constant lookup function + constLookup := func(name string) (int64, bool) { + sym := ctx.SymbolTable.Lookup(name, scope) + if sym != nil && sym.IsConst() { + return int64(sym.Value), true + } + return 0, false + } + + // Determine syntax and parse accordingly + if strings.ToUpper(params[0]) == "XOR" { + // Old syntax: XOR WITH GIVING/-> + if paramCount != 6 { + return fmt.Errorf("XOR: wrong number of parameters (%d), expected 6", paramCount) + } + + separator1 := strings.ToUpper(params[2]) + if separator1 != "WITH" { + return fmt.Errorf("XOR: parameter #3 must be 'WITH', got %q", params[2]) + } + + separator2 := strings.ToUpper(params[4]) + if separator2 != "GIVING" && separator2 != "->" { + return fmt.Errorf("XOR: parameter #5 must be 'GIVING' or '->', got %q", params[4]) + } + + // Parse destination + destName := params[5] + destSym := ctx.SymbolTable.Lookup(destName, scope) + if destSym == nil { + return fmt.Errorf("XOR: unknown variable %q", destName) + } + if destSym.IsConst() { + return fmt.Errorf("XOR: cannot assign to constant %q", destName) + } + c.destVarName = destSym.FullName() + c.destVarKind = destSym.GetVarKind() + + // Parse param1 + var err error + c.param1VarName, c.param1VarKind, c.param1Value, c.param1IsVar, err = compiler.ParseOperandParam( + params[1], ctx.SymbolTable, scope, constLookup) + if err != nil { + return fmt.Errorf("XOR: param1: %w", err) + } + + // Parse param2 + c.param2VarName, c.param2VarKind, c.param2Value, c.param2IsVar, err = compiler.ParseOperandParam( + params[3], ctx.SymbolTable, scope, constLookup) + if err != nil { + return fmt.Errorf("XOR: param2: %w", err) + } + + } else { + // New syntax: = ^ + if paramCount != 5 { + return fmt.Errorf("XOR: wrong number of parameters (%d), expected 5", paramCount) + } + + if params[1] != "=" { + return fmt.Errorf("XOR: expected '=' at position 2, got %q", params[1]) + } + + if params[3] != "^" { + return fmt.Errorf("XOR: expected '^' at position 4, got %q", params[3]) + } + + // Parse destination + destName := params[0] + destSym := ctx.SymbolTable.Lookup(destName, scope) + if destSym == nil { + return fmt.Errorf("XOR: unknown variable %q", destName) + } + if destSym.IsConst() { + return fmt.Errorf("XOR: cannot assign to constant %q", destName) + } + c.destVarName = destSym.FullName() + c.destVarKind = destSym.GetVarKind() + + // Parse param1 + var err error + c.param1VarName, c.param1VarKind, c.param1Value, c.param1IsVar, err = compiler.ParseOperandParam( + params[2], ctx.SymbolTable, scope, constLookup) + if err != nil { + return fmt.Errorf("XOR: param1: %w", err) + } + + // Parse param2 + c.param2VarName, c.param2VarKind, c.param2Value, c.param2IsVar, err = compiler.ParseOperandParam( + params[4], ctx.SymbolTable, scope, constLookup) + if err != nil { + return fmt.Errorf("XOR: param2: %w", err) + } + } + + return nil +} + +func (c *XorCommand) Generate(_ *compiler.CompilerContext) ([]string, error) { + var asm []string + + // If both params are literals, fold at compile time + if !c.param1IsVar && !c.param2IsVar { + result := c.param1Value ^ c.param2Value + lo := uint8(result & 0xFF) + hi := uint8((result >> 8) & 0xFF) + + asm = append(asm, fmt.Sprintf("\tlda #$%02x", lo)) + asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName)) + + if c.destVarKind == compiler.KindWord { + asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi)) + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName)) + } + + return asm, nil + } + + // At least one param is a variable - generate XOR code + // Load param1 + if c.param1IsVar { + asm = append(asm, fmt.Sprintf("\tlda %s", c.param1VarName)) + } else { + asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8(c.param1Value&0xFF))) + } + + // XOR with param2 + if c.param2IsVar { + asm = append(asm, fmt.Sprintf("\teor %s", c.param2VarName)) + } else { + asm = append(asm, fmt.Sprintf("\teor #$%02x", uint8(c.param2Value&0xFF))) + } + + // Store low byte + asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName)) + + // If destination is word, handle high byte + if c.destVarKind == compiler.KindWord { + // Load high byte of param1 + if c.param1IsVar { + if c.param1VarKind == compiler.KindWord { + asm = append(asm, fmt.Sprintf("\tlda %s+1", c.param1VarName)) + } else { + asm = append(asm, "\tlda #0") + } + } else { + hi := uint8((c.param1Value >> 8) & 0xFF) + asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi)) + } + + // XOR with high byte of param2 + if c.param2IsVar { + if c.param2VarKind == compiler.KindWord { + asm = append(asm, fmt.Sprintf("\teor %s+1", c.param2VarName)) + } else { + asm = append(asm, "\teor #0") + } + } else { + hi := uint8((c.param2Value >> 8) & 0xFF) + asm = append(asm, fmt.Sprintf("\teor #$%02x", hi)) + } + + // Store high byte + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName)) + } + + return asm, nil +} diff --git a/internal/commands/xor_test.go b/internal/commands/xor_test.go new file mode 100644 index 0000000..608ce89 --- /dev/null +++ b/internal/commands/xor_test.go @@ -0,0 +1,426 @@ +package commands + +import ( + "strings" + "testing" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" +) + +func TestXorCommand_WillHandle(t *testing.T) { + cmd := &XorCommand{} + + tests := []struct { + name string + line string + want bool + }{ + {"old syntax WITH/GIVING", "XOR a WITH b GIVING c", true}, + {"old syntax WITH/arrow", "XOR a WITH b -> c", true}, + {"new syntax", "result = x ^ y", true}, + {"not XOR", "ADD a TO b GIVING c", false}, + {"wrong param count", "XOR a b c", false}, + {"empty", "", false}, + {"new syntax wrong op", "result = x + y", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + line := preproc.Line{Text: tt.line, Kind: preproc.Source} + got := cmd.WillHandle(line) + if got != tt.want { + t.Errorf("WillHandle() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestXorCommand_OldSyntax(t *testing.T) { + tests := []struct { + name string + line string + setupVars func(*compiler.SymbolTable) + wantAsm []string + wantErr bool + }{ + { + name: "byte XOR byte -> byte (variables)", + line: "XOR a WITH b GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0xFF) + st.AddVar("b", "", compiler.KindByte, 0xAA) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda a", + "\teor b", + "\tsta result", + }, + }, + { + name: "byte XOR byte -> word", + line: "XOR a WITH b GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0xFF) + st.AddVar("b", "", compiler.KindByte, 0xAA) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda a", + "\teor b", + "\tsta result", + "\tlda #0", + "\teor #0", + "\tsta result+1", + }, + }, + { + name: "word XOR word -> word", + line: "XOR x WITH y GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("x", "", compiler.KindWord, 0x1234) + st.AddVar("y", "", compiler.KindWord, 0x5678) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda x", + "\teor y", + "\tsta result", + "\tlda x+1", + "\teor y+1", + "\tsta result+1", + }, + }, + { + name: "byte XOR literal -> byte", + line: "XOR a WITH $AA GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0xFF) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda a", + "\teor #$aa", + "\tsta result", + }, + }, + { + name: "literal XOR byte -> byte", + line: "XOR 255 WITH b GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("b", "", compiler.KindByte, 0xAA) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda #$ff", + "\teor b", + "\tsta result", + }, + }, + { + name: "constant folding: 255 XOR 170 -> byte", + line: "XOR 255 WITH 170 GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda #$55", + "\tsta result", + }, + }, + { + name: "constant folding: $FFFF XOR $AAAA -> word", + line: "XOR $FFFF WITH $AAAA GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda #$55", + "\tsta result", + "\tlda #$55", + "\tsta result+1", + }, + }, + { + name: "arrow syntax", + line: "XOR a WITH b -> result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0) + st.AddVar("b", "", compiler.KindByte, 0) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda a", + "\teor b", + "\tsta result", + }, + }, + { + name: "word XOR byte -> byte", + line: "XOR wval WITH bval GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("wval", "", compiler.KindWord, 0x1234) + st.AddVar("bval", "", compiler.KindByte, 0xFF) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda wval", + "\teor bval", + "\tsta result", + }, + }, + { + name: "word XOR byte -> word", + line: "XOR wval WITH bval GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("wval", "", compiler.KindWord, 0x1234) + st.AddVar("bval", "", compiler.KindByte, 0xFF) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda wval", + "\teor bval", + "\tsta result", + "\tlda wval+1", + "\teor #0", + "\tsta result+1", + }, + }, + { + name: "error: unknown destination variable", + line: "XOR a WITH b GIVING unknown", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0) + st.AddVar("b", "", compiler.KindByte, 0) + }, + wantErr: true, + }, + { + name: "error: wrong separator", + line: "XOR a TO b GIVING result", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0) + st.AddVar("b", "", compiler.KindByte, 0) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantErr: true, + }, + { + name: "error: cannot assign to constant", + line: "XOR a WITH b GIVING MAXVAL", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0) + st.AddVar("b", "", compiler.KindByte, 0) + st.AddConst("MAXVAL", "", compiler.KindByte, 255) + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := compiler.NewCompilerContext(&preproc.Pragma{}) + tt.setupVars(ctx.SymbolTable) + + cmd := &XorCommand{} + line := preproc.Line{Text: tt.line, Kind: preproc.Source} + + err := cmd.Interpret(line, ctx) + if tt.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("Interpret() error = %v", err) + } + + asm, err := cmd.Generate(ctx) + if err != nil { + t.Fatalf("Generate() error = %v", err) + } + + if !equalAsmXor(asm, tt.wantAsm) { + t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s", + strings.Join(asm, "\n"), + strings.Join(tt.wantAsm, "\n")) + } + }) + } +} + +func TestXorCommand_NewSyntax(t *testing.T) { + tests := []struct { + name string + line string + setupVars func(*compiler.SymbolTable) + wantAsm []string + wantErr bool + }{ + { + name: "byte ^ byte -> byte", + line: "result = a ^ b", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0xFF) + st.AddVar("b", "", compiler.KindByte, 0xAA) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda a", + "\teor b", + "\tsta result", + }, + }, + { + name: "byte ^ byte -> word", + line: "result = a ^ b", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0xFF) + st.AddVar("b", "", compiler.KindByte, 0xAA) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda a", + "\teor b", + "\tsta result", + "\tlda #0", + "\teor #0", + "\tsta result+1", + }, + }, + { + name: "word ^ word -> word", + line: "result = x ^ y", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("x", "", compiler.KindWord, 0x1234) + st.AddVar("y", "", compiler.KindWord, 0x5678) + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda x", + "\teor y", + "\tsta result", + "\tlda x+1", + "\teor y+1", + "\tsta result+1", + }, + }, + { + name: "variable ^ literal", + line: "result = a ^ $AA", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0xFF) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda a", + "\teor #$aa", + "\tsta result", + }, + }, + { + name: "constant folding", + line: "result = 255 ^ 170", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda #$55", + "\tsta result", + }, + }, + { + name: "constant folding word", + line: "result = $FFFF ^ $AAAA", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("result", "", compiler.KindWord, 0) + }, + wantAsm: []string{ + "\tlda #$55", + "\tsta result", + "\tlda #$55", + "\tsta result+1", + }, + }, + { + name: "using constant in expression", + line: "result = a ^ MASK", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0xFF) + st.AddConst("MASK", "", compiler.KindByte, 0xAA) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantAsm: []string{ + "\tlda a", + "\teor #$aa", + "\tsta result", + }, + }, + { + name: "error: unknown destination", + line: "unknown = a ^ b", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0) + st.AddVar("b", "", compiler.KindByte, 0) + }, + wantErr: true, + }, + { + name: "error: wrong operator", + line: "result = a + b", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("a", "", compiler.KindByte, 0) + st.AddVar("b", "", compiler.KindByte, 0) + st.AddVar("result", "", compiler.KindByte, 0) + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := compiler.NewCompilerContext(&preproc.Pragma{}) + tt.setupVars(ctx.SymbolTable) + + cmd := &XorCommand{} + line := preproc.Line{Text: tt.line, Kind: preproc.Source} + + err := cmd.Interpret(line, ctx) + if tt.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("Interpret() error = %v", err) + } + + asm, err := cmd.Generate(ctx) + if err != nil { + t.Fatalf("Generate() error = %v", err) + } + + if !equalAsmXor(asm, tt.wantAsm) { + t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s", + strings.Join(asm, "\n"), + strings.Join(tt.wantAsm, "\n")) + } + }) + } +} + +// equalAsmXor compares two assembly slices for equality +func equalAsmXor(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/internal/compiler/command.go b/internal/compiler/command.go index 3701646..d7ac6da 100644 --- a/internal/compiler/command.go +++ b/internal/compiler/command.go @@ -1,6 +1,7 @@ package compiler import ( + "c65gm/internal/utils" "fmt" "c65gm/internal/preproc" @@ -57,3 +58,47 @@ type UnhandledLineError struct { func (e *UnhandledLineError) Error() string { return fmt.Sprintf("%s:%d: unhandled line: %s", e.Line.Filename, e.Line.LineNo, e.Line.Text) } + +// ParseOperandParam parses a command parameter that can be a variable or expression +// Returns: varName, varKind, value, isVar, error +func ParseOperandParam( + param string, + symTable *SymbolTable, + scope []string, + constLookup utils.ConstantLookup, +) (varName string, varKind VarKind, value uint16, isVar bool, err error) { + // Try variable lookup first + sym := symTable.Lookup(param, scope) + if sym != nil { + varKind = sym.GetVarKind() + value = sym.Value + + // Constants are treated as literals (inlined), not variables + if sym.IsConst() { + varName = sym.FullName() // Preserve name for documentation + isVar = false + return + } + + // It's a variable + varName = sym.FullName() + isVar = true + return + } + + // Not a variable, must be an expression + val, evalErr := utils.EvaluateExpression(param, constLookup) + if evalErr != nil { + err = fmt.Errorf("not a valid variable or expression: %w", evalErr) + return + } + + if val < 0 || val > 65535 { + err = fmt.Errorf("value %d out of range (0-65535)", val) + return + } + + value = uint16(val) + isVar = false + return +} diff --git a/internal/compiler/symboltable.go b/internal/compiler/symboltable.go index 39e0fef..494ab8b 100644 --- a/internal/compiler/symboltable.go +++ b/internal/compiler/symboltable.go @@ -384,3 +384,11 @@ func GenerateVariables(st *SymbolTable) []string { return nil } + +// GetVarKind extracts VarKind from Symbol +func (s *Symbol) GetVarKind() VarKind { + if s.IsByte() { + return KindByte + } + return KindWord +} diff --git a/main.go b/main.go index 0a7646a..c639213 100644 --- a/main.go +++ b/main.go @@ -79,6 +79,10 @@ func registerCommands(comp *compiler.Compiler) { comp.Registry().Register(&commands.ByteCommand{}) comp.Registry().Register(&commands.WordCommand{}) comp.Registry().Register(&commands.AddCommand{}) + comp.Registry().Register(&commands.AndCommand{}) + comp.Registry().Register(&commands.OrCommand{}) + comp.Registry().Register(&commands.XorCommand{}) + comp.Registry().Register(&commands.SubtractCommand{}) } func writeOutput(filename string, lines []string) error {