package commands import ( "strings" "testing" "c65gm/internal/compiler" "c65gm/internal/preproc" ) func TestShiftLCommand_WillHandle(t *testing.T) { tests := []struct { name string text string want bool }{ // Old syntax {"old syntax BY/GIVING", "SHIFTL a BY b GIVING c", true}, {"old syntax <", "SHIFTL a << b -> c", true}, {"old syntax mixed case", "shiftl x by y giving z", true}, // New syntax {"new syntax basic", "result = a << b", true}, {"new syntax with literals", "x = 10 << 3", true}, // Should not handle {"not shiftl - shiftr", "SHIFTR a BY b GIVING c", false}, {"not shiftl - add", "result = a + b", false}, {"not shiftl - wrong params", "SHIFTL a b c", false}, {"empty", "", false}, {"just SHIFTL", "SHIFTL", false}, {"assignment without shift", "x = y", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cmd := &ShiftLCommand{} line := preproc.Line{Text: tt.text} if got := cmd.WillHandle(line); got != tt.want { t.Errorf("WillHandle() = %v, want %v", got, tt.want) } }) } } func TestShiftLCommand_Interpret_OldSyntax(t *testing.T) { tests := []struct { name string setup func(*compiler.CompilerContext) text string wantErr bool check func(*testing.T, *ShiftLCommand) }{ { name: "byte << byte -> byte", setup: func(ctx *compiler.CompilerContext) { ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 3, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) }, text: "SHIFTL a BY b GIVING c", wantErr: false, check: func(t *testing.T, cmd *ShiftLCommand) { if !cmd.sourceIsVar || cmd.sourceVarName != "a" { t.Errorf("source should be var 'a'") } if !cmd.amountIsVar || cmd.amountVarName != "b" { t.Errorf("amount should be var 'b'") } if cmd.destVarName != "c" || cmd.destVarKind != compiler.KindByte { t.Errorf("dest should be byte 'c'") } }, }, { name: "word << byte -> word", setup: func(ctx *compiler.CompilerContext) { ctx.SymbolTable.AddVar("x", "", compiler.KindWord, 1000, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("n", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) }, text: "SHIFTL x BY n GIVING result", wantErr: false, check: func(t *testing.T, cmd *ShiftLCommand) { if cmd.sourceVarKind != compiler.KindWord { t.Errorf("source should be word") } if cmd.destVarKind != compiler.KindWord { t.Errorf("dest should be word") } }, }, { name: "literal << var -> byte", setup: func(ctx *compiler.CompilerContext) { ctx.SymbolTable.AddVar("shift", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) }, text: "SHIFTL $FF BY shift GIVING result", wantErr: false, check: func(t *testing.T, cmd *ShiftLCommand) { if cmd.sourceIsVar { t.Errorf("source should be literal") } if cmd.sourceValue != 0xFF { t.Errorf("source value = %d, want 255", cmd.sourceValue) } }, }, { name: "var << literal -> byte", setup: func(ctx *compiler.CompilerContext) { ctx.SymbolTable.AddVar("value", "", compiler.KindByte, 1, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) }, text: "SHIFTL value BY 3 GIVING result", wantErr: false, check: func(t *testing.T, cmd *ShiftLCommand) { if cmd.amountIsVar { t.Errorf("amount should be literal") } if cmd.amountValue != 3 { t.Errorf("amount value = %d, want 3", cmd.amountValue) } }, }, { name: "arrow syntax", setup: func(ctx *compiler.CompilerContext) { ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) }, text: "SHIFTL a << b -> c", wantErr: false, check: func(t *testing.T, cmd *ShiftLCommand) { 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, preproc.Line{Filename: "test.c65", LineNo: 1}) }, text: "SHIFTL a BY b GIVING c", wantErr: true, }, { name: "assign to constant", setup: func(ctx *compiler.CompilerContext) { ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddConst("MAX", "", compiler.KindByte, 255, preproc.Line{Filename: "test.c65", LineNo: 1}) }, text: "SHIFTL a BY b GIVING MAX", wantErr: true, }, { name: "word amount variable (error)", setup: func(ctx *compiler.CompilerContext) { ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("b", "", compiler.KindWord, 2, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) }, text: "SHIFTL a BY b GIVING c", wantErr: true, }, { name: "amount constant > 255 (error)", setup: func(ctx *compiler.CompilerContext) { ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) }, text: "SHIFTL a BY 300 GIVING c", wantErr: true, }, { name: "wrong separator #3", setup: func(ctx *compiler.CompilerContext) { ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) }, text: "SHIFTL a WITH b GIVING c", wantErr: true, }, { name: "wrong separator #5", setup: func(ctx *compiler.CompilerContext) { ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) }, text: "SHIFTL a BY b INTO 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 := &ShiftLCommand{} 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 TestShiftLCommand_Interpret_NewSyntax(t *testing.T) { tests := []struct { name string setup func(*compiler.CompilerContext) text string wantErr bool check func(*testing.T, *ShiftLCommand) }{ { name: "dest = var << var", setup: func(ctx *compiler.CompilerContext) { ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) }, text: "result = a << b", wantErr: false, check: func(t *testing.T, cmd *ShiftLCommand) { if cmd.destVarName != "result" { t.Errorf("dest = %q, want 'result'", cmd.destVarName) } if !cmd.sourceIsVar || cmd.sourceVarName != "a" { t.Errorf("source should be var 'a'") } if !cmd.amountIsVar || cmd.amountVarName != "b" { t.Errorf("amount should be var 'b'") } }, }, { name: "dest = literal << literal", setup: func(ctx *compiler.CompilerContext) { ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) }, text: "result = $01 << 3", wantErr: false, check: func(t *testing.T, cmd *ShiftLCommand) { if cmd.sourceIsVar || cmd.amountIsVar { t.Errorf("both params should be literals") } if cmd.sourceValue != 1 || cmd.amountValue != 3 { t.Errorf("source=%d, amount=%d, want 1,3", cmd.sourceValue, cmd.amountValue) } }, }, { name: "word destination", setup: func(ctx *compiler.CompilerContext) { ctx.SymbolTable.AddVar("value", "", compiler.KindWord, 1000, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("shift", "", compiler.KindByte, 1, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) }, text: "result = value << shift", wantErr: false, check: func(t *testing.T, cmd *ShiftLCommand) { 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, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1}) }, text: "result = a << b", wantErr: true, }, { name: "wrong operator (not <<)", setup: func(ctx *compiler.CompilerContext) { ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) }, 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 := &ShiftLCommand{} 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 TestShiftLCommand_Generate(t *testing.T) { tests := []struct { name string setup func(*compiler.CompilerContext) *ShiftLCommand wantLines []string }{ { name: "constant folding - byte << 0", setup: func(ctx *compiler.CompilerContext) *ShiftLCommand { ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) return &ShiftLCommand{ sourceIsVar: false, sourceValue: 0x55, amountIsVar: false, amountValue: 0, destVarName: "result", destVarKind: compiler.KindByte, } }, wantLines: []string{ "\tlda #$55", "\tsta result", }, }, { name: "constant folding - byte << 3", setup: func(ctx *compiler.CompilerContext) *ShiftLCommand { ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) return &ShiftLCommand{ sourceIsVar: false, sourceValue: 0x01, amountIsVar: false, amountValue: 3, destVarName: "result", destVarKind: compiler.KindByte, } }, wantLines: []string{ "\tlda #$01", "\tasl", "\tasl", "\tasl", "\tsta result", }, }, { name: "constant folding - byte << 8 (zero)", setup: func(ctx *compiler.CompilerContext) *ShiftLCommand { ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) return &ShiftLCommand{ sourceIsVar: false, sourceValue: 0xFF, amountIsVar: false, amountValue: 8, destVarName: "result", destVarKind: compiler.KindByte, } }, wantLines: []string{ "\tlda #0", "\tsta result", }, }, { name: "constant folding - word << 1", setup: func(ctx *compiler.CompilerContext) *ShiftLCommand { ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) return &ShiftLCommand{ sourceIsVar: false, sourceValue: 0x1234, amountIsVar: false, amountValue: 1, destVarName: "result", destVarKind: compiler.KindWord, } }, wantLines: []string{ "\tlda #$34", "\tsta result", "\tlda #$12", "\tsta result+1", "\tasl result", "\trol result+1", }, }, { name: "constant folding - word << 16 (zero)", setup: func(ctx *compiler.CompilerContext) *ShiftLCommand { ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) return &ShiftLCommand{ sourceIsVar: false, sourceValue: 0xFFFF, amountIsVar: false, amountValue: 16, destVarName: "result", destVarKind: compiler.KindWord, } }, wantLines: []string{ "\tlda #0", "\tsta result", "\tsta result+1", }, }, { name: "byte variable << byte variable", setup: func(ctx *compiler.CompilerContext) *ShiftLCommand { ctx.SymbolTable.AddVar("value", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("shift", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) return &ShiftLCommand{ sourceIsVar: true, sourceVarName: "value", sourceVarKind: compiler.KindByte, amountIsVar: true, amountVarName: "shift", amountVarKind: compiler.KindByte, destVarName: "result", destVarKind: compiler.KindByte, } }, wantLines: []string{ "\tlda value", "\tldx shift", "\tbeq _L2", "_L1", "\tasl", "\tdex", "\tbne _L1", "_L2", "\tsta result", }, }, { name: "word variable << byte variable", setup: func(ctx *compiler.CompilerContext) *ShiftLCommand { ctx.SymbolTable.AddVar("value", "", compiler.KindWord, 1000, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("shift", "", compiler.KindByte, 3, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) return &ShiftLCommand{ sourceIsVar: true, sourceVarName: "value", sourceVarKind: compiler.KindWord, amountIsVar: true, amountVarName: "shift", amountVarKind: compiler.KindByte, destVarName: "result", destVarKind: compiler.KindWord, } }, wantLines: []string{ "\tlda value", "\tsta result", "\tlda value+1", "\tsta result+1", "\tldx shift", "\tbeq _L2", "_L1", "\tasl result", "\trol result+1", "\tdex", "\tbne _L1", "_L2", }, }, { name: "same source and dest", setup: func(ctx *compiler.CompilerContext) *ShiftLCommand { ctx.SymbolTable.AddVar("value", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("shift", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1}) return &ShiftLCommand{ sourceIsVar: true, sourceVarName: "value", sourceVarKind: compiler.KindByte, amountIsVar: true, amountVarName: "shift", amountVarKind: compiler.KindByte, destVarName: "value", destVarKind: compiler.KindByte, } }, wantLines: []string{ "\tlda value", "\tldx shift", "\tbeq _L2", "_L1", "\tasl", "\tdex", "\tbne _L1", "_L2", "\tsta value", }, }, { name: "WORD optimization - shift by 8", setup: func(ctx *compiler.CompilerContext) *ShiftLCommand { ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) return &ShiftLCommand{ sourceIsVar: false, sourceValue: 0x1234, amountIsVar: false, amountValue: 8, destVarName: "result", destVarKind: compiler.KindWord, } }, wantLines: []string{ "\tlda #$34", "\tsta result+1", "\tlda #0", "\tsta result", }, }, { name: "WORD optimization - shift by 12 (8 + 4)", setup: func(ctx *compiler.CompilerContext) *ShiftLCommand { ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) return &ShiftLCommand{ sourceIsVar: false, sourceValue: 0x00AB, amountIsVar: false, amountValue: 12, destVarName: "result", destVarKind: compiler.KindWord, } }, wantLines: []string{ "\tlda #$ab", "\tsta result+1", "\tlda #0", "\tsta result", "\tasl result+1", "\tasl result+1", "\tasl result+1", "\tasl result+1", }, }, { name: "BYTE to WORD conversion with shift", setup: func(ctx *compiler.CompilerContext) *ShiftLCommand { ctx.SymbolTable.AddVar("byteval", "", compiler.KindByte, 0x55, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) return &ShiftLCommand{ sourceIsVar: true, sourceVarName: "byteval", sourceVarKind: compiler.KindByte, amountIsVar: false, amountValue: 4, destVarName: "result", destVarKind: compiler.KindWord, } }, wantLines: []string{ "\tlda byteval", "\tsta result", "\tlda #0", "\tsta result+1", "\tasl result", "\trol result+1", "\tasl result", "\trol result+1", "\tasl result", "\trol result+1", "\tasl result", "\trol result+1", }, }, { name: "WORD to BYTE conversion with shift (truncation)", setup: func(ctx *compiler.CompilerContext) *ShiftLCommand { ctx.SymbolTable.AddVar("wordval", "", compiler.KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) return &ShiftLCommand{ sourceIsVar: true, sourceVarName: "wordval", sourceVarKind: compiler.KindWord, amountIsVar: false, amountValue: 4, destVarName: "result", destVarKind: compiler.KindByte, } }, wantLines: []string{ "\tlda wordval", "\tasl", "\tasl", "\tasl", "\tasl", "\tsta result", }, }, { name: "WORD same source and destination", setup: func(ctx *compiler.CompilerContext) *ShiftLCommand { ctx.SymbolTable.AddVar("value", "", compiler.KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1}) ctx.SymbolTable.AddVar("shift", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1}) return &ShiftLCommand{ sourceIsVar: true, sourceVarName: "value", sourceVarKind: compiler.KindWord, amountIsVar: true, amountVarName: "shift", amountVarKind: compiler.KindByte, destVarName: "value", destVarKind: compiler.KindWord, } }, wantLines: []string{ "\tldx shift", "\tbeq _L2", "_L1", "\tasl value", "\trol value+1", "\tdex", "\tbne _L1", "_L2", }, }, } 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 { // Skip exact label comparison (they're generated dynamically) if strings.HasPrefix(got[i], "_L") && strings.HasPrefix(tt.wantLines[i], "_L") { continue } if got[i] != tt.wantLines[i] { t.Errorf("Line %d:\ngot: %q\nwant: %q", i, got[i], tt.wantLines[i]) } } }) } }