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
|
## Development Guidelines
|
||||||
|
|
||||||
### Environment Constraints
|
### 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:
|
**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:
|
||||||
1. Read and analyze source code
|
|
||||||
2. Make code changes
|
|
||||||
3. Provide instructions for the user to compile and test
|
|
||||||
|
|
||||||
**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
|
### File Access Restrictions
|
||||||
**CRITICAL**: The agent must only access normal project files within the current working directory. The agent must NEVER:
|
**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
|
4. Consider adding examples in `examples/` directory
|
||||||
|
|
||||||
### Testing and Rebuilding
|
### 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:
|
#### Testing:
|
||||||
- Run all tests: `go test ./...`
|
- Run all tests: `go test ./...`
|
||||||
- Run specific package tests: `go test ./internal/compiler`
|
- Run specific package tests: `go test ./internal/compiler`
|
||||||
- Test with verbose output: `go test -v ./...`
|
- Test with verbose output: `go test -v ./...`
|
||||||
|
- Run specific test functions: `go test ./internal/compiler -v -run "TestMultiFuncGroup"`
|
||||||
|
|
||||||
#### Rebuilding c65gm:
|
#### Rebuilding c65gm:
|
||||||
When making changes to the compiler or library files, ask the user to rebuild c65gm using the build script:
|
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:
|
||||||
```bash
|
1. Make code changes (Go files and/or .c65 files)
|
||||||
./build_c65gm.sh
|
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`
|
||||||
The build script copies `lib/` into `internal/preproc/lib/` (for embedding into the binary) and then runs `go build -o c65gm`.
|
4. Run `go test ./...`
|
||||||
|
|
||||||
**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 ./...`
|
|
||||||
5. Test the changes with example .c65 files
|
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 with the compiled binary:
|
||||||
|
|
||||||
#### Compiling .c65 files:
|
|
||||||
```bash
|
```bash
|
||||||
C65LIBPATH=/app/lib ./c65gm -in input.c65 -out output.asm
|
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
|
## Quick Reference
|
||||||
|
|
||||||
### Compilation
|
### 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)
|
#### New Self-Contained Method (Recommended)
|
||||||
```bash
|
```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:
|
services:
|
||||||
opencode-deepseek-c65gm:
|
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
|
#image: ghcr.io/anomalyco/opencode:latest
|
||||||
user: "${DOCKER_UID}:${DOCKER_GID}"
|
user: "${DOCKER_UID}:${DOCKER_GID}"
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,7 @@
|
||||||
|
|
||||||
#INCLUDE <c64start.c65>
|
#INCLUDE <c64start.c65>
|
||||||
#INCLUDE <c64defs.c65>
|
#INCLUDE <c64defs.c65>
|
||||||
|
|
||||||
#PRAGMA _P_REMOVE_UNUSED 0
|
|
||||||
#INCLUDE <multdivlib.c65>
|
#INCLUDE <multdivlib.c65>
|
||||||
#PRAGMA _P_REMOVE_UNUSED 1
|
|
||||||
|
|
||||||
#INCLUDE <cbmiolib.c65>
|
#INCLUDE <cbmiolib.c65>
|
||||||
#INCLUDE <decoutlib.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) {
|
func (c *FendCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) {
|
||||||
funcName := ctx.FunctionHandler.CurrentFunction()
|
funcNames := ctx.FunctionHandler.GetCurrentFunctions()
|
||||||
ctx.FunctionHandler.EndFunction()
|
ctx.FunctionHandler.EndFunction()
|
||||||
// Return RTS followed by end marker
|
// Return RTS followed by end markers for all functions in the group
|
||||||
return []string{"\trts", fmt.Sprintf("; @@FUNC_END %s", funcName)}, nil
|
lines := []string{"\trts"}
|
||||||
|
for _, name := range funcNames {
|
||||||
|
lines = append(lines, fmt.Sprintf("; @@FUNC_END %s", name))
|
||||||
|
}
|
||||||
|
return lines, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -504,9 +504,25 @@ func (c *Compiler) removeUnusedFunctions(codeLines []string) ([]string, map[stri
|
||||||
if strings.HasPrefix(line, "; @@FUNC_BEGIN ") {
|
if strings.HasPrefix(line, "; @@FUNC_BEGIN ") {
|
||||||
parts := strings.Fields(line)
|
parts := strings.Fields(line)
|
||||||
if len(parts) >= 3 && toRemove[parts[2]] {
|
if len(parts) >= 3 && toRemove[parts[2]] {
|
||||||
funcName := parts[2]
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Find the function declaration to get file/line info
|
// Print info messages for all removed functions
|
||||||
|
for _, funcName := range groupNames {
|
||||||
var filename string
|
var filename string
|
||||||
var lineNo int
|
var lineNo int
|
||||||
for _, funcDecl := range c.ctx.FunctionHandler.functions {
|
for _, funcDecl := range c.ctx.FunctionHandler.functions {
|
||||||
|
|
@ -516,33 +532,33 @@ func (c *Compiler) removeUnusedFunctions(codeLines []string) ([]string, map[stri
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print info message to stdout
|
|
||||||
if filename != "" {
|
if filename != "" {
|
||||||
baseFilename := filepath.Base(filename)
|
baseFilename := filepath.Base(filename)
|
||||||
fmt.Printf("info:%s:%d:FUNC %s removed.\n", baseFilename, lineNo, funcName)
|
fmt.Printf("info:%s:%d:FUNC %s removed.\n", baseFilename, lineNo, funcName)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("info:unknown:0:FUNC %s removed.\n", funcName)
|
fmt.Printf("info:unknown:0:FUNC %s removed.\n", funcName)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Skip everything until matching @@FUNC_END
|
// Build set of end markers to find
|
||||||
foundEnd := false
|
endSet := make(map[string]bool)
|
||||||
for i < len(codeLines) {
|
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 ") {
|
if strings.HasPrefix(codeLines[i], "; @@FUNC_END ") {
|
||||||
// Check if this is the exact function end marker
|
p := strings.Fields(codeLines[i])
|
||||||
parts := strings.Fields(codeLines[i])
|
if len(p) >= 3 && endSet[p[2]] {
|
||||||
if len(parts) >= 3 && parts[2] == funcName {
|
delete(endSet, p[2])
|
||||||
foundEnd = true
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
i++
|
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
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,11 @@ type FuncDecl struct {
|
||||||
Line preproc.Line // Declaration location for warnings
|
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
|
// FunctionHandler manages function declarations and calls
|
||||||
type FunctionHandler struct {
|
type FunctionHandler struct {
|
||||||
functions []*FuncDecl
|
functions []*FuncDecl
|
||||||
|
|
@ -50,6 +55,10 @@ type FunctionHandler struct {
|
||||||
|
|
||||||
// Function usage tracking for unused function warnings
|
// Function usage tracking for unused function warnings
|
||||||
calledFunctions map[string]bool // funcName -> true if function is called
|
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
|
// 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),
|
absoluteAddrs: make(map[string]map[uint16]bool),
|
||||||
callGraph: make(map[string][]string),
|
callGraph: make(map[string][]string),
|
||||||
calledFunctions: make(map[string]bool),
|
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)
|
// EndFunction pops all functions from the stack (called by FEND)
|
||||||
func (fh *FunctionHandler) EndFunction() {
|
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]
|
fh.currentFuncs = fh.currentFuncs[:0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -670,6 +692,18 @@ func (fh *FunctionHandler) CurrentFunction() string {
|
||||||
return fh.currentFuncs[len(fh.currentFuncs)-1]
|
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
|
// findFunc finds a function declaration by name
|
||||||
func (fh *FunctionHandler) findFunc(name string) *FuncDecl {
|
func (fh *FunctionHandler) findFunc(name string) *FuncDecl {
|
||||||
for _, f := range fh.functions {
|
for _, f := range fh.functions {
|
||||||
|
|
@ -1017,6 +1051,21 @@ func (fh *FunctionHandler) CheckUnusedFunctions() []string {
|
||||||
continue
|
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
|
// Check if pragma indicates we should ignore unused warnings for this function
|
||||||
if fh.pragma != nil {
|
if fh.pragma != nil {
|
||||||
pragmaSet := fh.pragma.GetPragmaSetByIndex(funcDecl.Line.PragmaSetIndex)
|
pragmaSet := fh.pragma.GetPragmaSetByIndex(funcDecl.Line.PragmaSetIndex)
|
||||||
|
|
@ -1041,23 +1090,58 @@ func (fh *FunctionHandler) CheckUnusedFunctions() []string {
|
||||||
return warnings
|
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
|
// 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
|
// 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 {
|
func (fh *FunctionHandler) GetFunctionsToRemove() map[string]bool {
|
||||||
toRemove := make(map[string]bool)
|
toRemove := make(map[string]bool)
|
||||||
|
processed := make(map[string]bool)
|
||||||
|
|
||||||
for _, funcDecl := range fh.functions {
|
for _, funcDecl := range fh.functions {
|
||||||
// Skip functions that have been called
|
name := funcDecl.Name
|
||||||
if fh.calledFunctions[funcDecl.Name] {
|
if processed[name] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
processed[name] = true
|
||||||
|
|
||||||
// Check if pragma indicates we should remove this unused function
|
group := fh.funcToGroup[name]
|
||||||
if fh.pragma != nil {
|
if group == nil {
|
||||||
pragmaSet := fh.pragma.GetPragmaSetByIndex(funcDecl.Line.PragmaSetIndex)
|
// Standalone function - existing logic
|
||||||
removeValue := pragmaSet.GetPragma("_P_REMOVE_UNUSED")
|
if fh.calledFunctions[name] {
|
||||||
if removeValue != "" && removeValue != "0" {
|
continue
|
||||||
toRemove[funcDecl.Name] = true
|
}
|
||||||
|
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,10 +1151,19 @@ func (fh *FunctionHandler) GetFunctionsToRemove() map[string]bool {
|
||||||
|
|
||||||
// GetFunctionsWithRemovePragma returns a map of function names that have _P_REMOVE_UNUSED pragma enabled
|
// 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
|
// 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 {
|
func (fh *FunctionHandler) GetFunctionsWithRemovePragma() map[string]bool {
|
||||||
withPragma := make(map[string]bool)
|
withPragma := make(map[string]bool)
|
||||||
|
processed := make(map[string]bool)
|
||||||
|
|
||||||
for _, funcDecl := range fh.functions {
|
for _, funcDecl := range fh.functions {
|
||||||
|
if processed[funcDecl.Name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
group := fh.funcToGroup[funcDecl.Name]
|
||||||
|
if group == nil {
|
||||||
|
// Standalone function
|
||||||
if fh.pragma != nil {
|
if fh.pragma != nil {
|
||||||
pragmaSet := fh.pragma.GetPragmaSetByIndex(funcDecl.Line.PragmaSetIndex)
|
pragmaSet := fh.pragma.GetPragmaSetByIndex(funcDecl.Line.PragmaSetIndex)
|
||||||
removeValue := pragmaSet.GetPragma("_P_REMOVE_UNUSED")
|
removeValue := pragmaSet.GetPragma("_P_REMOVE_UNUSED")
|
||||||
|
|
@ -1078,6 +1171,27 @@ func (fh *FunctionHandler) GetFunctionsWithRemovePragma() map[string]bool {
|
||||||
withPragma[funcDecl.Name] = true
|
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
|
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