c65gm/internal/commands/for_test.go

595 lines
16 KiB
Go

package commands
import (
"strings"
"testing"
"c65gm/internal/compiler"
"c65gm/internal/preproc"
)
func TestForBasicTO(t *testing.T) {
tests := []struct {
name string
forLine string
setupVars func(*compiler.SymbolTable)
wantFor []string
wantNext []string
}{
{
name: "byte var TO byte literal",
forLine: "FOR i = 0 TO 10",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("i", "", compiler.KindByte, 0)
},
// Do-while style: initial guard (constant folded - 0<=10 is true, no code)
// then loop label
wantFor: []string{
"\tlda #$00",
"\tsta i",
"_LOOPSTART1",
},
// End check before increment
wantNext: []string{
"\tlda i",
"\tcmp #$0a",
"\tbeq _LOOPEND1",
"\tinc i",
"\tjmp _LOOPSTART1",
"_LOOPEND1",
},
},
{
name: "word var TO word literal",
forLine: "FOR counter = 0 TO 1000",
setupVars: func(st *compiler.SymbolTable) {
st.AddVar("counter", "", compiler.KindWord, 0)
},
// Do-while style: initial guard (constant folded - 0<=1000 is true, no code)
wantFor: []string{
"\tlda #$00",
"\tsta counter",
"\tsta counter+1",
"_LOOPSTART1",
},
// End check before increment (WORD comparison)
wantNext: []string{
"\tlda counter",
"\tcmp #$e8",
"\tbne +",
"\tlda counter+1",
"\tcmp #$03",
"\tbeq _LOOPEND1",
"+",
"\tinc counter",
"\tbne _L1",
"\tinc counter+1",
"_L1",
"\tjmp _LOOPSTART1",
"_LOOPEND1",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
tt.setupVars(ctx.SymbolTable)
forCmd := &ForCommand{}
nextCmd := &NextCommand{}
forLine := preproc.Line{
Text: tt.forLine,
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
nextLine := preproc.Line{
Text: "NEXT",
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
if err := forCmd.Interpret(forLine, ctx); err != nil {
t.Fatalf("FOR Interpret() error = %v", err)
}
forAsm, err := forCmd.Generate(ctx)
if err != nil {
t.Fatalf("FOR Generate() error = %v", err)
}
if err := nextCmd.Interpret(nextLine, ctx); err != nil {
t.Fatalf("NEXT Interpret() error = %v", err)
}
nextAsm, err := nextCmd.Generate(ctx)
if err != nil {
t.Fatalf("NEXT Generate() error = %v", err)
}
if !equalAsm(forAsm, tt.wantFor) {
t.Errorf("FOR Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(forAsm, "\n"),
strings.Join(tt.wantFor, "\n"))
}
if !equalAsm(nextAsm, tt.wantNext) {
t.Errorf("NEXT Generate() mismatch\ngot:\n%s\nwant:\n%s",
strings.Join(nextAsm, "\n"),
strings.Join(tt.wantNext, "\n"))
}
})
}
}
func TestForBreak(t *testing.T) {
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
ctx.SymbolTable.AddVar("i", "", compiler.KindByte, 0)
forCmd := &ForCommand{}
breakCmd := &BreakCommand{}
nextCmd := &NextCommand{}
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
forLine := preproc.Line{Text: "FOR i = 0 TO 10", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}
breakLine := preproc.Line{Text: "BREAK", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}
nextLine := preproc.Line{Text: "NEXT", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}
if err := forCmd.Interpret(forLine, ctx); err != nil {
t.Fatalf("FOR Interpret() error = %v", err)
}
forAsm, _ := forCmd.Generate(ctx)
_ = forAsm // body would go here
if err := breakCmd.Interpret(breakLine, ctx); err != nil {
t.Fatalf("BREAK Interpret() error = %v", err)
}
breakAsm, err := breakCmd.Generate(ctx)
if err != nil {
t.Fatalf("BREAK Generate() error = %v", err)
}
if err := nextCmd.Interpret(nextLine, ctx); err != nil {
t.Fatalf("NEXT Interpret() error = %v", err)
}
if len(breakAsm) != 1 || !strings.Contains(breakAsm[0], "jmp _LOOPEND") {
t.Errorf("BREAK should jump to loop end label, got: %v", breakAsm)
}
}
func TestForNested(t *testing.T) {
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
ctx.SymbolTable.AddVar("i", "", compiler.KindByte, 0)
ctx.SymbolTable.AddVar("j", "", compiler.KindByte, 0)
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
for1 := &ForCommand{}
for2 := &ForCommand{}
next1 := &NextCommand{}
next2 := &NextCommand{}
if err := for1.Interpret(preproc.Line{Text: "FOR i = 0 TO 10", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
t.Fatalf("FOR 1 error = %v", err)
}
asm1, err := for1.Generate(ctx)
if err != nil {
t.Fatalf("FOR 1 Generate error = %v", err)
}
if err := for2.Interpret(preproc.Line{Text: "FOR j = 0 TO 5", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
t.Fatalf("FOR 2 error = %v", err)
}
asm2, err := for2.Generate(ctx)
if err != nil {
t.Fatalf("FOR 2 Generate error = %v", err)
}
if err := next2.Interpret(preproc.Line{Text: "NEXT", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
t.Fatalf("NEXT 2 error = %v", err)
}
if err := next1.Interpret(preproc.Line{Text: "NEXT", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
t.Fatalf("NEXT 1 error = %v", err)
}
// Find loop start labels in the generated assembly
loopLabel1 := ""
loopLabel2 := ""
for _, line := range asm1 {
if strings.HasPrefix(line, "_LOOPSTART") {
loopLabel1 = line
break
}
}
for _, line := range asm2 {
if strings.HasPrefix(line, "_LOOPSTART") {
loopLabel2 = line
break
}
}
if loopLabel1 == "" || loopLabel2 == "" {
t.Fatal("Could not find loop labels")
}
if loopLabel1 == loopLabel2 {
t.Error("Nested loops should have different labels")
}
}
func TestForMixedWithWhile(t *testing.T) {
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
ctx.SymbolTable.AddVar("i", "", compiler.KindByte, 0)
ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0)
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
forCmd := &ForCommand{}
whileCmd := &WhileCommand{}
wendCmd := &WendCommand{}
nextCmd := &NextCommand{}
// FOR i = 0 TO 10
// WHILE x < 5
// WEND
// NEXT
if err := forCmd.Interpret(preproc.Line{Text: "FOR i = 0 TO 10", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
t.Fatalf("FOR error = %v", err)
}
_, _ = forCmd.Generate(ctx)
if err := whileCmd.Interpret(preproc.Line{Text: "WHILE x < 5", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
t.Fatalf("WHILE error = %v", err)
}
_, _ = whileCmd.Generate(ctx)
if err := wendCmd.Interpret(preproc.Line{Text: "WEND", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
t.Fatalf("WEND error = %v", err)
}
if err := nextCmd.Interpret(preproc.Line{Text: "NEXT", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
t.Fatalf("NEXT error = %v", err)
}
}
func TestForIllegalNesting(t *testing.T) {
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
ctx.SymbolTable.AddVar("i", "", compiler.KindByte, 0)
ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0)
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
forCmd := &ForCommand{}
whileCmd := &WhileCommand{}
nextCmd := &NextCommand{}
// FOR i = 0 TO 10
// WHILE x < 5
// NEXT <- ERROR: crossing loop boundaries
// WEND
if err := forCmd.Interpret(preproc.Line{Text: "FOR i = 0 TO 10", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
t.Fatalf("FOR error = %v", err)
}
_, _ = forCmd.Generate(ctx)
if err := whileCmd.Interpret(preproc.Line{Text: "WHILE x < 5", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
t.Fatalf("WHILE error = %v", err)
}
_, _ = whileCmd.Generate(ctx)
// NEXT should fail because of stack mismatch
err := nextCmd.Interpret(preproc.Line{Text: "NEXT", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
if err == nil {
t.Fatal("NEXT should fail when crossing loop boundaries")
}
if !strings.Contains(err.Error(), "mismatch") {
t.Errorf("Wrong error message: %v", err)
}
}
func TestNextWithoutFor(t *testing.T) {
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
cmd := &NextCommand{}
line := preproc.Line{
Text: "NEXT",
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
err := cmd.Interpret(line, ctx)
if err == nil {
t.Fatal("NEXT outside FOR loop should fail")
}
if !strings.Contains(err.Error(), "not inside FOR") {
t.Errorf("Wrong error message: %v", err)
}
}
func TestForWrongParamCount(t *testing.T) {
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
ctx.SymbolTable.AddVar("i", "", compiler.KindByte, 0)
tests := []string{
"FOR i",
"FOR i = 0",
"FOR i = 0 TO",
"FOR i = 0 TO 10 STEP 2", // STEP not supported
"FOR i = 0 TO 10 EXTRA",
}
for _, text := range tests {
cmd := &ForCommand{}
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 TestForInvalidDirection(t *testing.T) {
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
ctx.SymbolTable.AddVar("i", "", compiler.KindByte, 0)
cmd := &ForCommand{}
line := preproc.Line{
Text: "FOR i = 0 UPTO 10",
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
err := cmd.Interpret(line, ctx)
if err == nil {
t.Fatal("Should fail with invalid direction keyword")
}
if !strings.Contains(err.Error(), "TO") {
t.Errorf("Wrong error message: %v", err)
}
}
func TestForDOWNTORejected(t *testing.T) {
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
ctx.SymbolTable.AddVar("i", "", compiler.KindByte, 0)
cmd := &ForCommand{}
line := preproc.Line{
Text: "FOR i = 10 DOWNTO 0",
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
err := cmd.Interpret(line, ctx)
if err == nil {
t.Fatal("Should fail with DOWNTO")
}
if !strings.Contains(err.Error(), "not supported") {
t.Errorf("Wrong error message: %v", err)
}
}
func TestForConstVariable(t *testing.T) {
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
ctx.SymbolTable.AddConst("LIMIT", "", compiler.KindByte, 10)
cmd := &ForCommand{}
line := preproc.Line{
Text: "FOR LIMIT = 0 TO 10",
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
err := cmd.Interpret(line, ctx)
if err == nil {
t.Fatal("Should fail when using constant as loop variable")
}
if !strings.Contains(err.Error(), "constant") {
t.Errorf("Wrong error message: %v", err)
}
}
func TestForUnknownVariable(t *testing.T) {
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
cmd := &ForCommand{}
line := preproc.Line{
Text: "FOR unknown = 0 TO 10",
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
err := cmd.Interpret(line, ctx)
if err == nil {
t.Fatal("Should fail with unknown variable")
}
if !strings.Contains(err.Error(), "unknown") {
t.Errorf("Wrong error message: %v", err)
}
}
func TestForConstantEnd(t *testing.T) {
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
ctx.SymbolTable.AddVar("i", "", compiler.KindByte, 0)
ctx.SymbolTable.AddConst("MAX", "", compiler.KindByte, 100)
forCmd := &ForCommand{}
nextCmd := &NextCommand{}
forLine := preproc.Line{
Text: "FOR i = 0 TO MAX",
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
nextLine := preproc.Line{
Text: "NEXT",
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
if err := forCmd.Interpret(forLine, ctx); err != nil {
t.Fatalf("FOR Interpret() error = %v", err)
}
if _, err := forCmd.Generate(ctx); err != nil {
t.Fatalf("FOR Generate() error = %v", err)
}
if err := nextCmd.Interpret(nextLine, ctx); err != nil {
t.Fatalf("NEXT Interpret() error = %v", err)
}
// With do-while pattern, the constant end value appears in NEXT's end check
nextAsm, err := nextCmd.Generate(ctx)
if err != nil {
t.Fatalf("NEXT Generate() error = %v", err)
}
found := false
for _, inst := range nextAsm {
if strings.Contains(inst, "#$64") { // 100 in hex
found = true
break
}
}
if !found {
t.Error("Constant should be folded to immediate value")
}
}
func TestForByteMaxEndValue(t *testing.T) {
// FOR b = 0 TO 255 with BYTE iterator uses do-while pattern:
// Before incrementing, check if b == end and exit if so.
// This naturally handles the max value case (255) without overflow.
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 0)
forCmd := &ForCommand{}
nextCmd := &NextCommand{}
forLine := preproc.Line{
Text: "FOR b = 0 TO 255",
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
nextLine := preproc.Line{
Text: "NEXT",
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
if err := forCmd.Interpret(forLine, ctx); err != nil {
t.Fatalf("FOR Interpret() error = %v", err)
}
if _, err := forCmd.Generate(ctx); err != nil {
t.Fatalf("FOR Generate() error = %v", err)
}
if err := nextCmd.Interpret(nextLine, ctx); err != nil {
t.Fatalf("NEXT Interpret() error = %v", err)
}
nextAsm, err := nextCmd.Generate(ctx)
if err != nil {
t.Fatalf("NEXT Generate() error = %v", err)
}
// NEXT should check if b == 255 before incrementing
// Look for: lda b / cmp #$ff / beq _LOOPEND
hasLda := false
hasCmp255 := false
hasBeq := false
for _, line := range nextAsm {
if strings.Contains(line, "lda b") {
hasLda = true
}
if strings.Contains(line, "cmp #$ff") {
hasCmp255 = true
}
if strings.Contains(line, "beq _LOOPEND") {
hasBeq = true
}
}
if !hasLda || !hasCmp255 || !hasBeq {
t.Errorf("NEXT should generate overflow check for BYTE TO 255\ngot:\n%s",
strings.Join(nextAsm, "\n"))
}
}
func TestForWordMaxEndValue(t *testing.T) {
// FOR w = 0 TO 65535 with WORD iterator uses do-while pattern.
// Naturally handles the max value case (65535) without overflow.
pragma := preproc.NewPragma()
ctx := compiler.NewCompilerContext(pragma)
ctx.SymbolTable.AddVar("w", "", compiler.KindWord, 0)
forCmd := &ForCommand{}
nextCmd := &NextCommand{}
forLine := preproc.Line{
Text: "FOR w = 0 TO 65535",
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
nextLine := preproc.Line{
Text: "NEXT",
Kind: preproc.Source,
PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(),
}
if err := forCmd.Interpret(forLine, ctx); err != nil {
t.Fatalf("FOR Interpret() error = %v", err)
}
if _, err := forCmd.Generate(ctx); err != nil {
t.Fatalf("FOR Generate() error = %v", err)
}
if err := nextCmd.Interpret(nextLine, ctx); err != nil {
t.Fatalf("NEXT Interpret() error = %v", err)
}
nextAsm, err := nextCmd.Generate(ctx)
if err != nil {
t.Fatalf("NEXT Generate() error = %v", err)
}
// NEXT should check if w == 65535 ($FFFF) before incrementing
// Look for comparisons with $ff for both bytes
hasCmpFF := false
hasBeq := false
for _, line := range nextAsm {
if strings.Contains(line, "cmp #$ff") {
hasCmpFF = true
}
if strings.Contains(line, "beq _LOOPEND") {
hasBeq = true
}
}
if !hasCmpFF || !hasBeq {
t.Errorf("NEXT should generate overflow check for WORD TO 65535\ngot:\n%s",
strings.Join(nextAsm, "\n"))
}
}