Type Checking System: Propagator Network and Design
NOTE THAT THIS DOCUMENT IS OUTDATED AS RELEVANT CODE IS BEING REWRITTEN
Quick Start Guide
Chester’s type checking system is powered by a propagator network - a constraint-based approach that allows for complex type relationships.
Simple Visual Overview
                      ┌─────────────┐
                      │ Type System │
                      └─────────────┘
                             │
                             ▼
                     ┌───────────────┐
                     │ Type Checking │
                     └───────────────┘
                             │
                             ▼
┌────────────────────────────────────────────────────┐
│                 Propagator Network                 │
├────────────────┬─────────────────┬────────────────┤
│                │                 │                │
│    ┌───────┐   │    ┌───────┐    │   ┌───────┐   │
│    │ Cell  │◄──┼────┤Prop 1 │◄───┼───┤ Cell  │   │
│    └───────┘   │    └───────┘    │   └───────┘   │
│        ▲       │        ▲        │       ▲       │
│        │       │        │        │       │       │
│    ┌───────┐   │    ┌───────┐    │   ┌───────┐   │
│    │Prop 2 │◄──┼────┤ Cell  │◄───┼───┤Prop 3 │   │
│    └───────┘   │    └───────┘    │   └───────┘   │
│                │                 │                │
└────────────────┴─────────────────┴────────────────┘
Key Concepts in 30 Seconds
- Cells - Hold type information and track connections to propagators
- Propagators - Define constraints between cells and propagate type information
- Network - The collection of cells and propagators that work together
When type checking:
- Cells store type information (like “this variable has type Integer”)
- Propagators enforce constraints (like “parameter type must match argument type”)
- When a cell value changes, connected propagators activate to propagate that change
This reactive network allows complex type relationships (like union types) to be modeled effectively.
Architecture
1. Core Components
- 
Cells ( HoldCell)- Hold type information and state
- Track propagator connections:
class HoldCell[+T <: CellRW[?,?]] { var store: CellRW[?,?] // Current value var didChange: Boolean // Change tracking var readingPropagators: Vector[PIdOf[Propagator[?]]] var zonkingPropagators: Vector[PIdOf[Propagator[?]]] }
 
- 
Propagators - Base trait defining propagator behavior:
trait Propagator[Ability] { def readingCells: Set[CellIdAny] def writingCells: Set[CellIdAny] def defaultingCells: Set[CellIdAny] def run(using state: StateAbility[Ability], more: Ability): Boolean def naiveZonk(needed: Vector[CellIdAny]): ZonkResult }
 
- Base trait defining propagator behavior:
2. Key Propagator Types
- 
Unify Propagator - Handles type unification and subtyping
- Special cases for:
- Meta variables
- Union types
- Intersection types
- List types
- Record types
 
 
- 
UnionOf Propagator - Manages union type construction
- Handles:
- Component type collection
- Meta variable resolution
- Type compatibility checks
 
 
- 
LiteralType Propagator - Handles literal type inference
- Manages type constraints for literals
 
3. Propagation Process
- 
Registration def addPropagatorGetPid[T <: Propagator[Ability]](propagator: T) { // 1. Create propagator holder val id = new HoldPropagator[T](uniqId, propagator) // 2. Register with reading cells for (cell <- propagator.readingCells) { cell.readingPropagators :+= id } // 3. Register with zonking cells for (cell <- propagator.defaultingCells) { cell.zonkingPropagators :+= id } // 4. Initial run if (propagator.run) { id.alive = false } }
- 
Execution def tick(using more: Ability): Unit = { while (didChanged.nonEmpty) { val id = didChanged.removeHead() if (id.didChange) { id.didChange = false // Run reading propagators for (p <- id.readingPropagators if p.alive) { if (p.store.run) { p.alive = false } } } } }
3. Union Type Subtyping
Chester supports union types (A|B) with a sophisticated subtyping relationship managed by the propagator network. The subtyping rules are implemented in the unify method in Elaborater.scala.
Union Subtyping Rules
- 
Union-to-Union Subtyping: (A|B) <: (C|D)- For each type in the right union, at least one type in the left union must accept it
- Implemented by creating propagator connections between compatible component types
 
- 
Specific-to-Union Subtyping: A <: (B|C)- A specific type can be used where a union is expected if it’s compatible with any union member
- Example: Passing an Integerto a function expectingInteger|String
 
- 
Union-to-Specific Subtyping: (A|B) <: C- A union can be assigned to a specific type if all union members are compatible with that type
- Example: Returning an Integer|Floatfrom a function that promises to returnNumber
 
Implementation Challenges
The union type subtyping implementation addresses several challenges:
- Cell Coverage: Ensuring all cells (including component types) are properly covered by propagators
- Meta Variables in Unions: Special handling for meta variables that appear in unions
- Early Return Prevention: Avoiding early returns that could leave cells uncovered
- Component Tracking: Ensuring each component of a union has proper propagator connections
Current Implementation Status
The implementation of the core type system features has been completed with recent significant enhancements. Major milestones include:
1. Cell Coverage Improvements (2025-04-21)
The previously “hacky” approach to cell coverage has been completely redesigned:
- Removed the AutoConnectpropagator and related indirection
- Implemented direct type relationship handling during unification
- Created explicit relationships between types directly at creation/unification points
- Simplified codebase by removing several layers of indirection
- Maintained the same type checking capabilities with a cleaner implementation
2. Union Type Subtyping (2025-03-25)
Union types are now fully implemented with support for all subtyping relationships:
- Union-to-Union: (A|B) <: (C|D)with proper component compatibility
- Specific-to-Union: A <: (B|C)for cases like passingIntegertoInteger|String
- Union-to-Specific: (A|B) <: Cfor returning unions from specific return type functions
3. Trait Implementation (2025-03-19)
Basic trait support is now available:
- Empty traits and record extension using <:syntax
- Trait-record subtyping relation in type system
- TraitTypeTermrepresentation with proper error reporting
- Context tracking for trait processing
Remaining Challenges
- 
Type-Level Computation - Further improvements to recursive type-level function applications
- Advanced dependent types with multiple levels of abstraction
- More comprehensive testing of type-level computation
 
- 
Advanced Trait Features - Complete field requirement verification
- Multiple trait inheritance
- Trait methods and default implementations
- Trait-to-trait inheritance constraints
 
Testing Strategy
1. Coverage Tests
- Test basic cell coverage
- Test union type component coverage
- Test meta variable coverage
2. State Tests
- Test propagator lifecycle
- Test union type state
- Test meta variable resolution
3. Integration Tests
- Test complex type scenarios
- Test error handling
- Test performance
Trait Implementation
Chester’s type system now supports traits and record-trait subtyping relationships through the <: syntax, with the following features implemented:
1. Trait Definition and Implementation
// Define a trait
trait WithName {
  def name: String;
}
// Record implementing a trait
record Person(name: String, age: Integer) <: WithName;
// Using the record with correct field
def getName(p: Person): String = p.name;
2. Trait Subtyping Rules
The type system implements several trait-related subtyping rules:
- Record-Trait Subtyping: Records that extend traits are considered subtypes of those traits
- Trait-Record Compatibility: Traits can be used where their implementing records are expected
- Trait-Trait Inheritance: Traits can extend other traits
3. Implementation Components
The trait implementation consists of several key components:
- TraitTypeTerm in Term.scalafor trait type representation
- TraitStmtTerm for trait definitions with optional bodies
- processTraitStmt in ElaboraterBlock.scalato handle trait declarations
- checkTraitImplementation in TyckPropagator.scalato verify trait implementation
- Special context tracking with withProcessingTypeto handle trait bodies
4. Future Enhancements
Future work will focus on:
- Complete field requirement verification
- Multiple trait inheritance
- Trait methods and default implementations
Best Practices for Cell Management
OnceCell Usage Guidelines
The OnceCell implementation is a specialized cell type that enforces single-assignment semantics. Proper usage requires careful attention to avoid errors:
1. Cell Filling Best Practices
- Always check before filling: Before calling fill()on any cell, check if it already has a value usingstate.readUnstable(cell).
- Handle already-filled cells gracefully: When a cell already has a value, check if new value equals existing value.
- Use debug assertions: Include debug prints for cell operations to trace propagation flow.
2. Propagator Implementation Guidelines
- Declare cell dependencies correctly: Ensure all propagators correctly declare their reading and zonking cell dependencies.
- Implement naiveZonk correctly: The naiveZonkmethod should return appropriate dependencies for zonking.
- Be idempotent: Propagator’s runmethod should be idempotent - multiple calls with the same input should produce the same output.
3. Type-Level Function Handling
When implementing propagators that handle type-level functions:
- Watch for recursive effects: Type-level functions may cause recursive propagation attempts
- Avoid modifications during reduction: When reducing for type equality, don’t modify the original terms
- Use correct reduction mode: Use ReduceMode.TypeLevelfor reductions needed for type equality checks