Skip to content

Commit c8755f4

Browse files
committed
Introduce linktime dispatching (LinkTimeIf)
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`.
1 parent 7dc8677 commit c8755f4

File tree

35 files changed

+1328
-74
lines changed

35 files changed

+1328
-74
lines changed

compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5349,6 +5349,18 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
53495349
case UNWRAP_FROM_THROWABLE =>
53505350
// js.special.unwrapFromThrowable(arg)
53515351
js.UnwrapFromThrowable(genArgs1)
5352+
5353+
case LINKTIME_IF =>
5354+
// linkingInfo.linkTimeIf(cond, thenp, elsep)
5355+
assert(args.size == 3,
5356+
s"Expected exactly 3 arguments for JS primitive $code but got " +
5357+
s"${args.size} at $pos")
5358+
val condp = genLinkTimeTree(args(0)).getOrElse {
5359+
js.LinkTimeTree.BooleanConst(true) // dummy
5360+
}
5361+
val thenp = genExpr(args(1))
5362+
val elsep = genExpr(args(2))
5363+
js.LinkTimeIf(condp, thenp, elsep)(toIRType(tree.tpe))
53525364
}
53535365
}
53545366

@@ -6815,6 +6827,117 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
68156827
js.ApplyStatic(js.ApplyFlags.empty, className, method, Nil)(toIRType(sym.tpe))
68166828
}
68176829
}
6830+
6831+
private def genLinkTimeTree(cond: Tree)(
6832+
implicit pos: Position): Option[js.LinkTimeTree] = {
6833+
import js.LinkTimeOp._
6834+
cond match {
6835+
case Literal(Constant(b: Boolean)) =>
6836+
Some(js.LinkTimeTree.BooleanConst(b))
6837+
6838+
case Literal(Constant(i: Int)) =>
6839+
Some(js.LinkTimeTree.IntConst(i))
6840+
6841+
case Literal(_) =>
6842+
reporter.error(cond.pos, s"Invalid literal $cond inside linkTimeIf. " +
6843+
s"Only boolean and int values can be used in linkTimeIf.")
6844+
None
6845+
6846+
case Ident(name) =>
6847+
reporter.error(cond.pos, s"Invalid identifier $name inside linkTimeIf. " +
6848+
s"Only @linkTimeProperty annotated values can be used in linkTimeIf.")
6849+
None
6850+
6851+
// if(!foo()) (...)
6852+
case Apply(Select(Apply(prop, Nil), nme.UNARY_!), Nil) =>
6853+
getLinkTimeProperty(prop).map { p =>
6854+
js.LinkTimeTree.BinaryOp(
6855+
Boolean_==, p, js.LinkTimeTree.BooleanConst(false))
6856+
}.orElse {
6857+
reporter.error(prop.pos, s"Invalid identifier inside linkTimeIf. " +
6858+
s"Only @linkTimeProperty annotated values can be used in linkTimeIf.")
6859+
None
6860+
}
6861+
6862+
// if(foo()) (...)
6863+
case Apply(prop, Nil) =>
6864+
getLinkTimeProperty(prop).orElse {
6865+
reporter.error(prop.pos, s"Invalid identifier inside linkTimeIf. " +
6866+
s"Only @linkTimeProperty annotated values can be used in linkTimeIf.")
6867+
None
6868+
}
6869+
6870+
// if(lhs <comp> rhs) (...)
6871+
case Apply(Select(cond1, comp), List(cond2))
6872+
if comp != nme.ZAND && comp != nme.ZOR =>
6873+
(genLinkTimeTree(cond1), genLinkTimeTree(cond2)) match {
6874+
case (Some(c1), Some(c2)) =>
6875+
(if (c1.tpe == jstpe.IntType) {
6876+
comp match {
6877+
case nme.EQ => Some(Int_==)
6878+
case nme.NE => Some(Int_!=)
6879+
case nme.GT => Some(Int_>)
6880+
case nme.GE => Some(Int_>=)
6881+
case nme.LT => Some(Int_<)
6882+
case nme.LE => Some(Int_<=)
6883+
case _ =>
6884+
reporter.error(cond.pos, s"Invalid operation '$comp' inside linkTimeIf. " +
6885+
"Only '==', '!=', '>', '>=', '<', '<=' " +
6886+
"operations are allowed for integer values in linkTimeIf.")
6887+
None
6888+
}
6889+
} else if (c1.tpe == jstpe.BooleanType) {
6890+
comp match {
6891+
case nme.EQ => Some(Boolean_==)
6892+
case nme.NE => Some(Boolean_!=)
6893+
case _ =>
6894+
reporter.error(cond.pos, s"Invalid operation '$comp' inside linkTimeIf. " +
6895+
s"Only '==' and '!=' operations are allowed for boolean values in linkTimeIf.")
6896+
None
6897+
}
6898+
} else {
6899+
reporter.error(cond.pos, s"Invalid lhs type '${c1.tpe}'")
6900+
None
6901+
}).map { op =>
6902+
js.LinkTimeTree.BinaryOp(op, c1, c2)
6903+
}
6904+
case _ =>
6905+
None
6906+
}
6907+
6908+
// if(cond1 {&&,||} cond2) (...)
6909+
case Apply(Select(cond1, op), List(cond2)) if op == nme.ZAND || op == nme.ZOR =>
6910+
(genLinkTimeTree(cond1), genLinkTimeTree(cond2)) match {
6911+
case (Some(c1), Some(c2)) =>
6912+
val linkTimeOp = (op: @unchecked) match {
6913+
case nme.ZAND => Boolean_&&
6914+
case nme.ZOR => Boolean_||
6915+
}
6916+
Some(js.LinkTimeTree.BinaryOp(linkTimeOp, c1, c2))
6917+
case _ =>
6918+
None
6919+
}
6920+
6921+
case t =>
6922+
reporter.error(t.pos, s"Only @linkTimeProperty annotated values, int and boolean constants, " +
6923+
"and binary operations are allowd in linkTimeIf.")
6924+
None
6925+
}
6926+
}
6927+
}
6928+
6929+
private def getLinkTimeProperty(tree: Tree): Option[js.LinkTimeTree.Property] = {
6930+
if (tree.symbol == null) None
6931+
else {
6932+
tree.symbol.getAnnotation(LinkTimePropertyAnnotation)
6933+
.flatMap(_.args.headOption)
6934+
.flatMap {
6935+
case Literal(Constant(v: String)) =>
6936+
Some(js.LinkTimeTree.Property(
6937+
v, toIRType(tree.symbol.tpe.resultType))(tree.pos))
6938+
case _ => None
6939+
}
6940+
}
68186941
}
68196942

68206943
private lazy val hasNewCollections =

compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ trait JSDefinitions {
7272
lazy val JSGlobalScopeAnnotation = getRequiredClass("scala.scalajs.js.annotation.JSGlobalScope")
7373
lazy val JSOperatorAnnotation = getRequiredClass("scala.scalajs.js.annotation.JSOperator")
7474

75+
lazy val LinkTimePropertyAnnotation = getRequiredClass("scala.scalajs.js.annotation.linkTimeProperty")
76+
7577
lazy val JSImportNamespaceObject = getRequiredModule("scala.scalajs.js.annotation.JSImport.Namespace")
7678

7779
lazy val ExposedJSMemberAnnot = getRequiredClass("scala.scalajs.js.annotation.internal.ExposedJSMember")
@@ -128,6 +130,9 @@ trait JSDefinitions {
128130
lazy val DynamicImportThunkClass = getRequiredClass("scala.scalajs.runtime.DynamicImportThunk")
129131
lazy val DynamicImportThunkClass_apply = getMemberMethod(DynamicImportThunkClass, nme.apply)
130132

133+
lazy val LinkingInfoClass = getRequiredModule("scala.scalajs.LinkingInfo")
134+
lazy val LinkingInfoClass_linkTimeIf = getMemberMethod(LinkingInfoClass, newTermName("linkTimeIf"))
135+
131136
lazy val Tuple2_apply = getMemberMethod(TupleClass(2).companionModule, nme.apply)
132137

133138
// This is a def, since similar symbols (arrayUpdateMethod, etc.) are in runDefinitions

compiler/src/main/scala/org/scalajs/nscplugin/JSPrimitives.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ abstract class JSPrimitives {
7070
final val UNWRAP_FROM_THROWABLE = WRAP_AS_THROWABLE + 1 // js.special.unwrapFromThrowable
7171
final val DEBUGGER = UNWRAP_FROM_THROWABLE + 1 // js.special.debugger
7272

73-
final val LastJSPrimitiveCode = DEBUGGER
73+
final val LINKTIME_IF = DEBUGGER + 1 // LinkingInfo.linkTimeIf
74+
75+
final val LastJSPrimitiveCode = LINKTIME_IF
7476

7577
/** Initialize the map of primitive methods (for GenJSCode) */
7678
def init(): Unit = initWithPrimitives(addPrimitive)
@@ -123,6 +125,8 @@ abstract class JSPrimitives {
123125
addPrimitive(Special_wrapAsThrowable, WRAP_AS_THROWABLE)
124126
addPrimitive(Special_unwrapFromThrowable, UNWRAP_FROM_THROWABLE)
125127
addPrimitive(Special_debugger, DEBUGGER)
128+
129+
addPrimitive(LinkingInfoClass_linkTimeIf, LINKTIME_IF)
126130
}
127131

128132
def isJavaScriptPrimitive(code: Int): Boolean =

0 commit comments

Comments
 (0)