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

Skip to content

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
wants to merge 4 commits into from

Conversation

sjrd
Copy link
Member

@sjrd sjrd commented Jan 10, 2025

No description provided.

@sjrd sjrd force-pushed the typed-closures-with-desugaring-phase branch 2 times, most recently from b7729a6 to 2b926cc Compare January 29, 2025 12:31
@sjrd sjrd force-pushed the typed-closures-with-desugaring-phase branch 2 times, most recently from cf386b0 to 5098553 Compare February 3, 2025 09:12
sjrd added 4 commits February 6, 2025 14:03
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.
@sjrd sjrd force-pushed the typed-closures-with-desugaring-phase branch from 5098553 to 539d2be Compare February 6, 2025 13:50
@sjrd
Copy link
Member Author

sjrd commented Feb 6, 2025

Integrated into #5003, where all the discussion happened.

@sjrd sjrd closed this Feb 6, 2025
@sjrd sjrd deleted the typed-closures-with-desugaring-phase branch February 6, 2025 15:48
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.

1 participant