-
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
Support leak non-hot values to constructors #12711
Conversation
@liufengyun Could you explain more about the bug you mentioned in the email? |
I just add a test case which makes it more clear: abstract class A {
bar()
private val a = 5
def foo() = a
def bar(): Unit
}
class M extends A {
def bar() = promote(this) // error
def promote(m: M) = m.foo()
} The checker in the master does not catch the problem, because the set of fields it gets doesn't include those not accessible from the class The commit d12ebde fixes the problem: we just cannot use |
@liufengyun Remove line 515 ( |
Good catch, thank you @michelou ! Now it's fixed. |
* Despite that we have environment for evaluating expressions in secondary | ||
* constructors, we don't need to put environment as the cache key. The | ||
* reason is that constructor parameters are determined by the value of | ||
* `this` --- it suffices to make the value of `this` as part of the cache |
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.
These constructor parameters are the args
field in Warm
, 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.
Warm
only contain arguments to the constructor of the concrete class, but not arguments to its super-constructors.
object Promoted { | ||
class PromotionInfo { | ||
var isCurrentObjectPromoted: Boolean = false | ||
val values = mutable.Set.empty[Value] |
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.
Is isCurrentObjectPromoted
for ThisRef
and values
for Warm
values?
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. However, values
may also contain functions values.
@@ -251,14 +305,24 @@ class Semantic { | |||
|
|||
case (RefSet(refs1), RefSet(refs2)) => RefSet(refs1 ++ refs2) | |||
|
|||
def widen: Value = |
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.
Consider a more specific name such as widenArg
. Also consider a comment to say that this conservatively approximates the Value
with Hot
or 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.
Good suggestion.
|
||
case addr: Addr => | ||
val isLocal = meth.owner.isClass |
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.
Isn't a local method one whose owner is a method, and thus is not a class?
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.
Good catch, I'll add a test case to avoid such mistakes.
if target.isPrimaryConstructor then | ||
given Env = env2 |
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.
Is there a specific reason to prefer this pattern of given
over the use()
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.
I try to use given T = e
whenever possible. However, in same places we cannot easily use given
, because of scoping interference. I described the problem in more detail here.
|
||
def default() = Result(Hot, Nil) | ||
|
||
if sym.is(Flags.Param) then |
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.
&& would be clearer here than two ifs.
|
||
case tmref: TermRef => | ||
cases(tmref.prefix, thisV, klass, source).select(tmref.symbol, source) | ||
|
||
case tp @ ThisType(tref) => | ||
if tref.symbol.is(Flags.Package) then Result(Hot, Errors.empty) | ||
val cls = tref.classSymbol.asClass |
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 understand what this case corresponds to.
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.
cls.isStaticOwner
means it's a global object, klass.isContainedIn(cls)
means we have O.this
outside the body of the object O
. We don't check global objects, so simply return Hot
here.
} | ||
|
||
def superCall(tref: TypeRef, ctor: Symbol, source: Tree): Unit = | ||
type Handler = (() => Unit) => Unit |
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.
A comment would be helpful here to explain what Handler
s are used for.
case tree @ Block(stats, NewExpr(tref, New(tpt), ctor, argss)) => // can happen | ||
eval(stats, thisV, klass).foreach { res => errorBuffer ++= res.errors } | ||
errorBuffer ++= evalArgs(argss.flatten, thisV, klass) | ||
superCall(tref, ctor, tree) | ||
val (erros, args) = evalArgs(argss.flatten, 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.
type errors?
|
||
def typeRefOf(tp: Type)(using Context): TypeRef = tp.dealias.typeConstructor match { | ||
case tref: TypeRef => tref | ||
case hklambda: HKTypeLambda => typeRefOf(hklambda.resType) | ||
} | ||
|
||
opaque type Arg = Tree | ByNameArg | ||
type Arg = Tree | ByNameArg |
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 is it no longer opaque? Why was it opaque before?
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 a restriction in Scala 3: we cannot define an inline
method and an opaque type in the same object.
@EnzeXing Could you please have a look as well? |
val res = value.call(ctor, superType = NoType, source) | ||
Result(value, res.errors) | ||
val value = Warm(klass, outer, ctor, args.map(_.value).widenArgs).ensureExists | ||
val res = value.call(ctor, args, superType = NoType, source) |
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 problematic: still possible to crash.
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.
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.
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 errors are fewer in 98a0977 because the analysis just skipped the check.
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 kind of prefer 7f91775, as the code is more regular --- we only have one special case in variable resolution. As the examples are all invented, I don't expect the two have an observable difference in expressiveness nor usability.
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'm OK with 7f91775 as well. Simpler implementation is an advantage.
The previous code does not work, because private fields are not included.
With the flag <static>, the enclosing class will be skipped. See the documentation `SymDenotation.enclosingClass`
For traits, its outers will be proxy methods of the class that extends the trait. As the prefix is stable and is a valid value before any super constructor calls. Therefore, we may think the outers for traits are immediately set following the class parameters. Also, when trying promotion of warm values, we never try warm values whose fields are not fully filled -- which corresponds to promote ThisRef with non-initialized fields, and errors will be reported when the class is checked separately.
This aligns with the design of restricting method arguments to be hot. We only allow non-hot to constructors.
This is a simple fix which should have no impact on expressiveness and usability, as local classes inside secondary constructors are extremely rare.
@olhotak Do you have other comments or we are ready to merge it? |
You added some comments during our meeting on Tuesday. One was fixed by 7f91775 but are the other comments that you added addressed? I can't find them anymore. Aside from that, I think it's ready to merge. |
Sorry, I forgot to mention it: the handler issue is addressed in d9892f9, and the local classes in secondary constructors issue is addressed in ae566b4 (I squashed the two trials into one commit). |
OK, great. LGTM then. (I forgot about the handler issue.) |
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.
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.
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.
The following code will be OK:
The following will get an error: