Skip to content

Commit 682fec0

Browse files
committed
Implementation of byname implicits with recursive dictionaries
This is an implementation of byname implicit parameters with recursive dictionaries, intended as a language-level replacement for shapeless's Lazy type. As of this commit, implicit arguments can be marked as byname and at call sites recursive uses of implicit values are permitted if they occur in an implicit byname argument. Consider the following example, trait Foo { def next: Foo } object Foo { implicit def foo(implicit rec: Foo): Foo = new Foo { def next = rec } } val foo = implicitly[Foo] assert(foo eq foo.next) In current Scala this would diverge due to the recursive implicit argument rec of method foo. Under the scheme implemented in this commit we can mark the recursive implicit parameter as byname, trait Foo { def next: Foo } object Foo { implicit def foo(implicit rec: => Foo): Foo = new Foo { def next = rec } } val foo = implicitly[Foo] assert(foo eq foo.next) and recursive occurrences of this sort are extracted out as val members of a local synthetic object as follows, val foo: Foo = scala.Predef.implicitly[Foo]( { object LazyDefns$1 { val rec$1: Foo = Foo.foo(rec$1) // ^^^^^ // recursive knot tied here } LazyDefns$1.rec$1 } ) assert(foo eq foo.next) and the example compiles with the assertion successful. Note that the recursive use of rec$1 occurs within the byname argument of foo and is consequently deferred. The desugaring matches what a programmer would do to construct such a recursive value explicitly. This general pattern is essential to the derivation of type class instances for recursive data types, one of shapeless's most common applications. Byname implicits have a number of benefits over the macro implementation of Lazy in shapeless, + the implementation of Lazy in shapeless is extremely delicate, relying on non-portable compiler internals. By contrast there is already an initial implementation of byname implicits in Dotty: scala/scala3#1998. + the shapeless implementation is unable to modify divergence checking, so to solve recursive instances it effectively disables divergence checking altogether ... this means that incautious use of Lazy can cause the typechecker to loop indefinitely. The byname implicits implementation is able to both solve recursive occurrences and check for divergence. + the implementation of Lazy interferes with the heuristics for solving inductive implicits in scala#6481 because the latter depends on being able to verify that induction steps strictly reduce the size of the types being solved for; the additional Lazy type constructors make the type appear be non-decreasing in size. Whilst this could be special-cased, doing so would require some knowledge of shapeless to be incorporated into the compiler. Being a language-level feature, byname implicits can be accommodated directly in the induction heuristics. + in common cases more implicit arguments would have to be marked as Lazy than would have to be marked as byname under this PR due to restrictions on what the Lazy macro is able to do. Given that there is a runtime cost associated with capturing the thunks required for both Lazy and byname arguments, any reduction in the number is beneficial.
1 parent 964d127 commit 682fec0

39 files changed

+1740
-65
lines changed

spec/04-basic-declarations-and-definitions.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -693,9 +693,7 @@ function. That is, the argument is evaluated using _call-by-name_.
693693

694694
The by-name modifier is disallowed for parameters of classes that
695695
carry a `val` or `var` prefix, including parameters of case
696-
classes for which a `val` prefix is implicitly generated. The
697-
by-name modifier is also disallowed for
698-
[implicit parameters](07-implicits.html#implicit-parameters).
696+
classes for which a `val` prefix is implicitly generated.
699697

700698
###### Example
701699
The declaration

spec/07-implicits.md

Lines changed: 100 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -177,22 +177,91 @@ expansion:
177177
sort(arg)(x => magic(x)(x => magic(x)(x => ... )))
178178
```
179179

180-
To prevent such infinite expansions, the compiler keeps track of
181-
a stack of “open implicit types” for which implicit arguments are currently being
182-
searched. Whenever an implicit argument for type $T$ is searched, the
183-
“core type” of $T$ is added to the stack. Here, the _core type_
184-
of $T$ is $T$ with aliases expanded, top-level type [annotations](11-annotations.html#user-defined-annotations) and
185-
[refinements](03-types.html#compound-types) removed, and occurrences
186-
of top-level existentially bound variables replaced by their upper
187-
bounds. The core type is removed from the stack once the search for
188-
the implicit argument either definitely fails or succeeds. Everytime a
189-
core type is added to the stack, it is checked that this type does not
190-
dominate any of the other types in the set.
191-
192-
Here, a core type $T$ _dominates_ a type $U$ if $T$ is
193-
[equivalent](03-types.html#equivalence)
194-
to $U$, or if the top-level type constructors of $T$ and $U$ have a
195-
common element and $T$ is more complex than $U$.
180+
Such infinite expansions should be detected and reported as errors, however to support the deliberate
181+
implicit construction of recursive values we allow implicit arguments to be marked as by-name. At call
182+
sites recursive uses of implicit values are permitted if they occur in an implicit by-name argument.
183+
184+
Consider the following example,
185+
186+
```scala
187+
trait Foo {
188+
def next: Foo
189+
}
190+
191+
object Foo {
192+
implicit def foo(implicit rec: Foo): Foo =
193+
new Foo { def next = rec }
194+
}
195+
196+
val foo = implicitly[Foo]
197+
assert(foo eq foo.next)
198+
```
199+
200+
As with the `magic` case above this diverges due to the recursive implicit argument `rec` of method
201+
`foo`. If we mark the implicit argument as by-name,
202+
203+
```scala
204+
trait Foo {
205+
def next: Foo
206+
}
207+
208+
object Foo {
209+
implicit def foo(implicit rec: => Foo): Foo =
210+
new Foo { def next = rec }
211+
}
212+
213+
val foo = implicitly[Foo]
214+
assert(foo eq foo.next)
215+
```
216+
217+
the example compiles with the assertion successful.
218+
219+
When compiled, recursive by-name implicit arguments of this sort are extracted out as val members of a
220+
local synthetic object at call sites as follows,
221+
222+
```scala
223+
val foo: Foo = scala.Predef.implicitly[Foo](
224+
{
225+
object LazyDefns$1 {
226+
val rec$1: Foo = Foo.foo(rec$1)
227+
// ^^^^^
228+
// recursive knot tied here
229+
}
230+
LazyDefns$1.rec$1
231+
}
232+
)
233+
assert(foo eq foo.next)
234+
```
235+
236+
Note that the recursive use of `rec$1` occurs within the by-name argument of `foo` and is consequently
237+
deferred. The desugaring matches what a programmer would do to construct such a recursive value
238+
explicitly.
239+
240+
To prevent infinite expansions, such as the `magic` example above, the compiler keeps track of a stack
241+
of “open implicit types” for which implicit arguments are currently being searched. Whenever an
242+
implicit argument for type $T$ is searched, $T$ is added to the stack paired with the implicit
243+
definition which produces it, and whether it was required to satisfy a by-name implicit argument or
244+
not. The type is removed from the stack once the search for the implicit argument either definitely
245+
fails or succeeds. Everytime a type is about to be added to the stack, it is checked against
246+
existing entries which were produced by the same implicit definition and then,
247+
248+
+ if it is equivalent to some type which is already on the stack and there is a by-name argument between
249+
that entry and the top of the stack. In this case the search for that type succeeds immediately and
250+
the implicit argument is compiled as a recursive reference to the found argument. That argument is
251+
added as an entry in the synthesized implicit dictionary if it has not already been added.
252+
+ otherwise if the _core_ of the type _dominates_ the core of a type already on the stack, then the
253+
implicit expansion is said to _diverge_ and the search for that type fails immediately.
254+
+ otherwise it is added to the stack paired with the implicit definition which produces it.
255+
Implicit resolution continues with the implicit arguments of that definition (if any).
256+
257+
Here, the _core type_ of $T$ is $T$ with aliases expanded,
258+
top-level type [annotations](11-annotations.html#user-defined-annotations) and
259+
[refinements](03-types.html#compound-types) removed, and occurrences of top-level existentially bound
260+
variables replaced by their upper bounds.
261+
262+
A core type $T$ _dominates_ a type $U$ if $T$ is [equivalent](03-types.html#equivalence) to $U$,
263+
or if the top-level type constructors of $T$ and $U$ have a common element and $T$ is more complex
264+
than $U$ and the _covering sets_ of $T$ and $U$ are equal.
196265

197266
The set of _top-level type constructors_ $\mathit{ttcs}(T)$ of a type $T$ depends on the form of
198267
the type:
@@ -211,6 +280,21 @@ the type:
211280
- For any other singleton type, $\operatorname{complexity}(p.type) ~=~ 1 + \operatorname{complexity}(T)$, provided $p$ has type $T$;
212281
- For a compound type, `$\operatorname{complexity}(T_1$ with $\ldots$ with $T_n)$` $= \Sigma\operatorname{complexity}(T_i)$
213282

283+
The _covering set_ $\mathit{cs}(T)$ of a type $T$ is the set of type designators mentioned in a type.
284+
For example, given the following,
285+
286+
```scala
287+
type A = List[(Int, Int)]
288+
type B = List[(Int, (Int, Int))]
289+
type C = List[(Int, String)]
290+
```
291+
292+
the corresponding covering sets are:
293+
294+
- $\mathit{cs}(A)$: List, Tuple2, Int
295+
- $\mathit{cs}(B)$: List, Tuple2, Int
296+
- $\mathit{cs}(C)$: List, Tuple2, Int, String
297+
214298
###### Example
215299
When typing `sort(xs)` for some list `xs` of type `List[List[List[Int]]]`,
216300
the sequence of types for

src/compiler/scala/tools/nsc/ast/parser/Parsers.scala

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2402,10 +2402,6 @@ self =>
24022402
in.offset,
24032403
(if (mods.isMutable) "`var'" else "`val'") +
24042404
" parameters may not be call-by-name", skipIt = false)
2405-
else if (implicitmod != 0)
2406-
syntaxError(
2407-
in.offset,
2408-
"implicit parameters may not be call-by-name", skipIt = false)
24092405
else bynamemod = Flags.BYNAMEPARAM
24102406
}
24112407
paramType()

src/compiler/scala/tools/nsc/typechecker/Contexts.scala

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ import scala.reflect.internal.Reporter
1717
*/
1818
trait Contexts { self: Analyzer =>
1919
import global._
20-
import definitions.{ JavaLangPackage, ScalaPackage, PredefModule, ScalaXmlTopScope, ScalaXmlPackage }
20+
import definitions.{ AnyRefClass, JavaLangPackage, ScalaPackage, PredefModule, ScalaXmlTopScope, ScalaXmlPackage }
2121
import ContextMode._
22+
import scala.reflect.internal.Flags._
23+
2224

2325
protected def onTreeCheckerError(pos: Position, msg: String): Unit = ()
2426

@@ -244,6 +246,117 @@ trait Contexts { self: Analyzer =>
244246
openImplicits.nonEmpty && openImplicits.exists(x => !x.isView)
245247
}
246248

249+
private type ImplicitDict = List[(Type, (Symbol, Tree))]
250+
private var implicitDictionary: ImplicitDict = null
251+
252+
def implicitRootContext: Context = {
253+
if(implicitDictionary != null) this
254+
else if(outerIsNoContext || outer.openImplicits.isEmpty) {
255+
implicitDictionary = Nil
256+
this
257+
} else outer.implicitRootContext
258+
}
259+
260+
private def linkImpl(tpe: Type): Tree = {
261+
val sym =
262+
implicitDictionary.find(_._1 =:= tpe) match {
263+
case Some((_, (sym, _))) => sym
264+
case None =>
265+
val vname = unit.freshTermName("rec$")
266+
val vsym = owner.newValue(vname, newFlags = FINAL | SYNTHETIC) setInfo tpe
267+
implicitDictionary +:= (tpe -> (vsym, EmptyTree))
268+
vsym
269+
}
270+
gen.mkAttributedRef(sym) setType tpe
271+
}
272+
273+
def linkByNameImplicit(tpe: Type): Tree = implicitRootContext.linkImpl(tpe)
274+
275+
private def refImpl(tpe: Type): Tree =
276+
implicitDictionary.find(_._1 =:= tpe) match {
277+
case Some((_, (sym, _))) =>
278+
gen.mkAttributedRef(sym) setType tpe
279+
case None =>
280+
EmptyTree
281+
}
282+
283+
def refByNameImplicit(tpe: Type): Tree = implicitRootContext.refImpl(tpe)
284+
285+
private def defineImpl(tpe: Type, result: SearchResult): SearchResult = {
286+
@tailrec
287+
def patch(d: ImplicitDict, acc: ImplicitDict): (ImplicitDict, SearchResult) = d match {
288+
case Nil => (implicitDictionary, result)
289+
case (tp, (sym, EmptyTree)) :: tl if tp =:= tpe =>
290+
val ref = gen.mkAttributedRef(sym) setType tpe
291+
val res = new SearchResult(ref, result.subst, result.undetparams)
292+
(acc reverse_::: ((tpe, (sym, result.tree)) :: tl), res)
293+
case hd :: tl =>
294+
patch(tl, hd :: acc)
295+
}
296+
297+
val (d, res) = patch(implicitDictionary, Nil)
298+
implicitDictionary = d
299+
res
300+
}
301+
302+
def defineByNameImplicit(tpe: Type, result: SearchResult): SearchResult = implicitRootContext.defineImpl(tpe, result)
303+
304+
def emitImplicitDictionary(result: SearchResult): SearchResult =
305+
if(implicitDictionary == null || implicitDictionary.isEmpty || result.tree == EmptyTree) result
306+
else {
307+
val typer = newTyper(this)
308+
309+
@tailrec
310+
def prune(trees: List[Tree], pending: List[(Symbol, Tree)], acc: List[(Symbol, Tree)]): List[(Symbol, Tree)] = pending match {
311+
case Nil => acc
312+
case ps =>
313+
val (in, out) = ps.partition { case (vsym, rhs) => trees.exists(_.exists(_.symbol == vsym)) }
314+
if(in.isEmpty) acc
315+
else prune(in.map(_._2) ++ trees, out, in ++ acc)
316+
}
317+
318+
val pruned = prune(List(result.tree), implicitDictionary.map(_._2), List.empty[(Symbol, Tree)])
319+
if(pruned.isEmpty) result
320+
else {
321+
val (vsyms, vdefs) = pruned.map { case (vsym, rhs) =>
322+
(vsym, ValDef(Modifiers(FINAL | SYNTHETIC), vsym.name.toTermName, TypeTree(rhs.tpe), rhs))
323+
}.unzip
324+
325+
val ctor = DefDef(NoMods, nme.CONSTRUCTOR, Nil, ListOfNil, TypeTree(), Block(List(pendingSuperCall), Literal(Constant(()))))
326+
val mname = unit.freshTermName("LazyDefns$")
327+
val mdef =
328+
ModuleDef(Modifiers(SYNTHETIC), mname,
329+
Template(List(Ident(AnyRefClass)), noSelfType, ctor :: vdefs.toList)
330+
)
331+
332+
typer.namer.enterSym(mdef)
333+
334+
val mdef0 = typer.typed(mdef)
335+
val msym0 = mdef0.symbol
336+
val vsyms0 = vsyms.map { vsym =>
337+
mdef0.symbol.moduleClass.asClass.toType.decl(vsym.name)
338+
}
339+
340+
val vsymMap = (vsyms zip vsyms0).toMap
341+
342+
object patchRefs extends Transformer {
343+
override def transform(tree: Tree): Tree = {
344+
tree match {
345+
case i: Ident if vsymMap.contains(i.symbol) =>
346+
gen.mkAttributedSelect(gen.mkAttributedRef(msym0), vsymMap(i.symbol)) setType i.tpe
347+
case _ =>
348+
super.transform(tree)
349+
}
350+
}
351+
}
352+
353+
val tree0 = patchRefs.transform(result.tree)
354+
val tree1 = Block(mdef0, tree0).substituteSymbols(vsyms.toList, vsyms0.toList) setType tree.tpe
355+
356+
new SearchResult(tree1, result.subst, result.undetparams)
357+
}
358+
}
359+
247360
var prefix: Type = NoPrefix
248361

249362
def inSuperInit_=(value: Boolean) = this(SuperInit) = value

0 commit comments

Comments
 (0)