Skip to content

Commit f3b38df

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 9f0d899 commit f3b38df

File tree

32 files changed

+327
-114
lines changed

32 files changed

+327
-114
lines changed

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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -901,6 +901,11 @@ object Printers {
901901
print(className)
902902
printRow(captureValues, "](", ", ", ")")
903903

904+
case LinkTimeProperty(name) =>
905+
print("<linkTimeProperty>(")
906+
print(name)
907+
print(")")
908+
904909
// Transient
905910

906911
case Transient(value) =>

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

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import Names._
2525
import OriginalName.NoOriginalName
2626
import Position._
2727
import Trees._
28+
import LinkTimeProperty.{ProductionMode, ESVersion, UseECMAScript2015Semantics, IsWebAssembly, LinkerVersion}
2829
import Types._
2930
import Tags._
3031
import Version.Unversioned
@@ -572,6 +573,11 @@ object Serializers {
572573
writeName(className)
573574
writeTrees(captureValues)
574575

576+
case LinkTimeProperty(name) =>
577+
writeTagAndPos(TagLinkTimeProperty)
578+
writeString(name)
579+
writeType(tree.tpe)
580+
575581
case Transient(value) =>
576582
throw new InvalidIRException(tree,
577583
"Cannot serialize a transient IR node (its value is of class " +
@@ -1290,9 +1296,32 @@ object Serializers {
12901296
case TagUnwrapFromThrowable =>
12911297
UnwrapFromThrowable(readTree())
12921298

1293-
case TagJSNew => JSNew(readTree(), readTreeOrJSSpreads())
1294-
case TagJSPrivateSelect => JSPrivateSelect(readTree(), readFieldIdent())
1295-
case TagJSSelect => JSSelect(readTree(), readTree())
1299+
case TagJSNew => JSNew(readTree(), readTreeOrJSSpreads())
1300+
case TagJSPrivateSelect => JSPrivateSelect(readTree(), readFieldIdent())
1301+
1302+
case TagJSSelect =>
1303+
if (/* hacks.use17 */ true && buf.get(buf.position()) == TagJSLinkingInfo) { // scalastyle:ignore
1304+
val jsLinkingInfo = readTree()
1305+
readTree() match {
1306+
case StringLiteral("productionMode") =>
1307+
LinkTimeProperty(ProductionMode)(BooleanType)
1308+
case StringLiteral("esVersion") =>
1309+
LinkTimeProperty(ESVersion)(IntType)
1310+
case StringLiteral("assumingES6") =>
1311+
LinkTimeProperty(UseECMAScript2015Semantics)(BooleanType)
1312+
case StringLiteral("isWebAssembly") =>
1313+
LinkTimeProperty(IsWebAssembly)(BooleanType)
1314+
case StringLiteral("linkerVersion") =>
1315+
LinkTimeProperty(LinkerVersion)(StringType)
1316+
case StringLiteral("fileLevelThis") =>
1317+
JSGlobalRef(JSGlobalRef.FileLevelThis)
1318+
case otherItem =>
1319+
JSSelect(jsLinkingInfo, otherItem)
1320+
}
1321+
} else {
1322+
JSSelect(readTree(), readTree())
1323+
}
1324+
12961325
case TagJSFunctionApply => JSFunctionApply(readTree(), readTreeOrJSSpreads())
12971326
case TagJSMethodApply => JSMethodApply(readTree(), readTree(), readTreeOrJSSpreads())
12981327
case TagJSSuperSelect => JSSuperSelect(readTree(), readTree(), readTree())
@@ -1312,7 +1341,21 @@ object Serializers {
13121341
JSObjectConstr(List.fill(readInt())((readTree(), readTree())))
13131342
case TagJSGlobalRef => JSGlobalRef(readString())
13141343
case TagJSTypeOfGlobalRef => JSTypeOfGlobalRef(readTree().asInstanceOf[JSGlobalRef])
1315-
case TagJSLinkingInfo => JSLinkingInfo()
1344+
1345+
case TagJSLinkingInfo =>
1346+
if (/* hacks.use17 */ true) { // scalastyle:ignore
1347+
JSObjectConstr(List(
1348+
(StringLiteral("productionMode"), LinkTimeProperty(ProductionMode)(BooleanType)),
1349+
(StringLiteral("esVersion"), LinkTimeProperty(ESVersion)(IntType)),
1350+
(StringLiteral("assumingES6"), LinkTimeProperty(UseECMAScript2015Semantics)(BooleanType)),
1351+
(StringLiteral("isWebAssembly"), LinkTimeProperty(IsWebAssembly)(BooleanType)),
1352+
(StringLiteral("linkerVersion"), LinkTimeProperty(LinkerVersion)(StringType)),
1353+
(StringLiteral("fileLevelThis"), JSGlobalRef(JSGlobalRef.FileLevelThis))
1354+
))
1355+
} else {
1356+
throw new IOException(
1357+
s"Found invalid pre-1.18 JSLinkingInfo def at ${pos}")
1358+
}
13161359

13171360
case TagUndefined => Undefined()
13181361
case TagNull => Null()
@@ -1355,6 +1398,9 @@ object Serializers {
13551398

13561399
case TagCreateJSClass =>
13571400
CreateJSClass(readClassName(), readTrees())
1401+
1402+
case TagLinkTimeProperty =>
1403+
LinkTimeProperty(readString())(readType())
13581404
}
13591405
}
13601406

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

Lines changed: 3 additions & 0 deletions
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.18
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -966,8 +966,15 @@ object Trees {
966966
* [[isJSIdentifierName]]) *and* it is not reserved (see
967967
* [[ReservedJSIdentifierNames]]).
968968
*/
969-
def isValidJSGlobalRefName(name: String): Boolean =
970-
isJSIdentifierName(name) && !ReservedJSIdentifierNames.contains(name)
969+
def isValidJSGlobalRefName(name: String): Boolean = {
970+
(isJSIdentifierName(name) && !ReservedJSIdentifierNames.contains(name)) ||
971+
name == FileLevelThis
972+
}
973+
974+
/** The JavaScript value that is an alias to `this`
975+
* at the top-level of the generated file.
976+
*/
977+
final val FileLevelThis = "this"
971978
}
972979

973980
sealed case class JSTypeOfGlobalRef(globalRef: JSGlobalRef)(
@@ -1072,6 +1079,18 @@ object Trees {
10721079
val tpe = ClassType(ClassClass, nullable = false)
10731080
}
10741081

1082+
sealed case class LinkTimeProperty(name: String)(val tpe: Type)(
1083+
implicit val pos: Position)
1084+
extends Tree
1085+
1086+
object LinkTimeProperty {
1087+
final val ProductionMode = "core/productionMode"
1088+
final val ESVersion = "core/esVersion"
1089+
final val UseECMAScript2015Semantics = "core/useECMAScript2015Semantics"
1090+
final val IsWebAssembly = "core/isWebAssembly"
1091+
final val LinkerVersion = "core/linkerVersion"
1092+
}
1093+
10751094
// Atomic expressions
10761095

10771096
sealed case class VarRef(ident: LocalIdent)(val tpe: Type)(

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,14 @@ class PrintersTest {
937937
CreateJSClass("Foo", List(ref("x", IntType), ref("y", AnyType))))
938938
}
939939

940+
@Test def printLinkTimeProperty(): Unit = {
941+
assertPrintEquals(
942+
"""
943+
|linkTimeProperty(foo)
944+
""",
945+
LinkTimeProperty("foo")(StringType))
946+
}
947+
940948
@Test def printTransient(): Unit = {
941949
class MyTransient(expr: Tree) extends Transient.Value {
942950
val tpe: Type = AnyType

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,12 @@ object Analysis {
208208

209209
final case class ExponentOperatorWithoutES2016Support(from: From) extends Error
210210

211+
final case class InvalidLinkTimeProperty(
212+
linkTimePropertyName: String,
213+
linkTimePropertyType: Type,
214+
from: From
215+
) extends Error
216+
211217
sealed trait From
212218
final case class FromMethod(methodInfo: MethodInfo) extends From
213219
final case class FromDispatch(classInfo: ClassInfo, methodName: MethodName) extends From
@@ -264,6 +270,8 @@ object Analysis {
264270
"Uses import.meta with a module kind other than ESModule"
265271
case ExponentOperatorWithoutES2016Support(_) =>
266272
"Uses the ** operator with an ECMAScript version older than ES 2016"
273+
case InvalidLinkTimeProperty(name, tpe, _) =>
274+
s"Uses invalid link-time property ${name} of type ${tpe}"
267275
}
268276

269277
logger.log(level, headMsg)

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1478,6 +1478,14 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean,
14781478
_classSuperClassUsed.set(true)
14791479
}
14801480
}
1481+
1482+
if (data.referencedLinkTimeProperties.nonEmpty) {
1483+
for ((name, tpe) <- data.referencedLinkTimeProperties) {
1484+
if (!config.coreSpec.linkTimeProperties.validate(name, tpe)) {
1485+
_errors ::= InvalidLinkTimeProperty(name, tpe, from)
1486+
}
1487+
}
1488+
}
14811489
}
14821490

14831491
@tailrec

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

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,14 @@ object Infos {
7878
val isAbstract: Boolean,
7979
version: Version,
8080
byClass: Array[ReachabilityInfoInClass],
81-
globalFlags: ReachabilityInfo.Flags
82-
) extends ReachabilityInfo(version, byClass, globalFlags)
81+
globalFlags: ReachabilityInfo.Flags,
82+
referencedLinkTimeProperties: Array[(String, Type)]
83+
) extends ReachabilityInfo(version, byClass, globalFlags, referencedLinkTimeProperties)
8384

8485
object MethodInfo {
8586
def apply(isAbstract: Boolean, reachabilityInfo: ReachabilityInfo): MethodInfo = {
8687
import reachabilityInfo._
87-
new MethodInfo(isAbstract, version, byClass, globalFlags)
88+
new MethodInfo(isAbstract, version, byClass, globalFlags, referencedLinkTimeProperties)
8889
}
8990
}
9091

@@ -102,7 +103,8 @@ object Infos {
102103
*/
103104
val version: Version,
104105
val byClass: Array[ReachabilityInfoInClass],
105-
val globalFlags: ReachabilityInfo.Flags
106+
val globalFlags: ReachabilityInfo.Flags,
107+
val referencedLinkTimeProperties: Array[(String, Type)]
106108
)
107109

108110
object ReachabilityInfo {
@@ -196,8 +198,10 @@ object Infos {
196198
}
197199

198200
final class ReachabilityInfoBuilder(version: Version) {
201+
import ReachabilityInfoBuilder._
199202
private val byClass = mutable.Map.empty[ClassName, ReachabilityInfoInClassBuilder]
200203
private var flags: ReachabilityInfo.Flags = 0
204+
private val linkTimeProperties = mutable.ListBuffer.empty[(String, Type)]
201205

202206
private def forClass(cls: ClassName): ReachabilityInfoInClassBuilder =
203207
byClass.getOrElseUpdate(cls, new ReachabilityInfoInClassBuilder(cls))
@@ -390,8 +394,22 @@ object Infos {
390394
def addUsedClassSuperClass(): this.type =
391395
setFlag(ReachabilityInfo.FlagUsedClassSuperClass)
392396

393-
def result(): ReachabilityInfo =
394-
new ReachabilityInfo(version, byClass.valuesIterator.map(_.result()).toArray, flags)
397+
def addReferencedLinkTimeProperty(linkTimeProperty: LinkTimeProperty): this.type = {
398+
linkTimeProperties.append((linkTimeProperty.name, linkTimeProperty.tpe))
399+
this
400+
}
401+
402+
def result(): ReachabilityInfo = {
403+
val referencedLinkTimeProperties =
404+
if (linkTimeProperties.isEmpty) emptyLinkTimePropertyArray
405+
else linkTimeProperties.toArray
406+
new ReachabilityInfo(version, byClass.valuesIterator.map(_.result()).toArray, flags,
407+
referencedLinkTimeProperties)
408+
}
409+
}
410+
411+
object ReachabilityInfoBuilder {
412+
private val emptyLinkTimePropertyArray = new Array[(String, Type)](0)
395413
}
396414

397415
final class ReachabilityInfoInClassBuilder(val className: ClassName) {
@@ -744,6 +762,9 @@ object Infos {
744762
case VarDef(_, _, vtpe, _, _) =>
745763
builder.maybeAddReferencedClass(vtpe)
746764

765+
case linkTimeProperty: LinkTimeProperty =>
766+
builder.addReferencedLinkTimeProperty(linkTimeProperty)
767+
747768
case _ =>
748769
}
749770

linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ClassEmitter.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) {
4040
import sjsGen._
4141
import jsGen._
4242
import config._
43+
import coreSpec._
4344
import nameGen._
4445
import varGen._
4546

linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ private[emitter] object CoreJSLib {
6363
import sjsGen._
6464
import jsGen._
6565
import config._
66+
import coreSpec._
6667
import nameGen._
6768
import varGen._
6869
import esFeatures._
@@ -127,7 +128,7 @@ private[emitter] object CoreJSLib {
127128
}
128129

129130
private def buildPreObjectDefinitions(): List[Tree] = {
130-
defineLinkingInfo() :::
131+
defineFileLevelThis() :::
131132
defineJSBuiltinsSnapshotsAndPolyfills() :::
132133
declareCachedL0() :::
133134
defineCharClass() :::
@@ -154,22 +155,8 @@ private[emitter] object CoreJSLib {
154155
assignCachedL0()
155156
}
156157

157-
private def defineLinkingInfo(): List[Tree] = {
158-
// must be in sync with scala.scalajs.runtime.LinkingInfo
159-
160-
def objectFreeze(tree: Tree): Tree =
161-
Apply(genIdentBracketSelect(ObjectRef, "freeze"), tree :: Nil)
162-
163-
val linkingInfo = objectFreeze(ObjectConstr(List(
164-
str("esVersion") -> int(esVersion.edition),
165-
str("assumingES6") -> bool(useECMAScript2015Semantics), // different name for historical reasons
166-
str("isWebAssembly") -> bool(false),
167-
str("productionMode") -> bool(productionMode),
168-
str("linkerVersion") -> str(ScalaJSVersions.current),
169-
str("fileLevelThis") -> This()
170-
)))
171-
172-
extractWithGlobals(globalVarDef(VarField.linkingInfo, CoreVar, linkingInfo))
158+
private def defineFileLevelThis(): List[Tree] = {
159+
extractWithGlobals(globalVarDef(VarField.fileLevelThis, CoreVar, This()))
173160
}
174161

175162
private def defineJSBuiltinsSnapshotsAndPolyfills(): List[Tree] = {

0 commit comments

Comments
 (0)