From a233bb27861aae7c029f6299a4a379fc62af2129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 1 Sep 2024 16:52:31 +0200 Subject: [PATCH 1/3] Refactor: Rename `emitter.ArrayClassProperty` to `SyntheticProperty`. In the next commit, we will add `jl.Class.data` as a synthetic property, which we will have to handle in the same way as `ArrayClass` properties. We therefore rename the concept to be more general and include any synthetic property on Scala classes. --- .../linker/backend/emitter/CoreJSLib.scala | 20 ++++++------- .../backend/emitter/FunctionEmitter.scala | 14 +++++----- .../backend/emitter/NameCompressor.scala | 16 +++++------ .../linker/backend/emitter/SJSGen.scala | 16 +++++------ ...Property.scala => SyntheticProperty.scala} | 28 +++++++++++-------- 5 files changed, 50 insertions(+), 44 deletions(-) rename linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/{ArrayClassProperty.scala => SyntheticProperty.scala} (50%) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala index 2c27c125b9..792c1a5677 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala @@ -1126,7 +1126,7 @@ private[emitter] object CoreJSLib { ) ::: condDefs(esVersion >= ESVersion.ES2015 && nullPointers != CheckedBehavior.Unchecked)( defineFunction5(VarField.systemArraycopy) { (src, srcPos, dest, destPos, length) => - genArrayClassPropApply(src, ArrayClassProperty.copyTo, srcPos, dest, destPos, length) + genSyntheticPropApply(src, SyntheticProperty.copyTo, srcPos, dest, destPos, length) } ) ::: @@ -1150,7 +1150,7 @@ private[emitter] object CoreJSLib { srcPos, dest.u.length, destPos, length) }, For(let(i, 0), i < length, i := ((i + 1) | 0), { - genArrayClassPropApply(dest, ArrayClassProperty.set, + genSyntheticPropApply(dest, SyntheticProperty.set, (destPos + i) | 0, BracketSelect(srcArray, (srcPos + i) | 0)) }) ) @@ -1168,7 +1168,7 @@ private[emitter] object CoreJSLib { If(srcData && genIdentBracketSelect(srcData, cpn.isArrayClass), { // Fast path: the values are array of the same type if (esVersion >= ESVersion.ES2015 && nullPointers == CheckedBehavior.Unchecked) - genArrayClassPropApply(src, ArrayClassProperty.copyTo, srcPos, dest, destPos, length) + genSyntheticPropApply(src, SyntheticProperty.copyTo, srcPos, dest, destPos, length) else genCallHelper(VarField.systemArraycopy, src, srcPos, dest, destPos, length) }, { @@ -1440,8 +1440,8 @@ private[emitter] object CoreJSLib { genCallHelper(VarField.throwArrayIndexOutOfBoundsException, i)) } - val getName = genArrayClassPropertyForDef(ArrayClassProperty.get) - val setName = genArrayClassPropertyForDef(ArrayClassProperty.set) + val getName = genSyntheticPropertyForDef(SyntheticProperty.get) + val setName = genSyntheticPropertyForDef(SyntheticProperty.set) List( MethodDef(static = false, getName, paramList(i), None, { @@ -1464,7 +1464,7 @@ private[emitter] object CoreJSLib { val i = varRef("i") val v = varRef("v") - val setName = genArrayClassPropertyForDef(ArrayClassProperty.set) + val setName = genSyntheticPropertyForDef(SyntheticProperty.set) List( MethodDef(static = false, setName, paramList(i, v), None, { @@ -1481,7 +1481,7 @@ private[emitter] object CoreJSLib { val destPos = varRef("destPos") val length = varRef("length") - val copyToName = genArrayClassPropertyForDef(ArrayClassProperty.copyTo) + val copyToName = genSyntheticPropertyForDef(SyntheticProperty.copyTo) val methodDef = MethodDef(static = false, copyToName, paramList(srcPos, dest, destPos, length), None, { @@ -1789,7 +1789,7 @@ private[emitter] object CoreJSLib { val i = varRef("i") val v = varRef("v") - val setName = genArrayClassPropertyForDef(ArrayClassProperty.set) + val setName = genSyntheticPropertyForDef(SyntheticProperty.set) val boundsCheck = condTree(arrayIndexOutOfBounds != CheckedBehavior.Unchecked) { If((i < 0) || (i >= This().u.length), @@ -1821,7 +1821,7 @@ private[emitter] object CoreJSLib { val destPos = varRef("destPos") val length = varRef("length") - val copyToName = genArrayClassPropertyForDef(ArrayClassProperty.copyTo) + val copyToName = genSyntheticPropertyForDef(SyntheticProperty.copyTo) val methodDef = MethodDef(static = false, copyToName, paramList(srcPos, dest, destPos, length), None, { @@ -2269,7 +2269,7 @@ private[emitter] object CoreJSLib { // cannot extend AnyVal because this is not a static class private implicit class CustomTreeOps(private val self: Tree) { - def u: Tree = genArrayClassPropSelect(self, ArrayClassProperty.u) + def u: Tree = genSyntheticPropSelect(self, SyntheticProperty.u) } } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala index 4e322050b2..e1e010eeb2 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala @@ -632,11 +632,11 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { } if (checked) { - genArrayClassPropApply(genArray, ArrayClassProperty.set, genIndex, genRhs) + genSyntheticPropApply(genArray, SyntheticProperty.set, genIndex, genRhs) } else { js.Assign( js.BracketSelect( - genArrayClassPropSelect(genArray, ArrayClassProperty.u)(lhs.pos), + genSyntheticPropSelect(genArray, SyntheticProperty.u)(lhs.pos), genIndex)(lhs.pos), genRhs) } @@ -876,7 +876,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { def genUnchecked(): js.Tree = { if (esFeatures.esVersion >= ESVersion.ES2015 && semantics.nullPointers == CheckedBehavior.Unchecked) - genArrayClassPropApply(jsArgs.head, ArrayClassProperty.copyTo, jsArgs.tail) + genSyntheticPropApply(jsArgs.head, SyntheticProperty.copyTo, jsArgs.tail) else genCallHelper(VarField.systemArraycopy, jsArgs: _*) } @@ -2680,7 +2680,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case ArrayLength(array) => val newArray = transformExprNoChar(checkNotNull(array)) genIdentBracketSelect( - genArrayClassPropSelect(newArray, ArrayClassProperty.u), + genSyntheticPropSelect(newArray, SyntheticProperty.u), "length") case ArraySelect(array, index) => @@ -2688,9 +2688,9 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { val newIndex = transformExprNoChar(index) semantics.arrayIndexOutOfBounds match { case CheckedBehavior.Compliant | CheckedBehavior.Fatal => - genArrayClassPropApply(newArray, ArrayClassProperty.get, newIndex) + genSyntheticPropApply(newArray, SyntheticProperty.get, newIndex) case CheckedBehavior.Unchecked => - js.BracketSelect(genArrayClassPropSelect(newArray, ArrayClassProperty.u), newIndex) + js.BracketSelect(genSyntheticPropSelect(newArray, SyntheticProperty.u), newIndex) } case tree: RecordSelect => @@ -2794,7 +2794,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case Transient(ArrayToTypedArray(expr, primRef)) => val value = transformExprNoChar(checkNotNull(expr)) - val valueUnderlying = genArrayClassPropSelect(value, ArrayClassProperty.u) + val valueUnderlying = genSyntheticPropSelect(value, SyntheticProperty.u) if (es2015) { js.Apply(genIdentBracketSelect(valueUnderlying, "slice"), Nil) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameCompressor.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameCompressor.scala index 0db5ead5bf..e2e8d32d34 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameCompressor.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameCompressor.scala @@ -56,8 +56,8 @@ private[emitter] final class NameCompressor(config: Emitter.Config) { def genResolverFor(methodName: MethodName): Resolver = entries.getOrElseUpdate(methodName, new MethodNameEntry(methodName)).genResolver() - def genResolverFor(prop: ArrayClassProperty): Resolver = - entries.getOrElseUpdate(prop, new ArrayClassPropEntry(prop)).genResolver() + def genResolverFor(prop: SyntheticProperty): Resolver = + entries.getOrElseUpdate(prop, new SyntheticPropEntry(prop)).genResolver() def genResolverForAncestor(ancestor: ClassName): Resolver = ancestorEntries.getOrElseUpdate(ancestor, new AncestorNameEntry(ancestor)).genResolver() @@ -168,14 +168,14 @@ private[emitter] object NameCompressor { case (x: MethodNameEntry, y: MethodNameEntry) => x.methodName.compareTo(y.methodName) - case (x: ArrayClassPropEntry, y: ArrayClassPropEntry) => + case (x: SyntheticPropEntry, y: SyntheticPropEntry) => x.property.compareTo(y.property) case _ => def ordinalFor(x: PropertyNameEntry): Int = x match { - case _: FieldNameEntry => 1 - case _: MethodNameEntry => 2 - case _: ArrayClassPropEntry => 3 + case _: FieldNameEntry => 1 + case _: MethodNameEntry => 2 + case _: SyntheticPropEntry => 3 } ordinalFor(this) - ordinalFor(that) } @@ -195,11 +195,11 @@ private[emitter] object NameCompressor { override def toString(): String = s"MethodNameEntry(${methodName.nameString})" } - private final class ArrayClassPropEntry(val property: ArrayClassProperty) + private final class SyntheticPropEntry(val property: SyntheticProperty) extends PropertyNameEntry { protected def debugString: String = property.nonMinifiedName - override def toString(): String = s"ArrayClassPropEntry(${property.nonMinifiedName})" + override def toString(): String = s"SyntheticPropEntry(${property.nonMinifiedName})" } private final class AncestorNameEntry(val ancestor: ClassName) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SJSGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SJSGen.scala index 2a9b3cf93e..ef7aaf8ea8 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SJSGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SJSGen.scala @@ -356,29 +356,29 @@ private[emitter] final class SJSGen( } } - def genArrayClassPropApply(receiver: Tree, prop: ArrayClassProperty, args: Tree*)( + def genSyntheticPropApply(receiver: Tree, prop: SyntheticProperty, args: Tree*)( implicit pos: Position): Tree = { - genArrayClassPropApply(receiver, prop, args.toList) + genSyntheticPropApply(receiver, prop, args.toList) } - def genArrayClassPropApply(receiver: Tree, prop: ArrayClassProperty, args: List[Tree])( + def genSyntheticPropApply(receiver: Tree, prop: SyntheticProperty, args: List[Tree])( implicit pos: Position): Tree = { - Apply(genArrayClassPropSelect(receiver, prop), args) + Apply(genSyntheticPropSelect(receiver, prop), args) } - def genArrayClassPropSelect(qualifier: Tree, prop: ArrayClassProperty)( + def genSyntheticPropSelect(qualifier: Tree, prop: SyntheticProperty)( implicit pos: Position): Tree = { - DotSelect(qualifier, genArrayClassProperty(prop)) + DotSelect(qualifier, genSyntheticProperty(prop)) } - def genArrayClassProperty(prop: ArrayClassProperty)(implicit pos: Position): MaybeDelayedIdent = { + def genSyntheticProperty(prop: SyntheticProperty)(implicit pos: Position): MaybeDelayedIdent = { nameCompressor match { case None => Ident(prop.nonMinifiedName) case Some(compressor) => DelayedIdent(compressor.genResolverFor(prop)) } } - def genArrayClassPropertyForDef(prop: ArrayClassProperty)(implicit pos: Position): MaybeDelayedIdent = { + def genSyntheticPropertyForDef(prop: SyntheticProperty)(implicit pos: Position): MaybeDelayedIdent = { nameCompressor match { case None => Ident(prop.nonMinifiedName) case Some(compressor) => DelayedIdent(compressor.genResolverFor(prop), prop.originalName) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ArrayClassProperty.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SyntheticProperty.scala similarity index 50% rename from linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ArrayClassProperty.scala rename to linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SyntheticProperty.scala index c33d0a99e7..84afaceff1 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ArrayClassProperty.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SyntheticProperty.scala @@ -14,33 +14,39 @@ package org.scalajs.linker.backend.emitter import org.scalajs.ir.OriginalName -/** Represents a property of one of the special `ArrayClass`es. +/** Represents a synthetic property of some Scala classes. + * + * A synthetic property does not exist as `FieldDef` or `MethodDef` in the IR, + * but is added by the Emitter on some classes that belong to the Scala class + * hierarchy. + * + * Currently, these only include properties of the `ArrayClass`es. * * These properties live in the same namespace as Scala field and method * names, because the `ArrayClass`es extend `j.l.Object`. Therefore, they * must take part in the global property minification algorithm. */ -final class ArrayClassProperty(val nonMinifiedName: String) - extends Comparable[ArrayClassProperty] { +final class SyntheticProperty(val nonMinifiedName: String) + extends Comparable[SyntheticProperty] { val originalName: OriginalName = OriginalName(nonMinifiedName) - def compareTo(that: ArrayClassProperty): Int = + def compareTo(that: SyntheticProperty): Int = this.nonMinifiedName.compareTo(that.nonMinifiedName) - override def toString(): String = s"ArrayClassProperty($nonMinifiedName)" + override def toString(): String = s"SyntheticProperty($nonMinifiedName)" } -object ArrayClassProperty { - /** `ArrayClass.u`: the underlying array of typed array. */ - val u: ArrayClassProperty = new ArrayClassProperty("u") +object SyntheticProperty { + /** `ArrayClass.u`: the underlying array or typed array. */ + val u: SyntheticProperty = new SyntheticProperty("u") /** `ArrayClass.get()`: gets one element. */ - val get: ArrayClassProperty = new ArrayClassProperty("get") + val get: SyntheticProperty = new SyntheticProperty("get") /** `ArrayClass.set()`: sets one element. */ - val set: ArrayClassProperty = new ArrayClassProperty("set") + val set: SyntheticProperty = new SyntheticProperty("set") /** `ArrayClass.copyTo()`: copies from that array to another array. */ - val copyTo: ArrayClassProperty = new ArrayClassProperty("copyTo") + val copyTo: SyntheticProperty = new SyntheticProperty("copyTo") } From cafe73184f90cf959aee5c9347bb463bda7c003a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 13 Jun 2024 23:17:11 +0200 Subject: [PATCH 2/3] Replace the JS object given to `jl.Class` by primitive IR operations. `java.lang.Class` requires primitive capabilities that only the linker can provide for most of its core methods. This commit changes the fundamental way to provide those. In the process, remove creation of multi-dimensional arrays from the IR, and move it to user-space instead. --- Previously, the contract was that the linker provided a JS object with specified properties and methods to the constructor of `jl.Class`. For example, it contained a `"name"` property in order to implement `jl.Class.getName()`. We happened to reuse the `$TypeData` instance as that object, for convenience, and to avoid more indirections. This design grew organically over the years, and was eventually specified in Scala.js IR 1.0.0. However, it always had some shortcomings that I was not happy about. First, the `getSuperclass()` method had to be special-cased in the Analyzer and Emitter: its very presence requires additional metadata. When it is not reachable, we do not actually provide the supposedly specified member `"getSuperclass"()` on the JS object. That assumes that only `jl.Class.getSuperclass()` actually calls that method. In turn, that assumes it never gets inlined (!), since doing so would make it unreachable, but the feature would still be required. Second, for optimization purposes, the emitter needs direct access to the `$TypeData` stored inside `jl.Class` (for some of its Transients). There again, that assumed a field with a particular name be present in `jl.Class` in which the JS object would be stored. This implicit requirement was already leaking in the way `jl.Class` was written, in order to prevent scalac from renaming the field. --- In this commit, we completely change the approach, and therefore the IR spec. We remove the JS object passed to `jl.Class`; its constructor now takes no arguments. Instead, we add primitives in the IR, in the form of new `UnaryOp`s and `BinaryOp`s. These new primitives operate on an argument of type `jl.Class!`, and possibly a second argument. For example, `UnaryOp.Class_name` replaces the functionality previously offered by the `"name"` field. `BinaryOp.Class_superClass` replaces `"getSuperclass"()`. It is up to the backend to ensure it can, somehow, implement those operations given an instance of `jl.Class`. We choose to add a magic field to `jl.Class` at the Emitter level to store the corresponding instance of `$TypeData`, and implement the operations in terms of that. The approach has several benefits: * It solves the two fundamental issues of the JS object, mentioned above. * It does not require "public" (unminifiable) property names for the features; since there is no JS object spec, we can consistently minify those members. * Several methods that were given an *intrinsic* treatment by the optimizer now fall, more naturally, under regular folding of operations. --- Since we are changing the spec anyway, we use the opportunity to switch what is primitive about `Array.newInstance`. Previously, the overload with multiple dimensions was primitive, and the one with a single dimension delegated to it. This was a waste, since usages of the multi-dimensional overload are basically non-existent otherwise. Similarly, the `NewArray` node was designed for multiple dimensions, and the single dimension was a special case. We now make the one-dimensional operations the only primitives. We implement the multi-dimensional overload of `newInstance` in terms of the other one. We also use that overload as a replacement for multi-dimensional `NewArray` nodes. This simplifies the treatment in the linker, and produces shorter and more efficient code at the end of the day. --- The change of approach comes with a non-negligible cost for backward compatibility. It is entirely done using deserialization hacks. The most glaring issue is the synthesization of the multi-dimensional overload of `Array.newInstance`. In this commit, we do not change the library/compiler yet, so that we can exercise the deserialization hacks. --- .../main/scala/org/scalajs/ir/Printers.scala | 23 +- .../scala/org/scalajs/ir/Serializers.scala | 306 +++++++++++- .../src/main/scala/org/scalajs/ir/Trees.scala | 68 ++- .../scala/org/scalajs/ir/PrintersTest.scala | 15 + .../scalajs/linker/analyzer/Analysis.scala | 2 + .../scalajs/linker/analyzer/Analyzer.scala | 24 +- .../org/scalajs/linker/analyzer/Infos.scala | 14 + .../linker/backend/emitter/ClassEmitter.scala | 17 +- .../linker/backend/emitter/CoreJSLib.scala | 148 +++--- .../linker/backend/emitter/EmitterNames.scala | 1 - .../backend/emitter/FunctionEmitter.scala | 57 ++- .../backend/emitter/KnowledgeGuardian.scala | 27 +- .../linker/backend/emitter/SJSGen.scala | 67 ++- .../backend/emitter/SyntheticProperty.scala | 6 +- .../linker/backend/emitter/VarField.scala | 7 +- .../backend/wasmemitter/ClassEmitter.scala | 18 +- .../backend/wasmemitter/CoreWasmLib.scala | 437 +++++------------- .../linker/backend/wasmemitter/Emitter.scala | 2 +- .../backend/wasmemitter/FunctionEmitter.scala | 147 +++--- .../backend/wasmemitter/LoaderContent.scala | 15 - .../linker/backend/wasmemitter/VarGen.scala | 11 +- .../linker/checker/ClassDefChecker.scala | 4 +- .../scalajs/linker/checker/IRChecker.scala | 14 +- .../scalajs/linker/frontend/BaseLinker.scala | 7 +- .../scalajs/linker/frontend/LinkingUnit.scala | 3 +- .../org/scalajs/linker/frontend/Refiner.scala | 6 +- .../modulesplitter/ModuleSplitter.scala | 4 +- .../frontend/optimizer/OptimizerCore.scala | 192 ++++---- .../linker/standard/LinkedGlobalInfo.scala | 18 + .../scalajs/linker/standard/ModuleSet.scala | 4 +- .../org/scalajs/linker/LibrarySizeTest.scala | 6 +- project/BinaryIncompatibilities.scala | 1 + project/Build.scala | 16 +- .../testsuite/javalib/lang/ClassTest.scala | 6 +- 34 files changed, 992 insertions(+), 701 deletions(-) create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedGlobalInfo.scala diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala index ec0ff8c5f8..eca5c547f2 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala @@ -371,8 +371,14 @@ object Printers { } else { print(lhs) print((op: @switch) match { - case String_length => ".length" - case CheckNotNull => ".notNull" + case String_length => ".length" + case CheckNotNull => ".notNull" + case Class_name => ".name" + case Class_isPrimitive => ".isPrimitive" + case Class_isInterface => ".isInterface" + case Class_isArray => ".isArray" + case Class_componentType => ".componentType" + case Class_superClass => ".superClass" }) } @@ -413,6 +419,19 @@ object Printers { print(rhs) print(']') + case BinaryOp(op, lhs, rhs) if BinaryOp.isClassOp(op) => + import BinaryOp._ + print((op: @switch) match { + case Class_isInstance => "isInstance(" + case Class_isAssignableFrom => "isAssignableFrom(" + case Class_cast => "cast(" + case Class_newArray => "newArray(" + }) + print(lhs) + print(", ") + print(rhs) + print(')') + case BinaryOp(op, lhs, rhs) => import BinaryOp._ print('(') diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala index bad2b82fa5..45453943c5 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala @@ -22,6 +22,7 @@ import scala.collection.mutable import scala.concurrent._ import Names._ +import OriginalName.NoOriginalName import Position._ import Trees._ import Types._ @@ -1223,13 +1224,45 @@ object Serializers { ApplyDynamicImport(readApplyFlags(), readClassName(), readMethodIdent(), readTrees()) - case TagUnaryOp => UnaryOp(readByte(), readTree()) - case TagBinaryOp => BinaryOp(readByte(), readTree(), readTree()) - case TagNewArray => NewArray(readArrayTypeRef(), readTrees()) - case TagArrayValue => ArrayValue(readArrayTypeRef(), readTrees()) - case TagArrayLength => ArrayLength(readTree()) - case TagArraySelect => ArraySelect(readTree(), readTree())(readType()) - case TagRecordValue => RecordValue(readType().asInstanceOf[RecordType], readTrees()) + case TagUnaryOp => UnaryOp(readByte(), readTree()) + case TagBinaryOp => BinaryOp(readByte(), readTree(), readTree()) + + case TagNewArray => + val arrayTypeRef = readArrayTypeRef() + val lengths = readTrees() + lengths match { + case length :: Nil => + NewArray(arrayTypeRef, lengths) + + case _ => + if (true /* hacks.use16 */) { // scalastyle:ignore + // Rewrite as a call to j.l.r.Array.newInstance + val ArrayTypeRef(base, origDims) = arrayTypeRef + val newDims = origDims - lengths.size + if (newDims < 0) { + throw new IOException( + s"Illegal legacy NewArray node with ${lengths.size} lengths but dimension $origDims at $pos") + } + val newBase = + if (newDims == 0) base + else ArrayTypeRef(base, newDims) + + ApplyStatic( + ApplyFlags.empty, + HackNames.ReflectArrayClass, + MethodIdent(HackNames.newInstanceMultiName), + List(ClassOf(newBase), ArrayValue(ArrayTypeRef(IntRef, 1), lengths)))( + AnyType) + } else { + throw new IOException( + s"Illegal NewArray node with multiple lengths for IR version 1.17+ at $pos") + } + } + + case TagArrayValue => ArrayValue(readArrayTypeRef(), readTrees()) + case TagArrayLength => ArrayLength(readTree()) + case TagArraySelect => ArraySelect(readTree(), readTree())(readType()) + case TagRecordValue => RecordValue(readType().asInstanceOf[RecordType], readTrees()) case TagIsInstanceOf => val expr = readTree() @@ -1471,6 +1504,10 @@ object Serializers { if (hacks.use4 && kind.isJSClass) { // #4409: Filter out abstract methods in non-native JS classes for version < 1.5 methods0.filter(_.body.isDefined) + } else if (true /*hacks.use16*/ && cls == ClassClass) { // scalastyle:ignore + jlClassMethodsHack16(methods0) + } else if (true /*hacks.use16*/ && cls == HackNames.ReflectArrayModClass) { // scalastyle:ignore + jlReflectArrayMethodsHack16(methods0) } else { methods0 } @@ -1493,6 +1530,253 @@ object Serializers { optimizerHints) } + private def jlClassMethodsHack16(methods: List[MethodDef]): List[MethodDef] = { + for (method <- methods) yield { + implicit val pos = method.pos + + val methodName = method.methodName + val methodSimpleNameString = methodName.simpleName.nameString + + val thisJLClass = This()(ClassType(ClassClass, nullable = false)) + + if (methodName.isConstructor) { + val newName = MethodIdent(NoArgConstructorName)(method.name.pos) + val newBody = ApplyStatically(ApplyFlags.empty.withConstructor(true), + thisJLClass, ObjectClass, newName, Nil)(NoType) + MethodDef(method.flags, newName, method.originalName, + Nil, NoType, Some(newBody))( + method.optimizerHints, method.version) + } else { + def argRef = method.args.head.ref + def argRefNotNull = UnaryOp(UnaryOp.CheckNotNull, argRef) + + var forceInline = true // reset to false in the `case _ =>` + + val newBody: Tree = methodSimpleNameString match { + case "getName" => UnaryOp(UnaryOp.Class_name, thisJLClass) + case "isPrimitive" => UnaryOp(UnaryOp.Class_isPrimitive, thisJLClass) + case "isInterface" => UnaryOp(UnaryOp.Class_isInterface, thisJLClass) + case "isArray" => UnaryOp(UnaryOp.Class_isArray, thisJLClass) + case "getComponentType" => UnaryOp(UnaryOp.Class_componentType, thisJLClass) + case "getSuperclass" => UnaryOp(UnaryOp.Class_superClass, thisJLClass) + + case "isInstance" => BinaryOp(BinaryOp.Class_isInstance, thisJLClass, argRef) + case "isAssignableFrom" => BinaryOp(BinaryOp.Class_isAssignableFrom, thisJLClass, argRefNotNull) + case "cast" => BinaryOp(BinaryOp.Class_cast, thisJLClass, argRef) + + case _ => + forceInline = false + + /* Unfortunately, some of the other methods directly referred to + * `this.data["name"]`, instead of building on `this.getName()`. + * We must replace those occurrences with a `Class_name` as well. + */ + val transformer = new Transformers.Transformer { + override def transform(tree: Tree, isStat: Boolean): Tree = tree match { + case JSSelect(_, StringLiteral("name")) => + implicit val pos = tree.pos + UnaryOp(UnaryOp.Class_name, thisJLClass) + case _ => + super.transform(tree, isStat) + } + } + transformer.transform(method.body.get, isStat = method.resultType == NoType) + } + + val newOptimizerHints = + if (forceInline) method.optimizerHints.withInline(true) + else method.optimizerHints + + MethodDef(method.flags, method.name, method.originalName, + method.args, method.resultType, Some(newBody))( + newOptimizerHints, method.version) + } + } + } + + private def jlReflectArrayMethodsHack16(methods: List[MethodDef]): List[MethodDef] = { + /* Basically this method hard-codes new implementations for the two + * overloads of newInstance. + * It is horrible, but better than pollute everything else in the linker. + */ + + import HackNames._ + + def paramDef(name: String, ptpe: Type)(implicit pos: Position): ParamDef = + ParamDef(LocalIdent(LocalName(name)), NoOriginalName, ptpe, mutable = false) + + def varDef(name: String, vtpe: Type, rhs: Tree, mutable: Boolean = false)( + implicit pos: Position): VarDef = { + VarDef(LocalIdent(LocalName(name)), NoOriginalName, vtpe, mutable, rhs) + } + + val jlClassRef = ClassRef(ClassClass) + val intArrayTypeRef = ArrayTypeRef(IntRef, 1) + val objectRef = ClassRef(ObjectClass) + val objectArrayTypeRef = ArrayTypeRef(objectRef, 1) + + val jlClassType = ClassType(ClassClass, nullable = true) + + val newInstanceRecName = MethodName("newInstanceRec", + List(jlClassRef, intArrayTypeRef, IntRef), objectRef) + + val EAF = ApplyFlags.empty + + val newInstanceRecMethod = { + /* def newInstanceRec(componentType: jl.Class, dimensions: int[], offset: int): any = { + * val length: int = dimensions[offset] + * val result: any = newInstance(componentType, length) + * val innerOffset = offset + 1 + * if (innerOffset < dimensions.length) { + * val result2: Object[] = result.asInstanceOf[Object[]] + * val innerComponentType: jl.Class = componentType.getComponentType() + * var i: Int = 0 + * while (i != length) + * result2[i] = newInstanceRec(innerComponentType, dimensions, innerOffset) + * i = i + 1 + * } + * } + * result + * } + */ + + implicit val pos = Position.NoPosition + + val getComponentTypeName = MethodName("getComponentType", Nil, jlClassRef) + + val ths = This()(ClassType(ReflectArrayModClass, nullable = false)) + + val componentType = paramDef("componentType", jlClassType) + val dimensions = paramDef("dimensions", ArrayType(intArrayTypeRef, nullable = true)) + val offset = paramDef("offset", IntType) + + val length = varDef("length", IntType, ArraySelect(dimensions.ref, offset.ref)(IntType)) + val result = varDef("result", AnyType, + Apply(EAF, ths, MethodIdent(newInstanceSingleName), List(componentType.ref, length.ref))(AnyType)) + val innerOffset = varDef("innerOffset", IntType, + BinaryOp(BinaryOp.Int_+, offset.ref, IntLiteral(1))) + + val result2 = varDef("result2", ArrayType(objectArrayTypeRef, nullable = true), + AsInstanceOf(result.ref, ArrayType(objectArrayTypeRef, nullable = true))) + val innerComponentType = varDef("innerComponentType", jlClassType, + Apply(EAF, componentType.ref, MethodIdent(getComponentTypeName), Nil)(jlClassType)) + val i = varDef("i", IntType, IntLiteral(0), mutable = true) + + val body = { + Block( + length, + result, + innerOffset, + If(BinaryOp(BinaryOp.Int_<, innerOffset.ref, ArrayLength(dimensions.ref)), { + Block( + result2, + innerComponentType, + i, + While(BinaryOp(BinaryOp.Int_!=, i.ref, length.ref), { + Block( + Assign( + ArraySelect(result2.ref, i.ref)(AnyType), + Apply(EAF, ths, MethodIdent(newInstanceRecName), + List(innerComponentType.ref, dimensions.ref, innerOffset.ref))(AnyType) + ), + Assign( + i.ref, + BinaryOp(BinaryOp.Int_+, i.ref, IntLiteral(1)) + ) + ) + }) + ) + }, Skip())(NoType), + result.ref + ) + } + + MethodDef(MemberFlags.empty, MethodIdent(newInstanceRecName), + NoOriginalName, List(componentType, dimensions, offset), AnyType, + Some(body))( + OptimizerHints.empty, Version.fromInt(1)) + } + + /* Only for the temporary commit where we always apply the hack, because + * the hack is applied in the JavalibIRCleaner and then a second time + * for the true deserialization. + */ + val oldMethodsTemp = + methods.filterNot(_.methodName == newInstanceRecName) + + val newMethods = for (method <- oldMethodsTemp) yield { + method.methodName match { + case `newInstanceSingleName` => + // newInstance(jl.Class, int) --> newArray(jlClass.notNull, length) + + implicit val pos = method.pos + + val List(jlClassParam, lengthParam) = method.args + + val newBody = BinaryOp(BinaryOp.Class_newArray, + UnaryOp(UnaryOp.CheckNotNull, jlClassParam.ref), + lengthParam.ref) + + MethodDef(method.flags, method.name, method.originalName, + method.args, method.resultType, Some(newBody))( + method.optimizerHints.withInline(true), method.version) + + case `newInstanceMultiName` => + /* newInstance(jl.Class, int[]) --> + * var outermostComponentType: jl.Class = jlClassParam + * var i: int = 1 + * while (i != lengths.length) { + * outermostComponentType = getClass(this.newInstance(outermostComponentType, 0)) + * i = i + 1 + * } + * newInstanceRec(outermostComponentType, lengths, 0) + */ + + implicit val pos = method.pos + + val List(jlClassParam, lengthsParam) = method.args + + val newBody = { + val outermostComponentType = varDef("outermostComponentType", + jlClassType, jlClassParam.ref, mutable = true) + val i = varDef("i", IntType, IntLiteral(1), mutable = true) + + Block( + outermostComponentType, + i, + While(BinaryOp(BinaryOp.Int_!=, i.ref, ArrayLength(lengthsParam.ref)), { + Block( + Assign( + outermostComponentType.ref, + GetClass(Apply(EAF, This()(ClassType(ReflectArrayModClass, nullable = false)), + MethodIdent(newInstanceSingleName), + List(outermostComponentType.ref, IntLiteral(0)))(AnyType)) + ), + Assign( + i.ref, + BinaryOp(BinaryOp.Int_+, i.ref, IntLiteral(1)) + ) + ) + }), + Apply(EAF, This()(ClassType(ReflectArrayModClass, nullable = false)), + MethodIdent(newInstanceRecName), + List(outermostComponentType.ref, lengthsParam.ref, IntLiteral(0)))( + AnyType) + ) + } + + MethodDef(method.flags, method.name, method.originalName, + method.args, method.resultType, Some(newBody))( + method.optimizerHints, method.version) + + case _ => + method + } + } + + newInstanceRecMethod :: newMethods + } + private def jsConstructorHack( jsMethodProps: List[JSMethodPropDef]): (Option[JSConstructorDef], List[JSMethodPropDef]) = { val jsConstructorBuilder = new OptionBuilder[JSConstructorDef] @@ -2159,11 +2443,19 @@ object Serializers { ClassName("java.lang.CloneNotSupportedException") val SystemModule: ClassName = ClassName("java.lang.System$") + val ReflectArrayClass = + ClassName("java.lang.reflect.Array") + val ReflectArrayModClass = + ClassName("java.lang.reflect.Array$") val cloneName: MethodName = MethodName("clone", Nil, ClassRef(ObjectClass)) val identityHashCodeName: MethodName = MethodName("identityHashCode", List(ClassRef(ObjectClass)), IntRef) + val newInstanceSingleName: MethodName = + MethodName("newInstance", List(ClassRef(ClassClass), IntRef), ClassRef(ObjectClass)) + val newInstanceMultiName: MethodName = + MethodName("newInstance", List(ClassRef(ClassClass), ArrayTypeRef(IntRef, 1)), ClassRef(ObjectClass)) } private class OptionBuilder[T] { diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala index 34804cc421..3eb43c7f11 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala @@ -284,6 +284,9 @@ object Trees { } /** Unary operation. + * + * The `Class_x` operations take a `jl.Class!` argument, i.e., a + * non-nullable `jl.Class`. * * `CheckNotNull` throws NPEs subject to UB. * @@ -327,11 +330,22 @@ object Trees { // String.length, introduced in 1.11 final val String_length = 17 - // New in 1.17 + // Null check, introduced in 1.17 final val CheckNotNull = 18 + // Class operations, introduced in 1.17 + final val Class_name = 19 + final val Class_isPrimitive = 20 + final val Class_isInterface = 21 + final val Class_isArray = 22 + final val Class_componentType = 23 + final val Class_superClass = 24 + + def isClassOp(op: Code): Boolean = + op >= Class_name && op <= Class_superClass + def resultTypeOf(op: Code, argType: Type): Type = (op: @switch) match { - case Boolean_! => + case Boolean_! | Class_isPrimitive | Class_isInterface | Class_isArray => BooleanType case IntToChar => CharType @@ -349,10 +363,42 @@ object Trees { DoubleType case CheckNotNull => argType.toNonNullable + case Class_name => + StringType + case Class_componentType | Class_superClass => + ClassType(ClassClass, nullable = true) } } - /** Binary operation (always preserves pureness). */ + /** Binary operation. + * + * All binary operations follow common evaluation steps: + * + * 1. Let `lhsValue` be the result of evaluating `lhs`. + * 2. Let `rhsValue` be the result of evaluating `rhs`. + * 3. Perform an operation that depends on `op`, `lhsValue` and `rhsValue`. + * + * Unless `lhsValue` throws, `rhsValue` will therefore always be evaluated, + * even if the operation `op` would throw based only on `lhsValue`. + * + * The integer dividing operators (`Int_/`, `Int_%`, `Long_/` and `Long_%`) + * throw an `ArithmeticException` when their right-hand-side is 0. That + * exception is not subject to undefined behavior. + * + * `String_charAt` throws a `StringIndexOutOfBoundsException`. + * + * The `Class_x` operations take a `jl.Class!` as lhs, i.e., a + * non-nullable `jl.Class`. `Class_isAssignableFrom` also takes a + * `jl.Class!` as rhs. + * + * - `Class_isInstance` and `Class_isAssignableFrom` are pure. + * - `Class_cast` throws a CCE if its second argument is non-null and + * not an instance of the class represented by its first argument. + * - `Class_newArray` throws a `NegativeArraySizeException` if its second + * argument is negative. + * + * Otherwise, binary operations preserve pureness. + */ sealed case class BinaryOp(op: BinaryOp.Code, lhs: Tree, rhs: Tree)( implicit val pos: Position) extends Tree { @@ -435,12 +481,22 @@ object Trees { // New in 1.11 final val String_charAt = 58 + // New in 1.17 + final val Class_isInstance = 59 + final val Class_isAssignableFrom = 60 + final val Class_cast = 61 + final val Class_newArray = 62 + + def isClassOp(op: Code): Boolean = + op >= Class_isInstance && op <= Class_newArray + def resultTypeOf(op: Code): Type = (op: @switch) match { case === | !== | Boolean_== | Boolean_!= | Boolean_| | Boolean_& | Int_== | Int_!= | Int_< | Int_<= | Int_> | Int_>= | Long_== | Long_!= | Long_< | Long_<= | Long_> | Long_>= | - Double_== | Double_!= | Double_< | Double_<= | Double_> | Double_>= => + Double_== | Double_!= | Double_< | Double_<= | Double_> | Double_>= | + Class_isInstance | Class_isAssignableFrom => BooleanType case String_+ => StringType @@ -456,6 +512,10 @@ object Trees { DoubleType case String_charAt => CharType + case Class_cast => + AnyType + case Class_newArray => + AnyNotNullType } } diff --git a/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala b/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala index f3cef80ffa..16d5120ea3 100644 --- a/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala +++ b/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala @@ -394,6 +394,14 @@ class PrintersTest { assertPrintEquals("x.length", UnaryOp(String_length, ref("x", StringType))) assertPrintEquals("x.notNull", UnaryOp(CheckNotNull, ref("x", AnyType))) + + val classVarRef = ref("x", ClassType(ClassClass, nullable = false)) + assertPrintEquals("x.name", UnaryOp(Class_name, classVarRef)) + assertPrintEquals("x.isPrimitive", UnaryOp(Class_isPrimitive, classVarRef)) + assertPrintEquals("x.isInterface", UnaryOp(Class_isInterface, classVarRef)) + assertPrintEquals("x.isArray", UnaryOp(Class_isArray, classVarRef)) + assertPrintEquals("x.componentType", UnaryOp(Class_componentType, classVarRef)) + assertPrintEquals("x.superClass", UnaryOp(Class_superClass, classVarRef)) } @Test def printPseudoUnaryOp(): Unit = { @@ -539,6 +547,13 @@ class PrintersTest { assertPrintEquals("x[y]", BinaryOp(String_charAt, ref("x", StringType), ref("y", IntType))) + + val classVarRef = ref("x", ClassType(ClassClass, nullable = false)) + assertPrintEquals("isInstance(x, y)", BinaryOp(Class_isInstance, classVarRef, ref("y", AnyType))) + assertPrintEquals("isAssignableFrom(x, y)", + BinaryOp(Class_isAssignableFrom, classVarRef, ref("y", ClassType(ClassClass, nullable = false)))) + assertPrintEquals("cast(x, y)", BinaryOp(Class_cast, classVarRef, ref("y", AnyType))) + assertPrintEquals("newArray(x, y)", BinaryOp(Class_newArray, classVarRef, ref("y", IntType))) } @Test def printNewArray(): Unit = { diff --git a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analysis.scala b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analysis.scala index 6d0a02e83c..3c1e6f251b 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analysis.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analysis.scala @@ -37,6 +37,8 @@ trait Analysis { def classInfos: scala.collection.Map[ClassName, ClassInfo] def topLevelExportInfos: scala.collection.Map[(ModuleID, String), TopLevelExportInfo] + def isClassSuperClassUsed: Boolean + def errors: scala.collection.Seq[Error] } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala index 3931d2ab58..79f1ef432f 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala @@ -118,6 +118,9 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, def classInfos: scala.collection.Map[ClassName, Analysis.ClassInfo] = _classInfos + private val _classSuperClassUsed = new AtomicBoolean(false) + def isClassSuperClassUsed: Boolean = _classSuperClassUsed.get() + private[this] val _errors = new GrowingList[Error] override def errors: List[Error] = _errors.get() @@ -294,20 +297,11 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, /** Reach additional class data based on reflection methods being used. */ private def reachDataThroughReflection(): Unit = { - - val classClassInfo = _classInfos.get(ClassClass) - - /* If Class.getSuperclass() is reachable, we can reach the data of all + /* If Class_superClass is used, we can reach the data of all * superclasses of classes whose data we can already reach. */ - for { - getSuperclassMethodInfo <- - classClassInfo.flatMap(_.publicMethodInfos.get(getSuperclassMethodName)) - if getSuperclassMethodInfo.isReachable - } { - // calledFrom should always be nonEmpty if isReachable, but let's be robust - implicit val from = - getSuperclassMethodInfo.calledFrom.headOption.getOrElse(fromAnalyzer) + if (isClassSuperClassUsed) { + implicit val from = fromAnalyzer for (classInfo <- _classInfos.values.filter(_.isDataAccessed).toList) { @tailrec def loop(classInfo: ClassInfo): Unit = { @@ -1461,7 +1455,7 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, objectClassInfo.addStaticDependency(ClassClass) lookupClass(ClassClass) { clazz => clazz.instantiated() - clazz.callMethodStatically(MemberNamespace.Constructor, ObjectArgConstructorName) + clazz.callMethodStatically(MemberNamespace.Constructor, NoArgConstructorName) } } @@ -1479,6 +1473,10 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, config.coreSpec.esFeatures.esVersion < ESVersion.ES2016) { _errors ::= ExponentOperatorWithoutES2016Support(from) } + + if ((globalFlags & ReachabilityInfo.FlagUsedClassSuperClass) != 0) { + _classSuperClassUsed.set(true) + } } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala index a6ed3f6a6e..8dcb89c784 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala @@ -112,6 +112,7 @@ object Infos { final val FlagAccessedNewTarget = 1 << 1 final val FlagAccessedImportMeta = 1 << 2 final val FlagUsedExponentOperator = 1 << 3 + final val FlagUsedClassSuperClass = 1 << 4 } /** Things from a given class that are reached by one method. */ @@ -380,6 +381,9 @@ object Infos { def addUsedExponentOperator(): this.type = setFlag(ReachabilityInfo.FlagUsedExponentOperator) + def addUsedClassSuperClass(): this.type = + setFlag(ReachabilityInfo.FlagUsedClassSuperClass) + def result(): ReachabilityInfo = new ReachabilityInfo(version, byClass.valuesIterator.map(_.result()).toArray, flags) } @@ -650,6 +654,16 @@ object Infos { builder.maybeAddUsedInstanceTest(tpe) + case UnaryOp(op, _) => + import UnaryOp._ + + op match { + case Class_superClass => + builder.addUsedClassSuperClass() + case _ => + // do nothing + } + case BinaryOp(op, _, rhs) => import BinaryOp._ diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ClassEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ClassEmitter.scala index 6351e43614..0243d9f7bc 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ClassEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ClassEmitter.scala @@ -310,6 +310,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } initToInline.fold { + assert(className != ClassClass, s"java.lang.Class did not have an inlineable init") WithGlobals( js.Function(arrow = false, Nil, None, js.Block(superCtorCallAndFieldDefs))) } { initMethodDef => @@ -326,8 +327,20 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { for (generatedInitMethodFun <- generatedInitMethodFunWithGlobals) yield { val js.Function(arrow, args, restParam, initMethodFunBody) = generatedInitMethodFun - js.Function(arrow, args, restParam, - js.Block(superCtorCallAndFieldDefs ::: initMethodFunBody :: Nil)) + + if (className != ClassClass) { + js.Function(arrow, args, restParam, + js.Block(superCtorCallAndFieldDefs ::: initMethodFunBody :: Nil)) + } else { + // Inject the magical `data` argument and field + assert(args.isEmpty, s"Unexpected constructor arguments $args for java.lang.Class") + val dataParam = js.ParamDef(fileLevelVarIdent(VarField.data)) + val createDataField = js.Assign( + js.DotSelect(js.This(), genSyntheticPropertyForDef(SyntheticProperty.data)), + dataParam.ref) + js.Function(arrow, dataParam :: Nil, restParam, + js.Block(superCtorCallAndFieldDefs ::: createDataField :: initMethodFunBody :: Nil)) + } } } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala index 792c1a5677..e1ff675469 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala @@ -578,7 +578,7 @@ private[emitter] object CoreJSLib { str("char") }, { If(genIsScalaJSObject(value), { - genIdentBracketSelect(value DOT classData, cpn.name) + value DOT classData DOT cpn.name }, { typeof(value) }) @@ -665,33 +665,6 @@ private[emitter] object CoreJSLib { str("Cannot call isInstance() on a Class representing a JS trait/object") :: Nil)) } ::: - defineFunction2(VarField.newArrayObject) { (arrayClassData, lengths) => - Return(genCallHelper(VarField.newArrayObjectInternal, arrayClassData, lengths, int(0))) - } ::: - - defineFunction3(VarField.newArrayObjectInternal) { (arrayClassData, lengths, lengthIndex) => - val result = varRef("result") - val subArrayClassData = varRef("subArrayClassData") - val subLengthIndex = varRef("subLengthIndex") - val underlying = varRef("underlying") - val i = varRef("i") - - Block( - const(result, New(arrayClassData DOT cpn.constr, - BracketSelect(lengths, lengthIndex) :: Nil)), - If(lengthIndex < (lengths.length - 1), Block( - const(subArrayClassData, arrayClassData DOT cpn.componentData), - const(subLengthIndex, lengthIndex + 1), - const(underlying, result.u), - For(let(i, 0), i < underlying.length, i.++, { - BracketSelect(underlying, i) := - genCallHelper(VarField.newArrayObjectInternal, subArrayClassData, lengths, subLengthIndex) - }) - )), - Return(result) - ) - } ::: - defineFunction1(VarField.objectClone) { instance => // return Object.create(Object.getPrototypeOf(instance), $getOwnPropertyDescriptors(instance)); val callGetOwnPropertyDescriptors = genCallPolyfillableBuiltin( @@ -703,7 +676,7 @@ private[emitter] object CoreJSLib { defineFunction1(VarField.objectOrArrayClone) { instance => // return instance.$classData.isArrayClass ? instance.clone__O() : $objectClone(instance); - Return(If(genIdentBracketSelect(instance DOT classData, cpn.isArrayClass), + Return(If(instance DOT classData DOT cpn.isArrayClass, genApply(instance, cloneMethodName, Nil), genCallHelper(VarField.objectClone, instance))) } @@ -797,7 +770,7 @@ private[emitter] object CoreJSLib { StringLiteral(RuntimeClassNameMapperImpl.map( semantics.runtimeClassNameMapper, className.nameString)) }, - instance => genIdentBracketSelect(instance DOT classData, cpn.name), + instance => instance DOT classData DOT cpn.name, { if (nullPointers == CheckedBehavior.Unchecked) genApply(Null(), getNameMethodName, Nil) @@ -1132,7 +1105,7 @@ private[emitter] object CoreJSLib { condDefs(arrayStores != CheckedBehavior.Unchecked)( defineFunction5(VarField.systemArraycopyRefs) { (src, srcPos, dest, destPos, length) => - If(Apply(genIdentBracketSelect(dest DOT classData, cpn.isAssignableFrom), List(src DOT classData)), { + If(Apply(dest DOT classData DOT cpn.isAssignableFrom, List(src DOT classData)), { /* Fast-path, no need for array store checks. This always applies * for arrays of the same type, and a fortiori, when `src eq dest`. */ @@ -1165,7 +1138,7 @@ private[emitter] object CoreJSLib { const(srcData, src && (src DOT classData)), If(srcData === (dest && (dest DOT classData)), { // Both values have the same "data" (could also be falsy values) - If(srcData && genIdentBracketSelect(srcData, cpn.isArrayClass), { + If(srcData && (srcData DOT cpn.isArrayClass), { // Fast path: the values are array of the same type if (esVersion >= ESVersion.ES2015 && nullPointers == CheckedBehavior.Unchecked) genSyntheticPropApply(src, SyntheticProperty.copyTo, srcPos, dest, destPos, length) @@ -1574,9 +1547,6 @@ private[emitter] object CoreJSLib { def privateFieldSet(fieldName: String, value: Tree): Tree = This() DOT fieldName := value - def publicFieldSet(fieldName: String, value: Tree): Tree = - genIdentBracketSelect(This(), fieldName) := value - val ctor = { MethodDef(static = false, Ident("constructor"), Nil, None, { Block( @@ -1603,11 +1573,11 @@ private[emitter] object CoreJSLib { privateFieldSet(cpn.wrapArray, Undefined()), privateFieldSet(cpn.isJSType, bool(false)), - publicFieldSet(cpn.name, str("")), - publicFieldSet(cpn.isPrimitive, bool(false)), - publicFieldSet(cpn.isInterface, bool(false)), - publicFieldSet(cpn.isArrayClass, bool(false)), - publicFieldSet(cpn.isInstance, Undefined()) + privateFieldSet(cpn.name, str("")), + privateFieldSet(cpn.isPrimitive, bool(false)), + privateFieldSet(cpn.isInterface, bool(false)), + privateFieldSet(cpn.isArrayClass, bool(false)), + privateFieldSet(cpn.isInstance, Undefined()) ) }) } @@ -1631,9 +1601,9 @@ private[emitter] object CoreJSLib { const(self, This()), // capture `this` for use in arrow fun privateFieldSet(cpn.isAssignableFromFun, genArrowFunction(paramList(that), Return(that === self))), - publicFieldSet(cpn.name, displayName), - publicFieldSet(cpn.isPrimitive, bool(true)), - publicFieldSet(cpn.isInstance, + privateFieldSet(cpn.name, displayName), + privateFieldSet(cpn.isPrimitive, bool(true)), + privateFieldSet(cpn.isInstance, genArrowFunction(paramList(obj), Return(bool(false)))), If(arrayClass !== Undefined(), { // it is undefined for void privateFieldSet(cpn._arrayOf, @@ -1685,9 +1655,9 @@ private[emitter] object CoreJSLib { }) }), privateFieldSet(cpn.isJSType, kindOrCtor === 2), - publicFieldSet(cpn.name, fullName), - publicFieldSet(cpn.isInterface, kindOrCtor === 1), - publicFieldSet(cpn.isInstance, isInstance || { + privateFieldSet(cpn.name, fullName), + privateFieldSet(cpn.isInterface, kindOrCtor === 1), + privateFieldSet(cpn.isInstance, isInstance || { genArrowFunction(paramList(obj), { Return(!(!(obj && (obj DOT classData) && BracketSelect(obj DOT classData DOT cpn.ancestors, internalName)))) @@ -1720,8 +1690,8 @@ private[emitter] object CoreJSLib { privateFieldSet(cpn.arrayBase, arrayBase), privateFieldSet(cpn.arrayDepth, arrayDepth), privateFieldSet(cpn.arrayEncodedName, name), - publicFieldSet(cpn.name, name), - publicFieldSet(cpn.isArrayClass, bool(true)) + privateFieldSet(cpn.name, name), + privateFieldSet(cpn.isArrayClass, bool(true)) ) } @@ -1754,7 +1724,7 @@ private[emitter] object CoreJSLib { }) }) }), - publicFieldSet(cpn.isInstance, + privateFieldSet(cpn.isInstance, genArrowFunction(paramList(obj), Return(obj instanceof arrayClass))), Return(This()) ) @@ -1798,7 +1768,7 @@ private[emitter] object CoreJSLib { val storeCheck = { If((v !== Null()) && !(componentData DOT cpn.isJSType) && - !Apply(genIdentBracketSelect(componentData, cpn.isInstance), v :: Nil), + !Apply(componentData DOT cpn.isInstance, v :: Nil), genCallHelper(VarField.throwArrayStoreException, v)) } @@ -1882,7 +1852,7 @@ private[emitter] object CoreJSLib { Return(New(ArrayClass, array :: Nil)) })), const(self, This()), // don't rely on the lambda being called with `this` as receiver - publicFieldSet(cpn.isInstance, genArrowFunction(paramList(obj), { + privateFieldSet(cpn.isInstance, genArrowFunction(paramList(obj), { val data = varRef("data") Block( const(data, obj && (obj DOT classData)), @@ -1911,9 +1881,13 @@ private[emitter] object CoreJSLib { def getClassOf = { MethodDef(static = false, Ident(cpn.getClassOf), Nil, None, { + /* We call the NoArgConstructorName with an argument, because it is + * added at the emitter level for java.lang.Class, without being in + * the IR. + */ Block( If(!(This() DOT cpn._classOf), - This() DOT cpn._classOf := genScalaClassNew(ClassClass, ObjectArgConstructorName, This()), + This() DOT cpn._classOf := genScalaClassNew(ClassClass, NoArgConstructorName, This()), Skip()), Return(This() DOT cpn._classOf) ) @@ -1921,7 +1895,7 @@ private[emitter] object CoreJSLib { } def isAssignableFrom = { - /* This is the public method called by j.l.Class.isAssignableFrom. It + /* This is the method called by `Class_isAssignableFrom`. It * first performs a fast-path with `this === that`, and otherwise calls * the internal `isAssignableFromFun` function. * The reason for this decomposition (as opposed to performing the @@ -1931,7 +1905,7 @@ private[emitter] object CoreJSLib { * We only need a polymorphic dispatch in the slow path. */ val that = varRef("that") - MethodDef(static = false, StringLiteral(cpn.isAssignableFrom), + MethodDef(static = false, Ident(cpn.isAssignableFrom), paramList(that), None, { Return( (This() === that) || // fast path @@ -1939,22 +1913,22 @@ private[emitter] object CoreJSLib { }) } - def checkCast = { + def cast = { + assert(asInstanceOfs != CheckedBehavior.Unchecked) val obj = varRef("obj") - MethodDef(static = false, StringLiteral(cpn.checkCast), paramList(obj), None, - if (asInstanceOfs != CheckedBehavior.Unchecked) { + MethodDef(static = false, Ident(cpn.cast), paramList(obj), None, { + Block( If((obj !== Null()) && !(This() DOT cpn.isJSType) && - !Apply(genIdentBracketSelect(This(), cpn.isInstance), obj :: Nil), - genCallHelper(VarField.throwClassCastException, obj, genIdentBracketSelect(This(), cpn.name)), - Skip()) - } else { - Skip() - } - ) + !Apply(This() DOT cpn.isInstance, obj :: Nil), + genCallHelper(VarField.throwClassCastException, obj, This() DOT cpn.name), + Skip()), + Return(obj) + ) + }) } def getSuperclass = { - MethodDef(static = false, StringLiteral(cpn.getSuperclass), Nil, None, { + MethodDef(static = false, Ident(cpn.getSuperclass), Nil, None, { Return(If(This() DOT cpn.parentData, Apply(This() DOT cpn.parentData DOT cpn.getClassOf, Nil), Null())) @@ -1962,26 +1936,18 @@ private[emitter] object CoreJSLib { } def getComponentType = { - MethodDef(static = false, StringLiteral(cpn.getComponentType), Nil, None, { + MethodDef(static = false, Ident(cpn.getComponentType), Nil, None, { Return(If(This() DOT cpn.componentData, Apply(This() DOT cpn.componentData DOT cpn.getClassOf, Nil), Null())) }) } - def newArrayOfThisClass = { - val lengths = varRef("lengths") - val arrayClassData = varRef("arrayClassData") - val i = varRef("i") - MethodDef(static = false, StringLiteral(cpn.newArrayOfThisClass), - paramList(lengths), None, { - Block( - let(arrayClassData, This()), - For(let(i, 0), i < lengths.length, i.++, { - arrayClassData := Apply(arrayClassData DOT cpn.getArrayOf, Nil) - }), - Return(genCallHelper(VarField.newArrayObject, arrayClassData, lengths)) - ) + def newArray = { + val length = varRef("length") + MethodDef(static = false, Ident(cpn.newArray), + paramList(length), None, { + Return(New(Apply(This() DOT cpn.getArrayOf, Nil) DOT cpn.constr, length :: Nil)) }) } @@ -1995,11 +1961,16 @@ private[emitter] object CoreJSLib { if (globalKnowledge.isClassClassInstantiated) { List( getClassOf, - isAssignableFrom, - checkCast, + isAssignableFrom + ) ::: ( + if (asInstanceOfs != CheckedBehavior.Unchecked) + List(cast) + else + Nil + ) ::: List( getSuperclass, getComponentType, - newArrayOfThisClass + newArray ) } else if (arrayStores != CheckedBehavior.Unchecked) { List( @@ -2035,7 +2006,7 @@ private[emitter] object CoreJSLib { Block( const(arrayDepth, data DOT cpn.arrayDepth), Return(If(arrayDepth === depth, { - !genIdentBracketSelect(data DOT cpn.arrayBase, cpn.isPrimitive) + !(data DOT cpn.arrayBase DOT cpn.isPrimitive) }, { arrayDepth > depth })) @@ -2098,9 +2069,6 @@ private[emitter] object CoreJSLib { def privateFieldSet(fieldName: String, value: Tree): Tree = typeDataVar DOT fieldName := value - def publicFieldSet(fieldName: String, value: Tree): Tree = - genIdentBracketSelect(typeDataVar, fieldName) := value - extractWithGlobals( globalVarDef(VarField.d, ObjectClass, New(globalVar(VarField.TypeData, CoreVar), Nil))) ::: List( @@ -2108,11 +2076,11 @@ private[emitter] object CoreJSLib { privateFieldSet(cpn.arrayEncodedName, str("L" + fullName + ";")), privateFieldSet(cpn.isAssignableFromFun, { genArrowFunction(paramList(that), { - Return(!genIdentBracketSelect(that, cpn.isPrimitive)) + Return(!(that DOT cpn.isPrimitive)) }) }), - publicFieldSet(cpn.name, str(fullName)), - publicFieldSet(cpn.isInstance, + privateFieldSet(cpn.name, str(fullName)), + privateFieldSet(cpn.isInstance, genArrowFunction(paramList(obj), Return(obj !== Null()))), privateFieldSet(cpn._arrayOf, { Apply(New(globalVar(VarField.TypeData, CoreVar), Nil) DOT cpn.initSpecializedArray, List( @@ -2124,7 +2092,7 @@ private[emitter] object CoreJSLib { Block( const(thatDepth, that DOT cpn.arrayDepth), Return(If(thatDepth === 1, { - !genIdentBracketSelect(that DOT cpn.arrayBase, cpn.isPrimitive) + !(that DOT cpn.arrayBase DOT cpn.isPrimitive) }, { (thatDepth > 1) })) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/EmitterNames.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/EmitterNames.scala index ba31cd7019..a31366670f 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/EmitterNames.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/EmitterNames.scala @@ -26,7 +26,6 @@ private[emitter] object EmitterNames { // Field names - val dataFieldName = FieldName(ClassClass, SimpleFieldName("data")) val exceptionFieldName = FieldName(JavaScriptExceptionClass, SimpleFieldName("exception")) // Method names diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala index e1e010eeb2..184f46c365 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala @@ -1270,6 +1270,12 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case BinaryOp(BinaryOp.String_charAt, lhs, rhs) => allowBehavior(semantics.stringIndexOutOfBounds) && test(lhs) && test(rhs) + // Binary Class_x operations that can have side effects + case BinaryOp(BinaryOp.Class_cast, lhs, rhs) => + allowBehavior(semantics.asInstanceOfs) && test(lhs) && test(rhs) + case BinaryOp(BinaryOp.Class_newArray, lhs, rhs) => + allowBehavior(semantics.negativeArraySizes) && allowUnpure && test(lhs) && test(rhs) + // Expressions preserving pureness (modulo NPE) case Block(trees) => trees forall test case If(cond, thenp, elsep) => test(cond) && test(thenp) && test(elsep) @@ -2385,6 +2391,20 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { newLhs else genCallHelper(VarField.n, newLhs) + + // Class operations + case Class_name => + genGetDataOf(newLhs) DOT cpn.name + case Class_isPrimitive => + genGetDataOf(newLhs) DOT cpn.isPrimitive + case Class_isInterface => + genGetDataOf(newLhs) DOT cpn.isInterface + case Class_isArray => + genGetDataOf(newLhs) DOT cpn.isArrayClass + case Class_componentType => + js.Apply(genGetDataOf(newLhs) DOT cpn.getComponentType, Nil) + case Class_superClass => + js.Apply(genGetDataOf(newLhs) DOT cpn.getSuperclass, Nil) } case BinaryOp(op, lhs, rhs) => @@ -2392,6 +2412,11 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { val newLhs = transformExpr(lhs, preserveChar = (op == String_+)) val newRhs = transformExpr(rhs, preserveChar = (op == String_+)) + def extractClassData(origTree: Tree, jsTree: js.Tree): js.Tree = origTree match { + case ClassOf(typeRef) => genClassDataOf(typeRef)(implicitly, implicitly, origTree.pos) + case _ => genGetDataOf(jsTree) + } + (op: @switch) match { case === | !== => /* Semantically, this is an `Object.is` test in JS. However, we @@ -2664,10 +2689,29 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case CheckedBehavior.Unchecked => js.Apply(genIdentBracketSelect(newLhs, "charCodeAt"), List(newRhs)) } + + case Class_isInstance => + js.Apply(extractClassData(lhs, newLhs) DOT cpn.isInstance, newRhs :: Nil) + case Class_isAssignableFrom => + js.Apply(extractClassData(lhs, newLhs) DOT cpn.isAssignableFrom, + extractClassData(rhs, newRhs) :: Nil) + case Class_cast => + if (semantics.asInstanceOfs == CheckedBehavior.Unchecked) + js.Block(newLhs, newRhs) + else + js.Apply(extractClassData(lhs, newLhs) DOT cpn.cast, newRhs :: Nil) + case Class_newArray => + js.Apply(extractClassData(lhs, newLhs) DOT cpn.newArray, newRhs :: Nil) } case NewArray(typeRef, lengths) => - genNewArray(typeRef, lengths.map(transformExprNoChar)) + lengths match { + case length :: Nil => + js.New(genArrayConstrOf(typeRef), transformExprNoChar(length) :: Nil) + case _ => + throw new AssertionError( + s"Illegal legacy NewArray with lengths $lengths at $pos") + } case ArrayValue(typeRef, elems) => val preserveChar = typeRef match { @@ -2769,8 +2813,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case Transient(ZeroOf(runtimeClass)) => js.DotSelect( - genSelect(transformExprNoChar(checkNotNull(runtimeClass)), - FieldIdent(dataFieldName)), + genGetDataOf(transformExprNoChar(checkNotNull(runtimeClass))), js.Ident(cpn.zero)) case Transient(NativeArrayWrapper(elemClass, nativeArray)) => @@ -2781,9 +2824,8 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { extractWithGlobals( genNativeArrayWrapper(arrayTypeRef, newNativeArray)) case _ => - val elemClassData = genSelect( - transformExprNoChar(checkNotNull(elemClass)), - FieldIdent(dataFieldName)) + val elemClassData = + genGetDataOf(transformExprNoChar(checkNotNull(elemClass))) val arrayClassData = js.Apply( js.DotSelect(elemClassData, js.Ident(cpn.getArrayOf)), Nil) js.Apply(arrayClassData DOT cpn.wrapArray, newNativeArray :: Nil) @@ -3247,6 +3289,9 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { js.Apply(globalVar(field, (className, method.name)), args) } + private def genGetDataOf(jlClassValue: js.Tree)(implicit pos: Position): js.Tree = + genSyntheticPropSelect(jlClassValue, SyntheticProperty.data) + private def genCallPolyfillableBuiltin( builtin: PolyfillableBuiltin, args: js.Tree*)( implicit pos: Position): js.Tree = { diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/KnowledgeGuardian.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/KnowledgeGuardian.scala index 562ad1e519..d5c5d23342 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/KnowledgeGuardian.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/KnowledgeGuardian.scala @@ -98,11 +98,12 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { val invalidateAll = { if (specialInfo == null) { specialInfo = new SpecialInfo(objectClass, classClass, - arithmeticExceptionClass, hijackedClasses.result()) + arithmeticExceptionClass, hijackedClasses.result(), + moduleSet.globalInfo) false } else { specialInfo.update(objectClass, classClass, arithmeticExceptionClass, - hijackedClasses.result()) + hijackedClasses.result(), moduleSet.globalInfo) } } @@ -512,7 +513,8 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { private class SpecialInfo(initObjectClass: Option[LinkedClass], initClassClass: Option[LinkedClass], initArithmeticExceptionClass: Option[LinkedClass], - initHijackedClasses: Iterable[LinkedClass]) extends Unregisterable { + initHijackedClasses: Iterable[LinkedClass], + initGlobalInfo: LinkedGlobalInfo) extends Unregisterable { import SpecialInfo._ @@ -520,7 +522,7 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { computeInstantiatedSpecialClassBitSet(initClassClass, initArithmeticExceptionClass) private var isParentDataAccessed = - computeIsParentDataAccessed(initClassClass) + computeIsParentDataAccessed(initGlobalInfo) private var methodsInRepresentativeClasses = computeMethodsInRepresentativeClasses(initObjectClass, initHijackedClasses) @@ -539,7 +541,8 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { def update(objectClass: Option[LinkedClass], classClass: Option[LinkedClass], arithmeticExceptionClass: Option[LinkedClass], - hijackedClasses: Iterable[LinkedClass]): Boolean = { + hijackedClasses: Iterable[LinkedClass], + globalInfo: LinkedGlobalInfo): Boolean = { var invalidateAll = false val newInstantiatedSpecialClassBitSet = @@ -549,7 +552,7 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { invalidateAskers(instantiatedSpecialClassAskers) } - val newIsParentDataAccessed = computeIsParentDataAccessed(classClass) + val newIsParentDataAccessed = computeIsParentDataAccessed(globalInfo) if (newIsParentDataAccessed != isParentDataAccessed) { isParentDataAccessed = newIsParentDataAccessed invalidateAll = true @@ -589,16 +592,8 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { bitSet } - private def computeIsParentDataAccessed(classClass: Option[LinkedClass]): Boolean = { - def methodExists(linkedClass: LinkedClass, methodName: MethodName): Boolean = { - linkedClass.methods.exists { m => - m.flags.namespace == MemberNamespace.Public && - m.methodName == methodName - } - } - - classClass.exists(methodExists(_, getSuperclassMethodName)) - } + private def computeIsParentDataAccessed(globalInfo: LinkedGlobalInfo): Boolean = + globalInfo.isClassSuperClassUsed private def computeMethodsInRepresentativeClasses(objectClass: Option[LinkedClass], hijackedClasses: Iterable[LinkedClass]): List[(MethodName, Set[ClassName])] = { diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SJSGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SJSGen.scala index ef7aaf8ea8..44f8708265 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SJSGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SJSGen.scala @@ -55,7 +55,7 @@ private[emitter] final class SJSGen( /** `Char.c`: the int value of the character. */ val c = "c" - // --- TypeData private fields --- + // --- TypeData fields --- /** `TypeData.constr`: the run-time constructor of the class. */ val constr = if (minify) "C" else "constr" @@ -96,6 +96,18 @@ private[emitter] final class SJSGen( /** `TypeData.isJSType`: whether it is a JS type. */ val isJSType = if (minify) "J" else "isJSType" + /** `TypeData.name`: the user name of the class (the result of `jl.Class.getName()`). */ + val name = if (minify) "N" else "name" + + /** `TypeData.isPrimitive`: whether it is a primitive type. */ + val isPrimitive = if (minify) "X" else "isPrimitive" + + /** `TypeData.isInterface`: whether it is an interface type. */ + val isInterface = if (minify) "Y" else "isInterface" + + /** `TypeData.isArrayClass`: whether it is an array type. */ + val isArrayClass = if (minify) "Z" else "isArrayClass" + // --- TypeData constructors --- val initPrim = if (minify) "p" else "initPrim" @@ -106,7 +118,7 @@ private[emitter] final class SJSGen( val initArray = if (minify) "a" else "initArray" - // --- TypeData private methods --- + // --- TypeData methods --- /** `TypeData.getArrayOf()`: the `Type` instance for that type's array type. */ val getArrayOf = if (minify) "r" else "getArrayOf" @@ -114,35 +126,23 @@ private[emitter] final class SJSGen( /** `TypeData.getClassOf()`: the `jl.Class` instance for that type. */ val getClassOf = if (minify) "l" else "getClassOf" - // --- TypeData public fields --- never minified - - /** `TypeData.name`: public, the user name of the class (the result of `jl.Class.getName()`). */ - val name = "name" - - /** `TypeData.isPrimitive`: public, whether it is a primitive type. */ - val isPrimitive = "isPrimitive" - - /** `TypeData.isInterface`: public, whether it is an interface type. */ - val isInterface = "isInterface" - - /** `TypeData.isArrayClass`: public, whether it is an array type. */ - val isArrayClass = "isArrayClass" + /** `TypeData.getSuperclass()`: implementation of `Class_superClass`. */ + val getSuperclass = if (minify) "S" else "getSuperclass" - /** `TypeData.isInstance()`: public, implementation of `jl.Class.isInstance`. */ - val isInstance = "isInstance" + /** `TypeData.getComponentType()`: implementation of `Class_componentType`. */ + val getComponentType = if (minify) "Q" else "getComponentType" - /** `TypeData.isAssignableFrom()`: public, implementation of `jl.Class.isAssignableFrom`. */ - val isAssignableFrom = "isAssignableFrom" + /** `TypeData.isInstance()`: implementation of `Class_isInstance`. */ + val isInstance = if (minify) "I" else "isInstance" - // --- TypeData public methods --- never minified + /** `TypeData.isAssignableFrom()`: implementation of `Class_isAssignableFrom`. */ + val isAssignableFrom = if (minify) "R" else "isAssignableFrom" - val checkCast = "checkCast" + /** `TypeData.cast()`: implementation of `Class_cast`. */ + val cast = if (minify) "T" else "cast" - val getSuperclass = "getSuperclass" - - val getComponentType = "getComponentType" - - val newArrayOfThisClass = "newArrayOfThisClass" + /** `TypeData.newArray()`: implementation of `Class_newArray`. */ + val newArray = if (minify) "U" else "newArray" } /* This is a `val` because it is used at the top of every file, outside of @@ -714,21 +714,6 @@ private[emitter] final class SJSGen( } } - def genNewArray(arrayTypeRef: ArrayTypeRef, lengths: List[Tree])( - implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, - pos: Position): Tree = { - lengths match { - case Nil => - throw new IllegalArgumentException( - "Cannot create a new array with 0 dimensions") - case length :: Nil => - New(genArrayConstrOf(arrayTypeRef), length :: Nil) - case _ => - genCallHelper(VarField.newArrayObject, genClassDataOf(arrayTypeRef), - ArrayConstr(lengths)) - } - } - def genArrayValue(arrayTypeRef: ArrayTypeRef, elems: List[Tree])( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, tracking: GlobalRefTracking, pos: Position): WithGlobals[Tree] = { diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SyntheticProperty.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SyntheticProperty.scala index 84afaceff1..6e697988de 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SyntheticProperty.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/SyntheticProperty.scala @@ -20,7 +20,8 @@ import org.scalajs.ir.OriginalName * but is added by the Emitter on some classes that belong to the Scala class * hierarchy. * - * Currently, these only include properties of the `ArrayClass`es. + * Currently, these only include properties of the `ArrayClass`es, as well as + * the magic `jl.Class.data` property. * * These properties live in the same namespace as Scala field and method * names, because the `ArrayClass`es extend `j.l.Object`. Therefore, they @@ -49,4 +50,7 @@ object SyntheticProperty { /** `ArrayClass.copyTo()`: copies from that array to another array. */ val copyTo: SyntheticProperty = new SyntheticProperty("copyTo") + + /** `jl.Class.data`: the underlying `TypeData` of the class. */ + val data: SyntheticProperty = new SyntheticProperty("data") } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarField.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarField.scala index e166c997c5..a43a127f61 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarField.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarField.scala @@ -132,6 +132,9 @@ private[emitter] object VarField { /** Local field for dynamic imports. */ final val module = mk("$module") + /** Local field for the magic `data` argument to the constructor of `jl.Class`. */ + final val data = mk("$data") + // Core fields: Generated by the CoreJSLib /** The linking info object. */ @@ -231,10 +234,6 @@ private[emitter] object VarField { final val systemArraycopyFull = mk("$systemArraycopyFull") - final val newArrayObject = mk("$newArrayObject") - - final val newArrayObjectInternal = mk("$newArrayObjectInternal") - final val throwArrayCastException = mk("$throwArrayCastException") final val throwArrayIndexOutOfBoundsException = mk("$throwArrayIndexOutOFBoundsException") diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/ClassEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/ClassEmitter.scala index caa266fd10..a4ff9ceff4 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/ClassEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/ClassEmitter.scala @@ -365,9 +365,20 @@ class ClassEmitter(coreSpec: CoreSpec) { isMutable = true // initialized by the constructors, so always mutable at the Wasm level ) } + val jlClassDataField = if (className == ClassClass) { + // Inject the magic `data` field + watpe.StructField( + genFieldID.classData, + OriginalName("data"), + watpe.RefType(genTypeID.typeData), + isMutable = false + ) :: Nil + } else { + Nil + } val structTypeID = genTypeID.forClass(className) val superType = clazz.superClass.map(s => genTypeID.forClass(s.name)) - val structType = watpe.StructType(vtableField :: itablesField :: fields) + val structType = watpe.StructType(vtableField :: itablesField :: fields ::: jlClassDataField) val subType = watpe.SubType( structTypeID, makeDebugName(ns.ClassInstance, className), @@ -598,6 +609,9 @@ class ClassEmitter(coreSpec: CoreSpec) { makeDebugName(ns.NewDefault, className), clazz.pos ) + val dataParamOpt = + if (className == ClassClass) Some(fb.addParam("data", watpe.RefType(genTypeID.typeData))) + else None fb.setResultType(watpe.RefType(structTypeID)) fb += wa.GlobalGet(genGlobalID.forVTable(className)) @@ -610,6 +624,8 @@ class ClassEmitter(coreSpec: CoreSpec) { classInfo.allFieldDefs.foreach { f => fb += genZeroOf(f.ftpe) } + for (dataParam <- dataParamOpt) + fb += wa.LocalGet(dataParam) fb += wa.StructNew(structTypeID) fb.buildAndAddToModule() diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/CoreWasmLib.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/CoreWasmLib.scala index 4da2acaff9..90dfbb16ad 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/CoreWasmLib.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/CoreWasmLib.scala @@ -94,6 +94,17 @@ final class CoreWasmLib(coreSpec: CoreSpec) { ) } + private val primRefsWithArrayTypes = List( + BooleanRef -> KindBoolean, + CharRef -> KindChar, + ByteRef -> KindByte, + ShortRef -> KindShort, + IntRef -> KindInt, + LongRef -> KindLong, + FloatRef -> KindFloat, + DoubleRef -> KindDouble + ) + /** Generates definitions that must come *before* the code generated for regular classes. * * This notably includes the `typeData` definitions, since the vtable of `jl.Object` is a subtype @@ -421,25 +432,7 @@ final class CoreWasmLib(coreSpec: CoreSpec) { Nil ) - addHelperImport( - genFunctionID.genJSTypeMetaData, - List( - RefType(genTypeID.typeData), // typeData - RefType.extern, // name - Int32, // isPrimitive - Int32, // isArrayClass - Int32, // isInterface, - RefType.func, // isInstanceFun - RefType.func, // isAssignableFromFun - RefType.func, // checkCastFun - RefType.func, // getComponentTypeFun - RefType.func // newArrayOfThisClassFun - ), - List(RefType.any) - ) addHelperImport(genFunctionID.makeTypeError, List(RefType.extern), List(RefType.extern)) - addHelperImport(genFunctionID.jsArrayLength, List(anyref), List(Int32)) - addHelperImport(genFunctionID.jsArrayGetInt, List(anyref, Int32), List(Int32)) addHelperImport(genFunctionID.jsGlobalRefGet, List(RefType.extern), List(anyref)) addHelperImport(genFunctionID.jsGlobalRefSet, List(RefType.extern, anyref), Nil) @@ -699,13 +692,13 @@ final class CoreWasmLib(coreSpec: CoreSpec) { genIsInstance() genIsAssignableFrom() - genCheckCast() + if (semantics.asInstanceOfs != CheckedBehavior.Unchecked) + genCast() genGetComponentType() - genNewArrayOfThisClass() + genNewArray() genAnyGetClass() genAnyGetClassName() genAnyGetTypeData() - genNewArrayObject() genIdentityHashCode() genSearchReflectiveProxy() genArrayCloneFunctions() @@ -1087,51 +1080,18 @@ final class CoreWasmLib(coreSpec: CoreSpec) { val classInstanceLocal = fb.addLocal("classInstance", RefType(genTypeID.ClassStruct)) - // classInstance := newDefault$java.lang.Class() + // classInstance := newDefault$java.lang.Class(typeData) // leave it on the stack for the constructor call + fb += LocalGet(typeDataParam) fb += Call(genFunctionID.newDefault(ClassClass)) fb += LocalTee(classInstanceLocal) - /* The JS object containing metadata to pass as argument to the `jl.Class` constructor. - * Specified by https://lampwww.epfl.ch/~doeraene/sjsir-semantics/#sec-sjsir-createclassdataof - * Leave it on the stack. - */ - - // typeData - fb += LocalGet(typeDataParam) - // name - fb += LocalGet(typeDataParam) - fb += Call(genFunctionID.typeDataName) - // isPrimitive: (typeData.kind <= KindLastPrimitive) - fb += LocalGet(typeDataParam) - fb += StructGet(genTypeID.typeData, genFieldID.typeData.kind) - fb += I32Const(KindLastPrimitive) - fb += I32LeU - // isArrayClass: (typeData.kind == KindArray) - fb += LocalGet(typeDataParam) - fb += StructGet(genTypeID.typeData, genFieldID.typeData.kind) - fb += I32Const(KindArray) - fb += I32Eq - // isInterface: (typeData.kind == KindInterface) - fb += LocalGet(typeDataParam) - fb += StructGet(genTypeID.typeData, genFieldID.typeData.kind) - fb += I32Const(KindInterface) - fb += I32Eq - // the various helper functions - fb += ctx.refFuncWithDeclaration(genFunctionID.isInstance) - fb += ctx.refFuncWithDeclaration(genFunctionID.isAssignableFrom) - fb += ctx.refFuncWithDeclaration(genFunctionID.checkCast) - fb += ctx.refFuncWithDeclaration(genFunctionID.getComponentType) - fb += ctx.refFuncWithDeclaration(genFunctionID.newArrayOfThisClass) - // call the helper to build the metadata object - fb += Call(genFunctionID.genJSTypeMetaData) - - // Call java.lang.Class::(dataObject) + // Call java.lang.Class::() fb += Call( genFunctionID.forMethod( MemberNamespace.Constructor, ClassClass, - SpecialNames.AnyArgConstructorName + NoArgConstructorName ) ) @@ -2335,64 +2295,70 @@ final class CoreWasmLib(coreSpec: CoreSpec) { fb.buildAndAddToModule() } - /** `checkCast: (ref typeData), anyref -> []`. + /** `cast: (ref jlClass), anyref -> anyref`. * * Casts the given value to the given type; subject to undefined behaviors. + * + * This is the underlying func for the `Class_cast` operation. */ - private def genCheckCast()(implicit ctx: WasmContext): Unit = { - val typeDataType = RefType(genTypeID.typeData) + private def genCast()(implicit ctx: WasmContext): Unit = { + assert(semantics.asInstanceOfs != CheckedBehavior.Unchecked) - val fb = newFunctionBuilder(genFunctionID.checkCast) - val typeDataParam = fb.addParam("typeData", typeDataType) + val fb = newFunctionBuilder(genFunctionID.cast) + val jlClassParam = fb.addParam("jlClass", RefType(genTypeID.ClassStruct)) val valueParam = fb.addParam("value", RefType.anyref) + fb.setResultType(RefType.anyref) - if (semantics.asInstanceOfs != CheckedBehavior.Unchecked) { - fb.block() { successLabel => - // If typeData.kind == KindJSType, succeed - fb += LocalGet(typeDataParam) - fb += StructGet(genTypeID.typeData, genFieldID.typeData.kind) - fb += I32Const(KindJSType) - fb += I32Eq - fb += BrIf(successLabel) + val typeDataLocal = fb.addLocal("typeData", RefType(genTypeID.typeData)) - // If value is null, succeed - fb += LocalGet(valueParam) - fb += RefIsNull // consumes `value`, unlike `BrOnNull` which would leave it on the stack - fb += BrIf(successLabel) + fb.block() { successLabel => + // typeData := jlClass.data, leave it on the stack for the kind test + fb += LocalGet(jlClassParam) + fb += StructGet(genTypeID.ClassStruct, genFieldID.classData) + fb += LocalTee(typeDataLocal) - // If isInstance(typeData, value), succeed - fb += LocalGet(typeDataParam) - fb += LocalGet(valueParam) - fb += Call(genFunctionID.isInstance) - fb += BrIf(successLabel) + // If typeData.kind == KindJSType, succeed + fb += StructGet(genTypeID.typeData, genFieldID.typeData.kind) + fb += I32Const(KindJSType) + fb += I32Eq + fb += BrIf(successLabel) - // Otherwise, it is a CCE - fb += LocalGet(valueParam) - fb += LocalGet(typeDataParam) - fb += Call(genFunctionID.classCastException) - fb += Unreachable // for clarity; technically redundant since the stacks align - } + // If value is null, succeed + fb += LocalGet(valueParam) + fb += RefIsNull // consumes `value`, unlike `BrOnNull` which would leave it on the stack + fb += BrIf(successLabel) + + // If isInstance(typeData, value), succeed + fb += LocalGet(typeDataLocal) + fb += LocalGet(valueParam) + fb += Call(genFunctionID.isInstance) + fb += BrIf(successLabel) + + // Otherwise, it is a CCE + fb += LocalGet(valueParam) + fb += LocalGet(typeDataLocal) + fb += Call(genFunctionID.classCastException) + fb += Unreachable // for clarity; technically redundant since the stacks align } + fb += LocalGet(valueParam) + fb.buildAndAddToModule() } - /** `getComponentType: (ref typeData) -> (ref null jlClass)`. + /** `getComponentType: (ref jlClass) -> (ref null jlClass)`. * - * This is the underlying func for the `getComponentType()` closure inside class data objects. + * This is the underlying func for the `Class_componentType` operation. */ private def genGetComponentType()(implicit ctx: WasmContext): Unit = { - val typeDataType = RefType(genTypeID.typeData) - val fb = newFunctionBuilder(genFunctionID.getComponentType) - val typeDataParam = fb.addParam("typeData", typeDataType) + val jlClassParam = fb.addParam("jlClass", RefType(genTypeID.ClassStruct)) fb.setResultType(RefType.nullable(genTypeID.ClassStruct)) - val componentTypeDataLocal = fb.addLocal("componentTypeData", typeDataType) - fb.block() { nullResultLabel => // Try and extract non-null component type data - fb += LocalGet(typeDataParam) + fb += LocalGet(jlClassParam) + fb += StructGet(genTypeID.ClassStruct, genFieldID.classData) fb += StructGet(genTypeID.typeData, genFieldID.typeData.componentType) fb += BrOnNull(nullResultLabel) // Get the corresponding classOf @@ -2404,68 +2370,72 @@ final class CoreWasmLib(coreSpec: CoreSpec) { fb.buildAndAddToModule() } - /** `newArrayOfThisClass: (ref typeData), anyref -> (ref jlObject)`. + /** `newArray: (ref jlClass), i32 -> (ref jlObject)`. * - * This is the underlying func for the `newArrayOfThisClass()` closure inside class data objects. + * This is the underlying func for the `Class_newArray` operation. */ - private def genNewArrayOfThisClass()(implicit ctx: WasmContext): Unit = { + private def genNewArray()(implicit ctx: WasmContext): Unit = { val typeDataType = RefType(genTypeID.typeData) - val i32ArrayType = RefType(genTypeID.i32Array) + val arrayTypeDataType = RefType(genTypeID.ObjectVTable) - val fb = newFunctionBuilder(genFunctionID.newArrayOfThisClass) - val typeDataParam = fb.addParam("typeData", typeDataType) - val lengthsParam = fb.addParam("lengths", RefType.anyref) + val fb = newFunctionBuilder(genFunctionID.newArray) + val jlClassParam = fb.addParam("jlClass", RefType(genTypeID.ClassStruct)) + val lengthParam = fb.addParam("length", Int32) fb.setResultType(RefType(genTypeID.ObjectStruct)) - val lengthsLenLocal = fb.addLocal("lengthsLenLocal", Int32) - val lengthsValuesLocal = fb.addLocal("lengthsValues", i32ArrayType) - val iLocal = fb.addLocal("i", Int32) - - // lengthsLen := lengths.length // as a JS field access, through a helper - fb += LocalGet(lengthsParam) - fb += Call(genFunctionID.jsArrayLength) - fb += LocalTee(lengthsLenLocal) - - // lengthsValues := array.new lengthsLen - fb += ArrayNewDefault(genTypeID.i32Array) - fb += LocalSet(lengthsValuesLocal) - - // i := 0 - fb += I32Const(0) - fb += LocalSet(iLocal) + val componentTypeDataLocal = fb.addLocal("componentTypeData", RefType(genTypeID.typeData)) - // while (i != lengthsLen) - fb.whileLoop() { - fb += LocalGet(iLocal) - fb += LocalGet(lengthsLenLocal) - fb += I32Ne - } { - // lengthsValue[i] := lengths[i] (where the rhs is a JS field access, though a helper) + // Check negative array size + if (semantics.negativeArraySizes != CheckedBehavior.Unchecked) { + fb += LocalGet(lengthParam) + fb += I32Const(0) + fb += I32LtS + fb.ifThen() { + fb += LocalGet(lengthParam) + fb += Call(genFunctionID.throwNegativeArraySizeException) + fb += Unreachable + } + } - fb += LocalGet(lengthsValuesLocal) - fb += LocalGet(iLocal) + // componentTypeData := jlClass.data + fb += LocalGet(jlClassParam) + fb += StructGet(genTypeID.ClassStruct, genFieldID.classData) + fb += LocalTee(componentTypeDataLocal) - fb += LocalGet(lengthsParam) - fb += LocalGet(iLocal) - fb += Call(genFunctionID.jsArrayGetInt) + // Load the vtable and itables of the ArrayClass instance we will create + fb += I32Const(1) + fb += Call(genFunctionID.arrayTypeData) // vtable + fb += GlobalGet(genGlobalID.arrayClassITable) // itables - fb += ArraySet(genTypeID.i32Array) + // Load the length + fb += LocalGet(lengthParam) - // i += 1 - fb += LocalGet(iLocal) - fb += I32Const(1) - fb += I32Add - fb += LocalSet(iLocal) + // switch (componentTypeData.kind) + val switchClauseSig = FunctionType( + List(arrayTypeDataType, RefType(genTypeID.itables), Int32), + List(RefType(genTypeID.ObjectStruct)) + ) + fb.switch(switchClauseSig) { () => + // scrutinee + fb += LocalGet(componentTypeDataLocal) + fb += StructGet(genTypeID.typeData, genFieldID.typeData.kind) + }( + // case KindPrim => array.new_default underlyingPrimArray; struct.new PrimArray + primRefsWithArrayTypes.map { case (primRef, kind) => + List(kind) -> { () => + val arrayTypeRef = ArrayTypeRef(primRef, 1) + fb += ArrayNewDefault(genTypeID.underlyingOf(arrayTypeRef)) + fb += StructNew(genTypeID.forArrayClass(arrayTypeRef)) + () // required for correct type inference + } + }: _* + ) { () => + // case _ => array.new_default anyrefArray; struct.new ObjectArray + val arrayTypeRef = ArrayTypeRef(ClassRef(ObjectClass), 1) + fb += ArrayNewDefault(genTypeID.underlyingOf(arrayTypeRef)) + fb += StructNew(genTypeID.forArrayClass(arrayTypeRef)) } - // return newArrayObject(arrayTypeData(typeData, lengthsLen), lengthsValues, 0) - fb += LocalGet(typeDataParam) - fb += LocalGet(lengthsLenLocal) - fb += Call(genFunctionID.arrayTypeData) - fb += LocalGet(lengthsValuesLocal) - fb += I32Const(0) - fb += Call(genFunctionID.newArrayObject) - fb.buildAndAddToModule() } @@ -2676,187 +2646,6 @@ final class CoreWasmLib(coreSpec: CoreSpec) { fb.buildAndAddToModule() } - /** `newArrayObject`: `(ref typeData), (ref array i32), i32 -> (ref jl.Object)`. - * - * The arguments are `arrayTypeData`, `lengths` and `lengthIndex`. - * - * This recursive function creates a multi-dimensional array. The resulting array has type data - * `arrayTypeData` and length `lengths(lengthIndex)`. If `lengthIndex < `lengths.length - 1`, its - * elements are recursively initialized with `newArrayObject(arrayTypeData.componentType, - * lengths, lengthIndex - 1)`. - */ - private def genNewArrayObject()(implicit ctx: WasmContext): Unit = { - import genFieldID.typeData._ - - val typeDataType = RefType(genTypeID.typeData) - val i32ArrayType = RefType(genTypeID.i32Array) - val objectVTableType = RefType(genTypeID.ObjectVTable) - val arrayTypeDataType = objectVTableType - val itablesType = RefType(genTypeID.itables) - val nonNullObjectType = RefType(genTypeID.ObjectStruct) - val anyArrayType = RefType(genTypeID.anyArray) - - val fb = newFunctionBuilder(genFunctionID.newArrayObject) - val arrayTypeDataParam = fb.addParam("arrayTypeData", arrayTypeDataType) - val lengthsParam = fb.addParam("lengths", i32ArrayType) - val lengthIndexParam = fb.addParam("lengthIndex", Int32) - fb.setResultType(nonNullObjectType) - - val lenLocal = fb.addLocal("len", Int32) - val underlyingLocal = fb.addLocal("underlying", anyArrayType) - val subLengthIndexLocal = fb.addLocal("subLengthIndex", Int32) - val arrayComponentTypeDataLocal = fb.addLocal("arrayComponentTypeData", arrayTypeDataType) - val iLocal = fb.addLocal("i", Int32) - - /* High-level pseudo code of what this function does: - * - * def newArrayObject(arrayTypeData, lengths, lengthIndex) { - * // create an array of the right primitive type - * val len = lengths(lengthIndex) - * // possibly: check negative array size - * switch (arrayTypeData.componentType.kind) { - * // for primitives, return without recursion - * case KindBoolean => new Array[Boolean](len) - * ... - * case KindDouble => new Array[Double](len) - * - * // for reference array types, maybe recursively initialize - * case _ => - * val result = new Array[Object](len) // with arrayTypeData as vtable - * val subLengthIndex = lengthIndex + 1 - * if (subLengthIndex != lengths.length) { - * val arrayComponentTypeData = arrayTypeData.componentType - * for (i <- 0 until len) - * result(i) = newArrayObject(arrayComponentTypeData, lengths, subLengthIndex) - * } - * result - * } - * } - */ - - val primRefsWithArrayTypes = List( - BooleanRef -> KindBoolean, - CharRef -> KindChar, - ByteRef -> KindByte, - ShortRef -> KindShort, - IntRef -> KindInt, - LongRef -> KindLong, - FloatRef -> KindFloat, - DoubleRef -> KindDouble - ) - - // Load the vtable and itable of the resulting array on the stack - fb += LocalGet(arrayTypeDataParam) // vtable - fb += GlobalGet(genGlobalID.arrayClassITable) // itable - - // Load the first length - fb += LocalGet(lengthsParam) - fb += LocalGet(lengthIndexParam) - fb += ArrayGet(genTypeID.i32Array) - - // Check negative array size - if (semantics.negativeArraySizes != CheckedBehavior.Unchecked) { - fb += LocalTee(lenLocal) - fb += I32Const(0) - fb += I32LtS - fb.ifThen() { - fb += LocalGet(lenLocal) - fb += Call(genFunctionID.throwNegativeArraySizeException) - fb += Unreachable - } - fb += LocalGet(lenLocal) - } - - // componentTypeData := ref_as_non_null(arrayTypeData.componentType) - // switch (componentTypeData.kind) - val switchClauseSig = FunctionType( - List(arrayTypeDataType, itablesType, Int32), - List(nonNullObjectType) - ) - fb.switch(switchClauseSig) { () => - // scrutinee - fb += LocalGet(arrayTypeDataParam) - fb += StructGet(genTypeID.typeData, genFieldID.typeData.componentType) - fb += StructGet(genTypeID.typeData, genFieldID.typeData.kind) - }( - // For all the primitive types, by construction, this is the bottom dimension - // case KindPrim => array.new_default underlyingPrimArray; struct.new PrimArray - primRefsWithArrayTypes.map { case (primRef, kind) => - List(kind) -> { () => - val arrayTypeRef = ArrayTypeRef(primRef, 1) - fb += ArrayNewDefault(genTypeID.underlyingOf(arrayTypeRef)) - fb += StructNew(genTypeID.forArrayClass(arrayTypeRef)) - () // required for correct type inference - } - }: _* - ) { () => - // default -- all non-primitive array types - - // len := (which is the first length) - fb += LocalTee(lenLocal) - - // underlying := array.new_default anyArray - val arrayTypeRef = ArrayTypeRef(ClassRef(ObjectClass), 1) - fb += ArrayNewDefault(genTypeID.underlyingOf(arrayTypeRef)) - fb += LocalSet(underlyingLocal) - - // subLengthIndex := lengthIndex + 1 - fb += LocalGet(lengthIndexParam) - fb += I32Const(1) - fb += I32Add - fb += LocalTee(subLengthIndexLocal) - - // if subLengthIndex != lengths.length - fb += LocalGet(lengthsParam) - fb += ArrayLen - fb += I32Ne - fb.ifThen() { - // then, recursively initialize all the elements - - // arrayComponentTypeData := ref_cast arrayTypeData.componentTypeData - fb += LocalGet(arrayTypeDataParam) - fb += StructGet(genTypeID.typeData, genFieldID.typeData.componentType) - fb += RefCast(RefType(arrayTypeDataType.heapType)) - fb += LocalSet(arrayComponentTypeDataLocal) - - // i := 0 - fb += I32Const(0) - fb += LocalSet(iLocal) - - // while (i != len) - fb.whileLoop() { - fb += LocalGet(iLocal) - fb += LocalGet(lenLocal) - fb += I32Ne - } { - // underlying[i] := newArrayObject(arrayComponentType, lengths, subLengthIndex) - - fb += LocalGet(underlyingLocal) - fb += LocalGet(iLocal) - - fb += LocalGet(arrayComponentTypeDataLocal) - fb += LocalGet(lengthsParam) - fb += LocalGet(subLengthIndexLocal) - fb += Call(genFunctionID.newArrayObject) - - fb += ArraySet(genTypeID.anyArray) - - // i += 1 - fb += LocalGet(iLocal) - fb += I32Const(1) - fb += I32Add - fb += LocalSet(iLocal) - } - } - - // load underlying; struct.new ObjectArray - fb += LocalGet(underlyingLocal) - fb += StructNew(genTypeID.forArrayClass(arrayTypeRef)) - } - - fb.buildAndAddToModule() - } - /** `identityHashCode`: `anyref -> i32`. * * This is the implementation of `IdentityHashCode`. It is also used to compute the `hashCode()` diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Emitter.scala index 93b00423de..dca083127a 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Emitter.scala @@ -431,7 +431,7 @@ object Emitter { }, // TODO Ideally we should not require these, but rather adapt to their absence - instantiateClass(ClassClass, AnyArgConstructorName), + instantiateClass(ClassClass, NoArgConstructorName), instantiateClass(JSExceptionClass, AnyArgConstructorName), // See genIdentityHashCode in HelperFunctions diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala index 9626403260..fbe918f44b 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala @@ -1438,6 +1438,32 @@ private class FunctionEmitter private ( // Null check case CheckNotNull => genAsNonNullOrNPEFor(lhs) + + // Class operations + case Class_name => + fb += wa.StructGet(genTypeID.ClassStruct, genFieldID.classData) + fb += wa.Call(genFunctionID.typeDataName) + case Class_isPrimitive => + fb += wa.StructGet(genTypeID.ClassStruct, genFieldID.classData) + fb += wa.StructGet(genTypeID.typeData, genFieldID.typeData.kind) + fb += wa.I32Const(KindLastPrimitive) + fb += wa.I32LeU + case Class_isInterface => + fb += wa.StructGet(genTypeID.ClassStruct, genFieldID.classData) + fb += wa.StructGet(genTypeID.typeData, genFieldID.typeData.kind) + fb += wa.I32Const(KindInterface) + fb += wa.I32Eq + case Class_isArray => + fb += wa.StructGet(genTypeID.ClassStruct, genFieldID.classData) + fb += wa.StructGet(genTypeID.typeData, genFieldID.typeData.kind) + fb += wa.I32Const(KindArray) + fb += wa.I32Eq + case Class_componentType => + fb += wa.Call(genFunctionID.getComponentType) + case Class_superClass => + // FIXME Implement this + fb += wa.Drop + fb += wa.RefNull(watpe.HeapType.None) } tree.tpe @@ -1457,6 +1483,20 @@ private class FunctionEmitter private ( LongType } + /* Gen the given tree of type `jl.Class!` then extract its `classData`. + * If the arg is a literal `ClassOf`, we directly generate its type data. + */ + def genTreeClassData(arg: Tree): Unit = { + arg match { + case ClassOf(typeRef) => + markPosition(arg) + genLoadTypeData(fb, typeRef) + case _ => + genTreeAuto(arg) + fb += wa.StructGet(genTypeID.ClassStruct, genFieldID.classData) + } + } + (op: @switch) match { case === | !== => genEq(tree) @@ -1535,6 +1575,32 @@ private class FunctionEmitter private ( fb += wa.Call(genFunctionID.checkedStringCharAt) CharType + // Class operations for which genTreeAuto would not do the right thing + case Class_isInstance => + genTreeClassData(lhs) + genTree(rhs, AnyType) + markPosition(tree) + fb += wa.Call(genFunctionID.isInstance) + BooleanType + case Class_isAssignableFrom => + genTreeClassData(lhs) + genTreeClassData(rhs) + markPosition(tree) + fb += wa.Call(genFunctionID.isAssignableFrom) + BooleanType + case Class_cast => + if (semantics.asInstanceOfs != CheckedBehavior.Unchecked) { + genTreeAuto(lhs) + genTree(rhs, AnyType) + markPosition(tree) + fb += wa.Call(genFunctionID.cast) + AnyType + } else { + genTree(lhs, NoType) + genTreeAuto(rhs) + rhs.tpe + } + case _ => genTreeAuto(lhs) genTreeAuto(rhs) @@ -1689,6 +1755,8 @@ private class FunctionEmitter private ( case Double_<= => wa.F64Le case Double_> => wa.F64Gt case Double_>= => wa.F64Ge + + case Class_newArray => wa.Call(genFunctionID.newArray) } } @@ -2873,68 +2941,41 @@ private class FunctionEmitter private ( } private def genNewArray(tree: NewArray): Type = { - val NewArray(arrayTypeRef, lengths) = tree - - if (lengths.isEmpty || lengths.size > arrayTypeRef.dimensions) { - throw new AssertionError( - s"invalid lengths ${tree.lengths} for array type ${arrayTypeRef.displayName}") - } + val NewArray(arrayTypeRef, length :: Nil) = tree: @unchecked markPosition(tree) - if (lengths.size == 1) { - genLoadVTableAndITableForArray(fb, arrayTypeRef) + genLoadVTableAndITableForArray(fb, arrayTypeRef) - // Create the underlying array - genTree(lengths.head, IntType) - markPosition(tree) + // Create the underlying array + genTree(length, IntType) + markPosition(tree) - if (semantics.negativeArraySizes != CheckedBehavior.Unchecked) { - lengths.head match { - case IntLiteral(lengthValue) if lengthValue >= 0 => - () // always good - case _ => - // if length < 0 - val lengthLocal = addSyntheticLocal(watpe.Int32) - fb += wa.LocalTee(lengthLocal) - fb += wa.I32Const(0) - fb += wa.I32LtS - fb.ifThen() { - // then throw NegativeArraySizeException - fb += wa.LocalGet(lengthLocal) - fb += wa.Call(genFunctionID.throwNegativeArraySizeException) - fb += wa.Unreachable - } + if (semantics.negativeArraySizes != CheckedBehavior.Unchecked) { + length match { + case IntLiteral(lengthValue) if lengthValue >= 0 => + () // always good + case _ => + // if length < 0 + val lengthLocal = addSyntheticLocal(watpe.Int32) + fb += wa.LocalTee(lengthLocal) + fb += wa.I32Const(0) + fb += wa.I32LtS + fb.ifThen() { + // then throw NegativeArraySizeException fb += wa.LocalGet(lengthLocal) - } + fb += wa.Call(genFunctionID.throwNegativeArraySizeException) + fb += wa.Unreachable + } + fb += wa.LocalGet(lengthLocal) } + } - val underlyingArrayType = genTypeID.underlyingOf(arrayTypeRef) - fb += wa.ArrayNewDefault(underlyingArrayType) - - // Create the array object - fb += wa.StructNew(genTypeID.forArrayClass(arrayTypeRef)) - } else { - /* There is no Scala source code that produces `NewArray` with more than - * one specified dimension, so this branch is not tested. - * (The underlying function `newArrayObject` is tested as part of - * reflective array instantiations, though.) - */ - - // First arg to `newArrayObject`: the typeData of the array to create - genLoadArrayTypeData(fb, arrayTypeRef) - - // Second arg: an array of the lengths - for (length <- lengths) - genTree(length, IntType) - markPosition(tree) - fb += wa.ArrayNewFixed(genTypeID.i32Array, lengths.size) - - // Third arg: constant 0 (start index inside the array of lengths) - fb += wa.I32Const(0) + val underlyingArrayType = genTypeID.underlyingOf(arrayTypeRef) + fb += wa.ArrayNewDefault(underlyingArrayType) - fb += wa.Call(genFunctionID.newArrayObject) - } + // Create the array object + fb += wa.StructNew(genTypeID.forArrayClass(arrayTypeRef)) tree.tpe } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/LoaderContent.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/LoaderContent.scala index 90ce8400bf..af81fcd54e 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/LoaderContent.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/LoaderContent.scala @@ -167,22 +167,7 @@ const scalaJSHelpers = { idHashCodeSet: (map, obj, value) => map.set(obj, value), // Some support functions for CoreWasmLib - genJSTypeMetaData: (typeData, name, isPrimitive, isArrayClass, isInterface, isInstanceFun, - isAssignableFromFun, checkCastFun, getComponentTypeFun, newArrayOfThisClassFun) => ({ - __typeData: typeData, // (TODO hide this better? although nobody will notice anyway) - "name": name, - "isPrimitive": isPrimitive !== 0, - "isArrayClass": isArrayClass !== 0, - "isInterface": isInterface !== 0, - "isInstance": (value) => isInstanceFun(typeData, value) !== 0, - "isAssignableFrom": (that) => isAssignableFromFun(typeData, that.__typeData) !== 0, - "checkCast": (value) => checkCastFun(typeData, value), - "getComponentType": () => getComponentTypeFun(typeData), - "newArrayOfThisClass": (lengths) => newArrayOfThisClassFun(typeData, lengths), - }), makeTypeError: (msg) => new TypeError(msg), - jsArrayLength: (array) => array.length, - jsArrayGetInt: (array, index) => array[index], // JS interop jsGlobalRefGet: (globalRefName) => (new Function("return " + globalRefName))(), diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/VarGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/VarGen.scala index 893438b561..18659da4a7 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/VarGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/VarGen.scala @@ -156,10 +156,7 @@ object VarGen { case object idHashCodeGet extends JSHelperFunctionID case object idHashCodeSet extends JSHelperFunctionID - case object genJSTypeMetaData extends JSHelperFunctionID case object makeTypeError extends JSHelperFunctionID - case object jsArrayLength extends JSHelperFunctionID - case object jsArrayGetInt extends JSHelperFunctionID case object jsGlobalRefGet extends JSHelperFunctionID case object jsGlobalRefSet extends JSHelperFunctionID @@ -257,13 +254,12 @@ object VarGen { case object throwModuleInitError extends FunctionID case object isInstance extends FunctionID case object isAssignableFrom extends FunctionID - case object checkCast extends FunctionID + case object cast extends FunctionID case object getComponentType extends FunctionID - case object newArrayOfThisClass extends FunctionID + case object newArray extends FunctionID case object anyGetClass extends FunctionID case object anyGetClassName extends FunctionID case object anyGetTypeData extends FunctionID - case object newArrayObject extends FunctionID case object identityHashCode extends FunctionID case object searchReflectiveProxy extends FunctionID @@ -419,6 +415,9 @@ object VarGen { */ case object reflectiveProxies extends FieldID } + + /** The magic `data` field of type `(ref typeData)`, injected into `jl.Class`. */ + case object classData extends FieldID } object genTypeID { diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala index 5da8ba0a6f..45ab1530ca 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala @@ -666,8 +666,8 @@ private final class ClassDefChecker(classDef: ClassDef, case NewArray(typeRef, lengths) => if (lengths.isEmpty) reportError("NewArray must have non-0 dimensions") - if (lengths.size > typeRef.dimensions) - reportError("NewArray dimensions may not exceed its type") + if (lengths.size > 1) + reportError(i"Illegal legacy NewArray with ${lengths.size} dimensions") checkArrayTypeRef(typeRef) checkTrees(lengths, env) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala index 7efab4d3c4..347e56bf34 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala @@ -445,6 +445,9 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { StringType case CheckNotNull => AnyType + case Class_name | Class_isPrimitive | Class_isInterface | + Class_isArray | Class_componentType | Class_superClass => + ClassType(ClassClass, nullable = false) } typecheckExpect(lhs, env, expectedArgType) @@ -471,10 +474,17 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { DoubleType case String_charAt => StringType + case Class_isInstance | Class_isAssignableFrom | Class_cast | + Class_newArray => + ClassType(ClassClass, nullable = false) } val expectedRhsType = (op: @switch) match { - case Long_<< | Long_>>> | Long_>> | String_charAt => IntType - case _ => expectedLhsType + case Long_<< | Long_>>> | Long_>> | String_charAt | Class_newArray => + IntType + case Class_isInstance | Class_cast => + AnyType + case _ => + expectedLhsType } typecheckExpect(lhs, env, expectedLhsType) typecheckExpect(rhs, env, expectedRhsType) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/BaseLinker.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/BaseLinker.scala index f120dff28a..e9756e38e1 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/BaseLinker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/BaseLinker.scala @@ -91,9 +91,14 @@ final class BaseLinker(config: CommonPhaseConfig, checkIR: Boolean) { } yield { val (linkedClassDefs, linkedTopLevelExports) = assembled.unzip + val globalInfo = new LinkedGlobalInfo( + analysis.isClassSuperClassUsed + ) + new LinkingUnit(config.coreSpec, linkedClassDefs.toList, linkedTopLevelExports.flatten.toList, - moduleInitializers.toList) + moduleInitializers.toList, + globalInfo) } } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/LinkingUnit.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/LinkingUnit.scala index dd6f61e0c7..836147cba5 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/LinkingUnit.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/LinkingUnit.scala @@ -20,5 +20,6 @@ final class LinkingUnit private[frontend] ( val coreSpec: CoreSpec, val classDefs: List[LinkedClass], val topLevelExports: List[LinkedTopLevelExport], - val moduleInitializers: List[ModuleInitializer] + val moduleInitializers: List[ModuleInitializer], + val globalInfo: LinkedGlobalInfo ) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/Refiner.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/Refiner.scala index 985023e7ae..df7b262a00 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/Refiner.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/Refiner.scala @@ -58,8 +58,12 @@ final class Refiner(config: CommonPhaseConfig, checkIR: Boolean) { val (linkedClassDefs, linkedTopLevelExports) = assembled.unzip + val globalInfo = new LinkedGlobalInfo( + analysis.isClassSuperClassUsed + ) + new LinkingUnit(config.coreSpec, linkedClassDefs.toList, - linkedTopLevelExports.flatten.toList, moduleInitializers) + linkedTopLevelExports.flatten.toList, moduleInitializers, globalInfo) } irLoader.cleanAfterRun() diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/modulesplitter/ModuleSplitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/modulesplitter/ModuleSplitter.scala index 579d440265..2518679ca1 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/modulesplitter/ModuleSplitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/modulesplitter/ModuleSplitter.scala @@ -45,7 +45,7 @@ final class ModuleSplitter private (analyzer: ModuleAnalyzer) { * We have to do it here, otherwise we break the assumption, that all * non-abstract classes are reached through a static path. */ - new ModuleSet(unit.coreSpec, Nil, Nil) + new ModuleSet(unit.coreSpec, Nil, Nil, unit.globalInfo) } else { val analysis = logger.time("Module Splitter: Analyze Modules") { analyzer.analyze(dependencyInfo) @@ -124,7 +124,7 @@ final class ModuleSplitter private (analyzer: ModuleAnalyzer) { val modules = builders.values.map(_.result()).toList - new ModuleSet(unit.coreSpec, modules, abstractClasses.result()) + new ModuleSet(unit.coreSpec, modules, abstractClasses.result(), unit.globalInfo) } private def publicModuleDependencies( diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala index 82be00b249..034cfe8020 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala @@ -1553,13 +1553,17 @@ private[optimizer] abstract class OptimizerCore( finishTransformStat(lhs) case PreTransBinaryOp(op, lhs, rhs) => - // Here we need to preserve the side-effects of integer division/modulo and String_charAt import BinaryOp._ + implicit val pos = stat.pos + def newLhs = finishTransformStat(lhs) def finishNoSideEffects: Tree = - Block(newLhs, finishTransformStat(rhs))(stat.pos) + Block(newLhs, finishTransformStat(rhs)) + + def finishWithSideEffects: Tree = + BinaryOp(op, finishTransformExpr(lhs), finishTransformExpr(rhs)) (op: @switch) match { case Int_/ | Int_% => @@ -1567,19 +1571,21 @@ private[optimizer] abstract class OptimizerCore( case PreTransLit(IntLiteral(r)) if r != 0 => finishNoSideEffects case _ => - Block(newLhs, BinaryOp(op, IntLiteral(0)(stat.pos), - finishTransformExpr(rhs))(stat.pos))(stat.pos) + Block(newLhs, BinaryOp(op, IntLiteral(0), finishTransformExpr(rhs))) } case Long_/ | Long_% => rhs match { case PreTransLit(LongLiteral(r)) if r != 0L => finishNoSideEffects case _ => - Block(newLhs, BinaryOp(op, LongLiteral(0L)(stat.pos), - finishTransformExpr(rhs))(stat.pos))(stat.pos) + Block(newLhs, BinaryOp(op, LongLiteral(0L), finishTransformExpr(rhs))) } case String_charAt if semantics.stringIndexOutOfBounds != CheckedBehavior.Unchecked => - finishTransformExpr(stat) + finishWithSideEffects + case Class_cast if semantics.asInstanceOfs != CheckedBehavior.Unchecked => + finishWithSideEffects + case Class_newArray if semantics.negativeArraySizes != CheckedBehavior.Unchecked => + finishWithSideEffects case _ => finishNoSideEffects } @@ -1682,36 +1688,11 @@ private[optimizer] abstract class OptimizerCore( } case BinaryOp(op, lhs, rhs) => - // Here we need to preserve the side-effects of integer division/modulo and String_charAt - import BinaryOp._ - - implicit val pos = stat.pos - - def newLhs = keepOnlySideEffects(lhs) - - def finishNoSideEffects: Tree = - Block(newLhs, keepOnlySideEffects(rhs)) - - op match { - case Int_/ | Int_% => - rhs match { - case IntLiteral(r) if r != 0 => - finishNoSideEffects - case _ => - Block(newLhs, BinaryOp(op, IntLiteral(0), rhs)) - } - case Long_/ | Long_% => - rhs match { - case LongLiteral(r) if r != 0L => - finishNoSideEffects - case _ => - Block(newLhs, BinaryOp(op, LongLiteral(0L), rhs)) - } - case String_charAt if semantics.stringIndexOutOfBounds != CheckedBehavior.Unchecked => - stat - case _ => - finishNoSideEffects - } + /* The logic here exceeds the complexity threshold for keeping a copy + * compared to `finishTransformStat` at the Tree level. Instead, we + * convert to a PreTransBinaryOp and delegate to the other function. + */ + finishTransformStat(PreTransBinaryOp(op, lhs.toPreTransform, rhs.toPreTransform)(stat.pos)) case RecordValue(_, elems) => Block(elems.map(keepOnlySideEffects))(stat.pos) @@ -1927,7 +1908,8 @@ private[optimizer] abstract class OptimizerCore( case NotFoundPureSoFar => rec(rhs).mapOrKeepGoingIf(BinaryOp(op, lhs, _)) { (op: @switch) match { - case Int_/ | Int_% | Long_/ | Long_% | String_+ | String_charAt => + case Int_/ | Int_% | Long_/ | Long_% | String_+ | String_charAt | + Class_cast | Class_newArray => false case _ => true @@ -3192,41 +3174,8 @@ private[optimizer] abstract class OptimizerCore( // java.lang.Class - case ClassGetComponentType => - optTReceiver.get match { - case PreTransLit(ClassOf(ArrayTypeRef(base, depth))) => - contTree(ClassOf( - if (depth == 1) base - else ArrayTypeRef(base, depth - 1))) - case PreTransLit(ClassOf(ClassRef(_))) => - contTree(Null()) - case receiver => - default - } - case ClassGetName => optTReceiver.get match { - case PreTransMaybeBlock(bindingsAndStats, PreTransLit(ClassOf(typeRef))) => - def mappedClassName(className: ClassName): String = { - RuntimeClassNameMapperImpl.map( - config.coreSpec.semantics.runtimeClassNameMapper, - className.nameString) - } - - val nameString = typeRef match { - case primRef: PrimRef => - primRef.displayName - case ClassRef(className) => - mappedClassName(className) - case ArrayTypeRef(primRef: PrimRef, dimensions) => - "[" * dimensions + primRef.charCode - case ArrayTypeRef(ClassRef(className), dimensions) => - "[" * dimensions + "L" + mappedClassName(className) + ";" - } - - contTree(finishTransformBindings( - bindingsAndStats, StringLiteral(nameString))) - case PreTransMaybeBlock(bindingsAndStats, PreTransTree(MaybeCast(GetClass(expr)), _)) => contTree(finishTransformBindings( @@ -3236,18 +3185,6 @@ private[optimizer] abstract class OptimizerCore( default } - // java.lang.reflect.Array - - case ArrayNewInstance => - val List(tcomponentType, tlength) = targs - tcomponentType match { - case PreTransTree(ClassOf(elementTypeRef), _) if elementTypeRef != VoidRef => - val arrayTypeRef = ArrayTypeRef.of(elementTypeRef) - contTree(NewArray(arrayTypeRef, List(finishTransformExpr(tlength)))) - case _ => - default - } - // js.special case ObjectLiteral => @@ -3967,11 +3904,65 @@ private[optimizer] abstract class OptimizerCore( case CheckNotNull => checkNotNull(arg) + // Class operations + + case Class_name => + arg match { + case PreTransLit(ClassOf(typeRef)) => + PreTransLit(StringLiteral(constantClassNameOf(typeRef))) + case _ => + default + } + case Class_isPrimitive => + arg match { + case PreTransLit(ClassOf(typeRef)) => + PreTransLit(BooleanLiteral(typeRef.isInstanceOf[PrimRef])) + case _ => + default + } + case Class_isArray => + arg match { + case PreTransLit(ClassOf(typeRef)) => + PreTransLit(BooleanLiteral(typeRef.isInstanceOf[ArrayTypeRef])) + case _ => + default + } + case Class_componentType => + arg match { + case PreTransLit(ClassOf(ArrayTypeRef(base, depth))) => + PreTransLit(ClassOf( + if (depth == 1) base + else ArrayTypeRef(base, depth - 1))) + case PreTransLit(ClassOf(_)) => + PreTransLit(Null()) + case _ => + default + } + case _ => default } } + private def constantClassNameOf(typeRef: TypeRef): String = { + def mappedClassName(className: ClassName): String = { + RuntimeClassNameMapperImpl.map( + config.coreSpec.semantics.runtimeClassNameMapper, + className.nameString) + } + + typeRef match { + case primRef: PrimRef => + primRef.displayName + case ClassRef(className) => + mappedClassName(className) + case ArrayTypeRef(primRef: PrimRef, dimensions) => + "[" * dimensions + primRef.charCode + case ArrayTypeRef(ClassRef(className), dimensions) => + "[" * dimensions + "L" + mappedClassName(className) + ";" + } + } + /** Performs === for two literals. * The result is always known statically. * @@ -4988,6 +4979,30 @@ private[optimizer] abstract class OptimizerCore( case _ => default } + + case Class_isInstance | Class_isAssignableFrom => + /* We could do something clever here if we added a knowledge query to + * turn a TypeRef into a Type. Barring that, we cannot tell whether a + * ClassRef must become an `AnyType` or a `ClassType`. + */ + default + + case Class_cast => + if (semantics.asInstanceOfs == CheckedBehavior.Unchecked) { + PreTransBlock(finishTransformStat(lhs), rhs) + } else { + // Same comment as in isInstance and isAssignableFrom + default + } + + case Class_newArray => + lhs match { + case PreTransLit(ClassOf(elementTypeRef)) if elementTypeRef != VoidRef => + val arrayTypeRef = ArrayTypeRef.of(elementTypeRef) + NewArray(arrayTypeRef, List(finishTransformExpr(rhs))).toPreTransform + case _ => + default + } } } @@ -6519,12 +6534,9 @@ private[optimizer] object OptimizerCore { final val ArrayBuilderZeroOf = MathMaxDouble + 1 final val GenericArrayBuilderResult = ArrayBuilderZeroOf + 1 - final val ClassGetComponentType = GenericArrayBuilderResult + 1 - final val ClassGetName = ClassGetComponentType + 1 + final val ClassGetName = GenericArrayBuilderResult + 1 - final val ArrayNewInstance = ClassGetName + 1 - - final val ObjectLiteral = ArrayNewInstance + 1 + final val ObjectLiteral = ClassGetName + 1 final val ByteArrayToInt8Array = ObjectLiteral + 1 final val ShortArrayToInt16Array = ByteArrayToInt8Array + 1 @@ -6576,12 +6588,8 @@ private[optimizer] object OptimizerCore { m("numberOfLeadingZeros", List(I), I) -> IntegerNLZ ), ClassName("java.lang.Class") -> List( - m("getComponentType", Nil, ClassClassRef) -> ClassGetComponentType, m("getName", Nil, StringClassRef) -> ClassGetName ), - ClassName("java.lang.reflect.Array$") -> List( - m("newInstance", List(ClassClassRef, I), O) -> ArrayNewInstance - ), ClassName("scala.scalajs.js.special.package$") -> List( m("objectLiteral", List(SeqClassRef), JSObjectClassRef) -> ObjectLiteral ) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedGlobalInfo.scala b/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedGlobalInfo.scala new file mode 100644 index 0000000000..86253041af --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedGlobalInfo.scala @@ -0,0 +1,18 @@ +/* + * Scala.js (https://www.scala-js.org/) + * + * Copyright EPFL. + * + * Licensed under Apache License 2.0 + * (https://www.apache.org/licenses/LICENSE-2.0). + * + * See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + */ + +package org.scalajs.linker.standard + +/** Global information about a linked program. */ +final class LinkedGlobalInfo private[linker] ( + val isClassSuperClassUsed: Boolean +) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/standard/ModuleSet.scala b/linker/shared/src/main/scala/org/scalajs/linker/standard/ModuleSet.scala index 849ed527ed..c03557baa9 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/standard/ModuleSet.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/standard/ModuleSet.scala @@ -42,7 +42,9 @@ final class ModuleSet private[linker] ( * * For example, a native JS class that is needed for its load spec. */ - val abstractClasses: List[LinkedClass] + val abstractClasses: List[LinkedClass], + + val globalInfo: LinkedGlobalInfo ) { require(modules.isEmpty || modules.count(_.isRoot) == 1, "Must have exactly one root module") diff --git a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala index 6dd8041406..9ab95541de 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -70,9 +70,9 @@ class LibrarySizeTest { ) testLinkedSizes( - expectedFastLinkSize = 147707, - expectedFullLinkSizeWithoutClosure = 86729, - expectedFullLinkSizeWithClosure = 21768, + expectedFastLinkSize = 147629, + expectedFullLinkSizeWithoutClosure = 85718, + expectedFullLinkSizeWithClosure = 21625, classDefs, moduleInitializers = MainTestModuleInitializers ) diff --git a/project/BinaryIncompatibilities.scala b/project/BinaryIncompatibilities.scala index 3606a501a3..01c38bc201 100644 --- a/project/BinaryIncompatibilities.scala +++ b/project/BinaryIncompatibilities.scala @@ -31,6 +31,7 @@ object BinaryIncompatibilities { // private[linker], not an issue ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.linker.standard.CoreSpec.apply"), + ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.linker.standard.ModuleSet.this"), ) val LinkerInterface = Seq( diff --git a/project/Build.scala b/project/Build.scala index 9cda252d2e..745900effe 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2048,15 +2048,15 @@ object Build { case `default212Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 626000 to 627000, - fullLink = 97000 to 98000, + fastLink = 623000 to 624000, + fullLink = 96000 to 97000, fastLinkGz = 75000 to 79000, fullLinkGz = 25000 to 26000, )) } else { Some(ExpectedSizes( - fastLink = 425000 to 426000, - fullLink = 282000 to 283000, + fastLink = 423000 to 424000, + fullLink = 280000 to 281000, fastLinkGz = 60000 to 61000, fullLinkGz = 43000 to 44000, )) @@ -2065,15 +2065,15 @@ object Build { case `default213Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 451000 to 452000, + fastLink = 449000 to 450000, fullLink = 95000 to 96000, fastLinkGz = 58000 to 59000, - fullLinkGz = 26000 to 27000, + fullLinkGz = 25000 to 26000, )) } else { Some(ExpectedSizes( - fastLink = 306000 to 307000, - fullLink = 262000 to 263000, + fastLink = 304000 to 305000, + fullLink = 261000 to 262000, fastLinkGz = 48000 to 49000, fullLinkGz = 43000 to 44000, )) diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/ClassTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/ClassTest.scala index 5ec3a41777..a447e5c48b 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/ClassTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/lang/ClassTest.scala @@ -18,7 +18,7 @@ import org.junit.Assume._ import scala.runtime.BoxedUnit -import org.scalajs.testsuite.utils.AssertThrows.assertThrows +import org.scalajs.testsuite.utils.AssertThrows.{assertThrows, _} import org.scalajs.testsuite.utils.Platform._ class ClassTest { @@ -267,6 +267,10 @@ class ClassTest { s"classOf[Comparable[_]].isAssignableFrom($cls) should be true", classOf[Comparable[_]].isAssignableFrom(cls)) } + + // NPE if the rhs is null + + assertThrowsNPEIfCompliant(classOf[Object].isAssignableFrom(null)) } @Test def getComponentType(): Unit = { From e0fff46089edfc046f0615d8f59b31b27a133706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 14 Jun 2024 13:55:44 +0200 Subject: [PATCH 3/3] Emit the new `Class_x` operations into `jl.Class` from the compiler. We use the same technique as for `String_length` and `String_charAt`, for which we hijack the method body from the compiler and replace it with the appropriate operation. Likewise, we actually change the `NewArray` data type to contain a single length. We only enable the deserialization hacks of the previous commit when reading old IR. --- .../org/scalajs/nscplugin/GenJSCode.scala | 73 +++++++++++++++--- .../org/scalajs/nscplugin/JSDefinitions.scala | 2 + .../main/scala/org/scalajs/ir/Hashers.scala | 4 +- .../main/scala/org/scalajs/ir/Printers.scala | 12 ++- .../scala/org/scalajs/ir/Serializers.scala | 22 ++---- .../scala/org/scalajs/ir/Transformers.scala | 4 +- .../scala/org/scalajs/ir/Traversers.scala | 4 +- .../src/main/scala/org/scalajs/ir/Trees.scala | 2 +- .../scala/org/scalajs/ir/PrintersTest.scala | 8 +- javalib/src/main/scala/java/lang/Class.scala | 74 +++++-------------- .../main/scala/java/lang/reflect/Array.scala | 30 ++++++-- .../backend/emitter/FunctionEmitter.scala | 24 +++--- .../backend/wasmemitter/FunctionEmitter.scala | 2 +- .../linker/checker/ClassDefChecker.scala | 9 +-- .../scalajs/linker/checker/IRChecker.scala | 5 +- .../frontend/optimizer/OptimizerCore.scala | 16 ++-- project/BinaryIncompatibilities.scala | 6 ++ 17 files changed, 161 insertions(+), 136 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 277d432ffd..ef5d20f120 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -2188,15 +2188,45 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) isJSFunctionDef(currentClassSym)) { val flags = js.MemberFlags.empty.withNamespace(namespace) val body = { + def genAsUnaryOp(op: js.UnaryOp.Code): js.Tree = + js.UnaryOp(op, genThis()) + def genAsBinaryOp(op: js.BinaryOp.Code): js.Tree = + js.BinaryOp(op, genThis(), jsParams.head.ref) + def genAsBinaryOpRhsNotNull(op: js.BinaryOp.Code): js.Tree = + js.BinaryOp(op, genThis(), js.UnaryOp(js.UnaryOp.CheckNotNull, jsParams.head.ref)) + if (currentClassSym.get == HackedStringClass) { /* Hijack the bodies of String.length and String.charAt and replace * them with String_length and String_charAt operations, respectively. */ methodName.name match { - case `lengthMethodName` => - js.UnaryOp(js.UnaryOp.String_length, genThis()) - case `charAtMethodName` => - js.BinaryOp(js.BinaryOp.String_charAt, genThis(), jsParams.head.ref) + case `lengthMethodName` => genAsUnaryOp(js.UnaryOp.String_length) + case `charAtMethodName` => genAsBinaryOp(js.BinaryOp.String_charAt) + case _ => genBody() + } + } else if (currentClassSym.get == ClassClass) { + // Similar, for the Class_x operations + methodName.name match { + case `getNameMethodName` => genAsUnaryOp(js.UnaryOp.Class_name) + case `isPrimitiveMethodName` => genAsUnaryOp(js.UnaryOp.Class_isPrimitive) + case `isInterfaceMethodName` => genAsUnaryOp(js.UnaryOp.Class_isInterface) + case `isArrayMethodName` => genAsUnaryOp(js.UnaryOp.Class_isArray) + case `getComponentTypeMethodName` => genAsUnaryOp(js.UnaryOp.Class_componentType) + case `getSuperclassMethodName` => genAsUnaryOp(js.UnaryOp.Class_superClass) + + case `isInstanceMethodName` => genAsBinaryOp(js.BinaryOp.Class_isInstance) + case `isAssignableFromMethodName` => genAsBinaryOpRhsNotNull(js.BinaryOp.Class_isAssignableFrom) + case `castMethodName` => genAsBinaryOp(js.BinaryOp.Class_cast) + + case _ => genBody() + } + } else if (currentClassSym.get == JavaLangReflectArrayModClass) { + methodName.name match { + case `arrayNewInstanceMethodName` => + val List(jlClassParam, lengthParam) = jsParams + js.BinaryOp(js.BinaryOp.Class_newArray, + js.UnaryOp(js.UnaryOp.CheckNotNull, jlClassParam.ref), + lengthParam.ref) case _ => genBody() } @@ -3644,12 +3674,11 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) */ def genNewArray(arrayTypeRef: jstpe.ArrayTypeRef, arguments: List[js.Tree])( implicit pos: Position): js.Tree = { - assert(arguments.length <= arrayTypeRef.dimensions, - "too many arguments for array constructor: found " + arguments.length + - " but array has only " + arrayTypeRef.dimensions + - " dimension(s)") + assert(arguments.size == 1, + "expected exactly 1 argument for array constructor: found " + + s"${arguments.length} at $pos") - js.NewArray(arrayTypeRef, arguments) + js.NewArray(arrayTypeRef, arguments.head) } /** Gen JS code for an array literal. */ @@ -7084,6 +7113,32 @@ private object GenJSCode { private val charAtMethodName = MethodName("charAt", List(jstpe.IntRef), jstpe.CharRef) + private val getNameMethodName = + MethodName("getName", Nil, jstpe.ClassRef(ir.Names.BoxedStringClass)) + private val isPrimitiveMethodName = + MethodName("isPrimitive", Nil, jstpe.BooleanRef) + private val isInterfaceMethodName = + MethodName("isInterface", Nil, jstpe.BooleanRef) + private val isArrayMethodName = + MethodName("isArray", Nil, jstpe.BooleanRef) + private val getComponentTypeMethodName = + MethodName("getComponentType", Nil, jstpe.ClassRef(ir.Names.ClassClass)) + private val getSuperclassMethodName = + MethodName("getSuperclass", Nil, jstpe.ClassRef(ir.Names.ClassClass)) + + private val isInstanceMethodName = + MethodName("isInstance", List(jstpe.ClassRef(ir.Names.ObjectClass)), jstpe.BooleanRef) + private val isAssignableFromMethodName = + MethodName("isAssignableFrom", List(jstpe.ClassRef(ir.Names.ClassClass)), jstpe.BooleanRef) + private val castMethodName = + MethodName("cast", List(jstpe.ClassRef(ir.Names.ObjectClass)), jstpe.ClassRef(ir.Names.ObjectClass)) + + private val arrayNewInstanceMethodName = { + MethodName("newInstance", + List(jstpe.ClassRef(ir.Names.ClassClass), jstpe.IntRef), + jstpe.ClassRef(ir.Names.ObjectClass)) + } + private val thisOriginalName = OriginalName("this") private object BlockOrAlone { diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala b/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala index 43fa33aedd..8214985b6a 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/JSDefinitions.scala @@ -37,6 +37,8 @@ trait JSDefinitions { lazy val JavaLangVoidClass = getRequiredClass("java.lang.Void") + lazy val JavaLangReflectArrayModClass = getModuleIfDefined("java.lang.reflect.Array").moduleClass + lazy val BoxedUnitModClass = BoxedUnitModule.moduleClass lazy val ScalaJSJSPackageModule = getPackageObject("scala.scalajs.js") diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala index b1f69595a3..d37bf739e7 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala @@ -321,10 +321,10 @@ object Hashers { mixTree(lhs) mixTree(rhs) - case NewArray(typeRef, lengths) => + case NewArray(typeRef, length) => mixTag(TagNewArray) mixArrayTypeRef(typeRef) - mixTrees(lengths) + mixTrees(length :: Nil) // mixed as a list for historical reasons case ArrayValue(typeRef, elems) => mixTag(TagArrayValue) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala index eca5c547f2..34f5743e69 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala @@ -511,15 +511,13 @@ object Printers { print(rhs) print(')') - case NewArray(typeRef, lengths) => + case NewArray(typeRef, length) => print("new ") print(typeRef.base) - for (length <- lengths) { - print('[') - print(length) - print(']') - } - for (dim <- lengths.size until typeRef.dimensions) + print('[') + print(length) + print(']') + for (dim <- 1 until typeRef.dimensions) print("[]") case ArrayValue(typeRef, elems) => diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala index 45453943c5..ae2eec6b1f 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala @@ -363,9 +363,10 @@ object Serializers { writeTagAndPos(TagBinaryOp) writeByte(op); writeTree(lhs); writeTree(rhs) - case NewArray(tpe, lengths) => + case NewArray(tpe, length) => writeTagAndPos(TagNewArray) - writeArrayTypeRef(tpe); writeTrees(lengths) + writeArrayTypeRef(tpe) + writeTrees(length :: Nil) // written as a list of historical reasons case ArrayValue(tpe, elems) => writeTagAndPos(TagArrayValue) @@ -1232,10 +1233,10 @@ object Serializers { val lengths = readTrees() lengths match { case length :: Nil => - NewArray(arrayTypeRef, lengths) + NewArray(arrayTypeRef, length) case _ => - if (true /* hacks.use16 */) { // scalastyle:ignore + if (hacks.use16) { // Rewrite as a call to j.l.r.Array.newInstance val ArrayTypeRef(base, origDims) = arrayTypeRef val newDims = origDims - lengths.size @@ -1504,9 +1505,9 @@ object Serializers { if (hacks.use4 && kind.isJSClass) { // #4409: Filter out abstract methods in non-native JS classes for version < 1.5 methods0.filter(_.body.isDefined) - } else if (true /*hacks.use16*/ && cls == ClassClass) { // scalastyle:ignore + } else if (hacks.use16 && cls == ClassClass) { jlClassMethodsHack16(methods0) - } else if (true /*hacks.use16*/ && cls == HackNames.ReflectArrayModClass) { // scalastyle:ignore + } else if (hacks.use16 && cls == HackNames.ReflectArrayModClass) { jlReflectArrayMethodsHack16(methods0) } else { methods0 @@ -1697,14 +1698,7 @@ object Serializers { OptimizerHints.empty, Version.fromInt(1)) } - /* Only for the temporary commit where we always apply the hack, because - * the hack is applied in the JavalibIRCleaner and then a second time - * for the true deserialization. - */ - val oldMethodsTemp = - methods.filterNot(_.methodName == newInstanceRecName) - - val newMethods = for (method <- oldMethodsTemp) yield { + val newMethods = for (method <- methods) yield { method.methodName match { case `newInstanceSingleName` => // newInstance(jl.Class, int) --> newArray(jlClass.notNull, length) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala index 6d30327786..09b82b1757 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala @@ -114,8 +114,8 @@ object Transformers { case BinaryOp(op, lhs, rhs) => BinaryOp(op, transformExpr(lhs), transformExpr(rhs)) - case NewArray(tpe, lengths) => - NewArray(tpe, lengths map transformExpr) + case NewArray(tpe, length) => + NewArray(tpe, transformExpr(length)) case ArrayValue(tpe, elems) => ArrayValue(tpe, elems map transformExpr) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala index 8a8909cdce..2f68fac81f 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala @@ -101,8 +101,8 @@ object Traversers { traverse(lhs) traverse(rhs) - case NewArray(tpe, lengths) => - lengths foreach traverse + case NewArray(tpe, length) => + traverse(length) case ArrayValue(tpe, elems) => elems foreach traverse diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala index 3eb43c7f11..51d08ea2e9 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala @@ -519,7 +519,7 @@ object Trees { } } - sealed case class NewArray(typeRef: ArrayTypeRef, lengths: List[Tree])( + sealed case class NewArray(typeRef: ArrayTypeRef, length: Tree)( implicit val pos: Position) extends Tree { val tpe = ArrayType(typeRef, nullable = false) } diff --git a/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala b/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala index 16d5120ea3..57d80c3373 100644 --- a/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala +++ b/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala @@ -557,10 +557,10 @@ class PrintersTest { } @Test def printNewArray(): Unit = { - assertPrintEquals("new int[3]", NewArray(ArrayTypeRef(IntRef, 1), List(i(3)))) - assertPrintEquals("new int[3][]", NewArray(ArrayTypeRef(IntRef, 2), List(i(3)))) - assertPrintEquals("new java.lang.Object[3][4][][]", - NewArray(ArrayTypeRef(ObjectClass, 4), List(i(3), i(4)))) + assertPrintEquals("new int[3]", NewArray(ArrayTypeRef(IntRef, 1), i(3))) + assertPrintEquals("new int[3][]", NewArray(ArrayTypeRef(IntRef, 2), i(3))) + assertPrintEquals("new java.lang.Object[3][][][]", + NewArray(ArrayTypeRef(ObjectClass, 4), i(3))) } @Test def printArrayValue(): Unit = { diff --git a/javalib/src/main/scala/java/lang/Class.scala b/javalib/src/main/scala/java/lang/Class.scala index a39bfd3fa7..b0d80c788b 100644 --- a/javalib/src/main/scala/java/lang/Class.scala +++ b/javalib/src/main/scala/java/lang/Class.scala @@ -15,68 +15,39 @@ package java.lang import java.lang.constant.Constable import java.io.Serializable -import scala.scalajs.js - -@js.native -private trait ScalaJSClassData[A] extends js.Object { - val name: String = js.native - val isPrimitive: scala.Boolean = js.native - val isInterface: scala.Boolean = js.native - val isArrayClass: scala.Boolean = js.native - - def isInstance(obj: Any): scala.Boolean = js.native - def isAssignableFrom(that: ScalaJSClassData[_]): scala.Boolean = js.native - def checkCast(obj: Any): scala.Unit = js.native - - def getSuperclass(): Class[_ >: A] = js.native - def getComponentType(): Class[_] = js.native - - def newArrayOfThisClass(dimensions: js.Array[Int]): AnyRef = js.native -} - -final class Class[A] private (data0: Object) +final class Class[A] private () extends Object with Serializable with Constable { - private[this] val data: ScalaJSClassData[A] = - data0.asInstanceOf[ScalaJSClassData[A]] - private[this] var cachedSimpleName: String = _ - /** Access to `data` for other instances or `@inline` methods. - * - * Directly accessing the `data` field from `@inline` methods will cause - * scalac to make the field public and mangle its name. Since the Emitter - * relies on the field being called exactly `data` in some of its - * optimizations, we must avoid that. - * - * This non-`@noinline` method can be used to access the field without - * triggering scalac's mangling. Since it is a trivial accessor, the - * Scala.js optimizer will inline it anyway. - */ - private def getData(): ScalaJSClassData[A] = data - override def toString(): String = { (if (isInterface()) "interface " else if (isPrimitive()) "" else "class ")+getName() } + @inline def isInstance(obj: Any): scala.Boolean = - data.isInstance(obj) + throw new Error("Stub filled in by the compiler") + @inline def isAssignableFrom(that: Class[_]): scala.Boolean = - this.data.isAssignableFrom(that.getData()) + throw new Error("Stub filled in by the compiler") + @inline def isInterface(): scala.Boolean = - data.isInterface + throw new Error("Stub filled in by the compiler") + @inline def isArray(): scala.Boolean = - data.isArrayClass + throw new Error("Stub filled in by the compiler") + @inline def isPrimitive(): scala.Boolean = - data.isPrimitive + throw new Error("Stub filled in by the compiler") + @inline def getName(): String = - data.name + throw new Error("Stub filled in by the compiler") def getSimpleName(): String = { if (cachedSimpleName == null) @@ -108,7 +79,7 @@ final class Class[A] private (data0: Object) if (isArray()) { getComponentType().getSimpleName() + "[]" } else { - val name = data.name + val name = getName() var idx = name.length - 1 // Include trailing '$'s for module class names @@ -139,20 +110,15 @@ final class Class[A] private (data0: Object) } } + @inline def getSuperclass(): Class[_ >: A] = - data.getSuperclass() + throw new Error("Stub filled in by the compiler") + @inline def getComponentType(): Class[_] = - data.getComponentType() + throw new Error("Stub filled in by the compiler") @inline - def cast(obj: Any): A = { - getData().checkCast(obj) - obj.asInstanceOf[A] - } - - // java.lang.reflect.Array support - - private[lang] def newArrayOfThisClass(dimensions: js.Array[Int]): AnyRef = - data.newArrayOfThisClass(dimensions) + def cast(obj: Any): A = + throw new Error("Stub filled in by the compiler") } diff --git a/javalib/src/main/scala/java/lang/reflect/Array.scala b/javalib/src/main/scala/java/lang/reflect/Array.scala index b2f94b8906..f5f6aaa21f 100644 --- a/javalib/src/main/scala/java/lang/reflect/Array.scala +++ b/javalib/src/main/scala/java/lang/reflect/Array.scala @@ -17,18 +17,34 @@ import scala.scalajs.js import java.lang.Class object Array { + @inline def newInstance(componentType: Class[_], length: Int): AnyRef = - componentType.newArrayOfThisClass(js.Array(length)) + throw new Error("Stub filled in by the compiler") def newInstance(componentType: Class[_], dimensions: scala.Array[Int]): AnyRef = { - val jsDims = js.Array[Int]() - val len = dimensions.length - var i = 0 - while (i != len) { - jsDims.push(dimensions(i)) + def rec(componentType: Class[_], offset: Int): AnyRef = { + val length = dimensions(offset) + val result = newInstance(componentType, length) + val innerOffset = offset + 1 + if (innerOffset < dimensions.length) { + val result2 = result.asInstanceOf[Array[AnyRef]] + val innerComponentType = componentType.getComponentType() + var i = 0 + while (i != length) { + result2(i) = rec(innerComponentType, innerOffset) + i += 1 + } + } + result + } + + var outermostComponentType = componentType + var i = 1 + while (i != dimensions.length) { + outermostComponentType = newInstance(outermostComponentType, 0).getClass() i += 1 } - componentType.newArrayOfThisClass(jsDims) + rec(outermostComponentType, 0) } def getLength(array: AnyRef): Int = array match { diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala index 184f46c365..444cca7fb6 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala @@ -1047,8 +1047,8 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { if noExtractYet || semantics.asInstanceOfs == Unchecked => AsInstanceOf(rec(expr), tpe) - case NewArray(tpe, lengths) => - NewArray(tpe, recs(lengths)) + case NewArray(tpe, length) => + NewArray(tpe, rec(length)) case ArrayValue(tpe, elems) => ArrayValue(tpe, recs(elems)) case JSArrayConstr(items) if !needsToTranslateAnySpread(items) => @@ -1344,8 +1344,8 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { allowSideEffects && test(expr) // may TypeError // Array operations with conditional exceptions - case NewArray(tpe, lengths) => - allowBehavior(semantics.negativeArraySizes) && allowUnpure && lengths.forall(test) + case NewArray(tpe, length) => + allowBehavior(semantics.negativeArraySizes) && allowUnpure && test(length) case ArraySelect(array, index) => allowBehavior(semantics.arrayIndexOutOfBounds) && allowUnpure && testNPE(array) && test(index) @@ -1757,9 +1757,9 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { redo(BinaryOp(op, newLhs, newRhs))(env) } - case NewArray(tpe, lengths) => - unnest(lengths) { (newLengths, env) => - redo(NewArray(tpe, newLengths))(env) + case NewArray(tpe, length) => + unnest(length) { (newLength, env) => + redo(NewArray(tpe, newLength))(env) } case ArrayValue(tpe, elems) => @@ -2704,14 +2704,8 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { js.Apply(extractClassData(lhs, newLhs) DOT cpn.newArray, newRhs :: Nil) } - case NewArray(typeRef, lengths) => - lengths match { - case length :: Nil => - js.New(genArrayConstrOf(typeRef), transformExprNoChar(length) :: Nil) - case _ => - throw new AssertionError( - s"Illegal legacy NewArray with lengths $lengths at $pos") - } + case NewArray(typeRef, length) => + js.New(genArrayConstrOf(typeRef), transformExprNoChar(length) :: Nil) case ArrayValue(typeRef, elems) => val preserveChar = typeRef match { diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala index fbe918f44b..4be4ac40ac 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala @@ -2941,7 +2941,7 @@ private class FunctionEmitter private ( } private def genNewArray(tree: NewArray): Type = { - val NewArray(arrayTypeRef, length :: Nil) = tree: @unchecked + val NewArray(arrayTypeRef, length) = tree markPosition(tree) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala index 45ab1530ca..b569825470 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala @@ -663,14 +663,9 @@ private final class ClassDefChecker(classDef: ClassDef, checkTree(lhs, env) checkTree(rhs, env) - case NewArray(typeRef, lengths) => - if (lengths.isEmpty) - reportError("NewArray must have non-0 dimensions") - if (lengths.size > 1) - reportError(i"Illegal legacy NewArray with ${lengths.size} dimensions") - + case NewArray(typeRef, length) => checkArrayTypeRef(typeRef) - checkTrees(lengths, env) + checkTree(length, env) case ArrayValue(typeRef, elems) => checkArrayTypeRef(typeRef) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala index 347e56bf34..5bf5e259c4 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala @@ -489,9 +489,8 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { typecheckExpect(lhs, env, expectedLhsType) typecheckExpect(rhs, env, expectedRhsType) - case NewArray(typeRef, lengths) => - for (length <- lengths) - typecheckExpect(length, env, IntType) + case NewArray(typeRef, length) => + typecheckExpect(length, env, IntType) case ArrayValue(typeRef, elems) => val elemType = arrayElemType(typeRef) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala index 034cfe8020..264d1d8a30 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala @@ -530,8 +530,8 @@ private[optimizer] abstract class OptimizerCore( pretransformBinaryOp(tree)(finishTransform(isStat)) } - case NewArray(tpe, lengths) => - NewArray(tpe, lengths map transformExpr) + case NewArray(tpe, length) => + NewArray(tpe, transformExpr(length)) case ArrayValue(tpe, elems) => ArrayValue(tpe, elems map transformExpr) @@ -1665,10 +1665,10 @@ private[optimizer] abstract class OptimizerCore( case LoadModule(moduleClassName) => if (hasElidableConstructors(moduleClassName)) Skip()(stat.pos) else stat - case NewArray(_, lengths) if lengths.forall(isNonNegativeIntLiteral(_)) => + case NewArray(_, length) if isNonNegativeIntLiteral(length) => Skip()(stat.pos) - case NewArray(_, lengths) if semantics.negativeArraySizes == CheckedBehavior.Unchecked => - Block(lengths.map(keepOnlySideEffects))(stat.pos) + case NewArray(_, length) if semantics.negativeArraySizes == CheckedBehavior.Unchecked => + keepOnlySideEffects(length) case ArrayValue(_, elems) => Block(elems.map(keepOnlySideEffects(_)))(stat.pos) case ArrayLength(array) => @@ -1917,8 +1917,8 @@ private[optimizer] abstract class OptimizerCore( } } - case NewArray(typeRef, lengths) => - recs(lengths).mapOrKeepGoing(NewArray(typeRef, _)) + case NewArray(typeRef, length) => + rec(length).mapOrKeepGoing(NewArray(typeRef, _)) case ArrayValue(typeRef, elems) => recs(elems).mapOrKeepGoing(ArrayValue(typeRef, _)) @@ -4999,7 +4999,7 @@ private[optimizer] abstract class OptimizerCore( lhs match { case PreTransLit(ClassOf(elementTypeRef)) if elementTypeRef != VoidRef => val arrayTypeRef = ArrayTypeRef.of(elementTypeRef) - NewArray(arrayTypeRef, List(finishTransformExpr(rhs))).toPreTransform + NewArray(arrayTypeRef, finishTransformExpr(rhs)).toPreTransform case _ => default } diff --git a/project/BinaryIncompatibilities.scala b/project/BinaryIncompatibilities.scala index 01c38bc201..8c27dca150 100644 --- a/project/BinaryIncompatibilities.scala +++ b/project/BinaryIncompatibilities.scala @@ -9,6 +9,12 @@ object BinaryIncompatibilities { ProblemFilters.exclude[IncompatibleResultTypeProblem]("org.scalajs.ir.Trees#*.tpe"), + ProblemFilters.exclude[IncompatibleMethTypeProblem]("org.scalajs.ir.Trees#NewArray.this"), + ProblemFilters.exclude[IncompatibleMethTypeProblem]("org.scalajs.ir.Trees#NewArray.apply"), + ProblemFilters.exclude[IncompatibleMethTypeProblem]("org.scalajs.ir.Trees#NewArray.copy"), + ProblemFilters.exclude[IncompatibleResultTypeProblem]("org.scalajs.ir.Trees#NewArray.copy$default$2"), + ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Trees#NewArray.lengths"), + ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Trees#UnaryOp.resultTypeOf"), ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Types#ClassType.this"),