-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Proper laziness for by-name args of right-associative operators #5969
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Additional test case that could be added (works correctly):
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea, it's nice to see this being fixed! Could we do the transformation in typers? Why is there the intermediate step of marking the symbol lazy?
@@ -1703,7 +1707,7 @@ abstract class RefChecks extends Transform { | |||
assert(sym != NoSymbol, "transformCaseApply: name = " + name.debugString + " tree = " + tree + " / " + tree.getClass) //debug | |||
enterReference(tree.pos, sym) | |||
} | |||
tree | |||
eliminatedRightAssocValDefs.getOrElse(tree.symbol, tree) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general we cannot just move trees around like this, because the owner chain of symbols defined in the tree might mess up. This is maybe what's causing the following crash:
scala> class C { def f_:(x: => Int) = 0 }
defined class C
scala> val c = new C
c: C = C@4dd94931
scala> { val x = 1; x } f_: c
java.util.NoSuchElementException: key not found: value x
at scala.collection.MapLike.default(MapLike.scala:230)
at scala.collection.MapLike.default$(MapLike.scala:229)
at scala.collection.AbstractMap.default(Map.scala:59)
at scala.collection.mutable.HashMap.apply(HashMap.scala:61)
at scala.tools.nsc.backend.jvm.BCodeSkelBuilder$PlainSkelBuilder$locals$.load(BCodeSkelBuilder.scala:391)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, I assumed it would be safe here because I'm only moving it within the same block. But maybe that's no longer true in all cases after uncurry?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait, I'm not moving it within the same block. I'm eliminating the block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I see. It's owned by the ValDef
that gets eliminated, so I have to change it anyway, even within the same block.
@@ -1714,6 +1718,9 @@ abstract class RefChecks extends Transform { | |||
// probably not, until we allow parameterised extractors | |||
tree | |||
|
|||
case Block((vd: ValDef) :: Nil, expr) if vd.symbol.isLazy && vd.name.toString.startsWith(nme.RIGHT_ASSOC_OP_PREFIX) => | |||
eliminatedRightAssocValDefs += ((vd.symbol, vd.rhs)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing call to transform
on vd.rhs
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think there should be one
Yet another test case (that works correctly):
|
There are two steps because we first have to typecheck the application before we know that the definition can be removed. AFAICT typers only does a single full transformation of the tree. Could be done in |
That's what I was thinking, it might work that way. Also, instead of "abusing" the |
Looks like the |
case _ => statsTyped | ||
} | ||
|
||
treeCopy.Block(block, statsTyped2, expr1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This leaves a single-expression block in place, but I guess that's fine. We'd have to handle it outside typedBlock
, as this method returns a Block
.
scala> def foo = { 1 f_: c; 2 }
[[syntax trees at end of typer]] // <console>
def foo: Int = {
{
$line5.$read.$iw.$iw.c.f_:(1)
};
2
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typedBlock
could be changed to return a Tree
but I didn't want to add unnecessary complications. The empty blocks don't seem to cause any problem.
val args2 = (args1, mt.params) match { | ||
case ((ident: Ident) :: Nil, param :: Nil) if param.isByNameParam && rightAssocValDefs.contains(ident.symbol) => | ||
inlinedRightAssocValDefs += ident.symbol | ||
val rhs = rightAssocValDefs(ident.symbol) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer a symbol attachment over the two collections. Something like class RightAssocValDefAttachment(var rhsInlined)
, added in typedValDef
, the var could be set to true
here. Then you can use getAndRemoveAttachment
in typedBlock
.
We could even just add an empty marker attachment RightAssocValDefInlined
here, and skip the test in typedValDef
- what value does it add?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer a symbol attachment over the two collections.
Are you sure? I checked for other uses of attachments and they all seem to be for communication between phases. I didn't find any precedent for data of a single phase being stored in attachments.
skip the test in typedValDef - what value does it add?
It stores the RHS which is needed in doTypedApply. Is there a better way to get it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure?
To me it feels more like keeping state local, but it's fine either way in the end.
It stores the RHS which is needed in doTypedApply
Right, of course, I missed that.
test/files/run/t1980.scala
Outdated
implicit val i = 1 | ||
def k = { println("hi"); 1 } | ||
c.f_:(k) | ||
k f_: c |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe also test lazy evaluation here (and in all tests below), just to make sure.
This is very promising, but I think the desugaring needs to go to defs instead of lazy vals. If
then, logically
should be the same as
But it is only if it is expanded to
Or, the spec could simply demand that by-name arguments are not lifted out. That would be even clearer. |
But that's exactly what it does in this PR. |
Alternative pattern, suggested in peer-reviewing with Jason
That way we could live without the hash map / set altogether. |
You can't check if it's by-name before typing the RHS because the method could have mixed by-name and by-value overloads. You need to type the RHS before doing overload resolution to find the right method. |
Nice one! Could you add a test for that case? |
We should (separately) pursue an analogous change for the desugaring of default arguments: scala> def foo(a: => Any, b: => Any) = a
foo: (a: => Any, b: => Any)Any
scala> foo(b = toString, a = toString) //print
{
val x$1: () => String @scala.reflect.internal.annotations.uncheckedBounds = (() => $iw.this.toString());
val x$2: () => String @scala.reflect.internal.annotations.uncheckedBounds = (() => $iw.this.toString());
$line9.$read.$iw.$iw.foo(x$2.apply(), x$1.apply())
} // : Any |
// All typechecked RHS of ValDefs for right-associative operator desugaring | ||
val rightAssocValDefs = new mutable.AnyRefMap[Symbol, Tree] | ||
// Symbols of ValDefs for right-associative operator desugaring which are passed by name and have been inlined | ||
val inlinedRightAssocValDefs = new mutable.HashSet[Symbol] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private
?
@@ -2063,7 +2070,10 @@ trait Typers extends Adaptations with Tags with TypersTracking with PatternTyper | |||
} else tpt1.tpe | |||
transformedOrTyped(vdef.rhs, EXPRmode | BYVALmode, tpt2) | |||
} | |||
treeCopy.ValDef(vdef, typedMods, sym.name, tpt1, checkDead(rhs1)) setType NoType | |||
val vdef1 = treeCopy.ValDef(vdef, typedMods, sym.name, tpt1, checkDead(rhs1)) setType NoType | |||
if (sym.isSynthetic && sym.name.toString.startsWith(nme.RIGHT_ASSOC_OP_PREFIX)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should need the to toString
here.
treeCopy.Block(block, statsTyped, expr1) | ||
// Remove ValDef for right-associative by-value operator desugaring which has been inlined into expr1 | ||
val statsTyped2 = statsTyped match { | ||
case (vd: ValDef) :: Nil if inlinedRightAssocValDefs contains vd.symbol => Nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could eagerly clear the entry from the Map here if (inlinedRightAssocValDefs.remove(vd.symbol).isDefined) =>
val args2 = (args1, mt.params) match { | ||
case ((ident: Ident) :: Nil, param :: Nil) if param.isByNameParam && rightAssocValDefs.contains(ident.symbol) => | ||
inlinedRightAssocValDefs += ident.symbol | ||
val rhs = rightAssocValDefs(ident.symbol) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, I'd like to use Map#remove
here to clean up as we go.
Another approach here would be to modify the parser to emit a regular application, but mark it with a Pros:
Cons:
(I don't want to filibuster this PR with the alternative proposal, it can come as a follow up if we think it is worth pursuing.) We also should consider how easy/hard alternatives are to spec. Currently it quite prescriptive:
Perhaps we could abstract this to specify the evaluation without specifying the desugaring. The spec for named/default applications is also currently prescriptive but doesn't discuss the treatment of by-name params. |
Yes, I was also thinking about this option since I read your previous proposal yesterday. It would be nice to get the same level of type inference for right-associative operators that you get for other operators and method calls. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! I think you can squash all in one (and eliminate the no-op changes in RefChecks.scala)
I am still very positive on this, but believe it definitely needs a SIP. |
Can you prepare a quick SIP @szeiger and we discuss it in the next meeting? I'm scheduling one for this month. |
He did already scala/docs.scala-lang#805 |
Moving to M3, pending SIP |
@szeiger, could you update the PR description to reflect the latest implementation strategy? |
This fixes scala/bug#1980 as specified in SIP-34 (http://docs.scala-lang.org/sips/right-associative-by-name-operators.html) by changing the desugaring of right-associative operator syntax 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
Rebased and updated commit comment. Now that SIP-34 was accepted this should be ready to merge for M3. |
Implemented in Scala 2.13 in scala/scala#5969 Implemented in Scala 3 in scala/scala3#3841
Implemented in Scala 2.13 in scala/scala#5969 Implemented in Scala 3 in scala/scala3#3841
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 (revised) implementation is as follows:
Parsers
still performs theval
desugaring for all calls, exceptthat the generated synthetic names use the new
RIGHT_ASSOC_OP_PREFIX
to identify them later.
Everything else happens in
Typers
: After typechecking a ValDefresulting 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