From 37296cf627c51f6ed7716baed0fbe9e361c11ccd Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Mon, 17 Nov 2025 22:19:20 +0100 Subject: [PATCH 1/5] First non-working version of FOR/NEXT loops. (tests failing) --- internal/commands/break.go | 2 +- internal/commands/for.go | 280 +++++++++++++ internal/commands/for_test.go | 727 ++++++++++++++++++++++++++++++++++ internal/commands/next.go | 222 +++++++++++ internal/compiler/context.go | 2 + internal/compiler/forstack.go | 67 ++++ 6 files changed, 1299 insertions(+), 1 deletion(-) create mode 100644 internal/commands/for.go create mode 100644 internal/commands/for_test.go create mode 100644 internal/commands/next.go create mode 100644 internal/compiler/forstack.go diff --git a/internal/commands/break.go b/internal/commands/break.go index 7716915..f5e8835 100644 --- a/internal/commands/break.go +++ b/internal/commands/break.go @@ -11,7 +11,7 @@ import ( // BreakCommand handles BREAK statements // Syntax: BREAK -// Exits current WHILE loop +// Exits current loop type BreakCommand struct { skipLabel string } diff --git a/internal/commands/for.go b/internal/commands/for.go new file mode 100644 index 0000000..d7c26c7 --- /dev/null +++ b/internal/commands/for.go @@ -0,0 +1,280 @@ +package commands + +import ( + "fmt" + "strings" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" + "c65gm/internal/utils" +) + +// ForCommand handles FOR loop statements +// Syntax: FOR = TO [STEP ] +// +// FOR = DOWNTO [STEP ] +type ForCommand struct { + varName string + varKind compiler.VarKind + startOp *compiler.OperandInfo + endOp *compiler.OperandInfo + stepOp *compiler.OperandInfo + isDownto bool + useLongJump bool + loopLabel string + skipLabel string +} + +func (c *ForCommand) WillHandle(line preproc.Line) bool { + params, err := utils.ParseParams(line.Text) + if err != nil || len(params) == 0 { + return false + } + return strings.ToUpper(params[0]) == "FOR" +} + +func (c *ForCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error { + params, err := utils.ParseParams(line.Text) + if err != nil { + return err + } + + // FOR = TO/DOWNTO [STEP ] + // Minimum: 6 params (FOR var = start TO end) + // Maximum: 8 params (FOR var = start TO end STEP step) + if len(params) < 6 { // FOR keyword goes towards count + return fmt.Errorf("FOR: expected at least 5 parameters, got %d", len(params)) + } + + if len(params) != 6 && len(params) != 8 { + return fmt.Errorf("FOR: expected 5 or 7 parameters, got %d", len(params)) + } + + // Check '=' separator + if params[2] != "=" { + return fmt.Errorf("FOR: expected '=' at position 3, got %q", params[2]) + } + + scope := ctx.CurrentScope() + constLookup := func(name string) (int64, bool) { + sym := ctx.SymbolTable.Lookup(name, scope) + if sym != nil && sym.IsConst() { + return int64(sym.Value), true + } + return 0, false + } + + // Parse variable + varName := params[1] + varSym := ctx.SymbolTable.Lookup(varName, scope) + if varSym == nil { + return fmt.Errorf("FOR: unknown variable %q", varName) + } + if varSym.IsConst() { + return fmt.Errorf("FOR: cannot use constant %q as loop variable", varName) + } + c.varName = varSym.FullName() + c.varKind = varSym.GetVarKind() + + // Parse start value + var parseErr error + startVarName, startVarKind, startValue, startIsVar, parseErr := compiler.ParseOperandParam( + params[3], ctx.SymbolTable, scope, constLookup) + if parseErr != nil { + return fmt.Errorf("FOR: start value: %w", parseErr) + } + c.startOp = &compiler.OperandInfo{ + VarName: startVarName, + VarKind: startVarKind, + Value: startValue, + IsVar: startIsVar, + } + + // Parse direction (TO or DOWNTO) + direction := strings.ToUpper(params[4]) + if direction != "TO" && direction != "DOWNTO" { + return fmt.Errorf("FOR: expected 'TO' or 'DOWNTO' at position 5, got %q", params[4]) + } + c.isDownto = (direction == "DOWNTO") + + // Parse end value + endVarName, endVarKind, endValue, endIsVar, parseErr := compiler.ParseOperandParam( + params[5], ctx.SymbolTable, scope, constLookup) + if parseErr != nil { + return fmt.Errorf("FOR: end value: %w", parseErr) + } + c.endOp = &compiler.OperandInfo{ + VarName: endVarName, + VarKind: endVarKind, + Value: endValue, + IsVar: endIsVar, + } + + // Parse optional STEP + if len(params) == 8 { + if strings.ToUpper(params[6]) != "STEP" { + return fmt.Errorf("FOR: expected 'STEP' at position 7, got %q", params[6]) + } + + stepVarName, stepVarKind, stepValue, stepIsVar, parseErr := compiler.ParseOperandParam( + params[7], ctx.SymbolTable, scope, constLookup) + if parseErr != nil { + return fmt.Errorf("FOR: step value: %w", parseErr) + } + + // Check for zero or negative step if literal + if !stepIsVar { + if stepValue == 0 { + return fmt.Errorf("FOR: STEP cannot be zero") + } + // Since BYTE and WORD are unsigned, values > 32767 are treated as large positive + // We don't allow negative literals since they'd be interpreted as large unsigned + // This is a reasonable restriction for step values + } + + c.stepOp = &compiler.OperandInfo{ + VarName: stepVarName, + VarKind: stepVarKind, + Value: stepValue, + IsVar: stepIsVar, + } + } else { + // Default STEP 1 + c.stepOp = &compiler.OperandInfo{ + Value: 1, + IsVar: false, + } + } + + // Check pragma + ps := ctx.Pragma.GetPragmaSetByIndex(line.PragmaSetIndex) + longJumpPragma := ps.GetPragma("_P_USE_LONG_JUMP") + c.useLongJump = longJumpPragma != "" && longJumpPragma != "0" + + // Create labels + c.loopLabel = ctx.LoopStartStack.Push() + c.skipLabel = ctx.LoopEndStack.Push() + + // Push FOR info to ForStack + ctx.ForStack.Push(&compiler.ForLoopInfo{ + VarName: c.varName, + VarKind: c.varKind, + EndOperand: c.endOp, + StepOperand: c.stepOp, + IsDownto: c.isDownto, + LoopLabel: c.loopLabel, + SkipLabel: c.skipLabel, + }) + + return nil +} + +func (c *ForCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) { + var asm []string + + // Initial assignment: var = start + assignAsm := c.generateAssignment() + asm = append(asm, assignAsm...) + + // Emit loop label + asm = append(asm, c.loopLabel) + + // Generate comparison + // TO: continue if var <= end (skip if var > end) + // DOWNTO: continue if var >= end (skip if var < end) + var op comparisonOp + if c.isDownto { + op = opLess // skip if var < end + } else { + op = opGreater // skip if var > end + } + + varOp := &operandInfo{ + varName: c.varName, + varKind: c.varKind, + isVar: true, + } + + // Convert compiler.OperandInfo to commands.operandInfo for comparison + endOp := &operandInfo{ + varName: c.endOp.VarName, + varKind: c.endOp.VarKind, + value: c.endOp.Value, + isVar: c.endOp.IsVar, + } + + gen, err := newComparisonGenerator( + op, + varOp, + endOp, + c.useLongJump, + ctx.LoopEndStack, + ctx.GeneralStack, + ) + if err != nil { + return nil, fmt.Errorf("FOR: %w", err) + } + + cmpAsm, err := gen.generate() + if err != nil { + return nil, fmt.Errorf("FOR: %w", err) + } + + asm = append(asm, cmpAsm...) + return asm, nil +} + +func (c *ForCommand) generateAssignment() []string { + var asm []string + + // Variable assignment from startOp + if c.startOp.IsVar { + // Destination: byte + if c.varKind == compiler.KindByte { + // byte → byte or word → byte (take low byte) + asm = append(asm, fmt.Sprintf("\tlda %s", c.startOp.VarName)) + asm = append(asm, fmt.Sprintf("\tsta %s", c.varName)) + return asm + } + + // Destination: word + // byte → word (zero-extend) + if c.startOp.VarKind == compiler.KindByte { + asm = append(asm, fmt.Sprintf("\tlda %s", c.startOp.VarName)) + asm = append(asm, fmt.Sprintf("\tsta %s", c.varName)) + asm = append(asm, "\tlda #0") + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.varName)) + return asm + } + + // word → word (copy both bytes) + asm = append(asm, fmt.Sprintf("\tlda %s", c.startOp.VarName)) + asm = append(asm, fmt.Sprintf("\tsta %s", c.varName)) + asm = append(asm, fmt.Sprintf("\tlda %s+1", c.startOp.VarName)) + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.varName)) + return asm + } + + // Literal assignment + lo := uint8(c.startOp.Value & 0xFF) + hi := uint8((c.startOp.Value >> 8) & 0xFF) + + // Destination: byte + if c.varKind == compiler.KindByte { + asm = append(asm, fmt.Sprintf("\tlda #$%02x", lo)) + asm = append(asm, fmt.Sprintf("\tsta %s", c.varName)) + return asm + } + + // Destination: word + asm = append(asm, fmt.Sprintf("\tlda #$%02x", lo)) + asm = append(asm, fmt.Sprintf("\tsta %s", c.varName)) + + // Optimization: don't reload if lo == hi + if lo != hi { + asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi)) + } + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.varName)) + + return asm +} diff --git a/internal/commands/for_test.go b/internal/commands/for_test.go new file mode 100644 index 0000000..ed03c41 --- /dev/null +++ b/internal/commands/for_test.go @@ -0,0 +1,727 @@ +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) + }, + wantFor: []string{ + "\tlda #$00", + "\tsta i", + "_LOOPSTART1", + "\tlda i", + "\tcmp #$0a", + "\tbeq _L1", + "\tbcc _L1", + "\tjmp _LOOPEND1", + "_L1", + }, + wantNext: []string{ + "\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) + }, + wantFor: []string{ + "\tlda #$00", + "\tsta counter", + "\tsta counter+1", + "_LOOPSTART1", + "\tlda counter+1", + "\tcmp #$03", + "\tbcc _L1", + "\tbne _L2", + "\tlda counter", + "\tcmp #$e8", + "\tbeq _L1", + "\tbcc _L1", + "_L2", + "\tjmp _LOOPEND1", + "_L1", + }, + wantNext: []string{ + "\tinc counter", + "\tbne _L3", + "\tinc counter+1", + "_L3", + "\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 TestForBasicDOWNTO(t *testing.T) { + tests := []struct { + name string + forLine string + setupVars func(*compiler.SymbolTable) + wantFor []string + wantNext []string + }{ + { + name: "byte var DOWNTO byte literal", + forLine: "FOR i = 10 DOWNTO 0", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("i", "", compiler.KindByte, 0) + }, + wantFor: []string{ + "\tlda #$0a", + "\tsta i", + "_LOOPSTART1", + "\tlda i", + "\tbne _L1", + "\tjmp _LOOPEND1", + "_L1", + }, + wantNext: []string{ + "\tdec i", + "\tjmp _LOOPSTART1", + "_LOOPEND1", + }, + }, + { + name: "word var DOWNTO word literal", + forLine: "FOR counter = 1000 DOWNTO 0", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("counter", "", compiler.KindWord, 0) + }, + wantFor: []string{ + "\tlda #$e8", + "\tsta counter", + "\tlda #$03", + "\tsta counter+1", + "_LOOPSTART1", + "\tlda counter", + "\tbne _L1", + "\tlda counter+1", + "\tbne _L1", + "\tjmp _LOOPEND1", + "_L1", + }, + wantNext: []string{ + "\tlda counter", + "\tbne _L2", + "\tdec counter+1", + "_L2", + "\tdec counter", + "\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 TestForWithSTEP(t *testing.T) { + tests := []struct { + name string + forLine string + setupVars func(*compiler.SymbolTable) + checkNextAsm func([]string) bool + description string + }{ + { + name: "byte var TO with STEP 2", + forLine: "FOR i = 0 TO 10 STEP 2", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("i", "", compiler.KindByte, 0) + }, + checkNextAsm: func(asm []string) bool { + // Should contain adc #$02 + for _, line := range asm { + if strings.Contains(line, "adc #$02") { + return true + } + } + return false + }, + description: "STEP 2 should use adc #$02", + }, + { + name: "byte var DOWNTO with STEP 3", + forLine: "FOR i = 10 DOWNTO 0 STEP 3", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("i", "", compiler.KindByte, 0) + }, + checkNextAsm: func(asm []string) bool { + // Should contain sbc #$03 + for _, line := range asm { + if strings.Contains(line, "sbc #$03") { + return true + } + } + return false + }, + description: "STEP 3 should use sbc #$03", + }, + { + name: "byte var TO with variable STEP", + forLine: "FOR i = 0 TO 10 STEP stepval", + setupVars: func(st *compiler.SymbolTable) { + st.AddVar("i", "", compiler.KindByte, 0) + st.AddVar("stepval", "", compiler.KindByte, 0) + }, + checkNextAsm: func(asm []string) bool { + // Should contain adc stepval + for _, line := range asm { + if strings.Contains(line, "adc stepval") { + return true + } + } + return false + }, + description: "variable STEP should use adc variable", + }, + } + + 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) + } + + 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) + } + + if !tt.checkNextAsm(nextAsm) { + t.Errorf("%s\ngot:\n%s", tt.description, strings.Join(nextAsm, "\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) + } + + if asm1[0] == asm2[0] { + 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", + "FOR i = 0 TO 10 STEP 2 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") && !strings.Contains(err.Error(), "DOWNTO") { + t.Errorf("Wrong error message: %v", err) + } +} + +func TestForZeroStep(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 TO 10 STEP 0", + Kind: preproc.Source, + PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), + } + + err := cmd.Interpret(line, ctx) + if err == nil { + t.Fatal("Should fail with STEP 0") + } + if !strings.Contains(err.Error(), "STEP cannot be zero") { + 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) + } + + asm, err := forCmd.Generate(ctx) + if err != nil { + t.Fatalf("FOR Generate() error = %v", err) + } + + found := false + for _, inst := range asm { + if strings.Contains(inst, "#$64") { // 100 in hex + found = true + break + } + } + if !found { + t.Error("Constant should be folded to immediate value") + } + + if err := nextCmd.Interpret(nextLine, ctx); err != nil { + t.Fatalf("NEXT Interpret() error = %v", err) + } +} + +func TestForWordSTEP(t *testing.T) { + pragma := preproc.NewPragma() + ctx := compiler.NewCompilerContext(pragma) + ctx.SymbolTable.AddVar("counter", "", compiler.KindWord, 0) + + forCmd := &ForCommand{} + nextCmd := &NextCommand{} + + forLine := preproc.Line{ + Text: "FOR counter = 0 TO 1000 STEP 256", + 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) + } + + // Should handle both low and high bytes + foundLowAdd := false + foundHighAdd := false + for _, inst := range nextAsm { + if strings.Contains(inst, "adc #$00") { + foundLowAdd = true + } + if strings.Contains(inst, "adc #$01") { + foundHighAdd = true + } + } + if !foundLowAdd || !foundHighAdd { + t.Errorf("Word STEP should handle both bytes\ngot:\n%s", strings.Join(nextAsm, "\n")) + } +} diff --git a/internal/commands/next.go b/internal/commands/next.go new file mode 100644 index 0000000..f7e7217 --- /dev/null +++ b/internal/commands/next.go @@ -0,0 +1,222 @@ +package commands + +import ( + "fmt" + "strings" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" + "c65gm/internal/utils" +) + +// NextCommand handles NEXT statements +// Syntax: NEXT +// Increments/decrements loop variable and jumps back to loop start +type NextCommand struct { + info *compiler.ForLoopInfo +} + +func (c *NextCommand) WillHandle(line preproc.Line) bool { + params, err := utils.ParseParams(line.Text) + if err != nil || len(params) == 0 { + return false + } + return strings.ToUpper(params[0]) == "NEXT" +} + +func (c *NextCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error { + params, err := utils.ParseParams(line.Text) + if err != nil { + return err + } + + if len(params) != 1 { + return fmt.Errorf("NEXT: expected 1 parameter, got %d", len(params)) + } + + // Pop FOR info + info, err := ctx.ForStack.Pop() + if err != nil { + return fmt.Errorf("NEXT: not inside FOR loop") + } + c.info = info + + // Pop and validate labels + loopLabel, err := ctx.LoopStartStack.Pop() + if err != nil || loopLabel != info.LoopLabel { + return fmt.Errorf("NEXT: loop stack mismatch") + } + + skipLabel, err := ctx.LoopEndStack.Pop() + if err != nil || skipLabel != info.SkipLabel { + return fmt.Errorf("NEXT: loop stack mismatch") + } + + return nil +} + +func (c *NextCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) { + var asm []string + + // Generate increment/decrement + if c.info.IsDownto { + asm = append(asm, c.generateDecrement(ctx)...) + } else { + asm = append(asm, c.generateIncrement(ctx)...) + } + + // Jump back to loop start + asm = append(asm, fmt.Sprintf("\tjmp %s", c.info.LoopLabel)) + + // Emit skip label + asm = append(asm, c.info.SkipLabel) + + return asm, nil +} + +func (c *NextCommand) generateIncrement(ctx *compiler.CompilerContext) []string { + // Check for step = 1 literal optimization + if !c.info.StepOperand.IsVar && c.info.StepOperand.Value == 1 { + return c.generateIncrementByOne(ctx) + } + + // General case: var = var + step + return c.generateAdd() +} + +func (c *NextCommand) generateDecrement(ctx *compiler.CompilerContext) []string { + // Check for step = 1 literal optimization + if !c.info.StepOperand.IsVar && c.info.StepOperand.Value == 1 { + return c.generateDecrementByOne(ctx) + } + + // General case: var = var - step + return c.generateSubtract() +} + +func (c *NextCommand) generateIncrementByOne(ctx *compiler.CompilerContext) []string { + var asm []string + + if c.info.VarKind == compiler.KindByte { + asm = append(asm, fmt.Sprintf("\tinc %s", c.info.VarName)) + return asm + } + + // Word variable - handle carry to high byte + label := ctx.GeneralStack.Push() + asm = append(asm, fmt.Sprintf("\tinc %s", c.info.VarName)) + asm = append(asm, fmt.Sprintf("\tbne %s", label)) + asm = append(asm, fmt.Sprintf("\tinc %s+1", c.info.VarName)) + asm = append(asm, label) + + return asm +} + +func (c *NextCommand) generateDecrementByOne(ctx *compiler.CompilerContext) []string { + var asm []string + + if c.info.VarKind == compiler.KindByte { + asm = append(asm, fmt.Sprintf("\tdec %s", c.info.VarName)) + return asm + } + + // Word variable - handle borrow from high byte + label := ctx.GeneralStack.Push() + asm = append(asm, fmt.Sprintf("\tlda %s", c.info.VarName)) + asm = append(asm, fmt.Sprintf("\tbne %s", label)) + asm = append(asm, fmt.Sprintf("\tdec %s+1", c.info.VarName)) + asm = append(asm, label) + asm = append(asm, fmt.Sprintf("\tdec %s", c.info.VarName)) + + return asm +} + +func (c *NextCommand) generateAdd() []string { + var asm []string + + // var = var + step + stepOp := c.info.StepOperand + + asm = append(asm, "\tclc") + + // Load var low byte + asm = append(asm, fmt.Sprintf("\tlda %s", c.info.VarName)) + + // Add step low byte + if stepOp.IsVar { + asm = append(asm, fmt.Sprintf("\tadc %s", stepOp.VarName)) + } else { + asm = append(asm, fmt.Sprintf("\tadc #$%02x", uint8(stepOp.Value&0xFF))) + } + + // Store low byte + asm = append(asm, fmt.Sprintf("\tsta %s", c.info.VarName)) + + // If variable is word, handle high byte + if c.info.VarKind == compiler.KindWord { + // Load var high byte + asm = append(asm, fmt.Sprintf("\tlda %s+1", c.info.VarName)) + + // Add step high byte (with carry) + if stepOp.IsVar { + if stepOp.VarKind == compiler.KindWord { + asm = append(asm, fmt.Sprintf("\tadc %s+1", stepOp.VarName)) + } else { + asm = append(asm, "\tadc #0") + } + } else { + hi := uint8((stepOp.Value >> 8) & 0xFF) + asm = append(asm, fmt.Sprintf("\tadc #$%02x", hi)) + } + + // Store high byte + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.info.VarName)) + } + + return asm +} + +func (c *NextCommand) generateSubtract() []string { + var asm []string + + // var = var - step + stepOp := c.info.StepOperand + + asm = append(asm, "\tsec") + + // Load var low byte + asm = append(asm, fmt.Sprintf("\tlda %s", c.info.VarName)) + + // Subtract step low byte + if stepOp.IsVar { + asm = append(asm, fmt.Sprintf("\tsbc %s", stepOp.VarName)) + } else { + asm = append(asm, fmt.Sprintf("\tsbc #$%02x", uint8(stepOp.Value&0xFF))) + } + + // Store low byte + asm = append(asm, fmt.Sprintf("\tsta %s", c.info.VarName)) + + // If variable is word, handle high byte + if c.info.VarKind == compiler.KindWord { + // Load var high byte + asm = append(asm, fmt.Sprintf("\tlda %s+1", c.info.VarName)) + + // Subtract step high byte (with borrow) + if stepOp.IsVar { + if stepOp.VarKind == compiler.KindWord { + asm = append(asm, fmt.Sprintf("\tsbc %s+1", stepOp.VarName)) + } else { + asm = append(asm, "\tsbc #0") + } + } else { + hi := uint8((stepOp.Value >> 8) & 0xFF) + asm = append(asm, fmt.Sprintf("\tsbc #$%02x", hi)) + } + + // Store high byte + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.info.VarName)) + } + + return asm +} diff --git a/internal/compiler/context.go b/internal/compiler/context.go index c3cdcb2..52dad72 100644 --- a/internal/compiler/context.go +++ b/internal/compiler/context.go @@ -20,6 +20,7 @@ type CompilerContext struct { LoopEndStack *LabelStack // WHILE...WEND IfStack *LabelStack // IF...ENDIF GeneralStack *LabelStack // General purpose (GOSUB, etc) + ForStack *ForStack // For loop stack // Pragma access for per-line pragma lookup Pragma *preproc.Pragma @@ -38,6 +39,7 @@ func NewCompilerContext(pragma *preproc.Pragma) *CompilerContext { LoopEndStack: NewLabelStack("_LOOPEND"), IfStack: NewLabelStack("_I"), GeneralStack: generalStack, + ForStack: NewForStack(), Pragma: pragma, } diff --git a/internal/compiler/forstack.go b/internal/compiler/forstack.go new file mode 100644 index 0000000..1dba656 --- /dev/null +++ b/internal/compiler/forstack.go @@ -0,0 +1,67 @@ +package compiler + +import "fmt" + +// ForLoopInfo stores information about a FOR loop +type ForLoopInfo struct { + VarName string + VarKind VarKind + EndOperand *OperandInfo + StepOperand *OperandInfo + IsDownto bool + LoopLabel string + SkipLabel string +} + +// OperandInfo describes an operand (variable or literal) +type OperandInfo struct { + VarName string + VarKind VarKind + Value uint16 + IsVar bool +} + +// ForStack manages the stack of FOR loop contexts +type ForStack struct { + stack []*ForLoopInfo +} + +// NewForStack creates a new ForStack +func NewForStack() *ForStack { + return &ForStack{ + stack: make([]*ForLoopInfo, 0), + } +} + +// Push adds a new FOR loop context to the stack +func (fs *ForStack) Push(info *ForLoopInfo) { + fs.stack = append(fs.stack, info) +} + +// Peek returns the top FOR loop context without removing it +func (fs *ForStack) Peek() (*ForLoopInfo, error) { + if len(fs.stack) == 0 { + return nil, fmt.Errorf("stack underflow: FOR stack is empty") + } + return fs.stack[len(fs.stack)-1], nil +} + +// Pop removes and returns the top FOR loop context +func (fs *ForStack) Pop() (*ForLoopInfo, error) { + if len(fs.stack) == 0 { + return nil, fmt.Errorf("stack underflow: FOR stack is empty") + } + info := fs.stack[len(fs.stack)-1] + fs.stack = fs.stack[:len(fs.stack)-1] + return info, nil +} + +// IsEmpty returns true if the stack is empty +func (fs *ForStack) IsEmpty() bool { + return len(fs.stack) == 0 +} + +// Size returns the number of items on the stack +func (fs *ForStack) Size() int { + return len(fs.stack) +} From c647ff6ba9daeb8aa10dbd0fb01df07dc13eb6e7 Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Mon, 17 Nov 2025 22:48:31 +0100 Subject: [PATCH 2/5] Added FOR and NEXT to main.go --- main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/main.go b/main.go index bed664b..72622af 100644 --- a/main.go +++ b/main.go @@ -105,6 +105,8 @@ func registerCommands(comp *compiler.Compiler) { comp.Registry().Register(&commands.PokeWCommand{}) comp.Registry().Register(&commands.SubEndCommand{}) comp.Registry().Register(&commands.GosubCommand{}) + comp.Registry().Register(&commands.ForCommand{}) + comp.Registry().Register(&commands.NextCommand{}) } func writeOutput(filename string, lines []string) error { From ee741358399b4095548d80f7646c4be5a35e36db Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Mon, 17 Nov 2025 22:58:58 +0100 Subject: [PATCH 3/5] Fixed up comparision operator for FOR --- internal/commands/for.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/commands/for.go b/internal/commands/for.go index d7c26c7..f450176 100644 --- a/internal/commands/for.go +++ b/internal/commands/for.go @@ -184,9 +184,9 @@ func (c *ForCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) { // DOWNTO: continue if var >= end (skip if var < end) var op comparisonOp if c.isDownto { - op = opLess // skip if var < end + op = opGreaterEqual // skip if var < end } else { - op = opGreater // skip if var > end + op = opLessEqual // skip if var > end } varOp := &operandInfo{ From 4f515724771cd6f2df6dda459dddb85fd7315edb Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Mon, 17 Nov 2025 23:58:51 +0100 Subject: [PATCH 4/5] Changed for to not implement DOWNTO. --- internal/commands/for.go | 30 ++--- internal/commands/for_test.go | 201 +++++++++------------------------- internal/commands/next.go | 84 +------------- internal/compiler/forstack.go | 1 - 4 files changed, 64 insertions(+), 252 deletions(-) diff --git a/internal/commands/for.go b/internal/commands/for.go index f450176..5bb681c 100644 --- a/internal/commands/for.go +++ b/internal/commands/for.go @@ -11,15 +11,12 @@ import ( // ForCommand handles FOR loop statements // Syntax: FOR = TO [STEP ] -// -// FOR = DOWNTO [STEP ] type ForCommand struct { varName string varKind compiler.VarKind startOp *compiler.OperandInfo endOp *compiler.OperandInfo stepOp *compiler.OperandInfo - isDownto bool useLongJump bool loopLabel string skipLabel string @@ -90,12 +87,11 @@ func (c *ForCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) IsVar: startIsVar, } - // Parse direction (TO or DOWNTO) + // Parse direction (TO only) direction := strings.ToUpper(params[4]) - if direction != "TO" && direction != "DOWNTO" { - return fmt.Errorf("FOR: expected 'TO' or 'DOWNTO' at position 5, got %q", params[4]) + if direction != "TO" { + return fmt.Errorf("FOR: expected 'TO' at position 5, got %q (DOWNTO is not supported)", params[4]) } - c.isDownto = (direction == "DOWNTO") // Parse end value endVarName, endVarKind, endValue, endIsVar, parseErr := compiler.ParseOperandParam( @@ -161,7 +157,6 @@ func (c *ForCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) VarKind: c.varKind, EndOperand: c.endOp, StepOperand: c.stepOp, - IsDownto: c.isDownto, LoopLabel: c.loopLabel, SkipLabel: c.skipLabel, }) @@ -179,16 +174,7 @@ func (c *ForCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) { // Emit loop label asm = append(asm, c.loopLabel) - // Generate comparison - // TO: continue if var <= end (skip if var > end) - // DOWNTO: continue if var >= end (skip if var < end) - var op comparisonOp - if c.isDownto { - op = opGreaterEqual // skip if var < end - } else { - op = opLessEqual // skip if var > end - } - + // Generate comparison for TO loop: continue if var <= end (skip if var > end) varOp := &operandInfo{ varName: c.varName, varKind: c.varKind, @@ -204,7 +190,7 @@ func (c *ForCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) { } gen, err := newComparisonGenerator( - op, + opLessEqual, varOp, endOp, c.useLongJump, @@ -231,14 +217,14 @@ func (c *ForCommand) generateAssignment() []string { if c.startOp.IsVar { // Destination: byte if c.varKind == compiler.KindByte { - // byte → byte or word → byte (take low byte) + // byte → byte or word → byte (take low byte) asm = append(asm, fmt.Sprintf("\tlda %s", c.startOp.VarName)) asm = append(asm, fmt.Sprintf("\tsta %s", c.varName)) return asm } // Destination: word - // byte → word (zero-extend) + // byte → word (zero-extend) if c.startOp.VarKind == compiler.KindByte { asm = append(asm, fmt.Sprintf("\tlda %s", c.startOp.VarName)) asm = append(asm, fmt.Sprintf("\tsta %s", c.varName)) @@ -247,7 +233,7 @@ func (c *ForCommand) generateAssignment() []string { return asm } - // word → word (copy both bytes) + // word → word (copy both bytes) asm = append(asm, fmt.Sprintf("\tlda %s", c.startOp.VarName)) asm = append(asm, fmt.Sprintf("\tsta %s", c.varName)) asm = append(asm, fmt.Sprintf("\tlda %s+1", c.startOp.VarName)) diff --git a/internal/commands/for_test.go b/internal/commands/for_test.go index ed03c41..cff679a 100644 --- a/internal/commands/for_test.go +++ b/internal/commands/for_test.go @@ -26,12 +26,9 @@ func TestForBasicTO(t *testing.T) { "\tlda #$00", "\tsta i", "_LOOPSTART1", - "\tlda i", - "\tcmp #$0a", - "\tbeq _L1", - "\tbcc _L1", - "\tjmp _LOOPEND1", - "_L1", + "\tlda #$0a", + "\tcmp i", + "\tbcc _LOOPEND1", }, wantNext: []string{ "\tinc i", @@ -50,135 +47,20 @@ func TestForBasicTO(t *testing.T) { "\tsta counter", "\tsta counter+1", "_LOOPSTART1", - "\tlda counter+1", - "\tcmp #$03", - "\tbcc _L1", - "\tbne _L2", - "\tlda counter", - "\tcmp #$e8", - "\tbeq _L1", - "\tbcc _L1", - "_L2", - "\tjmp _LOOPEND1", + "\tlda #$03", + "\tcmp counter+1", + "\tbcc _LOOPEND1", + "\tbne _L1", + "\tlda #$e8", + "\tcmp counter", + "\tbcc _LOOPEND1", "_L1", }, wantNext: []string{ "\tinc counter", - "\tbne _L3", - "\tinc counter+1", - "_L3", - "\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 TestForBasicDOWNTO(t *testing.T) { - tests := []struct { - name string - forLine string - setupVars func(*compiler.SymbolTable) - wantFor []string - wantNext []string - }{ - { - name: "byte var DOWNTO byte literal", - forLine: "FOR i = 10 DOWNTO 0", - setupVars: func(st *compiler.SymbolTable) { - st.AddVar("i", "", compiler.KindByte, 0) - }, - wantFor: []string{ - "\tlda #$0a", - "\tsta i", - "_LOOPSTART1", - "\tlda i", - "\tbne _L1", - "\tjmp _LOOPEND1", - "_L1", - }, - wantNext: []string{ - "\tdec i", - "\tjmp _LOOPSTART1", - "_LOOPEND1", - }, - }, - { - name: "word var DOWNTO word literal", - forLine: "FOR counter = 1000 DOWNTO 0", - setupVars: func(st *compiler.SymbolTable) { - st.AddVar("counter", "", compiler.KindWord, 0) - }, - wantFor: []string{ - "\tlda #$e8", - "\tsta counter", - "\tlda #$03", - "\tsta counter+1", - "_LOOPSTART1", - "\tlda counter", - "\tbne _L1", - "\tlda counter+1", - "\tbne _L1", - "\tjmp _LOOPEND1", - "_L1", - }, - wantNext: []string{ - "\tlda counter", "\tbne _L2", - "\tdec counter+1", + "\tinc counter+1", "_L2", - "\tdec counter", "\tjmp _LOOPSTART1", "_LOOPEND1", }, @@ -262,23 +144,6 @@ func TestForWithSTEP(t *testing.T) { }, description: "STEP 2 should use adc #$02", }, - { - name: "byte var DOWNTO with STEP 3", - forLine: "FOR i = 10 DOWNTO 0 STEP 3", - setupVars: func(st *compiler.SymbolTable) { - st.AddVar("i", "", compiler.KindByte, 0) - }, - checkNextAsm: func(asm []string) bool { - // Should contain sbc #$03 - for _, line := range asm { - if strings.Contains(line, "sbc #$03") { - return true - } - } - return false - }, - description: "STEP 3 should use sbc #$03", - }, { name: "byte var TO with variable STEP", forLine: "FOR i = 0 TO 10 STEP stepval", @@ -419,7 +284,26 @@ func TestForNested(t *testing.T) { t.Fatalf("NEXT 1 error = %v", err) } - if asm1[0] == asm2[0] { + // 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") } } @@ -562,7 +446,28 @@ func TestForInvalidDirection(t *testing.T) { if err == nil { t.Fatal("Should fail with invalid direction keyword") } - if !strings.Contains(err.Error(), "TO") && !strings.Contains(err.Error(), "DOWNTO") { + 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) } } diff --git a/internal/commands/next.go b/internal/commands/next.go index f7e7217..f37d2d9 100644 --- a/internal/commands/next.go +++ b/internal/commands/next.go @@ -11,7 +11,7 @@ import ( // NextCommand handles NEXT statements // Syntax: NEXT -// Increments/decrements loop variable and jumps back to loop start +// Increments loop variable and jumps back to loop start type NextCommand struct { info *compiler.ForLoopInfo } @@ -58,12 +58,8 @@ func (c *NextCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext func (c *NextCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) { var asm []string - // Generate increment/decrement - if c.info.IsDownto { - asm = append(asm, c.generateDecrement(ctx)...) - } else { - asm = append(asm, c.generateIncrement(ctx)...) - } + // Generate increment + asm = append(asm, c.generateIncrement(ctx)...) // Jump back to loop start asm = append(asm, fmt.Sprintf("\tjmp %s", c.info.LoopLabel)) @@ -84,16 +80,6 @@ func (c *NextCommand) generateIncrement(ctx *compiler.CompilerContext) []string return c.generateAdd() } -func (c *NextCommand) generateDecrement(ctx *compiler.CompilerContext) []string { - // Check for step = 1 literal optimization - if !c.info.StepOperand.IsVar && c.info.StepOperand.Value == 1 { - return c.generateDecrementByOne(ctx) - } - - // General case: var = var - step - return c.generateSubtract() -} - func (c *NextCommand) generateIncrementByOne(ctx *compiler.CompilerContext) []string { var asm []string @@ -112,25 +98,6 @@ func (c *NextCommand) generateIncrementByOne(ctx *compiler.CompilerContext) []st return asm } -func (c *NextCommand) generateDecrementByOne(ctx *compiler.CompilerContext) []string { - var asm []string - - if c.info.VarKind == compiler.KindByte { - asm = append(asm, fmt.Sprintf("\tdec %s", c.info.VarName)) - return asm - } - - // Word variable - handle borrow from high byte - label := ctx.GeneralStack.Push() - asm = append(asm, fmt.Sprintf("\tlda %s", c.info.VarName)) - asm = append(asm, fmt.Sprintf("\tbne %s", label)) - asm = append(asm, fmt.Sprintf("\tdec %s+1", c.info.VarName)) - asm = append(asm, label) - asm = append(asm, fmt.Sprintf("\tdec %s", c.info.VarName)) - - return asm -} - func (c *NextCommand) generateAdd() []string { var asm []string @@ -175,48 +142,3 @@ func (c *NextCommand) generateAdd() []string { return asm } - -func (c *NextCommand) generateSubtract() []string { - var asm []string - - // var = var - step - stepOp := c.info.StepOperand - - asm = append(asm, "\tsec") - - // Load var low byte - asm = append(asm, fmt.Sprintf("\tlda %s", c.info.VarName)) - - // Subtract step low byte - if stepOp.IsVar { - asm = append(asm, fmt.Sprintf("\tsbc %s", stepOp.VarName)) - } else { - asm = append(asm, fmt.Sprintf("\tsbc #$%02x", uint8(stepOp.Value&0xFF))) - } - - // Store low byte - asm = append(asm, fmt.Sprintf("\tsta %s", c.info.VarName)) - - // If variable is word, handle high byte - if c.info.VarKind == compiler.KindWord { - // Load var high byte - asm = append(asm, fmt.Sprintf("\tlda %s+1", c.info.VarName)) - - // Subtract step high byte (with borrow) - if stepOp.IsVar { - if stepOp.VarKind == compiler.KindWord { - asm = append(asm, fmt.Sprintf("\tsbc %s+1", stepOp.VarName)) - } else { - asm = append(asm, "\tsbc #0") - } - } else { - hi := uint8((stepOp.Value >> 8) & 0xFF) - asm = append(asm, fmt.Sprintf("\tsbc #$%02x", hi)) - } - - // Store high byte - asm = append(asm, fmt.Sprintf("\tsta %s+1", c.info.VarName)) - } - - return asm -} diff --git a/internal/compiler/forstack.go b/internal/compiler/forstack.go index 1dba656..9e259d3 100644 --- a/internal/compiler/forstack.go +++ b/internal/compiler/forstack.go @@ -8,7 +8,6 @@ type ForLoopInfo struct { VarKind VarKind EndOperand *OperandInfo StepOperand *OperandInfo - IsDownto bool LoopLabel string SkipLabel string } From 315be292bf0cb4e5271c38254f581804b00ce5bf Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Tue, 18 Nov 2025 23:23:05 +0100 Subject: [PATCH 5/5] Added sensible handling for byte iterators --- internal/commands/for.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/internal/commands/for.go b/internal/commands/for.go index 5bb681c..79a8bcb 100644 --- a/internal/commands/for.go +++ b/internal/commands/for.go @@ -2,6 +2,7 @@ package commands import ( "fmt" + "os" "strings" "c65gm/internal/compiler" @@ -106,6 +107,26 @@ func (c *ForCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) IsVar: endIsVar, } + if c.varKind == compiler.KindByte { + // Error on literal out of range + if !c.startOp.IsVar && c.startOp.Value > 255 { + return fmt.Errorf("FOR: BYTE variable cannot start at literal %d (max 255)", c.startOp.Value) + } + if !c.endOp.IsVar && c.endOp.Value > 255 { + return fmt.Errorf("FOR: BYTE variable cannot loop to literal %d (max 255)", c.endOp.Value) + } + + // Warn on variable type mismatch + if c.startOp.IsVar && c.startOp.VarKind == compiler.KindWord { + _, _ = fmt.Fprintf(os.Stderr, "%s:%d: warning: BYTE loop variable with WORD start value truncates to low byte\n", + line.Filename, line.LineNo) + } + if c.endOp.IsVar && c.endOp.VarKind == compiler.KindWord { + _, _ = fmt.Fprintf(os.Stderr, "%s:%d: warning: BYTE loop variable with WORD end value may cause infinite loop\n", + line.Filename, line.LineNo) + } + } + // Parse optional STEP if len(params) == 8 { if strings.ToUpper(params[6]) != "STEP" {