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 }