package commands import ( "strings" "testing" "c65gm/internal/compiler" "c65gm/internal/preproc" ) func TestSwitchBasicByte(t *testing.T) { tests := []struct { name string setupVars func(*compiler.SymbolTable) caseValue string wantSwitch []string wantCase []string wantEndswitch []string }{ { name: "byte var with byte literal case", setupVars: func(st *compiler.SymbolTable) { st.AddVar("x", "", compiler.KindByte, 0) }, caseValue: "10", wantSwitch: []string{}, wantCase: []string{ "\tlda x", "\tcmp #$0a", "\tbne ", }, wantEndswitch: []string{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) tt.setupVars(ctx.SymbolTable) switchCmd := &SwitchCommand{} caseCmd := &CaseCommand{} endswitchCmd := &EndSwitchCommand{} pragmaIdx := pragma.GetCurrentPragmaSetIndex() if err := switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("SWITCH Interpret() error = %v", err) } switchAsm, err := switchCmd.Generate(ctx) if err != nil { t.Fatalf("SWITCH Generate() error = %v", err) } if err := caseCmd.Interpret(preproc.Line{Text: "CASE " + tt.caseValue, Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("CASE Interpret() error = %v", err) } caseAsm, err := caseCmd.Generate(ctx) if err != nil { t.Fatalf("CASE Generate() error = %v", err) } if err := endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("ENDSWITCH Interpret() error = %v", err) } endswitchAsm, err := endswitchCmd.Generate(ctx) if err != nil { t.Fatalf("ENDSWITCH Generate() error = %v", err) } if !equalAsmSwitch(switchAsm, tt.wantSwitch) { t.Errorf("SWITCH Generate() mismatch\ngot:\n%s\nwant:\n%s", strings.Join(switchAsm, "\n"), strings.Join(tt.wantSwitch, "\n")) } // For CASE, check that expected instructions are present if !containsInstructions(caseAsm, tt.wantCase) { t.Errorf("CASE Generate() missing expected instructions\ngot:\n%s\nwant to contain:\n%s", strings.Join(caseAsm, "\n"), strings.Join(tt.wantCase, "\n")) } // ENDSWITCH should emit at least one label if len(endswitchAsm) == 0 { t.Error("ENDSWITCH should generate at least end label") } }) } } func TestSwitchMultipleCases(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0) pragmaIdx := pragma.GetCurrentPragmaSetIndex() switchCmd := &SwitchCommand{} case1Cmd := &CaseCommand{} case2Cmd := &CaseCommand{} case3Cmd := &CaseCommand{} endswitchCmd := &EndSwitchCommand{} if err := switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("SWITCH error = %v", err) } switchCmd.Generate(ctx) if err := case1Cmd.Interpret(preproc.Line{Text: "CASE 1", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("CASE 1 error = %v", err) } case1Asm, _ := case1Cmd.Generate(ctx) if err := case2Cmd.Interpret(preproc.Line{Text: "CASE 2", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("CASE 2 error = %v", err) } case2Asm, _ := case2Cmd.Generate(ctx) if err := case3Cmd.Interpret(preproc.Line{Text: "CASE 3", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("CASE 3 error = %v", err) } _, _ = case3Cmd.Generate(ctx) if err := endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("ENDSWITCH error = %v", err) } endswitchAsm, _ := endswitchCmd.Generate(ctx) // First CASE should not have JMP at the beginning if len(case1Asm) > 0 && strings.Contains(case1Asm[0], "jmp") { t.Error("First CASE should not start with JMP") } // Second CASE should have implicit break (JMP) from previous case foundJmp := false for _, line := range case2Asm { if strings.Contains(line, "jmp") { foundJmp = true break } } if !foundJmp { t.Error("Second CASE should have implicit break JMP from previous case") } // ENDSWITCH should have end label if len(endswitchAsm) == 0 { t.Error("ENDSWITCH should have end label") } } func TestSwitchWithDefault(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0) pragmaIdx := pragma.GetCurrentPragmaSetIndex() switchCmd := &SwitchCommand{} case1Cmd := &CaseCommand{} defaultCmd := &DefaultCommand{} endswitchCmd := &EndSwitchCommand{} if err := switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("SWITCH error = %v", err) } switchCmd.Generate(ctx) if err := case1Cmd.Interpret(preproc.Line{Text: "CASE 1", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("CASE error = %v", err) } case1Cmd.Generate(ctx) if err := defaultCmd.Interpret(preproc.Line{Text: "DEFAULT", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("DEFAULT error = %v", err) } defaultAsm, err := defaultCmd.Generate(ctx) if err != nil { t.Fatalf("DEFAULT Generate() error = %v", err) } if err := endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("ENDSWITCH error = %v", err) } endswitchCmd.Generate(ctx) // DEFAULT should emit implicit break and skip label if len(defaultAsm) == 0 { t.Error("DEFAULT should emit code for implicit break") } } func TestSwitchCaseAfterDefault(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0) pragmaIdx := pragma.GetCurrentPragmaSetIndex() switchCmd := &SwitchCommand{} case1Cmd := &CaseCommand{} defaultCmd := &DefaultCommand{} case2Cmd := &CaseCommand{} switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) switchCmd.Generate(ctx) case1Cmd.Interpret(preproc.Line{Text: "CASE 1", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) case1Cmd.Generate(ctx) defaultCmd.Interpret(preproc.Line{Text: "DEFAULT", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) defaultCmd.Generate(ctx) // Try to add CASE after DEFAULT - should fail if err := case2Cmd.Interpret(preproc.Line{Text: "CASE 2", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { // This is expected to fail during Interpret return } _, err := case2Cmd.Generate(ctx) if err == nil { t.Fatal("CASE after DEFAULT should fail") } if !strings.Contains(err.Error(), "after DEFAULT") { t.Errorf("Wrong error message: %v", err) } } func TestSwitchMultipleDefaults(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0) pragmaIdx := pragma.GetCurrentPragmaSetIndex() switchCmd := &SwitchCommand{} case1Cmd := &CaseCommand{} default1Cmd := &DefaultCommand{} default2Cmd := &DefaultCommand{} switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) switchCmd.Generate(ctx) case1Cmd.Interpret(preproc.Line{Text: "CASE 1", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) case1Cmd.Generate(ctx) default1Cmd.Interpret(preproc.Line{Text: "DEFAULT", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) default1Cmd.Generate(ctx) // Try to add second DEFAULT - should fail err := default2Cmd.Interpret(preproc.Line{Text: "DEFAULT", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) if err == nil { t.Fatal("Multiple DEFAULT statements should fail") } if !strings.Contains(err.Error(), "multiple DEFAULT") { t.Errorf("Wrong error message: %v", err) } } func TestSwitchWithoutSwitch(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) pragmaIdx := pragma.GetCurrentPragmaSetIndex() caseCmd := &CaseCommand{} err := caseCmd.Interpret(preproc.Line{Text: "CASE 1", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) if err == nil { t.Fatal("CASE without SWITCH should fail") } if !strings.Contains(err.Error(), "not inside") { t.Errorf("Wrong error message: %v", err) } } func TestDefaultWithoutSwitch(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) pragmaIdx := pragma.GetCurrentPragmaSetIndex() defaultCmd := &DefaultCommand{} err := defaultCmd.Interpret(preproc.Line{Text: "DEFAULT", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) if err == nil { t.Fatal("DEFAULT without SWITCH should fail") } if !strings.Contains(err.Error(), "not inside") { t.Errorf("Wrong error message: %v", err) } } func TestEndswitchWithoutSwitch(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) pragmaIdx := pragma.GetCurrentPragmaSetIndex() endswitchCmd := &EndSwitchCommand{} err := endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) if err == nil { t.Fatal("ENDSWITCH without SWITCH should fail") } if !strings.Contains(err.Error(), "not inside") { t.Errorf("Wrong error message: %v", err) } } func TestSwitchWrongParamCount(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) pragmaIdx := pragma.GetCurrentPragmaSetIndex() tests := []string{ "SWITCH", "SWITCH x y", } for _, text := range tests { cmd := &SwitchCommand{} err := cmd.Interpret(preproc.Line{Text: text, Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) if err == nil { t.Errorf("Should fail with wrong param count: %s", text) } } } func TestCaseWrongParamCount(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0) pragmaIdx := pragma.GetCurrentPragmaSetIndex() switchCmd := &SwitchCommand{} switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) switchCmd.Generate(ctx) tests := []string{ "CASE", "CASE 1 2", } for _, text := range tests { cmd := &CaseCommand{} err := cmd.Interpret(preproc.Line{Text: text, Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) if err == nil { t.Errorf("Should fail with wrong param count: %s", text) } } } func TestDefaultWrongParamCount(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0) pragmaIdx := pragma.GetCurrentPragmaSetIndex() switchCmd := &SwitchCommand{} switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) switchCmd.Generate(ctx) cmd := &DefaultCommand{} err := cmd.Interpret(preproc.Line{Text: "DEFAULT extra", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) if err == nil { t.Error("DEFAULT with extra params should fail") } } func TestEndswitchWrongParamCount(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0) pragmaIdx := pragma.GetCurrentPragmaSetIndex() switchCmd := &SwitchCommand{} switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) switchCmd.Generate(ctx) cmd := &EndSwitchCommand{} err := cmd.Interpret(preproc.Line{Text: "ENDSWITCH extra", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) if err == nil { t.Error("ENDSWITCH with extra params should fail") } } func TestSwitchWordType(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("big_val", "", compiler.KindWord, 0) pragmaIdx := pragma.GetCurrentPragmaSetIndex() switchCmd := &SwitchCommand{} caseCmd := &CaseCommand{} endswitchCmd := &EndSwitchCommand{} if err := switchCmd.Interpret(preproc.Line{Text: "SWITCH big_val", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("SWITCH error = %v", err) } switchCmd.Generate(ctx) if err := caseCmd.Interpret(preproc.Line{Text: "CASE 1000", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("CASE error = %v", err) } caseAsm, err := caseCmd.Generate(ctx) if err != nil { t.Fatalf("CASE Generate() error = %v", err) } endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) endswitchCmd.Generate(ctx) // Should have high byte check for word foundHighByteCheck := false for _, inst := range caseAsm { if strings.Contains(inst, "big_val+1") { foundHighByteCheck = true break } } if !foundHighByteCheck { t.Error("Expected high byte check for word comparison") } } func TestSwitchWithConstant(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddConst("MAX_VAL", "", compiler.KindByte, 100) ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0) pragmaIdx := pragma.GetCurrentPragmaSetIndex() switchCmd := &SwitchCommand{} caseCmd := &CaseCommand{} endswitchCmd := &EndSwitchCommand{} if err := switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("SWITCH error = %v", err) } switchCmd.Generate(ctx) if err := caseCmd.Interpret(preproc.Line{Text: "CASE MAX_VAL", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("CASE error = %v", err) } caseAsm, err := caseCmd.Generate(ctx) if err != nil { t.Fatalf("CASE Generate() error = %v", err) } endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) endswitchCmd.Generate(ctx) // Constant should be folded to immediate value found := false for _, inst := range caseAsm { if strings.Contains(inst, "#$64") { // 100 = 0x64 found = true break } } if !found { t.Error("Constant should be folded to immediate value") } } func TestSwitchNested(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("outer", "", compiler.KindByte, 0) ctx.SymbolTable.AddVar("inner", "", compiler.KindByte, 0) pragmaIdx := pragma.GetCurrentPragmaSetIndex() switch1Cmd := &SwitchCommand{} case1Cmd := &CaseCommand{} switch2Cmd := &SwitchCommand{} case2Cmd := &CaseCommand{} endswitch2Cmd := &EndSwitchCommand{} endswitch1Cmd := &EndSwitchCommand{} if err := switch1Cmd.Interpret(preproc.Line{Text: "SWITCH outer", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("SWITCH 1 error = %v", err) } switch1Asm, _ := switch1Cmd.Generate(ctx) if err := case1Cmd.Interpret(preproc.Line{Text: "CASE 1", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("CASE 1 error = %v", err) } case1Cmd.Generate(ctx) if err := switch2Cmd.Interpret(preproc.Line{Text: "SWITCH inner", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("SWITCH 2 error = %v", err) } switch2Asm, _ := switch2Cmd.Generate(ctx) if err := case2Cmd.Interpret(preproc.Line{Text: "CASE 2", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("CASE 2 error = %v", err) } case2Cmd.Generate(ctx) if err := endswitch2Cmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("ENDSWITCH 2 error = %v", err) } endswitch2Asm, _ := endswitch2Cmd.Generate(ctx) if err := endswitch1Cmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("ENDSWITCH 1 error = %v", err) } endswitch1Asm, _ := endswitch1Cmd.Generate(ctx) // Both switches should generate assembly if len(switch1Asm) < 0 || len(switch2Asm) < 0 { // SWITCHes don't generate asm, just setup state } // Both ENDSWITCHes should generate labels if len(endswitch1Asm) == 0 || len(endswitch2Asm) == 0 { t.Error("Nested switches should both generate end labels") } // Labels should be different label1 := "" label2 := "" if len(endswitch1Asm) > 0 { label1 = endswitch1Asm[len(endswitch1Asm)-1] } if len(endswitch2Asm) > 0 { label2 = endswitch2Asm[len(endswitch2Asm)-1] } if label1 == label2 && label1 != "" { t.Error("Nested switches should have different end labels") } } func TestSwitchLongJump(t *testing.T) { pragma := preproc.NewPragma() pragma.AddPragma("_P_USE_LONG_JUMP", "1") ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0) pragmaIdx := pragma.GetCurrentPragmaSetIndex() switchCmd := &SwitchCommand{} case1Cmd := &CaseCommand{} endswitchCmd := &EndSwitchCommand{} if err := switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("SWITCH error = %v", err) } switchCmd.Generate(ctx) if err := case1Cmd.Interpret(preproc.Line{Text: "CASE 1", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("CASE 1 error = %v", err) } case1Asm, _ := case1Cmd.Generate(ctx) endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) endswitchCmd.Generate(ctx) // In long jump mode, comparison should have pattern: short branch + jmp + label // Look for both a short branch (beq/bne) and a jmp to _SKIPCASE foundShortBranch := false foundJmpToSkip := false foundLabel := false for _, inst := range case1Asm { if strings.Contains(inst, "beq ") || strings.Contains(inst, "bne ") { foundShortBranch = true } if strings.Contains(inst, "jmp ") && strings.Contains(inst, "_SKIPCASE") { foundJmpToSkip = true } // Labels don't start with tab trimmed := strings.TrimSpace(inst) if !strings.HasPrefix(inst, "\t") && strings.Contains(inst, "_") && len(trimmed) > 0 { foundLabel = true } } if !foundShortBranch { t.Error("Long jump mode should have short branch (beq/bne)") } if !foundJmpToSkip { t.Error("Long jump mode should have JMP to skip label in comparison") } if !foundLabel { t.Error("Long jump mode should have success label in comparison") } // Verify the pattern is different from normal mode pragmaNormal := preproc.NewPragma() ctxNormal := compiler.NewCompilerContext(pragmaNormal) ctxNormal.SymbolTable.AddVar("x", "", compiler.KindByte, 0) pragmaIdxNormal := pragmaNormal.GetCurrentPragmaSetIndex() switchCmdNormal := &SwitchCommand{} caseCmdNormal := &CaseCommand{} switchCmdNormal.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdxNormal}, ctxNormal) switchCmdNormal.Generate(ctxNormal) caseCmdNormal.Interpret(preproc.Line{Text: "CASE 1", Kind: preproc.Source, PragmaSetIndex: pragmaIdxNormal}, ctxNormal) normalAsm, _ := caseCmdNormal.Generate(ctxNormal) // Normal mode should NOT have jmp in comparison (only short branch) normalHasJmp := false for _, inst := range normalAsm { if strings.Contains(inst, "\tjmp") { normalHasJmp = true break } } if normalHasJmp { t.Error("Normal mode should not have JMP in comparison code") } } func TestSwitchOnConstant(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddConst("VALUE", "", compiler.KindByte, 5) pragmaIdx := pragma.GetCurrentPragmaSetIndex() switchCmd := &SwitchCommand{} case1Cmd := &CaseCommand{} case2Cmd := &CaseCommand{} endswitchCmd := &EndSwitchCommand{} if err := switchCmd.Interpret(preproc.Line{Text: "SWITCH VALUE", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("SWITCH on constant error = %v", err) } switchCmd.Generate(ctx) // CASE with matching constant - should be optimized away (constant folding) if err := case1Cmd.Interpret(preproc.Line{Text: "CASE 5", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("CASE 5 error = %v", err) } case1Asm, err := case1Cmd.Generate(ctx) if err != nil { t.Fatalf("CASE 5 Generate() error = %v", err) } // With constant folding, matching constant case generates no comparison code (optimization) if len(case1Asm) != 0 { t.Errorf("CASE with matching constant should be optimized away, got: %v", case1Asm) } // CASE with non-matching constant - should generate JMP to skip if err := case2Cmd.Interpret(preproc.Line{Text: "CASE 10", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("CASE 10 error = %v", err) } case2Asm, err := case2Cmd.Generate(ctx) if err != nil { t.Fatalf("CASE 10 Generate() error = %v", err) } // Non-matching constant should generate JMP to skip this case if len(case2Asm) == 0 { t.Error("CASE with non-matching constant should generate skip code") } foundJmp := false for _, line := range case2Asm { if strings.Contains(line, "jmp") { foundJmp = true break } } if !foundJmp { t.Error("Non-matching constant case should have JMP to skip") } endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) endswitchCmd.Generate(ctx) } func TestSwitchEmptyWithOnlyDefault(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0) pragmaIdx := pragma.GetCurrentPragmaSetIndex() switchCmd := &SwitchCommand{} defaultCmd := &DefaultCommand{} endswitchCmd := &EndSwitchCommand{} if err := switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("SWITCH error = %v", err) } switchCmd.Generate(ctx) if err := defaultCmd.Interpret(preproc.Line{Text: "DEFAULT", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("DEFAULT error = %v", err) } defaultAsm, err := defaultCmd.Generate(ctx) if err != nil { t.Fatalf("DEFAULT Generate() error = %v", err) } if err := endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("ENDSWITCH error = %v", err) } endswitchAsm, err := endswitchCmd.Generate(ctx) if err != nil { t.Fatalf("ENDSWITCH Generate() error = %v", err) } // DEFAULT without previous CASE should not emit JMP hasJmp := false for _, line := range defaultAsm { if strings.Contains(line, "jmp") { hasJmp = true break } } if hasJmp { t.Error("DEFAULT without previous CASE should not emit JMP") } // ENDSWITCH should still emit end label if len(endswitchAsm) == 0 { t.Error("ENDSWITCH should emit end label") } } func TestSwitchComparisonTypes(t *testing.T) { tests := []struct { name string varType compiler.VarKind caseValue string shouldWork bool }{ {"byte var byte case", compiler.KindByte, "10", true}, {"word var byte case", compiler.KindWord, "10", true}, {"byte var word case", compiler.KindByte, "1000", false}, {"word var word case", compiler.KindWord, "1000", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("x", "", tt.varType, 0) pragmaIdx := pragma.GetCurrentPragmaSetIndex() switchCmd := &SwitchCommand{} caseCmd := &CaseCommand{} endswitchCmd := &EndSwitchCommand{} switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) switchCmd.Generate(ctx) err := caseCmd.Interpret(preproc.Line{Text: "CASE " + tt.caseValue, Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) if tt.shouldWork && err != nil { t.Fatalf("CASE Interpret() unexpected error = %v", err) } if !tt.shouldWork && err == nil { t.Fatalf("CASE Interpret() should have failed but didn't") } if tt.shouldWork { _, err := caseCmd.Generate(ctx) if err != nil { t.Fatalf("CASE Generate() error = %v", err) } } endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) endswitchCmd.Generate(ctx) }) } } func TestSwitchWithVariableCase(t *testing.T) { tests := []struct { name string switchType compiler.VarKind caseType compiler.VarKind shouldWork bool }{ {"byte switch byte case", compiler.KindByte, compiler.KindByte, true}, {"byte switch word case", compiler.KindByte, compiler.KindWord, true}, {"word switch byte case", compiler.KindWord, compiler.KindByte, true}, {"word switch word case", compiler.KindWord, compiler.KindWord, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { pragma := preproc.NewPragma() ctx := compiler.NewCompilerContext(pragma) ctx.SymbolTable.AddVar("switch_var", "", tt.switchType, 0) ctx.SymbolTable.AddVar("case_var", "", tt.caseType, 0) pragmaIdx := pragma.GetCurrentPragmaSetIndex() switchCmd := &SwitchCommand{} caseCmd := &CaseCommand{} endswitchCmd := &EndSwitchCommand{} if err := switchCmd.Interpret(preproc.Line{Text: "SWITCH switch_var", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil { t.Fatalf("SWITCH error = %v", err) } switchCmd.Generate(ctx) err := caseCmd.Interpret(preproc.Line{Text: "CASE case_var", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) if tt.shouldWork && err != nil { t.Fatalf("CASE with variable unexpected error = %v", err) } if !tt.shouldWork && err == nil { t.Fatal("CASE with variable should have failed but didn't") } if tt.shouldWork { caseAsm, err := caseCmd.Generate(ctx) if err != nil { t.Fatalf("CASE Generate() error = %v", err) } // Verify assembly was generated for comparison if len(caseAsm) == 0 { t.Error("CASE with variable should generate comparison code") } } endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) endswitchCmd.Generate(ctx) }) } } func TestSwitchByteRangeValidation(t *testing.T) { tests := []struct { name string caseValue string expectError bool errorContains string }{ {"valid byte 0", "0", false, ""}, {"valid byte 255", "255", false, ""}, {"valid byte 100", "100", false, ""}, {"out of range 256", "256", true, "will never match BYTE variable"}, {"out of range 1000", "1000", true, "will never match BYTE variable"}, {"out of range 10000", "10000", true, "will never match BYTE variable"}, } 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) pragmaIdx := pragma.GetCurrentPragmaSetIndex() switchCmd := &SwitchCommand{} caseCmd := &CaseCommand{} switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) switchCmd.Generate(ctx) err := caseCmd.Interpret(preproc.Line{Text: "CASE " + tt.caseValue, Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx) if tt.expectError { if err == nil { t.Fatalf("Expected error for CASE %s but got none", tt.caseValue) } if !strings.Contains(err.Error(), tt.errorContains) { t.Errorf("Error message '%s' should contain '%s'", err.Error(), tt.errorContains) } } else { if err != nil { t.Fatalf("Unexpected error for CASE %s: %v", tt.caseValue, err) } } }) } } // Helper to compare assembly output func equalAsmSwitch(got, want []string) bool { if len(got) != len(want) { return false } for i := range got { if got[i] != want[i] { return false } } return true } // Helper to check if assembly contains expected instructions func containsInstructions(asm []string, expected []string) bool { for _, exp := range expected { found := false for _, line := range asm { if strings.Contains(line, strings.TrimSpace(exp)) { found = true break } } if !found { return false } } return true }