-
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
A simpler implementation of init checker #12495
Conversation
@@ -57,7 +59,14 @@ class Checker extends MiniPhase { | |||
env = Env(ctx.withOwner(cls), cache) | |||
) | |||
|
|||
Checking.checkClassBody(tree) | |||
// Checking.checkClassBody(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.
Should we remove checkClassBody
or is it too early? Also cache
, summarization, etc.
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, we will remove the existing version in another PR.
val tpl = tree.rhs.asInstanceOf[Template] | ||
val thisRef = ThisRef(cls) | ||
val heap = Objekt(cls, fields = mutable.Map.empty) | ||
val res = eval(tpl, thisRef, cls)(using heap, ctx, Vector.empty) |
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 discussed that we could in theory call init
here instead of eval
. I there are tricky reasons to prefer eval
, fine, but if not, then init
would make the intent clearer.
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.
Here the reason is only for consistency and tries to follow the Scala specification on initialization literally.
Inside the interpreter, beyond the cosmetical reason, another reason to reuse the cache infrastructure in early promotion of warm objects.
tests/init/neg/early-promote.scala
Outdated
@@ -24,7 +24,7 @@ class A { // checking A | |||
def c = new C | |||
} | |||
val b = new B() | |||
List(b) // Direct promotion works here | |||
List(b) // error: no promotion for objects that contain inner classes |
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 comment to explain why that's not safe.
@@ -1,6 +1,6 @@ | |||
class DoubleList { | |||
class Node(var prev: Node, var next: Node, data: Int) | |||
object sentinel extends Node(sentinel, sentinel, 0) | |||
object sentinel extends Node(sentinel, sentinel, 0) // error // error |
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 this change related to this PR or to the global objects checker? Or point to inner-loop.scala
.
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.
According to concrete semantics, the code will run into loop.
The previous version ignores the effects in parent calls (assumimg they are hot, except super ctor calls).
The current version is more eager in check, thus report two warnings: Promote the value under initialization to fully-initialized
. It's not related to the global object checker (which is not merged yet).
|
||
/** Abstract values | ||
* | ||
* Value = Hot | Cold | Warm | ThisRef | Fun | RefSet |
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 would be nice to give the <= relation on the abstract domain 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.
Good idea, I'll update the documentation.
|
||
// parents | ||
def initParent(parent: Tree) = parent match { | ||
case tree @ Block(stats, NewExpr(tref, New(tpt), ctor, argss)) => |
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 fully understand these cases of parent. Perhaps comment like // class A extends B(arg)
case Some(parent) => initParent(parent) | ||
case None => | ||
val tref = typeRefOf(klass.typeRef.baseType(mixin).typeConstructor) | ||
val ctor = tref.classSymbol.primaryConstructor |
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 the primary constructor always the one with zero args?
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.
According to the language spec, if the mix trait requires arguments, then the class must provide arguments to it explicitly in the parent list. That means we will encounter it in the Some
branch.
When a trait A
extends a parameterized trait B
, it cannot provide term arguments to B
. That can only be done in a concrete class.
case t: Tree => t | ||
case ByNameArg(t) => t | ||
|
||
object Call { |
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 something like this extractor elsewhere in Dotty for general use? If not, should there be (should this be moved out of init where other code can use 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.
The logic here is specific to our analysis, thus it's better to keep it here. The handling of ByNameArg
is also special to our purpose. In the compiler, we also do the same thing.
// ignored as they are all hot | ||
|
||
// follow constructor | ||
if !cls.defTree.isEmpty 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.
Consider using hasSource
instead of !defTree.isEmpty
.
else sym.matchingMember(cls.appliedRef) | ||
|
||
def resolveSuper(cls: ClassSymbol, superType: Type, sym: Symbol)(using Context): Symbol = { | ||
import annotation.tailrec |
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 something like this elsewhere in Dotty? Should there be?
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 isn't anything similar, so I think it's better to keep it here -- also because they are two important helper methods in the logic of the analysis.
* │ │ │ │ | ||
* └─────────┴──────┴───────┘ | ||
* Hot | ||
*/ |
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 diagram but I think RefSet should be above ThisRef(C), Warm(D), and Fun (but below 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.
Putting RefSet
above ThisRef(C)
is also problematic (it might not be a member). In the end, I added more text to explain the ordering with RefSet
.
Result(value2, errors) | ||
} | ||
|
||
def instantiate(klass: ClassSymbol, ctor: Symbol, 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.
OK, please add comment:
// call on prefix p of a new expression new p.C
val value = Fun(ddef.rhs, obj, klass) | ||
Result(value, Nil) | ||
case _ => | ||
// The reason is that we never evaluate an expression if `thisV` is |
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.
Can you explain why we don't evaluate an expression when thisV is 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.
That's part of the design of the system -- when we call a method on a cold value, the system reports a warning. That's why this branch cannot be reached.
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 now LGTM. Ready to be merged.
Thanks @olhotak . I'll rebase and if it's green, let's merge. I'll make two following-up PRs
|
This brings us closer to OOPSLA paper. The key insight to avoid cycles like the following ``` class A(b: B) { val b2 = new B(this) } class B(a: A) { val a2 = new A(this) } ``` is to use `Warm(C, outer)` to evaluate fields and methods of `C`. The evaluation results are cached uniformly. Therefore, there is no need to treat `Warm(C, outer)` as addresses.
tests/init/crash/explicitOuter.scala
The PR to remove the old init checker is #12561. |
A simpler implementation of init checker
This version is based on the same insights as the previous version [1]. The analysis is written as an abstract definitional interpreter instead of a type-and-effect system. The new version is simpler, more regular, and enjoys better extensibility.