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

Skip to content

Allow for linktime conditional branching #4997

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

Open
tanishiking opened this issue Jun 11, 2024 · 2 comments
Open

Allow for linktime conditional branching #4997

tanishiking opened this issue Jun 11, 2024 · 2 comments
Labels
language Affects language semantics.

Comments

@tanishiking
Copy link
Contributor

Part of #4991

Motivation

The motivation behind this feature was to allow switching the implementation based on whether the target is Wasm running in the browser (WasmJS) or a standalone Wasm application. For example, with regular expressions, WasmJS could reuse the JavaScript Regex implementation, while a standalone Wasm application would use a pure-Scala Regex implementation.

While the original motivation is from Wasm use cases, the feature itself is self-contained and can be used for optimization purposes in the JavaScript backend as well.

How to

The main idea is to introducing the new IR node LInkTimeIf like:

To put into writing something we discussed offline at some point: I believe the easiest and cleanest way to do this is with a new IR node that would look like

case class LinkTimeIf(cond: LinkTimeCondition, thenp: Tree, elsep: Tree) extends Tree

Then, our 3 main link-time passes process it as follows:

  • the Analyzer, for the reachability analysis, deals with it during the Infos builder. Since the Infos builder walks the IR trees to build Infos, it can resolve the LinkTimeCondition and only recurse in the appropriate branch
  • the OptimizerCore would trivially replace the LinkTimeIf with either thenp or elsep, allowing further optimizations
  • the backends would do the same, if any LinkTimeIf survives until then (normally, only if the optimizer is disabled)

The precise nature of LinkTimeCondition remains to be determined, but it would fairly limited. Definitely not a full Tree.

Originally posted by @sjrd in #4991 (comment)

Where the LinkTimeCondition should be like same as scala-native's LinktimeCondition

How to: frontend

  • Introduce @LinkTime (or @resolvedAtLinktime like SN) annotation, only the annotated values (functions?) can be used as part of LinkTimeCondition
  • if expression will be translated to LinkTimeIf instead of If, when the condition is only composed of @LinkTime annotated value, Literals, and BinaryOps.

For example, the if will be translated to LinkTimeIf because the condition is composed of @LinkTime annotated booleans + BinaryOp. (if it's mixed with the runtime-value, we should fail linking).

@LinkTime
def foo: Boolean = ...
@LinkTime
def bar: Boolean = ...

if (foo && bar) { ... }
@sjrd
Copy link
Member

sjrd commented Jun 11, 2024

I'm not sure about magically turning an if into a LinkTimeIf depending on some non-local information. That would be quite brittle, as there would no compile-time reporting if something that should be a LinkTimeIf falls back to a regular If.

I believe the more Scala.js way to do this would be a dedicated primitive in scala.scalajs.LinkingInfo. Something that we would write

LinkingInfo.linkTimeIf(LinkingInfo.isWebAssembly) {
  ifTrue
} {
  ifFalse
}

@tanishiking
Copy link
Contributor Author

tanishiking commented Jun 11, 2024

I believe the more Scala.js way to do this would be a dedicated primitive in scala.scalajs.LinkingInfo. Something that we would write

Thanks! That looks nicer 👍

Indeed, converting if to LinkTimeIf based on it's condition has the problem that, it is not clear that if-expr whether it will be evaluated at link time or not, at a glance (because it depends on whether the symbol in the condition is annotated with @LinkTime or not).
Providing an API like LinkingInfo.linkTimeIf seems better, since it becomes clear at a glance that it will be resolved at link time

tanishiking added a commit to tanishiking/scala-js that referenced this issue Jun 20, 2024
scala-js#4997

This commit introduces linktime dispatching with a new `LinkTimeIf` IR node.
The condition of `LinkTimeIf` will be evaluated at link-time and the dead
branch be eliminated at link-time by Optimizer or linker backend.

For example,

```scala
import scala.scalajs.LikningInfo._

val env = linkTimeIf(productionMode) {
  "prod"
} {
  "dev"
}
```

The code above under `.withProductionMode(true)` links to the following
at runtime.

```scala
val env = "prod"
```

This feature was originally motivated to allow switching the library
implementation based on whether it targets browser Wasm or
standalone Wasm (see scala-js#4991).
However, it should prove useful for further optimization through link-time
information-based dispatching.

**`LinkTimeIf` IR Node**

This change introduces a new IR node
`LinkTimeIf(cond: LinkTimeCondition, thenp: Tree, elsep: Tree)`,
that represents link-time dispatching.

`LinkTimeCondition` is a condition evaluated at link-time.
Currently, we have only a `Binary` class under `LinkTimeCondition`,
representing a simple binary operation that evaluates to a boolean value.
`Binary` does not allow nesting the condition.
`LinkTimeCondition` is defined as a `sealed trait` for future extensibility
(maybe we want to define more complex conditions?).

`LinkTimeValue` contains three subclasses: IntConst, BooleanConst, and Property.
Property contains a key to resolve a value at link-time.
`LinkTimeProperties.scala` is responsible for managing and resolving the link-time
value dictionary, which is accessible through `CoreSpec`.

For example, the following `LinkTimeIf` looks up the link-time value whose key is
"scala.scalajs.LinkingInfo.esVersion" and compares it with the integer constant 6.

```scala
LinkTimeIf(
  LinkTimeCondition(
    BinaryOp.Int_>=,
    Property("scala.scalajs.LinkingInfo.esVersion"),
    IntConst(6),
  ),
  thenp,
  elsep
)
```

**`LinkingInfo.linkTimeIf` and `@LinkTime` annotation**

This commit defines a new API to represent link-time dispatching:
`LinkingInfo.linkTimeIf(...) { } { }`, which compiles to the `LinkTimeIf` IR node.
For example, `linkTimeIf(esVersion >= ESVersion.ES2015)` compiles to the
IR above.

Note that only symbols annotated with `@LinkTime` or int/boolean constants
can be used in the condition of `linkTimeIf`.
Currently, `@LinkTime` is private to `scalajs` (users cannot define new link-time values),
and only a predefined set of link-time values are annotated with `@LinkTime`
(`productionMode` and `esVersion` for now).

When `@LinkTime` annotated values are used in `linkTimeIf`, they are translated to
`LinkTimeValue.Property(name)`, where `name` is the fully qualified name of the symbol.

For instance, if `someValue` is annotated with `@LinkTime`, it can be used in `linkTimeIf` like this:

```scala
linkTimeIf(someValue > 42) {
  // code for true branch
} {
  // code for false branch
}
```

This will be compiled to an IR node similar to the previous example, with `Property("fully.qualified.name.someValue")` in the condition.

**LinkTimeProperties to resolve and evaluate LinkTimeCondition/Value**

This commit defines a `LinkTimeProperty` that belongs to the `CoreSpec`
(making it accessible from various linker stages).
It constructs a link-time value dictionary from `Semantics` and `ESFeatures`,
and is responsible for resolving `LinkTimeValue.Property` and evaluating `LinkTimeCondition`.

**Analyzer doesn't follow the dead branch of linkTimeIf**

Now `Analyzer` evaluates the `LinkTimeIf` and follow only the live branch.
For example, under `productionMode = true`, `doSomethingDev` won't be
marked as reachable by `Analyzer`.

```scala
linkTimeIf(productionMode) {
  doSomethingProd()
} {
  doSomethingDev()
}
```

**Eliminate dead branch of LinkTimeIf**
Finally, the optimizer and linker-backends (in case the optimizer is
turned off) eliminate the dead branch of `LinkTimeIf`.
tanishiking added a commit to tanishiking/scala-js that referenced this issue Jun 20, 2024
scala-js#4997

This commit introduces linktime dispatching with a new `LinkTimeIf` IR node.
The condition of `LinkTimeIf` will be evaluated at link-time and the dead
branch be eliminated at link-time by Optimizer or linker backend.

For example,

```scala
import scala.scalajs.LikningInfo._

val env = linkTimeIf(productionMode) {
  "prod"
} {
  "dev"
}
```

The code above under `.withProductionMode(true)` links to the following
at runtime.

```scala
val env = "prod"
```

This feature was originally motivated to allow switching the library
implementation based on whether it targets browser Wasm or
standalone Wasm (see scala-js#4991).
However, it should prove useful for further optimization through link-time
information-based dispatching.

**`LinkTimeIf` IR Node**

This change introduces a new IR node
`LinkTimeIf(cond: LinkTimeCondition, thenp: Tree, elsep: Tree)`,
that represents link-time dispatching.

`LinkTimeCondition` is a condition evaluated at link-time.
Currently, we have only a `Binary` class under `LinkTimeCondition`,
representing a simple binary operation that evaluates to a boolean value.
`Binary` does not allow nesting the condition.
`LinkTimeCondition` is defined as a `sealed trait` for future extensibility
(maybe we want to define more complex conditions?).

`LinkTimeValue` contains three subclasses: IntConst, BooleanConst, and Property.
Property contains a key to resolve a value at link-time.
`LinkTimeProperties.scala` is responsible for managing and resolving the link-time
value dictionary, which is accessible through `CoreSpec`.

For example, the following `LinkTimeIf` looks up the link-time value whose key is
"scala.scalajs.LinkingInfo.esVersion" and compares it with the integer constant 6.

```scala
LinkTimeIf(
  LinkTimeCondition(
    BinaryOp.Int_>=,
    Property("scala.scalajs.LinkingInfo.esVersion"),
    IntConst(6),
  ),
  thenp,
  elsep
)
```

**`LinkingInfo.linkTimeIf` and `@LinkTime` annotation**

This commit defines a new API to represent link-time dispatching:
`LinkingInfo.linkTimeIf(...) { } { }`, which compiles to the `LinkTimeIf` IR node.
For example, `linkTimeIf(esVersion >= ESVersion.ES2015)` compiles to the
IR above.

Note that only symbols annotated with `@LinkTime` or int/boolean constants
can be used in the condition of `linkTimeIf`.
Currently, `@LinkTime` is private to `scalajs` (users cannot define new link-time values),
and only a predefined set of link-time values are annotated with `@LinkTime`
(`productionMode` and `esVersion` for now).

When `@LinkTime` annotated values are used in `linkTimeIf`, they are translated to
`LinkTimeValue.Property(name)`, where `name` is the fully qualified name of the symbol.

For instance, if `someValue` is annotated with `@LinkTime`, it can be used in `linkTimeIf` like this:

```scala
linkTimeIf(someValue > 42) {
  // code for true branch
} {
  // code for false branch
}
```

This will be compiled to an IR node similar to the previous example, with `Property("fully.qualified.name.someValue")` in the condition.

**LinkTimeProperties to resolve and evaluate LinkTimeCondition/Value**

This commit defines a `LinkTimeProperty` that belongs to the `CoreSpec`
(making it accessible from various linker stages).
It constructs a link-time value dictionary from `Semantics` and `ESFeatures`,
and is responsible for resolving `LinkTimeValue.Property` and evaluating `LinkTimeCondition`.

**Analyzer doesn't follow the dead branch of linkTimeIf**

Now `Analyzer` evaluates the `LinkTimeIf` and follow only the live branch.
For example, under `productionMode = true`, `doSomethingDev` won't be
marked as reachable by `Analyzer`.

```scala
linkTimeIf(productionMode) {
  doSomethingProd()
} {
  doSomethingDev()
}
```

**Eliminate dead branch of LinkTimeIf**
Finally, the optimizer and linker-backends (in case the optimizer is
turned off) eliminate the dead branch of `LinkTimeIf`.
tanishiking added a commit to tanishiking/scala-js that referenced this issue Jun 21, 2024
scala-js#4997

This commit introduces linktime dispatching with a new `LinkTimeIf` IR node.
The condition of `LinkTimeIf` will be evaluated at link-time and the dead
branch be eliminated at link-time by Optimizer or linker backend.

For example,

```scala
import scala.scalajs.LikningInfo._

val env = linkTimeIf(productionMode) {
  "prod"
} {
  "dev"
}
```

The code above under `.withProductionMode(true)` links to the following
at runtime.

```scala
val env = "prod"
```

This feature was originally motivated to allow switching the library
implementation based on whether it targets browser Wasm or
standalone Wasm (see scala-js#4991).
However, it should prove useful for further optimization through link-time
information-based dispatching.

**`LinkTimeIf` IR Node**

This change introduces a new IR node
`LinkTimeIf(cond: LinkTimeCondition, thenp: Tree, elsep: Tree)`,
that represents link-time dispatching.

`LinkTimeCondition` is a condition evaluated at link-time.
Currently, we have only a `Binary` class under `LinkTimeCondition`,
representing a simple binary operation that evaluates to a boolean value.
`Binary` does not allow nesting the condition.
`LinkTimeCondition` is defined as a `sealed trait` for future extensibility
(maybe we want to define more complex conditions?).

`LinkTimeValue` contains three subclasses: IntConst, BooleanConst, and Property.
Property contains a key to resolve a value at link-time.
`LinkTimeProperties.scala` is responsible for managing and resolving the link-time
value dictionary, which is accessible through `CoreSpec`.

For example, the following `LinkTimeIf` looks up the link-time value whose key is
"scala.scalajs.LinkingInfo.esVersion" and compares it with the integer constant 6.

```scala
LinkTimeIf(
  LinkTimeCondition(
    BinaryOp.Int_>=,
    Property("scala.scalajs.LinkingInfo.esVersion"),
    IntConst(6),
  ),
  thenp,
  elsep
)
```

**`LinkingInfo.linkTimeIf` and `@LinkTime` annotation**

This commit defines a new API to represent link-time dispatching:
`LinkingInfo.linkTimeIf(...) { } { }`, which compiles to the `LinkTimeIf` IR node.
For example, `linkTimeIf(esVersion >= ESVersion.ES2015)` compiles to the
IR above.

Note that only symbols annotated with `@LinkTime` or int/boolean constants
can be used in the condition of `linkTimeIf`.
Currently, `@LinkTime` is private to `scalajs` (users cannot define new link-time values),
and only a predefined set of link-time values are annotated with `@LinkTime`
(`productionMode` and `esVersion` for now).

When `@LinkTime` annotated values are used in `linkTimeIf`, they are translated to
`LinkTimeValue.Property(name)`, where `name` is the fully qualified name of the symbol.

For instance, if `someValue` is annotated with `@LinkTime`, it can be used in `linkTimeIf` like this:

```scala
linkTimeIf(someValue > 42) {
  // code for true branch
} {
  // code for false branch
}
```

This will be compiled to an IR node similar to the previous example, with `Property("fully.qualified.name.someValue")` in the condition.

**LinkTimeProperties to resolve and evaluate LinkTimeCondition/Value**

This commit defines a `LinkTimeProperty` that belongs to the `CoreSpec`
(making it accessible from various linker stages).
It constructs a link-time value dictionary from `Semantics` and `ESFeatures`,
and is responsible for resolving `LinkTimeValue.Property` and evaluating `LinkTimeCondition`.

**Analyzer doesn't follow the dead branch of linkTimeIf**

Now `Analyzer` evaluates the `LinkTimeIf` and follow only the live branch.
For example, under `productionMode = true`, `doSomethingDev` won't be
marked as reachable by `Analyzer`.

```scala
linkTimeIf(productionMode) {
  doSomethingProd()
} {
  doSomethingDev()
}
```

**Eliminate dead branch of LinkTimeIf**
Finally, the optimizer and linker-backends (in case the optimizer is
turned off) eliminate the dead branch of `LinkTimeIf`.
tanishiking added a commit to tanishiking/scala-js that referenced this issue Jun 21, 2024
scala-js#4997

This commit introduces linktime dispatching with a new `LinkTimeIf` IR node.
The condition of `LinkTimeIf` will be evaluated at link-time and the dead
branch be eliminated at link-time by Optimizer or linker backend.

For example,

```scala
import scala.scalajs.LikningInfo._

val env = linkTimeIf(productionMode) {
  "prod"
} {
  "dev"
}
```

The code above under `.withProductionMode(true)` links to the following
at runtime.

```scala
val env = "prod"
```

This feature was originally motivated to allow switching the library
implementation based on whether it targets browser Wasm or
standalone Wasm (see scala-js#4991).
However, it should prove useful for further optimization through link-time
information-based dispatching.

**`LinkTimeIf` IR Node**

This change introduces a new IR node
`LinkTimeIf(cond: LinkTimeCondition, thenp: Tree, elsep: Tree)`,
that represents link-time dispatching.

`LinkTimeCondition` is a condition evaluated at link-time.
Currently, we have only a `Binary` class under `LinkTimeCondition`,
representing a simple binary operation that evaluates to a boolean value.
`Binary` does not allow nesting the condition.
`LinkTimeCondition` is defined as a `sealed trait` for future extensibility
(maybe we want to define more complex conditions?).

`LinkTimeValue` contains three subclasses: IntConst, BooleanConst, and Property.
Property contains a key to resolve a value at link-time.
`LinkTimeProperties.scala` is responsible for managing and resolving the link-time
value dictionary, which is accessible through `CoreSpec`.

For example, the following `LinkTimeIf` looks up the link-time value whose key is
"scala.scalajs.LinkingInfo.esVersion" and compares it with the integer constant 6.

```scala
LinkTimeIf(
  LinkTimeCondition(
    BinaryOp.Int_>=,
    Property("core/esVersion"),
    IntConst(6),
  ),
  thenp,
  elsep
)
```

**`LinkingInfo.linkTimeIf` and `@linkTimeProperty` annotation**

This commit defines a new API to represent link-time dispatching:
`LinkingInfo.linkTimeIf(...) { } { }`, which compiles to the `LinkTimeIf` IR node.
For example, `linkTimeIf(esVersion >= ESVersion.ES2015)` compiles to the
IR above.

Note that only symbols annotated with `@linkTimeProperty` or int/boolean constants
can be used in the condition of `linkTimeIf`.
Currently, `@linkTimeProperty` is private to `scalajs` (users cannot define new link-time values),
and only a predefined set of link-time values are annotated
(`productionMode` and `esVersion` for now).

When `@linkTimeProperty(name)` annotated values are used in `linkTimeIf`,
they are translated to `LinkTimeValue.Property(name)`.

**LinkTimeProperties to resolve and evaluate LinkTimeCondition/Value**

This commit defines a `LinkTimeProperty` that belongs to the `CoreSpec`
(making it accessible from various linker stages).
It constructs a link-time value dictionary from `Semantics` and `ESFeatures`,
and is responsible for resolving `LinkTimeValue.Property` and evaluating `LinkTimeCondition`.

**Analyzer doesn't follow the dead branch of linkTimeIf**

Now `Analyzer` evaluates the `LinkTimeIf` and follow only the live branch.
For example, under `productionMode = true`, `doSomethingDev` won't be
marked as reachable by `Analyzer`.

```scala
linkTimeIf(productionMode) {
  doSomethingProd()
} {
  doSomethingDev()
}
```

**Eliminate dead branch of LinkTimeIf**
Finally, the optimizer and linker-backends (in case the optimizer is
turned off) eliminate the dead branch of `LinkTimeIf`.
tanishiking added a commit to tanishiking/scala-js that referenced this issue Jun 25, 2024
scala-js#4997

This commit introduces linktime dispatching with a new `LinkTimeIf` IR node.
The condition of `LinkTimeIf` will be evaluated at link-time and the dead
branch be eliminated at link-time by Optimizer or linker backend.

For example,

```scala
import scala.scalajs.LikningInfo._

val env = linkTimeIf(productionMode) {
  "prod"
} {
  "dev"
}
```

The code above under `.withProductionMode(true)` links to the following
at runtime.

```scala
val env = "prod"
```

This feature was originally motivated to allow switching the library
implementation based on whether it targets browser Wasm or
standalone Wasm (see scala-js#4991).
However, it should prove useful for further optimization through link-time
information-based dispatching.

**`LinkTimeIf` IR Node**

This change introduces a new IR node
`LinkTimeIf(cond: LinkTimeTree, thenp: Tree, elsep: Tree)`,
that represents link-time dispatching.

`LinkTimeTree` is a small set of IR tree evaluated at link-time.
Currently, `LinkTimeTree` contains `BinaryOp`, `IntConst`, `BooleanConst`, and `Property`.
- `BinaryOp` representing a simple binary operation that evaluates to a boolean value.
- `IntConst` and `BooleanConst` holds a constant of the type.
- `Property` contains a key to resolve a value at link-time, where `LinkTimeProperties.scala` is responsible for managing and resolving the link-time value dictionary, which is accessible through `CoreSpec`.

For example, the following `LinkTimeIf` looks up the link-time value whose key is
"scala.scalajs.LinkingInfo.esVersion" and compares it with the integer constant 6.

```scala
LinkTimeIf(
  BinaryOp(
    BinaryOp.Int_>=,
    Property("core/esVersion"),
    IntConst(6),
  ),
  thenp,
  elsep
)
```

**`LinkingInfo.linkTimeIf` and `@linkTimeProperty` annotation**

This commit defines a new API to represent link-time dispatching:
`LinkingInfo.linkTimeIf(...) { } { }`, which compiles to the `LinkTimeIf` IR node.
For example, `linkTimeIf(esVersion >= ESVersion.ES2015)` compiles to the
IR above.

Note that only symbols annotated with `@linkTimeProperty` or int/boolean constants
can be used in the condition of `linkTimeIf`.
Currently, `@linkTimeProperty` is private to `scalajs` (users cannot define new link-time values),
and only a predefined set of link-time values are annotated
(`productionMode` and `esVersion` for now).

When `@linkTimeProperty(name)` annotated values are used in `linkTimeIf`,
they are translated to `LinkTimeValue.Property(name)`.

**LinkTimeProperties to resolve and evaluate LinkTimeCondition/Value**

This commit defines a `LinkTimeProperty` that belongs to the `CoreSpec`
(making it accessible from various linker stages).
It constructs a link-time value dictionary from `Semantics` and `ESFeatures`,
and is responsible for resolving `LinkTimeValue.Property` and evaluating `LinkTimeTree`.

**Analyzer doesn't follow the dead branch of linkTimeIf**

Now `Analyzer` evaluates the `LinkTimeIf` and follow only the live branch.
For example, under `productionMode = true`, `doSomethingDev` won't be
marked as reachable by `Analyzer`.

```scala
linkTimeIf(productionMode) {
  doSomethingProd()
} {
  doSomethingDev()
}
```

**Eliminate dead branch of LinkTimeIf**
Finally, the optimizer and linker-backends (in case the optimizer is
turned off) eliminate the dead branch of `LinkTimeIf`.
tanishiking added a commit to tanishiking/scala-js that referenced this issue Jun 25, 2024
scala-js#4997

This commit introduces linktime dispatching with a new `LinkTimeIf` IR node.
The condition of `LinkTimeIf` will be evaluated at link-time and the dead
branch be eliminated at link-time by Optimizer or linker backend.

For example,

```scala
import scala.scalajs.LikningInfo._

val env = linkTimeIf(productionMode) {
  "prod"
} {
  "dev"
}
```

The code above under `.withProductionMode(true)` links to the following
at runtime.

```scala
val env = "prod"
```

This feature was originally motivated to allow switching the library
implementation based on whether it targets browser Wasm or
standalone Wasm (see scala-js#4991).
However, it should prove useful for further optimization through link-time
information-based dispatching.

**`LinkTimeIf` IR Node**

This change introduces a new IR node
`LinkTimeIf(cond: LinkTimeTree, thenp: Tree, elsep: Tree)`,
that represents link-time dispatching.

`LinkTimeTree` is a small set of IR tree evaluated at link-time.
Currently, `LinkTimeTree` contains `BinaryOp`, `IntConst`, `BooleanConst`, and `Property`.
- `BinaryOp` representing a simple binary operation that evaluates to a boolean value.
- `IntConst` and `BooleanConst` holds a constant of the type.
- `Property` contains a key to resolve a value at link-time, where `LinkTimeProperties.scala` is responsible for managing and resolving the link-time value dictionary, which is accessible through `CoreSpec`.

For example, the following `LinkTimeIf` looks up the link-time value whose key is
"scala.scalajs.LinkingInfo.esVersion" and compares it with the integer constant 6.

```scala
LinkTimeIf(
  BinaryOp(
    BinaryOp.Int_>=,
    Property("core/esVersion"),
    IntConst(6),
  ),
  thenp,
  elsep
)
```

**`LinkingInfo.linkTimeIf` and `@linkTimeProperty` annotation**

This commit defines a new API to represent link-time dispatching:
`LinkingInfo.linkTimeIf(...) { } { }`, which compiles to the `LinkTimeIf` IR node.
For example, `linkTimeIf(esVersion >= ESVersion.ES2015)` compiles to the
IR above.

Note that only symbols annotated with `@linkTimeProperty` or int/boolean constants
can be used in the condition of `linkTimeIf`.
Currently, `@linkTimeProperty` is private to `scalajs` (users cannot define new link-time values),
and only a predefined set of link-time values are annotated
(`productionMode` and `esVersion` for now).

When `@linkTimeProperty(name)` annotated values are used in `linkTimeIf`,
they are translated to `LinkTimeValue.Property(name)`.

**LinkTimeProperties to resolve and evaluate LinkTimeCondition/Value**

This commit defines a `LinkTimeProperty` that belongs to the `CoreSpec`
(making it accessible from various linker stages).
It constructs a link-time value dictionary from `Semantics` and `ESFeatures`,
and is responsible for resolving `LinkTimeValue.Property` and evaluating `LinkTimeTree`.

**Analyzer doesn't follow the dead branch of linkTimeIf**

Now `Analyzer` evaluates the `LinkTimeIf` and follow only the live branch.
For example, under `productionMode = true`, `doSomethingDev` won't be
marked as reachable by `Analyzer`.

```scala
linkTimeIf(productionMode) {
  doSomethingProd()
} {
  doSomethingDev()
}
```

**Eliminate dead branch of LinkTimeIf**
Finally, the optimizer and linker-backends (in case the optimizer is
turned off) eliminate the dead branch of `LinkTimeIf`.
tanishiking added a commit to tanishiking/scala-js that referenced this issue Jun 25, 2024
scala-js#4997

This commit introduces linktime dispatching with a new `LinkTimeIf` IR node.
The condition of `LinkTimeIf` will be evaluated at link-time and the dead
branch be eliminated at link-time by Optimizer or linker backend.

For example,

```scala
import scala.scalajs.LikningInfo._

val env = linkTimeIf(productionMode) {
  "prod"
} {
  "dev"
}
```

The code above under `.withProductionMode(true)` links to the following
at runtime.

```scala
val env = "prod"
```

This feature was originally motivated to allow switching the library
implementation based on whether it targets browser Wasm or
standalone Wasm (see scala-js#4991).
However, it should prove useful for further optimization through link-time
information-based dispatching.

**`LinkTimeIf` IR Node**

This change introduces a new IR node
`LinkTimeIf(cond: LinkTimeTree, thenp: Tree, elsep: Tree)`,
that represents link-time dispatching.

`LinkTimeTree` is a small set of IR tree evaluated at link-time.
Currently, `LinkTimeTree` contains `BinaryOp`, `IntConst`, `BooleanConst`, and `Property`.
- `BinaryOp` representing a simple binary operation that evaluates to a boolean value.
- `IntConst` and `BooleanConst` holds a constant of the type.
- `Property` contains a key to resolve a value at link-time, where `LinkTimeProperties.scala` is responsible for managing and resolving the link-time value dictionary, which is accessible through `CoreSpec`.

For example, the following `LinkTimeIf` looks up the link-time value whose key is
"scala.scalajs.LinkingInfo.esVersion" and compares it with the integer constant 6.

```scala
LinkTimeIf(
  BinaryOp(
    BinaryOp.Int_>=,
    Property("core/esVersion"),
    IntConst(6),
  ),
  thenp,
  elsep
)
```

**`LinkingInfo.linkTimeIf` and `@linkTimeProperty` annotation**

This commit defines a new API to represent link-time dispatching:
`LinkingInfo.linkTimeIf(...) { } { }`, which compiles to the `LinkTimeIf` IR node.
For example, `linkTimeIf(esVersion >= ESVersion.ES2015)` compiles to the
IR above.

Note that only symbols annotated with `@linkTimeProperty` or int/boolean constants
can be used in the condition of `linkTimeIf`.
Currently, `@linkTimeProperty` is private to `scalajs` (users cannot define new link-time values),
and only a predefined set of link-time values are annotated
(`productionMode` and `esVersion` for now).

When `@linkTimeProperty(name)` annotated values are used in `linkTimeIf`,
they are translated to `LinkTimeValue.Property(name)`.

**LinkTimeProperties to resolve and evaluate LinkTimeCondition/Value**

This commit defines a `LinkTimeProperty` that belongs to the `CoreSpec`
(making it accessible from various linker stages).
It constructs a link-time value dictionary from `Semantics` and `ESFeatures`,
and is responsible for resolving `LinkTimeValue.Property` and evaluating `LinkTimeTree`.

**Analyzer doesn't follow the dead branch of linkTimeIf**

Now `Analyzer` evaluates the `LinkTimeIf` and follow only the live branch.
For example, under `productionMode = true`, `doSomethingDev` won't be
marked as reachable by `Analyzer`.

```scala
linkTimeIf(productionMode) {
  doSomethingProd()
} {
  doSomethingDev()
}
```

**Eliminate dead branch of LinkTimeIf**
Finally, the optimizer and linker-backends (in case the optimizer is
turned off) eliminate the dead branch of `LinkTimeIf`.
tanishiking added a commit to tanishiking/scala-js that referenced this issue Jun 26, 2024
scala-js#4997

This commit introduces linktime dispatching with a new `LinkTimeIf` IR node.
The condition of `LinkTimeIf` will be evaluated at link-time and the dead
branch be eliminated at link-time by Optimizer or linker backend.

For example,

```scala
import scala.scalajs.LikningInfo._

val env = linkTimeIf(productionMode) {
  "prod"
} {
  "dev"
}
```

The code above under `.withProductionMode(true)` links to the following
at runtime.

```scala
val env = "prod"
```

This feature was originally motivated to allow switching the library
implementation based on whether it targets browser Wasm or
standalone Wasm (see scala-js#4991).
However, it should prove useful for further optimization through link-time
information-based dispatching.

**`LinkTimeIf` IR Node**

This change introduces a new IR node
`LinkTimeIf(cond: LinkTimeTree, thenp: Tree, elsep: Tree)`,
that represents link-time dispatching.

`LinkTimeTree` is a small set of IR tree evaluated at link-time.
Currently, `LinkTimeTree` contains `BinaryOp`, `IntConst`, `BooleanConst`, and `Property`.
- `BinaryOp` representing a simple binary operation that evaluates to a boolean value.
- `IntConst` and `BooleanConst` holds a constant of the type.
- `Property` contains a key to resolve a value at link-time, where `LinkTimeProperties.scala` is responsible for managing and resolving the link-time value dictionary, which is accessible through `CoreSpec`.

For example, the following `LinkTimeIf` looks up the link-time value whose key is
"scala.scalajs.LinkingInfo.esVersion" and compares it with the integer constant 6.

```scala
LinkTimeIf(
  BinaryOp(
    BinaryOp.Int_>=,
    Property("core/esVersion"),
    IntConst(6),
  ),
  thenp,
  elsep
)
```

**`LinkingInfo.linkTimeIf` and `@linkTimeProperty` annotation**

This commit defines a new API to represent link-time dispatching:
`LinkingInfo.linkTimeIf(...) { } { }`, which compiles to the `LinkTimeIf` IR node.
For example, `linkTimeIf(esVersion >= ESVersion.ES2015)` compiles to the
IR above.

Note that only symbols annotated with `@linkTimeProperty` or int/boolean constants
can be used in the condition of `linkTimeIf`.
Currently, `@linkTimeProperty` is private to `scalajs` (users cannot define new link-time values),
and only a predefined set of link-time values are annotated
(`productionMode` and `esVersion` for now).

When `@linkTimeProperty(name)` annotated values are used in `linkTimeIf`,
they are translated to `LinkTimeValue.Property(name)`.

**LinkTimeProperties to resolve and evaluate LinkTimeCondition/Value**

This commit defines a `LinkTimeProperty` that belongs to the `CoreSpec`
(making it accessible from various linker stages).
It constructs a link-time value dictionary from `Semantics` and `ESFeatures`,
and is responsible for resolving `LinkTimeValue.Property` and evaluating `LinkTimeTree`.

**Analyzer doesn't follow the dead branch of linkTimeIf**

Now `Analyzer` evaluates the `LinkTimeIf` and follow only the live branch.
For example, under `productionMode = true`, `doSomethingDev` won't be
marked as reachable by `Analyzer`.

```scala
linkTimeIf(productionMode) {
  doSomethingProd()
} {
  doSomethingDev()
}
```

**Eliminate dead branch of LinkTimeIf**
Finally, the optimizer and linker-backends (in case the optimizer is
turned off) eliminate the dead branch of `LinkTimeIf`.
tanishiking added a commit to tanishiking/scala-js that referenced this issue Jul 15, 2024
scala-js#4997

This commit introduces linktime dispatching with a new `LinkTimeIf` IR node.
The condition of `LinkTimeIf` will be evaluated at link-time and the dead
branch be eliminated at link-time by Optimizer or linker backend.

For example,

```scala
import scala.scalajs.LikningInfo._

val env = linkTimeIf(productionMode) {
  "prod"
} {
  "dev"
}
```

The code above under `.withProductionMode(true)` links to the following
at runtime.

```scala
val env = "prod"
```

This feature was originally motivated to allow switching the library
implementation based on whether it targets browser Wasm or
standalone Wasm (see scala-js#4991).
However, it should prove useful for further optimization through link-time
information-based dispatching.

**`LinkTimeIf` IR Node**

This change introduces a new IR node
`LinkTimeIf(cond: LinkTimeTree, thenp: Tree, elsep: Tree)`,
that represents link-time dispatching.

`LinkTimeTree` is a small set of IR tree evaluated at link-time.
Currently, `LinkTimeTree` contains `BinaryOp`, `IntConst`, `BooleanConst`, and `Property`.
- `BinaryOp` representing a simple binary operation that evaluates to a boolean value.
- `IntConst` and `BooleanConst` holds a constant of the type.
- `Property` contains a key to resolve a value at link-time, where `LinkTimeProperties.scala` is responsible for managing and resolving the link-time value dictionary, which is accessible through `CoreSpec`.

For example, the following `LinkTimeIf` looks up the link-time value whose key is
"scala.scalajs.LinkingInfo.esVersion" and compares it with the integer constant 6.

```scala
LinkTimeIf(
  BinaryOp(
    BinaryOp.Int_>=,
    Property("core/esVersion"),
    IntConst(6),
  ),
  thenp,
  elsep
)
```

**`LinkingInfo.linkTimeIf` and `@linkTimeProperty` annotation**

This commit defines a new API to represent link-time dispatching:
`LinkingInfo.linkTimeIf(...) { } { }`, which compiles to the `LinkTimeIf` IR node.
For example, `linkTimeIf(esVersion >= ESVersion.ES2015)` compiles to the
IR above.

Note that only symbols annotated with `@linkTimeProperty` or int/boolean constants
can be used in the condition of `linkTimeIf`.
Currently, `@linkTimeProperty` is private to `scalajs` (users cannot define new link-time values),
and only a predefined set of link-time values are annotated
(`productionMode` and `esVersion` for now).

When `@linkTimeProperty(name)` annotated values are used in `linkTimeIf`,
they are translated to `LinkTimeValue.Property(name)`.

**LinkTimeProperties to resolve and evaluate LinkTimeCondition/Value**

This commit defines a `LinkTimeProperty` that belongs to the `CoreSpec`
(making it accessible from various linker stages).
It constructs a link-time value dictionary from `Semantics` and `ESFeatures`,
and is responsible for resolving `LinkTimeValue.Property` and evaluating `LinkTimeTree`.

**Analyzer doesn't follow the dead branch of linkTimeIf**

Now `Analyzer` evaluates the `LinkTimeIf` and follow only the live branch.
For example, under `productionMode = true`, `doSomethingDev` won't be
marked as reachable by `Analyzer`.

```scala
linkTimeIf(productionMode) {
  doSomethingProd()
} {
  doSomethingDev()
}
```

**Eliminate dead branch of LinkTimeIf**
Finally, the optimizer and linker-backends (in case the optimizer is
turned off) eliminate the dead branch of `LinkTimeIf`.
tanishiking added a commit to tanishiking/scala-js that referenced this issue Jul 18, 2024
scala-js#4997

This commit introduces linktime dispatching with a new `LinkTimeIf` IR node.
The condition of `LinkTimeIf` will be evaluated at link-time and the dead
branch be eliminated at link-time by Optimizer or linker backend.

For example,

```scala
import scala.scalajs.LikningInfo._

val env = linkTimeIf(productionMode) {
  "prod"
} {
  "dev"
}
```

The code above under `.withProductionMode(true)` links to the following
at runtime.

```scala
val env = "prod"
```

This feature was originally motivated to allow switching the library
implementation based on whether it targets browser Wasm or
standalone Wasm (see scala-js#4991).
However, it should prove useful for further optimization through link-time
information-based dispatching.

**`LinkTimeIf` IR Node**

This change introduces a new IR node
`LinkTimeIf(cond: LinkTimeTree, thenp: Tree, elsep: Tree)`,
that represents link-time dispatching.

`LinkTimeTree` is a small set of IR tree evaluated at link-time.
Currently, `LinkTimeTree` contains `BinaryOp`, `IntConst`, `BooleanConst`, and `Property`.
- `BinaryOp` representing a simple binary operation that evaluates to a boolean value.
- `IntConst` and `BooleanConst` holds a constant of the type.
- `Property` contains a key to resolve a value at link-time, where `LinkTimeProperties.scala` is responsible for managing and resolving the link-time value dictionary, which is accessible through `CoreSpec`.

For example, the following `LinkTimeIf` looks up the link-time value whose key is
"scala.scalajs.LinkingInfo.esVersion" and compares it with the integer constant 6.

```scala
LinkTimeIf(
  BinaryOp(
    BinaryOp.Int_>=,
    Property("core/esVersion"),
    IntConst(6),
  ),
  thenp,
  elsep
)
```

**`LinkingInfo.linkTimeIf` and `@linkTimeProperty` annotation**

This commit defines a new API to represent link-time dispatching:
`LinkingInfo.linkTimeIf(...) { } { }`, which compiles to the `LinkTimeIf` IR node.
For example, `linkTimeIf(esVersion >= ESVersion.ES2015)` compiles to the
IR above.

Note that only symbols annotated with `@linkTimeProperty` or int/boolean constants
can be used in the condition of `linkTimeIf`.
Currently, `@linkTimeProperty` is private to `scalajs` (users cannot define new link-time values),
and only a predefined set of link-time values are annotated
(`productionMode` and `esVersion` for now).

When `@linkTimeProperty(name)` annotated values are used in `linkTimeIf`,
they are translated to `LinkTimeValue.Property(name)`.

**LinkTimeProperties to resolve and evaluate LinkTimeCondition/Value**

This commit defines a `LinkTimeProperty` that belongs to the `CoreSpec`
(making it accessible from various linker stages).
It constructs a link-time value dictionary from `Semantics` and `ESFeatures`,
and is responsible for resolving `LinkTimeValue.Property` and evaluating `LinkTimeTree`.

**Analyzer doesn't follow the dead branch of linkTimeIf**

Now `Analyzer` evaluates the `LinkTimeIf` and follow only the live branch.
For example, under `productionMode = true`, `doSomethingDev` won't be
marked as reachable by `Analyzer`.

```scala
linkTimeIf(productionMode) {
  doSomethingProd()
} {
  doSomethingDev()
}
```

**Eliminate dead branch of LinkTimeIf**
Finally, the optimizer and linker-backends (in case the optimizer is
turned off) eliminate the dead branch of `LinkTimeIf`.
@gzm0 gzm0 added the language Affects language semantics. label Aug 10, 2024
tanishiking added a commit to tanishiking/scala-js that referenced this issue Aug 20, 2024
scala-js#4997

This commit introduces linktime dispatching with a new `LinkTimeIf` IR node.
The condition of `LinkTimeIf` will be evaluated at link-time and the dead
branch be eliminated at link-time by Optimizer or linker backend.

For example,

```scala
import scala.scalajs.LikningInfo._

val env = linkTimeIf(productionMode) {
  "prod"
} {
  "dev"
}
```

The code above under `.withProductionMode(true)` links to the following
at runtime.

```scala
val env = "prod"
```

This feature was originally motivated to allow switching the library
implementation based on whether it targets browser Wasm or
standalone Wasm (see scala-js#4991).
However, it should prove useful for further optimization through link-time
information-based dispatching.

**`LinkTimeIf` IR Node**

This change introduces a new IR node
`LinkTimeIf(cond: LinkTimeTree, thenp: Tree, elsep: Tree)`,
that represents link-time dispatching.

`LinkTimeTree` is a small set of IR tree evaluated at link-time.
Currently, `LinkTimeTree` contains `BinaryOp`, `IntConst`, `BooleanConst`, and `Property`.
- `BinaryOp` representing a simple binary operation that evaluates to a boolean value.
- `IntConst` and `BooleanConst` holds a constant of the type.
- `Property` contains a key to resolve a value at link-time, where `LinkTimeProperties.scala` is responsible for managing and resolving the link-time value dictionary, which is accessible through `CoreSpec`.

For example, the following `LinkTimeIf` looks up the link-time value whose key is
"scala.scalajs.LinkingInfo.esVersion" and compares it with the integer constant 6.

```scala
LinkTimeIf(
  BinaryOp(
    BinaryOp.Int_>=,
    Property("core/esVersion"),
    IntConst(6),
  ),
  thenp,
  elsep
)
```

**`LinkingInfo.linkTimeIf` and `@linkTimeProperty` annotation**

This commit defines a new API to represent link-time dispatching:
`LinkingInfo.linkTimeIf(...) { } { }`, which compiles to the `LinkTimeIf` IR node.
For example, `linkTimeIf(esVersion >= ESVersion.ES2015)` compiles to the
IR above.

Note that only symbols annotated with `@linkTimeProperty` or int/boolean constants
can be used in the condition of `linkTimeIf`.
Currently, `@linkTimeProperty` is private to `scalajs` (users cannot define new link-time values),
and only a predefined set of link-time values are annotated
(`productionMode` and `esVersion` for now).

When `@linkTimeProperty(name)` annotated values are used in `linkTimeIf`,
they are translated to `LinkTimeValue.Property(name)`.

**LinkTimeProperties to resolve and evaluate LinkTimeCondition/Value**

This commit defines a `LinkTimeProperty` that belongs to the `CoreSpec`
(making it accessible from various linker stages).
It constructs a link-time value dictionary from `Semantics` and `ESFeatures`,
and is responsible for resolving `LinkTimeValue.Property` and evaluating `LinkTimeTree`.

**Analyzer doesn't follow the dead branch of linkTimeIf**

Now `Analyzer` evaluates the `LinkTimeIf` and follow only the live branch.
For example, under `productionMode = true`, `doSomethingDev` won't be
marked as reachable by `Analyzer`.

```scala
linkTimeIf(productionMode) {
  doSomethingProd()
} {
  doSomethingDev()
}
```

**Eliminate dead branch of LinkTimeIf**
Finally, the optimizer and linker-backends (in case the optimizer is
turned off) eliminate the dead branch of `LinkTimeIf`.
tanishiking added a commit to tanishiking/scala-js that referenced this issue Aug 20, 2024
scala-js#4997

This commit introduces linktime dispatching with a new `LinkTimeIf` IR node.
The condition of `LinkTimeIf` will be evaluated at link-time and the dead
branch be eliminated at link-time by Optimizer or linker backend.

For example,

```scala
import scala.scalajs.LikningInfo._

val env = linkTimeIf(productionMode) {
  "prod"
} {
  "dev"
}
```

The code above under `.withProductionMode(true)` links to the following
at runtime.

```scala
val env = "prod"
```

This feature was originally motivated to allow switching the library
implementation based on whether it targets browser Wasm or
standalone Wasm (see scala-js#4991).
However, it should prove useful for further optimization through link-time
information-based dispatching.

**`LinkTimeIf` IR Node**

This change introduces a new IR node
`LinkTimeIf(cond: LinkTimeTree, thenp: Tree, elsep: Tree)`,
that represents link-time dispatching.

`LinkTimeTree` is a small set of IR tree evaluated at link-time.
Currently, `LinkTimeTree` contains `BinaryOp`, `IntConst`, `BooleanConst`, and `Property`.
- `BinaryOp` representing a simple binary operation that evaluates to a boolean value.
- `IntConst` and `BooleanConst` holds a constant of the type.
- `Property` contains a key to resolve a value at link-time, where `LinkTimeProperties.scala` is responsible for managing and resolving the link-time value dictionary, which is accessible through `CoreSpec`.

For example, the following `LinkTimeIf` looks up the link-time value whose key is
"scala.scalajs.LinkingInfo.esVersion" and compares it with the integer constant 6.

```scala
LinkTimeIf(
  BinaryOp(
    BinaryOp.Int_>=,
    Property("core/esVersion"),
    IntConst(6),
  ),
  thenp,
  elsep
)
```

**`LinkingInfo.linkTimeIf` and `@linkTimeProperty` annotation**

This commit defines a new API to represent link-time dispatching:
`LinkingInfo.linkTimeIf(...) { } { }`, which compiles to the `LinkTimeIf` IR node.
For example, `linkTimeIf(esVersion >= ESVersion.ES2015)` compiles to the
IR above.

Note that only symbols annotated with `@linkTimeProperty` or int/boolean constants
can be used in the condition of `linkTimeIf`.
Currently, `@linkTimeProperty` is private to `scalajs` (users cannot define new link-time values),
and only a predefined set of link-time values are annotated
(`productionMode` and `esVersion` for now).

When `@linkTimeProperty(name)` annotated values are used in `linkTimeIf`,
they are translated to `LinkTimeValue.Property(name)`.

**LinkTimeProperties to resolve and evaluate LinkTimeCondition/Value**

This commit defines a `LinkTimeProperty` that belongs to the `CoreSpec`
(making it accessible from various linker stages).
It constructs a link-time value dictionary from `Semantics` and `ESFeatures`,
and is responsible for resolving `LinkTimeValue.Property` and evaluating `LinkTimeTree`.

**Analyzer doesn't follow the dead branch of linkTimeIf**

Now `Analyzer` evaluates the `LinkTimeIf` and follow only the live branch.
For example, under `productionMode = true`, `doSomethingDev` won't be
marked as reachable by `Analyzer`.

```scala
linkTimeIf(productionMode) {
  doSomethingProd()
} {
  doSomethingDev()
}
```

**Eliminate dead branch of LinkTimeIf**
Finally, the optimizer and linker-backends (in case the optimizer is
turned off) eliminate the dead branch of `LinkTimeIf`.
tanishiking added a commit to scala-wasm/scala-wasm that referenced this issue Aug 23, 2024
scala-js#4997

This commit introduces linktime dispatching with a new `LinkTimeIf` IR node.
The condition of `LinkTimeIf` will be evaluated at link-time and the dead
branch be eliminated at link-time by Optimizer or linker backend.

For example,

```scala
import scala.scalajs.LikningInfo._

val env = linkTimeIf(productionMode) {
  "prod"
} {
  "dev"
}
```

The code above under `.withProductionMode(true)` links to the following
at runtime.

```scala
val env = "prod"
```

This feature was originally motivated to allow switching the library
implementation based on whether it targets browser Wasm or
standalone Wasm (see scala-js#4991).
However, it should prove useful for further optimization through link-time
information-based dispatching.

**`LinkTimeIf` IR Node**

This change introduces a new IR node
`LinkTimeIf(cond: LinkTimeTree, thenp: Tree, elsep: Tree)`,
that represents link-time dispatching.

`LinkTimeTree` is a small set of IR tree evaluated at link-time.
Currently, `LinkTimeTree` contains `BinaryOp`, `IntConst`, `BooleanConst`, and `Property`.
- `BinaryOp` representing a simple binary operation that evaluates to a boolean value.
- `IntConst` and `BooleanConst` holds a constant of the type.
- `Property` contains a key to resolve a value at link-time, where `LinkTimeProperties.scala` is responsible for managing and resolving the link-time value dictionary, which is accessible through `CoreSpec`.

For example, the following `LinkTimeIf` looks up the link-time value whose key is
"scala.scalajs.LinkingInfo.esVersion" and compares it with the integer constant 6.

```scala
LinkTimeIf(
  BinaryOp(
    BinaryOp.Int_>=,
    Property("core/esVersion"),
    IntConst(6),
  ),
  thenp,
  elsep
)
```

**`LinkingInfo.linkTimeIf` and `@linkTimeProperty` annotation**

This commit defines a new API to represent link-time dispatching:
`LinkingInfo.linkTimeIf(...) { } { }`, which compiles to the `LinkTimeIf` IR node.
For example, `linkTimeIf(esVersion >= ESVersion.ES2015)` compiles to the
IR above.

Note that only symbols annotated with `@linkTimeProperty` or int/boolean constants
can be used in the condition of `linkTimeIf`.
Currently, `@linkTimeProperty` is private to `scalajs` (users cannot define new link-time values),
and only a predefined set of link-time values are annotated
(`productionMode` and `esVersion` for now).

When `@linkTimeProperty(name)` annotated values are used in `linkTimeIf`,
they are translated to `LinkTimeValue.Property(name)`.

**LinkTimeProperties to resolve and evaluate LinkTimeCondition/Value**

This commit defines a `LinkTimeProperty` that belongs to the `CoreSpec`
(making it accessible from various linker stages).
It constructs a link-time value dictionary from `Semantics` and `ESFeatures`,
and is responsible for resolving `LinkTimeValue.Property` and evaluating `LinkTimeTree`.

**Analyzer doesn't follow the dead branch of linkTimeIf**

Now `Analyzer` evaluates the `LinkTimeIf` and follow only the live branch.
For example, under `productionMode = true`, `doSomethingDev` won't be
marked as reachable by `Analyzer`.

```scala
linkTimeIf(productionMode) {
  doSomethingProd()
} {
  doSomethingDev()
}
```

**Eliminate dead branch of LinkTimeIf**
Finally, the optimizer and linker-backends (in case the optimizer is
turned off) eliminate the dead branch of `LinkTimeIf`.
tanishiking added a commit to tanishiking/scala-js that referenced this issue Aug 29, 2024
scala-js#4997

This commit introduces linktime dispatching with a new `LinkTimeIf` IR node.
The condition of `LinkTimeIf` will be evaluated at link-time and the dead
branch be eliminated at link-time by Optimizer or linker backend.

For example,

```scala
import scala.scalajs.LikningInfo._

val env = linkTimeIf(productionMode) {
  "prod"
} {
  "dev"
}
```

The code above under `.withProductionMode(true)` links to the following
at runtime.

```scala
val env = "prod"
```

This feature was originally motivated to allow switching the library
implementation based on whether it targets browser Wasm or
standalone Wasm (see scala-js#4991).
However, it should prove useful for further optimization through link-time
information-based dispatching.

**`LinkTimeIf` IR Node**

This change introduces a new IR node
`LinkTimeIf(cond: LinkTimeTree, thenp: Tree, elsep: Tree)`,
that represents link-time dispatching.

`LinkTimeTree` is a small set of IR tree evaluated at link-time.
Currently, `LinkTimeTree` contains `BinaryOp`, `IntConst`, `BooleanConst`, and `Property`.
- `BinaryOp` representing a simple binary operation that evaluates to a boolean value.
- `IntConst` and `BooleanConst` holds a constant of the type.
- `Property` contains a key to resolve a value at link-time, where `LinkTimeProperties.scala` is responsible for managing and resolving the link-time value dictionary, which is accessible through `CoreSpec`.

For example, the following `LinkTimeIf` looks up the link-time value whose key is
"scala.scalajs.LinkingInfo.esVersion" and compares it with the integer constant 6.

```scala
LinkTimeIf(
  BinaryOp(
    BinaryOp.Int_>=,
    Property("core/esVersion"),
    IntConst(6),
  ),
  thenp,
  elsep
)
```

**`LinkingInfo.linkTimeIf` and `@linkTimeProperty` annotation**

This commit defines a new API to represent link-time dispatching:
`LinkingInfo.linkTimeIf(...) { } { }`, which compiles to the `LinkTimeIf` IR node.
For example, `linkTimeIf(esVersion >= ESVersion.ES2015)` compiles to the
IR above.

Note that only symbols annotated with `@linkTimeProperty` or int/boolean constants
can be used in the condition of `linkTimeIf`.
Currently, `@linkTimeProperty` is private to `scalajs` (users cannot define new link-time values),
and only a predefined set of link-time values are annotated
(`productionMode` and `esVersion` for now).

When `@linkTimeProperty(name)` annotated values are used in `linkTimeIf`,
they are translated to `LinkTimeValue.Property(name)`.

**LinkTimeProperties to resolve and evaluate LinkTimeCondition/Value**

This commit defines a `LinkTimeProperty` that belongs to the `CoreSpec`
(making it accessible from various linker stages).
It constructs a link-time value dictionary from `Semantics` and `ESFeatures`,
and is responsible for resolving `LinkTimeValue.Property` and evaluating `LinkTimeTree`.

**Analyzer doesn't follow the dead branch of linkTimeIf**

Now `Analyzer` evaluates the `LinkTimeIf` and follow only the live branch.
For example, under `productionMode = true`, `doSomethingDev` won't be
marked as reachable by `Analyzer`.

```scala
linkTimeIf(productionMode) {
  doSomethingProd()
} {
  doSomethingDev()
}
```

**Eliminate dead branch of LinkTimeIf**
Finally, the optimizer and linker-backends (in case the optimizer is
turned off) eliminate the dead branch of `LinkTimeIf`.
sjrd added a commit to sjrd/scala-js that referenced this issue Jan 10, 2025
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]>
sjrd added a commit to sjrd/scala-js that referenced this issue Jan 10, 2025
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]>
sjrd added a commit to sjrd/scala-js that referenced this issue Jan 10, 2025
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]>
sjrd added a commit to sjrd/scala-js that referenced this issue Jan 10, 2025
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]>
sjrd added a commit to sjrd/scala-js that referenced this issue Jan 10, 2025
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]>
sjrd added a commit to sjrd/scala-js that referenced this issue Feb 3, 2025
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]>
sjrd added a commit to sjrd/scala-js that referenced this issue Feb 6, 2025
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]>
sjrd added a commit to sjrd/scala-js that referenced this issue Feb 6, 2025
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]>
sjrd added a commit to sjrd/scala-js that referenced this issue Mar 16, 2025
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]>
sjrd added a commit to sjrd/scala-js that referenced this issue Mar 18, 2025
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]>
sjrd added a commit to sjrd/scala-js that referenced this issue Apr 7, 2025
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]>
tanishiking added a commit to scala-wasm/scala-wasm that referenced this issue Apr 13, 2025
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]>
sjrd added a commit to sjrd/scala-js that referenced this issue Apr 22, 2025
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]>
tanishiking added a commit to scala-wasm/scala-wasm that referenced this issue Apr 25, 2025
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]>
tanishiking added a commit to scala-wasm/scala-wasm that referenced this issue Apr 25, 2025
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]>
sjrd added a commit to sjrd/scala-js that referenced this issue Apr 26, 2025
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]>
sjrd added a commit to sjrd/scala-js that referenced this issue Apr 26, 2025
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 add 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]>
sjrd added a commit to sjrd/scala-js that referenced this issue May 7, 2025
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 below 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 access to the
`linkTimeProperties` derived from 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 add 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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
language Affects language semantics.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants