# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""Fuel type system.

The classes in this module are used to describe types.  All variables
and expressions in Fuel are typed.  The type is usually deduced
implicitly.  For example, when the compiler sees the statement

  let A = {1, 2, 3}

it deduces that the type of A is 'set of int'.

"""

# XXX Do we need polymorphic functions?  It would be nice to be able
# to define a function taking an argument of type 'set of T' and
# returning a T (for example, min()).  Ditto for e.g. a function that
# takes an argument of type T and returns an argument of type 'set of
# T'.  Doing this with 'any' doesn't work.  Strawman syntax:
#
# func makeset(x: *T): set of *T:
#     return {x}
#
# i.e. the idea is that using * followed by a name creates a "free
# type variable".  This can also be done to declare matching argument
# types, e.g. the signature of contains() would be
#
# func contains(s: set of *T, x: *T): bool
#
# For now, we'll live without this.  (We may have to create a hack to
# define certain builtins properly.)


class DuplicateAttribute(Exception):
    "Exception raised for duplicate class attribute definitions."
    pass

class Type(object):

    """Abstract base class for types."""

class Basic(Type):

    """Basic type.

    These are named predefined types like bool and str.  There is some
    built-in knowledge about these in various places.

    """

    def __init__(self, name):
        assert isinstance(name, str)
        self.name = name

    def __repr__(self):
        return "Basic" + self.name.title()

    def __str__(self):
        return self.name

    def __reduce__(self):
        return "Basic" + self.name.title()

class Numeric(Basic):

    """Numeric type.

    A subclass of the Basic type for numeric types.  This makes it
    easier to do numeric coercions.

    """

# Instantiate basic types now.  This is significant for the numeric types.
# XXX is 'Basic' a good name prefix for these?
BasicInt = Numeric("int")
BasicFloat = Numeric("float")
BasicBool = Basic("bool")
BasicStr = Basic("str")
BasicVoid = Basic("void")     # No value; always loses coercions
BasicAny = Basic("any")       # Unconstrained type; always wins coercion

# The following are types that exist internally but cannot be spelled
# by the Fuel programmer; ordinary constants and variables cannot be
# of these types (except for BasicError of course)
BasicType = Basic("type")     # The type of types, classes and structs
BasicModule = Basic("module") # The type of modules
BasicError = Basic("error")   # Error expression; always wins coercions

class Set(Type):

    """Set type.  The etype argument/attribute is the element type.

    The type 'set of T' is represented by an instance of this class
    whose etype attribute represents T.

    The type of the empty set is 'set of void'.

    The type of the argument to len() is 'set of any' (except 'any'
    can't be spelled in Fuel syntax).

    """

    def __init__(self, etype):
        assert isinstance(etype, Type), repr(etype)
        self.etype = etype

    def __repr__(self):
        return "Set(%s)" % repr(self.etype)

    def __str__(self):
        return "set of %s" % str(self.etype)

    def __eq__(self, other):
        return isinstance(other, Set) and self.etype == other.etype

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

    def __hash__(self):
        # Convoluted way to make X, set of X, set of set of X, etc.,
        # all hash differently.
        t = self
        h = 0
        while isinstance(t, Set):
            h += 1000
            t = t.etype
        return hash(t) ^ h

class Interface(Type):

    """Interface type, also used for structs.

    A class has a number of named and typed attributes; each must be
    represented by a SymbolDefinition instance.

    """

    def __init__(self, name, defs):
        self.name = name
        self.defs = {}
        for key, value in defs:
            if key in self.defs:
                raise DuplicateAttribute("duplicate attribute name '%s'" % key)
            self.defs[key] = value

    def lookup(self, name):
        return self.defs.get(name)

    def __repr__(self):
        return "%s(%r, %r)" % (self.__class__.__name__, self.name, self.defs)

    def __str__(self):
        return self.name

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

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

    def __hash__(self):
        h = hash(self.name)
        for name, defn in self.defs.items():
            h ^= hash((name, defn.type, defn.value))
        return h

class Struct(Interface):

    """Struct type -- same as Interface except allows instantiation."""

class Function(Type):

    """Function type, also used for procedures."""

    def __init__(self, argnames, argtypes, defaults, resulttype):
        """Constructor.

        The arguments are (apologies for the parallel arrays):

        argnames -- tuple of strings giving the argument names
        argtypes -- tuple of Type instances giving the argument types
        defaults -- tuple of default values for arguments with defaults
        resulttype -- Type instance giving the result type

        The default tuple should only contain values for arguments
        with defaults; IOW, a function with four arguments, the last
        two of which have a default value, should have a defaults
        tuple of length two.  The default value should be a Python
        number or string, or None if the type is not one of those or
        if the default is dynamically calculated.

        """
        assert isinstance(argnames, tuple), repr(argnames)
        for argname in argnames:
            assert isinstance(argname, str)
        assert isinstance(argtypes, tuple)
        assert len(argnames) == len(argtypes)
        for argtype in argtypes:
            assert isinstance(argtype, Type)
        assert isinstance(defaults, tuple), defaults
        oktypes = (int, float, str, type(None))
        for x in defaults:
            assert x is None or isinstance(x, oktypes), x
        assert isinstance(resulttype, Type)

        self.argnames = argnames
        self.argtypes = argtypes
        self.defaults = defaults
        self.ndefaults = len(defaults)
        self.resulttype = resulttype

    def __repr__(self):
        return "Function(%r, %r, %r, %r)" % (self.argnames, self.argtypes,
                                             self.ndefaults, self.resulttype)

    def __str__(self):
        args = ["%s: %s" % (aname, atype)
                for (aname, atype) in zip(self.argnames, self.argtypes)]
        for i in range(self.ndefaults):
            args[-1-i] += " = ..."
        return "func(%s): %s" % (", ".join(args), self.resulttype)

    def __eq__(self, other):
        return (self.__class__ is other.__class__ and
                self.argnames == other.argnames and
                self.argtypes == other.argtypes and
                self.ndefaults == other.ndefaults and
                self.resulttype == other.resulttype)

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

    def __hash__(self):
        return hash((self.argnames, self.argtypes,
                     self.ndefaults, self.resulttype))

def coerce(t1, t2):
    "Coerce two types to a common type.  Return None if can't be done."
    assert t1 is not None and t2 is not None, "None types are not acceptable"
    if t1 == t2:
        # Nothing to do
        return t1
    if t1 is None or t2 is None:
        # Don't guess when one of the input types is unknown
        return None
    if t1 is BasicAny or t2 is BasicAny:
        # *any* always wins
        return BasicAny
    if t1 is BasicError or t2 is BasicError:
        # An error shuts up further errors
        return BasicError
    # void always *loses*
    if t1 is BasicVoid:
        return t2
    if t2 is BasicVoid:
        return t1
    if isinstance(t1, Numeric) and isinstance(t2, Numeric):
        # Different numeric types => one of them must be float, and wins.
        # XXX Fix this when we have more than two numeric types.
        return BasicFloat
    if isinstance(t1, Set) and isinstance(t2, Set):
        # For two sets, coerce the element types
        return Set(coerce(t1.etype, t2.etype))
    # Can't be done
    return None

def accepts(expected, actual):
    """Return True iff the expected type accepts the actual type.

    Example: accepts(float, int) returns True; accepts(int, float)
    returns False.

    A type always accepts itself.  The 'float' type accepts int and
    float.

    A set type accepts another if the former's element type accepts
    the latter's element type.

    Special cases: return True if either argument is *error*; *any*
    accepts all data types; void is acceptable for anything (but
    accepts nothing).

    """
    if expected is BasicError or actual is BasicError:
        return True
    if expected == actual or actual is BasicVoid:
        return True
    if isinstance(expected, Numeric) and isinstance(actual, Numeric):
        return expected is BasicFloat
    if isinstance(expected, Set) and isinstance(actual, Set):
        return accepts(expected.etype, actual.etype)
    if expected is BasicAny:
        return (isinstance(actual, Numeric) or
                actual is BasicBool or
                actual is BasicStr or
                isinstance(actual, Set) or
                isinstance(actual, Interface))
    return False

def bootstrap(st):
    "Define the built-in types, constants and functions in a symbol table."
    # Built-in types
    for bt in BasicInt, BasicFloat, BasicBool, BasicStr, BasicVoid, BasicAny:
        st.define(bt.name, None, BasicType, bt)
    # Built-in constants
    st.define("false", None, BasicBool, False)
    st.define("true", None, BasicBool, True)
    st.define("nil", None, BasicVoid, None)
    # Built-in functions (see sigma/runtime/rtutils.py)
    st.define("len", None, Function(("x",), (Set(BasicAny),), (), BasicInt))
    st.define("concat", None, Function(("s1", "s2", "s3", "s4", "s5",
                                        "s6", "s7", "s8", "s9", "s10"),
                                       (BasicStr,)*10, ("",)*8, BasicStr))
    st.define("substr", None, Function(("string", "start", "size"),
                                       (BasicStr, BasicInt, BasicInt),
                                       (1,), BasicStr))
    st.define("find", None, Function(("s", "t"), (BasicStr, BasicStr),
                                     (), BasicInt))
    st.define("lower", None, Function(("s",), (BasicStr,), (), BasicStr))
    st.define("upper", None, Function(("s",), (BasicStr,), (), BasicStr))
    st.define("strip", None, Function(("s",), (BasicStr,), (), BasicStr))
    st.define("size", None, Function(("s",), (BasicStr,), (), BasicInt))
    st.define("split", None, Function(("s", "delim"),
                                      (BasicStr, BasicStr),
                                      (None,), Set(BasicStr)))
    st.define("format", None, Function(("msg", "a1", "a2", "a3", "a4", "a5",
                                        "a6", "a7", "a8", "a9", "a10"),
                                       (BasicStr,) + (BasicAny,)*10,
                                       (None,)*10, BasicStr))
    st.define("replace", None, Function(("s", "t", "u"),
                                        (BasicStr, BasicStr, BasicStr),
                                        (), BasicStr))
    st.define("match", None, Function(("regex", "s"),
                                      (BasicStr, BasicStr),
                                      (), BasicBool))
    st.define("toint", None, Function(("s", "base"),
                                      (BasicStr, BasicInt),
                                      (10,), BasicInt))
    st.define("bitwise_and", None, Function(("a", "b"),
                                            (BasicInt, BasicInt),
                                            (), BasicInt))
    st.define("bitwise_or",  None, Function(("a", "b"),
                                            (BasicInt, BasicInt),
                                            (), BasicInt))
    st.define("bitwise_xor", None, Function(("a", "b"),
                                            (BasicInt, BasicInt),
                                            (), BasicInt))
    st.define("bitwise_not", None, Function(("a",),
                                            (BasicInt,),
                                            (), BasicInt))
    st.define("formatset", None, Function(("s", "sep"),
                                          (Set(BasicAny), BasicStr),
                                          (", ",), BasicStr))
