1009 lines
29 KiB
Go
1009 lines
29 KiB
Go
package compiler
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"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
|
|
|
|
// Absolute address tracking for overlap detection
|
|
absoluteAddrs map[string]map[uint16]bool // funcName -> set of absolute addresses used
|
|
callGraph map[string][]string // funcName -> list of functions it calls
|
|
}
|
|
|
|
// 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,
|
|
absoluteAddrs: make(map[string]map[uint16]bool),
|
|
callGraph: make(map[string][]string),
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
})
|
|
|
|
// Record absolute addresses used by this function
|
|
fh.recordAbsoluteAddresses(funcName, funcParams)
|
|
|
|
// Generate assembler label
|
|
return []string{funcName}, nil
|
|
}
|
|
|
|
// recordAbsoluteAddresses stores which absolute addresses a function uses
|
|
func (fh *FunctionHandler) recordAbsoluteAddresses(funcName string, params []*FuncParam) {
|
|
addrSet := make(map[uint16]bool)
|
|
|
|
for _, param := range params {
|
|
if param.Symbol.IsAbsolute() {
|
|
addr := param.Symbol.AbsAddr
|
|
// Mark address as used
|
|
addrSet[addr] = true
|
|
// If it's a word, also mark the next byte
|
|
if param.Symbol.IsWord() {
|
|
addrSet[addr+1] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Only store if function uses absolute addresses
|
|
if len(addrSet) > 0 {
|
|
fh.absoluteAddrs[funcName] = addrSet
|
|
}
|
|
}
|
|
|
|
// RecordAbsoluteVar is called by SymbolTable when an absolute variable is added to a function scope
|
|
func (fh *FunctionHandler) RecordAbsoluteVar(funcScope string, addr uint16, isWord bool) {
|
|
if funcScope == "" {
|
|
return // global variable, skip
|
|
}
|
|
|
|
if fh.absoluteAddrs[funcScope] == nil {
|
|
fh.absoluteAddrs[funcScope] = make(map[uint16]bool)
|
|
}
|
|
|
|
fh.absoluteAddrs[funcScope][addr] = true
|
|
if isWord {
|
|
fh.absoluteAddrs[funcScope][addr+1] = true
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
}
|
|
|
|
// Record the call in the call graph
|
|
fh.recordCall(funcName)
|
|
|
|
// Generate final assembly
|
|
asmLines = append(asmLines, inAssigns...)
|
|
asmLines = append(asmLines, fmt.Sprintf(" jsr %s", funcName))
|
|
asmLines = append(asmLines, outAssigns...)
|
|
|
|
return asmLines, nil
|
|
}
|
|
|
|
// recordCall tracks which function calls which
|
|
func (fh *FunctionHandler) recordCall(calledFunc string) {
|
|
// Get current function (caller)
|
|
if len(fh.currentFuncs) == 0 {
|
|
// Call from global scope - not inside a function
|
|
return
|
|
}
|
|
|
|
caller := fh.currentFuncs[len(fh.currentFuncs)-1]
|
|
|
|
// Add to call graph
|
|
if fh.callGraph[caller] == nil {
|
|
fh.callGraph[caller] = []string{}
|
|
}
|
|
|
|
// Avoid duplicates (though they're harmless for our analysis)
|
|
for _, existing := range fh.callGraph[caller] {
|
|
if existing == calledFunc {
|
|
return
|
|
}
|
|
}
|
|
|
|
fh.callGraph[caller] = append(fh.callGraph[caller], calledFunc)
|
|
}
|
|
|
|
// 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()
|
|
actualLabel := fh.constStrHandler.AddConstStr(labelName, arg, true, pragmaSet)
|
|
|
|
*inAssigns = append(*inAssigns,
|
|
fmt.Sprintf(" lda #<%s", actualLabel),
|
|
fmt.Sprintf(" sta %s", param.Symbol.FullName()),
|
|
fmt.Sprintf(" lda #>%s", actualLabel),
|
|
fmt.Sprintf(" sta %s+1", param.Symbol.FullName()),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (fh *FunctionHandler) processVarArg(sym *Symbol, param *FuncParam, funcName string, line preproc.Line, inAssigns, outAssigns *[]string) error {
|
|
if sym.IsConst() {
|
|
return fmt.Errorf("%s:%d: CALL %s: cannot pass constant to function", line.Filename, line.LineNo, funcName)
|
|
}
|
|
|
|
// Generate IN assignments (sym -> param)
|
|
if param.Direction.Has(DirIn) {
|
|
*inAssigns = append(*inAssigns,
|
|
fmt.Sprintf(" lda %s", sym.FullName()),
|
|
fmt.Sprintf(" sta %s", param.Symbol.FullName()),
|
|
)
|
|
if param.Symbol.IsWord() {
|
|
if sym.IsWord() {
|
|
*inAssigns = append(*inAssigns,
|
|
fmt.Sprintf(" lda %s+1", sym.FullName()),
|
|
fmt.Sprintf(" sta %s+1", param.Symbol.FullName()),
|
|
)
|
|
} else {
|
|
// byte -> word: zero extend
|
|
*inAssigns = append(*inAssigns,
|
|
" lda #0",
|
|
fmt.Sprintf(" sta %s+1", param.Symbol.FullName()),
|
|
)
|
|
}
|
|
} else if sym.IsWord() {
|
|
// word -> byte: lossy
|
|
fmt.Printf("%s:%d: warning: CALL %s: truncating word '%s' to byte parameter '%s'\n",
|
|
line.Filename, line.LineNo, funcName, sym.Name, param.Symbol.Name)
|
|
}
|
|
}
|
|
|
|
// Generate OUT assignments (param -> sym)
|
|
if param.Direction.Has(DirOut) {
|
|
*outAssigns = append(*outAssigns,
|
|
fmt.Sprintf(" lda %s", param.Symbol.FullName()),
|
|
fmt.Sprintf(" sta %s", sym.FullName()),
|
|
)
|
|
if sym.IsWord() {
|
|
if param.Symbol.IsWord() {
|
|
*outAssigns = append(*outAssigns,
|
|
fmt.Sprintf(" lda %s+1", param.Symbol.FullName()),
|
|
fmt.Sprintf(" sta %s+1", sym.FullName()),
|
|
)
|
|
} else {
|
|
// byte -> word: zero extend
|
|
*outAssigns = append(*outAssigns,
|
|
" lda #0",
|
|
fmt.Sprintf(" sta %s+1", sym.FullName()),
|
|
)
|
|
}
|
|
} else if param.Symbol.IsWord() {
|
|
// word -> byte: lossy
|
|
fmt.Printf("%s:%d: warning: CALL %s: truncating word parameter '%s' to byte '%s'\n",
|
|
line.Filename, line.LineNo, funcName, param.Symbol.Name, sym.Name)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// AbsoluteOverlap represents a detected overlap in absolute addresses
|
|
type AbsoluteOverlap struct {
|
|
Func1 string // First function using the address
|
|
Func2 string // Second function using the address
|
|
Address uint16 // Overlapping address
|
|
CallChain []string // Call chain from Func1 to Func2
|
|
}
|
|
|
|
// AnalyzeAbsoluteOverlaps checks for overlapping absolute addresses in call chains
|
|
func (fh *FunctionHandler) AnalyzeAbsoluteOverlaps() []AbsoluteOverlap {
|
|
// Use map to deduplicate: key is "func1:func2:addr", value is overlap with shortest chain
|
|
seen := make(map[string]*AbsoluteOverlap)
|
|
|
|
// For each function that uses absolute addresses
|
|
for funcName, addrSet := range fh.absoluteAddrs {
|
|
// Get all functions transitively called by this function
|
|
transitiveAddrs := fh.getTransitiveAddresses(funcName, make(map[string]bool))
|
|
|
|
// Check for overlaps
|
|
for addr := range addrSet {
|
|
for otherFunc, otherAddrs := range transitiveAddrs {
|
|
if otherAddrs[addr] {
|
|
// Found overlap!
|
|
callChain := fh.findCallChain(funcName, otherFunc)
|
|
|
|
// Create unique key for this conflict
|
|
key := fmt.Sprintf("%s:%s:%04x", funcName, otherFunc, addr)
|
|
|
|
// Keep only if this is the first occurrence or has shorter chain
|
|
if existing, exists := seen[key]; !exists || len(callChain) < len(existing.CallChain) {
|
|
seen[key] = &AbsoluteOverlap{
|
|
Func1: funcName,
|
|
Func2: otherFunc,
|
|
Address: addr,
|
|
CallChain: callChain,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert map to slice
|
|
var overlaps []AbsoluteOverlap
|
|
for _, overlap := range seen {
|
|
overlaps = append(overlaps, *overlap)
|
|
}
|
|
|
|
return overlaps
|
|
}
|
|
|
|
// ReportAbsoluteOverlaps analyzes and reports overlapping absolute addresses to stderr
|
|
func (fh *FunctionHandler) ReportAbsoluteOverlaps() {
|
|
overlaps := fh.AnalyzeAbsoluteOverlaps()
|
|
|
|
if len(overlaps) == 0 {
|
|
return
|
|
}
|
|
|
|
// Group overlaps by address
|
|
byAddress := make(map[uint16]map[string]bool)
|
|
for _, overlap := range overlaps {
|
|
if byAddress[overlap.Address] == nil {
|
|
byAddress[overlap.Address] = make(map[string]bool)
|
|
}
|
|
byAddress[overlap.Address][overlap.Func1] = true
|
|
byAddress[overlap.Address][overlap.Func2] = true
|
|
}
|
|
|
|
// Extract and sort addresses
|
|
addresses := make([]uint16, 0, len(byAddress))
|
|
for addr := range byAddress {
|
|
addresses = append(addresses, addr)
|
|
}
|
|
sort.Slice(addresses, func(i, j int) bool {
|
|
return addresses[i] < addresses[j]
|
|
})
|
|
|
|
_, _ = fmt.Fprintf(os.Stderr, "\nWarning: Detected overlapping absolute addresses in function call chains:\n\n")
|
|
|
|
// Report each address with its conflicting functions (in sorted order)
|
|
for _, addr := range addresses {
|
|
funcs := byAddress[addr]
|
|
funcList := make([]string, 0, len(funcs))
|
|
for f := range funcs {
|
|
funcList = append(funcList, f)
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(os.Stderr, " Address $%04X used by: %s\n", addr, strings.Join(funcList, ", "))
|
|
_, _ = fmt.Fprintf(os.Stderr, " %d function(s) share this address in overlapping call chains\n\n", len(funcList))
|
|
}
|
|
|
|
_, _ = fmt.Fprintf(os.Stderr, " These conflicts may cause data corruption when functions are active simultaneously.\n\n")
|
|
}
|
|
|
|
// getTransitiveAddresses returns all addresses used by transitively called functions
|
|
// Returns map: funcName -> set of addresses used by that function
|
|
func (fh *FunctionHandler) getTransitiveAddresses(funcName string, visited map[string]bool) map[string]map[uint16]bool {
|
|
result := make(map[string]map[uint16]bool)
|
|
|
|
// Avoid infinite recursion
|
|
if visited[funcName] {
|
|
return result
|
|
}
|
|
visited[funcName] = true
|
|
|
|
// Get functions directly called by this function
|
|
for _, calledFunc := range fh.callGraph[funcName] {
|
|
// Add addresses used by the called function
|
|
if addrs, exists := fh.absoluteAddrs[calledFunc]; exists {
|
|
result[calledFunc] = addrs
|
|
}
|
|
|
|
// Recursively get addresses from transitively called functions
|
|
transitiveAddrs := fh.getTransitiveAddresses(calledFunc, visited)
|
|
for f, addrs := range transitiveAddrs {
|
|
result[f] = addrs
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// findCallChain finds the call path from funcA to funcB
|
|
func (fh *FunctionHandler) findCallChain(from, to string) []string {
|
|
// BFS to find shortest path
|
|
type node struct {
|
|
funcName string
|
|
path []string
|
|
}
|
|
|
|
queue := []node{{funcName: from, path: []string{from}}}
|
|
visited := make(map[string]bool)
|
|
|
|
for len(queue) > 0 {
|
|
current := queue[0]
|
|
queue = queue[1:]
|
|
|
|
if visited[current.funcName] {
|
|
continue
|
|
}
|
|
visited[current.funcName] = true
|
|
|
|
if current.funcName == to {
|
|
return current.path
|
|
}
|
|
|
|
// Explore called functions
|
|
for _, calledFunc := range fh.callGraph[current.funcName] {
|
|
if !visited[calledFunc] {
|
|
newPath := make([]string, len(current.path)+1)
|
|
copy(newPath, current.path)
|
|
newPath[len(current.path)] = calledFunc
|
|
queue = append(queue, node{funcName: calledFunc, path: newPath})
|
|
}
|
|
}
|
|
}
|
|
|
|
// No path found (shouldn't happen if overlap detected correctly)
|
|
return []string{from, to}
|
|
}
|