Skip to content

Commit ffec78f

Browse files
committed
Proper laziness for by-name args of right-associative operators
This fixes scala/bug#1980 by changing the desugaring of right-associative operator syntax in the spec such that by-name operands now get the same desugaring as left-associative operators (except for the reversed operands, of course). Only by-value operands are pulled out into intermediate vals to preserve their left-to-right evaluation order. The implementation is as follows: - `Parsers` still performs the `val` desugaring for all calls, except that the generated synthetic names use the new `RIGHT_ASSOC_OP_PREFIX` to identify them later. - Everything else happens in `Typers`: After typechecking a ValDef resulting from desugaring of a right-associative operator its Symbol and RHS are stored in a Map (without knowing at that point if they are by-name or by-value). - After typechecking a method application with an Ident for one of these Symbols, check if the parameter is by-name in which case it is replaced with the RHS and the Symbol added to a Set. - After typechecking a Block, check for a leading ValDef with the Symbol that was inlined and remove the ValDef. Fixes scala/bug#1980
1 parent 8e7c5c7 commit ffec78f

File tree

6 files changed

+137
-7
lines changed

6 files changed

+137
-7
lines changed

spec/06-expressions.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -698,9 +698,9 @@ This expression is then interpreted as $e.\mathit{op}(e_1,\ldots,e_n)$.
698698

699699
A left-associative binary
700700
operation $e_1;\mathit{op};e_2$ is interpreted as $e_1.\mathit{op}(e_2)$. If $\mathit{op}$ is
701-
right-associative, the same operation is interpreted as
702-
`{ val $x$=$e_1$; $e_2$.$\mathit{op}$($x\,$) }`, where $x$ is a fresh
703-
name.
701+
right-associative and its parameter is passed by name, the same operation is interpreted as
702+
$e_2.\mathit{op}(e_1)$. If $\mathit{op}$ is right-associative and its parameter is passed by value,
703+
it is interpreted as `{ val $x$=$e_1$; $e_2$.$\mathit{op}$($x\,$) }`, where $x$ is a fresh name.
704704

705705
### Assignment Operators
706706

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -835,7 +835,7 @@ self =>
835835
if (treeInfo.isLeftAssoc(op)) {
836836
Apply(mkSelection(left), arguments)
837837
} else {
838-
val x = freshTermName()
838+
val x = freshTermName(nme.RIGHT_ASSOC_OP_PREFIX)
839839
Block(
840840
List(ValDef(Modifiers(symtab.Flags.SYNTHETIC | symtab.Flags.ARTIFACT), x, TypeTree(), stripParens(left))),
841841
Apply(mkSelection(right), List(Ident(x))))

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

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
4444

4545
final val shortenImports = false
4646

47+
// All typechecked RHS of ValDefs for right-associative operator desugaring
48+
private val rightAssocValDefs = new mutable.AnyRefMap[Symbol, Tree]
49+
// Symbols of ValDefs for right-associative operator desugaring which are passed by name and have been inlined
50+
private val inlinedRightAssocValDefs = new mutable.HashSet[Symbol]
51+
4752
// allows override of the behavior of the resetTyper method w.r.t comments
4853
def resetDocComments() = {
4954
clearDocComments()
@@ -54,6 +59,8 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
5459
resetContexts()
5560
resetImplicits()
5661
resetDocComments()
62+
rightAssocValDefs.clear()
63+
inlinedRightAssocValDefs.clear()
5764
}
5865

5966
sealed abstract class SilentResult[+T] {
@@ -2063,7 +2070,10 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
20632070
} else tpt1.tpe
20642071
transformedOrTyped(vdef.rhs, EXPRmode | BYVALmode, tpt2)
20652072
}
2066-
treeCopy.ValDef(vdef, typedMods, sym.name, tpt1, checkDead(rhs1)) setType NoType
2073+
val vdef1 = treeCopy.ValDef(vdef, typedMods, sym.name, tpt1, checkDead(rhs1)) setType NoType
2074+
if (sym.isSynthetic && sym.name.startsWith(nme.RIGHT_ASSOC_OP_PREFIX))
2075+
rightAssocValDefs += ((sym, vdef1.rhs))
2076+
vdef1
20672077
}
20682078

20692079
/** Enter all aliases of local parameter accessors.
@@ -2474,7 +2484,13 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
24742484
if (result0.nonEmpty) checkPure(result0, supple = true)
24752485
}
24762486

2477-
treeCopy.Block(block, statsTyped, expr1)
2487+
// Remove ValDef for right-associative by-value operator desugaring which has been inlined into expr1
2488+
val statsTyped2 = statsTyped match {
2489+
case (vd: ValDef) :: Nil if inlinedRightAssocValDefs remove vd.symbol => Nil
2490+
case _ => statsTyped
2491+
}
2492+
2493+
treeCopy.Block(block, statsTyped2, expr1)
24782494
.setType(if (treeInfo.isExprSafeToInline(block)) expr1.tpe else expr1.tpe.deconst)
24792495
} finally {
24802496
// enable escaping privates checking from the outside and recycle
@@ -3591,6 +3607,15 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
35913607
case _ => tp
35923608
}
35933609

3610+
// Inline RHS of ValDef for right-associative by-value operator desugaring
3611+
val args2 = (args1, mt.params) match {
3612+
case ((ident: Ident) :: Nil, param :: Nil) if param.isByNameParam && rightAssocValDefs.contains(ident.symbol) =>
3613+
inlinedRightAssocValDefs += ident.symbol
3614+
val rhs = rightAssocValDefs.remove(ident.symbol).get
3615+
rhs.changeOwner(ident.symbol -> context.owner) :: Nil
3616+
case _ => args1
3617+
}
3618+
35943619
/*
35953620
* This is translating uses of List() into Nil. This is less
35963621
* than ideal from a consistency standpoint, but it shouldn't be
@@ -3602,7 +3627,7 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper
36023627
if (args.isEmpty && canTranslateEmptyListToNil && fun.symbol.isInitialized && ListModule.hasCompleteInfo && (fun.symbol == List_apply))
36033628
atPos(tree.pos)(gen.mkNil setType restpe)
36043629
else
3605-
constfold(treeCopy.Apply(tree, fun, args1) setType ifPatternSkipFormals(restpe))
3630+
constfold(treeCopy.Apply(tree, fun, args2) setType ifPatternSkipFormals(restpe))
36063631
}
36073632
checkDead.updateExpr(fun) {
36083633
handleMonomorphicCall

src/reflect/scala/reflect/internal/StdNames.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ trait StdNames {
333333
val FRESH_SUFFIX = "macro$" // uses a keyword to avoid collisions with mangled names
334334
val QUAL_PREFIX = "qual$"
335335
val NAMEDARG_PREFIX = "x$"
336+
val RIGHT_ASSOC_OP_PREFIX = "rassoc$"
336337

337338
// Compiler internal names
338339
val ANYname: NameType = "<anyname>"

test/files/run/t1980.check

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
1. defining
2+
foo 1
3+
foo 2
4+
foo 3
5+
1. forcing
6+
bar 1
7+
bar 2
8+
bar 3
9+
2. defining
10+
2. forcing
11+
hi
12+
1
13+
hi
14+
1
15+
3. defining
16+
3. forcing
17+
hi
18+
1
19+
4. defining
20+
4. forcing
21+
1
22+
1
23+
1
24+
5. defining
25+
Int 1
26+
Int 3
27+
5. forcing
28+
String 4
29+
String 2

test/files/run/t1980.scala

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
class LazyList[+A](expr: => LazyList.Evaluated[A]) {
2+
def #:: [B >: A](elem: => B): LazyList[B] = new LazyList(Some((elem, this)))
3+
def ##:: [B >: A](elem: B): LazyList[B] = new LazyList(Some((elem, this)))
4+
def force: Unit = expr.foreach(_._2.force)
5+
}
6+
7+
object LazyList {
8+
type Evaluated[+A] = Option[(A, LazyList[A])]
9+
object Empty extends LazyList[Nothing](None)
10+
def empty[A]: LazyList[A] = Empty
11+
}
12+
13+
object Test extends App {
14+
def foo(i: Int) = { println("foo "+i); i }
15+
def bar(i: Int) = { println("bar "+i); i }
16+
println("1. defining")
17+
val xs1 = foo(1) ##:: foo(2) ##:: foo(3) ##:: LazyList.empty
18+
val xs2 = bar(1) #:: bar(2) #:: bar(3) #:: LazyList.empty
19+
println("1. forcing")
20+
xs1.force
21+
xs2.force
22+
23+
{
24+
println("2. defining")
25+
class C { def f_:(x: => Int)(implicit y: Int): () => Int = (() => x) }
26+
val c = new C
27+
implicit val i = 1
28+
def k = { println("hi"); 1 }
29+
val x1 = c.f_:(k)
30+
val x2 = k f_: c
31+
println("2. forcing")
32+
println(x1())
33+
println(x2())
34+
}
35+
36+
{
37+
println("3. defining")
38+
class C { def f_:[T](x: => T): () => T = (() => x) }
39+
val c = new C
40+
def k = { println("hi"); 1 }
41+
val x1 = k f_:[Any] c
42+
println("3. forcing")
43+
println(x1())
44+
}
45+
46+
// Ensure the owner chain is changed correctly when inlining
47+
{
48+
println("4. defining")
49+
class C { def f_:(x: => Int): () => Int = (() => x) }
50+
val c = new C
51+
val x1 = c.f_:({ val xxx = 1; xxx })
52+
val x2 = { val yyy = 1; yyy } f_: c
53+
val x3 = { val yyy = 1; yyy } f_: c
54+
println("4. forcing")
55+
println(x1())
56+
println(x2())
57+
println(x3())
58+
}
59+
60+
// Overloaded operator with by-name and by-value variants
61+
{
62+
println("5. defining")
63+
class C {
64+
val saved = new collection.mutable.ArrayBuffer[() => String]
65+
def force: Unit = saved.foreach(_.apply())
66+
def :: (x: Int): C = this
67+
def :: (x: => String): C = { saved += (() => x); this }
68+
}
69+
def genI(i: Int): Int = { println("Int "+i); i }
70+
def genS(s: String): String = { println("String "+s); s }
71+
val c = genI(1) :: genS("2") :: genI(3) :: genS("4") :: (new C)
72+
println("5. forcing")
73+
c.force
74+
}
75+
}

0 commit comments

Comments
 (0)