Fix pragma auto-removal for multi-FUNC groups sharing one FEND
This commit is contained in:
parent
8775ebaf43
commit
b6fce2a7f9
8 changed files with 482 additions and 73 deletions
36
AGENTS.md
36
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
|
||||
|
|
|
|||
12
Dockerfile
Normal file
12
Dockerfile
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -8,11 +8,7 @@
|
|||
|
||||
#INCLUDE <c64start.c65>
|
||||
#INCLUDE <c64defs.c65>
|
||||
|
||||
#PRAGMA _P_REMOVE_UNUSED 0
|
||||
#INCLUDE <multdivlib.c65>
|
||||
#PRAGMA _P_REMOVE_UNUSED 1
|
||||
|
||||
#INCLUDE <cbmiolib.c65>
|
||||
#INCLUDE <decoutlib.c65>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue