package compiler import ( "fmt" "strings" "c65gm/internal/preproc" ) // 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+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 different argument types if strings.HasPrefix(arg, "@") { // Label reference: @labelname if err := fh.processLabelArg(arg, param, funcName, line, &inAssigns); err != nil { return nil, err } } else if strings.HasPrefix(arg, "\"") && strings.HasSuffix(arg, "\"") { // String constant if err := fh.processStringArg(arg, param, funcName, line, pragmaSet, &inAssigns); err != nil { return nil, err } } else if sym := fh.symTable.Lookup(arg, fh.currentFuncs); sym != nil { // Variable reference if err := fh.processVarArg(sym, param, funcName, line, &inAssigns, &outAssigns); err != nil { return nil, err } } else { // Numeric constant if err := fh.processConstArg(arg, 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 } // parseImplicitDecl parses {BYTE varname} or {WORD varname} and adds to symbol table func (fh *FunctionHandler) parseImplicitDecl(decl string, funcName string) error { parts := strings.Fields(decl) if len(parts) != 2 { return fmt.Errorf("implicit declaration must be 'BYTE name' or 'WORD name', 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) } // Add variable to symbol table with function scope return fh.symTable.AddVar(varName, funcName, kind, 0) } // 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 }