c65gm/internal/commands/if_test.go

631 lines
15 KiB
Go

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
}
*/