package commands import ( "strings" "testing" "c65gm/internal/compiler" "c65gm/internal/preproc" ) func TestLetCommand_WillHandle(t *testing.T) { cmd := &LetCommand{} tests := []struct { name string line string want bool }{ {"old syntax LET/GET", "LET a GET b", true}, {"old syntax LET/equals", "LET a = 10", true}, {"new syntax", "result = value", true}, {"not LET - arithmetic", "result = a + b", false}, // 5 params {"not LET - keyword", "ADD a TO b GIVING c", false}, {"wrong param count", "LET a b", false}, {"empty", "", 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 TestLetCommand_OldSyntax(t *testing.T) { tests := []struct { name string line string setupVars func(*compiler.SymbolTable) wantAsm []string wantErr bool }{ { name: "LET byte GET byte", line: "LET a GET b", setupVars: func(st *compiler.SymbolTable) { st.AddVar("a", "", compiler.KindByte, 0) st.AddVar("b", "", compiler.KindByte, 10) }, wantAsm: []string{ "\tlda b", "\tsta a", }, }, { name: "LET byte = literal", line: "LET a = 100", setupVars: func(st *compiler.SymbolTable) { st.AddVar("a", "", compiler.KindByte, 0) }, wantAsm: []string{ "\tlda #$64", "\tsta a", }, }, { name: "LET word GET word", line: "LET x GET y", setupVars: func(st *compiler.SymbolTable) { st.AddVar("x", "", compiler.KindWord, 0) st.AddVar("y", "", compiler.KindWord, 0x1234) }, wantAsm: []string{ "\tlda y", "\tsta x", "\tlda y+1", "\tsta x+1", }, }, { name: "LET word = literal", line: "LET x = $1234", setupVars: func(st *compiler.SymbolTable) { st.AddVar("x", "", compiler.KindWord, 0) }, wantAsm: []string{ "\tlda #$34", "\tsta x", "\tlda #$12", "\tsta x+1", }, }, { name: "LET word GET byte (zero-extend)", line: "LET x GET b", setupVars: func(st *compiler.SymbolTable) { st.AddVar("x", "", compiler.KindWord, 0) st.AddVar("b", "", compiler.KindByte, 100) }, wantAsm: []string{ "\tlda b", "\tsta x", "\tlda #0", "\tsta x+1", }, }, { name: "LET byte GET word (take low byte)", line: "LET b GET x", setupVars: func(st *compiler.SymbolTable) { st.AddVar("b", "", compiler.KindByte, 0) st.AddVar("x", "", compiler.KindWord, 0x1234) }, wantAsm: []string{ "\tlda x", "\tsta b", }, }, { name: "LET word = $0000 (optimization)", line: "LET x = 0", setupVars: func(st *compiler.SymbolTable) { st.AddVar("x", "", compiler.KindWord, 0) }, wantAsm: []string{ "\tlda #$00", "\tsta x", "\tsta x+1", }, }, { name: "LET word = $FFFF (optimization)", line: "LET x = $FFFF", setupVars: func(st *compiler.SymbolTable) { st.AddVar("x", "", compiler.KindWord, 0) }, wantAsm: []string{ "\tlda #$ff", "\tsta x", "\tsta x+1", }, }, { name: "LET with constant", line: "LET a = MAXVAL", setupVars: func(st *compiler.SymbolTable) { st.AddVar("a", "", compiler.KindByte, 0) st.AddConst("MAXVAL", "", compiler.KindByte, 255) }, wantAsm: []string{ "\tlda #$ff", "\tsta a", }, }, { name: "error: unknown destination", line: "LET unknown GET a", setupVars: func(st *compiler.SymbolTable) { st.AddVar("a", "", compiler.KindByte, 0) }, wantErr: true, }, { name: "error: wrong separator", line: "LET a TO b", setupVars: func(st *compiler.SymbolTable) { st.AddVar("a", "", compiler.KindByte, 0) st.AddVar("b", "", compiler.KindByte, 0) }, wantErr: true, }, { name: "error: cannot assign to constant", line: "LET MAXVAL = 100", setupVars: func(st *compiler.SymbolTable) { 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 := &LetCommand{} 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 !equalAsmLet(asm, tt.wantAsm) { t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s", strings.Join(asm, "\n"), strings.Join(tt.wantAsm, "\n")) } }) } } func TestLetCommand_NewSyntax(t *testing.T) { tests := []struct { name string line string setupVars func(*compiler.SymbolTable) wantAsm []string wantErr bool }{ { name: "byte = byte", line: "a = b", setupVars: func(st *compiler.SymbolTable) { st.AddVar("a", "", compiler.KindByte, 0) st.AddVar("b", "", compiler.KindByte, 10) }, wantAsm: []string{ "\tlda b", "\tsta a", }, }, { name: "byte = literal", line: "a = 100", setupVars: func(st *compiler.SymbolTable) { st.AddVar("a", "", compiler.KindByte, 0) }, wantAsm: []string{ "\tlda #$64", "\tsta a", }, }, { name: "word = word", line: "x = y", setupVars: func(st *compiler.SymbolTable) { st.AddVar("x", "", compiler.KindWord, 0) st.AddVar("y", "", compiler.KindWord, 0x1234) }, wantAsm: []string{ "\tlda y", "\tsta x", "\tlda y+1", "\tsta x+1", }, }, { name: "word = literal", line: "x = $1234", setupVars: func(st *compiler.SymbolTable) { st.AddVar("x", "", compiler.KindWord, 0) }, wantAsm: []string{ "\tlda #$34", "\tsta x", "\tlda #$12", "\tsta x+1", }, }, { name: "word = byte (zero-extend)", line: "x = b", setupVars: func(st *compiler.SymbolTable) { st.AddVar("x", "", compiler.KindWord, 0) st.AddVar("b", "", compiler.KindByte, 100) }, wantAsm: []string{ "\tlda b", "\tsta x", "\tlda #0", "\tsta x+1", }, }, { name: "byte = word (take low byte)", line: "b = x", setupVars: func(st *compiler.SymbolTable) { st.AddVar("b", "", compiler.KindByte, 0) st.AddVar("x", "", compiler.KindWord, 0x1234) }, wantAsm: []string{ "\tlda x", "\tsta b", }, }, { name: "word = 0 (optimization)", line: "x = 0", setupVars: func(st *compiler.SymbolTable) { st.AddVar("x", "", compiler.KindWord, 0) }, wantAsm: []string{ "\tlda #$00", "\tsta x", "\tsta x+1", }, }, { name: "word = $FFFF (optimization)", line: "x = 65535", setupVars: func(st *compiler.SymbolTable) { st.AddVar("x", "", compiler.KindWord, 0) }, wantAsm: []string{ "\tlda #$ff", "\tsta x", "\tsta x+1", }, }, { name: "word = $0102 (different bytes, no optimization)", line: "x = $0102", setupVars: func(st *compiler.SymbolTable) { st.AddVar("x", "", compiler.KindWord, 0) }, wantAsm: []string{ "\tlda #$02", "\tsta x", "\tlda #$01", "\tsta x+1", }, }, { name: "using constant", line: "a = MAXVAL", setupVars: func(st *compiler.SymbolTable) { st.AddVar("a", "", compiler.KindByte, 0) st.AddConst("MAXVAL", "", compiler.KindByte, 255) }, wantAsm: []string{ "\tlda #$ff", "\tsta a", }, }, { name: "expression with constant", line: "a = 10+5", setupVars: func(st *compiler.SymbolTable) { st.AddVar("a", "", compiler.KindByte, 0) }, wantAsm: []string{ "\tlda #$0f", "\tsta a", }, }, { name: "error: unknown destination", line: "unknown = a", setupVars: func(st *compiler.SymbolTable) { st.AddVar("a", "", compiler.KindByte, 0) }, wantErr: true, }, { name: "error: cannot assign to constant", line: "MAXVAL = 100", setupVars: func(st *compiler.SymbolTable) { 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 := &LetCommand{} 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 !equalAsmLet(asm, tt.wantAsm) { t.Errorf("Generate() mismatch\ngot:\n%s\nwant:\n%s", strings.Join(asm, "\n"), strings.Join(tt.wantAsm, "\n")) } }) } } // equalAsmLet compares two assembly slices for equality func equalAsmLet(a, b []string) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true }