Merge pull request 'for_loops' (#1) from for_loops into main
Reviewed-on: #1
This commit is contained in:
commit
4aa6641583
7 changed files with 1134 additions and 1 deletions
|
|
@ -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
287
internal/commands/for.go
Normal 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
|
||||
}
|
||||
632
internal/commands/for_test.go
Normal file
632
internal/commands/for_test.go
Normal 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
144
internal/commands/next.go
Normal 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
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
66
internal/compiler/forstack.go
Normal file
66
internal/compiler/forstack.go
Normal 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)
|
||||
}
|
||||
2
main.go
2
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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue