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) 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) 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) if err == nil { t.Error("expected error for byte value > 255") } } func TestAddConst(t *testing.T) { st := NewSymbolTable() err := st.AddConst("MAX", "", KindByte, 255) 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) 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) 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) 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) 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") 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) if err != nil { t.Fatalf("first AddVar() error = %v", err) } // Attempt redeclaration err = st.AddVar("test", "", KindByte, 0) if err == nil { t.Error("expected error on redeclaration") } // Different scope should be OK err = st.AddVar("test", "main", KindByte, 0) 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) // Local in main st.AddVar("local", "main", KindByte, 0) // Local in nested function st.AddVar("inner", "main_helper", KindByte, 0) 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) st.AddVar("local", "main", KindByte, 0) 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) } 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) st.AddVar("b", "", KindByte, 0) 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) return st.Get("test") }, contains: []string{"Name=test", "BYTE"}, }, { name: "word constant", setup: func(st *SymbolTable) *Symbol { st.AddConst("MAX", "", KindWord, 65535) 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) return st.Get("ptr") }, contains: []string{"Name=ptr", "WORD", "@$0080", "ZP"}, }, { name: "label reference", setup: func(st *SymbolTable) *Symbol { st.AddLabel("handler", "", "irq") 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) st.AddConst("SIZE", "", KindWord, 0x1234) st.AddVar("notconst", "", KindByte, 0) // 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) st.AddAbsolute("ZP_PTR", "", KindWord, 0xFE) // Non-zero-page st.AddAbsolute("VIC", "", KindWord, 0xD000) // Regular var (should be skipped) st.AddVar("regular", "", KindByte, 0) 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) // Word variable st.AddVar("ptr", "", KindWord, 0x1234) // Label reference st.AddLabel("handler", "", "irq_routine") // Const (should be skipped) st.AddConst("SKIP", "", KindByte, 99) // Absolute (should be skipped) st.AddAbsolute("SKIP2", "", KindByte, 0x80) 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) 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) st.AddVar("local", "main", KindByte, 0) st.AddVar("nested", "main_helper", KindByte, 0) 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) st.AddAbsolute("ADDR", "", KindWord, 0xDEAD) st.AddVar("VAR", "", KindWord, 0xBEEF) 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") } }