Support type-level computation during type checking by adding type-level reduction mode. Example:
record A(a: Integer);
def idType(x: Type): Type = x;
let aT = A;
def getA(x: A): Integer = x.a;
def getA1(x: aT): Integer = x.a;
Elaborater.scala
: Type checks and elaborates terms, with support for evaluation during checkingReducer.scala
: Reduces terms to normal form, including function application- System preserves original terms in core representation unless reduction needed
Modify NaiveReducer
to support different reduction modes:
- Add
ReduceMode
enum for controlling reduction strategy - Make recursion explicit using passed reducer
- Support type-level reduction mode
Code Changes:
/** Controls how aggressively terms are reduced */
enum ReduceMode {
case TypeLevel // Only reduce type-level computations
case Normal // Normal reduction strategy
}
object NaiveReducer extends Reducer {
def reduce(term: Term, mode: ReduceMode)(using ctx: ReduceContext, r: Reducer): Term = {
// ... match on term ...
case FCallTerm(f, args, meta) =>
// ... reduce function and args ...
case Function(FunctionType(telescopes, retTy, _, _), body, _) =>
val substitutedBody = /* ... */
mode match {
case ReduceMode.Normal => r.reduce(substitutedBody)
case ReduceMode.TypeLevel => retTy match {
case Type(_, _) => r.reduce(substitutedBody)
case _ => substitutedBody
}
}
}
}
Key principle: Keep original terms in elaborated results, only reduce internally when needed.
Example:
let aT = idType(A); // Keep as idType(A) in elaborated result
let bT = aT; // Keep as aT in elaborated result
Use type-level reduction ONLY during field access checking:
- Keep original term in elaborated result
- Use reduction internally to verify field exists
- Handle field access on reduced type values
Example:
record A(a: Integer);
let aT = idType(A);
def getA1(x: aT): Integer = x.a; // Elaborated result keeps aT
// Internally reduce to check field
Test both preservation and reduction:
- Term preservation:
def idType(x: Type): Type = x;
let aT = idType(A); // Should stay as idType(A) in elaborated result
- Internal reduction for type checking:
record A(a: Integer);
let aT = idType(A);
def getA1(x: aT): Integer = x.a; // Should verify field exists by reduction
// But keep aT in elaborated result
- ALWAYS run
sbt rootJVM/test
before committing - Fix any test failures before proceeding
- Add new tests for any new functionality
- Update existing tests when modifying behavior
-
Term Preservation
def idType(x: Type): Type = x; let aT = idType(A); // Test elaborated result contains idType(A) let bT = aT; // Test elaborated result contains aT
-
Type-Level Reduction
record A(a: Integer); def idType(x: Type): Type = x; let aT = idType(A); def getA1(x: aT): Integer = x.a; // Test field access works def getA2(x: idType(A)): Integer = x.a; // Test direct type-level function application
-
Error Cases
record A(a: Integer); def wrongType(x: Type): Type = B; // Non-existent type let aT = wrongType(A); def getA(x: aT): Integer = x.a; // Should report clear error
-
Edge Cases
record A(a: Integer); def idType(x: Type): Type = x; let aT = idType(A); let bT = idType(aT); // Nested type-level computation def getA(x: bT): Integer = x.a; // Should work correctly
Type-level reduction should only happen in two specific places:
- During type equality checking in
unify
- This is necessary to determine if two types are equal after evaluating any type-level computations. - During field access type checking - When checking field access on a type constructed by a type-level function, we need to reduce the type to see its structure.
The elaborated result should preserve the original terms whenever possible:
- In field access (
DotCall
), store the original term inFieldAccessTerm
while using the reduced type only internally for checking. - In let/def bindings, store the original terms without reduction.
- When adding bindings to the context, use the original unreduced terms.
- DO NOT reduce terms during elaboration unless explicitly needed for type checking.
- DO NOT reflect internal reductions in the elaborated result.
- DO NOT reduce terms when adding them to the context.
- DO ensure that type-level reduction is used in
unify
for type equality checking. - DO ensure that field access type checking uses type-level reduction while preserving original terms.
When implementing new features:
- Write tests that verify original terms are preserved in elaborated results
- Write tests that verify type checking works correctly with type-level computation
- Write tests that verify field access works with types constructed by type-level functions
- Write tests that verify type equality checking works with reduced types
-
DO use type-level reduction ONLY for:
- Type equality checking in unification
- Field access checking on type-level terms
- NEVER in elaborated results
-
DO NOT use type-level reduction for:
- Elaboration of any terms
- Let/def binding processing
- Any place where original terms should be kept
-
Over-reduction
let aT = A; let bT = aT; // WRONG: Don't reduce to A during elaboration // RIGHT: Keep as aT in elaborated result
-
Loss of Original Terms
def idType(x: Type): Type = x; let aT = idType(A); def getA1(x: aT): Integer = x.a; // WRONG: Reducing idType(A) in result // RIGHT: Keep idType(A), only reduce for checking
-
Incorrect Reduction Timing
- WRONG: Reducing during elaboration
- RIGHT: Only reduce during type checking when needed
- Keep reduction internal to type checker
-
Term Preservation
- Always keep original terms in elaborated result
- Never reduce terms just for elaboration
- Preserve source code structure exactly
-
Minimal Reduction
- Only reduce when immediately necessary
- Keep reduction internal to type checker
- Never reduce speculatively
-
Clear Boundaries
- Elaboration never reduces
- Type checking reduces only when needed
- Keep original terms everywhere else