c65gm/internal/compiler/funchandler.go

752 lines
21 KiB
Go

package compiler
import (
"fmt"
"strings"
"c65gm/internal/preproc"
"c65gm/internal/utils"
)
// ParamDirection represents parameter passing direction
type ParamDirection uint8
const (
DirIn ParamDirection = 1 << iota
DirOut
)
func (d ParamDirection) Has(flag ParamDirection) bool {
return d&flag != 0
}
// FuncParam represents a function parameter
type FuncParam struct {
Symbol *Symbol
Direction ParamDirection
}
// FuncDecl represents a function declaration
type FuncDecl struct {
Name string
Params []*FuncParam
}
// FunctionHandler manages function declarations and calls
type FunctionHandler struct {
functions []*FuncDecl
currentFuncs []string // stack of current function names (for nested scope)
symTable *SymbolTable
labelStack *LabelStack
constStrHandler *ConstantStringHandler
pragma *preproc.Pragma
}
// NewFunctionHandler creates a new function handler
func NewFunctionHandler(st *SymbolTable, ls *LabelStack, csh *ConstantStringHandler, pragma *preproc.Pragma) *FunctionHandler {
return &FunctionHandler{
functions: make([]*FuncDecl, 0),
currentFuncs: make([]string, 0),
symTable: st,
labelStack: ls,
constStrHandler: csh,
pragma: pragma,
}
}
// HandleFuncDecl parses and processes a FUNC declaration
// Syntax: FUNC name ( param1 param2 ... )
// Or: FUNC name (void function)
func (fh *FunctionHandler) HandleFuncDecl(line preproc.Line) ([]string, error) {
// Normalize parentheses and commas
text := fixIntuitiveFuncs(line.Text)
params, err := parseParams(text)
if err != nil {
return nil, fmt.Errorf("%s:%d: %w", line.Filename, line.LineNo, err)
}
if len(params) < 2 {
return nil, fmt.Errorf("%s:%d: FUNC: expected at least function name", line.Filename, line.LineNo)
}
if strings.ToUpper(params[0]) != "FUNC" {
return nil, fmt.Errorf("%s:%d: not a FUNC declaration", line.Filename, line.LineNo)
}
funcName := params[1]
// Check for redeclaration
if fh.FuncExists(funcName) {
return nil, fmt.Errorf("%s:%d: function %q already declared", line.Filename, line.LineNo, funcName)
}
// Push function name to current function stack early
// (so param declarations get correct scope)
fh.currentFuncs = append(fh.currentFuncs, funcName)
// Parse parameters
var funcParams []*FuncParam
if len(params) == 2 {
// Void function: FUNC name
// No parameters
} else if len(params) >= 5 {
// FUNC name ( param1 param2 )
if params[2] != "(" || params[len(params)-1] != ")" {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1]
return nil, fmt.Errorf("%s:%d: FUNC: expected parentheses around parameters", line.Filename, line.LineNo)
}
// Extract params between ( and ) - need to handle {BYTE x} specially
rawParamTokens := params[3 : len(params)-1]
paramSpecs, err := buildComplexParams(rawParamTokens)
if err != nil {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1]
return nil, fmt.Errorf("%s:%d: FUNC %s: %w", line.Filename, line.LineNo, funcName, err)
}
for _, spec := range paramSpecs {
direction, varName, isImplicit, implicitDecl, err := parseParamSpec(spec)
if err != nil {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1]
return nil, fmt.Errorf("%s:%d: FUNC %s: %w", line.Filename, line.LineNo, funcName, err)
}
if isImplicit {
// Parse and add implicit variable declaration
// Format: {BYTE varname} or {WORD varname}
if err := fh.parseImplicitDecl(implicitDecl, funcName); err != nil {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1]
return nil, fmt.Errorf("%s:%d: FUNC %s: implicit declaration: %w", line.Filename, line.LineNo, funcName, err)
}
}
// Look up variable in symbol table
sym := fh.symTable.Lookup(varName, []string{funcName})
if sym == nil {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1]
return nil, fmt.Errorf("%s:%d: FUNC %s: parameter %q not declared", line.Filename, line.LineNo, funcName, varName)
}
if sym.IsConst() {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1]
return nil, fmt.Errorf("%s:%d: FUNC %s: parameter %q cannot be a constant", line.Filename, line.LineNo, funcName, varName)
}
funcParams = append(funcParams, &FuncParam{
Symbol: sym,
Direction: direction,
})
}
} else {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1]
return nil, fmt.Errorf("%s:%d: FUNC: invalid syntax", line.Filename, line.LineNo)
}
// Store function declaration
fh.functions = append(fh.functions, &FuncDecl{
Name: funcName,
Params: funcParams,
})
// Generate assembler label
return []string{funcName}, nil
}
// buildComplexParams handles parameter lists that may contain {BYTE x} style declarations
// Tokens like {BYTE x} are spread across multiple tokens and need to be reassembled
func buildComplexParams(tokens []string) ([]string, error) {
var result []string
var current string
inBraces := false
for _, token := range tokens {
hasStart := strings.Contains(token, "{")
hasEnd := strings.Contains(token, "}")
if !inBraces {
// Not currently in braces
if hasEnd && !hasStart {
return nil, fmt.Errorf("unexpected } without matching {")
}
if hasStart {
// Starting a brace block
inBraces = true
current = token
// Check if it also ends on same token
if hasEnd {
result = append(result, current)
current = ""
inBraces = false
}
} else {
// Regular param
result = append(result, token)
}
} else {
// Currently accumulating in braces
if hasStart {
return nil, fmt.Errorf("unexpected { while already in braces")
}
current += " " + token
if hasEnd {
result = append(result, current)
current = ""
inBraces = false
}
}
}
if inBraces {
return nil, fmt.Errorf("unclosed { in parameter list")
}
return result, nil
}
// HandleFuncCall generates code for a function call
// Syntax: CALL funcname ( arg1 arg2 ... )
// Or: funcname ( arg1 arg2 ... )
func (fh *FunctionHandler) HandleFuncCall(line preproc.Line) ([]string, error) {
// Normalize parentheses and commas
text := fixIntuitiveFuncs(line.Text)
params, err := parseParams(text)
if err != nil {
return nil, fmt.Errorf("%s:%d: %w", line.Filename, line.LineNo, err)
}
if len(params) < 1 {
return nil, fmt.Errorf("%s:%d: CALL: empty line", line.Filename, line.LineNo)
}
// Check if starts with CALL keyword
startsWithCall := strings.ToUpper(params[0]) == "CALL"
funcNameIdx := 0
if startsWithCall {
if len(params) < 2 {
return nil, fmt.Errorf("%s:%d: CALL: expected function name", line.Filename, line.LineNo)
}
funcNameIdx = 1
}
funcName := params[funcNameIdx]
// Check if function exists
funcDecl := fh.findFunc(funcName)
if funcDecl == nil {
return nil, fmt.Errorf("%s:%d: function %q not declared", line.Filename, line.LineNo, funcName)
}
// Parse call arguments
var callArgs []string
if len(params) == funcNameIdx+1 {
// No arguments: funcname or CALL funcname
callArgs = []string{}
} else if len(params) == funcNameIdx+3 && params[funcNameIdx+1] == "(" && params[funcNameIdx+2] == ")" {
// Empty parens: funcname() or CALL funcname()
callArgs = []string{}
} else if len(params) >= funcNameIdx+4 {
// funcname ( arg1 arg2 ) or CALL funcname ( arg1 arg2 )
if params[funcNameIdx+1] != "(" || params[len(params)-1] != ")" {
return nil, fmt.Errorf("%s:%d: CALL %s: expected parentheses around arguments", line.Filename, line.LineNo, funcName)
}
callArgs = params[funcNameIdx+2 : len(params)-1]
} else {
return nil, fmt.Errorf("%s:%d: CALL %s: invalid syntax", line.Filename, line.LineNo, funcName)
}
// Check argument count matches
if len(callArgs) != len(funcDecl.Params) {
return nil, fmt.Errorf("%s:%d: CALL %s: expected %d arguments, got %d",
line.Filename, line.LineNo, funcName, len(funcDecl.Params), len(callArgs))
}
// Get pragma set for this line
pragmaSet := fh.pragma.GetPragmaSetByIndex(line.PragmaSetIndex)
var asmLines []string
var inAssigns []string
var outAssigns []string
// Process each argument
for i, arg := range callArgs {
param := funcDecl.Params[i]
// Handle special cases first
if strings.HasPrefix(arg, "@") {
// Label reference: @labelname
if err := fh.processLabelArg(arg, param, funcName, line, &inAssigns); err != nil {
return nil, err
}
continue
}
if strings.HasPrefix(arg, "\"") && strings.HasSuffix(arg, "\"") {
// String constant
if err := fh.processStringArg(arg, param, funcName, line, pragmaSet, &inAssigns); err != nil {
return nil, err
}
continue
}
// Use ParseOperandParam for variables, constants, and expressions
constLookup := func(name string) (int64, bool) {
if sym := fh.symTable.Lookup(name, fh.currentFuncs); sym != nil && sym.IsConst() {
return int64(sym.Value), true
}
return 0, false
}
_, _, value, isVar, err := ParseOperandParam(
arg, fh.symTable, fh.currentFuncs, constLookup)
if err != nil {
return nil, fmt.Errorf("%s:%d: CALL %s arg %d (%q): %w",
line.Filename, line.LineNo, funcName, i+1, arg, err)
}
// Out/IO parameters must be writable variables
if param.Direction.Has(DirOut) && !isVar {
return nil, fmt.Errorf("%s:%d: CALL %s: cannot pass constant/expression to out/io parameter %q",
line.Filename, line.LineNo, funcName, param.Symbol.Name)
}
if isVar {
// Variable - get full symbol for type checking
sym := fh.symTable.Lookup(arg, fh.currentFuncs)
if sym == nil {
return nil, fmt.Errorf("%s:%d: CALL %s: internal error - variable %q not found",
line.Filename, line.LineNo, funcName, arg)
}
if err := fh.processVarArg(sym, param, funcName, line, &inAssigns, &outAssigns); err != nil {
return nil, err
}
} else {
// Constant or expression result
if err := fh.processConstValue(value, param, funcName, line, &inAssigns); err != nil {
return nil, err
}
}
}
// Generate final assembly
asmLines = append(asmLines, inAssigns...)
asmLines = append(asmLines, fmt.Sprintf(" jsr %s", funcName))
asmLines = append(asmLines, outAssigns...)
return asmLines, nil
}
// processLabelArg handles @label arguments
func (fh *FunctionHandler) processLabelArg(arg string, param *FuncParam, funcName string, line preproc.Line, inAssigns *[]string) error {
labelName := arg[1:] // strip @
if param.Symbol.IsByte() {
return fmt.Errorf("%s:%d: CALL %s: cannot pass label to byte parameter", line.Filename, line.LineNo, funcName)
}
if param.Direction.Has(DirOut) {
return fmt.Errorf("%s:%d: CALL %s: cannot pass label to out/io parameter", line.Filename, line.LineNo, funcName)
}
*inAssigns = append(*inAssigns,
fmt.Sprintf(" lda #<%s", labelName),
fmt.Sprintf(" sta %s", param.Symbol.FullName()),
fmt.Sprintf(" lda #>%s", labelName),
fmt.Sprintf(" sta %s+1", param.Symbol.FullName()),
)
return nil
}
// processStringArg handles "string" arguments
func (fh *FunctionHandler) processStringArg(arg string, param *FuncParam, funcName string, line preproc.Line, pragmaSet preproc.PragmaSet, inAssigns *[]string) error {
if param.Symbol.IsByte() {
return fmt.Errorf("%s:%d: CALL %s: cannot pass string to byte parameter", line.Filename, line.LineNo, funcName)
}
if param.Direction.Has(DirOut) {
return fmt.Errorf("%s:%d: CALL %s: cannot pass string to out/io parameter", line.Filename, line.LineNo, funcName)
}
// Generate label for string constant
labelName := fh.labelStack.Push()
fh.constStrHandler.AddConstStr(labelName, arg, true, pragmaSet)
*inAssigns = append(*inAssigns,
fmt.Sprintf(" lda #<%s", labelName),
fmt.Sprintf(" sta %s", param.Symbol.FullName()),
fmt.Sprintf(" lda #>%s", labelName),
fmt.Sprintf(" sta %s+1", param.Symbol.FullName()),
)
return nil
}
// processVarArg handles variable arguments
func (fh *FunctionHandler) processVarArg(sym *Symbol, param *FuncParam, funcName string, line preproc.Line, inAssigns, outAssigns *[]string) error {
// Check type compatibility
if (sym.IsByte() && param.Symbol.IsWord()) || (sym.IsWord() && param.Symbol.IsByte()) {
return fmt.Errorf("%s:%d: CALL %s: type mismatch for parameter %s", line.Filename, line.LineNo, funcName, param.Symbol.Name)
}
if sym.IsConst() {
return fmt.Errorf("%s:%d: CALL %s: cannot pass constant to function", line.Filename, line.LineNo, funcName)
}
// Generate IN assignments
if param.Direction.Has(DirIn) {
*inAssigns = append(*inAssigns,
fmt.Sprintf(" lda %s", sym.FullName()),
fmt.Sprintf(" sta %s", param.Symbol.FullName()),
)
if sym.IsWord() {
*inAssigns = append(*inAssigns,
fmt.Sprintf(" lda %s+1", sym.FullName()),
fmt.Sprintf(" sta %s+1", param.Symbol.FullName()),
)
}
}
// Generate OUT assignments
if param.Direction.Has(DirOut) {
*outAssigns = append(*outAssigns,
fmt.Sprintf(" lda %s", param.Symbol.FullName()),
fmt.Sprintf(" sta %s", sym.FullName()),
)
if sym.IsWord() {
*outAssigns = append(*outAssigns,
fmt.Sprintf(" lda %s+1", param.Symbol.FullName()),
fmt.Sprintf(" sta %s+1", sym.FullName()),
)
}
}
return nil
}
// processConstArg handles numeric constant arguments
func (fh *FunctionHandler) processConstArg(arg string, param *FuncParam, funcName string, line preproc.Line, inAssigns *[]string) error {
if param.Direction.Has(DirOut) {
return fmt.Errorf("%s:%d: CALL %s: cannot pass constant to out/io parameter", line.Filename, line.LineNo, funcName)
}
// Parse numeric value (supports decimal and hex with $ prefix)
var value int64
var err error
if strings.HasPrefix(arg, "$") {
_, err = fmt.Sscanf(arg[1:], "%x", &value)
} else {
_, err = fmt.Sscanf(arg, "%d", &value)
}
if err != nil {
return fmt.Errorf("%s:%d: CALL %s: invalid numeric constant %q", line.Filename, line.LineNo, funcName, arg)
}
if param.Symbol.IsByte() && (value < 0 || value > 255) {
return fmt.Errorf("%s:%d: CALL %s: constant %d out of byte range", line.Filename, line.LineNo, funcName, value)
}
if value < 0 || value > 65535 {
return fmt.Errorf("%s:%d: CALL %s: constant %d out of word range", line.Filename, line.LineNo, funcName, value)
}
lowByte := uint8(value & 0xFF)
highByte := uint8((value >> 8) & 0xFF)
*inAssigns = append(*inAssigns,
fmt.Sprintf(" lda #%d", lowByte),
fmt.Sprintf(" sta %s", param.Symbol.FullName()),
)
if param.Symbol.IsWord() {
// Optimize: only reload A if high byte differs
if highByte != lowByte {
*inAssigns = append(*inAssigns, fmt.Sprintf(" lda #%d", highByte))
}
*inAssigns = append(*inAssigns, fmt.Sprintf(" sta %s+1", param.Symbol.FullName()))
}
return nil
}
// processConstValue handles pre-computed constant/expression values
// Used after ParseOperandParam has already validated and computed the value
func (fh *FunctionHandler) processConstValue(value uint16, param *FuncParam, funcName string, line preproc.Line, inAssigns *[]string) error {
// Verify value fits in parameter type
if param.Symbol.IsByte() && value > 255 {
return fmt.Errorf("%s:%d: CALL %s: value %d out of byte range for parameter %q",
line.Filename, line.LineNo, funcName, value, param.Symbol.Name)
}
lowByte := uint8(value & 0xFF)
highByte := uint8((value >> 8) & 0xFF)
*inAssigns = append(*inAssigns,
fmt.Sprintf(" lda #%d", lowByte),
fmt.Sprintf(" sta %s", param.Symbol.FullName()),
)
if param.Symbol.IsWord() {
// Optimize: only reload A if high byte differs
if highByte != lowByte {
*inAssigns = append(*inAssigns, fmt.Sprintf(" lda #%d", highByte))
}
*inAssigns = append(*inAssigns, fmt.Sprintf(" sta %s+1", param.Symbol.FullName()))
}
return nil
}
// parseImplicitDecl parses {BYTE varname} or {WORD varname} or {BYTE varname @ address} and adds to symbol table
func (fh *FunctionHandler) parseImplicitDecl(decl string, funcName string) error {
parts := strings.Fields(decl)
if len(parts) != 2 && len(parts) != 4 {
return fmt.Errorf("implicit declaration must be 'TYPE name' or 'TYPE name @ addr', got: %q", decl)
}
typeStr := strings.ToUpper(parts[0])
varName := parts[1]
var kind VarKind
switch typeStr {
case "BYTE":
kind = KindByte
case "WORD":
kind = KindWord
default:
return fmt.Errorf("implicit declaration type must be BYTE or WORD, got: %s", typeStr)
}
if len(parts) == 2 {
// Simple: BYTE name or WORD name
return fh.symTable.AddVar(varName, funcName, kind, 0)
}
// Extended: BYTE name @ address or WORD name @ address
operator := parts[2]
addrStr := parts[3]
if operator != "@" {
return fmt.Errorf("expected '@' operator, got: %q", operator)
}
// Create constant lookup function for address evaluation
constLookup := func(name string) (int64, bool) {
sym := fh.symTable.Lookup(name, []string{funcName})
if sym != nil && sym.IsConst() {
return int64(sym.Value), true
}
return 0, false
}
// Parse address (supports $hex and decimal) using EvaluateExpression
addr, err := utils.EvaluateExpression(addrStr, constLookup)
if err != nil {
return fmt.Errorf("invalid address %q: %w", addrStr, err)
}
if addr < 0 || addr > 0xFFFF {
return fmt.Errorf("absolute address $%X out of range", addr)
}
return fh.symTable.AddAbsolute(varName, funcName, kind, uint16(addr))
}
// EndFunction pops all functions from the stack (called by FEND)
func (fh *FunctionHandler) EndFunction() {
fh.currentFuncs = fh.currentFuncs[:0]
}
// FuncExists checks if a function is declared
func (fh *FunctionHandler) FuncExists(name string) bool {
return fh.findFunc(name) != nil
}
// CurrentFunction returns the current function name (or empty if global scope)
func (fh *FunctionHandler) CurrentFunction() string {
if len(fh.currentFuncs) == 0 {
return ""
}
return fh.currentFuncs[len(fh.currentFuncs)-1]
}
// findFunc finds a function declaration by name
func (fh *FunctionHandler) findFunc(name string) *FuncDecl {
for _, f := range fh.functions {
if f.Name == name {
return f
}
}
return nil
}
// fixIntuitiveFuncs normalizes function syntax
// Separates '(' and ')' into own tokens, removes commas
// Example: "func(a,b)" -> "func ( a b )"
func fixIntuitiveFuncs(s string) string {
var result strings.Builder
inString := false
for i := 0; i < len(s); i++ {
ch := s[i]
if ch == '"' {
inString = !inString
result.WriteByte(ch)
continue
}
if !inString {
if ch == '(' || ch == ')' {
result.WriteByte(' ')
result.WriteByte(ch)
result.WriteByte(' ')
} else if ch == ',' {
result.WriteByte(' ')
} else {
result.WriteByte(ch)
}
} else {
result.WriteByte(ch)
}
}
return normalizeSpaces(result.String())
}
// normalizeSpaces reduces multiple spaces to single space
func normalizeSpaces(s string) string {
s = strings.TrimSpace(s)
var result strings.Builder
inString := false
lastWasSpace := false
for i := 0; i < len(s); i++ {
ch := s[i]
if ch == '"' {
inString = !inString
result.WriteByte(ch)
lastWasSpace = false
continue
}
if !inString {
if ch == ' ' || ch == '\t' {
if !lastWasSpace {
result.WriteByte(' ')
lastWasSpace = true
}
} else {
result.WriteByte(ch)
lastWasSpace = false
}
} else {
result.WriteByte(ch)
lastWasSpace = false
}
}
return result.String()
}
// parseParams splits line into space-separated parameters, respecting quoted strings
func parseParams(s string) ([]string, error) {
s = strings.TrimSpace(s)
if s == "" {
return []string{}, nil
}
var params []string
var current strings.Builder
inString := false
for i := 0; i < len(s); i++ {
ch := s[i]
if ch == '"' {
inString = !inString
current.WriteByte(ch)
continue
}
if !inString && (ch == ' ' || ch == '\t') {
if current.Len() > 0 {
params = append(params, current.String())
current.Reset()
}
} else {
current.WriteByte(ch)
}
}
if current.Len() > 0 {
params = append(params, current.String())
}
if inString {
return nil, fmt.Errorf("unterminated string in line")
}
return params, nil
}
// parseParamSpec parses a parameter specification
// Returns: direction, varName, isImplicit, implicitDecl, error
// Examples:
//
// "varname" -> DirIn, "varname", false, "", nil
// "in:varname" -> DirIn, "varname", false, "", nil
// "out:varname" -> DirOut, "varname", false, "", nil
// "io:varname" -> DirIn|DirOut, "varname", false, "", nil
// "{BYTE temp}" -> DirIn, "temp", true, "BYTE temp", nil
// "out:{WORD result}" -> DirOut, "result", true, "WORD result", nil
func parseParamSpec(spec string) (ParamDirection, string, bool, string, error) {
direction := DirIn // default
varName := spec
isImplicit := false
implicitDecl := ""
// Check for direction prefix
if strings.Contains(spec, ":") {
parts := strings.SplitN(spec, ":", 2)
if len(parts) != 2 {
return 0, "", false, "", fmt.Errorf("invalid parameter spec: %q", spec)
}
dirStr := strings.ToLower(parts[0])
varName = parts[1]
switch dirStr {
case "in":
direction = DirIn
case "out":
direction = DirOut
case "io":
direction = DirIn | DirOut
default:
return 0, "", false, "", fmt.Errorf("invalid parameter direction: %q", dirStr)
}
}
// Check for implicit declaration {TYPE name}
if strings.HasPrefix(varName, "{") && strings.HasSuffix(varName, "}") {
isImplicit = true
implicitDecl = varName[1 : len(varName)-1] // strip { }
// Extract variable name from implicit declaration
parts := strings.Fields(implicitDecl)
if len(parts) < 2 {
return 0, "", false, "", fmt.Errorf("invalid implicit declaration: %q", varName)
}
varName = parts[1]
}
return direction, varName, isImplicit, implicitDecl, nil
}