Merge pull request 'for_loops' (#1) from for_loops into main

Reviewed-on: #1
This commit is contained in:
mattiashz 2025-11-18 23:24:14 +01:00
commit 4aa6641583
7 changed files with 1134 additions and 1 deletions

View file

@ -11,7 +11,7 @@ import (
// BreakCommand handles BREAK statements
// Syntax: BREAK
// Exits current WHILE loop
// Exits current loop
type BreakCommand struct {
skipLabel string
}

287
internal/commands/for.go Normal file
View file

@ -0,0 +1,287 @@
package commands
import (
"fmt"
"os"
"strings"
"c65gm/internal/compiler"
"c65gm/internal/preproc"
"c65gm/internal/utils"
)
// ForCommand handles FOR loop statements
// Syntax: FOR <var> = <start> TO <end> [STEP <step>]
type ForCommand struct {
varName string
varKind compiler.VarKind
startOp *compiler.OperandInfo
endOp *compiler.OperandInfo
stepOp *compiler.OperandInfo
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 <var> = <start> TO/DOWNTO <end> [STEP <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 only)
direction := strings.ToUpper(params[4])
if direction != "TO" {
return fmt.Errorf("FOR: expected 'TO' at position 5, got %q (DOWNTO is not supported)", params[4])
}
// 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,
}
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" {
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,
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 for TO loop: continue if var <= end (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(
opLessEqual,
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
}

View file

@ -0,0 +1,632 @@
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 #$0a",
"\tcmp i",
"\tbcc _LOOPEND1",
},
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 #$03",
"\tcmp counter+1",
"\tbcc _LOOPEND1",
"\tbne _L1",
"\tlda #$e8",
"\tcmp counter",
"\tbcc _LOOPEND1",
"_L1",
},
wantNext: []string{
"\tinc counter",
"\tbne _L2",
"\tinc counter+1",
"_L2",
"\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 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)
}
// 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",
"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") {
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 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"))
}
}

144
internal/commands/next.go Normal file
View file

@ -0,0 +1,144 @@
package commands
import (
"fmt"
"strings"
"c65gm/internal/compiler"
"c65gm/internal/preproc"
"c65gm/internal/utils"
)
// NextCommand handles NEXT statements
// Syntax: NEXT
// Increments 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
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) 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) 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
}

View file

@ -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,
}

View file

@ -0,0 +1,66 @@
package compiler
import "fmt"
// ForLoopInfo stores information about a FOR loop
type ForLoopInfo struct {
VarName string
VarKind VarKind
EndOperand *OperandInfo
StepOperand *OperandInfo
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)
}

View file

@ -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 {