Plugin Architecture - lrytz/efftp GitHub Wiki
This page explains the main ideas in the architecture of the compiler plugin.
Effect Domains
Every effect domain is a subclass of EffectDomain
and defines:
- A representation for effects in the domain
- The lattice operations on those effects
- Methods to translate between the effect representation and the corresponding effect annotations
- Optionally a method computing the effect for specific abstract syntax trees (e.g.
try
/catch
trees in the case of exceptions)
If multiple effect domains are being checked, their domain definitions are combined when the plugin is created using the class BiEffectDomain
.
Type Constraints
Effect annotations extend from TypeConstraint
. This ensures that effect annotations in types are not erased by type maps such as asSeenFrom
:
class C {
val f1: (A => B) { def apply(a: A): B @pure } = .....
val f2 = this.f1
}
The type of f2
has effect annotations only because they extend TypeConstraint
, other annotations are removed in the asSeenFrom
type transformation.
Annotation Checker
The effects plugin registers an annotation checker, defined in class TypeCheckerPlugin
, which is invoked on subtype tests when at least one of the two types has an annotation.
The annotation checker is not used to check the latent effect of methods (explained below). It is only used to check subtyping of refinement types that have effect annotations. The following produces a type error due to the annotation checker:
val f: { def f: Unit @pure } = new { def f = { println() } }
The annotation checker also drives inference of refined types (again through subtyping tests issued by the compiler). In the following example, value c
has a refined type C { def f: Int @pure }
:
trait C { def f: Int }
val c = new C { def f = 10 }
Analyzer Plugin
The main component of the plugin is an analyzer plugin, defined in class TypeCheckerPlugin
.
Effect checking for method bodies is performed in the method pluginsTyped
which is invoked by the Scala type checker after type checking each tree. If the tree is a method definition, then the effect of the method body is computed and compared against the effect annotations on the methods return type.
Effect inference for method bodies is handled in the method pluginsTypeSig
. This method is called whenever the compiler first assigns a type to a symbol (i.e. when the lazy type of a symbol gets completed). For method symbols, if the type of the method was inferred (not annotated), the plugin will also infer the effect of the method body.
Anonymous functions get a special treatment in pluginsTyped
: the compiler always assigns a FunctionN
type to anonymous function trees (Function(params, body)
). The effects plugin assigns a more precise type to those anonymous functions which includes the inferred effect of the function body. This effect is added in the form of a type refinement, for instance (A => B) { def apply(a: A): B @pure }
.
Most of the other code in the file TypeCheckerPlugin
is necessary to correctly handle constructor effects.
Removing effect annotations from terms
Effect annotations only have a meaning if they appear on the return type of a method (with a few exceptions for constructors, default arguments, effect ascriptions).
However, because the plugin uses an annotation checker (see above), effect annotations on terms are not simply ignored: the annotation checker is invoked on every subtype test where one of the two types has some annotations.
Combined with the fact that effect annotations are type constraints (see above), this leads to misleading propagation of effect annotations:
def f(): Int @pure = 1
def g() = {
println()
f()
}
In this example, the Scala compiler assigns the type Int @pure
to the method invocation f()
. Since this is the last statement in the block, this type would be assigned as return type of method g
, which is clearly wrong - method g
is not pure.
A similar problem occurs when the expected type has effect annotations:
val i: Int = 1
def f(): Int @pure = i
When type checking the body of method f
, the Scala compiler uses Int @pure
as the expected effect. It will check if the the type of the body, Int
, is a subtype of the expected type. This type check will fail (due to the annotation checker) and lead to a spurious type mismatch error.
These two problems are overcome by removing effect annotations from terms and expected types:
- In
pluginsTyped
which is called after every tree that is type checked, we remove all effect annotations from the inferred type. This addresses the first example. - The method
pluginsPt
is called by the type checker before type checking a tree and allows modifying the expected type. The effects plugin removes all effect annotations from the expected type before type checking any tree. This addresses the second example.
Open Issues
There are still some unresolved issues related to the fact that the compiler propagates effect annotations to undesired places: