Also removing all function vars, consts and labels when a function is removed on pragma _P_REMOVE_UNUSED

This commit is contained in:
Mattias Hansson 2026-04-13 21:28:16 +02:00
parent e5ee83d3dd
commit bc6fdaf8f2
4 changed files with 79 additions and 39 deletions

View file

@ -223,8 +223,11 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
// Analyze for overlapping absolute addresses in function call chains
c.checkAbsoluteOverlaps()
// Check for unused variables and print warnings
warnings := c.ctx.SymbolTable.CheckUnused()
// Get functions with _P_REMOVE_UNUSED pragma (for suppressing variable warnings)
funcsWithRemovePragma := c.ctx.FunctionHandler.GetFunctionsWithRemovePragma()
// Check for unused variables and print warnings (skip variables in functions with remove pragma)
warnings := c.ctx.SymbolTable.CheckUnused(funcsWithRemovePragma)
for _, warning := range warnings {
_, _ = fmt.Fprintf(os.Stderr, "%s\n", warning)
}
@ -236,10 +239,10 @@ func (c *Compiler) Compile(lines []preproc.Line) ([]string, error) {
}
// Remove unused functions with _P_REMOVE_UNUSED pragma
codeOutput = c.removeUnusedFunctions(codeOutput)
codeOutput, removedFuncs := c.removeUnusedFunctions(codeOutput)
// Assemble final output with headers and footers
return c.assembleOutput(codeOutput), nil
return c.assembleOutput(codeOutput, removedFuncs), nil
}
// printErrorWithContext prints an error with source code context to stderr
@ -454,10 +457,11 @@ func findPipeOutsideStrings(line string, startFrom int) int {
}
// removeUnusedFunctions removes functions marked with _P_REMOVE_UNUSED pragma from assembly output
func (c *Compiler) removeUnusedFunctions(codeLines []string) []string {
// Returns the filtered code lines and a map of removed function names
func (c *Compiler) removeUnusedFunctions(codeLines []string) ([]string, map[string]bool) {
toRemove := c.ctx.FunctionHandler.GetFunctionsToRemove()
if len(toRemove) == 0 {
return codeLines
return codeLines, nil
}
var result []string
@ -515,11 +519,11 @@ func (c *Compiler) removeUnusedFunctions(codeLines []string) []string {
result = append(result, line)
i++
}
return result
return result, toRemove
}
// assembleOutput combines all generated sections into final assembly
func (c *Compiler) assembleOutput(codeLines []string) []string {
func (c *Compiler) assembleOutput(codeLines []string, removedFuncs map[string]bool) []string {
var output []string
// Header comment
@ -527,12 +531,12 @@ func (c *Compiler) assembleOutput(codeLines []string) []string {
output = append(output, "")
// Constants section
if constLines := GenerateConstants(c.ctx.SymbolTable); len(constLines) > 0 {
if constLines := GenerateConstants(c.ctx.SymbolTable, removedFuncs); len(constLines) > 0 {
output = append(output, constLines...)
}
// Absolute addresses section
if absLines := GenerateAbsolutes(c.ctx.SymbolTable); len(absLines) > 0 {
if absLines := GenerateAbsolutes(c.ctx.SymbolTable, removedFuncs); len(absLines) > 0 {
output = append(output, absLines...)
}
@ -543,7 +547,7 @@ func (c *Compiler) assembleOutput(codeLines []string) []string {
output = append(output, "")
// Variables section
if varLines := GenerateVariables(c.ctx.SymbolTable); len(varLines) > 0 {
if varLines := GenerateVariables(c.ctx.SymbolTable, removedFuncs); len(varLines) > 0 {
output = append(output, varLines...)
}

View file

@ -1064,3 +1064,21 @@ func (fh *FunctionHandler) GetFunctionsToRemove() map[string]bool {
return toRemove
}
// 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
func (fh *FunctionHandler) GetFunctionsWithRemovePragma() map[string]bool {
withPragma := 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
}
}
}
return withPragma
}

View file

@ -339,7 +339,8 @@ func (st *SymbolTable) ConstantLookupFunc(currentScopes []string) func(string) (
// CheckUnused returns warnings for unused variables
// Returns slice of warning messages for regular variables (not constants, not absolutes) that were never used
func (st *SymbolTable) CheckUnused() []string {
// excludeFuncs is a map of function names that will be removed (e.g., have _P_REMOVE_UNUSED pragma)
func (st *SymbolTable) CheckUnused(excludeFuncs map[string]bool) []string {
var warnings []string
for _, sym := range st.symbols {
// Skip constants and absolute variables (they shouldn't track usage)
@ -353,6 +354,11 @@ func (st *SymbolTable) CheckUnused() []string {
continue
}
// Skip variables in functions that will be removed
if sym.Scope != "" && excludeFuncs != nil && excludeFuncs[sym.Scope] {
continue
}
// Check if pragma indicates we should ignore unused warnings for this variable
if st.pragma != nil {
pragmaSet := st.pragma.GetPragmaSetByIndex(sym.Line.PragmaSetIndex)
@ -412,7 +418,7 @@ func (s *Symbol) String() string {
// Code generation functions for ACME assembler syntax
// GenerateConstants generates constant definitions (name = $value)
func GenerateConstants(st *SymbolTable) []string {
func GenerateConstants(st *SymbolTable, excludeScopes map[string]bool) []string {
var lines []string
hasConsts := false
@ -420,6 +426,10 @@ func GenerateConstants(st *SymbolTable) []string {
if !sym.IsConst() {
continue
}
// Skip constants scoped to removed functions
if excludeScopes != nil && sym.Scope != "" && excludeScopes[sym.Scope] {
continue
}
hasConsts = true
var line string
@ -450,7 +460,7 @@ func GenerateConstants(st *SymbolTable) []string {
}
// GenerateAbsolutes generates absolute address assignments (name = $addr)
func GenerateAbsolutes(st *SymbolTable) []string {
func GenerateAbsolutes(st *SymbolTable, excludeScopes map[string]bool) []string {
var lines []string
hasAbsolutes := false
@ -458,6 +468,10 @@ func GenerateAbsolutes(st *SymbolTable) []string {
if !sym.IsAbsolute() {
continue
}
// Skip absolutes scoped to removed functions
if excludeScopes != nil && sym.Scope != "" && excludeScopes[sym.Scope] {
continue
}
hasAbsolutes = true
var line string
@ -488,7 +502,7 @@ func GenerateAbsolutes(st *SymbolTable) []string {
}
// GenerateVariables generates variable declarations (name !8 $value)
func GenerateVariables(st *SymbolTable) []string {
func GenerateVariables(st *SymbolTable, excludeScopes map[string]bool) []string {
var lines []string
hasVars := false
@ -497,6 +511,10 @@ func GenerateVariables(st *SymbolTable) []string {
if sym.IsConst() || sym.IsAbsolute() {
continue
}
// Skip variables scoped to removed functions
if excludeScopes != nil && sym.Scope != "" && excludeScopes[sym.Scope] {
continue
}
hasVars = true
var line string

View file

@ -490,7 +490,7 @@ func TestGenerateConstants(t *testing.T) {
st.AddConst("SIZE", "", KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("notconst", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) // should be skipped
lines := GenerateConstants(st)
lines := GenerateConstants(st, nil)
if len(lines) == 0 {
t.Fatal("expected output lines")
@ -531,7 +531,7 @@ func TestGenerateAbsolutes(t *testing.T) {
// Regular var (should be skipped)
st.AddVar("regular", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
lines := GenerateAbsolutes(st)
lines := GenerateAbsolutes(st, nil)
if len(lines) == 0 {
t.Fatal("expected output lines")
@ -580,7 +580,7 @@ func TestGenerateVariables(t *testing.T) {
// Absolute (should be skipped)
st.AddAbsolute("SKIP2", "", KindByte, 0x80, preproc.Line{Filename: "test.c65", LineNo: 1})
lines := GenerateVariables(st)
lines := GenerateVariables(st, nil)
if len(lines) == 0 {
t.Fatal("expected output lines")
@ -624,23 +624,23 @@ func TestGenerateEmpty(t *testing.T) {
st := NewSymbolTable()
// Empty table
if lines := GenerateConstants(st); lines != nil {
if lines := GenerateConstants(st, nil); lines != nil {
t.Error("expected nil for empty constants")
}
if lines := GenerateAbsolutes(st); lines != nil {
if lines := GenerateAbsolutes(st, nil); lines != nil {
t.Error("expected nil for empty absolutes")
}
if lines := GenerateVariables(st); lines != nil {
if lines := GenerateVariables(st, nil); lines != nil {
t.Error("expected nil for empty variables")
}
// Only variables (no constants/absolutes)
st.AddVar("test", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
if lines := GenerateConstants(st); lines != nil {
if lines := GenerateConstants(st, nil); lines != nil {
t.Error("expected nil when no constants exist")
}
if lines := GenerateAbsolutes(st); lines != nil {
if lines := GenerateAbsolutes(st, nil); lines != nil {
t.Error("expected nil when no absolutes exist")
}
}
@ -652,7 +652,7 @@ func TestGenerateScopedVariables(t *testing.T) {
st.AddVar("local", "main", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("nested", "main_helper", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
lines := GenerateVariables(st)
lines := GenerateVariables(st, nil)
output := strings.Join(lines, "\n")
// Check full names are used
@ -674,9 +674,9 @@ func TestGenerateHexLowercase(t *testing.T) {
st.AddAbsolute("ADDR", "", KindWord, 0xDEAD, preproc.Line{Filename: "test.c65", LineNo: 1})
st.AddVar("VAR", "", KindWord, 0xBEEF, preproc.Line{Filename: "test.c65", LineNo: 1})
constLines := GenerateConstants(st)
absLines := GenerateAbsolutes(st)
varLines := GenerateVariables(st)
constLines := GenerateConstants(st, nil)
absLines := GenerateAbsolutes(st, nil)
varLines := GenerateVariables(st, nil)
output := strings.Join(append(append(constLines, absLines...), varLines...), "\n")
@ -716,7 +716,7 @@ func TestUsageTracking(t *testing.T) {
}
// Check that warning is generated
warnings := st.CheckUnused()
warnings := st.CheckUnused(nil)
if len(warnings) != 1 {
t.Fatalf("CheckUnused() returned %d warnings, want 1", len(warnings))
}
@ -748,7 +748,7 @@ func TestUsageTracking(t *testing.T) {
}
// Check that no warning is generated
warnings := st.CheckUnused()
warnings := st.CheckUnused(nil)
if len(warnings) != 0 {
t.Errorf("CheckUnused() returned %d warnings, want 0: %v", len(warnings), warnings)
}
@ -764,7 +764,7 @@ func TestUsageTracking(t *testing.T) {
}
// Check that no warning is generated
warnings := st.CheckUnused()
warnings := st.CheckUnused(nil)
if len(warnings) != 0 {
t.Errorf("CheckUnused() returned %d warnings for constant, want 0: %v", len(warnings), warnings)
}
@ -786,7 +786,7 @@ func TestUsageTracking(t *testing.T) {
}
// Check that no warning is generated
warnings := st.CheckUnused()
warnings := st.CheckUnused(nil)
if len(warnings) != 0 {
t.Errorf("CheckUnused() returned %d warnings for absolute variable, want 0: %v", len(warnings), warnings)
}
@ -821,7 +821,7 @@ func TestUsageTracking(t *testing.T) {
st.Lookup("local_used", []string{"myFunc"})
// Check warnings
warnings := st.CheckUnused()
warnings := st.CheckUnused(nil)
if len(warnings) != 2 {
t.Fatalf("CheckUnused() returned %d warnings, want 2: %v", len(warnings), warnings)
}
@ -873,7 +873,7 @@ func TestUsageTracking(t *testing.T) {
}
// No warnings should be generated
warnings := st.CheckUnused()
warnings := st.CheckUnused(nil)
if len(warnings) != 0 {
t.Errorf("CheckUnused() returned %d warnings for used variable, want 0", len(warnings))
}
@ -900,7 +900,7 @@ func TestUsageTracking(t *testing.T) {
}
// Warning should be generated
warnings := st.CheckUnused()
warnings := st.CheckUnused(nil)
if len(warnings) != 1 {
t.Errorf("CheckUnused() returned %d warnings, want 1", len(warnings))
}
@ -922,7 +922,7 @@ func TestUsageTracking(t *testing.T) {
st.Lookup("used2", []string{"func1"})
// Check warnings
warnings := st.CheckUnused()
warnings := st.CheckUnused(nil)
if len(warnings) != 2 {
t.Fatalf("CheckUnused() returned %d warnings, want 2: %v", len(warnings), warnings)
}
@ -970,7 +970,7 @@ func TestUsageTracking(t *testing.T) {
}
// Since we haven't used it, it should generate a warning
warnings := st.CheckUnused()
warnings := st.CheckUnused(nil)
if len(warnings) != 1 {
t.Errorf("CheckUnused() returned %d warnings for label reference, want 1", len(warnings))
}
@ -979,7 +979,7 @@ func TestUsageTracking(t *testing.T) {
st.Lookup("handler", []string{})
// Now no warning should be generated
warnings = st.CheckUnused()
warnings = st.CheckUnused(nil)
if len(warnings) != 0 {
t.Errorf("CheckUnused() returned %d warnings for used label reference, want 0", len(warnings))
}
@ -1022,7 +1022,7 @@ func TestUsageTracking(t *testing.T) {
}
// Warning should be generated for the variable
warnings := st.CheckUnused()
warnings := st.CheckUnused(nil)
if len(warnings) != 1 {
t.Errorf("CheckUnused() returned %d warnings, want 1", len(warnings))
}
@ -1067,7 +1067,7 @@ func TestUsageTracking(t *testing.T) {
}
// No warnings should be generated
warnings := st.CheckUnused()
warnings := st.CheckUnused(nil)
if len(warnings) != 0 {
t.Errorf("CheckUnused() returned %d warnings for constants/absolutes, want 0", len(warnings))
}