-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Allowing local variables to be initialized with non-hot values in initialization checker #12867
Conversation
@liufengyun @olhotak I'm looking forward to your comments |
} | ||
} | ||
} | ||
} |
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.
Thanks @EnzeXing . If we do it in the place where we give semantics to local variables, then we can reduce the total changes to just 2 or 3 lines.
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 would be case ValDef
in def cases
.
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.
Sorry, I was not clear, it's in the use site of variables, i.e. case TermRef(NoPrefix, _) =>
, not the definition site.
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.
You mean move the updating env part to the use site of variables?
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.
We don't need to update env
. The idea is to evaluate its initializer when the variable is accessed -- and it's automatically cached.
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.
@liufengyun Sorry but I'm still a bit confused. My idea is evaluate the local variables when they are initialized and stored it, so that we can just look up when accessing it. How can we evaluate its initializer when it's accessed?
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.
@liufengyun Sorry but I'm still a bit confused. My idea is evaluate the local variables when they are initialized and stored it, so that we can just look up when accessing it. How can we evaluate its initializer when it's accessed?
When the variable is accessed, rather than look up its value in env, we evaluate the rhs of the definition (ValDef) of the variable right there, when it's accessed.
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.
Suppose we have val v = new A in some local method, and the method accesses and returns v in the end. When we access v in the end, all we know about v is that it's a TermRef, but how can we know its rhs definition at this point? I thought we analyze val v = new A first and then accessing v at last, but how do we go the other way?
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.
From the TermRef
, we should be able to get its Symbol
, and from the Symbol
, call defTree
to find the ValDef
where that variable was defined.
We need to make sure that we're doing this only for TermRef
s that represent local variables. In particular, they should have NoPrefix
, but there are probably additional conditions to check to ensure that it's a local variable.
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.
From the
TermRef
, we should be able to get itsSymbol
, and from theSymbol
, calldefTree
to find theValDef
where that variable was defined.We need to make sure that we're doing this only for
TermRef
s that represent local variables. In particular, they should haveNoPrefix
, but there are probably additional conditions to check to ensure that it's a local variable.
Yes, that's exactly what I have in mind. Note that in case ValDef
, the rhs is already evaluated and thus cached (need to add cache = true
). The eval
operation is actually a retrieval from the cache.
We need to set up the correct thisV
for evaluating rhs
, here is an example for local methods:
Two corner cases to think about:
- invent an example where
thisV = Cold
. Just report a warning in this case. - invent an example where
env != Nil
.
There is a bug in handling local methods directly inside constructors called from local classes inside constructors: its env
is not set up correctly. We can discuss a fix in next meeting.
tests/init/neg/local-warm4.scala
Outdated
|
||
|
||
|
||
|
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.
Minor: useless empty lines.
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 would suggest inventing more tests that demonstrate the capture of local non-hot definitions in functions and local classes.
@@ -405,6 +406,7 @@ class Semantic { | |||
given Trace = trace1 | |||
val cls = target.owner.enclosingClass.asClass | |||
val ddef = target.defTree.asInstanceOf[DefDef] | |||
// try early promotion here; if returns error, returns cold |
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.
Irrelevant change?
} | ||
} | ||
} | ||
} |
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 would be case ValDef
in def cases
.
|
||
case If(cond, thenp, elsep) => | ||
val ress = eval(cond :: thenp :: elsep :: Nil, thisV, klass) | ||
val (ress, env2) = eval(cond :: thenp :: elsep :: Nil, thisV, klass) |
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 places where env
is unused, use _
instead (val (ress, _) = ???
).
val res = eval(vdef.rhs, addr, klass) | ||
if res.value.promote("Try promote", source).isEmpty then Result(Hot, Errors.empty) else res | ||
} | ||
case _ => ??? |
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.
How do we deal with other possible cases for thisV, like Fun or RefSet? Are they possible in this case?
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.
The situation is similar to what we do in Value.call
and Value.select
. There is some code duplication. To avoid that, we actually handle local calls inside Value.call
, which is more like a hack and is not ideal.
For the moment, we can create a helper method like def accessLocal(sym: Symbol, thisV: Value): Contextual[Result]
near the place where we define Value.call
.
@@ -855,7 +857,8 @@ class Semantic { | |||
case vdef : ValDef => | |||
// local val definition | |||
// TODO: support explicit @cold annotation for local definitions | |||
eval(vdef.rhs, thisV, klass).ensureHot("Local definitions may only hold initialized values", vdef) | |||
eval(vdef.rhs, thisV, klass, true) |
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've added cacheResult = true here to cache the result for rhs, but I've found that in several test cases, we evaluate rhs again when accessing local variable after. Is this the right way to cache the result?
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.
Use cache = true
instead of true
for better readability.
@liufengyun @olhotak I have re-implemented the code based on your thoughts, but I found that there is still a failed test: tests/pos/i5766.scala, and I think the reason is we somehow didn't cache the result successfully when evaluating the rhs of local val definition. I'm looking forward to your thoughts about how to fix that. Thanks! |
Do you know which local val in i5766 is causing problems? The test has no inline implicit def baz(implicit loop: => Foo): Baz = new Baz { def next = loop } |
The error is:
The expanded code is Test3.summon(
{
final class $_lazy_implicit_$8() extends Object(), Serializable {
val $_lazy_implicit_$7: Test3.Foo =
Test3.foo(
{
val loop$proxy1: Test3.Baz =
{
final class $anon() extends Object(), Test3.Baz {
def next: Test3.Foo =
$_lazy_implicit_$8.this.$_lazy_implicit_$7
}
new Object with Test3.Baz {...}():Test3.Baz
}:Test3.Baz
{
final class $anon() extends Object(), Test3.Bar {
def next: Test3.Baz = loop$proxy1
}
new Object with Test3.Bar {...}():Test3.Bar
}:Test3.Bar
}
)
}
val $_lazy_implicit_$9: $_lazy_implicit_$8 = new $_lazy_implicit_$8()
$_lazy_implicit_$9.$_lazy_implicit_$7
}
) |
val loop$proxy1 is the local val that seems to went wrong |
@EnzeXing can you add tracing code to |
I do find that when we access this local val, the defTree is different than the expr tree that we cached in when we evaluate the initializer |
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.
def runsAfter: Set[String] = Set.empty | ||
|
||
/** Whether this phase require synchronization of symbol.defTree? |
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.
/** Whether this phase require synchronization of symbol.defTree? | |
/** Whether this phase requires synchronization of symbol.defTree when enabled? |
@@ -692,7 +693,7 @@ class Semantic { | |||
} | |||
|
|||
/** Evaluate a list of expressions */ | |||
def eval(exprs: List[Tree], thisV: Addr, klass: ClassSymbol): Contextual[List[Result]] = | |||
def eval(exprs: List[Tree], thisV: Addr, klass: ClassSymbol): Contextual[List[Result]] = |
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.
def eval(exprs: List[Tree], thisV: Addr, klass: ClassSymbol): Contextual[List[Result]] = | |
def eval(exprs: List[Tree], thisV: Addr, klass: ClassSymbol): Contextual[List[Result]] = |
@@ -900,8 +901,27 @@ class Semantic { | |||
// It's always safe to approximate them with `Cold`. | |||
Result(Cold, Nil) | |||
else |
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.
We need to refactor the if/else
here, as in the current else
branch, we can still encounter sym.is(Flags.Param)
.
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 is not addressed?
Also, I'd suggest move the body of case tmref: TermRef if tmref.prefix == NoPrefix
to accessLocal
.
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 we have sym.is(Flags.Param)
but its owner is not a constructor? Then is this symbol an argument to a local method?
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, that's possible --- normal method arguments can also reach here.
} | ||
case addr: Addr => { | ||
val res = eval(vdef.rhs, addr, klass) | ||
if res.value.promote("Try promote", source).isEmpty then Result(Hot, Errors.empty) else res |
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 don't see why we need this. eval(vdef.rhs, addr, klass)
seems to be enough.
case Cold => { | ||
val error = AccessCold(sym, source, trace.toVector) | ||
Result(Hot, error :: 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.
Minor: don't use braces for the body of a case.
Here, simply return Result(Cold, Nil)
is enough.
val res = eval(vdef.rhs, addr, klass) | ||
if res.value.promote("Try promote", source).isEmpty then Result(Hot, Errors.empty) else res | ||
} | ||
case _ => ??? |
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.
The situation is similar to what we do in Value.call
and Value.select
. There is some code duplication. To avoid that, we actually handle local calls inside Value.call
, which is more like a hack and is not ideal.
For the moment, we can create a helper method like def accessLocal(sym: Symbol, thisV: Value): Contextual[Result]
near the place where we define Value.call
.
1a11195
to
90f2277
Compare
@liufengyun Could you please review it again? I have refactored the accessing local variable part and updating tests. I will fix the bug of Env soon. |
@@ -495,6 +496,29 @@ class Semantic { | |||
Result(value2, errors) | |||
} | |||
} | |||
|
|||
def accessLocal(sym: Symbol, default: Result, klass: ClassSymbol, source: Tree): Contextual[Result] = |
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.
It's better to remove default
here, as it simplifies the protocol.
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.
Do you mean to just hardcode it as Hot?
Oh, I see: you mean to move the whole case tmref ...
from cases
to accessLocal
. That case is what defines default
to be Hot
.
|
||
case addr: Addr => | ||
val res = eval(vdef.rhs, addr, klass) | ||
if res.value.promote("Try promote", source).isEmpty then Result(Hot, Errors.empty) else res |
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.
Why we call promote
here?
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.
It's tests/pos/i8892.scala. If we don't promote here, dotty will crash on this test.
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.
Thanks for the info. The crash seems to imply there is some bug elsewhere, it's difficult to justify the promotion here.
Could you try to debug the test and see why it crashes?
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'll take a closer look at 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.
@liufengyun It seems that we are selecting a non-exist field 1_<outer>
from Zero
in val j31
. I think we are meant to call Zero.++
, but "++" is transparent inlined def, so it somehow got translated to 1_<outer>
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.
@EnzeXing You can try the following patch: https://gist.github.com/liufengyun/de6198e4a8a28e090c41e62c8af65ed6
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.
With this patch, this test is resolved, but now we would fail on tests/init/special/i12128/Test_3.scala since dotty can't identify the object MacroCompat
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.
It works without any problem for me (including the test above). Could you please debug?
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.
nvm, this has been resolved.
@@ -855,7 +857,8 @@ class Semantic { | |||
case vdef : ValDef => | |||
// local val definition | |||
// TODO: support explicit @cold annotation for local definitions | |||
eval(vdef.rhs, thisV, klass).ensureHot("Local definitions may only hold initialized values", vdef) | |||
eval(vdef.rhs, thisV, klass, true) |
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.
Use cache = true
instead of true
for better readability.
@@ -855,7 +879,8 @@ class Semantic { | |||
case vdef : ValDef => | |||
// local val definition | |||
// TODO: support explicit @cold annotation for local definitions | |||
eval(vdef.rhs, thisV, klass).ensureHot("Local definitions may only hold initialized values", vdef) | |||
eval(vdef.rhs, thisV, klass, true) | |||
// .ensureHot("Local definitions may only hold initialized values", vdef) |
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.
Remove this line.
@@ -900,8 +901,27 @@ class Semantic { | |||
// It's always safe to approximate them with `Cold`. | |||
Result(Cold, Nil) | |||
else |
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 is not addressed?
Also, I'd suggest move the body of case tmref: TermRef if tmref.prefix == NoPrefix
to accessLocal
.
@liufengyun I have encountered a failed test on Github: secondary-ctor2.scala, but it didn't fail on my local computer. Do you know what may cause it to fail? |
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.
@liufengyun I have encountered a failed test on Github: secondary-ctor2.scala, but it didn't fail on my local computer. Do you know what may cause it to fail?
I think we need to remove the error annotation at line 9: that line should not report an error.
@@ -900,8 +901,27 @@ class Semantic { | |||
// It's always safe to approximate them with `Cold`. | |||
Result(Cold, Nil) | |||
else |
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, that's possible --- normal method arguments can also reach here.
tests/init/neg/local-warm4.check
Outdated
-- Error: tests/init/neg/local-warm4.scala:18:20 ----------------------------------------------------------------------- | ||
18 | a = newA // error | ||
| ^^^^ | ||
|Promote the value under initialization to fully-initialized. May only assign fully initialized value. Calling trace: |
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.
Let's change the first part of the message to: Cannot prove that the value is fully initialized. May ...
val res = eval(vdef.rhs, addr, klass) | ||
if res.value.promote("Try promote", source).isEmpty then Result(Hot, Errors.empty) else res | ||
|
||
case _ => ??? |
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.
Let's print some debug information here: report.error(...)
. We can define a helper method:
def reportImpossibleError(message: String)(using Trace, Context): Unit = ...
For end-users, this is better than a compiler crash and useful debug information can be reported.
This can be done in another PR.
6a4d226
to
25069bb
Compare
// It's always safe to approximate them with `Cold`. | ||
Result(Cold, Nil) | ||
else if sym.is(Flags.Param) then | ||
default() |
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.
For parameters for normal method calls, we just return Result(Hot, Nil)
since we expect that those arguments have been promoted in Call
, right?
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, that's correct.
ea89248
to
bfb10bb
Compare
@EnzeXing There are few minor issues:
|
Fixing inherit-non-hot.check
After scala#12711, it is no longer the case that `Warm < ThisRef`. The reason is that the class parameters for `ThisRef` are always hot, while it is not the case anymore after scala#12711. Actually even before scala#12711, it's not the case because the outers of warm objects may not be hot. The theory does not have inner classes, it thus does not suffer from the problem.
76eaaff
to
e901a36
Compare
@liufengyun Could you review it again? I've fixed those issue. |
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, thank you @EnzeXing 🎉
This PR will allow local variables to be initialized with non-hot values in the initialization checker, instead of ensuring local variables to be hot.