Skip to content

Compiler Architecture

← Back to Documentation

Internal documentation for Sushi compiler architecture and design.

Overview

Sushi follows a clean multi-pass compiler architecture:

Source Code (.sushi)
    ↓
Lark Parser (grammar.lark)
    ↓
AST Builder (semantics/ast_builder/)
    ↓
Multi-Pass Semantic Analysis (semantics/passes/)     ← always whole-program
    ↓
Per-Unit Fingerprint Computation                      ← cache check
    ↓
LLVM IR Generation (backend/codegen_llvm.py)          ← per-unit, cached .o files
    ↓
LLVM Optimization Pipeline
    ↓
Clang Linking (all .o files)
    ↓
Native Executable

For multi-unit projects, the compiler uses incremental compilation: each unit is compiled to its own .o file and cached in __sushi_cache__/. Only units whose semantic fingerprint has changed are recompiled. Single-file programs use the direct monolithic path.

Directory Structure

sushi/
β”œβ”€β”€ compiler/                   # Compiler package (entry point: compiler/cli.py:main)
β”‚   β”œβ”€β”€ cli.py                  # CLI argument parsing, main() entry point
β”‚   β”œβ”€β”€ pipeline.py             # Multi-file compilation orchestration
β”‚   β”œβ”€β”€ loader.py               # Unit loading and dependency resolution
β”‚   β”œβ”€β”€ cache.py                # Incremental compilation cache manager
β”‚   └── fingerprint.py          # Per-unit semantic fingerprint computation
β”œβ”€β”€ grammar.lark                # Lark grammar specification
β”œβ”€β”€ semantics/
β”‚   β”œβ”€β”€ ast_builder/           # Modular AST construction, split by concern
β”‚   β”‚   β”œβ”€β”€ builder.py         # Main orchestrator
β”‚   β”‚   β”œβ”€β”€ declarations/      # Top-level constructs
β”‚   β”‚   β”‚   β”œβ”€β”€ toplevel.py    # One table sorts a unit's declarations
β”‚   β”‚   β”‚   β”œβ”€β”€ functions.py   # Function parsing
β”‚   β”‚   β”‚   β”œβ”€β”€ structs.py     # Struct definitions
β”‚   β”‚   β”‚   β”œβ”€β”€ enums.py       # Enum definitions
β”‚   β”‚   β”‚   β”œβ”€β”€ extensions.py  # Extension methods
β”‚   β”‚   β”‚   β”œβ”€β”€ perks.py       # Perk definitions
β”‚   β”‚   β”‚   β”œβ”€β”€ constants.py   # Constant declarations
β”‚   β”‚   β”‚   └── imports.py     # Use statements
β”‚   β”‚   β”œβ”€β”€ expressions/       # Expression parsing
β”‚   β”‚   β”‚   β”œβ”€β”€ parser.py      # Main expression parser
β”‚   β”‚   β”‚   β”œβ”€β”€ literals.py    # Literal values
β”‚   β”‚   β”‚   β”œβ”€β”€ operators.py   # Binary/unary operators
β”‚   β”‚   β”‚   β”œβ”€β”€ calls.py       # Function calls
β”‚   β”‚   β”‚   β”œβ”€β”€ members.py     # Member access
β”‚   β”‚   β”‚   β”œβ”€β”€ arrays.py      # Array expressions
β”‚   β”‚   β”‚   └── chains.py      # Chained expressions
β”‚   β”‚   β”œβ”€β”€ statements/        # Statement parsing
β”‚   β”‚   β”‚   β”œβ”€β”€ parser.py      # Main statement parser
β”‚   β”‚   β”‚   β”œβ”€β”€ variables.py   # Let/rebind
β”‚   β”‚   β”‚   β”œβ”€β”€ returns.py     # Return statements
β”‚   β”‚   β”‚   β”œβ”€β”€ control_flow.py # If/elif/else
β”‚   β”‚   β”‚   β”œβ”€β”€ loops.py       # While/foreach
β”‚   β”‚   β”‚   β”œβ”€β”€ matching.py    # Pattern matching
β”‚   β”‚   β”‚   β”œβ”€β”€ blocks.py      # Block statements
β”‚   β”‚   β”‚   β”œβ”€β”€ flow.py        # Break/continue
β”‚   β”‚   β”‚   β”œβ”€β”€ io.py          # Print/println
β”‚   β”‚   β”‚   └── calls.py       # Statement-level calls
β”‚   β”‚   β”œβ”€β”€ types/             # Type parsing
β”‚   β”‚   β”‚   β”œβ”€β”€ parser.py      # Main type parser
β”‚   β”‚   β”‚   β”œβ”€β”€ generics.py    # Generic types
β”‚   β”‚   β”‚   β”œβ”€β”€ arrays.py      # Array types
β”‚   β”‚   β”‚   β”œβ”€β”€ references.py  # Reference types
β”‚   β”‚   β”‚   └── user_defined.py # Struct/enum types
β”‚   β”‚   β”œβ”€β”€ utils/             # Shared utilities
β”‚   β”‚   β”‚   β”œβ”€β”€ tree_navigation.py # Tree traversal
β”‚   β”‚   β”‚   β”œβ”€β”€ expression_discovery.py # Expression finding
β”‚   β”‚   β”‚   └── string_processing.py # String handling
β”‚   β”‚   └── exceptions.py      # Custom parsing exceptions
β”‚   β”œβ”€β”€ passes/
β”‚   β”‚   β”œβ”€β”€ collect/           # the collect pass
β”‚   β”‚   β”‚   β”œβ”€β”€ constants.py   # Constant definitions
β”‚   β”‚   β”‚   β”œβ”€β”€ functions.py   # Function signatures
β”‚   β”‚   β”‚   β”œβ”€β”€ structs.py     # Struct definitions
β”‚   β”‚   β”‚   β”œβ”€β”€ enums.py       # Enum definitions
β”‚   β”‚   β”‚   β”œβ”€β”€ perks.py       # Perk definitions
β”‚   β”‚   β”‚   └── utils.py       # Collection utilities
β”‚   β”‚   β”œβ”€β”€ scope.py           # the scope pass
β”‚   β”‚   β”œβ”€β”€ resolve.py         # the resolve pass
β”‚   β”‚   β”œβ”€β”€ const_eval.py      # constant evaluation (a helper, NOT a pass)
β”‚   β”‚   β”œβ”€β”€ derive.py          # the derive pass (hash + clone)
β”‚   β”‚   β”œβ”€β”€ finite_types.py    # the finite-types pass
β”‚   β”‚   β”œβ”€β”€ lift.py            # the lift pass (lambda lifting)
β”‚   β”‚   β”œβ”€β”€ borrow/            # the borrow pass (state in __init__.py,
β”‚   β”‚   β”‚   β”‚                  #   free-function siblings: statements, expressions,
β”‚   β”‚   β”‚   β”‚                  #   borrows, bindings, calls, consume, reads, writes,
β”‚   β”‚   β”‚   β”‚                  #   flow, diagnostics, types, destroy_effects)
β”‚   β”‚   └── types/             # the typecheck pass, modular
β”‚   β”‚       β”œβ”€β”€ utils.py       # Type utilities
β”‚   β”‚       β”œβ”€β”€ inference.py   # Type inference
β”‚   β”‚       β”œβ”€β”€ compatibility.py # Type compatibility
β”‚   β”‚       β”œβ”€β”€ propagation.py # Type propagation
β”‚   β”‚       β”œβ”€β”€ resolution.py  # Type resolution
β”‚   β”‚       β”œβ”€β”€ result_validation.py # Result handling
β”‚   β”‚       β”œβ”€β”€ field_matcher.py # Struct field matching
β”‚   β”‚       β”œβ”€β”€ perks.py       # Perk constraint checking
β”‚   β”‚       β”œβ”€β”€ expressions.py # Expression type checking
β”‚   β”‚       β”œβ”€β”€ matching.py    # Pattern match validation
β”‚   β”‚       β”œβ”€β”€ statements.py  # Statement validation
β”‚   β”‚       └── calls/         # Function call validation
β”‚   β”‚           β”œβ”€β”€ user_defined.py # User function calls
β”‚   β”‚           β”œβ”€β”€ methods.py # Method calls
β”‚   β”‚           β”œβ”€β”€ structs.py # Struct construction
β”‚   β”‚           β”œβ”€β”€ enums.py   # Enum construction
β”‚   β”‚           └── generics.py # Generic calls
β”‚   └── generics/
β”‚       β”œβ”€β”€ types.py           # Generic type definitions
β”‚       β”œβ”€β”€ name_mangling.py   # Name mangling for monomorphization
β”‚       β”œβ”€β”€ constraints.py     # Perk constraints
β”‚       β”œβ”€β”€ instantiate/       # the instantiate pass
β”‚       β”‚   β”œβ”€β”€ types.py       # Type instantiations
β”‚       β”‚   β”œβ”€β”€ functions.py   # Function instantiations
β”‚       β”‚   └── expressions.py # Expression instantiations
β”‚       β”œβ”€β”€ monomorphize/      # the monomorphize pass
β”‚       β”‚   β”œβ”€β”€ transformer.py # Main transformer
β”‚       β”‚   β”œβ”€β”€ types.py       # Type monomorphization
β”‚       β”‚   └── functions.py   # Function monomorphization
β”‚       └── providers/         # Generic type providers
β”‚           β”œβ”€β”€ interface.py   # Provider protocol
β”‚           └── registry.py    # Provider registry
β”œβ”€β”€ backend/
β”‚   β”œβ”€β”€ codegen_llvm.py        # Main LLVM orchestrator
β”‚   β”œβ”€β”€ constants/              # Centralized LLVM constants (DRY)
β”‚   β”‚   β”œβ”€β”€ llvm_values.py      # FALSE_I1, ZERO_I32, make_i32_const(), etc
β”‚   β”‚   β”œβ”€β”€ bit_widths.py
β”‚   β”‚   β”œβ”€β”€ error_codes.py
β”‚   β”‚   β”œβ”€β”€ hash_constants.py
β”‚   β”‚   β”œβ”€β”€ indices.py
β”‚   β”‚   └── sizes.py
β”‚   β”œβ”€β”€ gep_utils.py           # GetElementPtr utilities
β”‚   β”œβ”€β”€ enum_utils.py          # Enum tag/data utilities
β”‚   β”œβ”€β”€ destructors.py         # Unified recursive destruction
β”‚   β”œβ”€β”€ platform_detect.py     # Target platform detection
β”‚   β”œβ”€β”€ expressions/           # Expression emission (array codegen lives under
β”‚   β”‚   β”‚                      # backend/types/arrays/ instead, see below)
β”‚   β”‚   β”œβ”€β”€ literals.py
β”‚   β”‚   β”œβ”€β”€ operators.py       # Binary/unary operators
β”‚   β”‚   β”œβ”€β”€ memory.py
β”‚   β”‚   β”œβ”€β”€ casts.py
β”‚   β”‚   β”œβ”€β”€ structs.py         # Struct operations
β”‚   β”‚   β”œβ”€β”€ enums.py
β”‚   β”‚   β”œβ”€β”€ type_utils.py
β”‚   β”‚   └── calls/             # Subdivided for complexity
β”‚   β”‚       β”œβ”€β”€ dispatcher.py  # Main call routing
β”‚   β”‚       β”œβ”€β”€ generics.py    # Generic method instantiation
β”‚   β”‚       β”œβ”€β”€ intrinsics.py  # Compiler intrinsics
β”‚   β”‚       β”œβ”€β”€ file_open.py   # File I/O operations
β”‚   β”‚       β”œβ”€β”€ utils.py       # Call utilities
β”‚   β”‚       └── stdlib/        # Standard library calls, one module per area
β”‚   β”‚           # (io.py, strings.py, math.py, time.py, random.py, env.py,
β”‚   β”‚           #  process.py, primitives.py)
β”‚   β”œβ”€β”€ statements/            # Statement emission
β”‚   β”‚   β”œβ”€β”€ io.py
β”‚   β”‚   β”œβ”€β”€ loops.py           # while, foreach (19KB)
β”‚   β”‚   β”œβ”€β”€ control_flow.py
β”‚   β”‚   β”œβ”€β”€ returns.py
β”‚   β”‚   β”œβ”€β”€ variables.py       # let, rebind (13KB)
β”‚   β”‚   β”œβ”€β”€ matching.py        # Pattern matching (20KB)
β”‚   β”‚   β”œβ”€β”€ initialization.py  # Variable initialization patterns
β”‚   β”‚   └── utils.py           # Statement utilities (11KB)
β”‚   β”œβ”€β”€ runtime/               # Runtime support
β”‚   β”‚   β”œβ”€β”€ strings.py         # String operations
β”‚   β”‚   β”œβ”€β”€ formatting.py      # String interpolation
β”‚   β”‚   β”œβ”€β”€ errors.py          # Error handling
β”‚   β”‚   └── externs/           # Organized libc bindings
β”‚   β”‚       β”œβ”€β”€ libc_stdio.py  # printf, fopen, etc.
β”‚   β”‚       β”œβ”€β”€ libc_strings.py # strlen, strcmp, memcpy
β”‚   β”‚       β”œβ”€β”€ libc_ctype.py  # isalpha, isdigit, etc.
β”‚   β”‚       └── libc_process.py # exit, getenv, setenv
β”‚   β”œβ”€β”€ types/                 # Type-specific codegen
β”‚   β”‚   β”œβ”€β”€ arrays/             # Construction, indexing, bounds checks
β”‚   β”‚   β”‚   β”œβ”€β”€ dispatcher.py   # Array-method call routing
β”‚   β”‚   β”‚   β”œβ”€β”€ indexing.py     # Direct indexing, GEP helpers
β”‚   β”‚   β”‚   β”œβ”€β”€ bounds.py       # Bounds-check emission
β”‚   β”‚   β”‚   β”œβ”€β”€ literals.py     # Array literal construction
β”‚   β”‚   β”‚   β”œβ”€β”€ utils.py
β”‚   β”‚   β”‚   └── methods/        # Array method implementations
β”‚   β”‚   β”‚       β”œβ”€β”€ core.py     # len, get, push, pop
β”‚   β”‚   β”‚       β”œβ”€β”€ hashing.py  # Hash function generation
β”‚   β”‚   β”‚       β”œβ”€β”€ iterators.py # Iterator creation
β”‚   β”‚   β”‚       β”œβ”€β”€ safe_access.py # .get() Maybe@(T) wrapping
β”‚   β”‚   β”‚       β”œβ”€β”€ transforms.py # fill, reverse, clone
β”‚   β”‚   β”‚       └── utf8_validate.py # u8[].to_string_checked()
β”‚   β”‚   β”œβ”€β”€ primitives/          # i8..u64, f32/f64, bool
β”‚   β”‚   β”‚   β”œβ”€β”€ bit_reinterpret.py # to_bits()/from_bits()
β”‚   β”‚   β”‚   β”œβ”€β”€ hashing.py
β”‚   β”‚   β”‚   └── to_str.py
β”‚   β”‚   β”œβ”€β”€ structs.py
β”‚   β”‚   β”œβ”€β”€ enums.py
β”‚   β”‚   └── hash_utils.py
β”‚   β”œβ”€β”€ memory/                # Memory management
β”‚   β”‚   β”œβ”€β”€ scopes.py          # Scope-based cleanup
β”‚   β”‚   β”œβ”€β”€ dynamic_arrays.py  # Dynamic array management
β”‚   β”‚   └── heap.py            # Heap allocation (malloc/free)
β”‚   └── generics/              # Generic type implementations
β”‚       β”œβ”€β”€ codegen.py         # Generic code generation
β”‚       β”œβ”€β”€ enum_methods_base.py # Base for Result/Maybe
β”‚       β”œβ”€β”€ extensions.py      # Generic extension methods
β”‚       β”œβ”€β”€ maybe.py           # Maybe@(T) (19KB)
β”‚       β”œβ”€β”€ own.py             # Own@(T)
β”‚       β”œβ”€β”€ results.py         # Result@(T)
β”‚       β”œβ”€β”€ hashmap/           # HashMap@(K,V) implementation
β”‚       β”‚   β”œβ”€β”€ types.py
β”‚       β”‚   β”œβ”€β”€ validation.py
β”‚       β”‚   β”œβ”€β”€ utils.py
β”‚       β”‚   └── methods/
β”‚       β”‚       β”œβ”€β”€ core.py    # new, insert, get, contains
β”‚       β”‚       β”œβ”€β”€ mutations.py # remove, free, rehash
β”‚       β”‚       β”œβ”€β”€ debug.py   # debug printing
β”‚       β”‚       └── iterators.py # iterator support
β”‚       └── list/              # List@(T) implementation
β”‚           β”œβ”€β”€ types.py
β”‚           β”œβ”€β”€ validation.py
β”‚           β”œβ”€β”€ methods_simple.py # len, is_empty
β”‚           β”œβ”€β”€ methods_capacity.py # reserve, shrink
β”‚           β”œβ”€β”€ methods_modify.py # push, insert, remove
β”‚           β”œβ”€β”€ methods_destroy.py # free, destroy
β”‚           β”œβ”€β”€ methods_debug.py # debug printing
β”‚           └── methods_iter.py # iterator support
└── stdlib/
    β”œβ”€β”€ src/                   # Python source (LLVM IR generators)
    β”‚   β”œβ”€β”€ common.py          # Shared utilities
    β”‚   β”œβ”€β”€ conversions.py     # Type conversions
    β”‚   β”œβ”€β”€ error_emission.py  # Error code helpers
    β”‚   β”œβ”€β”€ ir_builders.py     # IR construction helpers
    β”‚   β”œβ”€β”€ ir_common.py       # Common IR patterns
    β”‚   β”œβ”€β”€ libc_declarations.py # Centralized libc declarations
    β”‚   β”œβ”€β”€ string_helpers.py  # String operation helpers
    β”‚   β”œβ”€β”€ type_converters.py # Type conversion utilities
    β”‚   β”œβ”€β”€ type_definitions.py # Type definition helpers
    β”‚   β”œβ”€β”€ collections/
    β”‚   β”‚   β”œβ”€β”€ strings/       # String operations (organized)
    β”‚   β”‚   β”‚   β”œβ”€β”€ common.py
    β”‚   β”‚   β”‚   β”œβ”€β”€ compiler/  # Built-in string ops
    β”‚   β”‚   β”‚   β”œβ”€β”€ intrinsics/ # Low-level UTF-8 ops
    β”‚   β”‚   β”‚   └── methods/   # High-level methods
    β”‚   β”‚   β”œβ”€β”€ list.py        # List@(T)
    β”‚   β”‚   └── hashmap.py     # HashMap@(K,V)
    β”‚   β”œβ”€β”€ io/
    β”‚   β”‚   β”œβ”€β”€ stdio/         # Platform-specific stdio
    β”‚   β”‚   β”‚   β”œβ”€β”€ common.py
    β”‚   β”‚   β”‚   β”œβ”€β”€ darwin.py
    β”‚   β”‚   β”‚   └── linux.py
    β”‚   β”‚   └── files/         # File operations
    β”‚   β”‚       β”œβ”€β”€ common.py
    β”‚   β”‚       β”œβ”€β”€ read.py
    β”‚   β”‚       β”œβ”€β”€ write.py
    β”‚   β”‚       β”œβ”€β”€ seek.py
    β”‚   β”‚       β”œβ”€β”€ status.py
    β”‚   β”‚       β”œβ”€β”€ iterators.py
    β”‚   β”‚       └── binary.py
    β”‚   β”œβ”€β”€ math/              # Math operations
    β”‚   β”‚   └── operations.py
    β”‚   β”œβ”€β”€ time/              # Time/sleep functions
    β”‚   β”œβ”€β”€ random/            # Random number generation
    β”‚   β”‚   └── generators.py
    β”‚   β”œβ”€β”€ sys/               # System modules
    β”‚   β”‚   β”œβ”€β”€ env/           # Environment variables
    β”‚   β”‚   └── process/       # Process control
    β”‚   └── _platform/         # Platform-specific implementations
    β”‚       β”œβ”€β”€ __init__.py    # get_platform_module() helper
    β”‚       β”œβ”€β”€ posix/         # POSIX implementations
    β”‚       β”œβ”€β”€ darwin/        # macOS implementations
    β”‚       └── linux/         # Linux implementations
    └── dist/                  # Platform-organized precompiled .bc files
        β”œβ”€β”€ darwin/            # macOS
        β”‚   β”œβ”€β”€ collections/strings.bc
        β”‚   β”œβ”€β”€ io/files.bc
        β”‚   β”œβ”€β”€ math.bc
        β”‚   β”œβ”€β”€ time.bc
        β”‚   β”œβ”€β”€ random.bc
        β”‚   └── sys/
        β”‚       β”œβ”€β”€ env.bc
        β”‚       └── process.bc
        └── linux/             # Linux (similar structure)

Semantic Passes

Fifteen passes, in this order. The passes have NAMES, not numbers -- a number goes out of order the moment a pass is inserted between two others. SemanticAnalyzer.check() is the code authority; semantic-passes.md documents each pass in detail.

whole program, once:
  collect -> externs -> libraries -> entrypoint -> instantiate -> monomorphize
     -> resolve -> finite-types -> derive -> shadowing -> effects

then per unit, in one loop:
  scope -> typecheck -> lift -> borrow

Six of them have no section below: externs (FFI signature validation), libraries (library symbol registration), entrypoint (main()'s signature), shadowing (an extension may not shadow a built-in, CE2097), effects (the destroy-effect summary) and lift (each lambda becomes a top-level function plus an environment). semantic-passes.md covers all six.

collect: headers and constants

Files: semantics/passes/collect/*.py

Responsibilities: - Parse constant definitions (collect/constants.py) - Collect function signatures (collect/functions.py) - Register struct definitions (collect/structs.py) - Register enum definitions (collect/enums.py) - Register perk definitions (collect/perks.py) - Build initial symbol table

Output: - Global constants map - Function signature registry - Generic type definitions

instantiate: generic instantiation collection

Files: semantics/generics/instantiate/*.py

Responsibilities: - Detect generic type usage (instantiate/types.py) - Detect generic method calls (instantiate/functions.py) - Infer type arguments from usage (instantiate/expressions.py) - Collect all required instantiations

Example:

let List@(i32) nums = List.new()  # Collect: List@(i32)
nums.push(42)                     # Collect: List@(i32).push

monomorphize: generic to concrete

Files: semantics/generics/monomorphize/*.py

Responsibilities: - Substitute generic type parameters (monomorphize/transformer.py) - Create concrete types from generic definitions (monomorphize/types.py) - Generate specialized function/method instances (monomorphize/functions.py)

Example:

extend Box@(T) unwrap() T
    ↓
extend Box@(i32) unwrap() i32
extend Box@(string) unwrap() string

resolve: field and variant type resolution

File: semantics/passes/resolve.py

Responsibilities: - Resolve every struct field type to the interned type the tables hold - Resolve every enum variant's associated types the same way

Example:

struct Rectangle:
    Point top_left      # collected as UnknownType("Point")
                        #        ↓
                        # resolved to the interned StructType

Type identity is nominal, so this pass is what makes the table entry the one authority (docs/design/type-identity.md).

derive: hash and clone auto-derivation

File: semantics/passes/derive.py

Responsibilities: - Auto-generate .hash() and .clone() for all types - Compose hash functions for structs - Validate hashability

Derived for: - Primitives (FxHash for ints, FNV-1a for strings) - Structs (field-wise hashing) - Enums (discriminant + variant data hashing) - Arrays (element-wise hashing)

scope: scope and variables

File: semantics/passes/scope.py

Responsibilities: - Variable declaration and usage tracking - Scope analysis - Move semantics validation - Borrow tracking initialization

Output: - Variable scopes - Move analysis results - Borrow tracking data

typecheck: type validation

Files: semantics/passes/types/*.py

Responsibilities: - Type checking all expressions - Result@(T) handling validation - Pattern match exhaustiveness - Type compatibility checking

Modular type checking: - types/resolution.py - Type resolution (Result@(T) wrapping) - types/propagation.py - Type propagation to constructors - types/result_validation.py - Result.Ok/Err validation - types/expressions.py - Expression type checking - types/statements.py - Statement validation - types/calls/*.py - Function call validation (user-defined, methods, structs, enums, generics) - types/matching.py - Pattern match validation - types/compatibility.py - Type compatibility checking - types/inference.py - Type inference - types/perks.py - Perk constraint checking - types/field_matcher.py - Struct field matching

borrow: borrow checking

File: semantics/passes/borrow/

  • __init__.py - BorrowChecker: the state, run(), and one callable's setup
  • statements.py / expressions.py - the two walks, each a match over the node
  • borrows.py / bindings.py / calls.py / consume.py - the rules
  • reads.py / types.py - what an expression reads, and the type algebra behind it
  • writes.py - the READONLY_RECEIVERS gate table
  • flow.py / state.py / diagnostics.py / destroy_effects.py

Responsibilities: - Ensure single active borrow per variable - Prevent move-while-borrowed - Prevent use-after-destroy - Track reference lifetimes

Errors detected: - CE2405: Use of moved variable - CE1007: Cannot rebind while borrowed - CE2406: Use of destroyed variable

Incremental Compilation

Multi-unit projects use per-unit .o file caching to avoid redundant LLVM codegen.

Architecture

Parse all .sushi files                           (always)
  β†’ Whole-program semantic analysis              (always, fast Python)
  β†’ Compute per-unit semantic fingerprints       (always, fast)
  β†’ Per-unit LLVM codegen β†’ .o file              (only if fingerprint changed)
  β†’ Link all .o files β†’ executable               (always)

Semantic analysis (passes 0-3) remains whole-program because generic instantiation collection and monomorphization need the complete call graph. This is pure Python and runs in well under a second. The expensive part β€” LLVM codegen + optimization + object emission β€” is cached per-unit.

Cache Structure

__sushi_cache__/
  cache.json                    ← manifest (compiler version, platform, opt level)
  units/
    main.o                      ← cached object file
    main.o.fingerprint          ← semantic fingerprint
    helpers/math.o
    helpers/math.o.fingerprint
    lib/mylib/mylib.o           ← a SOURCE library's unit, cached like any other
  stdlib/
    io_stdio.o                  ← compiled stdlib bitcode
  libsrc/
    mylib/mylib.sushi           ← a source library's materialized units
  libs/
    mylib.o                     ← a BINARY library's bitcode, compiled once

A source library has no separate object of its own: its units are ordinary units, so they land under units/ (namespaced lib/<library>/) and are invalidated by the same fingerprint machinery as the consumer's own files.

Fingerprint Computation

Each unit's fingerprint is a SHA-256 hash of: - Source file content - Public symbol signatures from dependencies - AST structure (structs, enums, extensions, perk impls, use statements) - Monomorphized extensions consumed by this unit

Linkage Rules

  • Public functions/constants: external linkage
  • Private functions/constants: internal linkage
  • Monomorphized generics: linkonce_odr linkage (linker deduplicates across units)
  • Inline runtime functions (llvm_strlen, llvm_strcmp, utf8_char_count): linkonce_odr

Key Files

  • compiler/pipeline.py β€” orchestrates monolithic vs incremental compilation paths
  • compiler/cache.py β€” CacheManager class: directory management, manifest, staleness detection
  • compiler/fingerprint.py β€” compute_unit_fingerprint(), compute_stdlib_fingerprint(), compute_lib_fingerprint()
  • backend/codegen_llvm.py β€” build_module_single_unit(), compile_single_unit_to_object(), link_object_files()

Backend Architecture

LLVM Code Generation

Main file: backend/codegen_llvm.py

Process: 1. Create LLVM module and function declarations 2. Emit standard library linkage 3. Generate code for each function 4. Apply optimization passes 5. Link with clang

Key classes: - LLVMCodeGenerator - Main orchestrator - ExpressionEmitter - Expression code generation - StatementEmitter - Statement code generation - TypeManager - LLVM type creation - MemoryManager - RAII and cleanup

Expression Emission

Located in backend/expressions/:

  • literals.py - Constants, strings, arrays
  • operators.py - Binary/unary operations
  • memory.py - Loads, stores, references
  • casts.py - Type conversions
  • structs.py - Struct field access
  • enums.py - Enum construction/matching
  • calls/ - Function/method calls, subdivided by concern: dispatcher.py (main call routing), generics.py, intrinsics.py, file_open.py, utils.py, and a stdlib/ subpackage with one module per stdlib area

Array expression codegen (construction, indexing, methods) lives under backend/types/arrays/ instead β€” see Type System below.

Statement Emission

Located in backend/statements/:

  • io.py - println, print, stdin/stdout
  • loops.py - while, foreach
  • control_flow.py - if/elif/else, break, continue
  • returns.py - Result.Ok/Err returns
  • variables.py - let, rebind
  • matching.py - match expressions

Type System

Located in backend/types/:

  • primitives/ - i8, i16, i32, i64, u8, u16, u32, u64, f32, f64, bool (bit_reinterpret.py, hashing.py, to_str.py)
  • arrays/ - Fixed and dynamic arrays: construction, indexing, bounds checks, and a methods/ subpackage for .len()/.get()/.push()/etc
  • structs.py - Struct layout and field access
  • enums.py - Enum discriminant and variant data
  • hash_utils.py - Hash function generation

Runtime Support

Located in backend/runtime/:

  • strings.py - String operations (len, size, find, split, trim)
  • formatting.py - String interpolation
  • errors.py - Runtime error messages
  • libc_externs.py - malloc, free, printf, etc.

Standard Library

Structure

stdlib/
β”œβ”€β”€ src/           # Python code that generates LLVM IR
└── dist/          # Precompiled .bc files

Import Mechanism

use <collections/strings>

Maps to: stdlib/dist/collections/strings.bc

Building stdlib

# Rebuild stdlib modules
cd stdlib/src/collections
python strings.py  # Generates ../../dist/collections/strings.bc

Optimization Pipeline

Levels

  • none - No optimization
  • mem2reg - SROA only (default)
  • O1 - Basic (CFG simplification, DCE, instruction combining)
  • O2 - Moderate (+ loop opts, GVN, SCCP, jump threading)
  • O3 - Aggressive (+ loop unrolling, strength reduction, inlining)

LLVM Passes

Applied in backend/codegen_llvm.py:apply_optimizations():

pm = llvm.ModulePassManager()

if opt_level == 'O1':
    pm.add_promote_memory_to_register_pass()
    pm.add_cfg_simplification_pass()
    pm.add_instruction_combining_pass()
    pm.add_dead_code_elimination_pass()

elif opt_level == 'O2':
    # O1 passes + ...
    pm.add_sccp_pass()
    pm.add_loop_rotation_pass()
    pm.add_gvn_pass()
    # ... etc

pm.run(module)

Recent Architectural Improvements

Breaking Circular Imports: Deferred Local Imports

There used to be a backend/interfaces.py module of Protocol classes for this purpose; it was deleted (Tier 4.5) and no Protocol-based scheme has replaced it (grep -rl Protocol sushi_lang/backend now returns nothing). The mechanism that actually breaks circular dependencies between backend components today is simpler: an import that would cycle at module-load time is written inside the function or method that needs it instead of at the top of the file, so the cycle only has to resolve at call time, by which point both modules have finished loading.

Example (backend/destructors.py, _emit_list_value_destructor):

def _emit_list_value_destructor(codegen, builder, value_ptr, value_type):
    from sushi_lang.backend.generics.list.types import extract_element_type
    element_type = extract_element_type(value_type, codegen)
    ...

generics/list/types.py needs destructor helpers, and destructors.py needs to inspect a List@(T)'s element type β€” a top-level import on either side would cycle. Deferring the import into the function body sidesteps it.

A second, narrower use of TYPE_CHECKING-guarded imports covers pure type annotations (no runtime import at all), e.g. backend/enum_utils.py:

if TYPE_CHECKING:
    from sushi_lang.backend.codegen_llvm import LLVMCodegen

Benefits (unchanged from the old Protocol approach): - Eliminates circular import issues - Reduces coupling between modules β€” a module only pays the import cost for the functions it actually calls

Centralized Utilities (DRY Principle)

Recent refactors have extracted common patterns into reusable utility modules:

LLVM Constants Module

File: backend/constants/llvm_values.py

Eliminates duplication of LLVM constant creation across 100+ call sites.

Provides:

# Boolean constants
FALSE_I1 = ir.Constant(ir.IntType(1), 0)
TRUE_I1 = ir.Constant(ir.IntType(1), 1)

# Integer constants
ZERO_I8 = ir.Constant(ir.IntType(8), 0)
ONE_I8 = ir.Constant(ir.IntType(8), 1)
ZERO_I32 = ir.Constant(ir.IntType(32), 0)
ONE_I32 = ir.Constant(ir.IntType(32), 1)
TWO_I32 = ir.Constant(ir.IntType(32), 2)
ZERO_I64 = ir.Constant(ir.IntType(64), 0)
ONE_I64 = ir.Constant(ir.IntType(64), 1)

# Factory functions
def make_i8_const(value: int) -> ir.Constant: ...
def make_i32_const(value: int) -> ir.Constant: ...
def make_i64_const(value: int) -> ir.Constant: ...

GetElementPtr Utilities

File: backend/gep_utils.py

Type-safe helpers for GEP (GetElementPtr) operations.

Functions:

def gep_struct_field(builder, struct_ptr, field_index):
    """Access struct field by index"""
    return builder.gep(struct_ptr, [ZERO_I32, make_i32_const(field_index)])

def gep_array_element(builder, array_ptr, index):
    """Access array element"""
    return builder.gep(array_ptr, [ZERO_I32, index])

def gep_dynamic_array_data(builder, dynarray_ptr):
    """Get data pointer from dynamic array struct"""
    return builder.gep(dynarray_ptr, [ZERO_I32, ZERO_I32])

def gep_dynamic_array_len(builder, dynarray_ptr):
    """Get length from dynamic array struct"""
    return builder.gep(dynarray_ptr, [ZERO_I32, ONE_I32])

def gep_dynamic_array_capacity(builder, dynarray_ptr):
    """Get capacity from dynamic array struct"""
    return builder.gep(dynarray_ptr, [ZERO_I32, TWO_I32])

Enum Utilities

File: backend/enum_utils.py

Centralized enum discriminant and variant data operations.

Functions:

def extract_enum_tag(builder, enum_ptr):
    """Load discriminant tag from enum"""
    tag_ptr = gep_struct_field(builder, enum_ptr, 0)
    return builder.load(tag_ptr)

def extract_enum_data(builder, enum_ptr, variant_type):
    """Extract variant data from enum"""
    data_ptr = gep_struct_field(builder, enum_ptr, 1)
    typed_ptr = builder.bitcast(data_ptr, variant_type.as_pointer())
    return builder.load(typed_ptr)

def compare_enum_variant(builder, enum_ptr, expected_tag):
    """Check if enum matches variant tag"""
    actual_tag = extract_enum_tag(builder, enum_ptr)
    return builder.icmp_signed('==', actual_tag, make_i32_const(expected_tag))

Unified Destructor Logic

File: backend/destructors.py

Replaces scattered destruction code with single recursive implementation.

Function:

def emit_value_destructor(codegen, builder, value, llvm_type, ast_type):
    """
    Recursively destroy value based on type.

    Handles:
    - Primitives: no-op
    - Strings: no-op (immutable)
    - Dynamic arrays: destroy elements, free buffer
    - Structs: destroy each field recursively
    - Enums: switch on discriminant, destroy variant data
    - Own@(T): destroy owned value, free pointer
    """
    # Type-aware dispatch...

Benefits: - Eliminates duplicated destruction logic (previously in 10+ files) - Ensures consistent cleanup behavior - Single point of maintenance for RAII - Powers HashMap.free(), List.destroy(), and error propagation cleanup

PassErrorReporter Helper

File: semantics/error_reporter.py

Reduces boilerplate in semantic passes by binding Reporter instance.

Before:

def validate_expression(expr, reporter):
    if not is_valid(expr):
        reporter.error("CE2001", f"Invalid expression: {expr}")
    # reporter passed to every function...

After:

error_reporter = PassErrorReporter(reporter)

def validate_expression(expr):
    if not is_valid(expr):
        error_reporter.error("CE2001", f"Invalid expression: {expr}")
    # No need to pass reporter around

Modularized Backend Structure

The backend has been organized into logical subdirectories for better maintainability:

  • backend/expressions/calls/ - Subdivided due to complexity; stdlib call routing is itself a stdlib/ subpackage, one module per stdlib area
  • backend/generics/ - Complete generic type system with HashMap and List implementations
  • backend/memory/ - Separated scope management, dynamic arrays, and heap operations
  • backend/runtime/externs/ - Organized libc bindings by category (stdio, strings, ctype, process)
  • backend/types/arrays/methods/ - Array method implementations organized by category

Key Design Patterns

Deferred Local Imports

There is no shared protocol/interface module. Circular dependencies between backend components are avoided by importing inside the function that needs the dependency rather than at module top level β€” see Breaking Circular Imports: Deferred Local Imports above for the mechanism and a concrete example from backend/destructors.py.

Recursive Destructors

MemoryManager.emit_value_destructor() handles cleanup for all types: - Primitives: no-op - Strings: no-op (immutable) - Arrays: iterate and destroy elements, free buffer - Structs: destroy each field, free struct - Enums: switch on discriminant, destroy variant data - Own@(T): destroy owned value, free pointer

Move Tracking

Variables marked as moved:

self.moved_variables.add(var_name)

Checked on every use:

if var_name in self.moved_variables:
    raise CompilerError(f"CE2405: Use of moved variable '{var_name}'")

Borrow Tracking

Active borrows tracked per variable:

self.active_borrows[var_name] = borrow_id

Prevents rebinding while borrowed:

if var_name in self.active_borrows:
    raise CompilerError(f"CE1007: Cannot rebind '{var_name}' while borrowed")

Error Handling

Error Code Ranges

  • CE0xxx - Internal errors (function-related)
  • CE1xxx - Scope/variable errors (undefined, moved, borrowed)
  • CE2xxx - Type errors (incompatible types, array bounds)
  • CE3xxx - Unit errors (module system)
  • CWxxxx - Warnings (unused Result, etc.)
  • RExxxx - Runtime errors (bounds check, malloc failure)

Error Reporting

Located in each pass file. Example:

raise CompilerError(
    f"CE2505: Cannot assign Result@({inner}) to {target_type} without handling. "
    f"Use .realise(default) to unwrap the Result."
)

Testing Strategy

Test Types

  • test_*.sushi - Must compile (exit 0)
  • test_warn_*.sushi - Compile with warnings (exit 1)
  • test_err_*.sushi - Must fail (exit 2)

Test Runner

python tests/run_tests.py

Compiles all tests and verifies expected exit codes.

Development Workflow

Adding a New Feature

  1. Update grammar (grammar.lark)
  2. Update AST builder (semantics/ast_builder/ β€” pick the matching module under declarations/, expressions/, statements/, or types/)
  3. Add semantic analysis (appropriate phase)
  4. Add code generation (backend/)
  5. Write tests (tests/test_feature.sushi)
  6. Run test suite

Debugging Compiler Issues

# See full traceback
./sushic --traceback program.sushi

# View AST
./sushic --dump-ast program.sushi

# View generated IR
./sushic --dump-ll program.sushi

# Save IR to file
./sushic --write-ll program.sushi

See also: - Semantic Passes - Detailed pass-by-pass analysis - Backend - LLVM code generation details