package compiler import ( "strings" "testing" "c65gm/internal/preproc" ) func TestSymbolFlags(t *testing.T) { tests := []struct { name string flags SymbolFlags isByte bool isWord bool isConst bool isAbs bool isZP bool isZPPtr bool }{ { name: "byte variable", flags: FlagByte, isByte: true, isWord: false, isConst: false, }, { name: "word variable", flags: FlagWord, isByte: false, isWord: true, isConst: false, }, { name: "byte constant", flags: FlagByte | FlagConst, isByte: true, isConst: true, }, { name: "absolute zero-page", flags: FlagWord | FlagAbsolute | FlagZeroPage, isWord: true, isAbs: true, isZP: true, isZPPtr: true, }, { name: "absolute non-zero-page", flags: FlagWord | FlagAbsolute, isWord: true, isAbs: true, isZP: false, isZPPtr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &Symbol{Flags: tt.flags} if s.IsByte() != tt.isByte { t.Errorf("IsByte() = %v, want %v", s.IsByte(), tt.isByte) } if s.IsWord() != tt.isWord { t.Errorf("IsWord() = %v, want %v", s.IsWord(), tt.isWord) } if s.IsConst() != tt.isConst { t.Errorf("IsConst() = %v, want %v", s.IsConst(), tt.isConst) } if s.IsAbsolute() != tt.isAbs { t.Errorf("IsAbsolute() = %v, want %v", s.IsAbsolute(), tt.isAbs) } if s.IsZeroPage() != tt.isZP { t.Errorf("IsZeroPage() = %v, want %v", s.IsZeroPage(), tt.isZP) } if s.IsZeroPagePointer() != tt.isZPPtr { t.Errorf("IsZeroPagePointer() = %v, want %v", s.IsZeroPagePointer(), tt.isZPPtr) } }) } } func TestSymbolFullName(t *testing.T) { tests := []struct { name string symName string scope string expected string }{ {"global", "counter", "", "counter"}, {"local", "temp", "main", "main_temp"}, {"nested", "var", "outer_inner", "outer_inner_var"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &Symbol{Name: tt.symName, Scope: tt.scope} if got := s.FullName(); got != tt.expected { t.Errorf("FullName() = %q, want %q", got, tt.expected) } }) } } func TestAddVar(t *testing.T) { st := NewSymbolTable() // Add byte var err := st.AddVar("counter", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) if err != nil { t.Fatalf("AddVar() error = %v", err) } sym := st.Get("counter") if sym == nil { t.Fatal("symbol not found") } if !sym.IsByte() { t.Error("expected byte variable") } if sym.IsConst() { t.Error("should not be const") } // Add word var err = st.AddVar("ptr", "", KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1}) if err != nil { t.Fatalf("AddVar() error = %v", err) } sym = st.Get("ptr") if !sym.IsWord() { t.Error("expected word variable") } if sym.Value != 0x1234 { t.Errorf("Value = %d, want %d", sym.Value, 0x1234) } // Test byte value range check err = st.AddVar("bad", "", KindByte, 256, preproc.Line{Filename: "test.c65", LineNo: 1}) if err == nil { t.Error("expected error for byte value > 255") } } func TestAddConst(t *testing.T) { st := NewSymbolTable() err := st.AddConst("MAX", "", KindByte, 255, preproc.Line{Filename: "test.c65", LineNo: 1}) if err != nil { t.Fatalf("AddConst() error = %v", err) } sym := st.Get("MAX") if sym == nil { t.Fatal("symbol not found") } if !sym.IsConst() { t.Error("expected constant") } if !sym.IsByte() { t.Error("expected byte constant") } if sym.Value != 255 { t.Errorf("Value = %d, want 255", sym.Value) } // Test byte range check err = st.AddConst("BAD", "", KindByte, 300, preproc.Line{Filename: "test.c65", LineNo: 1}) if err == nil { t.Error("expected error for byte const > 255") } } func TestAddAbsolute(t *testing.T) { st := NewSymbolTable() // Zero-page byte err := st.AddAbsolute("ZP_VAR", "", KindByte, 0x80, preproc.Line{Filename: "test.c65", LineNo: 1}) if err != nil { t.Fatalf("AddAbsolute() error = %v", err) } sym := st.Get("ZP_VAR") if !sym.IsAbsolute() { t.Error("expected absolute") } if !sym.IsZeroPage() { t.Error("expected zero-page flag for addr < $100") } if sym.AbsAddr != 0x80 { t.Errorf("AbsAddr = $%04X, want $0080", sym.AbsAddr) } // Zero-page word pointer err = st.AddAbsolute("ZP_PTR", "", KindWord, 0xFE, preproc.Line{Filename: "test.c65", LineNo: 1}) if err != nil { t.Fatalf("AddAbsolute() error = %v", err) } sym = st.Get("ZP_PTR") if !sym.IsZeroPagePointer() { t.Error("expected zero-page pointer (word addr < $FF)") } // Non-zero-page err = st.AddAbsolute("VIC", "", KindWord, 0xD000, preproc.Line{Filename: "test.c65", LineNo: 1}) if err != nil { t.Fatalf("AddAbsolute() error = %v", err) } sym = st.Get("VIC") if !sym.IsAbsolute() { t.Error("expected absolute") } if sym.IsZeroPage() { t.Error("should not have zero-page flag") } if sym.AbsAddr != 0xD000 { t.Errorf("AbsAddr = $%04X, want $D000", sym.AbsAddr) } } func TestAddLabel(t *testing.T) { st := NewSymbolTable() err := st.AddLabel("handler", "", "irq_vector", preproc.Line{Filename: "test.c65", LineNo: 1}) if err != nil { t.Fatalf("AddLabel() error = %v", err) } sym := st.Get("handler") if sym == nil { t.Fatal("symbol not found") } if !sym.IsWord() { t.Error("label ref should be word") } if !sym.Has(FlagLabelRef) { t.Error("expected FlagLabelRef") } if sym.LabelRef != "irq_vector" { t.Errorf("LabelRef = %q, want %q", sym.LabelRef, "irq_vector") } } func TestRedeclaration(t *testing.T) { st := NewSymbolTable() err := st.AddVar("test", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) if err != nil { t.Fatalf("first AddVar() error = %v", err) } // Attempt redeclaration err = st.AddVar("test", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) if err == nil { t.Error("expected error on redeclaration") } // Different scope should be OK err = st.AddVar("test", "main", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) if err != nil { t.Errorf("AddVar() with different scope error = %v", err) } } func TestLookup(t *testing.T) { st := NewSymbolTable() // Global variable st.AddVar("global", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) // Local in main st.AddVar("local", "main", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) // Local in nested function st.AddVar("inner", "main_helper", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) tests := []struct { name string searchName string currentScopes []string expectFound bool expectFull string }{ { name: "find global from empty scope", searchName: "global", currentScopes: []string{}, expectFound: true, expectFull: "global", }, { name: "find global from main scope", searchName: "global", currentScopes: []string{"main"}, expectFound: true, expectFull: "global", }, { name: "find local from same scope", searchName: "local", currentScopes: []string{"main"}, expectFound: true, expectFull: "main_local", }, { name: "shadow global with local", searchName: "local", currentScopes: []string{"main"}, expectFound: true, expectFull: "main_local", }, { name: "find inner from nested scope", searchName: "inner", currentScopes: []string{"main", "main_helper"}, expectFound: true, expectFull: "main_helper_inner", }, { name: "not found", searchName: "notexist", currentScopes: []string{}, expectFound: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sym := st.Lookup(tt.searchName, tt.currentScopes) if tt.expectFound { if sym == nil { t.Fatal("symbol not found") } if got := sym.FullName(); got != tt.expectFull { t.Errorf("FullName() = %q, want %q", got, tt.expectFull) } } else { if sym != nil { t.Errorf("expected not found, got %v", sym) } } }) } } func TestExpandName(t *testing.T) { st := NewSymbolTable() st.AddVar("global", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("local", "main", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) tests := []struct { name string searchName string currentScopes []string expected string }{ {"expand local", "local", []string{"main"}, "main_local"}, {"expand global", "global", []string{"main"}, "global"}, {"no expansion needed", "notfound", []string{}, "notfound"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := st.ExpandName(tt.searchName, tt.currentScopes) if got != tt.expected { t.Errorf("ExpandName() = %q, want %q", got, tt.expected) } }) } } func TestInsertionOrder(t *testing.T) { st := NewSymbolTable() names := []string{"first", "second", "third"} for _, name := range names { st.AddVar(name, "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) } symbols := st.Symbols() if len(symbols) != len(names) { t.Fatalf("Count = %d, want %d", len(symbols), len(names)) } for i, name := range names { if symbols[i].Name != name { t.Errorf("symbols[%d].Name = %q, want %q", i, symbols[i].Name, name) } } } func TestCount(t *testing.T) { st := NewSymbolTable() if st.Count() != 0 { t.Errorf("initial Count() = %d, want 0", st.Count()) } st.AddVar("a", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddVar("b", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) if st.Count() != 2 { t.Errorf("Count() = %d, want 2", st.Count()) } } func TestSymbolString(t *testing.T) { tests := []struct { name string setup func(*SymbolTable) *Symbol contains []string }{ { name: "byte variable", setup: func(st *SymbolTable) *Symbol { st.AddVar("test", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) return st.Get("test") }, contains: []string{"Name=test", "BYTE"}, }, { name: "word constant", setup: func(st *SymbolTable) *Symbol { st.AddConst("MAX", "", KindWord, 65535, preproc.Line{Filename: "test.c65", LineNo: 1}) return st.Get("MAX") }, contains: []string{"Name=MAX", "WORD", "CONST=65535"}, }, { name: "zero-page pointer", setup: func(st *SymbolTable) *Symbol { st.AddAbsolute("ptr", "", KindWord, 0x80, preproc.Line{Filename: "test.c65", LineNo: 1}) return st.Get("ptr") }, contains: []string{"Name=ptr", "WORD", "@$0080", "ZP"}, }, { name: "label reference", setup: func(st *SymbolTable) *Symbol { st.AddLabel("handler", "", "irq", preproc.Line{Filename: "test.c65", LineNo: 1}) return st.Get("handler") }, contains: []string{"Name=handler", "->irq"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { st := NewSymbolTable() sym := tt.setup(st) str := sym.String() for _, want := range tt.contains { if !containsSubstring(str, want) { t.Errorf("String() = %q, missing %q", str, want) } } }) } } func containsSubstring(s, substr string) bool { return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && containsAt(s, substr)) } func containsAt(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false } // Code generation tests func TestGenerateConstants(t *testing.T) { st := NewSymbolTable() // Add some constants st.AddConst("MAX", "", KindByte, 255, preproc.Line{Filename: "test.c65", LineNo: 1}) 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) if len(lines) == 0 { t.Fatal("expected output lines") } // Check header if lines[0] != ";Constant values (from c65gm)" { t.Errorf("expected header comment, got %q", lines[0]) } // Find constant definitions output := strings.Join(lines, "\n") if !strings.Contains(output, "MAX = $ff") { t.Error("expected byte constant with lowercase hex") } if !strings.Contains(output, "; 255") { t.Error("expected decimal comment for byte constant") } if !strings.Contains(output, "SIZE = $1234") { t.Error("expected word constant") } if strings.Contains(output, "notconst") { t.Error("regular variable should not appear in constants") } } func TestGenerateAbsolutes(t *testing.T) { st := NewSymbolTable() // Zero-page st.AddAbsolute("ZP_VAR", "", KindByte, 0x80, preproc.Line{Filename: "test.c65", LineNo: 1}) st.AddAbsolute("ZP_PTR", "", KindWord, 0xFE, preproc.Line{Filename: "test.c65", LineNo: 1}) // Non-zero-page st.AddAbsolute("VIC", "", KindWord, 0xD000, preproc.Line{Filename: "test.c65", LineNo: 1}) // Regular var (should be skipped) st.AddVar("regular", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) lines := GenerateAbsolutes(st) if len(lines) == 0 { t.Fatal("expected output lines") } // Check header if lines[0] != ";Absolute variable definitions (from c65gm)" { t.Errorf("expected header comment, got %q", lines[0]) } output := strings.Join(lines, "\n") // Zero-page should use 2 hex digits if !strings.Contains(output, "ZP_VAR = $80") { t.Error("expected zero-page with 2 hex digits") } if !strings.Contains(output, "ZP_PTR = $fe") { t.Error("expected zero-page pointer with 2 hex digits") } // Non-zero-page should use 4 hex digits if !strings.Contains(output, "VIC = $d000") { t.Error("expected non-zero-page with 4 hex digits") } if strings.Contains(output, "regular") { t.Error("regular variable should not appear in absolutes") } } func TestGenerateVariables(t *testing.T) { st := NewSymbolTable() // Byte variable st.AddVar("counter", "", KindByte, 42, preproc.Line{Filename: "test.c65", LineNo: 1}) // Word variable st.AddVar("ptr", "", KindWord, 0x1234, preproc.Line{Filename: "test.c65", LineNo: 1}) // Label reference st.AddLabel("handler", "", "irq_routine", preproc.Line{Filename: "test.c65", LineNo: 1}) // Const (should be skipped) st.AddConst("SKIP", "", KindByte, 99, preproc.Line{Filename: "test.c65", LineNo: 1}) // Absolute (should be skipped) st.AddAbsolute("SKIP2", "", KindByte, 0x80, preproc.Line{Filename: "test.c65", LineNo: 1}) lines := GenerateVariables(st) if len(lines) == 0 { t.Fatal("expected output lines") } // Check header if lines[0] != ";Variables (from c65gm)" { t.Errorf("expected header comment, got %q", lines[0]) } output := strings.Join(lines, "\n") // Byte variable with decimal comment if !strings.Contains(output, "counter\t!8 $2a") { t.Error("expected byte variable declaration") } if !strings.Contains(output, "; 42") { t.Error("expected decimal comment for byte") } // Word variable (low, high) if !strings.Contains(output, "ptr\t!8 $34, $12") { t.Error("expected word variable as two bytes (lo, hi)") } // Label reference if !strings.Contains(output, "handler\t!8 irq_routine") { t.Error("expected label reference with < and >") } // Should not contain constants or absolutes if strings.Contains(output, "SKIP") { t.Error("constants should not appear in variables") } if strings.Contains(output, "SKIP2") { t.Error("absolutes should not appear in variables") } } func TestGenerateEmpty(t *testing.T) { st := NewSymbolTable() // Empty table if lines := GenerateConstants(st); lines != nil { t.Error("expected nil for empty constants") } if lines := GenerateAbsolutes(st); lines != nil { t.Error("expected nil for empty absolutes") } if lines := GenerateVariables(st); 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 { t.Error("expected nil when no constants exist") } if lines := GenerateAbsolutes(st); lines != nil { t.Error("expected nil when no absolutes exist") } } func TestGenerateScopedVariables(t *testing.T) { st := NewSymbolTable() st.AddVar("global", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1}) 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) output := strings.Join(lines, "\n") // Check full names are used if !strings.Contains(output, "global\t!8") { t.Error("expected global variable") } if !strings.Contains(output, "main_local\t!8") { t.Error("expected scoped variable with full name") } if !strings.Contains(output, "main_helper_nested\t!8") { t.Error("expected nested scoped variable with full name") } } func TestGenerateHexLowercase(t *testing.T) { st := NewSymbolTable() st.AddConst("TEST", "", KindByte, 0xAB, preproc.Line{Filename: "test.c65", LineNo: 1}) 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) output := strings.Join(append(append(constLines, absLines...), varLines...), "\n") // Check all hex is lowercase if strings.Contains(output, "$AB") || strings.Contains(output, "$DEAD") || strings.Contains(output, "$BEEF") { t.Error("hex digits should be lowercase") } if !strings.Contains(output, "$ab") { t.Error("expected lowercase hex in constant") } if !strings.Contains(output, "$dead") { t.Error("expected lowercase hex in absolute") } if !strings.Contains(output, "$be") && !strings.Contains(output, "$ef") { t.Error("expected lowercase hex in variable") } } func TestUsageTracking(t *testing.T) { t.Run("Unused variable generates warning", func(t *testing.T) { st := NewSymbolTable() // Add an unused variable err := st.AddVar("unused", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 10}) if err != nil { t.Fatalf("AddVar() error = %v", err) } // Check that it's marked as unused sym := st.Get("unused") if sym == nil { t.Fatal("symbol not found") } if sym.IsUsed() { t.Errorf("IsUsed() = true, want false") } // Check that warning is generated warnings := st.CheckUnused() if len(warnings) != 1 { t.Fatalf("CheckUnused() returned %d warnings, want 1", len(warnings)) } expected := "test.c65:10: warning: variable 'unused' declared but never used" if warnings[0] != expected { t.Errorf("warning = %q, want %q", warnings[0], expected) } }) t.Run("Used variable doesn't generate warning", func(t *testing.T) { st := NewSymbolTable() // Add a variable err := st.AddVar("used", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 20}) if err != nil { t.Fatalf("AddVar() error = %v", err) } // Mark it as used via Lookup (simulating actual usage) sym := st.Lookup("used", []string{}) if sym == nil { t.Fatal("symbol not found") } // Check that it's marked as used if !sym.IsUsed() { t.Errorf("IsUsed() = false, want true") } // Check that no warning is generated warnings := st.CheckUnused() if len(warnings) != 0 { t.Errorf("CheckUnused() returned %d warnings, want 0: %v", len(warnings), warnings) } }) t.Run("Constants are ignored", func(t *testing.T) { st := NewSymbolTable() // Add a constant (should not generate warning) err := st.AddConst("MAX", "", KindByte, 100, preproc.Line{Filename: "test.c65", LineNo: 30}) if err != nil { t.Fatalf("AddConst() error = %v", err) } // Check that no warning is generated warnings := st.CheckUnused() if len(warnings) != 0 { t.Errorf("CheckUnused() returned %d warnings for constant, want 0: %v", len(warnings), warnings) } // Verify constant is not marked as used (constants shouldn't track usage) sym := st.Get("MAX") if sym.IsUsed() { t.Errorf("Constant IsUsed() = true, want false") } }) t.Run("Absolute variables are ignored", func(t *testing.T) { st := NewSymbolTable() // Add an absolute variable (should not generate warning) err := st.AddAbsolute("SCREEN", "", KindWord, 0xD000, preproc.Line{Filename: "test.c65", LineNo: 40}) if err != nil { t.Fatalf("AddAbsolute() error = %v", err) } // Check that no warning is generated warnings := st.CheckUnused() if len(warnings) != 0 { t.Errorf("CheckUnused() returned %d warnings for absolute variable, want 0: %v", len(warnings), warnings) } // Verify absolute variable is not marked as used (absolutes shouldn't track usage) sym := st.Get("SCREEN") if sym.IsUsed() { t.Errorf("Absolute variable IsUsed() = true, want false") } }) t.Run("Function-scoped variables", func(t *testing.T) { st := NewSymbolTable() // Add a global unused variable err := st.AddVar("global_unused", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 50}) if err != nil { t.Fatalf("AddVar() error = %v", err) } // Add a function-scoped unused variable err = st.AddVar("local_unused", "myFunc", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 60}) if err != nil { t.Fatalf("AddVar() error = %v", err) } // Add a function-scoped used variable err = st.AddVar("local_used", "myFunc", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 70}) if err != nil { t.Fatalf("AddVar() error = %v", err) } st.Lookup("local_used", []string{"myFunc"}) // Check warnings warnings := st.CheckUnused() if len(warnings) != 2 { t.Fatalf("CheckUnused() returned %d warnings, want 2: %v", len(warnings), warnings) } // Check warning formats expectedGlobal := "test.c65:50: warning: variable 'global_unused' declared but never used" expectedLocal := "test.c65:60: warning: variable 'local_unused' in function 'myFunc' declared but never used" foundGlobal := false foundLocal := false for _, w := range warnings { if w == expectedGlobal { foundGlobal = true } if w == expectedLocal { foundLocal = true } } if !foundGlobal { t.Errorf("Missing warning for global variable: %q", expectedGlobal) } if !foundLocal { t.Errorf("Missing warning for local variable: %q", expectedLocal) } }) t.Run("Multiple lookups don't affect usage flag", func(t *testing.T) { st := NewSymbolTable() // Add a variable err := st.AddVar("counter", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 80}) if err != nil { t.Fatalf("AddVar() error = %v", err) } // Lookup multiple times (should only mark as used once) sym1 := st.Lookup("counter", []string{}) sym2 := st.Lookup("counter", []string{}) sym3 := st.Lookup("counter", []string{}) if sym1 != sym2 || sym2 != sym3 { t.Error("Lookup should return same symbol instance") } // Should still be marked as used (idempotent) if !sym1.IsUsed() { t.Errorf("IsUsed() after multiple lookups = false, want true") } // No warnings should be generated warnings := st.CheckUnused() if len(warnings) != 0 { t.Errorf("CheckUnused() returned %d warnings for used variable, want 0", len(warnings)) } }) t.Run("LookupWithoutUsage doesn't mark as used", func(t *testing.T) { st := NewSymbolTable() // Add a variable err := st.AddVar("temp", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 90}) if err != nil { t.Fatalf("AddVar() error = %v", err) } // Use LookupWithoutUsage (validation only, not actual usage) sym := st.LookupWithoutUsage("temp", []string{}) if sym == nil { t.Fatal("symbol not found") } // Should NOT be marked as used if sym.IsUsed() { t.Errorf("IsUsed() after LookupWithoutUsage = true, want false") } // Warning should be generated warnings := st.CheckUnused() if len(warnings) != 1 { t.Errorf("CheckUnused() returned %d warnings, want 1", len(warnings)) } }) t.Run("Mixed variables with usage", func(t *testing.T) { st := NewSymbolTable() // Add various types of variables st.AddVar("unused1", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 100}) st.AddVar("used1", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 101}) st.AddConst("CONST1", "", KindByte, 255, preproc.Line{Filename: "test.c65", LineNo: 102}) st.AddAbsolute("ABS1", "", KindWord, 0xC000, preproc.Line{Filename: "test.c65", LineNo: 103}) st.AddVar("unused2", "func1", KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 104}) st.AddVar("used2", "func1", KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 105}) // Mark some as used st.Lookup("used1", []string{}) st.Lookup("used2", []string{"func1"}) // Check warnings warnings := st.CheckUnused() if len(warnings) != 2 { t.Fatalf("CheckUnused() returned %d warnings, want 2: %v", len(warnings), warnings) } // Verify correct warnings expected1 := "test.c65:100: warning: variable 'unused1' declared but never used" expected2 := "test.c65:104: warning: variable 'unused2' in function 'func1' declared but never used" found1 := false found2 := false for _, w := range warnings { if w == expected1 { found1 = true } if w == expected2 { found2 = true } } if !found1 { t.Errorf("Missing warning: %q", expected1) } if !found2 { t.Errorf("Missing warning: %q", expected2) } }) t.Run("Label references", func(t *testing.T) { st := NewSymbolTable() // Add a label reference variable err := st.AddLabel("handler", "", "irq_vector", preproc.Line{Filename: "test.c65", LineNo: 110}) if err != nil { t.Fatalf("AddLabel() error = %v", err) } // Label references are word variables that reference labels // They should be treated like regular variables for usage tracking sym := st.Get("handler") if !sym.Has(FlagLabelRef) { t.Error("Expected FlagLabelRef") } if !sym.IsWord() { t.Error("Label reference should be word") } // Since we haven't used it, it should generate a warning warnings := st.CheckUnused() if len(warnings) != 1 { t.Errorf("CheckUnused() returned %d warnings for label reference, want 1", len(warnings)) } // Mark it as used st.Lookup("handler", []string{}) // Now no warning should be generated warnings = st.CheckUnused() if len(warnings) != 0 { t.Errorf("CheckUnused() returned %d warnings for used label reference, want 0", len(warnings)) } }) t.Run("LookupConstant doesn't mark as used", func(t *testing.T) { st := NewSymbolTable() // Add a regular variable err := st.AddVar("var1", "", KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 120}) if err != nil { t.Fatalf("AddVar() error = %v", err) } // Add a constant err = st.AddConst("CONST1", "", KindByte, 100, preproc.Line{Filename: "test.c65", LineNo: 121}) if err != nil { t.Fatalf("AddConst() error = %v", err) } // Lookup constant (should not mark variable as used) val, found := st.LookupConstant("CONST1", []string{}) if !found { t.Fatal("constant not found") } if val != 100 { t.Errorf("constant value = %d, want 100", val) } // LookupConstant on non-constant variable (should not mark as used) val, found = st.LookupConstant("var1", []string{}) if found { t.Error("LookupConstant should return false for non-constant") } // Check that variable is still unused sym := st.Get("var1") if sym.IsUsed() { t.Errorf("IsUsed() after LookupConstant = true, want false") } // Warning should be generated for the variable warnings := st.CheckUnused() if len(warnings) != 1 { t.Errorf("CheckUnused() returned %d warnings, want 1", len(warnings)) } expected := "test.c65:120: warning: variable 'var1' declared but never used" if warnings[0] != expected { t.Errorf("warning = %q, want %q", warnings[0], expected) } }) t.Run("Lookup doesn't mark constants or absolutes as used", func(t *testing.T) { st := NewSymbolTable() // Add a constant err := st.AddConst("MAX", "", KindByte, 255, preproc.Line{Filename: "test.c65", LineNo: 130}) if err != nil { t.Fatalf("AddConst() error = %v", err) } // Add an absolute variable err = st.AddAbsolute("VIC", "", KindWord, 0xD000, preproc.Line{Filename: "test.c65", LineNo: 131}) if err != nil { t.Fatalf("AddAbsolute() error = %v", err) } // Lookup constant (should not mark as used) symConst := st.Lookup("MAX", []string{}) if symConst == nil { t.Fatal("constant not found") } if symConst.IsUsed() { t.Error("constant should not be marked as used after Lookup") } // Lookup absolute variable (should not mark as used) symAbs := st.Lookup("VIC", []string{}) if symAbs == nil { t.Fatal("absolute variable not found") } if symAbs.IsUsed() { t.Error("absolute variable should not be marked as used after Lookup") } // No warnings should be generated warnings := st.CheckUnused() if len(warnings) != 0 { t.Errorf("CheckUnused() returned %d warnings for constants/absolutes, want 0", len(warnings)) } }) }