package commands import ( "fmt" "strings" "c65gm/internal/compiler" "c65gm/internal/preproc" "c65gm/internal/utils" ) // GosubCommand handles GOSUB statements (subroutine call with return) // Syntax: // // GOSUB // GOSUB PASSING AS ACC|XREG|YREG [ AS XREG|YREG [ 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 }