diff --git a/internal/commands/gosub.go b/internal/commands/gosub.go new file mode 100644 index 0000000..97109ad --- /dev/null +++ b/internal/commands/gosub.go @@ -0,0 +1,276 @@ +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 +} diff --git a/internal/commands/peek.go b/internal/commands/peek.go new file mode 100644 index 0000000..d237f17 --- /dev/null +++ b/internal/commands/peek.go @@ -0,0 +1,269 @@ +package commands + +import ( + "fmt" + "strings" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" + "c65gm/internal/utils" +) + +// PeekCommand handles PEEK statements +// Syntax: PEEK
[offset] GIVING|-> +// Where address is: +// - Byte variable (self-modifying code) +// - Word variable (self-modifying code or ZP pointer with offset) +// - Expression (direct addressing) +type PeekCommand struct { + addrVarName string + addrVarKind compiler.VarKind + addrValue uint16 + isAddrVar bool + isZPPointer bool + + offsetVarName string + offsetValue uint8 + hasOffset bool + isOffsetVar bool + + destVarName string + destVarKind compiler.VarKind + + pragmaSet preproc.PragmaSet +} + +func (c *PeekCommand) WillHandle(line preproc.Line) bool { + params, err := utils.ParseParams(line.Text) + if err != nil || len(params) != 4 { + return false + } + return strings.ToUpper(params[0]) == "PEEK" +} + +func (c *PeekCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error { + // Clear state + c.addrVarName = "" + c.addrValue = 0 + c.isAddrVar = false + c.isZPPointer = false + c.offsetVarName = "" + c.offsetValue = 0 + c.hasOffset = false + c.isOffsetVar = false + c.destVarName = "" + + // Store pragma set for Generate phase + c.pragmaSet = ctx.Pragma.GetPragmaSetByIndex(line.PragmaSetIndex) + + params, err := utils.ParseParams(line.Text) + if err != nil { + return err + } + + if len(params) != 4 { + return fmt.Errorf("PEEK: expected 4 parameters, got %d", len(params)) + } + + scope := ctx.CurrentScope() + + // Validate separator (param 3) + sep := strings.ToUpper(params[2]) + if sep != "GIVING" && sep != "->" { + return fmt.Errorf("PEEK: parameter #3 must be 'GIVING' or '->', got %q", params[2]) + } + + // Parse address (param 2) - may have [offset] + addrParam := params[1] + baseAddr, offsetParam := parseOffset(addrParam) + + 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 + } + + // Try to parse base address + varName, varKind, value, isVar, err := compiler.ParseOperandParam( + baseAddr, ctx.SymbolTable, scope, constLookup) + + if err != nil { + return fmt.Errorf("PEEK: invalid address: %w", err) + } + + if isVar { + // It's a variable + c.addrVarName = varName + c.addrVarKind = varKind + c.isAddrVar = true + + // Check if it's a ZP pointer + addrSym := ctx.SymbolTable.Lookup(baseAddr, scope) + if addrSym != nil { + c.isZPPointer = addrSym.IsZeroPagePointer() + } + } else { + // It's an expression/constant + c.addrValue = value + c.isAddrVar = false + } + + // Parse offset if present + if offsetParam != "" { + // Offset only allowed with ZP pointers + if !c.isZPPointer { + return fmt.Errorf("PEEK: offset [%s] only allowed with zero-page word pointer", offsetParam) + } + + c.hasOffset = true + + // Try to parse offset as variable or expression + offsetVarName, _, offsetValue, isOffsetVar, err := compiler.ParseOperandParam( + offsetParam, ctx.SymbolTable, scope, constLookup) + + if err != nil { + return fmt.Errorf("PEEK: invalid offset: %w", err) + } + + if isOffsetVar { + // Check it's a byte variable + offsetSym := ctx.SymbolTable.Lookup(offsetParam, scope) + if offsetSym != nil && !offsetSym.IsByte() { + return fmt.Errorf("PEEK: offset variable %q must be byte type", offsetParam) + } + c.offsetVarName = offsetVarName + c.isOffsetVar = true + } else { + // Numeric offset - must fit in byte + if offsetValue > 255 { + return fmt.Errorf("PEEK: offset value %d out of byte range", offsetValue) + } + c.offsetValue = uint8(offsetValue) + c.isOffsetVar = false + } + } + + // Parse destination variable (param 4) + destParam := params[3] + destSym := ctx.SymbolTable.Lookup(destParam, scope) + if destSym == nil { + return fmt.Errorf("PEEK: unknown destination variable %q", destParam) + } + if destSym.IsConst() { + return fmt.Errorf("PEEK: cannot PEEK into constant %q", destParam) + } + c.destVarName = destSym.FullName() + c.destVarKind = destSym.GetVarKind() + + return nil +} + +func (c *PeekCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) { + var asm []string + + // Check for immutable code pragma when using self-modifying code + if c.isAddrVar && !c.isZPPointer { + if c.pragmaSet.GetPragma("_P_USE_IMMUTABLE_CODE") != "" { + return nil, fmt.Errorf("PEEK: USE_IMMUTABLE_CODE pragma set but construct requires self-modifying code (consider using zero page word for address)") + } + } + + // Case 1: ZP pointer with offset + if c.isZPPointer { + // Load offset into Y + if c.hasOffset { + if c.isOffsetVar { + asm = append(asm, fmt.Sprintf("\tldy %s", c.offsetVarName)) + } else { + asm = append(asm, fmt.Sprintf("\tldy #%d", c.offsetValue)) + } + } else { + asm = append(asm, "\tldy #0") + } + + // Clear high byte for word destination + if c.destVarKind == compiler.KindWord { + // Optimization: if offset is 0 and not a variable, use Y register (which is 0) + if !c.hasOffset || (!c.isOffsetVar && c.offsetValue == 0) { + asm = append(asm, fmt.Sprintf("\tsty %s+1", c.destVarName)) + } else { + asm = append(asm, "\tldx #0") + asm = append(asm, fmt.Sprintf("\tstx %s+1", c.destVarName)) + } + } + + // Indexed indirect load + asm = append(asm, fmt.Sprintf("\tlda (%s),y", c.addrVarName)) + asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName)) + return asm, nil + } + + // Case 2: Byte variable (self-modifying code) + if c.isAddrVar && c.addrVarKind == compiler.KindByte { + label := ctx.GeneralStack.Push() + asm = append(asm, fmt.Sprintf("\tlda %s", c.addrVarName)) + asm = append(asm, fmt.Sprintf("\tsta %s+1", label)) + asm = append(asm, label) + asm = append(asm, "\tlda $ff") + asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName)) + + // Clear high byte for word destination + if c.destVarKind == compiler.KindWord { + asm = append(asm, "\tlda #0") + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName)) + } + return asm, nil + } + + // Case 3: Word variable (self-modifying code) + if c.isAddrVar && c.addrVarKind == compiler.KindWord { + label := ctx.GeneralStack.Push() + asm = append(asm, fmt.Sprintf("\tlda %s", c.addrVarName)) + asm = append(asm, fmt.Sprintf("\tsta %s+1", label)) + asm = append(asm, fmt.Sprintf("\tlda %s+1", c.addrVarName)) + asm = append(asm, fmt.Sprintf("\tsta %s+2", label)) + asm = append(asm, label) + asm = append(asm, "\tlda $ffff") + asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName)) + + // Clear high byte for word destination + if c.destVarKind == compiler.KindWord { + asm = append(asm, "\tlda #0") + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName)) + } + return asm, nil + } + + // Case 4: Expression/constant address (direct addressing) + asm = append(asm, fmt.Sprintf("\tlda %d", c.addrValue)) + asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName)) + + // Clear high byte for word destination + if c.destVarKind == compiler.KindWord { + asm = append(asm, "\tlda #0") + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName)) + } + + return asm, nil +} + +// parseOffset extracts offset from syntax like "var[offset]" +// Returns (base, offset) where offset is empty if no brackets found +func parseOffset(param string) (base string, offset string) { + openBracket := strings.IndexByte(param, '[') + closeBracket := strings.IndexByte(param, ']') + + if openBracket == -1 || closeBracket == -1 { + return param, "" + } + + if closeBracket <= openBracket || closeBracket != len(param)-1 { + return param, "" + } + + base = param[:openBracket] + offset = param[openBracket+1 : closeBracket] + return base, offset +} diff --git a/internal/commands/peekw.go b/internal/commands/peekw.go new file mode 100644 index 0000000..74b0dab --- /dev/null +++ b/internal/commands/peekw.go @@ -0,0 +1,265 @@ +package commands + +import ( + "fmt" + "strings" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" + "c65gm/internal/utils" +) + +// PeekWCommand handles PEEKW statements (reads a word/2 bytes) +// Syntax: PEEKW
[offset] GIVING|-> +// Where address is: +// - Byte variable (zero-page indexed addressing) +// - Word variable (self-modifying code or ZP pointer with offset) +// - Expression (direct addressing) +// +// Destination must be a word variable +type PeekWCommand struct { + addrVarName string + addrVarKind compiler.VarKind + addrValue uint16 + isAddrVar bool + isZPPointer bool + + offsetVarName string + offsetValue uint8 + hasOffset bool + isOffsetVar bool + + destVarName string + + pragmaSet preproc.PragmaSet +} + +func (c *PeekWCommand) WillHandle(line preproc.Line) bool { + params, err := utils.ParseParams(line.Text) + if err != nil || len(params) != 4 { + return false + } + return strings.ToUpper(params[0]) == "PEEKW" +} + +func (c *PeekWCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error { + // Clear state + c.addrVarName = "" + c.addrValue = 0 + c.isAddrVar = false + c.isZPPointer = false + c.offsetVarName = "" + c.offsetValue = 0 + c.hasOffset = false + c.isOffsetVar = false + c.destVarName = "" + + // Store pragma set for Generate phase + c.pragmaSet = ctx.Pragma.GetPragmaSetByIndex(line.PragmaSetIndex) + + params, err := utils.ParseParams(line.Text) + if err != nil { + return err + } + + if len(params) != 4 { + return fmt.Errorf("PEEKW: expected 4 parameters, got %d", len(params)) + } + + scope := ctx.CurrentScope() + + // Validate separator (param 3) + sep := strings.ToUpper(params[2]) + if sep != "GIVING" && sep != "->" { + return fmt.Errorf("PEEKW: parameter #3 must be 'GIVING' or '->', got %q", params[2]) + } + + // Parse address (param 2) - may have [offset] + addrParam := params[1] + baseAddr, offsetParam := parseOffsetW(addrParam) + + 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 + } + + // Try to parse base address + varName, varKind, value, isVar, err := compiler.ParseOperandParam( + baseAddr, ctx.SymbolTable, scope, constLookup) + + if err != nil { + return fmt.Errorf("PEEKW: invalid address: %w", err) + } + + if isVar { + // It's a variable + c.addrVarName = varName + c.addrVarKind = varKind + c.isAddrVar = true + + // Check if it's a ZP pointer + addrSym := ctx.SymbolTable.Lookup(baseAddr, scope) + if addrSym != nil { + c.isZPPointer = addrSym.IsZeroPagePointer() + } + } else { + // It's an expression/constant + c.addrValue = value + c.isAddrVar = false + } + + // Parse offset if present + if offsetParam != "" { + // Offset only allowed with ZP pointers + if !c.isZPPointer { + return fmt.Errorf("PEEKW: offset [%s] only allowed with zero-page word pointer", offsetParam) + } + + c.hasOffset = true + + // Try to parse offset as variable or expression + offsetVarName, _, offsetValue, isOffsetVar, err := compiler.ParseOperandParam( + offsetParam, ctx.SymbolTable, scope, constLookup) + + if err != nil { + return fmt.Errorf("PEEKW: invalid offset: %w", err) + } + + if isOffsetVar { + // Check it's a byte variable + offsetSym := ctx.SymbolTable.Lookup(offsetParam, scope) + if offsetSym != nil && !offsetSym.IsByte() { + return fmt.Errorf("PEEKW: offset variable %q must be byte type", offsetParam) + } + c.offsetVarName = offsetVarName + c.isOffsetVar = true + } else { + // Numeric offset - must fit in byte + if offsetValue > 255 { + return fmt.Errorf("PEEKW: offset value %d out of byte range", offsetValue) + } + c.offsetValue = uint8(offsetValue) + c.isOffsetVar = false + } + } + + // Parse destination variable (param 4) + destParam := params[3] + destSym := ctx.SymbolTable.Lookup(destParam, scope) + if destSym == nil { + return fmt.Errorf("PEEKW: unknown destination variable %q", destParam) + } + if destSym.IsConst() { + return fmt.Errorf("PEEKW: cannot PEEKW into constant %q", destParam) + } + if !destSym.IsWord() { + return fmt.Errorf("PEEKW: destination %q must be word type", destParam) + } + c.destVarName = destSym.FullName() + + return nil +} + +func (c *PeekWCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) { + var asm []string + + // Case 1: ZP pointer with offset + if c.isZPPointer { + // Load offset into Y + if c.hasOffset { + if c.isOffsetVar { + asm = append(asm, fmt.Sprintf("\tldy %s", c.offsetVarName)) + } else { + asm = append(asm, fmt.Sprintf("\tldy #%d", c.offsetValue)) + } + } else { + asm = append(asm, "\tldy #0") + } + + // Indexed indirect load - low byte + asm = append(asm, fmt.Sprintf("\tlda (%s),y", c.addrVarName)) + asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName)) + + // Increment Y and load high byte + asm = append(asm, "\tiny") + asm = append(asm, fmt.Sprintf("\tlda (%s),y", c.addrVarName)) + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName)) + return asm, nil + } + + // Case 2: Byte variable (zero-page indexed addressing) + if c.isAddrVar && c.addrVarKind == compiler.KindByte { + asm = append(asm, fmt.Sprintf("\tldx %s", c.addrVarName)) + asm = append(asm, "\tlda $00,x") + asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName)) + asm = append(asm, "\tinx") + asm = append(asm, "\tlda $00,x") + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName)) + return asm, nil + } + + // Case 3: Word variable (self-modifying code) + if c.isAddrVar && c.addrVarKind == compiler.KindWord { + // Check for immutable code pragma + if c.pragmaSet.GetPragma("_P_USE_IMMUTABLE_CODE") != "" { + return nil, fmt.Errorf("PEEKW: USE_IMMUTABLE_CODE pragma set but construct requires self-modifying code (consider using zero page word for address)") + } + + label1 := ctx.GeneralStack.Push() + label2 := ctx.GeneralStack.Push() + + // Setup both labels with base address + asm = append(asm, fmt.Sprintf("\tlda %s", c.addrVarName)) + asm = append(asm, fmt.Sprintf("\tsta %s+1", label1)) + asm = append(asm, fmt.Sprintf("\tsta %s+1", label2)) + asm = append(asm, fmt.Sprintf("\tlda %s+1", c.addrVarName)) + asm = append(asm, fmt.Sprintf("\tsta %s+2", label1)) + asm = append(asm, fmt.Sprintf("\tsta %s+2", label2)) + + // Read low byte + asm = append(asm, label1) + asm = append(asm, "\tlda $ffff") + asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName)) + + // Increment address in label2 + asm = append(asm, fmt.Sprintf("\tinc %s+1", label2)) + asm = append(asm, fmt.Sprintf("\tbne %s", label2)) + asm = append(asm, fmt.Sprintf("\tinc %s+2", label2)) + + // Read high byte + asm = append(asm, label2) + asm = append(asm, "\tlda $ffff") + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName)) + return asm, nil + } + + // Case 4: Expression/constant address (direct addressing) + asm = append(asm, fmt.Sprintf("\tlda %d", c.addrValue)) + asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName)) + asm = append(asm, fmt.Sprintf("\tlda %d", c.addrValue+1)) + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName)) + + return asm, nil +} + +// parseOffsetW extracts offset from syntax like "var[offset]" +// Returns (base, offset) where offset is empty if no brackets found +func parseOffsetW(param string) (base string, offset string) { + openBracket := strings.IndexByte(param, '[') + closeBracket := strings.IndexByte(param, ']') + + if openBracket == -1 || closeBracket == -1 { + return param, "" + } + + if closeBracket <= openBracket || closeBracket != len(param)-1 { + return param, "" + } + + base = param[:openBracket] + offset = param[openBracket+1 : closeBracket] + return base, offset +} diff --git a/internal/commands/point.go b/internal/commands/point.go new file mode 100644 index 0000000..a7f6f7c --- /dev/null +++ b/internal/commands/point.go @@ -0,0 +1,140 @@ +package commands + +import ( + "fmt" + "strings" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" + "c65gm/internal/utils" +) + +// PointerCommand handles POINTER statements +// Syntax: POINTER TO|-> +// Where target is: +// - Variable name +// - Label name +// - Numeric address/expression +type PointerCommand struct { + pointerVarName string + + targetVarName string + targetLabel string + targetAddress uint16 + + isVar bool + isLabel bool +} + +func (c *PointerCommand) WillHandle(line preproc.Line) bool { + params, err := utils.ParseParams(line.Text) + if err != nil || len(params) != 4 { + return false + } + return strings.ToUpper(params[0]) == "POINTER" +} + +func (c *PointerCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error { + // Clear state + c.pointerVarName = "" + c.targetVarName = "" + c.targetLabel = "" + c.targetAddress = 0 + c.isVar = false + c.isLabel = false + + params, err := utils.ParseParams(line.Text) + if err != nil { + return err + } + + if len(params) != 4 { + return fmt.Errorf("POINTER: expected 4 parameters, got %d", len(params)) + } + + scope := ctx.CurrentScope() + + // Validate pointer variable (param 2) + ptrVarName := params[1] + ptrSym := ctx.SymbolTable.Lookup(ptrVarName, scope) + if ptrSym == nil { + return fmt.Errorf("POINTER: unknown variable %q", ptrVarName) + } + if !ptrSym.IsWord() { + return fmt.Errorf("POINTER: variable %q is not a word", ptrVarName) + } + c.pointerVarName = ptrSym.FullName() + + // Validate separator (param 3) + sep := strings.ToUpper(params[2]) + if sep != "TO" && sep != "->" { + return fmt.Errorf("POINTER: parameter #3 must be 'TO' or '->', got %q", params[2]) + } + + // Parse target (param 4) + targetParam := params[3] + + 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 + } + + // Try ParseOperandParam - handles variables, constants, expressions + varName, _, value, isVar, err := compiler.ParseOperandParam( + targetParam, ctx.SymbolTable, scope, constLookup) + + if err != nil { + // Not a variable or expression -> treat as label + c.targetLabel = targetParam + c.isLabel = true + return nil + } + + // It's either a variable or numeric address + if isVar { + c.targetVarName = varName + c.isVar = true + return nil + } + + // Numeric address + c.targetAddress = value + c.isVar = false + c.isLabel = false + return nil +} + +func (c *PointerCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) { + var asm []string + + // Label reference + if c.isLabel { + asm = append(asm, fmt.Sprintf("\tldx #<%s", c.targetLabel)) + asm = append(asm, fmt.Sprintf("\tlda #>%s", c.targetLabel)) + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.pointerVarName)) + asm = append(asm, fmt.Sprintf("\tstx %s", c.pointerVarName)) + return asm, nil + } + + // Variable reference + if c.isVar { + asm = append(asm, fmt.Sprintf("\tldx #<%s", c.targetVarName)) + asm = append(asm, fmt.Sprintf("\tlda #>%s", c.targetVarName)) + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.pointerVarName)) + asm = append(asm, fmt.Sprintf("\tstx %s", c.pointerVarName)) + return asm, nil + } + + // Numeric address - create temp label + tempLabel := ctx.GeneralStack.Push() + asm = append(asm, fmt.Sprintf("%s = %d", tempLabel, c.targetAddress)) + asm = append(asm, fmt.Sprintf("\tldx #<%s", tempLabel)) + asm = append(asm, fmt.Sprintf("\tlda #>%s", tempLabel)) + asm = append(asm, fmt.Sprintf("\tsta %s+1", c.pointerVarName)) + asm = append(asm, fmt.Sprintf("\tstx %s", c.pointerVarName)) + + return asm, nil +} diff --git a/internal/commands/poke.go b/internal/commands/poke.go new file mode 100644 index 0000000..431ec41 --- /dev/null +++ b/internal/commands/poke.go @@ -0,0 +1,284 @@ +package commands + +import ( + "fmt" + "strings" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" + "c65gm/internal/utils" +) + +// PokeCommand handles POKE statements (writes a byte to memory) +// Syntax: POKE
[offset] WITH|, +// Where address is: +// - Byte variable (self-modifying code) +// - Word variable (self-modifying code or ZP pointer with offset) +// - Expression (direct addressing) +// +// Value is a byte literal or byte/word variable (uses low byte) +type PokeCommand struct { + addrVarName string + addrVarKind compiler.VarKind + addrValue uint16 + isAddrVar bool + isZPPointer bool + + offsetVarName string + offsetValue uint8 + hasOffset bool + isOffsetVar bool + + valueVarName string + valueVarKind compiler.VarKind + valueLiteral uint8 + isValueVar bool + + pragmaSet preproc.PragmaSet +} + +func (c *PokeCommand) WillHandle(line preproc.Line) bool { + params, err := utils.ParseParams(line.Text) + if err != nil || len(params) != 4 { + return false + } + return strings.ToUpper(params[0]) == "POKE" +} + +func (c *PokeCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error { + // Clear state + c.addrVarName = "" + c.addrValue = 0 + c.isAddrVar = false + c.isZPPointer = false + c.offsetVarName = "" + c.offsetValue = 0 + c.hasOffset = false + c.isOffsetVar = false + c.valueVarName = "" + c.valueLiteral = 0 + c.isValueVar = false + + // Store pragma set for Generate phase + c.pragmaSet = ctx.Pragma.GetPragmaSetByIndex(line.PragmaSetIndex) + + params, err := utils.ParseParams(line.Text) + if err != nil { + return err + } + + if len(params) != 4 { + return fmt.Errorf("POKE: expected 4 parameters, got %d", len(params)) + } + + scope := ctx.CurrentScope() + + // Validate separator (param 3) + sep := strings.ToUpper(params[2]) + if sep != "WITH" && sep != "," { + return fmt.Errorf("POKE: parameter #3 must be 'WITH' or ',', got %q", params[2]) + } + + // Parse address (param 2) - may have [offset] + addrParam := params[1] + baseAddr, offsetParam := parsePOKEOffset(addrParam) + + 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 + } + + // Try to parse base address + varName, varKind, value, isVar, err := compiler.ParseOperandParam( + baseAddr, ctx.SymbolTable, scope, constLookup) + + if err != nil { + return fmt.Errorf("POKE: invalid address: %w", err) + } + + if isVar { + // It's a variable + c.addrVarName = varName + c.addrVarKind = varKind + c.isAddrVar = true + + // Check if it's a ZP pointer + addrSym := ctx.SymbolTable.Lookup(baseAddr, scope) + if addrSym != nil { + c.isZPPointer = addrSym.IsZeroPagePointer() + } + } else { + // It's an expression/constant + c.addrValue = value + c.isAddrVar = false + } + + // Parse offset if present + if offsetParam != "" { + // Offset only allowed with ZP pointers + if !c.isZPPointer { + return fmt.Errorf("POKE: offset [%s] only allowed with zero-page word pointer", offsetParam) + } + + c.hasOffset = true + + // Try to parse offset as variable or expression + offsetVarName, _, offsetValue, isOffsetVar, err := compiler.ParseOperandParam( + offsetParam, ctx.SymbolTable, scope, constLookup) + + if err != nil { + return fmt.Errorf("POKE: invalid offset: %w", err) + } + + if isOffsetVar { + // Check it's a byte variable + offsetSym := ctx.SymbolTable.Lookup(offsetParam, scope) + if offsetSym != nil && !offsetSym.IsByte() { + return fmt.Errorf("POKE: offset variable %q must be byte type", offsetParam) + } + c.offsetVarName = offsetVarName + c.isOffsetVar = true + } else { + // Numeric offset - must fit in byte + if offsetValue > 255 { + return fmt.Errorf("POKE: offset value %d out of byte range", offsetValue) + } + c.offsetValue = uint8(offsetValue) + c.isOffsetVar = false + } + } + + // Parse value (param 4) + valueParam := params[3] + + // Try to parse as variable or expression + valVarName, valVarKind, valLiteral, isValVar, err := compiler.ParseOperandParam( + valueParam, ctx.SymbolTable, scope, constLookup) + + if err != nil { + return fmt.Errorf("POKE: invalid value: %w", err) + } + + if isValVar { + // It's a variable (byte or word - we use low byte) + c.valueVarName = valVarName + c.valueVarKind = valVarKind + c.isValueVar = true + } else { + // It's a literal - must fit in byte + if valLiteral > 255 { + return fmt.Errorf("POKE: value %d out of byte range (0-255)", valLiteral) + } + c.valueLiteral = uint8(valLiteral) + c.isValueVar = false + } + + return nil +} + +func (c *PokeCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) { + var asm []string + + // Check for immutable code pragma when using self-modifying code + if c.isAddrVar && !c.isZPPointer { + if c.pragmaSet.GetPragma("_P_USE_IMMUTABLE_CODE") != "" { + return nil, fmt.Errorf("POKE: USE_IMMUTABLE_CODE pragma set but construct requires self-modifying code (consider using zero page word for address)") + } + } + + // Case 1: ZP pointer with offset + if c.isZPPointer { + // Load offset into Y + if c.hasOffset { + if c.isOffsetVar { + asm = append(asm, fmt.Sprintf("\tldy %s", c.offsetVarName)) + } else { + asm = append(asm, fmt.Sprintf("\tldy #%d", c.offsetValue)) + } + } else { + asm = append(asm, "\tldy #0") + } + + // Load value + if c.isValueVar { + asm = append(asm, fmt.Sprintf("\tlda %s", c.valueVarName)) + } else { + asm = append(asm, fmt.Sprintf("\tlda #%d", c.valueLiteral)) + } + + // Indexed indirect store + asm = append(asm, fmt.Sprintf("\tsta (%s),y", c.addrVarName)) + return asm, nil + } + + // Case 2: Byte variable (self-modifying code) + if c.isAddrVar && c.addrVarKind == compiler.KindByte { + label := ctx.GeneralStack.Push() + asm = append(asm, fmt.Sprintf("\tlda %s", c.addrVarName)) + asm = append(asm, fmt.Sprintf("\tsta %s+1", label)) + + // Load value + if c.isValueVar { + asm = append(asm, fmt.Sprintf("\tlda %s", c.valueVarName)) + } else { + asm = append(asm, fmt.Sprintf("\tlda #%d", c.valueLiteral)) + } + + asm = append(asm, label) + asm = append(asm, "\tsta $ff") + return asm, nil + } + + // Case 3: Word variable (self-modifying code) + if c.isAddrVar && c.addrVarKind == compiler.KindWord { + label := ctx.GeneralStack.Push() + asm = append(asm, fmt.Sprintf("\tlda %s", c.addrVarName)) + asm = append(asm, fmt.Sprintf("\tsta %s+1", label)) + asm = append(asm, fmt.Sprintf("\tlda %s+1", c.addrVarName)) + asm = append(asm, fmt.Sprintf("\tsta %s+2", label)) + + // Load value + if c.isValueVar { + asm = append(asm, fmt.Sprintf("\tlda %s", c.valueVarName)) + } else { + asm = append(asm, fmt.Sprintf("\tlda #%d", c.valueLiteral)) + } + + asm = append(asm, label) + asm = append(asm, "\tsta $ffff") + return asm, nil + } + + // Case 4: Expression/constant address (direct addressing) + if c.isValueVar { + asm = append(asm, fmt.Sprintf("\tlda %s", c.valueVarName)) + asm = append(asm, fmt.Sprintf("\tsta %d", c.addrValue)) + } else { + asm = append(asm, fmt.Sprintf("\tlda #%d", c.valueLiteral)) + asm = append(asm, fmt.Sprintf("\tsta %d", c.addrValue)) + } + + return asm, nil +} + +// parsePOKEOffset extracts offset from syntax like "var[offset]" +// Returns (base, offset) where offset is empty if no brackets found +func parsePOKEOffset(param string) (base string, offset string) { + openBracket := strings.IndexByte(param, '[') + closeBracket := strings.IndexByte(param, ']') + + if openBracket == -1 || closeBracket == -1 { + return param, "" + } + + if closeBracket <= openBracket || closeBracket != len(param)-1 { + return param, "" + } + + base = param[:openBracket] + offset = param[openBracket+1 : closeBracket] + return base, offset +} diff --git a/internal/commands/pokew.go b/internal/commands/pokew.go new file mode 100644 index 0000000..f21a0b5 --- /dev/null +++ b/internal/commands/pokew.go @@ -0,0 +1,316 @@ +package commands + +import ( + "fmt" + "strings" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" + "c65gm/internal/utils" +) + +// PokeWCommand handles POKEW statements (writes a word/2 bytes to memory) +// Syntax: POKEW
[offset] WITH|, +// Where address is: +// - Byte variable (zero-page indexed addressing) +// - Word variable (self-modifying code or ZP pointer with offset) +// - Expression (direct addressing) +// +// Value must be a word variable or word literal (0-65535) +type PokeWCommand struct { + addrVarName string + addrVarKind compiler.VarKind + addrValue uint16 + isAddrVar bool + isZPPointer bool + + offsetVarName string + offsetValue uint8 + hasOffset bool + isOffsetVar bool + + valueVarName string + valueLiteral uint16 + isValueVar bool + + pragmaSet preproc.PragmaSet +} + +func (c *PokeWCommand) WillHandle(line preproc.Line) bool { + params, err := utils.ParseParams(line.Text) + if err != nil || len(params) != 4 { + return false + } + return strings.ToUpper(params[0]) == "POKEW" +} + +func (c *PokeWCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error { + // Clear state + c.addrVarName = "" + c.addrValue = 0 + c.isAddrVar = false + c.isZPPointer = false + c.offsetVarName = "" + c.offsetValue = 0 + c.hasOffset = false + c.isOffsetVar = false + c.valueVarName = "" + c.valueLiteral = 0 + c.isValueVar = false + + // Store pragma set for Generate phase + c.pragmaSet = ctx.Pragma.GetPragmaSetByIndex(line.PragmaSetIndex) + + params, err := utils.ParseParams(line.Text) + if err != nil { + return err + } + + if len(params) != 4 { + return fmt.Errorf("POKEW: expected 4 parameters, got %d", len(params)) + } + + scope := ctx.CurrentScope() + + // Validate separator (param 3) + sep := strings.ToUpper(params[2]) + if sep != "WITH" && sep != "," { + return fmt.Errorf("POKEW: parameter #3 must be 'WITH' or ',', got %q", params[2]) + } + + // Parse address (param 2) - may have [offset] + addrParam := params[1] + baseAddr, offsetParam := parsePOKEWOffset(addrParam) + + 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 + } + + // Try to parse base address + varName, varKind, value, isVar, err := compiler.ParseOperandParam( + baseAddr, ctx.SymbolTable, scope, constLookup) + + if err != nil { + return fmt.Errorf("POKEW: invalid address: %w", err) + } + + if isVar { + // It's a variable + c.addrVarName = varName + c.addrVarKind = varKind + c.isAddrVar = true + + // Check if it's a ZP pointer + addrSym := ctx.SymbolTable.Lookup(baseAddr, scope) + if addrSym != nil { + c.isZPPointer = addrSym.IsZeroPagePointer() + } + } else { + // It's an expression/constant + c.addrValue = value + c.isAddrVar = false + } + + // Parse offset if present + if offsetParam != "" { + // Offset only allowed with ZP pointers + if !c.isZPPointer { + return fmt.Errorf("POKEW: offset [%s] only allowed with zero-page word pointer", offsetParam) + } + + c.hasOffset = true + + // Try to parse offset as variable or expression + offsetVarName, _, offsetValue, isOffsetVar, err := compiler.ParseOperandParam( + offsetParam, ctx.SymbolTable, scope, constLookup) + + if err != nil { + return fmt.Errorf("POKEW: invalid offset: %w", err) + } + + if isOffsetVar { + // Check it's a byte variable + offsetSym := ctx.SymbolTable.Lookup(offsetParam, scope) + if offsetSym != nil && !offsetSym.IsByte() { + return fmt.Errorf("POKEW: offset variable %q must be byte type", offsetParam) + } + c.offsetVarName = offsetVarName + c.isOffsetVar = true + } else { + // Numeric offset - must fit in byte + if offsetValue > 255 { + return fmt.Errorf("POKEW: offset value %d out of byte range", offsetValue) + } + c.offsetValue = uint8(offsetValue) + c.isOffsetVar = false + } + } + + // Parse value (param 4) + valueParam := params[3] + + // Try to parse as variable or expression + valVarName, valVarKind, valLiteral, isValVar, err := compiler.ParseOperandParam( + valueParam, ctx.SymbolTable, scope, constLookup) + + if err != nil { + return fmt.Errorf("POKEW: invalid value: %w", err) + } + + if isValVar { + // It's a variable - must be word + if valVarKind == compiler.KindByte { + return fmt.Errorf("POKEW: cannot use byte variable %q (use word variable or literal)", valueParam) + } + c.valueVarName = valVarName + c.isValueVar = true + } else { + // It's a literal word value + c.valueLiteral = valLiteral + c.isValueVar = false + } + + return nil +} + +func (c *PokeWCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) { + var asm []string + + // Check for immutable code pragma when using self-modifying code + if c.isAddrVar && c.addrVarKind == compiler.KindWord && !c.isZPPointer { + if c.pragmaSet.GetPragma("_P_USE_IMMUTABLE_CODE") != "" { + return nil, fmt.Errorf("POKEW: USE_IMMUTABLE_CODE pragma set but construct requires self-modifying code (consider using zero page word for address)") + } + } + + // Case 1: ZP pointer with offset + if c.isZPPointer { + // Load offset into Y + if c.hasOffset { + if c.isOffsetVar { + asm = append(asm, fmt.Sprintf("\tldy %s", c.offsetVarName)) + } else { + asm = append(asm, fmt.Sprintf("\tldy #%d", c.offsetValue)) + } + } else { + asm = append(asm, "\tldy #0") + } + + // Store low byte + if c.isValueVar { + asm = append(asm, fmt.Sprintf("\tlda %s", c.valueVarName)) + } else { + asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8(c.valueLiteral&0xFF))) + } + asm = append(asm, fmt.Sprintf("\tsta (%s),y", c.addrVarName)) + + // Increment Y and store high byte + asm = append(asm, "\tiny") + if c.isValueVar { + asm = append(asm, fmt.Sprintf("\tlda %s+1", c.valueVarName)) + } else { + asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8((c.valueLiteral>>8)&0xFF))) + } + asm = append(asm, fmt.Sprintf("\tsta (%s),y", c.addrVarName)) + return asm, nil + } + + // Case 2: Byte variable (zero-page indexed addressing) + if c.isAddrVar && c.addrVarKind == compiler.KindByte { + asm = append(asm, fmt.Sprintf("\tldx %s", c.addrVarName)) + + // Store low byte + if c.isValueVar { + asm = append(asm, fmt.Sprintf("\tlda %s", c.valueVarName)) + } else { + asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8(c.valueLiteral&0xFF))) + } + asm = append(asm, "\tsta $00,x") + + // Increment X and store high byte + asm = append(asm, "\tinx") + if c.isValueVar { + asm = append(asm, fmt.Sprintf("\tlda %s+1", c.valueVarName)) + } else { + asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8((c.valueLiteral>>8)&0xFF))) + } + asm = append(asm, "\tsta $00,x") + return asm, nil + } + + // Case 3: Word variable (self-modifying code) + if c.isAddrVar && c.addrVarKind == compiler.KindWord { + label1 := ctx.GeneralStack.Push() + label2 := ctx.GeneralStack.Push() + + // Setup both labels with base address + asm = append(asm, fmt.Sprintf("\tlda %s", c.addrVarName)) + asm = append(asm, fmt.Sprintf("\tsta %s+1", label1)) + asm = append(asm, fmt.Sprintf("\tsta %s+1", label2)) + asm = append(asm, fmt.Sprintf("\tlda %s+1", c.addrVarName)) + asm = append(asm, fmt.Sprintf("\tsta %s+2", label1)) + asm = append(asm, fmt.Sprintf("\tsta %s+2", label2)) + + // Store low byte + if c.isValueVar { + asm = append(asm, fmt.Sprintf("\tlda %s", c.valueVarName)) + } else { + asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8(c.valueLiteral&0xFF))) + } + asm = append(asm, label1) + asm = append(asm, "\tsta $ffff") + + // Increment address in label2 + asm = append(asm, fmt.Sprintf("\tinc %s+1", label2)) + asm = append(asm, fmt.Sprintf("\tbne %s", label2)) + asm = append(asm, fmt.Sprintf("\tinc %s+2", label2)) + + // Store high byte + asm = append(asm, label2) + if c.isValueVar { + asm = append(asm, fmt.Sprintf("\tlda %s+1", c.valueVarName)) + } else { + asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8((c.valueLiteral>>8)&0xFF))) + } + asm = append(asm, "\tsta $ffff") + return asm, nil + } + + // Case 4: Expression/constant address (direct addressing) + if c.isValueVar { + asm = append(asm, fmt.Sprintf("\tlda %s", c.valueVarName)) + asm = append(asm, fmt.Sprintf("\tsta %d", c.addrValue)) + asm = append(asm, fmt.Sprintf("\tlda %s+1", c.valueVarName)) + asm = append(asm, fmt.Sprintf("\tsta %d", c.addrValue+1)) + } else { + asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8(c.valueLiteral&0xFF))) + asm = append(asm, fmt.Sprintf("\tsta %d", c.addrValue)) + asm = append(asm, fmt.Sprintf("\tlda #$%02x", uint8((c.valueLiteral>>8)&0xFF))) + asm = append(asm, fmt.Sprintf("\tsta %d", c.addrValue+1)) + } + + return asm, nil +} + +// parsePOKEWOffset extracts offset from syntax like "var[offset]" +// Returns (base, offset) where offset is empty if no brackets found +func parsePOKEWOffset(param string) (base string, offset string) { + openBracket := strings.IndexByte(param, '[') + closeBracket := strings.IndexByte(param, ']') + + if openBracket == -1 || closeBracket == -1 { + return param, "" + } + + if closeBracket <= openBracket || closeBracket != len(param)-1 { + return param, "" + } + + base = param[:openBracket] + offset = param[openBracket+1 : closeBracket] + return base, offset +} diff --git a/internal/commands/subend.go b/internal/commands/subend.go new file mode 100644 index 0000000..61b011c --- /dev/null +++ b/internal/commands/subend.go @@ -0,0 +1,44 @@ +package commands + +import ( + "fmt" + "strings" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" + "c65gm/internal/utils" +) + +// SubEndCommand handles SUBEND/EXIT statements +// Syntax: SUBEND (no parameters) +// +// EXIT (no parameters) +// +// Generates RTS (return from subroutine) +type SubEndCommand struct{} + +func (c *SubEndCommand) WillHandle(line preproc.Line) bool { + params, err := utils.ParseParams(line.Text) + if err != nil || len(params) == 0 { + return false + } + keyword := strings.ToUpper(params[0]) + return keyword == "SUBEND" || keyword == "EXIT" +} + +func (c *SubEndCommand) Interpret(line preproc.Line, _ *compiler.CompilerContext) error { + params, err := utils.ParseParams(line.Text) + if err != nil { + return err + } + + if len(params) != 1 { + return fmt.Errorf("SUBEND: no parameters allowed") + } + + return nil +} + +func (c *SubEndCommand) Generate(_ *compiler.CompilerContext) ([]string, error) { + return []string{"\trts"}, nil +} diff --git a/main.go b/main.go index d8474a9..bed664b 100644 --- a/main.go +++ b/main.go @@ -98,6 +98,13 @@ func registerCommands(comp *compiler.Compiler) { comp.Registry().Register(&commands.GotoCommand{}) comp.Registry().Register(&commands.LabelCommand{}) comp.Registry().Register(&commands.OriginCommand{}) + comp.Registry().Register(&commands.PointerCommand{}) + comp.Registry().Register(&commands.PeekCommand{}) + comp.Registry().Register(&commands.PeekWCommand{}) + comp.Registry().Register(&commands.PokeCommand{}) + comp.Registry().Register(&commands.PokeWCommand{}) + comp.Registry().Register(&commands.SubEndCommand{}) + comp.Registry().Register(&commands.GosubCommand{}) } func writeOutput(filename string, lines []string) error {