Added call graph functionality and analysis/warning of absolute address variable reuse overlap

This commit is contained in:
Mattias Hansson 2026-01-02 15:15:57 +01:00
parent 35e4b77ee3
commit ebad3c2e16
5 changed files with 608 additions and 3 deletions

View file

@ -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

View file

@ -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
}

View file

@ -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}
}

View file

@ -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)
}
})
}
}

View file

@ -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,