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 { t.Errorf("param1 should be literal (constant inlined), got isVar=true") } if cmd.param1Value != 100 { t.Errorf("param1 value = %d, want 100", cmd.param1Value) } }, }, { 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) } }