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

Skip to content

LinkTimeIf - on top of desugaring in BaseLinker #5100

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 3, 2025

Reboot of #5000

@gzm0
Copy link
Contributor

gzm0 commented Jan 3, 2025

I'm glad to see you are digging this up. We need to make sure that whatever comes out of #5096 is compatible with this.

sjrd added 2 commits January 10, 2025 11:00
Previously, the emitters and the optimizer all had to perform the
same desugaring for `LinkTimeProperty` nodes. Instead, we now
perform the desugaring in the `BaseLinker`, after the reachability
analysis.

The reachability analysis records whether each method needs
desugaring or not. Methods that do not require desugaring are not
processed, and so incur no additional cost. Since very few methods
need desugaring, we do not cache the results.

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.
As well as the IR version to 1.19-SNAPSHOT.
@sjrd sjrd changed the title WiP LinkTimeIf LinkTimeIf - on top of desugaring in BaseLinker Jan 10, 2025
sjrd and others added 2 commits January 10, 2025 15:02
Thanks to our optimizer's ability to inline, constant-fold, and
then eliminate dead code, we have been able to write link-time
conditional branches for a long time. Typical examples include
polyfills, as illustrated in the documentation of `LinkingInfo`:

    if (esVersion >= ESVersion.ES2018 || featureTest())
      useES2018Feature()
    else
      usePolyfill()

which gets folded away to nothing but

    useES2018Feature()

when linking for ES2018+.

However, this only works because both branches can *link* during
the initial reachability analysis. We cannot use the same technique
when one of the branches would refuse to link in the first place.
The canonical example is the usage of the JS `**` operator, which
does not link under ES2016. The following snippet produces good
code when linking for ES2016+, but does not link at all for ES2015:

    def pow(x: Double, y: Double): Double = {
      if (esVersion >= ESVersion.ES2016) {
        (x.asInstanceOf[js.Dynamic] ** y.asInstanceOf[js.Dynamic]).asInstanceOf[Double]
      } {
        Math.pow(x, y)
      }
    }

---

This commit introduces `LinkingInfo.linkTimeIf`, a conditional
branch that is guaranteed by spec to be resolved at link-time.
Using a `linkTimeIf` instead of the `if` in `def pow`, we can
successfully link the fallback branch on ES2015, because the then
branch is not even followed by the reachability analysis.

In order to provide that guarantee, the corresponding `LinkTimeIf`
IR node has strong requirements on its condition. It must be a
"link-time expression", which is guaranteed to be resolved at
link-time. A link-time expression tree must be of the form:

* A `Literal` (of type `int`, `boolean` or `string`, although
  `string`s are not actually usable here).
* A `LinkTimeProperty`.
* One of the boolean operators.
* One of the int comparison operators.
* A nested `LinkTimeIf` (used to encode short-circuiting boolean
  `&&` and `||`).

The `ClassDefChecker` validates the above property, and ensures
that link-time expression trees are *well-typed*. Normally that is
the job of the IR checker. Here we *can* do in `ClassDefChecker`
because we only have the 3 primitive types to deal with; and we
*must* do it then, because the reachability analysis itself is only
sound if all link-time expression trees are well-typed.

The reachability analysis algorithm itself is not affected by
`LinkTimeIf`. Instead, we resolve link-time branches when building
the `Infos` of methods. We follow only the branch that is taken.
This means that `Infos` builders now require the `coreSpec`, but
that is the only additional piece of complexity in that area.

`LinkTimeIf`s nodes are later removed from the trees during
desugaring.

---

At the language and compiler level, we introduce
`LinkingInfo.linkTimeIf` as a primitive for `LinkTimeIf`. We need
a dedicated method to compile link-time expression trees, which
does incur some duplication, unfortunately. Other than that,
`linkTimeIf` is straightforward, by itself.

The problem is that the whole point of `linkTimeIf` is that we can
refer to *link-time properties*, and not just literals. However,
our link-time properties are all hidden behind regular method calls,
such as `LinkInfo.esVersion`. For optimizer-based branching with
`if`s, that is fine, as the method is always inlined, and the
optimizer can then see the constant. However, for `linkTimeIf`,
that does not work, as it does not follow the requirements of a
link-time expression tree.

If we were on Scala 3 only, we could declare `esVersion` and its
friends as an `inline def`, as follows:

    inline def esVersion: Int =
      linkTimePropertyInt("core/esVersion")

The `inline` keyword is guaranteed by the language to be resolved
at *compile*-time. Since the `linkTimePropertyInt` method is itself
a primitive replaced by a `LinkTimeProperty`, by the time we reach
our backend, we would see the latter, and all would be well.
The same cannot be said for the `@inline` optimizer hint, which is
all we have.

We therefore another language-level feature: `@linkTimeProperty`.
This annotation can (currently) only be used in our own library.
By contract, it must only be used on a method whose body is the
corresponding `linkTimePropertyX` primitive. With it, we can define
`esVersion` as:

    @inline @linkTimeProperty("core/esVersion")
    def esVersion: Int =
      linkTimePropertyInt("core/esVersion")

That annotation makes the body public, in a way. That means the
compiler back-end can now replace *call sites* to `esVersion` by
the `LinkTimeProperty`.

Semantically, `@linkTimeProperty` does nothing more than guaranteed
inlining (with strong restrictions on the shape of body).

Co-authored-by: Rikito Taniguchi <[email protected]>
We use a `linkTimeIf` to select a `bigint`-based implementation of
`parseFloatDecimalCorrection` when they are supported. We need a
`linkTimeIf` in this case because it uses the JS `**` operator,
which does not link below ES 2016.

The `bigint`-based implementation avoids bringing in the entire
`BigInteger` implementation, which is a major code size win if that
was the only reason `BigInteger` was needed.
@sjrd
Copy link
Member Author

sjrd commented Feb 6, 2025

Superseded by #5110.

@sjrd sjrd closed this Feb 6, 2025
@sjrd sjrd deleted the link-time-if branch February 6, 2025 15:49
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