Skip to content

Rewrite resolveThis in global init checker #23282

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

EnzeXing
Copy link
Contributor

@EnzeXing EnzeXing commented May 29, 2025

This PR resolves bugs in the redesigned global initialization checker; specifically, it now correctly resolves the value of parent.this in a child class, and removes the assertions that fail in community build projects

[test_scala2_library_tasty]

@EnzeXing EnzeXing force-pushed the global-init-checker-redesign branch from 798fd49 to 1e0eb9f Compare May 29, 2025 13:56
@@ -287,11 +287,19 @@ class Objects(using Context @constructorOnly):
def toScopeSet: ScopeSet = ScopeSet(values.asInstanceOf[Set[Scope]])

case class ScopeSet(scopes: Set[Scope]):
assert(scopes.forall(_.isRef) || scopes.forall(_.isEnv), "All scopes should have the same type!")
def isRefSet = scopes.forall(_.isRef)
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 keep an assertion that isRefSet || isEnvSet?

@@ -1969,6 +1977,11 @@ class Objects(using Context @constructorOnly):
case _ =>
report.warning("[Internal error] unexpected thisV = " + thisV + ", target = " + target.show + Trace.show, Trace.position)
Bottom
if resolveResult == Bottom && thisV.filterClass(target) == thisV then
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible that target could be a parent class of an outer class?

@olhotak
Copy link
Contributor

olhotak commented Jun 2, 2025

Here's an example of a ScopeSet that can have both classes and methods.

class A {
  var x = 42
  def change = (x = 44)
  class B {
    val i = 5
    def fooz = A.this.x // if this is an instance of class C, does A.this resolve to the C outer or to the B outer ???
    def fooz2 = A.this.x
    class D {
       def bar = fooz
       def bar2 = fooz2
    }
  }
}
class AA extends A {
  def foo = {
    //val y = 43
    val a = if(*) new A else new AAA
    class C /*outer AA*/ extends a.B /*outer A or AAA*/ {
       override val i = 6
       override def fooz2 = x
    }
    val bb: B = if(false) new a.B else new C
    val d =  new bb.D
    d.bar   // reads AA.x
    d.bar2 // reads AA.x
    }
}

d --outer-> {B or C} --outer-> {A or foo} --outer --> {A}
d.bar, looking for x
reach {A or foo}
A has x, foo doesn't
do I continue searching the outer of foo ???

@EnzeXing EnzeXing force-pushed the global-init-checker-redesign branch from 1e0eb9f to 8b89ae9 Compare June 3, 2025 15:09
@EnzeXing
Copy link
Contributor Author

EnzeXing commented Jun 3, 2025

@olhotak @liufengyun I have added two rather complex tests, feel free to run them with scala-cli.
With regard to multiple-outers.scala, here are my explanations:

  1. When accessing a field, the access chain from the enclosing class of the select expression to the enclosing class of the field is composed with two kinds of edges: $\overrightarrow{\text{outer}}$ and $\overrightarrow{\text{parent}}$. $\overrightarrow{\text{outer}}$ corresponds to a call of resolveThis, and $\overrightarrow{\text{parent}}$ relates to how we handle inherited fields
  2. In an access chain, $\overrightarrow{\text{outer}}$ can be followed by $\overrightarrow{\text{parent}}$, but $\overrightarrow{\text{parent}}$ cannot be followed by $\overrightarrow{\text{outer}}$. Example:
class A {
  val field_a = 5
  def bar(): Int = A.this.field_a
}

class B extends A {
  val field_b = field_a
  class C {
    val field_c = bar() // expands to B.this.bar()
    val field_c2 = field_a // C --> outer B --> parent A
  }
}

object O:
  val b = new B
  class D extends b.C {
    val field_d = field_b // D --> parent C --> outer B (invalid)
    // error: Not found: field_b
  }
  val d = new D
  1. At a particular step, if both two kinds of edges are valid, compiler issues an error for ambiguous access. Example:
class A {
  val x = 5
}

class B {
  val x = 6
  class C extends A {
    val f = x // error: Reference to x is ambiguous.
  }
}

@EnzeXing
Copy link
Contributor Author

EnzeXing commented Jun 3, 2025

The second test anon-class.scala is a tricky one, since it has the edge B $\overrightarrow{outer}$ B with the same dynamic class, so calling resolveThis cannot terminate. Maybe the potential solution is to stop when the combined outer set reaches a fixed point

@liufengyun
Copy link
Contributor

The second test anon-class.scala is a tricky one, since it has the edge B o u t e r → B with the same dynamic class, so calling resolveThis cannot terminate. Maybe the potential solution is to stop when the combined outer set reaches a fixed point

It's not clear why resolveThis should be a problem. I see the last PR changed resolveThis by removing the parameter klass: that is problematic semantically and I think it also caused the loop.

The following comment might help:

* Note that `klass` might be a super class of the object referred by `thisV`.
* The parameter `klass` is needed for `this` resolution. Consider the following code:
*
* class A {
* A.this
* class B extends A { A.this }
* }
*
* As can be seen above, the meaning of the expression `A.this` depends on where
* it is located.

@EnzeXing EnzeXing force-pushed the global-init-checker-redesign branch from 8b89ae9 to f2f9df3 Compare June 17, 2025 02:47
@EnzeXing EnzeXing marked this pull request as ready for review June 17, 2025 11:53
@olhotak
Copy link
Contributor

olhotak commented Jun 18, 2025

[test_scala2_library_tasty]

val outerEnvs = envSet.joinOuterEnvs
if outerEnvs != NoEnv then // Search for the outerEnvs of the current envSet
resolveEnvRecur(target, outerEnvs, bySymbol)
else // Search through the outerEnvs of the instances represented by `this`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this really make sense as a fallback?

We should know statically whether a symbol that we're looking for is in a local env (in that case the TermRef has NoPrefix) or whether it is in the body of the this object (or one of its outers) (in that case the TermRef has some form of the this object as prefix). It's odd to search both of these.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added another test local-class.scala to explain this case. In the test, method bar in loca class D tries to access c, which is a local variable without prefix. The scope in the Config is LocalEnv(bar) when this access happens, but c is defined in foo, which is not a method in D. When we find that bar does not have any direct enclosing method, we have to look through the outerEnv of D since c could be defined there, or defined in other outer methods that encloses some other local classes that encloses D.

// meth == NoSymbol for poly functions
if meth.name == nme.tupled then
value // a call like `fun.tupled`
else
code match
case ddef: DefDef =>
if meth.name == nme.apply then
given Scope = Env.ofDefDef(ddef, args.map(_.value), ScopeSet(Set(env)))
val funEnv = scope match {
case ref: Ref => Env.ofDefDef(ddef, args.map(_.value), thisV, Env.NoEnv)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it correct to pass thisV here, or should it be ref?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's supposed to pass in the value of thisV when the closure is created, and it is recorded in Fun. I have renamed thisV in this line to thisVOfClosure for clarification

Copy link
Contributor

@liufengyun liufengyun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, I only have a few minor comments for consideration.

* valsMap = sym -> val // maps variables to their values
* outersMap = sym -> ScopeSet // maps the possible outer scopes for a corresponding (parent) class
* heap.MutableData = Scope -> (valsMap, outersMap) // heap is mutable
* Config ::= (thisV: Value, scope: Scope, Heap, EnvMap)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible for scope in Config to be Ref? The same question for Fun.

These are the only place where the type Scope appears. If we can remove one concept, that would be nice (Occam's razor).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: regarding the name LocalEnv, maybe we can use something like EnvAddr or EnvRef?

@@ -187,12 +202,13 @@ class Objects(using Context @constructorOnly):

object OfClass:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (for future): We can consider rename OfClass to InstanceRef to make its nature of being an abstract address clear. The same for OfArray.

def initVar(field: Symbol, value: Value)(using Context, EnvMap.EnvMapMutableData) = log("Initialize " + field.show + " = " + value + " for " + this, printer) {
assert(field.is(Flags.Mutable), "Field is not mutable: " + field.show)
EnvMap.writeJoinVal(this, field, value)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (for future): Given that Var and Val have same logic, we can consider generalize a little the code.

* Ref ::= ObjectRef | OfClass | OfArray // values that represent a reference to some (global or instance) object
* ThisValue ::= Ref | Set(Ref) // possible values for 'this'
* RefSet ::= Set(ref) // set of refs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (for future): There is some overlapping between ValueSet and RefSet. It seems the former is a subset of the latter. If that is the case, we can consider introducing an opaque type with a special constructor to encode the invariant.

Edit: I see RefSet is made a subclass of ValueSet. That is a clever trick. Might be worth adding a comment there warning that equality is delegated to ValueSet thus no more fields should be added.

case outer : ThisValue =>
if klass.owner.is(Flags.Package) then
report.warning("[Internal error] top-level class should have `Package` as outer, class = " + klass.show + ", outer = " + outer.show + ", " + Trace.show, Trace.position)
Env.NoEnv
(Bottom, Env.NoEnv)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor (for future): Here we use Bottom to mean absence of value. I think it is OK. /cc: @olhotak

@EnzeXing EnzeXing force-pushed the global-init-checker-redesign branch from f2f9df3 to b1d5eae Compare June 20, 2025 02:42
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.

3 participants