diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go index db5ed41..6e13b66 100644 --- a/internal/compiler/compiler.go +++ b/internal/compiler/compiler.go @@ -129,10 +129,18 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) { return nil, fmt.Errorf("Unclosed SCRIPT block.") } + // Analyze for overlapping absolute addresses in function call chains + c.checkAbsoluteOverlaps() + // Assemble final output with headers and footers return c.assembleOutput(codeOutput), nil } +// checkAbsoluteOverlaps analyzes and warns about overlapping absolute addresses +func (c *Compiler) checkAbsoluteOverlaps() { + c.ctx.FunctionHandler.ReportAbsoluteOverlaps() +} + // assembleOutput combines all generated sections into final assembly func (c *Compiler) assembleOutput(codeLines []string) []string { var output []string diff --git a/internal/compiler/context.go b/internal/compiler/context.go index 0b489fe..86046a9 100644 --- a/internal/compiler/context.go +++ b/internal/compiler/context.go @@ -50,6 +50,9 @@ func NewCompilerContext(pragma *preproc.Pragma) *CompilerContext { // FunctionHandler needs references to other components ctx.FunctionHandler = NewFunctionHandler(symTable, generalStack, constStrHandler, pragma) + // Connect SymbolTable to FunctionHandler for absolute variable notifications + symTable.SetFunctionHandler(ctx.FunctionHandler) + return ctx } diff --git a/internal/compiler/funchandler.go b/internal/compiler/funchandler.go index 377d72e..8910098 100644 --- a/internal/compiler/funchandler.go +++ b/internal/compiler/funchandler.go @@ -2,6 +2,8 @@ package compiler import ( "fmt" + "os" + "sort" "strings" "c65gm/internal/preproc" @@ -40,6 +42,10 @@ type FunctionHandler struct { 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 @@ -51,6 +57,8 @@ func NewFunctionHandler(st *SymbolTable, ls *LabelStack, csh *ConstantStringHand labelStack: ls, constStrHandler: csh, pragma: pragma, + absoluteAddrs: make(map[string]map[uint16]bool), + callGraph: make(map[string][]string), } } @@ -150,10 +158,51 @@ func (fh *FunctionHandler) HandleFuncDecl(line preproc.Line) ([]string, error) { 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) { @@ -332,6 +381,9 @@ func (fh *FunctionHandler) HandleFuncCall(line preproc.Line) ([]string, error) { } } + // 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)) @@ -340,6 +392,31 @@ func (fh *FunctionHandler) HandleFuncCall(line preproc.Line) ([]string, error) { 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 @ @@ -768,3 +845,165 @@ func parseParamSpec(spec string) (ParamDirection, string, bool, string, error) { 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} +} diff --git a/internal/compiler/funchandler_test.go b/internal/compiler/funchandler_test.go index 0b78536..ea2f3a2 100644 --- a/internal/compiler/funchandler_test.go +++ b/internal/compiler/funchandler_test.go @@ -1,6 +1,7 @@ package compiler import ( + "fmt" "strings" "testing" @@ -1074,3 +1075,340 @@ func TestParseImplicitDecl_Absolute(t *testing.T) { }) } } + +func TestAbsoluteOverlapDetection(t *testing.T) { + tests := []struct { + name string + code string + wantOverlapAddr uint16 + wantFunc1 string + wantFunc2 string + }{ + { + name: "direct call with overlap", + code: ` +BYTE dummy +FUNC funcB ( {BYTE data @ $fa} ) +FEND + +FUNC funcA ( {BYTE temp @ $fa} ) + CALL funcB(dummy) +FEND +`, + wantOverlapAddr: 0xFA, + wantFunc1: "funcA", + wantFunc2: "funcB", + }, + { + name: "transitive call with overlap", + code: ` +BYTE dummy +FUNC funcC ( {BYTE data @ $fa} ) +FEND + +FUNC funcB + CALL funcC(dummy) +FEND + +FUNC funcA ( {BYTE temp @ $fa} ) + CALL funcB() +FEND +`, + wantOverlapAddr: 0xFA, + wantFunc1: "funcA", + wantFunc2: "funcC", + }, + { + name: "word overlap with byte", + code: ` +BYTE dummy +FUNC funcB ( {BYTE data @ $fb} ) +FEND + +FUNC funcA ( {WORD value @ $fa} ) + CALL funcB(dummy) +FEND +`, + wantOverlapAddr: 0xFB, + wantFunc1: "funcA", + wantFunc2: "funcB", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + st := NewSymbolTable() + ls := NewLabelStack("L") + csh := NewConstantStringHandler() + pragma := preproc.NewPragma() + fh := NewFunctionHandler(st, ls, csh, pragma) + + // Parse code into lines + lines := strings.Split(strings.TrimSpace(tt.code), "\n") + + // Process each line + for i, lineText := range lines { + lineText = strings.TrimSpace(lineText) + if lineText == "" { + continue + } + + pline := preproc.Line{ + Text: lineText, + LineNo: i + 1, + Filename: "test.c65", + } + + if strings.HasPrefix(lineText, "BYTE ") || strings.HasPrefix(lineText, "WORD ") { + // Variable declaration - add to symbol table + parts := strings.Fields(lineText) + if len(parts) >= 2 { + varKind := KindByte + if strings.ToUpper(parts[0]) == "WORD" { + varKind = KindWord + } + st.AddVar(parts[1], "", varKind, 0) + } + } else if strings.HasPrefix(lineText, "FUNC ") { + _, err := fh.HandleFuncDecl(pline) + if err != nil { + t.Fatalf("HandleFuncDecl failed: %v", err) + } + } else if strings.HasPrefix(lineText, "CALL ") { + _, err := fh.HandleFuncCall(pline) + if err != nil { + t.Fatalf("HandleFuncCall failed: %v", err) + } + } else if lineText == "FEND" { + fh.EndFunction() + } + } + + // Analyze overlaps + overlaps := fh.AnalyzeAbsoluteOverlaps() + + if len(overlaps) == 0 { + t.Fatalf("Expected overlap at address $%04X, but none detected", tt.wantOverlapAddr) + } + + // Find the expected overlap + found := false + for _, overlap := range overlaps { + if overlap.Address == tt.wantOverlapAddr && + overlap.Func1 == tt.wantFunc1 && + overlap.Func2 == tt.wantFunc2 { + found = true + break + } + } + + if !found { + t.Errorf("Expected overlap at $%04X between %s and %s, got: %+v", + tt.wantOverlapAddr, tt.wantFunc1, tt.wantFunc2, overlaps) + } + }) + } +} + +func TestAbsoluteLocalVariables(t *testing.T) { + tests := []struct { + name string + code string + wantOverlapAddr uint16 + wantFunc1 string + wantFunc2 string + }{ + { + name: "local absolute variables overlap", + code: ` +FUNC funcB + BYTE localB @ $fa +FEND + +FUNC funcA + BYTE localA @ $fa + CALL funcB() +FEND +`, + wantOverlapAddr: 0xFA, + wantFunc1: "funcA", + wantFunc2: "funcB", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + st := NewSymbolTable() + ls := NewLabelStack("L") + csh := NewConstantStringHandler() + pragma := preproc.NewPragma() + fh := NewFunctionHandler(st, ls, csh, pragma) + + // Connect symbol table to function handler + st.SetFunctionHandler(fh) + + // Parse code into lines + lines := strings.Split(strings.TrimSpace(tt.code), "\n") + currentFunc := "" + + // Process each line + for i, lineText := range lines { + lineText = strings.TrimSpace(lineText) + if lineText == "" { + continue + } + + pline := preproc.Line{ + Text: lineText, + LineNo: i + 1, + Filename: "test.c65", + } + + if strings.HasPrefix(lineText, "BYTE ") || strings.HasPrefix(lineText, "WORD ") { + // Parse variable declaration + parts := strings.Fields(lineText) + if len(parts) >= 4 && parts[2] == "@" { + // Absolute variable: BYTE name @ $addr + varKind := KindByte + if strings.ToUpper(parts[0]) == "WORD" { + varKind = KindWord + } + addrStr := strings.TrimPrefix(parts[3], "$") + var addr uint16 + fmt.Sscanf(addrStr, "%x", &addr) + st.AddAbsolute(parts[1], currentFunc, varKind, addr) + } + } else if strings.HasPrefix(lineText, "FUNC ") { + _, err := fh.HandleFuncDecl(pline) + if err != nil { + t.Fatalf("HandleFuncDecl failed: %v", err) + } + // Extract function name + funcParts := strings.Fields(lineText) + if len(funcParts) >= 2 { + currentFunc = funcParts[1] + } + } else if strings.HasPrefix(lineText, "CALL ") { + _, err := fh.HandleFuncCall(pline) + if err != nil { + t.Fatalf("HandleFuncCall failed: %v", err) + } + } else if lineText == "FEND" { + fh.EndFunction() + currentFunc = "" + } + } + + // Analyze overlaps + overlaps := fh.AnalyzeAbsoluteOverlaps() + + if len(overlaps) == 0 { + t.Fatalf("Expected overlap at address $%04X, but none detected", tt.wantOverlapAddr) + } + + // Find the expected overlap + found := false + for _, overlap := range overlaps { + if overlap.Address == tt.wantOverlapAddr && + overlap.Func1 == tt.wantFunc1 && + overlap.Func2 == tt.wantFunc2 { + found = true + break + } + } + + if !found { + t.Errorf("Expected overlap at $%04X between %s and %s, got: %+v", + tt.wantOverlapAddr, tt.wantFunc1, tt.wantFunc2, overlaps) + } + }) + } +} + +func TestAbsoluteNoOverlap(t *testing.T) { + tests := []struct { + name string + code string + }{ + { + name: "different addresses", + code: ` +BYTE dummy +FUNC funcB ( {BYTE data @ $fb} ) +FEND + +FUNC funcA ( {BYTE temp @ $fa} ) + CALL funcB(dummy) +FEND +`, + }, + { + name: "no call relationship", + code: ` +FUNC funcA ( {BYTE temp @ $fa} ) +FEND + +FUNC funcB ( {BYTE data @ $fa} ) +FEND +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + st := NewSymbolTable() + ls := NewLabelStack("L") + csh := NewConstantStringHandler() + pragma := preproc.NewPragma() + fh := NewFunctionHandler(st, ls, csh, pragma) + + // Parse code into lines + lines := strings.Split(strings.TrimSpace(tt.code), "\n") + + // Process each line + for i, lineText := range lines { + lineText = strings.TrimSpace(lineText) + if lineText == "" { + continue + } + + pline := preproc.Line{ + Text: lineText, + LineNo: i + 1, + Filename: "test.c65", + } + + if strings.HasPrefix(lineText, "BYTE ") || strings.HasPrefix(lineText, "WORD ") { + // Variable declaration - add to symbol table + parts := strings.Fields(lineText) + if len(parts) >= 2 { + varKind := KindByte + if strings.ToUpper(parts[0]) == "WORD" { + varKind = KindWord + } + st.AddVar(parts[1], "", varKind, 0) + } + } else if strings.HasPrefix(lineText, "FUNC ") { + _, err := fh.HandleFuncDecl(pline) + if err != nil { + t.Fatalf("HandleFuncDecl failed: %v", err) + } + } else if strings.HasPrefix(lineText, "CALL ") { + _, err := fh.HandleFuncCall(pline) + if err != nil { + t.Fatalf("HandleFuncCall failed: %v", err) + } + } else if lineText == "FEND" { + fh.EndFunction() + } + } + + // Analyze overlaps + overlaps := fh.AnalyzeAbsoluteOverlaps() + + if len(overlaps) > 0 { + t.Errorf("Expected no overlaps, but got: %+v", overlaps) + } + }) + } +} diff --git a/internal/compiler/symboltable.go b/internal/compiler/symboltable.go index 494ab8b..f1bc7d1 100644 --- a/internal/compiler/symboltable.go +++ b/internal/compiler/symboltable.go @@ -62,9 +62,16 @@ func (s *Symbol) FullName() string { // SymbolTable manages variable and constant declarations type SymbolTable struct { - symbols []*Symbol // insertion order - byFullName map[string]*Symbol // fullname -> symbol - byScope map[string]map[string]*Symbol // scope -> name -> symbol + symbols []*Symbol // insertion order + byFullName map[string]*Symbol // fullname -> symbol + byScope map[string]map[string]*Symbol // scope -> name -> symbol + funcHandler FunctionHandlerInterface // for notifying about absolute variables in functions +} + +// FunctionHandlerInterface allows SymbolTable to notify about absolute variables +// without creating a circular dependency +type FunctionHandlerInterface interface { + RecordAbsoluteVar(funcScope string, addr uint16, isWord bool) } // NewSymbolTable creates a new symbol table @@ -76,6 +83,11 @@ func NewSymbolTable() *SymbolTable { } } +// SetFunctionHandler sets the function handler for absolute variable notifications +func (st *SymbolTable) SetFunctionHandler(fh FunctionHandlerInterface) { + st.funcHandler = fh +} + // AddVar adds a regular variable (byte or word) func (st *SymbolTable) AddVar(name, scope string, kind VarKind, initValue uint16) error { var flags SymbolFlags @@ -149,6 +161,11 @@ func (st *SymbolTable) AddAbsolute(name, scope string, kind VarKind, addr uint16 return fmt.Errorf("unknown variable kind: %d", kind) } + // Notify function handler about absolute variable in function scope + if st.funcHandler != nil && scope != "" { + st.funcHandler.RecordAbsoluteVar(scope, addr, kind == KindWord) + } + return st.add(&Symbol{ Name: name, Scope: scope,