package commands import ( "strings" "testing" "c65gm/internal/compiler" "c65gm/internal/preproc" ) func TestIfBasicEqual(t *testing.T) { tests := []struct { name string ifLine string setupVars func(*compiler.SymbolTable) wantIf []string wantEndif []string }{ { name: "byte var == byte literal", ifLine: "IF x = 10", setupVars: func(st *compiler.SymbolTable) { st.AddVar("x", "", compiler.KindByte, 0) }, wantIf: []string{ "\tlda x", "\tcmp #$0a", "\tbne _I1", }, wantEndif: []string{ "_I1", }, }, { name: "word var == word literal", ifLine: "IF x = 1000", setupVars: func(st *compiler.SymbolTable) { st.AddVar("x", "", compiler.KindWord, 0) }, wantIf: []string{ "\tlda x", "\tcmp #$e8", "\tbne _I1", "\tlda x+1", "\tcmp #$03", "\tbne _I1", }, wantEndif: []string{ "_I1", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) tt.setupVars(ctx.SymbolTable) ifCmd := &IfCommand{} endifCmd := &EndIfCommand{} ifLine := preproc.Line{ Text: tt.ifLine, Kind: preproc.Source, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), } endifLine := preproc.Line{ Text: "ENDIF", Kind: preproc.Source, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), } if err := ifCmd.Interpret(ifLine, ctx); err != nil { t.Fatalf("IF Interpret() error = %v", err) } ifAsm, err := ifCmd.Generate(ctx) if err != nil { t.Fatalf("IF Generate() error = %v", err) } if err := endifCmd.Interpret(endifLine, ctx); err != nil { t.Fatalf("ENDIF Interpret() error = %v", err) } endifAsm, err := endifCmd.Generate(ctx) if err != nil { t.Fatalf("ENDIF Generate() error = %v", err) } if !equalAsm(ifAsm, tt.wantIf) { t.Errorf("IF Generate() mismatch\ngot:\n%s\nwant:\n%s", strings.Join(ifAsm, "\n"), strings.Join(tt.wantIf, "\n")) } if !equalAsm(endifAsm, tt.wantEndif) { t.Errorf("ENDIF Generate() mismatch\ngot:\n%s\nwant:\n%s", strings.Join(endifAsm, "\n"), strings.Join(tt.wantEndif, "\n")) } }) } } func TestIfElseEndif(t *testing.T) { tests := []struct { name string ifLine string setupVars func(*compiler.SymbolTable) wantIf []string wantElse []string wantEndif []string }{ { name: "byte var with else", ifLine: "IF x = 10", setupVars: func(st *compiler.SymbolTable) { st.AddVar("x", "", compiler.KindByte, 0) }, wantIf: []string{ "\tlda x", "\tcmp #$0a", "\tbne _I1", }, wantElse: []string{ "\tjmp _I2", "_I1", }, wantEndif: []string{ "_I2", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) tt.setupVars(ctx.SymbolTable) ifCmd := &IfCommand{} elseCmd := &ElseCommand{} endifCmd := &EndIfCommand{} pragmaIdx := pragma.GetCurrentPragmaSetIndex() if err := ifCmd.Interpret(preproc.Line{Text: tt.ifLine, Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("IF Interpret() error = %v", err) } ifAsm, err := ifCmd.Generate(ctx) if err != nil { t.Fatalf("IF Generate() error = %v", err) } if err := elseCmd.Interpret(preproc.Line{Text: "ELSE", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("ELSE Interpret() error = %v", err) } elseAsm, err := elseCmd.Generate(ctx) if err != nil { t.Fatalf("ELSE Generate() error = %v", err) } if err := endifCmd.Interpret(preproc.Line{Text: "ENDIF", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("ENDIF Interpret() error = %v", err) } endifAsm, err := endifCmd.Generate(ctx) if err != nil { t.Fatalf("ENDIF Generate() error = %v", err) } if !equalAsm(ifAsm, tt.wantIf) { t.Errorf("IF Generate() mismatch\ngot:\n%s\nwant:\n%s", strings.Join(ifAsm, "\n"), strings.Join(tt.wantIf, "\n")) } if !equalAsm(elseAsm, tt.wantElse) { t.Errorf("ELSE Generate() mismatch\ngot:\n%s\nwant:\n%s", strings.Join(elseAsm, "\n"), strings.Join(tt.wantElse, "\n")) } if !equalAsm(endifAsm, tt.wantEndif) { t.Errorf("ENDIF Generate() mismatch\ngot:\n%s\nwant:\n%s", strings.Join(endifAsm, "\n"), strings.Join(tt.wantEndif, "\n")) } }) } } func TestIfAllOperators(t *testing.T) { tests := []struct { name string line string wantInst string }{ {"equal", "IF x = 10", "bne"}, {"not equal", "IF x <> 10", "beq"}, {"greater", "IF x > 10", "bcs"}, {"less", "IF x < 10", "bcs"}, {"greater equal", "IF x >= 10", "bcc"}, {"less equal", "IF 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 := &IfCommand{} 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 TestIfMixedTypes(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 := &IfCommand{} line := preproc.Line{ Text: "IF 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 TestEndifWithoutIf(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) cmd := &EndIfCommand{} line := preproc.Line{ Text: "ENDIF", Kind: preproc.Source, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), } err := cmd.Interpret(line, ctx) if err == nil { t.Fatal("ENDIF without IF should fail") } if !strings.Contains(err.Error(), "not inside IF") { t.Errorf("Wrong error message: %v", err) } } func TestElseWithoutIf(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) cmd := &ElseCommand{} line := preproc.Line{ Text: "ELSE", Kind: preproc.Source, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), } err := cmd.Interpret(line, ctx) if err == nil { t.Fatal("ELSE without IF should fail") } if !strings.Contains(err.Error(), "not inside IF") { t.Errorf("Wrong error message: %v", err) } } func TestIfNested(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0) ctx.SymbolTable.AddVar("y", "", compiler.KindByte, 0) pragmaIdx := pragma.GetCurrentPragmaSetIndex() if1 := &IfCommand{} if2 := &IfCommand{} endif1 := &EndIfCommand{} endif2 := &EndIfCommand{} if err := if1.Interpret(preproc.Line{Text: "IF x = 10", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("IF 1 error = %v", err) } asm1, err := if1.Generate(ctx) if err != nil { t.Fatalf("IF 1 Generate error = %v", err) } if err := if2.Interpret(preproc.Line{Text: "IF y = 5", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("IF 2 error = %v", err) } asm2, err := if2.Generate(ctx) if err != nil { t.Fatalf("IF 2 Generate error = %v", err) } if err := endif2.Interpret(preproc.Line{Text: "ENDIF", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("ENDIF 2 error = %v", err) } if err := endif1.Interpret(preproc.Line{Text: "ENDIF", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("ENDIF 1 error = %v", err) } // Find labels in asm output label1 := findLabel(asm1) label2 := findLabel(asm2) if label1 == label2 { t.Error("Nested IFs should have different labels") } if label1 == "" || label2 == "" { t.Error("Should generate labels for both IFs") } } func TestIfNestedWithElse(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0) ctx.SymbolTable.AddVar("y", "", compiler.KindByte, 0) pragmaIdx := pragma.GetCurrentPragmaSetIndex() if1 := &IfCommand{} else1 := &ElseCommand{} if2 := &IfCommand{} endif2 := &EndIfCommand{} endif1 := &EndIfCommand{} if err := if1.Interpret(preproc.Line{Text: "IF x = 10", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("IF 1 error = %v", err) } if1.Generate(ctx) if err := else1.Interpret(preproc.Line{Text: "ELSE", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("ELSE 1 error = %v", err) } else1.Generate(ctx) if err := if2.Interpret(preproc.Line{Text: "IF y = 5", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("IF 2 error = %v", err) } if2.Generate(ctx) if err := endif2.Interpret(preproc.Line{Text: "ENDIF", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("ENDIF 2 error = %v", err) } if err := endif1.Interpret(preproc.Line{Text: "ENDIF", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("ENDIF 1 error = %v", err) } // If this doesn't crash, nesting with ELSE works } func TestIfLongJump(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 := &IfCommand{} line := preproc.Line{ Text: "IF 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 TestIfConstant(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 := &IfCommand{} line := preproc.Line{ Text: "IF 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 TestIfWrongParamCount(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) tests := []string{ "IF x", "IF x =", "IF x = 10 extra", } for _, text := range tests { cmd := &IfCommand{} 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) } } } func TestElseWrongParamCount(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0) // Setup IF first ifCmd := &IfCommand{} ifCmd.Interpret(preproc.Line{ Text: "IF x = 10", Kind: preproc.Source, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), }, ctx) cmd := &ElseCommand{} line := preproc.Line{ Text: "ELSE extra", Kind: preproc.Source, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), } err := cmd.Interpret(line, ctx) if err == nil { t.Error("ELSE with extra params should fail") } } func TestEndifWrongParamCount(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0) // Setup IF first ifCmd := &IfCommand{} ifCmd.Interpret(preproc.Line{ Text: "IF x = 10", Kind: preproc.Source, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), }, ctx) cmd := &EndIfCommand{} line := preproc.Line{ Text: "ENDIF extra", Kind: preproc.Source, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), } err := cmd.Interpret(line, ctx) if err == nil { t.Error("ENDIF with extra params should fail") } } func TestIfConstantFolding(t *testing.T) { tests := []struct { name string ifLine string shouldSkip bool }{ {"true condition", "IF 10 = 10", false}, {"false condition", "IF 10 = 5", true}, {"true not equal", "IF 10 <> 5", false}, {"false not equal", "IF 10 <> 10", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) cmd := &IfCommand{} line := preproc.Line{ Text: tt.ifLine, 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) } hasJump := false for _, inst := range asm { if strings.Contains(inst, "jmp") { hasJump = true break } } if tt.shouldSkip && !hasJump { t.Error("False constant should generate JMP to skip block") } if !tt.shouldSkip && len(asm) > 0 && hasJump { t.Error("True constant should not generate JMP") } }) } } // Helper to find label in assembly func findLabel(asm []string) string { for _, line := range asm { if strings.Contains(line, "_I") && !strings.HasPrefix(strings.TrimSpace(line), "\t") { return strings.TrimSpace(line) } if strings.Contains(line, "bne") || strings.Contains(line, "beq") { parts := strings.Fields(line) if len(parts) >= 2 { return parts[1] } } } return "" } /* // 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 } */