287 lines
7.7 KiB
Go
287 lines
7.7 KiB
Go
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
|
|
}
|