c65gm/internal/commands/switch_test.go

918 lines
28 KiB
Go

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
}