c65gm/internal/compiler/symboltable_test.go

1073 lines
27 KiB
Go

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