c65gm/internal/compiler/symboltable.go

512 lines
13 KiB
Go

package compiler
import (
"fmt"
"strings"
)
// VarKind represents the data type/size of a variable
type VarKind uint8
const (
KindByte VarKind = iota
KindWord
// Future: KindDWord, Kind24bit, etc
)
// SymbolFlags represents properties of a symbol as a bitfield
type SymbolFlags uint16
const (
FlagByte SymbolFlags = 1 << iota
FlagWord
FlagConst
FlagAbsolute
FlagZeroPage
FlagLabelRef
)
// Usage tracking for variables
const (
UsageNone uint8 = iota
UsageUsed
)
// Symbol represents a variable or constant declaration
type Symbol struct {
Name string
Scope string // empty string = global, otherwise function name
Flags SymbolFlags
Value uint16 // init value or const value
AbsAddr uint16 // if FlagAbsolute set
LabelRef string // if FlagLabelRef set
Usage uint8 // tracks variable usage (see Usage constants)
Filename string // file where variable was declared
LineNo int // line number where variable was declared
}
// Helper methods for Symbol
func (s *Symbol) Has(flag SymbolFlags) bool {
return s.Flags&flag != 0
}
func (s *Symbol) HasAll(flags SymbolFlags) bool {
return s.Flags&flags == flags
}
func (s *Symbol) IsByte() bool { return s.Has(FlagByte) }
func (s *Symbol) IsWord() bool { return s.Has(FlagWord) }
func (s *Symbol) IsConst() bool { return s.Has(FlagConst) }
func (s *Symbol) IsAbsolute() bool { return s.Has(FlagAbsolute) }
func (s *Symbol) IsZeroPage() bool { return s.Has(FlagZeroPage) }
func (s *Symbol) IsZeroPagePointer() bool { return s.HasAll(FlagAbsolute | FlagZeroPage | FlagWord) }
// MarkUsed marks the symbol as used (should not be called for constants or absolutes)
func (s *Symbol) MarkUsed() {
s.Usage = UsageUsed
}
// FullName returns the fully qualified name (scope.name or just name)
func (s *Symbol) FullName() string {
if s.Scope == "" {
return s.Name
}
return s.Scope + "_" + s.Name
}
// SymbolTable manages variable and constant declarations
type SymbolTable struct {
symbols []*Symbol // insertion order
byFullName map[string]*Symbol // fullname -> symbol
byScope map[string]map[string]*Symbol // scope -> name -> symbol
funcHandler FunctionHandlerInterface // for notifying about absolute variables in functions
}
// FunctionHandlerInterface allows SymbolTable to notify about absolute variables
// without creating a circular dependency
type FunctionHandlerInterface interface {
RecordAbsoluteVar(funcScope string, addr uint16, isWord bool)
}
// NewSymbolTable creates a new symbol table
func NewSymbolTable() *SymbolTable {
return &SymbolTable{
symbols: make([]*Symbol, 0),
byFullName: make(map[string]*Symbol),
byScope: make(map[string]map[string]*Symbol),
}
}
// SetFunctionHandler sets the function handler for absolute variable notifications
func (st *SymbolTable) SetFunctionHandler(fh FunctionHandlerInterface) {
st.funcHandler = fh
}
// AddVar adds a regular variable (byte or word)
func (st *SymbolTable) AddVar(name, scope string, kind VarKind, initValue uint16, filename string, lineNo int) error {
var flags SymbolFlags
switch kind {
case KindByte:
flags = FlagByte
if initValue > 255 {
return fmt.Errorf("byte variable %q init value %d out of range", name, initValue)
}
case KindWord:
flags = FlagWord
default:
return fmt.Errorf("unknown variable kind: %d", kind)
}
return st.add(&Symbol{
Name: name,
Scope: scope,
Flags: flags,
Value: initValue,
Filename: filename,
LineNo: lineNo,
})
}
// AddConst adds a constant (byte or word)
func (st *SymbolTable) AddConst(name, scope string, kind VarKind, value uint16, filename string, lineNo int) error {
var flags SymbolFlags
switch kind {
case KindByte:
flags = FlagByte | FlagConst
if value > 255 {
return fmt.Errorf("byte constant %q value %d out of range", name, value)
}
case KindWord:
flags = FlagWord | FlagConst
default:
return fmt.Errorf("unknown variable kind: %d", kind)
}
return st.add(&Symbol{
Name: name,
Scope: scope,
Flags: flags,
Value: value,
Filename: filename,
LineNo: lineNo,
})
}
// AddAbsolute adds a variable at a fixed memory address
func (st *SymbolTable) AddAbsolute(name, scope string, kind VarKind, addr uint16, filename string, lineNo int) error {
if addr > 0xFFFF {
return fmt.Errorf("absolute address %d exceeds 16-bit range", addr)
}
var flags SymbolFlags
switch kind {
case KindByte:
flags = FlagByte | FlagAbsolute
// Zero page check for bytes
if addr < 0x100 {
flags |= FlagZeroPage
}
case KindWord:
flags = FlagWord | FlagAbsolute
// Zero page check for words (pointer must fit in ZP)
if addr < 0xFF {
flags |= FlagZeroPage
}
default:
return fmt.Errorf("unknown variable kind: %d", kind)
}
// Notify function handler about absolute variable in function scope
if st.funcHandler != nil && scope != "" {
st.funcHandler.RecordAbsoluteVar(scope, addr, kind == KindWord)
}
return st.add(&Symbol{
Name: name,
Scope: scope,
Flags: flags,
AbsAddr: addr,
Filename: filename,
LineNo: lineNo,
})
}
// AddLabel adds a word variable that references a label
func (st *SymbolTable) AddLabel(name, scope string, labelRef string, filename string, lineNo int) error {
return st.add(&Symbol{
Name: name,
Scope: scope,
Flags: FlagWord | FlagLabelRef,
LabelRef: labelRef,
Filename: filename,
LineNo: lineNo,
})
}
// add is the internal method that actually adds a symbol
func (st *SymbolTable) add(sym *Symbol) error {
fullName := sym.FullName()
// Check for redeclaration
if _, exists := st.byFullName[fullName]; exists {
return fmt.Errorf("symbol %q already declared", fullName)
}
// Add to all indexes
st.symbols = append(st.symbols, sym)
st.byFullName[fullName] = sym
// Add to scope index
if st.byScope[sym.Scope] == nil {
st.byScope[sym.Scope] = make(map[string]*Symbol)
}
st.byScope[sym.Scope][sym.Name] = sym
return nil
}
// Lookup finds a symbol by name, resolving scope
// Searches local scope first (if currentScopes provided), then global
// Marks non-constant, non-absolute variables as used
func (st *SymbolTable) Lookup(name string, currentScopes []string) *Symbol {
// Try local scopes first (innermost to outermost)
for i := len(currentScopes) - 1; i >= 0; i-- {
scope := currentScopes[i]
if scopeMap, ok := st.byScope[scope]; ok {
if sym, ok := scopeMap[name]; ok {
// Mark as used if it's a regular variable (not constant, not absolute)
if !sym.IsConst() && !sym.IsAbsolute() {
sym.MarkUsed()
}
return sym
}
}
}
// Try global scope
if scopeMap, ok := st.byScope[""]; ok {
if sym, ok := scopeMap[name]; ok {
// Mark as used if it's a regular variable (not constant, not absolute)
if !sym.IsConst() && !sym.IsAbsolute() {
sym.MarkUsed()
}
return sym
}
}
return nil
}
// Get retrieves a symbol by its full name
func (st *SymbolTable) Get(fullName string) *Symbol {
return st.byFullName[fullName]
}
// Symbols returns all symbols in insertion order
func (st *SymbolTable) Symbols() []*Symbol {
return st.symbols
}
// Count returns the number of symbols
func (st *SymbolTable) Count() int {
return len(st.symbols)
}
// ExpandName resolves a local name to its full name using scope resolution
func (st *SymbolTable) ExpandName(name string, currentScopes []string) string {
sym := st.Lookup(name, currentScopes)
if sym != nil {
return sym.FullName()
}
return name
}
// LookupWithoutUsage finds a symbol by name without marking it as used
// Used for validation-only lookups (e.g., checking if variable exists)
func (st *SymbolTable) LookupWithoutUsage(name string, currentScopes []string) *Symbol {
// Try local scopes first (innermost to outermost)
for i := len(currentScopes) - 1; i >= 0; i-- {
scope := currentScopes[i]
if scopeMap, ok := st.byScope[scope]; ok {
if sym, ok := scopeMap[name]; ok {
return sym
}
}
}
// Try global scope
if scopeMap, ok := st.byScope[""]; ok {
if sym, ok := scopeMap[name]; ok {
return sym
}
}
return nil
}
// LookupConstant looks up a constant by name and returns its value if found
// Returns (value, true) if symbol exists and is a constant, (0, false) otherwise
func (st *SymbolTable) LookupConstant(name string, currentScopes []string) (int64, bool) {
sym := st.LookupWithoutUsage(name, currentScopes)
if sym != nil && sym.IsConst() {
return int64(sym.Value), true
}
return 0, false
}
// ConstantLookupFunc returns a ConstantLookup function that captures the current scopes
// This can be used directly with utils.EvaluateExpression
func (st *SymbolTable) ConstantLookupFunc(currentScopes []string) func(string) (int64, bool) {
return func(name string) (int64, bool) {
return st.LookupConstant(name, currentScopes)
}
}
// CheckUnused returns warnings for unused variables
// Returns slice of warning messages for regular variables (not constants, not absolutes) that were never used
func (st *SymbolTable) CheckUnused() []string {
var warnings []string
for _, sym := range st.symbols {
// Skip constants and absolute variables
if sym.IsConst() || sym.IsAbsolute() {
continue
}
// Check if variable was never used
if sym.Usage == UsageNone {
// Format warning message with file and line info
var scopeInfo string
if sym.Scope != "" {
scopeInfo = fmt.Sprintf(" in function '%s'", sym.Scope)
}
warning := fmt.Sprintf("%s:%d: warning: variable '%s'%s declared but never used",
sym.Filename, sym.LineNo, sym.Name, scopeInfo)
warnings = append(warnings, warning)
}
}
return warnings
}
// String representation for debugging
func (s *Symbol) String() string {
var parts []string
parts = append(parts, fmt.Sprintf("Name=%s", s.FullName()))
if s.IsByte() {
parts = append(parts, "BYTE")
} else if s.IsWord() {
parts = append(parts, "WORD")
}
if s.IsConst() {
parts = append(parts, fmt.Sprintf("CONST=%d", s.Value))
} else if s.IsAbsolute() {
parts = append(parts, fmt.Sprintf("@$%04X", s.AbsAddr))
if s.IsZeroPage() {
parts = append(parts, "ZP")
}
} else if s.Has(FlagLabelRef) {
parts = append(parts, fmt.Sprintf("->%s", s.LabelRef))
} else if s.Value != 0 {
parts = append(parts, fmt.Sprintf("=%d", s.Value))
}
// Add location info if available
if s.Filename != "" {
parts = append(parts, fmt.Sprintf("@%s:%d", s.Filename, s.LineNo))
}
return strings.Join(parts, " ")
}
// Code generation functions for ACME assembler syntax
// GenerateConstants generates constant definitions (name = $value)
func GenerateConstants(st *SymbolTable) []string {
var lines []string
hasConsts := false
for _, sym := range st.Symbols() {
if !sym.IsConst() {
continue
}
hasConsts = true
var line string
if sym.IsByte() {
// Byte constant with decimal comment
line = fmt.Sprintf("%s = $%02x\t; %d", sym.FullName(), sym.Value, sym.Value)
} else {
// Word constant
line = fmt.Sprintf("%s = $%04x", sym.FullName(), sym.Value)
}
lines = append(lines, line)
}
if hasConsts {
// Prepend header
result := []string{
";Constant values (from c65gm)",
"",
}
result = append(result, lines...)
result = append(result, "") // blank line after
return result
}
return nil
}
// GenerateAbsolutes generates absolute address assignments (name = $addr)
func GenerateAbsolutes(st *SymbolTable) []string {
var lines []string
hasAbsolutes := false
for _, sym := range st.Symbols() {
if !sym.IsAbsolute() {
continue
}
hasAbsolutes = true
var line string
if sym.IsZeroPage() {
// Zero-page: 2 hex digits
line = fmt.Sprintf("%s = $%02x", sym.FullName(), sym.AbsAddr)
} else {
// Non-zero-page: 4 hex digits
line = fmt.Sprintf("%s = $%04x", sym.FullName(), sym.AbsAddr)
}
lines = append(lines, line)
}
if hasAbsolutes {
// Prepend header
result := []string{
";Absolute variable definitions (from c65gm)",
"",
}
result = append(result, lines...)
result = append(result, "") // blank line after
return result
}
return nil
}
// GenerateVariables generates variable declarations (name !8 $value)
func GenerateVariables(st *SymbolTable) []string {
var lines []string
hasVars := false
for _, sym := range st.Symbols() {
// Skip constants and absolutes - they're handled separately
if sym.IsConst() || sym.IsAbsolute() {
continue
}
hasVars = true
var line string
if sym.IsByte() {
// Byte variable with decimal comment
line = fmt.Sprintf("%s\t!8 $%02x\t; %d", sym.FullName(), sym.Value&0xFF, sym.Value&0xFF)
} else if sym.Has(FlagLabelRef) {
// Word with label reference
line = fmt.Sprintf("%s\t!8 <%s, >%s", sym.FullName(), sym.LabelRef, sym.LabelRef)
} else {
// Word variable (split into low byte, high byte)
lo := sym.Value & 0xFF
hi := (sym.Value >> 8) & 0xFF
line = fmt.Sprintf("%s\t!8 $%02x, $%02x", sym.FullName(), lo, hi)
}
lines = append(lines, line)
}
if hasVars {
// Prepend header
result := []string{
";Variables (from c65gm)",
"",
}
result = append(result, lines...)
result = append(result, "") // blank line after
return result
}
return nil
}
// GetVarKind extracts VarKind from Symbol
func (s *Symbol) GetVarKind() VarKind {
if s.IsByte() {
return KindByte
}
return KindWord
}