-
Notifications
You must be signed in to change notification settings - Fork 395
NewLambda - on top of a separate desugaring phase #5111
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
Closed
Closed
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
b7729a6
to
2b926cc
Compare
cf386b0
to
5098553
Compare
Previously, the emitters and the optimizer all had to perform the same desugaring for `LinkTimeProperty` nodes. Instead, we now perform the desugaring in a dedicated phase, after the base linker. The reachability analysis records whether each method needs desugaring or not. We mark those that do so that the desugaring pass knows what to process. Methods that do not require desugaring are not processed, and so incur no additional cost. No caching is performed in `Desugarer`. It processes so few methods that caching makes it (slightly) *slower*. The machinery is heavy. It definitely outweighs the benefits in terms of duplication for `LinkTimeProperty` alone. However, the same machinery will be used to desugar `NewLambda` nodes. This commit serves as a stepping stone in that direction.
The `NewLambda` node creates an instance of an anonymous class from a `Descriptor` and a closure `fun`. The `Descriptor` specifies the shape of the anonymous class: a super class, a list of interfaces to implement, and the name of a single non-constructor method to provide. The body of the method calls the `fun` closure. At link time, the Analyzer and BaseLinker synthesize a unique such anonymous class per `Descriptor`. In practice, all the lambdas for a given target type share a common `Descriptor`. This is notably the case for all the Scala functions of arity N. `NewLambda` replaces the need for special `AnonFunctionN` classes in the library. Instead, classes of the right shape are synthesized at link-time. The scheme can also be used for most LambdaMetaFactory-style lambdas, although our `NewLambda` does not support bridge generation. In the common case where no bridges are necessary, we now also generate a `NewLambda`. This generalizes the code size optimization of having only one class per `Descriptor` to non-Scala functions. In order to truly support LMF-style lambdas, the closure `fun` must take parameters that match the (erased) type in their superinterface. Previously, for Scala `FunctionN`, we knew by construction that the parameters and result types were always `any`, and so JS `Closure`s were good enough. Now, we need closures that can accept different types. This is where `TypedClosure`s come into play (see below). --- When bridges are required, we still generate a custom class from the compiler backend. In that case, we statically inline the closure body in the produced SAM implementation. We have to do this not to expose typed closures across method calls. Moreover, we need the better static types for the parameters to be able to inline the closures without too much hassle. So this change *has* to be done in lockstep with the rest of this commit. --- A `TypedClosure` is like a `Closure` but without any semantics for JS interop. This is stronger than `Char`, which is "merely" opaque to JS. A `Char` can still be passed to JS and has a meaningful `toString()`. A `TypedClosure` *cannot* be passed to JS in any way. That is enforced by making their type *not* a subtype of `any` (like record types). Since a `TypedClosure` has no JS interop semantics, it is free to strongly, statically type its parameters and result type. Additionally, we can freely choose its representation in the best possible way for the given target. On JS, that remains an arrow function. On Wasm, however, we represent it as a pair of `(capture data pointer, function pointer)`. This allows to compile them in an efficient way that does not require going through a JS bridge closure. The latter has been shown to have a devastating impact on performance when a Scala function is used in a tight loop. The type of a `TypedClosure` is a `ClosureType`. It records its parameter types and its result type. Closure types are non-variant: they are only subtypes of themselves. As mentioned, they are not subtypes of `any`. They are however subtypes of `void` and supertypes of `nothing`. Unfortunately, they must also be nullable to have a default value, so they have nullable and non-nullable alternatives. To call a typed closure, we introduce a dedicated application node `ApplyTypedClosure`. IR checking ensures that actual arguments match the expected parameter types. The result type is directly used as the type of the application. There are no changes to the source language. In particular, there is no way to express typed closures or their types at the user level. The are only used for `NewLambda` nodes. In fact, `TypedClosure`s and `ApplyTypedClosure`s are not first-class at the IR level. Before desugaring, `TypedClosure`s are only allowed as direct children of `NewLambda` nodes. Desugaring transforms `NewLambda` nodes into `New`s of the synthesized anonymous classes. At that point, the two typed closure nodes become first-class expression trees. --- For Scala functions, these changes have no real impact on the JS output (only marginal naming differences). On Wasm, however, they make Scala functions much, much faster. Before, a Scala function in a tight loop would cause a Wasm implementation to be, in the worst measured case, 20x slower than on JS. After these changes, similar benchmarks become significantly faster on Wasm than on JS.
5098553
to
539d2be
Compare
Integrated into #5003, where all the discussion happened. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
No description provided.