918 lines
28 KiB
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
|
|
}
|