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

Fix variance loophole for private vars #18693

Merged
merged 2 commits into from
Oct 17, 2023
Merged

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Oct 13, 2023

In Scala 2 a setter was created at Typer for private, non-local vars. Variance
checking then worked on the setter. But in Scala 3, the setter is only created
later, which caused a loophole for variance checking.

This PR does actually better than Scala 2 in the following sense: A private variable counts as an invariant occurrence only if it is assigned with a selector different from this. Or conversely, a variable containing a covariant type parameter in its type can be read from different objects, but all assignments must be via this. The motivation is that such variables effectively behave like vals for the purposes of variance checking.

In Scala 2 a setter was created at Typer for private, non-local vars. Variance
 checking then worked on the setter. But in Scala 3, the setter is only created
later, which caused a loophole for variance checking.

This PR does actually better than Scala 2 in the following sense: A private variable
counts as an invariant occurrence only if it is assigned with a selector different
from `this`. Or conversely, a variable containing a covariant type parameter in its
type can be read from different objects, but all assignments must be via this. The
motivation is that such variables effectively behave like vals for the purposes
of variance checking.
@odersky odersky marked this pull request as ready for review October 13, 2023 16:48
@odersky odersky requested a review from sjrd October 13, 2023 20:25
/** Is `pre` the same as C.this, where C is exactly the owner of this symbol,
* or, if this symbol is protected, a subclass of the owner?
*/
def isCorrectThisType(pre: Type)(using Context): Boolean = pre match
Copy link
Member

Choose a reason for hiding this comment

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

Now that it's available outside of isAccessibleFrom, the name isCorrectThisType is not so clear anymore. Consider renaming to isAccessPrivilegedThisType or something like that?

Comment on lines 269 to 270
/** Update privateVarsSetNonLocally is symbol is a private variable
* that is selected from something other than `this` when assigned
Copy link
Member

Choose a reason for hiding this comment

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

Typo:

Suggested change
/** Update privateVarsSetNonLocally is symbol is a private variable
* that is selected from something other than `this` when assigned
/** Update privateVarsSetNonLocally if symbol is a private variable
* that is selected from something other than `this` when assigned

Comment on lines 491 to 493
case Assign(lhs @ Select(qual, _), _) =>
markVarAccess(lhs, qual)
super.transform(tree)
Copy link
Member

Choose a reason for hiding this comment

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

Do I understand correctly that private vars don't get a setter then? When mutated, they are directly Assigned instead of having an apply(setter, ...)?

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 setter gets added in a later phase, Getters.

Comment on lines 159 to 160
if sym.isClass then
VarianceChecker.check(tree, privateVarsSetNonLocally)
Copy link
Member

Choose a reason for hiding this comment

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

Does this work if we find an illegal access from the companion object, defined after the class? IIUC the flow of this phase, we won't explore the object before we get here.

A test case would be

class BoxWithCompanion[+A](value: A) {
  private var cached: A = value // error
  def get: A = cached
}

object BoxWithCompanion {
  def put[A](box: BoxWithCompanion[A], value: A): Unit = {
    box.cached = value
  }
}

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 catch; it does not.

tests/neg/i18588.scala Show resolved Hide resolved
@sjrd sjrd assigned odersky and unassigned sjrd Oct 16, 2023
We need to mark such assigns in the phase before PostTyper since they can
appear in companion objects that come after the variable declaration.
@odersky odersky assigned sjrd and unassigned odersky Oct 16, 2023
@odersky
Copy link
Contributor Author

odersky commented Oct 16, 2023

@sjrd Since the revision undoes all changes in PostTyper, it might be easiest to just review the total change set over the two commits directly.

Copy link
Member

@sjrd sjrd left a comment

Choose a reason for hiding this comment

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

Should the commits be squashed, if the first commit was mostly reverted?

Otherwise LGTM.

@sjrd sjrd assigned odersky and unassigned sjrd Oct 17, 2023
@odersky
Copy link
Contributor Author

odersky commented Oct 17, 2023

I think it's probably fine to keep the history.

@odersky odersky merged commit 89fa247 into scala:main Oct 17, 2023
18 checks passed
@odersky odersky deleted the fix-18588 branch October 17, 2023 11:56
/** An annotation to indicate that a private `var` was assigned with a prefix
* other than the `this` type of its owner.
*/
class AssignedNonLocally() extends Annotation
Copy link
Member

Choose a reason for hiding this comment

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

do these annotation classes have any consequences for library compatibility?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no, it's internal compiler stuff only.

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