Skip to content

Type Representation#

This document details how types are represented internally in Jac's native type system.

From Strings to Objects#

In the previous implementation, types were primarily represented as strings (e.g., "int", "list[str]", "MyCustomType"). The new system replaces these with a rich hierarchy of type objects.

graph LR
    A["String: 'list[int]'"] -->|Old System| B["expr_type = 'list[int]'"]
    C["AST for list[int]"] -->|New System| D["ListType(element_type=JAC_INT)"]

The Type Hierarchy#

The foundation of the type system is the abstract JacType class, which defines the interface for all type objects.

classDiagram
    direction LR
    class JacType {
        <<abstract>>
        +is_equivalent(other: JacType) bool
        +is_subtype_of(super_type: JacType, env: TypeEnvironment) bool
        +__str__() str
    }

    JacType <|-- PrimitiveType
    JacType <|-- AnyType
    JacType <|-- NothingType
    JacType <|-- ArchitypeType
    JacType <|-- CallableType
    JacType <|-- ContainerType
    JacType <|-- OptionalType
    JacType <|-- UnionType
    JacType <|-- TypeVariable

    ContainerType <|-- ListType
    ContainerType <|-- DictType
    ContainerType <|-- TupleType
    ContainerType <|-- SetType

Core Type Classes#

PrimitiveType#

Represents Jac's primitive types like int, float, str, bool, and bytes.

class PrimitiveType(JacType):
    def __init__(self, name: str):
        self.name = name  # e.g., "int", "float", "str"

ArchitypeType#

Represents Jac architypes (objects, nodes, edges, walkers) and enums.

class ArchitypeType(JacType):
    def __init__(self, name: str, sym_tab: UniScopeNode, arch_node: Architype):
        self.name = name  # Fully qualified name
        self.sym_tab = sym_tab  # Symbol table of the architype
        self.arch_node = arch_node  # AST node for the architype definition

This type maintains connections to both the symbol table and AST node of the architype, enabling member lookups and inheritance resolution.

CallableType#

Represents abilities, functions, and lambdas.

class CallableType(JacType):
    def __init__(self, param_types: list[JacType], return_type: JacType, name: Optional[str] = None):
        self.param_types = param_types
        self.return_type = return_type
        self.name = name  # Optional name of the callable

Container Types#

A family of types for collections:

class ListType(ContainerType):
    def __init__(self, element_type: JacType):
        self.element_type = element_type

class DictType(ContainerType):
    def __init__(self, key_type: JacType, value_type: JacType):
        self.key_type = key_type
        self.value_type = value_type

class TupleType(ContainerType):
    def __init__(self, item_types: Sequence[JacType], homogeneous_type: Optional[JacType] = None):
        self.item_types = tuple(item_types)  # For heterogeneous tuples
        self.homogeneous_type = homogeneous_type  # For tuple[T, ...]

Type Operators#

Types that combine or modify other types:

class OptionalType(JacType):
    def __init__(self, wrapped_type: JacType):
        self.wrapped_type = wrapped_type

class UnionType(JacType):
    def __init__(self, types: Sequence[JacType]):
        self.member_types = frozenset(self._normalize_union(types))

Special Types#

class AnyType(JacType):
    """Represents the 'any' type."""
    pass

class NothingType(JacType):
    """Represents the type for functions not returning a value."""
    pass

class TypeVariable(JacType):
    """Represents a type variable for generics."""
    def __init__(self, name: str, scope_id: int):
        self.name = name
        self.scope_id = scope_id  # Disambiguates type vars with same name

Type Operations#

Type Equivalence#

The is_equivalent method determines if two types are equivalent:

# In ListType
def is_equivalent(self, other: JacType) -> bool:
    return (isinstance(other, ListType) and
            self.element_type.is_equivalent(other.element_type))

# In ArchitypeType (nominal typing)
def is_equivalent(self, other: JacType) -> bool:
    return isinstance(other, ArchitypeType) and self.name == other.name

Different types have different equivalence rules: - Nominal equivalence for architypes (based on name) - Structural equivalence for containers and callables (based on structure)

Subtyping Relationships#

The is_subtype_of method determines if one type can be used where another is expected:

# In ArchitypeType
def is_subtype_of(self, super_type: JacType, env: TypeEnvironment) -> bool:
    if self.is_equivalent(super_type):
        return True
    if isinstance(super_type, AnyType):
        return True
    if isinstance(super_type, ArchitypeType):
        # Check inheritance chain
        for base in env.get_base_types(self):
            if base.is_equivalent(super_type) or base.is_subtype_of(super_type, env):
                return True
    return False

Subtyping relationships follow these principles: - A type is always a subtype of itself - A type is always a subtype of any - For architypes, subtyping follows the inheritance hierarchy - For containers, variance rules apply (e.g., covariance for lists) - For callables, parameter types are contravariant and return types are covariant

Singletons for Common Types#

For efficiency, common types are pre-instantiated:

JAC_INT = PrimitiveType("int")
JAC_FLOAT = PrimitiveType("float")
JAC_STRING = PrimitiveType("str")
JAC_BOOL = PrimitiveType("bool")
JAC_BYTES = PrimitiveType("bytes")
JAC_NULL = NothingType()
JAC_ANY = AnyType()
JAC_NOTHING = NothingType()

Integration with AST#

Type information is stored directly on AST nodes:

class Expr(UniNode):
    # ...
    def __init__(self):
        # ...
        self.jac_type: Optional[JacType] = None

This allows type information to be easily accessed during code generation and other compiler passes.

String Representation#

For error messages and debugging, types provide string representations:

# In ListType
def __str__(self) -> str:
    return f"list[{str(self.element_type)}]"

# In CallableType
def __str__(self) -> str:
    params_str = ", ".join(str(p) for p in self.param_types)
    return f"({params_str}) -> {str(self.return_type)}"

Example: Complex Type Representation#

Let's look at how a complex type is represented:

graph TD
    A["dict[str, list[Optional[MyClass]]]"] --> B["DictType"]
    B --> C["key_type: StringType"]
    B --> D["value_type: ListType"]
    D --> E["element_type: OptionalType"]
    E --> F["wrapped_type: ArchitypeType"]
    F --> G["name: 'MyClass'"]
    F --> H["sym_tab: MyClass's symbol table"]
    F --> I["arch_node: MyClass's AST node"]