c65gm/internal/commands/gosub.go

276 lines
7 KiB
Go

package commands
import (
"fmt"
"strings"
"c65gm/internal/compiler"
"c65gm/internal/preproc"
"c65gm/internal/utils"
)
// GosubCommand handles GOSUB statements (subroutine call with return)
// Syntax:
//
// GOSUB <target>
// GOSUB <target> PASSING <var1> AS ACC|XREG|YREG [<var2> AS XREG|YREG [<var3> AS YREG]]
//
// Where target is:
// - Label name
// - Variable (byte or word) - uses self-modifying code
// - Expression/address
type GosubCommand struct {
targetName string
targetKind compiler.VarKind
targetAddress uint16
isVar bool
isLabel bool
// Register passing
accVarName string
accVarKind compiler.VarKind
xregVarName string
xregVarKind compiler.VarKind
yregVarName string
yregVarKind compiler.VarKind
pragmaSet preproc.PragmaSet
}
func (c *GosubCommand) WillHandle(line preproc.Line) bool {
params, err := utils.ParseParams(line.Text)
if err != nil || len(params) == 0 {
return false
}
return strings.ToUpper(params[0]) == "GOSUB"
}
func (c *GosubCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error {
// Clear state
c.targetName = ""
c.targetAddress = 0
c.isVar = false
c.isLabel = false
c.accVarName = ""
c.xregVarName = ""
c.yregVarName = ""
// Store pragma set
c.pragmaSet = ctx.Pragma.GetPragmaSetByIndex(line.PragmaSetIndex)
params, err := utils.ParseParams(line.Text)
if err != nil {
return err
}
paramCount := len(params)
if paramCount != 2 && paramCount != 6 && paramCount != 9 && paramCount != 12 {
return fmt.Errorf("GOSUB: expected 2, 6, 9, or 12 parameters, got %d", paramCount)
}
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 target (param 2)
targetParam := params[1]
// Try ParseOperandParam - handles variables, constants, expressions
varName, varKind, value, isVar, err := compiler.ParseOperandParam(
targetParam, ctx.SymbolTable, scope, constLookup)
if err != nil {
// Not a variable or expression -> treat as label
c.targetName = targetParam
c.isLabel = true
} else if isVar {
// It's a variable
c.targetName = varName
c.targetKind = varKind
c.isVar = true
} else {
// It's an expression/constant
c.targetAddress = value
c.isVar = false
c.isLabel = false
}
// Parse PASSING clause if present
if paramCount > 2 {
if strings.ToUpper(params[2]) != "PASSING" {
return fmt.Errorf("GOSUB: parameter #3 must be 'PASSING', got %q", params[2])
}
// First register (params 4-6)
if err := c.parseRegisterParam(params[3], params[4], params[5], ctx, scope); err != nil {
return err
}
// Second register (params 7-9)
if paramCount >= 9 {
if err := c.parseRegisterParam(params[6], params[7], params[8], ctx, scope); err != nil {
return err
}
}
// Third register (params 10-12)
if paramCount == 12 {
if err := c.parseRegisterParam(params[9], params[10], params[11], ctx, scope); err != nil {
return err
}
}
}
return nil
}
func (c *GosubCommand) parseRegisterParam(varName, asWord, regName string, ctx *compiler.CompilerContext, scope []string) error {
if strings.ToUpper(asWord) != "AS" {
return fmt.Errorf("GOSUB: expected 'AS', got %q", asWord)
}
reg := strings.ToUpper(regName)
if reg != "ACC" && reg != "XREG" && reg != "YREG" {
return fmt.Errorf("GOSUB: register must be ACC, XREG, or YREG, got %q", regName)
}
// Look up variable
sym := ctx.SymbolTable.Lookup(varName, scope)
if sym == nil {
return fmt.Errorf("GOSUB: unknown variable %q for register %s", varName, reg)
}
fullName := sym.FullName()
kind := sym.GetVarKind()
// Assign to appropriate register
switch reg {
case "ACC":
if c.accVarName != "" {
return fmt.Errorf("GOSUB: ACC already assigned")
}
c.accVarName = fullName
c.accVarKind = kind
case "XREG":
if c.xregVarName != "" {
return fmt.Errorf("GOSUB: XREG already assigned")
}
c.xregVarName = fullName
c.xregVarKind = kind
case "YREG":
if c.yregVarName != "" {
return fmt.Errorf("GOSUB: YREG already assigned")
}
c.yregVarName = fullName
c.yregVarKind = kind
}
return nil
}
func (c *GosubCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) {
var asm []string
// Check for immutable code pragma when using variable target
if c.isVar {
if c.pragmaSet.GetPragma("_P_USE_IMMUTABLE_CODE") != "" {
return nil, fmt.Errorf("GOSUB: USE_IMMUTABLE_CODE pragma set but variable target requires self-modifying code")
}
}
// Generate based on target type
if c.isVar {
// Variable target - self-modifying code
label := ctx.GeneralStack.Push()
if c.targetKind == compiler.KindByte {
// Byte variable
asm = append(asm, fmt.Sprintf("\tlda %s", c.targetName))
asm = append(asm, fmt.Sprintf("\tsta %s+1", label))
asm = append(asm, label)
} else {
// Word variable
asm = append(asm, fmt.Sprintf("\tlda %s", c.targetName))
asm = append(asm, fmt.Sprintf("\tsta %s+1", label))
asm = append(asm, fmt.Sprintf("\tlda %s+1", c.targetName))
asm = append(asm, fmt.Sprintf("\tsta %s+2", label))
asm = append(asm, label)
}
// Pre-passing: load variables into registers
asm = append(asm, c.generatePrePassing()...)
// JSR with placeholder address
asm = append(asm, "\tjsr $0000")
// Post-passing: store registers back to variables
asm = append(asm, c.generatePostPassing()...)
} else if c.isLabel {
// Label target
asm = append(asm, c.generatePrePassing()...)
asm = append(asm, fmt.Sprintf("\tjsr %s", c.targetName))
asm = append(asm, c.generatePostPassing()...)
} else {
// Address target
asm = append(asm, c.generatePrePassing()...)
asm = append(asm, fmt.Sprintf("\tjsr %d", c.targetAddress))
asm = append(asm, c.generatePostPassing()...)
}
return asm, nil
}
func (c *GosubCommand) generatePrePassing() []string {
var asm []string
if c.accVarName != "" {
asm = append(asm, fmt.Sprintf("\tlda %s", c.accVarName))
}
if c.xregVarName != "" {
asm = append(asm, fmt.Sprintf("\tldx %s", c.xregVarName))
}
if c.yregVarName != "" {
asm = append(asm, fmt.Sprintf("\tldy %s", c.yregVarName))
}
return asm
}
func (c *GosubCommand) generatePostPassing() []string {
var asm []string
if c.accVarName != "" {
asm = append(asm, fmt.Sprintf("\tsta %s", c.accVarName))
// Clear high byte for word destination
if c.accVarKind == compiler.KindWord {
asm = append(asm, "\tlda #0")
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.accVarName))
}
}
if c.xregVarName != "" {
asm = append(asm, fmt.Sprintf("\tstx %s", c.xregVarName))
// Clear high byte for word destination
if c.xregVarKind == compiler.KindWord {
asm = append(asm, "\tldx #0")
asm = append(asm, fmt.Sprintf("\tstx %s+1", c.xregVarName))
}
}
if c.yregVarName != "" {
asm = append(asm, fmt.Sprintf("\tsty %s", c.yregVarName))
// Clear high byte for word destination
if c.yregVarKind == compiler.KindWord {
asm = append(asm, "\tldy #0")
asm = append(asm, fmt.Sprintf("\tsty %s+1", c.yregVarName))
}
}
return asm
}