Skip to content
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

Merged
merged 50 commits into from
May 21, 2021
Merged

Conversation

liufengyun
Copy link
Contributor

@liufengyun liufengyun commented May 17, 2021

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.

  1. A type-and-effect system for object initialization, OOPSLA, 2020.

@liufengyun liufengyun changed the title [WIP] A simpler implementation of init checker A simpler implementation of init checker May 17, 2021
@liufengyun liufengyun marked this pull request as ready for review May 17, 2021 09:44
@@ -57,7 +59,14 @@ class Checker extends MiniPhase {
env = Env(ctx.withOwner(cls), cache)
)

Checking.checkClassBody(tree)
// Checking.checkClassBody(tree)
Copy link
Contributor

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.

Copy link
Contributor Author

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)
Copy link
Contributor

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.

Copy link
Contributor Author

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.

@@ -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
Copy link
Contributor

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
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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.

Copy link
Contributor Author

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)) =>
Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Contributor Author

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 {
Copy link
Contributor

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)?

Copy link
Contributor Author

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
Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Contributor Author

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
*/
Copy link
Contributor

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).

Copy link
Contributor Author

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] =
Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

@olhotak olhotak left a 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.

@liufengyun
Copy link
Contributor Author

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

  • To remove the old checker and clean up docs
  • Refactor the domain to make it easier to implement escape to constructors without annotation (also implements caching of outers for warm)

@liufengyun liufengyun merged commit 450f8ac into scala:master May 21, 2021
@liufengyun liufengyun deleted the new-init branch May 21, 2021 17:18
@liufengyun
Copy link
Contributor Author

The PR to remove the old init checker is #12561.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants