package compiler import ( "fmt" "strings" "c65gm/internal/preproc" ) // 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 ) // Symbol represents a variable, constant, or label reference type Symbol struct { Name string // Variable name Scope string // Function scope (empty for global) Flags SymbolFlags // Type and properties Value uint16 // Initial value for variables, constant value for constants AbsAddr uint16 // Absolute address (for @ variables) LabelRef string // Label reference (for label variables) Usage uint8 // Usage tracking Line preproc.Line // Source line where declared (contains filename, line number, pragma index) } // Usage tracking for variables const ( UsageNone uint8 = 0 UsageUsed uint8 = 1 << 0 // Future flags can be added: // UsageRead = 1 << 1 // UsageWritten = 1 << 2 // UsageAddress = 1 << 3 ) // MarkUsed marks the symbol as used (should not be called for constants or absolutes) func (s *Symbol) MarkUsed() { s.Usage |= UsageUsed } // IsUsed returns true if the symbol has been marked as used func (s *Symbol) IsUsed() bool { return s.Usage&UsageUsed != 0 } // 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) } // 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 pragma *preproc.Pragma // for pragma lookup during warning generation } // 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 } // SetPragma sets the pragma reference for pragma lookup during warning generation func (st *SymbolTable) SetPragma(p *preproc.Pragma) { st.pragma = p } // AddVar adds a regular variable (byte or word) func (st *SymbolTable) AddVar(name, scope string, kind VarKind, initValue uint16, line preproc.Line) 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, Line: line, }) } // AddConst adds a constant (byte or word) func (st *SymbolTable) AddConst(name, scope string, kind VarKind, value uint16, line preproc.Line) 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, Line: line, }) } // AddAbsolute adds a variable at a fixed memory address func (st *SymbolTable) AddAbsolute(name, scope string, kind VarKind, addr uint16, line preproc.Line) 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, Line: line, }) } // AddLabel adds a word variable that references a label func (st *SymbolTable) AddLabel(name, scope string, labelRef string, line preproc.Line) error { return st.add(&Symbol{ Name: name, Scope: scope, Flags: FlagWord | FlagLabelRef, LabelRef: labelRef, Line: line, }) } // 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 // excludeFuncs is a map of function names that will be removed (e.g., have _P_REMOVE_UNUSED pragma) func (st *SymbolTable) CheckUnused(excludeFuncs map[string]bool) []string { var warnings []string for _, sym := range st.symbols { // Skip constants and absolute variables (they shouldn't track usage) if sym.IsConst() || sym.IsAbsolute() { // Sanity check: constants and absolutes should never be marked as used // If they are, it's a bug in the compiler if sym.IsUsed() { // This would be an internal error, but we'll just skip it continue } continue } // Skip variables in functions that will be removed if sym.Scope != "" && excludeFuncs != nil && excludeFuncs[sym.Scope] { continue } // Check if pragma indicates we should ignore unused warnings for this variable if st.pragma != nil { pragmaSet := st.pragma.GetPragmaSetByIndex(sym.Line.PragmaSetIndex) value := pragmaSet.GetPragma("_P_IGNORE_UNUSED") if value != "" && value != "0" { continue // Skip warning for this variable } } // Check if variable was never used if !sym.IsUsed() { // 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.Line.Filename, sym.Line.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.Line.Filename != "" { parts = append(parts, fmt.Sprintf("@%s:%d", s.Line.Filename, s.Line.LineNo)) } return strings.Join(parts, " ") } // Code generation functions for ACME assembler syntax // GenerateConstants generates constant definitions (name = $value) func GenerateConstants(st *SymbolTable, excludeScopes map[string]bool) []string { var lines []string hasConsts := false for _, sym := range st.Symbols() { if !sym.IsConst() { continue } // Skip constants scoped to removed functions if excludeScopes != nil && sym.Scope != "" && excludeScopes[sym.Scope] { 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, excludeScopes map[string]bool) []string { var lines []string hasAbsolutes := false for _, sym := range st.Symbols() { if !sym.IsAbsolute() { continue } // Skip absolutes scoped to removed functions if excludeScopes != nil && sym.Scope != "" && excludeScopes[sym.Scope] { 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, excludeScopes map[string]bool) []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 } // Skip variables scoped to removed functions if excludeScopes != nil && sym.Scope != "" && excludeScopes[sym.Scope] { 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 }