From d717002b274821f4d3a00852357bb8489d4db738 Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Sun, 11 Jan 2026 14:45:15 +0100 Subject: [PATCH] Updated FOR loop to do check at NEXT instead of at FOR handling edge cases 255 and 65536 better. Removed STEP --- commands.md | 6 +- examples/for_byte_max_test/cm.sh | 20 ++ .../for_byte_max_test/for_byte_max_test.c65 | 266 ++++++++++++++++++ examples/for_byte_max_test/start_in_vice.sh | 1 + internal/commands/for.go | 79 ++---- internal/commands/for_test.go | 257 ++++++++--------- internal/commands/next.go | 152 ++++++---- internal/compiler/forstack.go | 2 +- language.md | 4 - 9 files changed, 518 insertions(+), 269 deletions(-) create mode 100755 examples/for_byte_max_test/cm.sh create mode 100644 examples/for_byte_max_test/for_byte_max_test.c65 create mode 100644 examples/for_byte_max_test/start_in_vice.sh diff --git a/commands.md b/commands.md index f7e8c9a..6b2e9b5 100644 --- a/commands.md +++ b/commands.md @@ -201,7 +201,7 @@ Loop with automatic counter increment. **Syntax:** ``` -FOR = TO [STEP ] +FOR = TO ``` **Examples:** @@ -210,10 +210,6 @@ FOR i = 0 TO 10 screen = i NEXT -FOR x = 0 TO 255 STEP 2 - result = result + x -NEXT - FOR counter = start TO finish process(counter) NEXT diff --git a/examples/for_byte_max_test/cm.sh b/examples/for_byte_max_test/cm.sh new file mode 100755 index 0000000..39ec4eb --- /dev/null +++ b/examples/for_byte_max_test/cm.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# Define filename as variable +PROGNAME="for_byte_max_test" +# Only set C65LIBPATH if not already defined +if [ -z "$C65LIBPATH" ]; then + export C65LIBPATH=$(readlink -f "../../lib") +fi +# Compile +c65gm -in ${PROGNAME}.c65 -out ${PROGNAME}.s +if [ $? -ne 0 ]; then + echo "Compilation terminated" + exit 1 +fi +echo assemble. +acme ${PROGNAME}.s +if [ -f ${PROGNAME}.prg ]; then + rm ${PROGNAME}.prg +fi +# main.bin ${PROGNAME}.prg +mv main.bin main.prg diff --git a/examples/for_byte_max_test/for_byte_max_test.c65 b/examples/for_byte_max_test/for_byte_max_test.c65 new file mode 100644 index 0000000..625d7a4 --- /dev/null +++ b/examples/for_byte_max_test/for_byte_max_test.c65 @@ -0,0 +1,266 @@ +//----------------------------------------------------------- +// FOR Loop BYTE Max Value Test +// Verifies that FOR b = 0 TO 255 correctly iterates 256 times +// without causing an infinite loop. +//----------------------------------------------------------- + +#INCLUDE +#INCLUDE +#INCLUDE + +#PRAGMA _P_USE_CBM_STRINGS 1 + +GOTO start + +WORD count +BYTE iter + +//----------------------------------------------------------- +// Test 1: FOR BYTE = 0 TO 255 +// Should iterate exactly 256 times (0, 1, 2, ..., 255) +//----------------------------------------------------------- +FUNC test_byte_to_255 + LET count = 0 + + FOR iter = 0 TO 255 + count++ + NEXT + + lib_cbmio_print("byte 0-255: ") + lib_cbmio_hexoutw(count) + lib_cbmio_printlf(" (exp 0100)") +FEND + +//----------------------------------------------------------- +// Test 2: FOR BYTE = 250 TO 255 +// Should iterate exactly 6 times (250, 251, 252, 253, 254, 255) +//----------------------------------------------------------- +FUNC test_byte_250_to_255 + LET count = 0 + + FOR iter = 250 TO 255 + count++ + NEXT + + lib_cbmio_print("byte 250-255: ") + lib_cbmio_hexoutw(count) + lib_cbmio_printlf(" (exp 0006)") +FEND + +//----------------------------------------------------------- +// Test 3: Verify last iteration value +// iter should be 255 after the loop completes +//----------------------------------------------------------- +FUNC test_final_value + LET iter = 0 + + FOR iter = 0 TO 255 + // just iterate + NEXT + + lib_cbmio_print("final iter: ") + lib_cbmio_hexoutb(iter) + lib_cbmio_printlf(" (exp ff)") +FEND + +//----------------------------------------------------------- +// Test 4: FOR WORD = 65530 TO 65535 +// Should iterate exactly 6 times (65530, 65531, ..., 65535) +// Tests WORD overflow protection without taking forever +//----------------------------------------------------------- +WORD witer + +FUNC test_word_to_65535 + LET count = 0 + + FOR witer = 65530 TO 65535 + count++ + NEXT + + lib_cbmio_print("word 65530-65535: ") + lib_cbmio_hexoutw(count) + lib_cbmio_printlf(" (exp 0006)") +FEND + +//----------------------------------------------------------- +// Test 5: Verify WORD final value +// witer should be 65535 ($FFFF) after the loop completes +//----------------------------------------------------------- +FUNC test_word_final_value + LET witer = 0 + + FOR witer = 65530 TO 65535 + // just iterate + NEXT + + lib_cbmio_print("final witer: ") + lib_cbmio_hexoutw(witer) + lib_cbmio_printlf(" (exp ffff)") +FEND + +//----------------------------------------------------------- +// Test 6: FOR BYTE with variable end value +// Tests that variable end values work correctly +//----------------------------------------------------------- +BYTE endval + +FUNC test_byte_var_end + LET count = 0 + LET endval = 10 + + FOR iter = 0 TO endval + count++ + NEXT + + lib_cbmio_print("byte 0-var(10): ") + lib_cbmio_hexoutw(count) + lib_cbmio_printlf(" (exp 000b)") +FEND + +//----------------------------------------------------------- +// Test 7: FOR BYTE with variable end at max (255) +//----------------------------------------------------------- +FUNC test_byte_var_max + LET count = 0 + LET endval = 255 + + FOR iter = 250 TO endval + count++ + NEXT + + lib_cbmio_print("byte 250-var(255): ") + lib_cbmio_hexoutw(count) + lib_cbmio_printlf(" (exp 0006)") +FEND + +//----------------------------------------------------------- +// Test 8: FOR WORD with variable end value +//----------------------------------------------------------- +WORD wendval + +FUNC test_word_var_end + LET count = 0 + LET wendval = 100 + + FOR witer = 0 TO wendval + count++ + NEXT + + lib_cbmio_print("word 0-var(100): ") + lib_cbmio_hexoutw(count) + lib_cbmio_printlf(" (exp 0065)") +FEND + +//----------------------------------------------------------- +// Test 9: FOR WORD with variable end at max (65535) +//----------------------------------------------------------- +FUNC test_word_var_max + LET count = 0 + LET wendval = 65535 + + FOR witer = 65530 TO wendval + count++ + NEXT + + lib_cbmio_print("word 65530-var(max): ") + lib_cbmio_hexoutw(count) + lib_cbmio_printlf(" (exp 0006)") +FEND + +//----------------------------------------------------------- +// Test 10: FOR with start == end (single iteration) +//----------------------------------------------------------- +FUNC test_same_start_end + LET count = 0 + + FOR iter = 5 TO 5 + count++ + NEXT + + lib_cbmio_print("byte 5-5: ") + lib_cbmio_hexoutw(count) + lib_cbmio_printlf(" (exp 0001)") +FEND + +//----------------------------------------------------------- +// Test 11: FOR with start var == loop var (skip assignment) +// When doing FOR i = i TO 10, assignment should be skipped +//----------------------------------------------------------- +FUNC test_self_start + LET count = 0 + LET iter = 3 + + FOR iter = iter TO 5 + count++ + NEXT + + lib_cbmio_print("byte i=i to 5: ") + lib_cbmio_hexoutw(count) + lib_cbmio_printlf(" (exp 0003)") +FEND + +//----------------------------------------------------------- +// Test 12: Long jump FOR loop +// Uses _P_USE_LONG_JUMP pragma with a 200-byte buffer in +// the loop body. Without long jump, branches would fail +// as they're limited to ±127 bytes. +//----------------------------------------------------------- + +FUNC test_long_jump + LET count = 0 + +#PRAGMA _P_USE_LONG_JUMP 1 + FOR iter = 0 TO 5 +#PRAGMA _P_USE_LONG_JUMP 0 + count++ + GOTO _test_lj_after_buffer +ASM + !fill 200, 0 +ENDASM + LABEL _test_lj_after_buffer + NEXT + + lib_cbmio_print("long jump 0-5: ") + lib_cbmio_hexoutw(count) + lib_cbmio_printlf(" (exp 0006)") +FEND + + +//----------------------------------------------------------- +// Main program +//----------------------------------------------------------- +FUNC main + lib_cbmio_cls() + + lib_cbmio_printlf("for max value test") + lib_cbmio_lf() + + test_byte_to_255() + test_byte_250_to_255() + test_final_value() + test_word_to_65535() + test_word_final_value() + + lib_cbmio_lf() + lib_cbmio_printlf("variable end tests:") + test_byte_var_end() + test_byte_var_max() + test_word_var_end() + test_word_var_max() + + lib_cbmio_lf() + lib_cbmio_printlf("edge case tests:") + test_same_start_end() + test_self_start() + + lib_cbmio_lf() + lib_cbmio_printlf("long jump test:") + test_long_jump() + + lib_cbmio_lf() + lib_cbmio_printlf("tests complete!") +FEND + +LABEL start + main() + SUBEND diff --git a/examples/for_byte_max_test/start_in_vice.sh b/examples/for_byte_max_test/start_in_vice.sh new file mode 100644 index 0000000..865c48e --- /dev/null +++ b/examples/for_byte_max_test/start_in_vice.sh @@ -0,0 +1 @@ +x64 -autostartprgmode 1 main.prg diff --git a/internal/commands/for.go b/internal/commands/for.go index 77d014a..9a40dcc 100644 --- a/internal/commands/for.go +++ b/internal/commands/for.go @@ -11,13 +11,12 @@ import ( ) // ForCommand handles FOR loop statements -// Syntax: FOR = TO [STEP ] +// Syntax: FOR = TO type ForCommand struct { varName string varKind compiler.VarKind startOp *compiler.OperandInfo endOp *compiler.OperandInfo - stepOp *compiler.OperandInfo useLongJump bool loopLabel string skipLabel string @@ -37,15 +36,10 @@ func (c *ForCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) 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)) + // FOR = TO + // Exactly 6 params (FOR var = start TO end) + if len(params) != 6 { + return fmt.Errorf("FOR: expected 5 parameters (FOR var = start TO end), got %d", len(params)-1) } // Check '=' separator @@ -127,42 +121,6 @@ func (c *ForCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) } } - // 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") @@ -177,9 +135,9 @@ func (c *ForCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) VarName: c.varName, VarKind: c.varKind, EndOperand: c.endOp, - StepOperand: c.stepOp, LoopLabel: c.loopLabel, SkipLabel: c.skipLabel, + UseLongJump: c.useLongJump, }) return nil @@ -192,17 +150,14 @@ func (c *ForCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) { assignAsm := c.generateAssignment() asm = append(asm, assignAsm...) - // Emit loop label - asm = append(asm, c.loopLabel) - - // Generate comparison for TO loop: continue if var <= end (skip if var > end) - varOp := &operandInfo{ - varName: c.varName, - varKind: c.varKind, - isVar: true, + // Initial guard: skip loop if start > end (using start <= end, jump on FALSE) + startOp := &operandInfo{ + varName: c.startOp.VarName, + varKind: c.startOp.VarKind, + value: c.startOp.Value, + isVar: c.startOp.IsVar, } - // Convert compiler.OperandInfo to commands.operandInfo for comparison endOp := &operandInfo{ varName: c.endOp.VarName, varKind: c.endOp.VarKind, @@ -212,7 +167,7 @@ func (c *ForCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) { gen, err := newComparisonGenerator( opLessEqual, - varOp, + startOp, endOp, c.useLongJump, ctx.LoopEndStack, @@ -222,12 +177,16 @@ func (c *ForCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) { return nil, fmt.Errorf("FOR: %w", err) } - cmpAsm, err := gen.generate() + guardAsm, err := gen.generate() if err != nil { return nil, fmt.Errorf("FOR: %w", err) } - asm = append(asm, cmpAsm...) + asm = append(asm, guardAsm...) + + // Emit loop label (body starts here) + asm = append(asm, c.loopLabel) + return asm, nil } diff --git a/internal/commands/for_test.go b/internal/commands/for_test.go index cff679a..dde0514 100644 --- a/internal/commands/for_test.go +++ b/internal/commands/for_test.go @@ -22,15 +22,18 @@ func TestForBasicTO(t *testing.T) { 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", - "\tlda #$0a", - "\tcmp i", - "\tbcc _LOOPEND1", }, + // End check before increment wantNext: []string{ + "\tlda i", + "\tcmp #$0a", + "\tbeq _LOOPEND1", "\tinc i", "\tjmp _LOOPSTART1", "_LOOPEND1", @@ -42,25 +45,26 @@ func TestForBasicTO(t *testing.T) { 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", - "\tlda #$03", - "\tcmp counter+1", - "\tbcc _LOOPEND1", - "\tbne _L1", - "\tlda #$e8", - "\tcmp counter", - "\tbcc _LOOPEND1", - "_L1", }, + // End check before increment (WORD comparison) wantNext: []string{ + "\tlda counter", + "\tcmp #$e8", + "\tbne +", + "\tlda counter+1", + "\tcmp #$03", + "\tbeq _LOOPEND1", + "+", "\tinc counter", - "\tbne _L2", + "\tbne _L1", "\tinc counter+1", - "_L2", + "_L1", "\tjmp _LOOPSTART1", "_LOOPEND1", }, @@ -119,95 +123,6 @@ func TestForBasicTO(t *testing.T) { } } -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 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) @@ -411,8 +326,8 @@ func TestForWrongParamCount(t *testing.T) { "FOR i", "FOR i = 0", "FOR i = 0 TO", - "FOR i = 0 TO 10 STEP", - "FOR i = 0 TO 10 STEP 2 EXTRA", + "FOR i = 0 TO 10 STEP 2", // STEP not supported + "FOR i = 0 TO 10 EXTRA", } for _, text := range tests { @@ -472,27 +387,6 @@ func TestForDOWNTORejected(t *testing.T) { } } -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) @@ -558,13 +452,22 @@ func TestForConstantEnd(t *testing.T) { t.Fatalf("FOR Interpret() error = %v", err) } - asm, err := forCmd.Generate(ctx) - if err != nil { + 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 asm { + for _, inst := range nextAsm { if strings.Contains(inst, "#$64") { // 100 in hex found = true break @@ -573,22 +476,21 @@ func TestForConstantEnd(t *testing.T) { 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) { +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("counter", "", compiler.KindWord, 0) + ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 0) forCmd := &ForCommand{} nextCmd := &NextCommand{} forLine := preproc.Line{ - Text: "FOR counter = 0 TO 1000 STEP 256", + Text: "FOR b = 0 TO 255", Kind: preproc.Source, PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), } @@ -601,11 +503,9 @@ func TestForWordSTEP(t *testing.T) { 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) } @@ -615,18 +515,81 @@ func TestForWordSTEP(t *testing.T) { 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 + // 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(inst, "adc #$01") { - foundHighAdd = true + if strings.Contains(line, "cmp #$ff") { + hasCmp255 = true + } + if strings.Contains(line, "beq _LOOPEND") { + hasBeq = true } } - if !foundLowAdd || !foundHighAdd { - t.Errorf("Word STEP should handle both bytes\ngot:\n%s", strings.Join(nextAsm, "\n")) + + 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")) + } +} + diff --git a/internal/commands/next.go b/internal/commands/next.go index f37d2d9..7442309 100644 --- a/internal/commands/next.go +++ b/internal/commands/next.go @@ -58,6 +58,10 @@ func (c *NextCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext func (c *NextCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) { var asm []string + // Do-while style: check if var == end BEFORE incrementing. + // Exit loop if var == end, otherwise continue to increment. + asm = append(asm, c.generateEndCheck(ctx)...) + // Generate increment asm = append(asm, c.generateIncrement(ctx)...) @@ -70,17 +74,106 @@ func (c *NextCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) 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) +// generateEndCheck generates code to exit if var == end. +func (c *NextCommand) generateEndCheck(ctx *compiler.CompilerContext) []string { + var asm []string + endOp := c.info.EndOperand + + if c.info.VarKind == compiler.KindByte { + return c.generateByteEndCheck(ctx) } - // General case: var = var + step - return c.generateAdd() + // WORD comparison + if !endOp.IsVar { + // Literal end value + lo := uint8(endOp.Value & 0xFF) + hi := uint8((endOp.Value >> 8) & 0xFF) + + if !c.info.UseLongJump { + asm = append(asm, fmt.Sprintf("\tlda %s", c.info.VarName)) + asm = append(asm, fmt.Sprintf("\tcmp #$%02x", lo)) + asm = append(asm, fmt.Sprintf("\tbne +")) + asm = append(asm, fmt.Sprintf("\tlda %s+1", c.info.VarName)) + asm = append(asm, fmt.Sprintf("\tcmp #$%02x", hi)) + asm = append(asm, fmt.Sprintf("\tbeq %s", c.info.SkipLabel)) + asm = append(asm, "+") + } else { + continueLabel := ctx.GeneralStack.Push() + ctx.GeneralStack.Pop() + asm = append(asm, fmt.Sprintf("\tlda %s", c.info.VarName)) + asm = append(asm, fmt.Sprintf("\tcmp #$%02x", lo)) + asm = append(asm, fmt.Sprintf("\tbne %s", continueLabel)) + asm = append(asm, fmt.Sprintf("\tlda %s+1", c.info.VarName)) + asm = append(asm, fmt.Sprintf("\tcmp #$%02x", hi)) + asm = append(asm, fmt.Sprintf("\tbne %s", continueLabel)) + asm = append(asm, fmt.Sprintf("\tjmp %s", c.info.SkipLabel)) + asm = append(asm, continueLabel) + } + } else { + // Variable end value + if !c.info.UseLongJump { + asm = append(asm, fmt.Sprintf("\tlda %s", c.info.VarName)) + asm = append(asm, fmt.Sprintf("\tcmp %s", endOp.VarName)) + asm = append(asm, fmt.Sprintf("\tbne +")) + if endOp.VarKind == compiler.KindWord { + asm = append(asm, fmt.Sprintf("\tlda %s+1", c.info.VarName)) + asm = append(asm, fmt.Sprintf("\tcmp %s+1", endOp.VarName)) + } else { + // BYTE end var compared to WORD loop var + asm = append(asm, fmt.Sprintf("\tlda %s+1", c.info.VarName)) + asm = append(asm, "\tcmp #$00") + } + asm = append(asm, fmt.Sprintf("\tbeq %s", c.info.SkipLabel)) + asm = append(asm, "+") + } else { + continueLabel := ctx.GeneralStack.Push() + ctx.GeneralStack.Pop() + asm = append(asm, fmt.Sprintf("\tlda %s", c.info.VarName)) + asm = append(asm, fmt.Sprintf("\tcmp %s", endOp.VarName)) + asm = append(asm, fmt.Sprintf("\tbne %s", continueLabel)) + if endOp.VarKind == compiler.KindWord { + asm = append(asm, fmt.Sprintf("\tlda %s+1", c.info.VarName)) + asm = append(asm, fmt.Sprintf("\tcmp %s+1", endOp.VarName)) + } else { + asm = append(asm, fmt.Sprintf("\tlda %s+1", c.info.VarName)) + asm = append(asm, "\tcmp #$00") + } + asm = append(asm, fmt.Sprintf("\tbne %s", continueLabel)) + asm = append(asm, fmt.Sprintf("\tjmp %s", c.info.SkipLabel)) + asm = append(asm, continueLabel) + } + } + + return asm } -func (c *NextCommand) generateIncrementByOne(ctx *compiler.CompilerContext) []string { +// generateByteEndCheck generates end check for BYTE loop variable. +func (c *NextCommand) generateByteEndCheck(ctx *compiler.CompilerContext) []string { + var asm []string + endOp := c.info.EndOperand + + asm = append(asm, fmt.Sprintf("\tlda %s", c.info.VarName)) + + if !endOp.IsVar { + asm = append(asm, fmt.Sprintf("\tcmp #$%02x", uint8(endOp.Value))) + } else { + asm = append(asm, fmt.Sprintf("\tcmp %s", endOp.VarName)) + } + + if !c.info.UseLongJump { + asm = append(asm, fmt.Sprintf("\tbeq %s", c.info.SkipLabel)) + } else { + continueLabel := ctx.GeneralStack.Push() + ctx.GeneralStack.Pop() + asm = append(asm, fmt.Sprintf("\tbne %s", continueLabel)) + asm = append(asm, fmt.Sprintf("\tjmp %s", c.info.SkipLabel)) + asm = append(asm, continueLabel) + } + + return asm +} + +func (c *NextCommand) generateIncrement(ctx *compiler.CompilerContext) []string { var asm []string if c.info.VarKind == compiler.KindByte { @@ -97,48 +190,3 @@ func (c *NextCommand) generateIncrementByOne(ctx *compiler.CompilerContext) []st 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 -} diff --git a/internal/compiler/forstack.go b/internal/compiler/forstack.go index 9e259d3..de46a2a 100644 --- a/internal/compiler/forstack.go +++ b/internal/compiler/forstack.go @@ -7,9 +7,9 @@ type ForLoopInfo struct { VarName string VarKind VarKind EndOperand *OperandInfo - StepOperand *OperandInfo LoopLabel string SkipLabel string + UseLongJump bool } // OperandInfo describes an operand (variable or literal) diff --git a/language.md b/language.md index a73a1d3..7b3a286 100644 --- a/language.md +++ b/language.md @@ -242,10 +242,6 @@ FOR i = 0 TO 10 screen = i NEXT -FOR x = 0 TO 255 STEP 2 - result = result + x -NEXT - FOR counter = start TO finish process(counter) NEXT