Further discussions have convinced me that we should strive to uphold the principle that
A: T and m member of T implies A.m is defined.(*)
So we need an object A
defined alongside a context-bounded type A
that supports that strategy. And it should work also for multiple context bounds and context-bounded type members. At the same time defining that object as a run-time value (aka aggregate givens) will probably not fly. Sometimes the object cannot be created. At other times we do not want an extra object creation since it could entail an inefficient allocation. Finally, it does not work for type members since we’d end up with multiple object members, all with the same name.
But we can do the fancy phantom object trick. Here’s how this might be implemented.
-
In Namer, when encountering a context bounded type parameter
A: {C1, ..., Cn}
add an abstract valueval A: C1[A] & ... & Cn[A]
in the same scope as the type parameter. This needs to be added only as a symbol, no actual definition AST is necessary. When encountering a context bounded typetype A: C
, also add an abstract val symbolval A: C[A]
in the scope where the member is defined (and analogously for multiple context bounds). We call these synthetic symbols context bound companions. A context bound companion is not created if there is another value (not a method) defined in the same scope and with the same name as the companion (this includes explicitly named evidence parameters or members). -
These symbols participate as usual in type checking and inference done by Typer.
-
After Typer, analyze every reference
A.m
or possiblyp.A.m
whereA
is defined. The reference will have a symbol. If that symbol is in a base class of several context bounds, emit an error, the selection is ambiguous. Otherwise let the bound deriving the symbol’s owner beCi[A]
and letw
be the name of the evidence parameter or member corresponding to that bound. Replace the reference withw.m
orp.w.m
, respectively.Also, check that for every other accessible member named
m
in one of the other boundsCj[A]
we have one of the following:- the members are provably the same (i.e. type aliases of the same underlying type, or vals with the same singleton type), or
- the type of
m
in Ci[A] is a strict subtype of the type ofm
inCj[A]
.
One purpose of this check is to show that member selection does not depend on the order in which the context bounds appear.
-
Also, after Typer check that no references to context-bound companions remain. Flag each such reference as an error with a message like “context bound companion A is not a value, use an explicitly named evidence or a summon instead.”.
-
After mapping and checking selections on context bound companions, remove all context bound companions from their scopes.
I believe this would be a workable strategy.
(*) Strictly speaking, that principle really holds only for context bounds based on Self-type members (which is another reason why these work so nicely). For context bounds as type constructors, we’d have to tie the knot explicitly:
A: C and m member of C[A] implies A.m is defined.
The analogy with term selection is a bit less clear. Still it’s a useful and intuitively appealing principle.