package compiler import ( "strings" "testing" ) 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, "test.c65", 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, "test.c65", 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, "test.c65", 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, "test.c65", 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, "test.c65", 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, "test.c65", 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, "test.c65", 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, "test.c65", 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", "test.c65", 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, "test.c65", 1) if err != nil { t.Fatalf("first AddVar() error = %v", err) } // Attempt redeclaration err = st.AddVar("test", "", KindByte, 0, "test.c65", 1) if err == nil { t.Error("expected error on redeclaration") } // Different scope should be OK err = st.AddVar("test", "main", KindByte, 0, "test.c65", 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, "test.c65", 1) // Local in main st.AddVar("local", "main", KindByte, 0, "test.c65", 1) // Local in nested function st.AddVar("inner", "main_helper", KindByte, 0, "test.c65", 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, "test.c65", 1) st.AddVar("local", "main", KindByte, 0, "test.c65", 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, "test.c65", 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, "test.c65", 1) st.AddVar("b", "", KindByte, 0, "test.c65", 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, "test.c65", 1) return st.Get("test") }, contains: []string{"Name=test", "BYTE"}, }, { name: "word constant", setup: func(st *SymbolTable) *Symbol { st.AddConst("MAX", "", KindWord, 65535, "test.c65", 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, "test.c65", 1) return st.Get("ptr") }, contains: []string{"Name=ptr", "WORD", "@$0080", "ZP"}, }, { name: "label reference", setup: func(st *SymbolTable) *Symbol { st.AddLabel("handler", "", "irq", "test.c65", 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, "test.c65", 1) st.AddConst("SIZE", "", KindWord, 0x1234, "test.c65", 1) st.AddVar("notconst", "", KindByte, 0, "test.c65", 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, "test.c65", 1) st.AddAbsolute("ZP_PTR", "", KindWord, 0xFE, "test.c65", 1) // Non-zero-page st.AddAbsolute("VIC", "", KindWord, 0xD000, "test.c65", 1) // Regular var (should be skipped) st.AddVar("regular", "", KindByte, 0, "test.c65", 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, "test.c65", 1) // Word variable st.AddVar("ptr", "", KindWord, 0x1234, "test.c65", 1) // Label reference st.AddLabel("handler", "", "irq_routine", "test.c65", 1) // Const (should be skipped) st.AddConst("SKIP", "", KindByte, 99, "test.c65", 1) // Absolute (should be skipped) st.AddAbsolute("SKIP2", "", KindByte, 0x80, "test.c65", 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, "test.c65", 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, "test.c65", 1) st.AddVar("local", "main", KindByte, 0, "test.c65", 1) st.AddVar("nested", "main_helper", KindByte, 0, "test.c65", 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, "test.c65", 1) st.AddAbsolute("ADDR", "", KindWord, 0xDEAD, "test.c65", 1) st.AddVar("VAR", "", KindWord, 0xBEEF, "test.c65", 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, "test.c65", 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, "test.c65", 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, "test.c65", 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, "test.c65", 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, "test.c65", 50) if err != nil { t.Fatalf("AddVar() error = %v", err) } // Add a function-scoped unused variable err = st.AddVar("local_unused", "myFunc", KindByte, 0, "test.c65", 60) if err != nil { t.Fatalf("AddVar() error = %v", err) } // Add a function-scoped used variable err = st.AddVar("local_used", "myFunc", KindByte, 0, "test.c65", 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, "test.c65", 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, "test.c65", 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, "test.c65", 100) st.AddVar("used1", "", KindByte, 0, "test.c65", 101) st.AddConst("CONST1", "", KindByte, 255, "test.c65", 102) st.AddAbsolute("ABS1", "", KindWord, 0xC000, "test.c65", 103) st.AddVar("unused2", "func1", KindWord, 0, "test.c65", 104) st.AddVar("used2", "func1", KindWord, 0, "test.c65", 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", "test.c65", 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, "test.c65", 120) if err != nil { t.Fatalf("AddVar() error = %v", err) } // Add a constant err = st.AddConst("CONST1", "", KindByte, 100, "test.c65", 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, "test.c65", 130) if err != nil { t.Fatalf("AddConst() error = %v", err) } // Add an absolute variable err = st.AddAbsolute("VIC", "", KindWord, 0xD000, "test.c65", 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)) } }) }