diff --git a/internal/commands/add.go b/internal/commands/add.go new file mode 100644 index 0000000..38d0465 --- /dev/null +++ b/internal/commands/add.go @@ -0,0 +1,273 @@ +package commands + +import ( + "fmt" + "strings" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" + "c65gm/internal/utils" +) + +// AddCommand handles ADD operations +// Syntax: +// +// ADD TO GIVING # old syntax with TO/GIVING +// ADD + -> # old syntax with +/-> +// = + # new syntax +type AddCommand 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 *AddCommand) WillHandle(line preproc.Line) bool { + params, err := utils.ParseParams(line.Text) + if err != nil || len(params) == 0 { + return false + } + + // Old syntax: ADD ... (must have exactly 6 params) + if strings.ToUpper(params[0]) == "ADD" && len(params) == 6 { + return true + } + + // New syntax: = + + if len(params) == 5 && params[1] == "=" && params[3] == "+" { + return true + } + + return false +} + +func (c *AddCommand) 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]) == "ADD" { + // Old syntax: ADD TO/+ GIVING/-> + if paramCount != 6 { + return fmt.Errorf("ADD: wrong number of parameters (%d), expected 6", paramCount) + } + + separator1 := strings.ToUpper(params[2]) + if separator1 != "TO" && separator1 != "+" { + return fmt.Errorf("ADD: parameter #3 must be 'TO' or '+', got %q", params[2]) + } + + separator2 := strings.ToUpper(params[4]) + if separator2 != "GIVING" && separator2 != "->" { + return fmt.Errorf("ADD: 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("ADD: unknown variable %q", destName) + } + if destSym.IsConst() { + return fmt.Errorf("ADD: cannot assign to constant %q", destName) + } + c.destVarName = destSym.FullName() + c.destVarKind = getVarKind(destSym) + + // Parse param1 + if err := c.parseParam(params[1], &c.param1VarName, &c.param1VarKind, &c.param1Value, &c.param1IsVar, ctx, scope, constLookup); 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 { + return fmt.Errorf("ADD: param2: %w", err) + } + + } else { + // New syntax: = + + if paramCount != 5 { + return fmt.Errorf("ADD: wrong number of parameters (%d), expected 5", paramCount) + } + + if params[1] != "=" { + return fmt.Errorf("ADD: expected '=' at position 2, got %q", params[1]) + } + + if params[3] != "+" { + return fmt.Errorf("ADD: 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("ADD: unknown variable %q", destName) + } + if destSym.IsConst() { + return fmt.Errorf("ADD: cannot assign to constant %q", destName) + } + c.destVarName = destSym.FullName() + c.destVarKind = getVarKind(destSym) + + // Parse param1 + if err := c.parseParam(params[2], &c.param1VarName, &c.param1VarKind, &c.param1Value, &c.param1IsVar, ctx, scope, constLookup); 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 { + return fmt.Errorf("ADD: param2: %w", err) + } + } + + 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 + + // If both params are literals, fold at compile time + if !c.param1IsVar && !c.param2IsVar { + sum := uint32(c.param1Value) + uint32(c.param2Value) + lo := uint8(sum & 0xFF) + hi := uint8((sum >> 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 add code + asm = append(asm, "\tclc") + + // 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))) + } + + // Add param2 + if c.param2IsVar { + asm = append(asm, fmt.Sprintf("\tadc %s", c.param2VarName)) + } else { + asm = append(asm, fmt.Sprintf("\tadc #$%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)) + } + + // Add high byte of param2 + if c.param2IsVar { + if c.param2VarKind == compiler.KindWord { + asm = append(asm, fmt.Sprintf("\tadc %s+1", c.param2VarName)) + } else { + asm = append(asm, "\tadc #0") + } + } else { + hi := uint8((c.param2Value >> 8) & 0xFF) + asm = append(asm, fmt.Sprintf("\tadc #$%02x", hi)) + } + + // Store high byte + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName)) + } + + 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 new file mode 100644 index 0000000..cb9c6a6 --- /dev/null +++ b/internal/commands/add_test.go @@ -0,0 +1,717 @@ +package commands + +import ( + "strings" + "testing" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" +) + +func TestAddCommand_WillHandle(t *testing.T) { + tests := []struct { + name string + text string + want bool + }{ + // Old syntax + {"old syntax with TO/GIVING", "ADD a TO b GIVING c", true}, + {"old syntax with +/->", "ADD a + b -> c", true}, + {"old syntax mixed case", "add x to y giving z", true}, + + // New syntax + {"new syntax basic", "result = a + b", true}, + {"new syntax with literals", "x = 10 + 20", true}, + + // Should not handle + {"not add - subtract", "result = a - b", false}, + {"not add - multiply", "result = a * b", false}, + {"not add - wrong params", "ADD a b c", false}, + {"empty", "", false}, + {"just ADD", "ADD", false}, + {"assignment without add", "x = y", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := &AddCommand{} + line := preproc.Line{Text: tt.text} + if got := cmd.WillHandle(line); got != tt.want { + t.Errorf("WillHandle() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAddCommand_Interpret_OldSyntax(t *testing.T) { + tests := []struct { + name string + setup func(*compiler.CompilerContext) + text string + wantErr bool + check func(*testing.T, *AddCommand) + }{ + { + name: "byte + byte -> byte", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10) + ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 20) + ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0) + }, + text: "ADD a TO b GIVING c", + wantErr: false, + check: func(t *testing.T, cmd *AddCommand) { + if !cmd.param1IsVar || cmd.param1VarName != "a" { + t.Errorf("param1 should be var 'a'") + } + if !cmd.param2IsVar || cmd.param2VarName != "b" { + t.Errorf("param2 should be var 'b'") + } + if cmd.destVarName != "c" || cmd.destVarKind != compiler.KindByte { + t.Errorf("dest should be byte 'c'") + } + }, + }, + { + name: "word + word -> word", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("x", "", compiler.KindWord, 1000) + ctx.SymbolTable.AddVar("y", "", compiler.KindWord, 2000) + ctx.SymbolTable.AddVar("z", "", compiler.KindWord, 0) + }, + text: "ADD x TO y GIVING z", + wantErr: false, + check: func(t *testing.T, cmd *AddCommand) { + if cmd.destVarKind != compiler.KindWord { + t.Errorf("dest should be word") + } + }, + }, + { + name: "byte + word -> word", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10) + ctx.SymbolTable.AddVar("b", "", compiler.KindWord, 1000) + ctx.SymbolTable.AddVar("c", "", compiler.KindWord, 0) + }, + text: "ADD a TO b GIVING c", + wantErr: false, + check: func(t *testing.T, cmd *AddCommand) { + if cmd.param1VarKind != compiler.KindByte { + t.Errorf("param1 should be byte") + } + if cmd.param2VarKind != compiler.KindWord { + t.Errorf("param2 should be word") + } + if cmd.destVarKind != compiler.KindWord { + t.Errorf("dest should be word") + } + }, + }, + { + name: "literal + var -> var", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 20) + ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0) + }, + text: "ADD 10 TO b GIVING c", + wantErr: false, + check: func(t *testing.T, cmd *AddCommand) { + if cmd.param1IsVar { + t.Errorf("param1 should be literal") + } + if cmd.param1Value != 10 { + t.Errorf("param1 value = %d, want 10", cmd.param1Value) + } + }, + }, + { + name: "var + literal -> var", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10) + ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0) + }, + text: "ADD a TO 20 GIVING c", + wantErr: false, + check: func(t *testing.T, cmd *AddCommand) { + if cmd.param2IsVar { + t.Errorf("param2 should be literal") + } + if cmd.param2Value != 20 { + t.Errorf("param2 value = %d, want 20", cmd.param2Value) + } + }, + }, + { + name: "literal + literal -> var", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0) + }, + text: "ADD 15 TO 25 GIVING c", + wantErr: false, + check: func(t *testing.T, cmd *AddCommand) { + if cmd.param1IsVar || cmd.param2IsVar { + t.Errorf("both params should be literals") + } + if cmd.param1Value != 15 || cmd.param2Value != 25 { + t.Errorf("param values = %d, %d, want 15, 25", cmd.param1Value, cmd.param2Value) + } + }, + }, + { + name: "hex literal", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0) + }, + text: "ADD $10 TO $20 GIVING c", + wantErr: false, + check: func(t *testing.T, cmd *AddCommand) { + if cmd.param1Value != 0x10 || cmd.param2Value != 0x20 { + t.Errorf("param values = %d, %d, want 16, 32", cmd.param1Value, cmd.param2Value) + } + }, + }, + { + name: "constant usage", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddConst("MAX", "", compiler.KindByte, 100) + ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10) + ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0) + }, + 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") + } + }, + }, + { + name: "alternative syntax +/->", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10) + ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 20) + ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0) + }, + text: "ADD a + b -> c", + wantErr: false, + check: func(t *testing.T, cmd *AddCommand) { + if cmd.destVarName != "c" { + t.Errorf("dest should be c") + } + }, + }, + { + name: "unknown variable", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10) + }, + text: "ADD a TO b GIVING c", + wantErr: true, + }, + { + name: "assign to constant", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10) + ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 20) + ctx.SymbolTable.AddConst("MAX", "", compiler.KindByte, 100) + }, + text: "ADD a TO b GIVING MAX", + wantErr: true, + }, + { + name: "wrong parameter count", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10) + }, + text: "ADD a TO b", + wantErr: true, + }, + { + name: "wrong separator #3", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10) + ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 20) + ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0) + }, + text: "ADD a AND b GIVING c", + wantErr: true, + }, + { + name: "wrong separator #5", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10) + ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 20) + ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0) + }, + text: "ADD a TO b INTO c", + wantErr: true, + }, + { + name: "invalid expression", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0) + }, + text: "ADD @#$% TO 10 GIVING c", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := compiler.NewCompilerContext(preproc.NewPragma()) + if tt.setup != nil { + tt.setup(ctx) + } + + cmd := &AddCommand{} + line := preproc.Line{Text: tt.text} + + err := cmd.Interpret(line, ctx) + + if (err != nil) != tt.wantErr { + t.Errorf("Interpret() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr && tt.check != nil { + tt.check(t, cmd) + } + }) + } +} + +func TestAddCommand_Interpret_NewSyntax(t *testing.T) { + tests := []struct { + name string + setup func(*compiler.CompilerContext) + text string + wantErr bool + check func(*testing.T, *AddCommand) + }{ + { + name: "dest = var1 + var2", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10) + ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 20) + ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0) + }, + text: "result = a + b", + wantErr: false, + check: func(t *testing.T, cmd *AddCommand) { + if cmd.destVarName != "result" { + t.Errorf("dest = %q, want 'result'", cmd.destVarName) + } + if !cmd.param1IsVar || cmd.param1VarName != "a" { + t.Errorf("param1 should be var 'a'") + } + if !cmd.param2IsVar || cmd.param2VarName != "b" { + t.Errorf("param2 should be var 'b'") + } + }, + }, + { + name: "dest = literal + var", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 5) + ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0) + }, + text: "result = 100 + x", + wantErr: false, + check: func(t *testing.T, cmd *AddCommand) { + if cmd.param1IsVar { + t.Errorf("param1 should be literal") + } + if cmd.param1Value != 100 { + t.Errorf("param1 = %d, want 100", cmd.param1Value) + } + }, + }, + { + name: "dest = var + literal", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 5) + ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0) + }, + text: "result = x + 50", + wantErr: false, + check: func(t *testing.T, cmd *AddCommand) { + if cmd.param2IsVar { + t.Errorf("param2 should be literal") + } + if cmd.param2Value != 50 { + t.Errorf("param2 = %d, want 50", cmd.param2Value) + } + }, + }, + { + name: "dest = literal + literal", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0) + }, + text: "result = 30 + 70", + wantErr: false, + check: func(t *testing.T, cmd *AddCommand) { + if cmd.param1IsVar || cmd.param2IsVar { + t.Errorf("both params should be literals") + } + }, + }, + { + name: "word destination", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("a", "", compiler.KindWord, 1000) + ctx.SymbolTable.AddVar("b", "", compiler.KindWord, 2000) + ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0) + }, + text: "result = a + b", + wantErr: false, + check: func(t *testing.T, cmd *AddCommand) { + if cmd.destVarKind != compiler.KindWord { + t.Errorf("dest should be word") + } + }, + }, + { + name: "unknown dest variable", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10) + ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 20) + }, + text: "result = a + b", + wantErr: true, + }, + { + name: "assign to constant", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10) + ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 20) + ctx.SymbolTable.AddConst("MAX", "", compiler.KindByte, 100) + }, + text: "MAX = a + b", + wantErr: true, + }, + { + name: "wrong operator (not +)", + setup: func(ctx *compiler.CompilerContext) { + ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10) + ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 20) + ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0) + }, + text: "result = a - b", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := compiler.NewCompilerContext(preproc.NewPragma()) + if tt.setup != nil { + tt.setup(ctx) + } + + cmd := &AddCommand{} + line := preproc.Line{Text: tt.text} + + err := cmd.Interpret(line, ctx) + + if (err != nil) != tt.wantErr { + t.Errorf("Interpret() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !tt.wantErr && tt.check != nil { + tt.check(t, cmd) + } + }) + } +} + +func TestAddCommand_Generate(t *testing.T) { + tests := []struct { + name string + setup func(*compiler.CompilerContext) *AddCommand + wantLines []string + }{ + { + name: "constant folding - both literals to byte", + setup: func(ctx *compiler.CompilerContext) *AddCommand { + ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0) + return &AddCommand{ + param1IsVar: false, + param1Value: 10, + param2IsVar: false, + param2Value: 20, + destVarName: "result", + destVarKind: compiler.KindByte, + } + }, + wantLines: []string{ + "\tlda #$1e", + "\tsta result", + }, + }, + { + name: "constant folding - both literals to word", + setup: func(ctx *compiler.CompilerContext) *AddCommand { + ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0) + return &AddCommand{ + param1IsVar: false, + param1Value: 100, + param2IsVar: false, + param2Value: 200, + destVarName: "result", + destVarKind: compiler.KindWord, + } + }, + wantLines: []string{ + "\tlda #$2c", + "\tsta result", + "\tlda #$01", + "\tsta result+1", + }, + }, + { + name: "constant folding with overflow", + setup: func(ctx *compiler.CompilerContext) *AddCommand { + ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0) + return &AddCommand{ + param1IsVar: false, + param1Value: 200, + param2IsVar: false, + param2Value: 100, + destVarName: "result", + destVarKind: compiler.KindWord, + } + }, + wantLines: []string{ + "\tlda #$2c", + "\tsta result", + "\tlda #$01", + "\tsta result+1", + }, + }, + { + name: "byte + byte -> byte", + setup: func(ctx *compiler.CompilerContext) *AddCommand { + ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10) + ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 20) + ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0) + return &AddCommand{ + param1IsVar: true, + param1VarName: "a", + param1VarKind: compiler.KindByte, + param2IsVar: true, + param2VarName: "b", + param2VarKind: compiler.KindByte, + destVarName: "result", + destVarKind: compiler.KindByte, + } + }, + wantLines: []string{ + "\tclc", + "\tlda a", + "\tadc b", + "\tsta result", + }, + }, + { + name: "word + word -> word", + setup: func(ctx *compiler.CompilerContext) *AddCommand { + ctx.SymbolTable.AddVar("x", "", compiler.KindWord, 1000) + ctx.SymbolTable.AddVar("y", "", compiler.KindWord, 2000) + ctx.SymbolTable.AddVar("z", "", compiler.KindWord, 0) + return &AddCommand{ + param1IsVar: true, + param1VarName: "x", + param1VarKind: compiler.KindWord, + param2IsVar: true, + param2VarName: "y", + param2VarKind: compiler.KindWord, + destVarName: "z", + destVarKind: compiler.KindWord, + } + }, + wantLines: []string{ + "\tclc", + "\tlda x", + "\tadc y", + "\tsta z", + "\tlda x+1", + "\tadc y+1", + "\tsta z+1", + }, + }, + { + name: "byte + word -> word", + setup: func(ctx *compiler.CompilerContext) *AddCommand { + ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10) + ctx.SymbolTable.AddVar("w", "", compiler.KindWord, 1000) + ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0) + return &AddCommand{ + param1IsVar: true, + param1VarName: "a", + param1VarKind: compiler.KindByte, + param2IsVar: true, + param2VarName: "w", + param2VarKind: compiler.KindWord, + destVarName: "result", + destVarKind: compiler.KindWord, + } + }, + wantLines: []string{ + "\tclc", + "\tlda a", + "\tadc w", + "\tsta result", + "\tlda #0", + "\tadc w+1", + "\tsta result+1", + }, + }, + { + name: "literal + var -> byte", + setup: func(ctx *compiler.CompilerContext) *AddCommand { + ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10) + ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0) + return &AddCommand{ + param1IsVar: false, + param1Value: 5, + param2IsVar: true, + param2VarName: "a", + param2VarKind: compiler.KindByte, + destVarName: "result", + destVarKind: compiler.KindByte, + } + }, + wantLines: []string{ + "\tclc", + "\tlda #$05", + "\tadc a", + "\tsta result", + }, + }, + { + name: "var + literal -> word", + setup: func(ctx *compiler.CompilerContext) *AddCommand { + ctx.SymbolTable.AddVar("w", "", compiler.KindWord, 1000) + ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0) + return &AddCommand{ + param1IsVar: true, + param1VarName: "w", + param1VarKind: compiler.KindWord, + param2IsVar: false, + param2Value: 300, + destVarName: "result", + destVarKind: compiler.KindWord, + } + }, + wantLines: []string{ + "\tclc", + "\tlda w", + "\tadc #$2c", + "\tsta result", + "\tlda w+1", + "\tadc #$01", + "\tsta result+1", + }, + }, + { + name: "byte + byte -> word (promotion)", + setup: func(ctx *compiler.CompilerContext) *AddCommand { + ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10) + ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 20) + ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0) + return &AddCommand{ + param1IsVar: true, + param1VarName: "a", + param1VarKind: compiler.KindByte, + param2IsVar: true, + param2VarName: "b", + param2VarKind: compiler.KindByte, + destVarName: "result", + destVarKind: compiler.KindWord, + } + }, + wantLines: []string{ + "\tclc", + "\tlda a", + "\tadc b", + "\tsta result", + "\tlda #0", + "\tadc #0", + "\tsta result+1", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := compiler.NewCompilerContext(preproc.NewPragma()) + cmd := tt.setup(ctx) + + got, err := cmd.Generate(ctx) + if err != nil { + t.Fatalf("Generate() error = %v", err) + } + + if len(got) != len(tt.wantLines) { + t.Errorf("Generate() got %d lines, want %d lines\nGot:\n%s\nWant:\n%s", + len(got), len(tt.wantLines), + strings.Join(got, "\n"), strings.Join(tt.wantLines, "\n")) + return + } + + for i := range got { + if got[i] != tt.wantLines[i] { + t.Errorf("Line %d:\ngot: %q\nwant: %q", i, got[i], tt.wantLines[i]) + } + } + }) + } +} + +func TestAddCommand_Scopes(t *testing.T) { + ctx := compiler.NewCompilerContext(preproc.NewPragma()) + + // Global variables + ctx.SymbolTable.AddVar("globalA", "", compiler.KindByte, 10) + ctx.SymbolTable.AddVar("globalB", "", compiler.KindByte, 20) + ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0) + + // Simulate function declaration to enter scope + line := preproc.Line{Text: "FUNC myFunc"} + _, err := ctx.FunctionHandler.HandleFuncDecl(line) + if err != nil { + t.Fatalf("HandleFuncDecl() error = %v", err) + } + + // Function scope variables + ctx.SymbolTable.AddVar("localA", "myFunc", compiler.KindByte, 5) + ctx.SymbolTable.AddVar("localB", "myFunc", compiler.KindByte, 15) + + // Test local variables + cmd := &AddCommand{} + cmdLine := preproc.Line{Text: "result = localA + localB"} + if err := cmd.Interpret(cmdLine, ctx); err != nil { + t.Fatalf("Interpret() error = %v", err) + } + + if cmd.param1VarName != "myFunc_localA" { + t.Errorf("param1 = %q, want 'myFunc_localA'", cmd.param1VarName) + } + if cmd.param2VarName != "myFunc_localB" { + t.Errorf("param2 = %q, want 'myFunc_localB'", cmd.param2VarName) + } + + // Test global variables while in function scope + cmd2 := &AddCommand{} + cmdLine2 := preproc.Line{Text: "result = globalA + globalB"} + if err := cmd2.Interpret(cmdLine2, ctx); err != nil { + t.Fatalf("Interpret() error = %v", err) + } + + if cmd2.param1VarName != "globalA" { + t.Errorf("param1 = %q, want 'globalA'", cmd2.param1VarName) + } +} diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go index 4ddccbe..08ac382 100644 --- a/internal/compiler/compiler.go +++ b/internal/compiler/compiler.go @@ -68,6 +68,7 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) { return nil, fmt.Errorf("%s:%d: %w", line.Filename, line.LineNo, err) } + codeOutput = append(codeOutput, fmt.Sprintf("; %s", line.Text)) codeOutput = append(codeOutput, asmLines...) } diff --git a/main.go b/main.go index cd444ca..0a7646a 100644 --- a/main.go +++ b/main.go @@ -78,12 +78,7 @@ func registerCommands(comp *compiler.Compiler) { comp.Registry().Register(&commands.ByteCommand{}) comp.Registry().Register(&commands.WordCommand{}) - - // TODO: Add more commands as they're implemented: - // comp.Registry().Register(&commands.LetCommand{}) - // comp.Registry().Register(&commands.IfCommand{}) - // comp.Registry().Register(&commands.WhileCommand{}) - // etc. + comp.Registry().Register(&commands.AddCommand{}) } func writeOutput(filename string, lines []string) error {