-
Notifications
You must be signed in to change notification settings - Fork 396
Check restrictions on how this
values can be used in constructors.
#5019
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
Conversation
407962a
to
c13cf9a
Compare
So ... it turns out that we did (and still do) generate IR that does not follow those restrictions. There are two sources. Super ctor call to
|
c13cf9a
to
ef326f8
Compare
@gzm0 So I could indeed fix the It's still quite problematic because it does not retroactively fix code that was compiled with previous versions of the compiler. I don't know what to do about that. It's super rare; one almost has to write contrived code to run into the issue. But it's still IR that was considered valid before, that may be published somewhere in some library, and that we would "abandon" as being invalid now. |
Do we have an understanding of whether such IR actually works? (Especially in contrived downstream usage scenarios). Or in other words: Can we construct an end-to-end example of usage of a library that has such IR causes some form of user visible problem (linker or code crashing)? If yes, we have a good case to declare this a bug and essentially say the more restrictive class def checking catches more bugs in libraries (even if they were caused by Scala.js itself). |
As an alternative, we could investigate options to turn the restrictions down. For example, we could apply the restrictions only if there exists a non-nullable field in the ancestry chain. I guess the issues with many of these is going to be:
|
I don't see why such IR wouldn't work. I always thought this to be valid. We never made the assumption that
That's probably reason enough not to design the system this way, IMO. |
I initially was going to agree with this, but maybe there can still be value. For example, changing to non-nullable would be a binary compatible change for final classes (granted, final is not in the IR). So for example, data classes could profit from this. Two more ideas I had:
Both of these seem rather hard to do if feasible at all. Otherwise I think our options boil down to:
|
I ran an "audit" of all the .sjsir files found in all the published Scala.js libraries (on Maven Central). I used Scaladex to get a list of all the artifacts with their latest version. For each, I used the Coursier API to fetch their jar, then I ran the There were 7029 artifacts, and none of them yielded a "restricted Script and results here: IMO that should be enough evidence that we can just abandon IR that has restricted Ironically, there were 3 errors about the constructor call discipline in Akka.js. But that's in IR that they manually patch. There were also two errors in synthetic magic traits of the Scala 3 library. Those are .sjsir files that we never actually load, AFAICT. |
Agreed. |
2ab881f
to
576bcf1
Compare
All right. I switched the order of the commits, and cleaned up comments and commit messages. Ready for an actual review now. |
The audit is very nice as it pulled all the Scala.js 1.x artifacts including all the older versions even published for Scala 2.11. |
for { | ||
referrers <- methodReferences.get(symbol) | ||
referrer <- referrers | ||
} { | ||
// referrer calls `symbol`, so it transitively references `this` | ||
rec(referrer) | ||
} |
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.
for { | |
referrers <- methodReferences.get(symbol) | |
referrer <- referrers | |
} { | |
// referrer calls `symbol`, so it transitively references `this` | |
rec(referrer) | |
} | |
methodReferences.getOrElse(symbol, Nil).foreach(rec(_)) |
For readability? (IMO the nested comment is not necessary).
@@ -915,6 +932,8 @@ private final class ClassDefChecker(classDef: ClassDef, | |||
reportError(i"Cannot find `this` in scope") | |||
else if (tree.tpe != env.thisType) | |||
reportError(i"`this` of type ${env.thisType} typed as ${tree.tpe}") | |||
else if (env.isThisRestricted) |
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 the else? Wouldn't we want to report errors exhaustively (if sensible)?
reportError(i"Cannot find `this` in scope for StoreModule()") | ||
else if (env.isThisRestricted) |
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 the change to else? Wouldn't we want to report errors exhaustively (if sensible)?
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 figured some of these errors might lead to other errors. But I reverted to not having else
s. Same below.
For all lifted methods and methods that contain the body of lambdas, we analyze whether they (transitively) refer to `this`. If not, we emit them as static. The JVM backend does a similar analysis in the `delambdafy` phase, which comes after our backend. As a side effect, this solves an issue with lifted local methods coming from arguments to a super constructor call. They would previously result in references to `this` from the super constructor call arguments, which we will not consider as valid IR anymore. Since Scala lexical scoping rules ensure that they do not actually refer to `this`, they will now always be compiled as static.
Essentially, `this` cannot be used before the super constructor call, except to assign fields (but not reading). These restrictions match those enforced by the JVM for bytecode. While we never intended `this` values to be restricted in this way, in practice we never produced IR that would violate those restrictions. There was one exception before the parent commit: we could have lifted methods coming from the arguments to a super constructor call. Since they were lifted as instance methods, they caused the constructor to refer to `this` before the super constructor call. This is in theory a backward breaking change in the IR! However, we audited the entire ecosystem of libraries published on Maven Central. None of them contained IR that violates the new restriction. In the future, we may leverage those restrictions to validate strict field initialization. If a field *needs* to be initialized before it is ever read (for example, because it has a non-nullable reference type), then we can do so: we only have to check that it is definitely initialized before the super constructor call. The restrictions implemented in this commit ensure that nobody can read the field before the super constructor call starts.
576bcf1
to
274713e
Compare
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.
Updated.
reportError(i"Cannot find `this` in scope for StoreModule()") | ||
else if (env.isThisRestricted) |
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 figured some of these errors might lead to other errors. But I reverted to not having else
s. Same below.
Essentially,
this
cannot be used before the super constructor call, except to assign fields (but not reading).These restrictions match those enforced by the JVM for bytecode.
While we never intended
this
values to be restricted in this way, in practice we never produced IR that would violate those restrictions.In the future, we may leverage those restrictions to validate strict field initialization. If a field needs to be initialized before it is ever read (for example, because it has a non-nullable reference type), then we can do so: we only have to check that it is definitely initialized before the super constructor call. The restrictions implemented in this commit ensure that nobody can read the field before the super constructor call starts.