diff --git a/AGENTS.md b/AGENTS.md index 0586ce8..6f91ed9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -90,12 +90,13 @@ LABEL lib_mylib_skip ## Development Guidelines ### Environment Constraints -**CRITICAL - NO GO TOOLS AVAILABLE**: The agent runs in a Docker container without access to compilers, testing tools, or external build systems. The container does NOT have Go installed (`go` command not found). All compilation and testing must be performed by the user. The agent can only: -1. Read and analyze source code -2. Make code changes -3. Provide instructions for the user to compile and test +**Go is available**: The container has Go installed. The agent can run `go build`, `go test`, and other Go commands directly. However, always rebuild the Docker image first if you update the Dockerfile: -**NEVER attempt to run `go build`, `go test`, or any Go commands** - they will fail with "go: not found". +```bash +docker compose build +``` + +Then recreate the container: `docker compose up -d`. ### File Access Restrictions **CRITICAL**: The agent must only access normal project files within the current working directory. The agent must NEVER: @@ -119,30 +120,23 @@ All file operations must be restricted to the project's source code and document 4. Consider adding examples in `examples/` directory ### Testing and Rebuilding -**CRITICAL**: The agent cannot run tests or compile code. The container has no Go installation. Provide these instructions to the user: +**Go is available**: The agent can compile code and run tests directly. #### Testing: - Run all tests: `go test ./...` - Run specific package tests: `go test ./internal/compiler` - Test with verbose output: `go test -v ./...` +- Run specific test functions: `go test ./internal/compiler -v -run "TestMultiFuncGroup"` #### Rebuilding c65gm: -When making changes to the compiler or library files, ask the user to rebuild c65gm using the build script: -```bash -./build_c65gm.sh -``` -The build script copies `lib/` into `internal/preproc/lib/` (for embedding into the binary) and then runs `go build -o c65gm`. - -**IMPORTANT**: Library files (`lib/*.c65`) are embedded into the binary at build time. After modifying any `.c65` file in `lib/`, you MUST use `build_c65gm.sh` (not `go build` directly) to ensure the updated library is embedded. The agent should: -1. Make code changes -2. Ask the user to rebuild c65gm: `./build_c65gm.sh` -3. **Check file datetime**: Verify the c65gm binary was recently updated (use `ls -la c65gm` or similar) -4. Ask the user to run tests: `go test ./...` +Library files (`lib/*.c65`) are embedded into the binary at build time. After modifying any `.c65` file in `lib/`, you MUST use `build_c65gm.sh` (not `go build` directly) to ensure the updated library is embedded. The agent should: +1. Make code changes (Go files and/or .c65 files) +2. If .c65 files were modified: `sh build_c65gm.sh` +3. If only Go files were modified: `go build ./...` (for check) or `go build -o c65gm` +4. Run `go test ./...` 5. Test the changes with example .c65 files -**IMPORTANT**: Always check the file datetime of the c65gm binary after asking for a rebuild. If the timestamp hasn't changed, the user may have forgotten to rebuild, or the build may have failed. Testing with an old version is wasteful and can lead to incorrect conclusions. - -#### Compiling .c65 files: +#### Compiling .c65 files with the compiled binary: ```bash C65LIBPATH=/app/lib ./c65gm -in input.c65 -out output.asm ``` @@ -158,7 +152,7 @@ C65LIBPATH=/app/lib ./c65gm -in input.c65 -out output.asm ## Quick Reference ### Compilation -**IMPORTANT**: The agent cannot compile code. Provide these instructions to the user: +**IMPORTANT**: The agent can compile code. Run `go build ./...` or `sh build_c65gm.sh`. #### New Self-Contained Method (Recommended) ```bash diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2f29229 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM ghcr.io/anomalyco/opencode:1.14.48 + +RUN apk add --no-cache go gcc musl-dev + +ENV CGO_ENABLED=0 \ + GOROOT=/usr/lib/go \ + GOPATH=/go \ + PATH=$PATH:/usr/lib/go/bin:/go/bin + +RUN mkdir -p /go && chmod 777 /go + +WORKDIR /app diff --git a/docker-compose.yml b/docker-compose.yml index 895643c..d4efe53 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,7 @@ services: opencode-deepseek-c65gm: - image: ghcr.io/anomalyco/opencode:1.14.48 + build: . + #image: ghcr.io/anomalyco/opencode:1.14.48 #image: ghcr.io/anomalyco/opencode:latest user: "${DOCKER_UID}:${DOCKER_GID}" working_dir: /app diff --git a/examples/multdiv_demo/multdiv_demo.c65 b/examples/multdiv_demo/multdiv_demo.c65 index 50844dd..636d12c 100644 --- a/examples/multdiv_demo/multdiv_demo.c65 +++ b/examples/multdiv_demo/multdiv_demo.c65 @@ -8,11 +8,7 @@ #INCLUDE #INCLUDE - -#PRAGMA _P_REMOVE_UNUSED 0 #INCLUDE -#PRAGMA _P_REMOVE_UNUSED 1 - #INCLUDE #INCLUDE diff --git a/internal/commands/fend.go b/internal/commands/fend.go index 7610978..9bebb47 100644 --- a/internal/commands/fend.go +++ b/internal/commands/fend.go @@ -38,8 +38,12 @@ func (c *FendCommand) Interpret(line preproc.Line, _ *compiler.CompilerContext) } func (c *FendCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) { - funcName := ctx.FunctionHandler.CurrentFunction() + funcNames := ctx.FunctionHandler.GetCurrentFunctions() ctx.FunctionHandler.EndFunction() - // Return RTS followed by end marker - return []string{"\trts", fmt.Sprintf("; @@FUNC_END %s", funcName)}, nil + // Return RTS followed by end markers for all functions in the group + lines := []string{"\trts"} + for _, name := range funcNames { + lines = append(lines, fmt.Sprintf("; @@FUNC_END %s", name)) + } + return lines, nil } diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go index 30ab6d6..815d51c 100644 --- a/internal/compiler/compiler.go +++ b/internal/compiler/compiler.go @@ -504,45 +504,61 @@ func (c *Compiler) removeUnusedFunctions(codeLines []string) ([]string, map[stri if strings.HasPrefix(line, "; @@FUNC_BEGIN ") { parts := strings.Fields(line) if len(parts) >= 3 && toRemove[parts[2]] { - funcName := parts[2] - - // Find the function declaration to get file/line info - var filename string - var lineNo int - for _, funcDecl := range c.ctx.FunctionHandler.functions { - if funcDecl.Name == funcName { - filename = funcDecl.Line.Filename - lineNo = funcDecl.Line.LineNo + // Collect all consecutive @@FUNC_BEGIN markers in this group (all removable) + groupNames := []string{parts[2]} + j := i + 1 + for j < len(codeLines) { + if strings.HasPrefix(codeLines[j], "; @@FUNC_BEGIN ") { + p := strings.Fields(codeLines[j]) + if len(p) >= 3 && toRemove[p[2]] { + groupNames = append(groupNames, p[2]) + j++ + } else { + break + } + } else { break } } - - // Print info message to stdout - if filename != "" { - baseFilename := filepath.Base(filename) - fmt.Printf("info:%s:%d:FUNC %s removed.\n", baseFilename, lineNo, funcName) - } else { - fmt.Printf("info:unknown:0:FUNC %s removed.\n", funcName) + + // Print info messages for all removed functions + for _, funcName := range groupNames { + var filename string + var lineNo int + for _, funcDecl := range c.ctx.FunctionHandler.functions { + if funcDecl.Name == funcName { + filename = funcDecl.Line.Filename + lineNo = funcDecl.Line.LineNo + break + } + } + if filename != "" { + baseFilename := filepath.Base(filename) + fmt.Printf("info:%s:%d:FUNC %s removed.\n", baseFilename, lineNo, funcName) + } else { + fmt.Printf("info:unknown:0:FUNC %s removed.\n", funcName) + } } - // Skip everything until matching @@FUNC_END - foundEnd := false - for i < len(codeLines) { + // Build set of end markers to find + endSet := make(map[string]bool) + for _, name := range groupNames { + endSet[name] = true + } + + // Skip past all FUNC_BEGIN markers + i = j + + // Skip body until all corresponding @@FUNC_END markers are found + for i < len(codeLines) && len(endSet) > 0 { if strings.HasPrefix(codeLines[i], "; @@FUNC_END ") { - // Check if this is the exact function end marker - parts := strings.Fields(codeLines[i]) - if len(parts) >= 3 && parts[2] == funcName { - foundEnd = true - break + p := strings.Fields(codeLines[i]) + if len(p) >= 3 && endSet[p[2]] { + delete(endSet, p[2]) } } i++ } - // Skip the END marker line too if found - if foundEnd && i < len(codeLines) { - i++ - } - // If we didn't find the end marker, we've reached end of file continue } } diff --git a/internal/compiler/funchandler.go b/internal/compiler/funchandler.go index 08230f6..013d6d0 100644 --- a/internal/compiler/funchandler.go +++ b/internal/compiler/funchandler.go @@ -35,6 +35,11 @@ type FuncDecl struct { Line preproc.Line // Declaration location for warnings } +// FuncGroup represents a set of functions that share a single FEND (share the same body) +type FuncGroup struct { + Names []string // All function names in this group, in declaration order +} + // FunctionHandler manages function declarations and calls type FunctionHandler struct { functions []*FuncDecl @@ -50,6 +55,10 @@ type FunctionHandler struct { // Function usage tracking for unused function warnings calledFunctions map[string]bool // funcName -> true if function is called + + // Function groups for multi-FUNC-per-FEND tracking + funcGroups []*FuncGroup + funcToGroup map[string]*FuncGroup } // NewFunctionHandler creates a new function handler @@ -64,6 +73,8 @@ func NewFunctionHandler(st *SymbolTable, ls *LabelStack, csh *ConstantStringHand absoluteAddrs: make(map[string]map[uint16]bool), callGraph: make(map[string][]string), calledFunctions: make(map[string]bool), + funcGroups: make([]*FuncGroup, 0), + funcToGroup: make(map[string]*FuncGroup), } } @@ -654,6 +665,17 @@ func (fh *FunctionHandler) parseImplicitDecl(decl string, funcName string, line // EndFunction pops all functions from the stack (called by FEND) func (fh *FunctionHandler) EndFunction() { + // Record multi-function groups (2+ funcs sharing a single FEND) + if len(fh.currentFuncs) > 1 { + group := &FuncGroup{ + Names: make([]string, len(fh.currentFuncs)), + } + copy(group.Names, fh.currentFuncs) + fh.funcGroups = append(fh.funcGroups, group) + for _, name := range fh.currentFuncs { + fh.funcToGroup[name] = group + } + } fh.currentFuncs = fh.currentFuncs[:0] } @@ -670,6 +692,18 @@ func (fh *FunctionHandler) CurrentFunction() string { return fh.currentFuncs[len(fh.currentFuncs)-1] } +// GetCurrentFunctions returns all function names on the current stack +func (fh *FunctionHandler) GetCurrentFunctions() []string { + result := make([]string, len(fh.currentFuncs)) + copy(result, fh.currentFuncs) + return result +} + +// GetGroup returns the FuncGroup for a function, or nil if it's a standalone function +func (fh *FunctionHandler) GetGroup(name string) *FuncGroup { + return fh.funcToGroup[name] +} + // findFunc finds a function declaration by name func (fh *FunctionHandler) findFunc(name string) *FuncDecl { for _, f := range fh.functions { @@ -1017,6 +1051,21 @@ func (fh *FunctionHandler) CheckUnusedFunctions() []string { continue } + // For functions in a multi-FUNC group: if ANY sibling is called, + // suppress the warning (they share a body needed by the called sibling) + if group := fh.funcToGroup[funcDecl.Name]; group != nil { + anySiblingCalled := false + for _, name := range group.Names { + if fh.calledFunctions[name] { + anySiblingCalled = true + break + } + } + if anySiblingCalled { + continue + } + } + // Check if pragma indicates we should ignore unused warnings for this function if fh.pragma != nil { pragmaSet := fh.pragma.GetPragmaSetByIndex(funcDecl.Line.PragmaSetIndex) @@ -1041,23 +1090,58 @@ func (fh *FunctionHandler) CheckUnusedFunctions() []string { return warnings } +// hasRemovePragma checks if a specific FuncDecl has _P_REMOVE_UNUSED enabled +func hasRemovePragma(funcDecl *FuncDecl, pragma *preproc.Pragma) bool { + if pragma == nil { + return false + } + pragmaSet := pragma.GetPragmaSetByIndex(funcDecl.Line.PragmaSetIndex) + value := pragmaSet.GetPragma("_P_REMOVE_UNUSED") + return value != "" && value != "0" +} + // GetFunctionsToRemove returns a map of function names that should be removed from assembly output // Functions are removed if they are never called AND have _P_REMOVE_UNUSED pragma enabled +// For functions in a multi-FUNC group, ALL functions must be unused and ALL must have the pragma, +// or none are removed (they share a body atomically) func (fh *FunctionHandler) GetFunctionsToRemove() map[string]bool { toRemove := make(map[string]bool) + processed := make(map[string]bool) for _, funcDecl := range fh.functions { - // Skip functions that have been called - if fh.calledFunctions[funcDecl.Name] { + name := funcDecl.Name + if processed[name] { continue } + processed[name] = true - // Check if pragma indicates we should remove this unused function - if fh.pragma != nil { - pragmaSet := fh.pragma.GetPragmaSetByIndex(funcDecl.Line.PragmaSetIndex) - removeValue := pragmaSet.GetPragma("_P_REMOVE_UNUSED") - if removeValue != "" && removeValue != "0" { - toRemove[funcDecl.Name] = true + group := fh.funcToGroup[name] + if group == nil { + // Standalone function - existing logic + if fh.calledFunctions[name] { + continue + } + if hasRemovePragma(funcDecl, fh.pragma) { + toRemove[name] = true + } + } else { + // Multi-function group: all must be unused AND all must have pragma + allUnused := true + allHavePragma := true + for _, gname := range group.Names { + processed[gname] = true + if fh.calledFunctions[gname] { + allUnused = false + } + gDecl := fh.findFunc(gname) + if gDecl == nil || !hasRemovePragma(gDecl, fh.pragma) { + allHavePragma = false + } + } + if allUnused && allHavePragma { + for _, gname := range group.Names { + toRemove[gname] = true + } } } } @@ -1067,18 +1151,48 @@ func (fh *FunctionHandler) GetFunctionsToRemove() map[string]bool { // GetFunctionsWithRemovePragma returns a map of function names that have _P_REMOVE_UNUSED pragma enabled // This is used to suppress variable warnings for functions marked for removal +// For functions in a multi-FUNC group, if any member has the pragma, all group members are included func (fh *FunctionHandler) GetFunctionsWithRemovePragma() map[string]bool { withPragma := make(map[string]bool) - + processed := make(map[string]bool) + for _, funcDecl := range fh.functions { - if fh.pragma != nil { - pragmaSet := fh.pragma.GetPragmaSetByIndex(funcDecl.Line.PragmaSetIndex) - removeValue := pragmaSet.GetPragma("_P_REMOVE_UNUSED") - if removeValue != "" && removeValue != "0" { - withPragma[funcDecl.Name] = true + if processed[funcDecl.Name] { + continue + } + + group := fh.funcToGroup[funcDecl.Name] + if group == nil { + // Standalone function + if fh.pragma != nil { + pragmaSet := fh.pragma.GetPragmaSetByIndex(funcDecl.Line.PragmaSetIndex) + removeValue := pragmaSet.GetPragma("_P_REMOVE_UNUSED") + if removeValue != "" && removeValue != "0" { + withPragma[funcDecl.Name] = true + } + } + processed[funcDecl.Name] = true + } else { + // Multi-function group: if any has pragma, all are included + anyHasPragma := false + for _, gname := range group.Names { + processed[gname] = true + gDecl := fh.findFunc(gname) + if gDecl != nil && fh.pragma != nil { + pragmaSet := fh.pragma.GetPragmaSetByIndex(gDecl.Line.PragmaSetIndex) + removeValue := pragmaSet.GetPragma("_P_REMOVE_UNUSED") + if removeValue != "" && removeValue != "0" { + anyHasPragma = true + } + } + } + if anyHasPragma { + for _, gname := range group.Names { + withPragma[gname] = true + } } } } - + return withPragma } diff --git a/internal/compiler/funchandler_test.go b/internal/compiler/funchandler_test.go index 6cb58f2..5d47716 100644 --- a/internal/compiler/funchandler_test.go +++ b/internal/compiler/funchandler_test.go @@ -1686,3 +1686,275 @@ func TestCheckUnusedFunctions(t *testing.T) { } }) } + +// TestMultiFuncGroupWarnings tests that functions sharing an FEND don't generate spurious warnings +func TestMultiFuncGroupWarnings(t *testing.T) { + // Test 1: Multi-FUNC group with one called sibling → no warnings + t.Run("group with one called sibling suppresses warnings", func(t *testing.T) { + st := NewSymbolTable() + ls := NewLabelStack("_L") + csh := NewConstantStringHandler() + fh := NewFunctionHandler(st, ls, csh, preproc.NewPragma()) + + // Declare two functions in a group (simulating multi-FUNC-per-FEND pattern) + line1 := preproc.Line{ + RawText: "FUNC funcA ( {BYTE x} )", + Text: "FUNC funcA ( {BYTE x} )", + Filename: "test.c65", + LineNo: 10, + Kind: preproc.Source, + PragmaSetIndex: 0, + } + _, err := fh.HandleFuncDecl(line1) + if err != nil { + t.Fatalf("Failed to declare funcA: %v", err) + } + + line2 := preproc.Line{ + RawText: "FUNC funcB ( {BYTE x} )", + Text: "FUNC funcB ( {BYTE x} )", + Filename: "test.c65", + LineNo: 11, + Kind: preproc.Source, + PragmaSetIndex: 0, + } + _, err = fh.HandleFuncDecl(line2) + if err != nil { + t.Fatalf("Failed to declare funcB: %v", err) + } + + // End the function group (simulating FEND) + fh.EndFunction() + + // Verify the group was recorded + group := fh.GetGroup("funcA") + if group == nil { + t.Fatal("Expected funcA to be in a group") + } + if len(group.Names) != 2 || group.Names[0] != "funcA" || group.Names[1] != "funcB" { + t.Errorf("Expected group [funcA funcB], got %v", group.Names) + } + + // Only call funcB + line3 := preproc.Line{ + RawText: "CALL funcB ( 42 )", + Text: "CALL funcB ( 42 )", + Filename: "test.c65", + LineNo: 20, + Kind: preproc.Source, + PragmaSetIndex: 0, + } + _, err = fh.HandleFuncCall(line3) + if err != nil { + t.Fatalf("Failed to call funcB: %v", err) + } + + // Check for unused functions - should be empty (funcA's warning suppressed by funcB's usage) + warnings := fh.CheckUnusedFunctions() + if len(warnings) > 0 { + t.Errorf("Expected no warnings (sibling in group is called), got: %v", warnings) + } + }) + + // Test 2: Multi-FUNC group with none called → both get warnings + t.Run("group with none called shows warnings for all", func(t *testing.T) { + st := NewSymbolTable() + ls := NewLabelStack("_L") + csh := NewConstantStringHandler() + fh := NewFunctionHandler(st, ls, csh, preproc.NewPragma()) + + // Declare two functions in a group + line1 := preproc.Line{ + RawText: "FUNC funcX ( {BYTE a} )", + Text: "FUNC funcX ( {BYTE a} )", + Filename: "test.c65", + LineNo: 30, + Kind: preproc.Source, + PragmaSetIndex: 0, + } + _, err := fh.HandleFuncDecl(line1) + if err != nil { + t.Fatalf("Failed to declare funcX: %v", err) + } + + line2 := preproc.Line{ + RawText: "FUNC funcY ( {BYTE a} )", + Text: "FUNC funcY ( {BYTE a} )", + Filename: "test.c65", + LineNo: 31, + Kind: preproc.Source, + PragmaSetIndex: 0, + } + _, err = fh.HandleFuncDecl(line2) + if err != nil { + t.Fatalf("Failed to declare funcY: %v", err) + } + fh.EndFunction() + + // Neither is called - both should warn + warnings := fh.CheckUnusedFunctions() + if len(warnings) != 2 { + t.Fatalf("Expected 2 warnings for unused functions in group, got %d: %v", len(warnings), warnings) + } + + // Verify each function appears in a warning + foundX := false + foundY := false + for _, w := range warnings { + if strings.Contains(w, "'funcX'") { + foundX = true + } + if strings.Contains(w, "'funcY'") { + foundY = true + } + } + if !foundX { + t.Error("Missing warning for funcX") + } + if !foundY { + t.Error("Missing warning for funcY") + } + }) + + // Test 3: Standalone functions (not in group) unaffected + t.Run("standalone functions unaffected by new logic", func(t *testing.T) { + st := NewSymbolTable() + ls := NewLabelStack("_L") + csh := NewConstantStringHandler() + fh := NewFunctionHandler(st, ls, csh, preproc.NewPragma()) + + // Declare a standalone function + line1 := preproc.Line{ + RawText: "FUNC standalone ( {BYTE x} )", + Text: "FUNC standalone ( {BYTE x} )", + Filename: "test.c65", + LineNo: 40, + Kind: preproc.Source, + PragmaSetIndex: 0, + } + _, err := fh.HandleFuncDecl(line1) + if err != nil { + t.Fatalf("Failed to declare standalone: %v", err) + } + fh.EndFunction() + + // Not called - should warn + warnings := fh.CheckUnusedFunctions() + if len(warnings) != 1 { + t.Fatalf("Expected 1 warning for standalone function, got %d: %v", len(warnings), warnings) + } + if !strings.Contains(warnings[0], "'standalone'") { + t.Errorf("Expected warning about 'standalone', got: %s", warnings[0]) + } + }) +} + +// TestMultiFuncGroupRemoval tests GetFunctionsToRemove with multi-FUNC groups +func TestMultiFuncGroupRemoval(t *testing.T) { + // Test 1: Group with all unused and all have pragma → all removable + t.Run("all unused all have pragma", func(t *testing.T) { + pragma := preproc.NewPragma() + + st := NewSymbolTable() + ls := NewLabelStack("_L") + csh := NewConstantStringHandler() + fh := NewFunctionHandler(st, ls, csh, pragma) + + // Set _P_REMOVE_UNUSED before declaring funcA + pragma.AddPragma("_P_REMOVE_UNUSED", "1") + line1 := preproc.Line{ + RawText: "FUNC funcA ( {BYTE x} )", + Text: "FUNC funcA ( {BYTE x} )", + Filename: "test.c65", + LineNo: 10, + Kind: preproc.Source, + PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), + } + _, err := fh.HandleFuncDecl(line1) + if err != nil { + t.Fatalf("Failed to declare funcA: %v", err) + } + + // Set _P_REMOVE_UNUSED before declaring funcB + pragma.AddPragma("_P_REMOVE_UNUSED", "1") + line2 := preproc.Line{ + RawText: "FUNC funcB ( {BYTE x} )", + Text: "FUNC funcB ( {BYTE x} )", + Filename: "test.c65", + LineNo: 11, + Kind: preproc.Source, + PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), + } + _, err = fh.HandleFuncDecl(line2) + if err != nil { + t.Fatalf("Failed to declare funcB: %v", err) + } + fh.EndFunction() + + toRemove := fh.GetFunctionsToRemove() + if !toRemove["funcA"] || !toRemove["funcB"] { + t.Errorf("Expected both funcA and funcB to be removable, got: %v", toRemove) + } + if len(toRemove) != 2 { + t.Errorf("Expected exactly 2 functions to remove, got %d", len(toRemove)) + } + }) + + // Test 2: Group with one called → none removable + t.Run("one called none removable", func(t *testing.T) { + pragma := preproc.NewPragma() + + st := NewSymbolTable() + ls := NewLabelStack("_L") + csh := NewConstantStringHandler() + fh := NewFunctionHandler(st, ls, csh, pragma) + + pragma.AddPragma("_P_REMOVE_UNUSED", "1") + line1 := preproc.Line{ + RawText: "FUNC funcA ( {BYTE x} )", + Text: "FUNC funcA ( {BYTE x} )", + Filename: "test.c65", + LineNo: 10, + Kind: preproc.Source, + PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), + } + _, err := fh.HandleFuncDecl(line1) + if err != nil { + t.Fatalf("Failed to declare funcA: %v", err) + } + + pragma.AddPragma("_P_REMOVE_UNUSED", "1") + line2 := preproc.Line{ + RawText: "FUNC funcB ( {BYTE x} )", + Text: "FUNC funcB ( {BYTE x} )", + Filename: "test.c65", + LineNo: 11, + Kind: preproc.Source, + PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), + } + _, err = fh.HandleFuncDecl(line2) + if err != nil { + t.Fatalf("Failed to declare funcB: %v", err) + } + fh.EndFunction() + + // Call funcB + line3 := preproc.Line{ + RawText: "CALL funcB ( 42 )", + Text: "CALL funcB ( 42 )", + Filename: "test.c65", + LineNo: 20, + Kind: preproc.Source, + PragmaSetIndex: pragma.GetCurrentPragmaSetIndex(), + } + _, err = fh.HandleFuncCall(line3) + if err != nil { + t.Fatalf("Failed to call funcB: %v", err) + } + + toRemove := fh.GetFunctionsToRemove() + if len(toRemove) != 0 { + t.Errorf("Expected no removables (funcB is called), got: %v", toRemove) + } + }) +}