ADR-0002: Single-Pass Bidirectional Type Checking
Status
Implemented
Summary
Refactor semantic analysis to use a synthesize/check pattern, eliminating redundant tree traversals where infer_type() and analyze_inst() walk the same subtree separately.
Context
The current semantic analysis in gruel-air/src/sema.rs traverses the RIR multiple times:
infer_type()walks the tree to determine a type bottom-upanalyze_inst()walks the same tree again to emit AIR instructions
This happens in several places:
Allocwithout type annotation: infer initializer type, then analyze itBlockwith Unit context: infer last expression type, then analyze itFieldGet: infer base type, then analyze baseanalyze_comparison: infer LHS type, then analyze both operands
The redundancy comes from bidirectional type checking: we need to know a type before we can check against it, but determining the type requires traversing the subtree.
Decision
Refactor analyze_inst to use a synthesize/check pattern where:
- Synthesize mode: Infer the type bottom-up, emit AIR, return both
- Check mode: Verify against expected type top-down, emit AIR
This eliminates the separate infer_type() traversal by having analyze_inst always return the synthesized type along with the AIR reference.
Core Type: TypeExpectation
/// Describes what type we expect from an expression.
Modified analyze_inst Signature
/// Result of analyzing an instruction: the AIR reference and its type.
/// Analyze an RIR instruction, producing AIR instructions.
///
/// Returns both the AIR reference and the synthesized type.
/// When `expectation` is `Check(ty)`, validates that the result is compatible.
Implementation Phases
- Phase 1: Core refactor - Add TypeExpectation, AnalysisResult, migrate analyze_inst
Consequences
Positive
- Eliminates duplicate traversals: Each subtree is visited exactly once
- Cleaner code: Type synthesis and checking unified in one place
- Better error messages: We have full context when errors occur
- Foundation for type inference: This pattern scales to Hindley-Milner style inference
- Performance: Fewer function calls, better cache locality
Negative
- Larger return type: Every call returns
(AirRef, Type)instead of justAirRef - Migration effort: Significant refactor of ~1400 lines in sema.rs
- Testing: Need comprehensive tests to ensure behavior unchanged
Open Questions
None remaining.
Future Work
- Full Hindley-Milner type inference if needed for generics
References
- Bidirectional Typing - Dunfield & Krishnaswami
- Rust's type inference uses a similar synthesize/check pattern
- Swift's type checker explicitly uses bidirectional inference