package commands import ( "strings" "testing" "c65gm/internal/compiler" "c65gm/internal/preproc" ) func TestWhileBasicEqual(t *testing.T) { tests := []struct { name string whileLine string setupVars func(*compiler.SymbolTable) wantWhile []string wantWend []string }{ { name: "byte var == byte literal", whileLine: "WHILE x = 10", setupVars: func(st *compiler.SymbolTable) { st.AddVar("x", "", compiler.KindByte, 0) }, wantWhile: []string{ "_LOOP1", "\tlda x", "\tcmp #$0a", "\tbne _WEND1", }, wantWend: []string{ "\tjmp _LOOP1", "_WEND1", }, }, { name: "word var == word literal", whileLine: "WHILE x = 1000", setupVars: func(st *compiler.SymbolTable) { st.AddVar("x", "", compiler.KindWord, 0) }, wantWhile: []string{ "_LOOP1", "\tlda x", "\tcmp #$e8", "\tbne _WEND1", "\tlda x+1", "\tcmp #$03", "\tbne _WEND1", }, wantWend: []string{ "\tjmp _LOOP1", "_WEND1", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) tt.setupVars(ctx.SymbolTable) whileCmd := &WhileCommand{} wendCmd := &WendCommand{} whileLine := preproc.Line{ Text: tt.whileLine, Kind: preproc.Source, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), } wendLine := preproc.Line{ Text: "WEND", Kind: preproc.Source, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), } if err := whileCmd.Interpret(whileLine, ctx); err != nil { t.Fatalf("WHILE Interpret() error = %v", err) } whileAsm, err := whileCmd.Generate(ctx) if err != nil { t.Fatalf("WHILE Generate() error = %v", err) } if err := wendCmd.Interpret(wendLine, ctx); err != nil { t.Fatalf("WEND Interpret() error = %v", err) } wendAsm, err := wendCmd.Generate(ctx) if err != nil { t.Fatalf("WEND Generate() error = %v", err) } if !equalAsm(whileAsm, tt.wantWhile) { t.Errorf("WHILE Generate() mismatch\ngot:\n%s\nwant:\n%s", strings.Join(whileAsm, "\n"), strings.Join(tt.wantWhile, "\n")) } if !equalAsm(wendAsm, tt.wantWend) { t.Errorf("WEND Generate() mismatch\ngot:\n%s\nwant:\n%s", strings.Join(wendAsm, "\n"), strings.Join(tt.wantWend, "\n")) } }) } } func TestWhileAllOperators(t *testing.T) { tests := []struct { name string line string wantInst string }{ {"equal", "WHILE x = 10", "bne"}, {"not equal", "WHILE x <> 10", "beq"}, {"greater", "WHILE x > 10", "beq"}, {"less", "WHILE x < 10", "beq"}, {"greater equal", "WHILE x >= 10", "bcc"}, {"less equal", "WHILE x <= 10", "bcc"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0) cmd := &WhileCommand{} line := preproc.Line{ Text: tt.line, Kind: preproc.Source, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), } if err := cmd.Interpret(line, ctx); err != nil { t.Fatalf("Interpret() error = %v", err) } asm, err := cmd.Generate(ctx) if err != nil { t.Fatalf("Generate() error = %v", err) } found := false for _, inst := range asm { if strings.Contains(inst, tt.wantInst) { found = true break } } if !found { t.Errorf("Expected %s instruction not found in: %v", tt.wantInst, asm) } }) } } func TestWhileMixedTypes(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0) ctx.SymbolTable.AddVar("y", "", compiler.KindWord, 0) cmd := &WhileCommand{} line := preproc.Line{ Text: "WHILE x < y", Kind: preproc.Source, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), } if err := cmd.Interpret(line, ctx); err != nil { t.Fatalf("Interpret() error = %v", err) } asm, err := cmd.Generate(ctx) if err != nil { t.Fatalf("Generate() error = %v", err) } foundHighByteCheck := false for _, inst := range asm { if strings.Contains(inst, "y+1") { foundHighByteCheck = true break } } if !foundHighByteCheck { t.Error("Expected high byte check for word in mixed comparison") } } func TestWhileBreak(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0) whileCmd := &WhileCommand{} breakCmd := &BreakCommand{} wendCmd := &WendCommand{} pragmaIdx := pragma.GetCurrentPragmaSetIndex() whileLine := preproc.Line{Text: "WHILE x < 10", Kind: preproc.Source, PragmaSetIndex: pragmaIdx} breakLine := preproc.Line{Text: "BREAK", Kind: preproc.Source, PragmaSetIndex: pragmaIdx} wendLine := preproc.Line{Text: "WEND", Kind: preproc.Source, PragmaSetIndex: pragmaIdx} if err := whileCmd.Interpret(whileLine, ctx); err != nil { t.Fatalf("WHILE Interpret() error = %v", err) } whileAsm, _ := whileCmd.Generate(ctx) _ = whileAsm // body would go here if err := breakCmd.Interpret(breakLine, ctx); err != nil { t.Fatalf("BREAK Interpret() error = %v", err) } breakAsm, err := breakCmd.Generate(ctx) if err != nil { t.Fatalf("BREAK Generate() error = %v", err) } if err := wendCmd.Interpret(wendLine, ctx); err != nil { t.Fatalf("WEND Interpret() error = %v", err) } if len(breakAsm) != 1 || !strings.Contains(breakAsm[0], "jmp _WEND") { t.Errorf("BREAK should jump to WEND label, got: %v", breakAsm) } } func TestBreakOutsideLoop(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) cmd := &BreakCommand{} line := preproc.Line{ Text: "BREAK", Kind: preproc.Source, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), } err := cmd.Interpret(line, ctx) if err == nil { t.Fatal("BREAK outside loop should fail") } if !strings.Contains(err.Error(), "not inside WHILE") { t.Errorf("Wrong error message: %v", err) } } func TestWendWithoutWhile(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) cmd := &WendCommand{} line := preproc.Line{ Text: "WEND", Kind: preproc.Source, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), } err := cmd.Interpret(line, ctx) if err == nil { t.Fatal("WEND without WHILE should fail") } if !strings.Contains(err.Error(), "not inside WHILE") { t.Errorf("Wrong error message: %v", err) } } func TestWhileNested(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("i", "", compiler.KindByte, 0) ctx.SymbolTable.AddVar("j", "", compiler.KindByte, 0) pragmaIdx := pragma.GetCurrentPragmaSetIndex() while1 := &WhileCommand{} while2 := &WhileCommand{} wend1 := &WendCommand{} wend2 := &WendCommand{} if err := while1.Interpret(preproc.Line{Text: "WHILE i < 10", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("WHILE 1 error = %v", err) } asm1, err := while1.Generate(ctx) if err != nil { t.Fatalf("WHILE 1 Generate error = %v", err) } if err := while2.Interpret(preproc.Line{Text: "WHILE j < 5", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("WHILE 2 error = %v", err) } asm2, err := while2.Generate(ctx) if err != nil { t.Fatalf("WHILE 2 Generate error = %v", err) } if err := wend2.Interpret(preproc.Line{Text: "WEND", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("WEND 2 error = %v", err) } if err := wend1.Interpret(preproc.Line{Text: "WEND", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("WEND 1 error = %v", err) } if asm1[0] == asm2[0] { t.Error("Nested loops should have different labels") } } func TestWhileLongJump(t *testing.T) { pragma := preproc.NewPragma() pragma.AddPragma("_P_USE_LONG_JUMP", "1") ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0) cmd := &WhileCommand{} line := preproc.Line{ Text: "WHILE x = 10", Kind: preproc.Source, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), } if err := cmd.Interpret(line, ctx); err != nil { t.Fatalf("Interpret() error = %v", err) } asm, err := cmd.Generate(ctx) if err != nil { t.Fatalf("Generate() error = %v", err) } foundJmp := false for _, inst := range asm { if strings.Contains(inst, "jmp") { foundJmp = true break } } if !foundJmp { t.Error("Long jump mode should contain JMP instruction") } } func TestWhileConstant(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddConst("MAX", "", compiler.KindByte, 100) ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0) cmd := &WhileCommand{} line := preproc.Line{ Text: "WHILE x < MAX", Kind: preproc.Source, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), } if err := cmd.Interpret(line, ctx); err != nil { t.Fatalf("Interpret() error = %v", err) } asm, err := cmd.Generate(ctx) if err != nil { t.Fatalf("Generate() error = %v", err) } found := false for _, inst := range asm { if strings.Contains(inst, "#$64") { found = true break } } if !found { t.Error("Constant should be folded to immediate value") } } func TestWhileWrongParamCount(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) tests := []string{ "WHILE x", "WHILE x =", "WHILE x = 10 extra", } for _, text := range tests { cmd := &WhileCommand{} line := preproc.Line{ Text: text, Kind: preproc.Source, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), } err := cmd.Interpret(line, ctx) if err == nil { t.Errorf("Should fail with wrong param count: %s", text) } } } // Helper to compare assembly output func equalAsm(got, want []string) bool { if len(got) != len(want) { return false } for i := range got { if got[i] != want[i] { return false } } return true }