- The codebase mixes trait definitions with their implementations in a way that's hard to maintain
- The
Elaborater*
traits serve as interfaces whileProvide*
traits serve as implementations, but the separation is not clean - There are mutual dependencies between traits that make the current interface/implementation split ineffective
- There's a lot of boilerplate and repeated code patterns
Instead of using inheritance for implementation, use self-types to declare capabilities:
// Core capabilities
trait ElaborationCapability {
def newType: CellId[Term]
def unify(lhs: Term, rhs: Term): Unit
}
trait TypeCheckingCapability {
def checkType(expr: Expr): Term
def elabTy(expr: Option[Expr]): Term
}
// Elaboration capabilities
trait BlockElaborater { this: ElaborationCapability & TypeCheckingCapability =>
def elabBlock(
expr: Block,
ty0: CellIdOr[Term],
effects: CIdOf[EffectsCell]
): BlockTerm
protected def processDefLetDefStmt(
expr: LetDefStmt,
ctx: Context,
declarationsMap: Map[Expr, DeclarationInfo],
effects: CIdOf[EffectsCell]
): (Seq[StmtTerm], Context)
protected def processRecordStmt(
expr: RecordStmt,
ctx: Context,
declarationsMap: Map[Expr, DeclarationInfo],
effects: CIdOf[EffectsCell]
): (Seq[StmtTerm], Context)
}
trait FunctionElaborater { this: ElaborationCapability & TypeCheckingCapability =>
def elabFunction(
expr: FunctionExpr,
ty0: CellIdOr[Term],
effects: CIdOf[EffectsCell]
): Term
}
trait TypeElaborater { this: ElaborationCapability =>
def elab(
expr: Expr,
ty: CellIdOr[Term],
effects: CIdOf[EffectsCell]
): Term
}
Instead of rigid inheritance hierarchies, use intersection types to compose functionality:
type FullElaborater = BlockElaborater & FunctionElaborater & TypeElaborater
def createElaborater(using ctx: Context): FullElaborater = {
new BlockElaborater with FunctionElaborater with TypeElaborater
with ElaborationCapability with TypeCheckingCapability {
// Implementation details
}
}
- Clear Capability Requirements: Self-types make it explicit what capabilities each component needs
- Flexible Composition: Intersection types allow for more flexible composition of features
- Better Encapsulation: Implementation details stay in the implementation traits
- Reduced Coupling: Components only depend on the capabilities they need
- Easier Testing: Can mock individual capabilities for testing
- Define core capabilities as traits
- Convert existing traits to use self-types for declaring dependencies
- Use intersection types to compose the final implementation
- Gradually migrate existing code to new structure
ElaboraterBlock.scala
ElaboraterBase.scala
ElaboraterFunction.scala
ElaboraterFunctionCall.scala
ElaboraterCommon.scala
- Review and discuss this revised proposal
- Create proof of concept focusing on
ElaboraterBlock
and its dependencies - Test the new structure with existing functionality
- Plan full migration if proof of concept is successful