Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Merged
merged 1 commit into from
Apr 20, 2025

Conversation

sjrd
Copy link
Member

@sjrd sjrd commented Jan 30, 2025

Based on #5111.

We change the definition of AnonFunctionNs from final classes 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.

@sjrd sjrd force-pushed the ir-patch-old-anon-functions branch from 6c34d59 to 933719d Compare January 31, 2025 09:18
@sjrd sjrd force-pushed the ir-patch-old-anon-functions branch from 933719d to 493e3c6 Compare March 16, 2025 16:06
@sjrd sjrd marked this pull request as ready for review March 16, 2025 16:07
@sjrd sjrd requested a review from gzm0 March 16, 2025 16:07
@sjrd
Copy link
Member Author

sjrd commented Apr 6, 2025

@gzm0 For your next review spree, may I suggest you put this one on top of your queue? If you agree that we should do it at all, it would be much cleaner to have in 1.19.0 than in a future release. It is a strong complement to #5130 and #5003.

@gzm0
Copy link
Contributor

gzm0 commented Apr 6, 2025

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.

Just to check my understanding of the assumptions / compatibility guarantees here: We keep subtyping guarantees, but not type identity guarantees (so getClass will return something different).

We need to keep subtying guarantees because, despite scalajs.runtime being "private":

  • We promised binary backwards compatibility on it anyways (VERSIONING.md does not mention it).
  • We cannot guarantee that the types leak during compilation (at least not w/o investigation).

@gzm0
Copy link
Contributor

gzm0 commented Apr 6, 2025

First impression about this: I'm a bit split.

  • Pro: WASM gets faster w/o library recompiles (and library recompiles are quite a significant burden for the ecosystem, they need and IR bump, etc.).
  • Con: Quite a bit of complexity/risk in deserialization hacks, little upside for Scala.js-JS.

I'll have a more detailed look later today, hopefully helps me to form a more informed opinion.

@sjrd
Copy link
Member Author

sjrd commented Apr 6, 2025

Just to check my understanding of the assumptions / compatibility guarantees here: We keep subtyping guarantees, but not type identity guarantees (so getClass will return something different).

Yes, indeed. I think that one is fair because the particular getClass() one gets has always been an implementation detail. It's different on the JVM, for starters.

We need to keep subtying guarantees because, despite scalajs.runtime being "private":

  • We promised binary backwards compatibility on it anyways (VERSIONING.md does not mention it).
  • We cannot guarantee that the types leak during compilation (at least not w/o investigation).

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 vals for subexpressions. One of them could have a type derived from the specific New. We should not break that rewritten IR, since it was perfectly valid.

@sjrd sjrd force-pushed the ir-patch-old-anon-functions branch from 493e3c6 to af766f5 Compare April 7, 2025 16:36
@sjrd
Copy link
Member Author

sjrd commented Apr 7, 2025

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.

@sjrd
Copy link
Member Author

sjrd commented Apr 12, 2025

Pro: WASM gets faster w/o library recompiles (and library recompiles are quite a significant burden for the ecosystem, they need and IR bump, etc.).

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.

@gzm0
Copy link
Contributor

gzm0 commented Apr 13, 2025

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.

Copy link
Contributor

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

@sjrd sjrd requested a review from gzm0 April 14, 2025 11:26
Copy link
Contributor

@gzm0 gzm0 left a 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] = {
Copy link
Contributor

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

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

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

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

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:

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

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`.
@sjrd sjrd force-pushed the ir-patch-old-anon-functions branch from 6756059 to 9481522 Compare April 20, 2025 09:35
Copy link
Member Author

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

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.

@sjrd sjrd requested a review from gzm0 April 20, 2025 09:56
@gzm0 gzm0 merged commit d504303 into scala-js:main Apr 20, 2025
3 checks passed
@sjrd sjrd deleted the ir-patch-old-anon-functions branch April 20, 2025 15:19
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.

2 participants