-
Notifications
You must be signed in to change notification settings - Fork 395
Rewrite old IR with AnonFunctionN references to use NewLambda. #5122
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
6c34d59
to
933719d
Compare
933719d
to
493e3c6
Compare
Just to check my understanding of the assumptions / compatibility guarantees here: We keep subtyping guarantees, but not type identity guarantees (so We need to keep subtying guarantees because, despite
|
First impression about this: I'm a bit split.
I'll have a more detailed look later today, hopefully helps me to form a more informed opinion. |
Yes, indeed. I think that one is fair because the particular
Yes, that's right. Also, we don't actually control all the IR that is published. For example someone could have an IR post-processor that, for some reason, introduces more local |
493e3c6
to
af766f5
Compare
Rebased without change. I added half a sentence to the commit message to say that it also enhances the orphan await experience for the old libraries. |
I realized this is more important for Scala 3, in particular. In Scala 2, library maintainers only need to bump Scala.js and republish. There is little downside to doing it. The only real issue is with old, mature libraries that otherwise don't publish anymore. We might have a hard time convincing them to do a new release. In Scala 3, we need to wait for a new minor release of Scala 3, so that we can port the changes to the Scala 3 JS backend. That means libraires need to upgrade their Scala version to get the new things. For mainstream libraries, that's not really an option. They're supposed to stay on Scala 3.3.x (the LTS branch) until at least the next LTS comes out, but more likely until 3.3.x is declared EOL. The latter event is at least 1.5 years in the future. So without this PR, we're looking pretty far into the future before we can advertise Scala.js-on-3 to be fast on Wasm. |
After some thought, I'm convinced we should go forward with this. tl;dr: benefits are large for the Scala-WASM ecosystem, risks are limited to IR reading (no broken IR published) and manageable with tests (like this PR seems to already do). I'll proceed to detailed review. |
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.
First detailed review pass on the rewrites. I have not looked at the tests yet, but it feels to me the alternate proposal in the comment warrants sharing already.
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 agree we should go forward with this PR.
I've done a full review pass. I only have readability (and some minor memory usage) suggestions at this point.
Notably, I believe the amount of test coverage is appropriate.
anonFunctionArities.get(cls) | ||
} | ||
|
||
val anonFunctionDescriptors: IndexedSeq[NewLambda.Descriptor] = { |
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.
Lazy?
} | ||
|
||
val anonFunctionDescriptors: IndexedSeq[NewLambda.Descriptor] = { | ||
(0 to 22).map { arity => |
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.
Map over anonFunctionArities? (will also allow re-use of the same ClassName
instance).
case Block(stats) => stats | ||
case _ => oldBody :: Nil | ||
} | ||
val newBodyStats = oldBodyStats.filter(!_.isInstanceOf[Assign]) |
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 suppose this was the easiest way to get a valid constructor from the old constructor? It reads a bit odd to me; mostly in the sense that it feels it handles more cases than it actually does.
Compare with a solution that simply takes name
, originalName
, superClass
, pos
from the original class and throws everything away. IIUC that would do the same, but it is IMO much more explicit what it does.
You probably have a better feel for what this would entail. If you feel it is not appropriate, some explanation in a comment here would probably also remove the confusion.
superClass = AnonFunctionXXLClass, | ||
interfaces = Nil, | ||
methodName = MethodName(applySimpleName, List(ArrayTypeRef(ObjectRef, 1)), ObjectRef), | ||
paramTypes = List(ArrayType(ArrayTypeRef(ObjectRef, 1), nullable = true)), |
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 putting ArrayType(ArrayTypeRef(ObjectRef, 1), nullable = true)
also in a val here. It seems to be used 3 times (if we allow ourselves pulling the ref out of the type).
libraryDependencies := { | ||
libraryDependencies.value.map { dep => | ||
if (dep.name.startsWith(artifactNamePrefix)) | ||
dep.withRevision(dep.revision.substring(0, dep.revision.indexOf('+') + 1) + v) |
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 string replacement is very smart, but IMO quite hard to understand.
IIUC for every invocation of rewriteDepencenyVersion
, we know the revision
value we want to have at the end (we always know scalaVersion
and scalaJSVersion
and we know the versioning scheme of the specific artifact).
Have you considered a solution where you have a helper to remove / filter the unwanted artifact and pass the full version string of the replacement? It might be easier to understand what is going on. As it stands now, I needed to look here to reconstruct:
scala-js/sbt-plugin/src/main/scala/org/scalajs/sbtplugin/ScalaJSPluginInternal.scala
Lines 788 to 817 in e507300
if (isScala3(scalaV)) { | |
/* Remove scala3-library (non _sjs1) in case sbt-dotty was applied | |
* before sbt-scalajs. | |
*/ | |
val filteredPrev = prev.filterNot { dep => | |
dep.organization == scalaOrg && dep.name == "scala3-library" | |
} | |
filteredPrev ++ Seq( | |
scalaOrg %% "scala3-library_sjs1" % scalaV, | |
/* scala3-library_sjs1 depends on some version of scalajs-library_2.13, | |
* but we bump it to be at least scalaJSVersion. | |
* (It will also depend on some version of scalajs-scalalib_2.13, | |
* but we do not have to worry about that here.) | |
*/ | |
"org.scala-js" % "scalajs-library_2.13" % scalaJSVersion, | |
"org.scala-js" % "scalajs-test-bridge_2.13" % scalaJSVersion % "test" | |
) | |
} else { | |
prev ++ Seq( | |
compilerPlugin("org.scala-js" % "scalajs-compiler" % scalaJSVersion cross CrossVersion.full), | |
"org.scala-js" %% "scalajs-library" % scalaJSVersion, | |
/* scalajs-library depends on some version of scalajs-scalalib, | |
* but we want to make sure to bump it to be at least the one | |
* of our own `scalaVersion` (which would have back-published in | |
* the meantime). | |
*/ | |
"org.scala-js" %% "scalajs-scalalib" % s"$scalaV+$scalaJSVersion", | |
"org.scala-js" %% "scalajs-test-bridge" % scalaJSVersion % "test" | |
) | |
} |
Alternatively, a comment of what the different cases are here, potentially even a (useless) if
on the result of dep.revision.indexOf('+')
might be helpful.
.enablePlugins(ScalaJSPlugin) | ||
.settings( | ||
scalaVersion := "3.6.3", | ||
forceLibraryVersion(ScalaJSVersionBeforeTypedClosures), |
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 example here, more explicit dependencies would help me. IIUC, library version in the Scala 3 context only refers to the Scala.js stdlib, not to the Scala stdlib. Whereas in the Scala 2 context, it also refers to the Scala stdlib.
We change the definition of `AnonFunctionN`s from `final class`es with concrete `apply` methods to `sealed abstract` classes. We rewrite `New` nodes to them to use `NewLambda` instead. See comments in the IR deserializer for more details on the performed rewrites. The rewritten `NewLambda` must target classes with the same identity (and not directly `AbstractFunctionN`) because the class names can be used in *types* somewhere else in the IR. The result of the `NewLambda` nodes must stay compatible with those types. This change ensures that even old libraries benefit from all the new optimizations for lambdas on Wasm, and that they can participate in a call chain between `js.async` and an orphan `js.await`.
6756059
to
9481522
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.
Thanks for the review.
I believe I have addressed all the comments. I also squashed everything, but GitHub shows the last force-push diff if you want to see only the latest changes.
Based on #5111.We change the definition of
AnonFunctionN
s fromfinal class
es with concreteapply
methods tosealed abstract
classes. We rewriteNew
nodes to them to useNewLambda
instead.See comments in the IR deserializer for more details on the performed rewrites.
The rewritten
NewLambda
must target classes with the same identity (and not directlyAbstractFunctionN
) because the class names can be used in types somewhere else in the IR. The result of theNewLambda
nodes must stay compatible with those types.This change ensures that even old libraries benefit from all the new optimizations for lambdas on Wasm, and that they can participate in a call chain between
js.async
and an orphanjs.await
.