Skip to content

Commit aab4f4a

Browse files
committed
Introduce LinkTimeProperty
ref: #5000 Previously, accessing link-time information via `scala.scalajs.runtime.linkingInfo` required interacting with a JavaScript object. That introduced JS interop in the Wasm backend causing a slowdown, and could hinder the optimization pipeline (In fact, we folded the access to `JSLinkingInfo` into `Literal`s in `OptimizerCore`). `LinkTimeProperty` is a new IR node that is guaranteed to be transformed into a `Literal` at the optimizer or backend stage. We plan to introduce a new primitive, such as `linkTimePropertyXXX`, which will be transformed into a `LinkTimeProperty` in a later commit. Additionally, we will update `scala.scalajs.LinkingInfo` to use `linkTimePropertyXXX` instead of `runtime.linkingInfo`, allowing us to eliminate the JS object when accessing link-time information. This commit also deprecates the `JSLinkingInfo` IR node. For backward compatibility, we introduced a deserialization hack that transforms `JSSelect(JSLinkingInfo(), StringLiteral(...))` into the corresponding `LinkTimeProperty`. An isolated `JSLinkingInfo` will be deserialized into a `JSObjectConstr()` containing the corresponding `LinkTimeProperty` values. Also, this commit introduces validation for `LinkTimeProperty` during reachability analysis. The Analyzer now verifies that the `LinkTimeProperty` in the IR has a valid name and type pair, ensuring it can be resolved using the provided link-time information. If an invalid `LinkTimeProperty` is detected, an error will be recorded in the `Analysis`
1 parent 867f951 commit aab4f4a

33 files changed

+352
-108
lines changed

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

+5
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,11 @@ object Hashers {
550550
mixName(className)
551551
mixTrees(captureValues)
552552

553+
case LinkTimeProperty(name) =>
554+
mixTag(TagLinkTimeProperty)
555+
mixString(name)
556+
mixType(tree.tpe)
557+
553558
case Transient(value) =>
554559
throw new InvalidIRException(tree,
555560
"Cannot hash a transient IR node (its value is of class " +

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

+5
Original file line numberDiff line numberDiff line change
@@ -879,6 +879,11 @@ object Printers {
879879
print(className)
880880
printRow(captureValues, "](", ", ", ")")
881881

882+
case LinkTimeProperty(name) =>
883+
print("linkTimeProperty(")
884+
print(name)
885+
print(")")
886+
882887
// Transient
883888

884889
case Transient(value) =>

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

+46-4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import scala.concurrent._
2424
import Names._
2525
import Position._
2626
import Trees._
27+
import LinkTimeProperty._
2728
import Types._
2829
import Tags._
2930
import Version.Unversioned
@@ -570,6 +571,11 @@ object Serializers {
570571
writeName(className)
571572
writeTrees(captureValues)
572573

574+
case LinkTimeProperty(name) =>
575+
writeTagAndPos(TagLinkTimeProperty)
576+
writeString(name)
577+
writeType(tree.tpe)
578+
573579
case Transient(value) =>
574580
throw new InvalidIRException(tree,
575581
"Cannot serialize a transient IR node (its value is of class " +
@@ -1258,7 +1264,28 @@ object Serializers {
12581264

12591265
case TagJSNew => JSNew(readTree(), readTreeOrJSSpreads())
12601266
case TagJSPrivateSelect => JSPrivateSelect(readTree(), readFieldIdent())
1261-
case TagJSSelect => JSSelect(readTree(), readTree())
1267+
case TagJSSelect =>
1268+
if (hacks.use16 && buf.get(buf.position()) == TagJSLinkingInfo) {
1269+
val jsLinkingInfo = readTree()
1270+
readTree() match {
1271+
case StringLiteral("productionMode") =>
1272+
LinkTimeProperty(ProductionMode)(BooleanType)
1273+
case StringLiteral("esVersion") =>
1274+
LinkTimeProperty(ESVersion)(IntType)
1275+
case StringLiteral("assumingES6") =>
1276+
LinkTimeProperty(UseECMAScript2015Semantics)(BooleanType)
1277+
case StringLiteral("isWebAssembly") =>
1278+
LinkTimeProperty(IsWebAssembly)(BooleanType)
1279+
case StringLiteral("linkerVersion") =>
1280+
LinkTimeProperty(LinkerVersion)(StringType)
1281+
case StringLiteral("fileLevelThis") =>
1282+
JSGlobalRef(JSGlobalRef.FileLevelThis)
1283+
case otherItem =>
1284+
JSSelect(jsLinkingInfo, otherItem)
1285+
}
1286+
} else {
1287+
JSSelect(readTree(), readTree())
1288+
}
12621289
case TagJSFunctionApply => JSFunctionApply(readTree(), readTreeOrJSSpreads())
12631290
case TagJSMethodApply => JSMethodApply(readTree(), readTree(), readTreeOrJSSpreads())
12641291
case TagJSSuperSelect => JSSuperSelect(readTree(), readTree(), readTree())
@@ -1278,8 +1305,20 @@ object Serializers {
12781305
JSObjectConstr(List.fill(readInt())((readTree(), readTree())))
12791306
case TagJSGlobalRef => JSGlobalRef(readString())
12801307
case TagJSTypeOfGlobalRef => JSTypeOfGlobalRef(readTree().asInstanceOf[JSGlobalRef])
1281-
case TagJSLinkingInfo => JSLinkingInfo()
1282-
1308+
case TagJSLinkingInfo =>
1309+
if (hacks.use16) {
1310+
JSObjectConstr(List(
1311+
(StringLiteral("productionMode"), LinkTimeProperty(ProductionMode)(BooleanType)),
1312+
(StringLiteral("esVersion"), LinkTimeProperty(ESVersion)(IntType)),
1313+
(StringLiteral("assumingES6"), LinkTimeProperty(UseECMAScript2015Semantics)(BooleanType)),
1314+
(StringLiteral("isWebAssembly"), LinkTimeProperty(IsWebAssembly)(BooleanType)),
1315+
(StringLiteral("linkerVersion"), LinkTimeProperty(LinkerVersion)(StringType)),
1316+
(StringLiteral("fileLevelThis"), JSGlobalRef(JSGlobalRef.FileLevelThis))
1317+
))
1318+
} else {
1319+
throw new IOException(
1320+
s"Found invalid pre 1.16 JSLinkingInfo def at ${pos}")
1321+
}
12831322
case TagUndefined => Undefined()
12841323
case TagNull => Null()
12851324
case TagBooleanLiteral => BooleanLiteral(readBoolean())
@@ -1321,6 +1360,9 @@ object Serializers {
13211360

13221361
case TagCreateJSClass =>
13231362
CreateJSClass(readClassName(), readTrees())
1363+
1364+
case TagLinkTimeProperty =>
1365+
LinkTimeProperty(readString())(readType())
13241366
}
13251367
}
13261368

@@ -2150,7 +2192,7 @@ object Serializers {
21502192
assert(sourceVersion != "1.14", "source version 1.14 does not exist")
21512193
assert(sourceVersion != "1.15", "source version 1.15 does not exist")
21522194

2153-
val use16: Boolean = use13 || sourceVersion == "1.16"
2195+
val use16: Boolean = use13 || sourceVersion == "1.16" || sourceVersion == "1.17-SNAPSHOT"
21542196
}
21552197

21562198
/** Names needed for hacks. */

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

+3
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ private[ir] object Tags {
127127
final val TagWrapAsThrowable = TagJSNewTarget + 1
128128
final val TagUnwrapFromThrowable = TagWrapAsThrowable + 1
129129

130+
// New in 1.17
131+
final val TagLinkTimeProperty = TagUnwrapFromThrowable + 1
132+
130133
// Tags for member defs
131134

132135
final val TagFieldDef = 1

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ object Transformers {
223223
case _:Skip | _:Debugger | _:LoadModule | _:StoreModule |
224224
_:SelectStatic | _:SelectJSNativeMember | _:LoadJSConstructor |
225225
_:LoadJSModule | _:JSNewTarget | _:JSImportMeta | _:JSLinkingInfo |
226-
_:Literal | _:VarRef | _:This | _:JSGlobalRef =>
226+
_:Literal | _:VarRef | _:This | _:JSGlobalRef | _:LinkTimeProperty =>
227227
tree
228228
}
229229
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ object Traversers {
222222
case _:Skip | _:Debugger | _:LoadModule | _:StoreModule |
223223
_:SelectStatic | _:SelectJSNativeMember | _:LoadJSConstructor |
224224
_:LoadJSModule | _:JSNewTarget | _:JSImportMeta | _:JSLinkingInfo |
225-
_:Literal | _:VarRef | _:This | _:JSGlobalRef =>
225+
_:Literal | _:VarRef | _:This | _:JSGlobalRef | _:LinkTimeProperty =>
226226
}
227227

228228
def traverseClassDef(tree: ClassDef): Unit = {

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

+25-1
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,7 @@ object Trees {
857857

858858
val tpe = AnyType
859859

860-
require(isValidJSGlobalRefName(name),
860+
require(isValidJSGlobalRefName(name) || name == FileLevelThis,
861861
s"`$name` is not a valid global ref name")
862862
}
863863

@@ -897,6 +897,17 @@ object Trees {
897897
*/
898898
def isValidJSGlobalRefName(name: String): Boolean =
899899
isJSIdentifierName(name) && !ReservedJSIdentifierNames.contains(name)
900+
901+
902+
/** The JavaScript value that is an alias to `this`
903+
* at the top-level of the generated file.
904+
*
905+
* `scala.scalajs.js.special.fileLevelThis` should be compiled to
906+
* `JSGlobalRef("this")`, and the backends are expected to
907+
* - Generate `const fileLevelThis = this` at the top-level of the JS file.
908+
* - Transform `JSGlobalRef("this")` to reference to a variable `fileLevelThis`.
909+
*/
910+
final val FileLevelThis = "this"
900911
}
901912

902913
sealed case class JSTypeOfGlobalRef(globalRef: JSGlobalRef)(
@@ -1040,6 +1051,19 @@ object Trees {
10401051
val tpe = AnyType
10411052
}
10421053

1054+
sealed case class LinkTimeProperty(name: String)(val tpe: Type)(
1055+
implicit val pos: Position)
1056+
extends Tree
1057+
1058+
1059+
object LinkTimeProperty {
1060+
final val ProductionMode = "core/productionMode"
1061+
final val ESVersion = "core/esVersion"
1062+
final val UseECMAScript2015Semantics = "core/useECMAScript2015Semantics"
1063+
final val IsWebAssembly = "core/isWebAssembly"
1064+
final val LinkerVersion = "core/linkerVersion"
1065+
}
1066+
10431067
// Transient, a special one
10441068

10451069
/** A transient node for custom purposes.

ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala

+8
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,14 @@ class PrintersTest {
920920
CreateJSClass("Foo", List(ref("x", IntType), ref("y", AnyType))))
921921
}
922922

923+
@Test def printLinkTimeProperty(): Unit = {
924+
assertPrintEquals(
925+
"""
926+
|linkTimeProperty(foo)
927+
""",
928+
LinkTimeProperty("foo")(StringType))
929+
}
930+
923931
@Test def printTransient(): Unit = {
924932
class MyTransient(expr: Tree) extends Transient.Value {
925933
val tpe: Type = AnyType

linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analysis.scala

+5-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import org.scalajs.linker.standard.ModuleSet.ModuleID
2222

2323
import org.scalajs.ir.ClassKind
2424
import org.scalajs.ir.Names._
25-
import org.scalajs.ir.Trees.MemberNamespace
25+
import org.scalajs.ir.Trees.{MemberNamespace, LinkTimeProperty}
2626
import org.scalajs.ir.Types._
2727

2828
/** Reachability graph produced by the [[Analyzer]].
@@ -206,6 +206,8 @@ object Analysis {
206206

207207
final case class ExponentOperatorWithoutES2016Support(from: From) extends Error
208208

209+
final case class InvalidLinkTimeProperty(linkTimeProperty: LinkTimeProperty, from: From) extends Error
210+
209211
sealed trait From
210212
final case class FromMethod(methodInfo: MethodInfo) extends From
211213
final case class FromDispatch(classInfo: ClassInfo, methodName: MethodName) extends From
@@ -262,6 +264,8 @@ object Analysis {
262264
"Uses import.meta with a module kind other than ESModule"
263265
case ExponentOperatorWithoutES2016Support(_) =>
264266
"Uses the ** operator with an ECMAScript version older than ES 2016"
267+
case InvalidLinkTimeProperty(prop, _) =>
268+
s"Uses invalid link-time property ${prop.name} of type ${prop.tpe}"
265269
}
266270

267271
logger.log(level, headMsg)

linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala

+6-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ final class Analyzer(config: CommonPhaseConfig, initial: Boolean,
4949
new InfoLoader(irLoader,
5050
if (!checkIR) InfoLoader.NoIRCheck
5151
else if (initial) InfoLoader.InitialIRCheck
52-
else InfoLoader.InternalIRCheck
52+
else InfoLoader.InternalIRCheck,
53+
config.coreSpec.linkTimeProperties
5354
)
5455
}
5556

@@ -1480,6 +1481,10 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean,
14801481
_errors ::= ExponentOperatorWithoutES2016Support(from)
14811482
}
14821483
}
1484+
1485+
for (prop <- data.invalidLinkTimeProperties) {
1486+
_errors ::= InvalidLinkTimeProperty(prop, from)
1487+
}
14831488
}
14841489

14851490
@tailrec

linker/shared/src/main/scala/org/scalajs/linker/analyzer/InfoLoader.scala

+22-16
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ import org.scalajs.logging._
2525
import org.scalajs.linker.checker.ClassDefChecker
2626
import org.scalajs.linker.frontend.IRLoader
2727
import org.scalajs.linker.interface.LinkingException
28+
import org.scalajs.linker.standard.LinkTimeProperties
2829
import org.scalajs.linker.CollectionsCompat.MutableMapCompatOps
2930

3031
import Platform.emptyThreadSafeMap
3132

32-
private[analyzer] final class InfoLoader(irLoader: IRLoader, irCheckMode: InfoLoader.IRCheckMode) {
33+
private[analyzer] final class InfoLoader(irLoader: IRLoader, irCheckMode: InfoLoader.IRCheckMode,
34+
linkTimeProperties: LinkTimeProperties) {
3335
private var logger: Logger = _
3436
private val cache = emptyThreadSafeMap[ClassName, InfoLoader.ClassInfoCache]
3537

@@ -45,7 +47,7 @@ private[analyzer] final class InfoLoader(irLoader: IRLoader, irCheckMode: InfoLo
4547
if (irLoader.classExists(className)) {
4648
val infoCache = cache.getOrElseUpdate(className,
4749
new InfoLoader.ClassInfoCache(className, irLoader, irCheckMode))
48-
Some(infoCache.loadInfo(logger))
50+
Some(infoCache.loadInfo(logger, linkTimeProperties))
4951
} else {
5052
None
5153
}
@@ -75,7 +77,8 @@ private[analyzer] object InfoLoader {
7577
private var prevJSCtorInfo: Option[Infos.ReachabilityInfo] = None
7678
private var prevJSMethodPropDefInfos: List[Infos.ReachabilityInfo] = Nil
7779

78-
def loadInfo(logger: Logger)(implicit ec: ExecutionContext): Future[Infos.ClassInfo] = synchronized {
80+
def loadInfo(logger: Logger, linkTimeProperties: LinkTimeProperties)(
81+
implicit ec: ExecutionContext): Future[Infos.ClassInfo] = synchronized {
7982
/* If the cache was already used in this run, the classDef and info are
8083
* already correct, no matter what the versions say.
8184
*/
@@ -108,29 +111,30 @@ private[analyzer] object InfoLoader {
108111
}
109112
}
110113

111-
generateInfos(tree)
114+
generateInfos(tree, linkTimeProperties)
112115
}
113116
}
114117
}
115118

116119
info
117120
}
118121

119-
private def generateInfos(classDef: ClassDef): Infos.ClassInfo = {
122+
private def generateInfos(classDef: ClassDef,
123+
linkTimeProperties: LinkTimeProperties): Infos.ClassInfo = {
120124
val referencedFieldClasses = Infos.genReferencedFieldClasses(classDef.fields)
121125

122-
prevMethodInfos = genMethodInfos(classDef.methods, prevMethodInfos)
123-
prevJSCtorInfo = genJSCtorInfo(classDef.jsConstructor, prevJSCtorInfo)
126+
prevMethodInfos = genMethodInfos(classDef.methods, prevMethodInfos, linkTimeProperties)
127+
prevJSCtorInfo = genJSCtorInfo(classDef.jsConstructor, prevJSCtorInfo, linkTimeProperties)
124128
prevJSMethodPropDefInfos =
125-
genJSMethodPropDefInfos(classDef.jsMethodProps, prevJSMethodPropDefInfos)
129+
genJSMethodPropDefInfos(classDef.jsMethodProps, prevJSMethodPropDefInfos, linkTimeProperties)
126130

127131
val exportedMembers = prevJSCtorInfo.toList ::: prevJSMethodPropDefInfos
128132

129133
/* We do not cache top-level exports, because they're quite rare,
130134
* and usually quite small when they exist.
131135
*/
132136
val topLevelExports = classDef.topLevelExportDefs
133-
.map(Infos.generateTopLevelExportInfo(classDef.name.name, _))
137+
.map(Infos.generateTopLevelExportInfo(classDef.name.name, _, linkTimeProperties))
134138

135139
val jsNativeMembers = classDef.jsNativeMembers
136140
.map(m => m.name.name -> m.jsNativeLoadSpec).toMap
@@ -150,15 +154,15 @@ private[analyzer] object InfoLoader {
150154
}
151155

152156
private def genMethodInfos(methods: List[MethodDef],
153-
prevMethodInfos: MethodInfos): MethodInfos = {
157+
prevMethodInfos: MethodInfos, linkTimeProperties: LinkTimeProperties): MethodInfos = {
154158

155159
val builders = Array.fill(MemberNamespace.Count)(Map.newBuilder[MethodName, Infos.MethodInfo])
156160

157161
methods.foreach { method =>
158162
val info = prevMethodInfos(method.flags.namespace.ordinal)
159163
.get(method.methodName)
160164
.filter(_.version.sameVersion(method.version))
161-
.getOrElse(Infos.generateMethodInfo(method))
165+
.getOrElse(Infos.generateMethodInfo(method, linkTimeProperties))
162166

163167
builders(method.flags.namespace.ordinal) += method.methodName -> info
164168
}
@@ -167,16 +171,18 @@ private[analyzer] object InfoLoader {
167171
}
168172

169173
private def genJSCtorInfo(jsCtor: Option[JSConstructorDef],
170-
prevJSCtorInfo: Option[Infos.ReachabilityInfo]): Option[Infos.ReachabilityInfo] = {
174+
prevJSCtorInfo: Option[Infos.ReachabilityInfo],
175+
linkTimeProperties: LinkTimeProperties): Option[Infos.ReachabilityInfo] = {
171176
jsCtor.map { ctor =>
172177
prevJSCtorInfo
173178
.filter(_.version.sameVersion(ctor.version))
174-
.getOrElse(Infos.generateJSConstructorInfo(ctor))
179+
.getOrElse(Infos.generateJSConstructorInfo(ctor, linkTimeProperties))
175180
}
176181
}
177182

178183
private def genJSMethodPropDefInfos(jsMethodProps: List[JSMethodPropDef],
179-
prevJSMethodPropDefInfos: List[Infos.ReachabilityInfo]): List[Infos.ReachabilityInfo] = {
184+
prevJSMethodPropDefInfos: List[Infos.ReachabilityInfo],
185+
linkTimeProperties: LinkTimeProperties): List[Infos.ReachabilityInfo] = {
180186
/* For JS method and property definitions, we use their index in the list of
181187
* `linkedClass.exportedMembers` as their identity. We cannot use their name
182188
* because the name itself is a `Tree`.
@@ -190,13 +196,13 @@ private[analyzer] object InfoLoader {
190196

191197
if (prevJSMethodPropDefInfos.size != jsMethodProps.size) {
192198
// Regenerate everything.
193-
jsMethodProps.map(Infos.generateJSMethodPropDefInfo(_))
199+
jsMethodProps.map(Infos.generateJSMethodPropDefInfo(_, linkTimeProperties))
194200
} else {
195201
for {
196202
(prevInfo, member) <- prevJSMethodPropDefInfos.zip(jsMethodProps)
197203
} yield {
198204
if (prevInfo.version.sameVersion(member.version)) prevInfo
199-
else Infos.generateJSMethodPropDefInfo(member)
205+
else Infos.generateJSMethodPropDefInfo(member, linkTimeProperties)
200206
}
201207
}
202208
}

0 commit comments

Comments
 (0)