558 lines
15 KiB
Go
558 lines
15 KiB
Go
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
|
|
}
|