# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""Fuel symbol table."""

# Python imports
import sys
import logging

# Fuel compiler imports
from sigma.fuelc import astnode
from sigma.fuelc import typesys

class SymbolDefinition(object):

    """Definition of a symbol.

    A symbol has the following attributes:

    - name:  its name (a string)
    - node:  the AST node containing its definition; may be None
    - type:  its type (an instance of a class defined in typesys);
             maybe None if not yet derived
    - value: its value, if known; usually None, except for constants
    - doc:   its doc string; None if no doc string given

    For type and class names, the type is typesys.BasicType, and the
    value is the type that the name defines, e.g. a typesys.Basic
    instance for basic types, and a typesys.Interface instance for
    interfaces.

    """

    def __init__(self, name, node=None, type=None, value=None, doc=None):
        self.name = name
        self.node = node
        self.type = type
        self.value = value
        self.doc = doc

    def __reduce__(self):
        # Pickling shouldn't pickle the nodes
        return SymbolDefinition, (self.name, None, self.type, self.value,
                                  self.doc)

    def settype(self, type):
        "Set the type, once it's been deduced. Call at most once."
        assert self.type is None
        self.type = type

    def setvalue(self, value):
        "Set the value, once it's been deduced.  Call at most once."
        assert self.value is None
        self.value = value

    def setdoc(self, doc):
        "Set the doc string, once it's known.  Call at most once."
        assert self.doc is None
        self.doc = doc

    def __repr__(self):
        s = "SymbolDefinition(" + self.name
        if self.type is not None:
            s += ", type=%s" % repr(self.type)
        if self.value is not None:
            s += ", value=%s" % repr(self.value)
        if self.doc is not None:
            s += ", doc=%s" % repr(self.doc)
        s += ")"
        return s

    def __str__(self):
        s = self.name
        if self.type is not None:
            s += ": %s" % str(self.type)
        if self.value is not None:
            s += " = %s" % str(self.value)
        return "symbol(%s)" % s

    def __eq__(self, other):
        return (self.__class__ is other.__class__ and
                self.name == other.name and
                self.type == other.type and
                self.value == other.value)

    def __ne__(self, other):
        return not self.__eq__(other)

class ErrorHandler(object):

    def __init__(self):
        self.logger = logging.getLogger("fuelc.symtab")
        self.reset()

    def reset(self):
        self.errors = 0
        self.warnings = 0
        self.messages = 0
        self.context = None # Manipulated explicitly by SymbolTable.visit()

    def message(self, level, msg, args):
        "Issue a message.  Override this to handle errors and warnings."
        self.messages += 1
        msg = str(msg)
        if self.context is not None:
            # XXX Should somehow find the filename and report like SyntaxError
            try:
                lineno, offset = self.context
                msg = "Line %s (offset %s): %s" % (lineno, offset, msg)
            except:
                msg = "Line %s: %s" % (context, msg)
        if args:
            try:
                msg = msg % args
            except:
                msg = "%s %s" % (msg, " ".join(map(str, args)))
        self.logger.log(level, msg)

    def warn(self, msg, args):
        "Issue a warning message.  Extra args assume msg is a format string."
        self.warnings += 1
        self.message(logging.WARN, msg, args)

    def error(self, msg, args):
        "Issue an error message.  Extra args assume msg is a format string."
        self.errors += 1
        self.message(logging.ERROR, msg, args)

default_errorhandler = ErrorHandler()

class SymbolTable(object):

    """A symbol table maps names to symbol definitions.

    This corresponds to a scope.  A symbol table usually references
    another symbol table, to which failed lookups are directed; this
    corresponds to the concept of nested scopes.  The outermost scope
    is built-in and defines the built-in names.

    (Note: the term symbol here is unrelated to the concept of grammar
    symbols.  A symbol is simply a name defined or used in a program.)

    """

    def __init__(self, next=None, errorhandler=None):
        if next is not None:
            assert isinstance(next, SymbolTable), next
        if errorhandler is None:
            if next is None:
                errorhandler = default_errorhandler
            else:
                errorhandler = next.errorhandler
        self.next = next # Next outer scope
        self.errorhandler = errorhandler
        self.names = {}

    def define(self, name, node=None, type=None, value=None):
        """Define a symbol.

        This creates a SymbolDefinition instance and inserts it into
        the current symbol table.  The symbol must not already be
        defined.

        """
        if name in self.names:
            self.error("duplicate definition in same scope of %r", name)
        defn = SymbolDefinition(name, node, type, value)
        self.names[name] = defn
        return defn

    def adddefn(self, name, defn):
        assert isinstance(defn, SymbolDefinition)
        assert name == defn.name
        if name in self.names:
            self.error("duplicate definition in same scope of %r", name)
        self.names[name] = defn

    def lookup(self, name):
        """Look up a symbol.

        Return a SymbolDefinition instance.  If the symbol isn't
        defined in this scope, pass the lookup to to the next scope;
        if there is no next scope, return None.

        """
        assert isinstance(name, str)
        if name in self.names:
            defn = self.names[name]
            return defn
        if self.next is not None:
            return self.next.lookup(name)
        return None

    def dump(self, label=None):
        "Print the contents to stdout, for debugging."
        keys = sorted(self.names.keys())
        if label:
            print(label)
        for name in keys:
            sd = self.names[name]
            print("    %-20s : %-25s = %s" % (name, sd.type, sd.value))

    # The error message and visiting infrastructure are connected: the
    # generic visit() method sets the context used by the error() and
    # warn() methods.

    def warn(self, msg, *args):
        "Issue a warning message.  Extra args assume msg is a format string."
        self.errorhandler.warn(msg, args)

    def error(self, msg, *args):
        "Issue an error message.  Extra args assume msg is a format string."
        self.errorhandler.error(msg, args)

    def visit(self, prefix, node):
        """Generic visitor method.

        This calls and returns self.<prefix><classname>(node) where
        <prefix> is the prefix argument (typically a string of the
        form "foo_") and <classname> is the name of the class of the
        node argument.  It's a variation on the omnipresent dynamic
        dispatch pattern in Python.

        For example, self.visit("scandefs_", node), where node is an
        instance of a class named Expression, would call
        self.scandefs_Expression(node).  If such a method doesn't
        exist, an error message is issued and None is returned.

        """
        save_context = self.errorhandler.context
        try:
            self.errorhandler.context = node.context
            name = node.__class__.__name__
            method = getattr(self, prefix + name, None)
            if method is None:
                self.error("No %s%s visitor!!!", prefix, name)
                return None
            return method(node)
        finally:
            self.errorhandler.context = save_context

# Bootstrapping -- defining built-in names

builtin_symbols = SymbolTable()
typesys.bootstrap(builtin_symbols)
