Fortress: Coercion lifting
UPDATE AT BOTTOM!
The following is a pretty long post detailing my proposal for dealing with user-defined coercions in Fortress. It has nothing to do with my research really, but it sure took a long time to type up!
During disambiguation of any compilation unit, if there is a trait Foo[\T\] with a coercion declared as `coercion[\U\](…)`, create a new top-level function `coerce_Foo[\T, U\](…)` exactly as if the user had written it himself. This is the same function that will be called wherever a coercion to Foo occurs. These lifted declarations/definitions are annotated in the sample code below.
These definitions should be lifted after type checking occurs, mainly so that type checking work (and error messages) are not duplicated between the original definitions and the lifted ones. Since type checking of both implicit and explicit coercion invocations can be performed using the original coercion declarations, the lifted versions need not exist before checking use sites. Although this lifting can occur in a post-type checking desugaring phase, depending on how the invocation rewriting is handled, it might be necessary to lift them before type checking. (This would be the case if invocations are replaced with calls to the lifted names and then type checked as normal applications, rather than explicitly being given all necessary type information.)
api Decl
trait Foo[\T\]
coerce[\U\](x: Bar[\U\])
end
(*) Lifted declaration:
(*) coerce$Foo[\T, U\](x: Bar[\U\]): Foo[\T\]
object Bar[\T\]
makeFoo(): Foo[\T\]
end
end
component Impl
export Decl
trait Foo[\T\]
coerce[\U\](x: Bar[\U\]) = ...
end
(*) Lifted definition:
(*) coerce$Foo[\T, U\](x: Bar[\U\]) = ...
object Bar[\T\]
(*) COERCION
makeFoo() = self
(*) ------> coerce$Foo[\T, T\](self)
end
end
component Prog1
import Decl.{...}
export Executable
run() = do
(*) COERCION
x: Foo[\ZZ\] = Bar[\ZZ\]
(*) ---------> Decl.coerce$Foo[\ZZ, ZZ\](Bar[\ZZ\])
end
end
component Prog2
import Decl.{Foo => Oof}
(*) Additional import explained below.
(*) import Decl.coerce$Foo
export Executable
(*) Not the same as Decl.Foo!
trait Foo[\T\]
coerce[\U\](y: Bar[\U\]) = ...
end
(*) Lifted definition:
(*) coerce$Foo[\T, U\](y: Bar[\U\]) = ...
run() = do
(*) COERCION
x: Oof[\ZZ\] = Bar[\ZZ\]
(*) ---------> Decl.coerce$Foo[\ZZ, ZZ\](Bar[\ZZ\])
(*) COERCION
x: Foo[\RR\] = Bar[\RR\]
(*) ---------> coerce$Foo[\RR, RR\](Bar[\RR\])
end
end
Note that lifting the coercion declaration in the API Decl creates a new abstract declaration that must be implemented in any component that exports Decl. Indeed, whenever a component exporting Decl contains the definition required by the original `coercion` declaration, that definition will be lifted to the component level and thus satisfy the API’s requirement.
Now consider the coercion invocations in each of the three components above:
* Impl: The coercion on the body of `makeFoo` will be rewritten to the call `coerce$Foo[\T, T\](self)`. This function exists in scope due to its being lifted into the component level of Impl.
* Prog1: Because the argument is coerced to type Foo, which was imported through interface Decl, the rewritten call is made to the definition supplied through Decl. Since any component linked in with Prog1 to satisfy Decl will necessarily contain the definition `coerce$Foo`, this call will dispatch normally. Notice that the call is qualified unnecessarily — this merely simplifies the rewriting logic.
* Prog2 (a): Note that the trait Foo occurring in Decl has been renamed to Oof. The first coercion then refers to this renamed type. When the coercion call is inserted, we cannot simply write it as `APINAME.coerce$TRAITNAME(ARGS)` as with the others. Instead, we need to use the name for which the target trait type is an alias (i.e. Foo). Again we qualify the call with `Decl` since it refers to a coercion that must be suppled via the API Decl. Notice also the extra `import Decl.coerce$Foo`: whenever a trait with coercions is imported explicitly (i.e. not as part of an ellipsis), we need to add in the import for its lifted coercion so that the code gets linked in.
* Prog2 (b): The second coercion refers to the trait Foo declared in this component. Therefore, as was the case for the Impl component, we simply rewrite the invocation to the call `coerce$Foo(…)`. Note that this is completely unaffected by the import of `Decl.Foo`.
To recap, here are the rewriting procedures that need to be performed:
1. Any coercion declaration/definition `coerce[\U\]…` occurring in a trait Foo[\T\] is copied to a declaration/definition `coerce$Foo[\T, U\]…` at the level of the compilation unit.
2. In any component C that explicitly imports a trait Bar from API A, where Bar declares coercions, create an additional `import A.coerce$Bar`. Note that Bar is the actual name and not the alias, if there is one.
3. Wherever there is a coercion to trait type Baz[\T\] with inferred static arg U, it is rewritten to the call `B.coerce$Buzz[\T, U\](…)`, where B is the API from which B was imported (or empty if Baz was not imported), and Buzz is the actual name of the trait referred to by the alias Baz.
UPDATE: This has now been implemented and committed. I mainly stuck to this description but with a couple exceptions: all external coercions are qualified (as opposed to only the ones for explicitly imported traits), and the coercion declarations/definitions are actually moved to top-level, rather than copied.