Merge pull request 'auto_remove_unused_funcs' (#4) from auto_remove_unused_funcs into main

Reviewed-on: #4
This commit is contained in:
mattiashz 2026-04-13 20:49:43 +02:00
commit e5ee83d3dd
8 changed files with 213 additions and 51 deletions

View file

@ -116,12 +116,33 @@ All file operations must be restricted to the project's source code and document
3. Update documentation in appropriate .md files 3. Update documentation in appropriate .md files
4. Consider adding examples in `examples/` directory 4. Consider adding examples in `examples/` directory
### Testing ### Testing and Rebuilding
**IMPORTANT**: The agent cannot run tests. Provide these instructions to the user: **IMPORTANT**: The agent cannot run tests or compile code. Provide these instructions to the user:
#### 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 ./...`
#### Rebuilding c65gm:
When making changes to the compiler, ask the user to rebuild c65gm:
```bash
go build -o c65gm
```
This creates a fresh binary with your changes. The agent should:
1. Make code changes
2. Ask the user to rebuild c65gm: `go build -o c65gm`
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
**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:
```bash
C65LIBPATH=/app/lib ./c65gm -in input.c65 -out output.asm
```
## Common Pitfalls ## Common Pitfalls
1. **Expression evaluation**: Remember no operator precedence 1. **Expression evaluation**: Remember no operator precedence
@ -164,4 +185,25 @@ acme -f cbm -o output.prg output.asm
### Testing ### Testing
- `*_test.go` files throughout the codebase - `*_test.go` files throughout the codebase
- `examples/`: Functional test programs - `examples/`: Functional test programs
## Tool Notes
### Shell Environment
- **Shell**: Only `/bin/sh` (BusyBox) is available, not bash
- **The `bash` tool** in the interface runs commands through `/bin/sh` (BusyBox)
- **BusyBox limitations**: Many GNU extensions are not available
### grep Limitations
The environment uses BusyBox grep which doesn't support GNU grep extensions:
- **NOT supported**: `--include="*.go"`, `--exclude="*.md"`, etc.
- **Use instead**: `find /app -name "*.go" -type f -exec grep -l "pattern" {} \;`
- **Example**: Instead of `grep -r "CheckUnusedFunctions" /app --include="*.go"`, use:
```bash
find /app -name "*.go" -type f -exec grep -l "CheckUnusedFunctions" {} \;
```
### File Operations
- Use `find` with `-exec` for pattern-based file searches
- Use `grep` without GNU extensions for simple pattern matching
- Prefer the `bash` tool with proper BusyBox-compatible commands

View file

@ -38,6 +38,8 @@ 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()
ctx.FunctionHandler.EndFunction() 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 package commands
import ( import (
"fmt"
"strings" "strings"
"c65gm/internal/compiler" "c65gm/internal/compiler"
@ -16,7 +17,7 @@ import (
// FUNC name ( in:x out:y io:z ) # with direction modifiers // FUNC name ( in:x out:y io:z ) # with direction modifiers
// FUNC name ( {BYTE temp} out:result ) # with implicit declarations // FUNC name ( {BYTE temp} out:result ) # with implicit declarations
type FuncCommand struct { type FuncCommand struct {
asmOutput []string funcName string
} }
func (c *FuncCommand) WillHandle(line preproc.Line) bool { 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 { 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 { if err != nil {
return err return err
} }
c.asmOutput = asm c.funcName = funcName
return nil return nil
} }
func (c *FuncCommand) Generate(_ *compiler.CompilerContext) ([]string, error) { 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 ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"strings" "strings"
"c65gm/internal/preproc" "c65gm/internal/preproc"
@ -234,6 +235,9 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
_, _ = fmt.Fprintf(os.Stderr, "%s\n", warning) _, _ = 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 // Assemble final output with headers and footers
return c.assembleOutput(codeOutput), nil return c.assembleOutput(codeOutput), nil
} }
@ -449,6 +453,71 @@ func findPipeOutsideStrings(line string, startFrom int) int {
return -1 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 // assembleOutput combines all generated sections into final assembly
func (c *Compiler) assembleOutput(codeLines []string) []string { func (c *Compiler) assembleOutput(codeLines []string) []string {
var output []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 // HandleFuncDecl parses and processes a FUNC declaration
// Syntax: FUNC name ( param1 param2 ... ) // Syntax: FUNC name ( param1 param2 ... )
// Or: FUNC name (void function) // 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 // Normalize parentheses and commas
text := fixIntuitiveFuncs(line.Text) text := fixIntuitiveFuncs(line.Text)
params, err := parseParams(text) params, err := parseParams(text)
if err != nil { 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 { 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" { 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] funcName := params[1]
// Check for redeclaration // Check for redeclaration
if fh.FuncExists(funcName) { 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 // 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 ) // FUNC name ( param1 param2 )
if params[2] != "(" || params[len(params)-1] != ")" { if params[2] != "(" || params[len(params)-1] != ")" {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-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 // 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) paramSpecs, err := buildComplexParams(rawParamTokens)
if err != nil { if err != nil {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1] 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 { for _, spec := range paramSpecs {
direction, varName, isImplicit, implicitDecl, err := parseParamSpec(spec) direction, varName, isImplicit, implicitDecl, err := parseParamSpec(spec)
if err != nil { if err != nil {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1] 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 { if isImplicit {
@ -131,7 +132,7 @@ func (fh *FunctionHandler) HandleFuncDecl(line preproc.Line) ([]string, error) {
// Format: {BYTE varname} or {WORD varname} // Format: {BYTE varname} or {WORD varname}
if err := fh.parseImplicitDecl(implicitDecl, funcName, line); err != nil { if err := fh.parseImplicitDecl(implicitDecl, funcName, line); err != nil {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1] 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}) sym := fh.symTable.LookupWithoutUsage(varName, []string{funcName})
if sym == nil { if sym == nil {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1] 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() { if sym.IsConst() {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1] 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{ funcParams = append(funcParams, &FuncParam{
@ -154,7 +155,7 @@ func (fh *FunctionHandler) HandleFuncDecl(line preproc.Line) ([]string, error) {
} }
} else { } else {
fh.currentFuncs = fh.currentFuncs[:len(fh.currentFuncs)-1] 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 // Store function declaration
@ -167,8 +168,7 @@ func (fh *FunctionHandler) HandleFuncDecl(line preproc.Line) ([]string, error) {
// Record absolute addresses used by this function // Record absolute addresses used by this function
fh.recordAbsoluteAddresses(funcName, funcParams) fh.recordAbsoluteAddresses(funcName, funcParams)
// Generate assembler label return funcName, nil
return []string{funcName}, nil
} }
// recordAbsoluteAddresses stores which absolute addresses a function uses // recordAbsoluteAddresses stores which absolute addresses a function uses
@ -1020,10 +1020,16 @@ func (fh *FunctionHandler) CheckUnusedFunctions() []string {
// 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)
// Skip warning if _P_IGNORE_UNUSED is enabled
value := pragmaSet.GetPragma("_P_IGNORE_UNUSED") value := pragmaSet.GetPragma("_P_IGNORE_UNUSED")
if value != "" && value != "0" { if value != "" && value != "0" {
continue // Skip warning for this function continue // Skip warning for this function
} }
// Also skip warning if _P_REMOVE_UNUSED is enabled (function will be removed)
removeValue := pragmaSet.GetPragma("_P_REMOVE_UNUSED")
if removeValue != "" && removeValue != "0" {
continue // Skip warning for this function (it will be removed)
}
} }
// Format warning message with file and line info // Format warning message with file and line info
@ -1034,3 +1040,27 @@ func (fh *FunctionHandler) CheckUnusedFunctions() []string {
return warnings 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,16 +206,13 @@ func TestHandleFuncDecl_VoidFunction(t *testing.T) {
pragma := preproc.NewPragma() pragma := preproc.NewPragma()
fh := NewFunctionHandler(st, ls, csh, pragma) 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 { if err != nil {
t.Fatalf("HandleFuncDecl failed: %v", err) t.Fatalf("HandleFuncDecl failed: %v", err)
} }
if len(asm) != 1 { if funcName != "test_void" {
t.Fatalf("expected 1 asm line, got %d", len(asm)) t.Fatalf("expected funcName = \"test_void\", got %q", funcName)
}
if asm[0] != "test_void" {
t.Errorf("expected label 'test_void', got %q", asm[0])
} }
if !fh.FuncExists("test_void") { if !fh.FuncExists("test_void") {
@ -234,13 +231,13 @@ func TestHandleFuncDecl_WithExistingParams(t *testing.T) {
st.AddVar("x", "test_func", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("x", "test_func", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("y", "test_func", KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("y", "test_func", KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
asm, err := fh.HandleFuncDecl(makeLine("FUNC test_func ( x y )")) funcName, err := fh.HandleFuncDecl(makeLine("FUNC test_func ( x y )"))
if err != nil { if err != nil {
t.Fatalf("HandleFuncDecl failed: %v", err) t.Fatalf("HandleFuncDecl failed: %v", err)
} }
if len(asm) != 1 { if funcName != "test_func" {
t.Fatalf("expected 1 asm line, got %d", len(asm)) t.Fatalf("expected funcName = \"test_func\", got %q", funcName)
} }
funcDecl := fh.findFunc("test_func") funcDecl := fh.findFunc("test_func")
@ -259,13 +256,13 @@ func TestHandleFuncDecl_ImplicitDeclarations(t *testing.T) {
pragma := preproc.NewPragma() pragma := preproc.NewPragma()
fh := NewFunctionHandler(st, ls, csh, pragma) fh := NewFunctionHandler(st, ls, csh, pragma)
asm, err := fh.HandleFuncDecl(makeLine("FUNC test_impl ( {BYTE a} {WORD b} )")) funcName, err := fh.HandleFuncDecl(makeLine("FUNC test_impl ( {BYTE a} {WORD b} )"))
if err != nil { if err != nil {
t.Fatalf("HandleFuncDecl failed: %v", err) t.Fatalf("HandleFuncDecl failed: %v", err)
} }
if len(asm) != 1 { if funcName != "test_impl" {
t.Fatalf("expected 1 asm line, got %d", len(asm)) t.Fatalf("expected funcName = \"test_impl\", got %q", funcName)
} }
// Check that variables were declared // Check that variables were declared
@ -365,7 +362,7 @@ func TestHandleFuncDecl_Errors(t *testing.T) {
// Special case for redeclaration test // Special case for redeclaration test
if tt.name == "redeclaration" { if tt.name == "redeclaration" {
fh.HandleFuncDecl(makeLine("FUNC duplicate ( {BYTE x} )")) _, _ = fh.HandleFuncDecl(makeLine("FUNC duplicate ( {BYTE x} )"))
} }
_, err := fh.HandleFuncDecl(makeLine(tt.line)) _, err := fh.HandleFuncDecl(makeLine(tt.line))
@ -389,7 +386,7 @@ func TestHandleFuncCall_VarArgs(t *testing.T) {
// Declare function with params // Declare function with params
st.AddVar("param_a", "test_func", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("param_a", "test_func", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("param_b", "test_func", KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("param_b", "test_func", KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
fh.HandleFuncDecl(makeLine("FUNC test_func ( param_a param_b )")) _, _ = fh.HandleFuncDecl(makeLine("FUNC test_func ( param_a param_b )"))
// Declare caller variables // Declare caller variables
st.AddVar("var_a", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("var_a", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
@ -431,7 +428,7 @@ func TestHandleFuncCall_OutParams(t *testing.T) {
// Declare function with out param // Declare function with out param
st.AddVar("result", "get_result", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("result", "get_result", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
fh.HandleFuncDecl(makeLine("FUNC get_result ( out:result )")) _, _ = fh.HandleFuncDecl(makeLine("FUNC get_result ( out:result )"))
// Declare caller variable // Declare caller variable
st.AddVar("output", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("output", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
@ -471,7 +468,7 @@ func TestHandleFuncCall_ConstArgs(t *testing.T) {
// Declare function // Declare function
st.AddVar("x", "test_const", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("x", "test_const", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("y", "test_const", KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("y", "test_const", KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
fh.HandleFuncDecl(makeLine("FUNC test_const ( x y )")) _, _ = fh.HandleFuncDecl(makeLine("FUNC test_const ( x y )"))
asm, err := fh.HandleFuncCall(makeLine("CALL test_const ( 42 $1234 )")) asm, err := fh.HandleFuncCall(makeLine("CALL test_const ( 42 $1234 )"))
if err != nil { if err != nil {
@ -507,7 +504,7 @@ func TestHandleFuncCall_LabelArg(t *testing.T) {
// Declare function // Declare function
st.AddVar("ptr", "test_label", KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("ptr", "test_label", KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
fh.HandleFuncDecl(makeLine("FUNC test_label ( ptr )")) _, _ = fh.HandleFuncDecl(makeLine("FUNC test_label ( ptr )"))
asm, err := fh.HandleFuncCall(makeLine("CALL test_label ( @my_label )")) asm, err := fh.HandleFuncCall(makeLine("CALL test_label ( @my_label )"))
if err != nil { if err != nil {
@ -540,7 +537,7 @@ func TestHandleFuncCall_StringArg(t *testing.T) {
// Declare function // Declare function
st.AddVar("str_ptr", "print", KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("str_ptr", "print", KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
fh.HandleFuncDecl(makeLine("FUNC print ( str_ptr )")) _, _ = fh.HandleFuncDecl(makeLine("FUNC print ( str_ptr )"))
asm, err := fh.HandleFuncCall(makeLine(`CALL print ( "hello" )`)) asm, err := fh.HandleFuncCall(makeLine(`CALL print ( "hello" )`))
if err != nil { if err != nil {
@ -583,7 +580,7 @@ func TestHandleFuncCall_Errors(t *testing.T) {
name: "wrong arg count", name: "wrong arg count",
setup: func(fh *FunctionHandler, st *SymbolTable) { setup: func(fh *FunctionHandler, st *SymbolTable) {
st.AddVar("x", "test", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("x", "test", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
fh.HandleFuncDecl(makeLine("FUNC test ( x )")) _, _ = fh.HandleFuncDecl(makeLine("FUNC test ( x )"))
}, },
line: "CALL test ( 1 2 )", line: "CALL test ( 1 2 )",
wantErr: "expected 1 arguments, got 2", wantErr: "expected 1 arguments, got 2",
@ -594,7 +591,7 @@ func TestHandleFuncCall_Errors(t *testing.T) {
name: "type mismatch", name: "type mismatch",
setup: func(fh *FunctionHandler, st *SymbolTable) { setup: func(fh *FunctionHandler, st *SymbolTable) {
st.AddVar("param", "test", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("param", "test", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
fh.HandleFuncDecl(makeLine("FUNC test ( param )")) _, _ = fh.HandleFuncDecl(makeLine("FUNC test ( param )"))
st.AddVar("wvar", "", KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("wvar", "", KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
}, },
line: "CALL test ( wvar )", line: "CALL test ( wvar )",
@ -604,7 +601,7 @@ func TestHandleFuncCall_Errors(t *testing.T) {
name: "const to out param", name: "const to out param",
setup: func(fh *FunctionHandler, st *SymbolTable) { setup: func(fh *FunctionHandler, st *SymbolTable) {
st.AddVar("result", "test", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("result", "test", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
fh.HandleFuncDecl(makeLine("FUNC test ( out:result )")) _, _ = fh.HandleFuncDecl(makeLine("FUNC test ( out:result )"))
}, },
line: "CALL test ( 42 )", line: "CALL test ( 42 )",
wantErr: "out/io parameter", wantErr: "out/io parameter",
@ -613,7 +610,7 @@ func TestHandleFuncCall_Errors(t *testing.T) {
name: "label to byte param", name: "label to byte param",
setup: func(fh *FunctionHandler, st *SymbolTable) { setup: func(fh *FunctionHandler, st *SymbolTable) {
st.AddVar("x", "test", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("x", "test", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
fh.HandleFuncDecl(makeLine("FUNC test ( x )")) _, _ = fh.HandleFuncDecl(makeLine("FUNC test ( x )"))
}, },
line: "CALL test ( @label )", line: "CALL test ( @label )",
wantErr: "byte parameter", wantErr: "byte parameter",
@ -649,7 +646,7 @@ func TestHandleFuncCall_EmptyParens(t *testing.T) {
fh := NewFunctionHandler(st, ls, csh, pragma) fh := NewFunctionHandler(st, ls, csh, pragma)
// Declare void function // Declare void function
fh.HandleFuncDecl(makeLine("FUNC init")) _, _ = fh.HandleFuncDecl(makeLine("FUNC init"))
tests := []struct { tests := []struct {
name string name string
@ -692,7 +689,7 @@ func TestEndFunction(t *testing.T) {
fh := NewFunctionHandler(st, ls, csh, pragma) fh := NewFunctionHandler(st, ls, csh, pragma)
// Declare function (pushes to stack) // Declare function (pushes to stack)
fh.HandleFuncDecl(makeLine("FUNC test ( {BYTE x} )")) _, _ = fh.HandleFuncDecl(makeLine("FUNC test ( {BYTE x} )"))
if fh.CurrentFunction() != "test" { if fh.CurrentFunction() != "test" {
t.Errorf("current function = %q, want 'test'", fh.CurrentFunction()) t.Errorf("current function = %q, want 'test'", fh.CurrentFunction())
@ -717,12 +714,12 @@ func TestCurrentFunction(t *testing.T) {
t.Error("expected empty current function initially") t.Error("expected empty current function initially")
} }
fh.HandleFuncDecl(makeLine("FUNC func1 ( {BYTE x} )")) _, _ = fh.HandleFuncDecl(makeLine("FUNC func1 ( {BYTE x} )"))
if fh.CurrentFunction() != "func1" { if fh.CurrentFunction() != "func1" {
t.Errorf("expected 'func1', got %q", fh.CurrentFunction()) t.Errorf("expected 'func1', got %q", fh.CurrentFunction())
} }
fh.HandleFuncDecl(makeLine("FUNC func2 ( {BYTE y} )")) _, _ = fh.HandleFuncDecl(makeLine("FUNC func2 ( {BYTE y} )"))
if fh.CurrentFunction() != "func2" { if fh.CurrentFunction() != "func2" {
t.Errorf("expected 'func2', got %q", fh.CurrentFunction()) t.Errorf("expected 'func2', got %q", fh.CurrentFunction())
} }
@ -918,7 +915,7 @@ func TestHandleFuncCall_AbsoluteParams(t *testing.T) {
fh := NewFunctionHandler(st, ls, csh, pragma) fh := NewFunctionHandler(st, ls, csh, pragma)
// Declare function with absolute params // Declare function with absolute params
fh.HandleFuncDecl(makeLine("FUNC test_abs ( {BYTE param_a @ $fa} {WORD param_b @ $fb} )")) _, _ = fh.HandleFuncDecl(makeLine("FUNC test_abs ( {BYTE param_a @ $fa} {WORD param_b @ $fb} )"))
// Declare caller variables // Declare caller variables
st.AddVar("var_a", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("var_a", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
@ -959,7 +956,7 @@ func TestHandleFuncCall_AbsoluteOutParams(t *testing.T) {
fh := NewFunctionHandler(st, ls, csh, pragma) fh := NewFunctionHandler(st, ls, csh, pragma)
// Declare function with absolute out param // Declare function with absolute out param
fh.HandleFuncDecl(makeLine("FUNC get_result ( out:{BYTE result @ $fa} )")) _, _ = fh.HandleFuncDecl(makeLine("FUNC get_result ( out:{BYTE result @ $fa} )"))
// Declare caller variable // Declare caller variable
st.AddVar("output", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("output", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})

View file

@ -676,12 +676,20 @@ Special characters in defines:
Control compiler behavior: 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 ```c65
#PRAGMA _P_USE_LONG_JUMP 1 // Use JMP instead of branches #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_IMMUTABLE_CODE 1 // No self-modifying code (for ROM)
#PRAGMA _P_USE_CBM_STRINGS 1 // Use PETSCII encoding #PRAGMA _P_USE_CBM_STRINGS 1 // Use PETSCII encoding
#PRAGMA _P_IGNORE_UNUSED 1 // Suppress unused variable warnings #PRAGMA _P_IGNORE_UNUSED 1 // Suppress unused variable warnings
#PRAGMA _P_IGNORE_UNUSED 0 // Enable 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 ### Debug Directives

View file

@ -254,6 +254,12 @@ Sets compiler pragmas (options).
- Value: "0" enables warnings, any non-"0" value disables warnings - Value: "0" enables warnings, any non-"0" value disables warnings
- Note: Do not use `=` sign, use space: `#PRAGMA _P_IGNORE_UNUSED 1` - 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:** **Examples:**
``` ```
#PRAGMA _P_USE_LONG_JUMP 1 #PRAGMA _P_USE_LONG_JUMP 1
@ -262,6 +268,8 @@ Sets compiler pragmas (options).
#PRAGMA _P_USE_CBM_STRINGS 1 #PRAGMA _P_USE_CBM_STRINGS 1
#PRAGMA _P_IGNORE_UNUSED 1 //suppress unused variable warnings #PRAGMA _P_IGNORE_UNUSED 1 //suppress unused variable warnings
#PRAGMA _P_IGNORE_UNUSED 0 //enable 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)
``` ```
--- ---