1075 lines
29 KiB
Go
1075 lines
29 KiB
Go
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, nil)
|
|
|
|
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, nil)
|
|
|
|
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, nil)
|
|
|
|
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, nil); lines != nil {
|
|
t.Error("expected nil for empty constants")
|
|
}
|
|
if lines := GenerateAbsolutes(st, nil); lines != nil {
|
|
t.Error("expected nil for empty absolutes")
|
|
}
|
|
if lines := GenerateVariables(st, nil); 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, nil); lines != nil {
|
|
t.Error("expected nil when no constants exist")
|
|
}
|
|
if lines := GenerateAbsolutes(st, nil); 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, nil)
|
|
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, nil)
|
|
absLines := GenerateAbsolutes(st, nil)
|
|
varLines := GenerateVariables(st, nil)
|
|
|
|
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(nil)
|
|
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(nil)
|
|
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(nil)
|
|
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(nil)
|
|
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(nil)
|
|
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(nil)
|
|
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(nil)
|
|
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(nil)
|
|
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(nil)
|
|
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(nil)
|
|
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(nil)
|
|
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(nil)
|
|
if len(warnings) != 0 {
|
|
t.Errorf("CheckUnused() returned %d warnings for constants/absolutes, want 0", len(warnings))
|
|
}
|
|
})
|
|
}
|