auto_remove_unused_funcs #4

Merged
mattiashz merged 2 commits from auto_remove_unused_funcs into main 2026-04-13 20:49:44 +02:00
7 changed files with 143 additions and 22 deletions
Showing only changes of commit 652e147298 - Show all commits

View file

@ -38,6 +38,8 @@ func (c *FendCommand) Interpret(line preproc.Line, _ *compiler.CompilerContext)
}
func (c *FendCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) {
funcName := ctx.FunctionHandler.CurrentFunction()
ctx.FunctionHandler.EndFunction()
return []string{"\trts"}, nil
// Return RTS followed by end marker
return []string{"\trts", fmt.Sprintf("; @@FUNC_END %s", funcName)}, nil
}

View file

@ -1,6 +1,7 @@
package commands
import (
"fmt"
"strings"
"c65gm/internal/compiler"
@ -16,7 +17,7 @@ import (
// FUNC name ( in:x out:y io:z ) # with direction modifiers
// FUNC name ( {BYTE temp} out:result ) # with implicit declarations
type FuncCommand struct {
asmOutput []string
funcName string
}
func (c *FuncCommand) WillHandle(line preproc.Line) bool {
@ -28,17 +29,22 @@ func (c *FuncCommand) WillHandle(line preproc.Line) bool {
}
func (c *FuncCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error {
c.asmOutput = nil
c.funcName = ""
asm, err := ctx.FunctionHandler.HandleFuncDecl(line)
funcName, err := ctx.FunctionHandler.HandleFuncDecl(line)
if err != nil {
return err
}
c.asmOutput = asm
c.funcName = funcName
return nil
}
func (c *FuncCommand) Generate(_ *compiler.CompilerContext) ([]string, error) {
return c.asmOutput, nil
if c.funcName == "" {
return nil, nil // No function name parsed
}
// Prepend marker before the function label
marker := fmt.Sprintf("; @@FUNC_BEGIN %s", c.funcName)
return []string{marker, c.funcName}, nil
}

View file

@ -3,6 +3,7 @@ package compiler
import (
"fmt"
"os"
"path/filepath"
"strings"
"c65gm/internal/preproc"
@ -234,6 +235,9 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
_, _ = fmt.Fprintf(os.Stderr, "%s\n", warning)
}
// Remove unused functions with _P_REMOVE_UNUSED pragma
codeOutput = c.removeUnusedFunctions(codeOutput)
// Assemble final output with headers and footers
return c.assembleOutput(codeOutput), nil
}
@ -449,6 +453,71 @@ func findPipeOutsideStrings(line string, startFrom int) int {
return -1
}
// removeUnusedFunctions removes functions marked with _P_REMOVE_UNUSED pragma from assembly output
func (c *Compiler) removeUnusedFunctions(codeLines []string) []string {
toRemove := c.ctx.FunctionHandler.GetFunctionsToRemove()
if len(toRemove) == 0 {
return codeLines
}
var result []string
i := 0
for i < len(codeLines) {
line := codeLines[i]
// Check for function start marker
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
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)
}
// Skip everything until matching @@FUNC_END
foundEnd := false
for i < len(codeLines) {
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
}
}
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
}
}
result = append(result, line)
i++
}
return result
}
// assembleOutput combines all generated sections into final assembly
func (c *Compiler) assembleOutput(codeLines []string) []string {
var output []string

View file

@ -70,28 +70,29 @@ func NewFunctionHandler(st *SymbolTable, ls *LabelStack, csh *ConstantStringHand
// HandleFuncDecl parses and processes a FUNC declaration
// Syntax: FUNC name ( param1 param2 ... )
// Or: FUNC name (void function)
func (fh *FunctionHandler) HandleFuncDecl(line preproc.Line) ([]string, error) {
// Returns the function name
func (fh *FunctionHandler) HandleFuncDecl(line preproc.Line) (string, error) {
// Normalize parentheses and commas
text := fixIntuitiveFuncs(line.Text)
params, err := parseParams(text)
if err != nil {
return nil, fmt.Errorf("%s:%d: %w", line.Filename, line.LineNo, err)
return "", fmt.Errorf("%s:%d: %w", line.Filename, line.LineNo, err)
}
if len(params) < 2 {
return nil, fmt.Errorf("%s:%d: FUNC: expected at least function name", line.Filename, line.LineNo)
return "", fmt.Errorf("%s:%d: FUNC: expected at least function name", line.Filename, line.LineNo)
}
if strings.ToUpper(params[0]) != "FUNC" {
return nil, fmt.Errorf("%s:%d: not a FUNC declaration", line.Filename, line.LineNo)
return "", fmt.Errorf("%s:%d: not a FUNC declaration", line.Filename, line.LineNo)
}
funcName := params[1]
// Check for redeclaration
if fh.FuncExists(funcName) {
return nil, fmt.Errorf("%s:%d: function %q already declared", line.Filename, line.LineNo, funcName)
return "", fmt.Errorf("%s:%d: function %q already declared", line.Filename, line.LineNo, funcName)
}
// Push function name to current function stack early
@ -108,7 +109,7 @@ func (fh *FunctionHandler) HandleFuncDecl(line preproc.Line) ([]string, error) {
// FUNC name ( param1 param2 )
if params[2] != "(" || params[len(params)-1] != ")" {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1]
return nil, fmt.Errorf("%s:%d: FUNC: expected parentheses around parameters", line.Filename, line.LineNo)
return "", fmt.Errorf("%s:%d: FUNC: expected parentheses around parameters", line.Filename, line.LineNo)
}
// Extract params between ( and ) - need to handle {BYTE x} specially
@ -116,14 +117,14 @@ func (fh *FunctionHandler) HandleFuncDecl(line preproc.Line) ([]string, error) {
paramSpecs, err := buildComplexParams(rawParamTokens)
if err != nil {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1]
return nil, fmt.Errorf("%s:%d: FUNC %s: %w", line.Filename, line.LineNo, funcName, err)
return "", fmt.Errorf("%s:%d: FUNC %s: %w", line.Filename, line.LineNo, funcName, err)
}
for _, spec := range paramSpecs {
direction, varName, isImplicit, implicitDecl, err := parseParamSpec(spec)
if err != nil {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1]
return nil, fmt.Errorf("%s:%d: FUNC %s: %w", line.Filename, line.LineNo, funcName, err)
return "", fmt.Errorf("%s:%d: FUNC %s: %w", line.Filename, line.LineNo, funcName, err)
}
if isImplicit {
@ -131,7 +132,7 @@ func (fh *FunctionHandler) HandleFuncDecl(line preproc.Line) ([]string, error) {
// Format: {BYTE varname} or {WORD varname}
if err := fh.parseImplicitDecl(implicitDecl, funcName, line); err != nil {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1]
return nil, fmt.Errorf("%s:%d: FUNC %s: implicit declaration: %w", line.Filename, line.LineNo, funcName, err)
return "", fmt.Errorf("%s:%d: FUNC %s: implicit declaration: %w", line.Filename, line.LineNo, funcName, err)
}
}
@ -139,12 +140,12 @@ func (fh *FunctionHandler) HandleFuncDecl(line preproc.Line) ([]string, error) {
sym := fh.symTable.LookupWithoutUsage(varName, []string{funcName})
if sym == nil {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1]
return nil, fmt.Errorf("%s:%d: FUNC %s: parameter %q not declared", line.Filename, line.LineNo, funcName, varName)
return "", fmt.Errorf("%s:%d: FUNC %s: parameter %q not declared", line.Filename, line.LineNo, funcName, varName)
}
if sym.IsConst() {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1]
return nil, fmt.Errorf("%s:%d: FUNC %s: parameter %q cannot be a constant", line.Filename, line.LineNo, funcName, varName)
return "", fmt.Errorf("%s:%d: FUNC %s: parameter %q cannot be a constant", line.Filename, line.LineNo, funcName, varName)
}
funcParams = append(funcParams, &FuncParam{
@ -154,7 +155,7 @@ func (fh *FunctionHandler) HandleFuncDecl(line preproc.Line) ([]string, error) {
}
} else {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1]
return nil, fmt.Errorf("%s:%d: FUNC: invalid syntax", line.Filename, line.LineNo)
return "", fmt.Errorf("%s:%d: FUNC: invalid syntax", line.Filename, line.LineNo)
}
// Store function declaration
@ -167,8 +168,7 @@ func (fh *FunctionHandler) HandleFuncDecl(line preproc.Line) ([]string, error) {
// Record absolute addresses used by this function
fh.recordAbsoluteAddresses(funcName, funcParams)
// Generate assembler label
return []string{funcName}, nil
return funcName, nil
}
// recordAbsoluteAddresses stores which absolute addresses a function uses
@ -1034,3 +1034,27 @@ func (fh *FunctionHandler) CheckUnusedFunctions() []string {
return warnings
}
// 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
func (fh *FunctionHandler) GetFunctionsToRemove() map[string]bool {
toRemove := make(map[string]bool)
for _, funcDecl := range fh.functions {
// Skip functions that have been called
if fh.calledFunctions[funcDecl.Name] {
continue
}
// 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
}
}
}
return toRemove
}

View file

@ -206,11 +206,15 @@ func TestHandleFuncDecl_VoidFunction(t *testing.T) {
pragma := preproc.NewPragma()
fh := NewFunctionHandler(st, ls, csh, pragma)
asm, err := fh.HandleFuncDecl(makeLine("FUNC test_void"))
funcName, err := fh.HandleFuncDecl(makeLine("FUNC test_void"))
if err != nil {
t.Fatalf("HandleFuncDecl failed: %v", err)
}
if funcName != "test_void" {
t.Fatalf("expected funcName = \"test_void\", got %q", funcName)
}
if len(asm) != 1 {
t.Fatalf("expected 1 asm line, got %d", len(asm))
}
@ -293,7 +297,7 @@ func TestHandleFuncDecl_WithDirections(t *testing.T) {
pragma := preproc.NewPragma()
fh := NewFunctionHandler(st, ls, csh, pragma)
_, err := fh.HandleFuncDecl(makeLine("FUNC test_dir ( in:{BYTE a} out:{BYTE b} io:{WORD c} )"))
_, _, err := fh.HandleFuncDecl(makeLine("FUNC test_dir ( in:{BYTE a} out:{BYTE b} io:{WORD c} )"))
if err != nil {
t.Fatalf("HandleFuncDecl failed: %v", err)
}

View file

@ -676,12 +676,20 @@ Special characters in defines:
Control compiler behavior:
- `_P_USE_LONG_JUMP`: Use JMP instead of branches for large switch statements
- `_P_USE_IMMUTABLE_CODE`: Disable self-modifying code (for ROM)
- `_P_USE_CBM_STRINGS`: Use PETSCII encoding for strings
- `_P_IGNORE_UNUSED`: Suppress warnings for unused variables
- `_P_REMOVE_UNUSED`: Remove unused functions from assembly output (requires explicit pragma on each function)
```c65
#PRAGMA _P_USE_LONG_JUMP 1 // Use JMP instead of branches
#PRAGMA _P_USE_IMMUTABLE_CODE 1 // No self-modifying code (for ROM)
#PRAGMA _P_USE_CBM_STRINGS 1 // Use PETSCII encoding
#PRAGMA _P_IGNORE_UNUSED 1 // Suppress unused variable warnings
#PRAGMA _P_IGNORE_UNUSED 0 // Enable unused variable warnings
#PRAGMA _P_REMOVE_UNUSED 1 // Remove function if unused
#PRAGMA _P_REMOVE_UNUSED 0 // Keep function even if unused (default)
```
### Debug Directives

View file

@ -254,6 +254,12 @@ Sets compiler pragmas (options).
- Value: "0" enables warnings, any non-"0" value disables warnings
- Note: Do not use `=` sign, use space: `#PRAGMA _P_IGNORE_UNUSED 1`
**_P_REMOVE_UNUSED**
- When enabled (value ≠ "" and ≠ "0"), unused functions with this pragma will be removed from the final assembly output
- Requires function to be marked with `#PRAGMA _P_REMOVE_UNUSED 1` at function scope
- Functions are only removed if they are never called in the program
- The compiler will output an info message to stdout when a function is removed
**Examples:**
```
#PRAGMA _P_USE_LONG_JUMP 1
@ -262,6 +268,8 @@ Sets compiler pragmas (options).
#PRAGMA _P_USE_CBM_STRINGS 1
#PRAGMA _P_IGNORE_UNUSED 1 //suppress unused variable warnings
#PRAGMA _P_IGNORE_UNUSED 0 //enable unused variable warnings
#PRAGMA _P_REMOVE_UNUSED 1 //remove function if unused
#PRAGMA _P_REMOVE_UNUSED 0 //keep function even if unused (default)
```
---