717 lines
20 KiB
Go
717 lines
20 KiB
Go
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 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} 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
|
|
}
|