Fix pragma auto-removal for multi-FUNC groups sharing one FEND

This commit is contained in:
Mattias Hansson 2026-05-17 16:23:14 +02:00
parent 8775ebaf43
commit b6fce2a7f9
8 changed files with 482 additions and 73 deletions

View file

@ -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
View 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

View file

@ -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

View file

@ -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>

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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,15 +1151,45 @@ 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
}
}
}
}

View file

@ -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)
}
})
}