276 lines
7 KiB
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
|
|
}
|