Skip to content

Commit d53cbd2

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 d53cbd2

File tree

41 files changed

+1201
-89
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1201
-89
lines changed

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

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5349,6 +5349,16 @@ 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))
5359+
val thenp = genExpr(args(1))
5360+
val elsep = genExpr(args(2))
5361+
js.LinkTimeIf(condp, thenp, elsep)(toIRType(tree.tpe))
53525362
}
53535363
}
53545364

@@ -6815,8 +6825,103 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
68156825
js.ApplyStatic(js.ApplyFlags.empty, className, method, Nil)(toIRType(sym.tpe))
68166826
}
68176827
}
6828+
6829+
private def genLinkTimeTree(cond: Tree)(
6830+
implicit pos: Position): js.LinkTimeTree = {
6831+
import js.LinkTimeOp._
6832+
val dummy = js.LinkTimeTree.Property("dummy", toIRType(cond.tpe))
6833+
cond match {
6834+
case Literal(Constant(b: Boolean)) =>
6835+
js.LinkTimeTree.BooleanConst(b)
6836+
6837+
case Literal(Constant(i: Int)) =>
6838+
js.LinkTimeTree.IntConst(i)
6839+
6840+
case Literal(_) =>
6841+
reporter.error(cond.pos,
6842+
s"Invalid literal $cond inside linkTimeIf. " +
6843+
"Only boolean and int values can be used in linkTimeIf.")
6844+
dummy
6845+
6846+
case Ident(name) =>
6847+
reporter.error(cond.pos,
6848+
s"Invalid identifier $name inside linkTimeIf. " +
6849+
"Only @linkTimeProperty annotated values can be used in linkTimeIf.")
6850+
dummy
6851+
6852+
// !x
6853+
case Apply(Select(t, nme.UNARY_!), Nil) if cond.symbol == definitions.Boolean_not =>
6854+
val lt = genLinkTimeTree(t)
6855+
js.LinkTimeTree.BinaryOp(Boolean_==, lt, js.LinkTimeTree.BooleanConst(false))
6856+
6857+
// if(foo()) (...)
6858+
case Apply(prop, Nil) =>
6859+
getLinkTimeProperty(prop).getOrElse {
6860+
reporter.error(prop.pos,
6861+
s"Invalid identifier inside linkTimeIf. " +
6862+
"Only @linkTimeProperty annotated values can be used in linkTimeIf.")
6863+
dummy
6864+
}
6865+
6866+
// if(lhs <comp> rhs) (...)
6867+
case Apply(Select(cond1, comp), List(cond2)) =>
6868+
val tpe = toIRType(cond.tpe)
6869+
val c1 = genLinkTimeTree(cond1)
6870+
val c2 = genLinkTimeTree(cond2)
6871+
val dummyOp = -1
6872+
val op: Code =
6873+
if (c1.tpe == jstpe.IntType) {
6874+
comp match {
6875+
case nme.EQ => Int_==
6876+
case nme.NE => Int_!=
6877+
case nme.GT => Int_>
6878+
case nme.GE => Int_>=
6879+
case nme.LT => Int_<
6880+
case nme.LE => Int_<=
6881+
case _ =>
6882+
reporter.error(cond.pos,
6883+
s"Invalid operation '$comp' inside linkTimeIf. " +
6884+
"Only '==', '!=', '>', '>=', '<', '<=' " +
6885+
"operations are allowed for integer values in linkTimeIf.")
6886+
dummyOp
6887+
}
6888+
} else if (c1.tpe == jstpe.BooleanType) {
6889+
comp match {
6890+
case nme.EQ => Boolean_==
6891+
case nme.NE => Boolean_!=
6892+
case nme.ZAND => Boolean_&&
6893+
case nme.ZOR => Boolean_||
6894+
case _ =>
6895+
reporter.error(cond.pos,
6896+
s"Invalid operation '$comp' inside linkTimeIf. " +
6897+
"Only '==', '!=', '&&', and '||' operations are allowed for boolean values in linkTimeIf.")
6898+
dummyOp
6899+
}
6900+
} else {
6901+
dummyOp
6902+
}
6903+
if (op == dummyOp) dummy
6904+
else js.LinkTimeTree.BinaryOp(op, c1, c2)
6905+
6906+
case t =>
6907+
reporter.error(t.pos,
6908+
s"Only @linkTimeProperty annotated values, int and boolean constants, " +
6909+
"and binary operations are allowd in linkTimeIf.")
6910+
dummy
6911+
}
6912+
}
68186913
}
68196914

6915+
private def getLinkTimeProperty(tree: Tree): Option[js.LinkTimeTree.Property] = {
6916+
tree.symbol.getAnnotation(LinkTimePropertyAnnotation)
6917+
.flatMap(_.args.headOption)
6918+
.flatMap {
6919+
case Literal(Constant(v: String)) =>
6920+
Some(js.LinkTimeTree.Property(v, toIRType(tree.symbol.tpe.resultType))(tree.pos))
6921+
case _ => None
6922+
}
6923+
}
6924+
68206925
private lazy val hasNewCollections =
68216926
!scala.util.Properties.versionNumberString.startsWith("2.12.")
68226927

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 =
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/*
2+
* Scala.js (https://www.scala-js.org/)
3+
*
4+
* Copyright EPFL.
5+
*
6+
* Licensed under Apache License 2.0
7+
* (https://www.apache.org/licenses/LICENSE-2.0).
8+
*
9+
* See the NOTICE file distributed with this work for
10+
* additional information regarding copyright ownership.
11+
*/
12+
13+
package org.scalajs.nscplugin.test
14+
15+
import util._
16+
17+
import org.junit.Test
18+
import org.junit.Assert._
19+
20+
class LinkTimeIfTest extends TestHelpers {
21+
override def preamble: String = "import scala.scalajs.LinkingInfo._"
22+
23+
// scalastyle:off line.size.limit
24+
@Test
25+
def linkTimeErrorInvalidOp(): Unit = {
26+
"""
27+
object A {
28+
def foo =
29+
linkTimeIf((esVersion + 1) < ESVersion.ES2015) { } { }
30+
}
31+
""" hasErrors
32+
"""
33+
|newSource1.scala:4: error: Invalid operation '$plus' inside linkTimeIf. Only '==', '!=', '>', '>=', '<', '<=' operations are allowed for integer values in linkTimeIf.
34+
| linkTimeIf((esVersion + 1) < ESVersion.ES2015) { } { }
35+
| ^
36+
"""
37+
38+
"""
39+
object A {
40+
def foo =
41+
linkTimeIf(productionMode | true) { } { }
42+
}
43+
""" hasErrors
44+
"""
45+
|newSource1.scala:4: error: Invalid operation '$bar' inside linkTimeIf. Only '==', '!=', '&&', and '||' operations are allowed for boolean values in linkTimeIf.
46+
| linkTimeIf(productionMode | true) { } { }
47+
| ^
48+
"""
49+
}
50+
51+
@Test
52+
def linkTimeErrorInvalidEntities(): Unit = {
53+
"""
54+
object A {
55+
def foo(x: String) = {
56+
val bar = 1
57+
linkTimeIf(bar == 0) { } { }
58+
}
59+
}
60+
""" hasErrors
61+
"""
62+
|newSource1.scala:5: error: Invalid identifier bar inside linkTimeIf. Only @linkTimeProperty annotated values can be used in linkTimeIf.
63+
| linkTimeIf(bar == 0) { } { }
64+
| ^
65+
"""
66+
67+
"""
68+
object A {
69+
def foo(x: String) =
70+
linkTimeIf("foo" == x) { } { }
71+
}
72+
""" hasErrors
73+
"""
74+
|newSource1.scala:4: error: Invalid literal "foo" inside linkTimeIf. Only boolean and int values can be used in linkTimeIf.
75+
| linkTimeIf("foo" == x) { } { }
76+
| ^
77+
|newSource1.scala:4: error: Invalid identifier x inside linkTimeIf. Only @linkTimeProperty annotated values can be used in linkTimeIf.
78+
| linkTimeIf("foo" == x) { } { }
79+
| ^
80+
"""
81+
82+
"""
83+
object A {
84+
def bar = true
85+
def foo(x: String) =
86+
linkTimeIf(bar || !bar) { } { }
87+
}
88+
""" hasErrors
89+
"""
90+
|newSource1.scala:5: error: Invalid identifier inside linkTimeIf. Only @linkTimeProperty annotated values can be used in linkTimeIf.
91+
| linkTimeIf(bar || !bar) { } { }
92+
| ^
93+
|newSource1.scala:5: error: Invalid identifier inside linkTimeIf. Only @linkTimeProperty annotated values can be used in linkTimeIf.
94+
| linkTimeIf(bar || !bar) { } { }
95+
| ^
96+
"""
97+
}
98+
99+
@Test
100+
def linkTimeCondInvalidTree(): Unit = {
101+
"""
102+
object A {
103+
def bar = true
104+
def foo(x: String) =
105+
linkTimeIf(if(bar) true else false) { } { }
106+
}
107+
""" hasErrors
108+
"""
109+
|newSource1.scala:5: error: Only @linkTimeProperty annotated values, int and boolean constants, and binary operations are allowd in linkTimeIf.
110+
| linkTimeIf(if(bar) true else false) { } { }
111+
| ^
112+
"""
113+
}
114+
}

ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,13 @@ object Hashers {
206206
mixTree(elsep)
207207
mixType(tree.tpe)
208208

209+
case LinkTimeIf(cond, thenp, elsep) =>
210+
mixTag(TagLinkTimeIf)
211+
mixLinkTimeTree(cond)
212+
mixTree(thenp)
213+
mixTree(elsep)
214+
mixType(tree.tpe)
215+
209216
case While(cond, body) =>
210217
mixTag(TagWhile)
211218
mixTree(cond)
@@ -699,6 +706,27 @@ object Hashers {
699706
digestStream.writeInt(pos.column)
700707
}
701708

709+
private def mixLinkTimeTree(cond: LinkTimeTree): Unit = {
710+
cond match {
711+
case LinkTimeTree.BinaryOp(op, lhs, rhs) =>
712+
mixTag(TagLinkTimeTreeBinary)
713+
digestStream.writeInt(op)
714+
mixLinkTimeTree(lhs)
715+
mixLinkTimeTree(rhs)
716+
case LinkTimeTree.Property(name, tpe) =>
717+
mixTag(TagLinkTimeProperty)
718+
digestStream.writeUTF(name)
719+
mixType(tpe)
720+
case LinkTimeTree.BooleanConst(v) =>
721+
mixTag(TagLinkTimeBooleanConst)
722+
digestStream.writeBoolean(v)
723+
case LinkTimeTree.IntConst(v) =>
724+
mixTag(TagLinkTimeIntConst)
725+
digestStream.writeInt(v)
726+
}
727+
mixPos(cond.pos)
728+
}
729+
702730
@inline
703731
final def mixTag(tag: Int): Unit = mixInt(tag)
704732

ir/shared/src/main/scala/org/scalajs/ir/Printers.scala

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ object Printers {
134134
case node: MemberDef => print(node)
135135
case node: JSConstructorBody => printBlock(node.allStats)
136136
case node: TopLevelExportDef => print(node)
137+
case node: LinkTimeTree => print(node)
137138
}
138139
}
139140

@@ -218,6 +219,15 @@ object Printers {
218219
printBlock(elsep)
219220
}
220221

222+
case LinkTimeIf(cond, thenp, elsep) =>
223+
print("linkTimeIf (")
224+
print(cond)
225+
print(") ")
226+
227+
printBlock(thenp)
228+
print(" else ")
229+
printBlock(elsep)
230+
221231
case While(cond, body) =>
222232
print("while (")
223233
print(cond)
@@ -1174,6 +1184,38 @@ object Printers {
11741184
}
11751185
}
11761186

1187+
def print(cond: LinkTimeTree): Unit = {
1188+
import LinkTimeOp._
1189+
cond match {
1190+
case LinkTimeTree.BinaryOp(op, lhs, rhs) =>
1191+
print(lhs)
1192+
print(" ")
1193+
print(op match {
1194+
case Boolean_== => "=="
1195+
case Boolean_!= => "!="
1196+
case Boolean_|| => "||"
1197+
case Boolean_&& => "&&"
1198+
1199+
case Int_== => "=="
1200+
case Int_!= => "!="
1201+
case Int_< => "<"
1202+
case Int_<= => "<="
1203+
case Int_> => ">"
1204+
case Int_>= => ">="
1205+
})
1206+
print(" ")
1207+
print(rhs)
1208+
case LinkTimeTree.BooleanConst(v) =>
1209+
if (v) print("true") else print("false")
1210+
case LinkTimeTree.IntConst(v) =>
1211+
print(v.toString)
1212+
case LinkTimeTree.Property(name, _) =>
1213+
print("prop[")
1214+
print(name)
1215+
print("]")
1216+
}
1217+
}
1218+
11771219
def print(s: String): Unit =
11781220
out.write(s)
11791221

0 commit comments

Comments
 (0)