From 2f1fc326d0e10a576927fb96fb15979478f1c80a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 25 Sep 2023 10:48:06 +0200 Subject: [PATCH 001/298] Towards 1.14.1. --- ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala | 2 +- project/Build.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala index 245ca3eee9..4d7495cf96 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala @@ -17,7 +17,7 @@ import java.util.concurrent.ConcurrentHashMap import scala.util.matching.Regex object ScalaJSVersions extends VersionChecks( - current = "1.14.0", + current = "1.14.1-SNAPSHOT", binaryEmitted = "1.13" ) diff --git a/project/Build.scala b/project/Build.scala index 0e69559253..796bc8e4b7 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -338,7 +338,7 @@ object Build { val previousVersions = List("1.0.0", "1.0.1", "1.1.0", "1.1.1", "1.2.0", "1.3.0", "1.3.1", "1.4.0", "1.5.0", "1.5.1", "1.6.0", "1.7.0", "1.7.1", "1.8.0", "1.9.0", "1.10.0", "1.10.1", "1.11.0", "1.12.0", "1.13.0", - "1.13.1", "1.13.2") + "1.13.1", "1.13.2", "1.14.0") val previousVersion = previousVersions.last val previousBinaryCrossVersion = CrossVersion.binaryWith("sjs1_", "") From 1982a6b631a5b37ebbd4fe61655a45ad9b62bf93 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sat, 7 Oct 2023 22:08:03 +0200 Subject: [PATCH 002/298] Do not (ab)use blocks for top level trees --- .../linker/backend/emitter/ClassEmitter.scala | 122 ++-- .../linker/backend/emitter/CoreJSLib.scala | 524 +++++++++--------- .../linker/backend/emitter/Emitter.scala | 67 +-- .../linker/backend/emitter/JSGen.scala | 6 +- .../linker/backend/emitter/VarGen.scala | 18 +- .../linker/backend/emitter/WithGlobals.scala | 14 +- 6 files changed, 356 insertions(+), 395 deletions(-) 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 0c87bd53bf..be9fc9a993 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 @@ -45,23 +45,15 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { def buildClass(className: ClassName, kind: ClassKind, jsClassCaptures: Option[List[ParamDef]], hasClassInitializer: Boolean, - superClass: Option[ClassIdent], jsSuperClass: Option[Tree], useESClass: Boolean, ctor: js.Tree, + superClass: Option[ClassIdent], jsSuperClass: Option[Tree], useESClass: Boolean, ctorDefs: List[js.Tree], memberDefs: List[js.MethodDef], exportedDefs: List[js.Tree])( implicit moduleContext: ModuleContext, - globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[js.Tree] = { + globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[List[js.Tree]] = { - def allES6Defs = { - js.Block(ctor +: (memberDefs ++ exportedDefs)) match { - case js.Block(allDefs) => allDefs - case js.Skip() => Nil - case oneDef => List(oneDef) - } - } + def allES6Defs = ctorDefs ::: memberDefs ::: exportedDefs - def allES5Defs(classVar: js.Tree) = { - WithGlobals(js.Block( - ctor, assignES5ClassMembers(classVar, memberDefs), js.Block(exportedDefs: _*))) - } + def allES5Defs(classVar: js.Tree) = + WithGlobals(ctorDefs ::: assignES5ClassMembers(classVar, memberDefs) ::: exportedDefs) if (!kind.isJSClass) { assert(jsSuperClass.isEmpty, className) @@ -95,7 +87,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val entireClassDefWithGlobals = if (useESClass) { genJSSuperCtor(superClass, jsSuperClass).map { jsSuperClass => - classValueVar := js.ClassDef(Some(classValueIdent), Some(jsSuperClass), allES6Defs) + List(classValueVar := js.ClassDef(Some(classValueIdent), Some(jsSuperClass), allES6Defs)) } } else { allES5Defs(classValueVar) @@ -106,7 +98,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { entireClassDef <- entireClassDefWithGlobals createStaticFields <- genCreateStaticFieldsOfJSClass(className) } yield { - optStoreJSSuperClass.toList ::: entireClassDef :: createStaticFields + optStoreJSSuperClass.toList ::: entireClassDef ::: createStaticFields } jsClassCaptures.fold { @@ -125,7 +117,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { ) createAccessor <- globalFunctionDef("a", className, Nil, None, body) } yield { - js.Block(createClassValueVar, createAccessor) + createClassValueVar :: createAccessor } } { jsClassCaptures => val captureParamDefs = for (param <- jsClassCaptures) yield { @@ -173,7 +165,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { def genScalaClassConstructor(className: ClassName, superClass: Option[ClassIdent], useESClass: Boolean, initToInline: Option[MethodDef])( implicit moduleContext: ModuleContext, - globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[js.Tree] = { + globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[List[js.Tree]] = { assert(superClass.isDefined || className == ObjectClass, s"Class $className is missing a parent class") @@ -192,9 +184,9 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } if (args.isEmpty && isTrivialCtorBody) - js.Skip() + Nil else - js.MethodDef(static = false, js.Ident("constructor"), args, restParam, body) + js.MethodDef(static = false, js.Ident("constructor"), args, restParam, body) :: Nil } } else { import TreeDSL._ @@ -203,13 +195,13 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val chainProtoWithGlobals = superClass match { case None => - WithGlobals(js.Skip()) + WithGlobals.nil case Some(_) if shouldExtendJSError(className, superClass) => untrackedGlobalRef("Error").map(chainPrototypeWithLocalCtor(className, ctorVar, _)) case Some(parentIdent) => - WithGlobals(ctorVar.prototype := js.New(globalVar("h", parentIdent.name), Nil)) + WithGlobals(List(ctorVar.prototype := js.New(globalVar("h", parentIdent.name), Nil))) } for { @@ -220,17 +212,17 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { globalFunctionDef("h", className, Nil, None, js.Skip()) chainProto <- chainProtoWithGlobals } yield { - js.Block( - // Real constructor - js.DocComment("@constructor"), - realCtorDef, - chainProto, - genIdentBracketSelect(ctorVar.prototype, "constructor") := ctorVar, - - // Inheritable constructor - js.DocComment("@constructor"), - inheritableCtorDef, - globalVar("h", className).prototype := ctorVar.prototype + ( + // Real constructor + js.DocComment("@constructor") :: + realCtorDef ::: + chainProto ::: + (genIdentBracketSelect(ctorVar.prototype, "constructor") := ctorVar) :: + + // Inheritable constructor + js.DocComment("@constructor") :: + inheritableCtorDef ::: + (globalVar("h", className).prototype := ctorVar.prototype) :: Nil ) } } @@ -240,7 +232,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { def genJSConstructor(className: ClassName, superClass: Option[ClassIdent], jsSuperClass: Option[Tree], useESClass: Boolean, jsConstructorDef: JSConstructorDef)( implicit moduleContext: ModuleContext, - globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[js.Tree] = { + globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[List[js.Tree]] = { val JSConstructorDef(_, params, restParam, body) = jsConstructorDef val ctorFunWithGlobals = desugarToFunction(className, params, restParam, body) @@ -248,7 +240,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { if (useESClass) { for (fun <- ctorFunWithGlobals) yield { js.MethodDef(static = false, js.Ident("constructor"), - fun.args, fun.restParam, fun.body) + fun.args, fun.restParam, fun.body) :: Nil } } else { for { @@ -259,12 +251,10 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val ctorVar = fileLevelVar("b", genName(className)) - js.Block( - js.DocComment("@constructor"), - ctorVar := ctorFun, - chainPrototypeWithLocalCtor(className, ctorVar, superCtor), - genIdentBracketSelect(ctorVar.prototype, "constructor") := ctorVar - ) + js.DocComment("@constructor") :: + (ctorVar := ctorFun) :: + chainPrototypeWithLocalCtor(className, ctorVar, superCtor) ::: + (genIdentBracketSelect(ctorVar.prototype, "constructor") := ctorVar) :: Nil } } } @@ -348,12 +338,12 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } private def chainPrototypeWithLocalCtor(className: ClassName, ctorVar: js.Tree, - superCtor: js.Tree)(implicit pos: Position): js.Tree = { + superCtor: js.Tree)(implicit pos: Position): List[js.Tree] = { import TreeDSL._ val dummyCtor = fileLevelVar("hh", genName(className)) - js.Block( + List( js.DocComment("@constructor"), genConst(dummyCtor.ident, js.Function(false, Nil, None, js.Skip())), dummyCtor.prototype := superCtor.prototype, @@ -396,7 +386,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { globalVarDef("t", varScope, value, origName.orElse(name)) } - WithGlobals.list(defs) + WithGlobals.flatten(defs) } /** Generates the creation of the private JS field defs for a JavaScript @@ -424,7 +414,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } } - WithGlobals.list(defs) + WithGlobals.flatten(defs) } /** Generates the creation of the static fields for a JavaScript class. */ @@ -497,7 +487,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { def genStaticLikeMethod(className: ClassName, method: MethodDef)( implicit moduleContext: ModuleContext, - globalKnowledge: GlobalKnowledge): WithGlobals[js.Tree] = { + globalKnowledge: GlobalKnowledge): WithGlobals[List[js.Tree]] = { val methodBody = method.body.getOrElse( throw new AssertionError("Cannot generate an abstract method")) @@ -570,11 +560,11 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { private def genJSProperty(className: ClassName, kind: ClassKind, useESClass: Boolean, property: JSPropertyDef)( implicit moduleContext: ModuleContext, - globalKnowledge: GlobalKnowledge): WithGlobals[js.Tree] = { + globalKnowledge: GlobalKnowledge): WithGlobals[List[js.Tree]] = { if (useESClass) genJSPropertyES6(className, property) else - genJSPropertyES5(className, kind, property) + genJSPropertyES5(className, kind, property).map(_ :: Nil) } private def genJSPropertyES5(className: ClassName, kind: ClassKind, property: JSPropertyDef)( @@ -612,31 +602,27 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { private def genJSPropertyES6(className: ClassName, property: JSPropertyDef)( implicit moduleContext: ModuleContext, - globalKnowledge: GlobalKnowledge): WithGlobals[js.Tree] = { + globalKnowledge: GlobalKnowledge): WithGlobals[List[js.Tree]] = { implicit val pos = property.pos val static = property.flags.namespace.isStatic genMemberNameTree(property.name).flatMap { propName => - val getterWithGlobals = property.getterBody.fold { - WithGlobals[js.Tree](js.Skip()) - } { body => + val getterWithGlobals = property.getterBody.map { body => for (fun <- desugarToFunction(className, Nil, body, resultType = AnyType)) yield js.GetterDef(static, propName, fun.body) } - val setterWithGlobals = property.setterArgAndBody.fold { - WithGlobals[js.Tree](js.Skip()) - } { case (arg, body) => + val setterWithGlobals = property.setterArgAndBody.map { case (arg, body) => for (fun <- desugarToFunction(className, arg :: Nil, body, resultType = NoType)) yield js.SetterDef(static, propName, fun.args.head, fun.body) } for { - getter <- getterWithGlobals - setter <- setterWithGlobals + getter <- WithGlobals.option(getterWithGlobals) + setter <- WithGlobals.option(setterWithGlobals) } yield { - js.Block(getter, setter) + getter.toList ::: setter.toList } } } @@ -756,11 +742,11 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val createIsStatWithGlobals = if (needIsFunction) { globalFunctionDef("is", className, List(objParam), None, js.Return(isExpression)) } else { - WithGlobals(js.Skip()) + WithGlobals.nil } val createAsStatWithGlobals = if (semantics.asInstanceOfs == Unchecked) { - WithGlobals(js.Skip()) + WithGlobals.nil } else { globalFunctionDef("as", className, List(objParam), None, js.Return { val isCond = @@ -780,10 +766,10 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { createIsStat <- createIsStatWithGlobals createAsStat <- createAsStatWithGlobals } yield { - List(createIsStat, createAsStat) + createIsStat ::: createAsStat } } else { - WithGlobals(Nil) + WithGlobals.nil } } @@ -816,7 +802,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } val createAsArrayOfStatWithGlobals = if (semantics.asInstanceOfs == Unchecked) { - WithGlobals(js.Skip()) + WithGlobals.nil } else { globalFunctionDef("asArrayOf", className, List(objParam, depthParam), None, { js.Return { @@ -835,7 +821,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { createIsArrayOfStat <- createIsArrayOfStatWithGlobals createAsArrayOfStat <- createAsArrayOfStatWithGlobals } yield { - List(createIsArrayOfStat, createAsArrayOfStat) + createIsArrayOfStat ::: createAsArrayOfStat } } @@ -855,7 +841,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { superClass: Option[ClassIdent], ancestors: List[ClassName], jsNativeLoadSpec: Option[JSNativeLoadSpec])( implicit moduleContext: ModuleContext, - globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[js.Tree] = { + globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[List[js.Tree]] = { import TreeDSL._ val isObjectClass = @@ -959,7 +945,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { def genModuleAccessor(className: ClassName, kind: ClassKind)( implicit moduleContext: ModuleContext, - globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[js.Tree] = { + globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[List[js.Tree]] = { import TreeDSL._ val tpe = ClassType(className) @@ -1013,14 +999,14 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { globalFunctionDef("m", className, Nil, None, body) } - createAccessor.map(js.Block(createModuleInstanceField, _)) + createAccessor.map(createModuleInstanceField :: _) } def genExportedMember(className: ClassName, kind: ClassKind, useESClass: Boolean, member: JSMethodPropDef)( implicit moduleContext: ModuleContext, - globalKnowledge: GlobalKnowledge): WithGlobals[js.Tree] = { + globalKnowledge: GlobalKnowledge): WithGlobals[List[js.Tree]] = { member match { - case m: JSMethodDef => genJSMethod(className, kind, useESClass, m) + case m: JSMethodDef => genJSMethod(className, kind, useESClass, m).map(_ :: Nil) case p: JSPropertyDef => genJSProperty(className, kind, useESClass, p) } } 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 7abc4b2396..790ccfab2f 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 @@ -53,9 +53,9 @@ private[emitter] object CoreJSLib { * These must have class definitions (but not static fields) available. */ final class Lib private[CoreJSLib] ( - val preObjectDefinitions: Tree, - val postObjectDefinitions: Tree, - val initialization: Tree) + val preObjectDefinitions: List[Tree], + val postObjectDefinitions: List[Tree], + val initialization: List[Tree]) private class CoreJSLibBuilder(sjsGen: SJSGen)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge) { @@ -121,36 +121,36 @@ private[emitter] object CoreJSLib { WithGlobals(lib, trackedGlobalRefs) } - private def buildPreObjectDefinitions(): Tree = Block( - defineLinkingInfo(), - defineJSBuiltinsSnapshotsAndPolyfills(), - declareCachedL0(), - definePropertyName(), - defineCharClass(), - defineRuntimeFunctions(), - defineObjectGetClassFunctions(), - defineDispatchFunctions(), - defineArithmeticOps(), - defineES2015LikeHelpers(), - defineModuleHelpers(), - defineIntrinsics(), - defineIsPrimitiveFunctions(), + private def buildPreObjectDefinitions(): List[Tree] = { + defineLinkingInfo() ::: + defineJSBuiltinsSnapshotsAndPolyfills() ::: + declareCachedL0() ::: + definePropertyName() ::: + defineCharClass() ::: + defineRuntimeFunctions() ::: + defineObjectGetClassFunctions() ::: + defineDispatchFunctions() ::: + defineArithmeticOps() ::: + defineES2015LikeHelpers() ::: + defineModuleHelpers() ::: + defineIntrinsics() ::: + defineIsPrimitiveFunctions() ::: defineBoxFunctions() - ) + } - private def buildPostObjectDefinitions(): Tree = Block( - defineSpecializedArrayClasses(), - defineTypeDataClass(), - defineSpecializedIsArrayOfFunctions(), - defineSpecializedAsArrayOfFunctions(), + private def buildPostObjectDefinitions(): List[Tree] = { + defineSpecializedArrayClasses() ::: + defineTypeDataClass() ::: + defineSpecializedIsArrayOfFunctions() ::: + defineSpecializedAsArrayOfFunctions() ::: defineSpecializedTypeDatas() - ) + } - private def buildInitializations(): Tree = Block( + private def buildInitializations(): List[Tree] = { assignCachedL0() - ) + } - private def defineLinkingInfo(): Tree = { + private def defineLinkingInfo(): List[Tree] = { // must be in sync with scala.scalajs.runtime.LinkingInfo def objectFreeze(tree: Tree): Tree = @@ -167,7 +167,7 @@ private[emitter] object CoreJSLib { extractWithGlobals(globalVarDef("linkingInfo", CoreVar, linkingInfo)) } - private def defineJSBuiltinsSnapshotsAndPolyfills(): Tree = { + private def defineJSBuiltinsSnapshotsAndPolyfills(): List[Tree] = { def genPolyfillFor(builtin: PolyfillableBuiltin): Tree = builtin match { case ObjectIsBuiltin => val x = varRef("x") @@ -491,10 +491,7 @@ private[emitter] object CoreJSLib { Apply(funGenerator, Nil) } - val polyfillDefs = for { - builtin <- PolyfillableBuiltin.All - if esVersion < builtin.availableInESVersion - } yield { + PolyfillableBuiltin.All.withFilter(esVersion < _.availableInESVersion).flatMap { builtin => val polyfill = genPolyfillFor(builtin) val rhs = builtin match { case builtin: GlobalVarBuiltin => @@ -508,24 +505,23 @@ private[emitter] object CoreJSLib { } extractWithGlobals(globalVarDef(builtin.builtinName, CoreVar, rhs)) } - Block(polyfillDefs) } - private def declareCachedL0(): Tree = { - condTree(!allowBigIntsForLongs)( + private def declareCachedL0(): List[Tree] = { + condDefs(!allowBigIntsForLongs)( extractWithGlobals(globalVarDecl("L0", CoreVar)) ) } - private def assignCachedL0(): Tree = { - condTree(!allowBigIntsForLongs)(Block( + private def assignCachedL0(): List[Tree] = { + condDefs(!allowBigIntsForLongs)(List( globalVar("L0", CoreVar) := genScalaClassNew( LongImpl.RuntimeLongClass, LongImpl.initFromParts, 0, 0), genClassDataOf(LongRef) DOT "zero" := globalVar("L0", CoreVar) )) } - private def definePropertyName(): Tree = { + private def definePropertyName(): List[Tree] = { /* Encodes a property name for runtime manipulation. * * Usage: @@ -542,7 +538,7 @@ private[emitter] object CoreJSLib { } } - private def defineCharClass(): Tree = { + private def defineCharClass(): List[Tree] = { val ctor = { val c = varRef("c") MethodDef(static = false, Ident("constructor"), paramList(c), None, { @@ -560,15 +556,13 @@ private[emitter] object CoreJSLib { if (useClassesForRegularClasses) { extractWithGlobals(globalClassDef("Char", CoreVar, None, ctor :: toStr :: Nil)) } else { - Block( - defineFunction("Char", ctor.args, ctor.body), - assignES5ClassMembers(globalVar("Char", CoreVar), List(toStr)) - ) + defineFunction("Char", ctor.args, ctor.body) ::: + assignES5ClassMembers(globalVar("Char", CoreVar), List(toStr)) } } - private def defineRuntimeFunctions(): Tree = Block( - condTree(asInstanceOfs != CheckedBehavior.Unchecked || arrayStores != CheckedBehavior.Unchecked)( + private def defineRuntimeFunctions(): List[Tree] = ( + condDefs(asInstanceOfs != CheckedBehavior.Unchecked || arrayStores != CheckedBehavior.Unchecked)( /* Returns a safe string description of a value. * This helper is never called for `value === null`. As implemented, * it would return `"object"` if it were. @@ -604,15 +598,15 @@ private[emitter] object CoreJSLib { }) } } - ), + ) ::: - condTree(asInstanceOfs != CheckedBehavior.Unchecked)(Block( + condDefs(asInstanceOfs != CheckedBehavior.Unchecked)( defineFunction2("throwClassCastException") { (instance, classFullName) => Throw(maybeWrapInUBE(asInstanceOfs, { genScalaClassNew(ClassCastExceptionClass, StringArgConstructorName, genCallHelper("valueDescription", instance) + str(" cannot be cast to ") + classFullName) })) - }, + } ::: defineFunction3("throwArrayCastException") { (instance, classArrayEncodedName, depth) => Block( @@ -622,9 +616,9 @@ private[emitter] object CoreJSLib { genCallHelper("throwClassCastException", instance, classArrayEncodedName) ) } - )), + ) ::: - condTree(arrayIndexOutOfBounds != CheckedBehavior.Unchecked)( + condDefs(arrayIndexOutOfBounds != CheckedBehavior.Unchecked)( defineFunction1("throwArrayIndexOutOfBoundsException") { i => Throw(maybeWrapInUBE(arrayIndexOutOfBounds, { genScalaClassNew(ArrayIndexOutOfBoundsExceptionClass, @@ -632,9 +626,9 @@ private[emitter] object CoreJSLib { If(i === Null(), Null(), str("") + i)) })) } - ), + ) ::: - condTree(arrayStores != CheckedBehavior.Unchecked)( + condDefs(arrayStores != CheckedBehavior.Unchecked)( defineFunction1("throwArrayStoreException") { v => Throw(maybeWrapInUBE(arrayStores, { genScalaClassNew(ArrayStoreExceptionClass, @@ -642,31 +636,31 @@ private[emitter] object CoreJSLib { If(v === Null(), Null(), genCallHelper("valueDescription", v))) })) } - ), + ) ::: - condTree(negativeArraySizes != CheckedBehavior.Unchecked)( + condDefs(negativeArraySizes != CheckedBehavior.Unchecked)( defineFunction0("throwNegativeArraySizeException") { Throw(maybeWrapInUBE(negativeArraySizes, { genScalaClassNew(NegativeArraySizeExceptionClass, NoArgConstructorName) })) } - ), + ) ::: - condTree(moduleInit == CheckedBehavior.Fatal)( + condDefs(moduleInit == CheckedBehavior.Fatal)( defineFunction1("throwModuleInitError") { name => Throw(genScalaClassNew(UndefinedBehaviorErrorClass, StringArgConstructorName, str("Initializer of ") + name + str(" called before completion of its super constructor"))) } - ), + ) ::: - condTree(nullPointers != CheckedBehavior.Unchecked)(Block( + condDefs(nullPointers != CheckedBehavior.Unchecked)( defineFunction0("throwNullPointerException") { Throw(maybeWrapInUBE(nullPointers, { genScalaClassNew(NullPointerExceptionClass, NoArgConstructorName) })) - }, + } ::: // "checkNotNull", but with a very short name defineFunction1("n") { x => @@ -675,16 +669,16 @@ private[emitter] object CoreJSLib { Return(x) ) } - )), + ) ::: defineFunction1("noIsInstance") { instance => Throw(New(TypeErrorRef, str("Cannot call isInstance() on a Class representing a JS trait/object") :: Nil)) - }, + } ::: defineFunction2("newArrayObject") { (arrayClassData, lengths) => Return(genCallHelper("newArrayObjectInternal", arrayClassData, lengths, int(0))) - }, + } ::: defineFunction3("newArrayObjectInternal") { (arrayClassData, lengths, lengthIndex) => val result = varRef("result") @@ -707,7 +701,7 @@ private[emitter] object CoreJSLib { )), Return(result) ) - }, + } ::: defineFunction1("objectClone") { instance => // return Object.create(Object.getPrototypeOf(instance), $getOwnPropertyDescriptors(instance)); @@ -716,7 +710,7 @@ private[emitter] object CoreJSLib { Return(Apply(genIdentBracketSelect(ObjectRef, "create"), List( Apply(genIdentBracketSelect(ObjectRef, "getPrototypeOf"), instance :: Nil), callGetOwnPropertyDescriptors))) - }, + } ::: defineFunction1("objectOrArrayClone") { instance => // return instance.$classData.isArrayClass ? instance.clone__O() : $objectClone(instance); @@ -726,12 +720,12 @@ private[emitter] object CoreJSLib { } ) - private def defineObjectGetClassFunctions(): Tree = { + private def defineObjectGetClassFunctions(): List[Tree] = { // objectGetClass and objectClassName def defineObjectGetClassBasedFun(name: String, constantClassResult: ClassName => Tree, - scalaObjectResult: VarRef => Tree, jsObjectResult: Tree): Tree = { + scalaObjectResult: VarRef => Tree, jsObjectResult: Tree): List[Tree] = { defineFunction1(name) { instance => Switch(typeof(instance), List( str("string") -> { @@ -794,44 +788,41 @@ private[emitter] object CoreJSLib { } - Block( - /* We use isClassClassInstantiated as an over-approximation of whether - * the program contains any `GetClass` node. If `j.l.Class` is not - * instantiated, then we know that there is no `GetClass` node, and it is - * safe to omit the definition of `objectGetClass`. However, it is - * possible that we generate `objectGetClass` even if it is not - * necessary, in the case that `j.l.Class` is otherwise instantiated - * (i.e., through a `ClassOf` node). - */ - condTree(globalKnowledge.isClassClassInstantiated)( - defineObjectGetClassBasedFun("objectGetClass", - className => genClassOf(className), - instance => Apply(instance DOT classData DOT "getClassOf", Nil), - Null() - ) - ), - - defineObjectGetClassBasedFun("objectClassName", - { className => - StringLiteral(RuntimeClassNameMapperImpl.map( - semantics.runtimeClassNameMapper, className.nameString)) - }, - instance => genIdentBracketSelect(instance DOT classData, "name"), - { - if (nullPointers == CheckedBehavior.Unchecked) - Apply(Null() DOT genName(getNameMethodName), Nil) - else - genCallHelper("throwNullPointerException") - } + /* We use isClassClassInstantiated as an over-approximation of whether + * the program contains any `GetClass` node. If `j.l.Class` is not + * instantiated, then we know that there is no `GetClass` node, and it is + * safe to omit the definition of `objectGetClass`. However, it is + * possible that we generate `objectGetClass` even if it is not + * necessary, in the case that `j.l.Class` is otherwise instantiated + * (i.e., through a `ClassOf` node). + */ + condDefs(globalKnowledge.isClassClassInstantiated)( + defineObjectGetClassBasedFun("objectGetClass", + className => genClassOf(className), + instance => Apply(instance DOT classData DOT "getClassOf", Nil), + Null() ) + ) ::: + defineObjectGetClassBasedFun("objectClassName", + { className => + StringLiteral(RuntimeClassNameMapperImpl.map( + semantics.runtimeClassNameMapper, className.nameString)) + }, + instance => genIdentBracketSelect(instance DOT classData, "name"), + { + if (nullPointers == CheckedBehavior.Unchecked) + Apply(Null() DOT genName(getNameMethodName), Nil) + else + genCallHelper("throwNullPointerException") + } ) } - private def defineDispatchFunctions(): Tree = { + private def defineDispatchFunctions(): List[Tree] = { val instance = varRef("instance") def defineDispatcher(methodName: MethodName, args: List[VarRef], - body: Tree): Tree = { + body: Tree): List[Tree] = { defineFunction("dp_" + genName(methodName), paramList((instance :: args): _*), body) } @@ -844,7 +835,7 @@ private[emitter] object CoreJSLib { * - The implementation in java.lang.Object (if this is a JS object). */ def defineStandardDispatcher(methodName: MethodName, - implementingClasses: Set[ClassName]): Tree = { + implementingClasses: Set[ClassName]): List[Tree] = { val args = methodName.paramTypeRefs.indices.map(i => varRef("x" + i)).toList @@ -915,9 +906,7 @@ private[emitter] object CoreJSLib { val methodsInRepresentativeClasses = globalKnowledge.methodsInRepresentativeClasses() - val dispatchers = for { - (methodName, implementingClasses) <- methodsInRepresentativeClasses - } yield { + methodsInRepresentativeClasses.flatMap { case (methodName, implementingClasses) => if (methodName == toStringMethodName) { // toString()java.lang.String is special as per IR spec. defineDispatcher(toStringMethodName, Nil, { @@ -929,11 +918,9 @@ private[emitter] object CoreJSLib { defineStandardDispatcher(methodName, implementingClasses) } } - - Block(dispatchers) } - private def defineArithmeticOps(): Tree = { + private def defineArithmeticOps(): List[Tree] = { val throwDivByZero = { Throw(genScalaClassNew(ArithmeticExceptionClass, StringArgConstructorName, str("/ by zero"))) @@ -942,91 +929,85 @@ private[emitter] object CoreJSLib { def wrapBigInt64(tree: Tree): Tree = Apply(genIdentBracketSelect(BigIntRef, "asIntN"), 64 :: tree :: Nil) - Block( - defineFunction2("intDiv") { (x, y) => - If(y === 0, throwDivByZero, { - Return((x / y) | 0) - }) - }, - - defineFunction2("intMod") { (x, y) => - If(y === 0, throwDivByZero, { - Return((x % y) | 0) - }) - }, - - defineFunction1("doubleToInt") { x => - Return(If(x > 2147483647, 2147483647, If(x < -2147483648, -2147483648, x | 0))) - }, - - condTree(semantics.stringIndexOutOfBounds != CheckedBehavior.Unchecked)( - defineFunction2("charAt") { (s, i) => - val r = varRef("r") - - val throwStringIndexOutOfBoundsException = { - Throw(maybeWrapInUBE(semantics.stringIndexOutOfBounds, - genScalaClassNew(StringIndexOutOfBoundsExceptionClass, IntArgConstructorName, i))) - } - - Block( - const(r, Apply(genIdentBracketSelect(s, "charCodeAt"), List(i))), - If(r !== r, throwStringIndexOutOfBoundsException, Return(r)) - ) + defineFunction2("intDiv") { (x, y) => + If(y === 0, throwDivByZero, { + Return((x / y) | 0) + }) + } ::: + defineFunction2("intMod") { (x, y) => + If(y === 0, throwDivByZero, { + Return((x % y) | 0) + }) + } ::: + defineFunction1("doubleToInt") { x => + Return(If(x > 2147483647, 2147483647, If(x < -2147483648, -2147483648, x | 0))) + } ::: + condDefs(semantics.stringIndexOutOfBounds != CheckedBehavior.Unchecked)( + defineFunction2("charAt") { (s, i) => + val r = varRef("r") + + val throwStringIndexOutOfBoundsException = { + Throw(maybeWrapInUBE(semantics.stringIndexOutOfBounds, + genScalaClassNew(StringIndexOutOfBoundsExceptionClass, IntArgConstructorName, i))) } - ), - condTree(allowBigIntsForLongs)(Block( - defineFunction2("longDiv") { (x, y) => - If(y === bigInt(0), throwDivByZero, { - Return(wrapBigInt64(x / y)) - }) - }, - defineFunction2("longMod") { (x, y) => - If(y === bigInt(0), throwDivByZero, { - Return(wrapBigInt64(x % y)) - }) - }, + Block( + const(r, Apply(genIdentBracketSelect(s, "charCodeAt"), List(i))), + If(r !== r, throwStringIndexOutOfBoundsException, Return(r)) + ) + } + ) ::: + condDefs(allowBigIntsForLongs)( + defineFunction2("longDiv") { (x, y) => + If(y === bigInt(0), throwDivByZero, { + Return(wrapBigInt64(x / y)) + }) + } ::: + defineFunction2("longMod") { (x, y) => + If(y === bigInt(0), throwDivByZero, { + Return(wrapBigInt64(x % y)) + }) + } ::: - defineFunction1("doubleToLong")(x => Return { - If(x < double(-9223372036854775808.0), { // -2^63 - bigInt(-9223372036854775808L) + defineFunction1("doubleToLong")(x => Return { + If(x < double(-9223372036854775808.0), { // -2^63 + bigInt(-9223372036854775808L) + }, { + If (x >= double(9223372036854775808.0), { // 2^63 + bigInt(9223372036854775807L) }, { - If (x >= double(9223372036854775808.0), { // 2^63 - bigInt(9223372036854775807L) + If (x !== x, { // NaN + bigInt(0L) }, { - If (x !== x, { // NaN - bigInt(0L) - }, { - Apply(BigIntRef, - Apply(genIdentBracketSelect(MathRef, "trunc"), x :: Nil) :: Nil) - }) + Apply(BigIntRef, + Apply(genIdentBracketSelect(MathRef, "trunc"), x :: Nil) :: Nil) }) }) - }), + }) + }) ::: - defineFunction1("longToFloat") { x => - val abs = varRef("abs") - val y = varRef("y") - val absR = varRef("absR") + defineFunction1("longToFloat") { x => + val abs = varRef("abs") + val y = varRef("y") + val absR = varRef("absR") - // See RuntimeLong.toFloat for the strategy - Block( - const(abs, If(x < bigInt(0L), -x, x)), - const(y, If(abs <= bigInt(1L << 53) || (abs & bigInt(0xffffL)) === bigInt(0L), { - abs - }, { - (abs & bigInt(~0xffffL)) | bigInt(0x8000L) - })), - const(absR, Apply(NumberRef, y :: Nil)), - Return(genCallPolyfillableBuiltin(FroundBuiltin, If(x < bigInt(0L), -absR, absR))) - ) - } - )) + // See RuntimeLong.toFloat for the strategy + Block( + const(abs, If(x < bigInt(0L), -x, x)), + const(y, If(abs <= bigInt(1L << 53) || (abs & bigInt(0xffffL)) === bigInt(0L), { + abs + }, { + (abs & bigInt(~0xffffL)) | bigInt(0x8000L) + })), + const(absR, Apply(NumberRef, y :: Nil)), + Return(genCallPolyfillableBuiltin(FroundBuiltin, If(x < bigInt(0L), -absR, absR))) + ) + } ) } - private def defineES2015LikeHelpers(): Tree = Block( - condTree(esVersion < ESVersion.ES2015)( + private def defineES2015LikeHelpers(): List[Tree] = ( + condDefs(esVersion < ESVersion.ES2015)( defineFunction2("newJSObjectWithVarargs") { (ctor, args) => val instance = varRef("instance") val result = varRef("result") @@ -1041,7 +1022,7 @@ private[emitter] object CoreJSLib { Return(If(result === Null(), instance, result))) ) } - ), + ) ::: defineFunction2("resolveSuperRef") { (superClass, propName) => val getPrototypeOf = varRef("getPrototypeOf") @@ -1059,7 +1040,7 @@ private[emitter] object CoreJSLib { superProto := Apply(getPrototypeOf, superProto :: Nil) )) ) - }, + } ::: defineFunction3("superGet") { (superClass, self, propName) => val desc = varRef("desc") @@ -1074,7 +1055,7 @@ private[emitter] object CoreJSLib { genIdentBracketSelect(getter, "value"))) )) ) - }, + } ::: defineFunction4("superSet") { (superClass, self, propName, value) => val desc = varRef("desc") @@ -1095,8 +1076,8 @@ private[emitter] object CoreJSLib { } ) - private def defineModuleHelpers(): Tree = { - condTree(moduleKind == ModuleKind.CommonJSModule)( + private def defineModuleHelpers(): List[Tree] = { + condDefs(moduleKind == ModuleKind.CommonJSModule)( defineFunction1("moduleDefault") { m => Return(If( m && (typeof(m) === str("object")) && (str("default") in m), @@ -1106,8 +1087,8 @@ private[emitter] object CoreJSLib { ) } - private def defineIntrinsics(): Tree = Block( - condTree(arrayIndexOutOfBounds != CheckedBehavior.Unchecked)( + private def defineIntrinsics(): List[Tree] = ( + condDefs(arrayIndexOutOfBounds != CheckedBehavior.Unchecked)( defineFunction5("arraycopyCheckBounds") { (srcLen, srcPos, destLen, destPos, length) => If((srcPos < 0) || (destPos < 0) || (length < 0) || (srcPos > ((srcLen - length) | 0)) || @@ -1115,7 +1096,7 @@ private[emitter] object CoreJSLib { genCallHelper("throwArrayIndexOutOfBoundsException", Null()) }) } - ), + ) ::: defineFunction5("arraycopyGeneric") { (srcArray, srcPos, destArray, destPos, length) => val i = varRef("i") @@ -1136,20 +1117,20 @@ private[emitter] object CoreJSLib { }) }) ) - }, + } ::: - condTree(esVersion < ESVersion.ES2015)( + condDefs(esVersion < ESVersion.ES2015)( defineFunction5("systemArraycopy") { (src, srcPos, dest, destPos, length) => genCallHelper("arraycopyGeneric", src.u, srcPos, dest.u, destPos, length) } - ), - condTree(esVersion >= ESVersion.ES2015 && nullPointers != CheckedBehavior.Unchecked)( + ) ::: + condDefs(esVersion >= ESVersion.ES2015 && nullPointers != CheckedBehavior.Unchecked)( defineFunction5("systemArraycopy") { (src, srcPos, dest, destPos, length) => Apply(src DOT "copyTo", List(srcPos, dest, destPos, length)) } - ), + ) ::: - condTree(arrayStores != CheckedBehavior.Unchecked)(Block( + condDefs(arrayStores != CheckedBehavior.Unchecked)( defineFunction5("systemArraycopyRefs") { (src, srcPos, dest, destPos, length) => If(Apply(genIdentBracketSelect(dest DOT classData, "isAssignableFrom"), List(src DOT classData)), { /* Fast-path, no need for array store checks. This always applies @@ -1173,7 +1154,7 @@ private[emitter] object CoreJSLib { }) ) }) - }, + } ::: defineFunction5("systemArraycopyFull") { (src, srcPos, dest, destPos, length) => val ObjectArray = globalVar("ac", ObjectClass) @@ -1201,7 +1182,7 @@ private[emitter] object CoreJSLib { }) ) } - )), + ) ::: // systemIdentityHashCode locally { @@ -1334,11 +1315,12 @@ private[emitter] object CoreJSLib { } } - Block( + List( let(lastIDHash, 0), const(idHashCodeMap, if (esVersion >= ESVersion.ES2015) New(WeakMapRef, Nil) - else If(typeof(WeakMapRef) !== str("undefined"), New(WeakMapRef, Nil), Null())), + else If(typeof(WeakMapRef) !== str("undefined"), New(WeakMapRef, Nil), Null())) + ) ::: ( if (esVersion >= ESVersion.ES2015) { val f = weakMapBasedFunction defineFunction("systemIdentityHashCode", f.args, f.body) @@ -1350,43 +1332,41 @@ private[emitter] object CoreJSLib { } ) - private def defineIsPrimitiveFunctions(): Tree = { - def defineIsIntLike(name: String, specificTest: VarRef => Tree): Tree = { + private def defineIsPrimitiveFunctions(): List[Tree] = { + def defineIsIntLike(name: String, specificTest: VarRef => Tree): List[Tree] = { defineFunction1(name) { v => Return((typeof(v) === str("number")) && specificTest(v) && ((int(1) / v) !== (int(1) / double(-0.0)))) } } - Block( - defineIsIntLike("isByte", v => (v << 24 >> 24) === v), - defineIsIntLike("isShort", v => (v << 16 >> 16) === v), - defineIsIntLike("isInt", v => (v | 0) === v), - condTree(allowBigIntsForLongs)( - defineFunction1("isLong") { v => - Return((typeof(v) === str("bigint")) && - (Apply(genIdentBracketSelect(BigIntRef, "asIntN"), int(64) :: v :: Nil) === v)) - } - ), - condTree(strictFloats)( - defineFunction1("isFloat") { v => - Return((typeof(v) === str("number")) && - ((v !== v) || (genCallPolyfillableBuiltin(FroundBuiltin, v) === v))) - } - ) + defineIsIntLike("isByte", v => (v << 24 >> 24) === v) ::: + defineIsIntLike("isShort", v => (v << 16 >> 16) === v) ::: + defineIsIntLike("isInt", v => (v | 0) === v) ::: + condDefs(allowBigIntsForLongs)( + defineFunction1("isLong") { v => + Return((typeof(v) === str("bigint")) && + (Apply(genIdentBracketSelect(BigIntRef, "asIntN"), int(64) :: v :: Nil) === v)) + } + ) ::: + condDefs(strictFloats)( + defineFunction1("isFloat") { v => + Return((typeof(v) === str("number")) && + ((v !== v) || (genCallPolyfillableBuiltin(FroundBuiltin, v) === v))) + } ) } - private def defineBoxFunctions(): Tree = Block( + private def defineBoxFunctions(): List[Tree] = ( // Boxes for Chars defineFunction1("bC") { c => Return(New(globalVar("Char", CoreVar), c :: Nil)) - }, - extractWithGlobals(globalVarDef("bC0", CoreVar, genCallHelper("bC", 0))), - + } ::: + extractWithGlobals(globalVarDef("bC0", CoreVar, genCallHelper("bC", 0))) + ) ::: ( if (asInstanceOfs != CheckedBehavior.Unchecked) { // Unboxes for everything - def defineUnbox(name: String, boxedClassName: ClassName, resultExpr: VarRef => Tree): Tree = { + def defineUnbox(name: String, boxedClassName: ClassName, resultExpr: VarRef => Tree): List[Tree] = { val fullName = boxedClassName.nameString defineFunction1(name)(v => Return { If(genIsInstanceOfHijackedClass(v, boxedClassName) || (v === Null()), @@ -1395,29 +1375,29 @@ private[emitter] object CoreJSLib { }) } - Block( - defineUnbox("uV", BoxedUnitClass, _ => Undefined()), - defineUnbox("uZ", BoxedBooleanClass, v => !(!v)), - defineUnbox("uC", BoxedCharacterClass, v => If(v === Null(), 0, v DOT "c")), - defineUnbox("uB", BoxedByteClass, _ | 0), - defineUnbox("uS", BoxedShortClass, _ | 0), - defineUnbox("uI", BoxedIntegerClass, _ | 0), - defineUnbox("uJ", BoxedLongClass, v => If(v === Null(), genLongZero(), v)), + ( + defineUnbox("uV", BoxedUnitClass, _ => Undefined()) ::: + defineUnbox("uZ", BoxedBooleanClass, v => !(!v)) ::: + defineUnbox("uC", BoxedCharacterClass, v => If(v === Null(), 0, v DOT "c")) ::: + defineUnbox("uB", BoxedByteClass, _ | 0) ::: + defineUnbox("uS", BoxedShortClass, _ | 0) ::: + defineUnbox("uI", BoxedIntegerClass, _ | 0) ::: + defineUnbox("uJ", BoxedLongClass, v => If(v === Null(), genLongZero(), v)) ::: /* Since the type test ensures that v is either null or a float, we can * use + instead of fround. */ - defineUnbox("uF", BoxedFloatClass, v => +v), + defineUnbox("uF", BoxedFloatClass, v => +v) ::: - defineUnbox("uD", BoxedDoubleClass, v => +v), + defineUnbox("uD", BoxedDoubleClass, v => +v) ::: defineUnbox("uT", BoxedStringClass, v => If(v === Null(), StringLiteral(""), v)) ) } else { // Unboxes for Chars and Longs - Block( + ( defineFunction1("uC") { v => Return(If(v === Null(), 0, v DOT "c")) - }, + } ::: defineFunction1("uJ") { v => Return(If(v === Null(), genLongZero(), v)) } @@ -1430,8 +1410,8 @@ private[emitter] object CoreJSLib { * Other array classes are created dynamically from their TypeData's * `initArray` initializer, and extend the array class for `Object`. */ - private def defineSpecializedArrayClasses(): Tree = Block( - for (componentTypeRef <- specializedArrayTypeRefs) yield { + private def defineSpecializedArrayClasses(): List[Tree] = { + specializedArrayTypeRefs.flatMap { componentTypeRef => val ArrayClass = globalVar("ac", componentTypeRef) val isArrayOfObject = componentTypeRef == ClassRef(ObjectClass) @@ -1527,27 +1507,25 @@ private[emitter] object CoreJSLib { extractWithGlobals(globalClassDef("ac", componentTypeRef, Some(globalVar("c", ObjectClass)), ctor :: members)) } else { - val clsDef = Block( + val clsDef = { extractWithGlobals(globalFunctionDef("ac", componentTypeRef, - ctor.args, ctor.restParam, ctor.body)), - (ArrayClass.prototype := New(globalVar("h", ObjectClass), Nil)), - (ArrayClass.prototype DOT "constructor" := ArrayClass), + ctor.args, ctor.restParam, ctor.body)) ::: + (ArrayClass.prototype := New(globalVar("h", ObjectClass), Nil)) :: + (ArrayClass.prototype DOT "constructor" := ArrayClass) :: assignES5ClassMembers(ArrayClass, members) - ) + } componentTypeRef match { case _: ClassRef => - Block( - clsDef, - extractWithGlobals(globalFunctionDef("ah", ObjectClass, Nil, None, Skip())), - (globalVar("ah", ObjectClass).prototype := ArrayClass.prototype) - ) + clsDef ::: + extractWithGlobals(globalFunctionDef("ah", ObjectClass, Nil, None, Skip())) ::: + (globalVar("ah", ObjectClass).prototype := ArrayClass.prototype) :: Nil case _: PrimRef => clsDef } } } - ) + } private def genArrayClassConstructorBody(arg: VarRef, componentTypeRef: NonArrayTypeRef): Tree = { @@ -1579,7 +1557,7 @@ private[emitter] object CoreJSLib { }) } - private def defineTypeDataClass(): Tree = { + private def defineTypeDataClass(): List[Tree] = { def privateFieldSet(fieldName: String, value: Tree): Tree = This() DOT fieldName := value @@ -1836,10 +1814,10 @@ private[emitter] object CoreJSLib { ctor :: members) } else { Block( - FunctionDef(ArrayClass.ident, ctor.args, ctor.restParam, ctor.body), - ArrayClass.prototype := New(globalVar("ah", ObjectClass), Nil), - ArrayClass.prototype DOT "constructor" := ArrayClass, - assignES5ClassMembers(ArrayClass, members) + FunctionDef(ArrayClass.ident, ctor.args, ctor.restParam, ctor.body) :: + (ArrayClass.prototype := New(globalVar("ah", ObjectClass), Nil)) :: + (ArrayClass.prototype DOT "constructor" := ArrayClass) :: + assignES5ClassMembers(ArrayClass, members) ) } } @@ -1998,14 +1976,12 @@ private[emitter] object CoreJSLib { if (useClassesForRegularClasses) { extractWithGlobals(globalClassDef("TypeData", CoreVar, None, ctor :: members)) } else { - Block( - defineFunction("TypeData", ctor.args, ctor.body), - assignES5ClassMembers(globalVar("TypeData", CoreVar), members) - ) + defineFunction("TypeData", ctor.args, ctor.body) ::: + assignES5ClassMembers(globalVar("TypeData", CoreVar), members) } } - private def defineSpecializedIsArrayOfFunctions(): Tree = { + private def defineSpecializedIsArrayOfFunctions(): List[Tree] = { // isArrayOf_O val obj = varRef("obj") val depth = varRef("depth") @@ -2030,7 +2006,7 @@ private[emitter] object CoreJSLib { ) })) - val forPrims = for (primRef <- orderedPrimRefsWithoutVoid) yield { + val forPrims = orderedPrimRefsWithoutVoid.flatMap { primRef => val obj = varRef("obj") val depth = varRef("depth") extractWithGlobals(globalFunctionDef("isArrayOf", primRef, paramList(obj, depth), None, { @@ -2040,12 +2016,12 @@ private[emitter] object CoreJSLib { })) } - Block(forObj :: forPrims) + forObj ::: forPrims } - private def defineSpecializedAsArrayOfFunctions(): Tree = { - condTree(asInstanceOfs != CheckedBehavior.Unchecked)(Block( - for (typeRef <- specializedArrayTypeRefs) yield { + private def defineSpecializedAsArrayOfFunctions(): List[Tree] = { + condDefs(asInstanceOfs != CheckedBehavior.Unchecked)( + specializedArrayTypeRefs.flatMap { typeRef => val encodedName = typeRef match { case typeRef: PrimRef => typeRef.charCode.toString() case _ => "L" + ObjectClass.nameString + ";" @@ -2061,10 +2037,10 @@ private[emitter] object CoreJSLib { }) })) } - )) + ) } - private def defineSpecializedTypeDatas(): Tree = { + private def defineSpecializedTypeDatas(): List[Tree] = { /* d_O must be first to correctly populate the parentData of array * classes. Unlike all other type datas, we assign the first of d_O * directly in the generated code, rather than through an `initXyz` @@ -2087,9 +2063,9 @@ private[emitter] object CoreJSLib { def publicFieldSet(fieldName: String, value: Tree): Tree = genIdentBracketSelect(typeDataVar, fieldName) := value - Block( - extractWithGlobals( - globalVarDef("d", ObjectClass, New(globalVar("TypeData", CoreVar), Nil))), + extractWithGlobals( + globalVarDef("d", ObjectClass, New(globalVar("TypeData", CoreVar), Nil))) ::: + List( privateFieldSet("ancestors", ObjectConstr(List((Ident(genName(ObjectClass)) -> 1)))), privateFieldSet("arrayEncodedName", str("L" + fullName + ";")), privateFieldSet("isAssignableFromFun", { @@ -2122,7 +2098,7 @@ private[emitter] object CoreJSLib { ) } - val prims = for (primRef <- orderedPrimRefs) yield { + val prims = orderedPrimRefs.flatMap { primRef => /* Zero value, for use by the intrinsified code of * `scala.collection.mutable.ArrayBuilder.genericArrayBuilderResult`. * This code is Scala-specific, and "unboxes" `null` as the zero of @@ -2153,38 +2129,38 @@ private[emitter] object CoreJSLib { })) } - Block(obj :: prims) + obj ::: prims } - private def defineFunction(name: String, args: List[ParamDef], body: Tree): Tree = + private def defineFunction(name: String, args: List[ParamDef], body: Tree): List[Tree] = extractWithGlobals(globalFunctionDef(name, CoreVar, args, None, body)) private val argRefs = List.tabulate(5)(i => varRef("arg" + i)) - private def defineFunction0(name: String)(body: Tree): Tree = + private def defineFunction0(name: String)(body: Tree): List[Tree] = defineFunction(name, Nil, body) - private def defineFunction1(name: String)(body: VarRef => Tree): Tree = { + private def defineFunction1(name: String)(body: VarRef => Tree): List[Tree] = { val a :: _ = argRefs defineFunction(name, paramList(a), body(a)) } - private def defineFunction2(name: String)(body: (VarRef, VarRef) => Tree): Tree = { + private def defineFunction2(name: String)(body: (VarRef, VarRef) => Tree): List[Tree] = { val a :: b :: _ = argRefs defineFunction(name, paramList(a, b), body(a, b)) } - private def defineFunction3(name: String)(body: (VarRef, VarRef, VarRef) => Tree): Tree = { + private def defineFunction3(name: String)(body: (VarRef, VarRef, VarRef) => Tree): List[Tree] = { val a :: b :: c :: _ = argRefs defineFunction(name, paramList(a, b, c), body(a, b, c)) } - private def defineFunction4(name: String)(body: (VarRef, VarRef, VarRef, VarRef) => Tree): Tree = { + private def defineFunction4(name: String)(body: (VarRef, VarRef, VarRef, VarRef) => Tree): List[Tree] = { val a :: b :: c :: d :: _ = argRefs defineFunction(name, paramList(a, b, c, d), body(a, b, c, d)) } - private def defineFunction5(name: String)(body: (VarRef, VarRef, VarRef, VarRef, VarRef) => Tree): Tree = { + private def defineFunction5(name: String)(body: (VarRef, VarRef, VarRef, VarRef, VarRef) => Tree): List[Tree] = { val a :: b :: c :: d :: e :: _ = argRefs defineFunction(name, paramList(a, b, c, d, e), body(a, b, c, d, e)) } @@ -2216,6 +2192,10 @@ private[emitter] object CoreJSLib { if (cond) tree else Skip() + private def condDefs(cond: Boolean)(trees: => List[Tree]): List[Tree] = + if (cond) trees + else Nil + private def varRef(name: String): VarRef = VarRef(Ident(name)) private def const(ref: VarRef, rhs: Tree): LocalDef = diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala index 0c3babc589..d5b23e4525 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala @@ -241,11 +241,11 @@ final class Emitter(config: Emitter.Config) { * requires consistency between the Analyzer and the Emitter. As such, * it is crucial that we verify it. */ - val defTreesIterator: Iterator[js.Tree] = ( + val defTrees: List[js.Tree] = ( /* The definitions of the CoreJSLib that come before the definition * of `j.l.Object`. They depend on nothing else. */ - coreJSLib.map(_.preObjectDefinitions).iterator ++ + coreJSLib.iterator.flatMap(_.preObjectDefinitions) ++ /* The definition of `j.l.Object` class. Unlike other classes, this * does not include its instance tests nor metadata. @@ -257,7 +257,7 @@ final class Emitter(config: Emitter.Config) { * definitions of the array classes, as well as type data for * primitive types and for `j.l.Object`. */ - coreJSLib.map(_.postObjectDefinitions).iterator ++ + coreJSLib.iterator.flatMap(_.postObjectDefinitions) ++ /* All class definitions, except `j.l.Object`, which depend on * nothing but their superclasses. @@ -267,7 +267,7 @@ final class Emitter(config: Emitter.Config) { /* The initialization of the CoreJSLib, which depends on the * definition of classes (n.b. the RuntimeLong class). */ - coreJSLib.map(_.initialization).iterator ++ + coreJSLib.iterator.flatMap(_.initialization) ++ /* All static field definitions, which depend on nothing, except * those of type Long which need $L0. @@ -288,17 +288,7 @@ final class Emitter(config: Emitter.Config) { /* Module initializers, which by spec run at the end. */ moduleInitializers.iterator - ) - - /* Flatten all the top-level js.Block's, because we temporarily use - * them to gather several top-level trees under a single `js.Tree`. - * TODO We should improve this in the future. - */ - val defTrees: List[js.Tree] = defTreesIterator.flatMap { - case js.Block(stats) => stats - case js.Skip() => Nil - case stat => stat :: Nil - }.toList + ).toList // Make sure that there is at least one non-import definition. assert(!defTrees.isEmpty, { @@ -391,9 +381,6 @@ final class Emitter(config: Emitter.Config) { val main = List.newBuilder[js.Tree] - def addToMain(treeWithGlobals: WithGlobals[js.Tree]): Unit = - main += extractWithGlobals(treeWithGlobals) - val (linkedInlineableInit, linkedMethods) = classEmitter.extractInlineableInit(linkedClass)(classCache) @@ -420,7 +407,7 @@ final class Emitter(config: Emitter.Config) { val methodCache = classCache.getStaticLikeMethodCache(namespace, methodDef.methodName) - addToMain(methodCache.getOrElseUpdate(methodDef.version, + main ++= extractWithGlobals(methodCache.getOrElseUpdate(methodDef.version, classEmitter.genStaticLikeMethod(className, methodDef)(moduleContext, methodCache))) } } @@ -582,7 +569,7 @@ final class Emitter(config: Emitter.Config) { useESClass, // invalidated by class version (depends on kind, config and ancestry only) ctor, // invalidated directly memberMethods, // invalidated directly - exportedMembers // invalidated directly + exportedMembers.flatten // invalidated directly )(moduleContext, fullClassCache, linkedClass.pos) // pos invalidated by class version } yield { clazz @@ -590,7 +577,7 @@ final class Emitter(config: Emitter.Config) { }) } - addToMain(fullClass) + main ++= extractWithGlobals(fullClass) } if (className != ObjectClass) { @@ -608,12 +595,12 @@ final class Emitter(config: Emitter.Config) { */ if (classEmitter.needInstanceTests(linkedClass)(classCache)) { - addToMain(classTreeCache.instanceTests.getOrElseUpdate( + main += extractWithGlobals(classTreeCache.instanceTests.getOrElseUpdate( classEmitter.genInstanceTests(className, kind)(moduleContext, classCache, linkedClass.pos))) } if (linkedClass.hasRuntimeTypeInfo) { - addToMain(classTreeCache.typeData.getOrElseUpdate( + main ++= extractWithGlobals(classTreeCache.typeData.getOrElseUpdate( classEmitter.genTypeData( className, // invalidated by overall class cache (part of ancestors) kind, // invalidated by class version @@ -630,7 +617,7 @@ final class Emitter(config: Emitter.Config) { } if (linkedClass.kind.hasModuleAccessor && linkedClass.hasInstances) { - addToMain(classTreeCache.moduleAccessor.getOrElseUpdate( + main ++= extractWithGlobals(classTreeCache.moduleAccessor.getOrElseUpdate( classEmitter.genModuleAccessor(className, kind)(moduleContext, classCache, linkedClass.pos))) } @@ -772,15 +759,15 @@ final class Emitter(config: Emitter.Config) { private[this] var _cacheUsed = false private[this] val _methodCaches = - Array.fill(MemberNamespace.Count)(mutable.Map.empty[MethodName, MethodCache[js.Tree]]) + Array.fill(MemberNamespace.Count)(mutable.Map.empty[MethodName, MethodCache[List[js.Tree]]]) private[this] val _memberMethodCache = mutable.Map.empty[MethodName, MethodCache[js.MethodDef]] - private[this] var _constructorCache: Option[MethodCache[js.Tree]] = None + private[this] var _constructorCache: Option[MethodCache[List[js.Tree]]] = None private[this] val _exportedMembersCache = - mutable.Map.empty[Int, MethodCache[js.Tree]] + mutable.Map.empty[Int, MethodCache[List[js.Tree]]] private[this] var _fullClassCache: Option[FullClassCache] = None @@ -820,20 +807,20 @@ final class Emitter(config: Emitter.Config) { } def getStaticLikeMethodCache(namespace: MemberNamespace, - methodName: MethodName): MethodCache[js.Tree] = { + methodName: MethodName): MethodCache[List[js.Tree]] = { _methodCaches(namespace.ordinal) .getOrElseUpdate(methodName, new MethodCache) } - def getConstructorCache(): MethodCache[js.Tree] = { + def getConstructorCache(): MethodCache[List[js.Tree]] = { _constructorCache.getOrElse { - val cache = new MethodCache[js.Tree] + val cache = new MethodCache[List[js.Tree]] _constructorCache = Some(cache) cache } } - def getExportedMemberCache(idx: Int): MethodCache[js.Tree] = + def getExportedMemberCache(idx: Int): MethodCache[List[js.Tree]] = _exportedMembersCache.getOrElseUpdate(idx, new MethodCache) def getFullClassCache(): FullClassCache = { @@ -863,7 +850,7 @@ final class Emitter(config: Emitter.Config) { } } - private final class MethodCache[T <: js.Tree] extends knowledgeGuardian.KnowledgeAccessor { + private final class MethodCache[T] extends knowledgeGuardian.KnowledgeAccessor { private[this] var _tree: WithGlobals[T] = null private[this] var _lastVersion: Version = Version.Unversioned private[this] var _cacheUsed = false @@ -899,11 +886,11 @@ final class Emitter(config: Emitter.Config) { } private class FullClassCache extends knowledgeGuardian.KnowledgeAccessor { - private[this] var _tree: WithGlobals[js.Tree] = null + private[this] var _tree: WithGlobals[List[js.Tree]] = null private[this] var _lastVersion: Version = Version.Unversioned - private[this] var _lastCtor: WithGlobals[js.Tree] = null + private[this] var _lastCtor: WithGlobals[List[js.Tree]] = null private[this] var _lastMemberMethods: List[WithGlobals[js.MethodDef]] = null - private[this] var _lastExportedMembers: List[WithGlobals[js.Tree]] = null + private[this] var _lastExportedMembers: List[WithGlobals[List[js.Tree]]] = null private[this] var _cacheUsed = false override def invalidate(): Unit = { @@ -917,9 +904,9 @@ final class Emitter(config: Emitter.Config) { def startRun(): Unit = _cacheUsed = false - def getOrElseUpdate(version: Version, ctor: WithGlobals[js.Tree], - memberMethods: List[WithGlobals[js.MethodDef]], exportedMembers: List[WithGlobals[js.Tree]], - compute: => WithGlobals[js.Tree]): WithGlobals[js.Tree] = { + def getOrElseUpdate(version: Version, ctor: WithGlobals[List[js.Tree]], + memberMethods: List[WithGlobals[js.MethodDef]], exportedMembers: List[WithGlobals[List[js.Tree]]], + compute: => WithGlobals[List[js.Tree]]): WithGlobals[List[js.Tree]] = { @tailrec def allSame[A <: AnyRef](xs: List[A], ys: List[A]): Boolean = { @@ -1049,9 +1036,9 @@ object Emitter { private final class DesugaredClassCache { val privateJSFields = new OneTimeCache[WithGlobals[List[js.Tree]]] val instanceTests = new OneTimeCache[WithGlobals[js.Tree]] - val typeData = new OneTimeCache[WithGlobals[js.Tree]] + val typeData = new OneTimeCache[WithGlobals[List[js.Tree]]] val setTypeData = new OneTimeCache[js.Tree] - val moduleAccessor = new OneTimeCache[WithGlobals[js.Tree]] + val moduleAccessor = new OneTimeCache[WithGlobals[List[js.Tree]]] val staticInitialization = new OneTimeCache[List[js.Tree]] val staticFields = new OneTimeCache[WithGlobals[List[js.Tree]]] } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/JSGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/JSGen.scala index 6e3c9f22d9..c5e3aba380 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/JSGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/JSGen.scala @@ -157,17 +157,15 @@ private[emitter] final class JSGen(val config: Emitter.Config) { } def assignES5ClassMembers(classRef: Tree, members: List[MethodDef])( - implicit pos: Position): Tree = { + implicit pos: Position): List[Tree] = { import TreeDSL._ - val stats = for { + for { MethodDef(static, name, args, restParam, body) <- members } yield { val target = if (static) classRef else classRef.prototype genPropSelect(target, name) := Function(arrow = false, args, restParam, body) } - - Block(stats) } def genIIFE(captures: List[(ParamDef, Tree)], body: Tree)( diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarGen.scala index 0ca1cd3deb..cff6305663 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarGen.scala @@ -54,7 +54,7 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, def globalClassDef[T: Scope](field: String, scope: T, parentClass: Option[Tree], members: List[Tree], origName: OriginalName = NoOriginalName)( - implicit moduleContext: ModuleContext, pos: Position): WithGlobals[Tree] = { + implicit moduleContext: ModuleContext, pos: Position): WithGlobals[List[Tree]] = { val ident = globalVarIdent(field, scope, origName) maybeExport(ident, ClassDef(Some(ident), parentClass, members), mutable = false) } @@ -62,14 +62,14 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, def globalFunctionDef[T: Scope](field: String, scope: T, args: List[ParamDef], restParam: Option[ParamDef], body: Tree, origName: OriginalName = NoOriginalName)( - implicit moduleContext: ModuleContext, pos: Position): WithGlobals[Tree] = { + implicit moduleContext: ModuleContext, pos: Position): WithGlobals[List[Tree]] = { val ident = globalVarIdent(field, scope, origName) maybeExport(ident, FunctionDef(ident, args, restParam, body), mutable = false) } def globalVarDef[T: Scope](field: String, scope: T, value: Tree, origName: OriginalName = NoOriginalName)( - implicit moduleContext: ModuleContext, pos: Position): WithGlobals[Tree] = { + implicit moduleContext: ModuleContext, pos: Position): WithGlobals[List[Tree]] = { val ident = globalVarIdent(field, scope, origName) maybeExport(ident, genConst(ident, value), mutable = false) } @@ -77,7 +77,7 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, /** Attention: A globalVarDecl may only be modified from the module it was declared in. */ def globalVarDecl[T: Scope](field: String, scope: T, origName: OriginalName = NoOriginalName)( - implicit moduleContext: ModuleContext, pos: Position): WithGlobals[Tree] = { + implicit moduleContext: ModuleContext, pos: Position): WithGlobals[List[Tree]] = { val ident = globalVarIdent(field, scope, origName) maybeExport(ident, genEmptyMutableLet(ident), mutable = true) } @@ -88,7 +88,7 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, */ def globallyMutableVarDef[T: Scope](field: String, setterField: String, scope: T, value: Tree, origName: OriginalName = NoOriginalName)( - implicit moduleContext: ModuleContext, pos: Position): WithGlobals[Tree] = { + implicit moduleContext: ModuleContext, pos: Position): WithGlobals[List[Tree]] = { val ident = globalVarIdent(field, scope, origName) val varDef = genLet(ident, mutable = true, value) @@ -102,7 +102,7 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, val exports = Export(genExportIdent(ident) :: genExportIdent(setterIdent) :: Nil) - WithGlobals(Block(varDef, setter, exports)) + WithGlobals(List(varDef, setter, exports)) } else { maybeExport(ident, varDef, mutable = true) } @@ -269,9 +269,9 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, } private def maybeExport(ident: Ident, tree: Tree, mutable: Boolean)( - implicit moduleContext: ModuleContext, pos: Position): WithGlobals[Tree] = { + implicit moduleContext: ModuleContext, pos: Position): WithGlobals[List[Tree]] = { if (moduleContext.public) { - WithGlobals(tree) + WithGlobals(tree :: Nil) } else { val exportStat = config.moduleKind match { case ModuleKind.NoModule => @@ -299,7 +299,7 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, } } - exportStat.map(Block(tree, _)) + exportStat.map(tree :: _ :: Nil) } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/WithGlobals.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/WithGlobals.scala index 65b10a9ed1..6ff6b02544 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/WithGlobals.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/WithGlobals.scala @@ -98,10 +98,20 @@ private[emitter] object WithGlobals { * efficient. */ val values = xs.map(_.value) - val globalVarNames = xs.foldLeft(Set.empty[String]) { (prev, x) => + val globalVarNames = collectNames(xs) + WithGlobals(values, globalVarNames) + } + + def flatten[A](xs: List[WithGlobals[List[A]]]): WithGlobals[List[A]] = { + val values = xs.flatMap(_.value) + val globalVarNames = collectNames(xs) + WithGlobals(values, globalVarNames) + } + + private def collectNames(xs: List[WithGlobals[_]]): Set[String] = { + xs.foldLeft(Set.empty[String]) { (prev, x) => unionPreserveEmpty(prev, x.globalVarNames) } - WithGlobals(values, globalVarNames) } def option[A](xs: Option[WithGlobals[A]]): WithGlobals[Option[A]] = From 6c214241f53ed2903fbccfedbbabbb609bab56cc Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sat, 14 Oct 2023 15:11:22 +0200 Subject: [PATCH 003/298] Create a VarGen scope for dispatchers Dispatchers are the only place where we pass a dynamically computed string to the VarGen subsystem. --- .../scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala | 4 ++-- .../org/scalajs/linker/backend/emitter/FunctionEmitter.scala | 2 +- .../scala/org/scalajs/linker/backend/emitter/VarGen.scala | 5 +++++ 3 files changed, 8 insertions(+), 3 deletions(-) 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 790ccfab2f..d610e8cdb8 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 @@ -823,8 +823,8 @@ private[emitter] object CoreJSLib { def defineDispatcher(methodName: MethodName, args: List[VarRef], body: Tree): List[Tree] = { - defineFunction("dp_" + genName(methodName), - paramList((instance :: args): _*), body) + val params = paramList((instance :: args): _*) + extractWithGlobals(globalFunctionDef("dp", methodName, params, None, body)) } /* A standard dispatcher performs a type test on the instance and then 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 d0cd14d62c..fa7cb5ebb1 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 @@ -2247,7 +2247,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { js.Apply(newReceiver(false) DOT transformMethodIdent(method), newArgs) def genDispatchApply(): js.Tree = - genCallHelper("dp_" + genName(methodName), newReceiver(false) :: newArgs: _*) + js.Apply(globalVar("dp", methodName), newReceiver(false) :: newArgs) def genHijackedMethodApply(className: ClassName): js.Tree = genApplyStaticLike("f", className, method, newReceiver(className == BoxedCharacterClass) :: newArgs) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarGen.scala index cff6305663..a2a1a12153 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarGen.scala @@ -358,6 +358,11 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, def reprClass(x: (ClassName, MethodName)): ClassName = x._1 } + implicit object DispatcherScope extends Scope[MethodName] { + def subField(x: MethodName): String = genName(x) + def reprClass(x: MethodName): ClassName = ObjectClass + } + implicit object CoreJSLibScope extends Scope[CoreVar.type] { def subField(x: CoreVar.type): String = "" def reprClass(x: CoreVar.type): ClassName = ObjectClass From 414b2cda3d7edfa065e4a0044e35dfb516d1a70d Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 8 Oct 2023 18:02:10 +0200 Subject: [PATCH 004/298] Allocate all emitter var field strings statically --- .../linker/backend/emitter/ClassEmitter.scala | 108 +++---- .../linker/backend/emitter/CoreJSLib.scala | 284 +++++++++--------- .../backend/emitter/FunctionEmitter.scala | 80 ++--- .../backend/emitter/PolyfillableBuiltin.scala | 22 +- .../linker/backend/emitter/SJSGen.scala | 90 +++--- .../linker/backend/emitter/VarField.scala | 277 +++++++++++++++++ .../linker/backend/emitter/VarGen.scala | 42 +-- 7 files changed, 590 insertions(+), 313 deletions(-) create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarField.scala 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 be9fc9a993..0701d2fd84 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 @@ -62,14 +62,14 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val parentVarWithGlobals = for (parentIdent <- superClass) yield { implicit val pos = parentIdent.pos if (shouldExtendJSError(className, superClass)) untrackedGlobalRef("Error") - else WithGlobals(globalVar("c", parentIdent.name)) + else WithGlobals(globalVar(VarField.c, parentIdent.name)) } WithGlobals.option(parentVarWithGlobals).flatMap { parentVar => - globalClassDef("c", className, parentVar, allES6Defs) + globalClassDef(VarField.c, className, parentVar, allES6Defs) } } else { - allES5Defs(globalVar("c", className)) + allES5Defs(globalVar(VarField.c, className)) } } else { // Wrap the entire class def in an accessor function @@ -77,11 +77,11 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val genStoreJSSuperClass = jsSuperClass.map { jsSuperClass => for (rhs <- desugarExpr(jsSuperClass, resultType = AnyType)) yield { - js.VarDef(fileLevelVar("superClass").ident, Some(rhs)) + js.VarDef(fileLevelVar(VarField.superClass).ident, Some(rhs)) } } - val classValueIdent = fileLevelVarIdent("b", genName(className)) + val classValueIdent = fileLevelVarIdent(VarField.b, genName(className)) val classValueVar = js.VarRef(classValueIdent) val createClassValueVar = genEmptyMutableLet(classValueIdent) @@ -115,14 +115,14 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { }), js.Return(classValueVar) ) - createAccessor <- globalFunctionDef("a", className, Nil, None, body) + createAccessor <- globalFunctionDef(VarField.a, className, Nil, None, body) } yield { createClassValueVar :: createAccessor } } { jsClassCaptures => val captureParamDefs = for (param <- jsClassCaptures) yield { implicit val pos = param.pos - val ident = fileLevelVarIdent("cc", genName(param.name.name), + val ident = fileLevelVarIdent(VarField.cc, genName(param.name.name), param.originalName.orElse(param.name.name)) js.ParamDef(ident) } @@ -138,7 +138,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { Nil ) - globalFunctionDef("a", className, captureParamDefs, None, body) + globalFunctionDef(VarField.a, className, captureParamDefs, None, body) } } } @@ -191,7 +191,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } else { import TreeDSL._ - val ctorVar = globalVar("c", className) + val ctorVar = globalVar(VarField.c, className) val chainProtoWithGlobals = superClass match { case None => @@ -201,15 +201,15 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { untrackedGlobalRef("Error").map(chainPrototypeWithLocalCtor(className, ctorVar, _)) case Some(parentIdent) => - WithGlobals(List(ctorVar.prototype := js.New(globalVar("h", parentIdent.name), Nil))) + WithGlobals(List(ctorVar.prototype := js.New(globalVar(VarField.h, parentIdent.name), Nil))) } for { ctorFun <- jsConstructorFunWithGlobals realCtorDef <- - globalFunctionDef("c", className, ctorFun.args, ctorFun.restParam, ctorFun.body) + globalFunctionDef(VarField.c, className, ctorFun.args, ctorFun.restParam, ctorFun.body) inheritableCtorDef <- - globalFunctionDef("h", className, Nil, None, js.Skip()) + globalFunctionDef(VarField.h, className, Nil, None, js.Skip()) chainProto <- chainProtoWithGlobals } yield { ( @@ -222,7 +222,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { // Inheritable constructor js.DocComment("@constructor") :: inheritableCtorDef ::: - (globalVar("h", className).prototype := ctorVar.prototype) :: Nil + (globalVar(VarField.h, className).prototype := ctorVar.prototype) :: Nil ) } } @@ -249,7 +249,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } yield { import TreeDSL._ - val ctorVar = fileLevelVar("b", genName(className)) + val ctorVar = fileLevelVar(VarField.b, genName(className)) js.DocComment("@constructor") :: (ctorVar := ctorFun) :: @@ -263,7 +263,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[js.Tree] = { if (jsSuperClass.isDefined) { - WithGlobals(fileLevelVar("superClass")) + WithGlobals(fileLevelVar(VarField.superClass)) } else { genJSClassConstructor(superClass.get.name, keepOnlyDangerousVarNames = true) } @@ -341,7 +341,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { superCtor: js.Tree)(implicit pos: Position): List[js.Tree] = { import TreeDSL._ - val dummyCtor = fileLevelVar("hh", genName(className)) + val dummyCtor = fileLevelVar(VarField.hh, genName(className)) List( js.DocComment("@constructor"), @@ -381,9 +381,9 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val value = genZeroOf(ftpe) if (flags.isMutable) - globallyMutableVarDef("t", "u", varScope, value, origName.orElse(name)) + globallyMutableVarDef(VarField.t, VarField.u, varScope, value, origName.orElse(name)) else - globalVarDef("t", varScope, value, origName.orElse(name)) + globalVarDef(VarField.t, varScope, value, origName.orElse(name)) } WithGlobals.flatten(defs) @@ -410,7 +410,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } symbolValueWithGlobals.flatMap { symbolValue => - globalVarDef("r", (className, name), symbolValue, origName.orElse(name)) + globalVarDef(VarField.r, (className, name), symbolValue, origName.orElse(name)) } } @@ -426,7 +426,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { if field.flags.namespace.isStatic } yield { implicit val pos = field.pos - val classVarRef = fileLevelVar("b", genName(className)) + val classVarRef = fileLevelVar(VarField.b, genName(className)) val zero = genBoxedZeroOf(field.ftpe) field match { case FieldDef(_, name, originalName, _) => @@ -452,7 +452,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { def genStaticInitialization(className: ClassName)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): List[js.Tree] = { - val field = globalVar("sct", (className, StaticInitializerName), + val field = globalVar(VarField.sct, (className, StaticInitializerName), StaticInitializerOriginalName) js.Apply(field, Nil) :: Nil } @@ -462,7 +462,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): List[js.Tree] = { if (hasClassInitializer) { - val field = globalVar("sct", (className, ClassInitializerName), + val field = globalVar(VarField.sct, (className, ClassInitializerName), ClassInitializerOriginalName) js.Apply(field, Nil) :: Nil } else { @@ -518,12 +518,12 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } val field = namespace match { - case MemberNamespace.Public => "f" - case MemberNamespace.Private => "p" - case MemberNamespace.PublicStatic => "s" - case MemberNamespace.PrivateStatic => "ps" - case MemberNamespace.Constructor => "ct" - case MemberNamespace.StaticConstructor => "sct" + case MemberNamespace.Public => VarField.f + case MemberNamespace.Private => VarField.p + case MemberNamespace.PublicStatic => VarField.s + case MemberNamespace.PrivateStatic => VarField.ps + case MemberNamespace.Constructor => VarField.ct + case MemberNamespace.StaticConstructor => VarField.sct } val methodName = method.name.name @@ -633,8 +633,8 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { import TreeDSL._ val classVarRef = - if (kind.isJSClass) fileLevelVar("b", genName(className)) - else globalVar("c", className) + if (kind.isJSClass) fileLevelVar(VarField.b, genName(className)) + else globalVar(VarField.c, className) if (namespace.isStatic) classVarRef else classVarRef.prototype @@ -740,7 +740,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } val createIsStatWithGlobals = if (needIsFunction) { - globalFunctionDef("is", className, List(objParam), None, js.Return(isExpression)) + globalFunctionDef(VarField.is, className, List(objParam), None, js.Return(isExpression)) } else { WithGlobals.nil } @@ -748,15 +748,15 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val createAsStatWithGlobals = if (semantics.asInstanceOfs == Unchecked) { WithGlobals.nil } else { - globalFunctionDef("as", className, List(objParam), None, js.Return { + globalFunctionDef(VarField.as, className, List(objParam), None, js.Return { val isCond = - if (needIsFunction) js.Apply(globalVar("is", className), List(obj)) + if (needIsFunction) js.Apply(globalVar(VarField.is, className), List(obj)) else isExpression js.If(isCond || (obj === js.Null()), { obj }, { - genCallHelper("throwClassCastException", + genCallHelper(VarField.throwClassCastException, obj, js.StringLiteral(displayName)) }) }) @@ -791,7 +791,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val depth = depthParam.ref val createIsArrayOfStatWithGlobals = { - globalFunctionDef("isArrayOf", className, List(objParam, depthParam), None, { + globalFunctionDef(VarField.isArrayOf, className, List(objParam, depthParam), None, { js.Return(!(!({ genIsScalaJSObject(obj) && ((obj DOT "$classData" DOT "arrayDepth") === depth) && @@ -804,13 +804,13 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val createAsArrayOfStatWithGlobals = if (semantics.asInstanceOfs == Unchecked) { WithGlobals.nil } else { - globalFunctionDef("asArrayOf", className, List(objParam, depthParam), None, { + globalFunctionDef(VarField.asArrayOf, className, List(objParam, depthParam), None, { js.Return { - js.If(js.Apply(globalVar("isArrayOf", className), List(obj, depth)) || + js.If(js.Apply(globalVar(VarField.isArrayOf, className), List(obj, depth)) || (obj === js.Null()), { obj }, { - genCallHelper("throwArrayCastException", + genCallHelper(VarField.throwArrayCastException, obj, js.StringLiteral("L"+displayName+";"), depth) }) } @@ -858,7 +858,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { if (isObjectClass) js.Null() else js.Undefined() } { parent => - globalVar("d", parent.name) + globalVar(VarField.d, parent.name) } } else { js.Undefined() @@ -872,7 +872,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { /* Ancestors of hijacked classes, including java.lang.Object, have a * normal $is_pack_Class test but with a non-standard behavior. */ - WithGlobals(globalVar("is", className)) + WithGlobals(globalVar(VarField.is, className)) } else if (HijackedClasses.contains(className)) { /* Hijacked classes have a special isInstanceOf test. */ val xParam = js.ParamDef(js.Ident("x")) @@ -888,7 +888,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { * cannot be performed and must throw. */ if (kind != ClassKind.JSClass && kind != ClassKind.NativeJSClass) { - WithGlobals(globalVar("noIsInstance", CoreVar)) + WithGlobals(globalVar(VarField.noIsInstance, CoreVar)) } else if (kind == ClassKind.JSClass && !globalKnowledge.hasInstances(className)) { /* We need to constant-fold the instance test, to avoid emitting * `x instanceof $a_TheClass()`, because `$a_TheClass` won't be @@ -928,10 +928,10 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val prunedParams = allParams.reverse.dropWhile(_.isInstanceOf[js.Undefined]).reverse - val typeData = js.Apply(js.New(globalVar("TypeData", CoreVar), Nil) DOT "initClass", + val typeData = js.Apply(js.New(globalVar(VarField.TypeData, CoreVar), Nil) DOT "initClass", prunedParams) - globalVarDef("d", className, typeData) + globalVarDef(VarField.d, className, typeData) } } @@ -940,7 +940,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { globalKnowledge: GlobalKnowledge, pos: Position): js.Tree = { import TreeDSL._ - globalVar("c", className).prototype DOT "$classData" := globalVar("d", className) + globalVar(VarField.c, className).prototype DOT "$classData" := globalVar(VarField.d, className) } def genModuleAccessor(className: ClassName, kind: ClassKind)( @@ -953,7 +953,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { require(kind.hasModuleAccessor, s"genModuleAccessor called with non-module class: $className") - val moduleInstance = fileLevelVarIdent("n", genName(className)) + val moduleInstance = fileLevelVarIdent(VarField.n, genName(className)) val createModuleInstanceField = genEmptyMutableLet(moduleInstance) @@ -967,7 +967,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { genNonNativeJSClassConstructor(className), Nil) } else { - js.New(globalVar("c", className), Nil) + js.New(globalVar(VarField.c, className), Nil) } } } @@ -990,13 +990,13 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { ) }, js.If(moduleInstanceVar === js.Null(), { val decodedName = className.nameString.stripSuffix("$") - genCallHelper("throwModuleInitError", js.StringLiteral(decodedName)) + genCallHelper(VarField.throwModuleInitError, js.StringLiteral(decodedName)) }, js.Skip())) } val body = js.Block(initBlock, js.Return(moduleInstanceVar)) - globalFunctionDef("m", className, Nil, None, body) + globalFunctionDef(VarField.m, className, Nil, None, body) } createAccessor.map(createModuleInstanceField :: _) @@ -1062,7 +1062,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { genAssignToNoModuleExportVar(exportName, exportedValue) case ModuleKind.ESModule => - val field = fileLevelVar("e", exportName) + val field = fileLevelVar(VarField.e, exportName) val let = js.Let(field.ident, mutable = true, Some(exportedValue)) val exportStat = js.Export((field.ident -> js.ExportName(exportName)) :: Nil) WithGlobals(js.Block(let, exportStat)) @@ -1103,10 +1103,10 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { /* Initial value of the export. Updates are taken care of explicitly * when we assign to the static field. */ - genAssignToNoModuleExportVar(exportName, globalVar("t", varScope)) + genAssignToNoModuleExportVar(exportName, globalVar(VarField.t, varScope)) case ModuleKind.ESModule => - WithGlobals(globalVarExport("t", varScope, js.ExportName(exportName))) + WithGlobals(globalVarExport(VarField.t, varScope, js.ExportName(exportName))) case ModuleKind.CommonJSModule => globalRef("exports").flatMap { exportsVarRef => @@ -1115,7 +1115,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { js.StringLiteral(exportName), List( "get" -> js.Function(arrow = false, Nil, None, { - js.Return(globalVar("t", varScope)) + js.Return(globalVar(VarField.t, varScope)) }), "configurable" -> js.BooleanLiteral(true) ) @@ -1134,14 +1134,14 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { ModuleInitializerImpl.fromInitializer(initializer) match { case VoidMainMethod(className, mainMethodName) => - WithGlobals(js.Apply(globalVar("s", (className, mainMethodName)), Nil)) + WithGlobals(js.Apply(globalVar(VarField.s, (className, mainMethodName)), Nil)) case MainMethodWithArgs(className, mainMethodName, args) => val stringArrayTypeRef = ArrayTypeRef(ClassRef(BoxedStringClass), 1) val argsArrayWithGlobals = genArrayValue(stringArrayTypeRef, args.map(js.StringLiteral(_))) for (argsArray <- argsArrayWithGlobals) yield { - js.Apply(globalVar("s", (className, mainMethodName)), argsArray :: Nil) + js.Apply(globalVar(VarField.s, (className, mainMethodName)), argsArray :: 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 d610e8cdb8..8a04956ad8 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 @@ -164,7 +164,7 @@ private[emitter] object CoreJSLib { str("fileLevelThis") -> This() ))) - extractWithGlobals(globalVarDef("linkingInfo", CoreVar, linkingInfo)) + extractWithGlobals(globalVarDef(VarField.linkingInfo, CoreVar, linkingInfo)) } private def defineJSBuiltinsSnapshotsAndPolyfills(): List[Tree] = { @@ -503,21 +503,21 @@ private[emitter] object CoreJSLib { // NamespaceGlobalVar.builtinName || polyfill genIdentBracketSelect(globalRef(builtin.namespaceGlobalVar), builtin.builtinName) || polyfill } - extractWithGlobals(globalVarDef(builtin.builtinName, CoreVar, rhs)) + extractWithGlobals(globalVarDef(builtin.polyfillField, CoreVar, rhs)) } } private def declareCachedL0(): List[Tree] = { condDefs(!allowBigIntsForLongs)( - extractWithGlobals(globalVarDecl("L0", CoreVar)) + extractWithGlobals(globalVarDecl(VarField.L0, CoreVar)) ) } private def assignCachedL0(): List[Tree] = { condDefs(!allowBigIntsForLongs)(List( - globalVar("L0", CoreVar) := genScalaClassNew( + globalVar(VarField.L0, CoreVar) := genScalaClassNew( LongImpl.RuntimeLongClass, LongImpl.initFromParts, 0, 0), - genClassDataOf(LongRef) DOT "zero" := globalVar("L0", CoreVar) + genClassDataOf(LongRef) DOT "zero" := globalVar(VarField.L0, CoreVar) )) } @@ -532,7 +532,7 @@ private[emitter] object CoreJSLib { * Closure) but we must still get hold of a string of that name for * runtime reflection. */ - defineFunction1("propertyName") { obj => + defineFunction1(VarField.propertyName) { obj => val prop = varRef("prop") ForIn(genEmptyImmutableLet(prop.ident), obj, Return(prop)) } @@ -554,10 +554,10 @@ private[emitter] object CoreJSLib { } if (useClassesForRegularClasses) { - extractWithGlobals(globalClassDef("Char", CoreVar, None, ctor :: toStr :: Nil)) + extractWithGlobals(globalClassDef(VarField.Char, CoreVar, None, ctor :: toStr :: Nil)) } else { - defineFunction("Char", ctor.args, ctor.body) ::: - assignES5ClassMembers(globalVar("Char", CoreVar), List(toStr)) + defineFunction(VarField.Char, ctor.args, ctor.body) ::: + assignES5ClassMembers(globalVar(VarField.Char, CoreVar), List(toStr)) } } @@ -567,7 +567,7 @@ private[emitter] object CoreJSLib { * This helper is never called for `value === null`. As implemented, * it would return `"object"` if it were. */ - defineFunction1("valueDescription") { value => + defineFunction1(VarField.valueDescription) { value => Return { If(typeof(value) === str("number"), { If((value === 0) && (int(1) / value < 0), { @@ -601,25 +601,25 @@ private[emitter] object CoreJSLib { ) ::: condDefs(asInstanceOfs != CheckedBehavior.Unchecked)( - defineFunction2("throwClassCastException") { (instance, classFullName) => + defineFunction2(VarField.throwClassCastException) { (instance, classFullName) => Throw(maybeWrapInUBE(asInstanceOfs, { genScalaClassNew(ClassCastExceptionClass, StringArgConstructorName, - genCallHelper("valueDescription", instance) + str(" cannot be cast to ") + classFullName) + genCallHelper(VarField.valueDescription, instance) + str(" cannot be cast to ") + classFullName) })) } ::: - defineFunction3("throwArrayCastException") { (instance, classArrayEncodedName, depth) => + defineFunction3(VarField.throwArrayCastException) { (instance, classArrayEncodedName, depth) => Block( While(depth.prefix_--, { classArrayEncodedName := (str("[") + classArrayEncodedName) }), - genCallHelper("throwClassCastException", instance, classArrayEncodedName) + genCallHelper(VarField.throwClassCastException, instance, classArrayEncodedName) ) } ) ::: condDefs(arrayIndexOutOfBounds != CheckedBehavior.Unchecked)( - defineFunction1("throwArrayIndexOutOfBoundsException") { i => + defineFunction1(VarField.throwArrayIndexOutOfBoundsException) { i => Throw(maybeWrapInUBE(arrayIndexOutOfBounds, { genScalaClassNew(ArrayIndexOutOfBoundsExceptionClass, StringArgConstructorName, @@ -629,17 +629,17 @@ private[emitter] object CoreJSLib { ) ::: condDefs(arrayStores != CheckedBehavior.Unchecked)( - defineFunction1("throwArrayStoreException") { v => + defineFunction1(VarField.throwArrayStoreException) { v => Throw(maybeWrapInUBE(arrayStores, { genScalaClassNew(ArrayStoreExceptionClass, StringArgConstructorName, - If(v === Null(), Null(), genCallHelper("valueDescription", v))) + If(v === Null(), Null(), genCallHelper(VarField.valueDescription, v))) })) } ) ::: condDefs(negativeArraySizes != CheckedBehavior.Unchecked)( - defineFunction0("throwNegativeArraySizeException") { + defineFunction0(VarField.throwNegativeArraySizeException) { Throw(maybeWrapInUBE(negativeArraySizes, { genScalaClassNew(NegativeArraySizeExceptionClass, NoArgConstructorName) @@ -648,7 +648,7 @@ private[emitter] object CoreJSLib { ) ::: condDefs(moduleInit == CheckedBehavior.Fatal)( - defineFunction1("throwModuleInitError") { name => + defineFunction1(VarField.throwModuleInitError) { name => Throw(genScalaClassNew(UndefinedBehaviorErrorClass, StringArgConstructorName, str("Initializer of ") + name + str(" called before completion of its super constructor"))) @@ -656,31 +656,31 @@ private[emitter] object CoreJSLib { ) ::: condDefs(nullPointers != CheckedBehavior.Unchecked)( - defineFunction0("throwNullPointerException") { + defineFunction0(VarField.throwNullPointerException) { Throw(maybeWrapInUBE(nullPointers, { genScalaClassNew(NullPointerExceptionClass, NoArgConstructorName) })) } ::: // "checkNotNull", but with a very short name - defineFunction1("n") { x => + defineFunction1(VarField.n) { x => Block( - If(x === Null(), genCallHelper("throwNullPointerException")), + If(x === Null(), genCallHelper(VarField.throwNullPointerException)), Return(x) ) } ) ::: - defineFunction1("noIsInstance") { instance => + defineFunction1(VarField.noIsInstance) { instance => Throw(New(TypeErrorRef, str("Cannot call isInstance() on a Class representing a JS trait/object") :: Nil)) } ::: - defineFunction2("newArrayObject") { (arrayClassData, lengths) => - Return(genCallHelper("newArrayObjectInternal", arrayClassData, lengths, int(0))) + defineFunction2(VarField.newArrayObject) { (arrayClassData, lengths) => + Return(genCallHelper(VarField.newArrayObjectInternal, arrayClassData, lengths, int(0))) } ::: - defineFunction3("newArrayObjectInternal") { (arrayClassData, lengths, lengthIndex) => + defineFunction3(VarField.newArrayObjectInternal) { (arrayClassData, lengths, lengthIndex) => val result = varRef("result") val subArrayClassData = varRef("subArrayClassData") val subLengthIndex = varRef("subLengthIndex") @@ -696,14 +696,14 @@ private[emitter] object CoreJSLib { const(underlying, result.u), For(let(i, 0), i < underlying.length, i.++, { BracketSelect(underlying, i) := - genCallHelper("newArrayObjectInternal", subArrayClassData, lengths, subLengthIndex) + genCallHelper(VarField.newArrayObjectInternal, subArrayClassData, lengths, subLengthIndex) }) )), Return(result) ) } ::: - defineFunction1("objectClone") { instance => + defineFunction1(VarField.objectClone) { instance => // return Object.create(Object.getPrototypeOf(instance), $getOwnPropertyDescriptors(instance)); val callGetOwnPropertyDescriptors = genCallPolyfillableBuiltin( GetOwnPropertyDescriptorsBuiltin, instance) @@ -712,18 +712,18 @@ private[emitter] object CoreJSLib { callGetOwnPropertyDescriptors))) } ::: - defineFunction1("objectOrArrayClone") { instance => + defineFunction1(VarField.objectOrArrayClone) { instance => // return instance.$classData.isArrayClass ? instance.clone__O() : $objectClone(instance); Return(If(genIdentBracketSelect(instance DOT classData, "isArrayClass"), Apply(instance DOT genName(cloneMethodName), Nil), - genCallHelper("objectClone", instance))) + genCallHelper(VarField.objectClone, instance))) } ) private def defineObjectGetClassFunctions(): List[Tree] = { // objectGetClass and objectClassName - def defineObjectGetClassBasedFun(name: String, + def defineObjectGetClassBasedFun(name: VarField, constantClassResult: ClassName => Tree, scalaObjectResult: VarRef => Tree, jsObjectResult: Tree): List[Tree] = { defineFunction1(name) { instance => @@ -733,7 +733,7 @@ private[emitter] object CoreJSLib { }, str("number") -> { Block( - If(genCallHelper("isInt", instance), { + If(genCallHelper(VarField.isInt, instance), { If((instance << 24 >> 24) === instance, { Return(constantClassResult(BoxedByteClass)) }, { @@ -745,7 +745,7 @@ private[emitter] object CoreJSLib { }) }, { if (strictFloats) { - If(genCallHelper("isFloat", instance), { + If(genCallHelper(VarField.isFloat, instance), { Return(constantClassResult(BoxedFloatClass)) }, { Return(constantClassResult(BoxedDoubleClass)) @@ -767,7 +767,7 @@ private[emitter] object CoreJSLib { if (nullPointers == CheckedBehavior.Unchecked) Return(Apply(instance DOT genName(getClassMethodName), Nil)) else - genCallHelper("throwNullPointerException") + genCallHelper(VarField.throwNullPointerException) }, { If(genIsInstanceOfHijackedClass(instance, BoxedLongClass), { Return(constantClassResult(BoxedLongClass)) @@ -797,13 +797,13 @@ private[emitter] object CoreJSLib { * (i.e., through a `ClassOf` node). */ condDefs(globalKnowledge.isClassClassInstantiated)( - defineObjectGetClassBasedFun("objectGetClass", + defineObjectGetClassBasedFun(VarField.objectGetClass, className => genClassOf(className), instance => Apply(instance DOT classData DOT "getClassOf", Nil), Null() ) ) ::: - defineObjectGetClassBasedFun("objectClassName", + defineObjectGetClassBasedFun(VarField.objectClassName, { className => StringLiteral(RuntimeClassNameMapperImpl.map( semantics.runtimeClassNameMapper, className.nameString)) @@ -813,7 +813,7 @@ private[emitter] object CoreJSLib { if (nullPointers == CheckedBehavior.Unchecked) Apply(Null() DOT genName(getNameMethodName), Nil) else - genCallHelper("throwNullPointerException") + genCallHelper(VarField.throwNullPointerException) } ) } @@ -824,7 +824,7 @@ private[emitter] object CoreJSLib { def defineDispatcher(methodName: MethodName, args: List[VarRef], body: Tree): List[Tree] = { val params = paramList((instance :: args): _*) - extractWithGlobals(globalFunctionDef("dp", methodName, params, None, body)) + extractWithGlobals(globalFunctionDef(VarField.dp, methodName, params, None, body)) } /* A standard dispatcher performs a type test on the instance and then @@ -854,9 +854,9 @@ private[emitter] object CoreJSLib { def genHijackedMethodApply(className: ClassName): Tree = { val instanceAsPrimitive = - if (className == BoxedCharacterClass) genCallHelper("uC", instance) + if (className == BoxedCharacterClass) genCallHelper(VarField.uC, instance) else instance - Apply(globalVar("f", (className, methodName)), instanceAsPrimitive :: args) + Apply(globalVar(VarField.f, (className, methodName)), instanceAsPrimitive :: args) } def genBodyNoSwitch(hijackedClasses: List[ClassName]): Tree = { @@ -872,7 +872,7 @@ private[emitter] object CoreJSLib { if (implementedInObject) { val staticObjectCall: Tree = { - val fun = globalVar("c", ObjectClass).prototype DOT genName(methodName) + val fun = globalVar(VarField.c, ObjectClass).prototype DOT genName(methodName) Return(Apply(fun DOT "call", instance :: args)) } @@ -929,21 +929,21 @@ private[emitter] object CoreJSLib { def wrapBigInt64(tree: Tree): Tree = Apply(genIdentBracketSelect(BigIntRef, "asIntN"), 64 :: tree :: Nil) - defineFunction2("intDiv") { (x, y) => + defineFunction2(VarField.intDiv) { (x, y) => If(y === 0, throwDivByZero, { Return((x / y) | 0) }) } ::: - defineFunction2("intMod") { (x, y) => + defineFunction2(VarField.intMod) { (x, y) => If(y === 0, throwDivByZero, { Return((x % y) | 0) }) } ::: - defineFunction1("doubleToInt") { x => + defineFunction1(VarField.doubleToInt) { x => Return(If(x > 2147483647, 2147483647, If(x < -2147483648, -2147483648, x | 0))) } ::: condDefs(semantics.stringIndexOutOfBounds != CheckedBehavior.Unchecked)( - defineFunction2("charAt") { (s, i) => + defineFunction2(VarField.charAt) { (s, i) => val r = varRef("r") val throwStringIndexOutOfBoundsException = { @@ -958,18 +958,18 @@ private[emitter] object CoreJSLib { } ) ::: condDefs(allowBigIntsForLongs)( - defineFunction2("longDiv") { (x, y) => + defineFunction2(VarField.longDiv) { (x, y) => If(y === bigInt(0), throwDivByZero, { Return(wrapBigInt64(x / y)) }) } ::: - defineFunction2("longMod") { (x, y) => + defineFunction2(VarField.longMod) { (x, y) => If(y === bigInt(0), throwDivByZero, { Return(wrapBigInt64(x % y)) }) } ::: - defineFunction1("doubleToLong")(x => Return { + defineFunction1(VarField.doubleToLong)(x => Return { If(x < double(-9223372036854775808.0), { // -2^63 bigInt(-9223372036854775808L) }, { @@ -986,7 +986,7 @@ private[emitter] object CoreJSLib { }) }) ::: - defineFunction1("longToFloat") { x => + defineFunction1(VarField.longToFloat) { x => val abs = varRef("abs") val y = varRef("y") val absR = varRef("absR") @@ -1008,7 +1008,7 @@ private[emitter] object CoreJSLib { private def defineES2015LikeHelpers(): List[Tree] = ( condDefs(esVersion < ESVersion.ES2015)( - defineFunction2("newJSObjectWithVarargs") { (ctor, args) => + defineFunction2(VarField.newJSObjectWithVarargs) { (ctor, args) => val instance = varRef("instance") val result = varRef("result") @@ -1024,7 +1024,7 @@ private[emitter] object CoreJSLib { } ) ::: - defineFunction2("resolveSuperRef") { (superClass, propName) => + defineFunction2(VarField.resolveSuperRef) { (superClass, propName) => val getPrototypeOf = varRef("getPrototypeOf") val getOwnPropertyDescriptor = varRef("getOwnPropertyDescriptor") val superProto = varRef("superProto") @@ -1042,12 +1042,12 @@ private[emitter] object CoreJSLib { ) } ::: - defineFunction3("superGet") { (superClass, self, propName) => + defineFunction3(VarField.superGet) { (superClass, self, propName) => val desc = varRef("desc") val getter = varRef("getter") Block( - const(desc, genCallHelper("resolveSuperRef", superClass, propName)), + const(desc, genCallHelper(VarField.resolveSuperRef, superClass, propName)), If(desc !== Undefined(), Block( const(getter, genIdentBracketSelect(desc, "get")), Return(If(getter !== Undefined(), @@ -1057,12 +1057,12 @@ private[emitter] object CoreJSLib { ) } ::: - defineFunction4("superSet") { (superClass, self, propName, value) => + defineFunction4(VarField.superSet) { (superClass, self, propName, value) => val desc = varRef("desc") val setter = varRef("setter") Block( - const(desc, genCallHelper("resolveSuperRef", superClass, propName)), + const(desc, genCallHelper(VarField.resolveSuperRef, superClass, propName)), If(desc !== Undefined(), Block( const(setter, genIdentBracketSelect(desc, "set")), If(setter !== Undefined(), Block( @@ -1078,7 +1078,7 @@ private[emitter] object CoreJSLib { private def defineModuleHelpers(): List[Tree] = { condDefs(moduleKind == ModuleKind.CommonJSModule)( - defineFunction1("moduleDefault") { m => + defineFunction1(VarField.moduleDefault) { m => Return(If( m && (typeof(m) === str("object")) && (str("default") in m), BracketSelect(m, str("default")), @@ -1089,20 +1089,20 @@ private[emitter] object CoreJSLib { private def defineIntrinsics(): List[Tree] = ( condDefs(arrayIndexOutOfBounds != CheckedBehavior.Unchecked)( - defineFunction5("arraycopyCheckBounds") { (srcLen, srcPos, destLen, destPos, length) => + defineFunction5(VarField.arraycopyCheckBounds) { (srcLen, srcPos, destLen, destPos, length) => If((srcPos < 0) || (destPos < 0) || (length < 0) || (srcPos > ((srcLen - length) | 0)) || (destPos > ((destLen - length) | 0)), { - genCallHelper("throwArrayIndexOutOfBoundsException", Null()) + genCallHelper(VarField.throwArrayIndexOutOfBoundsException, Null()) }) } ) ::: - defineFunction5("arraycopyGeneric") { (srcArray, srcPos, destArray, destPos, length) => + defineFunction5(VarField.arraycopyGeneric) { (srcArray, srcPos, destArray, destPos, length) => val i = varRef("i") Block( if (arrayIndexOutOfBounds != CheckedBehavior.Unchecked) { - genCallHelper("arraycopyCheckBounds", srcArray.length, + genCallHelper(VarField.arraycopyCheckBounds, srcArray.length, srcPos, destArray.length, destPos, length) } else { Skip() @@ -1120,23 +1120,23 @@ private[emitter] object CoreJSLib { } ::: condDefs(esVersion < ESVersion.ES2015)( - defineFunction5("systemArraycopy") { (src, srcPos, dest, destPos, length) => - genCallHelper("arraycopyGeneric", src.u, srcPos, dest.u, destPos, length) + defineFunction5(VarField.systemArraycopy) { (src, srcPos, dest, destPos, length) => + genCallHelper(VarField.arraycopyGeneric, src.u, srcPos, dest.u, destPos, length) } ) ::: condDefs(esVersion >= ESVersion.ES2015 && nullPointers != CheckedBehavior.Unchecked)( - defineFunction5("systemArraycopy") { (src, srcPos, dest, destPos, length) => + defineFunction5(VarField.systemArraycopy) { (src, srcPos, dest, destPos, length) => Apply(src DOT "copyTo", List(srcPos, dest, destPos, length)) } ) ::: condDefs(arrayStores != CheckedBehavior.Unchecked)( - defineFunction5("systemArraycopyRefs") { (src, srcPos, dest, destPos, length) => + defineFunction5(VarField.systemArraycopyRefs) { (src, srcPos, dest, destPos, length) => If(Apply(genIdentBracketSelect(dest DOT classData, "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`. */ - genCallHelper("arraycopyGeneric", src.u, srcPos, dest.u, destPos, length) + genCallHelper(VarField.arraycopyGeneric, src.u, srcPos, dest.u, destPos, length) }, { /* Slow copy with "set" calls for every element. By construction, * we have `src ne dest` in this case. @@ -1146,7 +1146,7 @@ private[emitter] object CoreJSLib { Block( const(srcArray, src.u), condTree(arrayIndexOutOfBounds != CheckedBehavior.Unchecked) { - genCallHelper("arraycopyCheckBounds", srcArray.length, + genCallHelper(VarField.arraycopyCheckBounds, srcArray.length, srcPos, dest.u.length, destPos, length) }, For(let(i, 0), i < length, i := ((i + 1) | 0), { @@ -1156,8 +1156,8 @@ private[emitter] object CoreJSLib { }) } ::: - defineFunction5("systemArraycopyFull") { (src, srcPos, dest, destPos, length) => - val ObjectArray = globalVar("ac", ObjectClass) + defineFunction5(VarField.systemArraycopyFull) { (src, srcPos, dest, destPos, length) => + val ObjectArray = globalVar(VarField.ac, ObjectClass) val srcData = varRef("srcData") Block( @@ -1168,16 +1168,16 @@ private[emitter] object CoreJSLib { // Fast path: the values are array of the same type genUncheckedArraycopy(List(src, srcPos, dest, destPos, length)) }, { - genCallHelper("throwArrayStoreException", Null()) + genCallHelper(VarField.throwArrayStoreException, Null()) }) }, { /* src and dest are of different types; the only situation that * can still be valid is if they are two reference array types. */ If((src instanceof ObjectArray) && (dest instanceof ObjectArray), { - genCallHelper("systemArraycopyRefs", src, srcPos, dest, destPos, length) + genCallHelper(VarField.systemArraycopyRefs, src, srcPos, dest, destPos, length) }, { - genCallHelper("throwArrayStoreException", Null()) + genCallHelper(VarField.throwArrayStoreException, Null()) }) }) ) @@ -1188,8 +1188,8 @@ private[emitter] object CoreJSLib { locally { val WeakMapRef = globalRef("WeakMap") - val lastIDHash = fileLevelVar("lastIDHash") - val idHashCodeMap = fileLevelVar("idHashCodeMap") + val lastIDHash = fileLevelVar(VarField.lastIDHash) + val idHashCodeMap = fileLevelVar(VarField.idHashCodeMap) val obj = varRef("obj") val biHash = varRef("biHash") @@ -1198,7 +1198,7 @@ private[emitter] object CoreJSLib { def functionSkeleton(defaultImpl: Tree): Function = { def genHijackedMethodApply(className: ClassName, arg: Tree): Tree = - Apply(globalVar("f", (className, hashCodeMethodName)), arg :: Nil) + Apply(globalVar(VarField.f, (className, hashCodeMethodName)), arg :: Nil) def genReturnHijackedMethodApply(className: ClassName): Tree = Return(genHijackedMethodApply(className, obj)) @@ -1323,9 +1323,9 @@ private[emitter] object CoreJSLib { ) ::: ( if (esVersion >= ESVersion.ES2015) { val f = weakMapBasedFunction - defineFunction("systemIdentityHashCode", f.args, f.body) + defineFunction(VarField.systemIdentityHashCode, f.args, f.body) } else { - extractWithGlobals(globalVarDef("systemIdentityHashCode", CoreVar, + extractWithGlobals(globalVarDef(VarField.systemIdentityHashCode, CoreVar, If(idHashCodeMap !== Null(), weakMapBasedFunction, fieldBasedFunction))) } ) @@ -1333,24 +1333,24 @@ private[emitter] object CoreJSLib { ) private def defineIsPrimitiveFunctions(): List[Tree] = { - def defineIsIntLike(name: String, specificTest: VarRef => Tree): List[Tree] = { + def defineIsIntLike(name: VarField, specificTest: VarRef => Tree): List[Tree] = { defineFunction1(name) { v => Return((typeof(v) === str("number")) && specificTest(v) && ((int(1) / v) !== (int(1) / double(-0.0)))) } } - defineIsIntLike("isByte", v => (v << 24 >> 24) === v) ::: - defineIsIntLike("isShort", v => (v << 16 >> 16) === v) ::: - defineIsIntLike("isInt", v => (v | 0) === v) ::: + defineIsIntLike(VarField.isByte, v => (v << 24 >> 24) === v) ::: + defineIsIntLike(VarField.isShort, v => (v << 16 >> 16) === v) ::: + defineIsIntLike(VarField.isInt, v => (v | 0) === v) ::: condDefs(allowBigIntsForLongs)( - defineFunction1("isLong") { v => + defineFunction1(VarField.isLong) { v => Return((typeof(v) === str("bigint")) && (Apply(genIdentBracketSelect(BigIntRef, "asIntN"), int(64) :: v :: Nil) === v)) } ) ::: condDefs(strictFloats)( - defineFunction1("isFloat") { v => + defineFunction1(VarField.isFloat) { v => Return((typeof(v) === str("number")) && ((v !== v) || (genCallPolyfillableBuiltin(FroundBuiltin, v) === v))) } @@ -1359,46 +1359,46 @@ private[emitter] object CoreJSLib { private def defineBoxFunctions(): List[Tree] = ( // Boxes for Chars - defineFunction1("bC") { c => - Return(New(globalVar("Char", CoreVar), c :: Nil)) + defineFunction1(VarField.bC) { c => + Return(New(globalVar(VarField.Char, CoreVar), c :: Nil)) } ::: - extractWithGlobals(globalVarDef("bC0", CoreVar, genCallHelper("bC", 0))) + extractWithGlobals(globalVarDef(VarField.bC0, CoreVar, genCallHelper(VarField.bC, 0))) ) ::: ( if (asInstanceOfs != CheckedBehavior.Unchecked) { // Unboxes for everything - def defineUnbox(name: String, boxedClassName: ClassName, resultExpr: VarRef => Tree): List[Tree] = { + def defineUnbox(name: VarField, boxedClassName: ClassName, resultExpr: VarRef => Tree): List[Tree] = { val fullName = boxedClassName.nameString defineFunction1(name)(v => Return { If(genIsInstanceOfHijackedClass(v, boxedClassName) || (v === Null()), resultExpr(v), - genCallHelper("throwClassCastException", v, str(fullName))) + genCallHelper(VarField.throwClassCastException, v, str(fullName))) }) } ( - defineUnbox("uV", BoxedUnitClass, _ => Undefined()) ::: - defineUnbox("uZ", BoxedBooleanClass, v => !(!v)) ::: - defineUnbox("uC", BoxedCharacterClass, v => If(v === Null(), 0, v DOT "c")) ::: - defineUnbox("uB", BoxedByteClass, _ | 0) ::: - defineUnbox("uS", BoxedShortClass, _ | 0) ::: - defineUnbox("uI", BoxedIntegerClass, _ | 0) ::: - defineUnbox("uJ", BoxedLongClass, v => If(v === Null(), genLongZero(), v)) ::: + defineUnbox(VarField.uV, BoxedUnitClass, _ => Undefined()) ::: + defineUnbox(VarField.uZ, BoxedBooleanClass, v => !(!v)) ::: + defineUnbox(VarField.uC, BoxedCharacterClass, v => If(v === Null(), 0, v DOT "c")) ::: + defineUnbox(VarField.uB, BoxedByteClass, _ | 0) ::: + defineUnbox(VarField.uS, BoxedShortClass, _ | 0) ::: + defineUnbox(VarField.uI, BoxedIntegerClass, _ | 0) ::: + defineUnbox(VarField.uJ, BoxedLongClass, v => If(v === Null(), genLongZero(), v)) ::: /* Since the type test ensures that v is either null or a float, we can * use + instead of fround. */ - defineUnbox("uF", BoxedFloatClass, v => +v) ::: + defineUnbox(VarField.uF, BoxedFloatClass, v => +v) ::: - defineUnbox("uD", BoxedDoubleClass, v => +v) ::: - defineUnbox("uT", BoxedStringClass, v => If(v === Null(), StringLiteral(""), v)) + defineUnbox(VarField.uD, BoxedDoubleClass, v => +v) ::: + defineUnbox(VarField.uT, BoxedStringClass, v => If(v === Null(), StringLiteral(""), v)) ) } else { // Unboxes for Chars and Longs ( - defineFunction1("uC") { v => + defineFunction1(VarField.uC) { v => Return(If(v === Null(), 0, v DOT "c")) } ::: - defineFunction1("uJ") { v => + defineFunction1(VarField.uJ) { v => Return(If(v === Null(), genLongZero(), v)) } ) @@ -1412,7 +1412,7 @@ private[emitter] object CoreJSLib { */ private def defineSpecializedArrayClasses(): List[Tree] = { specializedArrayTypeRefs.flatMap { componentTypeRef => - val ArrayClass = globalVar("ac", componentTypeRef) + val ArrayClass = globalVar(VarField.ac, componentTypeRef) val isArrayOfObject = componentTypeRef == ClassRef(ObjectClass) val isTypedArray = usesUnderlyingTypedArray(componentTypeRef) @@ -1433,7 +1433,7 @@ private[emitter] object CoreJSLib { val boundsCheck = { If((i < 0) || (i >= This().u.length), - genCallHelper("throwArrayIndexOutOfBoundsException", i)) + genCallHelper(VarField.throwArrayIndexOutOfBoundsException, i)) } List( @@ -1476,7 +1476,7 @@ private[emitter] object CoreJSLib { if (isTypedArray) { Block( if (semantics.arrayIndexOutOfBounds != CheckedBehavior.Unchecked) { - genCallHelper("arraycopyCheckBounds", This().u.length, + genCallHelper(VarField.arraycopyCheckBounds, This().u.length, srcPos, dest.u.length, destPos, length) } else { Skip() @@ -1487,7 +1487,7 @@ private[emitter] object CoreJSLib { Nil) ) } else { - genCallHelper("arraycopyGeneric", This().u, srcPos, + genCallHelper(VarField.arraycopyGeneric, This().u, srcPos, dest.u, destPos, length) } }) @@ -1504,13 +1504,13 @@ private[emitter] object CoreJSLib { val members = getAndSet ::: copyTo ::: clone :: Nil if (useClassesForRegularClasses) { - extractWithGlobals(globalClassDef("ac", componentTypeRef, - Some(globalVar("c", ObjectClass)), ctor :: members)) + extractWithGlobals(globalClassDef(VarField.ac, componentTypeRef, + Some(globalVar(VarField.c, ObjectClass)), ctor :: members)) } else { val clsDef = { - extractWithGlobals(globalFunctionDef("ac", componentTypeRef, + extractWithGlobals(globalFunctionDef(VarField.ac, componentTypeRef, ctor.args, ctor.restParam, ctor.body)) ::: - (ArrayClass.prototype := New(globalVar("h", ObjectClass), Nil)) :: + (ArrayClass.prototype := New(globalVar(VarField.h, ObjectClass), Nil)) :: (ArrayClass.prototype DOT "constructor" := ArrayClass) :: assignES5ClassMembers(ArrayClass, members) } @@ -1518,8 +1518,8 @@ private[emitter] object CoreJSLib { componentTypeRef match { case _: ClassRef => clsDef ::: - extractWithGlobals(globalFunctionDef("ah", ObjectClass, Nil, None, Skip())) ::: - (globalVar("ah", ObjectClass).prototype := ArrayClass.prototype) :: Nil + extractWithGlobals(globalFunctionDef(VarField.ah, ObjectClass, Nil, None, Skip())) ::: + (globalVar(VarField.ah, ObjectClass).prototype := ArrayClass.prototype) :: Nil case _: PrimRef => clsDef } @@ -1533,7 +1533,7 @@ private[emitter] object CoreJSLib { If(typeof(arg) === str("number"), { val arraySizeCheck = condTree(negativeArraySizes != CheckedBehavior.Unchecked) { - If(arg < 0, genCallHelper("throwNegativeArraySizeException")) + If(arg < 0, genCallHelper(VarField.throwNegativeArraySizeException)) } getArrayUnderlyingTypedArrayClassRef(componentTypeRef) match { @@ -1624,7 +1624,7 @@ private[emitter] object CoreJSLib { genArrowFunction(paramList(obj), Return(bool(false)))), If(arrayClass !== Undefined(), { // it is undefined for void privateFieldSet("_arrayOf", - Apply(New(globalVar("TypeData", CoreVar), Nil) DOT "initSpecializedArray", + Apply(New(globalVar(VarField.TypeData, CoreVar), Nil) DOT "initSpecializedArray", List(This(), arrayClass, typedArrayClass))) }), Return(This()) @@ -1648,7 +1648,7 @@ private[emitter] object CoreJSLib { paramList(internalNameObj, isInterface, fullName, ancestors, isJSType, parentData, isInstance), None, { Block( - const(internalName, genCallHelper("propertyName", internalNameObj)), + const(internalName, genCallHelper(VarField.propertyName, internalNameObj)), if (globalKnowledge.isParentDataAccessed) privateFieldSet("parentData", parentData) else @@ -1765,13 +1765,13 @@ private[emitter] object CoreJSLib { val boundsCheck = condTree(arrayIndexOutOfBounds != CheckedBehavior.Unchecked) { If((i < 0) || (i >= This().u.length), - genCallHelper("throwArrayIndexOutOfBoundsException", i)) + genCallHelper(VarField.throwArrayIndexOutOfBoundsException, i)) } val storeCheck = { If((v !== Null()) && !(componentData DOT "isJSType") && !Apply(genIdentBracketSelect(componentData, "isInstance"), v :: Nil), - genCallHelper("throwArrayStoreException", v)) + genCallHelper(VarField.throwArrayStoreException, v)) } List( @@ -1794,7 +1794,7 @@ private[emitter] object CoreJSLib { val length = varRef("length") val methodDef = MethodDef(static = false, Ident("copyTo"), paramList(srcPos, dest, destPos, length), None, { - genCallHelper("arraycopyGeneric", This().u, srcPos, + genCallHelper(VarField.arraycopyGeneric, This().u, srcPos, dest.u, destPos, length) }) methodDef :: Nil @@ -1810,12 +1810,12 @@ private[emitter] object CoreJSLib { val members = set ::: copyTo ::: clone :: Nil if (useClassesForRegularClasses) { - ClassDef(Some(ArrayClass.ident), Some(globalVar("ac", ObjectClass)), + ClassDef(Some(ArrayClass.ident), Some(globalVar(VarField.ac, ObjectClass)), ctor :: members) } else { Block( FunctionDef(ArrayClass.ident, ctor.args, ctor.restParam, ctor.body) :: - (ArrayClass.prototype := New(globalVar("ah", ObjectClass), Nil)) :: + (ArrayClass.prototype := New(globalVar(VarField.ah, ObjectClass), Nil)) :: (ArrayClass.prototype DOT "constructor" := ArrayClass) :: assignES5ClassMembers(ArrayClass, members) ) @@ -1865,7 +1865,7 @@ private[emitter] object CoreJSLib { Block( If(!(This() DOT "_arrayOf"), This() DOT "_arrayOf" := - Apply(New(globalVar("TypeData", CoreVar), Nil) DOT "initArray", This() :: Nil), + Apply(New(globalVar(VarField.TypeData, CoreVar), Nil) DOT "initArray", This() :: Nil), Skip()), Return(This() DOT "_arrayOf") ) @@ -1908,7 +1908,7 @@ private[emitter] object CoreJSLib { if (asInstanceOfs != CheckedBehavior.Unchecked) { If((obj !== Null()) && !(This() DOT "isJSType") && !Apply(genIdentBracketSelect(This(), "isInstance"), obj :: Nil), - genCallHelper("throwClassCastException", obj, genIdentBracketSelect(This(), "name")), + genCallHelper(VarField.throwClassCastException, obj, genIdentBracketSelect(This(), "name")), Skip()) } else { Skip() @@ -1943,7 +1943,7 @@ private[emitter] object CoreJSLib { For(let(i, 0), i < lengths.length, i.++, { arrayClassData := Apply(arrayClassData DOT "getArrayOf", Nil) }), - Return(genCallHelper("newArrayObject", arrayClassData, lengths)) + Return(genCallHelper(VarField.newArrayObject, arrayClassData, lengths)) ) }) } @@ -1974,10 +1974,10 @@ private[emitter] object CoreJSLib { ) if (useClassesForRegularClasses) { - extractWithGlobals(globalClassDef("TypeData", CoreVar, None, ctor :: members)) + extractWithGlobals(globalClassDef(VarField.TypeData, CoreVar, None, ctor :: members)) } else { - defineFunction("TypeData", ctor.args, ctor.body) ::: - assignES5ClassMembers(globalVar("TypeData", CoreVar), members) + defineFunction(VarField.TypeData, ctor.args, ctor.body) ::: + assignES5ClassMembers(globalVar(VarField.TypeData, CoreVar), members) } } @@ -1988,7 +1988,7 @@ private[emitter] object CoreJSLib { val data = varRef("data") val arrayDepth = varRef("arrayDepth") - val forObj = extractWithGlobals(globalFunctionDef("isArrayOf", ObjectClass, paramList(obj, depth), None, { + val forObj = extractWithGlobals(globalFunctionDef(VarField.isArrayOf, ObjectClass, paramList(obj, depth), None, { Block( const(data, obj && (obj DOT "$classData")), If(!data, { @@ -2009,7 +2009,7 @@ private[emitter] object CoreJSLib { val forPrims = orderedPrimRefsWithoutVoid.flatMap { primRef => val obj = varRef("obj") val depth = varRef("depth") - extractWithGlobals(globalFunctionDef("isArrayOf", primRef, paramList(obj, depth), None, { + extractWithGlobals(globalFunctionDef(VarField.isArrayOf, primRef, paramList(obj, depth), None, { Return(!(!(obj && (obj DOT classData) && ((obj DOT classData DOT "arrayDepth") === depth) && ((obj DOT classData DOT "arrayBase") === genClassDataOf(primRef))))) @@ -2029,11 +2029,11 @@ private[emitter] object CoreJSLib { val obj = varRef("obj") val depth = varRef("depth") - extractWithGlobals(globalFunctionDef("asArrayOf", typeRef, paramList(obj, depth), None, { - If(Apply(globalVar("isArrayOf", typeRef), obj :: depth :: Nil) || (obj === Null()), { + extractWithGlobals(globalFunctionDef(VarField.asArrayOf, typeRef, paramList(obj, depth), None, { + If(Apply(globalVar(VarField.isArrayOf, typeRef), obj :: depth :: Nil) || (obj === Null()), { Return(obj) }, { - genCallHelper("throwArrayCastException", obj, str(encodedName), depth) + genCallHelper(VarField.throwArrayCastException, obj, str(encodedName), depth) }) })) } @@ -2055,7 +2055,7 @@ private[emitter] object CoreJSLib { val that = varRef("that") val obj = varRef("obj") - val typeDataVar = globalVar("d", ObjectClass) + val typeDataVar = globalVar(VarField.d, ObjectClass) def privateFieldSet(fieldName: String, value: Tree): Tree = typeDataVar DOT fieldName := value @@ -2064,7 +2064,7 @@ private[emitter] object CoreJSLib { genIdentBracketSelect(typeDataVar, fieldName) := value extractWithGlobals( - globalVarDef("d", ObjectClass, New(globalVar("TypeData", CoreVar), Nil))) ::: + globalVarDef(VarField.d, ObjectClass, New(globalVar(VarField.TypeData, CoreVar), Nil))) ::: List( privateFieldSet("ancestors", ObjectConstr(List((Ident(genName(ObjectClass)) -> 1)))), privateFieldSet("arrayEncodedName", str("L" + fullName + ";")), @@ -2077,9 +2077,9 @@ private[emitter] object CoreJSLib { publicFieldSet("isInstance", genArrowFunction(paramList(obj), Return(obj !== Null()))), privateFieldSet("_arrayOf", { - Apply(New(globalVar("TypeData", CoreVar), Nil) DOT "initSpecializedArray", List( + Apply(New(globalVar(VarField.TypeData, CoreVar), Nil) DOT "initSpecializedArray", List( typeDataVar, - globalVar("ac", ObjectClass), + globalVar(VarField.ac, ObjectClass), Undefined(), // typedArray genArrowFunction(paramList(that), { val thatDepth = varRef("thatDepth") @@ -2094,7 +2094,7 @@ private[emitter] object CoreJSLib { }) )) }), - globalVar("c", ObjectClass).prototype DOT "$classData" := typeDataVar + globalVar(VarField.c, ObjectClass).prototype DOT "$classData" := typeDataVar ) } @@ -2119,8 +2119,8 @@ private[emitter] object CoreJSLib { Undefined() } - extractWithGlobals(globalVarDef("d", primRef, { - Apply(New(globalVar("TypeData", CoreVar), Nil) DOT "initPrim", + extractWithGlobals(globalVarDef(VarField.d, primRef, { + Apply(New(globalVar(VarField.TypeData, CoreVar), Nil) DOT "initPrim", List(zero, str(primRef.charCode.toString()), str(primRef.displayName), if (primRef == VoidRef) Undefined() @@ -2132,35 +2132,35 @@ private[emitter] object CoreJSLib { obj ::: prims } - private def defineFunction(name: String, args: List[ParamDef], body: Tree): List[Tree] = + private def defineFunction(name: VarField, args: List[ParamDef], body: Tree): List[Tree] = extractWithGlobals(globalFunctionDef(name, CoreVar, args, None, body)) private val argRefs = List.tabulate(5)(i => varRef("arg" + i)) - private def defineFunction0(name: String)(body: Tree): List[Tree] = + private def defineFunction0(name: VarField)(body: Tree): List[Tree] = defineFunction(name, Nil, body) - private def defineFunction1(name: String)(body: VarRef => Tree): List[Tree] = { + private def defineFunction1(name: VarField)(body: VarRef => Tree): List[Tree] = { val a :: _ = argRefs defineFunction(name, paramList(a), body(a)) } - private def defineFunction2(name: String)(body: (VarRef, VarRef) => Tree): List[Tree] = { + private def defineFunction2(name: VarField)(body: (VarRef, VarRef) => Tree): List[Tree] = { val a :: b :: _ = argRefs defineFunction(name, paramList(a, b), body(a, b)) } - private def defineFunction3(name: String)(body: (VarRef, VarRef, VarRef) => Tree): List[Tree] = { + private def defineFunction3(name: VarField)(body: (VarRef, VarRef, VarRef) => Tree): List[Tree] = { val a :: b :: c :: _ = argRefs defineFunction(name, paramList(a, b, c), body(a, b, c)) } - private def defineFunction4(name: String)(body: (VarRef, VarRef, VarRef, VarRef) => Tree): List[Tree] = { + private def defineFunction4(name: VarField)(body: (VarRef, VarRef, VarRef, VarRef) => Tree): List[Tree] = { val a :: b :: c :: d :: _ = argRefs defineFunction(name, paramList(a, b, c, d), body(a, b, c, d)) } - private def defineFunction5(name: String)(body: (VarRef, VarRef, VarRef, VarRef, VarRef) => Tree): List[Tree] = { + private def defineFunction5(name: VarField)(body: (VarRef, VarRef, VarRef, VarRef, VarRef) => Tree): List[Tree] = { val a :: b :: c :: d :: e :: _ = argRefs defineFunction(name, paramList(a, b, c, d, e), body(a, b, c, d, e)) } 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 fa7cb5ebb1..c299d8fdd7 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 @@ -400,7 +400,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { private def newSyntheticVar()(implicit pos: Position): js.Ident = { syntheticVarCounter += 1 - fileLevelVarIdent("$x" + syntheticVarCounter) + fileLevelVarIdent(VarField.x, syntheticVarCounter.toString()) } @inline @@ -482,7 +482,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { implicit pos: Position): WithGlobals[js.Function] = { performOptimisticThenPessimisticRuns { - val thisIdent = fileLevelVarIdent("thiz", thisOriginalName) + val thisIdent = fileLevelVarIdent(VarField.thiz, thisOriginalName) val env = env0.withExplicitThis() val js.Function(jsArrow, jsParams, restParam, jsBody) = desugarToFunctionInternal(arrow = false, params, None, body, isStat, env) @@ -667,7 +667,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { unnest(superClass, qualifier, item, rhs) { (newSuperClass, newQualifier, newItem, newRhs, env0) => implicit val env = env0 - genCallHelper("superSet", transformExprNoChar(newSuperClass), + genCallHelper(VarField.superSet, transformExprNoChar(newSuperClass), transformExprNoChar(newQualifier), transformExprNoChar(item), transformExprNoChar(rhs)) } @@ -678,7 +678,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { if (needToUseGloballyMutableVarSetter(scope)) { unnest(rhs) { (rhs, env0) => implicit val env = env0 - js.Apply(globalVar("u", scope), transformExpr(rhs, lhs.tpe) :: Nil) + js.Apply(globalVar(VarField.u, scope), transformExpr(rhs, lhs.tpe) :: Nil) } } else { // Assign normally. @@ -692,7 +692,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case StoreModule(className, value) => unnest(value) { (newValue, env0) => implicit val env = env0 - js.Assign(globalVar("n", className), transformExprNoChar(newValue)) + js.Assign(globalVar(VarField.n, className), transformExprNoChar(newValue)) } case While(cond, body) => @@ -773,7 +773,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { } else { val superCtor = { if (globalKnowledge.hasStoredSuperClass(enclosingClassName)) { - fileLevelVar("superClass") + fileLevelVar(VarField.superClass) } else { val superClass = globalKnowledge.getSuperClassOfJSClass(enclosingClassName) @@ -879,9 +879,9 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case (PrimArray(srcPrimRef), PrimArray(destPrimRef)) if srcPrimRef == destPrimRef => genUncheckedArraycopy(jsArgs) case (RefArray(), RefArray()) => - genCallHelper("systemArraycopyRefs", jsArgs: _*) + genCallHelper(VarField.systemArraycopyRefs, jsArgs: _*) case _ => - genCallHelper("systemArraycopyFull", jsArgs: _*) + genCallHelper(VarField.systemArraycopyFull, jsArgs: _*) } } } @@ -2209,7 +2209,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { genSelect(transformExprNoChar(checkNotNull(qualifier)), className, field) case SelectStatic(className, item) => - globalVar("t", (className, item.name)) + globalVar(VarField.t, (className, item.name)) case SelectJSNativeMember(className, member) => val jsNativeLoadSpec = @@ -2247,10 +2247,10 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { js.Apply(newReceiver(false) DOT transformMethodIdent(method), newArgs) def genDispatchApply(): js.Tree = - js.Apply(globalVar("dp", methodName), newReceiver(false) :: newArgs) + js.Apply(globalVar(VarField.dp, methodName), newReceiver(false) :: newArgs) def genHijackedMethodApply(className: ClassName): js.Tree = - genApplyStaticLike("f", className, method, newReceiver(className == BoxedCharacterClass) :: newArgs) + genApplyStaticLike(VarField.f, className, method, newReceiver(className == BoxedCharacterClass) :: newArgs) if (isMaybeHijackedClass(receiver.tpe) && !methodName.isReflectiveProxy) { @@ -2300,20 +2300,20 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { val transformedArgs = newReceiver :: newArgs if (flags.isConstructor) { - genApplyStaticLike("ct", className, method, transformedArgs) + genApplyStaticLike(VarField.ct, className, method, transformedArgs) } else if (flags.isPrivate) { - genApplyStaticLike("p", className, method, transformedArgs) + genApplyStaticLike(VarField.p, className, method, transformedArgs) } else if (globalKnowledge.isInterface(className)) { - genApplyStaticLike("f", className, method, transformedArgs) + genApplyStaticLike(VarField.f, className, method, transformedArgs) } else { val fun = - globalVar("c", className).prototype DOT transformMethodIdent(method) + globalVar(VarField.c, className).prototype DOT transformMethodIdent(method) js.Apply(fun DOT "call", transformedArgs) } case ApplyStatic(flags, className, method, args) => genApplyStaticLike( - if (flags.isPrivate) "ps" else "s", + if (flags.isPrivate) VarField.ps else VarField.s, className, method, transformTypedArgs(method.name, args)) @@ -2354,7 +2354,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { else genLongMethodApply(newLhs, LongImpl.toInt) case DoubleToInt => - genCallHelper("doubleToInt", newLhs) + genCallHelper(VarField.doubleToInt, newLhs) case DoubleToFloat => genFround(newLhs) @@ -2366,14 +2366,14 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { genLongMethodApply(newLhs, LongImpl.toDouble) case DoubleToLong => if (useBigIntForLongs) - genCallHelper("doubleToLong", newLhs) + genCallHelper(VarField.doubleToLong, newLhs) else genLongModuleApply(LongImpl.fromDouble, newLhs) // Long -> Float (neither widening nor narrowing) case LongToFloat => if (useBigIntForLongs) - genCallHelper("longToFloat", newLhs) + genCallHelper(VarField.longToFloat, newLhs) else genLongMethodApply(newLhs, LongImpl.toFloat) @@ -2458,14 +2458,14 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case IntLiteral(r) if r != 0 => or0(js.BinaryOp(JSBinaryOp./, newLhs, newRhs)) case _ => - genCallHelper("intDiv", newLhs, newRhs) + genCallHelper(VarField.intDiv, newLhs, newRhs) } case Int_% => rhs match { case IntLiteral(r) if r != 0 => or0(js.BinaryOp(JSBinaryOp.%, newLhs, newRhs)) case _ => - genCallHelper("intMod", newLhs, newRhs) + genCallHelper(VarField.intMod, newLhs, newRhs) } case Int_| => js.BinaryOp(JSBinaryOp.|, newLhs, newRhs) @@ -2513,7 +2513,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case LongLiteral(r) if r != 0L => wrapBigInt64(js.BinaryOp(JSBinaryOp./, newLhs, newRhs)) case _ => - genCallHelper("longDiv", newLhs, newRhs) + genCallHelper(VarField.longDiv, newLhs, newRhs) } } else { genLongMethodApply(newLhs, LongImpl./, newRhs) @@ -2524,7 +2524,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case LongLiteral(r) if r != 0L => wrapBigInt64(js.BinaryOp(JSBinaryOp.%, newLhs, newRhs)) case _ => - genCallHelper("longMod", newLhs, newRhs) + genCallHelper(VarField.longMod, newLhs, newRhs) } } else { genLongMethodApply(newLhs, LongImpl.%, newRhs) @@ -2631,7 +2631,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case String_charAt => semantics.stringIndexOutOfBounds match { case CheckedBehavior.Compliant | CheckedBehavior.Fatal => - genCallHelper("charAt", newLhs, newRhs) + genCallHelper(VarField.charAt, newLhs, newRhs) case CheckedBehavior.Unchecked => js.Apply(genIdentBracketSelect(newLhs, "charCodeAt"), List(newRhs)) } @@ -2672,7 +2672,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { extractWithGlobals(genAsInstanceOf(transformExprNoChar(expr), tpe)) case GetClass(expr) => - genCallHelper("objectGetClass", transformExprNoChar(expr)) + genCallHelper(VarField.objectGetClass, transformExprNoChar(expr)) case Clone(expr) => val newExpr = transformExprNoChar(checkNotNull(expr)) @@ -2700,15 +2700,15 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { */ case ClassType(CloneableClass) | ClassType(SerializableClass) | ClassType(ObjectClass) | AnyType => - genCallHelper("objectOrArrayClone", newExpr) + genCallHelper(VarField.objectOrArrayClone, newExpr) // Otherwise, it is known not to be an array. case _ => - genCallHelper("objectClone", newExpr) + genCallHelper(VarField.objectClone, newExpr) } case IdentityHashCode(expr) => - genCallHelper("systemIdentityHashCode", transformExprNoChar(expr)) + genCallHelper(VarField.systemIdentityHashCode, transformExprNoChar(expr)) case WrapAsThrowable(expr) => val newExpr = transformExprNoChar(expr).asInstanceOf[js.VarRef] @@ -2727,7 +2727,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { // Transients case Transient(CheckNotNull(obj)) => - genCallHelper("n", transformExpr(obj, preserveChar = true)) + genCallHelper(VarField.n, transformExpr(obj, preserveChar = true)) case Transient(AssumeNotNull(obj)) => transformExpr(obj, preserveChar = true) @@ -2753,7 +2753,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { } case Transient(ObjectClassName(obj)) => - genCallHelper("objectClassName", transformExprNoChar(obj)) + genCallHelper(VarField.objectClassName, transformExprNoChar(obj)) case Transient(ArrayToTypedArray(expr, primRef)) => val value = transformExprNoChar(checkNotNull(expr)) @@ -2800,7 +2800,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case Transient(JSNewVararg(constr, argsArray)) => assert(!es2015, s"generated a JSNewVargs with ES 2015+ at ${tree.pos}") - genCallHelper("newJSObjectWithVarargs", + genCallHelper(VarField.newJSObjectWithVarargs, transformExprNoChar(constr), transformExprNoChar(argsArray)) case JSPrivateSelect(qualifier, className, field) => @@ -2840,7 +2840,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { transformExprNoChar(method)), args.map(transformJSArg)) case JSSuperSelect(superClass, qualifier, item) => - genCallHelper("superGet", transformExprNoChar(superClass), + genCallHelper(VarField.superGet, transformExprNoChar(superClass), transformExprNoChar(qualifier), transformExprNoChar(item)) case JSImportCall(arg) => @@ -2902,7 +2902,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { js.UnaryOp(JSUnaryOp.typeof, transformExprNoChar(globalRef)) case JSLinkingInfo() => - globalVar("linkingInfo", CoreVar) + globalVar(VarField.linkingInfo, CoreVar) // Literals @@ -2943,10 +2943,10 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { js.This() case VarKind.ExplicitThisAlias => - fileLevelVar("thiz") + fileLevelVar(VarField.thiz) case VarKind.ClassCapture => - fileLevelVar("cc", genName(name.name)) + fileLevelVar(VarField.cc, genName(name.name)) } case Transient(JSVarRef(name, _)) => @@ -2954,7 +2954,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case This() => if (env.hasExplicitThis) - fileLevelVar("thiz") + fileLevelVar(VarField.thiz) else js.This() @@ -2971,7 +2971,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { for ((value, expectedType) <- captureValues.zip(expectedTypes)) yield transformExpr(value, expectedType) } - js.Apply(globalVar("a", className), transformedArgs) + js.Apply(globalVar(VarField.a, className), transformedArgs) // Invalid trees @@ -2984,7 +2984,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { if (preserveChar || tree.tpe != CharType) baseResult else - genCallHelper("bC", baseResult) + genCallHelper(VarField.bC, baseResult) } private def transformApplyDynamicImport(tree: ApplyDynamicImport)( @@ -3012,7 +3012,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { } val innerCall = extractWithGlobals { - withDynamicGlobalVar("s", (className, method.name)) { v => + withDynamicGlobalVar(VarField.s, (className, method.name)) { v => js.Apply(v, newArgs) } } @@ -3232,7 +3232,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { keepOnlyDangerousVarNames = false) } - private def genApplyStaticLike(field: String, className: ClassName, + private def genApplyStaticLike(field: VarField, className: ClassName, method: MethodIdent, args: List[js.Tree])( implicit pos: Position): js.Tree = { js.Apply(globalVar(field, (className, method.name)), args) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/PolyfillableBuiltin.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/PolyfillableBuiltin.scala index 743c9b437d..908d264a9f 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/PolyfillableBuiltin.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/PolyfillableBuiltin.scala @@ -15,7 +15,7 @@ package org.scalajs.linker.backend.emitter import org.scalajs.linker.interface.ESVersion private[emitter] sealed abstract class PolyfillableBuiltin( - val builtinName: String, val availableInESVersion: ESVersion) + val polyfillField: VarField, val availableInESVersion: ESVersion) private[emitter] object PolyfillableBuiltin { lazy val All: List[PolyfillableBuiltin] = List( @@ -27,18 +27,18 @@ private[emitter] object PolyfillableBuiltin { ) sealed abstract class GlobalVarBuiltin(val globalVar: String, - builtinName: String, availableInESVersion: ESVersion) - extends PolyfillableBuiltin(builtinName, availableInESVersion) + polyfillField: VarField, availableInESVersion: ESVersion) + extends PolyfillableBuiltin(polyfillField, availableInESVersion) sealed abstract class NamespacedBuiltin(val namespaceGlobalVar: String, - builtinName: String, availableInESVersion: ESVersion) - extends PolyfillableBuiltin(builtinName, availableInESVersion) + val builtinName: String, polyfillField: VarField, availableInESVersion: ESVersion) + extends PolyfillableBuiltin(polyfillField, availableInESVersion) - case object ObjectIsBuiltin extends NamespacedBuiltin("Object", "is", ESVersion.ES2015) - case object ImulBuiltin extends NamespacedBuiltin("Math", "imul", ESVersion.ES2015) - case object FroundBuiltin extends NamespacedBuiltin("Math", "fround", ESVersion.ES2015) + case object ObjectIsBuiltin extends NamespacedBuiltin("Object", "is", VarField.is, ESVersion.ES2015) + case object ImulBuiltin extends NamespacedBuiltin("Math", "imul", VarField.imul, ESVersion.ES2015) + case object FroundBuiltin extends NamespacedBuiltin("Math", "fround", VarField.fround, ESVersion.ES2015) case object PrivateSymbolBuiltin - extends GlobalVarBuiltin("Symbol", "privateJSFieldSymbol", ESVersion.ES2015) - case object GetOwnPropertyDescriptorsBuiltin - extends NamespacedBuiltin("Object", "getOwnPropertyDescriptors", ESVersion.ES2017) + extends GlobalVarBuiltin("Symbol", VarField.privateJSFieldSymbol, ESVersion.ES2015) + case object GetOwnPropertyDescriptorsBuiltin extends NamespacedBuiltin("Object", + "getOwnPropertyDescriptors", VarField.getOwnPropertyDescriptors, ESVersion.ES2017) } 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 e36ee63582..0449e0ed92 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 @@ -87,7 +87,7 @@ private[emitter] final class SJSGen( if (useBigIntForLongs) BigIntLiteral(0L) else - globalVar("L0", CoreVar) + globalVar(VarField.L0, CoreVar) } def genBoxedZeroOf(tpe: Type)( @@ -100,7 +100,7 @@ private[emitter] final class SJSGen( def genBoxedCharZero()( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): Tree = { - globalVar("bC0", CoreVar) + globalVar(VarField.bC0, CoreVar) } def genLongModuleApply(methodName: MethodName, args: Tree*)( @@ -153,7 +153,7 @@ private[emitter] final class SJSGen( if (esFeatures.esVersion >= ESVersion.ES2015 && semantics.nullPointers == CheckedBehavior.Unchecked) Apply(args.head DOT "copyTo", args.tail) else - genCallHelper("systemArraycopy", args: _*) + genCallHelper(VarField.systemArraycopy, args: _*) } def genSelect(receiver: Tree, className: ClassName, field: irt.FieldIdent)( @@ -178,7 +178,7 @@ private[emitter] final class SJSGen( pos: Position): Tree = { val fieldName = { implicit val pos = field.pos - globalVar("r", (className, field.name)) + globalVar(VarField.r, (className, field.name)) } BracketSelect(receiver, fieldName) @@ -199,7 +199,7 @@ private[emitter] final class SJSGen( !globalKnowledge.isInterface(className)) { genIsInstanceOfClass(expr, className) } else { - Apply(globalVar("is", className), List(expr)) + Apply(globalVar(VarField.is, className), List(expr)) } case ArrayType(arrayTypeRef) => @@ -207,15 +207,15 @@ private[emitter] final class SJSGen( case ArrayTypeRef(_:PrimRef | ClassRef(ObjectClass), 1) => expr instanceof genArrayConstrOf(arrayTypeRef) case ArrayTypeRef(base, depth) => - Apply(typeRefVar("isArrayOf", base), List(expr, IntLiteral(depth))) + Apply(typeRefVar(VarField.isArrayOf, base), List(expr, IntLiteral(depth))) } case UndefType => expr === Undefined() case BooleanType => typeof(expr) === "boolean" - case CharType => expr instanceof globalVar("Char", CoreVar) - case ByteType => genCallHelper("isByte", expr) - case ShortType => genCallHelper("isShort", expr) - case IntType => genCallHelper("isInt", expr) + case CharType => expr instanceof globalVar(VarField.Char, CoreVar) + case ByteType => genCallHelper(VarField.isByte, expr) + case ShortType => genCallHelper(VarField.isShort, expr) + case IntType => genCallHelper(VarField.isInt, expr) case LongType => genIsLong(expr) case FloatType => genIsFloat(expr) case DoubleType => typeof(expr) === "number" @@ -239,7 +239,7 @@ private[emitter] final class SJSGen( */ BooleanLiteral(false) } else { - expr instanceof globalVar("c", className) + expr instanceof globalVar(VarField.c, className) } } @@ -251,10 +251,10 @@ private[emitter] final class SJSGen( className match { case BoxedUnitClass => expr === Undefined() case BoxedBooleanClass => typeof(expr) === "boolean" - case BoxedCharacterClass => expr instanceof globalVar("Char", CoreVar) - case BoxedByteClass => genCallHelper("isByte", expr) - case BoxedShortClass => genCallHelper("isShort", expr) - case BoxedIntegerClass => genCallHelper("isInt", expr) + case BoxedCharacterClass => expr instanceof globalVar(VarField.Char, CoreVar) + case BoxedByteClass => genCallHelper(VarField.isByte, expr) + case BoxedShortClass => genCallHelper(VarField.isShort, expr) + case BoxedIntegerClass => genCallHelper(VarField.isInt, expr) case BoxedLongClass => genIsLong(expr) case BoxedFloatClass => genIsFloat(expr) case BoxedDoubleClass => typeof(expr) === "number" @@ -267,8 +267,8 @@ private[emitter] final class SJSGen( pos: Position): Tree = { import TreeDSL._ - if (useBigIntForLongs) genCallHelper("isLong", expr) - else expr instanceof globalVar("c", LongImpl.RuntimeLongClass) + if (useBigIntForLongs) genCallHelper(VarField.isLong, expr) + else expr instanceof globalVar(VarField.c, LongImpl.RuntimeLongClass) } private def genIsFloat(expr: Tree)( @@ -276,7 +276,7 @@ private[emitter] final class SJSGen( pos: Position): Tree = { import TreeDSL._ - if (semantics.strictFloats) genCallHelper("isFloat", expr) + if (semantics.strictFloats) genCallHelper(VarField.isFloat, expr) else typeof(expr) === "number" } @@ -295,9 +295,9 @@ private[emitter] final class SJSGen( case UndefType => wg(Block(expr, Undefined())) case BooleanType => wg(!(!expr)) - case CharType => wg(genCallHelper("uC", expr)) + case CharType => wg(genCallHelper(VarField.uC, expr)) case ByteType | ShortType| IntType => wg(expr | 0) - case LongType => wg(genCallHelper("uJ", expr)) + case LongType => wg(genCallHelper(VarField.uJ, expr)) case DoubleType => wg(UnaryOp(irt.JSUnaryOp.+, expr)) case StringType => wg(expr || StringLiteral("")) @@ -313,21 +313,21 @@ private[emitter] final class SJSGen( case ClassType(ObjectClass) => expr case ClassType(className) => - Apply(globalVar("as", className), List(expr)) + Apply(globalVar(VarField.as, className), List(expr)) case ArrayType(ArrayTypeRef(base, depth)) => - Apply(typeRefVar("asArrayOf", base), List(expr, IntLiteral(depth))) - - case UndefType => genCallHelper("uV", expr) - case BooleanType => genCallHelper("uZ", expr) - case CharType => genCallHelper("uC", expr) - case ByteType => genCallHelper("uB", expr) - case ShortType => genCallHelper("uS", expr) - case IntType => genCallHelper("uI", expr) - case LongType => genCallHelper("uJ", expr) - case FloatType => genCallHelper("uF", expr) - case DoubleType => genCallHelper("uD", expr) - case StringType => genCallHelper("uT", expr) + Apply(typeRefVar(VarField.asArrayOf, base), List(expr, IntLiteral(depth))) + + case UndefType => genCallHelper(VarField.uV, expr) + case BooleanType => genCallHelper(VarField.uZ, expr) + case CharType => genCallHelper(VarField.uC, expr) + case ByteType => genCallHelper(VarField.uB, expr) + case ShortType => genCallHelper(VarField.uS, expr) + case IntType => genCallHelper(VarField.uI, expr) + case LongType => genCallHelper(VarField.uJ, expr) + case FloatType => genCallHelper(VarField.uF, expr) + case DoubleType => genCallHelper(VarField.uD, expr) + case StringType => genCallHelper(VarField.uT, expr) case AnyType => expr case NoType | NullType | NothingType | _:RecordType => @@ -386,7 +386,7 @@ private[emitter] final class SJSGen( BoxedFloatClass ) ::: nonSmallNumberHijackedClassesOrderedForTypeTests - def genCallHelper(helperName: String, args: Tree*)( + def genCallHelper(helperName: VarField, args: Tree*)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): Tree = { Apply(globalVar(helperName, CoreVar), args.toList) @@ -405,7 +405,7 @@ private[emitter] final class SJSGen( Apply(genIdentBracketSelect(namespace, builtin.builtinName), args.toList) } } else { - WithGlobals(genCallHelper(builtin.builtinName, args: _*)) + WithGlobals(genCallHelper(builtin.polyfillField, args: _*)) } } @@ -413,18 +413,18 @@ private[emitter] final class SJSGen( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): Tree = { import TreeDSL._ - Apply(globalVar("m", moduleClass), Nil) + Apply(globalVar(VarField.m, moduleClass), Nil) } def genScalaClassNew(className: ClassName, ctor: MethodName, args: Tree*)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): Tree = { - val encodedClassVar = globalVar("c", className) + val encodedClassVar = globalVar(VarField.c, className) val argsList = args.toList if (globalKnowledge.hasInlineableInit(className)) { New(encodedClassVar, argsList) } else { - Apply(globalVar("ct", (className, ctor)), New(encodedClassVar, Nil) :: argsList) + Apply(globalVar(VarField.ct, (className, ctor)), New(encodedClassVar, Nil) :: argsList) } } @@ -456,7 +456,7 @@ private[emitter] final class SJSGen( def genNonNativeJSClassConstructor(className: ClassName)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): Tree = { - Apply(globalVar("a", className), Nil) + Apply(globalVar(VarField.a, className), Nil) } def genLoadJSFromSpec(spec: irt.JSNativeLoadSpec, @@ -487,7 +487,7 @@ private[emitter] final class SJSGen( val moduleValue = VarRef(externalModuleFieldIdent(module)) path match { case "default" :: rest if moduleKind == ModuleKind.CommonJSModule => - val defaultField = genCallHelper("moduleDefault", moduleValue) + val defaultField = genCallHelper(VarField.moduleDefault, moduleValue) WithGlobals(pathSelection(defaultField, rest)) case _ => WithGlobals(pathSelection(moduleValue, path)) @@ -513,7 +513,7 @@ private[emitter] final class SJSGen( case length :: Nil => New(genArrayConstrOf(arrayTypeRef), length :: Nil) case _ => - genCallHelper("newArrayObject", genClassDataOf(arrayTypeRef), + genCallHelper(VarField.newArrayObject, genClassDataOf(arrayTypeRef), ArrayConstr(lengths)) } } @@ -551,9 +551,9 @@ private[emitter] final class SJSGen( arrayTypeRef match { case ArrayTypeRef(primRef: PrimRef, 1) => - globalVar("ac", primRef) + globalVar(VarField.ac, primRef) case ArrayTypeRef(ClassRef(ObjectClass), 1) => - globalVar("ac", ObjectClass) + globalVar(VarField.ac, ObjectClass) case _ => genClassDataOf(arrayTypeRef) DOT "constr" } @@ -576,7 +576,7 @@ private[emitter] final class SJSGen( pos: Position): Tree = { typeRef match { case typeRef: NonArrayTypeRef => - typeRefVar("d", typeRef) + typeRefVar(VarField.d, typeRef) case ArrayTypeRef(base, dims) => val baseData = genClassDataOf(base) @@ -598,6 +598,6 @@ private[emitter] final class SJSGen( if (semantics.nullPointers == CheckedBehavior.Unchecked) obj else - genCallHelper("n", obj) + genCallHelper(VarField.n, obj) } } 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 new file mode 100644 index 0000000000..2be691d96e --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarField.scala @@ -0,0 +1,277 @@ +/* + * 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.backend.emitter + +/** Namespace for generated fields. + * + * Mainly to avoid duplicate strings in memory. + * + * Also gives us additional compile-time safety against typos. + */ +private[emitter] final class VarField private (val str: String) extends AnyVal + +private[emitter] object VarField { + private def mk(str: String): VarField = { + require(str(0) == '$') + new VarField(str) + } + + // Scala class related fields. + + /** Scala classes (constructor functions). */ + final val c = mk("$c") + + /** Inheritable constructor functions for Scala classes. */ + final val h = mk("$h") + + /** Scala class constructors (). */ + final val ct = mk("$ct") + + /** Scala class initializers (). */ + final val sct = mk("$sct") + + /** Private (instance) methods. */ + final val p = mk("$p") + + /** Public static methods. */ + final val s = mk("$s") + + /** Private static methods. */ + final val ps = mk("$ps") + + /** Interface default and hijacked public methods. */ + final val f = mk("$f") + + /** Static fields. */ + final val t = mk("$t") + + /** Scala module accessor. */ + final val m = mk("$m") + + /** Var / let to store Scala module instance. + * + * Also used for null check in CoreJSLib. + */ + final val n = mk("$n") + + // JS class related fields. + + /** JS Class acessor / factories. */ + final val a = mk("$a") + + /** Var / let to store (top-level) JS Class. */ + final val b = mk("$b") + + /** Names for private JS fields. */ + final val r = mk("$r") + + // Reflection + + /** Class data. */ + final val d = mk("$d") + + /** isInstanceOf functions. + * + * Also used as Object.is polyfill. + */ + final val is = mk("$is") + + /** asInstanceOf functions. */ + final val as = mk("$as") + + /** isInstanceOf for array functions. */ + final val isArrayOf = mk("$isArrayOf") + + /** asInstanceOf for array functions. */ + final val asArrayOf = mk("$asArrayOf") + + // Modules + + /** External ES module imports. */ + final val i = mk("$i") + + /** Internal ES module imports. */ + final val j = mk("$j") + + /** ES module const export names. */ + final val e = mk("$e") + + /** Setters for globally mutable vars (for ES Modules). */ + final val u = mk("$u") + + // Local fields: Used to generate non-clashing *local* identifiers. + + /** Synthetic vars for the FunctionEmitter. */ + final val x = mk("$x") + + /** Dummy inheritable constructors for JS classes. */ + final val hh = mk("$hh") + + /** Local field for class captures. */ + final val cc = mk("$cc") + + /** Local field for super class. */ + final val superClass = mk("$superClass") + + /** Local field for this replacement. */ + final val thiz = mk("$thiz") + + /** Local field for dynamic imports. */ + final val module = mk("$module") + + // Core fields: Generated by the CoreJSLib + + /** The linking info object. */ + final val linkingInfo = mk("$linkingInfo") + + /** The TypeData class. */ + final val TypeData = mk("$TypeData") + + /** Long zero. */ + final val L0 = mk("$L0") + + /** Dispatchers. */ + final val dp = mk("$dp") + + // Char + + /** The Char class. */ + final val Char = mk("$Char") + + /** Boxed Char zero. */ + final val bC0 = mk("$bC0") + + /** Box char. */ + final val bC = mk("$bC") + + final val charAt = mk("$charAt") + + // Object helpers + + final val objectClone = mk("$objectClone") + + final val objectOrArrayClone = mk("$objectOrArrayClone") + + final val objectGetClass = mk("$objectGetClass") + + final val objectClassName = mk("$objectClassName") + + final val throwNullPointerException = mk("$throwNullPointerException") + + final val throwModuleInitError = mk("$throwModuleInitError") + + final val valueDescription = mk("$valueDescription") + + final val propertyName = mk("$propertyName") + + // ID hash subsystem + + final val systemIdentityHashCode = mk("$systemIdentityHashCode") + + final val lastIDHash = mk("$lastIDHash") + + final val idHashCodeMap = mk("$idHashCodeMap") + + // Cast helpers + + final val isByte = mk("$isByte") + + final val isShort = mk("$isShort") + + final val isInt = mk("$isInt") + + final val isLong = mk("$isLong") + + final val isFloat = mk("$isFloat") + + final val throwClassCastException = mk("$throwClassCastException") + + final val noIsInstance = mk("$noIsInstance") + + // Unboxes + final val uV = mk("$uV") + final val uZ = mk("$uZ") + final val uC = mk("$uC") + final val uB = mk("$uB") + final val uS = mk("$uS") + final val uI = mk("$uI") + final val uJ = mk("$uJ") + final val uF = mk("$uF") + final val uD = mk("$uD") + final val uT = mk("$uT") + + // Arrays + + /** Array constructors. */ + final val ac = mk("$ac") + + /** Inheritable array constructors. */ + final val ah = mk("$ah") + + final val arraycopyGeneric = mk("$arraycopyGeneric") + + final val arraycopyCheckBounds = mk("$arraycopyCheckBounds") + + final val systemArraycopy = mk("$systemArraycopy") + + final val systemArraycopyRefs = mk("$systemArraycopyRefs") + + 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") + + final val throwArrayStoreException = mk("$throwArrayStoreException") + + final val throwNegativeArraySizeException = mk("$throwNegativeArraySizeException") + + // JS helpers + + final val newJSObjectWithVarargs = mk("$newJSObjectWithVarargs") + + final val superGet = mk("$superGet") + + final val superSet = mk("$superSet") + + final val resolveSuperRef = mk("$resolveSuperRef") + + final val moduleDefault = mk("$moduleDefault") + + // Arithmetic Call Helpers + + final val intDiv = mk("$intDiv") + + final val intMod = mk("$intMod") + + final val longToFloat = mk("$longToFloat") + + final val longDiv = mk("$longDiv") + + final val longMod = mk("$longMod") + + final val doubleToLong = mk("$doubleToLong") + + final val doubleToInt = mk("$doubleToInt") + + // Polyfills + + final val imul = mk("$imul") + final val fround = mk("$fround") + final val privateJSFieldSymbol = mk("$privateJSFieldSymbol") + final val getOwnPropertyDescriptors = mk("$getOwnPropertyDescriptors") +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarGen.scala index a2a1a12153..b58ac77235 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarGen.scala @@ -39,7 +39,7 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, import jsGen._ import nameGen._ - def globalVar[T: Scope](field: String, scope: T, + def globalVar[T: Scope](field: VarField, scope: T, origName: OriginalName = NoOriginalName)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): Tree = { @@ -51,7 +51,7 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, } } - def globalClassDef[T: Scope](field: String, scope: T, + def globalClassDef[T: Scope](field: VarField, scope: T, parentClass: Option[Tree], members: List[Tree], origName: OriginalName = NoOriginalName)( implicit moduleContext: ModuleContext, pos: Position): WithGlobals[List[Tree]] = { @@ -59,7 +59,7 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, maybeExport(ident, ClassDef(Some(ident), parentClass, members), mutable = false) } - def globalFunctionDef[T: Scope](field: String, scope: T, + def globalFunctionDef[T: Scope](field: VarField, scope: T, args: List[ParamDef], restParam: Option[ParamDef], body: Tree, origName: OriginalName = NoOriginalName)( implicit moduleContext: ModuleContext, pos: Position): WithGlobals[List[Tree]] = { @@ -67,7 +67,7 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, maybeExport(ident, FunctionDef(ident, args, restParam, body), mutable = false) } - def globalVarDef[T: Scope](field: String, scope: T, value: Tree, + def globalVarDef[T: Scope](field: VarField, scope: T, value: Tree, origName: OriginalName = NoOriginalName)( implicit moduleContext: ModuleContext, pos: Position): WithGlobals[List[Tree]] = { val ident = globalVarIdent(field, scope, origName) @@ -75,7 +75,7 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, } /** Attention: A globalVarDecl may only be modified from the module it was declared in. */ - def globalVarDecl[T: Scope](field: String, scope: T, + def globalVarDecl[T: Scope](field: VarField, scope: T, origName: OriginalName = NoOriginalName)( implicit moduleContext: ModuleContext, pos: Position): WithGlobals[List[Tree]] = { val ident = globalVarIdent(field, scope, origName) @@ -86,7 +86,7 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, * module. As such, an additional field needs to be provided for an * additional setter. This is used when generating ES modules. */ - def globallyMutableVarDef[T: Scope](field: String, setterField: String, + def globallyMutableVarDef[T: Scope](field: VarField, setterField: VarField, scope: T, value: Tree, origName: OriginalName = NoOriginalName)( implicit moduleContext: ModuleContext, pos: Position): WithGlobals[List[Tree]] = { val ident = globalVarIdent(field, scope, origName) @@ -116,7 +116,7 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, globalKnowledge.getModule(scopeType.reprClass(scope)) != moduleContext.moduleID } - def globalVarExport[T: Scope](field: String, scope: T, exportName: ExportName, + def globalVarExport[T: Scope](field: VarField, scope: T, exportName: ExportName, origName: OriginalName = NoOriginalName)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): Tree = { @@ -133,12 +133,12 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, } /** Apply the provided body to a dynamically loaded global var */ - def withDynamicGlobalVar[T: Scope](field: String, scope: T)(body: Tree => Tree)( + def withDynamicGlobalVar[T: Scope](field: VarField, scope: T)(body: Tree => Tree)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[Tree] = { val ident = globalVarIdent(field, scope) - val module = fileLevelVarIdent("$module") + val module = fileLevelVarIdent(VarField.module) def unitPromise = { globalRef("Promise").map { promise => @@ -186,7 +186,7 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, } } - private def globalVarIdent[T](field: String, scope: T, + private def globalVarIdent[T](field: VarField, scope: T, origName: OriginalName = NoOriginalName)( implicit pos: Position, scopeType: Scope[T]): Ident = { genericIdent(field, scopeType.subField(scope), origName) @@ -205,7 +205,7 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, * * Returns the relevant coreJSLibVar for primitive types, globalVar otherwise. */ - def typeRefVar(field: String, typeRef: NonArrayTypeRef)( + def typeRefVar(field: VarField, typeRef: NonArrayTypeRef)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): Tree = { /* Explicitly bringing `PrimRefScope` and `ClassScope` as local implicit @@ -229,41 +229,41 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, } } - def fileLevelVar(field: String, subField: String, + def fileLevelVar(field: VarField, subField: String, origName: OriginalName = NoOriginalName)( implicit pos: Position): VarRef = { VarRef(fileLevelVarIdent(field, subField, origName)) } - def fileLevelVar(field: String)(implicit pos: Position): VarRef = + def fileLevelVar(field: VarField)(implicit pos: Position): VarRef = VarRef(fileLevelVarIdent(field)) - def fileLevelVarIdent(field: String, subField: String, + def fileLevelVarIdent(field: VarField, subField: String, origName: OriginalName = NoOriginalName)( implicit pos: Position): Ident = { genericIdent(field, subField, origName) } - def fileLevelVarIdent(field: String)(implicit pos: Position): Ident = + def fileLevelVarIdent(field: VarField)(implicit pos: Position): Ident = fileLevelVarIdent(field, NoOriginalName) - def fileLevelVarIdent(field: String, origName: OriginalName)( + def fileLevelVarIdent(field: VarField, origName: OriginalName)( implicit pos: Position): Ident = { genericIdent(field, "", origName) } def externalModuleFieldIdent(moduleName: String)(implicit pos: Position): Ident = - fileLevelVarIdent("i", genModuleName(moduleName), OriginalName(moduleName)) + fileLevelVarIdent(VarField.i, genModuleName(moduleName), OriginalName(moduleName)) def internalModuleFieldIdent(module: ModuleID)(implicit pos: Position): Ident = - fileLevelVarIdent("j", genModuleName(module.id), OriginalName(module.id)) + fileLevelVarIdent(VarField.j, genModuleName(module.id), OriginalName(module.id)) - private def genericIdent(field: String, subField: String, + private def genericIdent(field: VarField, subField: String, origName: OriginalName = NoOriginalName)( implicit pos: Position): Ident = { val name = - if (subField == "") "$" + field - else "$" + field + "_" + subField + if (subField == "") field.str + else field.str + "_" + subField Ident(avoidClashWithGlobalRef(name), origName) } From 11a5e4ae597251f920f97b596cbce283218e6efd Mon Sep 17 00:00:00 2001 From: Eric K Richardson Date: Wed, 1 Nov 2023 08:16:00 -0700 Subject: [PATCH 005/298] Add java.io.FilterReader and tests, move other reader tests to individual files --- .../src/main/scala/java/io/FilterReader.scala | 35 ++ .../javalib/io/BufferedReaderTest.scala | 166 ++++++++ .../javalib/io/FilterReaderTest.scala | 55 +++ .../javalib/io/InputStreamReaderTest.scala | 86 ++++ .../testsuite/javalib/io/ReaderTest.scala | 34 ++ .../testsuite/javalib/io/ReadersTest.scala | 366 ------------------ .../javalib/io/StringReaderTest.scala | 139 +++++++ 7 files changed, 515 insertions(+), 366 deletions(-) create mode 100644 javalib/src/main/scala/java/io/FilterReader.scala create mode 100644 test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/BufferedReaderTest.scala create mode 100644 test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/FilterReaderTest.scala create mode 100644 test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/InputStreamReaderTest.scala create mode 100644 test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/ReaderTest.scala delete mode 100644 test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/ReadersTest.scala create mode 100644 test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/StringReaderTest.scala diff --git a/javalib/src/main/scala/java/io/FilterReader.scala b/javalib/src/main/scala/java/io/FilterReader.scala new file mode 100644 index 0000000000..810c875dde --- /dev/null +++ b/javalib/src/main/scala/java/io/FilterReader.scala @@ -0,0 +1,35 @@ +/* + * 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 java.io + +abstract class FilterReader protected (protected val in: Reader) extends Reader { + + in.getClass() // null check + + override def close(): Unit = in.close() + + override def mark(readLimit: Int): Unit = in.mark(readLimit) + + override def markSupported(): Boolean = in.markSupported() + + override def read(): Int = in.read() + + override def read(buffer: Array[Char], offset: Int, count: Int): Int = + in.read(buffer, offset, count) + + override def ready(): Boolean = in.ready() + + override def reset(): Unit = in.reset() + + override def skip(count: Long): Long = in.skip(count) +} diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/BufferedReaderTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/BufferedReaderTest.scala new file mode 100644 index 0000000000..c922ef1691 --- /dev/null +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/BufferedReaderTest.scala @@ -0,0 +1,166 @@ +/* + * 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.testsuite.javalib.io + +import java.io._ + +import org.junit.Test +import org.junit.Assert._ + +import org.scalajs.testsuite.utils.AssertThrows.assertThrows + +class BufferedReaderTest { + + val str = "line1\nline2\r\n\nline4\rline5" + def newReader: BufferedReader = new BufferedReader(new StringReader(str), 3) + + @Test def close(): Unit = { + class UnderlyingReader extends StringReader(str) { + var closeCount: Int = 0 + + override def close(): Unit = { + closeCount += 1 + /* Do not call super.close(), to ensure IOExceptions come from + * BufferedReader, and not the underlying reader. + */ + } + } + + val underlying = new UnderlyingReader + val r = new BufferedReader(underlying) + r.read() + assertEquals(0, underlying.closeCount) + r.close() + assertEquals(1, underlying.closeCount) + + // close() actually prevents further use of the reader + assertThrows(classOf[IOException], r.mark(1)) + assertThrows(classOf[IOException], r.read()) + assertThrows(classOf[IOException], r.read(new Array[Char](1), 0, 1)) + assertThrows(classOf[IOException], r.read(new Array[Char](1))) + assertThrows(classOf[IOException], r.readLine()) + assertThrows(classOf[IOException], r.ready()) + assertThrows(classOf[IOException], r.reset()) + assertThrows(classOf[IOException], r.skip(1L)) + assertThrows(classOf[IllegalArgumentException], r.skip(-1L)) + + // close() is idempotent + r.close() + assertEquals(1, underlying.closeCount) + } + + @Test def read(): Unit = { + val r = newReader + + for (c <- str) { + assertEquals(c, r.read().toChar) + } + assertEquals(-1, r.read()) + } + + @Test def readArrayChar(): Unit = { + var read = 0 + val r = newReader + val buf = new Array[Char](15) + + // twice to force filling internal buffer + for (_ <- 0 to 1) { + val len = r.read(buf) + assertTrue(len > 0) + + for (i <- 0 until len) + assertEquals(str.charAt(i + read), buf(i)) + + read += len + } + } + + @Test def readArrayCharIntInt(): Unit = { + var read = 0 + val r = newReader + val buf = new Array[Char](15) + + // twice to force filling internal buffer + for (_ <- 0 to 1) { + val len = r.read(buf, 1, 10) + assertTrue(len > 0) + assertTrue(len < 11) + + for (i <- 0 until len) + assertEquals(str.charAt(i + read), buf(i + 1)) + + read += len + } + } + + @Test def markAndReset(): Unit = { + val r = newReader + assertEquals('l': Int, r.read()) + + // force moving and resizing buffer + r.mark(10) + + for (i <- 0 until 10) { + assertEquals(str.charAt(i + 1): Int, r.read()) + } + + r.reset() + + for (i <- 1 until str.length) { + assertEquals(str.charAt(i): Int, r.read()) + } + } + + @Test def readLine(): Unit = { + val r = newReader + + assertEquals("line1", r.readLine()) + assertEquals("line2", r.readLine()) + assertEquals("", r.readLine()) + assertEquals("line4", r.readLine()) + assertEquals("line5", r.readLine()) + assertEquals(null, r.readLine()) + } + + @Test def readLineEmptyStream(): Unit = { + val r = new BufferedReader(new StringReader("")) + + assertEquals(null, r.readLine()) + } + + @Test def readLineEmptyLinesOnly(): Unit = { + val r = new BufferedReader(new StringReader("\n\r\n\r\r\n"), 1) + + for (_ <- 1 to 4) + assertEquals("", r.readLine()) + + assertEquals(null, r.readLine()) + } + + @Test def skipReturns0AfterReachingEnd(): Unit = { + val r = newReader + assertEquals(25, r.skip(100)) + assertEquals(-1, r.read()) + + assertEquals(0, r.skip(100)) + assertEquals(-1, r.read()) + } + + @Test def markSupported(): Unit = { + assertTrue(newReader.markSupported) + } + + @Test def markThrowsWithNegativeLookahead(): Unit = { + assertThrows(classOf[IllegalArgumentException], newReader.mark(-10)) + } +} diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/FilterReaderTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/FilterReaderTest.scala new file mode 100644 index 0000000000..851d8a7ff0 --- /dev/null +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/FilterReaderTest.scala @@ -0,0 +1,55 @@ +/* + * 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.testsuite.javalib.io + +import java.io._ + +import org.junit.Test +import org.junit.Assert._ + +import org.scalajs.testsuite.utils.AssertThrows.{ + assertThrows, + assertThrowsNPEIfCompliant +} + +class FilterReaderTest { + // use StringReader as delegate + val str = "asdf" + def newFilterReader: FilterReader = new FilterReader(new StringReader(str)) {} + + @Test def nullCtorArgThrows(): Unit = { + assertThrowsNPEIfCompliant(new FilterReader(null) {}) + } + + // test delegation + @Test def close(): Unit = { + val fr = newFilterReader + + fr.close() + fr.close() // multiple is fine + assertThrows(classOf[IOException], fr.read()) + } + + @Test def markSupported(): Unit = { + assertTrue(newFilterReader.markSupported) + } + + @Test def read(): Unit = { + val r = newFilterReader + + for (c <- str) { + assertEquals(c, r.read().toChar) + } + assertEquals(-1, r.read()) + } +} diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/InputStreamReaderTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/InputStreamReaderTest.scala new file mode 100644 index 0000000000..fa4ab4d4d2 --- /dev/null +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/InputStreamReaderTest.scala @@ -0,0 +1,86 @@ +/* + * 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.testsuite.javalib.io + +import scala.annotation.tailrec + +import java.io._ + +import org.junit.Test +import org.junit.Assert._ + +import org.scalajs.testsuite.utils.AssertThrows.assertThrows + +class InputStreamReaderTest { + + @Test def readUTF8(): Unit = { + + val buf = Array[Byte](72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, + 46, -29, -127, -109, -29, -126, -109, -29, -127, -85, -29, -127, -95, + -29, -127, -81, -26, -105, -91, -26, -100, -84, -24, -86, -98, -29, + -126, -110, -24, -86, -83, -29, -126, -127, -29, -127, -66, -29, -127, + -103, -29, -127, -117, -29, -128, -126) + + val r = new InputStreamReader(new ByteArrayInputStream(buf)) + + def expectRead(str: String): Unit = { + val buf = new Array[Char](str.length) + @tailrec + def readAll(readSoFar: Int): Int = { + if (readSoFar == buf.length) readSoFar + else { + val newlyRead = r.read(buf, readSoFar, buf.length - readSoFar) + if (newlyRead == -1) readSoFar + else readAll(readSoFar + newlyRead) + } + } + assertEquals(str.length, readAll(0)) + assertEquals(str, new String(buf)) + } + + expectRead("Hello World.") + expectRead("こんにちは") + expectRead("日本語を読めますか。") + assertEquals(-1, r.read()) + } + + @Test def readEOFThrows(): Unit = { + val data = "Lorem ipsum".getBytes() + val streamReader = new InputStreamReader(new ByteArrayInputStream(data)) + val bytes = new Array[Char](11) + + assertEquals(11, streamReader.read(bytes)) + // Do it twice to check for a regression where this used to throw + assertEquals(-1, streamReader.read(bytes)) + assertEquals(-1, streamReader.read(bytes)) + assertThrows(classOf[IndexOutOfBoundsException], + streamReader.read(bytes, 10, 3)) + assertEquals(0, streamReader.read(new Array[Char](0))) + } + + @Test def skipReturns0AfterReachingEnd(): Unit = { + val data = "Lorem ipsum".getBytes() + val r = new InputStreamReader(new ByteArrayInputStream(data)) + assertTrue(r.skip(100) > 0) + assertEquals(-1, r.read()) + + assertEquals(0, r.skip(100)) + assertEquals(-1, r.read()) + } + + @Test def markThrowsNotSupported(): Unit = { + val data = "Lorem ipsum".getBytes() + val r = new InputStreamReader(new ByteArrayInputStream(data)) + assertThrows(classOf[IOException], r.mark(0)) + } +} diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/ReaderTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/ReaderTest.scala new file mode 100644 index 0000000000..0039020c2d --- /dev/null +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/ReaderTest.scala @@ -0,0 +1,34 @@ +/* + * 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.testsuite.javalib.io + +import java.io._ + +import org.junit.Test +import org.junit.Assert._ + +/** Tests for our implementation of java.io._ reader classes */ +class ReaderTest { + object MyReader extends java.io.Reader { + def read(dbuf: Array[Char], off: Int, len: Int): Int = { + java.util.Arrays.fill(dbuf, off, off + len, 'A') + len + } + def close(): Unit = () + } + + @Test def skipIntIfPossible(): Unit = { + assertEquals(42, MyReader.skip(42)) + assertEquals(10000, MyReader.skip(10000)) // more than the 8192 batch size + } +} diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/ReadersTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/ReadersTest.scala deleted file mode 100644 index 65eb2660a9..0000000000 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/ReadersTest.scala +++ /dev/null @@ -1,366 +0,0 @@ -/* - * 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.testsuite.javalib.io - -import scala.annotation.tailrec - -import java.io._ - -import org.junit.Test -import org.junit.Assert._ - -import org.scalajs.testsuite.utils.AssertThrows.assertThrows - -/** Tests for our implementation of java.io._ reader classes */ -class ReaderTest { - object MyReader extends java.io.Reader { - def read(dbuf: Array[Char], off: Int, len: Int): Int = { - java.util.Arrays.fill(dbuf, off, off + len, 'A') - len - } - def close(): Unit = () - } - - @Test def skipIntIfPossible(): Unit = { - assertEquals(42, MyReader.skip(42)) - assertEquals(10000, MyReader.skip(10000)) // more than the 8192 batch size - } -} - -class StringReaderTest { - val str = "asdf" - def newReader: StringReader = new StringReader(str) - - @Test def read()(): Unit = { - val r = newReader - - for (c <- str) { - assertEquals(c, r.read().toChar) - } - - assertEquals(-1, r.read()) - } - - @Test def readArrayCharIntInt(): Unit = { - val r = newReader - val buf = new Array[Char](10) - - assertEquals(4, r.read(buf, 2, 8)) - assertArrayEquals(buf.map(_.toInt), Array[Int](0,0,'a','s','d','f',0,0,0,0)) - assertEquals(-1, r.read(buf, 2, 8)) // #1560 - } - - @Test def readCharBuffer(): Unit = { - val r = newReader - val buf0 = java.nio.CharBuffer.allocate(25) - buf0.position(3) - val buf = buf0.slice() - buf.position(4) - buf.limit(14) - - assertEquals(4, r.read(buf)) - assertEquals(8, buf.position()) - buf.flip() - assertArrayEquals(buf.toString().map(_.toInt).toArray, - Array[Int](0, 0, 0, 0, 'a', 's', 'd', 'f')) - } - - @Test def ready(): Unit = { - val r = newReader - - for (c <- str) { - assertTrue(r.ready()) - assertEquals(c, r.read().toChar) - } - - assertTrue(r.ready()) - assertEquals(-1, r.read()) - - r.close() - assertThrows(classOf[IOException], r.ready()) - } - - @Test def markReset(): Unit = { - val r = newReader - r.mark(str.length) - - for (c <- str) { - assertEquals(c, r.read().toChar) - } - assertEquals(-1, r.read()) - - r.reset() - - for (c <- str) { - assertEquals(c, r.read().toChar) - } - assertEquals(-1, r.read()) - } - - @Test def skip(): Unit = { - val r = newReader - - assertEquals('a': Int, r.read()) - assertEquals(2, r.skip(2L).toInt) - - assertEquals('f': Int, r.read()) - assertEquals(-1, r.read()) - } - - @Test def close(): Unit = { - val r = newReader - - r.close() - assertThrows(classOf[IOException], r.read()) - } - - @Test def mark(): Unit = { - assertTrue(newReader.markSupported) - } - - @Test def markThrowsWithNegativeLookahead(): Unit = { - assertThrows(classOf[IllegalArgumentException], newReader.mark(-10)) - } - - @Test def skipAcceptsNegativeLookaheadAsLookback(): Unit = { - // StringReader.skip accepts negative lookahead - val r = newReader - assertEquals("already head", 0, r.skip(-1)) - assertEquals('a', r.read()) - - assertEquals(1, r.skip(1)) - assertEquals('d', r.read()) - - assertEquals(-2, r.skip(-2)) - assertEquals('s', r.read()) - } - - @Test def skipReturns0AfterReachingEnd(): Unit = { - val r = newReader - assertEquals(4, r.skip(100)) - assertEquals(-1, r.read()) - - assertEquals(0, r.skip(-100)) - assertEquals(-1, r.read()) - } - -} - -class BufferedReaderTest { - - val str = "line1\nline2\r\n\nline4\rline5" - def newReader: BufferedReader = new BufferedReader(new StringReader(str), 3) - - @Test def close(): Unit = { - class UnderlyingReader extends StringReader(str) { - var closeCount: Int = 0 - - override def close(): Unit = { - closeCount += 1 - /* Do not call super.close(), to ensure IOExceptions come from - * BufferedReader, and not the underlying reader. - */ - } - } - - val underlying = new UnderlyingReader - val r = new BufferedReader(underlying) - r.read() - assertEquals(0, underlying.closeCount) - r.close() - assertEquals(1, underlying.closeCount) - - // close() actually prevents further use of the reader - assertThrows(classOf[IOException], r.mark(1)) - assertThrows(classOf[IOException], r.read()) - assertThrows(classOf[IOException], r.read(new Array[Char](1), 0, 1)) - assertThrows(classOf[IOException], r.read(new Array[Char](1))) - assertThrows(classOf[IOException], r.readLine()) - assertThrows(classOf[IOException], r.ready()) - assertThrows(classOf[IOException], r.reset()) - assertThrows(classOf[IOException], r.skip(1L)) - assertThrows(classOf[IllegalArgumentException], r.skip(-1L)) - - // close() is idempotent - r.close() - assertEquals(1, underlying.closeCount) - } - - @Test def read(): Unit = { - val r = newReader - - for (c <- str) { - assertEquals(c, r.read().toChar) - } - assertEquals(-1, r.read()) - } - - @Test def readArrayChar(): Unit = { - var read = 0 - val r = newReader - val buf = new Array[Char](15) - - // twice to force filling internal buffer - for (_ <- 0 to 1) { - val len = r.read(buf) - assertTrue(len > 0) - - for (i <- 0 until len) - assertEquals(str.charAt(i+read), buf(i)) - - read += len - } - } - - @Test def readArrayCharIntInt(): Unit = { - var read = 0 - val r = newReader - val buf = new Array[Char](15) - - // twice to force filling internal buffer - for (_ <- 0 to 1) { - val len = r.read(buf, 1, 10) - assertTrue(len > 0) - assertTrue(len < 11) - - for (i <- 0 until len) - assertEquals(str.charAt(i+read), buf(i+1)) - - read += len - } - } - - @Test def markAndReset(): Unit = { - val r = newReader - assertEquals('l': Int, r.read()) - - // force moving and resizing buffer - r.mark(10) - - for (i <- 0 until 10) { - assertEquals(str.charAt(i+1): Int, r.read()) - } - - r.reset() - - for (i <- 1 until str.length) { - assertEquals(str.charAt(i): Int, r.read()) - } - } - - @Test def readLine(): Unit = { - val r = newReader - - assertEquals("line1", r.readLine()) - assertEquals("line2", r.readLine()) - assertEquals("", r.readLine()) - assertEquals("line4", r.readLine()) - assertEquals("line5", r.readLine()) - assertEquals(null, r.readLine()) - } - - @Test def readLineEmptyStream(): Unit = { - val r = new BufferedReader(new StringReader("")) - - assertEquals(null, r.readLine()) - } - - @Test def readLineEmptyLinesOnly(): Unit = { - val r = new BufferedReader(new StringReader("\n\r\n\r\r\n"), 1) - - for (_ <- 1 to 4) - assertEquals("", r.readLine()) - - assertEquals(null, r.readLine()) - } - - @Test def skipReturns0AfterReachingEnd(): Unit = { - val r = newReader - assertEquals(25, r.skip(100)) - assertEquals(-1, r.read()) - - assertEquals(0, r.skip(100)) - assertEquals(-1, r.read()) - } - - @Test def markSupported(): Unit = { - assertTrue(newReader.markSupported) - } - - @Test def markThrowsWithNegativeLookahead(): Unit = { - assertThrows(classOf[IllegalArgumentException], newReader.mark(-10)) - } -} - -class InputStreamReaderTest { - - @Test def readUTF8(): Unit = { - - val buf = Array[Byte](72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, - 46, -29, -127, -109, -29, -126, -109, -29, -127, -85, -29, -127, -95, - -29, -127, -81, -26, -105, -91, -26, -100, -84, -24, -86, -98, -29, - -126, -110, -24, -86, -83, -29, -126, -127, -29, -127, -66, -29, -127, - -103, -29, -127, -117, -29, -128, -126) - - val r = new InputStreamReader(new ByteArrayInputStream(buf)) - - def expectRead(str: String): Unit = { - val buf = new Array[Char](str.length) - @tailrec - def readAll(readSoFar: Int): Int = { - if (readSoFar == buf.length) readSoFar - else { - val newlyRead = r.read(buf, readSoFar, buf.length - readSoFar) - if (newlyRead == -1) readSoFar - else readAll(readSoFar + newlyRead) - } - } - assertEquals(str.length, readAll(0)) - assertEquals(str, new String(buf)) - } - - expectRead("Hello World.") - expectRead("こんにちは") - expectRead("日本語を読めますか。") - assertEquals(-1, r.read()) - } - - @Test def readEOFThrows(): Unit = { - val data = "Lorem ipsum".getBytes() - val streamReader = new InputStreamReader(new ByteArrayInputStream(data)) - val bytes = new Array[Char](11) - - assertEquals(11, streamReader.read(bytes)) - // Do it twice to check for a regression where this used to throw - assertEquals(-1, streamReader.read(bytes)) - assertEquals(-1, streamReader.read(bytes)) - assertThrows(classOf[IndexOutOfBoundsException], streamReader.read(bytes, 10, 3)) - assertEquals(0, streamReader.read(new Array[Char](0))) - } - - @Test def skipReturns0AfterReachingEnd(): Unit = { - val data = "Lorem ipsum".getBytes() - val r = new InputStreamReader(new ByteArrayInputStream(data)) - assertTrue(r.skip(100) > 0) - assertEquals(-1, r.read()) - - assertEquals(0, r.skip(100)) - assertEquals(-1, r.read()) - } - - @Test def markThrowsNotSupported(): Unit = { - val data = "Lorem ipsum".getBytes() - val r = new InputStreamReader(new ByteArrayInputStream(data)) - assertThrows(classOf[IOException], r.mark(0)) - } -} diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/StringReaderTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/StringReaderTest.scala new file mode 100644 index 0000000000..0c23d55c7b --- /dev/null +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/io/StringReaderTest.scala @@ -0,0 +1,139 @@ +/* + * 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.testsuite.javalib.io + +import java.io._ + +import org.junit.Test +import org.junit.Assert._ + +import org.scalajs.testsuite.utils.AssertThrows.assertThrows + +class StringReaderTest { + val str = "asdf" + def newReader: StringReader = new StringReader(str) + + @Test def read()(): Unit = { + val r = newReader + + for (c <- str) { + assertEquals(c, r.read().toChar) + } + + assertEquals(-1, r.read()) + } + + @Test def readArrayCharIntInt(): Unit = { + val r = newReader + val buf = new Array[Char](10) + + assertEquals(4, r.read(buf, 2, 8)) + assertArrayEquals(buf.map(_.toInt), + Array[Int](0, 0, 'a', 's', 'd', 'f', 0, 0, 0, 0)) + assertEquals(-1, r.read(buf, 2, 8)) // #1560 + } + + @Test def readCharBuffer(): Unit = { + val r = newReader + val buf0 = java.nio.CharBuffer.allocate(25) + buf0.position(3) + val buf = buf0.slice() + buf.position(4) + buf.limit(14) + + assertEquals(4, r.read(buf)) + assertEquals(8, buf.position()) + buf.flip() + assertArrayEquals(buf.toString().map(_.toInt).toArray, + Array[Int](0, 0, 0, 0, 'a', 's', 'd', 'f')) + } + + @Test def ready(): Unit = { + val r = newReader + + for (c <- str) { + assertTrue(r.ready()) + assertEquals(c, r.read().toChar) + } + + assertTrue(r.ready()) + assertEquals(-1, r.read()) + + r.close() + assertThrows(classOf[IOException], r.ready()) + } + + @Test def markReset(): Unit = { + val r = newReader + r.mark(str.length) + + for (c <- str) { + assertEquals(c, r.read().toChar) + } + assertEquals(-1, r.read()) + + r.reset() + + for (c <- str) { + assertEquals(c, r.read().toChar) + } + assertEquals(-1, r.read()) + } + + @Test def skip(): Unit = { + val r = newReader + + assertEquals('a': Int, r.read()) + assertEquals(2, r.skip(2L).toInt) + + assertEquals('f': Int, r.read()) + assertEquals(-1, r.read()) + } + + @Test def close(): Unit = { + val r = newReader + + r.close() + assertThrows(classOf[IOException], r.read()) + } + + @Test def mark(): Unit = { + assertTrue(newReader.markSupported) + } + + @Test def markThrowsWithNegativeLookahead(): Unit = { + assertThrows(classOf[IllegalArgumentException], newReader.mark(-10)) + } + + @Test def skipAcceptsNegativeLookaheadAsLookback(): Unit = { + // StringReader.skip accepts negative lookahead + val r = newReader + assertEquals("already head", 0, r.skip(-1)) + assertEquals('a', r.read()) + + assertEquals(1, r.skip(1)) + assertEquals('d', r.read()) + + assertEquals(-2, r.skip(-2)) + assertEquals('s', r.read()) + } + + @Test def skipReturns0AfterReachingEnd(): Unit = { + val r = newReader + assertEquals(4, r.skip(100)) + assertEquals(-1, r.read()) + + assertEquals(0, r.skip(-100)) + assertEquals(-1, r.read()) + } +} From 95346cf427ea4e78e177c299bbff61b4e0efa219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 8 Nov 2023 12:17:49 +0100 Subject: [PATCH 006/298] Bump the version to 1.15.0-SNAPSHOT for the upcoming changes. --- ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala index 4d7495cf96..c97b15f22f 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala @@ -17,7 +17,7 @@ import java.util.concurrent.ConcurrentHashMap import scala.util.matching.Regex object ScalaJSVersions extends VersionChecks( - current = "1.14.1-SNAPSHOT", + current = "1.15.0-SNAPSHOT", binaryEmitted = "1.13" ) From b50df89ffcea28ed1d758295a6e12348a3a9568f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 8 Nov 2023 12:26:42 +0100 Subject: [PATCH 007/298] Split the scalalib .sjsir files in a separate artifact scalajs-scalalib. Previously, the `.sjsir` files of the scalalib (and libraryAux) were bundled inside scalajs-library.jar. With the plan to drop forward binary compatibility of the upstream scala-library.jar, this model will not work anymore. We now publish those `.sjsir` files in their own artifact `scalajs-scalalib.jar`. It is versioned *both* with the Scala version number and the Scala.js version number, in a way that will allow Ivy resolution to pick the right one. At the POM level, `scalajs-scalalib` depends on `scalajs-javalib`. `scalajs-library` depends on the other two. However, in terms of *actual* content dependencies, as is, `scalajs-scalalib` also depends on `scalajs-library`. If the former is present on a classpath but not (a recent enough version of) the latter, linking errors can appear. This should not be an issue because any real build that depends on `scalajs-scalalib` will also depend on `scalajs-library`. Moreover, if a more recent `scalajs-scalalib` is picked up by Ivy resolution that implicitly depends on a more recent `scalajs-library`, the library that introduces that dependency would also *explicitly* depend on the more recent `scalajs-library`, and so the latter would also be picked up by Ivy resolution. The sbt plugin explicitly adds a dependency on the `scalajs-scalalib` with a Scala/Scala.js version combination that matches `scalaVersion` and `scalaJSVersion`. This way, if it uses a Scala.js version that was built for Scala 2.13.12 but it itself uses Scala 2.13.15, it will get the back-published `scalajs-library` built for Scala 2.13.15. --- build.sbt | 3 +- project/Build.scala | 114 ++++++++++++------ .../sbtplugin/ScalaJSPluginInternal.scala | 8 ++ scripts/publish.sh | 6 +- 4 files changed, 92 insertions(+), 39 deletions(-) diff --git a/build.sbt b/build.sbt index e3374e3358..e9bdde6b6b 100644 --- a/build.sbt +++ b/build.sbt @@ -14,8 +14,9 @@ val sbtPlugin = Build.plugin val javalibintf = Build.javalibintf val javalibInternal = Build.javalibInternal val javalib = Build.javalib -val scalalib = Build.scalalib +val scalalibInternal = Build.scalalibInternal val libraryAux = Build.libraryAux +val scalalib = Build.scalalib val library = Build.library val testInterface = Build.testInterface val testBridge = Build.testBridge diff --git a/project/Build.scala b/project/Build.scala index 796bc8e4b7..8deb00803d 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -197,7 +197,7 @@ object MyScalaJSPlugin extends AutoPlugin { */ libraryDependencies ~= { libDeps => val blacklist = - Set("scalajs-compiler", "scalajs-library", "scalajs-test-bridge") + Set("scalajs-compiler", "scalajs-library", "scalajs-scalalib", "scalajs-test-bridge") libDeps.filterNot(dep => blacklist.contains(dep.name)) }, @@ -638,7 +638,7 @@ object Build { * - `"semver-spec"` for artifacts whose public API can only break in major releases (e.g., `library`) * * At the moment, we only set the version scheme for artifacts in the - * "library ecosystem", i.e., scalajs-javalib scalajs-library, + * "library ecosystem", i.e., scalajs-javalib, scalajs-scalalib, scalajs-library, * scalajs-test-interface, scalajs-junit-runtime and scalajs-test-bridge. * Artifacts of the "tools ecosystem" do not have a version scheme set, as * the jury is still out on what is the best way to specify them. @@ -749,7 +749,7 @@ object Build { } } - /** Depends on library and, by artificial transitivity, on the javalib. */ + /** Depends on library and, by artificial transitivity, on the javalib and scalalib. */ def dependsOnLibrary2_12: Project = { val library = LocalProject("library2_12") @@ -757,9 +757,9 @@ object Build { val project1 = project .dependsOn(library) - /* Because the javalib's exportsJar is false, but its actual products are - * only in its jar, we must manually add the jar on the internal - * classpath. + /* Because the javalib's and scalalib's exportsJar is false, but their + * actual products are only in their jar, we must manually add the jars + * on the internal classpath. * Once published, only jars are ever used, so this is fine. */ if (isGeneratingForIDE) { @@ -772,6 +772,12 @@ object Build { Test / internalDependencyClasspath += (javalib / Compile / packageBin).value, ) + .settings( + Compile / internalDependencyClasspath += + (scalalib.v2_12 / Compile / packageBin).value, + Test / internalDependencyClasspath += + (scalalib.v2_12 / Compile / packageBin).value, + ) } } @@ -818,21 +824,21 @@ object Build { } } - /** Depends on library and, by transitivity, on the javalib. */ + /** Depends on library and, by artificial transitivity, on the javalib and scalalib. */ def dependsOnLibrary: MultiScalaProject = { // Add a real dependency on the library val project1 = project .dependsOn(library) - /* Because the javalib's exportsJar is false, but its actual products are - * only in its jar, we must manually add the jar on the internal - * classpath. + /* Because the javalib's and scalalib's exportsJar is false, but their + * actual products are only in their jar, we must manually add the jars + * on the internal classpath. * Once published, only jars are ever used, so this is fine. */ if (isGeneratingForIDE) { project1 } else { - // Actually add classpath dependencies on the javalib jar + // Actually add classpath dependencies on the javalib and scalalib jars project1 .settings( Compile / internalDependencyClasspath += @@ -840,6 +846,14 @@ object Build { Test / internalDependencyClasspath += (javalib / Compile / packageBin).value, ) + .zippedSettings(scalalib) { scalalib => + Def.settings( + Compile / internalDependencyClasspath += + (scalalib / Compile / packageBin).value, + Test / internalDependencyClasspath += + (scalalib / Compile / packageBin).value, + ) + } } } @@ -919,7 +933,7 @@ object Build { linkerInterface, linkerInterfaceJS, linker, linkerJS, testAdapter, javalibintf, - javalibInternal, javalib, scalalib, libraryAux, library, + javalibInternal, javalib, scalalibInternal, libraryAux, scalalib, library, testInterface, jUnitRuntime, testBridge, jUnitPlugin, jUnitAsyncJS, jUnitAsyncJVM, jUnitTestOutputsJS, jUnitTestOutputsJVM, helloworld, reversi, testingExample, testSuite, testSuiteJVM, @@ -1358,12 +1372,14 @@ object Build { // JS libs publishLocal in javalib, + publishLocal in scalalib.v2_12, publishLocal in library.v2_12, publishLocal in testInterface.v2_12, publishLocal in testBridge.v2_12, publishLocal in jUnitRuntime.v2_12, publishLocal in irProjectJS.v2_12, + publishLocal in scalalib.v2_13, publishLocal in library.v2_13, publishLocal in testInterface.v2_13, publishLocal in testBridge.v2_13, @@ -1478,7 +1494,7 @@ object Build { * copied from `javalibInternal`. * * This the "public" version of the javalib, as depended on by the `library` - * and published on Maven. + * and `scalalib`, and published on Maven. */ lazy val javalib: Project = Project( id = "javalib", base = file("javalib-public") @@ -1506,8 +1522,13 @@ object Build { }, ) - lazy val scalalib: MultiScalaProject = MultiScalaProject( - id = "scalalib", base = file("scalalib") + /** The project that actually compiles the `scalalib`, but which is not + * exposed. + * + * Instead, its products are copied in `scalalib`. + */ + lazy val scalalibInternal: MultiScalaProject = MultiScalaProject( + id = "scalalibInternal", base = file("scalalib") ).enablePlugins( MyScalaJSPlugin ).settings( @@ -1523,7 +1544,7 @@ object Build { s"https://raw.githubusercontent.com/scala/scala/v${scalaVersion.value}/src/library/") option ++ prev }, - name := "Scala library for Scala.js", + name := "scalajs-scalalib-internal", publishArtifact in Compile := false, NoIDEExport.noIDEExportSettings, delambdafySetting, @@ -1669,12 +1690,54 @@ object Build { recompileAllOrNothingSettings, ).withScalaJSCompiler.dependsOnLibraryNoJar + /** An empty project, without source nor dependencies (other than the javalib), + * whose products are copied from `scalalibInternal` and `libraryAux`. + * + * This the "public" version of the scalalib, as depended on by the `library` + * and published on Maven. + */ + lazy val scalalib: MultiScalaProject = MultiScalaProject( + id = "scalalib", base = file("scalalib-public") + ).dependsOn( + javalib, + ).settings( + commonSettings, + name := "scalajs-scalalib", + publishSettings(Some(VersionScheme.BreakOnMajor)), + + /* The scalalib has a special version number that encodes both the Scala + * version and the Scala.js version. This allows us to back-publish for + * newer versions of Scala and older versions of Scala.js. The Scala + * version comes first so that Ivy resolution will choose 2.13.20+1.15.0 + * over 2.13.18+1.16.0. The former might not be as optimized as the + * latter, but at least it will contain all the binary API that might be + * required. + */ + version := scalaVersion.value + "+" + scalaJSVersion, + + exportJars := false, // very important, otherwise there's a cycle with the `library` + ).zippedSettings(Seq("scalalibInternal", "libraryAux"))(localProjects => + inConfig(Compile)(Seq( + // Use the .sjsir files from scalalibInternal and libraryAux (but not the .class files) + Compile / packageBin / mappings := { + val scalalibInternalMappings = (localProjects(0) / packageBin / mappings).value + val libraryAuxMappings = (localProjects(1) / packageBin / mappings).value + val allMappings = scalalibInternalMappings ++ libraryAuxMappings + allMappings.filter(_._2.endsWith(".sjsir")) + }, + )) + ) + lazy val library: MultiScalaProject = MultiScalaProject( id = "library", base = file("library") ).enablePlugins( MyScalaJSPlugin ).dependsOn( + // Project dependencies javalibintf % Provided, javalib, + ).dependsOn( + // MultiScalaProject dependencies + scalalib, ).settings( commonSettings, publishSettings(Some(VersionScheme.BreakOnMajor)), @@ -1727,25 +1790,6 @@ object Build { */ dependencyClasspath in doc ++= exportedProducts.value, )) - ).zippedSettings(Seq("scalalib", "libraryAux"))(localProjects => - inConfig(Compile)(Seq( - /* Add the .sjsir files from other lib projects - * (but not .class files) - */ - mappings in packageBin := { - val libraryMappings = (mappings in packageBin).value - - val filter = ("*.sjsir": NameFilter) - - val otherProducts = ( - (products in localProjects(0)).value ++ - (products in localProjects(1)).value) - val otherMappings = - otherProducts.flatMap(base => Path.selectSubpaths(base, filter)) - - libraryMappings ++ otherMappings - }, - )) ).withScalaJSCompiler // The Scala.js version of sbt-testing-interface diff --git a/sbt-plugin/src/main/scala/org/scalajs/sbtplugin/ScalaJSPluginInternal.scala b/sbt-plugin/src/main/scala/org/scalajs/sbtplugin/ScalaJSPluginInternal.scala index c45eabf03c..6f1d6f66a4 100644 --- a/sbt-plugin/src/main/scala/org/scalajs/sbtplugin/ScalaJSPluginInternal.scala +++ b/sbt-plugin/src/main/scala/org/scalajs/sbtplugin/ScalaJSPluginInternal.scala @@ -795,6 +795,8 @@ private[sbtplugin] object ScalaJSPluginInternal { scalaOrg %% "scala3-library_sjs1" % scalaV, /* scala3-library_sjs1 depends on some version of scalajs-library_2.13, * but we bump it to be at least scalaJSVersion. + * (It will also depend on some version of scalajs-scalalib_2.13, + * but we do not have to worry about that here.) */ "org.scala-js" % "scalajs-library_2.13" % scalaJSVersion, "org.scala-js" % "scalajs-test-bridge_2.13" % scalaJSVersion % "test" @@ -803,6 +805,12 @@ private[sbtplugin] object ScalaJSPluginInternal { prev ++ Seq( compilerPlugin("org.scala-js" % "scalajs-compiler" % scalaJSVersion cross CrossVersion.full), "org.scala-js" %% "scalajs-library" % scalaJSVersion, + /* scalajs-library depends on some version of scalajs-scalalib, + * but we want to make sure to bump it to be at least the one + * of our own `scalaVersion` (which would have back-published in + * the meantime). + */ + "org.scala-js" %% "scalajs-scalalib" % s"$scalaV+$scalaJSVersion", "org.scala-js" %% "scalajs-test-bridge" % scalaJSVersion % "test" ) } diff --git a/scripts/publish.sh b/scripts/publish.sh index 6a6ac44d00..1158326a4e 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -10,7 +10,7 @@ fi SUFFIXES="2_12 2_13" JAVA_LIBS="javalibintf javalib" -COMPILER="compiler jUnitPlugin" +FULL_SCALA_LIBS="compiler jUnitPlugin scalalib" JS_LIBS="library irJS linkerInterfaceJS linkerJS testInterface testBridge jUnitRuntime" JVM_LIBS="ir linkerInterface linker testAdapter" SCALA_LIBS="$JS_LIBS $JVM_LIBS" @@ -22,10 +22,10 @@ for p in $JAVA_LIBS; do done $CMD $ARGS -# Publish compiler +# Publish artifacts built with the full Scala version for s in $SUFFIXES; do ARGS="" - for p in $COMPILER; do + for p in $FULL_SCALA_LIBS; do ARGS="$ARGS +$p$s/publishSigned" done $CMD $ARGS From df42b552753fffa0759f0ad62abc72c7002074d4 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Tue, 26 Dec 2023 15:59:52 +0100 Subject: [PATCH 008/298] Version 1.15.0. --- ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala index c97b15f22f..b6476e0c09 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala @@ -17,7 +17,7 @@ import java.util.concurrent.ConcurrentHashMap import scala.util.matching.Regex object ScalaJSVersions extends VersionChecks( - current = "1.15.0-SNAPSHOT", + current = "1.15.0", binaryEmitted = "1.13" ) From a6a5597c28d2a01be43e3a240f610ae2724bcb55 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Wed, 27 Dec 2023 16:38:21 +0100 Subject: [PATCH 009/298] Towards 1.15.1. --- ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala | 2 +- project/Build.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala index b6476e0c09..8dfbb764e2 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala @@ -17,7 +17,7 @@ import java.util.concurrent.ConcurrentHashMap import scala.util.matching.Regex object ScalaJSVersions extends VersionChecks( - current = "1.15.0", + current = "1.15.1-SNAPSHOT", binaryEmitted = "1.13" ) diff --git a/project/Build.scala b/project/Build.scala index 8deb00803d..93209c3ddb 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -338,7 +338,7 @@ object Build { val previousVersions = List("1.0.0", "1.0.1", "1.1.0", "1.1.1", "1.2.0", "1.3.0", "1.3.1", "1.4.0", "1.5.0", "1.5.1", "1.6.0", "1.7.0", "1.7.1", "1.8.0", "1.9.0", "1.10.0", "1.10.1", "1.11.0", "1.12.0", "1.13.0", - "1.13.1", "1.13.2", "1.14.0") + "1.13.1", "1.13.2", "1.14.0", "1.15.0") val previousVersion = previousVersions.last val previousBinaryCrossVersion = CrossVersion.binaryWith("sjs1_", "") From 102cae69d8b5fbbfb14a16eabbdbdd6aa1742242 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 28 Dec 2023 13:58:49 +0100 Subject: [PATCH 010/298] Add "create a release" to release steps --- RELEASING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASING.md b/RELEASING.md index 60965a93fa..b143e9a93e 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -26,6 +26,7 @@ 1. When merging the release announcement PR (after proper review): - Update the latest/ URLs (use `~/setlatestapi.sh ` on webserver) + - Create a release on the core scala-js repository. - Announce on Twitter using the @scala_js account - Announce on [Gitter](https://gitter.im/scala-js/scala-js) - Cross-post as an Announcement in Scala Users ([example][7]) From 3572a4b53b20f5d11964cef0e95852d184ca1d52 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Tue, 2 Jan 2024 14:06:44 +0100 Subject: [PATCH 011/298] Test that we can link twice on the same linker with GCC Ensures that we catch issues like this: https://github.com/scala-js/scala-js/pull/4917#issuecomment-1873311523 --- .../org/scalajs/linker/GCCLinkerTest.scala | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/linker/jvm/src/test/scala/org/scalajs/linker/GCCLinkerTest.scala b/linker/jvm/src/test/scala/org/scalajs/linker/GCCLinkerTest.scala index a399e4588b..1872712651 100644 --- a/linker/jvm/src/test/scala/org/scalajs/linker/GCCLinkerTest.scala +++ b/linker/jvm/src/test/scala/org/scalajs/linker/GCCLinkerTest.scala @@ -16,9 +16,13 @@ import org.junit.Test import org.scalajs.junit.async._ +import org.scalajs.logging._ + import org.scalajs.linker.interface.StandardConfig +import org.scalajs.linker.testutils.{MemClassDefIRFile, TestIRRepo} import org.scalajs.linker.testutils.LinkingUtils._ +import org.scalajs.linker.testutils.TestIRBuilder._ class GCCLinkerTest { import scala.concurrent.ExecutionContext.Implicits.global @@ -30,4 +34,30 @@ class GCCLinkerTest { */ testLink(Nil, Nil, config = StandardConfig().withClosureCompiler(true)) } + + @Test + def linkIncrementalSmoke(): AsyncResult = await { + /* Check that linking twice works. GCC trees are highly mutable, so if we + * (re-)use them wrongly over multiple runs, things can fail unexpectedly. + * + * We change something about the code in the second run to force the linker + * to actually re-run. + */ + def classDef(text: String) = + MemClassDefIRFile(mainTestClassDef(consoleLog(str(text)))) + + val moduleInitializers = MainTestModuleInitializers + + val config = StandardConfig().withCheckIR(true).withClosureCompiler(true) + val linker = StandardImpl.linker(config) + + val output = MemOutputDirectory() + val logger = new ScalaConsoleLogger(Level.Error) + + for { + lib <- TestIRRepo.minilib + _ <- linker.link(lib :+ classDef("test 1"), moduleInitializers, output, logger) + _ <- linker.link(lib :+ classDef("test 2"), moduleInitializers, output, logger) + } yield () + } } From e55b62448b4508a13a2f28e9b0dbc4922931d8d2 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 28 Dec 2023 16:32:42 +0100 Subject: [PATCH 012/298] Basic tests for javascript.Printers We're going to do modifications, so we add some tests. --- .../backend/javascript/PrintersTest.scala | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala diff --git a/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala new file mode 100644 index 0000000000..c43b1a28e9 --- /dev/null +++ b/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala @@ -0,0 +1,164 @@ +/* + * 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.backend.javascript + +import scala.language.implicitConversions + +import java.nio.charset.StandardCharsets + +import org.junit.Test +import org.junit.Assert._ + +import org.scalajs.ir + +import Trees._ + +class PrintersTest { + + private implicit val pos: ir.Position = ir.Position.NoPosition + + private implicit def str2ident(name: String): Ident = + Ident(name, ir.OriginalName.NoOriginalName) + + private def assertPrintEquals(expected: String, tree: Tree): Unit = { + val out = new ByteArrayWriter + val printer = new Printers.JSTreePrinter(out) + printer.printTopLevelTree(tree) + assertEquals(expected.stripMargin.trim + "\n", + new String(out.toByteArray(), StandardCharsets.UTF_8)) + } + + @Test def printFunctionDef(): Unit = { + assertPrintEquals( + """ + |function test() { + | const x = 2; + | return x + |} + """, + FunctionDef("test", Nil, None, Block( + Let("x", mutable = false, Some(IntLiteral(2))), + Return(VarRef("x")))) + ) + + assertPrintEquals( + """ + |function test() { + | /**/ + |} + """, + FunctionDef("test", Nil, None, Skip()) + ) + } + + @Test def printClassDef(): Unit = { + assertPrintEquals( + """ + |class MyClass extends foo.Other { + |} + """, + ClassDef(Some("MyClass"), Some(DotSelect(VarRef("foo"), "Other")), Nil) + ) + + assertPrintEquals( + """ + |class MyClass { + | foo() { + | /**/ + | }; + | get a() { + | return 1 + | }; + | set a(x) { + | /**/ + | }; + |} + """, + ClassDef(Some("MyClass"), None, List( + MethodDef(false, "foo", Nil, None, Skip()), + GetterDef(false, "a", Return(IntLiteral(1))), + SetterDef(false, "a", ParamDef("x"), Skip()) + )) + ) + } + + @Test def printDocComment(): Unit = { + assertPrintEquals( + """ + | /** test */ + """, + DocComment("test") + ) + } + + @Test def printFor(): Unit = { + assertPrintEquals( + """ + |for (let x = 1; (x < 15); x = (x + 1)) { + | /**/ + |}; + """, + For(Let("x", true, Some(IntLiteral(1))), + BinaryOp(ir.Trees.JSBinaryOp.<, VarRef("x"), IntLiteral(15)), + Assign(VarRef("x"), BinaryOp(ir.Trees.JSBinaryOp.+, VarRef("x"), IntLiteral(1))), + Skip()) + ) + } + + @Test def printForIn(): Unit = { + assertPrintEquals( + """ + |for (var x in foo) { + | /**/ + |}; + """, + ForIn(VarDef("x", None), VarRef("foo"), Skip()) + ) + } + + @Test def printIf(): Unit = { + assertPrintEquals( + """ + |if (false) { + | 1 + |}; + """, + If(BooleanLiteral(false), IntLiteral(1), Skip()) + ) + + assertPrintEquals( + """ + |if (false) { + | 1 + |} else { + | 2 + |}; + """, + If(BooleanLiteral(false), IntLiteral(1), IntLiteral(2)) + ) + + assertPrintEquals( + """ + |if (false) { + | 1 + |} else if (true) { + | 2 + |} else { + | 3 + |}; + """, + If(BooleanLiteral(false), IntLiteral(1), + If(BooleanLiteral(true), IntLiteral(2), IntLiteral(3))) + ) + } +} From c2ed2bb42f4dbe7e401c9b4ee130b251fe764aad Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Mon, 25 Dec 2023 20:20:25 +0100 Subject: [PATCH 013/298] Print semicolon as part of statement in javascript Printer This is necesary for a subsequent change, where we pre-print statements into byte buffers. When we later assemble the statement into the surrounding block, we do not have type information anymore (and hence cannot judge whether a semicolon is necessary). Note that this now prints a semicolon after the last statement in a block (which we didn't previously). This does increase fastopt size (somewhat artificially), but more closely corresponds: - to what a human would write - the ECMAScript spec (e.g. https://tc39.es/ecma262/#sec-expression-statement) --- .../linker/backend/javascript/Printers.scala | 66 +++++++++++++------ .../org/scalajs/linker/LibrarySizeTest.scala | 4 +- .../backend/javascript/PrintersTest.scala | 32 ++++----- project/Build.scala | 4 +- 4 files changed, 67 insertions(+), 39 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala index 2df5acc9f9..e2c57c92eb 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala @@ -73,17 +73,10 @@ object Printers { } case _ => printStat(tree) - if (shouldPrintSepAfterTree(tree)) - print(';') println() } } - protected def shouldPrintSepAfterTree(tree: Tree): Boolean = tree match { - case _:DocComment | _:FunctionDef | _:ClassDef => false - case _ => true - } - protected def printRow(ts: List[Tree], start: Char, end: Char): Unit = { print(start) var rest = ts @@ -105,11 +98,8 @@ object Printers { val x = rest.head rest = rest.tail printStat(x) - if (rest.nonEmpty) { - if (shouldPrintSepAfterTree(x)) - print(';') + if (rest.nonEmpty) println() - } } case _ => @@ -146,6 +136,11 @@ object Printers { printTree(tree, isStat = false) def printTree(tree: Tree, isStat: Boolean): Unit = { + def printSeparatorIfStat() = { + if (isStat) + print(';') + } + tree match { // Comments @@ -178,6 +173,8 @@ object Printers { print(" = ") print(rhs) } + // VarDef is an "expr" in a "For" / "ForIn" tree + printSeparatorIfStat() case Let(ident, mutable, optRhs) => print(if (mutable) "let " else "const ") @@ -186,6 +183,8 @@ object Printers { print(" = ") print(rhs) } + // Let is an "expr" in a "For" / "ForIn" tree + printSeparatorIfStat() case ParamDef(ident) => print(ident) @@ -210,10 +209,12 @@ object Printers { print(lhs) print(" = ") print(rhs) + printSeparatorIfStat() case Return(expr) => print("return ") print(expr) + print(';') case If(cond, thenp, elsep) => if (isStat) { @@ -306,19 +307,22 @@ object Printers { case Throw(expr) => print("throw ") print(expr) + print(';') case Break(label) => - if (label.isEmpty) print("break") + if (label.isEmpty) print("break;") else { print("break ") print(label.get) + print(';') } case Continue(label) => - if (label.isEmpty) print("continue") + if (label.isEmpty) print("continue;") else { print("continue ") print(label.get) + print(';') } case Switch(selector, cases, default) => @@ -354,7 +358,7 @@ object Printers { print('}') case Debugger() => - print("debugger") + print("debugger;") // Expressions @@ -375,6 +379,7 @@ object Printers { print(')') } printArgs(args) + printSeparatorIfStat() case DotSelect(qualifier, item) => qualifier match { @@ -387,27 +392,33 @@ object Printers { } print(".") print(item) + printSeparatorIfStat() case BracketSelect(qualifier, item) => print(qualifier) print('[') print(item) print(']') + printSeparatorIfStat() case Apply(fun, args) => print(fun) printArgs(args) + printSeparatorIfStat() case ImportCall(arg) => print("import(") print(arg) print(')') + printSeparatorIfStat() case NewTarget() => print("new.target") + printSeparatorIfStat() case ImportMeta() => print("import.meta") + printSeparatorIfStat() case Spread(items) => print("...") @@ -416,6 +427,7 @@ object Printers { case Delete(prop) => print("delete ") print(prop) + printSeparatorIfStat() case UnaryOp(op, lhs) => import ir.Trees.JSUnaryOp._ @@ -433,6 +445,7 @@ object Printers { } print(lhs) print(')') + printSeparatorIfStat() case IncDec(prefix, inc, arg) => val op = if (inc) "++" else "--" @@ -443,6 +456,7 @@ object Printers { if (!prefix) print(op) print(')') + printSeparatorIfStat() case BinaryOp(op, lhs, rhs) => import ir.Trees.JSBinaryOp._ @@ -482,13 +496,15 @@ object Printers { print(' ') print(rhs) print(')') + printSeparatorIfStat() case ArrayConstr(items) => printRow(items, '[', ']') + printSeparatorIfStat() case ObjectConstr(Nil) => if (isStat) - print("({})") // force expression position for the object literal + print("({});") // force expression position for the object literal else print("{}") @@ -514,18 +530,21 @@ object Printers { println() print('}') if (isStat) - print(')') + print(");") // Literals case Undefined() => print("(void 0)") + printSeparatorIfStat() case Null() => print("null") + printSeparatorIfStat() case BooleanLiteral(value) => print(if (value) "true" else "false") + printSeparatorIfStat() case IntLiteral(value) => if (value >= 0) { @@ -535,6 +554,7 @@ object Printers { print(value.toString) print(')') } + printSeparatorIfStat() case DoubleLiteral(value) => if (value == 0 && 1 / value < 0) { @@ -546,11 +566,13 @@ object Printers { print(value.toString) print(')') } + printSeparatorIfStat() case StringLiteral(value) => print('\"') printEscapeJS(value) print('\"') + printSeparatorIfStat() case BigIntLiteral(value) => if (value >= 0) { @@ -561,14 +583,17 @@ object Printers { print(value.toString) print("n)") } + printSeparatorIfStat() // Atomic expressions case VarRef(ident) => print(ident) + printSeparatorIfStat() case This() => print("this") + printSeparatorIfStat() case Function(arrow, args, restParam, body) => if (arrow) { @@ -595,6 +620,7 @@ object Printers { printBlock(body) print(')') } + printSeparatorIfStat() // Named function definition @@ -624,8 +650,7 @@ object Printers { var rest = members while (rest.nonEmpty) { println() - print(rest.head) - print(';') + printStat(rest.head) rest = rest.tail } undent(); println(); print('}') @@ -677,12 +702,14 @@ object Printers { } print(" } from ") print(from: Tree) + print(';') case ImportNamespace(binding, from) => print("import * as ") print(binding) print(" from ") print(from: Tree) + print(';') case Export(bindings) => print("export { ") @@ -699,7 +726,7 @@ object Printers { print(binding._2) rest = rest.tail } - print(" }") + print(" };") case ExportImport(bindings, from) => print("export { ") @@ -718,6 +745,7 @@ object Printers { } print(" } from ") print(from: Tree) + print(';') case _ => throw new IllegalArgumentException( 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 a64f546d68..6695edfb35 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -70,8 +70,8 @@ class LibrarySizeTest { ) testLinkedSizes( - expectedFastLinkSize = 150031, - expectedFullLinkSizeWithoutClosure = 130655, + expectedFastLinkSize = 150534, + expectedFullLinkSizeWithoutClosure = 131079, expectedFullLinkSizeWithClosure = 21394, classDefs, moduleInitializers = MainTestModuleInitializers diff --git a/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala index c43b1a28e9..621910f9a7 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala @@ -43,7 +43,7 @@ class PrintersTest { """ |function test() { | const x = 2; - | return x + | return x; |} """, FunctionDef("test", Nil, None, Block( @@ -75,13 +75,13 @@ class PrintersTest { |class MyClass { | foo() { | /**/ - | }; + | } | get a() { - | return 1 - | }; + | return 1; + | } | set a(x) { | /**/ - | }; + | } |} """, ClassDef(Some("MyClass"), None, List( @@ -106,7 +106,7 @@ class PrintersTest { """ |for (let x = 1; (x < 15); x = (x + 1)) { | /**/ - |}; + |} """, For(Let("x", true, Some(IntLiteral(1))), BinaryOp(ir.Trees.JSBinaryOp.<, VarRef("x"), IntLiteral(15)), @@ -120,7 +120,7 @@ class PrintersTest { """ |for (var x in foo) { | /**/ - |}; + |} """, ForIn(VarDef("x", None), VarRef("foo"), Skip()) ) @@ -130,8 +130,8 @@ class PrintersTest { assertPrintEquals( """ |if (false) { - | 1 - |}; + | 1; + |} """, If(BooleanLiteral(false), IntLiteral(1), Skip()) ) @@ -139,10 +139,10 @@ class PrintersTest { assertPrintEquals( """ |if (false) { - | 1 + | 1; |} else { - | 2 - |}; + | 2; + |} """, If(BooleanLiteral(false), IntLiteral(1), IntLiteral(2)) ) @@ -150,12 +150,12 @@ class PrintersTest { assertPrintEquals( """ |if (false) { - | 1 + | 1; |} else if (true) { - | 2 + | 2; |} else { - | 3 - |}; + | 3; + |} """, If(BooleanLiteral(false), IntLiteral(1), If(BooleanLiteral(true), IntLiteral(2), IntLiteral(3))) diff --git a/project/Build.scala b/project/Build.scala index 93209c3ddb..da13fdd781 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1967,7 +1967,7 @@ object Build { scalaVersion.value match { case `default212Version` => Some(ExpectedSizes( - fastLink = 772000 to 773000, + fastLink = 775000 to 776000, fullLink = 145000 to 146000, fastLinkGz = 91000 to 92000, fullLinkGz = 35000 to 36000, @@ -1975,7 +1975,7 @@ object Build { case `default213Version` => Some(ExpectedSizes( - fastLink = 480000 to 481000, + fastLink = 481000 to 483000, fullLink = 102000 to 103000, fastLinkGz = 62000 to 63000, fullLinkGz = 27000 to 28000, From 197af9a726414fc68345920fadc5bd77c419f0fd Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 28 Dec 2023 09:13:59 +0100 Subject: [PATCH 014/298] Do not print skip's in blocks --- .../scalajs/linker/backend/javascript/Printers.scala | 12 +++++++----- .../scala/org/scalajs/linker/LibrarySizeTest.scala | 4 ++-- .../linker/backend/javascript/PrintersTest.scala | 5 ----- project/Build.scala | 6 +++--- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala index e2c57c92eb..adf58ee214 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala @@ -90,19 +90,21 @@ object Printers { } protected def printBlock(tree: Tree): Unit = { - print('{'); indent(); println() + print('{'); indent(); tree match { + case Skip() => + // do not print anything + case tree: Block => var rest = tree.stats while (rest.nonEmpty) { - val x = rest.head + println() + printStat(rest.head) rest = rest.tail - printStat(x) - if (rest.nonEmpty) - println() } case _ => + println() printStat(tree) } undent(); println(); print('}') 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 6695edfb35..9dcc074647 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -70,8 +70,8 @@ class LibrarySizeTest { ) testLinkedSizes( - expectedFastLinkSize = 150534, - expectedFullLinkSizeWithoutClosure = 131079, + expectedFastLinkSize = 150339, + expectedFullLinkSizeWithoutClosure = 130884, expectedFullLinkSizeWithClosure = 21394, classDefs, moduleInitializers = MainTestModuleInitializers diff --git a/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala index 621910f9a7..2397717164 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala @@ -54,7 +54,6 @@ class PrintersTest { assertPrintEquals( """ |function test() { - | /**/ |} """, FunctionDef("test", Nil, None, Skip()) @@ -74,13 +73,11 @@ class PrintersTest { """ |class MyClass { | foo() { - | /**/ | } | get a() { | return 1; | } | set a(x) { - | /**/ | } |} """, @@ -105,7 +102,6 @@ class PrintersTest { assertPrintEquals( """ |for (let x = 1; (x < 15); x = (x + 1)) { - | /**/ |} """, For(Let("x", true, Some(IntLiteral(1))), @@ -119,7 +115,6 @@ class PrintersTest { assertPrintEquals( """ |for (var x in foo) { - | /**/ |} """, ForIn(VarDef("x", None), VarRef("foo"), Skip()) diff --git a/project/Build.scala b/project/Build.scala index da13fdd781..758975e8f0 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1967,15 +1967,15 @@ object Build { scalaVersion.value match { case `default212Version` => Some(ExpectedSizes( - fastLink = 775000 to 776000, + fastLink = 770000 to 771000, fullLink = 145000 to 146000, - fastLinkGz = 91000 to 92000, + fastLinkGz = 90000 to 91000, fullLinkGz = 35000 to 36000, )) case `default213Version` => Some(ExpectedSizes( - fastLink = 481000 to 483000, + fastLink = 479000 to 480000, fullLink = 102000 to 103000, fastLinkGz = 62000 to 63000, fullLinkGz = 27000 to 28000, From 7c3797d02d7194a051bc51c98299f09650134330 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 28 Dec 2023 18:22:15 +0100 Subject: [PATCH 015/298] Do not abuse a block to group instance tests This was forgotten in 1982a6b631a5b37ebbd4fe61655a45ad9b62bf93. --- .../org/scalajs/linker/backend/emitter/ClassEmitter.scala | 4 ++-- .../scala/org/scalajs/linker/backend/emitter/Emitter.scala | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 0701d2fd84..ca58616785 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 @@ -683,12 +683,12 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { def genInstanceTests(className: ClassName, kind: ClassKind)( implicit moduleContext: ModuleContext, - globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[js.Tree] = { + globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[List[js.Tree]] = { for { single <- genSingleInstanceTests(className, kind) array <- genArrayInstanceTests(className) } yield { - js.Block(single ::: array) + single ::: array } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala index d5b23e4525..d89de7ae4d 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala @@ -595,7 +595,7 @@ final class Emitter(config: Emitter.Config) { */ if (classEmitter.needInstanceTests(linkedClass)(classCache)) { - main += extractWithGlobals(classTreeCache.instanceTests.getOrElseUpdate( + main ++= extractWithGlobals(classTreeCache.instanceTests.getOrElseUpdate( classEmitter.genInstanceTests(className, kind)(moduleContext, classCache, linkedClass.pos))) } @@ -1035,7 +1035,7 @@ object Emitter { private final class DesugaredClassCache { val privateJSFields = new OneTimeCache[WithGlobals[List[js.Tree]]] - val instanceTests = new OneTimeCache[WithGlobals[js.Tree]] + val instanceTests = new OneTimeCache[WithGlobals[List[js.Tree]]] val typeData = new OneTimeCache[WithGlobals[List[js.Tree]]] val setTypeData = new OneTimeCache[js.Tree] val moduleAccessor = new OneTimeCache[WithGlobals[List[js.Tree]]] From bdea4d7ddb6ac6917bae270903c803ff5fdb3166 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 29 Dec 2023 15:55:20 +0100 Subject: [PATCH 016/298] Do not abuse a block to group const field export statements This was forgotten in 1982a6b631a5b37ebbd4fe61655a45ad9b62bf93. --- .../linker/backend/emitter/ClassEmitter.scala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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 ca58616785..144672a471 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 @@ -1028,16 +1028,16 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { case e: TopLevelMethodExportDef => genTopLevelMethodExportDef(e) case e: TopLevelFieldExportDef => - genTopLevelFieldExportDef(topLevelExport.owningClass, e) + genTopLevelFieldExportDef(topLevelExport.owningClass, e).map(_ :: Nil) } } - WithGlobals.list(exportsWithGlobals) + WithGlobals.flatten(exportsWithGlobals) } private def genTopLevelMethodExportDef(tree: TopLevelMethodExportDef)( implicit moduleContext: ModuleContext, - globalKnowledge: GlobalKnowledge): WithGlobals[js.Tree] = { + globalKnowledge: GlobalKnowledge): WithGlobals[List[js.Tree]] = { import TreeDSL._ val JSMethodDef(flags, StringLiteral(exportName), args, restParam, body) = @@ -1056,22 +1056,22 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { private def genConstValueExportDef(exportName: String, exportedValue: js.Tree)( - implicit pos: Position): WithGlobals[js.Tree] = { + implicit pos: Position): WithGlobals[List[js.Tree]] = { moduleKind match { case ModuleKind.NoModule => - genAssignToNoModuleExportVar(exportName, exportedValue) + genAssignToNoModuleExportVar(exportName, exportedValue).map(_ :: Nil) case ModuleKind.ESModule => val field = fileLevelVar(VarField.e, exportName) val let = js.Let(field.ident, mutable = true, Some(exportedValue)) val exportStat = js.Export((field.ident -> js.ExportName(exportName)) :: Nil) - WithGlobals(js.Block(let, exportStat)) + WithGlobals(List(let, exportStat)) case ModuleKind.CommonJSModule => globalRef("exports").map { exportsVarRef => js.Assign( genBracketSelect(exportsVarRef, js.StringLiteral(exportName)), - exportedValue) + exportedValue) :: Nil } } } From 135932afa2601f9f2b914c1fdea806f4d3903c25 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Thu, 28 Dec 2023 18:24:23 +0100 Subject: [PATCH 017/298] Simplify printTopLevelTree in JS printer Because it does not need to flatten blocks anymore, we can simply print it as a statement and add a trailing newline. --- .../linker/backend/javascript/Printers.scala | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala index adf58ee214..c50426a5d8 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala @@ -62,19 +62,8 @@ object Printers { } def printTopLevelTree(tree: Tree): Unit = { - tree match { - case Skip() => - // do not print anything - case tree: Block => - var rest = tree.stats - while (rest.nonEmpty) { - printTopLevelTree(rest.head) - rest = rest.tail - } - case _ => - printStat(tree) - println() - } + printStat(tree) + println() } protected def printRow(ts: List[Tree], start: Char, end: Char): Unit = { From de0d65c6df5a51a8b024363f1382723b245b1c20 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 29 Dec 2023 16:14:18 +0100 Subject: [PATCH 018/298] Restrict visibility of JS Printer methods They were unnecessarily protected. --- .../linker/backend/javascript/Printers.scala | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala index c50426a5d8..035f21ef0a 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala @@ -66,7 +66,7 @@ object Printers { println() } - protected def printRow(ts: List[Tree], start: Char, end: Char): Unit = { + private def printRow(ts: List[Tree], start: Char, end: Char): Unit = { print(start) var rest = ts while (rest.nonEmpty) { @@ -78,7 +78,7 @@ object Printers { print(end) } - protected def printBlock(tree: Tree): Unit = { + private def printBlock(tree: Tree): Unit = { print('{'); indent(); tree match { case Skip() => @@ -99,7 +99,7 @@ object Printers { undent(); println(); print('}') } - protected def printSig(args: List[ParamDef], restParam: Option[ParamDef]): Unit = { + private def printSig(args: List[ParamDef], restParam: Option[ParamDef]): Unit = { print("(") var rem = args while (rem.nonEmpty) { @@ -117,13 +117,13 @@ object Printers { print(") ") } - protected def printArgs(args: List[Tree]): Unit = + private def printArgs(args: List[Tree]): Unit = printRow(args, '(', ')') - protected def printStat(tree: Tree): Unit = + private def printStat(tree: Tree): Unit = printTree(tree, isStat = true) - protected def print(tree: Tree): Unit = + private def print(tree: Tree): Unit = printTree(tree, isStat = false) def printTree(tree: Tree, isStat: Boolean): Unit = { @@ -760,7 +760,7 @@ object Printers { print("]") } - protected def print(exportName: ExportName): Unit = + private def print(exportName: ExportName): Unit = printEscapeJS(exportName.name) /** Prints an ASCII string -- use for syntax strings, not for user strings. */ From bd58bbbb4e2e2cf3fb5fb2c7643f0dc0dd8940e0 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 29 Dec 2023 16:16:39 +0100 Subject: [PATCH 019/298] Print trailing newline as part of statement This is necessary so we can partially print substatements more easily (which we'll do in a subsequent PR). It will also allow us to fully remove the concept of "top-level" tree. --- .../linker/backend/javascript/Printers.scala | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala index 035f21ef0a..7d0235bed3 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala @@ -44,6 +44,9 @@ object Printers { protected def println(): Unit = { out.write('\n') + } + + protected def printIndent(): Unit = { val indentArray = this.indentArray val indentMargin = this.indentMargin val bigEnoughIndentArray = @@ -63,7 +66,6 @@ object Printers { def printTopLevelTree(tree: Tree): Unit = { printStat(tree) - println() } private def printRow(ts: List[Tree], start: Char, end: Char): Unit = { @@ -79,7 +81,7 @@ object Printers { } private def printBlock(tree: Tree): Unit = { - print('{'); indent(); + print('{'); indent(); println() tree match { case Skip() => // do not print anything @@ -87,16 +89,14 @@ object Printers { case tree: Block => var rest = tree.stats while (rest.nonEmpty) { - println() printStat(rest.head) rest = rest.tail } case _ => - println() printStat(tree) } - undent(); println(); print('}') + undent(); printIndent(); print('}') } private def printSig(args: List[ParamDef], restParam: Option[ParamDef]): Unit = { @@ -120,12 +120,22 @@ object Printers { private def printArgs(args: List[Tree]): Unit = printRow(args, '(', ')') - private def printStat(tree: Tree): Unit = + /** Prints a stat including leading indent and trailing newline. */ + private def printStat(tree: Tree): Unit = { + printIndent() printTree(tree, isStat = true) + println() + } private def print(tree: Tree): Unit = printTree(tree, isStat = false) + /** Print the "meat" of a tree. + * + * Even if it is a stat: + * - No leading indent. + * - No trailing newline. + */ def printTree(tree: Tree, isStat: Boolean): Unit = { def printSeparatorIfStat() = { if (isStat) @@ -144,12 +154,12 @@ object Printers { } else { print("/** ") print(lines.head) - println() + println(); printIndent() var rest = lines.tail while (rest.nonEmpty) { print(" * ") print(rest.head) - println() + println(); printIndent() rest = rest.tail } print(" */") @@ -326,7 +336,7 @@ object Printers { while (rest.nonEmpty) { val next = rest.head rest = rest.tail - println() + println(); printIndent() print("case ") print(next._1) print(':') @@ -339,13 +349,13 @@ object Printers { default match { case Skip() => case _ => - println() + println(); printIndent() print("default: ") printBlock(default) } undent() - println() + println(); printIndent() print('}') case Debugger() => @@ -509,16 +519,17 @@ object Printers { while (rest.nonEmpty) { val x = rest.head rest = rest.tail + printIndent() print(x._1) print(": ") print(x._2) if (rest.nonEmpty) { print(',') - println() } + println() } undent() - println() + printIndent() print('}') if (isStat) print(");") @@ -637,14 +648,13 @@ object Printers { print(" extends ") print(optParentClass.get) } - print(" {"); indent() + print(" {"); indent(); println() var rest = members while (rest.nonEmpty) { - println() printStat(rest.head) rest = rest.tail } - undent(); println(); print('}') + undent(); printIndent(); print('}') case MethodDef(static, name, params, restParam, body) => if (static) @@ -801,6 +811,13 @@ object Printers { override protected def println(): Unit = { super.println() sourceMap.nextLine() + column = 0 + } + + override protected def printIndent(): Unit = { + assert(column == 0) + + super.printIndent() column = this.getIndentMargin() } From cae909ad8d661eb6af63a6946236a5b63b9181d5 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 29 Dec 2023 16:21:36 +0100 Subject: [PATCH 020/298] Remove printTopLevelTree and replace it with printStat --- .../org/scalajs/linker/backend/BasicLinkerBackend.scala | 4 ++-- .../org/scalajs/linker/backend/javascript/Printers.scala | 6 +----- .../scalajs/linker/backend/javascript/PrintersTest.scala | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala index a1238e7433..564ebbb99c 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala @@ -283,7 +283,7 @@ private object BasicLinkerBackend { val jsCodeWriter = new ByteArrayWriter() val printer = new Printers.JSTreePrinter(jsCodeWriter) - printer.printTopLevelTree(tree) + printer.printStat(tree) new PrintedTree(jsCodeWriter.toByteArray(), SourceMapWriter.Fragment.Empty) } @@ -321,7 +321,7 @@ private object BasicLinkerBackend { val smFragmentBuilder = new SourceMapWriter.FragmentBuilder() val printer = new Printers.JSTreePrinterWithSourceMap(jsCodeWriter, smFragmentBuilder) - printer.printTopLevelTree(tree) + printer.printStat(tree) smFragmentBuilder.complete() new PrintedTree(jsCodeWriter.toByteArray(), smFragmentBuilder.result()) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala index 7d0235bed3..9690946e69 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala @@ -64,10 +64,6 @@ object Printers { newIndentArray } - def printTopLevelTree(tree: Tree): Unit = { - printStat(tree) - } - private def printRow(ts: List[Tree], start: Char, end: Char): Unit = { print(start) var rest = ts @@ -121,7 +117,7 @@ object Printers { printRow(args, '(', ')') /** Prints a stat including leading indent and trailing newline. */ - private def printStat(tree: Tree): Unit = { + final def printStat(tree: Tree): Unit = { printIndent() printTree(tree, isStat = true) println() diff --git a/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala index 2397717164..316f2c3907 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala @@ -33,7 +33,7 @@ class PrintersTest { private def assertPrintEquals(expected: String, tree: Tree): Unit = { val out = new ByteArrayWriter val printer = new Printers.JSTreePrinter(out) - printer.printTopLevelTree(tree) + printer.printStat(tree) assertEquals(expected.stripMargin.trim + "\n", new String(out.toByteArray(), StandardCharsets.UTF_8)) } From c12b25368dee2d5871ba1666277fa387f5b8f8e2 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sat, 4 Nov 2023 13:56:54 +0100 Subject: [PATCH 021/298] Only pass isJSClass to ClassEmitter (instead of entire ClassKind) This allows us to invalidate generated exported members less often (and in following commits, also class members). --- .../main/scala/org/scalajs/ir/Version.scala | 10 ++++++ .../linker/backend/emitter/ClassEmitter.scala | 33 +++++++++---------- .../linker/backend/emitter/Emitter.scala | 24 +++++++++----- 3 files changed, 41 insertions(+), 26 deletions(-) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Version.scala b/ir/shared/src/main/scala/org/scalajs/ir/Version.scala index f30be5f7ee..0228d63c86 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Version.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Version.scala @@ -80,6 +80,16 @@ object Version { def fromBytes(bytes: Array[Byte]): Version = make(Type.Ephemeral, bytes) + /** Create a non-hash version from a Byte. + * + * Strictly equivalent to (but potentially more efficient): + * {{{ + * fromBytes(Array[Byte](i)) + * }}} + */ + def fromByte(i: Byte): Version = + new Version(Array(Type.Ephemeral, i)) + /** Create a non-hash version from an Int. * * Strictly equivalent to (but potentially more efficient): 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 144672a471..34b69507ea 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 @@ -43,7 +43,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { import nameGen._ import varGen._ - def buildClass(className: ClassName, kind: ClassKind, jsClassCaptures: Option[List[ParamDef]], + def buildClass(className: ClassName, isJSClass: Boolean, jsClassCaptures: Option[List[ParamDef]], hasClassInitializer: Boolean, superClass: Option[ClassIdent], jsSuperClass: Option[Tree], useESClass: Boolean, ctorDefs: List[js.Tree], memberDefs: List[js.MethodDef], exportedDefs: List[js.Tree])( @@ -55,7 +55,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { def allES5Defs(classVar: js.Tree) = WithGlobals(ctorDefs ::: assignES5ClassMembers(classVar, memberDefs) ::: exportedDefs) - if (!kind.isJSClass) { + if (!isJSClass) { assert(jsSuperClass.isEmpty, className) if (useESClass) { @@ -534,7 +534,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } /** Generates a JS method. */ - private def genJSMethod(className: ClassName, kind: ClassKind, useESClass: Boolean, + private def genJSMethod(className: ClassName, isJSClass: Boolean, useESClass: Boolean, method: JSMethodDef)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge): WithGlobals[js.Tree] = { @@ -550,29 +550,29 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { if (useESClass) { js.MethodDef(static = namespace.isStatic, propName, methodFun.args, methodFun.restParam, methodFun.body) } else { - val targetObject = exportTargetES5(className, kind, namespace) + val targetObject = exportTargetES5(className, isJSClass, namespace) js.Assign(genPropSelect(targetObject, propName), methodFun) } } } /** Generates a property. */ - private def genJSProperty(className: ClassName, kind: ClassKind, useESClass: Boolean, + private def genJSProperty(className: ClassName, isJSClass: Boolean, useESClass: Boolean, property: JSPropertyDef)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge): WithGlobals[List[js.Tree]] = { if (useESClass) genJSPropertyES6(className, property) else - genJSPropertyES5(className, kind, property).map(_ :: Nil) + genJSPropertyES5(className, isJSClass, property).map(_ :: Nil) } - private def genJSPropertyES5(className: ClassName, kind: ClassKind, property: JSPropertyDef)( + private def genJSPropertyES5(className: ClassName, isJSClass: Boolean, property: JSPropertyDef)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge): WithGlobals[js.Tree] = { implicit val pos = property.pos - val targetObject = exportTargetES5(className, kind, property.flags.namespace) + val targetObject = exportTargetES5(className, isJSClass, property.flags.namespace) // optional getter definition val optGetterWithGlobals = property.getterBody map { body => @@ -627,13 +627,13 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } } - private def exportTargetES5(className: ClassName, kind: ClassKind, namespace: MemberNamespace)( + private def exportTargetES5(className: ClassName, isJSClass: Boolean, namespace: MemberNamespace)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): js.Tree = { import TreeDSL._ val classVarRef = - if (kind.isJSClass) fileLevelVar(VarField.b, genName(className)) + if (isJSClass) fileLevelVar(VarField.b, genName(className)) else globalVar(VarField.c, className) if (namespace.isStatic) classVarRef @@ -943,16 +943,13 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { globalVar(VarField.c, className).prototype DOT "$classData" := globalVar(VarField.d, className) } - def genModuleAccessor(className: ClassName, kind: ClassKind)( + def genModuleAccessor(className: ClassName, isJSClass: Boolean)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[List[js.Tree]] = { import TreeDSL._ val tpe = ClassType(className) - require(kind.hasModuleAccessor, - s"genModuleAccessor called with non-module class: $className") - val moduleInstance = fileLevelVarIdent(VarField.n, genName(className)) val createModuleInstanceField = genEmptyMutableLet(moduleInstance) @@ -962,7 +959,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val assignModule = { moduleInstanceVar := { - if (kind == ClassKind.JSModuleClass) { + if (isJSClass) { js.New( genNonNativeJSClassConstructor(className), Nil) @@ -1002,12 +999,12 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { createAccessor.map(createModuleInstanceField :: _) } - def genExportedMember(className: ClassName, kind: ClassKind, useESClass: Boolean, member: JSMethodPropDef)( + def genExportedMember(className: ClassName, isJSClass: Boolean, useESClass: Boolean, member: JSMethodPropDef)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge): WithGlobals[List[js.Tree]] = { member match { - case m: JSMethodDef => genJSMethod(className, kind, useESClass, m).map(_ :: Nil) - case p: JSPropertyDef => genJSProperty(className, kind, useESClass, p) + case m: JSMethodDef => genJSMethod(className, isJSClass, useESClass, m).map(_ :: Nil) + case p: JSPropertyDef => genJSProperty(className, isJSClass, useESClass, p) } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala index d89de7ae4d..28c20a4a66 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala @@ -412,8 +412,12 @@ final class Emitter(config: Emitter.Config) { } } + val isJSClass = kind.isJSClass + // Class definition if (linkedClass.hasInstances && kind.isAnyNonNativeClass) { + val isJSClassVersion = Version.fromByte(if (isJSClass) 1 else 0) + /* Is this class compiled as an ECMAScript `class`? * * See JSGen.useClassesForRegularClasses for the rationale here. @@ -430,13 +434,17 @@ final class Emitter(config: Emitter.Config) { * observable change; whereas rewiring Throwable to extend `Error` when * it does not actually directly extend `Object` would break everything, * so we need to be more careful there. + * + * For caching, isJSClassVersion can be used to guard use of `useESClass`: + * it is the only "dynamic" value it depends on. The rest is configuration + * or part of the cache key (ancestors). */ val useESClass = if (jsGen.useClassesForRegularClasses) { assert(jsGen.useClassesForJSClassesAndThrowables) true } else { jsGen.useClassesForJSClassesAndThrowables && - (kind.isJSClass || linkedClass.ancestors.contains(ThrowableClass)) + (isJSClass || linkedClass.ancestors.contains(ThrowableClass)) } // JS constructor @@ -448,7 +456,7 @@ final class Emitter(config: Emitter.Config) { */ val ctorCache = classCache.getConstructorCache() - if (linkedClass.kind.isJSClass) { + if (isJSClass) { assert(linkedInlineableInit.isEmpty) val jsConstructorDef = linkedClass.jsConstructorDef.getOrElse { @@ -533,13 +541,13 @@ final class Emitter(config: Emitter.Config) { (member, idx) <- linkedClass.exportedMembers.zipWithIndex } yield { val memberCache = classCache.getExportedMemberCache(idx) - val version = Version.combine(linkedClass.version, member.version) + val version = Version.combine(isJSClassVersion, member.version) memberCache.getOrElseUpdate(version, classEmitter.genExportedMember( className, // invalidated by overall class cache - kind, // invalidated by class verison - useESClass, // invalidated by class version (combined) - member // invalidated by version (combined) + isJSClass, // invalidated by isJSClassVersion + useESClass, // invalidated by isJSClassVersion + member // invalidated by version )(moduleContext, memberCache)) } @@ -561,7 +569,7 @@ final class Emitter(config: Emitter.Config) { exportedMembers <- WithGlobals.list(exportedMembersWithGlobals) clazz <- classEmitter.buildClass( className, // invalidated by overall class cache (part of ancestors) - linkedClass.kind, // invalidated by class version + isJSClass, // invalidated by class version linkedClass.jsClassCaptures, // invalidated by class version hasClassInitializer, // invalidated by class version (optimizer cannot remove it) linkedClass.superClass, // invalidated by class version @@ -618,7 +626,7 @@ final class Emitter(config: Emitter.Config) { if (linkedClass.kind.hasModuleAccessor && linkedClass.hasInstances) { main ++= extractWithGlobals(classTreeCache.moduleAccessor.getOrElseUpdate( - classEmitter.genModuleAccessor(className, kind)(moduleContext, classCache, linkedClass.pos))) + classEmitter.genModuleAccessor(className, isJSClass)(moduleContext, classCache, linkedClass.pos))) } // Static fields From 81a235d7468c40db12d65d1684b5d31bacc53f6d Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Tue, 2 Jan 2024 12:04:02 +0100 Subject: [PATCH 022/298] Use Version.fromByte in KnowledgeGuardian Now that we have it, we might as well save some memory. --- .../org/scalajs/linker/backend/emitter/KnowledgeGuardian.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 cd3bd4a687..2d7f18b19c 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 @@ -374,7 +374,7 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { */ private def computeFieldDefsVersion(linkedClass: LinkedClass): Version = { val hasAnyJSField = linkedClass.fields.exists(_.isInstanceOf[JSFieldDef]) - val hasAnyJSFieldVersion = Version.fromInt(if (hasAnyJSField) 1 else 0) + val hasAnyJSFieldVersion = Version.fromByte(if (hasAnyJSField) 1 else 0) val scalaFieldNamesVersion = linkedClass.fields.collect { case FieldDef(_, FieldIdent(name), _, _) => Version.fromUTF8String(name.encoded) } From 14144ac402c470cb65063e0d15ac0c2df0bbfea8 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sat, 4 Nov 2023 13:59:25 +0100 Subject: [PATCH 023/298] Move member assignment from buildClass to genMember This simplifies buildClass. Thanks to the special version for isJSClass, this will lead to acceptable invalidations (only if the class kind changes, which should be rare). --- .../linker/backend/emitter/ClassEmitter.scala | 29 ++++++++++--------- .../linker/backend/emitter/CoreJSLib.scala | 9 ++++++ .../linker/backend/emitter/Emitter.scala | 22 ++++++++------ .../linker/backend/emitter/JSGen.scala | 12 -------- 4 files changed, 37 insertions(+), 35 deletions(-) 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 34b69507ea..1a5a965e55 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 @@ -45,16 +45,11 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { def buildClass(className: ClassName, isJSClass: Boolean, jsClassCaptures: Option[List[ParamDef]], hasClassInitializer: Boolean, - superClass: Option[ClassIdent], jsSuperClass: Option[Tree], useESClass: Boolean, ctorDefs: List[js.Tree], - memberDefs: List[js.MethodDef], exportedDefs: List[js.Tree])( + superClass: Option[ClassIdent], jsSuperClass: Option[Tree], useESClass: Boolean, + members: List[js.Tree])( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[List[js.Tree]] = { - def allES6Defs = ctorDefs ::: memberDefs ::: exportedDefs - - def allES5Defs(classVar: js.Tree) = - WithGlobals(ctorDefs ::: assignES5ClassMembers(classVar, memberDefs) ::: exportedDefs) - if (!isJSClass) { assert(jsSuperClass.isEmpty, className) @@ -66,10 +61,10 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } WithGlobals.option(parentVarWithGlobals).flatMap { parentVar => - globalClassDef(VarField.c, className, parentVar, allES6Defs) + globalClassDef(VarField.c, className, parentVar, members) } } else { - allES5Defs(globalVar(VarField.c, className)) + WithGlobals(members) } } else { // Wrap the entire class def in an accessor function @@ -87,10 +82,10 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val entireClassDefWithGlobals = if (useESClass) { genJSSuperCtor(superClass, jsSuperClass).map { jsSuperClass => - List(classValueVar := js.ClassDef(Some(classValueIdent), Some(jsSuperClass), allES6Defs)) + List(classValueVar := js.ClassDef(Some(classValueIdent), Some(jsSuperClass), members)) } } else { - allES5Defs(classValueVar) + WithGlobals(members) } val classDefStatsWithGlobals = for { @@ -470,9 +465,9 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } } - def genMemberMethod(className: ClassName, method: MethodDef)( + def genMemberMethod(className: ClassName, isJSClass: Boolean, useESClass: Boolean, method: MethodDef)( implicit moduleContext: ModuleContext, - globalKnowledge: GlobalKnowledge): WithGlobals[js.MethodDef] = { + globalKnowledge: GlobalKnowledge): WithGlobals[js.Tree] = { assert(method.flags.namespace == MemberNamespace.Public) implicit val pos = method.pos @@ -481,7 +476,13 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { methodFun <- desugarToFunction(className, method.args, method.body.get, method.resultType) } yield { val jsMethodName = genMemberMethodIdent(method.name, method.originalName) - js.MethodDef(static = false, jsMethodName, methodFun.args, methodFun.restParam, methodFun.body) + + if (useESClass) { + js.MethodDef(static = false, jsMethodName, methodFun.args, methodFun.restParam, methodFun.body) + } else { + val targetObject = exportTargetES5(className, isJSClass, MemberNamespace.Public) + js.Assign(genPropSelect(targetObject, jsMethodName), methodFun) + } } } 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 8a04956ad8..def0d13434 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 @@ -2132,6 +2132,15 @@ private[emitter] object CoreJSLib { obj ::: prims } + private def assignES5ClassMembers(classRef: Tree, members: List[MethodDef]): List[Tree] = { + for { + MethodDef(static, name, args, restParam, body) <- members + } yield { + val target = if (static) classRef else classRef.prototype + genPropSelect(target, name) := Function(arrow = false, args, restParam, body) + } + } + private def defineFunction(name: VarField, args: List[ParamDef], body: Tree): List[Tree] = extractWithGlobals(globalFunctionDef(name, CoreVar, args, None, body)) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala index 28c20a4a66..ea5e4b09bd 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala @@ -532,8 +532,14 @@ final class Emitter(config: Emitter.Config) { val methodCache = classCache.getMemberMethodCache(method.methodName) - methodCache.getOrElseUpdate(method.version, - classEmitter.genMemberMethod(className, method)(moduleContext, methodCache)) + val version = Version.combine(isJSClassVersion, method.version) + methodCache.getOrElseUpdate(version, + classEmitter.genMemberMethod( + className, // invalidated by overall class cache + isJSClass, // invalidated by isJSClassVersion + useESClass, // invalidated by isJSClassVersion + method // invalidated by method.version + )(moduleContext, methodCache)) } // Exported Members @@ -575,9 +581,7 @@ final class Emitter(config: Emitter.Config) { linkedClass.superClass, // invalidated by class version linkedClass.jsSuperClass, // invalidated by class version useESClass, // invalidated by class version (depends on kind, config and ancestry only) - ctor, // invalidated directly - memberMethods, // invalidated directly - exportedMembers.flatten // invalidated directly + ctor ::: memberMethods ::: exportedMembers.flatten // all 3 invalidated directly )(moduleContext, fullClassCache, linkedClass.pos) // pos invalidated by class version } yield { clazz @@ -770,7 +774,7 @@ final class Emitter(config: Emitter.Config) { Array.fill(MemberNamespace.Count)(mutable.Map.empty[MethodName, MethodCache[List[js.Tree]]]) private[this] val _memberMethodCache = - mutable.Map.empty[MethodName, MethodCache[js.MethodDef]] + mutable.Map.empty[MethodName, MethodCache[js.Tree]] private[this] var _constructorCache: Option[MethodCache[List[js.Tree]]] = None @@ -810,7 +814,7 @@ final class Emitter(config: Emitter.Config) { } def getMemberMethodCache( - methodName: MethodName): MethodCache[js.MethodDef] = { + methodName: MethodName): MethodCache[js.Tree] = { _memberMethodCache.getOrElseUpdate(methodName, new MethodCache) } @@ -897,7 +901,7 @@ final class Emitter(config: Emitter.Config) { private[this] var _tree: WithGlobals[List[js.Tree]] = null private[this] var _lastVersion: Version = Version.Unversioned private[this] var _lastCtor: WithGlobals[List[js.Tree]] = null - private[this] var _lastMemberMethods: List[WithGlobals[js.MethodDef]] = null + private[this] var _lastMemberMethods: List[WithGlobals[js.Tree]] = null private[this] var _lastExportedMembers: List[WithGlobals[List[js.Tree]]] = null private[this] var _cacheUsed = false @@ -913,7 +917,7 @@ final class Emitter(config: Emitter.Config) { def startRun(): Unit = _cacheUsed = false def getOrElseUpdate(version: Version, ctor: WithGlobals[List[js.Tree]], - memberMethods: List[WithGlobals[js.MethodDef]], exportedMembers: List[WithGlobals[List[js.Tree]]], + memberMethods: List[WithGlobals[js.Tree]], exportedMembers: List[WithGlobals[List[js.Tree]]], compute: => WithGlobals[List[js.Tree]]): WithGlobals[List[js.Tree]] = { @tailrec diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/JSGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/JSGen.scala index c5e3aba380..3ed36197a5 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/JSGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/JSGen.scala @@ -156,18 +156,6 @@ private[emitter] final class JSGen(val config: Emitter.Config) { } } - def assignES5ClassMembers(classRef: Tree, members: List[MethodDef])( - implicit pos: Position): List[Tree] = { - import TreeDSL._ - - for { - MethodDef(static, name, args, restParam, body) <- members - } yield { - val target = if (static) classRef else classRef.prototype - genPropSelect(target, name) := Function(arrow = false, args, restParam, body) - } - } - def genIIFE(captures: List[(ParamDef, Tree)], body: Tree)( implicit pos: Position): Tree = { val (params, args) = captures.unzip From afef6bd76984772c4e5a1902ee307678059d8c49 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 17 Dec 2023 16:08:08 +0100 Subject: [PATCH 024/298] Cache (potential) jsSuperClass tree separately This removes the only desugarExpr call from buildClass. Since we'll stop caching buildClass going forward, this is important. --- .../linker/backend/emitter/ClassEmitter.scala | 31 ++++++++++--------- .../linker/backend/emitter/Emitter.scala | 12 +++++-- 2 files changed, 26 insertions(+), 17 deletions(-) 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 1a5a965e55..08a3bcbf20 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 @@ -45,13 +45,13 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { def buildClass(className: ClassName, isJSClass: Boolean, jsClassCaptures: Option[List[ParamDef]], hasClassInitializer: Boolean, - superClass: Option[ClassIdent], jsSuperClass: Option[Tree], useESClass: Boolean, + superClass: Option[ClassIdent], storeJSSuperClass: Option[js.Tree], useESClass: Boolean, members: List[js.Tree])( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[List[js.Tree]] = { if (!isJSClass) { - assert(jsSuperClass.isEmpty, className) + assert(storeJSSuperClass.isEmpty, className) if (useESClass) { val parentVarWithGlobals = for (parentIdent <- superClass) yield { @@ -70,18 +70,12 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { // Wrap the entire class def in an accessor function import TreeDSL._ - val genStoreJSSuperClass = jsSuperClass.map { jsSuperClass => - for (rhs <- desugarExpr(jsSuperClass, resultType = AnyType)) yield { - js.VarDef(fileLevelVar(VarField.superClass).ident, Some(rhs)) - } - } - val classValueIdent = fileLevelVarIdent(VarField.b, genName(className)) val classValueVar = js.VarRef(classValueIdent) val createClassValueVar = genEmptyMutableLet(classValueIdent) val entireClassDefWithGlobals = if (useESClass) { - genJSSuperCtor(superClass, jsSuperClass).map { jsSuperClass => + genJSSuperCtor(superClass, storeJSSuperClass.isDefined).map { jsSuperClass => List(classValueVar := js.ClassDef(Some(classValueIdent), Some(jsSuperClass), members)) } } else { @@ -89,11 +83,10 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } val classDefStatsWithGlobals = for { - optStoreJSSuperClass <- WithGlobals.option(genStoreJSSuperClass) entireClassDef <- entireClassDefWithGlobals createStaticFields <- genCreateStaticFieldsOfJSClass(className) } yield { - optStoreJSSuperClass.toList ::: entireClassDef ::: createStaticFields + storeJSSuperClass.toList ::: entireClassDef ::: createStaticFields } jsClassCaptures.fold { @@ -225,7 +218,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { /** Generates the JS constructor for a JS class. */ def genJSConstructor(className: ClassName, superClass: Option[ClassIdent], - jsSuperClass: Option[Tree], useESClass: Boolean, jsConstructorDef: JSConstructorDef)( + hasJSSuperClass: Boolean, useESClass: Boolean, jsConstructorDef: JSConstructorDef)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[List[js.Tree]] = { @@ -240,7 +233,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } else { for { ctorFun <- ctorFunWithGlobals - superCtor <- genJSSuperCtor(superClass, jsSuperClass) + superCtor <- genJSSuperCtor(superClass, hasJSSuperClass) } yield { import TreeDSL._ @@ -254,16 +247,24 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } } - private def genJSSuperCtor(superClass: Option[ClassIdent], jsSuperClass: Option[Tree])( + private def genJSSuperCtor(superClass: Option[ClassIdent], hasJSSuperClass: Boolean)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[js.Tree] = { - if (jsSuperClass.isDefined) { + if (hasJSSuperClass) { WithGlobals(fileLevelVar(VarField.superClass)) } else { genJSClassConstructor(superClass.get.name, keepOnlyDangerousVarNames = true) } } + def genStoreJSSuperClass(jsSuperClass: Tree)( + implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, + pos: Position): WithGlobals[js.Tree] = { + for (rhs <- desugarExpr(jsSuperClass, resultType = AnyType)) yield { + js.VarDef(fileLevelVar(VarField.superClass).ident, Some(rhs)) + } + } + /** Generates the JavaScript constructor of a class, as a `js.Function`. * * For ECMAScript 2015, that `js.Function` must be decomposed and reformed diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala index ea5e4b09bd..a5191cdf8c 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala @@ -447,6 +447,13 @@ final class Emitter(config: Emitter.Config) { (isJSClass || linkedClass.ancestors.contains(ThrowableClass)) } + val hasJSSuperClass = linkedClass.jsSuperClass.isDefined + + val storeJSSuperClass = linkedClass.jsSuperClass.map { jsSuperClass => + extractWithGlobals(classTreeCache.storeJSSuperClass.getOrElseUpdate( + classEmitter.genStoreJSSuperClass(jsSuperClass)(moduleContext, classCache, linkedClass.pos))) + } + // JS constructor val ctorWithGlobals = { /* The constructor depends both on the class version, and the version @@ -468,7 +475,7 @@ final class Emitter(config: Emitter.Config) { classEmitter.genJSConstructor( className, // invalidated by overall class cache (part of ancestors) linkedClass.superClass, // invalidated by class version - linkedClass.jsSuperClass, // invalidated by class version + hasJSSuperClass, // invalidated by class version useESClass, // invalidated by class version jsConstructorDef // part of ctor version )(moduleContext, ctorCache, linkedClass.pos)) @@ -579,7 +586,7 @@ final class Emitter(config: Emitter.Config) { linkedClass.jsClassCaptures, // invalidated by class version hasClassInitializer, // invalidated by class version (optimizer cannot remove it) linkedClass.superClass, // invalidated by class version - linkedClass.jsSuperClass, // invalidated by class version + storeJSSuperClass, // invalidated by class version useESClass, // invalidated by class version (depends on kind, config and ancestry only) ctor ::: memberMethods ::: exportedMembers.flatten // all 3 invalidated directly )(moduleContext, fullClassCache, linkedClass.pos) // pos invalidated by class version @@ -1047,6 +1054,7 @@ object Emitter { private final class DesugaredClassCache { val privateJSFields = new OneTimeCache[WithGlobals[List[js.Tree]]] + val storeJSSuperClass = new OneTimeCache[WithGlobals[js.Tree]] val instanceTests = new OneTimeCache[WithGlobals[List[js.Tree]]] val typeData = new OneTimeCache[WithGlobals[List[js.Tree]]] val setTypeData = new OneTimeCache[js.Tree] From 4b52fa070f3a1337639f55f0253aabcfaadd18f1 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sat, 30 Dec 2023 11:11:06 +0100 Subject: [PATCH 025/298] Replace DocComment tree with dedicated JSDocConstructor tree This simplifies pretty much all usage sites. --- .../closure/ClosureAstTransformer.scala | 55 ++++++------------- .../linker/backend/emitter/ClassEmitter.scala | 14 ++--- .../linker/backend/javascript/Printers.scala | 26 ++------- .../linker/backend/javascript/Trees.scala | 4 +- .../backend/javascript/PrintersTest.scala | 8 ++- 5 files changed, 36 insertions(+), 71 deletions(-) diff --git a/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureAstTransformer.scala b/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureAstTransformer.scala index fa759a4858..79ad4562ec 100644 --- a/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureAstTransformer.scala +++ b/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureAstTransformer.scala @@ -43,7 +43,8 @@ private class ClosureAstTransformer(featureSet: FeatureSet, def transformScript(topLevelTrees: List[Tree]): Node = { val script = setNodePosition(new Node(Token.SCRIPT), NoPosition) - transformBlockStats(topLevelTrees)(NoPosition).foreach(script.addChildToBack(_)) + for (stat <- topLevelTrees) + script.addChildToBack(transformStat(stat)(NoPosition)) script.putProp(Node.FEATURE_SET, featureSet) script } @@ -55,6 +56,20 @@ private class ClosureAstTransformer(featureSet: FeatureSet, implicit val pos = pos_in wrapTransform(tree) { + case JSDocConstructor(tree) => + val node = transformStat(tree) + // The @constructor must be propagated through an ExprResult node + val trg = + if (node.isExprResult()) node.getChildAtIndex(0) + else node + val ctorDoc = { + val b = JSDocInfo.builder() + b.recordConstructor() + b.build() + } + trg.setJSDocInfo(ctorDoc) + node + case VarDef(ident, optRhs) => val node = transformName(ident) optRhs.foreach(rhs => node.addChildToFront(transformExpr(rhs))) @@ -448,45 +463,11 @@ private class ClosureAstTransformer(featureSet: FeatureSet, def transformBlock(stats: List[Tree], blockPos: Position): Node = { val block = new Node(Token.BLOCK) - for (node <- transformBlockStats(stats)(blockPos)) - block.addChildToBack(node) + for (stat <- stats) + block.addChildToBack(transformStat(stat)(blockPos)) block } - def transformBlockStats(stats: List[Tree])( - implicit parentPos: Position): List[Node] = { - - @inline def ctorDoc(): JSDocInfo = { - val b = JSDocInfo.builder() - b.recordConstructor() - b.build() - } - - // The Rhino IR attaches DocComments to the following nodes (rather than - // having individual nodes). We preprocess these here. - @tailrec - def loop(ts: List[Tree], nextIsCtor: Boolean, acc: List[Node]): List[Node] = ts match { - case DocComment(text) :: tss => - loop(tss, nextIsCtor = text.startsWith("@constructor"), acc) - - case t :: tss => - val node = transformStat(t) - if (nextIsCtor) { - // The @constructor must be propagated through an ExprResult node - val trg = - if (node.isExprResult()) node.getChildAtIndex(0) - else node - trg.setJSDocInfo(ctorDoc()) - } - loop(tss, nextIsCtor = false, node :: acc) - - case Nil => - acc.reverse - } - - loop(stats, nextIsCtor = false, Nil) - } - @inline private def wrapTransform(tree: Tree)(body: Tree => Node)( implicit pos: Position): Node = { 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 144672a471..fc7054c1e6 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 @@ -214,14 +214,14 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } yield { ( // Real constructor - js.DocComment("@constructor") :: - realCtorDef ::: + js.JSDocConstructor(realCtorDef.head) :: + realCtorDef.tail ::: chainProto ::: (genIdentBracketSelect(ctorVar.prototype, "constructor") := ctorVar) :: // Inheritable constructor - js.DocComment("@constructor") :: - inheritableCtorDef ::: + js.JSDocConstructor(inheritableCtorDef.head) :: + inheritableCtorDef.tail ::: (globalVar(VarField.h, className).prototype := ctorVar.prototype) :: Nil ) } @@ -251,8 +251,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val ctorVar = fileLevelVar(VarField.b, genName(className)) - js.DocComment("@constructor") :: - (ctorVar := ctorFun) :: + js.JSDocConstructor(ctorVar := ctorFun) :: chainPrototypeWithLocalCtor(className, ctorVar, superCtor) ::: (genIdentBracketSelect(ctorVar.prototype, "constructor") := ctorVar) :: Nil } @@ -344,8 +343,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val dummyCtor = fileLevelVar(VarField.hh, genName(className)) List( - js.DocComment("@constructor"), - genConst(dummyCtor.ident, js.Function(false, Nil, None, js.Skip())), + js.JSDocConstructor(genConst(dummyCtor.ident, js.Function(false, Nil, None, js.Skip()))), dummyCtor.prototype := superCtor.prototype, ctorVar.prototype := js.New(dummyCtor, Nil) ) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala index 9690946e69..4d675d4dec 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala @@ -139,27 +139,11 @@ object Printers { } tree match { - // Comments - - case DocComment(text) => - val lines = text.split("\n").toList - if (lines.tail.isEmpty) { - print("/** ") - print(lines.head) - print(" */") - } else { - print("/** ") - print(lines.head) - println(); printIndent() - var rest = lines.tail - while (rest.nonEmpty) { - print(" * ") - print(rest.head) - println(); printIndent() - rest = rest.tail - } - print(" */") - } + case JSDocConstructor(tree) => + print("/** @constructor */") + println(); printIndent() + // not printStat: we must not print the trailing newline. + printTree(tree, isStat = true) // Definitions diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala index 27da8e50c1..efcf98e609 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala @@ -39,9 +39,9 @@ object Trees { } } - // Comments + // Constructor comment / annotation. - sealed case class DocComment(text: String)(implicit val pos: Position) + sealed case class JSDocConstructor(tree: Tree)(implicit val pos: Position) extends Tree // Identifiers and properties diff --git a/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala index 316f2c3907..86c9215f02 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala @@ -89,12 +89,14 @@ class PrintersTest { ) } - @Test def printDocComment(): Unit = { + @Test def printJSDocConstructor(): Unit = { assertPrintEquals( """ - | /** test */ + |/** @constructor */ + |ctor = (function() { + |}); """, - DocComment("test") + JSDocConstructor(Assign(VarRef("ctor"), Function(false, Nil, None, Skip()))) ) } From 6d072557808220d3eedb9209cc2b90d29b34357a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 25 Jan 2024 14:51:03 +0100 Subject: [PATCH 026/298] Do not put `O` (`jl.Object`) in the `ancestors` dictionaries. It is never read, because all the functions that would read it are special-cased, so these are wasted bytes. --- .../org/scalajs/linker/backend/emitter/ClassEmitter.scala | 2 +- .../org/scalajs/linker/backend/emitter/CoreJSLib.scala | 3 +-- .../test/scala/org/scalajs/linker/LibrarySizeTest.scala | 6 +++--- project/Build.scala | 8 ++++---- 4 files changed, 9 insertions(+), 10 deletions(-) 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 144672a471..ad953d86ef 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 @@ -865,7 +865,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } val ancestorsRecord = js.ObjectConstr( - ancestors.map(ancestor => (js.Ident(genName(ancestor)), js.IntLiteral(1)))) + ancestors.withFilter(_ != ObjectClass).map(ancestor => (js.Ident(genName(ancestor)), js.IntLiteral(1)))) val isInstanceFunWithGlobals: WithGlobals[js.Tree] = { if (globalKnowledge.isAncestorOfHijackedClass(className)) { 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 8a04956ad8..6bfac4a983 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 @@ -1687,7 +1687,6 @@ private[emitter] object CoreJSLib { else Skip(), privateFieldSet("ancestors", ObjectConstr(List( - Ident(genName(ObjectClass)) -> 1, Ident(genName(CloneableClass)) -> 1, Ident(genName(SerializableClass)) -> 1 ))), @@ -2066,7 +2065,7 @@ private[emitter] object CoreJSLib { extractWithGlobals( globalVarDef(VarField.d, ObjectClass, New(globalVar(VarField.TypeData, CoreVar), Nil))) ::: List( - privateFieldSet("ancestors", ObjectConstr(List((Ident(genName(ObjectClass)) -> 1)))), + privateFieldSet("ancestors", ObjectConstr(Nil)), privateFieldSet("arrayEncodedName", str("L" + fullName + ";")), privateFieldSet("isAssignableFromFun", { genArrowFunction(paramList(that), { 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 9dcc074647..aa851ca438 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 = 150339, - expectedFullLinkSizeWithoutClosure = 130884, - expectedFullLinkSizeWithClosure = 21394, + expectedFastLinkSize = 150063, + expectedFullLinkSizeWithoutClosure = 130664, + expectedFullLinkSizeWithClosure = 21325, classDefs, moduleInitializers = MainTestModuleInitializers ) diff --git a/project/Build.scala b/project/Build.scala index 758975e8f0..f9d72c08e0 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1967,16 +1967,16 @@ object Build { scalaVersion.value match { case `default212Version` => Some(ExpectedSizes( - fastLink = 770000 to 771000, - fullLink = 145000 to 146000, + fastLink = 768000 to 769000, + fullLink = 144000 to 145000, fastLinkGz = 90000 to 91000, fullLinkGz = 35000 to 36000, )) case `default213Version` => Some(ExpectedSizes( - fastLink = 479000 to 480000, - fullLink = 102000 to 103000, + fastLink = 478000 to 479000, + fullLink = 101000 to 102000, fastLinkGz = 62000 to 63000, fullLinkGz = 27000 to 28000, )) From 86c18be06bfff8c0245e1ab18b404bcb238a56ba Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 15 Oct 2023 20:05:42 +0200 Subject: [PATCH 027/298] Fuse emitting and printing of trees in the backend This allows us to use the Emitter's powerful caching mechanism to directly cache printed trees (as byte buffers) and not cache JavaScript trees anymore at all. This reduces in-between run memory usage on the test suite from 1.12 GB (not GiB) to 1.00 GB on my machine (roughly 10%). Runtime performance (both batch and incremental) is unaffected. It is worth pointing out, that due to how the Emitter caches trees, classes that end up being ES6 classes is performed will be held twice in memory (once the individual methods, once the entire class). On the test suite, this is the case for 710 cases out of 6538. --- .../closure/ClosureLinkerBackend.scala | 9 +- .../linker/backend/BasicLinkerBackend.scala | 169 ++++++-------- .../linker/backend/emitter/ClassEmitter.scala | 6 +- .../linker/backend/emitter/CoreJSLib.scala | 22 +- .../linker/backend/emitter/Emitter.scala | 213 +++++++++++------- .../linker/backend/javascript/Printers.scala | 30 ++- .../linker/backend/javascript/Trees.scala | 18 ++ .../linker/BasicLinkerBackendTest.scala | 30 +-- .../org/scalajs/linker/EmitterTest.scala | 94 ++++++++ .../backend/javascript/PrintersTest.scala | 24 +- 10 files changed, 383 insertions(+), 232 deletions(-) diff --git a/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureLinkerBackend.scala b/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureLinkerBackend.scala index 003e873773..1f532767e2 100644 --- a/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureLinkerBackend.scala +++ b/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureLinkerBackend.scala @@ -60,7 +60,7 @@ final class ClosureLinkerBackend(config: LinkerBackendImpl.Config) .withTrackAllGlobalRefs(true) .withInternalModulePattern(m => OutputPatternsImpl.moduleName(config.outputPatterns, m.id)) - new Emitter(emitterConfig) + new Emitter(emitterConfig, ClosureLinkerBackend.PostTransformer) } val symbolRequirements: SymbolRequirement = emitter.symbolRequirements @@ -295,4 +295,11 @@ private object ClosureLinkerBackend { Function.prototype.apply; var NaN = 0.0/0.0, Infinity = 1.0/0.0, undefined = void 0; """ + + private object PostTransformer extends Emitter.PostTransformer[js.Tree] { + // Do not apply ClosureAstTransformer eagerly: + // The ASTs used by closure are highly mutable, so re-using them is non-trivial. + // Since closure is slow anyways, we haven't built the optimization. + def transformStats(trees: List[js.Tree], indent: Int): List[js.Tree] = trees + } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala index 564ebbb99c..4faef57c0a 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala @@ -17,6 +17,8 @@ import scala.concurrent._ import java.nio.ByteBuffer import java.nio.charset.StandardCharsets +import java.util.concurrent.atomic.AtomicInteger + import org.scalajs.logging.Logger import org.scalajs.linker.interface.{IRFile, OutputDirectory, Report} @@ -36,12 +38,19 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) import BasicLinkerBackend._ + private[this] var totalModules = 0 + private[this] val rewrittenModules = new AtomicInteger(0) + private[this] val emitter = { val emitterConfig = Emitter.Config(config.commonConfig.coreSpec) .withJSHeader(config.jsHeader) .withInternalModulePattern(m => OutputPatternsImpl.moduleName(config.outputPatterns, m.id)) - new Emitter(emitterConfig) + val postTransformer = + if (config.sourceMap) PostTransformerWithSourceMap + else PostTransformerWithoutSourceMap + + new Emitter(emitterConfig, postTransformer) } val symbolRequirements: SymbolRequirement = emitter.symbolRequirements @@ -61,6 +70,11 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) implicit ec: ExecutionContext): Future[Report] = { verifyModuleSet(moduleSet) + // Reset stats. + + totalModules = moduleSet.modules.size + rewrittenModules.set(0) + val emitterResult = logger.time("Emitter") { emitter.emit(moduleSet, logger) } @@ -68,24 +82,25 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) val skipContentCheck = !isFirstRun isFirstRun = false - printedModuleSetCache.startRun(moduleSet) val allChanged = printedModuleSetCache.updateGlobal(emitterResult.header, emitterResult.footer) val writer = new OutputWriter(output, config, skipContentCheck) { protected def writeModuleWithoutSourceMap(moduleID: ModuleID, force: Boolean): Option[ByteBuffer] = { val cache = printedModuleSetCache.getModuleCache(moduleID) - val changed = cache.update(emitterResult.body(moduleID)) + val printedTrees = emitterResult.body(moduleID) + + val changed = cache.update(printedTrees) if (force || changed || allChanged) { - printedModuleSetCache.incRewrittenModules() + rewrittenModules.incrementAndGet() val jsFileWriter = new ByteArrayWriter(sizeHintFor(cache.getPreviousFinalJSFileSize())) jsFileWriter.write(printedModuleSetCache.headerBytes) jsFileWriter.writeASCIIString("'use strict';\n") - for (printedTree <- cache.printedTrees) + for (printedTree <- printedTrees) jsFileWriter.write(printedTree.jsCode) jsFileWriter.write(printedModuleSetCache.footerBytes) @@ -99,10 +114,12 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) protected def writeModuleWithSourceMap(moduleID: ModuleID, force: Boolean): Option[(ByteBuffer, ByteBuffer)] = { val cache = printedModuleSetCache.getModuleCache(moduleID) - val changed = cache.update(emitterResult.body(moduleID)) + val printedTrees = emitterResult.body(moduleID) + + val changed = cache.update(printedTrees) if (force || changed || allChanged) { - printedModuleSetCache.incRewrittenModules() + rewrittenModules.incrementAndGet() val jsFileWriter = new ByteArrayWriter(sizeHintFor(cache.getPreviousFinalJSFileSize())) val sourceMapWriter = new ByteArrayWriter(sizeHintFor(cache.getPreviousFinalSourceMapSize())) @@ -120,7 +137,7 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) jsFileWriter.writeASCIIString("'use strict';\n") smWriter.nextLine() - for (printedTree <- cache.printedTrees) { + for (printedTree <- printedTrees) { jsFileWriter.write(printedTree.jsCode) smWriter.insertFragment(printedTree.sourceMapFragment) } @@ -145,9 +162,15 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) writer.write(moduleSet) }.andThen { case _ => printedModuleSetCache.cleanAfterRun() - printedModuleSetCache.logStats(logger) + logStats(logger) } } + + private def logStats(logger: Logger): Unit = { + // Message extracted in BasicLinkerBackendTest + logger.debug( + s"BasicBackend: total modules: $totalModules; re-written: ${rewrittenModules.get()}") + } } private object BasicLinkerBackend { @@ -161,20 +184,6 @@ private object BasicLinkerBackend { private val modules = new java.util.concurrent.ConcurrentHashMap[ModuleID, PrintedModuleCache] - private var totalModules = 0 - private val rewrittenModules = new java.util.concurrent.atomic.AtomicInteger(0) - - private var totalTopLevelTrees = 0 - private var recomputedTopLevelTrees = 0 - - def startRun(moduleSet: ModuleSet): Unit = { - totalModules = moduleSet.modules.size - rewrittenModules.set(0) - - totalTopLevelTrees = 0 - recomputedTopLevelTrees = 0 - } - def updateGlobal(header: String, footer: String): Boolean = { if (header == lastHeader && footer == lastFooter) { false @@ -193,61 +202,32 @@ private object BasicLinkerBackend { def headerNewLineCount: Int = _headerNewLineCountCache def getModuleCache(moduleID: ModuleID): PrintedModuleCache = { - val result = modules.computeIfAbsent(moduleID, { _ => - if (withSourceMaps) new PrintedModuleCacheWithSourceMaps - else new PrintedModuleCache - }) - + val result = modules.computeIfAbsent(moduleID, _ => new PrintedModuleCache) result.startRun() result } - def incRewrittenModules(): Unit = - rewrittenModules.incrementAndGet() - def cleanAfterRun(): Unit = { val iter = modules.entrySet().iterator() while (iter.hasNext()) { val moduleCache = iter.next().getValue() - if (moduleCache.cleanAfterRun()) { - totalTopLevelTrees += moduleCache.getTotalTopLevelTrees - recomputedTopLevelTrees += moduleCache.getRecomputedTopLevelTrees - } else { + if (!moduleCache.cleanAfterRun()) { iter.remove() } } } - - def logStats(logger: Logger): Unit = { - /* These messages are extracted in BasicLinkerBackendTest to assert that - * we do not invalidate anything in a no-op second run. - */ - logger.debug( - s"BasicBackend: total top-level trees: $totalTopLevelTrees; re-computed: $recomputedTopLevelTrees") - logger.debug( - s"BasicBackend: total modules: $totalModules; re-written: ${rewrittenModules.get()}") - } - } - - private final class PrintedTree(val jsCode: Array[Byte], val sourceMapFragment: SourceMapWriter.Fragment) { - var cachedUsed: Boolean = false } private sealed class PrintedModuleCache { private var cacheUsed = false private var changed = false - private var lastJSTrees: List[js.Tree] = Nil - private var printedTreesCache: List[PrintedTree] = Nil - private val cache = new java.util.IdentityHashMap[js.Tree, PrintedTree] + private var lastPrintedTrees: List[js.PrintedTree] = Nil private var previousFinalJSFileSize: Int = 0 private var previousFinalSourceMapSize: Int = 0 - private var recomputedTopLevelTrees = 0 - def startRun(): Unit = { cacheUsed = true - recomputedTopLevelTrees = 0 } def getPreviousFinalJSFileSize(): Int = previousFinalJSFileSize @@ -259,72 +239,51 @@ private object BasicLinkerBackend { previousFinalSourceMapSize = finalSourceMapSize } - def update(newJSTrees: List[js.Tree]): Boolean = { - val changed = !newJSTrees.corresponds(lastJSTrees)(_ eq _) + def update(newPrintedTrees: List[js.PrintedTree]): Boolean = { + val changed = !newPrintedTrees.corresponds(lastPrintedTrees)(_ eq _) this.changed = changed if (changed) { - printedTreesCache = newJSTrees.map(getOrComputePrintedTree(_)) - lastJSTrees = newJSTrees + lastPrintedTrees = newPrintedTrees } changed } - private def getOrComputePrintedTree(tree: js.Tree): PrintedTree = { - val result = cache.computeIfAbsent(tree, { (tree: js.Tree) => - recomputedTopLevelTrees += 1 - computePrintedTree(tree) - }) - - result.cachedUsed = true - result - } - - protected def computePrintedTree(tree: js.Tree): PrintedTree = { - val jsCodeWriter = new ByteArrayWriter() - val printer = new Printers.JSTreePrinter(jsCodeWriter) - - printer.printStat(tree) - - new PrintedTree(jsCodeWriter.toByteArray(), SourceMapWriter.Fragment.Empty) + def cleanAfterRun(): Boolean = { + val wasUsed = cacheUsed + cacheUsed = false + wasUsed } + } - def printedTrees: List[PrintedTree] = printedTreesCache + private object PostTransformerWithoutSourceMap extends Emitter.PostTransformer[js.PrintedTree] { + def transformStats(trees: List[js.Tree], indent: Int): List[js.PrintedTree] = { + if (trees.isEmpty) { + Nil // Fast path + } else { + val jsCodeWriter = new ByteArrayWriter() + val printer = new Printers.JSTreePrinter(jsCodeWriter, indent) - def cleanAfterRun(): Boolean = { - if (cacheUsed) { - cacheUsed = false - - if (changed) { - val iter = cache.entrySet().iterator() - while (iter.hasNext()) { - val printedTree = iter.next().getValue() - if (printedTree.cachedUsed) - printedTree.cachedUsed = false - else - iter.remove() - } - } + trees.map(printer.printStat(_)) - true - } else { - false + js.PrintedTree(jsCodeWriter.toByteArray(), SourceMapWriter.Fragment.Empty) :: Nil } } - - def getTotalTopLevelTrees: Int = lastJSTrees.size - def getRecomputedTopLevelTrees: Int = recomputedTopLevelTrees } - private final class PrintedModuleCacheWithSourceMaps extends PrintedModuleCache { - override protected def computePrintedTree(tree: js.Tree): PrintedTree = { - val jsCodeWriter = new ByteArrayWriter() - val smFragmentBuilder = new SourceMapWriter.FragmentBuilder() - val printer = new Printers.JSTreePrinterWithSourceMap(jsCodeWriter, smFragmentBuilder) + private object PostTransformerWithSourceMap extends Emitter.PostTransformer[js.PrintedTree] { + def transformStats(trees: List[js.Tree], indent: Int): List[js.PrintedTree] = { + if (trees.isEmpty) { + Nil // Fast path + } else { + val jsCodeWriter = new ByteArrayWriter() + val smFragmentBuilder = new SourceMapWriter.FragmentBuilder() + val printer = new Printers.JSTreePrinterWithSourceMap(jsCodeWriter, smFragmentBuilder, indent) - printer.printStat(tree) - smFragmentBuilder.complete() + trees.map(printer.printStat(_)) + smFragmentBuilder.complete() - new PrintedTree(jsCodeWriter.toByteArray(), smFragmentBuilder.result()) + js.PrintedTree(jsCodeWriter.toByteArray(), smFragmentBuilder.result()) :: Nil + } } } } 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 424b962989..aade6c4b8a 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 @@ -45,7 +45,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { def buildClass(className: ClassName, isJSClass: Boolean, jsClassCaptures: Option[List[ParamDef]], hasClassInitializer: Boolean, - superClass: Option[ClassIdent], storeJSSuperClass: Option[js.Tree], useESClass: Boolean, + superClass: Option[ClassIdent], storeJSSuperClass: List[js.Tree], useESClass: Boolean, members: List[js.Tree])( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[List[js.Tree]] = { @@ -75,7 +75,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val createClassValueVar = genEmptyMutableLet(classValueIdent) val entireClassDefWithGlobals = if (useESClass) { - genJSSuperCtor(superClass, storeJSSuperClass.isDefined).map { jsSuperClass => + genJSSuperCtor(superClass, storeJSSuperClass.nonEmpty).map { jsSuperClass => List(classValueVar := js.ClassDef(Some(classValueIdent), Some(jsSuperClass), members)) } } else { @@ -86,7 +86,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { entireClassDef <- entireClassDefWithGlobals createStaticFields <- genCreateStaticFieldsOfJSClass(className) } yield { - storeJSSuperClass.toList ::: entireClassDef ::: createStaticFields + storeJSSuperClass ::: entireClassDef ::: createStaticFields } jsClassCaptures.fold { 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 97330e7ccf..290bb7f362 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 @@ -32,9 +32,9 @@ import PolyfillableBuiltin._ private[emitter] object CoreJSLib { - def build(sjsGen: SJSGen, moduleContext: ModuleContext, - globalKnowledge: GlobalKnowledge): WithGlobals[Lib] = { - new CoreJSLibBuilder(sjsGen)(moduleContext, globalKnowledge).build() + def build[E](sjsGen: SJSGen, postTransform: List[Tree] => E, moduleContext: ModuleContext, + globalKnowledge: GlobalKnowledge): WithGlobals[Lib[E]] = { + new CoreJSLibBuilder(sjsGen)(moduleContext, globalKnowledge).build(postTransform) } /** A fully built CoreJSLib @@ -52,10 +52,10 @@ private[emitter] object CoreJSLib { * @param initialization Things that depend on Scala.js generated classes. * These must have class definitions (but not static fields) available. */ - final class Lib private[CoreJSLib] ( - val preObjectDefinitions: List[Tree], - val postObjectDefinitions: List[Tree], - val initialization: List[Tree]) + final class Lib[E] private[CoreJSLib] ( + val preObjectDefinitions: E, + val postObjectDefinitions: E, + val initialization: E) private class CoreJSLibBuilder(sjsGen: SJSGen)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge) { @@ -115,9 +115,11 @@ private[emitter] object CoreJSLib { private val specializedArrayTypeRefs: List[NonArrayTypeRef] = ClassRef(ObjectClass) :: orderedPrimRefsWithoutVoid - def build(): WithGlobals[Lib] = { - val lib = new Lib(buildPreObjectDefinitions(), - buildPostObjectDefinitions(), buildInitializations()) + def build[E](postTransform: List[Tree] => E): WithGlobals[Lib[E]] = { + val lib = new Lib( + postTransform(buildPreObjectDefinitions()), + postTransform(buildPostObjectDefinitions()), + postTransform(buildInitializations())) WithGlobals(lib, trackedGlobalRefs) } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala index a5191cdf8c..6035764aca 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala @@ -33,7 +33,8 @@ import EmitterNames._ import GlobalRefUtils._ /** Emits a desugared JS tree to a builder */ -final class Emitter(config: Emitter.Config) { +final class Emitter[E >: Null <: js.Tree]( + config: Emitter.Config, postTransformer: Emitter.PostTransformer[E]) { import Emitter._ import config._ @@ -71,13 +72,16 @@ final class Emitter(config: Emitter.Config) { private[this] var statsClassesInvalidated: Int = 0 private[this] var statsMethodsReused: Int = 0 private[this] var statsMethodsInvalidated: Int = 0 + private[this] var statsPostTransforms: Int = 0 + private[this] var statsNestedPostTransforms: Int = 0 + private[this] var statsNestedPostTransformsAvoided: Int = 0 val symbolRequirements: SymbolRequirement = Emitter.symbolRequirements(config) val injectedIRFiles: Seq[IRFile] = PrivateLibHolder.files - def emit(moduleSet: ModuleSet, logger: Logger): Result = { + def emit(moduleSet: ModuleSet, logger: Logger): Result[E] = { val WithGlobals(body, globalRefs) = emitInternal(moduleSet, logger) moduleKind match { @@ -108,12 +112,15 @@ final class Emitter(config: Emitter.Config) { } private def emitInternal(moduleSet: ModuleSet, - logger: Logger): WithGlobals[Map[ModuleID, List[js.Tree]]] = { + logger: Logger): WithGlobals[Map[ModuleID, List[E]]] = { // Reset caching stats. statsClassesReused = 0 statsClassesInvalidated = 0 statsMethodsReused = 0 statsMethodsInvalidated = 0 + statsPostTransforms = 0 + statsNestedPostTransforms = 0 + statsNestedPostTransformsAvoided = 0 // Update GlobalKnowledge. val invalidateAll = knowledgeGuardian.update(moduleSet) @@ -128,13 +135,17 @@ final class Emitter(config: Emitter.Config) { try { emitAvoidGlobalClash(moduleSet, logger, secondAttempt = false) } finally { - // Report caching stats. + // Report caching stats (extracted in EmitterTest). logger.debug( s"Emitter: Class tree cache stats: reused: $statsClassesReused -- "+ s"invalidated: $statsClassesInvalidated") logger.debug( s"Emitter: Method tree cache stats: reused: $statsMethodsReused -- "+ s"invalidated: $statsMethodsInvalidated") + logger.debug( + s"Emitter: Post transforms: total: $statsPostTransforms -- " + + s"nested: $statsNestedPostTransforms -- " + + s"nested avoided: $statsNestedPostTransformsAvoided") // Inform caches about run completion. state.moduleCaches.filterInPlace((_, c) => c.cleanAfterRun()) @@ -142,6 +153,14 @@ final class Emitter(config: Emitter.Config) { } } + private def postTransform(trees: List[js.Tree], indent: Int): List[E] = { + statsPostTransforms += 1 + postTransformer.transformStats(trees, indent) + } + + private def postTransform(tree: js.Tree, indent: Int): List[E] = + postTransform(tree :: Nil, indent) + /** Emits all JavaScript code avoiding clashes with global refs. * * If, at the end of the process, the set of accessed dangerous globals has @@ -150,7 +169,7 @@ final class Emitter(config: Emitter.Config) { */ @tailrec private def emitAvoidGlobalClash(moduleSet: ModuleSet, - logger: Logger, secondAttempt: Boolean): WithGlobals[Map[ModuleID, List[js.Tree]]] = { + logger: Logger, secondAttempt: Boolean): WithGlobals[Map[ModuleID, List[E]]] = { val result = emitOnce(moduleSet, logger) val mentionedDangerousGlobalRefs = @@ -175,7 +194,7 @@ final class Emitter(config: Emitter.Config) { } private def emitOnce(moduleSet: ModuleSet, - logger: Logger): WithGlobals[Map[ModuleID, List[js.Tree]]] = { + logger: Logger): WithGlobals[Map[ModuleID, List[E]]] = { // Genreate classes first so we can measure time separately. val generatedClasses = logger.time("Emitter: Generate Classes") { moduleSet.modules.map { module => @@ -200,7 +219,7 @@ final class Emitter(config: Emitter.Config) { val moduleImports = extractWithGlobals { moduleCache.getOrComputeImports(module.externalDependencies, module.internalDependencies) { - genModuleImports(module) + genModuleImports(module).map(postTransform(_, 0)) } } @@ -210,7 +229,7 @@ final class Emitter(config: Emitter.Config) { */ moduleCache.getOrComputeTopLevelExports(module.topLevelExports) { classEmitter.genTopLevelExports(module.topLevelExports)( - moduleContext, moduleCache) + moduleContext, moduleCache).map(postTransform(_, 0)) } } @@ -220,7 +239,7 @@ final class Emitter(config: Emitter.Config) { WithGlobals.list(initializers.map { initializer => classEmitter.genModuleInitializer(initializer)( moduleContext, moduleCache) - }) + }).map(postTransform(_, 0)) } } @@ -241,7 +260,7 @@ final class Emitter(config: Emitter.Config) { * requires consistency between the Analyzer and the Emitter. As such, * it is crucial that we verify it. */ - val defTrees: List[js.Tree] = ( + val defTrees: List[E] = ( /* The definitions of the CoreJSLib that come before the definition * of `j.l.Object`. They depend on nothing else. */ @@ -357,7 +376,7 @@ final class Emitter(config: Emitter.Config) { } private def genClass(linkedClass: LinkedClass, - moduleContext: ModuleContext): GeneratedClass = { + moduleContext: ModuleContext): GeneratedClass[E] = { val className = linkedClass.className val classCache = classCaches.getOrElseUpdate( @@ -379,7 +398,7 @@ final class Emitter(config: Emitter.Config) { // Main part - val main = List.newBuilder[js.Tree] + val main = List.newBuilder[E] val (linkedInlineableInit, linkedMethods) = classEmitter.extractInlineableInit(linkedClass)(classCache) @@ -388,7 +407,7 @@ final class Emitter(config: Emitter.Config) { if (kind.isJSClass) { val fieldDefs = classTreeCache.privateJSFields.getOrElseUpdate { classEmitter.genCreatePrivateJSFieldDefsOfJSClass(className)( - moduleContext, classCache) + moduleContext, classCache).map(postTransform(_, 0)) } main ++= extractWithGlobals(fieldDefs) } @@ -407,8 +426,10 @@ final class Emitter(config: Emitter.Config) { val methodCache = classCache.getStaticLikeMethodCache(namespace, methodDef.methodName) - main ++= extractWithGlobals(methodCache.getOrElseUpdate(methodDef.version, - classEmitter.genStaticLikeMethod(className, methodDef)(moduleContext, methodCache))) + main ++= extractWithGlobals(methodCache.getOrElseUpdate(methodDef.version, { + classEmitter.genStaticLikeMethod(className, methodDef)(moduleContext, methodCache) + .map(postTransform(_, 0)) + })) } } @@ -447,11 +468,21 @@ final class Emitter(config: Emitter.Config) { (isJSClass || linkedClass.ancestors.contains(ThrowableClass)) } + val memberIndent = { + (if (isJSClass) 1 else 0) + // accessor function + (if (useESClass) 1 else 0) // nesting from class + } + val hasJSSuperClass = linkedClass.jsSuperClass.isDefined - val storeJSSuperClass = linkedClass.jsSuperClass.map { jsSuperClass => - extractWithGlobals(classTreeCache.storeJSSuperClass.getOrElseUpdate( - classEmitter.genStoreJSSuperClass(jsSuperClass)(moduleContext, classCache, linkedClass.pos))) + val storeJSSuperClass = if (hasJSSuperClass) { + extractWithGlobals(classTreeCache.storeJSSuperClass.getOrElseUpdate({ + val jsSuperClass = linkedClass.jsSuperClass.get + classEmitter.genStoreJSSuperClass(jsSuperClass)(moduleContext, classCache, linkedClass.pos) + .map(postTransform(_, 1)) + })) + } else { + Nil } // JS constructor @@ -478,7 +509,7 @@ final class Emitter(config: Emitter.Config) { hasJSSuperClass, // invalidated by class version useESClass, // invalidated by class version jsConstructorDef // part of ctor version - )(moduleContext, ctorCache, linkedClass.pos)) + )(moduleContext, ctorCache, linkedClass.pos).map(postTransform(_, memberIndent))) } else { val ctorVersion = linkedInlineableInit.fold { Version.combine(linkedClass.version) @@ -492,7 +523,7 @@ final class Emitter(config: Emitter.Config) { linkedClass.superClass, // invalidated by class version useESClass, // invalidated by class version, linkedInlineableInit // part of ctor version - )(moduleContext, ctorCache, linkedClass.pos)) + )(moduleContext, ctorCache, linkedClass.pos).map(postTransform(_, memberIndent))) } } @@ -546,7 +577,7 @@ final class Emitter(config: Emitter.Config) { isJSClass, // invalidated by isJSClassVersion useESClass, // invalidated by isJSClassVersion method // invalidated by method.version - )(moduleContext, methodCache)) + )(moduleContext, methodCache).map(postTransform(_, memberIndent))) } // Exported Members @@ -561,7 +592,7 @@ final class Emitter(config: Emitter.Config) { isJSClass, // invalidated by isJSClassVersion useESClass, // invalidated by isJSClassVersion member // invalidated by version - )(moduleContext, memberCache)) + )(moduleContext, memberCache).map(postTransform(_, memberIndent))) } val hasClassInitializer: Boolean = { @@ -578,8 +609,9 @@ final class Emitter(config: Emitter.Config) { memberMethodsWithGlobals, exportedMembersWithGlobals, { for { ctor <- ctorWithGlobals - memberMethods <- WithGlobals.list(memberMethodsWithGlobals) - exportedMembers <- WithGlobals.list(exportedMembersWithGlobals) + memberMethods <- WithGlobals.flatten(memberMethodsWithGlobals) + exportedMembers <- WithGlobals.flatten(exportedMembersWithGlobals) + allMembers = ctor ::: memberMethods ::: exportedMembers clazz <- classEmitter.buildClass( className, // invalidated by overall class cache (part of ancestors) isJSClass, // invalidated by class version @@ -588,10 +620,17 @@ final class Emitter(config: Emitter.Config) { linkedClass.superClass, // invalidated by class version storeJSSuperClass, // invalidated by class version useESClass, // invalidated by class version (depends on kind, config and ancestry only) - ctor ::: memberMethods ::: exportedMembers.flatten // all 3 invalidated directly + allMembers // invalidated directly )(moduleContext, fullClassCache, linkedClass.pos) // pos invalidated by class version } yield { - clazz + // Avoid a nested post transform if we just got the original members back. + if (clazz eq allMembers) { + statsNestedPostTransformsAvoided += 1 + allMembers + } else { + statsNestedPostTransforms += 1 + postTransform(clazz, 0) + } } }) } @@ -614,8 +653,10 @@ final class Emitter(config: Emitter.Config) { */ if (classEmitter.needInstanceTests(linkedClass)(classCache)) { - main ++= extractWithGlobals(classTreeCache.instanceTests.getOrElseUpdate( - classEmitter.genInstanceTests(className, kind)(moduleContext, classCache, linkedClass.pos))) + main ++= extractWithGlobals(classTreeCache.instanceTests.getOrElseUpdate({ + classEmitter.genInstanceTests(className, kind)(moduleContext, classCache, linkedClass.pos) + .map(postTransform(_, 0)) + })) } if (linkedClass.hasRuntimeTypeInfo) { @@ -626,18 +667,22 @@ final class Emitter(config: Emitter.Config) { linkedClass.superClass, // invalidated by class version linkedClass.ancestors, // invalidated by overall class cache (identity) linkedClass.jsNativeLoadSpec // invalidated by class version - )(moduleContext, classCache, linkedClass.pos))) + )(moduleContext, classCache, linkedClass.pos).map(postTransform(_, 0)))) } if (linkedClass.hasInstances && kind.isClass && linkedClass.hasRuntimeTypeInfo) { - main += classTreeCache.setTypeData.getOrElseUpdate( - classEmitter.genSetTypeData(className)(moduleContext, classCache, linkedClass.pos)) + main ++= classTreeCache.setTypeData.getOrElseUpdate({ + val tree = classEmitter.genSetTypeData(className)(moduleContext, classCache, linkedClass.pos) + postTransform(tree, 0) + }) } } if (linkedClass.kind.hasModuleAccessor && linkedClass.hasInstances) { - main ++= extractWithGlobals(classTreeCache.moduleAccessor.getOrElseUpdate( - classEmitter.genModuleAccessor(className, isJSClass)(moduleContext, classCache, linkedClass.pos))) + main ++= extractWithGlobals(classTreeCache.moduleAccessor.getOrElseUpdate({ + classEmitter.genModuleAccessor(className, isJSClass)(moduleContext, classCache, linkedClass.pos) + .map(postTransform(_, 0)) + })) } // Static fields @@ -645,15 +690,19 @@ final class Emitter(config: Emitter.Config) { val staticFields = if (linkedClass.kind.isJSType) { Nil } else { - extractWithGlobals(classTreeCache.staticFields.getOrElseUpdate( - classEmitter.genCreateStaticFieldsOfScalaClass(className)(moduleContext, classCache))) + extractWithGlobals(classTreeCache.staticFields.getOrElseUpdate({ + classEmitter.genCreateStaticFieldsOfScalaClass(className)(moduleContext, classCache) + .map(postTransform(_, 0)) + })) } // Static initialization val staticInitialization = if (classEmitter.needStaticInitialization(linkedClass)) { - classTreeCache.staticInitialization.getOrElseUpdate( - classEmitter.genStaticInitialization(className)(moduleContext, classCache, linkedClass.pos)) + classTreeCache.staticInitialization.getOrElseUpdate({ + val tree = classEmitter.genStaticInitialization(className)(moduleContext, classCache, linkedClass.pos) + postTransform(tree, 0) + }) } else { Nil } @@ -674,14 +723,14 @@ final class Emitter(config: Emitter.Config) { private final class ModuleCache extends knowledgeGuardian.KnowledgeAccessor { private[this] var _cacheUsed: Boolean = false - private[this] var _importsCache: WithGlobals[List[js.Tree]] = WithGlobals.nil + private[this] var _importsCache: WithGlobals[List[E]] = WithGlobals.nil private[this] var _lastExternalDependencies: Set[String] = Set.empty private[this] var _lastInternalDependencies: Set[ModuleID] = Set.empty - private[this] var _topLevelExportsCache: WithGlobals[List[js.Tree]] = WithGlobals.nil + private[this] var _topLevelExportsCache: WithGlobals[List[E]] = WithGlobals.nil private[this] var _lastTopLevelExports: List[LinkedTopLevelExport] = Nil - private[this] var _initializersCache: WithGlobals[List[js.Tree]] = WithGlobals.nil + private[this] var _initializersCache: WithGlobals[List[E]] = WithGlobals.nil private[this] var _lastInitializers: List[ModuleInitializer.Initializer] = Nil override def invalidate(): Unit = { @@ -702,7 +751,7 @@ final class Emitter(config: Emitter.Config) { } def getOrComputeImports(externalDependencies: Set[String], internalDependencies: Set[ModuleID])( - compute: => WithGlobals[List[js.Tree]]): WithGlobals[List[js.Tree]] = { + compute: => WithGlobals[List[E]]): WithGlobals[List[E]] = { _cacheUsed = true @@ -715,7 +764,7 @@ final class Emitter(config: Emitter.Config) { } def getOrComputeTopLevelExports(topLevelExports: List[LinkedTopLevelExport])( - compute: => WithGlobals[List[js.Tree]]): WithGlobals[List[js.Tree]] = { + compute: => WithGlobals[List[E]]): WithGlobals[List[E]] = { _cacheUsed = true @@ -754,7 +803,7 @@ final class Emitter(config: Emitter.Config) { } def getOrComputeInitializers(initializers: List[ModuleInitializer.Initializer])( - compute: => WithGlobals[List[js.Tree]]): WithGlobals[List[js.Tree]] = { + compute: => WithGlobals[List[E]]): WithGlobals[List[E]] = { _cacheUsed = true @@ -773,20 +822,20 @@ final class Emitter(config: Emitter.Config) { } private final class ClassCache extends knowledgeGuardian.KnowledgeAccessor { - private[this] var _cache: DesugaredClassCache = null + private[this] var _cache: DesugaredClassCache[List[E]] = null private[this] var _lastVersion: Version = Version.Unversioned private[this] var _cacheUsed = false private[this] val _methodCaches = - Array.fill(MemberNamespace.Count)(mutable.Map.empty[MethodName, MethodCache[List[js.Tree]]]) + Array.fill(MemberNamespace.Count)(mutable.Map.empty[MethodName, MethodCache[List[E]]]) private[this] val _memberMethodCache = - mutable.Map.empty[MethodName, MethodCache[js.Tree]] + mutable.Map.empty[MethodName, MethodCache[List[E]]] - private[this] var _constructorCache: Option[MethodCache[List[js.Tree]]] = None + private[this] var _constructorCache: Option[MethodCache[List[E]]] = None private[this] val _exportedMembersCache = - mutable.Map.empty[Int, MethodCache[List[js.Tree]]] + mutable.Map.empty[Int, MethodCache[List[E]]] private[this] var _fullClassCache: Option[FullClassCache] = None @@ -807,12 +856,12 @@ final class Emitter(config: Emitter.Config) { _fullClassCache.foreach(_.startRun()) } - def getCache(version: Version): DesugaredClassCache = { + def getCache(version: Version): DesugaredClassCache[List[E]] = { if (_cache == null || !_lastVersion.sameVersion(version)) { invalidate() statsClassesInvalidated += 1 _lastVersion = version - _cache = new DesugaredClassCache + _cache = new DesugaredClassCache[List[E]] } else { statsClassesReused += 1 } @@ -821,25 +870,25 @@ final class Emitter(config: Emitter.Config) { } def getMemberMethodCache( - methodName: MethodName): MethodCache[js.Tree] = { + methodName: MethodName): MethodCache[List[E]] = { _memberMethodCache.getOrElseUpdate(methodName, new MethodCache) } def getStaticLikeMethodCache(namespace: MemberNamespace, - methodName: MethodName): MethodCache[List[js.Tree]] = { + methodName: MethodName): MethodCache[List[E]] = { _methodCaches(namespace.ordinal) .getOrElseUpdate(methodName, new MethodCache) } - def getConstructorCache(): MethodCache[List[js.Tree]] = { + def getConstructorCache(): MethodCache[List[E]] = { _constructorCache.getOrElse { - val cache = new MethodCache[List[js.Tree]] + val cache = new MethodCache[List[E]] _constructorCache = Some(cache) cache } } - def getExportedMemberCache(idx: Int): MethodCache[List[js.Tree]] = + def getExportedMemberCache(idx: Int): MethodCache[List[E]] = _exportedMembersCache.getOrElseUpdate(idx, new MethodCache) def getFullClassCache(): FullClassCache = { @@ -905,11 +954,11 @@ final class Emitter(config: Emitter.Config) { } private class FullClassCache extends knowledgeGuardian.KnowledgeAccessor { - private[this] var _tree: WithGlobals[List[js.Tree]] = null + private[this] var _tree: WithGlobals[List[E]] = null private[this] var _lastVersion: Version = Version.Unversioned - private[this] var _lastCtor: WithGlobals[List[js.Tree]] = null - private[this] var _lastMemberMethods: List[WithGlobals[js.Tree]] = null - private[this] var _lastExportedMembers: List[WithGlobals[List[js.Tree]]] = null + private[this] var _lastCtor: WithGlobals[List[E]] = null + private[this] var _lastMemberMethods: List[WithGlobals[List[E]]] = null + private[this] var _lastExportedMembers: List[WithGlobals[List[E]]] = null private[this] var _cacheUsed = false override def invalidate(): Unit = { @@ -923,9 +972,9 @@ final class Emitter(config: Emitter.Config) { def startRun(): Unit = _cacheUsed = false - def getOrElseUpdate(version: Version, ctor: WithGlobals[List[js.Tree]], - memberMethods: List[WithGlobals[js.Tree]], exportedMembers: List[WithGlobals[List[js.Tree]]], - compute: => WithGlobals[List[js.Tree]]): WithGlobals[List[js.Tree]] = { + def getOrElseUpdate(version: Version, ctor: WithGlobals[List[E]], + memberMethods: List[WithGlobals[List[E]]], exportedMembers: List[WithGlobals[List[E]]], + compute: => WithGlobals[List[E]]): WithGlobals[List[E]] = { @tailrec def allSame[A <: AnyRef](xs: List[A], ys: List[A]): Boolean = { @@ -960,11 +1009,11 @@ final class Emitter(config: Emitter.Config) { private class CoreJSLibCache extends knowledgeGuardian.KnowledgeAccessor { private[this] var _lastModuleContext: ModuleContext = _ - private[this] var _lib: WithGlobals[CoreJSLib.Lib] = _ + private[this] var _lib: WithGlobals[CoreJSLib.Lib[List[E]]] = _ - def build(moduleContext: ModuleContext): WithGlobals[CoreJSLib.Lib] = { + def build(moduleContext: ModuleContext): WithGlobals[CoreJSLib.Lib[List[E]]] = { if (_lib == null || _lastModuleContext != moduleContext) { - _lib = CoreJSLib.build(sjsGen, moduleContext, this) + _lib = CoreJSLib.build(sjsGen, postTransform(_, 0), moduleContext, this) _lastModuleContext = moduleContext } _lib @@ -979,9 +1028,9 @@ final class Emitter(config: Emitter.Config) { object Emitter { /** Result of an emitter run. */ - final class Result private[Emitter]( + final class Result[E] private[Emitter]( val header: String, - val body: Map[ModuleID, List[js.Tree]], + val body: Map[ModuleID, List[E]], val footer: String, val topLevelVarDecls: List[String], val globalRefs: Set[String] @@ -1052,22 +1101,26 @@ object Emitter { new Config(coreSpec.semantics, coreSpec.moduleKind, coreSpec.esFeatures) } - private final class DesugaredClassCache { - val privateJSFields = new OneTimeCache[WithGlobals[List[js.Tree]]] - val storeJSSuperClass = new OneTimeCache[WithGlobals[js.Tree]] - val instanceTests = new OneTimeCache[WithGlobals[List[js.Tree]]] - val typeData = new OneTimeCache[WithGlobals[List[js.Tree]]] - val setTypeData = new OneTimeCache[js.Tree] - val moduleAccessor = new OneTimeCache[WithGlobals[List[js.Tree]]] - val staticInitialization = new OneTimeCache[List[js.Tree]] - val staticFields = new OneTimeCache[WithGlobals[List[js.Tree]]] + trait PostTransformer[E] { + def transformStats(trees: List[js.Tree], indent: Int): List[E] + } + + private final class DesugaredClassCache[E >: Null] { + val privateJSFields = new OneTimeCache[WithGlobals[E]] + val storeJSSuperClass = new OneTimeCache[WithGlobals[E]] + val instanceTests = new OneTimeCache[WithGlobals[E]] + val typeData = new OneTimeCache[WithGlobals[E]] + val setTypeData = new OneTimeCache[E] + val moduleAccessor = new OneTimeCache[WithGlobals[E]] + val staticInitialization = new OneTimeCache[E] + val staticFields = new OneTimeCache[WithGlobals[E]] } - private final class GeneratedClass( + private final class GeneratedClass[E]( val className: ClassName, - val main: List[js.Tree], - val staticFields: List[js.Tree], - val staticInitialization: List[js.Tree], + val main: List[E], + val staticFields: List[E], + val staticInitialization: List[E], val trackedGlobalRefs: Set[String] ) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala index 4d675d4dec..a6d632a1cd 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala @@ -12,6 +12,8 @@ package org.scalajs.linker.backend.javascript +import java.nio.charset.StandardCharsets + import scala.annotation.switch // Unimport default print and println to avoid invoking them by mistake @@ -31,10 +33,10 @@ import Trees._ object Printers { private val ReusableIndentArray = Array.fill(128)(' '.toByte) - class JSTreePrinter(protected val out: ByteArrayWriter) { + class JSTreePrinter(protected val out: ByteArrayWriter, initIndent: Int = 0) { private final val IndentStep = 2 - private var indentMargin = 0 + private var indentMargin = initIndent * IndentStep private var indentArray = ReusableIndentArray private def indent(): Unit = indentMargin += IndentStep @@ -117,10 +119,15 @@ object Printers { printRow(args, '(', ')') /** Prints a stat including leading indent and trailing newline. */ - final def printStat(tree: Tree): Unit = { - printIndent() - printTree(tree, isStat = true) - println() + final def printStat(tree: Tree): Unit = tree match { + case tree: PrintedTree => + // PrintedTree already contains indent and trailing newline. + print(tree) + + case _ => + printIndent() + printTree(tree, isStat = true) + println() } private def print(tree: Tree): Unit = @@ -750,6 +757,9 @@ object Printers { print("]") } + protected def print(printedTree: PrintedTree): Unit = + out.write(printedTree.jsCode) + private def print(exportName: ExportName): Unit = printEscapeJS(exportName.name) @@ -762,7 +772,8 @@ object Printers { } class JSTreePrinterWithSourceMap(_out: ByteArrayWriter, - sourceMap: SourceMapWriter.Builder) extends JSTreePrinter(_out) { + sourceMap: SourceMapWriter.Builder, initIndent: Int) + extends JSTreePrinter(_out, initIndent) { private var column = 0 @@ -788,6 +799,11 @@ object Printers { sourceMap.endNode(column) } + override protected def print(printedTree: PrintedTree): Unit = { + super.print(printedTree) + sourceMap.insertFragment(printedTree.sourceMapFragment) + } + override protected def println(): Unit = { super.println() sourceMap.nextLine() diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala index efcf98e609..ec5b72e850 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala @@ -499,4 +499,22 @@ object Trees { from: StringLiteral)( implicit val pos: Position) extends Tree + + /** An already printed tree. + * + * This is a special purpose node to store partially transformed trees. + * + * A cleaner abstraction would be to have something like ir.Tree.Transient + * (for different output formats), but for now, we do not need this. + */ + sealed case class PrintedTree(jsCode: Array[Byte], + sourceMapFragment: SourceMapWriter.Fragment) extends Tree { + val pos: Position = Position.NoPosition + + override def show: String = new String(jsCode, StandardCharsets.UTF_8) + } + + object PrintedTree { + def empty: PrintedTree = PrintedTree(Array(), SourceMapWriter.Fragment.Empty) + } } diff --git a/linker/shared/src/test/scala/org/scalajs/linker/BasicLinkerBackendTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/BasicLinkerBackendTest.scala index 1ce36b9153..2da5fe2324 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/BasicLinkerBackendTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/BasicLinkerBackendTest.scala @@ -20,6 +20,7 @@ import org.junit.Test import org.junit.Assert._ import org.scalajs.ir.Trees._ +import org.scalajs.ir.Version import org.scalajs.junit.async._ @@ -33,17 +34,17 @@ import org.scalajs.logging._ class BasicLinkerBackendTest { import scala.concurrent.ExecutionContext.Implicits.global - private val BackendInvalidatedTopLevelTreesStatsMessage = - raw"""BasicBackend: total top-level trees: (\d+); re-computed: (\d+)""".r + private val BackendInvalidatedPrintedTreesStatsMessage = + raw"""BasicBackend: total top-level printed trees: (\d+); re-computed: (\d+)""".r private val BackendInvalidatedModulesStatsMessage = raw"""BasicBackend: total modules: (\d+); re-written: (\d+)""".r /** Makes sure that linking a "substantial" program (using `println`) twice - * does not invalidate any top-level tree nor module in the second run. + * does not invalidate any module in the second run. */ @Test - def noInvalidatedTopLevelTreeOrModuleInSecondRun(): AsyncResult = await { + def noInvalidatedModuleInSecondRun(): AsyncResult = await { import ModuleSplitStyle._ val classDefs = List( @@ -60,7 +61,7 @@ class BasicLinkerBackendTest { .withModuleSplitStyle(splitStyle) val linker = StandardImpl.linker(config) - val classDefsFiles = classDefs.map(MemClassDefIRFile(_)) + val classDefsFiles = classDefs.map(MemClassDefIRFile(_, Version.fromInt(0))) val initializers = MainTestModuleInitializers val outputDir = MemOutputDirectory() @@ -74,25 +75,6 @@ class BasicLinkerBackendTest { val lines1 = logger1.allLogLines val lines2 = logger2.allLogLines - // Top-level trees - - val Seq(totalTrees1, recomputedTrees1) = - lines1.assertContainsMatch(BackendInvalidatedTopLevelTreesStatsMessage).map(_.toInt) - - val Seq(totalTrees2, recomputedTrees2) = - lines2.assertContainsMatch(BackendInvalidatedTopLevelTreesStatsMessage).map(_.toInt) - - // At the time of writing this test, totalTrees1 reports 382 trees - assertTrue( - s"Not enough total top-level trees (got $totalTrees1); extraction must have gone wrong", - totalTrees1 > 300) - - assertEquals("First run must invalidate every top-level tree", totalTrees1, recomputedTrees1) - assertEquals("Second run must have the same total top-level trees as first run", totalTrees1, totalTrees2) - assertEquals("Second run must not invalidate any top-level tree", 0, recomputedTrees2) - - // Modules - val Seq(totalModules1, rewrittenModules1) = lines1.assertContainsMatch(BackendInvalidatedModulesStatsMessage).map(_.toInt) diff --git a/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala index 17512130bc..935c2a57ae 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala @@ -20,6 +20,7 @@ import org.junit.Test import org.junit.Assert._ import org.scalajs.ir.Trees._ +import org.scalajs.ir.Version import org.scalajs.junit.async._ @@ -128,6 +129,99 @@ class EmitterTest { logger.allLogLines.assertContains(EmitterSetOfDangerousGlobalRefsChangedMessage) } } + + private val EmitterClassTreeCacheStatsMessage = + raw"""Emitter: Class tree cache stats: reused: (\d+) -- invalidated: (\d+)""".r + + private val EmitterMethodTreeCacheStatsMessage = + raw"""Emitter: Method tree cache stats: reused: (\d+) -- invalidated: (\d+)""".r + + private val EmitterPostTransformStatsMessage = + raw"""Emitter: Post transforms: total: (\d+) -- nested: (\d+) -- nested avoided: (\d+)""".r + + /** Makes sure that linking a "substantial" program (using `println`) twice + * does not invalidate any cache or top-level tree in the second run. + */ + @Test + def noInvalidatedCacheOrTopLevelTreeInSecondRun(): AsyncResult = await { + val classDefs = List( + mainTestClassDef(systemOutPrintln(str("Hello world!"))) + ) + + val logger1 = new CapturingLogger + val logger2 = new CapturingLogger + + val config = StandardConfig() + .withCheckIR(true) + .withModuleKind(ModuleKind.ESModule) + + val linker = StandardImpl.linker(config) + val classDefsFiles = classDefs.map(MemClassDefIRFile(_, Version.fromInt(0))) + + val initializers = MainTestModuleInitializers + val outputDir = MemOutputDirectory() + + for { + javalib <- TestIRRepo.javalib + allIRFiles = javalib ++ classDefsFiles + _ <- linker.link(allIRFiles, initializers, outputDir, logger1) + _ <- linker.link(allIRFiles, initializers, outputDir, logger2) + } yield { + val lines1 = logger1.allLogLines + val lines2 = logger2.allLogLines + + // Class tree caches + + val Seq(classCacheReused1, classCacheInvalidated1) = + lines1.assertContainsMatch(EmitterClassTreeCacheStatsMessage).map(_.toInt) + + val Seq(classCacheReused2, classCacheInvalidated2) = + lines2.assertContainsMatch(EmitterClassTreeCacheStatsMessage).map(_.toInt) + + // At the time of writing this test, classCacheInvalidated1 reports 47 + assertTrue( + s"Not enough invalidated class caches (got $classCacheInvalidated1); extraction must have gone wrong", + classCacheInvalidated1 > 40) + + assertEquals("First run must not reuse any class cache", 0, classCacheReused1) + + assertEquals("Second run must reuse all class caches", classCacheReused2, classCacheInvalidated1) + assertEquals("Second run must not invalidate any class cache", 0, classCacheInvalidated2) + + // Method tree caches + + val Seq(methodCacheReused1, methodCacheInvalidated1) = + lines1.assertContainsMatch(EmitterMethodTreeCacheStatsMessage).map(_.toInt) + + val Seq(methodCacheReused2, methodCacheInvalidated2) = + lines2.assertContainsMatch(EmitterMethodTreeCacheStatsMessage).map(_.toInt) + + // At the time of writing this test, methodCacheInvalidated1 reports 107 + assertTrue( + s"Not enough invalidated method caches (got $methodCacheInvalidated1); extraction must have gone wrong", + methodCacheInvalidated1 > 100) + + assertEquals("First run must not reuse any method cache", 0, methodCacheReused1) + + assertEquals("Second run must reuse all method caches", methodCacheReused2, methodCacheInvalidated1) + assertEquals("Second run must not invalidate any method cache", 0, methodCacheInvalidated2) + + // Post transforms + + val Seq(postTransforms1, _, _) = + lines1.assertContainsMatch(EmitterPostTransformStatsMessage).map(_.toInt) + + val Seq(postTransforms2, _, _) = + lines2.assertContainsMatch(EmitterPostTransformStatsMessage).map(_.toInt) + + // At the time of writing this test, postTransformsTotal1 reports 216 + assertTrue( + s"Not enough post transforms (got $postTransforms1); extraction must have gone wrong", + postTransforms1 > 200) + + assertEquals("Second run must not have any post transforms", 0, postTransforms2) + } + } } object EmitterTest { diff --git a/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala index 86c9215f02..ba4848f668 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala @@ -14,7 +14,7 @@ package org.scalajs.linker.backend.javascript import scala.language.implicitConversions -import java.nio.charset.StandardCharsets +import java.nio.charset.StandardCharsets.UTF_8 import org.junit.Test import org.junit.Assert._ @@ -35,7 +35,7 @@ class PrintersTest { val printer = new Printers.JSTreePrinter(out) printer.printStat(tree) assertEquals(expected.stripMargin.trim + "\n", - new String(out.toByteArray(), StandardCharsets.UTF_8)) + new String(out.toByteArray(), UTF_8)) } @Test def printFunctionDef(): Unit = { @@ -158,4 +158,24 @@ class PrintersTest { If(BooleanLiteral(true), IntLiteral(2), IntLiteral(3))) ) } + + @Test def showPrintedTree(): Unit = { + val tree = PrintedTree("test".getBytes(UTF_8), SourceMapWriter.Fragment.Empty) + + assertEquals("test", tree.show) + } + + @Test def showNestedPrintedTree(): Unit = { + val tree = PrintedTree(" test\n".getBytes(UTF_8), SourceMapWriter.Fragment.Empty) + + val str = While(BooleanLiteral(false), tree).show + assertEquals( + """ + |while (false) { + | test + |} + """.stripMargin.trim, + str + ) + } } From 5c56042c11adc6df0f830c71d35b053864bc0b66 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 17 Dec 2023 18:59:07 +0100 Subject: [PATCH 028/298] Track in Emitter whether a module changed in an incremental run In the next commit, we want to avoid caching entire classes because of the memory cost. However, the BasicLinkerBackend relies on the identity of the generated trees to detect changes: Since that identity will change if we stop caching them, we need to provide an explicit "changed" signal. --- .../closure/ClosureLinkerBackend.scala | 3 +- .../linker/backend/BasicLinkerBackend.scala | 19 +--- .../linker/backend/emitter/Emitter.scala | 98 ++++++++++++------- 3 files changed, 68 insertions(+), 52 deletions(-) diff --git a/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureLinkerBackend.scala b/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureLinkerBackend.scala index 1f532767e2..64160204ac 100644 --- a/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureLinkerBackend.scala +++ b/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureLinkerBackend.scala @@ -106,7 +106,8 @@ final class ClosureLinkerBackend(config: LinkerBackendImpl.Config) sjsModule <- moduleSet.modules.headOption } yield { val closureChunk = logger.time("Closure: Create trees)") { - buildChunk(emitterResult.body(sjsModule.id)) + val (trees, _) = emitterResult.body(sjsModule.id) + buildChunk(trees) } logger.time("Closure: Compiler pass") { diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala index 4faef57c0a..e07e31597b 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala @@ -88,9 +88,7 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) val writer = new OutputWriter(output, config, skipContentCheck) { protected def writeModuleWithoutSourceMap(moduleID: ModuleID, force: Boolean): Option[ByteBuffer] = { val cache = printedModuleSetCache.getModuleCache(moduleID) - val printedTrees = emitterResult.body(moduleID) - - val changed = cache.update(printedTrees) + val (printedTrees, changed) = emitterResult.body(moduleID) if (force || changed || allChanged) { rewrittenModules.incrementAndGet() @@ -114,9 +112,7 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) protected def writeModuleWithSourceMap(moduleID: ModuleID, force: Boolean): Option[(ByteBuffer, ByteBuffer)] = { val cache = printedModuleSetCache.getModuleCache(moduleID) - val printedTrees = emitterResult.body(moduleID) - - val changed = cache.update(printedTrees) + val (printedTrees, changed) = emitterResult.body(moduleID) if (force || changed || allChanged) { rewrittenModules.incrementAndGet() @@ -220,8 +216,6 @@ private object BasicLinkerBackend { private sealed class PrintedModuleCache { private var cacheUsed = false - private var changed = false - private var lastPrintedTrees: List[js.PrintedTree] = Nil private var previousFinalJSFileSize: Int = 0 private var previousFinalSourceMapSize: Int = 0 @@ -239,15 +233,6 @@ private object BasicLinkerBackend { previousFinalSourceMapSize = finalSourceMapSize } - def update(newPrintedTrees: List[js.PrintedTree]): Boolean = { - val changed = !newPrintedTrees.corresponds(lastPrintedTrees)(_ eq _) - this.changed = changed - if (changed) { - lastPrintedTrees = newPrintedTrees - } - changed - } - def cleanAfterRun(): Boolean = { val wasUsed = cacheUsed cacheUsed = false diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala index 6035764aca..8aafe7b745 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala @@ -112,7 +112,7 @@ final class Emitter[E >: Null <: js.Tree]( } private def emitInternal(moduleSet: ModuleSet, - logger: Logger): WithGlobals[Map[ModuleID, List[E]]] = { + logger: Logger): WithGlobals[Map[ModuleID, (List[E], Boolean)]] = { // Reset caching stats. statsClassesReused = 0 statsClassesInvalidated = 0 @@ -169,7 +169,7 @@ final class Emitter[E >: Null <: js.Tree]( */ @tailrec private def emitAvoidGlobalClash(moduleSet: ModuleSet, - logger: Logger, secondAttempt: Boolean): WithGlobals[Map[ModuleID, List[E]]] = { + logger: Logger, secondAttempt: Boolean): WithGlobals[Map[ModuleID, (List[E], Boolean)]] = { val result = emitOnce(moduleSet, logger) val mentionedDangerousGlobalRefs = @@ -194,7 +194,7 @@ final class Emitter[E >: Null <: js.Tree]( } private def emitOnce(moduleSet: ModuleSet, - logger: Logger): WithGlobals[Map[ModuleID, List[E]]] = { + logger: Logger): WithGlobals[Map[ModuleID, (List[E], Boolean)]] = { // Genreate classes first so we can measure time separately. val generatedClasses = logger.time("Emitter: Generate Classes") { moduleSet.modules.map { module => @@ -212,18 +212,26 @@ final class Emitter[E >: Null <: js.Tree]( val moduleTrees = logger.time("Emitter: Write trees") { moduleSet.modules.map { module => + var changed = false + def extractChangedAndWithGlobals[T](x: (WithGlobals[T], Boolean)): T = { + changed ||= x._2 + extractWithGlobals(x._1) + } + val moduleContext = ModuleContext.fromModule(module) val moduleCache = state.moduleCaches.getOrElseUpdate(module.id, new ModuleCache) val moduleClasses = generatedClasses(module.id) - val moduleImports = extractWithGlobals { + changed ||= moduleClasses.exists(_.changed) + + val moduleImports = extractChangedAndWithGlobals { moduleCache.getOrComputeImports(module.externalDependencies, module.internalDependencies) { genModuleImports(module).map(postTransform(_, 0)) } } - val topLevelExports = extractWithGlobals { + val topLevelExports = extractChangedAndWithGlobals { /* We cache top level exports all together, rather than individually, * since typically there are few. */ @@ -233,7 +241,7 @@ final class Emitter[E >: Null <: js.Tree]( } } - val moduleInitializers = extractWithGlobals { + val moduleInitializers = extractChangedAndWithGlobals { val initializers = module.initializers.toList moduleCache.getOrComputeInitializers(initializers) { WithGlobals.list(initializers.map { initializer => @@ -324,7 +332,7 @@ final class Emitter[E >: Null <: js.Tree]( trackedGlobalRefs = unionPreserveEmpty(trackedGlobalRefs, genClass.trackedGlobalRefs) } - module.id -> allTrees + module.id -> (allTrees, changed) } } @@ -382,8 +390,14 @@ final class Emitter[E >: Null <: js.Tree]( val classCache = classCaches.getOrElseUpdate( new ClassID(linkedClass.ancestors, moduleContext), new ClassCache) + var changed = false + def extractChanged[T](x: (T, Boolean)): T = { + changed ||= x._2 + x._1 + } + val classTreeCache = - classCache.getCache(linkedClass.version) + extractChanged(classCache.getCache(linkedClass.version)) val kind = linkedClass.kind @@ -396,6 +410,9 @@ final class Emitter[E >: Null <: js.Tree]( withGlobals.value } + def extractWithGlobalsAndChanged[T](x: (WithGlobals[T], Boolean)): T = + extractWithGlobals(extractChanged(x)) + // Main part val main = List.newBuilder[E] @@ -426,7 +443,7 @@ final class Emitter[E >: Null <: js.Tree]( val methodCache = classCache.getStaticLikeMethodCache(namespace, methodDef.methodName) - main ++= extractWithGlobals(methodCache.getOrElseUpdate(methodDef.version, { + main ++= extractWithGlobalsAndChanged(methodCache.getOrElseUpdate(methodDef.version, { classEmitter.genStaticLikeMethod(className, methodDef)(moduleContext, methodCache) .map(postTransform(_, 0)) })) @@ -486,7 +503,7 @@ final class Emitter[E >: Null <: js.Tree]( } // JS constructor - val ctorWithGlobals = { + val ctorWithGlobals = extractChanged { /* The constructor depends both on the class version, and the version * of the inlineable init, if there is one. * @@ -571,13 +588,13 @@ final class Emitter[E >: Null <: js.Tree]( classCache.getMemberMethodCache(method.methodName) val version = Version.combine(isJSClassVersion, method.version) - methodCache.getOrElseUpdate(version, + extractChanged(methodCache.getOrElseUpdate(version, classEmitter.genMemberMethod( className, // invalidated by overall class cache isJSClass, // invalidated by isJSClassVersion useESClass, // invalidated by isJSClassVersion method // invalidated by method.version - )(moduleContext, methodCache).map(postTransform(_, memberIndent))) + )(moduleContext, methodCache).map(postTransform(_, memberIndent)))) } // Exported Members @@ -586,13 +603,13 @@ final class Emitter[E >: Null <: js.Tree]( } yield { val memberCache = classCache.getExportedMemberCache(idx) val version = Version.combine(isJSClassVersion, member.version) - memberCache.getOrElseUpdate(version, + extractChanged(memberCache.getOrElseUpdate(version, classEmitter.genExportedMember( className, // invalidated by overall class cache isJSClass, // invalidated by isJSClassVersion useESClass, // invalidated by isJSClassVersion member // invalidated by version - )(moduleContext, memberCache).map(postTransform(_, memberIndent))) + )(moduleContext, memberCache).map(postTransform(_, memberIndent)))) } val hasClassInitializer: Boolean = { @@ -602,7 +619,7 @@ final class Emitter[E >: Null <: js.Tree]( } } - val fullClass = { + val fullClass = extractChanged { val fullClassCache = classCache.getFullClassCache() fullClassCache.getOrElseUpdate(linkedClass.version, ctorWithGlobals, @@ -714,7 +731,8 @@ final class Emitter[E >: Null <: js.Tree]( main.result(), staticFields, staticInitialization, - trackedGlobalRefs + trackedGlobalRefs, + changed ) } @@ -751,7 +769,7 @@ final class Emitter[E >: Null <: js.Tree]( } def getOrComputeImports(externalDependencies: Set[String], internalDependencies: Set[ModuleID])( - compute: => WithGlobals[List[E]]): WithGlobals[List[E]] = { + compute: => WithGlobals[List[E]]): (WithGlobals[List[E]], Boolean) = { _cacheUsed = true @@ -759,20 +777,25 @@ final class Emitter[E >: Null <: js.Tree]( _importsCache = compute _lastExternalDependencies = externalDependencies _lastInternalDependencies = internalDependencies + (_importsCache, true) + } else { + (_importsCache, false) } - _importsCache + } def getOrComputeTopLevelExports(topLevelExports: List[LinkedTopLevelExport])( - compute: => WithGlobals[List[E]]): WithGlobals[List[E]] = { + compute: => WithGlobals[List[E]]): (WithGlobals[List[E]], Boolean) = { _cacheUsed = true if (!sameTopLevelExports(topLevelExports, _lastTopLevelExports)) { _topLevelExportsCache = compute _lastTopLevelExports = topLevelExports + (_topLevelExportsCache, true) + } else { + (_topLevelExportsCache, false) } - _topLevelExportsCache } private def sameTopLevelExports(tles1: List[LinkedTopLevelExport], tles2: List[LinkedTopLevelExport]): Boolean = { @@ -803,15 +826,17 @@ final class Emitter[E >: Null <: js.Tree]( } def getOrComputeInitializers(initializers: List[ModuleInitializer.Initializer])( - compute: => WithGlobals[List[E]]): WithGlobals[List[E]] = { + compute: => WithGlobals[List[E]]): (WithGlobals[List[E]], Boolean) = { _cacheUsed = true if (initializers != _lastInitializers) { _initializersCache = compute _lastInitializers = initializers + (_initializersCache, true) + } else { + (_initializersCache, false) } - _initializersCache } def cleanAfterRun(): Boolean = { @@ -856,17 +881,18 @@ final class Emitter[E >: Null <: js.Tree]( _fullClassCache.foreach(_.startRun()) } - def getCache(version: Version): DesugaredClassCache[List[E]] = { + def getCache(version: Version): (DesugaredClassCache[List[E]], Boolean) = { + _cacheUsed = true if (_cache == null || !_lastVersion.sameVersion(version)) { invalidate() statsClassesInvalidated += 1 _lastVersion = version _cache = new DesugaredClassCache[List[E]] + (_cache, true) } else { statsClassesReused += 1 + (_cache, false) } - _cacheUsed = true - _cache } def getMemberMethodCache( @@ -932,17 +958,18 @@ final class Emitter[E >: Null <: js.Tree]( def startRun(): Unit = _cacheUsed = false def getOrElseUpdate(version: Version, - v: => WithGlobals[T]): WithGlobals[T] = { + v: => WithGlobals[T]): (WithGlobals[T], Boolean) = { + _cacheUsed = true if (_tree == null || !_lastVersion.sameVersion(version)) { invalidate() statsMethodsInvalidated += 1 _tree = v _lastVersion = version + (_tree, true) } else { statsMethodsReused += 1 + (_tree, false) } - _cacheUsed = true - _tree } def cleanAfterRun(): Boolean = { @@ -974,7 +1001,7 @@ final class Emitter[E >: Null <: js.Tree]( def getOrElseUpdate(version: Version, ctor: WithGlobals[List[E]], memberMethods: List[WithGlobals[List[E]]], exportedMembers: List[WithGlobals[List[E]]], - compute: => WithGlobals[List[E]]): WithGlobals[List[E]] = { + compute: => WithGlobals[List[E]]): (WithGlobals[List[E]], Boolean) = { @tailrec def allSame[A <: AnyRef](xs: List[A], ys: List[A]): Boolean = { @@ -984,6 +1011,8 @@ final class Emitter[E >: Null <: js.Tree]( } } + _cacheUsed = true + if (_tree == null || !version.sameVersion(_lastVersion) || (_lastCtor ne ctor) || !allSame(_lastMemberMethods, memberMethods) || !allSame(_lastExportedMembers, exportedMembers)) { @@ -993,10 +1022,10 @@ final class Emitter[E >: Null <: js.Tree]( _lastCtor = ctor _lastMemberMethods = memberMethods _lastExportedMembers = exportedMembers + (_tree, true) + } else { + (_tree, false) } - - _cacheUsed = true - _tree } def cleanAfterRun(): Boolean = { @@ -1030,7 +1059,7 @@ object Emitter { /** Result of an emitter run. */ final class Result[E] private[Emitter]( val header: String, - val body: Map[ModuleID, List[E]], + val body: Map[ModuleID, (List[E], Boolean)], val footer: String, val topLevelVarDecls: List[String], val globalRefs: Set[String] @@ -1121,7 +1150,8 @@ object Emitter { val main: List[E], val staticFields: List[E], val staticInitialization: List[E], - val trackedGlobalRefs: Set[String] + val trackedGlobalRefs: Set[String], + val changed: Boolean ) private final class OneTimeCache[A >: Null] { From 42efb2aa153b3feea58cda36ee0d25f896e52202 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sat, 23 Dec 2023 18:07:04 +0100 Subject: [PATCH 029/298] Do not cache overall class This reduces some memory overhead for negligible performance cost. Residual (post link memory) benchmarks for the test suite: Baseline: 1.13 GB, new 1.01 GB --- .../linker/backend/emitter/Emitter.scala | 119 +++++++++--------- .../org/scalajs/linker/EmitterTest.scala | 11 +- 2 files changed, 70 insertions(+), 60 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala index 8aafe7b745..1906ffe84f 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala @@ -619,37 +619,41 @@ final class Emitter[E >: Null <: js.Tree]( } } - val fullClass = extractChanged { - val fullClassCache = classCache.getFullClassCache() - - fullClassCache.getOrElseUpdate(linkedClass.version, ctorWithGlobals, - memberMethodsWithGlobals, exportedMembersWithGlobals, { - for { - ctor <- ctorWithGlobals - memberMethods <- WithGlobals.flatten(memberMethodsWithGlobals) - exportedMembers <- WithGlobals.flatten(exportedMembersWithGlobals) - allMembers = ctor ::: memberMethods ::: exportedMembers - clazz <- classEmitter.buildClass( - className, // invalidated by overall class cache (part of ancestors) - isJSClass, // invalidated by class version - linkedClass.jsClassCaptures, // invalidated by class version - hasClassInitializer, // invalidated by class version (optimizer cannot remove it) - linkedClass.superClass, // invalidated by class version - storeJSSuperClass, // invalidated by class version - useESClass, // invalidated by class version (depends on kind, config and ancestry only) - allMembers // invalidated directly - )(moduleContext, fullClassCache, linkedClass.pos) // pos invalidated by class version - } yield { - // Avoid a nested post transform if we just got the original members back. - if (clazz eq allMembers) { - statsNestedPostTransformsAvoided += 1 - allMembers - } else { - statsNestedPostTransforms += 1 - postTransform(clazz, 0) - } + val fullClass = { + val fullClassChangeTracker = classCache.getFullClassChangeTracker() + + // Put changed state into a val to avoid short circuiting behavior of ||. + val classChanged = fullClassChangeTracker.trackChanged( + linkedClass.version, ctorWithGlobals, + memberMethodsWithGlobals, exportedMembersWithGlobals) + + changed ||= classChanged + + for { + ctor <- ctorWithGlobals + memberMethods <- WithGlobals.flatten(memberMethodsWithGlobals) + exportedMembers <- WithGlobals.flatten(exportedMembersWithGlobals) + allMembers = ctor ::: memberMethods ::: exportedMembers + clazz <- classEmitter.buildClass( + className, // invalidated by overall class cache (part of ancestors) + isJSClass, // invalidated by class version + linkedClass.jsClassCaptures, // invalidated by class version + hasClassInitializer, // invalidated by class version (optimizer cannot remove it) + linkedClass.superClass, // invalidated by class version + storeJSSuperClass, // invalidated by class version + useESClass, // invalidated by class version (depends on kind, config and ancestry only) + allMembers // invalidated directly + )(moduleContext, fullClassChangeTracker, linkedClass.pos) // pos invalidated by class version + } yield { + // Avoid a nested post transform if we just got the original members back. + if (clazz eq allMembers) { + statsNestedPostTransformsAvoided += 1 + allMembers + } else { + statsNestedPostTransforms += 1 + postTransform(clazz, 0) } - }) + } } main ++= extractWithGlobals(fullClass) @@ -862,7 +866,7 @@ final class Emitter[E >: Null <: js.Tree]( private[this] val _exportedMembersCache = mutable.Map.empty[Int, MethodCache[List[E]]] - private[this] var _fullClassCache: Option[FullClassCache] = None + private[this] var _fullClassChangeTracker: Option[FullClassChangeTracker] = None override def invalidate(): Unit = { /* Do not invalidate contained methods, as they have their own @@ -878,7 +882,7 @@ final class Emitter[E >: Null <: js.Tree]( _methodCaches.foreach(_.valuesIterator.foreach(_.startRun())) _memberMethodCache.valuesIterator.foreach(_.startRun()) _constructorCache.foreach(_.startRun()) - _fullClassCache.foreach(_.startRun()) + _fullClassChangeTracker.foreach(_.startRun()) } def getCache(version: Version): (DesugaredClassCache[List[E]], Boolean) = { @@ -917,10 +921,10 @@ final class Emitter[E >: Null <: js.Tree]( def getExportedMemberCache(idx: Int): MethodCache[List[E]] = _exportedMembersCache.getOrElseUpdate(idx, new MethodCache) - def getFullClassCache(): FullClassCache = { - _fullClassCache.getOrElse { - val cache = new FullClassCache - _fullClassCache = Some(cache) + def getFullClassChangeTracker(): FullClassChangeTracker = { + _fullClassChangeTracker.getOrElse { + val cache = new FullClassChangeTracker + _fullClassChangeTracker = Some(cache) cache } } @@ -934,8 +938,8 @@ final class Emitter[E >: Null <: js.Tree]( _exportedMembersCache.filterInPlace((_, c) => c.cleanAfterRun()) - if (_fullClassCache.exists(!_.cleanAfterRun())) - _fullClassCache = None + if (_fullClassChangeTracker.exists(!_.cleanAfterRun())) + _fullClassChangeTracker = None if (!_cacheUsed) invalidate() @@ -980,28 +984,26 @@ final class Emitter[E >: Null <: js.Tree]( } } - private class FullClassCache extends knowledgeGuardian.KnowledgeAccessor { - private[this] var _tree: WithGlobals[List[E]] = null + private class FullClassChangeTracker extends knowledgeGuardian.KnowledgeAccessor { private[this] var _lastVersion: Version = Version.Unversioned private[this] var _lastCtor: WithGlobals[List[E]] = null private[this] var _lastMemberMethods: List[WithGlobals[List[E]]] = null private[this] var _lastExportedMembers: List[WithGlobals[List[E]]] = null - private[this] var _cacheUsed = false + private[this] var _trackerUsed = false override def invalidate(): Unit = { super.invalidate() - _tree = null _lastVersion = Version.Unversioned _lastCtor = null _lastMemberMethods = null _lastExportedMembers = null } - def startRun(): Unit = _cacheUsed = false + def startRun(): Unit = _trackerUsed = false - def getOrElseUpdate(version: Version, ctor: WithGlobals[List[E]], - memberMethods: List[WithGlobals[List[E]]], exportedMembers: List[WithGlobals[List[E]]], - compute: => WithGlobals[List[E]]): (WithGlobals[List[E]], Boolean) = { + def trackChanged(version: Version, ctor: WithGlobals[List[E]], + memberMethods: List[WithGlobals[List[E]]], + exportedMembers: List[WithGlobals[List[E]]]): Boolean = { @tailrec def allSame[A <: AnyRef](xs: List[A], ys: List[A]): Boolean = { @@ -1011,28 +1013,33 @@ final class Emitter[E >: Null <: js.Tree]( } } - _cacheUsed = true + _trackerUsed = true - if (_tree == null || !version.sameVersion(_lastVersion) || (_lastCtor ne ctor) || - !allSame(_lastMemberMethods, memberMethods) || - !allSame(_lastExportedMembers, exportedMembers)) { + val changed = { + !version.sameVersion(_lastVersion) || + (_lastCtor ne ctor) || + !allSame(_lastMemberMethods, memberMethods) || + !allSame(_lastExportedMembers, exportedMembers) + } + + if (changed) { + // Input has changed or we were invalidated. + // Clean knowledge tracking and re-track dependencies. invalidate() - _tree = compute _lastVersion = version _lastCtor = ctor _lastMemberMethods = memberMethods _lastExportedMembers = exportedMembers - (_tree, true) - } else { - (_tree, false) } + + changed } def cleanAfterRun(): Boolean = { - if (!_cacheUsed) + if (!_trackerUsed) invalidate() - _cacheUsed + _trackerUsed } } diff --git a/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala index 935c2a57ae..1f7884c0f1 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala @@ -208,18 +208,21 @@ class EmitterTest { // Post transforms - val Seq(postTransforms1, _, _) = + val Seq(postTransforms1, nestedPostTransforms1, _) = lines1.assertContainsMatch(EmitterPostTransformStatsMessage).map(_.toInt) - val Seq(postTransforms2, _, _) = + val Seq(postTransforms2, nestedPostTransforms2, _) = lines2.assertContainsMatch(EmitterPostTransformStatsMessage).map(_.toInt) - // At the time of writing this test, postTransformsTotal1 reports 216 + // At the time of writing this test, postTransforms1 reports 216 assertTrue( s"Not enough post transforms (got $postTransforms1); extraction must have gone wrong", postTransforms1 > 200) - assertEquals("Second run must not have any post transforms", 0, postTransforms2) + assertEquals("Second run must only have nested post transforms", + nestedPostTransforms2, postTransforms2) + assertEquals("Both runs must have the same number of nested post transforms", + nestedPostTransforms1, nestedPostTransforms2) } } } From d5cef60d4f25e875d18f9b902038b67aa15daca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 31 Jan 2024 16:51:03 +0100 Subject: [PATCH 030/298] Fix #4934: Report errors in Analyzer only after all tasks are completed. Previously, as soon as one task completed with a Failure, we let the WorkTracker's main promise complete with that Failure. If other tasks were still running, they would leak and potentially cause worse errors down the line. In particular, they would cause `NullPointerException`s, which are UBEs on Scala.js. --- .../scalajs/linker/analyzer/Analyzer.scala | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) 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 34a2be0d59..11a997ef27 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 @@ -1572,22 +1572,30 @@ private object AnalyzerRun { private class WorkTracker(implicit ec: ExecutionContext) { private val pending = new AtomicInteger(0) + private val failures = new AtomicReference[List[Throwable]](Nil) @volatile private var _allowComplete = false private val promise = Promise[Unit]() def track(fut: Future[Unit]): Unit = { pending.incrementAndGet() - fut.onComplete { - case Success(_) => - if (pending.decrementAndGet() == 0) - tryComplete() - - case Failure(t) => - promise.tryFailure(t) + fut.onComplete { result => + result match { + case Success(_) => () + case Failure(t) => addFailure(t) + } + if (pending.decrementAndGet() == 0) + tryComplete() } } + @tailrec + private def addFailure(t: Throwable): Unit = { + val prev = failures.get() + if (!failures.compareAndSet(prev, t :: prev)) + addFailure(t) + } + private def tryComplete(): Unit = { /* Note that after _allowComplete is true and pending == 0, we are sure * that no new task will be submitted concurrently: @@ -1596,7 +1604,14 @@ private object AnalyzerRun { * more tasks) are running anymore. */ if (_allowComplete && pending.get() == 0) { - promise.trySuccess(()) + failures.get() match { + case Nil => + promise.success(()) + case firstFailure :: moreFailures => + for (t <- moreFailures) + firstFailure.addSuppressed(t) + promise.failure(firstFailure) + } } } From 5de6c1d46a6e2d0aa53add1dcf1be7ebbe646693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 31 Jan 2024 12:11:44 +0100 Subject: [PATCH 031/298] Generalize the side-effect-free analysis of constructors to all classes. Previously, we only analyzed *module* class constructors for side-effect-freedom. This allowed us to get rid of unused `LoadModule`s. We now generalize the same analysis to all Scala classes. We take a little shortcut by bundling *all* the constructors of a class together as being side-effect-free or not. This is an over-approximation in theory, but in practice it is unlikely that it will make a difference. This shortcut allows our analysis to be straightforward even in the presence of constructor delegation chains. --- .../frontend/optimizer/IncOptimizer.scala | 81 ++++++++++--------- .../frontend/optimizer/OptimizerCore.scala | 13 +-- project/Build.scala | 8 +- .../testsuite/compiler/NullPointersTest.scala | 3 +- 4 files changed, 56 insertions(+), 49 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala index 8982107805..f6876b2a4e 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala @@ -380,8 +380,9 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: var subclasses: collOps.ParIterable[Class] = collOps.emptyParIterable var isInstantiated: Boolean = linkedClass.hasInstances - private var hasElidableModuleAccessor: Boolean = computeHasElidableModuleAccessor(linkedClass) - private val hasElidableModuleAccessorAskers = collOps.emptyMap[Processable, Unit] + /** True if *all* constructors of this class are recursively elidable. */ + private var hasElidableConstructors: Boolean = computeHasElidableConstructors(linkedClass) + private val hasElidableConstructorsAskers = collOps.emptyMap[Processable, Unit] var fields: List[AnyFieldDef] = linkedClass.fields var fieldsRead: Set[FieldName] = linkedClass.fieldsRead @@ -507,12 +508,12 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: for (methodName <- methodAttributeChanges) myInterface.tagStaticCallersOf(namespace, methodName) - // Module class specifics - val newHasElidableModuleAccessor = computeHasElidableModuleAccessor(linkedClass) - if (hasElidableModuleAccessor != newHasElidableModuleAccessor) { - hasElidableModuleAccessor = newHasElidableModuleAccessor - hasElidableModuleAccessorAskers.keysIterator.foreach(_.tag()) - hasElidableModuleAccessorAskers.clear() + // Elidable constructors + val newHasElidableConstructors = computeHasElidableConstructors(linkedClass) + if (hasElidableConstructors != newHasElidableConstructors) { + hasElidableConstructors = newHasElidableConstructors + hasElidableConstructorsAskers.keysIterator.foreach(_.tag()) + hasElidableConstructorsAskers.clear() } // Inlineable class @@ -543,25 +544,21 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: subclasses = collOps.finishAdd(subclassAcc) } - def askHasElidableModuleAccessor(asker: Processable): Boolean = { - hasElidableModuleAccessorAskers.put(asker, ()) + def askHasElidableConstructors(asker: Processable): Boolean = { + hasElidableConstructorsAskers.put(asker, ()) asker.registerTo(this) - hasElidableModuleAccessor + hasElidableConstructors } /** UPDATE PASS ONLY. */ - private def computeHasElidableModuleAccessor(linkedClass: LinkedClass): Boolean = { - def lookupModuleConstructor: Option[MethodImpl] = { - myInterface - .staticLike(MemberNamespace.Constructor) - .methods - .get(NoArgConstructorName) + private def computeHasElidableConstructors(linkedClass: LinkedClass): Boolean = { + if (isAdHocElidableConstructors(className)) { + true + } else { + superClass.forall(_.hasElidableConstructors) && // this was always updated before myself + myInterface.staticLike(MemberNamespace.Constructor) + .methods.valuesIterator.forall(computeIsElidableConstructor) } - - val isModuleClass = linkedClass.kind == ClassKind.ModuleClass - - isAdHocElidableModuleAccessor(className) || - (isModuleClass && lookupModuleConstructor.exists(isElidableModuleConstructor)) } /** UPDATE PASS ONLY. */ @@ -624,16 +621,17 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: } /** UPDATE PASS ONLY. */ - private def isElidableModuleConstructor(impl: MethodImpl): Boolean = { + private def computeIsElidableConstructor(impl: MethodImpl): Boolean = { def isTriviallySideEffectFree(tree: Tree): Boolean = tree match { case _:VarRef | _:Literal | _:This | _:Skip => true case _ => false } + def isElidableStat(tree: Tree): Boolean = tree match { case Block(stats) => stats.forall(isElidableStat) case Assign(Select(This(), _, _), rhs) => isTriviallySideEffectFree(rhs) - // Mixin constructor + // Mixin constructor -- test whether its body is entirely empty case ApplyStatically(flags, This(), className, methodName, Nil) if !flags.isPrivate && !classes.contains(className) => // Since className is not in classes, it must be a default method call. @@ -646,22 +644,27 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: } } - // Delegation to another constructor (super or in the same class) - case ApplyStatically(flags, This(), className, methodName, args) - if flags.isConstructor => - args.forall(isTriviallySideEffectFree) && { - getInterface(className) - .staticLike(MemberNamespace.Constructor) - .methods - .get(methodName.name) - .exists(isElidableModuleConstructor) - } + /* Delegation to another constructor (super or in the same class) + * + * - for super constructor calls, we have already checked before getting + * here that the super class has elidable constructors, so by + * construction they are elidable and we do not need to test them + * - for other constructors in the same class, we will collectively + * treat them as all-elidable or non-elidable; therefore, we do not + * need to check them either at this point. + * + * We only need to check the arguments to the constructor, not their + * bodies. + */ + case ApplyStatically(flags, This(), _, _, args) if flags.isConstructor => + args.forall(isTriviallySideEffectFree) case StoreModule(_, _) => true case _ => isTriviallySideEffectFree(tree) } + impl.originalDef.body.fold { - throw new AssertionError("Module constructor cannot be abstract") + throw new AssertionError("Constructor cannot be abstract") } { body => isElidableStat(body) } @@ -692,7 +695,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: } def unregisterDependee(dependee: Processable): Unit = { - hasElidableModuleAccessorAskers.remove(dependee) + hasElidableConstructorsAskers.remove(dependee) } } @@ -1349,8 +1352,8 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: protected def getAncestorsOf(intfName: ClassName): List[ClassName] = getInterface(intfName).askAncestors(asker) - protected def hasElidableModuleAccessor(moduleClassName: ClassName): Boolean = - classes(moduleClassName).askHasElidableModuleAccessor(asker) + protected def hasElidableConstructors(className: ClassName): Boolean = + classes(className).askHasElidableConstructors(asker) protected def tryNewInlineableClass( className: ClassName): Option[OptimizerCore.InlineableClassStructure] = { @@ -1380,6 +1383,6 @@ object IncOptimizer { def apply(config: CommonPhaseConfig): IncOptimizer = new IncOptimizer(config, SeqCollOps) - private val isAdHocElidableModuleAccessor: Set[ClassName] = + private val isAdHocElidableConstructors: Set[ClassName] = Set(ClassName("scala.Predef$")) } 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 d2454ea80f..6b65697fe8 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 @@ -66,11 +66,11 @@ private[optimizer] abstract class OptimizerCore( /** Returns the list of ancestors of a class or interface. */ protected def getAncestorsOf(className: ClassName): List[ClassName] - /** Tests whether the given module class has an elidable accessor. - * In other words, whether it is safe to discard a LoadModule of that - * module class which is not used. + /** Tests whether *all* the constructors of the given class are elidable. + * In other words, whether it is safe to discard a New or LoadModule of that + * class which is not used. */ - protected def hasElidableModuleAccessor(moduleClassName: ClassName): Boolean + protected def hasElidableConstructors(className: ClassName): Boolean /** Tests whether the given class is inlineable. * @@ -1578,8 +1578,11 @@ private[optimizer] abstract class OptimizerCore( case Skip() => keepOnlySideEffects(Block(init)(stat.pos)) case lastEffects => Block(init, lastEffects)(stat.pos) } + case New(className, _, args) => + if (hasElidableConstructors(className)) Block(args.map(keepOnlySideEffects(_)))(stat.pos) + else stat case LoadModule(moduleClassName) => - if (hasElidableModuleAccessor(moduleClassName)) Skip()(stat.pos) + if (hasElidableConstructors(moduleClassName)) Skip()(stat.pos) else stat case NewArray(_, lengths) if lengths.forall(isNonNegativeIntLiteral(_)) => Skip()(stat.pos) diff --git a/project/Build.scala b/project/Build.scala index f9d72c08e0..9ff7b777a7 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1967,17 +1967,17 @@ object Build { scalaVersion.value match { case `default212Version` => Some(ExpectedSizes( - fastLink = 768000 to 769000, + fastLink = 682000 to 683000, fullLink = 144000 to 145000, - fastLinkGz = 90000 to 91000, + fastLinkGz = 82000 to 83000, fullLinkGz = 35000 to 36000, )) case `default213Version` => Some(ExpectedSizes( - fastLink = 478000 to 479000, + fastLink = 476000 to 477000, fullLink = 101000 to 102000, - fastLinkGz = 62000 to 63000, + fastLinkGz = 61000 to 62000, fullLinkGz = 27000 to 28000, )) diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/NullPointersTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/NullPointersTest.scala index d76225c466..4ff13defd1 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/NullPointersTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/NullPointersTest.scala @@ -23,7 +23,8 @@ class NullPointersTest { import NullPointersTest._ // Instantiate Tester somewhere, otherwise plenty of tests are moot - new Tester(0) + @noinline def keep(x: Any): Unit = () + keep(new Tester(0)) @noinline private def nullOf[T >: Null]: T = null From 7814252c8869cae75652c4dad1e0bee4e48611ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 2 Feb 2024 11:28:14 +0100 Subject: [PATCH 032/298] Better codegen for the null check on outer pointers in constructors. When introducing outer pointers, scalac generates a null check in the constructor of nested classes. The null check looks like this: if ($outer.eq(null)) throw null else this.$outer = $outer This is a bad shape for our optimizations, notably because the explicit null check cannot be considered UB at the IR level if we compile it as is, although in the original *language*, that would clearly fall into UB. Therefore, we now intercept that shape and rewrite it as follows instead: ($outer) // null check subject to UB this.$outer = $outer // the `else` branch in general --- .../org/scalajs/nscplugin/GenJSCode.scala | 49 ++++++++++++++++--- .../frontend/optimizer/OptimizerCore.scala | 4 +- project/Build.scala | 6 +-- 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index e4c9ecadd9..2cf95451db 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -2270,13 +2270,50 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) toIRType(sym.tpe), sym.isMutable, rhsTree) } - case If(cond, thenp, elsep) => - val tpe = - if (isStat) jstpe.NoType - else toIRType(tree.tpe) + case tree @ If(cond, thenp, elsep) => + def default: js.Tree = { + val tpe = + if (isStat) jstpe.NoType + else toIRType(tree.tpe) + + js.If(genExpr(cond), genStatOrExpr(thenp, isStat), + genStatOrExpr(elsep, isStat))(tpe) + } - js.If(genExpr(cond), genStatOrExpr(thenp, isStat), - genStatOrExpr(elsep, isStat))(tpe) + if (isStat && currentMethodSym.isClassConstructor) { + /* Nested classes that need an outer pointer have a weird shape for + * assigning it, with an explicit null check. It looks like this: + * + * if ($outer.eq(null)) + * throw null + * else + * this.$outer = $outer + * + * This is a bad shape for our optimizations, notably because the + * explicit null check cannot be considered UB at the IR level if + * we compile it as is, although in the original *language*, that + * would clearly fall into UB. + * + * Therefore, we intercept that shape and rewrite it as follows + * instead: + * + * ($outer) // null check subject to UB + * this.$outer = $outer // the `else` branch in general + */ + tree match { + case If(Apply(fun @ Select(outer: Ident, nme.eq), Literal(Constant(null)) :: Nil), + Throw(Literal(Constant(null))), elsep) + if outer.symbol.isOuterParam && fun.symbol == definitions.Object_eq => + js.Block( + js.GetClass(genExpr(outer)), // null check + genStat(elsep) + ) + case _ => + default + } + } else { + default + } case Return(expr) => js.Return(toIRType(expr.tpe) match { 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 6b65697fe8..534fd345be 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 @@ -2791,7 +2791,9 @@ private[optimizer] abstract class OptimizerCore( * if (cond) throw e * else value * - * Typical shape of initialization of outer pointer of inner classes. + * Typical shape of initialization of outer pointer of inner classes + * coming from Scala.js < 1.15.1 (since 1.15.1, we intercept that shape + * already in the compiler back-end). */ case If(cond, th: Throw, Assign(Select(This(), _, _), value)) :: rest => // work around a bug of the compiler (these should be @-bindings) diff --git a/project/Build.scala b/project/Build.scala index 9ff7b777a7..31c63099fb 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1967,15 +1967,15 @@ object Build { scalaVersion.value match { case `default212Version` => Some(ExpectedSizes( - fastLink = 682000 to 683000, - fullLink = 144000 to 145000, + fastLink = 681000 to 682000, + fullLink = 142000 to 143000, fastLinkGz = 82000 to 83000, fullLinkGz = 35000 to 36000, )) case `default213Version` => Some(ExpectedSizes( - fastLink = 476000 to 477000, + fastLink = 475000 to 476000, fullLink = 101000 to 102000, fastLinkGz = 61000 to 62000, fullLinkGz = 27000 to 28000, From fea064dd526433181a37ae6a967866a2131f19db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 2 Feb 2024 13:10:23 +0100 Subject: [PATCH 033/298] Handle `GetClass` in the side-effect-free analysis of constructors. When NPEs are unchecked, `GetClass` is side-effect-free. This helps enormously the analysis, as it now allows many inner classes to be considered to have elidable constructors. --- .../linker/frontend/optimizer/IncOptimizer.scala | 13 ++++++++++--- project/Build.scala | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala index f6876b2a4e..2ed19a5b19 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala @@ -29,7 +29,7 @@ import org.scalajs.logging._ import org.scalajs.linker._ import org.scalajs.linker.backend.emitter.LongImpl import org.scalajs.linker.frontend.LinkingUnit -import org.scalajs.linker.interface.ModuleKind +import org.scalajs.linker.interface.{CheckedBehavior, ModuleKind} import org.scalajs.linker.standard._ import org.scalajs.linker.CollectionsCompat._ @@ -623,8 +623,15 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: /** UPDATE PASS ONLY. */ private def computeIsElidableConstructor(impl: MethodImpl): Boolean = { def isTriviallySideEffectFree(tree: Tree): Boolean = tree match { - case _:VarRef | _:Literal | _:This | _:Skip => true - case _ => false + case _:VarRef | _:Literal | _:This | _:Skip => + true + + case GetClass(expr) => + config.coreSpec.semantics.nullPointers == CheckedBehavior.Unchecked && + isTriviallySideEffectFree(expr) + + case _ => + false } def isElidableStat(tree: Tree): Boolean = tree match { diff --git a/project/Build.scala b/project/Build.scala index 31c63099fb..85ad9026ee 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1968,9 +1968,9 @@ object Build { case `default212Version` => Some(ExpectedSizes( fastLink = 681000 to 682000, - fullLink = 142000 to 143000, + fullLink = 111000 to 112000, fastLinkGz = 82000 to 83000, - fullLinkGz = 35000 to 36000, + fullLinkGz = 28000 to 29000, )) case `default213Version` => From a99abdef340a9fdd236644c232716a81e92c0873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 2 Feb 2024 14:29:29 +0100 Subject: [PATCH 034/298] Handle `Closure` in the side-effect-free analysis of constructors. --- .../org/scalajs/linker/frontend/optimizer/IncOptimizer.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala index 2ed19a5b19..9c433fb62c 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala @@ -626,6 +626,9 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: case _:VarRef | _:Literal | _:This | _:Skip => true + case Closure(_, _, _, _, _, captureValues) => + captureValues.forall(isTriviallySideEffectFree(_)) + case GetClass(expr) => config.coreSpec.semantics.nullPointers == CheckedBehavior.Unchecked && isTriviallySideEffectFree(expr) From e47c278ed9b7dac0850587e02daefa46cd58093d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 31 Jan 2024 18:01:38 +0100 Subject: [PATCH 035/298] Make the side-effect-free analysis of constructors graph-aware. Previously, as soon as a class A's constructor instantiated another class B (including accessing module instances), A's constructors would be considered non-elidable. That is even if B's constructors are elidable. In this commit, we implement a graph-based analysis that solves the above limitation. First, during the incremental pass of the linker (the UDPATE pass), we compute the "elidable constructor infos" of every class: - whether any of its constructors contains potentially side-effecting code, *other than* instantiating other classes; if yes, it is `NotElidable`; - otherwise, the set of classes that its constructors collectively instantiate, which is then `DependentOn`. We then insert a third pass in the `IncOptimizer`: the ELIDABLE CTORS pass. This pass computes the transitive closure of `NotElidable` by following the reverse `DependentOn` relationships. This pass is not incremental nor parallel. However, it is only linear in the number of classes (`Class`es, `ModuleClass`es and `HijackedClass`es). That makes it very fast regardless (on order of 10 ms for our test suite). Therefore, it remains acceptable even for `fastLink`. --- .../frontend/optimizer/IncOptimizer.scala | 129 ++++++++++++++++-- project/Build.scala | 14 +- 2 files changed, 122 insertions(+), 21 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala index 9c433fb62c..6ba6bca95b 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala @@ -83,6 +83,11 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: updateAndTagEverything(unit) } + logger.time("Optimizer: Elidable constructors") { + /** ELIDABLE CTORS PASS */ + updateElidableConstructors() + } + logger.time("Optimizer: Optimizer part") { /* PROCESS PASS */ processAllTaggedMethods(logger) @@ -259,6 +264,47 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: } + /** Elidable constructors: compute the fix point of hasElidableConstructors. + * + * ELIDABLE CTORS PASS ONLY. (This IS the elidable ctors pass). + */ + private def updateElidableConstructors(): Unit = { + import ElidableConstructorsInfo._ + + /* Invariant: when something is in the stack, its + * elidableConstructorsInfo was set to NotElidable. + */ + val toProcessStack = mutable.ArrayBuffer.empty[Class] + + // Build the graph and initial stack from the infos + for (cls <- classes.valuesIterator) { + cls.elidableConstructorsInfo match { + case NotElidable => + toProcessStack += cls + case DependentOn(dependencies) => + for (dependency <- dependencies) + classes(dependency).elidableConstructorsDependents += cls + } + } + + // Propagate + while (toProcessStack.nonEmpty) { + val cls = toProcessStack.remove(toProcessStack.size - 1) + + for (dependent <- cls.elidableConstructorsDependents) { + if (dependent.elidableConstructorsInfo != NotElidable) { + dependent.elidableConstructorsInfo = NotElidable + toProcessStack += dependent + } + } + } + + // Set the final value of hasElidableConstructors + for (cls <- classes.valuesIterator) { + cls.setHasElidableConstructors(cls.elidableConstructorsInfo != NotElidable) + } + } + /** Optimizer part: process all methods that need reoptimizing. * PROCESS PASS ONLY. (This IS the process pass). */ @@ -380,8 +426,14 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: var subclasses: collOps.ParIterable[Class] = collOps.emptyParIterable var isInstantiated: Boolean = linkedClass.hasInstances + // Temporary information used to eventually derive `hasElidableConstructors` + var elidableConstructorsInfo: ElidableConstructorsInfo = + computeElidableConstructorsInfo(linkedClass) + val elidableConstructorsDependents: mutable.ArrayBuffer[Class] = mutable.ArrayBuffer.empty + /** True if *all* constructors of this class are recursively elidable. */ - private var hasElidableConstructors: Boolean = computeHasElidableConstructors(linkedClass) + private var hasElidableConstructors: Boolean = + elidableConstructorsInfo != ElidableConstructorsInfo.NotElidable // initial educated guess private val hasElidableConstructorsAskers = collOps.emptyMap[Processable, Unit] var fields: List[AnyFieldDef] = linkedClass.fields @@ -509,12 +561,8 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: myInterface.tagStaticCallersOf(namespace, methodName) // Elidable constructors - val newHasElidableConstructors = computeHasElidableConstructors(linkedClass) - if (hasElidableConstructors != newHasElidableConstructors) { - hasElidableConstructors = newHasElidableConstructors - hasElidableConstructorsAskers.keysIterator.foreach(_.tag()) - hasElidableConstructorsAskers.clear() - } + elidableConstructorsInfo = computeElidableConstructorsInfo(linkedClass) + elidableConstructorsDependents.clear() // Inlineable class if (updateTryNewInlineable(linkedClass)) { @@ -528,6 +576,15 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: } } + /** ELIDABLE CTORS PASS ONLY. */ + def setHasElidableConstructors(newHasElidableConstructors: Boolean): Unit = { + if (hasElidableConstructors != newHasElidableConstructors) { + hasElidableConstructors = newHasElidableConstructors + hasElidableConstructorsAskers.keysIterator.foreach(_.tag()) + hasElidableConstructorsAskers.clear() + } + } + /** UPDATE PASS ONLY. */ def walkForAdditions( getNewChildren: ClassName => collOps.ParIterable[LinkedClass]): Unit = { @@ -551,13 +608,25 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: } /** UPDATE PASS ONLY. */ - private def computeHasElidableConstructors(linkedClass: LinkedClass): Boolean = { + private def computeElidableConstructorsInfo(linkedClass: LinkedClass): ElidableConstructorsInfo = { + import ElidableConstructorsInfo._ + if (isAdHocElidableConstructors(className)) { - true + AlwaysElidable } else { - superClass.forall(_.hasElidableConstructors) && // this was always updated before myself - myInterface.staticLike(MemberNamespace.Constructor) - .methods.valuesIterator.forall(computeIsElidableConstructor) + // It's OK to look at the superClass like this because it will always be updated before myself + var result = superClass.fold(ElidableConstructorsInfo.AlwaysElidable)(_.elidableConstructorsInfo) + + if (result == NotElidable) { + // fast path + result + } else { + val ctorIterator = myInterface.staticLike(MemberNamespace.Constructor).methods.valuesIterator + while (result != NotElidable && ctorIterator.hasNext) { + result = result.mergeWith(computeCtorElidableInfo(ctorIterator.next())) + } + result + } } } @@ -621,7 +690,9 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: } /** UPDATE PASS ONLY. */ - private def computeIsElidableConstructor(impl: MethodImpl): Boolean = { + private def computeCtorElidableInfo(impl: MethodImpl): ElidableConstructorsInfo = { + val dependenciesBuilder = Set.newBuilder[ClassName] + def isTriviallySideEffectFree(tree: Tree): Boolean = tree match { case _:VarRef | _:Literal | _:This | _:Skip => true @@ -633,6 +704,14 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: config.coreSpec.semantics.nullPointers == CheckedBehavior.Unchecked && isTriviallySideEffectFree(expr) + case New(className, _, args) => + dependenciesBuilder += className + args.forall(isTriviallySideEffectFree(_)) + + case LoadModule(className) => + dependenciesBuilder += className + true + case _ => false } @@ -676,7 +755,10 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: impl.originalDef.body.fold { throw new AssertionError("Constructor cannot be abstract") } { body => - isElidableStat(body) + if (isElidableStat(body)) + ElidableConstructorsInfo.DependentOn(dependenciesBuilder.result()) + else + ElidableConstructorsInfo.NotElidable } } @@ -1395,4 +1477,23 @@ object IncOptimizer { private val isAdHocElidableConstructors: Set[ClassName] = Set(ClassName("scala.Predef$")) + + sealed abstract class ElidableConstructorsInfo { + import ElidableConstructorsInfo._ + + final def mergeWith(that: ElidableConstructorsInfo): ElidableConstructorsInfo = (this, that) match { + case (DependentOn(deps1), DependentOn(deps2)) => + DependentOn(deps1 ++ deps2) + case _ => + NotElidable + } + } + + object ElidableConstructorsInfo { + case object NotElidable extends ElidableConstructorsInfo + + final case class DependentOn(dependencies: Set[ClassName]) extends ElidableConstructorsInfo + + val AlwaysElidable: ElidableConstructorsInfo = DependentOn(Set.empty) + } } diff --git a/project/Build.scala b/project/Build.scala index 85ad9026ee..9bc1560729 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1967,18 +1967,18 @@ object Build { scalaVersion.value match { case `default212Version` => Some(ExpectedSizes( - fastLink = 681000 to 682000, - fullLink = 111000 to 112000, + fastLink = 676000 to 677000, + fullLink = 103000 to 104000, fastLinkGz = 82000 to 83000, - fullLinkGz = 28000 to 29000, + fullLinkGz = 26000 to 27000, )) case `default213Version` => Some(ExpectedSizes( - fastLink = 475000 to 476000, - fullLink = 101000 to 102000, - fastLinkGz = 61000 to 62000, - fullLinkGz = 27000 to 28000, + fastLink = 468000 to 469000, + fullLink = 100000 to 101000, + fastLinkGz = 60000 to 61000, + fullLinkGz = 26000 to 27000, )) case _ => From 5f3323dc16ce9adb456dc02224be370791398625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 2 Feb 2024 18:42:12 +0100 Subject: [PATCH 036/298] Hard-code `scala.package$` as having elidable constructors. Like `scala.Predef$`. --- .../scalajs/linker/frontend/optimizer/IncOptimizer.scala | 2 +- project/Build.scala | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala index 6ba6bca95b..e8db94edb1 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala @@ -1476,7 +1476,7 @@ object IncOptimizer { new IncOptimizer(config, SeqCollOps) private val isAdHocElidableConstructors: Set[ClassName] = - Set(ClassName("scala.Predef$")) + Set(ClassName("scala.Predef$"), ClassName("scala.package$")) sealed abstract class ElidableConstructorsInfo { import ElidableConstructorsInfo._ diff --git a/project/Build.scala b/project/Build.scala index 9bc1560729..263683ee52 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1967,9 +1967,9 @@ object Build { scalaVersion.value match { case `default212Version` => Some(ExpectedSizes( - fastLink = 676000 to 677000, - fullLink = 103000 to 104000, - fastLinkGz = 82000 to 83000, + fastLink = 642000 to 643000, + fullLink = 102000 to 103000, + fastLinkGz = 77000 to 78000, fullLinkGz = 26000 to 27000, )) From 26300676c8acf6bdacf1b89a12dfef1be66a49dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 2 Feb 2024 14:24:09 +0100 Subject: [PATCH 037/298] Use `def`s for `hashCode` in 2.13.x Manifests, like in 2.12.x. Originally we made the change from the upstream `val`s to `def`s in 7865a5d48776198fa205d882de48f930ade43e41. We then accidentally reverted that change in the 2.13.x in 1a7f013641edd77dd427e05a50f9d38462fbd154. This commit aligns the 2.13.x overrides with 2.12.x again. --- scalalib/overrides-2.13/scala/reflect/Manifest.scala | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scalalib/overrides-2.13/scala/reflect/Manifest.scala b/scalalib/overrides-2.13/scala/reflect/Manifest.scala index 4f84b5e639..053b0c485d 100644 --- a/scalalib/overrides-2.13/scala/reflect/Manifest.scala +++ b/scalalib/overrides-2.13/scala/reflect/Manifest.scala @@ -152,8 +152,7 @@ abstract class AnyValManifest[T <: AnyVal](override val toString: String) extend case _ => false } override def equals(that: Any): Boolean = this eq that.asInstanceOf[AnyRef] - @transient - override val hashCode = System.identityHashCode(this) + override def hashCode = System.identityHashCode(this) } /** `ManifestFactory` defines factory methods for manifests. @@ -402,8 +401,7 @@ object ManifestFactory { private abstract class PhantomManifest[T](_runtimeClass: Predef.Class[_], override val toString: String) extends ClassTypeManifest[T](None, _runtimeClass, Nil) { override def equals(that: Any): Boolean = this eq that.asInstanceOf[AnyRef] - @transient - override val hashCode = System.identityHashCode(this) + override def hashCode = System.identityHashCode(this) } /** Manifest for the class type `clazz[args]`, where `clazz` is From 2e4594f0739cc58b74f1de7d5c4cc51b72a1371a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 5 Feb 2024 14:06:14 +0100 Subject: [PATCH 038/298] Fix #4929: Fix logic for moving early assignements in JS ctors. Previously, we moved all statements in the constructors after the super constructor call. However, it turns out that there are statements that must be kept before, notably local `val`s generated for default arguments to the super constructor. We now keep statements where they are by default. We only move statements of the form `C.this.field = ident;`, which are the only ones that require access to `this`. This requires only a little bit of special-casing for outer pointer null checks. Fortunately, we already had some logic to identify and decompose those, which we reuse here. --- .../org/scalajs/nscplugin/GenJSCode.scala | 120 +++++++++++++++--- .../jsinterop/NonNativeJSTypeTestScala2.scala | 43 +++++++ .../jsinterop/NonNativeJSTypeTest.scala | 36 ++++++ 3 files changed, 184 insertions(+), 15 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 2cf95451db..5d46b0617b 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -1472,18 +1472,78 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val sym = dd.symbol assert(sym.isPrimaryConstructor, s"called with non-primary ctor: $sym") + var preSuperStats = List.newBuilder[js.Tree] var jsSuperCall: Option[js.JSSuperConstructorCall] = None - val jsStats = List.newBuilder[js.Tree] + val postSuperStats = List.newBuilder[js.Tree] - /* Move all statements after the super constructor call since JS - * cannot access `this` before the super constructor call. + /* Move param accessor initializers and early initializers after the + * super constructor call since JS cannot access `this` before the super + * constructor call. * * scalac inserts statements before the super constructor call for early * initializers and param accessor initializers (including val's and var's - * declared in the params). We move those after the super constructor - * call, and are therefore executed later than for a Scala class. + * declared in the params). Those statements include temporary local `val` + * definitions (for true early initializers only) and the assignments, + * whose rhs'es are always simple Idents (either constructor params or the + * temporary local `val`s). + * + * There can also be local `val`s before the super constructor call for + * default arguments to the super constructor. These must remain before. + * + * Our strategy is therefore to move only the field assignments after the + * super constructor call. They are therefore executed later than for a + * Scala class (as specified for non-native JS classes semantics). + * However, side effects and evaluation order of all the other + * computations remains unchanged. + * + * For a somewhat extreme example of the shapes we can get here, consider + * the source code: + * + * class Parent(output: Any = "output", callbackObject: Any = "callbackObject") extends js.Object { + * println(s"Parent constructor; $output; $callbackObject") + * } + * + * class Child(val foo: Int, callbackObject: Any, val bar: Int) extends { + * val xyz = foo + bar + * val yz = { println(xyz); xyz + 2 } + * } with Parent(callbackObject = { println(foo); xyz + bar }) { + * println("Child constructor") + * println(xyz) + * } + * + * At this phase, for the constructor of `Child`, we receive the following + * scalac Tree: + * + * def (foo: Int, callbackObject: Object, bar: Int): helloworld.Child = { + * Child.this.foo = foo; // param accessor assignment, moved + * Child.this.bar = bar; // param accessor assignment, moved + * val xyz: Int = foo.+(bar); // note that these still use the ctor params, not the fields + * Child.this.xyz = xyz; // early initializer, moved + * val yz: Int = { + * scala.Predef.println(scala.Int.box(xyz)); // note that this uses the local val, not the field + * xyz.+(2) + * }; + * Child.this.yz = yz; // early initializer, moved + * { + * val x$1: Int = { + * scala.Predef.println(scala.Int.box(foo)); + * xyz.+(bar) // here as well, we use the local vals, not the fields + * }; + * val x$2: Object = helloworld.this.Parent.$default$1(); + * Child.super.(x$2, scala.Int.box(x$1)) + * }; + * scala.Predef.println("Child constructor"); + * scala.Predef.println(scala.Int.box(Child.this.xyz())); + * () + * } + * */ withPerMethodBodyState(sym) { + def isThisFieldAssignment(tree: Tree): Boolean = tree match { + case Assign(Select(ths: This, _), Ident(_)) => ths.symbol == currentClassSym.get + case _ => false + } + flatStats(stats).foreach { case tree @ Apply(fun @ Select(Super(This(_), _), _), args) if fun.symbol.isClassConstructor => @@ -1491,14 +1551,27 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) implicit val pos = tree.pos jsSuperCall = Some(js.JSSuperConstructorCall(genPrimitiveJSArgs(fun.symbol, args))) - case stat => - val jsStat = genStat(stat) + case tree if jsSuperCall.isDefined => + // Once we're past the super constructor call, everything goes after. + postSuperStats += genStat(tree) - assert(jsSuperCall.isDefined || !jsStat.isInstanceOf[js.VarDef], - "Trying to move a local VarDef after the super constructor call " + - s"of a non-native JS class at ${dd.pos}") + case tree if isThisFieldAssignment(tree) => + /* If that shape appears before the jsSuperCall, it is an early + * initializer or param accessor initializer. We move it. + */ + postSuperStats += genStat(tree) + + case tree @ OuterPointerNullCheck(outer, assign) if isThisFieldAssignment(assign) => + /* Variant of the above with an outer pointer null check. The actual + * null check remains before the super call, while the associated + * assignment is moved after. + */ + preSuperStats += js.GetClass(genExpr(outer))(tree.pos) + postSuperStats += genStat(assign) - jsStats += jsStat + case stat => + // Other statements are left before. + preSuperStats += genStat(stat) } } @@ -1506,7 +1579,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) s"construtor at ${dd.pos}") new PrimaryJSCtor(sym, genParamsAndInfo(sym, vparamss), - js.JSConstructorBody(Nil, jsSuperCall.get, jsStats.result())(dd.pos)) + js.JSConstructorBody(preSuperStats.result(), jsSuperCall.get, postSuperStats.result())(dd.pos)) } private def genSecondaryJSClassCtor(dd: DefDef): SplitSecondaryJSCtor = { @@ -2301,9 +2374,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) * this.$outer = $outer // the `else` branch in general */ tree match { - case If(Apply(fun @ Select(outer: Ident, nme.eq), Literal(Constant(null)) :: Nil), - Throw(Literal(Constant(null))), elsep) - if outer.symbol.isOuterParam && fun.symbol == definitions.Object_eq => + case OuterPointerNullCheck(outer, elsep) => js.Block( js.GetClass(genExpr(outer)), // null check genStat(elsep) @@ -2566,6 +2637,25 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) } } // end of GenJSCode.genExpr() + /** Extractor for the shape of outer pointer null check. + * + * See the comment in `case If(...) =>` of `genExpr`. + * + * When successful, returns the pair `(outer, elsep)` where `outer` is the + * `Ident` of the outer constructor parameter, and `elsep` is the else + * branch of the condition. + */ + private object OuterPointerNullCheck { + def unapply(tree: If): Option[(Ident, Tree)] = tree match { + case If(Apply(fun @ Select(outer: Ident, nme.eq), Literal(Constant(null)) :: Nil), + Throw(Literal(Constant(null))), elsep) + if outer.symbol.isOuterParam && fun.symbol == definitions.Object_eq => + Some((outer, elsep)) + case _ => + None + } + } + /** Gen JS this of the current class. * Normally encoded straightforwardly as a JS this. * But must be replaced by the tail-jump-this local variable if there diff --git a/test-suite/js/src/test/require-scala2/org/scalajs/testsuite/jsinterop/NonNativeJSTypeTestScala2.scala b/test-suite/js/src/test/require-scala2/org/scalajs/testsuite/jsinterop/NonNativeJSTypeTestScala2.scala index ea1c4c3f90..c3fcc9d69b 100644 --- a/test-suite/js/src/test/require-scala2/org/scalajs/testsuite/jsinterop/NonNativeJSTypeTestScala2.scala +++ b/test-suite/js/src/test/require-scala2/org/scalajs/testsuite/jsinterop/NonNativeJSTypeTestScala2.scala @@ -12,6 +12,8 @@ package org.scalajs.testsuite.jsinterop +import scala.collection.mutable + import scala.scalajs.js import scala.scalajs.js.annotation._ @@ -36,6 +38,29 @@ class NonNativeJSTypeTestScala2 { assertNull(obj.asInstanceOf[js.Dynamic].valueClass) } + @Test def callSuperConstructorWithDefaultParamsAndEarlyInitializers_Issue4929(): Unit = { + import ConstructorSuperCallWithDefaultParamsAndEarlyInitializers._ + + sideEffects.clear() + + val child = new Child(4, "hello", 23) + assertEquals(4, child.foo) + assertEquals(23, child.bar) + assertEquals(27, child.xyz) + assertEquals(29, child.yz) + + assertEquals( + List( + "27", + "4", + "Parent constructor; param1, 27, param1-27", + "Child constructor; 4, hello, 23", + "27, 29" + ), + sideEffects.toList + ) + } + } object NonNativeJSTypeTestScala2 { @@ -46,4 +71,22 @@ object NonNativeJSTypeTestScala2 { class SomeValueClass(val i: Int) extends AnyVal + object ConstructorSuperCallWithDefaultParamsAndEarlyInitializers { + val sideEffects = mutable.ListBuffer.empty[String] + + class Parent(parentParam1: Any = "param1", parentParam2: Any = "param2")( + dependentParam: String = s"$parentParam1-$parentParam2") + extends js.Object { + sideEffects += s"Parent constructor; $parentParam1, $parentParam2, $dependentParam" + } + + class Child(val foo: Int, parentParam2: Any, val bar: Int) extends { + val xyz = foo + bar + val yz = { sideEffects += xyz.toString(); xyz + 2 } + } with Parent(parentParam2 = { sideEffects += foo.toString(); foo + bar })() { + sideEffects += s"Child constructor; $foo, $parentParam2, $bar" + sideEffects += s"$xyz, $yz" + } + } + } diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/NonNativeJSTypeTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/NonNativeJSTypeTest.scala index 94cdf52847..0417489023 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/NonNativeJSTypeTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/NonNativeJSTypeTest.scala @@ -12,6 +12,8 @@ package org.scalajs.testsuite.jsinterop +import scala.collection.mutable + import scala.scalajs.js import scala.scalajs.js.annotation._ @@ -1306,6 +1308,25 @@ class NonNativeJSTypeTest { assertEquals(js.undefined, foo4.description) } + @Test def callSuperConstructorWithDefaultParams_Issue4929(): Unit = { + import ConstructorSuperCallWithDefaultParams._ + + sideEffects.clear() + + val child = new Child(4, "hello", 23) + assertEquals(4, child.foo) + assertEquals(23, child.bar) + + assertEquals( + List( + "4", + "Parent constructor; param1, 27, param1-27", + "Child constructor; 4, hello, 23" + ), + sideEffects.toList + ) + } + @Test def callSuperConstructorWithColonAsterisk(): Unit = { class CallSuperCtorWithSpread(x: Int, y: Int, z: Int) extends NativeParentClassWithVarargs(x, Seq(y, z): _*) @@ -2040,6 +2061,21 @@ object NonNativeJSTypeTest { def this(x: Int, y: Int) = this(x)(y.toString, js.undefined) } + object ConstructorSuperCallWithDefaultParams { + val sideEffects = mutable.ListBuffer.empty[String] + + class Parent(parentParam1: Any = "param1", parentParam2: Any = "param2")( + dependentParam: String = s"$parentParam1-$parentParam2") + extends js.Object { + sideEffects += s"Parent constructor; $parentParam1, $parentParam2, $dependentParam" + } + + class Child(val foo: Int, parentParam2: Any, val bar: Int) + extends Parent(parentParam2 = { sideEffects += foo.toString(); foo + bar })() { + sideEffects += s"Child constructor; $foo, $parentParam2, $bar" + } + } + class OverloadedConstructorParamNumber(val foo: Int) extends js.Object { def this(x: Int, y: Int) = this(x + y) def this(x: Int, y: Int, z: Int) = this(x + y, z) From db3b3ab640910b09a5a333a437c9aa02286e2ab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 24 Jan 2024 13:42:20 +0100 Subject: [PATCH 039/298] Use `genSelect` in the intrinsics that access `jl.Class.data`. Instead of hard-coding the `"jl_Class__f_data"` string. --- .../org/scalajs/linker/backend/emitter/EmitterNames.scala | 1 + .../scalajs/linker/backend/emitter/FunctionEmitter.scala | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) 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 17c03b75dc..4c1bcebc2b 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,6 +26,7 @@ private[emitter] object EmitterNames { // Field names + val dataFieldName = FieldName("data") val exceptionFieldName = FieldName("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 c299d8fdd7..3f8dc28427 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 @@ -2733,7 +2733,8 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case Transient(ZeroOf(runtimeClass)) => js.DotSelect( - js.DotSelect(transformExprNoChar(checkNotNull(runtimeClass)), js.Ident("jl_Class__f_data")), + genSelect(transformExprNoChar(checkNotNull(runtimeClass)), + ClassClass, FieldIdent(dataFieldName)), js.Ident("zero")) case Transient(NativeArrayWrapper(elemClass, nativeArray)) => @@ -2744,9 +2745,9 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { extractWithGlobals( genNativeArrayWrapper(arrayTypeRef, newNativeArray)) case _ => - val elemClassData = js.DotSelect( + val elemClassData = genSelect( transformExprNoChar(checkNotNull(elemClass)), - js.Ident("jl_Class__f_data")) + ClassClass, FieldIdent(dataFieldName)) val arrayClassData = js.Apply( js.DotSelect(elemClassData, js.Ident("getArrayOf")), Nil) js.Apply(arrayClassData DOT "wrapArray", newNativeArray :: Nil) From 96980516b4e8f477ca54a70f6d076678f8f94513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 24 Jan 2024 13:46:51 +0100 Subject: [PATCH 040/298] Go through SJSGen to generate Scala method names. This centralizes the decision on how to emit method names in one place, like we already had for fields. --- .../linker/backend/emitter/ClassEmitter.scala | 2 +- .../linker/backend/emitter/CoreJSLib.scala | 14 ++--- .../backend/emitter/FunctionEmitter.scala | 54 +++++++++---------- .../linker/backend/emitter/SJSGen.scala | 17 +++++- 4 files changed, 47 insertions(+), 40 deletions(-) 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 aade6c4b8a..4a502b6331 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 @@ -670,7 +670,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { private def genMemberMethodIdent(ident: MethodIdent, originalName: OriginalName): js.Ident = { - val jsName = genName(ident.name) + val jsName = genMethodName(ident.name) js.Ident(jsName, genOriginalName(ident.name, originalName, jsName))( ident.pos) } 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 290bb7f362..6aff172312 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 @@ -717,7 +717,7 @@ private[emitter] object CoreJSLib { defineFunction1(VarField.objectOrArrayClone) { instance => // return instance.$classData.isArrayClass ? instance.clone__O() : $objectClone(instance); Return(If(genIdentBracketSelect(instance DOT classData, "isArrayClass"), - Apply(instance DOT genName(cloneMethodName), Nil), + genApply(instance, cloneMethodName, Nil), genCallHelper(VarField.objectClone, instance))) } ) @@ -767,7 +767,7 @@ private[emitter] object CoreJSLib { ), { If(instance === Null(), { if (nullPointers == CheckedBehavior.Unchecked) - Return(Apply(instance DOT genName(getClassMethodName), Nil)) + Return(genApply(instance, getClassMethodName, Nil)) else genCallHelper(VarField.throwNullPointerException) }, { @@ -813,7 +813,7 @@ private[emitter] object CoreJSLib { instance => genIdentBracketSelect(instance DOT classData, "name"), { if (nullPointers == CheckedBehavior.Unchecked) - Apply(Null() DOT genName(getNameMethodName), Nil) + genApply(Null(), getNameMethodName, Nil) else genCallHelper(VarField.throwNullPointerException) } @@ -862,7 +862,7 @@ private[emitter] object CoreJSLib { } def genBodyNoSwitch(hijackedClasses: List[ClassName]): Tree = { - val normalCall = Return(Apply(instance DOT genName(methodName), args)) + val normalCall = Return(genApply(instance, methodName, args)) def hijackedDispatch(default: Tree) = { hijackedClasses.foldRight(default) { (className, next) => @@ -874,7 +874,7 @@ private[emitter] object CoreJSLib { if (implementedInObject) { val staticObjectCall: Tree = { - val fun = globalVar(VarField.c, ObjectClass).prototype DOT genName(methodName) + val fun = globalVar(VarField.c, ObjectClass).prototype DOT genMethodName(methodName) Return(Apply(fun DOT "call", instance :: args)) } @@ -1498,7 +1498,7 @@ private[emitter] object CoreJSLib { Nil } - val clone = MethodDef(static = false, Ident(genName(cloneMethodName)), Nil, None, { + val clone = MethodDef(static = false, Ident(genMethodName(cloneMethodName)), Nil, None, { Return(New(ArrayClass, Apply(genIdentBracketSelect(This().u, "slice"), Nil) :: Nil)) }) @@ -1803,7 +1803,7 @@ private[emitter] object CoreJSLib { Nil } - val clone = MethodDef(static = false, Ident(genName(cloneMethodName)), Nil, None, { + val clone = MethodDef(static = false, Ident(genMethodName(cloneMethodName)), Nil, None, { Return(New(ArrayClass, Apply(genIdentBracketSelect(This().u, "slice"), Nil) :: Nil)) }) 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 3f8dc28427..9ee6f42ecd 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 @@ -2352,7 +2352,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { if (useBigIntForLongs) js.Apply(genGlobalVarRef("Number"), List(wrapBigInt32(newLhs))) else - genLongMethodApply(newLhs, LongImpl.toInt) + genApply(newLhs, LongImpl.toInt) case DoubleToInt => genCallHelper(VarField.doubleToInt, newLhs) case DoubleToFloat => @@ -2363,7 +2363,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { if (useBigIntForLongs) js.Apply(genGlobalVarRef("Number"), List(newLhs)) else - genLongMethodApply(newLhs, LongImpl.toDouble) + genApply(newLhs, LongImpl.toDouble) case DoubleToLong => if (useBigIntForLongs) genCallHelper(VarField.doubleToLong, newLhs) @@ -2375,7 +2375,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { if (useBigIntForLongs) genCallHelper(VarField.longToFloat, newLhs) else - genLongMethodApply(newLhs, LongImpl.toFloat) + genApply(newLhs, LongImpl.toFloat) // String.length case String_length => @@ -2488,25 +2488,25 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { if (useBigIntForLongs) wrapBigInt64(js.BinaryOp(JSBinaryOp.+, newLhs, newRhs)) else - genLongMethodApply(newLhs, LongImpl.+, newRhs) + genApply(newLhs, LongImpl.+, newRhs) case Long_- => lhs match { case LongLiteral(0L) => if (useBigIntForLongs) wrapBigInt64(js.UnaryOp(JSUnaryOp.-, newRhs)) else - genLongMethodApply(newRhs, LongImpl.UNARY_-) + genApply(newRhs, LongImpl.UNARY_-) case _ => if (useBigIntForLongs) wrapBigInt64(js.BinaryOp(JSBinaryOp.-, newLhs, newRhs)) else - genLongMethodApply(newLhs, LongImpl.-, newRhs) + genApply(newLhs, LongImpl.-, newRhs) } case Long_* => if (useBigIntForLongs) wrapBigInt64(js.BinaryOp(JSBinaryOp.*, newLhs, newRhs)) else - genLongMethodApply(newLhs, LongImpl.*, newRhs) + genApply(newLhs, LongImpl.*, newRhs) case Long_/ => if (useBigIntForLongs) { rhs match { @@ -2516,7 +2516,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { genCallHelper(VarField.longDiv, newLhs, newRhs) } } else { - genLongMethodApply(newLhs, LongImpl./, newRhs) + genApply(newLhs, LongImpl./, newRhs) } case Long_% => if (useBigIntForLongs) { @@ -2527,78 +2527,78 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { genCallHelper(VarField.longMod, newLhs, newRhs) } } else { - genLongMethodApply(newLhs, LongImpl.%, newRhs) + genApply(newLhs, LongImpl.%, newRhs) } case Long_| => if (useBigIntForLongs) wrapBigInt64(js.BinaryOp(JSBinaryOp.|, newLhs, newRhs)) else - genLongMethodApply(newLhs, LongImpl.|, newRhs) + genApply(newLhs, LongImpl.|, newRhs) case Long_& => if (useBigIntForLongs) wrapBigInt64(js.BinaryOp(JSBinaryOp.&, newLhs, newRhs)) else - genLongMethodApply(newLhs, LongImpl.&, newRhs) + genApply(newLhs, LongImpl.&, newRhs) case Long_^ => lhs match { case LongLiteral(-1L) => if (useBigIntForLongs) wrapBigInt64(js.UnaryOp(JSUnaryOp.~, newRhs)) else - genLongMethodApply(newRhs, LongImpl.UNARY_~) + genApply(newRhs, LongImpl.UNARY_~) case _ => if (useBigIntForLongs) wrapBigInt64(js.BinaryOp(JSBinaryOp.^, newLhs, newRhs)) else - genLongMethodApply(newLhs, LongImpl.^, newRhs) + genApply(newLhs, LongImpl.^, newRhs) } case Long_<< => if (useBigIntForLongs) wrapBigInt64(js.BinaryOp(JSBinaryOp.<<, newLhs, bigIntShiftRhs(newRhs))) else - genLongMethodApply(newLhs, LongImpl.<<, newRhs) + genApply(newLhs, LongImpl.<<, newRhs) case Long_>>> => if (useBigIntForLongs) wrapBigInt64(js.BinaryOp(JSBinaryOp.>>, wrapBigIntU64(newLhs), bigIntShiftRhs(newRhs))) else - genLongMethodApply(newLhs, LongImpl.>>>, newRhs) + genApply(newLhs, LongImpl.>>>, newRhs) case Long_>> => if (useBigIntForLongs) wrapBigInt64(js.BinaryOp(JSBinaryOp.>>, newLhs, bigIntShiftRhs(newRhs))) else - genLongMethodApply(newLhs, LongImpl.>>, newRhs) + genApply(newLhs, LongImpl.>>, newRhs) case Long_== => if (useBigIntForLongs) js.BinaryOp(JSBinaryOp.===, newLhs, newRhs) else - genLongMethodApply(newLhs, LongImpl.===, newRhs) + genApply(newLhs, LongImpl.===, newRhs) case Long_!= => if (useBigIntForLongs) js.BinaryOp(JSBinaryOp.!==, newLhs, newRhs) else - genLongMethodApply(newLhs, LongImpl.!==, newRhs) + genApply(newLhs, LongImpl.!==, newRhs) case Long_< => if (useBigIntForLongs) js.BinaryOp(JSBinaryOp.<, newLhs, newRhs) else - genLongMethodApply(newLhs, LongImpl.<, newRhs) + genApply(newLhs, LongImpl.<, newRhs) case Long_<= => if (useBigIntForLongs) js.BinaryOp(JSBinaryOp.<=, newLhs, newRhs) else - genLongMethodApply(newLhs, LongImpl.<=, newRhs) + genApply(newLhs, LongImpl.<=, newRhs) case Long_> => if (useBigIntForLongs) js.BinaryOp(JSBinaryOp.>, newLhs, newRhs) else - genLongMethodApply(newLhs, LongImpl.>, newRhs) + genApply(newLhs, LongImpl.>, newRhs) case Long_>= => if (useBigIntForLongs) js.BinaryOp(JSBinaryOp.>=, newLhs, newRhs) else - genLongMethodApply(newLhs, LongImpl.>=, newRhs) + genApply(newLhs, LongImpl.>=, newRhs) case Float_+ => genFround(js.BinaryOp(JSBinaryOp.+, newLhs, newRhs)) case Float_- => genFround(js.BinaryOp(JSBinaryOp.-, newLhs, newRhs)) @@ -2684,7 +2684,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { * those cases, leaving a `Clone()` node an array. */ case _: ArrayType => - js.Apply(newExpr DOT genName(cloneMethodName), Nil) + genApply(newExpr, cloneMethodName, Nil) /* Otherwise, if it might be an array, use the full dispatcher. * In theory, only the `CloneableClass` case is required, since @@ -3201,7 +3201,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { js.Ident(genName(ident.name))(ident.pos) private def transformMethodIdent(ident: MethodIdent): js.Ident = - js.Ident(genName(ident.name))(ident.pos) + js.Ident(genMethodName(ident.name))(ident.pos) private def transformLocalVarIdent(ident: LocalIdent): js.Ident = js.Ident(transformLocalName(ident.name))(ident.pos) @@ -3268,12 +3268,6 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { js.Apply(genIdentBracketSelect(genGlobalVarRef("BigInt"), "asUintN"), List(js.IntLiteral(n), tree)) } - - private def genLongMethodApply(receiver: js.Tree, methodName: MethodName, - args: js.Tree*)(implicit pos: Position): js.Tree = { - import TreeDSL._ - js.Apply(receiver DOT genName(methodName), args.toList) - } } } 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 0449e0ed92..6100df00eb 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 @@ -107,8 +107,8 @@ private[emitter] final class SJSGen( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): Tree = { import TreeDSL._ - Apply( - genLoadModule(LongImpl.RuntimeLongModuleClass) DOT genName(methodName), + genApply( + genLoadModule(LongImpl.RuntimeLongModuleClass), methodName, args.toList) } @@ -172,6 +172,19 @@ private[emitter] final class SJSGen( private def genFieldJSName(className: ClassName, field: irt.FieldIdent): String = genName(className) + "__f_" + genName(field.name) + def genApply(receiver: Tree, methodName: MethodName, args: List[Tree])( + implicit pos: Position): Tree = { + Apply(DotSelect(receiver, Ident(genMethodName(methodName))), args) + } + + def genApply(receiver: Tree, methodName: MethodName, args: Tree*)( + implicit pos: Position): Tree = { + genApply(receiver, methodName, args.toList) + } + + def genMethodName(methodName: MethodName): String = + genName(methodName) + def genJSPrivateSelect(receiver: Tree, className: ClassName, field: irt.FieldIdent)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, From 73ad25c7be8c8a723c8f59fd018e198b5c86a44d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 24 Jan 2024 14:24:05 +0100 Subject: [PATCH 041/298] Transform and traverse the `name` subtree of `JSMethodPropDef`s. --- ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala | 4 ++-- ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) 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 99a72aedb9..879864c047 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala @@ -267,7 +267,7 @@ object Transformers { case JSPropertyDef(flags, name, getterBody, setterArgAndBody) => JSPropertyDef( flags, - name, + transformExpr(name), getterBody.map(transformStat), setterArgAndBody map { case (arg, body) => (arg, transformStat(body)) @@ -277,7 +277,7 @@ object Transformers { def transformJSMethodDef(jsMethodDef: JSMethodDef): JSMethodDef = { val JSMethodDef(flags, name, args, restParam, body) = jsMethodDef - JSMethodDef(flags, name, args, restParam, transformExpr(body))( + JSMethodDef(flags, transformExpr(name), args, restParam, transformExpr(body))( jsMethodDef.optimizerHints, Unversioned)(jsMethodDef.pos) } 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 7a4f5d9756..be26304b96 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala @@ -246,10 +246,12 @@ object Traversers { def traverseJSMethodPropDef(jsMethodPropDef: JSMethodPropDef): Unit = { jsMethodPropDef match { - case JSMethodDef(_, _, _, _, body) => + case JSMethodDef(_, name, _, _, body) => + traverse(name) traverse(body) - case JSPropertyDef(_, _, getterBody, setterArgAndBody) => + case JSPropertyDef(_, name, getterBody, setterArgAndBody) => + traverse(name) getterBody.foreach(traverse) setterArgAndBody.foreach(argAndBody => traverse(argAndBody._2)) } From 374b13e6e82cfe7f5f380d7de6b8be7f9245d966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 24 Jan 2024 17:30:11 +0100 Subject: [PATCH 042/298] Use implicits to be more explicit about global refs tracking. Normally, unless we track all global refs for the benefit of GCC, we prune away non-dangerous global refs as soon as possible, to preserve empty sets. This is done in two ways: * Within `FunctionEmitter`, we track everything for local purposes, but then prune the non-dangerous global refs when escaping back to `ClassEmitter`. * Everywhere else, we don't track non-dangerous global refs in the first place. This policy was broken in two places, because of helper code that is called both from inside and outside `FunctionEmitter`. In general, the way we applied that policy was very fragile, notably because it was not explicit what methods could be called in what context. We now use an `implicit GlobalRefTracking` in methods that generate global refs. The `GlobalRefTracking` indicates which set of global refs need to be tracked in the current context. Ironically, these implicits make it more explicit when we need to track what. --- .../linker/backend/emitter/ClassEmitter.scala | 20 +++--- .../linker/backend/emitter/CoreJSLib.scala | 5 +- .../linker/backend/emitter/Emitter.scala | 14 ++++- .../backend/emitter/FunctionEmitter.scala | 55 +++++++++-------- .../backend/emitter/GlobalRefTracking.scala | 47 ++++++++++++++ .../linker/backend/emitter/JSGen.scala | 16 +++-- .../linker/backend/emitter/SJSGen.scala | 61 +++++++++---------- .../linker/backend/emitter/VarGen.scala | 20 +++--- 8 files changed, 147 insertions(+), 91 deletions(-) create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/GlobalRefTracking.scala 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 4a502b6331..dff91a1fd1 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 @@ -43,6 +43,9 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { import nameGen._ import varGen._ + private implicit val globalRefTracking: GlobalRefTracking = + topLevelGlobalRefTracking + def buildClass(className: ClassName, isJSClass: Boolean, jsClassCaptures: Option[List[ParamDef]], hasClassInitializer: Boolean, superClass: Option[ClassIdent], storeJSSuperClass: List[js.Tree], useESClass: Boolean, @@ -56,7 +59,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { if (useESClass) { val parentVarWithGlobals = for (parentIdent <- superClass) yield { implicit val pos = parentIdent.pos - if (shouldExtendJSError(className, superClass)) untrackedGlobalRef("Error") + if (shouldExtendJSError(className, superClass)) globalRef("Error") else WithGlobals(globalVar(VarField.c, parentIdent.name)) } @@ -186,7 +189,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { WithGlobals.nil case Some(_) if shouldExtendJSError(className, superClass) => - untrackedGlobalRef("Error").map(chainPrototypeWithLocalCtor(className, ctorVar, _)) + globalRef("Error").map(chainPrototypeWithLocalCtor(className, ctorVar, _)) case Some(parentIdent) => WithGlobals(List(ctorVar.prototype := js.New(globalVar(VarField.h, parentIdent.name), Nil))) @@ -252,7 +255,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { if (hasJSSuperClass) { WithGlobals(fileLevelVar(VarField.superClass)) } else { - genJSClassConstructor(superClass.get.name, keepOnlyDangerousVarNames = true) + genJSClassConstructor(superClass.get.name) } } @@ -899,8 +902,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { })) } else { for { - jsCtor <- genJSClassConstructor(className, jsNativeLoadSpec, - keepOnlyDangerousVarNames = true) + jsCtor <- genJSClassConstructor(className, jsNativeLoadSpec) } yield { genArrowFunction(List(js.ParamDef(js.Ident("x"))), None, js.Return { js.VarRef(js.Ident("x")) instanceof jsCtor @@ -1075,12 +1077,8 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { private def genAssignToNoModuleExportVar(exportName: String, rhs: js.Tree)( implicit pos: Position): WithGlobals[js.Tree] = { - val dangerousGlobalRefs: Set[String] = - if (GlobalRefUtils.isDangerousGlobalRef(exportName)) Set(exportName) - else Set.empty - WithGlobals( - js.Assign(js.VarRef(js.Ident(exportName)), rhs), - dangerousGlobalRefs) + for (exportVar <- globalRef(exportName)) yield + js.Assign(exportVar, rhs) } private def genTopLevelFieldExportDef(className: ClassName, 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 6aff172312..b36e35783d 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 @@ -71,6 +71,9 @@ private[emitter] object CoreJSLib { implicit private val noPosition: Position = Position.NoPosition + private implicit val globalRefTracking: GlobalRefTracking = + topLevelGlobalRefTracking + private var trackedGlobalRefs = Set.empty[String] private def globalRef(name: String): VarRef = { @@ -81,7 +84,7 @@ private[emitter] object CoreJSLib { private def trackGlobalRef(name: String): Unit = { // We never access dangerous global refs from the core JS lib assert(!GlobalRefUtils.isDangerousGlobalRef(name)) - if (trackAllGlobalRefs) + if (globalRefTracking.shouldTrack(name)) trackedGlobalRefs += name } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala index 1906ffe84f..1a8b9bc517 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala @@ -39,6 +39,9 @@ final class Emitter[E >: Null <: js.Tree]( import Emitter._ import config._ + private implicit val globalRefTracking: GlobalRefTracking = + config.topLevelGlobalRefTracking + private val knowledgeGuardian = new KnowledgeGuardian(config) private val uncachedKnowledge = new knowledgeGuardian.KnowledgeAccessor {} @@ -173,15 +176,16 @@ final class Emitter[E >: Null <: js.Tree]( val result = emitOnce(moduleSet, logger) val mentionedDangerousGlobalRefs = - if (!trackAllGlobalRefs) result.globalVarNames - else GlobalRefUtils.keepOnlyDangerousGlobalRefs(result.globalVarNames) + GlobalRefTracking.Dangerous.refineFrom(topLevelGlobalRefTracking, result.globalVarNames) if (mentionedDangerousGlobalRefs == state.lastMentionedDangerousGlobalRefs) { result } else { assert(!secondAttempt, "Uh oh! The second attempt gave a different set of dangerous " + - "global refs than the first one.") + "global refs than the first one.\n" + + "Before:" + state.lastMentionedDangerousGlobalRefs.toList.sorted.mkString("\n ", "\n ", "\n") + + "After:" + mentionedDangerousGlobalRefs.toList.sorted.mkString("\n ", "\n ", "")) // !!! This log message is tested in EmitterTest logger.debug( @@ -1096,6 +1100,10 @@ object Emitter { trackAllGlobalRefs = false) } + private[emitter] val topLevelGlobalRefTracking: GlobalRefTracking = + if (trackAllGlobalRefs) GlobalRefTracking.All + else GlobalRefTracking.Dangerous + def withSemantics(f: Semantics => Semantics): Config = copy(semantics = f(semantics)) 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 9ee6f42ecd..d2f83eb4a3 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 @@ -259,7 +259,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { def desugarToFunction(enclosingClassName: ClassName, params: List[ParamDef], body: Tree, resultType: Type)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, - pos: Position): WithGlobals[js.Function] = { + globalRefTracking: GlobalRefTracking, pos: Position): WithGlobals[js.Function] = { desugarToFunction(enclosingClassName, params, restParam = None, body, resultType) } @@ -269,10 +269,10 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { def desugarToFunction(enclosingClassName: ClassName, params: List[ParamDef], restParam: Option[ParamDef], body: JSConstructorBody)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, - pos: Position): WithGlobals[js.Function] = { + globalRefTracking: GlobalRefTracking, pos: Position): WithGlobals[js.Function] = { val bodyBlock = Block(body.allStats)(body.pos) - new JSDesugar().desugarToFunction(params, restParam, bodyBlock, - isStat = false, + new JSDesugar(globalRefTracking).desugarToFunction( + params, restParam, bodyBlock, isStat = false, Env.empty(AnyType).withEnclosingClassName(Some(enclosingClassName))) } @@ -281,9 +281,9 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { def desugarToFunction(enclosingClassName: ClassName, params: List[ParamDef], restParam: Option[ParamDef], body: Tree, resultType: Type)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, - pos: Position): WithGlobals[js.Function] = { - new JSDesugar().desugarToFunction(params, restParam, body, - isStat = resultType == NoType, + globalRefTracking: GlobalRefTracking, pos: Position): WithGlobals[js.Function] = { + new JSDesugar(globalRefTracking).desugarToFunction( + params, restParam, body, isStat = resultType == NoType, Env.empty(resultType).withEnclosingClassName(Some(enclosingClassName))) } @@ -293,9 +293,9 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { def desugarToFunctionWithExplicitThis(enclosingClassName: ClassName, params: List[ParamDef], body: Tree, resultType: Type)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, - pos: Position): WithGlobals[js.Function] = { - new JSDesugar().desugarToFunctionWithExplicitThis(params, body, - isStat = resultType == NoType, + globalRefTracking: GlobalRefTracking, pos: Position): WithGlobals[js.Function] = { + new JSDesugar(globalRefTracking).desugarToFunctionWithExplicitThis( + params, body, isStat = resultType == NoType, Env.empty(resultType).withEnclosingClassName(Some(enclosingClassName))) } @@ -304,15 +304,16 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { def desugarToFunction(params: List[ParamDef], restParam: Option[ParamDef], body: Tree, resultType: Type)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, - pos: Position): WithGlobals[js.Function] = { - new JSDesugar().desugarToFunction(params, restParam, body, - isStat = resultType == NoType, Env.empty(resultType)) + globalRefTracking: GlobalRefTracking, pos: Position): WithGlobals[js.Function] = { + new JSDesugar(globalRefTracking).desugarToFunction( + params, restParam, body, isStat = resultType == NoType, + Env.empty(resultType)) } /** Desugars a class-level expression. */ def desugarExpr(expr: Tree, resultType: Type)( - implicit moduleContext: ModuleContext, - globalKnowledge: GlobalKnowledge): WithGlobals[js.Tree] = { + implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, + globalRefTracking: GlobalRefTracking): WithGlobals[js.Tree] = { implicit val pos = expr.pos for (fun <- desugarToFunction(Nil, None, expr, resultType)) yield { @@ -326,9 +327,13 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { } } - private class JSDesugar()( + private class JSDesugar(outerGlobalRefTracking: GlobalRefTracking)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge) { + // Inside JSDesugar, we always track everything + private implicit val globalRefTracking: GlobalRefTracking = + GlobalRefTracking.All + // For convenience private val es2015 = esFeatures.esVersion >= ESVersion.ES2015 @@ -410,7 +415,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { val result = body if (!isOptimisticNamingRun || !globalVarNames.exists(localVarNames)) { /* At this point, filter out the global refs that do not need to be - * tracked across functions and classes. + * tracked in the outer context. * * By default, only dangerous global refs need to be tracked outside of * functions, to power `mentionedDangerousGlobalRefs` In that case, the @@ -422,11 +427,10 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { * need to track all global variables across functions and classes. This is * slower, but running GCC will take most of the time anyway in that case. */ - val globalRefs = - if (trackAllGlobalRefs) globalVarNames.toSet - else GlobalRefUtils.keepOnlyDangerousGlobalRefs(globalVarNames.toSet) + val outerGlobalRefs = + outerGlobalRefTracking.refineFrom(globalRefTracking, globalVarNames.toSet) - WithGlobals(result, globalRefs) + WithGlobals(result, outerGlobalRefs) } else { /* Clear the local var names, but *not* the global var names. * In the pessimistic run, we will use the knowledge gathered during @@ -2214,8 +2218,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case SelectJSNativeMember(className, member) => val jsNativeLoadSpec = globalKnowledge.getJSNativeLoadSpec(className, member.name) - extractWithGlobals(genLoadJSFromSpec( - jsNativeLoadSpec, keepOnlyDangerousVarNames = false)) + extractWithGlobals(genLoadJSFromSpec(jsNativeLoadSpec)) case Apply(_, receiver, method, args) => val methodName = method.name @@ -2863,8 +2866,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { genLoadModule(className) case Some(spec) => - extractWithGlobals( - genLoadJSFromSpec(spec, keepOnlyDangerousVarNames = false)) + extractWithGlobals(genLoadJSFromSpec(spec)) } case JSUnaryOp(op, lhs) => @@ -3229,8 +3231,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { */ private def genJSClassConstructor(className: ClassName)( implicit pos: Position): WithGlobals[js.Tree] = { - sjsGen.genJSClassConstructor(className, - keepOnlyDangerousVarNames = false) + sjsGen.genJSClassConstructor(className) } private def genApplyStaticLike(field: VarField, className: ClassName, diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/GlobalRefTracking.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/GlobalRefTracking.scala new file mode 100644 index 0000000000..13f32d5ebc --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/GlobalRefTracking.scala @@ -0,0 +1,47 @@ +/* + * 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.backend.emitter + +/** Amount of global ref tracking that we need to do in a given context. + * + * - When using GCC, we always track all global refs; + * - Otherwise, inside the body of a function, in `FunctionEmitter`, we track + * all global refs so that we can identify collisions with locally allocated + * variable names; + * - Otherwise, we only track dangerous global refs. + */ +private[emitter] sealed abstract class GlobalRefTracking { + import GlobalRefTracking._ + + def shouldTrack(globalRef: String): Boolean = this match { + case All => true + case Dangerous => GlobalRefUtils.isDangerousGlobalRef(globalRef) + } + + /** Given a set of global refs tracked under the rules of `fromTracking`, + * keep only the ones needed according to `this`. + */ + def refineFrom(fromTracking: GlobalRefTracking, globalRefs: Set[String]): Set[String] = { + if (this == fromTracking) + globalRefs + else if (this == Dangerous) + GlobalRefUtils.keepOnlyDangerousGlobalRefs(globalRefs) + else + throw new AssertionError(s"Cannot refine set of global refs from $fromTracking to $this") + } +} + +private[emitter] object GlobalRefTracking { + case object All extends GlobalRefTracking + case object Dangerous extends GlobalRefTracking +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/JSGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/JSGen.scala index 3ed36197a5..9d0150c2c9 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/JSGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/JSGen.scala @@ -127,7 +127,7 @@ private[emitter] final class JSGen(val config: Emitter.Config) { } def genDefineProperty(obj: Tree, prop: Tree, descriptor: List[(String, Tree)])( - implicit pos: Position): WithGlobals[Tree] = { + implicit tracking: GlobalRefTracking, pos: Position): WithGlobals[Tree] = { val descriptorTree = ObjectConstr(descriptor.map(x => StringLiteral(x._1) -> x._2)) @@ -137,14 +137,12 @@ private[emitter] final class JSGen(val config: Emitter.Config) { } } - def globalRef(name: String)(implicit pos: Position): WithGlobals[VarRef] = - WithGlobals(VarRef(Ident(name)), Set(name)) - - def untrackedGlobalRef(name: String)(implicit pos: Position): WithGlobals[VarRef] = { - assert(!GlobalRefUtils.isDangerousGlobalRef(name)) - - if (trackAllGlobalRefs) globalRef(name) - else WithGlobals(VarRef(Ident(name))) + def globalRef(name: String)( + implicit tracking: GlobalRefTracking, pos: Position): WithGlobals[VarRef] = { + val trackedSet: Set[String] = + if (tracking.shouldTrack(name)) Set(name) + else Set.empty + WithGlobals(VarRef(Ident(name)), trackedSet) } def genPropSelect(qual: Tree, item: PropertyName)( 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 6100df00eb..4cc050d33c 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 @@ -112,11 +112,18 @@ private[emitter] final class SJSGen( args.toList) } - def usesUnderlyingTypedArray(elemTypeRef: NonArrayTypeRef): Boolean = - getArrayUnderlyingTypedArrayClassRef(elemTypeRef)(Position.NoPosition).nonEmpty + def usesUnderlyingTypedArray(elemTypeRef: NonArrayTypeRef): Boolean = { + /* We are only interested in whether `getArrayUnderlyingTypedArrayClassRef` + * returns a `Some` or not. We do not keep the result, so the `Position` + * and the `GlobalRefTracking` are irrelevant. + */ + implicit val dontCareGlobalRefTracking = GlobalRefTracking.Dangerous + implicit val dontCarePosition = Position.NoPosition + getArrayUnderlyingTypedArrayClassRef(elemTypeRef).nonEmpty + } def getArrayUnderlyingTypedArrayClassRef(elemTypeRef: NonArrayTypeRef)( - implicit pos: Position): Option[WithGlobals[VarRef]] = { + implicit tracking: GlobalRefTracking, pos: Position): Option[WithGlobals[VarRef]] = { elemTypeRef match { case _ if esFeatures.esVersion < ESVersion.ES2015 => None case primRef: PrimRef => typedArrayRef(primRef) @@ -125,7 +132,7 @@ private[emitter] final class SJSGen( } def typedArrayRef(primRef: PrimRef)( - implicit pos: Position): Option[WithGlobals[VarRef]] = { + implicit tracking: GlobalRefTracking, pos: Position): Option[WithGlobals[VarRef]] = { def some(name: String) = Some(globalRef(name)) primRef match { @@ -295,7 +302,7 @@ private[emitter] final class SJSGen( def genAsInstanceOf(expr: Tree, tpe: Type)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, - pos: Position): WithGlobals[Tree] = { + tracking: GlobalRefTracking, pos: Position): WithGlobals[Tree] = { import TreeDSL._ // Local short-hand of WithGlobals(...) @@ -407,7 +414,7 @@ private[emitter] final class SJSGen( def genCallPolyfillableBuiltin(builtin: PolyfillableBuiltin, args: Tree*)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, - pos: Position): WithGlobals[Tree] = { + tracking: GlobalRefTracking, pos: Position): WithGlobals[Tree] = { if (esFeatures.esVersion >= builtin.availableInESVersion) { builtin match { case builtin: GlobalVarBuiltin => @@ -441,28 +448,25 @@ private[emitter] final class SJSGen( } } - def genJSClassConstructor(className: ClassName, - keepOnlyDangerousVarNames: Boolean)( + def genJSClassConstructor(className: ClassName)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, - pos: Position): WithGlobals[Tree] = { + tracking: GlobalRefTracking, pos: Position): WithGlobals[Tree] = { genJSClassConstructor(className, - globalKnowledge.getJSNativeLoadSpec(className), - keepOnlyDangerousVarNames) + globalKnowledge.getJSNativeLoadSpec(className)) } def genJSClassConstructor(className: ClassName, - spec: Option[irt.JSNativeLoadSpec], - keepOnlyDangerousVarNames: Boolean)( + spec: Option[irt.JSNativeLoadSpec])( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, - pos: Position): WithGlobals[Tree] = { + tracking: GlobalRefTracking, pos: Position): WithGlobals[Tree] = { spec match { case None => // This is a non-native JS class WithGlobals(genNonNativeJSClassConstructor(className)) case Some(spec) => - genLoadJSFromSpec(spec, keepOnlyDangerousVarNames) + genLoadJSFromSpec(spec) } } @@ -472,10 +476,9 @@ private[emitter] final class SJSGen( Apply(globalVar(VarField.a, className), Nil) } - def genLoadJSFromSpec(spec: irt.JSNativeLoadSpec, - keepOnlyDangerousVarNames: Boolean)( + def genLoadJSFromSpec(spec: irt.JSNativeLoadSpec)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, - pos: Position): WithGlobals[Tree] = { + tracking: GlobalRefTracking, pos: Position): WithGlobals[Tree] = { def pathSelection(from: Tree, path: List[String]): Tree = { path.foldLeft(from) { @@ -484,17 +487,9 @@ private[emitter] final class SJSGen( } spec match { - case irt.JSNativeLoadSpec.Global(globalRef, path) => - val globalVarRef = VarRef(Ident(globalRef)) - val globalVarNames = { - if (keepOnlyDangerousVarNames && !trackAllGlobalRefs && - !GlobalRefUtils.isDangerousGlobalRef(globalRef)) { - Set.empty[String] - } else { - Set(globalRef) - } - } - WithGlobals(pathSelection(globalVarRef, path), globalVarNames) + case irt.JSNativeLoadSpec.Global(globalRefName, path) => + for (globalVarRef <- globalRef(globalRefName)) yield + pathSelection(globalVarRef, path) case irt.JSNativeLoadSpec.Import(module, path) => val moduleValue = VarRef(externalModuleFieldIdent(module)) @@ -509,9 +504,9 @@ private[emitter] final class SJSGen( case irt.JSNativeLoadSpec.ImportWithGlobalFallback(importSpec, globalSpec) => moduleKind match { case ModuleKind.NoModule => - genLoadJSFromSpec(globalSpec, keepOnlyDangerousVarNames) + genLoadJSFromSpec(globalSpec) case ModuleKind.ESModule | ModuleKind.CommonJSModule => - genLoadJSFromSpec(importSpec, keepOnlyDangerousVarNames) + genLoadJSFromSpec(importSpec) } } } @@ -533,13 +528,13 @@ private[emitter] final class SJSGen( def genArrayValue(arrayTypeRef: ArrayTypeRef, elems: List[Tree])( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, - pos: Position): WithGlobals[Tree] = { + tracking: GlobalRefTracking, pos: Position): WithGlobals[Tree] = { genNativeArrayWrapper(arrayTypeRef, ArrayConstr(elems)) } def genNativeArrayWrapper(arrayTypeRef: ArrayTypeRef, nativeArray: Tree)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, - pos: Position): WithGlobals[Tree] = { + tracking: GlobalRefTracking, pos: Position): WithGlobals[Tree] = { val argWithGlobals = arrayTypeRef match { case ArrayTypeRef(elemTypeRef, 1) => getArrayUnderlyingTypedArrayClassRef(elemTypeRef) match { diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarGen.scala index b58ac77235..7b7f87859d 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarGen.scala @@ -54,7 +54,8 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, def globalClassDef[T: Scope](field: VarField, scope: T, parentClass: Option[Tree], members: List[Tree], origName: OriginalName = NoOriginalName)( - implicit moduleContext: ModuleContext, pos: Position): WithGlobals[List[Tree]] = { + implicit moduleContext: ModuleContext, + globalRefTracking: GlobalRefTracking, pos: Position): WithGlobals[List[Tree]] = { val ident = globalVarIdent(field, scope, origName) maybeExport(ident, ClassDef(Some(ident), parentClass, members), mutable = false) } @@ -62,14 +63,16 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, def globalFunctionDef[T: Scope](field: VarField, scope: T, args: List[ParamDef], restParam: Option[ParamDef], body: Tree, origName: OriginalName = NoOriginalName)( - implicit moduleContext: ModuleContext, pos: Position): WithGlobals[List[Tree]] = { + implicit moduleContext: ModuleContext, + globalRefTracking: GlobalRefTracking, pos: Position): WithGlobals[List[Tree]] = { val ident = globalVarIdent(field, scope, origName) maybeExport(ident, FunctionDef(ident, args, restParam, body), mutable = false) } def globalVarDef[T: Scope](field: VarField, scope: T, value: Tree, origName: OriginalName = NoOriginalName)( - implicit moduleContext: ModuleContext, pos: Position): WithGlobals[List[Tree]] = { + implicit moduleContext: ModuleContext, + globalRefTracking: GlobalRefTracking, pos: Position): WithGlobals[List[Tree]] = { val ident = globalVarIdent(field, scope, origName) maybeExport(ident, genConst(ident, value), mutable = false) } @@ -77,7 +80,8 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, /** Attention: A globalVarDecl may only be modified from the module it was declared in. */ def globalVarDecl[T: Scope](field: VarField, scope: T, origName: OriginalName = NoOriginalName)( - implicit moduleContext: ModuleContext, pos: Position): WithGlobals[List[Tree]] = { + implicit moduleContext: ModuleContext, + globalRefTracking: GlobalRefTracking, pos: Position): WithGlobals[List[Tree]] = { val ident = globalVarIdent(field, scope, origName) maybeExport(ident, genEmptyMutableLet(ident), mutable = true) } @@ -88,7 +92,8 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, */ def globallyMutableVarDef[T: Scope](field: VarField, setterField: VarField, scope: T, value: Tree, origName: OriginalName = NoOriginalName)( - implicit moduleContext: ModuleContext, pos: Position): WithGlobals[List[Tree]] = { + implicit moduleContext: ModuleContext, + globalRefTracking: GlobalRefTracking, pos: Position): WithGlobals[List[Tree]] = { val ident = globalVarIdent(field, scope, origName) val varDef = genLet(ident, mutable = true, value) @@ -135,7 +140,7 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, /** Apply the provided body to a dynamically loaded global var */ def withDynamicGlobalVar[T: Scope](field: VarField, scope: T)(body: Tree => Tree)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, - pos: Position): WithGlobals[Tree] = { + globalRefTracking: GlobalRefTracking, pos: Position): WithGlobals[Tree] = { val ident = globalVarIdent(field, scope) val module = fileLevelVarIdent(VarField.module) @@ -269,7 +274,8 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, } private def maybeExport(ident: Ident, tree: Tree, mutable: Boolean)( - implicit moduleContext: ModuleContext, pos: Position): WithGlobals[List[Tree]] = { + implicit moduleContext: ModuleContext, + globalRefTracking: GlobalRefTracking, pos: Position): WithGlobals[List[Tree]] = { if (moduleContext.public) { WithGlobals(tree :: Nil) } else { From 7b1fece0fb665a40aa2ed05445b8df6ec2cdc7d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 12 Feb 2024 11:41:47 +0100 Subject: [PATCH 043/298] Bump the version to 1.16.0-SNAPSHOT for the upcoming changes. As well as the IR version to 1.16-SNAPSHOT. --- ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala index 8dfbb764e2..91d113055f 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala @@ -17,8 +17,8 @@ import java.util.concurrent.ConcurrentHashMap import scala.util.matching.Regex object ScalaJSVersions extends VersionChecks( - current = "1.15.1-SNAPSHOT", - binaryEmitted = "1.13" + current = "1.16.0-SNAPSHOT", + binaryEmitted = "1.16-SNAPSHOT" ) /** Helper class to allow for testing of logic. */ From 659d51808b94e46a13efd7599d6119d23ea07dfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 12 Feb 2024 13:12:20 +0100 Subject: [PATCH 044/298] Remove the parameters to `StoreModule` IR nodes. `StoreModule` was always implicitly assumed to be used only in the constructor of the corresponding module class, and with a `This()` rhs. We now include that assumption as a core truth of `StoreModule()`, and therefore remove its two arguments. A deserialization hack reads the old arguments and throws if they do not conform to the above assumption, before discarding them. --- .../org/scalajs/nscplugin/GenJSCode.scala | 5 +- .../main/scala/org/scalajs/ir/Hashers.scala | 4 +- .../main/scala/org/scalajs/ir/Printers.scala | 7 +-- .../scala/org/scalajs/ir/Serializers.scala | 22 +++++++- .../scala/org/scalajs/ir/Transformers.scala | 10 ++-- .../scala/org/scalajs/ir/Traversers.scala | 10 ++-- .../src/main/scala/org/scalajs/ir/Trees.scala | 3 +- .../scala/org/scalajs/ir/PrintersTest.scala | 3 +- .../backend/emitter/FunctionEmitter.scala | 9 +-- .../linker/checker/ClassDefChecker.scala | 38 ++++++++++--- .../scalajs/linker/checker/IRChecker.scala | 11 +--- .../frontend/optimizer/IncOptimizer.scala | 4 +- .../frontend/optimizer/OptimizerCore.scala | 9 +-- .../linker/checker/ClassDefCheckerTest.scala | 56 +++++++++++++++++++ project/BinaryIncompatibilities.scala | 3 + 15 files changed, 135 insertions(+), 59 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 5d46b0617b..2c2c0478ea 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -3191,10 +3191,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) if (isStaticModule(currentClassSym) && !isModuleInitialized.value && currentMethodSym.isClassConstructor) { isModuleInitialized.value = true - val className = encodeClassName(currentClassSym) - val initModule = - js.StoreModule(className, js.This()(jstpe.ClassType(className))) - js.Block(superCall, initModule) + js.Block(superCall, js.StoreModule()) } else { superCall } 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 9246cc6874..e363efccfb 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala @@ -259,10 +259,8 @@ object Hashers { mixTag(TagLoadModule) mixName(className) - case StoreModule(className, value) => + case StoreModule() => mixTag(TagStoreModule) - mixName(className) - mixTree(value) case Select(qualifier, className, field) => mixTag(TagSelect) 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 f3efeb3d52..deb6ded863 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala @@ -296,11 +296,8 @@ object Printers { print("mod:") print(className) - case StoreModule(className, value) => - print("mod:") - print(className) - print("<-") - print(value) + case StoreModule() => + print("") case Select(qualifier, className, field) => print(qualifier) 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 a2eb58cd91..f4c02e8760 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala @@ -318,9 +318,8 @@ object Serializers { writeTagAndPos(TagLoadModule) writeName(className) - case StoreModule(className, value) => + case StoreModule() => writeTagAndPos(TagStoreModule) - writeName(className); writeTree(value) case Select(qualifier, className, field) => writeTagAndPos(TagSelect) @@ -1012,6 +1011,7 @@ object Serializers { private[this] var lastPosition: Position = Position.NoPosition + private[this] var enclosingClassName: ClassName = _ private[this] var thisTypeForHack8: Type = NoType def deserializeEntryPointsInfo(): EntryPointsInfo = { @@ -1156,7 +1156,18 @@ object Serializers { case TagNew => New(readClassName(), readMethodIdent(), readTrees()) case TagLoadModule => LoadModule(readClassName()) - case TagStoreModule => StoreModule(readClassName(), readTree()) + + case TagStoreModule => + if (hacks.use13) { + val cls = readClassName() + val rhs = readTree() + if (cls != enclosingClassName || !rhs.isInstanceOf[This]) { + throw new IOException( + s"Illegal legacy StoreModule(${cls.nameString}, $rhs) " + + s"found in class ${enclosingClassName.nameString}") + } + } + StoreModule() case TagSelect => val qualifier = readTree() @@ -1359,8 +1370,11 @@ object Serializers { def readClassDef(): ClassDef = { implicit val pos = readPosition() + val name = readClassIdent() val cls = name.name + enclosingClassName = cls + val originalName = readOriginalName() val kind = ClassKind.fromByte(readByte()) @@ -2073,6 +2087,8 @@ object Serializers { private val use11: Boolean = use8 || sourceVersion == "1.11" val use12: Boolean = use11 || sourceVersion == "1.12" + + val use13: Boolean = use12 || sourceVersion == "1.13" } /** Names needed for hacks. */ 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 879864c047..415db46f33 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala @@ -91,9 +91,6 @@ object Transformers { case New(className, ctor, args) => New(className, ctor, args map transformExpr) - case StoreModule(className, value) => - StoreModule(className, transformExpr(value)) - case Select(qualifier, className, field) => Select(transformExpr(qualifier), className, field)(tree.tpe) @@ -223,9 +220,10 @@ object Transformers { // Trees that need not be transformed - case _:Skip | _:Debugger | _:LoadModule | _:SelectStatic | _:SelectJSNativeMember | - _:LoadJSConstructor | _:LoadJSModule | _:JSNewTarget | _:JSImportMeta | - _:JSLinkingInfo | _:Literal | _:VarRef | _:This | _:JSGlobalRef => + case _:Skip | _:Debugger | _:LoadModule | _:StoreModule | + _:SelectStatic | _:SelectJSNativeMember | _:LoadJSConstructor | + _:LoadJSModule | _:JSNewTarget | _:JSImportMeta | _:JSLinkingInfo | + _:Literal | _:VarRef | _:This | _:JSGlobalRef => tree } } 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 be26304b96..2040341a74 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala @@ -77,9 +77,6 @@ object Traversers { case New(_, _, args) => args foreach traverse - case StoreModule(_, value) => - traverse(value) - case Select(qualifier, _, _) => traverse(qualifier) @@ -222,9 +219,10 @@ object Traversers { // Trees that need not be traversed - case _:Skip | _:Debugger | _:LoadModule | _:SelectStatic | _:SelectJSNativeMember | - _:LoadJSConstructor | _:LoadJSModule | _:JSNewTarget | _:JSImportMeta | - _:JSLinkingInfo | _:Literal | _:VarRef | _:This | _:JSGlobalRef => + case _:Skip | _:Debugger | _:LoadModule | _:StoreModule | + _:SelectStatic | _:SelectJSNativeMember | _:LoadJSConstructor | + _:LoadJSModule | _:JSNewTarget | _:JSImportMeta | _:JSLinkingInfo | + _:Literal | _:VarRef | _:This | _:JSGlobalRef => } def traverseClassDef(tree: ClassDef): Unit = { 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 25b1c54575..631e39642a 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala @@ -237,8 +237,7 @@ object Trees { val tpe = ClassType(className) } - sealed case class StoreModule(className: ClassName, value: Tree)( - implicit val pos: Position) extends Tree { + sealed case class StoreModule()(implicit val pos: Position) extends Tree { val tpe = NoType // cannot be in expression position } 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 b8af4c7fe0..03c1628f16 100644 --- a/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala +++ b/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala @@ -302,8 +302,7 @@ class PrintersTest { } @Test def printStoreModule(): Unit = { - assertPrintEquals("mod:scala.Predef$<-this", - StoreModule("scala.Predef$", This()("scala.Predef$"))) + assertPrintEquals("", StoreModule()) } @Test def printSelect(): Unit = { 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 d2f83eb4a3..128e8ebeed 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 @@ -693,11 +693,12 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { pushLhsInto(Lhs.Assign(lhs), rhs, tailPosLabels) } - case StoreModule(className, value) => - unnest(value) { (newValue, env0) => - implicit val env = env0 - js.Assign(globalVar(VarField.n, className), transformExprNoChar(newValue)) + case StoreModule() => + val enclosingClassName = env.enclosingClassName.getOrElse { + throw new AssertionError( + "Need enclosing class for StoreModule().") } + js.Assign(globalVar(VarField.n, enclosingClassName), js.This()) case While(cond, body) => val loopEnv = env.withInLoopForVarCapture(true) 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 9ce0e7175a..ba64a3b9b1 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 @@ -292,7 +292,10 @@ private final class ClassDefChecker(classDef: ClassDef, // Body val thisType = if (static) NoType else instanceThisType - body.foreach(checkTree(_, Env.fromParams(params).withThisType(thisType))) + val bodyEnv = Env.fromParams(params) + .withThisType(thisType) + .withInConstructor(isConstructor) + body.foreach(checkTree(_, bodyEnv)) } private def checkJSConstructorDef(ctorDef: JSConstructorDef): Unit = withPerMethodState { @@ -311,6 +314,7 @@ private final class ClassDefChecker(classDef: ClassDef, val startEnv = Env.fromParams(classDef.jsClassCaptures.getOrElse(Nil) ++ params ++ restParam) .withHasNewTarget(true) + .withInConstructor(true) val envJustBeforeSuper = body.beforeSuper.foldLeft(startEnv) { (prevEnv, stat) => checkTree(stat, prevEnv) @@ -608,8 +612,13 @@ private final class ClassDefChecker(classDef: ClassDef, case _: LoadModule => - case StoreModule(_, value) => - checkTree(value, env) + case StoreModule() => + if (!classDef.kind.hasModuleAccessor) + reportError(i"Illegal StoreModule inside class of kind ${classDef.kind}") + if (!env.inConstructor) + reportError(i"Illegal StoreModule outside of constructor") + if (env.thisType == NoType) // can happen before JSSuperConstructorCall in JSModuleClass + reportError(i"Cannot find `this` in scope for StoreModule()") case Select(qualifier, _, _) => checkTree(qualifier, env) @@ -922,7 +931,9 @@ object ClassDefChecker { /** Local variables in scope (including through closures). */ val locals: Map[LocalName, LocalDef], /** Return types by label. */ - val returnLabels: Set[LabelName] + val returnLabels: Set[LabelName], + /** Whether we are in a constructor of the class. */ + val inConstructor: Boolean ) { import Env._ @@ -938,25 +949,36 @@ object ClassDefChecker { def withLabel(label: LabelName): Env = copy(returnLabels = returnLabels + label) + def withInConstructor(inConstructor: Boolean): Env = + copy(inConstructor = inConstructor) + private def copy( hasNewTarget: Boolean = hasNewTarget, thisType: Type = thisType, locals: Map[LocalName, LocalDef] = locals, - returnLabels: Set[LabelName] = returnLabels + returnLabels: Set[LabelName] = returnLabels, + inConstructor: Boolean = inConstructor ): Env = { - new Env(hasNewTarget, thisType, locals, returnLabels) + new Env(hasNewTarget, thisType, locals, returnLabels, inConstructor) } } private object Env { val empty: Env = - new Env(hasNewTarget = false, thisType = NoType, Map.empty, Set.empty) + new Env(hasNewTarget = false, thisType = NoType, Map.empty, Set.empty, inConstructor = false) def fromParams(params: List[ParamDef]): Env = { val paramLocalDefs = for (p @ ParamDef(ident, _, tpe, mutable) <- params) yield ident.name -> LocalDef(ident.name, tpe, mutable) - new Env(hasNewTarget = false, thisType = NoType, paramLocalDefs.toMap, Set.empty) + + new Env( + hasNewTarget = false, + thisType = NoType, + paramLocalDefs.toMap, + Set.empty, + inConstructor = false + ) } } 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 2e3678a6d4..418629d55b 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 @@ -319,14 +319,9 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { if (clazz.kind != ClassKind.ModuleClass) reportError("LoadModule of non-module class $className") - case StoreModule(className, value) => - val clazz = lookupClass(className) - if (!clazz.kind.hasModuleAccessor) - reportError("StoreModule of non-module class $className") - val expectedType = - if (clazz.kind == ClassKind.JSModuleClass) AnyType - else ClassType(className) - typecheckExpect(value, env, expectedType) + case StoreModule() => + // Nothing to check; everything is checked in ClassDefChecker + () case Select(qualifier, className, FieldIdent(item)) => val c = lookupClass(className) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala index e8db94edb1..05763942ff 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala @@ -748,8 +748,8 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: case ApplyStatically(flags, This(), _, _, args) if flags.isConstructor => args.forall(isTriviallySideEffectFree) - case StoreModule(_, _) => true - case _ => isTriviallySideEffectFree(tree) + case StoreModule() => true + case _ => isTriviallySideEffectFree(tree) } impl.originalDef.body.fold { 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 534fd345be..a417440b22 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 @@ -171,7 +171,7 @@ private[optimizer] abstract class OptimizerCore( private def tryElimStoreModule(body: Tree): Tree = { implicit val pos = body.pos body match { - case StoreModule(_, _) => + case StoreModule() => Skip() case Block(stats) => val (before, from) = stats.span(!_.isInstanceOf[StoreModule]) @@ -458,9 +458,6 @@ private[optimizer] abstract class OptimizerCore( case New(className, ctor, args) => New(className, ctor, args map transformExpr) - case StoreModule(className, value) => - StoreModule(className, transformExpr(value)) - case tree: Select => trampoline { pretransformSelectCommon(tree, isLhsOfAssign = false)( @@ -656,8 +653,8 @@ private[optimizer] abstract class OptimizerCore( // Trees that need not be transformed - case _:Skip | _:Debugger | _:LoadModule | _:SelectStatic | - _:JSNewTarget | _:JSImportMeta | _:JSLinkingInfo | + case _:Skip | _:Debugger | _:LoadModule | _:StoreModule | + _:SelectStatic | _:JSNewTarget | _:JSImportMeta | _:JSLinkingInfo | _:JSGlobalRef | _:JSTypeOfGlobalRef | _:Literal => tree diff --git a/linker/shared/src/test/scala/org/scalajs/linker/checker/ClassDefCheckerTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/checker/ClassDefCheckerTest.scala index d092a04ee4..75717babd3 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/checker/ClassDefCheckerTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/checker/ClassDefCheckerTest.scala @@ -305,6 +305,62 @@ class ClassDefCheckerTest { Closure(arrow = false, Nil, Nil, None, This()(ClassType("Foo")), Nil), "`this` of type any typed as Foo") } + + @Test + def storeModule(): Unit = { + val ctorFlags = EMF.withNamespace(MemberNamespace.Constructor) + + val superCtorCall = ApplyStatically(EAF.withConstructor(true), This()(ClassType("Foo")), + ObjectClass, NoArgConstructorName, Nil)(NoType) + + assertError( + classDef( + "Foo", + kind = ClassKind.Class, + superClass = Some(ObjectClass), + methods = List( + MethodDef(ctorFlags, NoArgConstructorName, NON, Nil, NoType, Some { + Block( + superCtorCall, + StoreModule() + ) + })(EOH, UNV) + ) + ), + "Illegal StoreModule inside class of kind Class" + ) + + assertError( + classDef( + "Foo", + kind = ClassKind.ModuleClass, + superClass = Some(ObjectClass), + methods = List( + trivialCtor("Foo"), + MethodDef(EMF, MethodName("foo", Nil, VoidRef), NON, Nil, NoType, Some { + Block( + StoreModule() + ) + })(EOH, UNV) + ) + ), + "Illegal StoreModule outside of constructor" + ) + + assertError( + classDef( + "Foo", + kind = ClassKind.JSModuleClass, + superClass = Some("scala.scalajs.js.Object"), + jsConstructor = Some( + JSConstructorDef(JSCtorFlags, Nil, None, + JSConstructorBody(StoreModule() :: Nil, JSSuperConstructorCall(Nil), Undefined() :: Nil))( + EOH, UNV) + ) + ), + "Cannot find `this` in scope for StoreModule()" + ) + } } private object ClassDefCheckerTest { diff --git a/project/BinaryIncompatibilities.scala b/project/BinaryIncompatibilities.scala index 4713fe6bf8..0144862b7b 100644 --- a/project/BinaryIncompatibilities.scala +++ b/project/BinaryIncompatibilities.scala @@ -5,6 +5,9 @@ import com.typesafe.tools.mima.core.ProblemFilters._ object BinaryIncompatibilities { val IR = Seq( + // !!! Breaking, OK in minor release + ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Trees#StoreModule.*"), + ProblemFilters.exclude[IncompatibleResultTypeProblem]("org.scalajs.ir.Trees#StoreModule.unapply"), ) val Linker = Seq( From 723663b76a4dc2775dbab12f3c268e33990b6b55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 17 Feb 2024 11:21:05 +0100 Subject: [PATCH 045/298] Refactor: Make FieldName a composite of ClassName and SimpleFieldName. Previously, `FieldName` only represented the *simple* name of a field. It was complemented everywhere with the enclosing `ClassName`, for namespacing purposes. We now make `FieldName` a composite, like `MethodName`. It contains a `ClassName` and a `SimpleFieldName`. Structurally, `SimpleFieldName` is the same as the old `FieldName`. This removes the need to pass additional, out-of-band `ClassName`s everywhere a `FieldName` or `FieldIdent` was used. In addition to the readability improvements, this might improve performance. We previously often had to create (temporary) pairs of `(ClassName, FieldName)` as keys of maps. Now, we can directly use the `FieldName`s instead. While the IR names, types and trees are significantly impacted by this change, the `.sjsir` format is unchanged. --- .../org/scalajs/nscplugin/GenJSCode.scala | 20 +-- .../org/scalajs/nscplugin/JSEncoding.scala | 7 +- .../main/scala/org/scalajs/ir/Hashers.scala | 20 ++- .../src/main/scala/org/scalajs/ir/Names.scala | 84 +++++++-- .../scala/org/scalajs/ir/OriginalName.scala | 8 + .../main/scala/org/scalajs/ir/Printers.scala | 29 +-- .../scala/org/scalajs/ir/Serializers.scala | 88 ++++++--- .../scala/org/scalajs/ir/Transformers.scala | 8 +- .../scala/org/scalajs/ir/Traversers.scala | 4 +- .../src/main/scala/org/scalajs/ir/Trees.scala | 15 +- .../src/main/scala/org/scalajs/ir/Types.scala | 4 +- .../scala/org/scalajs/ir/PrintersTest.scala | 34 ++-- .../scala/org/scalajs/ir/TestIRBuilder.scala | 10 +- .../org/scalajs/linker/analyzer/Infos.scala | 48 ++--- .../linker/backend/emitter/ClassEmitter.scala | 29 ++- .../linker/backend/emitter/EmitterNames.scala | 4 +- .../backend/emitter/FunctionEmitter.scala | 65 ++++--- .../backend/emitter/GlobalKnowledge.scala | 5 +- .../backend/emitter/KnowledgeGuardian.scala | 17 +- .../linker/backend/emitter/NameGen.scala | 14 +- .../linker/backend/emitter/SJSGen.scala | 16 +- .../linker/backend/emitter/VarGen.scala | 8 +- .../linker/checker/ClassDefChecker.scala | 6 +- .../linker/checker/ErrorReporter.scala | 1 + .../scalajs/linker/checker/IRChecker.scala | 27 +-- .../frontend/optimizer/IncOptimizer.scala | 14 +- .../frontend/optimizer/OptimizerCore.scala | 169 ++++++++---------- .../org/scalajs/linker/OptimizerTest.scala | 18 +- .../linker/checker/ClassDefCheckerTest.scala | 38 +++- .../linker/testutils/TestIRBuilder.scala | 8 +- project/BinaryIncompatibilities.scala | 13 ++ project/JavalibIRCleaner.scala | 5 +- 32 files changed, 479 insertions(+), 357 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 2c2c0478ea..6ad055b5ee 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -27,7 +27,7 @@ import scala.reflect.internal.Flags import org.scalajs.ir import org.scalajs.ir.{Trees => js, Types => jstpe, ClassKind, Hashers, OriginalName} -import org.scalajs.ir.Names.{LocalName, FieldName, SimpleMethodName, MethodName, ClassName} +import org.scalajs.ir.Names.{LocalName, SimpleFieldName, FieldName, SimpleMethodName, MethodName, ClassName} import org.scalajs.ir.OriginalName.NoOriginalName import org.scalajs.ir.Trees.OptimizerHints import org.scalajs.ir.Version.Unversioned @@ -6337,7 +6337,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val classType = jstpe.ClassType(className) // val f: Any - val fFieldIdent = js.FieldIdent(FieldName("f")) + val fFieldIdent = js.FieldIdent(FieldName(className, SimpleFieldName("f"))) val fFieldDef = js.FieldDef(js.MemberFlags.empty, fFieldIdent, NoOriginalName, jstpe.AnyType) @@ -6353,8 +6353,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) jstpe.NoType, Some(js.Block(List( js.Assign( - js.Select(js.This()(classType), className, fFieldIdent)( - jstpe.AnyType), + js.Select(js.This()(classType), fFieldIdent)(jstpe.AnyType), fParamDef.ref), js.ApplyStatically(js.ApplyFlags.empty.withConstructor(true), js.This()(classType), @@ -6405,7 +6404,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) }.map((ensureBoxed _).tupled) val call = js.JSFunctionApply( - js.Select(js.This()(classType), className, fFieldIdent)(jstpe.AnyType), + js.Select(js.This()(classType), fFieldIdent)(jstpe.AnyType), actualParams) val body = fromAny(call, enteringPhase(currentRun.posterasurePhase) { @@ -6746,14 +6745,12 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) js.JSSelect(qual, genPrivateFieldsSymbol()), encodeFieldSymAsStringLiteral(sym)) } else { - js.JSPrivateSelect(qual, encodeClassName(sym.owner), - encodeFieldSym(sym)) + js.JSPrivateSelect(qual, encodeFieldSym(sym)) } (f, true) } else if (jsInterop.topLevelExportsOf(sym).nonEmpty) { - val f = js.SelectStatic(encodeClassName(sym.owner), - encodeFieldSym(sym))(jstpe.AnyType) + val f = js.SelectStatic(encodeFieldSym(sym))(jstpe.AnyType) (f, true) } else if (jsInterop.staticExportsOf(sym).nonEmpty) { val exportInfo = jsInterop.staticExportsOf(sym).head @@ -6764,7 +6761,6 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) (f, true) } else { - val className = encodeClassName(sym.owner) val fieldIdent = encodeFieldSym(sym) /* #4370 Fields cannot have type NothingType, so we box them as @@ -6774,11 +6770,11 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) */ toIRType(sym.tpe) match { case jstpe.NothingType => - val f = js.Select(qual, className, fieldIdent)( + val f = js.Select(qual, fieldIdent)( encodeClassType(RuntimeNothingClass)) (f, true) case ftpe => - val f = js.Select(qual, className, fieldIdent)(ftpe) + val f = js.Select(qual, fieldIdent)(ftpe) (f, false) } } diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/JSEncoding.scala b/compiler/src/main/scala/org/scalajs/nscplugin/JSEncoding.scala index 432abaaa7a..56f089bf1e 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/JSEncoding.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/JSEncoding.scala @@ -18,7 +18,7 @@ import scala.tools.nsc._ import org.scalajs.ir import org.scalajs.ir.{Trees => js, Types => jstpe} -import org.scalajs.ir.Names.{LocalName, LabelName, FieldName, SimpleMethodName, MethodName, ClassName} +import org.scalajs.ir.Names.{LocalName, LabelName, SimpleFieldName, FieldName, SimpleMethodName, MethodName, ClassName} import org.scalajs.ir.OriginalName import org.scalajs.ir.OriginalName.NoOriginalName import org.scalajs.ir.UTF8String @@ -178,8 +178,9 @@ trait JSEncoding[G <: Global with Singleton] extends SubComponent { def encodeFieldSym(sym: Symbol)(implicit pos: Position): js.FieldIdent = { requireSymIsField(sym) - val name = sym.name.dropLocal - js.FieldIdent(FieldName(name.toString())) + val className = encodeClassName(sym.owner) + val simpleName = SimpleFieldName(sym.name.dropLocal.toString()) + js.FieldIdent(FieldName(className, simpleName)) } def encodeFieldSymAsStringLiteral(sym: Symbol)( 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 e363efccfb..bbf0b85409 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala @@ -262,16 +262,14 @@ object Hashers { case StoreModule() => mixTag(TagStoreModule) - case Select(qualifier, className, field) => + case Select(qualifier, field) => mixTag(TagSelect) mixTree(qualifier) - mixName(className) mixFieldIdent(field) mixType(tree.tpe) - case SelectStatic(className, field) => + case SelectStatic(field) => mixTag(TagSelectStatic) - mixName(className) mixFieldIdent(field) mixType(tree.tpe) @@ -351,7 +349,7 @@ object Hashers { case RecordSelect(record, field) => mixTag(TagRecordSelect) mixTree(record) - mixFieldIdent(field) + mixSimpleFieldIdent(field) mixType(tree.tpe) case IsInstanceOf(expr, testType) => @@ -389,10 +387,9 @@ object Hashers { mixTree(ctor) mixTreeOrJSSpreads(args) - case JSPrivateSelect(qualifier, className, field) => + case JSPrivateSelect(qualifier, field) => mixTag(TagJSPrivateSelect) mixTree(qualifier) - mixName(className) mixFieldIdent(field) case JSSelect(qualifier, item) => @@ -651,11 +648,18 @@ object Hashers { mixName(ident.name) } - def mixFieldIdent(ident: FieldIdent): Unit = { + def mixSimpleFieldIdent(ident: SimpleFieldIdent): Unit = { mixPos(ident.pos) mixName(ident.name) } + def mixFieldIdent(ident: FieldIdent): Unit = { + // For historical reasons, the className comes *before* the position + mixName(ident.name.className) + mixPos(ident.pos) + mixName(ident.name.simpleName) + } + def mixMethodIdent(ident: MethodIdent): Unit = { mixPos(ident.pos) mixMethodName(ident.name) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Names.scala b/ir/shared/src/main/scala/org/scalajs/ir/Names.scala index cee7f057cb..3e4a429e2a 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Names.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Names.scala @@ -99,7 +99,7 @@ object Names { def apply(name: String): LocalName = LocalName(UTF8String(name)) - private[Names] def fromFieldName(name: FieldName): LocalName = + private[Names] def fromSimpleFieldName(name: SimpleFieldName): LocalName = new LocalName(name.encoded) } @@ -137,38 +137,90 @@ object Names { LabelName(UTF8String(name)) } - /** The name of a field. + /** The simple name of a field (excluding its enclosing class). * * Field names must be non-empty, and can contain any Unicode code point * except `/ . ; [`. */ - final class FieldName private (encoded: UTF8String) - extends Name(encoded) with Comparable[FieldName] { + final class SimpleFieldName private (encoded: UTF8String) + extends Name(encoded) with Comparable[SimpleFieldName] { - type ThisName = FieldName + type ThisName = SimpleFieldName override def equals(that: Any): Boolean = { (this eq that.asInstanceOf[AnyRef]) || (that match { - case that: FieldName => equalsName(that) - case _ => false + case that: SimpleFieldName => equalsName(that) + case _ => false }) } - protected def stringPrefix: String = "FieldName" + protected def stringPrefix: String = "SimpleFieldName" - final def withSuffix(suffix: String): FieldName = - FieldName(this.encoded ++ UTF8String(suffix)) + final def withSuffix(suffix: String): SimpleFieldName = + SimpleFieldName(this.encoded ++ UTF8String(suffix)) final def toLocalName: LocalName = - LocalName.fromFieldName(this) + LocalName.fromSimpleFieldName(this) } - object FieldName { - def apply(name: UTF8String): FieldName = - new FieldName(validateSimpleEncodedName(name)) + object SimpleFieldName { + def apply(name: UTF8String): SimpleFieldName = + new SimpleFieldName(validateSimpleEncodedName(name)) + + def apply(name: String): SimpleFieldName = + SimpleFieldName(UTF8String(name)) + } - def apply(name: String): FieldName = - FieldName(UTF8String(name)) + /** The full name of a field, including its simple name and its enclosing + * class name. + */ + final class FieldName private ( + val className: ClassName, val simpleName: SimpleFieldName) + extends Comparable[FieldName] { + + import FieldName._ + + private val _hashCode: Int = { + import scala.util.hashing.MurmurHash3._ + var acc = -1025990011 // "FieldName".hashCode() + acc = mix(acc, className.##) + acc = mix(acc, simpleName.##) + finalizeHash(acc, 2) + } + + override def equals(that: Any): Boolean = { + (this eq that.asInstanceOf[AnyRef]) || (that match { + case that: FieldName => + this._hashCode == that._hashCode && // fail fast on different hash codes + this.className == that.className && + this.simpleName == that.simpleName + case _ => + false + }) + } + + override def hashCode(): Int = _hashCode + + def compareTo(that: FieldName): Int = { + val classNameCmp = this.className.compareTo(that.className) + if (classNameCmp != 0) + classNameCmp + else + this.simpleName.compareTo(that.simpleName) + } + + protected def stringPrefix: String = "FieldName" + + def nameString: String = + className.nameString + "::" + simpleName.nameString + + override def toString(): String = + "FieldName<" + nameString + ">" + } + + object FieldName { + def apply(className: ClassName, simpleName: SimpleFieldName): FieldName = + new FieldName(className, simpleName) } /** The simple name of a method (excluding its signature). diff --git a/ir/shared/src/main/scala/org/scalajs/ir/OriginalName.scala b/ir/shared/src/main/scala/org/scalajs/ir/OriginalName.scala index d2211095d5..7ae6745e53 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/OriginalName.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/OriginalName.scala @@ -53,6 +53,10 @@ final class OriginalName private (private val bytes: Array[Byte]) if (isDefined) this else OriginalName(name) + // new in 1.16.0; added as last overload to preserve binary compatibility + def orElse(name: FieldName): OriginalName = + orElse(name.simpleName) + def getOrElse(name: Name): UTF8String = getOrElse(name.encoded) @@ -71,6 +75,10 @@ final class OriginalName private (private val bytes: Array[Byte]) else UTF8String(name) } + // new in 1.16.0; added as last overload to preserve binary compatibility + def getOrElse(name: FieldName): UTF8String = + getOrElse(name.simpleName) + override def toString(): String = if (isDefined) s"OriginalName($unsafeGet)" else "NoOriginalName" 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 deb6ded863..875a4e4c0b 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala @@ -123,6 +123,7 @@ object Printers { node match { case node: LocalIdent => print(node) case node: LabelIdent => print(node) + case node: SimpleFieldIdent => print(node) case node: FieldIdent => print(node) case node: MethodIdent => print(node) case node: ClassIdent => print(node) @@ -299,16 +300,12 @@ object Printers { case StoreModule() => print("") - case Select(qualifier, className, field) => + case Select(qualifier, field) => print(qualifier) print('.') - print(className) - print("::") print(field) - case SelectStatic(className, field) => - print(className) - print("::") + case SelectStatic(field) => print(field) case SelectJSNativeMember(className, member) => @@ -572,11 +569,11 @@ object Printers { case JSNew(ctor, args) => def containsOnlySelectsFromAtom(tree: Tree): Boolean = tree match { - case JSPrivateSelect(qual, _, _) => containsOnlySelectsFromAtom(qual) - case JSSelect(qual, _) => containsOnlySelectsFromAtom(qual) - case VarRef(_) => true - case This() => true - case _ => false // in particular, Apply + case JSPrivateSelect(qual, _) => containsOnlySelectsFromAtom(qual) + case JSSelect(qual, _) => containsOnlySelectsFromAtom(qual) + case VarRef(_) => true + case This() => true + case _ => false // in particular, Apply } if (containsOnlySelectsFromAtom(ctor)) { print("new ") @@ -588,11 +585,9 @@ object Printers { } printArgs(args) - case JSPrivateSelect(qualifier, className, field) => + case JSPrivateSelect(qualifier, field) => print(qualifier) print('.') - print(className) - print("::") print(field) case JSSelect(qualifier, item) => @@ -1113,6 +1108,9 @@ object Printers { def print(ident: LabelIdent): Unit = print(ident.name) + def print(ident: SimpleFieldIdent): Unit = + print(ident.name) + def print(ident: FieldIdent): Unit = print(ident.name) @@ -1125,6 +1123,9 @@ object Printers { def print(name: Name): Unit = printEscapeJS(name.nameString, out) + def print(name: FieldName): Unit = + printEscapeJS(name.nameString, out) + def print(name: MethodName): Unit = printEscapeJS(name.nameString, out) 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 f4c02e8760..9be664598a 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala @@ -321,14 +321,14 @@ object Serializers { case StoreModule() => writeTagAndPos(TagStoreModule) - case Select(qualifier, className, field) => + case Select(qualifier, field) => writeTagAndPos(TagSelect) - writeTree(qualifier); writeName(className); writeFieldIdent(field) + writeTree(qualifier); writeFieldIdent(field) writeType(tree.tpe) - case SelectStatic(className, field) => + case SelectStatic(field) => writeTagAndPos(TagSelectStatic) - writeName(className); writeFieldIdent(field) + writeFieldIdent(field) writeType(tree.tpe) case SelectJSNativeMember(className, member) => @@ -385,7 +385,7 @@ object Serializers { case RecordSelect(record, field) => writeTagAndPos(TagRecordSelect) - writeTree(record); writeFieldIdent(field) + writeTree(record); writeSimpleFieldIdent(field) writeType(tree.tpe) case IsInstanceOf(expr, testType) => @@ -420,9 +420,9 @@ object Serializers { writeTagAndPos(TagJSNew) writeTree(ctor); writeTreeOrJSSpreads(args) - case JSPrivateSelect(qualifier, className, field) => + case JSPrivateSelect(qualifier, field) => writeTagAndPos(TagJSPrivateSelect) - writeTree(qualifier); writeName(className); writeFieldIdent(field) + writeTree(qualifier); writeFieldIdent(field) case JSSelect(qualifier, item) => writeTagAndPos(TagJSSelect) @@ -632,7 +632,7 @@ object Serializers { case FieldDef(flags, name, originalName, ftpe) => writeByte(TagFieldDef) writeInt(MemberFlags.toBits(flags)) - writeFieldIdent(name) + writeFieldIdentForEnclosingClass(name) writeOriginalName(originalName) writeType(ftpe) @@ -762,7 +762,7 @@ object Serializers { case TopLevelFieldExportDef(moduleID, exportName, field) => writeByte(TagTopLevelFieldExportDef) - writeString(moduleID); writeString(exportName); writeFieldIdent(field) + writeString(moduleID); writeString(exportName); writeFieldIdentForEnclosingClass(field) } } @@ -782,11 +782,23 @@ object Serializers { writeName(ident.name) } - def writeFieldIdent(ident: FieldIdent): Unit = { + def writeSimpleFieldIdent(ident: SimpleFieldIdent): Unit = { writePosition(ident.pos) writeName(ident.name) } + def writeFieldIdent(ident: FieldIdent): Unit = { + // For historical reasons, the className comes *before* the position + writeName(ident.name.className) + writePosition(ident.pos) + writeName(ident.name.simpleName) + } + + def writeFieldIdentForEnclosingClass(ident: FieldIdent): Unit = { + writePosition(ident.pos) + writeName(ident.name.simpleName) + } + def writeMethodIdent(ident: MethodIdent): Unit = { writePosition(ident.pos) writeMethodName(ident.name) @@ -1003,12 +1015,23 @@ object Serializers { private[this] var encodedNames: Array[UTF8String] = _ private[this] var localNames: Array[LocalName] = _ private[this] var labelNames: Array[LabelName] = _ - private[this] var fieldNames: Array[FieldName] = _ + private[this] var simpleFieldNames: Array[SimpleFieldName] = _ private[this] var simpleMethodNames: Array[SimpleMethodName] = _ private[this] var classNames: Array[ClassName] = _ private[this] var methodNames: Array[MethodName] = _ private[this] var strings: Array[String] = _ + /** Uniqueness cache for FieldName's. + * + * For historical reasons, the `ClassName` and `SimpleFieldName` + * components of `FieldName`s are store separately in the `.sjsir` format. + * Since most if not all occurrences of any particular `FieldName` + * typically come from a single `.sjsir` file, we use a uniqueness cache + * to make them all `eq`, consuming less memory and speeding up equality + * tests. + */ + private[this] val uniqueFieldNames = mutable.AnyRefMap.empty[FieldName, FieldName] + private[this] var lastPosition: Position = Position.NoPosition private[this] var enclosingClassName: ClassName = _ @@ -1031,7 +1054,7 @@ object Serializers { } localNames = new Array(encodedNames.length) labelNames = new Array(encodedNames.length) - fieldNames = new Array(encodedNames.length) + simpleFieldNames = new Array(encodedNames.length) simpleMethodNames = new Array(encodedNames.length) classNames = new Array(encodedNames.length) methodNames = Array.fill(readInt()) { @@ -1171,7 +1194,6 @@ object Serializers { case TagSelect => val qualifier = readTree() - val className = readClassName() val field = readFieldIdent() val tpe = readType() @@ -1179,12 +1201,12 @@ object Serializers { /* Note [Nothing FieldDef rewrite] * qual.field[nothing] --> throw qual.field[null] */ - Throw(Select(qualifier, className, field)(NullType)) + Throw(Select(qualifier, field)(NullType)) } else { - Select(qualifier, className, field)(tpe) + Select(qualifier, field)(tpe) } - case TagSelectStatic => SelectStatic(readClassName(), readFieldIdent())(readType()) + case TagSelectStatic => SelectStatic(readFieldIdent())(readType()) case TagSelectJSNativeMember => SelectJSNativeMember(readClassName(), readMethodIdent()) case TagApply => @@ -1219,7 +1241,7 @@ object Serializers { UnwrapFromThrowable(readTree()) case TagJSNew => JSNew(readTree(), readTreeOrJSSpreads()) - case TagJSPrivateSelect => JSPrivateSelect(readTree(), readClassName(), readFieldIdent()) + case TagJSPrivateSelect => JSPrivateSelect(readTree(), readFieldIdent()) case TagJSSelect => JSSelect(readTree(), readTree()) case TagJSFunctionApply => JSFunctionApply(readTree(), readTreeOrJSSpreads()) case TagJSMethodApply => JSMethodApply(readTree(), readTree(), readTreeOrJSSpreads()) @@ -1495,7 +1517,7 @@ object Serializers { private def readFieldDef()(implicit pos: Position): FieldDef = { val flags = MemberFlags.fromBits(readInt()) - val name = readFieldIdent() + val name = readFieldIdentForEnclosingClass() val originalName = readOriginalName() val ftpe0 = readType() @@ -1721,7 +1743,9 @@ object Serializers { case TagTopLevelJSClassExportDef => TopLevelJSClassExportDef(readModuleID(), readString()) case TagTopLevelModuleExportDef => TopLevelModuleExportDef(readModuleID(), readString()) case TagTopLevelMethodExportDef => TopLevelMethodExportDef(readModuleID(), readJSMethodDef()) - case TagTopLevelFieldExportDef => TopLevelFieldExportDef(readModuleID(), readString(), readFieldIdent()) + + case TagTopLevelFieldExportDef => + TopLevelFieldExportDef(readModuleID(), readString(), readFieldIdentForEnclosingClass()) } } @@ -1739,8 +1763,22 @@ object Serializers { } def readFieldIdent(): FieldIdent = { + // For historical reasons, the className comes *before* the position + val className = readClassName() + implicit val pos = readPosition() + val simpleName = readSimpleFieldName() + FieldIdent(makeFieldName(className, simpleName)) + } + + def readFieldIdentForEnclosingClass(): FieldIdent = { implicit val pos = readPosition() - FieldIdent(readFieldName()) + val simpleName = readSimpleFieldName() + FieldIdent(makeFieldName(enclosingClassName, simpleName)) + } + + private def makeFieldName(className: ClassName, simpleName: SimpleFieldName): FieldName = { + val newFieldName = FieldName(className, simpleName) + uniqueFieldNames.getOrElseUpdate(newFieldName, newFieldName) } def readMethodIdent(): MethodIdent = { @@ -1826,7 +1864,7 @@ object Serializers { case TagRecordType => RecordType(List.fill(readInt()) { - val name = readFieldName() + val name = readSimpleFieldName() val originalName = readString() val tpe = readType() val mutable = readBoolean() @@ -1959,14 +1997,14 @@ object Serializers { } } - private def readFieldName(): FieldName = { + private def readSimpleFieldName(): SimpleFieldName = { val i = readInt() - val existing = fieldNames(i) + val existing = simpleFieldNames(i) if (existing ne null) { existing } else { - val result = FieldName(encodedNames(i)) - fieldNames(i) = result + val result = SimpleFieldName(encodedNames(i)) + simpleFieldNames(i) = result result } } 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 415db46f33..6d30327786 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala @@ -91,8 +91,8 @@ object Transformers { case New(className, ctor, args) => New(className, ctor, args map transformExpr) - case Select(qualifier, className, field) => - Select(transformExpr(qualifier), className, field)(tree.tpe) + case Select(qualifier, field) => + Select(transformExpr(qualifier), field)(tree.tpe) case Apply(flags, receiver, method, args) => Apply(flags, transformExpr(receiver), method, @@ -158,8 +158,8 @@ object Transformers { case JSNew(ctor, args) => JSNew(transformExpr(ctor), args.map(transformExprOrJSSpread)) - case JSPrivateSelect(qualifier, className, field) => - JSPrivateSelect(transformExpr(qualifier), className, field) + case JSPrivateSelect(qualifier, field) => + JSPrivateSelect(transformExpr(qualifier), field) case JSSelect(qualifier, item) => JSSelect(transformExpr(qualifier), transformExpr(item)) 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 2040341a74..8a8909cdce 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala @@ -77,7 +77,7 @@ object Traversers { case New(_, _, args) => args foreach traverse - case Select(qualifier, _, _) => + case Select(qualifier, _) => traverse(qualifier) case Apply(_, receiver, _, args) => @@ -147,7 +147,7 @@ object Traversers { traverse(ctor) args.foreach(traverseTreeOrJSSpread) - case JSPrivateSelect(qualifier, _, _) => + case JSPrivateSelect(qualifier, _) => traverse(qualifier) case JSSelect(qualifier, item) => 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 631e39642a..c3d206624a 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala @@ -59,6 +59,9 @@ object Trees { sealed case class LabelIdent(name: LabelName)(implicit val pos: Position) extends IRNode + sealed case class SimpleFieldIdent(name: SimpleFieldName)(implicit val pos: Position) + extends IRNode + sealed case class FieldIdent(name: FieldName)(implicit val pos: Position) extends IRNode @@ -241,13 +244,10 @@ object Trees { val tpe = NoType // cannot be in expression position } - sealed case class Select(qualifier: Tree, className: ClassName, - field: FieldIdent)( - val tpe: Type)( + sealed case class Select(qualifier: Tree, field: FieldIdent)(val tpe: Type)( implicit val pos: Position) extends AssignLhs - sealed case class SelectStatic(className: ClassName, field: FieldIdent)( - val tpe: Type)( + sealed case class SelectStatic(field: FieldIdent)(val tpe: Type)( implicit val pos: Position) extends AssignLhs sealed case class SelectJSNativeMember(className: ClassName, member: MethodIdent)( @@ -465,7 +465,7 @@ object Trees { sealed case class RecordValue(tpe: RecordType, elems: List[Tree])( implicit val pos: Position) extends Tree - sealed case class RecordSelect(record: Tree, field: FieldIdent)( + sealed case class RecordSelect(record: Tree, field: SimpleFieldIdent)( val tpe: Type)( implicit val pos: Position) extends AssignLhs @@ -512,8 +512,7 @@ object Trees { val tpe = AnyType } - sealed case class JSPrivateSelect(qualifier: Tree, className: ClassName, - field: FieldIdent)( + sealed case class JSPrivateSelect(qualifier: Tree, field: FieldIdent)( implicit val pos: Position) extends AssignLhs { val tpe = AnyType } diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Types.scala b/ir/shared/src/main/scala/org/scalajs/ir/Types.scala index 459f42f457..4f91fd3319 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Types.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Types.scala @@ -143,12 +143,12 @@ object Types { * The compiler itself never generates record types. */ final case class RecordType(fields: List[RecordType.Field]) extends Type { - def findField(name: FieldName): RecordType.Field = + def findField(name: SimpleFieldName): RecordType.Field = fields.find(_.name == name).get } object RecordType { - final case class Field(name: FieldName, originalName: OriginalName, + final case class Field(name: SimpleFieldName, originalName: OriginalName, tpe: Type, mutable: Boolean) } 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 03c1628f16..ab3c6be098 100644 --- a/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala +++ b/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala @@ -307,12 +307,12 @@ class PrintersTest { @Test def printSelect(): Unit = { assertPrintEquals("x.test.Test::f", - Select(ref("x", "test.Test"), "test.Test", "f")(IntType)) + Select(ref("x", "test.Test"), FieldName("test.Test", "f"))(IntType)) } @Test def printSelectStatic(): Unit = { assertPrintEquals("test.Test::f", - SelectStatic("test.Test", "f")(IntType)) + SelectStatic(FieldName("test.Test", "f"))(IntType)) } @Test def printApply(): Unit = { @@ -606,21 +606,21 @@ class PrintersTest { assertPrintEquals("new C()", JSNew(ref("C", AnyType), Nil)) assertPrintEquals("new C(4, 5)", JSNew(ref("C", AnyType), List(i(4), i(5)))) assertPrintEquals("new x.test.Test::C(4, 5)", - JSNew(JSPrivateSelect(ref("x", AnyType), "test.Test", "C"), List(i(4), i(5)))) + JSNew(JSPrivateSelect(ref("x", AnyType), FieldName("test.Test", "C")), List(i(4), i(5)))) assertPrintEquals("""new x["C"]()""", JSNew(JSSelect(ref("x", AnyType), StringLiteral("C")), Nil)) val fApplied = JSFunctionApply(ref("f", AnyType), Nil) assertPrintEquals("new (f())()", JSNew(fApplied, Nil)) assertPrintEquals("new (f().test.Test::C)(4, 5)", - JSNew(JSPrivateSelect(fApplied, "test.Test", "C"), List(i(4), i(5)))) + JSNew(JSPrivateSelect(fApplied, FieldName("test.Test", "C")), List(i(4), i(5)))) assertPrintEquals("""new (f()["C"])()""", JSNew(JSSelect(fApplied, StringLiteral("C")), Nil)) } @Test def printJSPrivateSelect(): Unit = { assertPrintEquals("x.test.Test::f", - JSPrivateSelect(ref("x", AnyType), "test.Test", "f")) + JSPrivateSelect(ref("x", AnyType), FieldName("test.Test", "f"))) } @Test def printJSSelect(): Unit = { @@ -634,12 +634,12 @@ class PrintersTest { JSFunctionApply(ref("f", AnyType), List(i(3), i(4)))) assertPrintEquals("(0, x.test.Test::f)()", - JSFunctionApply(JSPrivateSelect(ref("x", AnyType), "test.Test", "f"), Nil)) + JSFunctionApply(JSPrivateSelect(ref("x", AnyType), FieldName("test.Test", "f")), Nil)) assertPrintEquals("""(0, x["f"])()""", JSFunctionApply(JSSelect(ref("x", AnyType), StringLiteral("f")), Nil)) assertPrintEquals("(0, x.test.Test::f)()", - JSFunctionApply(Select(ref("x", "test.Test"), "test.Test", "f")(AnyType), + JSFunctionApply(Select(ref("x", "test.Test"), FieldName("test.Test", "f"))(AnyType), Nil)) } @@ -1137,7 +1137,7 @@ class PrintersTest { assertPrintEquals( """ |module class Test extends java.lang.Object { - | val x: int + | val Test::x: int | def m;I(): int = | constructor def constructor(): any = { | super() @@ -1151,7 +1151,7 @@ class PrintersTest { """, ClassDef("Test", NON, ClassKind.ModuleClass, None, Some(ObjectClass), Nil, None, None, - List(FieldDef(MemberFlags.empty, "x", NON, IntType)), + List(FieldDef(MemberFlags.empty, FieldName("Test", "x"), NON, IntType)), List(MethodDef(MemberFlags.empty, MethodName("m", Nil, I), NON, Nil, IntType, None)(NoOptHints, UNV)), Some(JSConstructorDef(MemberFlags.empty.withNamespace(Constructor), Nil, None, JSConstructorBody(Nil, JSSuperConstructorCall(Nil), Nil))(NoOptHints, UNV)), @@ -1163,12 +1163,12 @@ class PrintersTest { } @Test def printFieldDef(): Unit = { - assertPrintEquals("val x: int", - FieldDef(MemberFlags.empty, "x", NON, IntType)) - assertPrintEquals("var y: any", - FieldDef(MemberFlags.empty.withMutable(true), "y", NON, AnyType)) - assertPrintEquals("val x{orig name}: int", - FieldDef(MemberFlags.empty, "x", TestON, IntType)) + assertPrintEquals("val Test::x: int", + FieldDef(MemberFlags.empty, FieldName("Test", "x"), NON, IntType)) + assertPrintEquals("var Test::y: any", + FieldDef(MemberFlags.empty.withMutable(true), FieldName("Test", "y"), NON, AnyType)) + assertPrintEquals("val Test::x{orig name}: int", + FieldDef(MemberFlags.empty, FieldName("Test", "x"), TestON, IntType)) } @Test def printJSFieldDef(): Unit = { @@ -1428,8 +1428,8 @@ class PrintersTest { @Test def printTopLevelFieldExportDef(): Unit = { assertPrintEquals( """ - |export top[moduleID="main"] static field x$1 as "x" + |export top[moduleID="main"] static field Test::x$1 as "x" """, - TopLevelFieldExportDef("main", "x", "x$1")) + TopLevelFieldExportDef("main", "x", FieldName("Test", "x$1"))) } } diff --git a/ir/shared/src/test/scala/org/scalajs/ir/TestIRBuilder.scala b/ir/shared/src/test/scala/org/scalajs/ir/TestIRBuilder.scala index 83255c1ca2..3cc7058b8f 100644 --- a/ir/shared/src/test/scala/org/scalajs/ir/TestIRBuilder.scala +++ b/ir/shared/src/test/scala/org/scalajs/ir/TestIRBuilder.scala @@ -37,8 +37,8 @@ object TestIRBuilder { val NoOptHints = OptimizerHints.empty // String -> Name conversions - implicit def string2fieldName(name: String): FieldName = - FieldName(name) + implicit def string2simpleFieldName(name: String): SimpleFieldName = + SimpleFieldName(name) implicit def string2className(name: String): ClassName = ClassName(name) @@ -47,8 +47,8 @@ object TestIRBuilder { LocalIdent(LocalName(name)) implicit def string2labelIdent(name: String): LabelIdent = LabelIdent(LabelName(name)) - implicit def string2fieldIdent(name: String): FieldIdent = - FieldIdent(FieldName(name)) + implicit def string2simpleFieldIdent(name: String): SimpleFieldIdent = + SimpleFieldIdent(SimpleFieldName(name)) implicit def string2classIdent(name: String): ClassIdent = ClassIdent(ClassName(name)) @@ -59,6 +59,8 @@ object TestIRBuilder { ClassRef(ClassName(className)) // Name -> Ident conversions + implicit def fieldName2fieldIdent(name: FieldName): FieldIdent = + FieldIdent(name) implicit def methodName2methodIdent(name: MethodName): MethodIdent = MethodIdent(name) implicit def className2classRef(className: ClassName): ClassRef = 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 d063cd5b6a..fc57860d25 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 @@ -194,23 +194,23 @@ object Infos { private def forClass(cls: ClassName): ReachabilityInfoInClassBuilder = byClass.getOrElseUpdate(cls, new ReachabilityInfoInClassBuilder(cls)) - def addFieldRead(cls: ClassName, field: FieldName): this.type = { - forClass(cls).addFieldRead(field) + def addFieldRead(field: FieldName): this.type = { + forClass(field.className).addFieldRead(field) this } - def addFieldWritten(cls: ClassName, field: FieldName): this.type = { - forClass(cls).addFieldWritten(field) + def addFieldWritten(field: FieldName): this.type = { + forClass(field.className).addFieldWritten(field) this } - def addStaticFieldRead(cls: ClassName, field: FieldName): this.type = { - forClass(cls).addStaticFieldRead(field) + def addStaticFieldRead(field: FieldName): this.type = { + forClass(field.className).addStaticFieldRead(field) this } - def addStaticFieldWritten(cls: ClassName, field: FieldName): this.type = { - forClass(cls).addStaticFieldWritten(field) + def addStaticFieldWritten(field: FieldName): this.type = { + forClass(field.className).addStaticFieldWritten(field) this } @@ -560,8 +560,8 @@ object Infos { case topLevelFieldExport: TopLevelFieldExportDef => val field = topLevelFieldExport.field.name - builder.addStaticFieldRead(enclosingClass, field) - builder.addStaticFieldWritten(enclosingClass, field) + builder.addStaticFieldRead(field) + builder.addStaticFieldWritten(field) } builder.result() @@ -576,14 +576,14 @@ object Infos { */ case Assign(lhs, rhs) => lhs match { - case Select(qualifier, className, field) => - builder.addFieldWritten(className, field.name) + case Select(qualifier, field) => + builder.addFieldWritten(field.name) traverse(qualifier) - case SelectStatic(className, field) => - builder.addStaticFieldWritten(className, field.name) - case JSPrivateSelect(qualifier, className, field) => - builder.addStaticallyReferencedClass(className) // for the private name of the field - builder.addFieldWritten(className, field.name) + case SelectStatic(field) => + builder.addStaticFieldWritten(field.name) + case JSPrivateSelect(qualifier, field) => + builder.addStaticallyReferencedClass(field.name.className) // for the private name of the field + builder.addFieldWritten(field.name) traverse(qualifier) case _ => traverse(lhs) @@ -596,10 +596,10 @@ object Infos { case New(className, ctor, _) => builder.addInstantiatedClass(className, ctor.name) - case Select(_, className, field) => - builder.addFieldRead(className, field.name) - case SelectStatic(className, field) => - builder.addStaticFieldRead(className, field.name) + case Select(_, field) => + builder.addFieldRead(field.name) + case SelectStatic(field) => + builder.addStaticFieldRead(field.name) case SelectJSNativeMember(className, member) => builder.addJSNativeMemberUsed(className, member.name) @@ -687,9 +687,9 @@ object Infos { case UnwrapFromThrowable(_) => builder.addUsedInstanceTest(JavaScriptExceptionClass) - case JSPrivateSelect(_, className, field) => - builder.addStaticallyReferencedClass(className) // for the private name of the field - builder.addFieldRead(className, field.name) + case JSPrivateSelect(_, field) => + builder.addStaticallyReferencedClass(field.name.className) // for the private name of the field + builder.addFieldRead(field.name) case JSNewTarget() => builder.addAccessNewTarget() 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 dff91a1fd1..211b335e29 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 @@ -298,18 +298,15 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[js.Function] = { val superCtorCallAndFieldDefs = if (forESClass) { - val fieldDefs = genFieldDefsOfScalaClass(className, + val fieldDefs = genFieldDefsOfScalaClass( globalKnowledge.getFieldDefs(className)) if (superClass.isEmpty) fieldDefs else js.Apply(js.Super(), Nil) :: fieldDefs } else { - val allFields = - globalKnowledge.getAllScalaClassFieldDefs(className) - allFields.flatMap { classAndFields => - genFieldDefsOfScalaClass(classAndFields._1, classAndFields._2) - } + val allFields = globalKnowledge.getAllScalaClassFieldDefs(className) + genFieldDefsOfScalaClass(allFields) } initToInline.fold { @@ -349,8 +346,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } /** Generates the creation of fields for a Scala class. */ - private def genFieldDefsOfScalaClass(className: ClassName, - fields: List[AnyFieldDef])( + private def genFieldDefsOfScalaClass(fields: List[AnyFieldDef])( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge): List[js.Tree] = { for { @@ -359,7 +355,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } yield { val field = anyField.asInstanceOf[FieldDef] implicit val pos = field.pos - js.Assign(genSelect(js.This(), className, field.name, field.originalName), + js.Assign(genSelect(js.This(), field.name, field.originalName), genZeroOf(field.ftpe)) } } @@ -374,13 +370,12 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } yield { implicit val pos = field.pos - val varScope = (className, name) val value = genZeroOf(ftpe) if (flags.isMutable) - globallyMutableVarDef(VarField.t, VarField.u, varScope, value, origName.orElse(name)) + globallyMutableVarDef(VarField.t, VarField.u, name, value, origName.orElse(name)) else - globalVarDef(VarField.t, varScope, value, origName.orElse(name)) + globalVarDef(VarField.t, name, value, origName.orElse(name)) } WithGlobals.flatten(defs) @@ -407,7 +402,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } symbolValueWithGlobals.flatMap { symbolValue => - globalVarDef(VarField.r, (className, name), symbolValue, origName.orElse(name)) + globalVarDef(VarField.r, name, symbolValue, origName.orElse(name)) } } @@ -1091,17 +1086,15 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { implicit val pos = tree.pos - val varScope = (className, field.name) - moduleKind match { case ModuleKind.NoModule => /* Initial value of the export. Updates are taken care of explicitly * when we assign to the static field. */ - genAssignToNoModuleExportVar(exportName, globalVar(VarField.t, varScope)) + genAssignToNoModuleExportVar(exportName, globalVar(VarField.t, field.name)) case ModuleKind.ESModule => - WithGlobals(globalVarExport(VarField.t, varScope, js.ExportName(exportName))) + WithGlobals(globalVarExport(VarField.t, field.name, js.ExportName(exportName))) case ModuleKind.CommonJSModule => globalRef("exports").flatMap { exportsVarRef => @@ -1110,7 +1103,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { js.StringLiteral(exportName), List( "get" -> js.Function(arrow = false, Nil, None, { - js.Return(globalVar(VarField.t, varScope)) + js.Return(globalVar(VarField.t, field.name)) }), "configurable" -> js.BooleanLiteral(true) ) 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 4c1bcebc2b..ba31cd7019 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,8 +26,8 @@ private[emitter] object EmitterNames { // Field names - val dataFieldName = FieldName("data") - val exceptionFieldName = FieldName("exception") + 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 128e8ebeed..32b8baca4f 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 @@ -459,11 +459,11 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { } def makeRecordFieldIdent(recIdent: js.Ident, - fieldName: FieldName, fieldOrigName: OriginalName)( + fieldName: SimpleFieldName, fieldOrigName: OriginalName)( implicit pos: Position): js.Ident = { /* "__" is a safe separator for generated names because JSGen avoids it - * when generating `LocalName`s and `FieldName`s. + * when generating `LocalName`s and `SimpleFieldName`s. */ val name = recIdent.name + "__" + genName(fieldName) val originalName = OriginalName( @@ -607,11 +607,11 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case Assign(lhs, rhs) => lhs match { - case Select(qualifier, className, field) => + case Select(qualifier, field) => unnest(checkNotNull(qualifier), rhs) { (newQualifier, newRhs, env0) => implicit val env = env0 js.Assign( - genSelect(transformExprNoChar(newQualifier), className, field)(lhs.pos), + genSelect(transformExprNoChar(newQualifier), field)(lhs.pos), transformExpr(newRhs, lhs.tpe)) } @@ -648,12 +648,12 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { mutable = true)(lhs.tpe)) pushLhsInto(Lhs.Assign(newLhs), rhs, tailPosLabels) - case JSPrivateSelect(qualifier, className, field) => + case JSPrivateSelect(qualifier, field) => unnest(qualifier, rhs) { (newQualifier, newRhs, env0) => implicit val env = env0 js.Assign( - genJSPrivateSelect(transformExprNoChar(newQualifier), - className, field)(moduleContext, globalKnowledge, lhs.pos), + genJSPrivateSelect(transformExprNoChar(newQualifier), field)( + moduleContext, globalKnowledge, lhs.pos), transformExprNoChar(newRhs)) } @@ -676,13 +676,11 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { transformExprNoChar(rhs)) } - case SelectStatic(className, item) => - val scope = (className, item.name) - - if (needToUseGloballyMutableVarSetter(scope)) { + case SelectStatic(item) => + if (needToUseGloballyMutableVarSetter(item.name)) { unnest(rhs) { (rhs, env0) => implicit val env = env0 - js.Apply(globalVar(VarField.u, scope), transformExpr(rhs, lhs.tpe) :: Nil) + js.Apply(globalVar(VarField.u, item.name), transformExpr(rhs, lhs.tpe) :: Nil) } } else { // Assign normally. @@ -837,7 +835,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { field match { case FieldDef(_, name, _, _) => js.Assign( - genJSPrivateSelect(js.This(), enclosingClassName, name), + genJSPrivateSelect(js.This(), name), zero) case JSFieldDef(_, name, _) => @@ -1066,8 +1064,8 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case New(className, constr, args) if noExtractYet => New(className, constr, recs(args)) - case Select(qualifier, className, item) if noExtractYet => - Select(rec(qualifier), className, item)(arg.tpe) + case Select(qualifier, item) if noExtractYet => + Select(rec(qualifier), item)(arg.tpe) case Apply(flags, receiver, method, args) if noExtractYet => val newArgs = recs(args) Apply(flags, rec(receiver), method, newArgs)(arg.tpe) @@ -1292,9 +1290,9 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { testNPE(obj) // Expressions preserving side-effect freedom (modulo NPE) - case Select(qualifier, _, _) => + case Select(qualifier, _) => allowUnpure && testNPE(qualifier) - case SelectStatic(_, _) => + case SelectStatic(_) => allowUnpure case ArrayValue(tpe, elems) => allowUnpure && (elems forall test) @@ -1354,7 +1352,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { allowSideEffects && test(fun) && (args.forall(testJSArg)) case Transient(JSNewVararg(ctor, argArray)) => allowSideEffects && test(ctor) && test(argArray) - case JSPrivateSelect(qualifier, _, _) => + case JSPrivateSelect(qualifier, _) => allowSideEffects && test(qualifier) case JSSelect(qualifier, item) => allowSideEffects && test(qualifier) && test(item) @@ -1467,10 +1465,9 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { val base = js.Assign(transformExpr(lhs, preserveChar = true), transformExpr(rhs, lhs.tpe)) lhs match { - case SelectStatic(className, FieldIdent(field)) + case SelectStatic(FieldIdent(field)) if moduleKind == ModuleKind.NoModule => - val mirrors = - globalKnowledge.getStaticFieldMirrors(className, field) + val mirrors = globalKnowledge.getStaticFieldMirrors(field) mirrors.foldLeft(base) { (prev, mirror) => js.Assign(genGlobalVarRef(mirror), prev) } @@ -1716,9 +1713,9 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { redo(New(className, ctor, newArgs))(env) } - case Select(qualifier, className, item) => + case Select(qualifier, item) => unnest(qualifier) { (newQualifier, env) => - redo(Select(newQualifier, className, item)(rhs.tpe))(env) + redo(Select(newQualifier, item)(rhs.tpe))(env) } case Apply(flags, receiver, method, args) => @@ -1924,9 +1921,9 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { redo(JSImportCall(newArg))(env) } - case JSPrivateSelect(qualifier, className, field) => + case JSPrivateSelect(qualifier, field) => unnest(qualifier) { (newQualifier, env) => - redo(JSPrivateSelect(newQualifier, className, field))(env) + redo(JSPrivateSelect(newQualifier, field))(env) } case JSSelect(qualifier, item) => @@ -2210,11 +2207,11 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case LoadModule(className) => genLoadModule(className) - case Select(qualifier, className, field) => - genSelect(transformExprNoChar(checkNotNull(qualifier)), className, field) + case Select(qualifier, field) => + genSelect(transformExprNoChar(checkNotNull(qualifier)), field) - case SelectStatic(className, item) => - globalVar(VarField.t, (className, item.name)) + case SelectStatic(item) => + globalVar(VarField.t, item.name) case SelectJSNativeMember(className, member) => val jsNativeLoadSpec = @@ -2725,7 +2722,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { val newExpr = transformExprNoChar(expr).asInstanceOf[js.VarRef] js.If( genIsInstanceOfClass(newExpr, JavaScriptExceptionClass), - genSelect(newExpr, JavaScriptExceptionClass, FieldIdent(exceptionFieldName)), + genSelect(newExpr, FieldIdent(exceptionFieldName)), genCheckNotNull(newExpr)) // Transients @@ -2738,7 +2735,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case Transient(ZeroOf(runtimeClass)) => js.DotSelect( genSelect(transformExprNoChar(checkNotNull(runtimeClass)), - ClassClass, FieldIdent(dataFieldName)), + FieldIdent(dataFieldName)), js.Ident("zero")) case Transient(NativeArrayWrapper(elemClass, nativeArray)) => @@ -2751,7 +2748,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case _ => val elemClassData = genSelect( transformExprNoChar(checkNotNull(elemClass)), - ClassClass, FieldIdent(dataFieldName)) + FieldIdent(dataFieldName)) val arrayClassData = js.Apply( js.DotSelect(elemClassData, js.Ident("getArrayOf")), Nil) js.Apply(arrayClassData DOT "wrapArray", newNativeArray :: Nil) @@ -2808,8 +2805,8 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { genCallHelper(VarField.newJSObjectWithVarargs, transformExprNoChar(constr), transformExprNoChar(argsArray)) - case JSPrivateSelect(qualifier, className, field) => - genJSPrivateSelect(transformExprNoChar(qualifier), className, field) + case JSPrivateSelect(qualifier, field) => + genJSPrivateSelect(transformExprNoChar(qualifier), field) case JSSelect(qualifier, item) => genBracketSelect(transformExprNoChar(qualifier), diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/GlobalKnowledge.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/GlobalKnowledge.scala index 1122c4b935..84592f0ec1 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/GlobalKnowledge.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/GlobalKnowledge.scala @@ -33,8 +33,7 @@ private[emitter] trait GlobalKnowledge { * It is invalid to call this method with anything but a `Class` or * `ModuleClass`. */ - def getAllScalaClassFieldDefs( - className: ClassName): List[(ClassName, List[AnyFieldDef])] + def getAllScalaClassFieldDefs(className: ClassName): List[AnyFieldDef] /** Tests whether the specified class uses an inlineable init. * @@ -82,7 +81,7 @@ private[emitter] trait GlobalKnowledge { def getFieldDefs(className: ClassName): List[AnyFieldDef] /** The global variables that mirror a given static field. */ - def getStaticFieldMirrors(className: ClassName, field: FieldName): List[String] + def getStaticFieldMirrors(field: FieldName): List[String] /** The module containing this class definition. * 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 2d7f18b19c..1e9f26e285 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 @@ -178,7 +178,7 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { def isInterface(className: ClassName): Boolean = classes(className).askIsInterface(this) - def getAllScalaClassFieldDefs(className: ClassName): List[(ClassName, List[AnyFieldDef])] = + def getAllScalaClassFieldDefs(className: ClassName): List[AnyFieldDef] = classes(className).askAllScalaClassFieldDefs(this) def hasInlineableInit(className: ClassName): Boolean = @@ -205,8 +205,8 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { def getFieldDefs(className: ClassName): List[AnyFieldDef] = classes(className).askFieldDefs(this) - def getStaticFieldMirrors(className: ClassName, field: FieldName): List[String] = - classes(className).askStaticFieldMirrors(this, field) + def getStaticFieldMirrors(field: FieldName): List[String] = + classes(field.className).askStaticFieldMirrors(this, field) def getModule(className: ClassName): ModuleID = classes(className).askModule(this) @@ -366,8 +366,8 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { * which will change every time the reachability analysis of the * `JSFieldDef`s changes (because we either keep all or none of * them), and - * - the list of names of the `FieldDef`s, which will change every time - * the reachability analysis of the `FieldDef`s changes. + * - the list of simple names of the `FieldDef`s, which will change every + * time the reachability analysis of the `FieldDef`s changes. * * We do not try to use the names of `JSFieldDef`s because they are * `Tree`s, which are not efficiently comparable nor versionable here. @@ -376,7 +376,7 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { val hasAnyJSField = linkedClass.fields.exists(_.isInstanceOf[JSFieldDef]) val hasAnyJSFieldVersion = Version.fromByte(if (hasAnyJSField) 1 else 0) val scalaFieldNamesVersion = linkedClass.fields.collect { - case FieldDef(_, FieldIdent(name), _, _) => Version.fromUTF8String(name.encoded) + case FieldDef(_, FieldIdent(name), _, _) => Version.fromUTF8String(name.simpleName.encoded) } Version.combine((linkedClass.version :: hasAnyJSFieldVersion :: scalaFieldNamesVersion): _*) } @@ -396,15 +396,14 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { isInterface } - def askAllScalaClassFieldDefs( - invalidatable: Invalidatable): List[(ClassName, List[AnyFieldDef])] = { + def askAllScalaClassFieldDefs(invalidatable: Invalidatable): List[AnyFieldDef] = { invalidatable.registeredTo(this) superClassAskers += invalidatable fieldDefsAskers += invalidatable val inheritedFieldDefs = if (superClass == null) Nil else classes(superClass).askAllScalaClassFieldDefs(invalidatable) - inheritedFieldDefs :+ (className -> fieldDefs) + inheritedFieldDefs ::: fieldDefs } def askHasInlineableInit(invalidatable: Invalidatable): Boolean = { diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameGen.scala index 3f21ea8adb..552dd545bd 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameGen.scala @@ -47,8 +47,8 @@ private[emitter] final class NameGen { cache } - private val genFieldNameCache = - mutable.Map.empty[FieldName, String] + private val genSimpleFieldNameCache = + mutable.Map.empty[SimpleFieldName, String] private val genMethodNameCache = mutable.Map.empty[MethodName, String] @@ -107,7 +107,10 @@ private[emitter] final class NameGen { } def genName(name: LabelName): String = genNameGeneric(name, genLabelNameCache) - def genName(name: FieldName): String = genNameGeneric(name, genFieldNameCache) + def genName(name: SimpleFieldName): String = genNameGeneric(name, genSimpleFieldNameCache) + + def genName(name: FieldName): String = + genName(name.className) + "__f_" + genName(name.simpleName) def genName(name: MethodName): String = { genMethodNameCache.getOrElseUpdate(name, { @@ -210,6 +213,11 @@ private[emitter] final class NameGen { genOriginalName(name.encoded, originalName, jsName) } + def genOriginalName(name: FieldName, originalName: OriginalName, + jsName: String): OriginalName = { + genOriginalName(name.simpleName, originalName, jsName) + } + def genOriginalName(name: MethodName, originalName: OriginalName, jsName: String): OriginalName = { genOriginalName(name.simpleName, originalName, jsName) 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 4cc050d33c..4541d3a292 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 @@ -163,22 +163,19 @@ private[emitter] final class SJSGen( genCallHelper(VarField.systemArraycopy, args: _*) } - def genSelect(receiver: Tree, className: ClassName, field: irt.FieldIdent)( + def genSelect(receiver: Tree, field: irt.FieldIdent)( implicit pos: Position): Tree = { - DotSelect(receiver, Ident(genFieldJSName(className, field))(field.pos)) + DotSelect(receiver, Ident(genName(field.name))(field.pos)) } - def genSelect(receiver: Tree, className: ClassName, field: irt.FieldIdent, + def genSelect(receiver: Tree, field: irt.FieldIdent, originalName: OriginalName)( implicit pos: Position): Tree = { - val jsName = genFieldJSName(className, field) + val jsName = genName(field.name) val jsOrigName = genOriginalName(field.name, originalName, jsName) DotSelect(receiver, Ident(jsName, jsOrigName)(field.pos)) } - private def genFieldJSName(className: ClassName, field: irt.FieldIdent): String = - genName(className) + "__f_" + genName(field.name) - def genApply(receiver: Tree, methodName: MethodName, args: List[Tree])( implicit pos: Position): Tree = { Apply(DotSelect(receiver, Ident(genMethodName(methodName))), args) @@ -192,13 +189,12 @@ private[emitter] final class SJSGen( def genMethodName(methodName: MethodName): String = genName(methodName) - def genJSPrivateSelect(receiver: Tree, className: ClassName, - field: irt.FieldIdent)( + def genJSPrivateSelect(receiver: Tree, field: irt.FieldIdent)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): Tree = { val fieldName = { implicit val pos = field.pos - globalVar(VarField.r, (className, field.name)) + globalVar(VarField.r, field.name) } BracketSelect(receiver, fieldName) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarGen.scala index 7b7f87859d..11d93244d9 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/VarGen.scala @@ -350,11 +350,11 @@ private[emitter] final class VarGen(jsGen: JSGen, nameGen: NameGen, def reprClass(x: ClassName): ClassName = x } - implicit object FieldScope extends Scope[(ClassName, FieldName)] { - def subField(x: (ClassName, FieldName)): String = - genName(x._1) + "__" + genName(x._2) + implicit object FieldScope extends Scope[FieldName] { + def subField(x: FieldName): String = + genName(x.className) + "__" + genName(x.simpleName) - def reprClass(x: (ClassName, FieldName)): ClassName = x._1 + def reprClass(x: FieldName): ClassName = x.className } implicit object MethodScope extends Scope[(ClassName, MethodName)] { 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 ba64a3b9b1..981d065512 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 @@ -214,6 +214,8 @@ private final class ClassDefChecker(classDef: ClassDef, case FieldDef(_, FieldIdent(name), _, ftpe) => if (!classDef.kind.isAnyNonNativeClass) reportError("illegal FieldDef (only non native classes may contain fields)") + if (name.className != classDef.className) + reportError(i"illegal FieldDef with name $name in class ${classDef.className}") if (fields(namespace.ordinal).put(name, ftpe).isDefined) reportError(i"duplicate ${namespace.prefixString}field '$name'") @@ -620,7 +622,7 @@ private final class ClassDefChecker(classDef: ClassDef, if (env.thisType == NoType) // can happen before JSSuperConstructorCall in JSModuleClass reportError(i"Cannot find `this` in scope for StoreModule()") - case Select(qualifier, _, _) => + case Select(qualifier, _) => checkTree(qualifier, env) case _: SelectStatic => @@ -714,7 +716,7 @@ private final class ClassDefChecker(classDef: ClassDef, checkTree(ctor, env) checkTreeOrSpreads(args, env) - case JSPrivateSelect(qualifier, className, field) => + case JSPrivateSelect(qualifier, _) => checkTree(qualifier, env) case JSSelect(qualifier, item) => diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/ErrorReporter.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/ErrorReporter.scala index 35cef51bf6..18c6527d13 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/checker/ErrorReporter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/ErrorReporter.scala @@ -36,6 +36,7 @@ private[checker] object ErrorReporter { private def format(arg: Any): String = { arg match { case arg: Name => arg.nameString + case arg: FieldName => arg.nameString case arg: MethodName => arg.displayName case arg: IRNode => arg.show case arg: TypeRef => arg.displayName 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 418629d55b..8f399fa984 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 @@ -225,23 +225,23 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { typecheckExpect(body, env.withLabeledReturnType(label.name, tpe), tpe) case Assign(lhs, rhs) => - def checkNonStaticField(receiver: Tree, className: ClassName, name: FieldName): Unit = { + def checkNonStaticField(receiver: Tree, name: FieldName): Unit = { receiver match { - case This() if env.inConstructorOf == Some(className) => + case This() if env.inConstructorOf == Some(name.className) => // ok case _ => - if (lookupClass(className).lookupField(name).exists(!_.flags.isMutable)) + if (lookupClass(name.className).lookupField(name).exists(!_.flags.isMutable)) reportError(i"Assignment to immutable field $name.") } } lhs match { - case Select(receiver, className, FieldIdent(name)) => - checkNonStaticField(receiver, className, name) - case JSPrivateSelect(receiver, className, FieldIdent(name)) => - checkNonStaticField(receiver, className, name) - case SelectStatic(className, FieldIdent(name)) => - val c = lookupClass(className) + case Select(receiver, FieldIdent(name)) => + checkNonStaticField(receiver, name) + case JSPrivateSelect(receiver, FieldIdent(name)) => + checkNonStaticField(receiver, name) + case SelectStatic(FieldIdent(name)) => + val c = lookupClass(name.className) for { f <- c.lookupStaticField(name) if !f.flags.isMutable @@ -323,7 +323,8 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { // Nothing to check; everything is checked in ClassDefChecker () - case Select(qualifier, className, FieldIdent(item)) => + case Select(qualifier, FieldIdent(item)) => + val className = item.className val c = lookupClass(className) val kind = c.kind if (!kind.isClass) { @@ -354,7 +355,8 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { } } - case SelectStatic(className, FieldIdent(item)) => + case SelectStatic(FieldIdent(item)) => + val className = item.className val checkedClass = lookupClass(className) if (checkedClass.kind.isJSType) { reportError(i"Cannot select static $item of JS type $className") @@ -530,8 +532,9 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter) { for (arg <- args) typecheckExprOrSpread(arg, env) - case JSPrivateSelect(qualifier, className, field) => + case JSPrivateSelect(qualifier, field) => typecheckExpr(qualifier, env) + val className = field.name.className val checkedClass = lookupClass(className) if (!checkedClass.kind.isJSClass && checkedClass.kind != ClassKind.AbstractJSType) { reportError(i"Cannot select JS private field $field of non-JS class $className") diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala index 05763942ff..6d8cc24f28 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala @@ -645,7 +645,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: field = anyField.asInstanceOf[FieldDef] if parent.fieldsRead.contains(field.name.name) } yield { - parent.className -> field + field } Some(new OptimizerCore.InlineableClassStructure(allFields)) @@ -717,8 +717,8 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: } def isElidableStat(tree: Tree): Boolean = tree match { - case Block(stats) => stats.forall(isElidableStat) - case Assign(Select(This(), _, _), rhs) => isTriviallySideEffectFree(rhs) + case Block(stats) => stats.forall(isElidableStat) + case Assign(Select(This(), _), rhs) => isTriviallySideEffectFree(rhs) // Mixin constructor -- test whether its body is entirely empty case ApplyStatically(flags, This(), className, methodName, Nil) @@ -1462,11 +1462,11 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: } } - protected def isFieldRead(className: ClassName, fieldName: FieldName): Boolean = - getInterface(className).askFieldRead(fieldName, asker) + protected def isFieldRead(fieldName: FieldName): Boolean = + getInterface(fieldName.className).askFieldRead(fieldName, asker) - protected def isStaticFieldRead(className: ClassName, fieldName: FieldName): Boolean = - getInterface(className).askStaticFieldRead(fieldName, asker) + protected def isStaticFieldRead(fieldName: FieldName): Boolean = + getInterface(fieldName.className).askStaticFieldRead(fieldName, asker) } } 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 a417440b22..2dca9842bd 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 @@ -88,10 +88,10 @@ private[optimizer] abstract class OptimizerCore( target: ImportTarget): Option[JSNativeLoadSpec.Import] /** Returns true if the given (non-static) field is ever read. */ - protected def isFieldRead(className: ClassName, fieldName: FieldName): Boolean + protected def isFieldRead(fieldName: FieldName): Boolean /** Returns true if the given static field is ever read. */ - protected def isStaticFieldRead(className: ClassName, fieldName: FieldName): Boolean + protected def isStaticFieldRead(fieldName: FieldName): Boolean private val localNameAllocator = new FreshNameAllocator.Local @@ -180,9 +180,9 @@ private[optimizer] abstract class OptimizerCore( } else { val after = from.tail val afterIsTrivial = after.forall { - case Assign(Select(This(), _, _), _:Literal | _:VarRef) => + case Assign(Select(This(), _), _:Literal | _:VarRef) => true - case Assign(SelectStatic(_, _), _:Literal | _:VarRef) => + case Assign(SelectStatic(_), _:Literal | _:VarRef) => true case _ => false @@ -334,15 +334,15 @@ private[optimizer] abstract class OptimizerCore( } lhs match { - case Select(qualifier, className, FieldIdent(name)) if !isFieldRead(className, name) => + case Select(qualifier, FieldIdent(name)) if !isFieldRead(name) => // Field is never read. Drop assign, keep side effects only. Block(transformStat(qualifier), transformStat(rhs)) - case SelectStatic(className, FieldIdent(name)) if !isStaticFieldRead(className, name) => + case SelectStatic(FieldIdent(name)) if !isStaticFieldRead(name) => // Field is never read. Drop assign, keep side effects only. transformStat(rhs) - case JSPrivateSelect(qualifier, className, FieldIdent(name)) if !isFieldRead(className, name) => + case JSPrivateSelect(qualifier, FieldIdent(name)) if !isFieldRead(name) => // Field is never read. Drop assign, keep side effects only. Block(transformStat(qualifier), transformStat(rhs)) @@ -574,8 +574,8 @@ private[optimizer] abstract class OptimizerCore( case JSNew(ctor, args) => JSNew(transformExpr(ctor), transformExprsOrSpreads(args)) - case JSPrivateSelect(qualifier, className, field) => - JSPrivateSelect(transformExpr(qualifier), className, field) + case JSPrivateSelect(qualifier, field) => + JSPrivateSelect(transformExpr(qualifier), field) case tree: JSSelect => trampoline { @@ -967,7 +967,7 @@ private[optimizer] abstract class OptimizerCore( } else if (baseTpe == NullType) { cont(checkNotNull(texpr)) } else if (isSubtype(baseTpe, JavaScriptExceptionClassType)) { - pretransformSelectCommon(AnyType, texpr, JavaScriptExceptionClass, + pretransformSelectCommon(AnyType, texpr, FieldIdent(exceptionFieldName), isLhsOfAssign = false)(cont) } else { if (texpr.tpe.isExact || !isSubtype(JavaScriptExceptionClassType, baseTpe)) @@ -1171,16 +1171,15 @@ private[optimizer] abstract class OptimizerCore( private def pretransformSelectCommon(tree: Select, isLhsOfAssign: Boolean)( cont: PreTransCont)( implicit scope: Scope): TailRec[Tree] = { - val Select(qualifier, className, field) = tree + val Select(qualifier, field) = tree pretransformExpr(qualifier) { preTransQual => - pretransformSelectCommon(tree.tpe, preTransQual, className, field, - isLhsOfAssign)(cont)(scope, tree.pos) + pretransformSelectCommon(tree.tpe, preTransQual, field, isLhsOfAssign)( + cont)(scope, tree.pos) } } private def pretransformSelectCommon(expectedType: Type, - preTransQual: PreTransform, className: ClassName, field: FieldIdent, - isLhsOfAssign: Boolean)( + preTransQual: PreTransform, field: FieldIdent, isLhsOfAssign: Boolean)( cont: PreTransCont)( implicit scope: Scope, pos: Position): TailRec[Tree] = { /* Note: Callers are expected to have already removed writes to fields that @@ -1190,7 +1189,7 @@ private[optimizer] abstract class OptimizerCore( preTransQual match { case PreTransLocalDef(LocalDef(_, _, InlineClassBeingConstructedReplacement(_, fieldLocalDefs, cancelFun))) => - val fieldLocalDef = fieldLocalDefs(FieldID(className, field)) + val fieldLocalDef = fieldLocalDefs(field.name) if (!isLhsOfAssign || fieldLocalDef.mutable) { cont(fieldLocalDef.toPreTransform) } else { @@ -1205,18 +1204,18 @@ private[optimizer] abstract class OptimizerCore( case PreTransLocalDef(LocalDef(_, _, InlineClassInstanceReplacement(_, fieldLocalDefs, cancelFun))) => - val fieldLocalDef = fieldLocalDefs(FieldID(className, field)) + val fieldLocalDef = fieldLocalDefs(field.name) assert(!isLhsOfAssign || fieldLocalDef.mutable, s"assign to immutable field at $pos") cont(fieldLocalDef.toPreTransform) // Select the lo or hi "field" of a Long literal case PreTransLit(LongLiteral(value)) if useRuntimeLong => val itemName = field.name - assert(itemName == inlinedRTLongLoField || - itemName == inlinedRTLongHiField) + assert(itemName.simpleName == inlinedRTLongLoField || + itemName.simpleName == inlinedRTLongHiField) assert(expectedType == IntType) val resultValue = - if (itemName == inlinedRTLongLoField) value.toInt + if (itemName.simpleName == inlinedRTLongLoField) value.toInt else (value >>> 32).toInt cont(PreTransLit(IntLiteral(resultValue))) @@ -1224,8 +1223,13 @@ private[optimizer] abstract class OptimizerCore( resolveLocalDef(preTransQual) match { case PreTransRecordTree(newQual, origType, cancelFun) => val recordType = newQual.tpe.asInstanceOf[RecordType] - val recordField = recordType.findField(field.name) - val sel = RecordSelect(newQual, field)(recordField.tpe) + /* FIXME How come this lookup requires only the `simpleName`? + * The `recordType` is created at `InlineableClassStructure.recordType`, + * where it uses an allocator. Something fishy is going on here. + * (And no, this is not dead code.) + */ + val recordField = recordType.findField(field.name.simpleName) + val sel = RecordSelect(newQual, SimpleFieldIdent(recordField.name))(recordField.tpe) sel.tpe match { case _: RecordType => cont(PreTransRecordTree(sel, RefinedType(expectedType), cancelFun)) @@ -1235,7 +1239,7 @@ private[optimizer] abstract class OptimizerCore( case PreTransTree(newQual, newQualType) => val newQual1 = maybeAssumeNotNull(newQual, newQualType) - cont(PreTransTree(Select(newQual1, className, field)(expectedType), + cont(PreTransTree(Select(newQual1, field)(expectedType), RefinedType(expectedType))) } } @@ -1332,7 +1336,7 @@ private[optimizer] abstract class OptimizerCore( if (!isImmutableType(recordType)) cancelFun() PreTransRecordTree( - RecordValue(recordType, structure.fieldIDs.map( + RecordValue(recordType, structure.fieldNames.map( id => fieldLocalDefs(id).newReplacement)), tpe, cancelFun) @@ -1591,7 +1595,7 @@ private[optimizer] abstract class OptimizerCore( checkNotNullStatement(array)(stat.pos) case ArraySelect(array, index) if semantics.arrayIndexOutOfBounds == CheckedBehavior.Unchecked => Block(checkNotNullStatement(array)(stat.pos), keepOnlySideEffects(index))(stat.pos) - case Select(qualifier, _, _) => + case Select(qualifier, _) => checkNotNullStatement(qualifier)(stat.pos) case Closure(_, _, _, _, _, captureValues) => Block(captureValues.map(keepOnlySideEffects))(stat.pos) @@ -2221,13 +2225,13 @@ private[optimizer] abstract class OptimizerCore( "There was a This(), there should be a receiver") cont(checkNotNull(optReceiver.get._2)) - case Select(This(), className, field) if formals.isEmpty => + case Select(This(), field) if formals.isEmpty => assert(optReceiver.isDefined, "There was a This(), there should be a receiver") - pretransformSelectCommon(body.tpe, optReceiver.get._2, className, field, + pretransformSelectCommon(body.tpe, optReceiver.get._2, field, isLhsOfAssign = false)(cont) - case Assign(lhs @ Select(This(), className, field), VarRef(LocalIdent(rhsName))) + case Assign(lhs @ Select(This(), field), VarRef(LocalIdent(rhsName))) if formals.size == 1 && formals.head.name.name == rhsName => assert(isStat, "Found Assign in expression position") assert(optReceiver.isDefined, @@ -2236,11 +2240,11 @@ private[optimizer] abstract class OptimizerCore( val treceiver = optReceiver.get._2 val trhs = args.head - if (!isFieldRead(className, field.name)) { + if (!isFieldRead(field.name)) { // Field is never read, discard assign, keep side effects only. cont(PreTransTree(finishTransformArgsAsStat(), RefinedType.NoRefinedType)) } else { - pretransformSelectCommon(lhs.tpe, treceiver, className, field, + pretransformSelectCommon(lhs.tpe, treceiver, field, isLhsOfAssign = true) { tlhs => pretransformAssign(tlhs, args.head)(cont) } @@ -2582,7 +2586,7 @@ private[optimizer] abstract class OptimizerCore( elemLocalDef match { case LocalDef(RefinedType(ClassType(Tuple2Class), _, _), false, InlineClassInstanceReplacement(structure, tupleFields, _)) => - val List(key, value) = structure.fieldIDs.map(tupleFields) + val List(key, value) = structure.fieldNames.map(tupleFields) (key.newReplacement, value.newReplacement) case _ => @@ -2658,7 +2662,7 @@ private[optimizer] abstract class OptimizerCore( withNewLocalDefs(initialFieldBindings) { (initialFieldLocalDefList, cont1) => val initialFieldLocalDefs = - structure.fieldIDs.zip(initialFieldLocalDefList).toMap + structure.fieldNames.zip(initialFieldLocalDefList).toMap inlineClassConstructorBody(allocationSite, structure, initialFieldLocalDefs, className, className, ctor, args, cancelFun) { (finalFieldLocalDefs, cont2) => @@ -2674,10 +2678,10 @@ private[optimizer] abstract class OptimizerCore( private def inlineClassConstructorBody( allocationSite: AllocationSite, structure: InlineableClassStructure, - inputFieldsLocalDefs: Map[FieldID, LocalDef], className: ClassName, + inputFieldsLocalDefs: Map[FieldName, LocalDef], className: ClassName, ctorClass: ClassName, ctor: MethodIdent, args: List[PreTransform], cancelFun: CancelFun)( - buildInner: (Map[FieldID, LocalDef], PreTransCont) => TailRec[Tree])( + buildInner: (Map[FieldName, LocalDef], PreTransCont) => TailRec[Tree])( cont: PreTransCont)( implicit scope: Scope): TailRec[Tree] = tailcall { @@ -2714,9 +2718,9 @@ private[optimizer] abstract class OptimizerCore( private def inlineClassConstructorBodyList( allocationSite: AllocationSite, structure: InlineableClassStructure, - thisLocalDef: LocalDef, inputFieldsLocalDefs: Map[FieldID, LocalDef], + thisLocalDef: LocalDef, inputFieldsLocalDefs: Map[FieldName, LocalDef], className: ClassName, stats: List[Tree], cancelFun: CancelFun)( - buildInner: (Map[FieldID, LocalDef], PreTransCont) => TailRec[Tree])( + buildInner: (Map[FieldName, LocalDef], PreTransCont) => TailRec[Tree])( cont: PreTransCont)( implicit scope: Scope): TailRec[Tree] = { @@ -2745,18 +2749,17 @@ private[optimizer] abstract class OptimizerCore( inlineClassConstructorBodyList(allocationSite, structure, thisLocalDef, inputFieldsLocalDefs, className, rest, cancelFun)(buildInner)(cont) - case Assign(s @ Select(ths: This, className, field), value) :: rest - if !inputFieldsLocalDefs.contains(FieldID(className, field)) => + case Assign(s @ Select(ths: This, field), value) :: rest + if !inputFieldsLocalDefs.contains(field.name) => // Field is being optimized away. Only keep side effects of the write. withStat(value, rest) - case Assign(s @ Select(ths: This, className, field), value) :: rest - if !inputFieldsLocalDefs(FieldID(className, field)).mutable => + case Assign(s @ Select(ths: This, field), value) :: rest + if !inputFieldsLocalDefs(field.name).mutable => pretransformExpr(value) { tvalue => - val fieldID = FieldID(className, field) - val originalName = structure.fieldOriginalName(fieldID) + val originalName = structure.fieldOriginalName(field.name) val binding = Binding( - Binding.Local(field.name.toLocalName, originalName), + Binding.Local(field.name.simpleName.toLocalName, originalName), s.tpe, false, tvalue) withNewLocalDef(binding) { (localDef, cont1) => if (localDef.contains(thisLocalDef)) { @@ -2766,7 +2769,7 @@ private[optimizer] abstract class OptimizerCore( cancelFun() } val newFieldsLocalDefs = - inputFieldsLocalDefs.updated(fieldID, localDef) + inputFieldsLocalDefs.updated(field.name, localDef) val newThisLocalDef = LocalDef(thisLocalDef.tpe, false, InlineClassBeingConstructedReplacement(structure, newFieldsLocalDefs, cancelFun)) val restScope = @@ -2792,7 +2795,7 @@ private[optimizer] abstract class OptimizerCore( * coming from Scala.js < 1.15.1 (since 1.15.1, we intercept that shape * already in the compiler back-end). */ - case If(cond, th: Throw, Assign(Select(This(), _, _), value)) :: rest => + case If(cond, th: Throw, Assign(Select(This(), _), value)) :: rest => // work around a bug of the compiler (these should be @-bindings) val stat = stats.head.asInstanceOf[If] val ass = stat.elsep.asInstanceOf[Assign] @@ -5083,7 +5086,8 @@ private[optimizer] object OptimizerCore { private val JavaScriptExceptionClassType = ClassType(JavaScriptExceptionClass) private val ThrowableClassType = ClassType(ThrowableClass) - private val exceptionFieldName = FieldName("exception") + private val exceptionFieldName = + FieldName(JavaScriptExceptionClass, SimpleFieldName("exception")) private val AnyArgConstructorName = MethodName.constructor(List(ClassRef(ObjectClass))) @@ -5092,34 +5096,31 @@ private[optimizer] object OptimizerCore { private val ClassTagApplyMethodName = MethodName("apply", List(ClassRef(ClassClass)), ClassRef(ClassName("scala.reflect.ClassTag"))) - final class InlineableClassStructure( - /** `List[ownerClassName -> fieldDef]`. */ - private val allFields: List[(ClassName, FieldDef)]) { - - private[OptimizerCore] val fieldIDs: List[FieldID] = - allFields.map(field => FieldID(field._1, field._2)) + final class InlineableClassStructure(private val allFields: List[FieldDef]) { + private[OptimizerCore] val fieldNames: List[FieldName] = + allFields.map(_.name.name) private[OptimizerCore] val recordType: RecordType = { val allocator = new FreshNameAllocator.Field val recordFields = for { - (className, f @ FieldDef(flags, FieldIdent(name), originalName, ftpe)) <- allFields + f @ FieldDef(flags, FieldIdent(name), originalName, ftpe) <- allFields } yield { assert(!flags.namespace.isStatic, s"unexpected static field in InlineableClassStructure at ${f.pos}") - RecordType.Field(allocator.freshName(name), originalName, ftpe, + RecordType.Field(allocator.freshName(name.simpleName), originalName, ftpe, flags.isMutable) } RecordType(recordFields) } - private val recordFieldNames: Map[FieldID, RecordType.Field] = { - val elems = for (((className, fieldDef), recordField) <- allFields.zip(recordType.fields)) - yield FieldID(className, fieldDef) -> recordField + private val recordFieldNames: Map[FieldName, RecordType.Field] = { + val elems = for ((fieldDef, recordField) <- allFields.zip(recordType.fields)) + yield fieldDef.name.name -> recordField elems.toMap } - private[OptimizerCore] def fieldOriginalName(fieldID: FieldID): OriginalName = - recordFieldNames(fieldID).originalName + private[OptimizerCore] def fieldOriginalName(fieldName: FieldName): OriginalName = + recordFieldNames(fieldName).originalName override def equals(that: Any): Boolean = that match { case that: InlineableClassStructure => @@ -5132,7 +5133,7 @@ private[optimizer] object OptimizerCore { override def toString(): String = { allFields - .map(f => s"${f._1.nameString}::${f._2.name.name.nameString}: ${f._2.ftpe}") + .map(f => s"${f.name.name.nameString}: ${f.ftpe}") .mkString("InlineableClassStructure(", ", ", ")") } } @@ -5276,7 +5277,7 @@ private[optimizer] object OptimizerCore { */ case InlineClassInstanceReplacement(structure, fieldLocalDefs, _) if tpe.base == ClassType(LongImpl.RuntimeLongClass) => - val List(loField, hiField) = structure.fieldIDs + val List(loField, hiField) = structure.fieldNames val lo = fieldLocalDefs(loField).newReplacement val hi = fieldLocalDefs(hiField).newReplacement createNewLong(lo, hi) @@ -5341,12 +5342,12 @@ private[optimizer] object OptimizerCore { private final case class InlineClassBeingConstructedReplacement( structure: InlineableClassStructure, - fieldLocalDefs: Map[FieldID, LocalDef], + fieldLocalDefs: Map[FieldName, LocalDef], cancelFun: CancelFun) extends LocalDefReplacement private final case class InlineClassInstanceReplacement( structure: InlineableClassStructure, - fieldLocalDefs: Map[FieldID, LocalDef], + fieldLocalDefs: Map[FieldName, LocalDef], cancelFun: CancelFun) extends LocalDefReplacement private final case class InlineJSArrayReplacement( @@ -5798,8 +5799,8 @@ private[optimizer] object OptimizerCore { val RecordType(List(loField, hiField)) = recordVarRef.tpe createNewLong( - RecordSelect(recordVarRef, FieldIdent(loField.name))(IntType), - RecordSelect(recordVarRef, FieldIdent(hiField.name))(IntType)) + RecordSelect(recordVarRef, SimpleFieldIdent(loField.name))(IntType), + RecordSelect(recordVarRef, SimpleFieldIdent(hiField.name))(IntType)) } /** Creates a new instance of `RuntimeLong` from its `lo` and `hi` parts. */ @@ -6056,9 +6057,9 @@ private[optimizer] object OptimizerCore { true // Shape of accessors - case Select(This(), _, _) if params.isEmpty => + case Select(This(), _) if params.isEmpty => true - case Assign(Select(This(), _, _), VarRef(_)) if params.size == 1 => + case Assign(Select(This(), _), VarRef(_)) if params.size == 1 => true // Shape of trivial call-super constructors @@ -6150,7 +6151,7 @@ private[optimizer] object OptimizerCore { */ private def isSmallTree(tree: TreeOrJSSpread): Boolean = tree match { case _:VarRef | _:Literal => true - case Select(This(), _, _) => true + case Select(This(), _) => true case UnaryOp(_, lhs) => isSmallTree(lhs) case BinaryOp(_, lhs, rhs) => isSmallTree(lhs) && isSmallTree(rhs) case JSUnaryOp(_, lhs) => isSmallTree(lhs) @@ -6165,7 +6166,7 @@ private[optimizer] object OptimizerCore { case Apply(_, receiver, _, args) => areSimpleArgs(receiver :: args) case ApplyStatically(_, receiver, _, _, args) => areSimpleArgs(receiver :: args) case ApplyStatic(_, _, _, args) => areSimpleArgs(args) - case Select(qual, _, _) => isSimpleArg(qual) + case Select(qual, _) => isSimpleArg(qual) case IsInstanceOf(inner, _) => isSimpleArg(inner) case Block(List(inner, Undefined())) => @@ -6319,11 +6320,11 @@ private[optimizer] object OptimizerCore { name.withSuffix(suffix) } - private val InitialFieldMap: Map[FieldName, Int] = + private val InitialFieldMap: Map[SimpleFieldName, Int] = Map.empty - final class Field extends FreshNameAllocator[FieldName](InitialFieldMap) { - protected def nameWithSuffix(name: FieldName, suffix: String): FieldName = + final class Field extends FreshNameAllocator[SimpleFieldName](InitialFieldMap) { + protected def nameWithSuffix(name: SimpleFieldName, suffix: String): SimpleFieldName = name.withSuffix(suffix) } @@ -6337,30 +6338,6 @@ private[optimizer] object OptimizerCore { else OriginalName(base) } - final class FieldID private (val ownerClassName: ClassName, val name: FieldName) { - override def equals(that: Any): Boolean = that match { - case that: FieldID => - this.ownerClassName == that.ownerClassName && - this.name == that.name - case _ => - false - } - - override def hashCode(): Int = - ownerClassName.## ^ name.## - - override def toString(): String = - s"FieldID($ownerClassName, $name)" - } - - object FieldID { - def apply(ownerClassName: ClassName, field: FieldIdent): FieldID = - new FieldID(ownerClassName, field.name) - - def apply(ownerClassName: ClassName, fieldDef: FieldDef): FieldID = - new FieldID(ownerClassName, fieldDef.name.name) - } - private sealed abstract class IsUsed { def isUsed: Boolean } diff --git a/linker/shared/src/test/scala/org/scalajs/linker/OptimizerTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/OptimizerTest.scala index 608005203e..441b146638 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/OptimizerTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/OptimizerTest.scala @@ -203,14 +203,14 @@ class OptimizerTest { fields = List( // static var foo: java.lang.String FieldDef(EMF.withNamespace(PublicStatic).withMutable(true), - "foo", NON, StringType) + FieldName(MainTestClassName, "foo"), NON, StringType) ), methods = List( trivialCtor(MainTestClassName), // static def foo(): java.lang.String = Test::foo MethodDef(EMF.withNamespace(MemberNamespace.PublicStatic), fooGetter, NON, Nil, StringType, Some({ - SelectStatic(MainTestClassName, "foo")(StringType) + SelectStatic(FieldName(MainTestClassName, "foo"))(StringType) }))(EOH, UNV), // static def main(args: String[]) { println(Test::foo()) } mainMethodDef({ @@ -223,7 +223,7 @@ class OptimizerTest { for (moduleSet <- linkToModuleSet(classDefs, MainTestModuleInitializers)) yield { val mainClassDef = findClass(moduleSet, MainTestClassName).get assertTrue(mainClassDef.fields.exists { - case FieldDef(_, FieldIdent(name), _, _) => name == FieldName("foo") + case FieldDef(_, FieldIdent(name), _, _) => name == FieldName(MainTestClassName, "foo") case _ => false }) } @@ -491,10 +491,10 @@ class OptimizerTest { classDef("Foo", kind = ClassKind.Class, superClass = Some(ObjectClass), fields = List( // x: Witness - FieldDef(EMF.withMutable(witnessMutable), "x", NON, witnessType), + FieldDef(EMF.withMutable(witnessMutable), FieldName("Foo", "x"), NON, witnessType), // y: Int - FieldDef(EMF, "y", NON, IntType) + FieldDef(EMF, FieldName("Foo", "y"), NON, IntType) ), methods = List( // def this() = { @@ -502,13 +502,13 @@ class OptimizerTest { // this.y = 5 // } MethodDef(EMF.withNamespace(Constructor), NoArgConstructorName, NON, Nil, NoType, Some(Block( - Assign(Select(This()(ClassType("Foo")), "Foo", "x")(witnessType), Null()), - Assign(Select(This()(ClassType("Foo")), "Foo", "y")(IntType), int(5)) + Assign(Select(This()(ClassType("Foo")), FieldName("Foo", "x"))(witnessType), Null()), + Assign(Select(This()(ClassType("Foo")), FieldName("Foo", "y"))(IntType), int(5)) )))(EOH, UNV), // def method(): Int = this.y MethodDef(EMF, methodName, NON, Nil, IntType, Some { - Select(This()(ClassType("Foo")), "Foo", "y")(IntType) + Select(This()(ClassType("Foo")), FieldName("Foo", "y"))(IntType) })(EOH, UNV) ), optimizerHints = EOH.withInline(classInline) @@ -527,7 +527,7 @@ class OptimizerTest { moduleSet <- linkToModuleSet(classDefs, MainTestModuleInitializers) } yield { findClass(moduleSet, "Foo").get.fields match { - case List(FieldDef(_, FieldIdent(name), _, _)) if name == FieldName("y") => + case List(FieldDef(_, FieldIdent(name), _, _)) if name == FieldName("Foo", "y") => // ok case fields => diff --git a/linker/shared/src/test/scala/org/scalajs/linker/checker/ClassDefCheckerTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/checker/ClassDefCheckerTest.scala index 75717babd3..e9f6ba2306 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/checker/ClassDefCheckerTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/checker/ClassDefCheckerTest.scala @@ -122,15 +122,47 @@ class ClassDefCheckerTest { "interfaces may not have a superClass") } + @Test + def fieldDefClassName(): Unit = { + assertError( + classDef( + "A", + superClass = Some(ObjectClass), + fields = List( + FieldDef(EMF, FieldName("B", "foo"), NON, IntType) + ), + methods = List(trivialCtor("A")) + ), + "illegal FieldDef with name B::foo in class A" + ) + + // evidence that we do not need an explicit check for top-level field exports + assertError( + classDef( + "A", + kind = ClassKind.ModuleClass, + superClass = Some(ObjectClass), + fields = List( + FieldDef(EMF.withNamespace(MemberNamespace.PublicStatic), FieldName("A", "foo"), NON, IntType) + ), + methods = List(trivialCtor("A")), + topLevelExportDefs = List( + TopLevelFieldExportDef("main", "foo", FieldName("B", "foo")) + ) + ), + "Cannot export non-existent static field 'B::foo'" + ) + } + @Test def noDuplicateFields(): Unit = { assertError( classDef("A", superClass = Some(ObjectClass), fields = List( - FieldDef(EMF, "foobar", NON, IntType), - FieldDef(EMF, "foobar", NON, BooleanType) + FieldDef(EMF, FieldName("A", "foobar"), NON, IntType), + FieldDef(EMF, FieldName("A", "foobar"), NON, BooleanType) )), - "duplicate field 'foobar'") + "duplicate field 'A::foobar'") } @Test diff --git a/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRBuilder.scala b/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRBuilder.scala index 8b73664dc8..be6dbb33a9 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRBuilder.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRBuilder.scala @@ -155,8 +155,8 @@ object TestIRBuilder { LocalName(name) implicit def string2LabelName(name: String): LabelName = LabelName(name) - implicit def string2FieldName(name: String): FieldName = - FieldName(name) + implicit def string2SimpleFieldName(name: String): SimpleFieldName = + SimpleFieldName(name) implicit def string2ClassName(name: String): ClassName = ClassName(name) @@ -164,13 +164,13 @@ object TestIRBuilder { LocalIdent(LocalName(name)) implicit def string2LabelIdent(name: String): LabelIdent = LabelIdent(LabelName(name)) - implicit def string2FieldIdent(name: String): FieldIdent = - FieldIdent(FieldName(name)) implicit def string2ClassIdent(name: String): ClassIdent = ClassIdent(ClassName(name)) implicit def localName2LocalIdent(name: LocalName): LocalIdent = LocalIdent(name) + implicit def fieldName2FieldIdent(name: FieldName): FieldIdent = + FieldIdent(name) implicit def methodName2MethodIdent(name: MethodName): MethodIdent = MethodIdent(name) diff --git a/project/BinaryIncompatibilities.scala b/project/BinaryIncompatibilities.scala index 0144862b7b..6d2c642453 100644 --- a/project/BinaryIncompatibilities.scala +++ b/project/BinaryIncompatibilities.scala @@ -6,8 +6,21 @@ import com.typesafe.tools.mima.core.ProblemFilters._ object BinaryIncompatibilities { val IR = Seq( // !!! Breaking, OK in minor release + + ProblemFilters.exclude[MissingTypesProblem]("org.scalajs.ir.Names$FieldName"), + ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Names#FieldName.*"), + ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Names#LocalName.fromFieldName"), + + ProblemFilters.exclude[IncompatibleMethTypeProblem]("org.scalajs.ir.Types#RecordType.findField"), + ProblemFilters.exclude[MemberProblem]("org.scalajs.ir.Types#RecordType#Field.*"), + ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Trees#StoreModule.*"), ProblemFilters.exclude[IncompatibleResultTypeProblem]("org.scalajs.ir.Trees#StoreModule.unapply"), + + ProblemFilters.exclude[MemberProblem]("org.scalajs.ir.Trees#JSPrivateSelect.*"), + ProblemFilters.exclude[MemberProblem]("org.scalajs.ir.Trees#RecordSelect.*"), + ProblemFilters.exclude[MemberProblem]("org.scalajs.ir.Trees#Select.*"), + ProblemFilters.exclude[MemberProblem]("org.scalajs.ir.Trees#SelectStatic.*"), ) val Linker = Seq( diff --git a/project/JavalibIRCleaner.scala b/project/JavalibIRCleaner.scala index 58fdc14a8a..305fc74212 100644 --- a/project/JavalibIRCleaner.scala +++ b/project/JavalibIRCleaner.scala @@ -439,8 +439,9 @@ final class JavalibIRCleaner(baseDirectoryURI: URI) { case New(className, ctor, args) => New(transformNonJSClassName(className), transformMethodIdent(ctor), args) - case Select(qualifier, className, field) => - Select(qualifier, transformNonJSClassName(className), field)(transformType(tree.tpe)) + case Select(qualifier, field @ FieldIdent(fieldName)) => + val newFieldName = FieldName(transformNonJSClassName(fieldName.className), fieldName.simpleName) + Select(qualifier, FieldIdent(newFieldName)(field.pos))(transformType(tree.tpe)) case t: Apply => Apply(t.flags, t.receiver, transformMethodIdent(t.method), t.args)( From 0854041850154a62a5a265b4f14ebe1b9b2445fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 12 Feb 2024 18:20:01 +0100 Subject: [PATCH 046/298] Codegen: Replace LoadModule(myClass) by This() after StoreModule(). As the comment explains, this will help the elidable constructors analysis to give stronger guarantees. --- .../org/scalajs/nscplugin/GenJSCode.scala | 24 +++++++++++++++---- .../nscplugin/test/OptimizationTest.scala | 22 +++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 6ad055b5ee..a093b718da 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -6613,11 +6613,25 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) if (sym.hasAnnotation(JSGlobalScopeAnnotation)) { MaybeGlobalScope.GlobalScope(pos) } else { - val className = encodeClassName(sym) - val tree = - if (isJSType(sym)) js.LoadJSModule(className) - else js.LoadModule(className) - MaybeGlobalScope.NotGlobalScope(tree) + if (sym == currentClassSym.get && isModuleInitialized.get != null && isModuleInitialized.value) { + /* This is a LoadModule(myClass) after the StoreModule(). It is + * guaranteed to always return the `this` value. We eagerly replace + * it by a `This()` node to help the elidable constructors analysis + * of the linker. If we don't do this, then the analysis must + * tolerate `LoadModule(myClass)` after `StoreModule()` to be + * side-effect-free, but that would weaken the guarantees resulting + * from the analysis. In particular, it cannot guarantee that the + * result of a `LoadModule()` of a module with elidable constructors + * is always fully initialized. + */ + MaybeGlobalScope.NotGlobalScope(genThis()) + } else { + val className = encodeClassName(sym) + val tree = + if (isJSType(sym)) js.LoadJSModule(className) + else js.LoadModule(className) + MaybeGlobalScope.NotGlobalScope(tree) + } } } } diff --git a/compiler/src/test/scala/org/scalajs/nscplugin/test/OptimizationTest.scala b/compiler/src/test/scala/org/scalajs/nscplugin/test/OptimizationTest.scala index f3a5ab9ac9..7e140ebd38 100644 --- a/compiler/src/test/scala/org/scalajs/nscplugin/test/OptimizationTest.scala +++ b/compiler/src/test/scala/org/scalajs/nscplugin/test/OptimizationTest.scala @@ -560,6 +560,28 @@ class OptimizationTest extends JSASTTest { assertTrue(flags.inline) } + + @Test + def loadModuleAfterStoreModuleIsThis: Unit = { + val testName = ClassName("Test$") + + """ + object Test { + private val selfPair = (Test, Test) + } + """.hasNot("LoadModule") { + case js.LoadModule(_) => + } + + // Confidence check + """ + object Test { + private def selfPair = (Test, Test) + } + """.hasExactly(2, "LoadModule") { + case js.LoadModule(`testName`) => + } + } } object OptimizationTest { From b6db0f5b9d9444fc25af5ecb00378121a7e98b09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 12 Feb 2024 19:24:52 +0100 Subject: [PATCH 047/298] New side-effect-free analysis of ctors that also requires acyclic init. Previously, the inter-class side-effect-free analysis of constructors allowed cycles in the initialization graph. For example, `A` and `B` would be considered to have elidable constructors in object A { B } object B { A } To do that, the actual algorithm started from the known-`NotElidable` classes and propagated that to the classes depending on them. Now, we instead also require that the initialization graph be *acyclic* for a class to have elidable constructors. To do that, we reverse the algorithm: we start from known-`AcyclicElidable` classes and propagate that to classes that *only* depend on them. Now, why would we impose *more* requirements for a class to have elidable constructors? Because it guarantees that the result of `LoadModule` is actually non-null *and* fully initialized. That means we can also follow `Select`ions and `Apply`s of getter-shaped methods on these. Counter-intuitively, that means *more* classes can be considered to have elidable constructors. The additional guarantee of being fully initialized will also be used in the following commit. --- .../frontend/optimizer/IncOptimizer.scala | 157 +++++++++++++++--- project/Build.scala | 4 +- 2 files changed, 135 insertions(+), 26 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala index 6d8cc24f28..6118b1153e 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala @@ -271,37 +271,93 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: private def updateElidableConstructors(): Unit = { import ElidableConstructorsInfo._ - /* Invariant: when something is in the stack, its - * elidableConstructorsInfo was set to NotElidable. - */ val toProcessStack = mutable.ArrayBuffer.empty[Class] - // Build the graph and initial stack from the infos + /* Invariants for this algo: + * - when a class is in the stack, its elidableConstructorsInfo was set + * to AcyclicElidable, + * - when a class has `DependentOn` as its info, its + * `elidableConstructorsRemainingDependenciesCount` is the number of + * classes in its `dependencies` that have not yet been *processed* as + * AcyclicElidable. + * + * During this algorithm, the info can transition from DependentOn to + * + * - NotElidable, if its getter dependencies were not satisfied. + * - AcyclicElidable. + * + * Other transitions are not possible. + */ + + def isGetter(classAndMethodName: (ClassName, MethodName)): Boolean = { + val (className, methodName) = classAndMethodName + classes(className).lookupMethod(methodName).exists { m => + m.originalDef.body match { + case Some(Select(This(), _)) => true + case _ => false + } + } + } + + /* Initialization: + * - Prune classes with unsatisfied getter dependencies + * - Build reverse dependencies + * - Initialize `elidableConstructorsRemainingDependenciesCount` for `DependentOn` classes + * - Initialize the stack with dependency-free classes + */ for (cls <- classes.valuesIterator) { cls.elidableConstructorsInfo match { - case NotElidable => + case DependentOn(deps, getterDeps) => + if (!getterDeps.forall(isGetter(_))) { + cls.elidableConstructorsInfo = NotElidable + } else { + if (deps.isEmpty) { + cls.elidableConstructorsInfo = AcyclicElidable + toProcessStack += cls + } else { + cls.elidableConstructorsRemainingDependenciesCount = deps.size + deps.foreach(dep => classes(dep).elidableConstructorsDependents += cls) + } + } + case AcyclicElidable => toProcessStack += cls - case DependentOn(dependencies) => - for (dependency <- dependencies) - classes(dependency).elidableConstructorsDependents += cls + case NotElidable => + () } } - // Propagate + /* Propagate AcyclicElidable + * When a class `cls` is on the stack, it is known to be AcyclicElidable. + * Go to all its dependents and decrement their count of remaining + * dependencies. If the count reaches 0, then all the dependencies of the + * class are known to be AcyclicElidable, and so the new class is known to + * be AcyclicElidable. + */ while (toProcessStack.nonEmpty) { val cls = toProcessStack.remove(toProcessStack.size - 1) for (dependent <- cls.elidableConstructorsDependents) { - if (dependent.elidableConstructorsInfo != NotElidable) { - dependent.elidableConstructorsInfo = NotElidable - toProcessStack += dependent + dependent.elidableConstructorsInfo match { + case DependentOn(_, _) => + dependent.elidableConstructorsRemainingDependenciesCount -= 1 + if (dependent.elidableConstructorsRemainingDependenciesCount == 0) { + dependent.elidableConstructorsInfo = AcyclicElidable + toProcessStack += dependent + } + case NotElidable => + () + case AcyclicElidable => + throw new AssertionError( + s"Unexpected dependent link from class ${cls.className.nameString} " + + s"to ${dependent.className.nameString} which is AcyclicElidable" + ) } } } // Set the final value of hasElidableConstructors for (cls <- classes.valuesIterator) { - cls.setHasElidableConstructors(cls.elidableConstructorsInfo != NotElidable) + cls.setHasElidableConstructors() } } @@ -430,6 +486,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: var elidableConstructorsInfo: ElidableConstructorsInfo = computeElidableConstructorsInfo(linkedClass) val elidableConstructorsDependents: mutable.ArrayBuffer[Class] = mutable.ArrayBuffer.empty + var elidableConstructorsRemainingDependenciesCount: Int = 0 /** True if *all* constructors of this class are recursively elidable. */ private var hasElidableConstructors: Boolean = @@ -562,7 +619,6 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: // Elidable constructors elidableConstructorsInfo = computeElidableConstructorsInfo(linkedClass) - elidableConstructorsDependents.clear() // Inlineable class if (updateTryNewInlineable(linkedClass)) { @@ -577,12 +633,21 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: } /** ELIDABLE CTORS PASS ONLY. */ - def setHasElidableConstructors(newHasElidableConstructors: Boolean): Unit = { + def setHasElidableConstructors(): Unit = { + import ElidableConstructorsInfo._ + + val newHasElidableConstructors = elidableConstructorsInfo == AcyclicElidable + if (hasElidableConstructors != newHasElidableConstructors) { hasElidableConstructors = newHasElidableConstructors hasElidableConstructorsAskers.keysIterator.foreach(_.tag()) hasElidableConstructorsAskers.clear() } + + // Release memory that we won't use anymore + if (!newHasElidableConstructors) + elidableConstructorsInfo = NotElidable // get rid of DependentOn + elidableConstructorsDependents.clear() // also resets state for next run } /** UPDATE PASS ONLY. */ @@ -612,10 +677,10 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: import ElidableConstructorsInfo._ if (isAdHocElidableConstructors(className)) { - AlwaysElidable + AcyclicElidable } else { // It's OK to look at the superClass like this because it will always be updated before myself - var result = superClass.fold(ElidableConstructorsInfo.AlwaysElidable)(_.elidableConstructorsInfo) + var result = superClass.fold[ElidableConstructorsInfo](AcyclicElidable)(_.elidableConstructorsInfo) if (result == NotElidable) { // fast path @@ -691,8 +756,17 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: /** UPDATE PASS ONLY. */ private def computeCtorElidableInfo(impl: MethodImpl): ElidableConstructorsInfo = { + /* Dependencies on other classes to have acyclic elidable constructors + * It is possible for the enclosing class name to be added to this set, + * if the constructor depends on its own class. In that case, the + * analysis will naturally treat it as a cycle and will conclude that the + * class does not have elidable constructors. + */ val dependenciesBuilder = Set.newBuilder[ClassName] + // Dependencies on certain methods to be getters + val getterDependenciesBuilder = Set.newBuilder[(ClassName, MethodName)] + def isTriviallySideEffectFree(tree: Tree): Boolean = tree match { case _:VarRef | _:Literal | _:This | _:Skip => true @@ -712,6 +786,28 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: dependenciesBuilder += className true + case Select(LoadModule(className), _) => + /* If the given module can be loaded without cycles, it is guaranteed + * to be non-null, and therefore the Select is side-effect-free. + */ + dependenciesBuilder += className + true + + case Select(This(), _) => + true + + case Apply(_, LoadModule(className), MethodIdent(methodName), Nil) + if !methodName.isReflectiveProxy => + // For a getter-like call, we need the method to actually be a getter. + dependenciesBuilder += className + getterDependenciesBuilder += ((className, methodName)) + true + + case Apply(_, This(), MethodIdent(methodName), Nil) + if !methodName.isReflectiveProxy => + getterDependenciesBuilder += ((className, methodName)) + true + case _ => false } @@ -755,10 +851,16 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: impl.originalDef.body.fold { throw new AssertionError("Constructor cannot be abstract") } { body => - if (isElidableStat(body)) - ElidableConstructorsInfo.DependentOn(dependenciesBuilder.result()) - else + if (isElidableStat(body)) { + val dependencies = dependenciesBuilder.result() + val getterDependencies = getterDependenciesBuilder.result() + if (dependencies.isEmpty && getterDependencies.isEmpty) + ElidableConstructorsInfo.AcyclicElidable + else + ElidableConstructorsInfo.DependentOn(dependencies, getterDependencies) + } else { ElidableConstructorsInfo.NotElidable + } } } @@ -1482,8 +1584,12 @@ object IncOptimizer { import ElidableConstructorsInfo._ final def mergeWith(that: ElidableConstructorsInfo): ElidableConstructorsInfo = (this, that) match { - case (DependentOn(deps1), DependentOn(deps2)) => - DependentOn(deps1 ++ deps2) + case (DependentOn(deps1, getterDeps1), DependentOn(deps2, getterDeps2)) => + DependentOn(deps1 ++ deps2, getterDeps1 ++ getterDeps2) + case (AcyclicElidable, _) => + that + case (_, AcyclicElidable) => + this case _ => NotElidable } @@ -1492,8 +1598,11 @@ object IncOptimizer { object ElidableConstructorsInfo { case object NotElidable extends ElidableConstructorsInfo - final case class DependentOn(dependencies: Set[ClassName]) extends ElidableConstructorsInfo + case object AcyclicElidable extends ElidableConstructorsInfo - val AlwaysElidable: ElidableConstructorsInfo = DependentOn(Set.empty) + final case class DependentOn( + dependencies: Set[ClassName], + getterDependencies: Set[(ClassName, MethodName)] + ) extends ElidableConstructorsInfo } } diff --git a/project/Build.scala b/project/Build.scala index 263683ee52..f3c9cbedac 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1975,8 +1975,8 @@ object Build { case `default213Version` => Some(ExpectedSizes( - fastLink = 468000 to 469000, - fullLink = 100000 to 101000, + fastLink = 463000 to 464000, + fullLink = 99000 to 100000, fastLinkGz = 60000 to 61000, fullLinkGz = 26000 to 27000, )) From 3348716c2c22f379815eaa83459dc87bb89eec6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 9 Feb 2024 15:31:24 +0100 Subject: [PATCH 048/298] Inline the "body" of fields of modules when possible. If a module `M` has elidable constructors, and an immutable field `f` of `M` is initialized with an "easy" value, we can replace all occurrences of `M.f` by that value. Easy values are literals, loads of other modules, or accesses to fields/getters of other modules. We can do this because having elidable constructors implies that the initialization of the module is acyclic. So once it is fully initialized, all fields have indeed been initialized in a predictable way. This is particularly effective for all the forwarders declared in `scala.Predef$` in `scala.package$`. They contain a series of vals like val Nil = scala.collection.immutable.Nil When typical Scala code refers to `Nil`, it means `scala.Nil` rather than `sci.Nil` We are now able to "inline" those references instead of going through the alias. In addition to generating less code, this tends to give a better type to the result. For example `sci.Nil` is known to be non-nullable, whereas `scala.Nil` could be null. --- .../src/main/scala/org/scalajs/ir/Trees.scala | 20 +- .../frontend/optimizer/IncOptimizer.scala | 241 ++++++++++++++++-- .../frontend/optimizer/OptimizerCore.scala | 155 +++++++++-- project/Build.scala | 2 +- 4 files changed, 373 insertions(+), 45 deletions(-) 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 c3d206624a..2dd8e43d36 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala @@ -904,7 +904,11 @@ object Trees { // Literals - /** Marker for literals. Literals are always pure. */ + /** Marker for literals. Literals are always pure. + * + * All `Literal`s can be compared for equality. The equality does not take + * the `pos` into account. + */ sealed trait Literal extends Tree /** Marker for literals that can be used in a [[Match]] case. @@ -960,11 +964,25 @@ object Trees { sealed case class FloatLiteral(value: Float)( implicit val pos: Position) extends Literal { val tpe = FloatType + + override def equals(that: Any): Boolean = that match { + case that: FloatLiteral => java.lang.Float.compare(this.value, that.value) == 0 + case _ => false + } + + override def hashCode(): Int = java.lang.Float.hashCode(value) } sealed case class DoubleLiteral(value: Double)( implicit val pos: Position) extends Literal { val tpe = DoubleType + + override def equals(that: Any): Boolean = that match { + case that: DoubleLiteral => java.lang.Double.compare(this.value, that.value) == 0 + case _ => false + } + + override def hashCode(): Int = java.lang.Double.hashCode(value) } sealed case class StringLiteral(value: String)( diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala index 6118b1153e..d092397b51 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala @@ -33,6 +33,8 @@ import org.scalajs.linker.interface.{CheckedBehavior, ModuleKind} import org.scalajs.linker.standard._ import org.scalajs.linker.CollectionsCompat._ +import OptimizerCore.InlineableFieldBodies.FieldBody + /** Incremental optimizer. * * An incremental optimizer optimizes a [[LinkingUnit]] @@ -497,6 +499,13 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: var fieldsRead: Set[FieldName] = linkedClass.fieldsRead var tryNewInlineable: Option[OptimizerCore.InlineableClassStructure] = None + /** The "bodies" of fields that can "inlined", *provided that* the enclosing + * module class has elidable constructors. + */ + private var inlineableFieldBodies: OptimizerCore.InlineableFieldBodies = + computeInlineableFieldBodies(linkedClass) + private val inlineableFieldBodiesAskers = collOps.emptyMap[Processable, Unit] + setupAfterCreation(linkedClass) override def toString(): String = @@ -620,6 +629,14 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: // Elidable constructors elidableConstructorsInfo = computeElidableConstructorsInfo(linkedClass) + // Inlineable field bodies + val newInlineableFieldBodies = computeInlineableFieldBodies(linkedClass) + if (inlineableFieldBodies != newInlineableFieldBodies) { + inlineableFieldBodies = newInlineableFieldBodies + inlineableFieldBodiesAskers.keysIterator.foreach(_.tag()) + inlineableFieldBodiesAskers.clear() + } + // Inlineable class if (updateTryNewInlineable(linkedClass)) { for (method <- methods.values; if method.methodName.isConstructor) @@ -672,6 +689,22 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: hasElidableConstructors } + def askInlineableFieldBodies(asker: Processable): OptimizerCore.InlineableFieldBodies = { + inlineableFieldBodiesAskers.put(asker, ()) + + if (inlineableFieldBodies.isEmpty) { + // Avoid asking for `hasInlineableConstructors`; we always get here for non-ModuleClass'es + asker.registerTo(this) + inlineableFieldBodies + } else { + // No need for asker.registerTo(this) in this branch; it is done anyway in askHasElidableConstructors + if (askHasElidableConstructors(asker)) + inlineableFieldBodies + else + OptimizerCore.InlineableFieldBodies.Empty + } + } + /** UPDATE PASS ONLY. */ private def computeElidableConstructorsInfo(linkedClass: LinkedClass): ElidableConstructorsInfo = { import ElidableConstructorsInfo._ @@ -695,6 +728,39 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: } } + /** UPDATE PASS ONLY. */ + private def computeInlineableFieldBodies( + linkedClass: LinkedClass): OptimizerCore.InlineableFieldBodies = { + import OptimizerCore.InlineableFieldBodies + + if (linkedClass.kind != ClassKind.ModuleClass) { + InlineableFieldBodies.Empty + } else { + myInterface.staticLike(MemberNamespace.Constructor).methods.get(NoArgConstructorName) match { + case None => + InlineableFieldBodies.Empty + + case Some(ctor) => + val initFieldBodies = for { + fieldDef <- computeAllInstanceFieldDefs() + if !fieldDef.flags.isMutable + } yield { + // the zero value is always a Literal because the ftpe is not a RecordType + val zeroValue = zeroOf(fieldDef.ftpe)(NoPosition).asInstanceOf[Literal] + fieldDef.name.name -> FieldBody.Literal(zeroValue) + } + + if (initFieldBodies.isEmpty) { + // fast path + InlineableFieldBodies.Empty + } else { + val finalFieldBodies = interpretConstructor(ctor, initFieldBodies.toMap, Nil) + new InlineableFieldBodies(finalFieldBodies) + } + } + } + } + /** UPDATE PASS ONLY. */ def updateTryNewInlineable(linkedClass: LinkedClass): Boolean = { val oldTryNewInlineable = tryNewInlineable @@ -702,23 +768,27 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: tryNewInlineable = if (!linkedClass.optimizerHints.inline) { None } else { - val allFields = for { - parent <- reverseParentChain - anyField <- parent.fields - if !anyField.flags.namespace.isStatic - // non-JS class may only contain FieldDefs (no JSFieldDef) - field = anyField.asInstanceOf[FieldDef] - if parent.fieldsRead.contains(field.name.name) - } yield { - field - } - + val allFields = computeAllInstanceFieldDefs() Some(new OptimizerCore.InlineableClassStructure(allFields)) } tryNewInlineable != oldTryNewInlineable } + /** UPDATE PASS ONLY, used by `computeInlineableFieldBodies` and `updateTryNewInlineable`. */ + private def computeAllInstanceFieldDefs(): List[FieldDef] = { + for { + parent <- reverseParentChain + anyField <- parent.fields + if !anyField.flags.namespace.isStatic + // non-JS class may only contain FieldDefs (no JSFieldDef) + field = anyField.asInstanceOf[FieldDef] + if parent.fieldsRead.contains(field.name.name) + } yield { + field + } + } + /** UPDATE PASS ONLY. */ private[this] def setupAfterCreation(linkedClass: LinkedClass): Unit = { if (batchMode) { @@ -864,6 +934,128 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: } } + /** UPDATE PASS ONLY. */ + private def interpretConstructor(impl: MethodImpl, + fieldBodies: Map[FieldName, FieldBody], + paramBodies: List[Option[FieldBody]]): Map[FieldName, FieldBody] = { + + /* This method performs a kind of abstract intepretation of the given + * given constructor `impl`. It *assumes* that the enclosing class ends + * up having elidable constructors. If that is not the case, the + * computation must not crash but its result can be arbitrary. + */ + + type FieldBodyMap = Map[FieldName, FieldBody] + type ParamBodyMap = Map[LocalName, FieldBody] + + def interpretSelfGetter(methodName: MethodName, + fieldBodies: FieldBodyMap): Option[FieldBody] = { + methods.get(methodName).flatMap { impl => + impl.originalDef.body match { + case Some(Select(This(), FieldIdent(fieldName))) => + fieldBodies.get(fieldName) + + case _ => + /* If we get here, it means the class won't have elidable + * constructors, and therefore we can return arbitrary values + * from the whole method, so we don't care. + */ + None + } + } + } + + def interpretExpr(tree: Tree, fieldBodies: FieldBodyMap, + paramBodies: ParamBodyMap): Option[FieldBody] = { + tree match { + case lit: Literal => + Some(FieldBody.Literal(lit)) + + case VarRef(LocalIdent(valName)) => + paramBodies.get(valName) + + case LoadModule(moduleClassName) => + Some(FieldBody.LoadModule(moduleClassName, tree.pos)) + + case Select(qual @ LoadModule(moduleClassName), FieldIdent(fieldName)) => + val moduleBody = FieldBody.LoadModule(moduleClassName, qual.pos) + Some(FieldBody.ModuleSelect(moduleBody, fieldName, tree.tpe, tree.pos)) + + case Select(This(), FieldIdent(fieldName)) => + fieldBodies.get(fieldName) + + case Apply(_, receiver @ LoadModule(moduleClassName), MethodIdent(methodName), Nil) + if !methodName.isReflectiveProxy => + val moduleBody = FieldBody.LoadModule(moduleClassName, receiver.pos) + Some(FieldBody.ModuleGetter(moduleBody, methodName, tree.tpe, tree.pos)) + + case Apply(_, This(), MethodIdent(methodName), Nil) + if !methodName.isReflectiveProxy => + interpretSelfGetter(methodName, fieldBodies) + + case _ => + None + } + } + + def interpretBody(body: Tree, fieldBodies: FieldBodyMap, + paramBodies: ParamBodyMap): FieldBodyMap = { + body match { + case Block(stats) => + stats.foldLeft(fieldBodies) { (prev, stat) => + interpretBody(stat, prev, paramBodies) + } + + case Skip() => + fieldBodies + + case Assign(Select(This(), FieldIdent(fieldName)), rhs) => + if (!fieldBodies.contains(fieldName)) { + // It is a mutable field or it is dce'ed as never read, don't track it + fieldBodies + } else { + interpretExpr(rhs, fieldBodies, paramBodies) match { + case Some(newFieldBody) => + fieldBodies.updated(fieldName, newFieldBody) + case None => + // Unknown value; don't track it anymore + fieldBodies - fieldName + } + } + + // Mixin constructor -- assume it is empty + case ApplyStatically(flags, This(), className, methodName, Nil) + if !flags.isPrivate && !classes.contains(className) => + fieldBodies + + // Delegation to another constructor + case ApplyStatically(flags, This(), className, MethodIdent(methodName), args) if flags.isConstructor => + val argBodies = args.map(interpretExpr(_, fieldBodies, paramBodies)) + val ctorImpl = getInterface(className) + .staticLike(MemberNamespace.Constructor) + .methods(methodName) + interpretConstructor(ctorImpl, fieldBodies, argBodies) + + case _ => + /* Other statements cannot affect the eventual fieldBodies + * (assuming the class ends up having elidable constructors, as always). + */ + fieldBodies + } + } + + impl.originalDef.body.fold { + throw new AssertionError(s"Constructor $impl cannot be abstract") + } { body => + val paramBodiesMap: ParamBodyMap = + impl.originalDef.args.zip(paramBodies).collect { + case (paramDef, Some(paramBody)) => paramDef.name.name -> paramBody + }.toMap + + interpretBody(body, fieldBodies, paramBodiesMap) + } + } + /** All the methods of this class, including inherited ones. * It has () so we remember this is an expensive operation. * UPDATE PASS ONLY. @@ -890,6 +1082,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: def unregisterDependee(dependee: Processable): Unit = { hasElidableConstructorsAskers.remove(dependee) + inlineableFieldBodiesAskers.remove(dependee) } } @@ -1421,8 +1614,8 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: throw new AssertionError("Methods to optimize must be concrete") } - val (newParams, newBody) = new Optimizer(this, this.toString()).optimize( - Some(this), owner.untrackedThisType, params, jsClassCaptures = Nil, + val (newParams, newBody) = new Optimizer(this, Some(this), this.toString()).optimize( + owner.untrackedThisType, params, jsClassCaptures = Nil, resultType, body, isNoArgCtor = name.name == NoArgConstructorName) MethodDef(static, name, originalName, @@ -1446,8 +1639,8 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: case originalDef @ JSMethodDef(flags, name, params, restParam, body) => val thisType = owner.untrackedThisType(flags.namespace) - val (newParamsAndRest, newBody) = new Optimizer(this, this.toString()).optimize( - None, thisType, params ++ restParam.toList, owner.untrackedJSClassCaptures, + val (newParamsAndRest, newBody) = new Optimizer(this, None, this.toString()).optimize( + thisType, params ++ restParam.toList, owner.untrackedJSClassCaptures, AnyType, body, isNoArgCtor = false) val (newParams, newRestParam) = @@ -1462,14 +1655,14 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: val jsClassCaptures = owner.untrackedJSClassCaptures val newGetterBody = getterBody.map { body => - val (_, newBody) = new Optimizer(this, "get " + this.toString()).optimize( - None, thisType, Nil, jsClassCaptures, AnyType, body, isNoArgCtor = false) + val (_, newBody) = new Optimizer(this, None, "get " + this.toString()).optimize( + thisType, Nil, jsClassCaptures, AnyType, body, isNoArgCtor = false) newBody } val newSetterArgAndBody = setterArgAndBody.map { case (param, body) => - val (List(newParam), newBody) = new Optimizer(this, "set " + this.toString()).optimize( - None, thisType, List(param), jsClassCaptures, AnyType, body, + val (List(newParam), newBody) = new Optimizer(this, None, "set " + this.toString()).optimize( + thisType, List(param), jsClassCaptures, AnyType, body, isNoArgCtor = false) (newParam, newBody) } @@ -1494,8 +1687,8 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: val thisType = owner.untrackedThisType(flags.namespace) - val (newParamsAndRest, newRawBody) = new Optimizer(this, this.toString()).optimize( - None, thisType, params ++ restParam.toList, owner.untrackedJSClassCaptures, AnyType, + val (newParamsAndRest, newRawBody) = new Optimizer(this, None, this.toString()).optimize( + thisType, params ++ restParam.toList, owner.untrackedJSClassCaptures, AnyType, Block(body.allStats)(body.pos), isNoArgCtor = false) val (newParams, newRestParam) = @@ -1522,7 +1715,8 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: * * All methods are PROCESS PASS ONLY */ - private final class Optimizer(asker: Processable, debugID: String) + private final class Optimizer( + asker: Processable, protected val myself: Option[MethodImpl], debugID: String) extends OptimizerCore(config, debugID) { import OptimizerCore.ImportTarget @@ -1549,6 +1743,9 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: protected def hasElidableConstructors(className: ClassName): Boolean = classes(className).askHasElidableConstructors(asker) + protected def inlineableFieldBodies(className: ClassName): OptimizerCore.InlineableFieldBodies = + classes.get(className).fold(OptimizerCore.InlineableFieldBodies.Empty)(_.askInlineableFieldBodies(asker)) + protected def tryNewInlineableClass( className: ClassName): Option[OptimizerCore.InlineableClassStructure] = { classes(className).tryNewInlineable 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 2dca9842bd..d9c57e3b9a 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 @@ -46,6 +46,8 @@ private[optimizer] abstract class OptimizerCore( type MethodID <: AbstractMethodID + protected val myself: Option[MethodID] + private def semantics: Semantics = config.coreSpec.semantics // Uncomment and adapt to print debug messages only during one method @@ -72,6 +74,13 @@ private[optimizer] abstract class OptimizerCore( */ protected def hasElidableConstructors(className: ClassName): Boolean + /** Returns the inlineable field bodies of this module class. + * + * If the class is not a module class, or if it does not have inlineable + * accessors, the resulting `InlineableFieldBodies` is always empty. + */ + protected def inlineableFieldBodies(className: ClassName): InlineableFieldBodies + /** Tests whether the given class is inlineable. * * @return @@ -141,7 +150,7 @@ private[optimizer] abstract class OptimizerCore( private val intrinsics = Intrinsics.buildIntrinsics(config.coreSpec.esFeatures) - def optimize(myself: Option[MethodID], thisType: Type, params: List[ParamDef], + def optimize(thisType: Type, params: List[ParamDef], jsClassCaptures: List[ParamDef], resultType: Type, body: Tree, isNoArgCtor: Boolean): (List[ParamDef], Tree) = { try { @@ -1220,28 +1229,72 @@ private[optimizer] abstract class OptimizerCore( cont(PreTransLit(IntLiteral(resultValue))) case _ => - resolveLocalDef(preTransQual) match { - case PreTransRecordTree(newQual, origType, cancelFun) => - val recordType = newQual.tpe.asInstanceOf[RecordType] - /* FIXME How come this lookup requires only the `simpleName`? - * The `recordType` is created at `InlineableClassStructure.recordType`, - * where it uses an allocator. Something fishy is going on here. - * (And no, this is not dead code.) - */ - val recordField = recordType.findField(field.name.simpleName) - val sel = RecordSelect(newQual, SimpleFieldIdent(recordField.name))(recordField.tpe) - sel.tpe match { - case _: RecordType => - cont(PreTransRecordTree(sel, RefinedType(expectedType), cancelFun)) - case _ => - cont(PreTransTree(sel, RefinedType(sel.tpe))) - } + def default: TailRec[Tree] = { + resolveLocalDef(preTransQual) match { + case PreTransRecordTree(newQual, origType, cancelFun) => + val recordType = newQual.tpe.asInstanceOf[RecordType] + /* FIXME How come this lookup requires only the `simpleName`? + * The `recordType` is created at `InlineableClassStructure.recordType`, + * where it uses an allocator. Something fishy is going on here. + * (And no, this is not dead code.) + */ + val recordField = recordType.findField(field.name.simpleName) + val sel = RecordSelect(newQual, SimpleFieldIdent(recordField.name))(recordField.tpe) + sel.tpe match { + case _: RecordType => + cont(PreTransRecordTree(sel, RefinedType(expectedType), cancelFun)) + case _ => + cont(PreTransTree(sel, RefinedType(sel.tpe))) + } - case PreTransTree(newQual, newQualType) => - val newQual1 = maybeAssumeNotNull(newQual, newQualType) - cont(PreTransTree(Select(newQual1, field)(expectedType), - RefinedType(expectedType))) + case PreTransTree(newQual, newQualType) => + val newQual1 = maybeAssumeNotNull(newQual, newQualType) + cont(PreTransTree(Select(newQual1, field)(expectedType), + RefinedType(expectedType))) + } } + + preTransQual.tpe match { + // Try to inline an inlineable field body + case RefinedType(ClassType(qualClassName), _, _) if !isLhsOfAssign => + if (myself.exists(m => m.enclosingClassName == qualClassName && m.methodName.isConstructor)) { + /* Within the constructor of a class, we cannot trust the + * inlineable field bodies of that class, since they only reflect + * the values of fields when the instance is fully initialized. + */ + default + } else { + inlineableFieldBodies(qualClassName).fieldBodies.get(field.name) match { + case None => + default + case Some(fieldBody) => + val qualSideEffects = checkNotNullStatement(preTransQual) + val fieldBodyTree = fieldBodyToTree(fieldBody) + pretransformExpr(fieldBodyTree) { preTransFieldBody => + cont(PreTransBlock(qualSideEffects, preTransFieldBody)) + } + } + } + case _ => + default + } + } + } + + private def fieldBodyToTree(fieldBody: InlineableFieldBodies.FieldBody): Tree = { + import InlineableFieldBodies.FieldBody + + implicit val pos = fieldBody.pos + + fieldBody match { + case FieldBody.Literal(literal, _) => + literal + case FieldBody.LoadModule(moduleClassName, _) => + LoadModule(moduleClassName) + case FieldBody.ModuleSelect(qualifier, fieldName, tpe, _) => + Select(fieldBodyToTree(qualifier), FieldIdent(fieldName))(tpe) + case FieldBody.ModuleGetter(qualifier, methodName, tpe, _) => + Apply(ApplyFlags.empty, fieldBodyToTree(qualifier), MethodIdent(methodName), Nil)(tpe) } } @@ -5138,6 +5191,66 @@ private[optimizer] object OptimizerCore { } } + final class InlineableFieldBodies( + val fieldBodies: Map[FieldName, InlineableFieldBodies.FieldBody] + ) { + def isEmpty: Boolean = fieldBodies.isEmpty + + override def equals(that: Any): Boolean = that match { + case that: InlineableFieldBodies => + this.fieldBodies == that.fieldBodies + case _ => + false + } + + override def hashCode(): Int = fieldBodies.## + + override def toString(): String = { + fieldBodies + .map(f => s"${f._1.nameString}: ${f._2}") + .mkString("InlineableFieldBodies(", ", ", ")") + } + } + + object InlineableFieldBodies { + /** The body of field that we can inline. + * + * This hierarchy mirrors the small subset of `Tree`s that we need to + * represent field bodies that we can inline. + * + * Unlike `Tree`, `FieldBody` guarantees a comprehensive equality test + * representing its full structure. It is generally not safe to compare + * `Tree`s for equality, but for `FieldBody` it is safe. We use equality + * in `IncOptimizer` to detect changes. + * + * This is also why the members of the hierarchy contain an explicit + * `Position` in their primary parameter list. + */ + sealed abstract class FieldBody { + val pos: Position + } + + object FieldBody { + final case class Literal(literal: Trees.Literal, pos: Position) extends FieldBody { + require(pos == literal.pos, s"TreeBody.Literal.pos must be the same as its Literal") + } + + object Literal { + def apply(literal: Trees.Literal): Literal = + Literal(literal, literal.pos) + } + + final case class LoadModule(moduleClassName: ClassName, pos: Position) + extends FieldBody + final case class ModuleSelect(qualifier: LoadModule, + fieldName: FieldName, tpe: Type, pos: Position) extends FieldBody + final case class ModuleGetter(qualifier: LoadModule, + methodName: MethodName, tpe: Type, pos: Position) extends FieldBody + } + + val Empty: InlineableFieldBodies = new InlineableFieldBodies(Map.empty) + } + private final val MaxRollbacksPerMethod = 256 private final class TooManyRollbacksException diff --git a/project/Build.scala b/project/Build.scala index f3c9cbedac..25eec2454a 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1975,7 +1975,7 @@ object Build { case `default213Version` => Some(ExpectedSizes( - fastLink = 463000 to 464000, + fastLink = 462000 to 463000, fullLink = 99000 to 100000, fastLinkGz = 60000 to 61000, fullLinkGz = 26000 to 27000, From 31452d4b2f81ab253adb2bdfb6761ec7411fd55e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 9 Feb 2024 15:56:53 +0100 Subject: [PATCH 049/298] Turn fields of scala.reflect.* back into `val`s. They are `val`s in the original version of the files. We previously turned them into `def`s because it allowed our inliner to do a better job. However, now that we can "inline" the fields of modules with elidable constructors, that is no longer necessary. Interestingly, this allows `scala.reflect.package$` and `scala.reflect.ClassManifestFactory$` to have elidable constructors, which they did not have before. That is because they referred to the `def`s in their constructor, which now have a getter shape and therefore can be understood by the side-effect-free analysis. --- project/Build.scala | 4 +- .../scala/reflect/ClassTag.scala | 30 ++++----- .../scala/reflect/Manifest.scala | 30 ++++----- .../scala/reflect/ClassTag.scala | 30 ++++----- .../scala/reflect/Manifest.scala | 62 +++++++++---------- 5 files changed, 78 insertions(+), 78 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index 25eec2454a..a14202f7f9 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1967,8 +1967,8 @@ object Build { scalaVersion.value match { case `default212Version` => Some(ExpectedSizes( - fastLink = 642000 to 643000, - fullLink = 102000 to 103000, + fastLink = 640000 to 641000, + fullLink = 101000 to 102000, fastLinkGz = 77000 to 78000, fullLinkGz = 26000 to 27000, )) diff --git a/scalalib/overrides-2.12/scala/reflect/ClassTag.scala b/scalalib/overrides-2.12/scala/reflect/ClassTag.scala index f9dc5dd716..20fac6a392 100644 --- a/scalalib/overrides-2.12/scala/reflect/ClassTag.scala +++ b/scalalib/overrides-2.12/scala/reflect/ClassTag.scala @@ -133,21 +133,21 @@ trait ClassTag[T] extends ClassManifestDeprecatedApis[T] with Equals with Serial * Class tags corresponding to primitive types and constructor/extractor for ClassTags. */ object ClassTag { - def Byte : ClassTag[scala.Byte] = ManifestFactory.Byte - def Short : ClassTag[scala.Short] = ManifestFactory.Short - def Char : ClassTag[scala.Char] = ManifestFactory.Char - def Int : ClassTag[scala.Int] = ManifestFactory.Int - def Long : ClassTag[scala.Long] = ManifestFactory.Long - def Float : ClassTag[scala.Float] = ManifestFactory.Float - def Double : ClassTag[scala.Double] = ManifestFactory.Double - def Boolean : ClassTag[scala.Boolean] = ManifestFactory.Boolean - def Unit : ClassTag[scala.Unit] = ManifestFactory.Unit - def Any : ClassTag[scala.Any] = ManifestFactory.Any - def Object : ClassTag[java.lang.Object] = ManifestFactory.Object - def AnyVal : ClassTag[scala.AnyVal] = ManifestFactory.AnyVal - def AnyRef : ClassTag[scala.AnyRef] = ManifestFactory.AnyRef - def Nothing : ClassTag[scala.Nothing] = ManifestFactory.Nothing - def Null : ClassTag[scala.Null] = ManifestFactory.Null + val Byte : ClassTag[scala.Byte] = ManifestFactory.Byte + val Short : ClassTag[scala.Short] = ManifestFactory.Short + val Char : ClassTag[scala.Char] = ManifestFactory.Char + val Int : ClassTag[scala.Int] = ManifestFactory.Int + val Long : ClassTag[scala.Long] = ManifestFactory.Long + val Float : ClassTag[scala.Float] = ManifestFactory.Float + val Double : ClassTag[scala.Double] = ManifestFactory.Double + val Boolean : ClassTag[scala.Boolean] = ManifestFactory.Boolean + val Unit : ClassTag[scala.Unit] = ManifestFactory.Unit + val Any : ClassTag[scala.Any] = ManifestFactory.Any + val Object : ClassTag[java.lang.Object] = ManifestFactory.Object + val AnyVal : ClassTag[scala.AnyVal] = ManifestFactory.AnyVal + val AnyRef : ClassTag[scala.AnyRef] = ManifestFactory.AnyRef + val Nothing : ClassTag[scala.Nothing] = ManifestFactory.Nothing + val Null : ClassTag[scala.Null] = ManifestFactory.Null @inline private class GenericClassTag[T](val runtimeClass: jClass[_]) extends ClassTag[T] diff --git a/scalalib/overrides-2.12/scala/reflect/Manifest.scala b/scalalib/overrides-2.12/scala/reflect/Manifest.scala index f38ce59e4d..a7b8169481 100644 --- a/scalalib/overrides-2.12/scala/reflect/Manifest.scala +++ b/scalalib/overrides-2.12/scala/reflect/Manifest.scala @@ -93,7 +93,7 @@ object ManifestFactory { override def newArrayBuilder(): ArrayBuilder[Byte] = new ArrayBuilder.ofByte() private def readResolve(): Any = Manifest.Byte } - def Byte: AnyValManifest[Byte] = ByteManifest + val Byte: AnyValManifest[Byte] = ByteManifest private object ShortManifest extends AnyValManifest[scala.Short]("Short") { def runtimeClass = java.lang.Short.TYPE @@ -102,7 +102,7 @@ object ManifestFactory { override def newArrayBuilder(): ArrayBuilder[Short] = new ArrayBuilder.ofShort() private def readResolve(): Any = Manifest.Short } - def Short: AnyValManifest[Short] = ShortManifest + val Short: AnyValManifest[Short] = ShortManifest private object CharManifest extends AnyValManifest[scala.Char]("Char") { def runtimeClass = java.lang.Character.TYPE @@ -111,7 +111,7 @@ object ManifestFactory { override def newArrayBuilder(): ArrayBuilder[Char] = new ArrayBuilder.ofChar() private def readResolve(): Any = Manifest.Char } - def Char: AnyValManifest[Char] = CharManifest + val Char: AnyValManifest[Char] = CharManifest private object IntManifest extends AnyValManifest[scala.Int]("Int") { def runtimeClass = java.lang.Integer.TYPE @@ -120,7 +120,7 @@ object ManifestFactory { override def newArrayBuilder(): ArrayBuilder[Int] = new ArrayBuilder.ofInt() private def readResolve(): Any = Manifest.Int } - def Int: AnyValManifest[Int] = IntManifest + val Int: AnyValManifest[Int] = IntManifest private object LongManifest extends AnyValManifest[scala.Long]("Long") { def runtimeClass = java.lang.Long.TYPE @@ -129,7 +129,7 @@ object ManifestFactory { override def newArrayBuilder(): ArrayBuilder[Long] = new ArrayBuilder.ofLong() private def readResolve(): Any = Manifest.Long } - def Long: AnyValManifest[Long] = LongManifest + val Long: AnyValManifest[Long] = LongManifest private object FloatManifest extends AnyValManifest[scala.Float]("Float") { def runtimeClass = java.lang.Float.TYPE @@ -138,7 +138,7 @@ object ManifestFactory { override def newArrayBuilder(): ArrayBuilder[Float] = new ArrayBuilder.ofFloat() private def readResolve(): Any = Manifest.Float } - def Float: AnyValManifest[Float] = FloatManifest + val Float: AnyValManifest[Float] = FloatManifest private object DoubleManifest extends AnyValManifest[scala.Double]("Double") { def runtimeClass = java.lang.Double.TYPE @@ -147,7 +147,7 @@ object ManifestFactory { override def newArrayBuilder(): ArrayBuilder[Double] = new ArrayBuilder.ofDouble() private def readResolve(): Any = Manifest.Double } - def Double: AnyValManifest[Double] = DoubleManifest + val Double: AnyValManifest[Double] = DoubleManifest private object BooleanManifest extends AnyValManifest[scala.Boolean]("Boolean") { def runtimeClass = java.lang.Boolean.TYPE @@ -156,7 +156,7 @@ object ManifestFactory { override def newArrayBuilder(): ArrayBuilder[Boolean] = new ArrayBuilder.ofBoolean() private def readResolve(): Any = Manifest.Boolean } - def Boolean: AnyValManifest[Boolean] = BooleanManifest + val Boolean: AnyValManifest[Boolean] = BooleanManifest private object UnitManifest extends AnyValManifest[scala.Unit]("Unit") { def runtimeClass = java.lang.Void.TYPE @@ -168,7 +168,7 @@ object ManifestFactory { else super.arrayClass(tp) private def readResolve(): Any = Manifest.Unit } - def Unit: AnyValManifest[Unit] = UnitManifest + val Unit: AnyValManifest[Unit] = UnitManifest private object AnyManifest extends PhantomManifest[scala.Any](classOf[java.lang.Object], "Any") { override def runtimeClass = classOf[java.lang.Object] @@ -176,7 +176,7 @@ object ManifestFactory { override def <:<(that: ClassManifest[_]): Boolean = (that eq this) private def readResolve(): Any = Manifest.Any } - def Any: Manifest[scala.Any] = AnyManifest + val Any: Manifest[scala.Any] = AnyManifest private object ObjectManifest extends PhantomManifest[java.lang.Object](classOf[java.lang.Object], "Object") { override def runtimeClass = classOf[java.lang.Object] @@ -184,9 +184,9 @@ object ManifestFactory { override def <:<(that: ClassManifest[_]): Boolean = (that eq this) || (that eq Any) private def readResolve(): Any = Manifest.Object } - def Object: Manifest[java.lang.Object] = ObjectManifest + val Object: Manifest[java.lang.Object] = ObjectManifest - def AnyRef: Manifest[scala.AnyRef] = Object.asInstanceOf[Manifest[scala.AnyRef]] + val AnyRef: Manifest[scala.AnyRef] = Object private object AnyValManifest extends PhantomManifest[scala.AnyVal](classOf[java.lang.Object], "AnyVal") { override def runtimeClass = classOf[java.lang.Object] @@ -194,7 +194,7 @@ object ManifestFactory { override def <:<(that: ClassManifest[_]): Boolean = (that eq this) || (that eq Any) private def readResolve(): Any = Manifest.AnyVal } - def AnyVal: Manifest[scala.AnyVal] = AnyValManifest + val AnyVal: Manifest[scala.AnyVal] = AnyValManifest private object NullManifest extends PhantomManifest[scala.Null](classOf[scala.runtime.Null$], "Null") { override def runtimeClass = classOf[scala.runtime.Null$] @@ -203,7 +203,7 @@ object ManifestFactory { (that ne null) && (that ne Nothing) && !(that <:< AnyVal) private def readResolve(): Any = Manifest.Null } - def Null: Manifest[scala.Null] = NullManifest + val Null: Manifest[scala.Null] = NullManifest private object NothingManifest extends PhantomManifest[scala.Nothing](classOf[scala.runtime.Nothing$], "Nothing") { override def runtimeClass = classOf[scala.runtime.Nothing$] @@ -211,7 +211,7 @@ object ManifestFactory { override def <:<(that: ClassManifest[_]): Boolean = (that ne null) private def readResolve(): Any = Manifest.Nothing } - def Nothing: Manifest[scala.Nothing] = NothingManifest + val Nothing: Manifest[scala.Nothing] = NothingManifest private class SingletonTypeManifest[T <: AnyRef](value: AnyRef) extends Manifest[T] { lazy val runtimeClass = value.getClass diff --git a/scalalib/overrides-2.13/scala/reflect/ClassTag.scala b/scalalib/overrides-2.13/scala/reflect/ClassTag.scala index 253d55cefb..a66a1a6a8e 100644 --- a/scalalib/overrides-2.13/scala/reflect/ClassTag.scala +++ b/scalalib/overrides-2.13/scala/reflect/ClassTag.scala @@ -96,21 +96,21 @@ trait ClassTag[T] extends ClassManifestDeprecatedApis[T] with Equals with Serial object ClassTag { import ManifestFactory._ - @uncheckedStable def Byte : ByteManifest = ManifestFactory.Byte - @uncheckedStable def Short : ShortManifest = ManifestFactory.Short - @uncheckedStable def Char : CharManifest = ManifestFactory.Char - @uncheckedStable def Int : IntManifest = ManifestFactory.Int - @uncheckedStable def Long : LongManifest = ManifestFactory.Long - @uncheckedStable def Float : FloatManifest = ManifestFactory.Float - @uncheckedStable def Double : DoubleManifest = ManifestFactory.Double - @uncheckedStable def Boolean : BooleanManifest = ManifestFactory.Boolean - @uncheckedStable def Unit : UnitManifest = ManifestFactory.Unit - @uncheckedStable def Any : ClassTag[scala.Any] = ManifestFactory.Any - @uncheckedStable def Object : ClassTag[java.lang.Object] = ManifestFactory.Object - @uncheckedStable def AnyVal : ClassTag[scala.AnyVal] = ManifestFactory.AnyVal - @uncheckedStable def AnyRef : ClassTag[scala.AnyRef] = ManifestFactory.AnyRef - @uncheckedStable def Nothing : ClassTag[scala.Nothing] = ManifestFactory.Nothing - @uncheckedStable def Null : ClassTag[scala.Null] = ManifestFactory.Null + val Byte : ByteManifest = ManifestFactory.Byte + val Short : ShortManifest = ManifestFactory.Short + val Char : CharManifest = ManifestFactory.Char + val Int : IntManifest = ManifestFactory.Int + val Long : LongManifest = ManifestFactory.Long + val Float : FloatManifest = ManifestFactory.Float + val Double : DoubleManifest = ManifestFactory.Double + val Boolean : BooleanManifest = ManifestFactory.Boolean + val Unit : UnitManifest = ManifestFactory.Unit + val Any : ClassTag[scala.Any] = ManifestFactory.Any + val Object : ClassTag[java.lang.Object] = ManifestFactory.Object + val AnyVal : ClassTag[scala.AnyVal] = ManifestFactory.AnyVal + val AnyRef : ClassTag[scala.AnyRef] = ManifestFactory.AnyRef + val Nothing : ClassTag[scala.Nothing] = ManifestFactory.Nothing + val Null : ClassTag[scala.Null] = ManifestFactory.Null @inline @SerialVersionUID(1L) diff --git a/scalalib/overrides-2.13/scala/reflect/Manifest.scala b/scalalib/overrides-2.13/scala/reflect/Manifest.scala index 053b0c485d..faa2fe6f6e 100644 --- a/scalalib/overrides-2.13/scala/reflect/Manifest.scala +++ b/scalalib/overrides-2.13/scala/reflect/Manifest.scala @@ -80,22 +80,22 @@ object Manifest { def valueManifests: List[AnyValManifest[_]] = ManifestFactory.valueManifests - def Byte: ManifestFactory.ByteManifest = ManifestFactory.Byte - def Short: ManifestFactory.ShortManifest = ManifestFactory.Short - def Char: ManifestFactory.CharManifest = ManifestFactory.Char - def Int: ManifestFactory.IntManifest = ManifestFactory.Int - def Long: ManifestFactory.LongManifest = ManifestFactory.Long - def Float: ManifestFactory.FloatManifest = ManifestFactory.Float - def Double: ManifestFactory.DoubleManifest = ManifestFactory.Double - def Boolean: ManifestFactory.BooleanManifest = ManifestFactory.Boolean - def Unit: ManifestFactory.UnitManifest = ManifestFactory.Unit - - def Any: Manifest[scala.Any] = ManifestFactory.Any - def Object: Manifest[java.lang.Object] = ManifestFactory.Object - def AnyRef: Manifest[scala.AnyRef] = ManifestFactory.AnyRef - def AnyVal: Manifest[scala.AnyVal] = ManifestFactory.AnyVal - def Null: Manifest[scala.Null] = ManifestFactory.Null - def Nothing: Manifest[scala.Nothing] = ManifestFactory.Nothing + val Byte: ManifestFactory.ByteManifest = ManifestFactory.Byte + val Short: ManifestFactory.ShortManifest = ManifestFactory.Short + val Char: ManifestFactory.CharManifest = ManifestFactory.Char + val Int: ManifestFactory.IntManifest = ManifestFactory.Int + val Long: ManifestFactory.LongManifest = ManifestFactory.Long + val Float: ManifestFactory.FloatManifest = ManifestFactory.Float + val Double: ManifestFactory.DoubleManifest = ManifestFactory.Double + val Boolean: ManifestFactory.BooleanManifest = ManifestFactory.Boolean + val Unit: ManifestFactory.UnitManifest = ManifestFactory.Unit + + val Any: Manifest[scala.Any] = ManifestFactory.Any + val Object: Manifest[java.lang.Object] = ManifestFactory.Object + val AnyRef: Manifest[scala.AnyRef] = ManifestFactory.AnyRef + val AnyVal: Manifest[scala.AnyVal] = ManifestFactory.AnyVal + val Null: Manifest[scala.Null] = ManifestFactory.Null + val Nothing: Manifest[scala.Nothing] = ManifestFactory.Nothing /** Manifest for the singleton type `value.type`. */ def singleType[T <: AnyRef](value: AnyRef): Manifest[T] = @@ -181,7 +181,7 @@ object ManifestFactory { private def readResolve(): Any = Manifest.Byte } private object ByteManifest extends ByteManifest - def Byte: ByteManifest = ByteManifest + val Byte: ByteManifest = ByteManifest @SerialVersionUID(1L) private[reflect] class ShortManifest extends AnyValManifest[scala.Short]("Short") { @@ -198,7 +198,7 @@ object ManifestFactory { private def readResolve(): Any = Manifest.Short } private object ShortManifest extends ShortManifest - def Short: ShortManifest = ShortManifest + val Short: ShortManifest = ShortManifest @SerialVersionUID(1L) private[reflect] class CharManifest extends AnyValManifest[scala.Char]("Char") { @@ -215,7 +215,7 @@ object ManifestFactory { private def readResolve(): Any = Manifest.Char } private object CharManifest extends CharManifest - def Char: CharManifest = CharManifest + val Char: CharManifest = CharManifest @SerialVersionUID(1L) private[reflect] class IntManifest extends AnyValManifest[scala.Int]("Int") { @@ -232,7 +232,7 @@ object ManifestFactory { private def readResolve(): Any = Manifest.Int } private object IntManifest extends IntManifest - def Int: IntManifest = IntManifest + val Int: IntManifest = IntManifest @SerialVersionUID(1L) private[reflect] class LongManifest extends AnyValManifest[scala.Long]("Long") { @@ -249,7 +249,7 @@ object ManifestFactory { private def readResolve(): Any = Manifest.Long } private object LongManifest extends LongManifest - def Long: LongManifest = LongManifest + val Long: LongManifest = LongManifest @SerialVersionUID(1L) private[reflect] class FloatManifest extends AnyValManifest[scala.Float]("Float") { @@ -266,7 +266,7 @@ object ManifestFactory { private def readResolve(): Any = Manifest.Float } private object FloatManifest extends FloatManifest - def Float: FloatManifest = FloatManifest + val Float: FloatManifest = FloatManifest @SerialVersionUID(1L) private[reflect] class DoubleManifest extends AnyValManifest[scala.Double]("Double") { @@ -284,7 +284,7 @@ object ManifestFactory { private def readResolve(): Any = Manifest.Double } private object DoubleManifest extends DoubleManifest - def Double: DoubleManifest = DoubleManifest + val Double: DoubleManifest = DoubleManifest @SerialVersionUID(1L) private[reflect] class BooleanManifest extends AnyValManifest[scala.Boolean]("Boolean") { @@ -301,7 +301,7 @@ object ManifestFactory { private def readResolve(): Any = Manifest.Boolean } private object BooleanManifest extends BooleanManifest - def Boolean: BooleanManifest = BooleanManifest + val Boolean: BooleanManifest = BooleanManifest @SerialVersionUID(1L) private[reflect] class UnitManifest extends AnyValManifest[scala.Unit]("Unit") { @@ -321,7 +321,7 @@ object ManifestFactory { private def readResolve(): Any = Manifest.Unit } private object UnitManifest extends UnitManifest - def Unit: UnitManifest = UnitManifest + val Unit: UnitManifest = UnitManifest private object AnyManifest extends PhantomManifest[scala.Any](classOf[java.lang.Object], "Any") { override def runtimeClass = classOf[java.lang.Object] @@ -329,7 +329,7 @@ object ManifestFactory { override def <:<(that: ClassManifest[_]): Boolean = (that eq this) private def readResolve(): Any = Manifest.Any } - def Any: Manifest[scala.Any] = AnyManifest + val Any: Manifest[scala.Any] = AnyManifest private object ObjectManifest extends PhantomManifest[java.lang.Object](classOf[java.lang.Object], "Object") { override def runtimeClass = classOf[java.lang.Object] @@ -337,9 +337,9 @@ object ManifestFactory { override def <:<(that: ClassManifest[_]): Boolean = (that eq this) || (that eq Any) private def readResolve(): Any = Manifest.Object } - def Object: Manifest[java.lang.Object] = ObjectManifest + val Object: Manifest[java.lang.Object] = ObjectManifest - def AnyRef: Manifest[scala.AnyRef] = Object.asInstanceOf[Manifest[scala.AnyRef]] + val AnyRef: Manifest[scala.AnyRef] = Object private object AnyValManifest extends PhantomManifest[scala.AnyVal](classOf[java.lang.Object], "AnyVal") { override def runtimeClass = classOf[java.lang.Object] @@ -347,7 +347,7 @@ object ManifestFactory { override def <:<(that: ClassManifest[_]): Boolean = (that eq this) || (that eq Any) private def readResolve(): Any = Manifest.AnyVal } - def AnyVal: Manifest[scala.AnyVal] = AnyValManifest + val AnyVal: Manifest[scala.AnyVal] = AnyValManifest private object NullManifest extends PhantomManifest[scala.Null](classOf[scala.runtime.Null$], "Null") { override def runtimeClass = classOf[scala.runtime.Null$] @@ -356,7 +356,7 @@ object ManifestFactory { (that ne null) && (that ne Nothing) && !(that <:< AnyVal) private def readResolve(): Any = Manifest.Null } - def Null: Manifest[scala.Null] = NullManifest + val Null: Manifest[scala.Null] = NullManifest private object NothingManifest extends PhantomManifest[scala.Nothing](classOf[scala.runtime.Nothing$], "Nothing") { override def runtimeClass = classOf[scala.runtime.Nothing$] @@ -364,7 +364,7 @@ object ManifestFactory { override def <:<(that: ClassManifest[_]): Boolean = (that ne null) private def readResolve(): Any = Manifest.Nothing } - def Nothing: Manifest[scala.Nothing] = NothingManifest + val Nothing: Manifest[scala.Nothing] = NothingManifest @SerialVersionUID(1L) private class SingletonTypeManifest[T <: AnyRef](value: AnyRef) extends Manifest[T] { From 08d0286e21020a9788d1f97e8390d5736a865ca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 15 Feb 2024 11:48:06 +0100 Subject: [PATCH 050/298] Inline `genUnchecked` at its two call sites. It had been factored out in `SJSGen` because of one condition that happens to be repeated. However, it is clearer that it does the right thing at each of its call sites. Given the amount of information that needed to be passed to this helper, inlining it twice actually removes additional checks. Ultimately, it seems simpler this way. --- .../scalajs/linker/backend/emitter/CoreJSLib.scala | 5 ++++- .../linker/backend/emitter/FunctionEmitter.scala | 11 +++++++++-- .../scalajs/linker/backend/emitter/SJSGen.scala | 14 -------------- 3 files changed, 13 insertions(+), 17 deletions(-) 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 b36e35783d..89e4ad86df 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 @@ -1171,7 +1171,10 @@ private[emitter] object CoreJSLib { // Both values have the same "data" (could also be falsy values) If(srcData && genIdentBracketSelect(srcData, "isArrayClass"), { // Fast path: the values are array of the same type - genUncheckedArraycopy(List(src, srcPos, dest, destPos, length)) + if (esVersion >= ESVersion.ES2015 && nullPointers == CheckedBehavior.Unchecked) + Apply(src DOT "copyTo", List(srcPos, dest, destPos, length)) + else + genCallHelper(VarField.systemArraycopy, src, srcPos, dest, destPos, length) }, { genCallHelper(VarField.throwArrayStoreException, Null()) }) 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 32b8baca4f..75b4bdeb61 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 @@ -875,12 +875,19 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { implicit val env = env0 val jsArgs = newArgs.map(transformExprNoChar(_)) + def genUnchecked(): js.Tree = { + if (esFeatures.esVersion >= ESVersion.ES2015 && semantics.nullPointers == CheckedBehavior.Unchecked) + js.Apply(jsArgs.head DOT "copyTo", jsArgs.tail) + else + genCallHelper(VarField.systemArraycopy, jsArgs: _*) + } + if (semantics.arrayStores == Unchecked) { - genUncheckedArraycopy(jsArgs) + genUnchecked() } else { (src.tpe, dest.tpe) match { case (PrimArray(srcPrimRef), PrimArray(destPrimRef)) if srcPrimRef == destPrimRef => - genUncheckedArraycopy(jsArgs) + genUnchecked() case (RefArray(), RefArray()) => genCallHelper(VarField.systemArraycopyRefs, jsArgs: _*) case _ => 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 4541d3a292..e7ea19a9db 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 @@ -149,20 +149,6 @@ private[emitter] final class SJSGen( } } - def genUncheckedArraycopy(args: List[Tree])( - implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, - pos: Position): Tree = { - import TreeDSL._ - - assert(args.lengthCompare(5) == 0, - s"wrong number of args for genUncheckedArrayCopy: $args") - - if (esFeatures.esVersion >= ESVersion.ES2015 && semantics.nullPointers == CheckedBehavior.Unchecked) - Apply(args.head DOT "copyTo", args.tail) - else - genCallHelper(VarField.systemArraycopy, args: _*) - } - def genSelect(receiver: Tree, field: irt.FieldIdent)( implicit pos: Position): Tree = { DotSelect(receiver, Ident(genName(field.name))(field.pos)) From ec2a24e7dbc75094d122d3077fb65106db105fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 26 Feb 2024 16:01:18 +0100 Subject: [PATCH 051/298] Fix #4954: Count `!_allowComplete` as a fake pending task of WorkTracker. --- .../scalajs/linker/analyzer/Analyzer.scala | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) 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 11a997ef27..534a30ef85 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 @@ -1571,9 +1571,11 @@ private object AnalyzerRun { MethodName("getSuperclass", Nil, ClassRef(ClassClass)) private class WorkTracker(implicit ec: ExecutionContext) { - private val pending = new AtomicInteger(0) + /** The number of tasks that have started but not completed, `+ 1` until + * `allowComplete()` gets called. + */ + private val pending = new AtomicInteger(1) private val failures = new AtomicReference[List[Throwable]](Nil) - @volatile private var _allowComplete = false private val promise = Promise[Unit]() def track(fut: Future[Unit]): Unit = { @@ -1584,8 +1586,7 @@ private object AnalyzerRun { case Success(_) => () case Failure(t) => addFailure(t) } - if (pending.decrementAndGet() == 0) - tryComplete() + decrementPending() } } @@ -1596,28 +1597,33 @@ private object AnalyzerRun { addFailure(t) } - private def tryComplete(): Unit = { - /* Note that after _allowComplete is true and pending == 0, we are sure - * that no new task will be submitted concurrently: - * - _allowComplete guarantees us that no external task will be added anymore - * - pending == 0 guarantees us that no internal task (which might create - * more tasks) are running anymore. + private def decrementPending(): Unit = { + /* When `pending` reaches 0, we are sure that all started tasks have + * completed, and that `allowComplete()` was called. Therefore, no + * further task can be concurrently added, and we are done. */ - if (_allowComplete && pending.get() == 0) { - failures.get() match { - case Nil => - promise.success(()) - case firstFailure :: moreFailures => - for (t <- moreFailures) - firstFailure.addSuppressed(t) - promise.failure(firstFailure) - } + if (pending.decrementAndGet() == 0) + complete() + } + + private def complete(): Unit = { + failures.get() match { + case Nil => + promise.success(()) + case firstFailure :: moreFailures => + for (t <- moreFailures) + firstFailure.addSuppressed(t) + promise.failure(firstFailure) } } + /** Signals that no new top-level tasks will be started, and that it is + * therefore OK to complete the tracker once all ongoing tasks have finished. + * + * `allowComplete()` must not be called more than once. + */ def allowComplete(): Future[Unit] = { - _allowComplete = true - tryComplete() + decrementPending() promise.future } } From 18e1862b09bf82a2f621e4ae274aab8baf823abd Mon Sep 17 00:00:00 2001 From: Rikito Taniguchi Date: Sat, 2 Mar 2024 01:36:52 +0900 Subject: [PATCH 052/298] Fix #4957: Fix error message for ConflictingTopLevelExport fix https://github.com/scala-js/scala-js/issues/4957 --- .../src/main/scala/org/scalajs/linker/analyzer/Analysis.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1ad5f24785..6d0a02e83c 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 @@ -242,7 +242,7 @@ object Analysis { "is not a valid JavaScript identifier " + "(did you want to emit a module instead?)" case ConflictingTopLevelExport(moduleID, exportName, infos) => - s"Conflicting top level exports for module $moduleID, name $exportName " + s"Conflicting top level exports for module $moduleID, name $exportName " + "involving " + infos.map(_.owningClass.nameString).mkString(", ") case ImportWithoutModuleSupport(module, info, None, _) => s"${info.displayName} needs to be imported from module " + From 44aacace31712d2abc558458db282ef7b1ef820a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 26 Feb 2024 10:03:06 +0100 Subject: [PATCH 053/298] Introduce `javascript.Trees.DelayedIdent`. We introduce a new kind of node in JS ASTs: `DelayedIdent`. A delayed ident is like an `Ident`, but its `name` is provided by a resolver, to be determined later. This allows us to build a JS AST with `DelayedIdent`s whose final names will only be known later. Since pretty-printing requires to resolve the name, it might throw and is not so well suited to `show` for debugging purposes anymore. We therefore introduce `JSTreeShowPrinter`, which avoids resolving the names. Instead, it uses the `debugString` method of the resolver, which can be constructed to display meaningful debugging information. `DelayedIdent` is not yet actually used in this commit, but will be in a subsequent commit for minifying property names. --- .../closure/ClosureAstTransformer.scala | 16 ++--- .../linker/backend/emitter/JSGen.scala | 6 +- .../linker/backend/javascript/Printers.scala | 33 +++++++++- .../linker/backend/javascript/Trees.scala | 61 ++++++++++++++++--- .../backend/javascript/PrintersTest.scala | 37 ++++++++++- 5 files changed, 127 insertions(+), 26 deletions(-) diff --git a/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureAstTransformer.scala b/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureAstTransformer.scala index 79ad4562ec..cad0fd9434 100644 --- a/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureAstTransformer.scala +++ b/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureAstTransformer.scala @@ -210,9 +210,9 @@ private class ClosureAstTransformer(featureSet: FeatureSet, private def transformClassMember(member: Tree): Node = { implicit val pos = member.pos - def newFixedPropNode(token: Token, static: Boolean, name: Ident, + def newFixedPropNode(token: Token, static: Boolean, name: MaybeDelayedIdent, function: Node): Node = { - val node = Node.newString(token, name.name) + val node = Node.newString(token, name.resolveName()) node.addChildToBack(function) node.setStaticMember(static) node @@ -258,7 +258,7 @@ private class ClosureAstTransformer(featureSet: FeatureSet, val node = newComputedPropNode(static, nameExpr, function) node.putBooleanProp(Node.COMPUTED_PROP_METHOD, true) node - case name: Ident => + case name: MaybeDelayedIdent => newFixedPropNode(Token.MEMBER_FUNCTION_DEF, static, name, function) } @@ -274,7 +274,7 @@ private class ClosureAstTransformer(featureSet: FeatureSet, val node = newComputedPropNode(static, nameExpr, function) node.putBooleanProp(Node.COMPUTED_PROP_GETTER, true) node - case name: Ident => + case name: MaybeDelayedIdent => newFixedPropNode(Token.GETTER_DEF, static, name, function) } @@ -290,7 +290,7 @@ private class ClosureAstTransformer(featureSet: FeatureSet, val node = newComputedPropNode(static, nameExpr, function) node.putBooleanProp(Node.COMPUTED_PROP_SETTER, true) node - case name: Ident => + case name: MaybeDelayedIdent => newFixedPropNode(Token.SETTER_DEF, static, name, function) } @@ -321,7 +321,7 @@ private class ClosureAstTransformer(featureSet: FeatureSet, args.foreach(arg => node.addChildToBack(transformExpr(arg))) node case DotSelect(qualifier, item) => - val node = Node.newString(Token.GETPROP, item.name) + val node = Node.newString(Token.GETPROP, item.resolveName()) node.addChildToBack(transformExpr(qualifier)) setNodePosition(node, item.pos.orElse(pos)) case BracketSelect(qualifier, item) => @@ -435,8 +435,8 @@ private class ClosureAstTransformer(featureSet: FeatureSet, val transformedValue = transformExpr(value) val node = name match { - case Ident(name, _) => - Node.newString(Token.STRING_KEY, name) + case name: MaybeDelayedIdent => + Node.newString(Token.STRING_KEY, name.resolveName()) case StringLiteral(name) => val node = Node.newString(Token.STRING_KEY, name) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/JSGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/JSGen.scala index 9d0150c2c9..4da09323c5 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/JSGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/JSGen.scala @@ -148,9 +148,9 @@ private[emitter] final class JSGen(val config: Emitter.Config) { def genPropSelect(qual: Tree, item: PropertyName)( implicit pos: Position): Tree = { item match { - case item: Ident => DotSelect(qual, item) - case item: StringLiteral => genBracketSelect(qual, item) - case ComputedName(tree) => genBracketSelect(qual, tree) + case item: MaybeDelayedIdent => DotSelect(qual, item) + case item: StringLiteral => genBracketSelect(qual, item) + case ComputedName(tree) => genBracketSelect(qual, tree) } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala index a6d632a1cd..2b99af7010 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala @@ -747,9 +747,13 @@ object Printers { protected def print(ident: Ident): Unit = printEscapeJS(ident.name) + protected def print(ident: DelayedIdent): Unit = + printEscapeJS(ident.resolveName()) + private final def print(propName: PropertyName): Unit = propName match { - case lit: StringLiteral => print(lit: Tree) - case ident: Ident => print(ident) + case lit: StringLiteral => print(lit: Tree) + case ident: Ident => print(ident) + case ident: DelayedIdent => print(ident) case ComputedName(tree) => print("[") @@ -799,6 +803,14 @@ object Printers { sourceMap.endNode(column) } + override protected def print(ident: DelayedIdent): Unit = { + if (ident.pos.isDefined) + sourceMap.startIdentNode(column, ident.pos, ident.originalName) + printEscapeJS(ident.resolveName()) + if (ident.pos.isDefined) + sourceMap.endNode(column) + } + override protected def print(printedTree: PrintedTree): Unit = { super.print(printedTree) sourceMap.insertFragment(printedTree.sourceMapFragment) @@ -830,4 +842,21 @@ object Printers { } } + /** Shows a `Tree` for debugging purposes, not for pretty-printing. */ + private[javascript] def showTree(tree: Tree): String = { + val writer = new ByteArrayWriter() + val printer = new Printers.JSTreeShowPrinter(writer) + printer.printTree(tree, isStat = true) + new String(writer.toByteArray(), StandardCharsets.US_ASCII) + } + + /** A printer that shows `Tree`s for debugging, not for pretty-printing. */ + private class JSTreeShowPrinter(_out: ByteArrayWriter, initIndent: Int = 0) + extends JSTreePrinter(_out, initIndent) { + override protected def print(ident: DelayedIdent): Unit = { + print("") + } + } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala index ec5b72e850..0c0b820e82 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala @@ -31,12 +31,7 @@ object Trees { abstract sealed class Tree { val pos: Position - def show: String = { - val writer = new ByteArrayWriter() - val printer = new Printers.JSTreePrinter(writer) - printer.printTree(this, isStat = true) - new String(writer.toByteArray(), StandardCharsets.US_ASCII) - } + def show: String = Printers.showTree(this) } // Constructor comment / annotation. @@ -50,16 +45,26 @@ object Trees { def pos: Position } + sealed trait MaybeDelayedIdent extends PropertyName { + def resolveName(): String + } + sealed case class Ident(name: String, originalName: OriginalName)( - implicit val pos: Position) extends PropertyName { - require(Ident.isValidJSIdentifierName(name), - s"'$name' is not a valid JS identifier name") + implicit val pos: Position) extends MaybeDelayedIdent { + Ident.requireValidJSIdentifierName(name) + + def resolveName(): String = name } object Ident { def apply(name: String)(implicit pos: Position): Ident = new Ident(name, NoOriginalName) + def requireValidJSIdentifierName(name: String): Unit = { + require(isValidJSIdentifierName(name), + s"'$name' is not a valid JS identifier name") + } + /** Tests whether the given string is a valid `IdentifierName` for the * ECMAScript language specification. * @@ -87,6 +92,42 @@ object Trees { } } + /** An ident whose real name will be resolved later. */ + sealed case class DelayedIdent(resolver: DelayedIdent.Resolver, originalName: OriginalName)( + implicit val pos: Position) + extends MaybeDelayedIdent { + + def resolveName(): String = { + val name = resolver.resolve() + Ident.requireValidJSIdentifierName(name) + name + } + } + + object DelayedIdent { + def apply(resolver: DelayedIdent.Resolver)(implicit pos: Position): DelayedIdent = + new DelayedIdent(resolver, NoOriginalName) + + /** Resolver for the eventual name of a `DelayedIdent`. */ + trait Resolver { + /** Resolves the eventual name of the delayed ident. + * + * @throws java.lang.IllegalStateException + * if this resolver is not yet ready to resolve a name + */ + def resolve(): String + + /** A string representing this resolver for debugging purposes. + * + * The result of this method is used when calling `show` on the + * associated `DelayedIdent`. Once the resolver is ready, this method is + * encouraged to return the same string as `resolve()`, but it is not + * mandatory to do so. + */ + def debugString: String + } + } + sealed case class ComputedName(tree: Tree) extends PropertyName { def pos: Position = tree.pos } @@ -231,7 +272,7 @@ object Trees { implicit val pos: Position) extends Tree - sealed case class DotSelect(qualifier: Tree, item: Ident)( + sealed case class DotSelect(qualifier: Tree, item: MaybeDelayedIdent)( implicit val pos: Position) extends Tree diff --git a/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala index ba4848f668..a2a8108fa8 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala @@ -30,12 +30,16 @@ class PrintersTest { private implicit def str2ident(name: String): Ident = Ident(name, ir.OriginalName.NoOriginalName) - private def assertPrintEquals(expected: String, tree: Tree): Unit = { + private def printTree(tree: Tree): String = { val out = new ByteArrayWriter val printer = new Printers.JSTreePrinter(out) printer.printStat(tree) - assertEquals(expected.stripMargin.trim + "\n", - new String(out.toByteArray(), UTF_8)) + new String(out.toByteArray(), UTF_8) + } + + private def assertPrintEquals(expected: String, tree: Tree): Unit = { + val printResult = printTree(tree) + assertEquals(expected.stripMargin.trim + "\n", printResult) } @Test def printFunctionDef(): Unit = { @@ -159,6 +163,33 @@ class PrintersTest { ) } + @Test def delayedIdentPrintVersusShow(): Unit = { + locally { + object resolver extends DelayedIdent.Resolver { + def resolve(): String = "foo" + def debugString: String = "bar" + } + + val tree = DotSelect(VarRef("x"), DelayedIdent(resolver)) + + assertPrintEquals("x.foo;", tree) + assertEquals("x.;", tree.show) + } + + // Even when `resolve()` throws, `show` still succeeds based on `debugString`. + locally { + object resolver extends DelayedIdent.Resolver { + def resolve(): String = throw new IllegalStateException("not ready") + def debugString: String = "bar" + } + + val tree = DotSelect(VarRef("x"), DelayedIdent(resolver)) + + assertThrows(classOf[IllegalStateException], () => printTree(tree)) + assertEquals("x.;", tree.show) + } + } + @Test def showPrintedTree(): Unit = { val tree = PrintedTree("test".getBytes(UTF_8), SourceMapWriter.Fragment.Empty) From 298c60a0b5817a04ffd507e6ea0a793beb0a0486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 26 Feb 2024 10:33:22 +0100 Subject: [PATCH 054/298] Make `JSTreePrinter.printTree` protected. The only remaining public method is now `printStat(tree: Tree)`. --- .../org/scalajs/linker/backend/javascript/Printers.scala | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala index 2b99af7010..09a3b8648a 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala @@ -139,7 +139,7 @@ object Printers { * - No leading indent. * - No trailing newline. */ - def printTree(tree: Tree, isStat: Boolean): Unit = { + protected def printTree(tree: Tree, isStat: Boolean): Unit = { def printSeparatorIfStat() = { if (isStat) print(';') @@ -781,7 +781,7 @@ object Printers { private var column = 0 - override def printTree(tree: Tree, isStat: Boolean): Unit = { + override protected def printTree(tree: Tree, isStat: Boolean): Unit = { val pos = tree.pos if (pos.isDefined) sourceMap.startNode(column, pos) @@ -846,13 +846,16 @@ object Printers { private[javascript] def showTree(tree: Tree): String = { val writer = new ByteArrayWriter() val printer = new Printers.JSTreeShowPrinter(writer) - printer.printTree(tree, isStat = true) + printer.printTreeForShow(tree) new String(writer.toByteArray(), StandardCharsets.US_ASCII) } /** A printer that shows `Tree`s for debugging, not for pretty-printing. */ private class JSTreeShowPrinter(_out: ByteArrayWriter, initIndent: Int = 0) extends JSTreePrinter(_out, initIndent) { + def printTreeForShow(tree: Tree): Unit = + printTree(tree, isStat = true) + override protected def print(ident: DelayedIdent): Unit = { print(" Date: Mon, 26 Feb 2024 11:56:16 +0100 Subject: [PATCH 055/298] Refactoring: Restrict responsibility of gen member idents to SJSGen. Previously, `SJSGen` generated `Ident`s for field members, but only names for method members. Generating the `Ident`s for methods was left in `ClassEmitter` and `Function`. Now, we concentrate that responsibility in `SJSGen` only. In addition, we make a clear distinction between idents generated for *definitions*, which receive an `OriginalName`, and those used for *use sites*, which never receive one. --- .../linker/backend/emitter/ClassEmitter.scala | 23 +++++-------------- .../linker/backend/emitter/CoreJSLib.scala | 8 ++++--- .../backend/emitter/FunctionEmitter.scala | 7 ++---- .../linker/backend/emitter/SJSGen.scala | 23 +++++++++++++++---- 4 files changed, 32 insertions(+), 29 deletions(-) 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 211b335e29..e0154b1782 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 @@ -355,7 +355,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } yield { val field = anyField.asInstanceOf[FieldDef] implicit val pos = field.pos - js.Assign(genSelect(js.This(), field.name, field.originalName), + js.Assign(genSelectForDef(js.This(), field.name, field.originalName), genZeroOf(field.ftpe)) } } @@ -422,8 +422,11 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val zero = genBoxedZeroOf(field.ftpe) field match { case FieldDef(_, name, originalName, _) => + /* TODO This seems to be dead code, which is somehow reassuring + * because I don't know what it is supposed to achieve. + */ WithGlobals( - js.Assign(js.DotSelect(classVarRef, genMemberFieldIdent(name, originalName)), zero)) + js.Assign(genSelectForDef(classVarRef, name, originalName), zero)) case JSFieldDef(_, name, _) => for (propName <- genMemberNameTree(name)) yield js.Assign(genPropSelect(classVarRef, propName), zero) @@ -472,7 +475,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { for { methodFun <- desugarToFunction(className, method.args, method.body.get, method.resultType) } yield { - val jsMethodName = genMemberMethodIdent(method.name, method.originalName) + val jsMethodName = genMethodIdentForDef(method.name, method.originalName) if (useESClass) { js.MethodDef(static = false, jsMethodName, methodFun.args, methodFun.restParam, methodFun.body) @@ -659,20 +662,6 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } } - private def genMemberFieldIdent(ident: FieldIdent, - originalName: OriginalName): js.Ident = { - val jsName = genName(ident.name) - js.Ident(jsName, genOriginalName(ident.name, originalName, jsName))( - ident.pos) - } - - private def genMemberMethodIdent(ident: MethodIdent, - originalName: OriginalName): js.Ident = { - val jsName = genMethodName(ident.name) - js.Ident(jsName, genOriginalName(ident.name, originalName, jsName))( - ident.pos) - } - def needInstanceTests(tree: LinkedClass)( implicit globalKnowledge: GlobalKnowledge): Boolean = { tree.hasInstanceTests || (tree.hasRuntimeTypeInfo && 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 89e4ad86df..3fdefbbfb5 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 @@ -877,7 +877,7 @@ private[emitter] object CoreJSLib { if (implementedInObject) { val staticObjectCall: Tree = { - val fun = globalVar(VarField.c, ObjectClass).prototype DOT genMethodName(methodName) + val fun = globalVar(VarField.c, ObjectClass).prototype DOT genMethodIdent(methodName) Return(Apply(fun DOT "call", instance :: args)) } @@ -1504,7 +1504,8 @@ private[emitter] object CoreJSLib { Nil } - val clone = MethodDef(static = false, Ident(genMethodName(cloneMethodName)), Nil, None, { + val cloneMethodIdent = genMethodIdentForDef(cloneMethodName, NoOriginalName) + val clone = MethodDef(static = false, cloneMethodIdent, Nil, None, { Return(New(ArrayClass, Apply(genIdentBracketSelect(This().u, "slice"), Nil) :: Nil)) }) @@ -1809,7 +1810,8 @@ private[emitter] object CoreJSLib { Nil } - val clone = MethodDef(static = false, Ident(genMethodName(cloneMethodName)), Nil, None, { + val cloneMethodIdent = genMethodIdentForDef(cloneMethodName, NoOriginalName) + val clone = MethodDef(static = false, cloneMethodIdent, Nil, None, { Return(New(ArrayClass, Apply(genIdentBracketSelect(This().u, "slice"), Nil) :: Nil)) }) 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 75b4bdeb61..95cf6e1c33 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 @@ -2252,7 +2252,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { val newArgs = transformTypedArgs(method.name, args) def genNormalApply(): js.Tree = - js.Apply(newReceiver(false) DOT transformMethodIdent(method), newArgs) + js.Apply(newReceiver(false) DOT genMethodIdent(method), newArgs) def genDispatchApply(): js.Tree = js.Apply(globalVar(VarField.dp, methodName), newReceiver(false) :: newArgs) @@ -2315,7 +2315,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { genApplyStaticLike(VarField.f, className, method, transformedArgs) } else { val fun = - globalVar(VarField.c, className).prototype DOT transformMethodIdent(method) + globalVar(VarField.c, className).prototype DOT genMethodIdent(method) js.Apply(fun DOT "call", transformedArgs) } @@ -3207,9 +3207,6 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { private def transformLabelIdent(ident: LabelIdent): js.Ident = js.Ident(genName(ident.name))(ident.pos) - private def transformMethodIdent(ident: MethodIdent): js.Ident = - js.Ident(genMethodName(ident.name))(ident.pos) - private def transformLocalVarIdent(ident: LocalIdent): js.Ident = js.Ident(transformLocalName(ident.name))(ident.pos) 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 e7ea19a9db..615b1e94a0 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 @@ -154,7 +154,7 @@ private[emitter] final class SJSGen( DotSelect(receiver, Ident(genName(field.name))(field.pos)) } - def genSelect(receiver: Tree, field: irt.FieldIdent, + def genSelectForDef(receiver: Tree, field: irt.FieldIdent, originalName: OriginalName)( implicit pos: Position): Tree = { val jsName = genName(field.name) @@ -164,7 +164,7 @@ private[emitter] final class SJSGen( def genApply(receiver: Tree, methodName: MethodName, args: List[Tree])( implicit pos: Position): Tree = { - Apply(DotSelect(receiver, Ident(genMethodName(methodName))), args) + Apply(DotSelect(receiver, genMethodIdent(methodName)), args) } def genApply(receiver: Tree, methodName: MethodName, args: Tree*)( @@ -172,8 +172,23 @@ private[emitter] final class SJSGen( genApply(receiver, methodName, args.toList) } - def genMethodName(methodName: MethodName): String = - genName(methodName) + def genMethodIdent(methodIdent: irt.MethodIdent): Ident = + genMethodIdent(methodIdent.name)(methodIdent.pos) + + def genMethodIdentForDef(methodIdent: irt.MethodIdent, + originalName: OriginalName): Ident = { + genMethodIdentForDef(methodIdent.name, originalName)(methodIdent.pos) + } + + def genMethodIdent(methodName: MethodName)(implicit pos: Position): Ident = + Ident(genName(methodName)) + + def genMethodIdentForDef(methodName: MethodName, originalName: OriginalName)( + implicit pos: Position): Ident = { + val jsName = genName(methodName) + val jsOrigName = genOriginalName(methodName, originalName, jsName) + Ident(jsName, jsOrigName) + } def genJSPrivateSelect(receiver: Tree, field: irt.FieldIdent)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, From 41b7b9c8751087602a197fc8ad591c74809e2f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 24 Jan 2024 14:37:59 +0100 Subject: [PATCH 056/298] Minify property names ourselves in fullLink when we don't use GCC. When emitting ES modules, we cannot use Closure because it does not support ES modules the way we need it to. This results in files that are much larger than with other module kinds. Off-the-shelf JavaScript bundlers/minifier can compensate for that to a large extent for local and file-local variables, but they do not have enough semantic information to do it on property names. We therefore add our own property name compressor. When enabled, the emitter computes the frequency of every field and method name in the entire program. It then uses those frequencies to allocate short names to them, with the shortest ones allocated to the most used properties. In order to compute the frequencies, we count how many times `genMethodName` is called for any particular `MethodName` (same for other kinds of names) during JS AST generation. That means that while we generate the JS AST, we do not know the final frequencies, and therefore the eventually allocated names. We use `DelayedIdent`s to defer the actual resolution until after the frequencies are computed. Obviously, this breaks any sort of incremental behavior. Since we do not cache the frequency counts per calling method, we have to force re-generation of the whole AST at each run to re-count. Therefore, we invalidate all the emitter caches on every run when the new minifier is enabled. This should not be a problem as it is only intended to be used for fullLink. An alternative would be to store the counts along with global refs in `WithGlobals`, but the overhead would then leak pretty strongly on incremental runs that do not minify. This strategy also prevents fusing AST generation and pretty-printing. When minifying, we demand that the `postTransformer` be `PostTransformer.Identity`. This adds a bit of handling to `BasicLinkerBackend` to deal with the two possible kinds of trees received from the emitter, but nothing too invasive. We automatically enable the new minifier under fullLink when GCC is disabled. This can be overridden with a `scalaJSLinkerConfig` setting. --- Jenkinsfile | 110 +++++---- .../linker/interface/StandardConfig.scala | 22 ++ .../closure/ClosureLinkerBackend.scala | 15 +- .../linker/backend/BasicLinkerBackend.scala | 85 +++++-- .../linker/backend/LinkerBackendImpl.scala | 21 +- .../backend/emitter/ArrayClassProperty.scala | 46 ++++ .../linker/backend/emitter/CoreJSLib.scala | 37 ++- .../linker/backend/emitter/Emitter.scala | 47 +++- .../backend/emitter/FunctionEmitter.scala | 22 +- .../backend/emitter/NameCompressor.scala | 230 ++++++++++++++++++ .../linker/backend/emitter/NameGen.scala | 4 +- .../linker/backend/emitter/SJSGen.scala | 86 ++++++- .../linker/backend/emitter/TreeDSL.scala | 3 +- .../standard/StandardLinkerBackend.scala | 1 + .../org/scalajs/linker/EmitterTest.scala | 2 +- .../org/scalajs/linker/LibrarySizeTest.scala | 3 +- .../backend/emitter/NameCompressorTest.scala | 53 ++++ project/Build.scala | 81 ++++-- .../sbtplugin/ScalaJSPluginInternal.scala | 1 + .../scalajs/testsuite/utils/BuildInfo.scala | 3 +- .../scalajs/testsuite/utils/Platform.scala | 5 +- .../testsuite/compiler/OptimizerTest.scala | 9 +- .../testsuite/jsinterop/MiscInteropTest.scala | 4 +- .../testsuite/library/StackTraceTest.scala | 2 +- .../scalajs/testsuite/utils/Platform.scala | 4 +- .../scalajs/testsuite/compiler/LongTest.scala | 6 +- .../compiler/ReflectiveCallTest.scala | 2 +- 27 files changed, 758 insertions(+), 146 deletions(-) create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ArrayClassProperty.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameCompressor.scala create mode 100644 linker/shared/src/test/scala/org/scalajs/linker/backend/emitter/NameCompressorTest.scala diff --git a/Jenkinsfile b/Jenkinsfile index 158b73cc4e..1c9dc60c29 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -185,6 +185,9 @@ def Tasks = [ reversi$v/fastLinkJS \ reversi$v/fullLinkJS \ reversi$v/checksizes && + sbtretry ++$scala \ + 'set Global/enableMinifyEverywhere := true' \ + reversi$v/checksizes && sbtretry ++$scala javalibintf/compile:doc compiler$v/compile:doc library$v/compile:doc \ testInterface$v/compile:doc testBridge$v/compile:doc && sbtretry ++$scala headerCheck && @@ -199,68 +202,84 @@ def Tasks = [ "test-suite-default-esversion": ''' setJavaVersion $java npm install && - sbtretry ++$scala jUnitTestOutputsJVM$v/test jUnitTestOutputsJS$v/test testBridge$v/test \ + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + jUnitTestOutputsJVM$v/test jUnitTestOutputsJS$v/test testBridge$v/test \ 'set scalaJSStage in Global := FullOptStage' jUnitTestOutputsJS$v/test testBridge$v/test && - sbtretry ++$scala $testSuite$v/test $testSuite$v/testHtmlJSDom && - sbtretry 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test \ + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + $testSuite$v/test $testSuite$v/testHtmlJSDom && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSStage in Global := FullOptStage' \ + $testSuite$v/test \ $testSuite$v/testHtmlJSDom && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= { _.withSemantics(_.withStrictFloats(false)) }' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withAllowBigIntsForLongs(true)))' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withAllowBigIntsForLongs(true)).withOptimizer(false))' \ - ++$scala $testSuite$v/test && - sbtretry \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withAllowBigIntsForLongs(true)))' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withAllowBigIntsForLongs(true)).withOptimizer(false))' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withAvoidLetsAndConsts(false).withAvoidClasses(false)))' \ - ++$scala $testSuite$v/test && - sbtretry \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withESFeatures(_.withAvoidLetsAndConsts(false).withAvoidClasses(false)))' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.CommonJSModule))' \ - ++$scala $testSuite$v/test && - sbtretry \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.CommonJSModule))' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.CommonJSModule))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleSplitStyle(ModuleSplitStyle.SmallestModules))' \ - ++$scala $testSuite$v/test && - sbtretry 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.CommonJSModule))' \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.CommonJSModule))' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test && - sbtretry \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.ESModule))' \ - ++$scala $testSuite$v/test && - sbtretry \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleSplitStyle(ModuleSplitStyle.SmallestModules))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.ESModule))' \ - ++$scala $testSuite$v/test && - sbtretry \ + $testSuite$v/test && + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleSplitStyle(ModuleSplitStyle.SmallModulesFor(List("org.scalajs.testsuite"))))' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.ESModule))' \ - ++$scala $testSuite$v/test && - sbtretry \ + $testSuite$v/test && + # The following tests the same thing whether testMinify is true or false; we also set it for regularity. + sbtretry ++$scala 'set Global/enableMinifyEverywhere := $testMinify' \ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withModuleKind(ModuleKind.ESModule))' \ 'set scalaJSStage in Global := FullOptStage' \ - ++$scala $testSuite$v/test + $testSuite$v/test ''', "test-suite-custom-esversion-force-polyfills": ''' @@ -504,9 +523,10 @@ mainScalaVersions.each { scalaVersion -> quickMatrix.add([task: "main", scala: scalaVersion, java: javaVersion]) quickMatrix.add([task: "tools", scala: scalaVersion, java: javaVersion]) } - quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: mainJavaVersion, testSuite: "testSuite"]) + quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: mainJavaVersion, testMinify: "false", testSuite: "testSuite"]) + quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: mainJavaVersion, testMinify: "true", testSuite: "testSuite"]) quickMatrix.add([task: "test-suite-custom-esversion", scala: scalaVersion, java: mainJavaVersion, esVersion: "ES5_1", testSuite: "testSuite"]) - quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: mainJavaVersion, testSuite: "scalaTestSuite"]) + quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: mainJavaVersion, testMinify: "false", testSuite: "scalaTestSuite"]) quickMatrix.add([task: "test-suite-custom-esversion", scala: scalaVersion, java: mainJavaVersion, esVersion: "ES5_1", testSuite: "scalaTestSuite"]) quickMatrix.add([task: "bootstrap", scala: scalaVersion, java: mainJavaVersion]) quickMatrix.add([task: "partest-fastopt", scala: scalaVersion, java: mainJavaVersion]) @@ -527,7 +547,7 @@ otherScalaVersions.each { scalaVersion -> } mainScalaVersions.each { scalaVersion -> otherJavaVersions.each { javaVersion -> - quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: javaVersion, testSuite: "testSuite"]) + quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: javaVersion, testMinify: "false", testSuite: "testSuite"]) } fullMatrix.add([task: "partest-noopt", scala: scalaVersion, java: mainJavaVersion]) fullMatrix.add([task: "partest-fullopt", scala: scalaVersion, java: mainJavaVersion]) diff --git a/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/StandardConfig.scala b/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/StandardConfig.scala index d867c1d1bf..40644b5b9f 100644 --- a/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/StandardConfig.scala +++ b/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/StandardConfig.scala @@ -46,6 +46,19 @@ final class StandardConfig private ( val relativizeSourceMapBase: Option[URI], /** Name patterns for output. */ val outputPatterns: OutputPatterns, + /** Apply Scala.js-specific minification of the produced .js files. + * + * When enabled, the linker more aggressively reduces the size of the + * generated code, at the cost of readability and debuggability. It does + * not perform size optimizations that would negatively impact run-time + * performance. + * + * The focus is on optimizations that general-purpose JavaScript minifiers + * cannot do on their own. For the best results, we expect the Scala.js + * minifier to be used in conjunction with a general-purpose JavaScript + * minifier. + */ + val minify: Boolean, /** Whether to use the Google Closure Compiler pass, if it is available. * On the JavaScript platform, this does not have any effect. */ @@ -80,6 +93,7 @@ final class StandardConfig private ( sourceMap = true, relativizeSourceMapBase = None, outputPatterns = OutputPatterns.Defaults, + minify = false, closureCompilerIfAvailable = false, prettyPrint = false, batchMode = false, @@ -148,6 +162,9 @@ final class StandardConfig private ( def withOutputPatterns(f: OutputPatterns => OutputPatterns): StandardConfig = copy(outputPatterns = f(outputPatterns)) + def withMinify(minify: Boolean): StandardConfig = + copy(minify = minify) + def withClosureCompilerIfAvailable(closureCompilerIfAvailable: Boolean): StandardConfig = copy(closureCompilerIfAvailable = closureCompilerIfAvailable) @@ -173,6 +190,7 @@ final class StandardConfig private ( | sourceMap = $sourceMap, | relativizeSourceMapBase = $relativizeSourceMapBase, | outputPatterns = $outputPatterns, + | minify = $minify, | closureCompilerIfAvailable = $closureCompilerIfAvailable, | prettyPrint = $prettyPrint, | batchMode = $batchMode, @@ -192,6 +210,7 @@ final class StandardConfig private ( sourceMap: Boolean = sourceMap, outputPatterns: OutputPatterns = outputPatterns, relativizeSourceMapBase: Option[URI] = relativizeSourceMapBase, + minify: Boolean = minify, closureCompilerIfAvailable: Boolean = closureCompilerIfAvailable, prettyPrint: Boolean = prettyPrint, batchMode: Boolean = batchMode, @@ -209,6 +228,7 @@ final class StandardConfig private ( sourceMap, relativizeSourceMapBase, outputPatterns, + minify, closureCompilerIfAvailable, prettyPrint, batchMode, @@ -237,6 +257,7 @@ object StandardConfig { .addField("relativizeSourceMapBase", config.relativizeSourceMapBase.map(_.toASCIIString())) .addField("outputPatterns", config.outputPatterns) + .addField("minify", config.minify) .addField("closureCompilerIfAvailable", config.closureCompilerIfAvailable) .addField("prettyPrint", config.prettyPrint) @@ -264,6 +285,7 @@ object StandardConfig { * - `sourceMap`: `true` * - `relativizeSourceMapBase`: `None` * - `outputPatterns`: [[OutputPatterns.Defaults]] + * - `minify`: `false` * - `closureCompilerIfAvailable`: `false` * - `prettyPrint`: `false` * - `batchMode`: `false` diff --git a/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureLinkerBackend.scala b/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureLinkerBackend.scala index 64160204ac..7532e0be47 100644 --- a/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureLinkerBackend.scala +++ b/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureLinkerBackend.scala @@ -54,13 +54,19 @@ final class ClosureLinkerBackend(config: LinkerBackendImpl.Config) s"Cannot use module kind $moduleKind with the Closure Compiler") private[this] val emitter = { + // Note that we do not transfer `minify` -- Closure will do its own thing anyway val emitterConfig = Emitter.Config(config.commonConfig.coreSpec) .withJSHeader(config.jsHeader) .withOptimizeBracketSelects(false) .withTrackAllGlobalRefs(true) .withInternalModulePattern(m => OutputPatternsImpl.moduleName(config.outputPatterns, m.id)) - new Emitter(emitterConfig, ClosureLinkerBackend.PostTransformer) + // Do not apply ClosureAstTransformer eagerly: + // The ASTs used by closure are highly mutable, so re-using them is non-trivial. + // Since closure is slow anyways, we haven't built the optimization. + val postTransformer = Emitter.PostTransformer.Identity + + new Emitter(emitterConfig, postTransformer) } val symbolRequirements: SymbolRequirement = emitter.symbolRequirements @@ -296,11 +302,4 @@ private object ClosureLinkerBackend { Function.prototype.apply; var NaN = 0.0/0.0, Infinity = 1.0/0.0, undefined = void 0; """ - - private object PostTransformer extends Emitter.PostTransformer[js.Tree] { - // Do not apply ClosureAstTransformer eagerly: - // The ASTs used by closure are highly mutable, so re-using them is non-trivial. - // Since closure is slow anyways, we haven't built the optimization. - def transformStats(trees: List[js.Tree], indent: Int): List[js.Tree] = trees - } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala index e07e31597b..fa7e616880 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala @@ -41,16 +41,19 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) private[this] var totalModules = 0 private[this] val rewrittenModules = new AtomicInteger(0) - private[this] val emitter = { + private[this] val bodyPrinter: BodyPrinter = { + if (config.minify) IdentityPostTransformerBasedBodyPrinter + else if (config.sourceMap) PrintedTreeWithSourceMapBodyPrinter + else PrintedTreeWithoutSourceMapBodyPrinter + } + + private[this] val emitter: Emitter[bodyPrinter.TreeType] = { val emitterConfig = Emitter.Config(config.commonConfig.coreSpec) .withJSHeader(config.jsHeader) .withInternalModulePattern(m => OutputPatternsImpl.moduleName(config.outputPatterns, m.id)) + .withMinify(config.minify) - val postTransformer = - if (config.sourceMap) PostTransformerWithSourceMap - else PostTransformerWithoutSourceMap - - new Emitter(emitterConfig, postTransformer) + new Emitter(emitterConfig, bodyPrinter.postTransformer) } val symbolRequirements: SymbolRequirement = emitter.symbolRequirements @@ -82,13 +85,14 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) val skipContentCheck = !isFirstRun isFirstRun = false - val allChanged = + val allChanged0 = printedModuleSetCache.updateGlobal(emitterResult.header, emitterResult.footer) + val allChanged = allChanged0 || config.minify val writer = new OutputWriter(output, config, skipContentCheck) { protected def writeModuleWithoutSourceMap(moduleID: ModuleID, force: Boolean): Option[ByteBuffer] = { val cache = printedModuleSetCache.getModuleCache(moduleID) - val (printedTrees, changed) = emitterResult.body(moduleID) + val (trees, changed) = emitterResult.body(moduleID) if (force || changed || allChanged) { rewrittenModules.incrementAndGet() @@ -98,8 +102,7 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) jsFileWriter.write(printedModuleSetCache.headerBytes) jsFileWriter.writeASCIIString("'use strict';\n") - for (printedTree <- printedTrees) - jsFileWriter.write(printedTree.jsCode) + bodyPrinter.printWithoutSourceMap(trees, jsFileWriter) jsFileWriter.write(printedModuleSetCache.footerBytes) @@ -112,7 +115,7 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) protected def writeModuleWithSourceMap(moduleID: ModuleID, force: Boolean): Option[(ByteBuffer, ByteBuffer)] = { val cache = printedModuleSetCache.getModuleCache(moduleID) - val (printedTrees, changed) = emitterResult.body(moduleID) + val (trees, changed) = emitterResult.body(moduleID) if (force || changed || allChanged) { rewrittenModules.incrementAndGet() @@ -133,10 +136,7 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) jsFileWriter.writeASCIIString("'use strict';\n") smWriter.nextLine() - for (printedTree <- printedTrees) { - jsFileWriter.write(printedTree.jsCode) - smWriter.insertFragment(printedTree.sourceMapFragment) - } + bodyPrinter.printWithSourceMap(trees, jsFileWriter, smWriter) jsFileWriter.write(printedModuleSetCache.footerBytes) jsFileWriter.write(("//# sourceMappingURL=" + sourceMapURI + "\n").getBytes(StandardCharsets.UTF_8)) @@ -240,6 +240,57 @@ private object BasicLinkerBackend { } } + private abstract class BodyPrinter { + type TreeType >: Null <: js.Tree + + val postTransformer: Emitter.PostTransformer[TreeType] + + def printWithoutSourceMap(trees: List[TreeType], jsFileWriter: ByteArrayWriter): Unit + def printWithSourceMap(trees: List[TreeType], jsFileWriter: ByteArrayWriter, smWriter: SourceMapWriter): Unit + } + + private object IdentityPostTransformerBasedBodyPrinter extends BodyPrinter { + type TreeType = js.Tree + + val postTransformer: Emitter.PostTransformer[TreeType] = Emitter.PostTransformer.Identity + + def printWithoutSourceMap(trees: List[TreeType], jsFileWriter: ByteArrayWriter): Unit = { + val printer = new Printers.JSTreePrinter(jsFileWriter) + for (tree <- trees) + printer.printStat(tree) + } + + def printWithSourceMap(trees: List[TreeType], jsFileWriter: ByteArrayWriter, smWriter: SourceMapWriter): Unit = { + val printer = new Printers.JSTreePrinterWithSourceMap(jsFileWriter, smWriter, initIndent = 0) + for (tree <- trees) + printer.printStat(tree) + } + } + + private abstract class PrintedTreeBasedBodyPrinter( + val postTransformer: Emitter.PostTransformer[js.PrintedTree] + ) extends BodyPrinter { + type TreeType = js.PrintedTree + + def printWithoutSourceMap(trees: List[TreeType], jsFileWriter: ByteArrayWriter): Unit = { + for (tree <- trees) + jsFileWriter.write(tree.jsCode) + } + + def printWithSourceMap(trees: List[TreeType], jsFileWriter: ByteArrayWriter, smWriter: SourceMapWriter): Unit = { + for (tree <- trees) { + jsFileWriter.write(tree.jsCode) + smWriter.insertFragment(tree.sourceMapFragment) + } + } + } + + private object PrintedTreeWithoutSourceMapBodyPrinter + extends PrintedTreeBasedBodyPrinter(PostTransformerWithoutSourceMap) + + private object PrintedTreeWithSourceMapBodyPrinter + extends PrintedTreeBasedBodyPrinter(PostTransformerWithSourceMap) + private object PostTransformerWithoutSourceMap extends Emitter.PostTransformer[js.PrintedTree] { def transformStats(trees: List[js.Tree], indent: Int): List[js.PrintedTree] = { if (trees.isEmpty) { @@ -248,7 +299,7 @@ private object BasicLinkerBackend { val jsCodeWriter = new ByteArrayWriter() val printer = new Printers.JSTreePrinter(jsCodeWriter, indent) - trees.map(printer.printStat(_)) + trees.foreach(printer.printStat(_)) js.PrintedTree(jsCodeWriter.toByteArray(), SourceMapWriter.Fragment.Empty) :: Nil } @@ -264,7 +315,7 @@ private object BasicLinkerBackend { val smFragmentBuilder = new SourceMapWriter.FragmentBuilder() val printer = new Printers.JSTreePrinterWithSourceMap(jsCodeWriter, smFragmentBuilder, indent) - trees.map(printer.printStat(_)) + trees.foreach(printer.printStat(_)) smFragmentBuilder.complete() js.PrintedTree(jsCodeWriter.toByteArray(), smFragmentBuilder.result()) :: Nil diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/LinkerBackendImpl.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/LinkerBackendImpl.scala index e1795d4493..0fc8f5169b 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/LinkerBackendImpl.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/LinkerBackendImpl.scala @@ -53,6 +53,8 @@ object LinkerBackendImpl { val outputPatterns: OutputPatterns, /** Base path to relativize paths in the source map. */ val relativizeSourceMapBase: Option[URI], + /** Whether to use Scala.js' minifier for property names. */ + val minify: Boolean, /** Whether to use the Google Closure Compiler pass, if it is available. * On the JavaScript platform, this does not have any effect. */ @@ -69,6 +71,7 @@ object LinkerBackendImpl { sourceMap = true, outputPatterns = OutputPatterns.Defaults, relativizeSourceMapBase = None, + minify = false, closureCompilerIfAvailable = false, prettyPrint = false, maxConcurrentWrites = 50) @@ -91,6 +94,9 @@ object LinkerBackendImpl { def withRelativizeSourceMapBase(relativizeSourceMapBase: Option[URI]): Config = copy(relativizeSourceMapBase = relativizeSourceMapBase) + def withMinify(minify: Boolean): Config = + copy(minify = minify) + def withClosureCompilerIfAvailable(closureCompilerIfAvailable: Boolean): Config = copy(closureCompilerIfAvailable = closureCompilerIfAvailable) @@ -106,12 +112,21 @@ object LinkerBackendImpl { sourceMap: Boolean = sourceMap, outputPatterns: OutputPatterns = outputPatterns, relativizeSourceMapBase: Option[URI] = relativizeSourceMapBase, + minify: Boolean = minify, closureCompilerIfAvailable: Boolean = closureCompilerIfAvailable, prettyPrint: Boolean = prettyPrint, maxConcurrentWrites: Int = maxConcurrentWrites): Config = { - new Config(commonConfig, jsHeader, sourceMap, outputPatterns, - relativizeSourceMapBase, closureCompilerIfAvailable, prettyPrint, - maxConcurrentWrites) + new Config( + commonConfig, + jsHeader, + sourceMap, + outputPatterns, + relativizeSourceMapBase, + minify, + closureCompilerIfAvailable, + prettyPrint, + maxConcurrentWrites + ) } } 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/ArrayClassProperty.scala new file mode 100644 index 0000000000..c33d0a99e7 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/ArrayClassProperty.scala @@ -0,0 +1,46 @@ +/* + * 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.backend.emitter + +import org.scalajs.ir.OriginalName + +/** Represents a property of one of the special `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] { + + val originalName: OriginalName = OriginalName(nonMinifiedName) + + def compareTo(that: ArrayClassProperty): Int = + this.nonMinifiedName.compareTo(that.nonMinifiedName) + + override def toString(): String = s"ArrayClassProperty($nonMinifiedName)" +} + +object ArrayClassProperty { + /** `ArrayClass.u`: the underlying array of typed array. */ + val u: ArrayClassProperty = new ArrayClassProperty("u") + + /** `ArrayClass.get()`: gets one element. */ + val get: ArrayClassProperty = new ArrayClassProperty("get") + + /** `ArrayClass.set()`: sets one element. */ + val set: ArrayClassProperty = new ArrayClassProperty("set") + + /** `ArrayClass.copyTo()`: copies from that array to another array. */ + val copyTo: ArrayClassProperty = new ArrayClassProperty("copyTo") +} 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 3fdefbbfb5..a96dea3018 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 @@ -1131,7 +1131,7 @@ private[emitter] object CoreJSLib { ) ::: condDefs(esVersion >= ESVersion.ES2015 && nullPointers != CheckedBehavior.Unchecked)( defineFunction5(VarField.systemArraycopy) { (src, srcPos, dest, destPos, length) => - Apply(src DOT "copyTo", List(srcPos, dest, destPos, length)) + genArrayClassPropApply(src, ArrayClassProperty.copyTo, srcPos, dest, destPos, length) } ) ::: @@ -1155,7 +1155,8 @@ private[emitter] object CoreJSLib { srcPos, dest.u.length, destPos, length) }, For(let(i, 0), i < length, i := ((i + 1) | 0), { - Apply(dest DOT "set", List((destPos + i) | 0, BracketSelect(srcArray, (srcPos + i) | 0))) + genArrayClassPropApply(dest, ArrayClassProperty.set, + (destPos + i) | 0, BracketSelect(srcArray, (srcPos + i) | 0)) }) ) }) @@ -1172,7 +1173,7 @@ private[emitter] object CoreJSLib { If(srcData && genIdentBracketSelect(srcData, "isArrayClass"), { // Fast path: the values are array of the same type if (esVersion >= ESVersion.ES2015 && nullPointers == CheckedBehavior.Unchecked) - Apply(src DOT "copyTo", List(srcPos, dest, destPos, length)) + genArrayClassPropApply(src, ArrayClassProperty.copyTo, srcPos, dest, destPos, length) else genCallHelper(VarField.systemArraycopy, src, srcPos, dest, destPos, length) }, { @@ -1444,14 +1445,17 @@ private[emitter] object CoreJSLib { genCallHelper(VarField.throwArrayIndexOutOfBoundsException, i)) } + val getName = genArrayClassPropertyForDef(ArrayClassProperty.get) + val setName = genArrayClassPropertyForDef(ArrayClassProperty.set) + List( - MethodDef(static = false, Ident("get"), paramList(i), None, { + MethodDef(static = false, getName, paramList(i), None, { Block( boundsCheck, Return(BracketSelect(This().u, i)) ) }), - MethodDef(static = false, Ident("set"), paramList(i, v), None, { + MethodDef(static = false, setName, paramList(i, v), None, { Block( boundsCheck, BracketSelect(This().u, i) := v @@ -1465,8 +1469,10 @@ private[emitter] object CoreJSLib { val i = varRef("i") val v = varRef("v") + val setName = genArrayClassPropertyForDef(ArrayClassProperty.set) + List( - MethodDef(static = false, Ident("set"), paramList(i, v), None, { + MethodDef(static = false, setName, paramList(i, v), None, { BracketSelect(This().u, i) := v }) ) @@ -1479,7 +1485,10 @@ private[emitter] object CoreJSLib { val dest = varRef("dest") val destPos = varRef("destPos") val length = varRef("length") - val methodDef = MethodDef(static = false, Ident("copyTo"), + + val copyToName = genArrayClassPropertyForDef(ArrayClassProperty.copyTo) + + val methodDef = MethodDef(static = false, copyToName, paramList(srcPos, dest, destPos, length), None, { if (isTypedArray) { Block( @@ -1771,6 +1780,8 @@ private[emitter] object CoreJSLib { val i = varRef("i") val v = varRef("v") + val setName = genArrayClassPropertyForDef(ArrayClassProperty.set) + val boundsCheck = condTree(arrayIndexOutOfBounds != CheckedBehavior.Unchecked) { If((i < 0) || (i >= This().u.length), genCallHelper(VarField.throwArrayIndexOutOfBoundsException, i)) @@ -1783,7 +1794,7 @@ private[emitter] object CoreJSLib { } List( - MethodDef(static = false, Ident("set"), paramList(i, v), None, { + MethodDef(static = false, setName, paramList(i, v), None, { Block( boundsCheck, storeCheck, @@ -1800,7 +1811,10 @@ private[emitter] object CoreJSLib { val dest = varRef("dest") val destPos = varRef("destPos") val length = varRef("length") - val methodDef = MethodDef(static = false, Ident("copyTo"), + + val copyToName = genArrayClassPropertyForDef(ArrayClassProperty.copyTo) + + val methodDef = MethodDef(static = false, copyToName, paramList(srcPos, dest, destPos, length), None, { genCallHelper(VarField.arraycopyGeneric, This().u, srcPos, dest.u, destPos, length) @@ -2237,5 +2251,10 @@ private[emitter] object CoreJSLib { private def double(d: Double): DoubleLiteral = DoubleLiteral(d) private def bigInt(i: Long): BigIntLiteral = BigIntLiteral(i) + + // 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) + } } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala index 1a8b9bc517..506dec4d4a 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala @@ -39,6 +39,9 @@ final class Emitter[E >: Null <: js.Tree]( import Emitter._ import config._ + require(!config.minify || postTransformer == PostTransformer.Identity, + "When using the 'minify' option, the postTransformer must be Identity.") + private implicit val globalRefTracking: GlobalRefTracking = config.topLevelGlobalRefTracking @@ -49,10 +52,14 @@ final class Emitter[E >: Null <: js.Tree]( private val nameGen: NameGen = new NameGen private class State(val lastMentionedDangerousGlobalRefs: Set[String]) { + val nameCompressor = + if (minify) Some(new NameCompressor(config)) + else None + val sjsGen: SJSGen = { val jsGen = new JSGen(config) val varGen = new VarGen(jsGen, nameGen, lastMentionedDangerousGlobalRefs) - new SJSGen(jsGen, nameGen, varGen) + new SJSGen(jsGen, nameGen, varGen, nameCompressor) } val classEmitter: ClassEmitter = new ClassEmitter(sjsGen) @@ -87,7 +94,7 @@ final class Emitter[E >: Null <: js.Tree]( def emit(moduleSet: ModuleSet, logger: Logger): Result[E] = { val WithGlobals(body, globalRefs) = emitInternal(moduleSet, logger) - moduleKind match { + val result = moduleKind match { case ModuleKind.NoModule => assert(moduleSet.modules.size <= 1) val topLevelVars = moduleSet.modules @@ -112,6 +119,19 @@ final class Emitter[E >: Null <: js.Tree]( case ModuleKind.ESModule | ModuleKind.CommonJSModule => new Result(config.jsHeader, body, "", Nil, globalRefs) } + + for (compressor <- state.nameCompressor) { + compressor.allocateNames(moduleSet, logger) + + /* Throw away the whole state, but keep the mentioned dangerous global refs. + * Note that instances of the name compressor's entries are still alive + * at this point, since they are referenced from `DelayedIdent` nodes in + * the result trees. + */ + state = new State(state.lastMentionedDangerousGlobalRefs) + } + + result } private def emitInternal(moduleSet: ModuleSet, @@ -1084,7 +1104,8 @@ object Emitter { val jsHeader: String, val internalModulePattern: ModuleID => String, val optimizeBracketSelects: Boolean, - val trackAllGlobalRefs: Boolean + val trackAllGlobalRefs: Boolean, + val minify: Boolean ) { private def this( semantics: Semantics, @@ -1097,7 +1118,9 @@ object Emitter { jsHeader = "", internalModulePattern = "./" + _.id, optimizeBracketSelects = true, - trackAllGlobalRefs = false) + trackAllGlobalRefs = false, + minify = false + ) } private[emitter] val topLevelGlobalRefTracking: GlobalRefTracking = @@ -1127,6 +1150,9 @@ object Emitter { def withTrackAllGlobalRefs(trackAllGlobalRefs: Boolean): Config = copy(trackAllGlobalRefs = trackAllGlobalRefs) + def withMinify(minify: Boolean): Config = + copy(minify = minify) + private def copy( semantics: Semantics = semantics, moduleKind: ModuleKind = moduleKind, @@ -1134,9 +1160,12 @@ object Emitter { jsHeader: String = jsHeader, internalModulePattern: ModuleID => String = internalModulePattern, optimizeBracketSelects: Boolean = optimizeBracketSelects, - trackAllGlobalRefs: Boolean = trackAllGlobalRefs): Config = { + trackAllGlobalRefs: Boolean = trackAllGlobalRefs, + minify: Boolean = minify + ): Config = { new Config(semantics, moduleKind, esFeatures, jsHeader, - internalModulePattern, optimizeBracketSelects, trackAllGlobalRefs) + internalModulePattern, optimizeBracketSelects, trackAllGlobalRefs, + minify) } } @@ -1149,6 +1178,12 @@ object Emitter { def transformStats(trees: List[js.Tree], indent: Int): List[E] } + object PostTransformer { + object Identity extends PostTransformer[js.Tree] { + def transformStats(trees: List[js.Tree], indent: Int): List[js.Tree] = trees + } + } + private final class DesugaredClassCache[E >: Null] { val privateJSFields = new OneTimeCache[WithGlobals[E]] val storeJSSuperClass = new OneTimeCache[WithGlobals[E]] 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 95cf6e1c33..6a8b9ca3dd 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,12 +632,11 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { } if (checked) { - js.Apply(js.DotSelect(genArray, js.Ident("set")), - List(genIndex, genRhs)) + genArrayClassPropApply(genArray, ArrayClassProperty.set, genIndex, genRhs) } else { js.Assign( js.BracketSelect( - js.DotSelect(genArray, js.Ident("u"))(lhs.pos), + genArrayClassPropSelect(genArray, ArrayClassProperty.u)(lhs.pos), genIndex)(lhs.pos), genRhs) } @@ -877,7 +876,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { def genUnchecked(): js.Tree = { if (esFeatures.esVersion >= ESVersion.ES2015 && semantics.nullPointers == CheckedBehavior.Unchecked) - js.Apply(jsArgs.head DOT "copyTo", jsArgs.tail) + genArrayClassPropApply(jsArgs.head, ArrayClassProperty.copyTo, jsArgs.tail) else genCallHelper(VarField.systemArraycopy, jsArgs: _*) } @@ -2657,17 +2656,19 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { genArrayValue(typeRef, elems.map(transformExpr(_, preserveChar)))) case ArrayLength(array) => - genIdentBracketSelect(js.DotSelect(transformExprNoChar(checkNotNull(array)), - js.Ident("u")), "length") + val newArray = transformExprNoChar(checkNotNull(array)) + genIdentBracketSelect( + genArrayClassPropSelect(newArray, ArrayClassProperty.u), + "length") case ArraySelect(array, index) => val newArray = transformExprNoChar(checkNotNull(array)) val newIndex = transformExprNoChar(index) semantics.arrayIndexOutOfBounds match { case CheckedBehavior.Compliant | CheckedBehavior.Fatal => - js.Apply(js.DotSelect(newArray, js.Ident("get")), List(newIndex)) + genArrayClassPropApply(newArray, ArrayClassProperty.get, newIndex) case CheckedBehavior.Unchecked => - js.BracketSelect(js.DotSelect(newArray, js.Ident("u")), newIndex) + js.BracketSelect(genArrayClassPropSelect(newArray, ArrayClassProperty.u), newIndex) } case tree: RecordSelect => @@ -2766,12 +2767,13 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case Transient(ArrayToTypedArray(expr, primRef)) => val value = transformExprNoChar(checkNotNull(expr)) + val valueUnderlying = genArrayClassPropSelect(value, ArrayClassProperty.u) if (es2015) { - js.Apply(genIdentBracketSelect(value.u, "slice"), Nil) + js.Apply(genIdentBracketSelect(valueUnderlying, "slice"), Nil) } else { val typedArrayClass = extractWithGlobals(typedArrayRef(primRef).get) - js.New(typedArrayClass, value.u :: Nil) + js.New(typedArrayClass, valueUnderlying :: Nil) } case Transient(TypedArrayToArray(expr, primRef)) => 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 new file mode 100644 index 0000000000..f23287c6bf --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameCompressor.scala @@ -0,0 +1,230 @@ +/* + * 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.backend.emitter + +import scala.annotation.{switch, tailrec} + +import java.util.Comparator + +import scala.collection.mutable + +import org.scalajs.ir.Names._ +import org.scalajs.linker.backend.javascript.Trees.DelayedIdent.Resolver +import org.scalajs.linker.standard.ModuleSet +import org.scalajs.logging.Logger + +private[emitter] final class NameCompressor(config: Emitter.Config) { + import NameCompressor._ + + private val entries: EntryMap = mutable.AnyRefMap.empty + + private var namesAllocated: Boolean = false + + def allocateNames(moduleSet: ModuleSet, logger: Logger): Unit = { + assert(!namesAllocated, "Cannot allocate names a second time") + + val propertyNamesToAvoid = logger.time("Name compressor: Collect property names to avoid") { + collectPropertyNamesToAvoid(moduleSet) + } + + logger.time("Name compressor: Allocate property names") { + allocatePropertyNames(entries, propertyNamesToAvoid) + } + + namesAllocated = true + } + + def genResolverFor(fieldName: FieldName): Resolver = + entries.getOrElseUpdate(fieldName, new FieldNameEntry(fieldName)).genResolver() + + def genResolverFor(methodName: MethodName): Resolver = + entries.getOrElseUpdate(methodName, new MethodNameEntry(methodName)).genResolver() + + def genResolverFor(prop: ArrayClassProperty): Resolver = + entries.getOrElseUpdate(prop, new ArrayClassPropEntry(prop)).genResolver() + + /** Collects the property names to avoid for Scala instance members. + * + * We collect the names of exported members in Scala classes. These live in + * the same namespace as Scala methods and fields. Therefore, we must avoid + * them when allocating names for that namespace. + */ + private def collectPropertyNamesToAvoid(moduleSet: ModuleSet): Set[String] = { + import org.scalajs.ir.Trees._ + + val builder = Set.newBuilder[String] + + builder ++= BasePropertyNamesToAvoid + + for { + module <- moduleSet.modules + linkedClass <- module.classDefs + if linkedClass.kind.isClass + exportedMember <- linkedClass.exportedMembers + } { + (exportedMember: @unchecked) match { + case JSMethodDef(_, StringLiteral(name), _, _, _) => + builder += name + case JSPropertyDef(_, StringLiteral(name), _, _) => + builder += name + } + } + + builder.result() + } +} + +private[emitter] object NameCompressor { + /** Base set of names that should be avoided when allocating property names + * in any namespace. + * + * This set contains: + * + * - the reserved JS identifiers (not technically invalid by spec, but JS + * minifiers tend to avoid them anyway: `foo.if` is playing with fire), + * - the `"then"` name, because it is used to identify `Thenable`s by + * spec and therefore lives in the same namespace as the properties of + * *all* objects, + */ + private val BasePropertyNamesToAvoid: Set[String] = + NameGen.ReservedJSIdentifierNames + "then" + + private def allocatePropertyNames(entries: EntryMap, + namesToAvoid: collection.Set[String]): Unit = { + val comparator: Comparator[PropertyNameEntry] = + Comparator.comparingInt[PropertyNameEntry](_.occurrences).reversed() // by decreasing order of occurrences + .thenComparing(Comparator.naturalOrder[PropertyNameEntry]()) // tie-break + + val orderedEntries = entries.values.toArray + java.util.Arrays.sort(orderedEntries, comparator) + + val generator = new NameGenerator(namesToAvoid) + + for (entry <- orderedEntries) + entry.allocatedName = generator.nextString() + } + + /** Keys of this map are `FieldName | MethodName | ArrayClassProperty`. */ + private type EntryMap = mutable.AnyRefMap[AnyRef, PropertyNameEntry] + + private sealed abstract class PropertyNameEntry extends Comparable[PropertyNameEntry] { + var occurrences: Int = 0 + var allocatedName: String = null + + protected def debugString: String + + private object resolver extends Resolver { + def resolve(): String = { + if (allocatedName == null) + throw new IllegalStateException(s"Cannot resolve name before it was allocated, for $this") + allocatedName + } + + def debugString: String = PropertyNameEntry.this.debugString + + override def toString(): String = debugString + } + + private def incOccurrences(): Unit = { + if (allocatedName != null) + throw new IllegalStateException(s"Cannot increase occurrences after name was allocated for $this") + occurrences += 1 + } + + def genResolver(): Resolver = { + incOccurrences() + resolver + } + + def compareTo(that: PropertyNameEntry): Int = (this, that) match { + case (x: FieldNameEntry, y: FieldNameEntry) => + x.fieldName.compareTo(y.fieldName) + + case (x: MethodNameEntry, y: MethodNameEntry) => + x.methodName.compareTo(y.methodName) + + case (x: ArrayClassPropEntry, y: ArrayClassPropEntry) => + x.property.compareTo(y.property) + + case _ => + def ordinalFor(x: PropertyNameEntry): Int = x match { + case _: FieldNameEntry => 1 + case _: MethodNameEntry => 2 + case _: ArrayClassPropEntry => 3 + } + ordinalFor(this) - ordinalFor(that) + } + } + + private final class FieldNameEntry(val fieldName: FieldName) + extends PropertyNameEntry { + protected def debugString: String = fieldName.nameString + + override def toString(): String = s"FieldNameEntry(${fieldName.nameString})" + } + + private final class MethodNameEntry(val methodName: MethodName) + extends PropertyNameEntry { + protected def debugString: String = methodName.nameString + + override def toString(): String = s"MethodNameEntry(${methodName.nameString})" + } + + private final class ArrayClassPropEntry(val property: ArrayClassProperty) + extends PropertyNameEntry { + protected def debugString: String = property.nonMinifiedName + + override def toString(): String = s"ArrayClassPropEntry(${property.nonMinifiedName})" + } + + // private[emitter] for tests + private[emitter] final class NameGenerator(namesToAvoid: collection.Set[String]) { + /* 6 because 52 * (62**5) > Int.MaxValue + * i.e., to exceed this size we would need more than Int.MaxValue different names. + */ + private val charArray = new Array[Char](6) + charArray(0) = 'a' + private var charCount = 1 + + @tailrec + private def incAtIndex(idx: Int): Unit = { + (charArray(idx): @switch) match { + case '9' => + charArray(idx) = 'a' + case 'z' => + charArray(idx) = 'A' + case 'Z' => + if (idx > 0) { + charArray(idx) = '0' + incAtIndex(idx - 1) + } else { + java.util.Arrays.fill(charArray, '0') + charArray(0) = 'a' + charCount += 1 + } + case c => + charArray(idx) = (c + 1).toChar + } + } + + @tailrec + final def nextString(): String = { + val s = new String(charArray, 0, charCount) + incAtIndex(charCount - 1) + if (namesToAvoid.contains(s)) + nextString() + else + s + } + } +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameGen.scala index 552dd545bd..ae8502211d 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/NameGen.scala @@ -301,7 +301,7 @@ private[emitter] final class NameGen { } } -private object NameGen { +private[emitter] object NameGen { private final val FullwidthSpacingUnderscore = '\uff3f' private final val GreekSmallLetterDelta = '\u03b4' @@ -371,7 +371,7 @@ private object NameGen { * not actually mean `void 0`, and who knows what JS engine performance * cliffs we can trigger with that. */ - private final val ReservedJSIdentifierNames: Set[String] = Set( + private[emitter] final val ReservedJSIdentifierNames: Set[String] = Set( "arguments", "await", "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do", "else", "enum", "eval", "export", "extends", "false", "finally", "for", "function", "if", 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 615b1e94a0..9e8810afc3 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 @@ -32,7 +32,8 @@ import PolyfillableBuiltin._ private[emitter] final class SJSGen( val jsGen: JSGen, val nameGen: NameGen, - val varGen: VarGen + val varGen: VarGen, + val nameCompressor: Option[NameCompressor] ) { import jsGen._ @@ -151,15 +152,36 @@ private[emitter] final class SJSGen( def genSelect(receiver: Tree, field: irt.FieldIdent)( implicit pos: Position): Tree = { - DotSelect(receiver, Ident(genName(field.name))(field.pos)) + DotSelect(receiver, genFieldIdent(field.name)(field.pos)) } def genSelectForDef(receiver: Tree, field: irt.FieldIdent, originalName: OriginalName)( implicit pos: Position): Tree = { - val jsName = genName(field.name) - val jsOrigName = genOriginalName(field.name, originalName, jsName) - DotSelect(receiver, Ident(jsName, jsOrigName)(field.pos)) + DotSelect(receiver, genFieldIdentForDef(field.name, originalName)(field.pos)) + } + + private def genFieldIdent(fieldName: FieldName)( + implicit pos: Position): MaybeDelayedIdent = { + nameCompressor match { + case None => + Ident(genName(fieldName)) + case Some(compressor) => + DelayedIdent(compressor.genResolverFor(fieldName)) + } + } + + private def genFieldIdentForDef(fieldName: FieldName, + originalName: OriginalName)( + implicit pos: Position): MaybeDelayedIdent = { + nameCompressor match { + case None => + val jsName = genName(fieldName) + val jsOrigName = genOriginalName(fieldName, originalName, jsName) + Ident(jsName, jsOrigName) + case Some(compressor) => + DelayedIdent(compressor.genResolverFor(fieldName), originalName.orElse(fieldName)) + } } def genApply(receiver: Tree, methodName: MethodName, args: List[Tree])( @@ -172,22 +194,60 @@ private[emitter] final class SJSGen( genApply(receiver, methodName, args.toList) } - def genMethodIdent(methodIdent: irt.MethodIdent): Ident = + def genMethodIdent(methodIdent: irt.MethodIdent): MaybeDelayedIdent = genMethodIdent(methodIdent.name)(methodIdent.pos) def genMethodIdentForDef(methodIdent: irt.MethodIdent, - originalName: OriginalName): Ident = { + originalName: OriginalName): MaybeDelayedIdent = { genMethodIdentForDef(methodIdent.name, originalName)(methodIdent.pos) } - def genMethodIdent(methodName: MethodName)(implicit pos: Position): Ident = - Ident(genName(methodName)) + def genMethodIdent(methodName: MethodName)(implicit pos: Position): MaybeDelayedIdent = { + nameCompressor match { + case None => Ident(genName(methodName)) + case Some(compressor) => DelayedIdent(compressor.genResolverFor(methodName)) + } + } def genMethodIdentForDef(methodName: MethodName, originalName: OriginalName)( - implicit pos: Position): Ident = { - val jsName = genName(methodName) - val jsOrigName = genOriginalName(methodName, originalName, jsName) - Ident(jsName, jsOrigName) + implicit pos: Position): MaybeDelayedIdent = { + nameCompressor match { + case None => + val jsName = genName(methodName) + val jsOrigName = genOriginalName(methodName, originalName, jsName) + Ident(jsName, jsOrigName) + case Some(compressor) => + DelayedIdent(compressor.genResolverFor(methodName), originalName.orElse(methodName)) + } + } + + def genArrayClassPropApply(receiver: Tree, prop: ArrayClassProperty, args: Tree*)( + implicit pos: Position): Tree = { + genArrayClassPropApply(receiver, prop, args.toList) + } + + def genArrayClassPropApply(receiver: Tree, prop: ArrayClassProperty, args: List[Tree])( + implicit pos: Position): Tree = { + Apply(genArrayClassPropSelect(receiver, prop), args) + } + + def genArrayClassPropSelect(qualifier: Tree, prop: ArrayClassProperty)( + implicit pos: Position): Tree = { + DotSelect(qualifier, genArrayClassProperty(prop)) + } + + def genArrayClassProperty(prop: ArrayClassProperty)(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 = { + nameCompressor match { + case None => Ident(prop.nonMinifiedName) + case Some(compressor) => DelayedIdent(compressor.genResolverFor(prop), prop.originalName) + } } def genJSPrivateSelect(receiver: Tree, field: irt.FieldIdent)( diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/TreeDSL.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/TreeDSL.scala index 0063f17d20..540936dc78 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/TreeDSL.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/TreeDSL.scala @@ -24,7 +24,7 @@ private[emitter] object TreeDSL { extends AnyVal { /** Select a member */ - def DOT(field: Ident)(implicit pos: Position): DotSelect = + def DOT(field: MaybeDelayedIdent)(implicit pos: Position): DotSelect = DotSelect(self, field) /** Select a member */ @@ -112,7 +112,6 @@ private[emitter] object TreeDSL { def prototype(implicit pos: Position): Tree = self DOT "prototype" def length(implicit pos: Position): Tree = self DOT "length" - def u(implicit pos: Position): Tree = self DOT "u" } def typeof(expr: Tree)(implicit pos: Position): Tree = diff --git a/linker/shared/src/main/scala/org/scalajs/linker/standard/StandardLinkerBackend.scala b/linker/shared/src/main/scala/org/scalajs/linker/standard/StandardLinkerBackend.scala index 4ea98e8679..ff27b8452f 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/standard/StandardLinkerBackend.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/standard/StandardLinkerBackend.scala @@ -23,6 +23,7 @@ object StandardLinkerBackend { .withSourceMap(config.sourceMap) .withOutputPatterns(config.outputPatterns) .withRelativizeSourceMapBase(config.relativizeSourceMapBase) + .withMinify(config.minify) .withClosureCompilerIfAvailable(config.closureCompilerIfAvailable) .withPrettyPrint(config.prettyPrint) .withMaxConcurrentWrites(config.maxConcurrentWrites) diff --git a/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala index 1f7884c0f1..50e726106e 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala @@ -63,7 +63,7 @@ class EmitterTest { config = config) fullContent <- linkToContent(classDefs, moduleInitializers = MainTestModuleInitializers, - config = config.withClosureCompilerIfAvailable(true)) + config = config.withClosureCompilerIfAvailable(true).withMinify(true)) } yield { def testContent(content: String): Unit = { if (!content.startsWith(header)) { 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 aa851ca438..33b1bd7ff4 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -71,7 +71,7 @@ class LibrarySizeTest { testLinkedSizes( expectedFastLinkSize = 150063, - expectedFullLinkSizeWithoutClosure = 130664, + expectedFullLinkSizeWithoutClosure = 95680, expectedFullLinkSizeWithClosure = 21325, classDefs, moduleInitializers = MainTestModuleInitializers @@ -98,6 +98,7 @@ object LibrarySizeTest { val fullLinkConfig = config .withSemantics(_.optimized) .withClosureCompilerIfAvailable(true) + .withMinify(true) val fastLinker = StandardImpl.linker(config) val fullLinker = StandardImpl.linker(fullLinkConfig) diff --git a/linker/shared/src/test/scala/org/scalajs/linker/backend/emitter/NameCompressorTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/backend/emitter/NameCompressorTest.scala new file mode 100644 index 0000000000..db2f5e722b --- /dev/null +++ b/linker/shared/src/test/scala/org/scalajs/linker/backend/emitter/NameCompressorTest.scala @@ -0,0 +1,53 @@ +/* + * 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.backend.emitter + +import org.junit.Test +import org.junit.Assert._ + +class NameCompressorTest { + @Test def testNameGenerator(): Unit = { + // all the one-letter strings + val letterStrings = (('a' to 'z') ++ ('A' to 'Z')).map(_.toString()) + + // all the one-letter-or-digit strings + val letterOrDigitStrings = ('0' to '9').map(_.toString()) ++ letterStrings + + val expectedOneCharIdents = letterStrings + + val expectedTwoCharIdents = for { + firstChar <- letterStrings + secondChar <- letterOrDigitStrings + ident = firstChar + secondChar + if ident != "do" && ident != "if" && ident != "in" // reserved JS identifiers that will be avoided + } yield { + ident + } + + val firstFewExpectedThreeCharIdents = { + letterOrDigitStrings.map("a0" + _) ++ + letterOrDigitStrings.map("a1" + _) + } + + val expectedSequenceStart = + expectedOneCharIdents ++ expectedTwoCharIdents ++ firstFewExpectedThreeCharIdents + + // Now actually test + + val namesToAvoid = NameGen.ReservedJSIdentifierNames + val generator = new NameCompressor.NameGenerator(namesToAvoid) + + for (expected <- expectedSequenceStart) + assertEquals(expected, generator.nextString()) + } +} diff --git a/project/Build.scala b/project/Build.scala index a14202f7f9..ffcd864afe 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -53,6 +53,9 @@ object ExposedValues extends AutoPlugin { val default213ScalaVersion: SettingKey[String] = settingKey("the default Scala 2.13.x version for this build (derived from cross213ScalaVersions)") + val enableMinifyEverywhere: SettingKey[Boolean] = + settingKey("force usage of the `minify` option of the linker in all contexts (fast and full)") + // set scalaJSLinkerConfig in someProject ~= makeCompliant val makeCompliant: StandardConfig => StandardConfig = { _.withSemantics { semantics => @@ -102,6 +105,8 @@ object ExposedValues extends AutoPlugin { } } +import ExposedValues.autoImport.enableMinifyEverywhere + final case class ExpectedSizes(fastLink: Range, fullLink: Range, fastLinkGz: Range, fullLinkGz: Range) @@ -143,6 +148,15 @@ object MyScalaJSPlugin extends AutoPlugin { } override def globalSettings: Seq[Setting[_]] = Def.settings( + // can be overridden with a 'set' command + enableMinifyEverywhere := false, + + scalaJSLinkerConfig := { + scalaJSLinkerConfig.value + .withCheckIR(true) + .withMinify(enableMinifyEverywhere.value) + }, + fullClasspath in scalaJSLinkerImpl := { (fullClasspath in (Build.linker.v2_12, Runtime)).value }, @@ -201,10 +215,24 @@ object MyScalaJSPlugin extends AutoPlugin { libDeps.filterNot(dep => blacklist.contains(dep.name)) }, - scalaJSLinkerConfig ~= (_.withCheckIR(true)), - wantSourceMaps := true, + // If `enableMinifyEverywhere` is used, make sure to deactive GCC in fullLinkJS + Compile / fullLinkJS / scalaJSLinkerConfig := { + val prev = (Compile / fullLinkJS / scalaJSLinkerConfig).value + if (enableMinifyEverywhere.value) + prev.withClosureCompiler(false) + else + prev + }, + Test / fullLinkJS / scalaJSLinkerConfig := { + val prev = (Test / fullLinkJS / scalaJSLinkerConfig).value + if (enableMinifyEverywhere.value) + prev.withClosureCompiler(false) + else + prev + }, + jsEnv := new NodeJSEnv( NodeJSEnv.Config().withSourceMap(wantSourceMaps.value)), @@ -231,6 +259,7 @@ object MyScalaJSPlugin extends AutoPlugin { checksizes := { val logger = streams.value.log + val useMinifySizes = enableMinifyEverywhere.value val maybeExpected = expectedSizes.value /* The deprecated tasks do exactly what we want in terms of module / @@ -239,7 +268,7 @@ object MyScalaJSPlugin extends AutoPlugin { val fast = (fastOptJS in Compile).value.data val full = (fullOptJS in Compile).value.data - val desc = s"${thisProject.value.id} Scala ${scalaVersion.value}" + val desc = s"${thisProject.value.id} Scala ${scalaVersion.value}, useMinifySizes = $useMinifySizes" maybeExpected.fold { logger.info(s"Ignoring checksizes for " + desc) @@ -1963,23 +1992,42 @@ object Build { MyScalaJSPlugin.expectedSizes := { val default212Version = default212ScalaVersion.value val default213Version = default213ScalaVersion.value + val useMinifySizes = enableMinifyEverywhere.value scalaVersion.value match { case `default212Version` => - Some(ExpectedSizes( - fastLink = 640000 to 641000, - fullLink = 101000 to 102000, - fastLinkGz = 77000 to 78000, - fullLinkGz = 26000 to 27000, - )) + if (!useMinifySizes) { + Some(ExpectedSizes( + fastLink = 640000 to 641000, + fullLink = 101000 to 102000, + fastLinkGz = 77000 to 78000, + fullLinkGz = 26000 to 27000, + )) + } else { + Some(ExpectedSizes( + fastLink = 538000 to 539000, + fullLink = 371000 to 372000, + fastLinkGz = 71000 to 72000, + fullLinkGz = 51000 to 52000, + )) + } case `default213Version` => - Some(ExpectedSizes( - fastLink = 462000 to 463000, - fullLink = 99000 to 100000, - fastLinkGz = 60000 to 61000, - fullLinkGz = 26000 to 27000, - )) + if (!useMinifySizes) { + Some(ExpectedSizes( + fastLink = 462000 to 463000, + fullLink = 99000 to 100000, + fastLinkGz = 60000 to 61000, + fullLinkGz = 26000 to 27000, + )) + } else { + Some(ExpectedSizes( + fastLink = 373000 to 374000, + fullLink = 332000 to 333000, + fastLinkGz = 55000 to 56000, + fullLinkGz = 50000 to 51000, + )) + } case _ => None @@ -2227,7 +2275,8 @@ object Build { "isNoModule" -> (moduleKind == ModuleKind.NoModule), "isESModule" -> (moduleKind == ModuleKind.ESModule), "isCommonJSModule" -> (moduleKind == ModuleKind.CommonJSModule), - "isFullOpt" -> (stage == Stage.FullOpt), + "usesClosureCompiler" -> linkerConfig.closureCompiler, + "hasMinifiedNames" -> (linkerConfig.closureCompiler || linkerConfig.minify), "compliantAsInstanceOfs" -> (sems.asInstanceOfs == CheckedBehavior.Compliant), "compliantArrayIndexOutOfBounds" -> (sems.arrayIndexOutOfBounds == CheckedBehavior.Compliant), "compliantArrayStores" -> (sems.arrayStores == CheckedBehavior.Compliant), diff --git a/sbt-plugin/src/main/scala/org/scalajs/sbtplugin/ScalaJSPluginInternal.scala b/sbt-plugin/src/main/scala/org/scalajs/sbtplugin/ScalaJSPluginInternal.scala index 6f1d6f66a4..02d4eb89f1 100644 --- a/sbt-plugin/src/main/scala/org/scalajs/sbtplugin/ScalaJSPluginInternal.scala +++ b/sbt-plugin/src/main/scala/org/scalajs/sbtplugin/ScalaJSPluginInternal.scala @@ -470,6 +470,7 @@ private[sbtplugin] object ScalaJSPluginInternal { prevConfig .withSemantics(_.optimized) .withClosureCompiler(useClosure) + .withMinify(true) // ignored if we actually use Closure .withCheckIR(true) // for safety, fullOpt is slow anyways. }, diff --git a/test-suite/js/src/main/scala-ide-stubs/org/scalajs/testsuite/utils/BuildInfo.scala b/test-suite/js/src/main/scala-ide-stubs/org/scalajs/testsuite/utils/BuildInfo.scala index cbd1a4f46d..d577790522 100644 --- a/test-suite/js/src/main/scala-ide-stubs/org/scalajs/testsuite/utils/BuildInfo.scala +++ b/test-suite/js/src/main/scala-ide-stubs/org/scalajs/testsuite/utils/BuildInfo.scala @@ -22,7 +22,8 @@ private[utils] object BuildInfo { final val isNoModule = false final val isESModule = false final val isCommonJSModule = false - final val isFullOpt = false + final val usesClosureCompiler = false + final val hasMinifiedNames = false final val compliantAsInstanceOfs = false final val compliantArrayIndexOutOfBounds = false final val compliantArrayStores = false diff --git a/test-suite/js/src/main/scala/org/scalajs/testsuite/utils/Platform.scala b/test-suite/js/src/main/scala/org/scalajs/testsuite/utils/Platform.scala index cbf49e2d92..ac1c4132b3 100644 --- a/test-suite/js/src/main/scala/org/scalajs/testsuite/utils/Platform.scala +++ b/test-suite/js/src/main/scala/org/scalajs/testsuite/utils/Platform.scala @@ -68,7 +68,10 @@ object Platform { def sourceMaps: Boolean = BuildInfo.hasSourceMaps && executingInNodeJS - def isInFullOpt: Boolean = BuildInfo.isFullOpt + def usesClosureCompiler: Boolean = BuildInfo.usesClosureCompiler + + def hasMinifiedNames: Boolean = BuildInfo.hasMinifiedNames + def isInProductionMode: Boolean = BuildInfo.productionMode def hasCompliantAsInstanceOfs: Boolean = BuildInfo.compliantAsInstanceOfs diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/OptimizerTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/OptimizerTest.scala index 2e8878cef9..833ae38e64 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/OptimizerTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/OptimizerTest.scala @@ -321,19 +321,22 @@ class OptimizerTest { } @Test def foldingDoubleWithDecimalAndString(): Unit = { - assumeFalse("Assumed not executing in FullOpt", isInFullOpt) + assumeFalse("GCC wrongly optimizes this code", usesClosureCompiler) + assertEquals("1.2323919403474454e+21hello", 1.2323919403474454E21 + "hello") assertEquals("hello1.2323919403474454e+21", "hello" + 1.2323919403474454E21) } @Test def foldingDoubleThatJVMWouldPrintInScientificNotationAndString(): Unit = { - assumeFalse("Assumed not executing in FullOpt", isInFullOpt) + assumeFalse("GCC wrongly optimizes this code", usesClosureCompiler) + assertEquals("123456789012345hello", 123456789012345d + "hello") assertEquals("hello123456789012345", "hello" + 123456789012345d) } @Test def foldingDoublesToString(): Unit = { - assumeFalse("Assumed not executing in FullOpt", isInFullOpt) + assumeFalse("GCC wrongly optimizes this code", usesClosureCompiler) + @noinline def toStringNoInline(v: Double): String = v.toString @inline def test(v: Double): Unit = assertEquals(toStringNoInline(v), v.toString) diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/MiscInteropTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/MiscInteropTest.scala index 2877c74ac4..f477cffcc6 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/MiscInteropTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/MiscInteropTest.scala @@ -42,7 +42,7 @@ class MiscInteropTest { assumeFalse( "GCC wrongly optimizes this code, " + "see https://github.com/google/closure-compiler/issues/3498", - isInFullOpt) + usesClosureCompiler) @noinline def nonExistentGlobalVarNoInline(): Any = js.Dynamic.global.thisGlobalVarDoesNotExist @@ -197,7 +197,7 @@ class MiscInteropTest { // Emitted classes @Test def meaningfulNameProperty(): Unit = { - assumeFalse("Assumed not executing in FullOpt", isInFullOpt) + assumeFalse("Need non-minified names", hasMinifiedNames) def nameOf(obj: Any): js.Any = obj.asInstanceOf[js.Dynamic].constructor.name diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/library/StackTraceTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/library/StackTraceTest.scala index d931b4bb0d..03315cf014 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/library/StackTraceTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/library/StackTraceTest.scala @@ -52,7 +52,7 @@ class StackTraceTest { @Test def decodeClassNameAndMethodName(): Unit = { assumeTrue("Assume Node.js", executingInNodeJS) - assumeFalse("Assume fullopt-stage", isInFullOpt) + assumeFalse("Assume non-minified names", hasMinifiedNames) val Error = js.constructorOf[js.Error] val oldStackTraceLimit = Error.stackTraceLimit diff --git a/test-suite/jvm/src/main/scala/org/scalajs/testsuite/utils/Platform.scala b/test-suite/jvm/src/main/scala/org/scalajs/testsuite/utils/Platform.scala index a3d908bf8e..6b0a9c412a 100644 --- a/test-suite/jvm/src/main/scala/org/scalajs/testsuite/utils/Platform.scala +++ b/test-suite/jvm/src/main/scala/org/scalajs/testsuite/utils/Platform.scala @@ -40,7 +40,9 @@ object Platform { else Integer.parseInt(v.takeWhile(_.isDigit)) } - def isInFullOpt: Boolean = false + def usesClosureCompiler: Boolean = false + + def hasMinifiedNames: Boolean = false def hasCompliantAsInstanceOfs: Boolean = true def hasCompliantArrayIndexOutOfBounds: Boolean = true diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/LongTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/LongTest.scala index a21f8ff802..dd9ed7a5a6 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/LongTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/LongTest.scala @@ -620,7 +620,7 @@ class LongTest { test(-1, lg(-1)) // Closure seems to incorrectly rewrite the constant on the right :-( - val epsilon = if (isInFullOpt) 1E4f else 0.0f + val epsilon = if (usesClosureCompiler) 1E4f else 0.0f test(9.223372E18f, MaxVal, epsilon) test(-9.223372E18f, MinVal, epsilon) @@ -674,7 +674,7 @@ class LongTest { test(-1, lg(-1)) // Closure seems to incorrectly rewrite the constant on the right :-( - val epsilon = if (isInFullOpt) 1E4 else 0.0 + val epsilon = if (usesClosureCompiler) 1E4 else 0.0 test(9.223372036854776E18, MaxVal, epsilon) test(-9.223372036854776E18, MinVal, epsilon) @@ -722,7 +722,7 @@ class LongTest { test(lg(0), -Double.MinPositiveValue) test(MaxVal, twoPow63) test(MaxVal, twoPow63NextUp) - if (!isInFullOpt) { + if (!usesClosureCompiler) { // GCC incorrectly rewrites the Double constants on the rhs test(lg(-1024, 2147483647), twoPow63NextDown) test(MinVal, -twoPow63) diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/ReflectiveCallTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/ReflectiveCallTest.scala index 1fe0250028..98bd0f275e 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/ReflectiveCallTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/ReflectiveCallTest.scala @@ -346,7 +346,7 @@ class ReflectiveCallTest { assumeFalse( "GCC is a bit too eager in its optimizations in this error case", - Platform.isInFullOpt) + Platform.usesClosureCompiler) type ObjWithAnyRefPrimitives = Any { def eq(that: AnyRef): Boolean From 792866359d5cfe459e05295c680de4297c0e7cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 25 Jan 2024 15:34:14 +0100 Subject: [PATCH 057/298] Compress the ancestor names used for instance tests. --- .../linker/backend/emitter/ClassEmitter.scala | 7 ++-- .../linker/backend/emitter/CoreJSLib.scala | 4 +- .../backend/emitter/NameCompressor.scala | 40 +++++++++++++++---- .../linker/backend/emitter/SJSGen.scala | 7 ++++ .../org/scalajs/linker/LibrarySizeTest.scala | 2 +- project/Build.scala | 16 ++++---- 6 files changed, 55 insertions(+), 21 deletions(-) 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 e0154b1782..9fa8a09156 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 @@ -821,7 +821,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { ancestors: js.Tree)( implicit pos: Position): js.Tree = { import TreeDSL._ - ancestors DOT genName(className) + ancestors DOT genAncestorIdent(className) } def genTypeData(className: ClassName, kind: ClassKind, @@ -852,7 +852,8 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } val ancestorsRecord = js.ObjectConstr( - ancestors.withFilter(_ != ObjectClass).map(ancestor => (js.Ident(genName(ancestor)), js.IntLiteral(1)))) + ancestors.withFilter(_ != ObjectClass).map(ancestor => (genAncestorIdent(ancestor), js.IntLiteral(1))) + ) val isInstanceFunWithGlobals: WithGlobals[js.Tree] = { if (globalKnowledge.isAncestorOfHijackedClass(className)) { @@ -901,7 +902,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { isInstanceFunWithGlobals.flatMap { isInstanceFun => val allParams = List( - js.ObjectConstr(List(js.Ident(genName(className)) -> js.IntLiteral(0))), + js.ObjectConstr(List(genAncestorIdent(className) -> js.IntLiteral(0))), js.BooleanLiteral(kind == ClassKind.Interface), js.StringLiteral(RuntimeClassNameMapperImpl.map( semantics.runtimeClassNameMapper, className.nameString)), 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 a96dea3018..23e3242c30 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 @@ -1705,8 +1705,8 @@ private[emitter] object CoreJSLib { else Skip(), privateFieldSet("ancestors", ObjectConstr(List( - Ident(genName(CloneableClass)) -> 1, - Ident(genName(SerializableClass)) -> 1 + genAncestorIdent(CloneableClass) -> 1, + genAncestorIdent(SerializableClass) -> 1 ))), privateFieldSet("componentData", componentData), privateFieldSet("arrayBase", arrayBase), 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 f23287c6bf..0db5ead5bf 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 @@ -17,6 +17,7 @@ import scala.annotation.{switch, tailrec} import java.util.Comparator import scala.collection.mutable +import scala.reflect.ClassTag import org.scalajs.ir.Names._ import org.scalajs.linker.backend.javascript.Trees.DelayedIdent.Resolver @@ -27,6 +28,7 @@ private[emitter] final class NameCompressor(config: Emitter.Config) { import NameCompressor._ private val entries: EntryMap = mutable.AnyRefMap.empty + private val ancestorEntries: AncestorEntryMap = mutable.AnyRefMap.empty private var namesAllocated: Boolean = false @@ -41,6 +43,10 @@ private[emitter] final class NameCompressor(config: Emitter.Config) { allocatePropertyNames(entries, propertyNamesToAvoid) } + logger.time("Name compressor: Allocate ancestor names") { + allocatePropertyNames(ancestorEntries, BasePropertyNamesToAvoid) + } + namesAllocated = true } @@ -53,6 +59,9 @@ private[emitter] final class NameCompressor(config: Emitter.Config) { def genResolverFor(prop: ArrayClassProperty): Resolver = entries.getOrElseUpdate(prop, new ArrayClassPropEntry(prop)).genResolver() + def genResolverForAncestor(ancestor: ClassName): Resolver = + ancestorEntries.getOrElseUpdate(ancestor, new AncestorNameEntry(ancestor)).genResolver() + /** Collects the property names to avoid for Scala instance members. * * We collect the names of exported members in Scala classes. These live in @@ -99,11 +108,11 @@ private[emitter] object NameCompressor { private val BasePropertyNamesToAvoid: Set[String] = NameGen.ReservedJSIdentifierNames + "then" - private def allocatePropertyNames(entries: EntryMap, - namesToAvoid: collection.Set[String]): Unit = { - val comparator: Comparator[PropertyNameEntry] = - Comparator.comparingInt[PropertyNameEntry](_.occurrences).reversed() // by decreasing order of occurrences - .thenComparing(Comparator.naturalOrder[PropertyNameEntry]()) // tie-break + private def allocatePropertyNames[K <: AnyRef, E <: BaseEntry with Comparable[E]: ClassTag]( + entries: mutable.AnyRefMap[K, E], namesToAvoid: collection.Set[String]): Unit = { + val comparator: Comparator[E] = + Comparator.comparingInt[E](_.occurrences).reversed() // by decreasing order of occurrences + .thenComparing(Comparator.naturalOrder[E]()) // tie-break val orderedEntries = entries.values.toArray java.util.Arrays.sort(orderedEntries, comparator) @@ -117,7 +126,9 @@ private[emitter] object NameCompressor { /** Keys of this map are `FieldName | MethodName | ArrayClassProperty`. */ private type EntryMap = mutable.AnyRefMap[AnyRef, PropertyNameEntry] - private sealed abstract class PropertyNameEntry extends Comparable[PropertyNameEntry] { + private type AncestorEntryMap = mutable.AnyRefMap[ClassName, AncestorNameEntry] + + private sealed abstract class BaseEntry { var occurrences: Int = 0 var allocatedName: String = null @@ -130,7 +141,7 @@ private[emitter] object NameCompressor { allocatedName } - def debugString: String = PropertyNameEntry.this.debugString + def debugString: String = BaseEntry.this.debugString override def toString(): String = debugString } @@ -145,6 +156,10 @@ private[emitter] object NameCompressor { incOccurrences() resolver } + } + + private sealed abstract class PropertyNameEntry + extends BaseEntry with Comparable[PropertyNameEntry] { def compareTo(that: PropertyNameEntry): Int = (this, that) match { case (x: FieldNameEntry, y: FieldNameEntry) => @@ -187,6 +202,17 @@ private[emitter] object NameCompressor { override def toString(): String = s"ArrayClassPropEntry(${property.nonMinifiedName})" } + private final class AncestorNameEntry(val ancestor: ClassName) + extends BaseEntry with Comparable[AncestorNameEntry] { + + def compareTo(that: AncestorNameEntry): Int = + this.ancestor.compareTo(that.ancestor) + + protected def debugString: String = ancestor.nameString + + override def toString(): String = s"AncestorNameEntry(${ancestor.nameString})" + } + // private[emitter] for tests private[emitter] final class NameGenerator(namesToAvoid: collection.Set[String]) { /* 6 because 52 * (62**5) > Int.MaxValue 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 9e8810afc3..d1c46bc023 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 @@ -250,6 +250,13 @@ private[emitter] final class SJSGen( } } + def genAncestorIdent(ancestor: ClassName)(implicit pos: Position): MaybeDelayedIdent = { + nameCompressor match { + case None => Ident(genName(ancestor)) + case Some(compressor) => DelayedIdent(compressor.genResolverForAncestor(ancestor)) + } + } + def genJSPrivateSelect(receiver: Tree, field: irt.FieldIdent)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): Tree = { 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 33b1bd7ff4..2d9e678334 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -71,7 +71,7 @@ class LibrarySizeTest { testLinkedSizes( expectedFastLinkSize = 150063, - expectedFullLinkSizeWithoutClosure = 95680, + expectedFullLinkSizeWithoutClosure = 93868, expectedFullLinkSizeWithClosure = 21325, classDefs, moduleInitializers = MainTestModuleInitializers diff --git a/project/Build.scala b/project/Build.scala index ffcd864afe..de6ff233a9 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2005,10 +2005,10 @@ object Build { )) } else { Some(ExpectedSizes( - fastLink = 538000 to 539000, - fullLink = 371000 to 372000, - fastLinkGz = 71000 to 72000, - fullLinkGz = 51000 to 52000, + fastLink = 499000 to 500000, + fullLink = 341000 to 342000, + fastLinkGz = 69000 to 70000, + fullLinkGz = 50000 to 51000, )) } @@ -2022,10 +2022,10 @@ object Build { )) } else { Some(ExpectedSizes( - fastLink = 373000 to 374000, - fullLink = 332000 to 333000, - fastLinkGz = 55000 to 56000, - fullLinkGz = 50000 to 51000, + fastLink = 352000 to 353000, + fullLink = 312000 to 313000, + fastLinkGz = 54000 to 55000, + fullLinkGz = 49000 to 50000, )) } From 280870dec269cfe02ad14835f3affbe48f261ddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 25 Jan 2024 17:23:46 +0100 Subject: [PATCH 058/298] Minify core (internal) property names to one letter each. --- .../linker/backend/emitter/ClassEmitter.scala | 12 +- .../linker/backend/emitter/CoreJSLib.scala | 234 +++++++++--------- .../backend/emitter/FunctionEmitter.scala | 6 +- .../linker/backend/emitter/SJSGen.scala | 108 +++++++- .../org/scalajs/linker/LibrarySizeTest.scala | 2 +- project/Build.scala | 8 +- 6 files changed, 236 insertions(+), 134 deletions(-) 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 9fa8a09156..11f8c3df33 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 @@ -705,7 +705,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { !(!( genIsScalaJSObject(obj) && genIsClassNameInAncestors(className, - obj DOT "$classData" DOT "ancestors") + obj DOT cpn.classData DOT cpn.ancestors) )) } @@ -781,9 +781,9 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { globalFunctionDef(VarField.isArrayOf, className, List(objParam, depthParam), None, { js.Return(!(!({ genIsScalaJSObject(obj) && - ((obj DOT "$classData" DOT "arrayDepth") === depth) && + ((obj DOT cpn.classData DOT cpn.arrayDepth) === depth) && genIsClassNameInAncestors(className, - obj DOT "$classData" DOT "arrayBase" DOT "ancestors") + obj DOT cpn.classData DOT cpn.arrayBase DOT cpn.ancestors) }))) }) } @@ -814,7 +814,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { private def genIsScalaJSObject(obj: js.Tree)(implicit pos: Position): js.Tree = { import TreeDSL._ - obj && (obj DOT "$classData") + obj && (obj DOT cpn.classData) } private def genIsClassNameInAncestors(className: ClassName, @@ -915,7 +915,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val prunedParams = allParams.reverse.dropWhile(_.isInstanceOf[js.Undefined]).reverse - val typeData = js.Apply(js.New(globalVar(VarField.TypeData, CoreVar), Nil) DOT "initClass", + val typeData = js.Apply(js.New(globalVar(VarField.TypeData, CoreVar), Nil) DOT cpn.initClass, prunedParams) globalVarDef(VarField.d, className, typeData) @@ -927,7 +927,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { globalKnowledge: GlobalKnowledge, pos: Position): js.Tree = { import TreeDSL._ - globalVar(VarField.c, className).prototype DOT "$classData" := globalVar(VarField.d, className) + globalVar(VarField.c, className).prototype DOT cpn.classData := globalVar(VarField.d, className) } def genModuleAccessor(className: ClassName, isJSClass: Boolean)( 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 23e3242c30..6dc814cd87 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 @@ -106,7 +106,7 @@ private[emitter] object CoreJSLib { // Conditional global references that we often use private def ReflectRef = globalRef("Reflect") - private val classData = Ident("$classData") + private val classData = Ident(cpn.classData) private val orderedPrimRefsWithoutVoid = { List(BooleanRef, CharRef, ByteRef, ShortRef, IntRef, LongRef, @@ -522,7 +522,7 @@ private[emitter] object CoreJSLib { condDefs(!allowBigIntsForLongs)(List( globalVar(VarField.L0, CoreVar) := genScalaClassNew( LongImpl.RuntimeLongClass, LongImpl.initFromParts, 0, 0), - genClassDataOf(LongRef) DOT "zero" := globalVar(VarField.L0, CoreVar) + genClassDataOf(LongRef) DOT cpn.zero := globalVar(VarField.L0, CoreVar) )) } @@ -547,14 +547,14 @@ private[emitter] object CoreJSLib { val ctor = { val c = varRef("c") MethodDef(static = false, Ident("constructor"), paramList(c), None, { - This() DOT "c" := c + This() DOT cpn.c := c }) } val toStr = { MethodDef(static = false, Ident("toString"), Nil, None, { Return(Apply(genIdentBracketSelect(StringRef, "fromCharCode"), - (This() DOT "c") :: Nil)) + (This() DOT cpn.c) :: Nil)) }) } @@ -594,7 +594,7 @@ private[emitter] object CoreJSLib { str("char") }, { If(genIsScalaJSObject(value), { - genIdentBracketSelect(value DOT classData, "name") + genIdentBracketSelect(value DOT classData, cpn.name) }, { typeof(value) }) @@ -693,10 +693,10 @@ private[emitter] object CoreJSLib { val i = varRef("i") Block( - const(result, New(arrayClassData DOT "constr", + const(result, New(arrayClassData DOT cpn.constr, BracketSelect(lengths, lengthIndex) :: Nil)), If(lengthIndex < (lengths.length - 1), Block( - const(subArrayClassData, arrayClassData DOT "componentData"), + const(subArrayClassData, arrayClassData DOT cpn.componentData), const(subLengthIndex, lengthIndex + 1), const(underlying, result.u), For(let(i, 0), i < underlying.length, i.++, { @@ -719,7 +719,7 @@ private[emitter] object CoreJSLib { defineFunction1(VarField.objectOrArrayClone) { instance => // return instance.$classData.isArrayClass ? instance.clone__O() : $objectClone(instance); - Return(If(genIdentBracketSelect(instance DOT classData, "isArrayClass"), + Return(If(genIdentBracketSelect(instance DOT classData, cpn.isArrayClass), genApply(instance, cloneMethodName, Nil), genCallHelper(VarField.objectClone, instance))) } @@ -804,7 +804,7 @@ private[emitter] object CoreJSLib { condDefs(globalKnowledge.isClassClassInstantiated)( defineObjectGetClassBasedFun(VarField.objectGetClass, className => genClassOf(className), - instance => Apply(instance DOT classData DOT "getClassOf", Nil), + instance => Apply(instance DOT classData DOT cpn.getClassOf, Nil), Null() ) ) ::: @@ -813,7 +813,7 @@ private[emitter] object CoreJSLib { StringLiteral(RuntimeClassNameMapperImpl.map( semantics.runtimeClassNameMapper, className.nameString)) }, - instance => genIdentBracketSelect(instance DOT classData, "name"), + instance => genIdentBracketSelect(instance DOT classData, cpn.name), { if (nullPointers == CheckedBehavior.Unchecked) genApply(Null(), getNameMethodName, Nil) @@ -1137,7 +1137,7 @@ private[emitter] object CoreJSLib { condDefs(arrayStores != CheckedBehavior.Unchecked)( defineFunction5(VarField.systemArraycopyRefs) { (src, srcPos, dest, destPos, length) => - If(Apply(genIdentBracketSelect(dest DOT classData, "isAssignableFrom"), List(src DOT classData)), { + If(Apply(genIdentBracketSelect(dest DOT classData, 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`. */ @@ -1170,7 +1170,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, "isArrayClass"), { + 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) @@ -1387,7 +1387,7 @@ private[emitter] object CoreJSLib { ( defineUnbox(VarField.uV, BoxedUnitClass, _ => Undefined()) ::: defineUnbox(VarField.uZ, BoxedBooleanClass, v => !(!v)) ::: - defineUnbox(VarField.uC, BoxedCharacterClass, v => If(v === Null(), 0, v DOT "c")) ::: + defineUnbox(VarField.uC, BoxedCharacterClass, v => If(v === Null(), 0, v DOT cpn.c)) ::: defineUnbox(VarField.uB, BoxedByteClass, _ | 0) ::: defineUnbox(VarField.uS, BoxedShortClass, _ | 0) ::: defineUnbox(VarField.uI, BoxedIntegerClass, _ | 0) ::: @@ -1405,7 +1405,7 @@ private[emitter] object CoreJSLib { // Unboxes for Chars and Longs ( defineFunction1(VarField.uC) { v => - Return(If(v === Null(), 0, v DOT "c")) + Return(If(v === Null(), 0, v DOT cpn.c)) } ::: defineFunction1(VarField.uJ) { v => Return(If(v === Null(), genLongZero(), v)) @@ -1585,34 +1585,34 @@ private[emitter] object CoreJSLib { val ctor = { MethodDef(static = false, Ident("constructor"), Nil, None, { Block( - privateFieldSet("constr", Undefined()), + privateFieldSet(cpn.constr, Undefined()), if (globalKnowledge.isParentDataAccessed) - privateFieldSet("parentData", Undefined()) + privateFieldSet(cpn.parentData, Undefined()) else Skip(), - privateFieldSet("ancestors", Null()), - privateFieldSet("componentData", Null()), - privateFieldSet("arrayBase", Null()), - privateFieldSet("arrayDepth", int(0)), - privateFieldSet("zero", Null()), - privateFieldSet("arrayEncodedName", str("")), - privateFieldSet("_classOf", Undefined()), - privateFieldSet("_arrayOf", Undefined()), + privateFieldSet(cpn.ancestors, Null()), + privateFieldSet(cpn.componentData, Null()), + privateFieldSet(cpn.arrayBase, Null()), + privateFieldSet(cpn.arrayDepth, int(0)), + privateFieldSet(cpn.zero, Null()), + privateFieldSet(cpn.arrayEncodedName, str("")), + privateFieldSet(cpn._classOf, Undefined()), + privateFieldSet(cpn._arrayOf, Undefined()), /* A lambda for the logic of the public `isAssignableFrom`, * without its fast-path. See the comment on the definition of * `isAssignableFrom` for the rationale of this decomposition. */ - privateFieldSet("isAssignableFromFun", Undefined()), + privateFieldSet(cpn.isAssignableFromFun, Undefined()), - privateFieldSet("wrapArray", Undefined()), - privateFieldSet("isJSType", bool(false)), + privateFieldSet(cpn.wrapArray, Undefined()), + privateFieldSet(cpn.isJSType, bool(false)), - publicFieldSet("name", str("")), - publicFieldSet("isPrimitive", bool(false)), - publicFieldSet("isInterface", bool(false)), - publicFieldSet("isArrayClass", bool(false)), - publicFieldSet("isInstance", Undefined()) + publicFieldSet(cpn.name, str("")), + publicFieldSet(cpn.isPrimitive, bool(false)), + publicFieldSet(cpn.isInterface, bool(false)), + publicFieldSet(cpn.isArrayClass, bool(false)), + publicFieldSet(cpn.isInstance, Undefined()) ) }) } @@ -1627,22 +1627,22 @@ private[emitter] object CoreJSLib { val that = varRef("that") val depth = varRef("depth") val obj = varRef("obj") - MethodDef(static = false, Ident("initPrim"), + MethodDef(static = false, Ident(cpn.initPrim), paramList(zero, arrayEncodedName, displayName, arrayClass, typedArrayClass), None, { Block( - privateFieldSet("ancestors", ObjectConstr(Nil)), - privateFieldSet("zero", zero), - privateFieldSet("arrayEncodedName", arrayEncodedName), + privateFieldSet(cpn.ancestors, ObjectConstr(Nil)), + privateFieldSet(cpn.zero, zero), + privateFieldSet(cpn.arrayEncodedName, arrayEncodedName), const(self, This()), // capture `this` for use in arrow fun - privateFieldSet("isAssignableFromFun", + privateFieldSet(cpn.isAssignableFromFun, genArrowFunction(paramList(that), Return(that === self))), - publicFieldSet("name", displayName), - publicFieldSet("isPrimitive", bool(true)), - publicFieldSet("isInstance", + publicFieldSet(cpn.name, displayName), + publicFieldSet(cpn.isPrimitive, bool(true)), + publicFieldSet(cpn.isInstance, genArrowFunction(paramList(obj), Return(bool(false)))), If(arrayClass !== Undefined(), { // it is undefined for void - privateFieldSet("_arrayOf", - Apply(New(globalVar(VarField.TypeData, CoreVar), Nil) DOT "initSpecializedArray", + privateFieldSet(cpn._arrayOf, + Apply(New(globalVar(VarField.TypeData, CoreVar), Nil) DOT cpn.initSpecializedArray, List(This(), arrayClass, typedArrayClass))) }), Return(This()) @@ -1662,29 +1662,29 @@ private[emitter] object CoreJSLib { val that = varRef("that") val depth = varRef("depth") val obj = varRef("obj") - MethodDef(static = false, Ident("initClass"), + MethodDef(static = false, Ident(cpn.initClass), paramList(internalNameObj, isInterface, fullName, ancestors, isJSType, parentData, isInstance), None, { Block( const(internalName, genCallHelper(VarField.propertyName, internalNameObj)), if (globalKnowledge.isParentDataAccessed) - privateFieldSet("parentData", parentData) + privateFieldSet(cpn.parentData, parentData) else Skip(), - privateFieldSet("ancestors", ancestors), - privateFieldSet("arrayEncodedName", str("L") + fullName + str(";")), - privateFieldSet("isAssignableFromFun", { + privateFieldSet(cpn.ancestors, ancestors), + privateFieldSet(cpn.arrayEncodedName, str("L") + fullName + str(";")), + privateFieldSet(cpn.isAssignableFromFun, { genArrowFunction(paramList(that), { - Return(!(!(BracketSelect(that DOT "ancestors", internalName)))) + Return(!(!(BracketSelect(that DOT cpn.ancestors, internalName)))) }) }), - privateFieldSet("isJSType", !(!isJSType)), - publicFieldSet("name", fullName), - publicFieldSet("isInterface", isInterface), - publicFieldSet("isInstance", isInstance || { + privateFieldSet(cpn.isJSType, !(!isJSType)), + publicFieldSet(cpn.name, fullName), + publicFieldSet(cpn.isInterface, isInterface), + publicFieldSet(cpn.isInstance, isInstance || { genArrowFunction(paramList(obj), { Return(!(!(obj && (obj DOT classData) && - BracketSelect(obj DOT classData DOT "ancestors", internalName)))) + BracketSelect(obj DOT classData DOT cpn.ancestors, internalName)))) }) }), Return(This()) @@ -1698,22 +1698,22 @@ private[emitter] object CoreJSLib { Block( arrayClass.prototype DOT classData := This(), - const(name, str("[") + (componentData DOT "arrayEncodedName")), - privateFieldSet("constr", arrayClass), + const(name, str("[") + (componentData DOT cpn.arrayEncodedName)), + privateFieldSet(cpn.constr, arrayClass), if (globalKnowledge.isParentDataAccessed) - privateFieldSet("parentData", genClassDataOf(ObjectClass)) + privateFieldSet(cpn.parentData, genClassDataOf(ObjectClass)) else Skip(), - privateFieldSet("ancestors", ObjectConstr(List( + privateFieldSet(cpn.ancestors, ObjectConstr(List( genAncestorIdent(CloneableClass) -> 1, genAncestorIdent(SerializableClass) -> 1 ))), - privateFieldSet("componentData", componentData), - privateFieldSet("arrayBase", arrayBase), - privateFieldSet("arrayDepth", arrayDepth), - privateFieldSet("arrayEncodedName", name), - publicFieldSet("name", name), - publicFieldSet("isArrayClass", bool(true)) + privateFieldSet(cpn.componentData, componentData), + privateFieldSet(cpn.arrayBase, arrayBase), + privateFieldSet(cpn.arrayDepth, arrayDepth), + privateFieldSet(cpn.arrayEncodedName, name), + publicFieldSet(cpn.name, name), + publicFieldSet(cpn.isArrayClass, bool(true)) ) } @@ -1726,15 +1726,15 @@ private[emitter] object CoreJSLib { val that = varRef("that") val obj = varRef("obj") val array = varRef("array") - MethodDef(static = false, Ident("initSpecializedArray"), + MethodDef(static = false, Ident(cpn.initSpecializedArray), paramList(componentData, arrayClass, typedArrayClass, isAssignableFromFun), None, { Block( initArrayCommonBody(arrayClass, componentData, componentData, 1), const(self, This()), // capture `this` for use in arrow fun - privateFieldSet("isAssignableFromFun", isAssignableFromFun || { + privateFieldSet(cpn.isAssignableFromFun, isAssignableFromFun || { genArrowFunction(paramList(that), Return(self === that)) }), - privateFieldSet("wrapArray", { + privateFieldSet(cpn.wrapArray, { If(typedArrayClass, { genArrowFunction(paramList(array), { Return(New(arrayClass, New(typedArrayClass, array :: Nil) :: Nil)) @@ -1745,7 +1745,7 @@ private[emitter] object CoreJSLib { }) }) }), - publicFieldSet("isInstance", + publicFieldSet(cpn.isInstance, genArrowFunction(paramList(obj), Return(obj instanceof arrayClass))), Return(This()) ) @@ -1762,7 +1762,7 @@ private[emitter] object CoreJSLib { val self = varRef("self") val obj = varRef("obj") val array = varRef("array") - MethodDef(static = false, Ident("initArray"), + MethodDef(static = false, Ident(cpn.initArray), paramList(componentData), None, { val ArrayClassDef = { val ctor = { @@ -1788,8 +1788,8 @@ private[emitter] object CoreJSLib { } val storeCheck = { - If((v !== Null()) && !(componentData DOT "isJSType") && - !Apply(genIdentBracketSelect(componentData, "isInstance"), v :: Nil), + If((v !== Null()) && !(componentData DOT cpn.isJSType) && + !Apply(genIdentBracketSelect(componentData, cpn.isInstance), v :: Nil), genCallHelper(VarField.throwArrayStoreException, v)) } @@ -1847,28 +1847,28 @@ private[emitter] object CoreJSLib { Block( ArrayClassDef, - const(arrayBase, (componentData DOT "arrayBase") || componentData), - const(arrayDepth, (componentData DOT "arrayDepth") + 1), + const(arrayBase, (componentData DOT cpn.arrayBase) || componentData), + const(arrayDepth, (componentData DOT cpn.arrayDepth) + 1), initArrayCommonBody(ArrayClass, componentData, arrayBase, arrayDepth), const(isAssignableFromFun, { genArrowFunction(paramList(that), { val thatDepth = varRef("thatDepth") Block( - const(thatDepth, that DOT "arrayDepth"), + const(thatDepth, that DOT cpn.arrayDepth), Return(If(thatDepth === arrayDepth, { - Apply(arrayBase DOT "isAssignableFromFun", (that DOT "arrayBase") :: Nil) + Apply(arrayBase DOT cpn.isAssignableFromFun, (that DOT cpn.arrayBase) :: Nil) }, { (thatDepth > arrayDepth) && (arrayBase === genClassDataOf(ObjectClass)) })) ) }) }), - privateFieldSet("isAssignableFromFun", isAssignableFromFun), - privateFieldSet("wrapArray", genArrowFunction(paramList(array), { + privateFieldSet(cpn.isAssignableFromFun, isAssignableFromFun), + privateFieldSet(cpn.wrapArray, genArrowFunction(paramList(array), { Return(New(ArrayClass, array :: Nil)) })), const(self, This()), // don't rely on the lambda being called with `this` as receiver - publicFieldSet("isInstance", genArrowFunction(paramList(obj), { + publicFieldSet(cpn.isInstance, genArrowFunction(paramList(obj), { val data = varRef("data") Block( const(data, obj && (obj DOT classData)), @@ -1884,24 +1884,24 @@ private[emitter] object CoreJSLib { } val getArrayOf = { - MethodDef(static = false, Ident("getArrayOf"), Nil, None, { + MethodDef(static = false, Ident(cpn.getArrayOf), Nil, None, { Block( - If(!(This() DOT "_arrayOf"), - This() DOT "_arrayOf" := - Apply(New(globalVar(VarField.TypeData, CoreVar), Nil) DOT "initArray", This() :: Nil), + If(!(This() DOT cpn._arrayOf), + This() DOT cpn._arrayOf := + Apply(New(globalVar(VarField.TypeData, CoreVar), Nil) DOT cpn.initArray, This() :: Nil), Skip()), - Return(This() DOT "_arrayOf") + Return(This() DOT cpn._arrayOf) ) }) } def getClassOf = { - MethodDef(static = false, Ident("getClassOf"), Nil, None, { + MethodDef(static = false, Ident(cpn.getClassOf), Nil, None, { Block( - If(!(This() DOT "_classOf"), - This() DOT "_classOf" := genScalaClassNew(ClassClass, ObjectArgConstructorName, This()), + If(!(This() DOT cpn._classOf), + This() DOT cpn._classOf := genScalaClassNew(ClassClass, ObjectArgConstructorName, This()), Skip()), - Return(This() DOT "_classOf") + Return(This() DOT cpn._classOf) ) }) } @@ -1917,21 +1917,21 @@ private[emitter] object CoreJSLib { * We only need a polymorphic dispatch in the slow path. */ val that = varRef("that") - MethodDef(static = false, StringLiteral("isAssignableFrom"), + MethodDef(static = false, StringLiteral(cpn.isAssignableFrom), paramList(that), None, { Return( (This() === that) || // fast path - Apply(This() DOT "isAssignableFromFun", that :: Nil)) + Apply(This() DOT cpn.isAssignableFromFun, that :: Nil)) }) } def checkCast = { val obj = varRef("obj") - MethodDef(static = false, StringLiteral("checkCast"), paramList(obj), None, + MethodDef(static = false, StringLiteral(cpn.checkCast), paramList(obj), None, if (asInstanceOfs != CheckedBehavior.Unchecked) { - If((obj !== Null()) && !(This() DOT "isJSType") && - !Apply(genIdentBracketSelect(This(), "isInstance"), obj :: Nil), - genCallHelper(VarField.throwClassCastException, obj, genIdentBracketSelect(This(), "name")), + 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() @@ -1940,17 +1940,17 @@ private[emitter] object CoreJSLib { } def getSuperclass = { - MethodDef(static = false, StringLiteral("getSuperclass"), Nil, None, { - Return(If(This() DOT "parentData", - Apply(This() DOT "parentData" DOT "getClassOf", Nil), + MethodDef(static = false, StringLiteral(cpn.getSuperclass), Nil, None, { + Return(If(This() DOT cpn.parentData, + Apply(This() DOT cpn.parentData DOT cpn.getClassOf, Nil), Null())) }) } def getComponentType = { - MethodDef(static = false, StringLiteral("getComponentType"), Nil, None, { - Return(If(This() DOT "componentData", - Apply(This() DOT "componentData" DOT "getClassOf", Nil), + MethodDef(static = false, StringLiteral(cpn.getComponentType), Nil, None, { + Return(If(This() DOT cpn.componentData, + Apply(This() DOT cpn.componentData DOT cpn.getClassOf, Nil), Null())) }) } @@ -1959,12 +1959,12 @@ private[emitter] object CoreJSLib { val lengths = varRef("lengths") val arrayClassData = varRef("arrayClassData") val i = varRef("i") - MethodDef(static = false, StringLiteral("newArrayOfThisClass"), + 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 "getArrayOf", Nil) + arrayClassData := Apply(arrayClassData DOT cpn.getArrayOf, Nil) }), Return(genCallHelper(VarField.newArrayObject, arrayClassData, lengths)) ) @@ -2013,14 +2013,14 @@ private[emitter] object CoreJSLib { val forObj = extractWithGlobals(globalFunctionDef(VarField.isArrayOf, ObjectClass, paramList(obj, depth), None, { Block( - const(data, obj && (obj DOT "$classData")), + const(data, obj && (obj DOT cpn.classData)), If(!data, { Return(BooleanLiteral(false)) }, { Block( - const(arrayDepth, data DOT "arrayDepth"), + const(arrayDepth, data DOT cpn.arrayDepth), Return(If(arrayDepth === depth, { - !genIdentBracketSelect(data DOT "arrayBase", "isPrimitive") + !genIdentBracketSelect(data DOT cpn.arrayBase, cpn.isPrimitive) }, { arrayDepth > depth })) @@ -2034,8 +2034,8 @@ private[emitter] object CoreJSLib { val depth = varRef("depth") extractWithGlobals(globalFunctionDef(VarField.isArrayOf, primRef, paramList(obj, depth), None, { Return(!(!(obj && (obj DOT classData) && - ((obj DOT classData DOT "arrayDepth") === depth) && - ((obj DOT classData DOT "arrayBase") === genClassDataOf(primRef))))) + ((obj DOT classData DOT cpn.arrayDepth) === depth) && + ((obj DOT classData DOT cpn.arrayBase) === genClassDataOf(primRef))))) })) } @@ -2089,27 +2089,27 @@ private[emitter] object CoreJSLib { extractWithGlobals( globalVarDef(VarField.d, ObjectClass, New(globalVar(VarField.TypeData, CoreVar), Nil))) ::: List( - privateFieldSet("ancestors", ObjectConstr(Nil)), - privateFieldSet("arrayEncodedName", str("L" + fullName + ";")), - privateFieldSet("isAssignableFromFun", { + privateFieldSet(cpn.ancestors, ObjectConstr(Nil)), + privateFieldSet(cpn.arrayEncodedName, str("L" + fullName + ";")), + privateFieldSet(cpn.isAssignableFromFun, { genArrowFunction(paramList(that), { - Return(!genIdentBracketSelect(that, "isPrimitive")) + Return(!genIdentBracketSelect(that, cpn.isPrimitive)) }) }), - publicFieldSet("name", str(fullName)), - publicFieldSet("isInstance", + publicFieldSet(cpn.name, str(fullName)), + publicFieldSet(cpn.isInstance, genArrowFunction(paramList(obj), Return(obj !== Null()))), - privateFieldSet("_arrayOf", { - Apply(New(globalVar(VarField.TypeData, CoreVar), Nil) DOT "initSpecializedArray", List( + privateFieldSet(cpn._arrayOf, { + Apply(New(globalVar(VarField.TypeData, CoreVar), Nil) DOT cpn.initSpecializedArray, List( typeDataVar, globalVar(VarField.ac, ObjectClass), Undefined(), // typedArray genArrowFunction(paramList(that), { val thatDepth = varRef("thatDepth") Block( - const(thatDepth, that DOT "arrayDepth"), + const(thatDepth, that DOT cpn.arrayDepth), Return(If(thatDepth === 1, { - !genIdentBracketSelect(that DOT "arrayBase", "isPrimitive") + !genIdentBracketSelect(that DOT cpn.arrayBase, cpn.isPrimitive) }, { (thatDepth > 1) })) @@ -2117,7 +2117,7 @@ private[emitter] object CoreJSLib { }) )) }), - globalVar(VarField.c, ObjectClass).prototype DOT "$classData" := typeDataVar + globalVar(VarField.c, ObjectClass).prototype DOT cpn.classData := typeDataVar ) } @@ -2143,7 +2143,7 @@ private[emitter] object CoreJSLib { } extractWithGlobals(globalVarDef(VarField.d, primRef, { - Apply(New(globalVar(VarField.TypeData, CoreVar), Nil) DOT "initPrim", + Apply(New(globalVar(VarField.TypeData, CoreVar), Nil) DOT cpn.initPrim, List(zero, str(primRef.charCode.toString()), str(primRef.displayName), if (primRef == VoidRef) Undefined() 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 6a8b9ca3dd..a518948b97 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 @@ -2744,7 +2744,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { js.DotSelect( genSelect(transformExprNoChar(checkNotNull(runtimeClass)), FieldIdent(dataFieldName)), - js.Ident("zero")) + js.Ident(cpn.zero)) case Transient(NativeArrayWrapper(elemClass, nativeArray)) => val newNativeArray = transformExprNoChar(nativeArray) @@ -2758,8 +2758,8 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { transformExprNoChar(checkNotNull(elemClass)), FieldIdent(dataFieldName)) val arrayClassData = js.Apply( - js.DotSelect(elemClassData, js.Ident("getArrayOf")), Nil) - js.Apply(arrayClassData DOT "wrapArray", newNativeArray :: Nil) + js.DotSelect(elemClassData, js.Ident(cpn.getArrayOf)), Nil) + js.Apply(arrayClassData DOT cpn.wrapArray, newNativeArray :: Nil) } case Transient(ObjectClassName(obj)) => 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 d1c46bc023..b52be22348 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 @@ -43,6 +43,108 @@ private[emitter] final class SJSGen( val useBigIntForLongs = esFeatures.allowBigIntsForLongs + /** Core Property Names. */ + object cpn { + // --- Scala.js objects --- + + /** The class-wide classData field of Scala.js objects, which references their TypeData. */ + val classData = "$classData" // always in full; it is used as identification of Scala.js objects + + // --- Class --- + + /** `Char.c`: the int value of the character. */ + val c = "c" + + // --- TypeData private fields --- + + /** `TypeData.constr`: the run-time constructor of the class. */ + val constr = if (minify) "C" else "constr" + + /** `TypeData.parentData`: the super class data. */ + val parentData = if (minify) "P" else "parentData" + + /** `TypeData.ancestors`: dictionary where keys are the ancestor names of all ancestors. */ + val ancestors = if (minify) "n" else "ancestors" + + /** `TypeData.componentData`: the `TypeData` of the component type of an array type. */ + val componentData = if (minify) "O" else "componentData" + + /** `TypeData.arrayBase`: the `TypeData` of the base type of an array type. */ + val arrayBase = if (minify) "B" else "arrayBase" + + /** `TypeData.arrayDepth`: the depth of an array type. */ + val arrayDepth = if (minify) "D" else "arrayDepth" + + /** `TypeData.zero`: the zero value of the type. */ + val zero = if (minify) "z" else "zero" + + /** `TypeData.arrayEncodedName`: the name of the type as it appears in its array type's name. */ + val arrayEncodedName = if (minify) "E" else "arrayEncodedName" + + /** `TypeData._classOf`: the field storing the `jl.Class` instance for that type. */ + val _classOf = if (minify) "L" else "_classOf" + + /** `TypeData._arrayOf`: the field storing the `TypeData` for that type's array type. */ + val _arrayOf = if (minify) "A" else "_arrayOf" + + /** `TypeData.isAssignableFromFun`: the implementation of `jl.Class.isAssignableFrom` without fast path. */ + val isAssignableFromFun = if (minify) "F" else "isAssignableFromFun" + + /** `TypeData.wrapArray`: the function to create an ArrayClass instance from a JS array of its elements. */ + val wrapArray = if (minify) "w" else "wrapArray" + + /** `TypeData.isJSType`: whether it is a JS type. */ + val isJSType = if (minify) "J" else "isJSType" + + // --- TypeData constructors --- + + val initPrim = if (minify) "p" else "initPrim" + + val initClass = if (minify) "i" else "initClass" + + val initSpecializedArray = if (minify) "y" else "initSpecializedArray" + + val initArray = if (minify) "a" else "initArray" + + // --- TypeData private methods --- + + /** `TypeData.getArrayOf()`: the `Type` instance for that type's array type. */ + val getArrayOf = if (minify) "r" else "getArrayOf" + + /** `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.isInstance()`: public, implementation of `jl.Class.isInstance`. */ + val isInstance = "isInstance" + + /** `TypeData.isAssignableFrom()`: public, implementation of `jl.Class.isAssignableFrom`. */ + val isAssignableFrom = "isAssignableFrom" + + // --- TypeData public methods --- never minified + + val checkCast = "checkCast" + + val getSuperclass = "getSuperclass" + + val getComponentType = "getComponentType" + + val newArrayOfThisClass = "newArrayOfThisClass" + } + def genZeroOf(tpe: Type)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): Tree = { @@ -627,14 +729,14 @@ private[emitter] final class SJSGen( case ArrayTypeRef(ClassRef(ObjectClass), 1) => globalVar(VarField.ac, ObjectClass) case _ => - genClassDataOf(arrayTypeRef) DOT "constr" + genClassDataOf(arrayTypeRef) DOT cpn.constr } } def genClassOf(typeRef: TypeRef)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): Tree = { - Apply(DotSelect(genClassDataOf(typeRef), Ident("getClassOf")), Nil) + Apply(DotSelect(genClassDataOf(typeRef), Ident(cpn.getClassOf)), Nil) } def genClassOf(className: ClassName)( @@ -653,7 +755,7 @@ private[emitter] final class SJSGen( case ArrayTypeRef(base, dims) => val baseData = genClassDataOf(base) (1 to dims).foldLeft[Tree](baseData) { (prev, _) => - Apply(DotSelect(prev, Ident("getArrayOf")), Nil) + Apply(DotSelect(prev, Ident(cpn.getArrayOf)), Nil) } } } 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 2d9e678334..dca2b88b47 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -71,7 +71,7 @@ class LibrarySizeTest { testLinkedSizes( expectedFastLinkSize = 150063, - expectedFullLinkSizeWithoutClosure = 93868, + expectedFullLinkSizeWithoutClosure = 92648, expectedFullLinkSizeWithClosure = 21325, classDefs, moduleInitializers = MainTestModuleInitializers diff --git a/project/Build.scala b/project/Build.scala index de6ff233a9..c6ae6168e5 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2005,8 +2005,8 @@ object Build { )) } else { Some(ExpectedSizes( - fastLink = 499000 to 500000, - fullLink = 341000 to 342000, + fastLink = 494000 to 495000, + fullLink = 337000 to 338000, fastLinkGz = 69000 to 70000, fullLinkGz = 50000 to 51000, )) @@ -2022,8 +2022,8 @@ object Build { )) } else { Some(ExpectedSizes( - fastLink = 352000 to 353000, - fullLink = 312000 to 313000, + fastLink = 347000 to 348000, + fullLink = 307000 to 308000, fastLinkGz = 54000 to 55000, fullLinkGz = 49000 to 50000, )) From 1afad42e64cf801c7cc90a55afaf942e1a5aa25e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 24 Feb 2024 10:07:41 +0100 Subject: [PATCH 059/298] Fix #4949: Always wrap object literals with `()`. --- .../linker/backend/javascript/Printers.scala | 21 ++++++++++--------- .../org/scalajs/linker/LibrarySizeTest.scala | 4 ++-- project/Build.scala | 10 ++++----- .../testsuite/jsinterop/DynamicTest.scala | 14 ++++++++++--- 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala index 09a3b8648a..33ec9cc020 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala @@ -491,15 +491,17 @@ object Printers { printSeparatorIfStat() case ObjectConstr(Nil) => - if (isStat) - print("({});") // force expression position for the object literal - else - print("{}") + /* #4949 Always wrap object literals with () in case they end up at + * the start of an `ExpressionStatement`. + */ + print("({})") + printSeparatorIfStat() case ObjectConstr(fields) => - if (isStat) - print('(') // force expression position for the object literal - print('{') + /* #4949 Always wrap object literals with () in case they end up at + * the start of an `ExpressionStatement`. + */ + print("({") indent() println() var rest = fields @@ -517,9 +519,8 @@ object Printers { } undent() printIndent() - print('}') - if (isStat) - print(");") + print("})") + printSeparatorIfStat() // Literals 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 dca2b88b47..fbc5b93948 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -70,8 +70,8 @@ class LibrarySizeTest { ) testLinkedSizes( - expectedFastLinkSize = 150063, - expectedFullLinkSizeWithoutClosure = 92648, + expectedFastLinkSize = 150205, + expectedFullLinkSizeWithoutClosure = 92762, expectedFullLinkSizeWithClosure = 21325, classDefs, moduleInitializers = MainTestModuleInitializers diff --git a/project/Build.scala b/project/Build.scala index c6ae6168e5..f6d3d596ab 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1998,14 +1998,14 @@ object Build { case `default212Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 640000 to 641000, + fastLink = 641000 to 642000, fullLink = 101000 to 102000, fastLinkGz = 77000 to 78000, fullLinkGz = 26000 to 27000, )) } else { Some(ExpectedSizes( - fastLink = 494000 to 495000, + fastLink = 495000 to 496000, fullLink = 337000 to 338000, fastLinkGz = 69000 to 70000, fullLinkGz = 50000 to 51000, @@ -2015,15 +2015,15 @@ object Build { case `default213Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 462000 to 463000, + fastLink = 463000 to 464000, fullLink = 99000 to 100000, fastLinkGz = 60000 to 61000, fullLinkGz = 26000 to 27000, )) } else { Some(ExpectedSizes( - fastLink = 347000 to 348000, - fullLink = 307000 to 308000, + fastLink = 348000 to 349000, + fullLink = 308000 to 309000, fastLinkGz = 54000 to 55000, fullLinkGz = 49000 to 50000, )) diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/DynamicTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/DynamicTest.scala index 27b7f224c1..bd23946f2e 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/DynamicTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/DynamicTest.scala @@ -109,11 +109,19 @@ class DynamicTest { assertJSUndefined(obj_anything) } - @Test def objectLiteralInStatementPosition_Issue1627(): Unit = { - // Just make sure it does not cause a SyntaxError + @Test def objectLiteralInStatementPosition_Issue1627_Issue4949(): Unit = { + @noinline def dynProp(): String = "foo" + + // Just make sure those statements do not cause a SyntaxError js.Dynamic.literal(foo = "bar") - // and also test the case without param (different code path in Printers) js.Dynamic.literal() + js.Dynamic.literal(foo = "bar").foo + js.Dynamic.literal(foo = () => "bar").foo() + js.Dynamic.literal(foo = "bar").foo = "babar" + js.Dynamic.literal(foo = "foo").selectDynamic(dynProp()) + js.Dynamic.literal(foo = "foo").updateDynamic(dynProp())("babar") + js.Dynamic.literal(foo = () => "bar").applyDynamic(dynProp())() + js.Dynamic.literal(foo = "bar") + js.Dynamic.literal(foobar = "babar") } @Test def objectLiteralConstructionWithDynamicNaming(): Unit = { From e8b7dd7b9f59ed47eee939b5bd8d395327b10102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 25 Feb 2024 11:26:58 +0100 Subject: [PATCH 060/298] Add some tests for printing of `ObjectConstr` nodes. --- .../backend/javascript/PrintersTest.scala | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala index a2a8108fa8..2c5de62dd1 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/backend/javascript/PrintersTest.scala @@ -163,6 +163,34 @@ class PrintersTest { ) } + @Test def printObjectLiteral(): Unit = { + assertPrintEquals("({});", ObjectConstr(Nil)) + + assertPrintEquals( + """ + |({ + | "foo": 1 + |}); + """, + ObjectConstr(List(StringLiteral("foo") -> IntLiteral(1))) + ) + + assertPrintEquals( + """ + |({ + | "foo": 1, + | ["bar"]: 2, + | baz: 3 + |}); + """, + ObjectConstr(List( + StringLiteral("foo") -> IntLiteral(1), + ComputedName(StringLiteral("bar")) -> IntLiteral(2), + Ident("baz") -> IntLiteral(3) + )) + ) + } + @Test def delayedIdentPrintVersusShow(): Unit = { locally { object resolver extends DelayedIdent.Resolver { From 8b772e219c29f1bbc0336da71b8b40c9d4d22afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 26 Jan 2024 17:43:51 +0100 Subject: [PATCH 061/298] Under Minify, assign prototypes temporarily through `$p`. When minifying, a susprisingly large amount of bytes in the resulting .js file are caused by the `prototype`s in: C.prototype.f = function(...) { ... }; C.prototype.g = function(...) { ... }; We can get rid of them by assigning `C.prototype` once to a temporary variable, then reusing it many times: $p = C.prototype; $p.f = function(...) { ... }; $p.f = function(...) { ... }; This commit implements that strategy when the `minify` config is on. --- .../linker/backend/emitter/ClassEmitter.scala | 20 +++++------ .../linker/backend/emitter/CoreJSLib.scala | 27 +++++++++------ .../linker/backend/emitter/Emitter.scala | 10 ++++++ .../linker/backend/emitter/SJSGen.scala | 33 +++++++++++++++++++ .../linker/backend/emitter/VarField.scala | 5 ++- .../org/scalajs/linker/LibrarySizeTest.scala | 2 +- project/Build.scala | 16 ++++----- 7 files changed, 83 insertions(+), 30 deletions(-) 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 11f8c3df33..8b50efda5e 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 @@ -186,13 +186,13 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val chainProtoWithGlobals = superClass match { case None => - WithGlobals.nil + WithGlobals(setPrototypeVar(ctorVar)) case Some(_) if shouldExtendJSError(className, superClass) => - globalRef("Error").map(chainPrototypeWithLocalCtor(className, ctorVar, _)) + globalRef("Error").map(chainPrototypeWithLocalCtor(className, ctorVar, _, localDeclPrototypeVar = false)) case Some(parentIdent) => - WithGlobals(List(ctorVar.prototype := js.New(globalVar(VarField.h, parentIdent.name), Nil))) + WithGlobals(List(genAssignPrototype(ctorVar, js.New(globalVar(VarField.h, parentIdent.name), Nil)))) } for { @@ -208,12 +208,12 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { js.JSDocConstructor(realCtorDef.head) :: realCtorDef.tail ::: chainProto ::: - (genIdentBracketSelect(ctorVar.prototype, "constructor") := ctorVar) :: + (genIdentBracketSelect(prototypeFor(ctorVar), "constructor") := ctorVar) :: // Inheritable constructor js.JSDocConstructor(inheritableCtorDef.head) :: inheritableCtorDef.tail ::: - (globalVar(VarField.h, className).prototype := ctorVar.prototype) :: Nil + (globalVar(VarField.h, className).prototype := prototypeFor(ctorVar)) :: Nil ) } } @@ -243,8 +243,8 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val ctorVar = fileLevelVar(VarField.b, genName(className)) js.JSDocConstructor(ctorVar := ctorFun) :: - chainPrototypeWithLocalCtor(className, ctorVar, superCtor) ::: - (genIdentBracketSelect(ctorVar.prototype, "constructor") := ctorVar) :: Nil + chainPrototypeWithLocalCtor(className, ctorVar, superCtor, localDeclPrototypeVar = true) ::: + (genIdentBracketSelect(prototypeFor(ctorVar), "constructor") := ctorVar) :: Nil } } } @@ -333,7 +333,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } private def chainPrototypeWithLocalCtor(className: ClassName, ctorVar: js.Tree, - superCtor: js.Tree)(implicit pos: Position): List[js.Tree] = { + superCtor: js.Tree, localDeclPrototypeVar: Boolean)(implicit pos: Position): List[js.Tree] = { import TreeDSL._ val dummyCtor = fileLevelVar(VarField.hh, genName(className)) @@ -341,7 +341,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { List( js.JSDocConstructor(genConst(dummyCtor.ident, js.Function(false, Nil, None, js.Skip()))), dummyCtor.prototype := superCtor.prototype, - ctorVar.prototype := js.New(dummyCtor, Nil) + genAssignPrototype(ctorVar, js.New(dummyCtor, Nil), localDeclPrototypeVar) ) } @@ -638,7 +638,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { else globalVar(VarField.c, className) if (namespace.isStatic) classVarRef - else classVarRef.prototype + else prototypeFor(classVarRef) } def genMemberNameTree(name: Tree)( 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 6dc814cd87..43d58795ad 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 @@ -562,6 +562,7 @@ private[emitter] object CoreJSLib { extractWithGlobals(globalClassDef(VarField.Char, CoreVar, None, ctor :: toStr :: Nil)) } else { defineFunction(VarField.Char, ctor.args, ctor.body) ::: + setPrototypeVar(globalVar(VarField.Char, CoreVar)) ::: assignES5ClassMembers(globalVar(VarField.Char, CoreVar), List(toStr)) } } @@ -1528,8 +1529,8 @@ private[emitter] object CoreJSLib { val clsDef = { extractWithGlobals(globalFunctionDef(VarField.ac, componentTypeRef, ctor.args, ctor.restParam, ctor.body)) ::: - (ArrayClass.prototype := New(globalVar(VarField.h, ObjectClass), Nil)) :: - (ArrayClass.prototype DOT "constructor" := ArrayClass) :: + genAssignPrototype(ArrayClass, New(globalVar(VarField.h, ObjectClass), Nil)) :: + (prototypeFor(ArrayClass) DOT "constructor" := ArrayClass) :: assignES5ClassMembers(ArrayClass, members) } @@ -1537,7 +1538,7 @@ private[emitter] object CoreJSLib { case _: ClassRef => clsDef ::: extractWithGlobals(globalFunctionDef(VarField.ah, ObjectClass, Nil, None, Skip())) ::: - (globalVar(VarField.ah, ObjectClass).prototype := ArrayClass.prototype) :: Nil + (globalVar(VarField.ah, ObjectClass).prototype := prototypeFor(ArrayClass)) :: Nil case _: PrimRef => clsDef } @@ -1697,7 +1698,6 @@ private[emitter] object CoreJSLib { val name = varRef("name") Block( - arrayClass.prototype DOT classData := This(), const(name, str("[") + (componentData DOT cpn.arrayEncodedName)), privateFieldSet(cpn.constr, arrayClass), if (globalKnowledge.isParentDataAccessed) @@ -1729,6 +1729,7 @@ private[emitter] object CoreJSLib { MethodDef(static = false, Ident(cpn.initSpecializedArray), paramList(componentData, arrayClass, typedArrayClass, isAssignableFromFun), None, { Block( + arrayClass.prototype DOT classData := This(), initArrayCommonBody(arrayClass, componentData, componentData, 1), const(self, This()), // capture `this` for use in arrow fun privateFieldSet(cpn.isAssignableFromFun, isAssignableFromFun || { @@ -1833,14 +1834,19 @@ private[emitter] object CoreJSLib { val members = set ::: copyTo ::: clone :: Nil if (useClassesForRegularClasses) { - ClassDef(Some(ArrayClass.ident), Some(globalVar(VarField.ac, ObjectClass)), - ctor :: members) + Block( + ClassDef(Some(ArrayClass.ident), Some(globalVar(VarField.ac, ObjectClass)), + ctor :: members), + ArrayClass.prototype DOT cpn.classData := This() + ) } else { Block( FunctionDef(ArrayClass.ident, ctor.args, ctor.restParam, ctor.body) :: - (ArrayClass.prototype := New(globalVar(VarField.ah, ObjectClass), Nil)) :: - (ArrayClass.prototype DOT "constructor" := ArrayClass) :: - assignES5ClassMembers(ArrayClass, members) + genAssignPrototype(ArrayClass, New(globalVar(VarField.ah, ObjectClass), Nil), localDecl = true) :: + (prototypeFor(ArrayClass) DOT "constructor" := ArrayClass) :: + assignES5ClassMembers(ArrayClass, members) ::: + (prototypeFor(ArrayClass) DOT cpn.classData := This()) :: + Nil ) } } @@ -2000,6 +2006,7 @@ private[emitter] object CoreJSLib { extractWithGlobals(globalClassDef(VarField.TypeData, CoreVar, None, ctor :: members)) } else { defineFunction(VarField.TypeData, ctor.args, ctor.body) ::: + setPrototypeVar(globalVar(VarField.TypeData, CoreVar)) ::: assignES5ClassMembers(globalVar(VarField.TypeData, CoreVar), members) } } @@ -2159,7 +2166,7 @@ private[emitter] object CoreJSLib { for { MethodDef(static, name, args, restParam, body) <- members } yield { - val target = if (static) classRef else classRef.prototype + val target = if (static) classRef else prototypeFor(classRef) genPropSelect(target, name) := Function(arrow = false, args, restParam, body) } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala index 506dec4d4a..c7de499363 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala @@ -64,6 +64,11 @@ final class Emitter[E >: Null <: js.Tree]( val classEmitter: ClassEmitter = new ClassEmitter(sjsGen) + val everyFileStart: List[E] = { + // This postTransform does not count in the statistics + postTransformer.transformStats(sjsGen.declarePrototypeVar, 0) + } + val coreJSLibCache: CoreJSLibCache = new CoreJSLibCache val moduleCaches: mutable.Map[ModuleID, ModuleCache] = mutable.Map.empty @@ -293,6 +298,11 @@ final class Emitter[E >: Null <: js.Tree]( * it is crucial that we verify it. */ val defTrees: List[E] = ( + /* The declaration of the `$p` variable that temporarily holds + * prototypes. + */ + state.everyFileStart.iterator ++ + /* The definitions of the CoreJSLib that come before the definition * of `j.l.Object`. They depend on nothing else. */ 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 b52be22348..5b7b846b8c 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 @@ -145,6 +145,39 @@ private[emitter] final class SJSGen( val newArrayOfThisClass = "newArrayOfThisClass" } + /* This is a `val` because it is used at the top of every file, outside of + * any cache. Fortunately it does not depend on any dynamic content. + */ + val declarePrototypeVar: List[Tree] = { + implicit val pos = Position.NoPosition + if (minify) VarDef(fileLevelVarIdent(VarField.p), None) :: Nil + else Nil + } + + def prototypeFor(classRef: Tree)(implicit pos: Position): Tree = { + import TreeDSL._ + if (minify) fileLevelVar(VarField.p) + else classRef.prototype + } + + def genAssignPrototype(classRef: Tree, value: Tree, localDecl: Boolean = false)(implicit pos: Position): Tree = { + import TreeDSL._ + val assign = classRef.prototype := value + if (!minify) + assign + else if (localDecl) + VarDef(fileLevelVarIdent(VarField.p), Some(assign)) + else + fileLevelVar(VarField.p) := assign + } + + /** Under `minify`, set `$p` to `classRef.prototype`. */ + def setPrototypeVar(classRef: Tree)(implicit pos: Position): List[Tree] = { + import TreeDSL._ + if (minify) (fileLevelVar(VarField.p) := classRef.prototype) :: Nil + else Nil + } + def genZeroOf(tpe: Type)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): Tree = { 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 2be691d96e..10ac75518a 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 @@ -40,7 +40,10 @@ private[emitter] object VarField { /** Scala class initializers (). */ final val sct = mk("$sct") - /** Private (instance) methods. */ + /** Private (instance) methods. + * + * Also used for the `prototype` of the current class when minifying. + */ final val p = mk("$p") /** Public static methods. */ 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 fbc5b93948..92c572391a 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -71,7 +71,7 @@ class LibrarySizeTest { testLinkedSizes( expectedFastLinkSize = 150205, - expectedFullLinkSizeWithoutClosure = 92762, + expectedFullLinkSizeWithoutClosure = 90108, expectedFullLinkSizeWithClosure = 21325, classDefs, moduleInitializers = MainTestModuleInitializers diff --git a/project/Build.scala b/project/Build.scala index f6d3d596ab..2be6f5a261 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2005,10 +2005,10 @@ object Build { )) } else { Some(ExpectedSizes( - fastLink = 495000 to 496000, - fullLink = 337000 to 338000, - fastLinkGz = 69000 to 70000, - fullLinkGz = 50000 to 51000, + fastLink = 454000 to 455000, + fullLink = 306000 to 307000, + fastLinkGz = 65000 to 66000, + fullLinkGz = 47000 to 48000, )) } @@ -2022,10 +2022,10 @@ object Build { )) } else { Some(ExpectedSizes( - fastLink = 348000 to 349000, - fullLink = 308000 to 309000, - fastLinkGz = 54000 to 55000, - fullLinkGz = 49000 to 50000, + fastLink = 325000 to 326000, + fullLink = 285000 to 286000, + fastLinkGz = 51000 to 52000, + fullLinkGz = 47000 to 48000, )) } From 229573b5427b4b2ddfb22551154478c09a720a61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 21 Feb 2024 13:12:45 +0100 Subject: [PATCH 062/298] Be smarter about how we use params to `initClass`. * Merge `isInterface` and `isJSType` as a single parameter `kind` that is an integer. * Use the first element of `ancestors` instead of independently passing the `internalName` (this is safe because ES 2015 guarantees the order of `getOwnPropertyNames`). * Really remove the `parentData` parameter when reachability analysis says it is not accessed. --- .../linker/backend/emitter/ClassEmitter.scala | 27 +++++------ .../linker/backend/emitter/CoreJSLib.scala | 46 ++++++++----------- .../linker/backend/emitter/VarField.scala | 2 - .../scalajs/linker/standard/LinkedClass.scala | 8 ++++ .../org/scalajs/linker/LibrarySizeTest.scala | 6 +-- project/Build.scala | 26 +++++------ 6 files changed, 56 insertions(+), 59 deletions(-) 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 8b50efda5e..8af483b5ce 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 @@ -836,21 +836,26 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val isJSType = kind.isJSType - val isJSTypeParam = - if (isJSType) js.BooleanLiteral(true) - else js.Undefined() + val kindParam = { + if (isJSType) js.IntLiteral(2) + else if (kind == ClassKind.Interface) js.IntLiteral(1) + else js.IntLiteral(0) + } - val parentData = if (globalKnowledge.isParentDataAccessed) { - superClass.fold[js.Tree] { + val parentDataOpt = if (globalKnowledge.isParentDataAccessed) { + val parentData = superClass.fold[js.Tree] { if (isObjectClass) js.Null() else js.Undefined() } { parent => globalVar(VarField.d, parent.name) } + parentData :: Nil } else { - js.Undefined() + Nil } + assert(ancestors.headOption.contains(className), + s"The ancestors of ${className.nameString} do not start with itself: $ancestors") val ancestorsRecord = js.ObjectConstr( ancestors.withFilter(_ != ObjectClass).map(ancestor => (genAncestorIdent(ancestor), js.IntLiteral(1))) ) @@ -902,15 +907,11 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { isInstanceFunWithGlobals.flatMap { isInstanceFun => val allParams = List( - js.ObjectConstr(List(genAncestorIdent(className) -> js.IntLiteral(0))), - js.BooleanLiteral(kind == ClassKind.Interface), + kindParam, js.StringLiteral(RuntimeClassNameMapperImpl.map( semantics.runtimeClassNameMapper, className.nameString)), - ancestorsRecord, - isJSTypeParam, - parentData, - isInstanceFun - ) + ancestorsRecord + ) ::: parentDataOpt ::: isInstanceFun :: Nil val prunedParams = allParams.reverse.dropWhile(_.isInstanceOf[js.Undefined]).reverse 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 43d58795ad..75106195d2 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 @@ -130,7 +130,6 @@ private[emitter] object CoreJSLib { defineLinkingInfo() ::: defineJSBuiltinsSnapshotsAndPolyfills() ::: declareCachedL0() ::: - definePropertyName() ::: defineCharClass() ::: defineRuntimeFunctions() ::: defineObjectGetClassFunctions() ::: @@ -526,23 +525,6 @@ private[emitter] object CoreJSLib { )) } - private def definePropertyName(): List[Tree] = { - /* Encodes a property name for runtime manipulation. - * - * Usage: - * env.propertyName({someProp:0}) - * Returns: - * "someProp" - * Useful when the property is renamed by a global optimizer (like - * Closure) but we must still get hold of a string of that name for - * runtime reflection. - */ - defineFunction1(VarField.propertyName) { obj => - val prop = varRef("prop") - ForIn(genEmptyImmutableLet(prop.ident), obj, Return(prop)) - } - } - private def defineCharClass(): List[Tree] = { val ctor = { val c = varRef("c") @@ -1652,23 +1634,31 @@ private[emitter] object CoreJSLib { } val initClass = { - val internalNameObj = varRef("internalNameObj") - val isInterface = varRef("isInterface") + // This is an int, where 1 means isInterface; 2 means isJSType; 0 otherwise + val kind = varRef("kind") + + val hasParentData = globalKnowledge.isParentDataAccessed + val fullName = varRef("fullName") val ancestors = varRef("ancestors") - val isJSType = varRef("isJSType") val parentData = varRef("parentData") val isInstance = varRef("isInstance") val internalName = varRef("internalName") val that = varRef("that") val depth = varRef("depth") val obj = varRef("obj") - MethodDef(static = false, Ident(cpn.initClass), - paramList(internalNameObj, isInterface, fullName, ancestors, - isJSType, parentData, isInstance), None, { + val params = + if (hasParentData) paramList(kind, fullName, ancestors, parentData, isInstance) + else paramList(kind, fullName, ancestors, isInstance) + MethodDef(static = false, Ident(cpn.initClass), params, None, { Block( - const(internalName, genCallHelper(VarField.propertyName, internalNameObj)), - if (globalKnowledge.isParentDataAccessed) + /* Extract the internalName, which is the first property of ancestors. + * We use `getOwnPropertyNames()`, which since ES 2015 guarantees + * to return non-integer string keys in creation order. + */ + const(internalName, + BracketSelect(Apply(genIdentBracketSelect(ObjectRef, "getOwnPropertyNames"), List(ancestors)), 0)), + if (hasParentData) privateFieldSet(cpn.parentData, parentData) else Skip(), @@ -1679,9 +1669,9 @@ private[emitter] object CoreJSLib { Return(!(!(BracketSelect(that DOT cpn.ancestors, internalName)))) }) }), - privateFieldSet(cpn.isJSType, !(!isJSType)), + privateFieldSet(cpn.isJSType, kind === 2), publicFieldSet(cpn.name, fullName), - publicFieldSet(cpn.isInterface, isInterface), + publicFieldSet(cpn.isInterface, kind === 1), publicFieldSet(cpn.isInstance, isInstance || { genArrowFunction(paramList(obj), { Return(!(!(obj && (obj DOT classData) && 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 10ac75518a..0e695a24c7 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 @@ -175,8 +175,6 @@ private[emitter] object VarField { final val valueDescription = mk("$valueDescription") - final val propertyName = mk("$propertyName") - // ID hash subsystem final val systemIdentityHashCode = mk("$systemIdentityHashCode") diff --git a/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedClass.scala b/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedClass.scala index 3f796633ae..58b71fb725 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedClass.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedClass.scala @@ -29,6 +29,11 @@ import org.scalajs.ir.Names.{ClassName, FieldName} * P+1. The converse is not true. This guarantees that versions can be used * reliably to determine at phase P+1 whether a linked class coming from phase * P must be reprocessed. + * + * @param ancestors + * List of all the ancestor classes and interfaces of this class. It always + * contains this class name and `java.lang.Object`. This class name is + * always the first element of the list. */ final class LinkedClass( // Stuff from Tree @@ -61,6 +66,9 @@ final class LinkedClass( val version: Version) { + require(ancestors.headOption.contains(name.name), + s"ancestors for ${name.name.nameString} must start with itself: $ancestors") + def className: ClassName = name.name val hasStaticInitializer: Boolean = { 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 92c572391a..b813d24f2a 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 = 150205, - expectedFullLinkSizeWithoutClosure = 90108, - expectedFullLinkSizeWithClosure = 21325, + expectedFastLinkSize = 148754, + expectedFullLinkSizeWithoutClosure = 89358, + expectedFullLinkSizeWithClosure = 22075, classDefs, moduleInitializers = MainTestModuleInitializers ) diff --git a/project/Build.scala b/project/Build.scala index 2be6f5a261..d7db8498ca 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1998,34 +1998,34 @@ object Build { case `default212Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 641000 to 642000, - fullLink = 101000 to 102000, - fastLinkGz = 77000 to 78000, + fastLink = 634000 to 635000, + fullLink = 102000 to 103000, + fastLinkGz = 76000 to 77000, fullLinkGz = 26000 to 27000, )) } else { Some(ExpectedSizes( - fastLink = 454000 to 455000, - fullLink = 306000 to 307000, + fastLink = 450000 to 451000, + fullLink = 303000 to 304000, fastLinkGz = 65000 to 66000, - fullLinkGz = 47000 to 48000, + fullLinkGz = 46000 to 47000, )) } case `default213Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 463000 to 464000, - fullLink = 99000 to 100000, - fastLinkGz = 60000 to 61000, - fullLinkGz = 26000 to 27000, + fastLink = 458000 to 459000, + fullLink = 100000 to 101000, + fastLinkGz = 59000 to 60000, + fullLinkGz = 27000 to 28000, )) } else { Some(ExpectedSizes( - fastLink = 325000 to 326000, - fullLink = 285000 to 286000, + fastLink = 322000 to 323000, + fullLink = 282000 to 283000, fastLinkGz = 51000 to 52000, - fullLinkGz = 47000 to 48000, + fullLinkGz = 46000 to 47000, )) } From 7b2708673abf5efa1ec966ccdac4ca3c54449a32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 21 Feb 2024 17:51:39 +0100 Subject: [PATCH 063/298] Store `$c.prototype.$classData` as part of `$TypeData().initClass()`. This removes the largest source of uncompressible and non-removable occurrences of `$classData` identifiers. It does increase the size of the GCC output in the `LibrarySizeTest`, but it does not translate to the `reversi` checksizes, so it is probably a small codebase artifact. --- .../linker/backend/emitter/ClassEmitter.scala | 23 +++++++------- .../linker/backend/emitter/CoreJSLib.scala | 18 +++++++---- .../linker/backend/emitter/Emitter.scala | 31 +++++++++++++------ .../org/scalajs/linker/LibrarySizeTest.scala | 6 ++-- project/Build.scala | 28 ++++++++--------- 5 files changed, 63 insertions(+), 43 deletions(-) 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 8af483b5ce..e2f2419835 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 @@ -826,7 +826,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { def genTypeData(className: ClassName, kind: ClassKind, superClass: Option[ClassIdent], ancestors: List[ClassName], - jsNativeLoadSpec: Option[JSNativeLoadSpec])( + jsNativeLoadSpec: Option[JSNativeLoadSpec], hasInstances: Boolean)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[List[js.Tree]] = { import TreeDSL._ @@ -836,9 +836,18 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val isJSType = kind.isJSType - val kindParam = { + /* The `kindOrCtor` param is either: + * - an int: 1 means isInterface; 2 means isJSType; 0 otherwise + * - a Scala class constructor: means 0 + assign `kindOrCtor.prototype.$classData = ;` + * + * We must only assign the `$classData` if the class is a regular + * (non-hijacked) Scala class, and if it has instances. Otherwise there is + * no Scala class constructor for the class at all. + */ + val kindOrCtorParam = { if (isJSType) js.IntLiteral(2) else if (kind == ClassKind.Interface) js.IntLiteral(1) + else if (kind.isClass && hasInstances) globalVar(VarField.c, className) else js.IntLiteral(0) } @@ -907,7 +916,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { isInstanceFunWithGlobals.flatMap { isInstanceFun => val allParams = List( - kindParam, + kindOrCtorParam, js.StringLiteral(RuntimeClassNameMapperImpl.map( semantics.runtimeClassNameMapper, className.nameString)), ancestorsRecord @@ -923,14 +932,6 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { } } - def genSetTypeData(className: ClassName)( - implicit moduleContext: ModuleContext, - globalKnowledge: GlobalKnowledge, pos: Position): js.Tree = { - import TreeDSL._ - - globalVar(VarField.c, className).prototype DOT cpn.classData := globalVar(VarField.d, className) - } - def genModuleAccessor(className: ClassName, isJSClass: Boolean)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[List[js.Tree]] = { 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 75106195d2..b476811b0e 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 @@ -1634,8 +1634,11 @@ private[emitter] object CoreJSLib { } val initClass = { - // This is an int, where 1 means isInterface; 2 means isJSType; 0 otherwise - val kind = varRef("kind") + /* This is either: + * - an int: 1 means isInterface; 2 means isJSType; 0 otherwise + * - a Scala class constructor: means 0 + assign `kindOrCtor.prototype.$classData = this;` + */ + val kindOrCtor = varRef("kindOrCtor") val hasParentData = globalKnowledge.isParentDataAccessed @@ -1648,8 +1651,8 @@ private[emitter] object CoreJSLib { val depth = varRef("depth") val obj = varRef("obj") val params = - if (hasParentData) paramList(kind, fullName, ancestors, parentData, isInstance) - else paramList(kind, fullName, ancestors, isInstance) + if (hasParentData) paramList(kindOrCtor, fullName, ancestors, parentData, isInstance) + else paramList(kindOrCtor, fullName, ancestors, isInstance) MethodDef(static = false, Ident(cpn.initClass), params, None, { Block( /* Extract the internalName, which is the first property of ancestors. @@ -1669,15 +1672,18 @@ private[emitter] object CoreJSLib { Return(!(!(BracketSelect(that DOT cpn.ancestors, internalName)))) }) }), - privateFieldSet(cpn.isJSType, kind === 2), + privateFieldSet(cpn.isJSType, kindOrCtor === 2), publicFieldSet(cpn.name, fullName), - publicFieldSet(cpn.isInterface, kind === 1), + publicFieldSet(cpn.isInterface, kindOrCtor === 1), publicFieldSet(cpn.isInstance, isInstance || { genArrowFunction(paramList(obj), { Return(!(!(obj && (obj DOT classData) && BracketSelect(obj DOT classData DOT cpn.ancestors, internalName)))) }) }), + If(typeof(kindOrCtor) !== str("number"), { + kindOrCtor.prototype DOT cpn.classData := This() + }), Return(This()) ) }) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala index c7de499363..cdf17260e8 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala @@ -716,21 +716,16 @@ final class Emitter[E >: Null <: js.Tree]( if (linkedClass.hasRuntimeTypeInfo) { main ++= extractWithGlobals(classTreeCache.typeData.getOrElseUpdate( + linkedClass.hasInstances, classEmitter.genTypeData( className, // invalidated by overall class cache (part of ancestors) kind, // invalidated by class version linkedClass.superClass, // invalidated by class version linkedClass.ancestors, // invalidated by overall class cache (identity) - linkedClass.jsNativeLoadSpec // invalidated by class version + linkedClass.jsNativeLoadSpec, // invalidated by class version + linkedClass.hasInstances // invalidated directly (it is the input to `getOrElseUpdate`) )(moduleContext, classCache, linkedClass.pos).map(postTransform(_, 0)))) } - - if (linkedClass.hasInstances && kind.isClass && linkedClass.hasRuntimeTypeInfo) { - main ++= classTreeCache.setTypeData.getOrElseUpdate({ - val tree = classEmitter.genSetTypeData(className)(moduleContext, classCache, linkedClass.pos) - postTransform(tree, 0) - }) - } } if (linkedClass.kind.hasModuleAccessor && linkedClass.hasInstances) { @@ -1198,7 +1193,7 @@ object Emitter { val privateJSFields = new OneTimeCache[WithGlobals[E]] val storeJSSuperClass = new OneTimeCache[WithGlobals[E]] val instanceTests = new OneTimeCache[WithGlobals[E]] - val typeData = new OneTimeCache[WithGlobals[E]] + val typeData = new InputEqualityCache[Boolean, WithGlobals[E]] val setTypeData = new OneTimeCache[E] val moduleAccessor = new OneTimeCache[WithGlobals[E]] val staticInitialization = new OneTimeCache[E] @@ -1223,6 +1218,24 @@ object Emitter { } } + /** A cache that depends on an `input: I`, testing with `==`. + * + * @tparam I + * the type of input, for which `==` must meaningful + */ + private final class InputEqualityCache[I, A >: Null] { + private[this] var lastInput: Option[I] = None + private[this] var value: A = null + + def getOrElseUpdate(input: I, v: => A): A = { + if (!lastInput.contains(input)) { + value = v + lastInput = Some(input) + } + value + } + } + private case class ClassID( ancestors: List[ClassName], moduleContext: ModuleContext) 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 b813d24f2a..cfa7b749fc 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 = 148754, - expectedFullLinkSizeWithoutClosure = 89358, - expectedFullLinkSizeWithClosure = 22075, + expectedFastLinkSize = 147707, + expectedFullLinkSizeWithoutClosure = 88733, + expectedFullLinkSizeWithClosure = 21802, classDefs, moduleInitializers = MainTestModuleInitializers ) diff --git a/project/Build.scala b/project/Build.scala index d7db8498ca..bf039b251f 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1998,16 +1998,16 @@ object Build { case `default212Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 634000 to 635000, - fullLink = 102000 to 103000, - fastLinkGz = 76000 to 77000, - fullLinkGz = 26000 to 27000, + fastLink = 626000 to 627000, + fullLink = 98000 to 99000, + fastLinkGz = 75000 to 79000, + fullLinkGz = 25000 to 26000, )) } else { Some(ExpectedSizes( - fastLink = 450000 to 451000, - fullLink = 303000 to 304000, - fastLinkGz = 65000 to 66000, + fastLink = 442000 to 443000, + fullLink = 297000 to 298000, + fastLinkGz = 64000 to 65000, fullLinkGz = 46000 to 47000, )) } @@ -2015,16 +2015,16 @@ object Build { case `default213Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 458000 to 459000, - fullLink = 100000 to 101000, - fastLinkGz = 59000 to 60000, - fullLinkGz = 27000 to 28000, + fastLink = 452000 to 453000, + fullLink = 96000 to 97000, + fastLinkGz = 58000 to 59000, + fullLinkGz = 26000 to 27000, )) } else { Some(ExpectedSizes( - fastLink = 322000 to 323000, - fullLink = 282000 to 283000, - fastLinkGz = 51000 to 52000, + fastLink = 316000 to 317000, + fullLink = 276000 to 277000, + fastLinkGz = 50000 to 51000, fullLinkGz = 46000 to 47000, )) } From 58bec3f5b099856d961c1544cc6bbf4249224729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 15 Mar 2024 19:59:06 +0100 Subject: [PATCH 064/298] Optimize `String_+` for `char` at the emitter level. Previously, our implementation of `Character.toString(c)` used JS interop itself to produce `String.fromCharCode(c)`. This was the only hijacked class to do so, as the other ones use `"" + c` instead. The reason was that the function emitter used to box chars in string concatenation to get the behavior of `$Char.toString()`, and we wanted to avoid the boxing. We now make `FunctionEmitter` smarter about primitive `char`s in `String_+`: it does not box anymore, and instead calls a dedicated helper that calls `String.fromCharCode(c)`. We replace the user-space implementation of `Character.toString(c)` with `"" + c`, which aligns it with the other hijacked classes. This is better because the optimization is more widely applicable: it applies to all string concatenations, including those generated by string interpolators, instead of only explicit `toString()` calls. Moreover, it automatically allows to constant-fold `c.toString()` when `c` is a constant character. --- .../src/main/scala/java/lang/Character.scala | 2 +- .../linker/backend/emitter/CoreJSLib.scala | 3 +++ .../backend/emitter/FunctionEmitter.scala | 19 ++++++++++++------- .../linker/backend/emitter/VarField.scala | 2 ++ 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/javalib/src/main/scala/java/lang/Character.scala b/javalib/src/main/scala/java/lang/Character.scala index b260948a6d..e5f132fd49 100644 --- a/javalib/src/main/scala/java/lang/Character.scala +++ b/javalib/src/main/scala/java/lang/Character.scala @@ -118,7 +118,7 @@ object Character { @inline def hashCode(value: Char): Int = value.toInt @inline def toString(c: Char): String = - js.Dynamic.global.String.fromCharCode(c.toInt).asInstanceOf[String] + "" + c def toString(codePoint: Int): String = { if (isBmpCodePoint(codePoint)) { 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 6dc814cd87..3dbcbe8afb 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 @@ -947,6 +947,9 @@ private[emitter] object CoreJSLib { defineFunction1(VarField.doubleToInt) { x => Return(If(x > 2147483647, 2147483647, If(x < -2147483648, -2147483648, x | 0))) } ::: + defineFunction1(VarField.charToString) { x => + Return(Apply(genIdentBracketSelect(StringRef, "fromCharCode"), x :: Nil)) + } ::: condDefs(semantics.stringIndexOutOfBounds != CheckedBehavior.Unchecked)( defineFunction2(VarField.charAt) { (s, i) => val r = varRef("r") 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 a518948b97..58979155fc 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 @@ -2391,8 +2391,8 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case BinaryOp(op, lhs, rhs) => import BinaryOp._ - val newLhs = transformExprNoChar(lhs) - val newRhs = transformExprNoChar(rhs) + val newLhs = transformExpr(lhs, preserveChar = (op == String_+)) + val newRhs = transformExpr(rhs, preserveChar = (op == String_+)) (op: @switch) match { case === | !== => @@ -2445,11 +2445,16 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { js.BinaryOp(JSBinaryOp.!==, newLhs, newRhs) case String_+ => - if (lhs.tpe == StringType || rhs.tpe == StringType) { - js.BinaryOp(JSBinaryOp.+, newLhs, newRhs) - } else { - js.BinaryOp(JSBinaryOp.+, js.BinaryOp(JSBinaryOp.+, - js.StringLiteral(""), newLhs), newRhs) + def charToString(t: js.Tree): js.Tree = + genCallHelper(VarField.charToString, t) + + (lhs.tpe, rhs.tpe) match { + case (CharType, CharType) => charToString(newLhs) + charToString(newRhs) + case (CharType, _) => charToString(newLhs) + newRhs + case (_, CharType) => newLhs + charToString(newRhs) + case (StringType, _) => newLhs + newRhs + case (_, StringType) => newLhs + newRhs + case _ => (js.StringLiteral("") + newLhs) + newRhs } case Int_+ => or0(js.BinaryOp(JSBinaryOp.+, newLhs, newRhs)) 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 2be691d96e..b89239a63a 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 @@ -154,6 +154,8 @@ private[emitter] object VarField { /** Box char. */ final val bC = mk("$bC") + final val charToString = mk("$cToS") + final val charAt = mk("$charAt") // Object helpers From 0dd63ff17f9ad57e8c29c525e58efc8d8ca3a942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 23 Feb 2024 17:54:42 +0100 Subject: [PATCH 065/298] Under Minify, inline `VarDef`s used only once when we can. During the optimizer, when emitting a `VarDef(x, ..., rhs)`, we try to inline it if it has been used exactly once. In order to do that, we look at the `body` in which it will be available, and replace its only occurrence if it occurs in the first evaluation context following only pure subexpressions. See the long comment in the code for more details. --- .../frontend/optimizer/OptimizerCore.scala | 343 +++++++++++++++++- .../linker/standard/CommonPhaseConfig.scala | 5 +- .../org/scalajs/linker/LibrarySizeTest.scala | 4 +- project/BinaryIncompatibilities.scala | 2 + project/Build.scala | 22 +- 5 files changed, 343 insertions(+), 33 deletions(-) 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 d9c57e3b9a..7c484195b5 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 @@ -417,7 +417,7 @@ private[optimizer] abstract class OptimizerCore( val (newName, newOriginalName) = freshLocalName(name, originalName, mutable = false) val localDef = LocalDef(RefinedType(AnyType), mutable = false, - ReplaceWithVarRef(newName, newSimpleState(Used), None)) + ReplaceWithVarRef(newName, newSimpleState(UsedAtLeastOnce), None)) val newBody = { val bodyScope = scope.withEnv(scope.env.withLocalDef(name, localDef)) transformStat(body)(bodyScope) @@ -430,7 +430,7 @@ private[optimizer] abstract class OptimizerCore( val (newName, newOriginalName) = freshLocalName(name, originalName, mutable = false) val localDef = LocalDef(RefinedType(AnyType), true, - ReplaceWithVarRef(newName, newSimpleState(Used), None)) + ReplaceWithVarRef(newName, newSimpleState(UsedAtLeastOnce), None)) val newHandler = { val handlerScope = scope.withEnv(scope.env.withLocalDef(name, localDef)) transform(handler, isStat)(handlerScope) @@ -1380,7 +1380,7 @@ private[optimizer] abstract class OptimizerCore( case PreTransLocalDef(localDef @ LocalDef(tpe, _, replacement)) => replacement match { case ReplaceWithRecordVarRef(name, recordType, used, cancelFun) => - used.value = Used + used.value = used.value.inc PreTransRecordTree( VarRef(LocalIdent(name))(recordType), tpe, cancelFun) @@ -1599,18 +1599,21 @@ private[optimizer] abstract class OptimizerCore( if (used.value.isUsed) { val ident = LocalIdent(name) - val varDef = resolveLocalDef(value) match { + resolveLocalDef(value) match { case PreTransRecordTree(valueTree, valueTpe, cancelFun) => val recordType = valueTree.tpe.asInstanceOf[RecordType] if (!isImmutableType(recordType)) cancelFun() - VarDef(ident, originalName, recordType, mutable, valueTree) + Block(VarDef(ident, originalName, recordType, mutable, valueTree), innerBody) case PreTransTree(valueTree, valueTpe) => - VarDef(ident, originalName, tpe.base, mutable, valueTree) + val optimized = + if (used.value.count == 1 && config.minify) tryInsertAtFirstEvalContext(name, valueTree, innerBody) + else None + optimized.getOrElse { + Block(VarDef(ident, originalName, tpe.base, mutable, valueTree), innerBody) + } } - - Block(varDef, innerBody) } else { val valueSideEffects = finishTransformStat(value) Block(valueSideEffects, innerBody) @@ -1713,6 +1716,282 @@ private[optimizer] abstract class OptimizerCore( case _ => false } + /** Tries to insert `valTree` in place of the (unique) occurrence of `valName` in `body`. + * + * This function assumes that `valName` is used only once, and only inside + * `body`. It does not assume that `valTree` or `body` are side-effect-free. + * + * The replacement is done only if we can show that it will not affect + * evaluation order. In practice, this means that we only replace if we find + * the occurrence of `valName` in the first evaluation context of `body`. + * In other words, we verify that all the expressions that will evaluate + * before `valName` in `body` are pure. + * + * We consider a `VarRef(y)` pure if `valTree` does not contain any + * assignment to `y`. + * + * For example, we can replace `x` in the following bodies: + * + * {{{ + * x + * x + e + * x.foo(...) + * x.f + * e + x // if `e` is pure + * e0.foo(...e1, x, ...) // if `e0` is pure and non-null, and the `...e1`s are pure + * if (x) { ... } else { ... } + * }}} + * + * Why is this interesting? Mostly because of inlining. + * + * Inlining tends to create many bindings for the receivers and arguments of + * methods. We must do that to preserve evaluation order, and not to evaluate + * them multiple times. However, very often, the receiver and arguments are + * used exactly once in the inlined body, and in-order. Using this strategy, + * we can take the right-hand-sides of the synthetic bindings and inline them + * directly inside the body. + * + * This in turn allows more trees to remain JS-level expressions, which means + * that `FunctionEmitter` has to `unnest` less often, further reducing the + * amount of temporary variables. + * + * --- + * + * Note that we can never cross any potential undefined behavior, even when + * the corresponding semantics are `Unchecked`. That is because the + * `valTree` could throw itself, preventing the normal behavior of the code + * to reach the undefined behavior in the first place. Consider for example: + * + * {{{ + * val x: Foo = ... // maybe null + * val y: Int = if (x == null) throw new Exception() else 1 + * x.foo(y) + * }}} + * + * We cannot inline `y` in this example, because that would change + * observable behavior if `x` is `null`. + * + * It is OK to cross the potential UB if we can prove that it will not + * actually trigger, for example if we know that `x` is not null. + * + * --- + * + * We only call this function when the `minify` option is on. This is for two + * reasons: + * + * - it can be detrimental to debuggability, as even user-written `val`s can + * disappear, and their right-hand-side be evaluated out-of-order compared + * to the source code; + * - it is non-linear, as we can perform several traversals of the same body, + * if it follows a sequence of `VarDef`s that can each be successfully + * inserted. + */ + private def tryInsertAtFirstEvalContext(valName: LocalName, valTree: Tree, body: Tree): Option[Tree] = { + import EvalContextInsertion._ + + object valTreeInfo extends Traversers.Traverser { + val mutatedLocalVars = mutable.Set.empty[LocalName] + + traverse(valTree) + + override def traverse(tree: Tree): Unit = { + super.traverse(tree) + tree match { + case Assign(VarRef(ident), _) => mutatedLocalVars += ident.name + case _ => () + } + } + } + + def recs(bodies: List[Tree]): EvalContextInsertion[List[Tree]] = bodies match { + case Nil => + NotFoundPureSoFar + case firstBody :: restBodies => + rec(firstBody) match { + case Success(newFirstBody) => Success(newFirstBody :: restBodies) + case NotFoundPureSoFar => recs(restBodies).mapOrKeepGoing(firstBody :: _) + case Failed => Failed + } + } + + def rec(body: Tree): EvalContextInsertion[Tree] = { + implicit val pos = body.pos + + body match { + case VarRef(ident) => + if (ident.name == valName) + Success(valTree) + else if (valTreeInfo.mutatedLocalVars.contains(ident.name)) + Failed + else + NotFoundPureSoFar + + case Skip() => + NotFoundPureSoFar + + case Block(stats) => + recs(stats).mapOrKeepGoing(Block(_)) + + case Labeled(label, tpe, innerBody) => + rec(innerBody).mapOrKeepGoing(Labeled(label, tpe, _)) + + case Return(expr, label) => + rec(expr).mapOrFailed(Return(_, label)) + + case If(cond, thenp, elsep) => + rec(cond).mapOrFailed(If(_, thenp, elsep)(body.tpe)) + + case Throw(expr) => + rec(expr).mapOrFailed(Throw(_)) + + case Match(selector, cases, default) => + rec(selector).mapOrFailed(Match(_, cases, default)(body.tpe)) + + case New(className, ctor, args) => + recs(args).mapOrKeepGoingIf(New(className, ctor, _))( + keepGoingIf = hasElidableConstructors(className)) + + case LoadModule(className) => + if (hasElidableConstructors(className)) NotFoundPureSoFar + else Failed + + case Select(qual, field) => + rec(qual).mapOrFailed(Select(_, field)(body.tpe)) + + case Apply(flags, receiver, method, args) => + rec(receiver) match { + case Success(newReceiver) => + Success(Apply(flags, newReceiver, method, args)(body.tpe)) + case NotFoundPureSoFar if isNotNull(receiver) => + recs(args).mapOrFailed(Apply(flags, receiver, method, _)(body.tpe)) + case _ => + Failed + } + + case ApplyStatically(flags, receiver, className, method, args) => + rec(receiver) match { + case Success(newReceiver) => + Success(ApplyStatically(flags, newReceiver, className, method, args)(body.tpe)) + case NotFoundPureSoFar if isNotNull(receiver) => + recs(args).mapOrFailed(ApplyStatically(flags, receiver, className, method, _)(body.tpe)) + case _ => + Failed + } + + case ApplyStatic(flags, className, method, args) => + recs(args).mapOrFailed(ApplyStatic(flags, className, method, _)(body.tpe)) + + case UnaryOp(op, arg) => + rec(arg).mapOrKeepGoing(UnaryOp(op, _)) + + case BinaryOp(op, lhs, rhs) => + import BinaryOp._ + + rec(lhs) match { + case Success(newLhs) => Success(BinaryOp(op, newLhs, rhs)) + case Failed => Failed + + case NotFoundPureSoFar => + rec(rhs).mapOrKeepGoingIf(BinaryOp(op, lhs, _)) { + (op: @switch) match { + case Int_/ | Int_% | Long_/ | Long_% | String_+ | String_charAt => + false + case _ => + true + } + } + } + + case NewArray(typeRef, lengths) => + recs(lengths).mapOrKeepGoing(NewArray(typeRef, _)) + + case ArrayValue(typeRef, elems) => + recs(elems).mapOrKeepGoing(ArrayValue(typeRef, _)) + + case ArrayLength(array) => + rec(array).mapOrKeepGoingIf(ArrayLength(_))(keepGoingIf = isNotNull(array)) + + case ArraySelect(array, index) => + rec(array) match { + case Success(newArray) => + Success(ArraySelect(newArray, index)(body.tpe)) + case NotFoundPureSoFar if isNotNull(array) => + rec(index).mapOrFailed(ArraySelect(array, _)(body.tpe)) + case _ => + Failed + } + + case RecordValue(tpe, elems) => + recs(elems).mapOrKeepGoing(RecordValue(tpe, _)) + + case RecordSelect(record, field) => + rec(record).mapOrKeepGoingIf(RecordSelect(_, field)(body.tpe)) { + // We can keep going if the selected field is immutable + val RecordType(fields) = record.tpe: @unchecked + !fields.find(_.name == field.name).get.mutable + } + + case IsInstanceOf(expr, testType) => + rec(expr).mapOrKeepGoing(IsInstanceOf(_, testType)) + + case AsInstanceOf(expr, tpe) => + rec(expr).mapOrFailed(AsInstanceOf(_, tpe)) + + case GetClass(expr) => + rec(expr).mapOrKeepGoingIf(GetClass(_))(keepGoingIf = isNotNull(expr)) + + case Clone(expr) => + rec(expr).mapOrFailed(Clone(_)) + + case JSUnaryOp(op, arg) => + rec(arg).mapOrFailed(JSUnaryOp(op, _)) + + case JSBinaryOp(op, lhs, rhs) => + rec(lhs) match { + case Success(newLhs) => + Success(JSBinaryOp(op, newLhs, rhs)) + case NotFoundPureSoFar => + rec(rhs).mapOrKeepGoingIf(JSBinaryOp(op, lhs, _))( + keepGoingIf = op == JSBinaryOp.=== || op == JSBinaryOp.!==) + case Failed => + Failed + } + + case JSArrayConstr(items) => + if (items.exists(_.isInstanceOf[JSSpread])) + Failed // in theory we could do something better here, but the complexity is not worth it + else + recs(items.asInstanceOf[List[Tree]]).mapOrKeepGoing(JSArrayConstr(_)) + + case _: Literal => + NotFoundPureSoFar + + case This() => + NotFoundPureSoFar + + case Closure(arrow, captureParams, params, restParam, body, captureValues) => + recs(captureValues).mapOrKeepGoing(Closure(arrow, captureParams, params, restParam, body, _)) + + case _ => + Failed + } + } + + rec(body) match { + case Success(result) => Some(result) + case Failed => None + + case NotFoundPureSoFar => + /* The val was never actually used. This can happen even when the + * variable was `used` exactly once, because `used` tracks the number + * of times we have generated a `VarRef` for it. In some cases, the + * generated `VarRef` is later discarded through `keepOnlySideEffects` + * somewhere else. + */ + Some(Block(keepOnlySideEffects(valTree), body)(body.pos)) + } + } + private def pretransformApply(tree: Apply, isStat: Boolean, usePreTransform: Boolean)( cont: PreTransCont)( @@ -2066,7 +2345,7 @@ private[optimizer] abstract class OptimizerCore( if (target != expectedTarget) cancelFun() - used.value = Used + used.value = used.value.inc val module = VarRef(LocalIdent(moduleVarName))(AnyType) path.foldLeft[Tree](module) { (inner, pathElem) => JSSelect(inner, StringLiteral(pathElem)) @@ -2097,7 +2376,7 @@ private[optimizer] abstract class OptimizerCore( captureParams, params, body, captureLocalDefs, alreadyUsed, cancelFun))) if !alreadyUsed.value.isUsed && argsNoSpread.size <= params.size => - alreadyUsed.value = Used + alreadyUsed.value = alreadyUsed.value.inc val missingArgCount = params.size - argsNoSpread.size val expandedArgs = if (missingArgCount == 0) argsNoSpread @@ -4991,7 +5270,7 @@ private[optimizer] abstract class OptimizerCore( case PreTransTree(VarRef(LocalIdent(refName)), _) if !localIsMutable(refName) => buildInner(LocalDef(computeRefinedType(), false, - ReplaceWithVarRef(refName, newSimpleState(Used), None)), cont) + ReplaceWithVarRef(refName, newSimpleState(UsedAtLeastOnce), None)), cont) case _ => withDedicatedVar(computeRefinedType()) @@ -5343,7 +5622,7 @@ private[optimizer] object OptimizerCore { def newReplacement(implicit pos: Position): Tree = this.replacement match { case ReplaceWithVarRef(name, used, _) => - used.value = Used + used.value = used.value.inc VarRef(LocalIdent(name))(tpe.base) /* Allocate an instance of RuntimeLong on the fly. @@ -5352,7 +5631,7 @@ private[optimizer] object OptimizerCore { */ case ReplaceWithRecordVarRef(name, recordType, used, _) if tpe.base == ClassType(LongImpl.RuntimeLongClass) => - used.value = Used + used.value = used.value.inc createNewLong(VarRef(LocalIdent(name))(recordType)) case ReplaceWithRecordVarRef(_, _, _, cancelFun) => @@ -6451,14 +6730,40 @@ private[optimizer] object OptimizerCore { else OriginalName(base) } - private sealed abstract class IsUsed { - def isUsed: Boolean + private final case class IsUsed(count: Int) { + def isUsed: Boolean = count > 0 + + lazy val inc: IsUsed = IsUsed(count + 1) } - private case object Used extends IsUsed { - override def isUsed: Boolean = true + + private val Unused: IsUsed = IsUsed(count = 0) + private val UsedAtLeastOnce: IsUsed = Unused.inc + + private sealed abstract class EvalContextInsertion[+A] { + import EvalContextInsertion._ + + def mapOrKeepGoing[B](f: A => B): EvalContextInsertion[B] = this match { + case Success(a) => Success(f(a)) + case NotFoundPureSoFar => NotFoundPureSoFar + case Failed => Failed + } + + def mapOrKeepGoingIf[B](f: A => B)(keepGoingIf: => Boolean): EvalContextInsertion[B] = this match { + case Success(a) => Success(f(a)) + case NotFoundPureSoFar => if (keepGoingIf) NotFoundPureSoFar else Failed + case Failed => Failed + } + + def mapOrFailed[B](f: A => B): EvalContextInsertion[B] = this match { + case Success(a) => Success(f(a)) + case _ => Failed + } } - private case object Unused extends IsUsed { - override def isUsed: Boolean = false + + private object EvalContextInsertion { + final case class Success[+A](result: A) extends EvalContextInsertion[A] + case object Failed extends EvalContextInsertion[Nothing] + case object NotFoundPureSoFar extends EvalContextInsertion[Nothing] } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/standard/CommonPhaseConfig.scala b/linker/shared/src/main/scala/org/scalajs/linker/standard/CommonPhaseConfig.scala index 700a5b1ff7..66b0c0f9ef 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/standard/CommonPhaseConfig.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/standard/CommonPhaseConfig.scala @@ -18,6 +18,8 @@ import org.scalajs.linker.interface._ final class CommonPhaseConfig private ( /** Core specification. */ val coreSpec: CoreSpec, + /** Apply Scala.js-specific minification of the produced .js files. */ + val minify: Boolean, /** Whether things that can be parallelized should be parallelized. * On the JavaScript platform, this setting is typically ignored. */ @@ -37,6 +39,7 @@ final class CommonPhaseConfig private ( private def this() = { this( coreSpec = CoreSpec.Defaults, + minify = false, parallel = true, batchMode = false) } @@ -47,6 +50,6 @@ private[linker] object CommonPhaseConfig { private[linker] def fromStandardConfig(config: StandardConfig): CommonPhaseConfig = { val coreSpec = CoreSpec(config.semantics, config.moduleKind, config.esFeatures) - new CommonPhaseConfig(coreSpec, config.parallel, config.batchMode) + new CommonPhaseConfig(coreSpec, config.minify, config.parallel, config.batchMode) } } 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 cfa7b749fc..c55930f74f 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/LibrarySizeTest.scala @@ -71,8 +71,8 @@ class LibrarySizeTest { testLinkedSizes( expectedFastLinkSize = 147707, - expectedFullLinkSizeWithoutClosure = 88733, - expectedFullLinkSizeWithClosure = 21802, + expectedFullLinkSizeWithoutClosure = 86729, + expectedFullLinkSizeWithClosure = 21768, classDefs, moduleInitializers = MainTestModuleInitializers ) diff --git a/project/BinaryIncompatibilities.scala b/project/BinaryIncompatibilities.scala index 6d2c642453..49cbe2d2a0 100644 --- a/project/BinaryIncompatibilities.scala +++ b/project/BinaryIncompatibilities.scala @@ -24,6 +24,8 @@ object BinaryIncompatibilities { ) val Linker = Seq( + // private, not an issue + ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.linker.standard.CommonPhaseConfig.this"), ) val LinkerInterface = Seq( diff --git a/project/Build.scala b/project/Build.scala index bf039b251f..43f4be4847 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1999,16 +1999,16 @@ object Build { if (!useMinifySizes) { Some(ExpectedSizes( fastLink = 626000 to 627000, - fullLink = 98000 to 99000, + fullLink = 97000 to 98000, fastLinkGz = 75000 to 79000, fullLinkGz = 25000 to 26000, )) } else { Some(ExpectedSizes( - fastLink = 442000 to 443000, - fullLink = 297000 to 298000, - fastLinkGz = 64000 to 65000, - fullLinkGz = 46000 to 47000, + fastLink = 433000 to 434000, + fullLink = 288000 to 289000, + fastLinkGz = 62000 to 63000, + fullLinkGz = 44000 to 45000, )) } @@ -2016,16 +2016,16 @@ object Build { if (!useMinifySizes) { Some(ExpectedSizes( fastLink = 452000 to 453000, - fullLink = 96000 to 97000, + fullLink = 94000 to 95000, fastLinkGz = 58000 to 59000, - fullLinkGz = 26000 to 27000, + fullLinkGz = 25000 to 26000, )) } else { Some(ExpectedSizes( - fastLink = 316000 to 317000, - fullLink = 276000 to 277000, - fastLinkGz = 50000 to 51000, - fullLinkGz = 46000 to 47000, + fastLink = 309000 to 310000, + fullLink = 265000 to 266000, + fastLinkGz = 49000 to 50000, + fullLinkGz = 43000 to 44000, )) } From 8b5af310db7e7cc7b918a42b14f3f25fa91bde6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 16 Mar 2024 10:41:40 +0100 Subject: [PATCH 066/298] Reduce the memory pressure of `BackwardsCompatTest`. Previously, we were running the tests for *all* previous versions in parallel, because of the behavior of `Future.traverse`. While it can reduce the wall clock time of running that test, it comes at a huge memory consumption cost, increasing with every new release. It seems we have recently hit the limit of what is reasonable, especially on JS. Even JS retains things in parallel in memory because the IO handling concurrently overlaps. We now ensure a completely sequential behavior for this test. For each previous version, in sequence, we 1. create a new cache for the IR files of the version, 2. load the IR files, 3. execute the test, 4. free the cache. This commit hopefully fixes #4961. --- .../scalajs/linker/BackwardsCompatTest.scala | 23 ++++----- .../scalajs/linker/testutils/TestIRRepo.scala | 51 ++++++++++++++++--- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/linker/shared/src/test/scala/org/scalajs/linker/BackwardsCompatTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/BackwardsCompatTest.scala index ca12a09277..54a1a0f105 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/BackwardsCompatTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/BackwardsCompatTest.scala @@ -80,18 +80,17 @@ class BackwardsCompatTest { val classDefFiles = classDefs.map(MemClassDefIRFile(_)) val logger = new ScalaConsoleLogger(Level.Error) - Future.traverse(TestIRRepo.previousLibs.toSeq) { case (version, libFuture) => - libFuture.flatMap { lib => - val config = StandardConfig().withCheckIR(true) - val linker = StandardImpl.linker(config) - val out = MemOutputDirectory() - - linker.link(lib ++ classDefFiles, moduleInitializers, out, logger) - }.recover { - case e: Throwable => - throw new AssertionError( - s"linking stdlib $version failed: ${e.getMessage()}", e) - } + TestIRRepo.sequentiallyForEachPreviousLib { (version, lib) => + val config = StandardConfig().withCheckIR(true) + val linker = StandardImpl.linker(config) + val out = MemOutputDirectory() + + linker.link(lib ++ classDefFiles, moduleInitializers, out, logger) + .recover { + case e: Throwable => + throw new AssertionError( + s"linking stdlib $version failed: ${e.getMessage()}", e) + } } } } diff --git a/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRRepo.scala b/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRRepo.scala index 61fa025c77..f3048d0623 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRRepo.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/testutils/TestIRRepo.scala @@ -13,7 +13,6 @@ package org.scalajs.linker.testutils import scala.concurrent._ -import scala.concurrent.ExecutionContext.Implicits.global import org.scalajs.linker.StandardImpl import org.scalajs.linker.interface.IRFile @@ -21,14 +20,54 @@ import org.scalajs.linker.interface.IRFile object TestIRRepo { private val globalIRCache = StandardImpl.irFileCache() - val minilib: Future[Seq[IRFile]] = load(StdlibHolder.minilib) - val javalib: Future[Seq[IRFile]] = load(StdlibHolder.javalib) + val minilib: Future[Seq[IRFile]] = loadGlobal(StdlibHolder.minilib) + val javalib: Future[Seq[IRFile]] = loadGlobal(StdlibHolder.javalib) val empty: Future[Seq[IRFile]] = Future.successful(Nil) - val previousLibs: Map[String, Future[Seq[IRFile]]] = - StdlibHolder.previousLibs.map(x => x._1 -> load(x._2)) - private def load(stdlibPath: String) = { + private def loadGlobal(stdlibPath: String): Future[Seq[IRFile]] = { + import scala.concurrent.ExecutionContext.Implicits.global + Platform.loadJar(stdlibPath) .flatMap(globalIRCache.newCache.cached _) } + + /** For each previous lib, calls `f(version, irFiles)`, and combines the result. + * + * This method applies `f` *sequentially*. It waits until the returned + * `Future` completes before moving on to the next iteration. + */ + def sequentiallyForEachPreviousLib[A](f: (String, Seq[IRFile]) => Future[A])( + implicit ec: ExecutionContext): Future[List[A]] = { + + // sort for determinism + val sortedPreviousLibs = StdlibHolder.previousLibs.toList.sortBy(_._1) + + sequentialFutureTraverse(sortedPreviousLibs) { case (version, path) => + Platform.loadJar(path).flatMap { files => + val cache = globalIRCache.newCache + cache + .cached(files) + .flatMap(f(version, _)) + .andThen { case _ => cache.free() } + } + } + } + + /** Like `Future.traverse`, but waits until each `Future` has completed + * before starting the next one. + */ + private def sequentialFutureTraverse[A, B](items: List[A])(f: A => Future[B])( + implicit ec: ExecutionContext): Future[List[B]] = { + items match { + case Nil => + Future.successful(Nil) + case head :: tail => + for { + headResult <- f(head) + tailResult <- sequentialFutureTraverse(tail)(f) + } yield { + headResult :: tailResult + } + } + } } From a00497878ed250360a27622fea29ebfbb35c3690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 16 Mar 2024 10:47:41 +0100 Subject: [PATCH 067/298] CI: Decompose the ir/linkerInterface/linker JS tests in separate processes. This should also help with the memory pressure of `linkerJS/test`. --- Jenkinsfile | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 1c9dc60c29..108cd81b6b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -402,10 +402,18 @@ def Tasks = [ npm install && sbtnoretry ++$scala linker$v/test && sbtnoretry linkerPrivateLibrary/test && - sbtnoretry ++$scala irJS$v/test linkerJS$v/test linkerInterfaceJS$v/test && + sbtnoretry ++$scala irJS$v/test && + sbtnoretry ++$scala linkerInterfaceJS$v/test && + sbtnoretry ++$scala linkerJS$v/test && sbtnoretry 'set scalaJSStage in Global := FullOptStage' \ 'set scalaJSStage in testSuite.v$v := FastOptStage' \ - ++$scala irJS$v/test linkerJS$v/test linkerInterfaceJS$v/test && + ++$scala irJS$v/test && + sbtnoretry 'set scalaJSStage in Global := FullOptStage' \ + 'set scalaJSStage in testSuite.v$v := FastOptStage' \ + ++$scala linkerInterfaceJS$v/test && + sbtnoretry 'set scalaJSStage in Global := FullOptStage' \ + 'set scalaJSStage in testSuite.v$v := FastOptStage' \ + ++$scala linkerJS$v/test && sbtnoretry ++$scala testSuite$v/bootstrap:test && sbtnoretry 'set scalaJSStage in Global := FullOptStage' \ 'set scalaJSStage in testSuite.v$v := FastOptStage' \ From af924f4d5ef381e3d26bf5044aa056e25ba215c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 15 Mar 2024 22:33:33 +0100 Subject: [PATCH 068/298] Add LinkedClass.hasDirectInstances. It exposes whether the given class is directly instantiated. This is required for the WebAssembly backend, which needs to build vtables for concrete classes only. --- .../main/scala/org/scalajs/linker/frontend/BaseLinker.scala | 1 + .../main/scala/org/scalajs/linker/standard/LinkedClass.scala | 1 + project/BinaryIncompatibilities.scala | 3 +++ 3 files changed, 5 insertions(+) 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 692eb8a7f1..f120dff28a 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 @@ -166,6 +166,7 @@ private[frontend] object BaseLinker { classDef.pos, ancestors.toList, hasInstances = classInfo.isAnySubclassInstantiated, + hasDirectInstances = classInfo.isInstantiated, hasInstanceTests = classInfo.areInstanceTestsUsed, hasRuntimeTypeInfo = classInfo.isDataAccessed, fieldsRead = classInfo.fieldsRead.toSet, diff --git a/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedClass.scala b/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedClass.scala index 58b71fb725..afa257b289 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedClass.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/standard/LinkedClass.scala @@ -55,6 +55,7 @@ final class LinkedClass( // Actual Linking info val ancestors: List[ClassName], val hasInstances: Boolean, + val hasDirectInstances: Boolean, val hasInstanceTests: Boolean, val hasRuntimeTypeInfo: Boolean, val fieldsRead: Set[FieldName], diff --git a/project/BinaryIncompatibilities.scala b/project/BinaryIncompatibilities.scala index 49cbe2d2a0..e37b343839 100644 --- a/project/BinaryIncompatibilities.scala +++ b/project/BinaryIncompatibilities.scala @@ -24,6 +24,9 @@ object BinaryIncompatibilities { ) val Linker = Seq( + // !!! Breaking, OK in minor release + ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.linker.standard.LinkedClass.this"), + // private, not an issue ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.linker.standard.CommonPhaseConfig.this"), ) From f05baed4b572756dfcf3069eada2b9eeafc7b6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 15 Mar 2024 22:48:47 +0100 Subject: [PATCH 069/298] Only set `$c_C.prototype.$classData` if `C` has *direct* instances. It is not useful for classes that only have strict sub instances, i.e., classes that are effectively abstract. --- .../org/scalajs/linker/backend/emitter/ClassEmitter.scala | 4 ++-- .../scala/org/scalajs/linker/backend/emitter/Emitter.scala | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 e2f2419835..2d0dd515ea 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 @@ -826,7 +826,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { def genTypeData(className: ClassName, kind: ClassKind, superClass: Option[ClassIdent], ancestors: List[ClassName], - jsNativeLoadSpec: Option[JSNativeLoadSpec], hasInstances: Boolean)( + jsNativeLoadSpec: Option[JSNativeLoadSpec], hasDirectInstances: Boolean)( implicit moduleContext: ModuleContext, globalKnowledge: GlobalKnowledge, pos: Position): WithGlobals[List[js.Tree]] = { import TreeDSL._ @@ -847,7 +847,7 @@ private[emitter] final class ClassEmitter(sjsGen: SJSGen) { val kindOrCtorParam = { if (isJSType) js.IntLiteral(2) else if (kind == ClassKind.Interface) js.IntLiteral(1) - else if (kind.isClass && hasInstances) globalVar(VarField.c, className) + else if (kind.isClass && hasDirectInstances) globalVar(VarField.c, className) else js.IntLiteral(0) } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala index cdf17260e8..9d49e5855b 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala @@ -716,14 +716,14 @@ final class Emitter[E >: Null <: js.Tree]( if (linkedClass.hasRuntimeTypeInfo) { main ++= extractWithGlobals(classTreeCache.typeData.getOrElseUpdate( - linkedClass.hasInstances, + linkedClass.hasDirectInstances, classEmitter.genTypeData( className, // invalidated by overall class cache (part of ancestors) kind, // invalidated by class version linkedClass.superClass, // invalidated by class version linkedClass.ancestors, // invalidated by overall class cache (identity) linkedClass.jsNativeLoadSpec, // invalidated by class version - linkedClass.hasInstances // invalidated directly (it is the input to `getOrElseUpdate`) + linkedClass.hasDirectInstances // invalidated directly (it is the input to `getOrElseUpdate`) )(moduleContext, classCache, linkedClass.pos).map(postTransform(_, 0)))) } } From e4861b0ede528c9dcf11263bc7004b5cf851af5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 17 Mar 2024 10:24:44 +0100 Subject: [PATCH 070/298] Upgrade to Scala 2.12.19. --- Jenkinsfile | 5 +- .../{2.12.18 => 2.12.19}/BlacklistedTests.txt | 0 .../{2.12.18 => 2.12.19}/neg/choices.check | 0 .../neg/partestInvalidFlag.check | 0 .../{2.12.18 => 2.12.19}/neg/t11952b.check | 0 .../neg/t6446-additional.check | 0 .../{2.12.18 => 2.12.19}/neg/t6446-list.check | 0 .../neg/t6446-missing.check | 0 .../neg/t6446-show-phases.check | 0 .../neg/t7494-no-options.check | 0 .../run/Course-2002-01.check | 0 .../run/Course-2002-02.check | 0 .../run/Course-2002-04.check | 0 .../run/Course-2002-08.check | 0 .../run/Course-2002-09.check | 0 .../run/Course-2002-10.check | 0 .../{2.12.18 => 2.12.19}/run/Meter.check | 0 .../run/MeterCaseClass.check | 0 .../run/anyval-box-types.check | 0 .../scalajs/{2.12.18 => 2.12.19}/run/bugs.sem | 0 .../run/caseClassHash.check | 0 .../{2.12.18 => 2.12.19}/run/classof.check | 0 .../{2.12.18 => 2.12.19}/run/deeps.check | 0 .../run/dynamic-anyval.check | 0 .../{2.12.18 => 2.12.19}/run/exceptions-2.sem | 0 .../run/exceptions-nest.check | 0 .../run/exceptions-nest.sem | 0 .../run/impconvtimes.check | 0 .../{2.12.18 => 2.12.19}/run/imports.check | 0 .../run/inlineHandlers.sem | 0 .../run/interpolation.check | 0 .../run/interpolationMultiline1.check | 0 .../run/macro-bundle-static.check | 0 .../run/macro-bundle-toplevel.check | 0 .../run/macro-bundle-whitebox-decl.check | 0 .../{2.12.18 => 2.12.19}/run/misc.check | 0 .../run/optimizer-array-load.sem | 0 .../{2.12.18 => 2.12.19}/run/pf-catch.sem | 0 .../{2.12.18 => 2.12.19}/run/promotion.check | 0 .../{2.12.18 => 2.12.19}/run/runtime.check | 0 .../{2.12.18 => 2.12.19}/run/spec-self.check | 0 .../{2.12.18 => 2.12.19}/run/structural.check | 0 .../{2.12.18 => 2.12.19}/run/t0421-new.check | 0 .../{2.12.18 => 2.12.19}/run/t0421-old.check | 0 .../{2.12.18 => 2.12.19}/run/t1503.sem | 0 .../{2.12.18 => 2.12.19}/run/t3702.check | 0 .../{2.12.18 => 2.12.19}/run/t4148.sem | 0 .../{2.12.18 => 2.12.19}/run/t4617.check | 0 .../{2.12.18 => 2.12.19}/run/t5356.check | 0 .../{2.12.18 => 2.12.19}/run/t5552.check | 0 .../{2.12.18 => 2.12.19}/run/t5568.check | 0 .../{2.12.18 => 2.12.19}/run/t5629b.check | 0 .../{2.12.18 => 2.12.19}/run/t5680.check | 0 .../{2.12.18 => 2.12.19}/run/t5866.check | 0 .../run/t6318_primitives.check | 0 .../{2.12.18 => 2.12.19}/run/t6662.check | 0 .../{2.12.18 => 2.12.19}/run/t6827.sem | 0 .../{2.12.18 => 2.12.19}/run/t7657.check | 0 .../{2.12.18 => 2.12.19}/run/t7763.sem | 0 .../{2.12.18 => 2.12.19}/run/t8570a.check | 0 .../{2.12.18 => 2.12.19}/run/t8601b.sem | 0 .../{2.12.18 => 2.12.19}/run/t8601c.sem | 0 .../{2.12.18 => 2.12.19}/run/t8601d.sem | 0 .../{2.12.18 => 2.12.19}/run/t8764.check | 0 .../{2.12.18 => 2.12.19}/run/t9387b.check | 0 .../{2.12.18 => 2.12.19}/run/t9656.check | 0 .../run/try-catch-unify.check | 0 .../run/virtpatmat_switch.check | 0 .../run/virtpatmat_typetag.check | 0 project/Build.scala | 1 + .../change-config-and-source/build.sbt | 2 +- .../incremental/change-config/build.sbt | 2 +- .../incremental/fix-compile-error/build.sbt | 2 +- .../linker/concurrent-linker-use/build.sbt | 2 +- .../sbt-test/linker/custom-linker/build.sbt | 4 +- .../no-root-dependency-resolution/build.sbt | 2 +- .../linker/non-existent-classpath/build.sbt | 2 +- .../sbt-test/settings/cross-version/build.sbt | 2 +- .../src/sbt-test/settings/env-vars/build.sbt | 2 +- .../settings/legacy-link-empty/build.sbt | 2 +- .../settings/legacy-link-tasks/build.sbt | 2 +- .../sbt-test/settings/module-init/build.sbt | 2 +- .../sbt-test/settings/source-map/build.sbt | 2 +- .../testing/multi-framework/build.sbt | 2 +- .../resources/2.12.19/BlacklistedTests.txt | 197 ++++++++++++++++++ 85 files changed, 216 insertions(+), 17 deletions(-) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/BlacklistedTests.txt (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/neg/choices.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/neg/partestInvalidFlag.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/neg/t11952b.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/neg/t6446-additional.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/neg/t6446-list.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/neg/t6446-missing.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/neg/t6446-show-phases.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/neg/t7494-no-options.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/Course-2002-01.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/Course-2002-02.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/Course-2002-04.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/Course-2002-08.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/Course-2002-09.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/Course-2002-10.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/Meter.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/MeterCaseClass.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/anyval-box-types.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/bugs.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/caseClassHash.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/classof.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/deeps.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/dynamic-anyval.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/exceptions-2.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/exceptions-nest.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/exceptions-nest.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/impconvtimes.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/imports.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/inlineHandlers.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/interpolation.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/interpolationMultiline1.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/macro-bundle-static.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/macro-bundle-toplevel.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/macro-bundle-whitebox-decl.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/misc.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/optimizer-array-load.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/pf-catch.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/promotion.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/runtime.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/spec-self.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/structural.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t0421-new.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t0421-old.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t1503.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t3702.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t4148.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t4617.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t5356.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t5552.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t5568.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t5629b.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t5680.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t5866.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t6318_primitives.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t6662.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t6827.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t7657.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t7763.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t8570a.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t8601b.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t8601c.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t8601d.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t8764.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t9387b.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/t9656.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/try-catch-unify.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/virtpatmat_switch.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.12.18 => 2.12.19}/run/virtpatmat_typetag.check (100%) create mode 100644 scala-test-suite/src/test/resources/2.12.19/BlacklistedTests.txt diff --git a/Jenkinsfile b/Jenkinsfile index 108cd81b6b..f4886117a6 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -479,8 +479,8 @@ def otherJavaVersions = ["11", "16"] def allJavaVersions = otherJavaVersions.clone() allJavaVersions << mainJavaVersion -def mainScalaVersion = "2.12.18" -def mainScalaVersions = ["2.12.18", "2.13.12"] +def mainScalaVersion = "2.12.19" +def mainScalaVersions = ["2.12.19", "2.13.12"] def otherScalaVersions = [ "2.12.2", "2.12.3", @@ -497,6 +497,7 @@ def otherScalaVersions = [ "2.12.15", "2.12.16", "2.12.17", + "2.12.18", "2.13.0", "2.13.1", "2.13.2", diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/BlacklistedTests.txt b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/BlacklistedTests.txt similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/BlacklistedTests.txt rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/BlacklistedTests.txt diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/neg/choices.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/neg/choices.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/neg/choices.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/neg/choices.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/neg/partestInvalidFlag.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/neg/partestInvalidFlag.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/neg/partestInvalidFlag.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/neg/partestInvalidFlag.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/neg/t11952b.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/neg/t11952b.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/neg/t11952b.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/neg/t11952b.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/neg/t6446-additional.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/neg/t6446-additional.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/neg/t6446-additional.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/neg/t6446-additional.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/neg/t6446-list.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/neg/t6446-list.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/neg/t6446-list.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/neg/t6446-list.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/neg/t6446-missing.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/neg/t6446-missing.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/neg/t6446-missing.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/neg/t6446-missing.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/neg/t6446-show-phases.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/neg/t6446-show-phases.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/neg/t6446-show-phases.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/neg/t6446-show-phases.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/neg/t7494-no-options.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/neg/t7494-no-options.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/neg/t7494-no-options.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/neg/t7494-no-options.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/Course-2002-01.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/Course-2002-01.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/Course-2002-01.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/Course-2002-01.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/Course-2002-02.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/Course-2002-02.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/Course-2002-02.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/Course-2002-02.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/Course-2002-04.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/Course-2002-04.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/Course-2002-04.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/Course-2002-04.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/Course-2002-08.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/Course-2002-08.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/Course-2002-08.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/Course-2002-08.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/Course-2002-09.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/Course-2002-09.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/Course-2002-09.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/Course-2002-09.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/Course-2002-10.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/Course-2002-10.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/Course-2002-10.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/Course-2002-10.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/Meter.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/Meter.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/Meter.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/Meter.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/MeterCaseClass.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/MeterCaseClass.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/MeterCaseClass.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/MeterCaseClass.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/anyval-box-types.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/anyval-box-types.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/anyval-box-types.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/anyval-box-types.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/bugs.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/bugs.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/bugs.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/bugs.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/caseClassHash.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/caseClassHash.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/caseClassHash.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/caseClassHash.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/classof.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/classof.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/classof.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/classof.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/deeps.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/deeps.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/deeps.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/deeps.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/dynamic-anyval.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/dynamic-anyval.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/dynamic-anyval.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/dynamic-anyval.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/exceptions-2.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/exceptions-2.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/exceptions-2.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/exceptions-2.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/exceptions-nest.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/exceptions-nest.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/exceptions-nest.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/exceptions-nest.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/exceptions-nest.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/exceptions-nest.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/exceptions-nest.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/exceptions-nest.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/impconvtimes.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/impconvtimes.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/impconvtimes.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/impconvtimes.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/imports.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/imports.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/imports.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/imports.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/inlineHandlers.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/inlineHandlers.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/inlineHandlers.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/inlineHandlers.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/interpolation.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/interpolation.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/interpolation.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/interpolation.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/interpolationMultiline1.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/interpolationMultiline1.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/interpolationMultiline1.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/interpolationMultiline1.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/macro-bundle-static.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/macro-bundle-static.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/macro-bundle-static.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/macro-bundle-static.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/macro-bundle-toplevel.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/macro-bundle-toplevel.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/macro-bundle-toplevel.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/macro-bundle-toplevel.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/macro-bundle-whitebox-decl.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/macro-bundle-whitebox-decl.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/macro-bundle-whitebox-decl.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/macro-bundle-whitebox-decl.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/misc.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/misc.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/misc.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/misc.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/optimizer-array-load.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/optimizer-array-load.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/optimizer-array-load.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/optimizer-array-load.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/pf-catch.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/pf-catch.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/pf-catch.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/pf-catch.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/promotion.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/promotion.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/promotion.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/promotion.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/runtime.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/runtime.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/runtime.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/runtime.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/spec-self.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/spec-self.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/spec-self.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/spec-self.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/structural.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/structural.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/structural.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/structural.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t0421-new.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t0421-new.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t0421-new.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t0421-new.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t0421-old.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t0421-old.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t0421-old.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t0421-old.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t1503.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t1503.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t1503.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t1503.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t3702.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t3702.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t3702.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t3702.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t4148.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t4148.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t4148.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t4148.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t4617.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t4617.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t4617.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t4617.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t5356.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t5356.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t5356.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t5356.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t5552.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t5552.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t5552.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t5552.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t5568.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t5568.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t5568.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t5568.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t5629b.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t5629b.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t5629b.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t5629b.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t5680.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t5680.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t5680.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t5680.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t5866.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t5866.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t5866.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t5866.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t6318_primitives.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t6318_primitives.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t6318_primitives.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t6318_primitives.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t6662.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t6662.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t6662.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t6662.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t6827.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t6827.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t6827.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t6827.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t7657.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t7657.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t7657.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t7657.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t7763.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t7763.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t7763.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t7763.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t8570a.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t8570a.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t8570a.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t8570a.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t8601b.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t8601b.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t8601b.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t8601b.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t8601c.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t8601c.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t8601c.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t8601c.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t8601d.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t8601d.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t8601d.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t8601d.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t8764.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t8764.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t8764.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t8764.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t9387b.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t9387b.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t9387b.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t9387b.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t9656.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t9656.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/t9656.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/t9656.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/try-catch-unify.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/try-catch-unify.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/try-catch-unify.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/try-catch-unify.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/virtpatmat_switch.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/virtpatmat_switch.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/virtpatmat_switch.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/virtpatmat_switch.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/virtpatmat_typetag.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/virtpatmat_typetag.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.18/run/virtpatmat_typetag.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.12.19/run/virtpatmat_typetag.check diff --git a/project/Build.scala b/project/Build.scala index 43f4be4847..d392347803 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -917,6 +917,7 @@ object Build { "2.12.16", "2.12.17", "2.12.18", + "2.12.19", ), cross213ScalaVersions := Seq( "2.13.0", diff --git a/sbt-plugin/src/sbt-test/incremental/change-config-and-source/build.sbt b/sbt-plugin/src/sbt-test/incremental/change-config-and-source/build.sbt index d12712d68d..d0f231b6de 100644 --- a/sbt-plugin/src/sbt-test/incremental/change-config-and-source/build.sbt +++ b/sbt-plugin/src/sbt-test/incremental/change-config-and-source/build.sbt @@ -1,4 +1,4 @@ -scalaVersion := "2.12.18" +scalaVersion := "2.12.19" enablePlugins(ScalaJSPlugin) diff --git a/sbt-plugin/src/sbt-test/incremental/change-config/build.sbt b/sbt-plugin/src/sbt-test/incremental/change-config/build.sbt index d12712d68d..d0f231b6de 100644 --- a/sbt-plugin/src/sbt-test/incremental/change-config/build.sbt +++ b/sbt-plugin/src/sbt-test/incremental/change-config/build.sbt @@ -1,4 +1,4 @@ -scalaVersion := "2.12.18" +scalaVersion := "2.12.19" enablePlugins(ScalaJSPlugin) diff --git a/sbt-plugin/src/sbt-test/incremental/fix-compile-error/build.sbt b/sbt-plugin/src/sbt-test/incremental/fix-compile-error/build.sbt index d12712d68d..d0f231b6de 100644 --- a/sbt-plugin/src/sbt-test/incremental/fix-compile-error/build.sbt +++ b/sbt-plugin/src/sbt-test/incremental/fix-compile-error/build.sbt @@ -1,4 +1,4 @@ -scalaVersion := "2.12.18" +scalaVersion := "2.12.19" enablePlugins(ScalaJSPlugin) diff --git a/sbt-plugin/src/sbt-test/linker/concurrent-linker-use/build.sbt b/sbt-plugin/src/sbt-test/linker/concurrent-linker-use/build.sbt index f3238517cd..0c84905ef6 100644 --- a/sbt-plugin/src/sbt-test/linker/concurrent-linker-use/build.sbt +++ b/sbt-plugin/src/sbt-test/linker/concurrent-linker-use/build.sbt @@ -11,7 +11,7 @@ lazy val concurrentUseOfLinkerTest = taskKey[Any]("") name := "Scala.js sbt test" version := scalaJSVersion -scalaVersion := "2.12.18" +scalaVersion := "2.12.19" enablePlugins(ScalaJSPlugin) diff --git a/sbt-plugin/src/sbt-test/linker/custom-linker/build.sbt b/sbt-plugin/src/sbt-test/linker/custom-linker/build.sbt index 2576235cf2..1d4b83961d 100644 --- a/sbt-plugin/src/sbt-test/linker/custom-linker/build.sbt +++ b/sbt-plugin/src/sbt-test/linker/custom-linker/build.sbt @@ -13,14 +13,14 @@ inThisBuild(Def.settings( version := scalaJSVersion, - scalaVersion := "2.12.18", + scalaVersion := "2.12.19", )) lazy val check = taskKey[Any]("") lazy val customLinker = project.in(file("custom-linker")) .settings( - scalaVersion := "2.12.18", // needs to match the minor version of Scala used by sbt + scalaVersion := "2.12.19", // needs to match the minor version of Scala used by sbt libraryDependencies += "org.scala-js" %% "scalajs-linker" % scalaJSVersion, ) diff --git a/sbt-plugin/src/sbt-test/linker/no-root-dependency-resolution/build.sbt b/sbt-plugin/src/sbt-test/linker/no-root-dependency-resolution/build.sbt index 541d53caf8..2359461fa6 100644 --- a/sbt-plugin/src/sbt-test/linker/no-root-dependency-resolution/build.sbt +++ b/sbt-plugin/src/sbt-test/linker/no-root-dependency-resolution/build.sbt @@ -1,7 +1,7 @@ name := "Scala.js sbt test" version in ThisBuild := scalaJSVersion -scalaVersion in ThisBuild := "2.12.18" +scalaVersion in ThisBuild := "2.12.19" // Disable the IvyPlugin on the root project disablePlugins(sbt.plugins.IvyPlugin) diff --git a/sbt-plugin/src/sbt-test/linker/non-existent-classpath/build.sbt b/sbt-plugin/src/sbt-test/linker/non-existent-classpath/build.sbt index bf0b1a8bc8..a3d81f1d03 100644 --- a/sbt-plugin/src/sbt-test/linker/non-existent-classpath/build.sbt +++ b/sbt-plugin/src/sbt-test/linker/non-existent-classpath/build.sbt @@ -1,5 +1,5 @@ version := scalaJSVersion -scalaVersion := "2.12.18" +scalaVersion := "2.12.19" enablePlugins(ScalaJSPlugin) diff --git a/sbt-plugin/src/sbt-test/settings/cross-version/build.sbt b/sbt-plugin/src/sbt-test/settings/cross-version/build.sbt index 98b9b4802a..bab1e57ecf 100644 --- a/sbt-plugin/src/sbt-test/settings/cross-version/build.sbt +++ b/sbt-plugin/src/sbt-test/settings/cross-version/build.sbt @@ -3,7 +3,7 @@ import org.scalajs.sbtplugin.ScalaJSCrossVersion val check = taskKey[Unit]("Run checks of this test") version := scalaJSVersion -scalaVersion := "2.12.18" +scalaVersion := "2.12.19" lazy val js = project.enablePlugins(ScalaJSPlugin).settings( check := { diff --git a/sbt-plugin/src/sbt-test/settings/env-vars/build.sbt b/sbt-plugin/src/sbt-test/settings/env-vars/build.sbt index 55967e1eb6..83878dc189 100644 --- a/sbt-plugin/src/sbt-test/settings/env-vars/build.sbt +++ b/sbt-plugin/src/sbt-test/settings/env-vars/build.sbt @@ -1,5 +1,5 @@ inThisBuild(Def.settings( - scalaVersion := "2.12.18", + scalaVersion := "2.12.19", )) lazy val sharedSettings = Def.settings( diff --git a/sbt-plugin/src/sbt-test/settings/legacy-link-empty/build.sbt b/sbt-plugin/src/sbt-test/settings/legacy-link-empty/build.sbt index e8419e778a..4044def10a 100644 --- a/sbt-plugin/src/sbt-test/settings/legacy-link-empty/build.sbt +++ b/sbt-plugin/src/sbt-test/settings/legacy-link-empty/build.sbt @@ -1,4 +1,4 @@ version := scalaJSVersion -scalaVersion := "2.12.18" +scalaVersion := "2.12.19" enablePlugins(ScalaJSPlugin) diff --git a/sbt-plugin/src/sbt-test/settings/legacy-link-tasks/build.sbt b/sbt-plugin/src/sbt-test/settings/legacy-link-tasks/build.sbt index 309e32ab98..001fe4b7ca 100644 --- a/sbt-plugin/src/sbt-test/settings/legacy-link-tasks/build.sbt +++ b/sbt-plugin/src/sbt-test/settings/legacy-link-tasks/build.sbt @@ -1,7 +1,7 @@ val checkNoClosure = taskKey[Unit]("Check that fullOptJS wasn't run with closure") version := scalaJSVersion -scalaVersion := "2.12.18" +scalaVersion := "2.12.19" enablePlugins(ScalaJSPlugin) diff --git a/sbt-plugin/src/sbt-test/settings/module-init/build.sbt b/sbt-plugin/src/sbt-test/settings/module-init/build.sbt index a70d51266f..b3cb9bef50 100644 --- a/sbt-plugin/src/sbt-test/settings/module-init/build.sbt +++ b/sbt-plugin/src/sbt-test/settings/module-init/build.sbt @@ -3,7 +3,7 @@ import org.scalajs.linker.interface.ModuleInitializer val check = taskKey[Unit]("Run checks of this test") version := scalaJSVersion -scalaVersion := "2.12.18" +scalaVersion := "2.12.19" enablePlugins(ScalaJSPlugin) diff --git a/sbt-plugin/src/sbt-test/settings/source-map/build.sbt b/sbt-plugin/src/sbt-test/settings/source-map/build.sbt index 7bfe7a52b6..fa3901cdcc 100644 --- a/sbt-plugin/src/sbt-test/settings/source-map/build.sbt +++ b/sbt-plugin/src/sbt-test/settings/source-map/build.sbt @@ -3,7 +3,7 @@ import org.scalajs.linker.interface.ModuleInitializer val check = taskKey[Unit]("Run checks of this test") version := scalaJSVersion -scalaVersion := "2.12.18" +scalaVersion := "2.12.19" enablePlugins(ScalaJSPlugin) diff --git a/sbt-plugin/src/sbt-test/testing/multi-framework/build.sbt b/sbt-plugin/src/sbt-test/testing/multi-framework/build.sbt index 4b3100395b..5a8b4240c9 100644 --- a/sbt-plugin/src/sbt-test/testing/multi-framework/build.sbt +++ b/sbt-plugin/src/sbt-test/testing/multi-framework/build.sbt @@ -1,5 +1,5 @@ inThisBuild(version := scalaJSVersion) -inThisBuild(scalaVersion := "2.12.18") +inThisBuild(scalaVersion := "2.12.19") lazy val root = project.in(file(".")). aggregate(multiTestJS, multiTestJVM) diff --git a/scala-test-suite/src/test/resources/2.12.19/BlacklistedTests.txt b/scala-test-suite/src/test/resources/2.12.19/BlacklistedTests.txt new file mode 100644 index 0000000000..6c78101e5b --- /dev/null +++ b/scala-test-suite/src/test/resources/2.12.19/BlacklistedTests.txt @@ -0,0 +1,197 @@ +## Do not compile +scala/lang/annotations/BytecodeTest.scala +scala/lang/annotations/RunTest.scala +scala/lang/traits/BytecodeTest.scala +scala/lang/traits/RunTest.scala +scala/lang/primitives/NaNTest.scala +scala/lang/primitives/BoxUnboxTest.scala +scala/collection/SeqTest.scala +scala/collection/Sizes.scala +scala/collection/immutable/HashMapTest.scala +scala/collection/immutable/HashSetTest.scala +scala/collection/immutable/ListMapTest.scala +scala/collection/immutable/MapHashcodeTest.scala +scala/collection/immutable/SetTest.scala +scala/collection/immutable/SeqTest.scala +scala/collection/immutable/SmallMapTest.scala +scala/collection/immutable/SortedMapTest.scala +scala/collection/immutable/SortedSetTest.scala +scala/collection/immutable/TreeMapTest.scala +scala/collection/immutable/TreeSetTest.scala +scala/reflect/ClassOfTest.scala +scala/reflect/QTest.scala +scala/reflect/io/AbstractFileTest.scala +scala/reflect/io/ZipArchiveTest.scala +scala/reflect/internal/util/AbstractFileClassLoaderTest.scala +scala/reflect/internal/util/FileUtilsTest.scala +scala/reflect/internal/util/SourceFileTest.scala +scala/reflect/internal/util/StringOpsTest.scala +scala/reflect/internal/util/WeakHashSetTest.scala +scala/reflect/internal/LongNamesTest.scala +scala/reflect/internal/MirrorsTest.scala +scala/reflect/internal/NamesTest.scala +scala/reflect/internal/PositionsTest.scala +scala/reflect/internal/PrintersTest.scala +scala/reflect/internal/ScopeTest.scala +scala/reflect/internal/TreeGenTest.scala +scala/reflect/internal/TypesTest.scala +scala/reflect/macros/AttachmentsTest.scala +scala/reflect/runtime/ReflectionUtilsShowTest.scala +scala/reflect/runtime/ThreadSafetyTest.scala +scala/runtime/BooleanBoxingTest.scala +scala/runtime/ByteBoxingTest.scala +scala/runtime/CharBoxingTest.scala +scala/runtime/DoubleBoxingTest.scala +scala/runtime/IntBoxingTest.scala +scala/runtime/FloatBoxingTest.scala +scala/runtime/LongBoxingTest.scala +scala/runtime/ShortBoxingTest.scala +scala/tools/cmd/CommandLineParserTest.scala +scala/tools/nsc/Build.scala +scala/tools/nsc/DeterminismTest.scala +scala/tools/nsc/DeterminismTester.scala +scala/tools/nsc/FileUtils.scala +scala/tools/nsc/GlobalCustomizeClassloaderTest.scala +scala/tools/nsc/PickleWriteTest.scala +scala/tools/nsc/PipelineMainTest.scala +scala/tools/nsc/async/AnnotationDrivenAsync.scala +scala/tools/nsc/async/CustomFuture.scala +scala/tools/nsc/backend/jvm/BTypesTest.scala +scala/tools/nsc/backend/jvm/BytecodeTest.scala +scala/tools/nsc/backend/jvm/ClassfileParserTest.scala +scala/tools/nsc/backend/jvm/DefaultMethodTest.scala +scala/tools/nsc/backend/jvm/DirectCompileTest.scala +scala/tools/nsc/backend/jvm/GenericSignaturesTest.scala +scala/tools/nsc/backend/jvm/IndyLambdaDirectTest.scala +scala/tools/nsc/backend/jvm/IndyLambdaTest.scala +scala/tools/nsc/backend/jvm/IndySammyTest.scala +scala/tools/nsc/backend/jvm/InnerClassAttributeTest.scala +scala/tools/nsc/backend/jvm/LineNumberTest.scala +scala/tools/nsc/backend/jvm/NestedClassesCollectorTest.scala +scala/tools/nsc/backend/jvm/OptimizedBytecodeTest.scala +scala/tools/nsc/backend/jvm/PerRunInitTest.scala +scala/tools/nsc/backend/jvm/StringConcatTest.scala +scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzerTest.scala +scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerTest.scala +scala/tools/nsc/backend/jvm/opt/AnalyzerTest.scala +scala/tools/nsc/backend/jvm/opt/BoxUnboxAndInlineTest.scala +scala/tools/nsc/backend/jvm/opt/BoxUnboxTest.scala +scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala +scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala +scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala +scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala +scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala +scala/tools/nsc/backend/jvm/opt/EmptyLabelsAndLineNumbersTest.scala +scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala +scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala +scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala +scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +scala/tools/nsc/backend/jvm/opt/InlineSourceMatcherTest.scala +scala/tools/nsc/backend/jvm/opt/InlineWarningTest.scala +scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala +scala/tools/nsc/backend/jvm/opt/ScalaInlineInfoTest.scala +scala/tools/nsc/backend/jvm/opt/SimplifyJumpsTest.scala +scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala +scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala +scala/tools/nsc/ScriptRunnerTest.scala +scala/tools/nsc/classpath/AggregateClassPathTest.scala +scala/tools/nsc/classpath/JrtClassPathTest.scala +scala/tools/nsc/classpath/MultiReleaseJarTest.scala +scala/tools/nsc/classpath/PathResolverBaseTest.scala +scala/tools/nsc/classpath/VirtualDirectoryClassPathTest.scala +scala/tools/nsc/classpath/ZipAndJarFileLookupFactoryTest.scala +scala/tools/nsc/doc/html/HtmlDocletTest.scala +scala/tools/nsc/interpreter/CompletionTest.scala +scala/tools/nsc/interpreter/ScriptedTest.scala +scala/tools/nsc/interpreter/TabulatorTest.scala +scala/tools/nsc/parser/ParserTest.scala +scala/tools/nsc/reporters/ConsoleReporterTest.scala +scala/tools/nsc/reporters/WConfTest.scala +scala/tools/nsc/settings/ScalaVersionTest.scala +scala/tools/nsc/settings/SettingsTest.scala +scala/tools/nsc/settings/TargetTest.scala +scala/tools/nsc/symtab/CannotHaveAttrsTest.scala +scala/tools/nsc/symtab/FlagsTest.scala +scala/tools/nsc/symtab/FreshNameExtractorTest.scala +scala/tools/nsc/symtab/StdNamesTest.scala +scala/tools/nsc/symtab/SymbolLoadersAssociatedFileTest.scala +scala/tools/nsc/symtab/SymbolTableForUnitTesting.scala +scala/tools/nsc/symtab/SymbolTableTest.scala +scala/tools/nsc/symtab/classfile/PicklerTest.scala +scala/tools/nsc/transform/MixinTest.scala +scala/tools/nsc/transform/SpecializationTest.scala +scala/tools/nsc/transform/ThicketTransformerTest.scala +scala/tools/nsc/transform/delambdafy/DelambdafyTest.scala +scala/tools/nsc/transform/patmat/SolvingTest.scala +scala/tools/nsc/transform/patmat/PatmatBytecodeTest.scala +scala/tools/nsc/typechecker/Implicits.scala +scala/tools/nsc/typechecker/NamerTest.scala +scala/tools/nsc/typechecker/ParamAliasTest.scala +scala/tools/nsc/typechecker/TreeAttachmentTest.scala +scala/tools/nsc/typechecker/TypedTreeTest.scala +scala/tools/nsc/util/StackTraceTest.scala +scala/tools/testing/AllocationTest.scala +scala/tools/testing/BytecodeTesting.scala +scala/tools/testing/JOL.scala +scala/tools/testing/RunTesting.scala +scala/tools/testing/VirtualCompilerTesting.scala +scala/util/matching/RegexTest.scala + +## Do not link +scala/MatchErrorSerializationTest.scala +scala/PartialFunctionSerializationTest.scala +scala/lang/stringinterpol/StringContextTest.scala +scala/collection/IteratorTest.scala +scala/collection/NewBuilderTest.scala +scala/collection/ParallelConsistencyTest.scala +scala/collection/SetMapRulesTest.scala +scala/collection/SeqViewTest.scala +scala/collection/SetMapConsistencyTest.scala +scala/collection/concurrent/TrieMapTest.scala +scala/collection/convert/WrapperSerializationTest.scala +scala/collection/immutable/ListTest.scala +scala/collection/immutable/RedBlackTreeSerialFormat.scala +scala/collection/immutable/StreamTest.scala +scala/collection/immutable/StringLikeTest.scala +scala/collection/immutable/VectorTest.scala +scala/collection/mutable/AnyRefMapTest.scala +scala/collection/mutable/ArrayBufferTest.scala +scala/collection/mutable/MutableListTest.scala +scala/collection/mutable/OpenHashMapTest.scala +scala/collection/mutable/PriorityQueueTest.scala +scala/collection/parallel/TaskTest.scala +scala/collection/parallel/immutable/ParRangeTest.scala +scala/concurrent/FutureTest.scala +scala/concurrent/duration/SerializationTest.scala +scala/concurrent/impl/DefaultPromiseTest.scala +scala/io/SourceTest.scala +scala/runtime/ScalaRunTimeTest.scala +scala/sys/process/PipedProcessTest.scala +scala/sys/process/ProcessTest.scala +scala/tools/testing/AssertUtilTest.scala +scala/tools/testing/AssertThrowsTest.scala +scala/util/SpecVersionTest.scala +scala/util/SystemPropertiesTest.scala + +## Tests fail + +# Reflection +scala/reflect/ClassTagTest.scala + +# Require strict-floats +scala/math/BigDecimalTest.scala + +# Difference of getClass() on primitive values +scala/collection/immutable/RangeTest.scala + +# Test fails only some times with +# 'set scalaJSOptimizerOptions in scalaTestSuite ~= (_.withDisableOptimizer(true))' +# and' 'set scalaJSUseRhino in Global := false' +scala/collection/immutable/PagedSeqTest.scala + +# Bugs +scala/collection/convert/MapWrapperTest.scala + +# Tests passed but are too slow (timeouts) +scala/collection/immutable/ListSetTest.scala +scala/util/SortingTest.scala From 24266f98fcfa0e4133a3af1d504643b021f30c36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 17 Mar 2024 11:05:35 +0100 Subject: [PATCH 071/298] Avoid an internal deprecation in junit-runtime. Scala 2.13.13 starts warning about these deprecations, which we must therefore avoid. It might have missed them before because they are secondary constructors. --- junit-runtime/src/main/scala/org/junit/Assume.scala | 4 ++-- .../main/scala/org/junit/AssumptionViolatedException.scala | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/junit-runtime/src/main/scala/org/junit/Assume.scala b/junit-runtime/src/main/scala/org/junit/Assume.scala index ba9bdf8011..3d44f8be5d 100644 --- a/junit-runtime/src/main/scala/org/junit/Assume.scala +++ b/junit-runtime/src/main/scala/org/junit/Assume.scala @@ -33,13 +33,13 @@ object Assume { @noinline def assumeThat[T](actual: T, matcher: Matcher[T]): Unit = { if (!matcher.matches(actual.asInstanceOf[AnyRef])) - throw new AssumptionViolatedException(actual, matcher) + throw new AssumptionViolatedException(null, matcher, actual) } @noinline def assumeThat[T](message: String, actual: T, matcher: Matcher[T]): Unit = { if (!matcher.matches(actual.asInstanceOf[AnyRef])) - throw new AssumptionViolatedException(message, actual, matcher) + throw new AssumptionViolatedException(message, matcher, actual) } @noinline diff --git a/junit-runtime/src/main/scala/org/junit/AssumptionViolatedException.scala b/junit-runtime/src/main/scala/org/junit/AssumptionViolatedException.scala index a2adc0db01..315bcfa0e3 100644 --- a/junit-runtime/src/main/scala/org/junit/AssumptionViolatedException.scala +++ b/junit-runtime/src/main/scala/org/junit/AssumptionViolatedException.scala @@ -19,6 +19,10 @@ class AssumptionViolatedException protected (fAssumption: String, def this(message: String, expected: Any, matcher: Matcher[_]) = this(message, true, fMatcher = matcher, fValue = expected.asInstanceOf[AnyRef]) + // Non-deprecated access to the full constructor for use in `Assume.scala` + private[junit] def this(message: String, matcher: Matcher[_], actual: Any) = + this(message, true, fMatcher = matcher, fValue = actual.asInstanceOf[AnyRef]) + def this(message: String) = this(message, false, null, null) From 10634fc03818fd2d2f84db2c4f3422d971eee769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 17 Mar 2024 11:22:16 +0100 Subject: [PATCH 072/298] Upgrade to Scala 2.13.13. --- Jenkinsfile | 5 +- .../scala/scalajs/js/WrappedDictionary.scala | 4 + .../scala/scalajs/js/WrappedMap.scala | 4 + .../{2.13.12 => 2.13.13}/BlacklistedTests.txt | 5 +- .../{2.13.12 => 2.13.13}/neg/choices.check | 0 .../neg/partestInvalidFlag.check | 0 .../{2.13.12 => 2.13.13}/neg/t11952b.check | 0 .../{2.13.12 => 2.13.13}/neg/t12494.check | 0 .../neg/t6446-additional.check | 0 .../{2.13.12 => 2.13.13}/neg/t6446-list.check | 0 .../neg/t6446-missing.check | 0 .../neg/t6446-show-phases.check | 0 .../neg/t7494-no-options.check | 0 .../run/Course-2002-01.check | 0 .../run/Course-2002-02.check | 0 .../run/Course-2002-04.check | 0 .../run/Course-2002-08.check | 0 .../run/Course-2002-09.check | 0 .../run/Course-2002-10.check | 0 .../{2.13.12 => 2.13.13}/run/Meter.check | 0 .../run/MeterCaseClass.check | 0 .../run/anyval-box-types.check | 0 .../scalajs/{2.13.12 => 2.13.13}/run/bugs.sem | 0 .../run/caseClassHash.check | 0 .../{2.13.12 => 2.13.13}/run/classof.check | 0 .../{2.13.12 => 2.13.13}/run/deeps.check | 0 .../run/dynamic-anyval.check | 0 .../{2.13.12 => 2.13.13}/run/exceptions-2.sem | 0 .../run/exceptions-nest.check | 0 .../run/exceptions-nest.sem | 0 .../run/impconvtimes.check | 0 .../{2.13.12 => 2.13.13}/run/imports.check | 0 .../run/inlineHandlers.sem | 0 .../run/interpolation.check | 0 .../run/interpolationMultiline1.check | 0 .../run/macro-bundle-static.check | 0 .../run/macro-bundle-toplevel.check | 0 .../run/macro-bundle-whitebox-decl.check | 0 ...expand-varargs-implicit-over-varargs.check | 0 .../{2.13.12 => 2.13.13}/run/misc.check | 0 .../run/optimizer-array-load.sem | 0 .../{2.13.12 => 2.13.13}/run/pf-catch.sem | 0 .../{2.13.12 => 2.13.13}/run/promotion.check | 0 .../{2.13.12 => 2.13.13}/run/runtime.check | 0 .../run/sammy_vararg_cbn.check | 0 .../{2.13.12 => 2.13.13}/run/spec-self.check | 0 .../run/string-switch.check | 0 .../{2.13.12 => 2.13.13}/run/structural.check | 0 .../{2.13.12 => 2.13.13}/run/t0421-new.check | 0 .../{2.13.12 => 2.13.13}/run/t0421-old.check | 0 .../{2.13.12 => 2.13.13}/run/t12221.check | 0 .../{2.13.12 => 2.13.13}/run/t1503.sem | 0 .../{2.13.12 => 2.13.13}/run/t3702.check | 0 .../{2.13.12 => 2.13.13}/run/t4148.sem | 0 .../{2.13.12 => 2.13.13}/run/t4617.check | 0 .../{2.13.12 => 2.13.13}/run/t5356.check | 0 .../{2.13.12 => 2.13.13}/run/t5552.check | 0 .../{2.13.12 => 2.13.13}/run/t5568.check | 0 .../{2.13.12 => 2.13.13}/run/t5629b.check | 0 .../{2.13.12 => 2.13.13}/run/t5680.check | 0 .../{2.13.12 => 2.13.13}/run/t5866.check | 0 .../{2.13.12 => 2.13.13}/run/t5966.check | 0 .../{2.13.12 => 2.13.13}/run/t6265.check | 0 .../run/t6318_primitives.check | 0 .../{2.13.12 => 2.13.13}/run/t6662.check | 0 .../{2.13.12 => 2.13.13}/run/t6827.sem | 0 .../{2.13.12 => 2.13.13}/run/t7657.check | 0 .../{2.13.12 => 2.13.13}/run/t7763.sem | 0 .../{2.13.12 => 2.13.13}/run/t8570a.check | 0 .../{2.13.12 => 2.13.13}/run/t8601b.sem | 0 .../{2.13.12 => 2.13.13}/run/t8601c.sem | 0 .../{2.13.12 => 2.13.13}/run/t8601d.sem | 0 .../{2.13.12 => 2.13.13}/run/t8764.check | 0 .../{2.13.12 => 2.13.13}/run/t9387b.check | 0 .../run/try-catch-unify.check | 0 .../run/virtpatmat_switch.check | 0 .../run/virtpatmat_typetag.check | 0 project/Build.scala | 20 +- .../src/sbt-test/cross-version/2.13/build.sbt | 2 +- .../sbt-test/scala3/tasty-reader/build.sbt | 2 +- .../resources/2.13.13/BlacklistedTests.txt | 247 ++++++++++++++++++ 81 files changed, 282 insertions(+), 7 deletions(-) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/BlacklistedTests.txt (99%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/neg/choices.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/neg/partestInvalidFlag.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/neg/t11952b.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/neg/t12494.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/neg/t6446-additional.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/neg/t6446-list.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/neg/t6446-missing.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/neg/t6446-show-phases.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/neg/t7494-no-options.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/Course-2002-01.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/Course-2002-02.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/Course-2002-04.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/Course-2002-08.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/Course-2002-09.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/Course-2002-10.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/Meter.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/MeterCaseClass.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/anyval-box-types.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/bugs.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/caseClassHash.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/classof.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/deeps.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/dynamic-anyval.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/exceptions-2.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/exceptions-nest.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/exceptions-nest.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/impconvtimes.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/imports.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/inlineHandlers.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/interpolation.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/interpolationMultiline1.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/macro-bundle-static.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/macro-bundle-toplevel.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/macro-bundle-whitebox-decl.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/macro-expand-varargs-implicit-over-varargs.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/misc.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/optimizer-array-load.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/pf-catch.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/promotion.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/runtime.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/sammy_vararg_cbn.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/spec-self.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/string-switch.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/structural.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t0421-new.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t0421-old.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t12221.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t1503.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t3702.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t4148.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t4617.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t5356.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t5552.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t5568.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t5629b.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t5680.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t5866.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t5966.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t6265.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t6318_primitives.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t6662.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t6827.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t7657.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t7763.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t8570a.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t8601b.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t8601c.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t8601d.sem (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t8764.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/t9387b.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/try-catch-unify.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/virtpatmat_switch.check (100%) rename partest-suite/src/test/resources/scala/tools/partest/scalajs/{2.13.12 => 2.13.13}/run/virtpatmat_typetag.check (100%) create mode 100644 scala-test-suite/src/test/resources/2.13.13/BlacklistedTests.txt diff --git a/Jenkinsfile b/Jenkinsfile index f4886117a6..9ca49c5e3c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -480,7 +480,7 @@ def allJavaVersions = otherJavaVersions.clone() allJavaVersions << mainJavaVersion def mainScalaVersion = "2.12.19" -def mainScalaVersions = ["2.12.19", "2.13.12"] +def mainScalaVersions = ["2.12.19", "2.13.13"] def otherScalaVersions = [ "2.12.2", "2.12.3", @@ -509,7 +509,8 @@ def otherScalaVersions = [ "2.13.8", "2.13.9", "2.13.10", - "2.13.11" + "2.13.11", + "2.13.12" ] def scala3Version = "3.2.1" diff --git a/library/src/main/scala-new-collections/scala/scalajs/js/WrappedDictionary.scala b/library/src/main/scala-new-collections/scala/scalajs/js/WrappedDictionary.scala index a7624b3e46..bd379384a9 100644 --- a/library/src/main/scala-new-collections/scala/scalajs/js/WrappedDictionary.scala +++ b/library/src/main/scala-new-collections/scala/scalajs/js/WrappedDictionary.scala @@ -99,6 +99,10 @@ final class WrappedDictionary[A](private val dict: js.Dictionary[A]) def iterator: scala.collection.Iterator[(String, A)] = new DictionaryIterator(dict) + /* Warning silenced in build for 2.13.13+: + * overriding method keys in trait MapOps is deprecated (since 2.13.13): + * This method should be an alias for keySet + */ @inline override def keys: scala.collection.Iterable[String] = js.Object.keys(dict.asInstanceOf[js.Object]) diff --git a/library/src/main/scala-new-collections/scala/scalajs/js/WrappedMap.scala b/library/src/main/scala-new-collections/scala/scalajs/js/WrappedMap.scala index 04d2f08517..83d4ace69d 100644 --- a/library/src/main/scala-new-collections/scala/scalajs/js/WrappedMap.scala +++ b/library/src/main/scala-new-collections/scala/scalajs/js/WrappedMap.scala @@ -95,6 +95,10 @@ final class WrappedMap[K, V](private val underlying: js.Map[K, V]) def iterator: scala.collection.Iterator[(K, V)] = underlying.jsIterator().toIterator.map(kv => (kv._1, kv._2)) + /* Warning silenced in build for 2.13.13+: + * overriding method keys in trait MapOps is deprecated (since 2.13.13): + * This method should be an alias for keySet + */ @inline override def keys: scala.collection.Iterable[K] = underlying.asInstanceOf[js.Map.Raw[K, V]].keys().toIterator.to(Iterable) diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/BlacklistedTests.txt b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/BlacklistedTests.txt similarity index 99% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/BlacklistedTests.txt rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/BlacklistedTests.txt index f91b84d6d8..72066a2706 100644 --- a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/BlacklistedTests.txt +++ b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/BlacklistedTests.txt @@ -832,6 +832,7 @@ run/t12390.scala run/repl-release.scala run/eta-dependent.scala run/t10655.scala +run/repl-suspended-warnings.scala # Using Scala Script (partest.ScriptTest) @@ -950,7 +951,6 @@ run/t10641.scala run/t10751.scala run/t10819.scala run/t11385.scala -run/t11731.scala run/t11746.scala run/t11815.scala run/splain.scala @@ -972,6 +972,7 @@ run/package-object-toolbox.scala run/package-object-with-inner-class-in-ancestor.scala run/package-object-with-inner-class-in-ancestor-simpler.scala run/package-object-with-inner-class-in-ancestor-simpler-still.scala +run/t7324.scala run/t10171 # partest.StubErrorMessageTest @@ -994,6 +995,7 @@ run/t12597.scala # partest.ASMConverters run/t9403 +run/nonfatal.scala # partest.BytecodeTest run/t7106 @@ -1119,6 +1121,7 @@ run/t12195 run/t12380 run/t12523 run/t12290 +run/t9714 # Using scala-script run/t7791-script-linenums.scala diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/neg/choices.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/neg/choices.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/neg/choices.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/neg/choices.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/neg/partestInvalidFlag.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/neg/partestInvalidFlag.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/neg/partestInvalidFlag.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/neg/partestInvalidFlag.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/neg/t11952b.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/neg/t11952b.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/neg/t11952b.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/neg/t11952b.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/neg/t12494.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/neg/t12494.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/neg/t12494.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/neg/t12494.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/neg/t6446-additional.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/neg/t6446-additional.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/neg/t6446-additional.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/neg/t6446-additional.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/neg/t6446-list.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/neg/t6446-list.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/neg/t6446-list.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/neg/t6446-list.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/neg/t6446-missing.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/neg/t6446-missing.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/neg/t6446-missing.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/neg/t6446-missing.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/neg/t6446-show-phases.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/neg/t6446-show-phases.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/neg/t6446-show-phases.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/neg/t6446-show-phases.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/neg/t7494-no-options.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/neg/t7494-no-options.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/neg/t7494-no-options.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/neg/t7494-no-options.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/Course-2002-01.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/Course-2002-01.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/Course-2002-01.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/Course-2002-01.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/Course-2002-02.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/Course-2002-02.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/Course-2002-02.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/Course-2002-02.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/Course-2002-04.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/Course-2002-04.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/Course-2002-04.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/Course-2002-04.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/Course-2002-08.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/Course-2002-08.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/Course-2002-08.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/Course-2002-08.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/Course-2002-09.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/Course-2002-09.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/Course-2002-09.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/Course-2002-09.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/Course-2002-10.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/Course-2002-10.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/Course-2002-10.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/Course-2002-10.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/Meter.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/Meter.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/Meter.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/Meter.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/MeterCaseClass.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/MeterCaseClass.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/MeterCaseClass.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/MeterCaseClass.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/anyval-box-types.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/anyval-box-types.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/anyval-box-types.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/anyval-box-types.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/bugs.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/bugs.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/bugs.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/bugs.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/caseClassHash.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/caseClassHash.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/caseClassHash.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/caseClassHash.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/classof.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/classof.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/classof.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/classof.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/deeps.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/deeps.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/deeps.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/deeps.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/dynamic-anyval.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/dynamic-anyval.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/dynamic-anyval.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/dynamic-anyval.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/exceptions-2.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/exceptions-2.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/exceptions-2.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/exceptions-2.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/exceptions-nest.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/exceptions-nest.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/exceptions-nest.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/exceptions-nest.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/exceptions-nest.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/exceptions-nest.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/exceptions-nest.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/exceptions-nest.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/impconvtimes.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/impconvtimes.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/impconvtimes.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/impconvtimes.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/imports.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/imports.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/imports.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/imports.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/inlineHandlers.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/inlineHandlers.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/inlineHandlers.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/inlineHandlers.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/interpolation.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/interpolation.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/interpolation.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/interpolation.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/interpolationMultiline1.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/interpolationMultiline1.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/interpolationMultiline1.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/interpolationMultiline1.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/macro-bundle-static.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/macro-bundle-static.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/macro-bundle-static.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/macro-bundle-static.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/macro-bundle-toplevel.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/macro-bundle-toplevel.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/macro-bundle-toplevel.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/macro-bundle-toplevel.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/macro-bundle-whitebox-decl.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/macro-bundle-whitebox-decl.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/macro-bundle-whitebox-decl.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/macro-bundle-whitebox-decl.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/macro-expand-varargs-implicit-over-varargs.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/macro-expand-varargs-implicit-over-varargs.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/macro-expand-varargs-implicit-over-varargs.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/macro-expand-varargs-implicit-over-varargs.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/misc.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/misc.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/misc.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/misc.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/optimizer-array-load.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/optimizer-array-load.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/optimizer-array-load.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/optimizer-array-load.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/pf-catch.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/pf-catch.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/pf-catch.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/pf-catch.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/promotion.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/promotion.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/promotion.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/promotion.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/runtime.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/runtime.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/runtime.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/runtime.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/sammy_vararg_cbn.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/sammy_vararg_cbn.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/sammy_vararg_cbn.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/sammy_vararg_cbn.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/spec-self.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/spec-self.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/spec-self.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/spec-self.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/string-switch.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/string-switch.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/string-switch.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/string-switch.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/structural.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/structural.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/structural.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/structural.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t0421-new.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t0421-new.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t0421-new.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t0421-new.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t0421-old.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t0421-old.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t0421-old.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t0421-old.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t12221.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t12221.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t12221.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t12221.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t1503.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t1503.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t1503.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t1503.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t3702.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t3702.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t3702.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t3702.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t4148.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t4148.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t4148.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t4148.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t4617.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t4617.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t4617.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t4617.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t5356.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t5356.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t5356.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t5356.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t5552.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t5552.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t5552.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t5552.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t5568.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t5568.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t5568.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t5568.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t5629b.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t5629b.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t5629b.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t5629b.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t5680.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t5680.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t5680.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t5680.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t5866.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t5866.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t5866.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t5866.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t5966.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t5966.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t5966.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t5966.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t6265.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t6265.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t6265.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t6265.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t6318_primitives.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t6318_primitives.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t6318_primitives.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t6318_primitives.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t6662.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t6662.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t6662.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t6662.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t6827.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t6827.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t6827.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t6827.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t7657.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t7657.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t7657.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t7657.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t7763.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t7763.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t7763.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t7763.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t8570a.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t8570a.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t8570a.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t8570a.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t8601b.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t8601b.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t8601b.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t8601b.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t8601c.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t8601c.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t8601c.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t8601c.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t8601d.sem b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t8601d.sem similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t8601d.sem rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t8601d.sem diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t8764.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t8764.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t8764.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t8764.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t9387b.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t9387b.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/t9387b.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/t9387b.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/try-catch-unify.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/try-catch-unify.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/try-catch-unify.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/try-catch-unify.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/virtpatmat_switch.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/virtpatmat_switch.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/virtpatmat_switch.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/virtpatmat_switch.check diff --git a/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/virtpatmat_typetag.check b/partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/virtpatmat_typetag.check similarity index 100% rename from partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.12/run/virtpatmat_typetag.check rename to partest-suite/src/test/resources/scala/tools/partest/scalajs/2.13.13/run/virtpatmat_typetag.check diff --git a/project/Build.scala b/project/Build.scala index d392347803..fb1d1efc85 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -933,6 +933,7 @@ object Build { "2.13.10", "2.13.11", "2.13.12", + "2.13.13", ), default212ScalaVersion := cross212ScalaVersions.value.last, @@ -1779,6 +1780,21 @@ object Build { previousArtifactSetting, mimaBinaryIssueFilters ++= BinaryIncompatibilities.Library, + /* Silence a Scala 2.13.13+ warning that we cannot address without breaking our API. + * See `js.WrappedDictionary.keys` and `js.WrappedMap.keys`. + */ + scalacOptions ++= { + /* We only need the option in 2.13.13+, but listing all previous 2.13.x + * versions is cumberson. We only exclude 2.13.0 and 2.13.1 because + * they did not support -Wconf at all. + */ + val v = scalaVersion.value + if (v.startsWith("2.13.") && v != "2.13.0" && v != "2.13.1") + List("-Wconf:msg=overriding method keys in trait MapOps is deprecated:s") + else + Nil + }, + test in Test := { streams.value.log.warn("Skipping library/test. Run testSuite/test to test library.") }, @@ -2016,14 +2032,14 @@ object Build { case `default213Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 452000 to 453000, + fastLink = 451000 to 452000, fullLink = 94000 to 95000, fastLinkGz = 58000 to 59000, fullLinkGz = 25000 to 26000, )) } else { Some(ExpectedSizes( - fastLink = 309000 to 310000, + fastLink = 308000 to 309000, fullLink = 265000 to 266000, fastLinkGz = 49000 to 50000, fullLinkGz = 43000 to 44000, diff --git a/sbt-plugin/src/sbt-test/cross-version/2.13/build.sbt b/sbt-plugin/src/sbt-test/cross-version/2.13/build.sbt index 66419e36d6..9eaf7236fc 100644 --- a/sbt-plugin/src/sbt-test/cross-version/2.13/build.sbt +++ b/sbt-plugin/src/sbt-test/cross-version/2.13/build.sbt @@ -2,6 +2,6 @@ enablePlugins(ScalaJSPlugin) enablePlugins(ScalaJSJUnitPlugin) version := scalaJSVersion -scalaVersion := "2.13.12" +scalaVersion := "2.13.13" scalaJSUseMainModuleInitializer := true diff --git a/sbt-plugin/src/sbt-test/scala3/tasty-reader/build.sbt b/sbt-plugin/src/sbt-test/scala3/tasty-reader/build.sbt index 283968ec84..a0af60a58e 100644 --- a/sbt-plugin/src/sbt-test/scala3/tasty-reader/build.sbt +++ b/sbt-plugin/src/sbt-test/scala3/tasty-reader/build.sbt @@ -10,7 +10,7 @@ lazy val app = project.in(file("app")) .enablePlugins(ScalaJSPlugin) .dependsOn(testlib) .settings( - scalaVersion := "2.13.12", + scalaVersion := "2.13.13", scalacOptions += "-Ytasty-reader", scalaJSUseMainModuleInitializer := true ) diff --git a/scala-test-suite/src/test/resources/2.13.13/BlacklistedTests.txt b/scala-test-suite/src/test/resources/2.13.13/BlacklistedTests.txt new file mode 100644 index 0000000000..c813883a16 --- /dev/null +++ b/scala-test-suite/src/test/resources/2.13.13/BlacklistedTests.txt @@ -0,0 +1,247 @@ +## Do not compile +scala/ExtractorTest.scala +scala/OptionTest.scala +scala/SerializationStabilityTest.scala +scala/StringTest.scala +scala/collection/FactoriesTest.scala +scala/collection/LazyZipOpsTest.scala +scala/collection/SeqTest.scala +scala/collection/immutable/HashMapTest.scala +scala/collection/immutable/HashSetTest.scala +scala/collection/immutable/IndexedSeqTest.scala +scala/collection/immutable/IntMapTest.scala +scala/collection/immutable/LazyListTest.scala +scala/collection/immutable/ListMapTest.scala +scala/collection/immutable/LongMapTest.scala +scala/collection/immutable/MapHashcodeTest.scala +scala/collection/immutable/SeqTest.scala +scala/collection/immutable/SmallMapTest.scala +scala/collection/immutable/SortedMapTest.scala +scala/collection/immutable/SortedSetTest.scala +scala/collection/immutable/TreeMapTest.scala +scala/collection/immutable/TreeSetTest.scala +scala/lang/annotations/BytecodeTest.scala +scala/lang/annotations/RunTest.scala +scala/lang/traits/BytecodeTest.scala +scala/lang/traits/RunTest.scala +scala/lang/primitives/NaNTest.scala +scala/reflect/ClassOfTest.scala +scala/reflect/FieldAccessTest.scala +scala/reflect/QTest.scala +scala/reflect/macros/AttachmentsTest.scala +scala/reflect/io/ZipArchiveTest.scala +scala/reflect/internal/InferTest.scala +scala/reflect/internal/LongNamesTest.scala +scala/reflect/internal/MirrorsTest.scala +scala/reflect/internal/NamesTest.scala +scala/reflect/internal/PositionsTest.scala +scala/reflect/internal/PrintersTest.scala +scala/reflect/internal/ScopeTest.scala +scala/reflect/internal/SubstMapTest.scala +scala/reflect/internal/TreeGenTest.scala +scala/reflect/internal/TypesTest.scala +scala/reflect/internal/util/AbstractFileClassLoaderTest.scala +scala/reflect/internal/util/FileUtilsTest.scala +scala/reflect/internal/util/SourceFileTest.scala +scala/reflect/internal/util/StringOpsTest.scala +scala/reflect/internal/util/WeakHashSetTest.scala +scala/reflect/io/AbstractFileTest.scala +scala/reflect/runtime/ReflectionUtilsShowTest.scala +scala/reflect/runtime/ThreadSafetyTest.scala +scala/runtime/BooleanBoxingTest.scala +scala/runtime/ByteBoxingTest.scala +scala/runtime/CharBoxingTest.scala +scala/runtime/DoubleBoxingTest.scala +scala/runtime/IntBoxingTest.scala +scala/runtime/FloatBoxingTest.scala +scala/runtime/LongBoxingTest.scala +scala/runtime/ShortBoxingTest.scala +scala/tools/nsc/Build.scala +scala/tools/nsc/DeterminismTest.scala +scala/tools/nsc/DeterminismTester.scala +scala/tools/nsc/FileUtils.scala +scala/tools/nsc/GlobalCustomizeClassloaderTest.scala +scala/tools/nsc/MainRunnerTest.scala +scala/tools/nsc/PhaseAssemblyTest.scala +scala/tools/nsc/PickleWriteTest.scala +scala/tools/nsc/PipelineMainTest.scala +scala/tools/nsc/ScriptRunnerTest.scala +scala/tools/nsc/async/AnnotationDrivenAsyncTest.scala +scala/tools/nsc/async/CustomFuture.scala +scala/tools/nsc/backend/jvm/BTypesTest.scala +scala/tools/nsc/backend/jvm/BytecodeTest.scala +scala/tools/nsc/backend/jvm/ClassfileParserTest.scala +scala/tools/nsc/backend/jvm/DefaultMethodTest.scala +scala/tools/nsc/backend/jvm/DirectCompileTest.scala +scala/tools/nsc/backend/jvm/GenericSignaturesTest.scala +scala/tools/nsc/backend/jvm/IndyLambdaTest.scala +scala/tools/nsc/backend/jvm/IndySammyTest.scala +scala/tools/nsc/backend/jvm/InnerClassAttributeTest.scala +scala/tools/nsc/backend/jvm/LineNumberTest.scala +scala/tools/nsc/backend/jvm/NestedClassesCollectorTest.scala +scala/tools/nsc/backend/jvm/OptimizedBytecodeTest.scala +scala/tools/nsc/backend/jvm/PerRunInitTest.scala +scala/tools/nsc/backend/jvm/StringConcatTest.scala +scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzerTest.scala +scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerTest.scala +scala/tools/nsc/backend/jvm/analysis/TypeFlowAnalyzerTest.scala +scala/tools/nsc/backend/jvm/opt/AnalyzerTest.scala +scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala +scala/tools/nsc/backend/jvm/opt/BoxUnboxAndInlineTest.scala +scala/tools/nsc/backend/jvm/opt/BoxUnboxTest.scala +scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala +scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala +scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala +scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala +scala/tools/nsc/backend/jvm/opt/EmptyLabelsAndLineNumbersTest.scala +scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala +scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala +scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala +scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +scala/tools/nsc/backend/jvm/opt/InlineSourceMatcherTest.scala +scala/tools/nsc/backend/jvm/opt/InlineWarningTest.scala +scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala +scala/tools/nsc/backend/jvm/opt/ScalaInlineInfoTest.scala +scala/tools/nsc/backend/jvm/opt/SimplifyJumpsTest.scala +scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala +scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala +scala/tools/nsc/classpath/AggregateClassPathTest.scala +scala/tools/nsc/classpath/JrtClassPathTest.scala +scala/tools/nsc/classpath/MultiReleaseJarTest.scala +scala/tools/nsc/classpath/PathResolverBaseTest.scala +scala/tools/nsc/classpath/VirtualDirectoryClassPathTest.scala +scala/tools/nsc/classpath/ZipAndJarFileLookupFactoryTest.scala +scala/tools/nsc/doc/html/HtmlDocletTest.scala +scala/tools/nsc/doc/html/ModelFactoryTest.scala +scala/tools/nsc/doc/html/StringLiteralTest.scala +scala/tools/nsc/interpreter/CompletionTest.scala +scala/tools/nsc/interpreter/ScriptedTest.scala +scala/tools/nsc/interpreter/TabulatorTest.scala +scala/tools/nsc/parser/ParserTest.scala +scala/tools/nsc/reporters/ConsoleReporterTest.scala +scala/tools/nsc/reporters/PositionFilterTest.scala +scala/tools/nsc/reporters/WConfTest.scala +scala/tools/nsc/reporters/AbstractCodeActionTest.scala +scala/tools/nsc/reporters/CodeActionXsource3Test.scala +scala/tools/nsc/reporters/CodeActionTest.scala +scala/tools/nsc/settings/ScalaVersionTest.scala +scala/tools/nsc/settings/SettingsTest.scala +scala/tools/nsc/settings/TargetTest.scala +scala/tools/nsc/symtab/CannotHaveAttrsTest.scala +scala/tools/nsc/symtab/FlagsTest.scala +scala/tools/nsc/symtab/FreshNameExtractorTest.scala +scala/tools/nsc/symtab/StdNamesTest.scala +scala/tools/nsc/symtab/SymbolTableForUnitTesting.scala +scala/tools/nsc/symtab/SymbolTableTest.scala +scala/tools/nsc/symtab/classfile/PicklerTest.scala +scala/tools/nsc/transform/ErasureTest.scala +scala/tools/nsc/transform/MixinTest.scala +scala/tools/nsc/transform/ReleaseFenceTest.scala +scala/tools/nsc/transform/SpecializationTest.scala +scala/tools/nsc/transform/ThicketTransformerTest.scala +scala/tools/nsc/transform/UncurryTest.scala +scala/tools/nsc/transform/delambdafy/DelambdafyTest.scala +scala/tools/nsc/transform/patmat/SolvingTest.scala +scala/tools/nsc/transform/patmat/PatmatBytecodeTest.scala +scala/tools/nsc/typechecker/ConstantFolderTest.scala +scala/tools/nsc/typechecker/ImplicitsTest.scala +scala/tools/nsc/typechecker/InferencerTest.scala +scala/tools/nsc/typechecker/NamerTest.scala +scala/tools/nsc/typechecker/OverridingPairsTest.scala +scala/tools/nsc/typechecker/ParamAliasTest.scala +scala/tools/nsc/typechecker/TreeAttachmentTest.scala +scala/tools/nsc/typechecker/TypedTreeTest.scala +scala/tools/nsc/QuickfixTest.scala +scala/tools/nsc/util/StackTraceTest.scala +scala/tools/testkit/ReflectUtilTest.scala +scala/tools/xsbt/BridgeTesting.scala +scala/tools/xsbt/BasicBridgeTest.scala +scala/tools/xsbt/ClassNameTest.scala +scala/tools/xsbt/CodeActionTest.scala +scala/tools/xsbt/DependencyTest.scala +scala/tools/xsbt/ExtractAPITest.scala +scala/tools/xsbt/ExtractUsedNamesTest.scala +scala/tools/xsbt/InteractiveConsoleInterfaceTest.scala +scala/tools/xsbt/SameAPI.scala +scala/tools/xsbt/TestCallback.scala +scala/util/ChainingOpsTest.scala + +## Do not link +scala/CollectTest.scala +scala/MatchErrorSerializationTest.scala +scala/PartialFunctionSerializationTest.scala +scala/lang/stringinterpol/StringContextTest.scala +scala/collection/IterableTest.scala +scala/collection/IteratorTest.scala +scala/collection/NewBuilderTest.scala +scala/collection/SeqViewTest.scala +scala/collection/SetMapConsistencyTest.scala +scala/collection/SetMapRulesTest.scala +scala/collection/Sizes.scala +scala/collection/ViewTest.scala +scala/collection/concurrent/TrieMapTest.scala +scala/collection/convert/EqualsTest.scala +scala/collection/convert/JConcurrentMapWrapperTest.scala +scala/collection/convert/WrapperSerializationTest.scala +scala/collection/immutable/ChampMapSmokeTest.scala +scala/collection/immutable/ChampSetSmokeTest.scala +scala/collection/immutable/LazyListGCTest.scala +scala/collection/immutable/LazyListLazinessTest.scala +scala/collection/immutable/ListTest.scala +scala/collection/immutable/SerializationTest.scala +scala/collection/immutable/StreamTest.scala +scala/collection/immutable/StringLikeTest.scala +scala/collection/immutable/VectorTest.scala +scala/collection/mutable/AnyRefMapTest.scala +scala/collection/mutable/ArrayBufferTest.scala +scala/collection/mutable/ListBufferTest.scala +scala/collection/mutable/OpenHashMapTest.scala +scala/collection/mutable/PriorityQueueTest.scala +scala/collection/mutable/SerializationTest.scala +scala/concurrent/FutureTest.scala +scala/concurrent/duration/SerializationTest.scala +scala/concurrent/impl/DefaultPromiseTest.scala +scala/io/SourceTest.scala +scala/jdk/AccumulatorTest.scala +scala/jdk/DurationConvertersTest.scala +scala/jdk/FunctionConvertersTest.scala +scala/jdk/OptionConvertersTest.scala +scala/jdk/StepperConversionTest.scala +scala/jdk/StepperTest.scala +scala/jdk/StreamConvertersTest.scala +scala/jdk/StreamConvertersTypingTest.scala +scala/math/OrderingTest.scala +scala/runtime/ScalaRunTimeTest.scala +scala/sys/env.scala +scala/sys/process/ParserTest.scala +scala/sys/process/PipedProcessTest.scala +scala/sys/process/ProcessBuilderTest.scala +scala/sys/process/ProcessTest.scala +scala/tools/testkit/AssertUtilTest.scala +scala/util/PropertiesTest.scala +scala/util/SpecVersionTest.scala +scala/util/SystemPropertiesTest.scala + +## Tests fail + +# Reflection +scala/reflect/ClassTagTest.scala + +# Require strict-floats +scala/math/BigDecimalTest.scala + +# Tests passed but are too slow (timeouts) +scala/collection/immutable/ListSetTest.scala +scala/util/SortingTest.scala + +# Whitebox testing of ArrayBuilder.ofUnit +scala/collection/mutable/ArrayBuilderTest.scala + +# Relies on undefined behavior +scala/collection/MapTest.scala +scala/collection/StringOpsTest.scala +scala/collection/StringParsersTest.scala +scala/collection/convert/CollectionConvertersTest.scala +scala/collection/convert/MapWrapperTest.scala +scala/collection/immutable/NumericRangeTest.scala +scala/math/BigIntTest.scala From ba98bb274e910a3318fc0832fb720ab50c159e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 17 Mar 2024 17:11:15 +0100 Subject: [PATCH 073/298] Version 1.16.0. --- ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala index 91d113055f..e932635d21 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala @@ -17,8 +17,8 @@ import java.util.concurrent.ConcurrentHashMap import scala.util.matching.Regex object ScalaJSVersions extends VersionChecks( - current = "1.16.0-SNAPSHOT", - binaryEmitted = "1.16-SNAPSHOT" + current = "1.16.0", + binaryEmitted = "1.16" ) /** Helper class to allow for testing of logic. */ From 80eafa9ecb54f6763c0df32350c9c6cd8e7343c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 18 Mar 2024 13:20:08 +0100 Subject: [PATCH 074/298] Towards 1.16.1. --- .../org/scalajs/ir/ScalaJSVersions.scala | 2 +- project/BinaryIncompatibilities.scala | 21 ------------------- project/Build.scala | 2 +- 3 files changed, 2 insertions(+), 23 deletions(-) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala index e932635d21..eb920f2071 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala @@ -17,7 +17,7 @@ import java.util.concurrent.ConcurrentHashMap import scala.util.matching.Regex object ScalaJSVersions extends VersionChecks( - current = "1.16.0", + current = "1.16.1-SNAPSHOT", binaryEmitted = "1.16" ) diff --git a/project/BinaryIncompatibilities.scala b/project/BinaryIncompatibilities.scala index e37b343839..4713fe6bf8 100644 --- a/project/BinaryIncompatibilities.scala +++ b/project/BinaryIncompatibilities.scala @@ -5,30 +5,9 @@ import com.typesafe.tools.mima.core.ProblemFilters._ object BinaryIncompatibilities { val IR = Seq( - // !!! Breaking, OK in minor release - - ProblemFilters.exclude[MissingTypesProblem]("org.scalajs.ir.Names$FieldName"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Names#FieldName.*"), - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Names#LocalName.fromFieldName"), - - ProblemFilters.exclude[IncompatibleMethTypeProblem]("org.scalajs.ir.Types#RecordType.findField"), - ProblemFilters.exclude[MemberProblem]("org.scalajs.ir.Types#RecordType#Field.*"), - - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Trees#StoreModule.*"), - ProblemFilters.exclude[IncompatibleResultTypeProblem]("org.scalajs.ir.Trees#StoreModule.unapply"), - - ProblemFilters.exclude[MemberProblem]("org.scalajs.ir.Trees#JSPrivateSelect.*"), - ProblemFilters.exclude[MemberProblem]("org.scalajs.ir.Trees#RecordSelect.*"), - ProblemFilters.exclude[MemberProblem]("org.scalajs.ir.Trees#Select.*"), - ProblemFilters.exclude[MemberProblem]("org.scalajs.ir.Trees#SelectStatic.*"), ) val Linker = Seq( - // !!! Breaking, OK in minor release - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.linker.standard.LinkedClass.this"), - - // private, not an issue - ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.linker.standard.CommonPhaseConfig.this"), ) val LinkerInterface = Seq( diff --git a/project/Build.scala b/project/Build.scala index fb1d1efc85..6460bd86bf 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -367,7 +367,7 @@ object Build { val previousVersions = List("1.0.0", "1.0.1", "1.1.0", "1.1.1", "1.2.0", "1.3.0", "1.3.1", "1.4.0", "1.5.0", "1.5.1", "1.6.0", "1.7.0", "1.7.1", "1.8.0", "1.9.0", "1.10.0", "1.10.1", "1.11.0", "1.12.0", "1.13.0", - "1.13.1", "1.13.2", "1.14.0", "1.15.0") + "1.13.1", "1.13.2", "1.14.0", "1.15.0", "1.16.0") val previousVersion = previousVersions.last val previousBinaryCrossVersion = CrossVersion.binaryWith("sjs1_", "") From c8f34b9bb4e3b3fd43a12041d80ac27207ad0432 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 Mar 2024 21:51:03 +0000 Subject: [PATCH 075/298] Bump express from 4.18.2 to 4.19.2 Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2. - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/master/History.md) - [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2) --- updated-dependencies: - dependency-name: express dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- package-lock.json | 140 +++++++++++++++++++--------------------------- package.json | 2 +- 2 files changed, 58 insertions(+), 84 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9ab82aa9f1..78045751fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,7 @@ "packages": { "": { "devDependencies": { - "express": "4.18.2", + "express": "4.19.2", "jsdom": "16.7.0", "jszip": "3.8.0", "source-map-support": "0.5.19" @@ -130,13 +130,13 @@ "dev": true }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dev": true, "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -144,7 +144,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -153,21 +153,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/browser-process-hrtime": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", @@ -256,9 +241,9 @@ } }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "dev": true, "engines": { "node": ">= 0.6" @@ -468,17 +453,17 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dev": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -509,21 +494,6 @@ "node": ">= 0.10.0" } }, - "node_modules/express/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/express/node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1062,6 +1032,21 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -1078,9 +1063,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dev": true, "dependencies": { "bytes": "3.1.2", @@ -1562,13 +1547,13 @@ "dev": true }, "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dev": true, "requires": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -1576,20 +1561,9 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" - }, - "dependencies": { - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, - "requires": { - "side-channel": "^1.0.4" - } - } } }, "browser-process-hrtime": { @@ -1653,9 +1627,9 @@ "dev": true }, "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "dev": true }, "cookie-signature": { @@ -1816,17 +1790,17 @@ "dev": true }, "express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dev": true, "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -1854,15 +1828,6 @@ "vary": "~1.1.2" }, "dependencies": { - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, - "requires": { - "side-channel": "^1.0.4" - } - }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2279,6 +2244,15 @@ "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dev": true }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + }, "querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -2292,9 +2266,9 @@ "dev": true }, "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dev": true, "requires": { "bytes": "3.1.2", diff --git a/package.json b/package.json index 9301eea5bc..d091e07ea6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "devDependencies": { - "express": "4.18.2", + "express": "4.19.2", "jsdom": "16.7.0", "jszip": "3.8.0", "source-map-support": "0.5.19" From 05f39f54db00e417e3b05002af8712996a6d927c Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 17 Mar 2024 10:37:44 +0100 Subject: [PATCH 076/298] Compress SourceMapWriter.Fragment data We use a similar compression strategy than source maps themselves to reduce the in-memory footprint. After linking the test suite, residual memory usage is as follows: | what | main [MB] | PR [MB] | |-------------------|----------:|--------:| | sbt overall heap | 842 | 772 | | backend retained | 147 | 77 | | frontend retained | 215 | 215 | --- .../linker/backend/BasicLinkerBackend.scala | 15 +- .../backend/javascript/SourceMapWriter.scala | 302 +++++++++++++----- 2 files changed, 227 insertions(+), 90 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala index fa7e616880..3c1125348e 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala @@ -41,9 +41,11 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) private[this] var totalModules = 0 private[this] val rewrittenModules = new AtomicInteger(0) + private[this] val fragmentIndex = new SourceMapWriter.Index + private[this] val bodyPrinter: BodyPrinter = { if (config.minify) IdentityPostTransformerBasedBodyPrinter - else if (config.sourceMap) PrintedTreeWithSourceMapBodyPrinter + else if (config.sourceMap) new PrintedTreeWithSourceMapBodyPrinter(fragmentIndex) else PrintedTreeWithoutSourceMapBodyPrinter } @@ -127,7 +129,7 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) val sourceMapURI = OutputPatternsImpl.sourceMapURI(config.outputPatterns, moduleID.id) val smWriter = new SourceMapWriter(sourceMapWriter, jsFileURI, - config.relativizeSourceMapBase) + config.relativizeSourceMapBase, fragmentIndex) jsFileWriter.write(printedModuleSetCache.headerBytes) for (_ <- 0 until printedModuleSetCache.headerNewLineCount) @@ -288,8 +290,8 @@ private object BasicLinkerBackend { private object PrintedTreeWithoutSourceMapBodyPrinter extends PrintedTreeBasedBodyPrinter(PostTransformerWithoutSourceMap) - private object PrintedTreeWithSourceMapBodyPrinter - extends PrintedTreeBasedBodyPrinter(PostTransformerWithSourceMap) + private class PrintedTreeWithSourceMapBodyPrinter(fragmentIndex: SourceMapWriter.Index) + extends PrintedTreeBasedBodyPrinter(new PostTransformerWithSourceMap(fragmentIndex)) private object PostTransformerWithoutSourceMap extends Emitter.PostTransformer[js.PrintedTree] { def transformStats(trees: List[js.Tree], indent: Int): List[js.PrintedTree] = { @@ -306,13 +308,14 @@ private object BasicLinkerBackend { } } - private object PostTransformerWithSourceMap extends Emitter.PostTransformer[js.PrintedTree] { + private class PostTransformerWithSourceMap(fragmentIndex: SourceMapWriter.Index) + extends Emitter.PostTransformer[js.PrintedTree] { def transformStats(trees: List[js.Tree], indent: Int): List[js.PrintedTree] = { if (trees.isEmpty) { Nil // Fast path } else { val jsCodeWriter = new ByteArrayWriter() - val smFragmentBuilder = new SourceMapWriter.FragmentBuilder() + val smFragmentBuilder = new SourceMapWriter.FragmentBuilder(fragmentIndex) val printer = new Printers.JSTreePrinterWithSourceMap(jsCodeWriter, smFragmentBuilder, indent) trees.foreach(printer.printStat(_)) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/SourceMapWriter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/SourceMapWriter.scala index 17b8380891..d46b90dd6a 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/SourceMapWriter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/SourceMapWriter.scala @@ -16,8 +16,10 @@ import java.io._ import java.net.URI import java.nio.ByteBuffer import java.nio.charset.StandardCharsets +import java.util.concurrent.ConcurrentHashMap import java.{util => ju} +import scala.annotation.switch import scala.collection.mutable.{ArrayBuffer, ListBuffer} import org.scalajs.ir @@ -36,6 +38,12 @@ object SourceMapWriter { private final val VLQBaseMask = VLQBase - 1 private final val VLQContinuationBit = VLQBase + // Constants for fragments + private final val FragNewLine = 0 + private final val FragColOnly = 1 + private final val FragColAndPos = 2 + private final val FragColPosName = 3 + private final class NodePosStack { private var topIndex: Int = -1 private var posStack: Array[Position] = new Array(128) @@ -66,24 +74,14 @@ object SourceMapWriter { } } - private sealed abstract class FragmentElement - - private object FragmentElement { - case object NewLine extends FragmentElement - - // name is nullable - final case class Segment(columnInGenerated: Int, pos: Position, name: String) - extends FragmentElement - } - final class Fragment private[SourceMapWriter] ( - private[SourceMapWriter] val elements: Array[FragmentElement]) + private[SourceMapWriter] val data: Array[Byte]) extends AnyVal object Fragment { val Empty: Fragment = new Fragment(new Array(0)) } - sealed abstract class Builder { + sealed abstract class Builder(fragmentIndex: Index) { // Strings are nullable in this stack private val nodePosStack = new SourceMapWriter.NodePosStack nodePosStack.push(NoPosition, null) @@ -126,17 +124,43 @@ object SourceMapWriter { final def insertFragment(fragment: Fragment): Unit = { require(pendingColumnInGenerated < 0, s"Cannot add fragment when in the middle of a line") - val elements = fragment.elements - val len = elements.length - var i = 0 - while (i != len) { - elements(i) match { - case FragmentElement.Segment(columnInGenerated, pos, name) => - doWriteSegment(columnInGenerated, pos, name) - case FragmentElement.NewLine => + val buf = ByteBuffer.wrap(fragment.data) + + var columnInGenerated = 0 + var sourceIndex = 0 + var line: Int = 0 + var column: Int = 0 + var nameIndex: Int = 0 + + while (buf.hasRemaining()) { + (buf.get(): @switch) match { + case FragNewLine => doWriteNewLine() + + case FragColOnly => + columnInGenerated += readRawVLQ(buf) + doWriteSegment(columnInGenerated, null, 0, 0, null) + + case FragColAndPos => + columnInGenerated += readRawVLQ(buf) + sourceIndex += readRawVLQ(buf) + line += readRawVLQ(buf) + column += readRawVLQ(buf) + + val source = fragmentIndex.sources(sourceIndex) + doWriteSegment(columnInGenerated, source, line, column, null) + + case FragColPosName => + columnInGenerated += readRawVLQ(buf) + sourceIndex += readRawVLQ(buf) + line += readRawVLQ(buf) + column += readRawVLQ(buf) + nameIndex += readRawVLQ(buf) + + val source = fragmentIndex.sources(sourceIndex) + val name = fragmentIndex.names(nameIndex) + doWriteSegment(columnInGenerated, source, line, column, name) } - i += 1 } } @@ -169,47 +193,177 @@ object SourceMapWriter { } private def writePendingSegment(): Unit = { - if (pendingColumnInGenerated >= 0) - doWriteSegment(pendingColumnInGenerated, pendingPos, pendingName) + if (pendingColumnInGenerated >= 0) { + if (pendingPos.isEmpty) { + doWriteSegment(pendingColumnInGenerated, null, 0, 0, null) + } else { + doWriteSegment(pendingColumnInGenerated, + pendingPos.source, pendingPos.line, pendingPos.column, pendingName) + } + } + } + + private def readRawVLQ(buf: ByteBuffer): Int = { + var shift = 0 + var value = 0 + + while ({ + val i = buf.get() + value |= (i & 0x7f) << shift + (i & 0x80) != 0 + }) { + shift += 7 + } + + val neg = (value & 1) != 0 + value >>>= 1 + + /* technically, in the neg branch, we'd need to map + * value == 0 to Int.MinValue. However, given that this is not a realistic + * value for what we are dealing with here, we skip that check to avoid a + * branch. + */ + if (neg) -value else value } protected def doWriteNewLine(): Unit - protected def doWriteSegment(columnInGenerated: Int, pos: Position, name: String): Unit + protected def doWriteSegment(columnInGenerated: Int, source: SourceFile, line: Int, column: Int, name: String): Unit protected def doComplete(): Unit } - final class FragmentBuilder extends Builder { - private val elements = new ArrayBuffer[FragmentElement] + final class FragmentBuilder(index: Index) extends Builder(index) { + private val data = new ByteArrayWriter() + + private var lastColumnInGenerated = 0 + private var lastSource: SourceFile = null + private var lastSourceIndex = 0 + private var lastLine: Int = 0 + private var lastColumn: Int = 0 + private var lastNameIndex: Int = 0 protected def doWriteNewLine(): Unit = - elements += FragmentElement.NewLine + data.write(FragNewLine) + + protected def doWriteSegment(columnInGenerated: Int, source: SourceFile, + line: Int, column: Int, name: String): Unit = { + val MaxSegmentLength = 1 + 5 * 5 // segment type + max 5 rawVLQ of max 5 bytes each + val buffer = data.unsafeStartDirectWrite(maxBytes = MaxSegmentLength) + var offset = data.currentSize + + // Write segment type + buffer(offset) = { + if (source == null) FragColOnly + else if (name == null) FragColAndPos + else FragColPosName + } + offset += 1 + + offset = writeRawVLQ(buffer, offset, columnInGenerated-lastColumnInGenerated) + lastColumnInGenerated = columnInGenerated + + if (source != null) { + if (source eq lastSource) { // highly likely + buffer(offset) = 0 + offset += 1 + } else { + val sourceIndex = index.sourceToIndex(source) + offset = writeRawVLQ(buffer, offset, sourceIndex-lastSourceIndex) + lastSource = source + lastSourceIndex = sourceIndex + } - protected def doWriteSegment(columnInGenerated: Int, pos: Position, name: String): Unit = - elements += FragmentElement.Segment(columnInGenerated, pos, name) + // Line field + offset = writeRawVLQ(buffer, offset, line - lastLine) + lastLine = line - protected def doComplete(): Unit = { - if (elements.nonEmpty && elements.last != FragmentElement.NewLine) - throw new IllegalStateException("Trying to complete a fragment in the middle of a line") + // Column field + offset = writeRawVLQ(buffer, offset, column - lastColumn) + lastColumn = column + + // Name field + if (name != null) { + val nameIndex = index.nameToIndex(name) + offset = writeRawVLQ(buffer, offset, nameIndex-lastNameIndex) + lastNameIndex = nameIndex + } + } + data.unsafeEndDirectWrite(offset) + } + + protected def doComplete(): Unit = () + + private def writeRawVLQ(buffer: Array[Byte], offset0: Int, value0: Int): Int = { + // See comment in writeBase64VLQ + val signExtended = value0 >> 31 + var value = (((value0 ^ signExtended) - signExtended) << 1) | (signExtended & 1) + + var offset = offset0 + + while ({ + if ((value & ~0x7f) != 0) + buffer(offset) = ((value & 0x7f) | 0x80).toByte + else + buffer(offset) = (value & 0x7f).toByte + + offset += 1 + value >>>= 7 + + value != 0 + }) () + + offset } def result(): Fragment = - new Fragment(elements.toArray) + new Fragment(data.toByteArray()) + } + + final class Index { + private[SourceMapWriter] val sources = new ArrayBuffer[SourceFile] + private val _srcToIndex = new ConcurrentHashMap[SourceFile, Integer] + + private[SourceMapWriter] val names = new ArrayBuffer[String] + private val _nameToIndex = new ConcurrentHashMap[String, Integer] + + private[SourceMapWriter] def sourceToIndex(source: SourceFile): Int = { + val existing = _srcToIndex.get(source) + if (existing != null) { + existing.intValue() + } else { + sources.synchronized { + val index = sources.size + _srcToIndex.put(source, index) + sources += source + index + } + } + } + + private[SourceMapWriter] def nameToIndex(name: String): Int = { + val existing = _nameToIndex.get(name) + if (existing != null) { + existing.intValue() + } else { + names.synchronized { + val index = names.size + _nameToIndex.put(name, index) + names += name + index + } + } + } } } final class SourceMapWriter(out: ByteArrayWriter, jsFileName: String, - relativizeBaseURI: Option[URI]) - extends SourceMapWriter.Builder { + relativizeBaseURI: Option[URI], fragmentIndex: SourceMapWriter.Index) + extends SourceMapWriter.Builder(fragmentIndex) { import SourceMapWriter._ - private val sources = new ListBuffer[SourceFile] - private val _srcToIndex = new ju.HashMap[SourceFile, Integer] - - private val names = new ListBuffer[String] - private val _nameToIndex = new ju.HashMap[String, Integer] + private val outIndex = new Index private var lineCountInGenerated = 0 private var lastColumnInGenerated = 0 @@ -222,30 +376,6 @@ final class SourceMapWriter(out: ByteArrayWriter, jsFileName: String, writeHeader() - private def sourceToIndex(source: SourceFile): Int = { - val existing = _srcToIndex.get(source) - if (existing != null) { - existing.intValue() - } else { - val index = sources.size - _srcToIndex.put(source, index) - sources += source - index - } - } - - private def nameToIndex(name: String): Int = { - val existing = _nameToIndex.get(name) - if (existing != null) { - existing.intValue() - } else { - val index = names.size - _nameToIndex.put(name, index) - names += name - index - } - } - private def writeJSONString(s: String): Unit = { out.write('\"') out.writeASCIIEscapedJSString(s) @@ -266,7 +396,8 @@ final class SourceMapWriter(out: ByteArrayWriter, jsFileName: String, firstSegmentOfLine = true } - protected def doWriteSegment(columnInGenerated: Int, pos: Position, name: String): Unit = { + protected def doWriteSegment(columnInGenerated: Int, source: SourceFile, + line: Int, column: Int, name: String): Unit = { // scalastyle:off return /* This method is incredibly performance-sensitive, so we resort to @@ -288,23 +419,18 @@ final class SourceMapWriter(out: ByteArrayWriter, jsFileName: String, offset = writeBase64VLQ(buffer, offset, columnInGenerated-lastColumnInGenerated) lastColumnInGenerated = columnInGenerated - // If the position is NoPosition, stop here - if (pos.isEmpty) { + if (source == null) { + // The position was NoPosition, stop here out.unsafeEndDirectWrite(offset) return } - // Extract relevant properties of pendingPos - val source = pos.source - val line = pos.line - val column = pos.column - // Source index field if (source eq lastSource) { // highly likely buffer(offset) = 'A' // 0 in Base64VLQ offset += 1 } else { - val sourceIndex = sourceToIndex(source) + val sourceIndex = outIndex.sourceToIndex(source) offset = writeBase64VLQ(buffer, offset, sourceIndex-lastSourceIndex) lastSource = source lastSourceIndex = sourceIndex @@ -320,7 +446,7 @@ final class SourceMapWriter(out: ByteArrayWriter, jsFileName: String, // Name field if (name != null) { - val nameIndex = nameToIndex(name) + val nameIndex = outIndex.nameToIndex(name) offset = writeBase64VLQ(buffer, offset, nameIndex-lastNameIndex) lastNameIndex = nameIndex } @@ -332,21 +458,29 @@ final class SourceMapWriter(out: ByteArrayWriter, jsFileName: String, protected def doComplete(): Unit = { val relativizeBaseURI = this.relativizeBaseURI // local copy - var restSources = sources.result() + val sources = outIndex.sources // local copy + val sourcesLen = sources.length + out.writeASCIIString("\",\n\"sources\": [") - while (restSources.nonEmpty) { - writeJSONString(SourceFileUtil.webURI(relativizeBaseURI, restSources.head)) - restSources = restSources.tail - if (restSources.nonEmpty) + + var i = 0 + while (i < sourcesLen) { + writeJSONString(SourceFileUtil.webURI(relativizeBaseURI, sources(i))) + i += 1 + if (i < sourcesLen) out.writeASCIIString(", ") } - var restNames = names.result() + val names = outIndex.names // local copy + val namesLen = names.length + out.writeASCIIString("],\n\"names\": [") - while (restNames.nonEmpty) { - writeJSONString(restNames.head) - restNames = restNames.tail - if (restNames.nonEmpty) + + i = 0 + while (i < namesLen) { + writeJSONString(names(i)) + i += 1 + if (i < namesLen) out.writeASCIIString(", ") } out.writeASCIIString("],\n\"lineCount\": ") From 448eb1eb4f6e072863b6b3e4f8a5a5ac53d785f0 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 10 Mar 2024 16:17:23 +0100 Subject: [PATCH 077/298] Remove nested post transforms They are not useful and only make us consume more memory. I initially introduced them because it looked like downstream could avoid running post transforms at all. This seems to not be the case, so there is no point in attempting to provide it. --- .../linker/backend/BasicLinkerBackend.scala | 77 ++--------- .../linker/backend/emitter/Emitter.scala | 129 ++++++++---------- .../org/scalajs/linker/EmitterTest.scala | 15 +- 3 files changed, 78 insertions(+), 143 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala index 3c1125348e..2d6feafa4f 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala @@ -43,19 +43,19 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) private[this] val fragmentIndex = new SourceMapWriter.Index - private[this] val bodyPrinter: BodyPrinter = { - if (config.minify) IdentityPostTransformerBasedBodyPrinter - else if (config.sourceMap) new PrintedTreeWithSourceMapBodyPrinter(fragmentIndex) - else PrintedTreeWithoutSourceMapBodyPrinter - } + private[this] val emitter: Emitter = { + val postTransformer = { + if (config.minify) Emitter.PostTransformer.Identity + else if (config.sourceMap) new PostTransformerWithSourceMap(fragmentIndex) + else PostTransformerWithoutSourceMap + } - private[this] val emitter: Emitter[bodyPrinter.TreeType] = { val emitterConfig = Emitter.Config(config.commonConfig.coreSpec) .withJSHeader(config.jsHeader) .withInternalModulePattern(m => OutputPatternsImpl.moduleName(config.outputPatterns, m.id)) .withMinify(config.minify) - new Emitter(emitterConfig, bodyPrinter.postTransformer) + new Emitter(emitterConfig, postTransformer) } val symbolRequirements: SymbolRequirement = emitter.symbolRequirements @@ -104,7 +104,9 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) jsFileWriter.write(printedModuleSetCache.headerBytes) jsFileWriter.writeASCIIString("'use strict';\n") - bodyPrinter.printWithoutSourceMap(trees, jsFileWriter) + val printer = new Printers.JSTreePrinter(jsFileWriter) + for (tree <- trees) + printer.printStat(tree) jsFileWriter.write(printedModuleSetCache.footerBytes) @@ -138,7 +140,9 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) jsFileWriter.writeASCIIString("'use strict';\n") smWriter.nextLine() - bodyPrinter.printWithSourceMap(trees, jsFileWriter, smWriter) + val printer = new Printers.JSTreePrinterWithSourceMap(jsFileWriter, smWriter, initIndent = 0) + for (tree <- trees) + printer.printStat(tree) jsFileWriter.write(printedModuleSetCache.footerBytes) jsFileWriter.write(("//# sourceMappingURL=" + sourceMapURI + "\n").getBytes(StandardCharsets.UTF_8)) @@ -242,58 +246,7 @@ private object BasicLinkerBackend { } } - private abstract class BodyPrinter { - type TreeType >: Null <: js.Tree - - val postTransformer: Emitter.PostTransformer[TreeType] - - def printWithoutSourceMap(trees: List[TreeType], jsFileWriter: ByteArrayWriter): Unit - def printWithSourceMap(trees: List[TreeType], jsFileWriter: ByteArrayWriter, smWriter: SourceMapWriter): Unit - } - - private object IdentityPostTransformerBasedBodyPrinter extends BodyPrinter { - type TreeType = js.Tree - - val postTransformer: Emitter.PostTransformer[TreeType] = Emitter.PostTransformer.Identity - - def printWithoutSourceMap(trees: List[TreeType], jsFileWriter: ByteArrayWriter): Unit = { - val printer = new Printers.JSTreePrinter(jsFileWriter) - for (tree <- trees) - printer.printStat(tree) - } - - def printWithSourceMap(trees: List[TreeType], jsFileWriter: ByteArrayWriter, smWriter: SourceMapWriter): Unit = { - val printer = new Printers.JSTreePrinterWithSourceMap(jsFileWriter, smWriter, initIndent = 0) - for (tree <- trees) - printer.printStat(tree) - } - } - - private abstract class PrintedTreeBasedBodyPrinter( - val postTransformer: Emitter.PostTransformer[js.PrintedTree] - ) extends BodyPrinter { - type TreeType = js.PrintedTree - - def printWithoutSourceMap(trees: List[TreeType], jsFileWriter: ByteArrayWriter): Unit = { - for (tree <- trees) - jsFileWriter.write(tree.jsCode) - } - - def printWithSourceMap(trees: List[TreeType], jsFileWriter: ByteArrayWriter, smWriter: SourceMapWriter): Unit = { - for (tree <- trees) { - jsFileWriter.write(tree.jsCode) - smWriter.insertFragment(tree.sourceMapFragment) - } - } - } - - private object PrintedTreeWithoutSourceMapBodyPrinter - extends PrintedTreeBasedBodyPrinter(PostTransformerWithoutSourceMap) - - private class PrintedTreeWithSourceMapBodyPrinter(fragmentIndex: SourceMapWriter.Index) - extends PrintedTreeBasedBodyPrinter(new PostTransformerWithSourceMap(fragmentIndex)) - - private object PostTransformerWithoutSourceMap extends Emitter.PostTransformer[js.PrintedTree] { + private object PostTransformerWithoutSourceMap extends Emitter.PostTransformer { def transformStats(trees: List[js.Tree], indent: Int): List[js.PrintedTree] = { if (trees.isEmpty) { Nil // Fast path @@ -309,7 +262,7 @@ private object BasicLinkerBackend { } private class PostTransformerWithSourceMap(fragmentIndex: SourceMapWriter.Index) - extends Emitter.PostTransformer[js.PrintedTree] { + extends Emitter.PostTransformer { def transformStats(trees: List[js.Tree], indent: Int): List[js.PrintedTree] = { if (trees.isEmpty) { Nil // Fast path diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala index 9d49e5855b..a5f851cea0 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala @@ -33,8 +33,7 @@ import EmitterNames._ import GlobalRefUtils._ /** Emits a desugared JS tree to a builder */ -final class Emitter[E >: Null <: js.Tree]( - config: Emitter.Config, postTransformer: Emitter.PostTransformer[E]) { +final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransformer) { import Emitter._ import config._ @@ -64,7 +63,7 @@ final class Emitter[E >: Null <: js.Tree]( val classEmitter: ClassEmitter = new ClassEmitter(sjsGen) - val everyFileStart: List[E] = { + val everyFileStart: List[js.Tree] = { // This postTransform does not count in the statistics postTransformer.transformStats(sjsGen.declarePrototypeVar, 0) } @@ -88,15 +87,13 @@ final class Emitter[E >: Null <: js.Tree]( private[this] var statsMethodsReused: Int = 0 private[this] var statsMethodsInvalidated: Int = 0 private[this] var statsPostTransforms: Int = 0 - private[this] var statsNestedPostTransforms: Int = 0 - private[this] var statsNestedPostTransformsAvoided: Int = 0 val symbolRequirements: SymbolRequirement = Emitter.symbolRequirements(config) val injectedIRFiles: Seq[IRFile] = PrivateLibHolder.files - def emit(moduleSet: ModuleSet, logger: Logger): Result[E] = { + def emit(moduleSet: ModuleSet, logger: Logger): Result = { val WithGlobals(body, globalRefs) = emitInternal(moduleSet, logger) val result = moduleKind match { @@ -140,15 +137,13 @@ final class Emitter[E >: Null <: js.Tree]( } private def emitInternal(moduleSet: ModuleSet, - logger: Logger): WithGlobals[Map[ModuleID, (List[E], Boolean)]] = { + logger: Logger): WithGlobals[Map[ModuleID, (List[js.Tree], Boolean)]] = { // Reset caching stats. statsClassesReused = 0 statsClassesInvalidated = 0 statsMethodsReused = 0 statsMethodsInvalidated = 0 statsPostTransforms = 0 - statsNestedPostTransforms = 0 - statsNestedPostTransformsAvoided = 0 // Update GlobalKnowledge. val invalidateAll = knowledgeGuardian.update(moduleSet) @@ -170,10 +165,7 @@ final class Emitter[E >: Null <: js.Tree]( logger.debug( s"Emitter: Method tree cache stats: reused: $statsMethodsReused -- "+ s"invalidated: $statsMethodsInvalidated") - logger.debug( - s"Emitter: Post transforms: total: $statsPostTransforms -- " + - s"nested: $statsNestedPostTransforms -- " + - s"nested avoided: $statsNestedPostTransformsAvoided") + logger.debug(s"Emitter: Post transforms: $statsPostTransforms") // Inform caches about run completion. state.moduleCaches.filterInPlace((_, c) => c.cleanAfterRun()) @@ -181,12 +173,12 @@ final class Emitter[E >: Null <: js.Tree]( } } - private def postTransform(trees: List[js.Tree], indent: Int): List[E] = { + private def postTransform(trees: List[js.Tree], indent: Int): List[js.Tree] = { statsPostTransforms += 1 postTransformer.transformStats(trees, indent) } - private def postTransform(tree: js.Tree, indent: Int): List[E] = + private def postTransform(tree: js.Tree, indent: Int): List[js.Tree] = postTransform(tree :: Nil, indent) /** Emits all JavaScript code avoiding clashes with global refs. @@ -197,7 +189,7 @@ final class Emitter[E >: Null <: js.Tree]( */ @tailrec private def emitAvoidGlobalClash(moduleSet: ModuleSet, - logger: Logger, secondAttempt: Boolean): WithGlobals[Map[ModuleID, (List[E], Boolean)]] = { + logger: Logger, secondAttempt: Boolean): WithGlobals[Map[ModuleID, (List[js.Tree], Boolean)]] = { val result = emitOnce(moduleSet, logger) val mentionedDangerousGlobalRefs = @@ -223,7 +215,7 @@ final class Emitter[E >: Null <: js.Tree]( } private def emitOnce(moduleSet: ModuleSet, - logger: Logger): WithGlobals[Map[ModuleID, (List[E], Boolean)]] = { + logger: Logger): WithGlobals[Map[ModuleID, (List[js.Tree], Boolean)]] = { // Genreate classes first so we can measure time separately. val generatedClasses = logger.time("Emitter: Generate Classes") { moduleSet.modules.map { module => @@ -297,7 +289,7 @@ final class Emitter[E >: Null <: js.Tree]( * requires consistency between the Analyzer and the Emitter. As such, * it is crucial that we verify it. */ - val defTrees: List[E] = ( + val defTrees: List[js.Tree] = ( /* The declaration of the `$p` variable that temporarily holds * prototypes. */ @@ -418,7 +410,7 @@ final class Emitter[E >: Null <: js.Tree]( } private def genClass(linkedClass: LinkedClass, - moduleContext: ModuleContext): GeneratedClass[E] = { + moduleContext: ModuleContext): GeneratedClass = { val className = linkedClass.className val classCache = classCaches.getOrElseUpdate( @@ -449,7 +441,7 @@ final class Emitter[E >: Null <: js.Tree]( // Main part - val main = List.newBuilder[E] + val main = List.newBuilder[js.Tree] val (linkedInlineableInit, linkedMethods) = classEmitter.extractInlineableInit(linkedClass)(classCache) @@ -679,14 +671,7 @@ final class Emitter[E >: Null <: js.Tree]( allMembers // invalidated directly )(moduleContext, fullClassChangeTracker, linkedClass.pos) // pos invalidated by class version } yield { - // Avoid a nested post transform if we just got the original members back. - if (clazz eq allMembers) { - statsNestedPostTransformsAvoided += 1 - allMembers - } else { - statsNestedPostTransforms += 1 - postTransform(clazz, 0) - } + clazz } } @@ -774,14 +759,14 @@ final class Emitter[E >: Null <: js.Tree]( private final class ModuleCache extends knowledgeGuardian.KnowledgeAccessor { private[this] var _cacheUsed: Boolean = false - private[this] var _importsCache: WithGlobals[List[E]] = WithGlobals.nil + private[this] var _importsCache: WithGlobals[List[js.Tree]] = WithGlobals.nil private[this] var _lastExternalDependencies: Set[String] = Set.empty private[this] var _lastInternalDependencies: Set[ModuleID] = Set.empty - private[this] var _topLevelExportsCache: WithGlobals[List[E]] = WithGlobals.nil + private[this] var _topLevelExportsCache: WithGlobals[List[js.Tree]] = WithGlobals.nil private[this] var _lastTopLevelExports: List[LinkedTopLevelExport] = Nil - private[this] var _initializersCache: WithGlobals[List[E]] = WithGlobals.nil + private[this] var _initializersCache: WithGlobals[List[js.Tree]] = WithGlobals.nil private[this] var _lastInitializers: List[ModuleInitializer.Initializer] = Nil override def invalidate(): Unit = { @@ -802,7 +787,7 @@ final class Emitter[E >: Null <: js.Tree]( } def getOrComputeImports(externalDependencies: Set[String], internalDependencies: Set[ModuleID])( - compute: => WithGlobals[List[E]]): (WithGlobals[List[E]], Boolean) = { + compute: => WithGlobals[List[js.Tree]]): (WithGlobals[List[js.Tree]], Boolean) = { _cacheUsed = true @@ -818,7 +803,7 @@ final class Emitter[E >: Null <: js.Tree]( } def getOrComputeTopLevelExports(topLevelExports: List[LinkedTopLevelExport])( - compute: => WithGlobals[List[E]]): (WithGlobals[List[E]], Boolean) = { + compute: => WithGlobals[List[js.Tree]]): (WithGlobals[List[js.Tree]], Boolean) = { _cacheUsed = true @@ -859,7 +844,7 @@ final class Emitter[E >: Null <: js.Tree]( } def getOrComputeInitializers(initializers: List[ModuleInitializer.Initializer])( - compute: => WithGlobals[List[E]]): (WithGlobals[List[E]], Boolean) = { + compute: => WithGlobals[List[js.Tree]]): (WithGlobals[List[js.Tree]], Boolean) = { _cacheUsed = true @@ -880,20 +865,20 @@ final class Emitter[E >: Null <: js.Tree]( } private final class ClassCache extends knowledgeGuardian.KnowledgeAccessor { - private[this] var _cache: DesugaredClassCache[List[E]] = null + private[this] var _cache: DesugaredClassCache = null private[this] var _lastVersion: Version = Version.Unversioned private[this] var _cacheUsed = false private[this] val _methodCaches = - Array.fill(MemberNamespace.Count)(mutable.Map.empty[MethodName, MethodCache[List[E]]]) + Array.fill(MemberNamespace.Count)(mutable.Map.empty[MethodName, MethodCache[List[js.Tree]]]) private[this] val _memberMethodCache = - mutable.Map.empty[MethodName, MethodCache[List[E]]] + mutable.Map.empty[MethodName, MethodCache[List[js.Tree]]] - private[this] var _constructorCache: Option[MethodCache[List[E]]] = None + private[this] var _constructorCache: Option[MethodCache[List[js.Tree]]] = None private[this] val _exportedMembersCache = - mutable.Map.empty[Int, MethodCache[List[E]]] + mutable.Map.empty[Int, MethodCache[List[js.Tree]]] private[this] var _fullClassChangeTracker: Option[FullClassChangeTracker] = None @@ -914,13 +899,13 @@ final class Emitter[E >: Null <: js.Tree]( _fullClassChangeTracker.foreach(_.startRun()) } - def getCache(version: Version): (DesugaredClassCache[List[E]], Boolean) = { + def getCache(version: Version): (DesugaredClassCache, Boolean) = { _cacheUsed = true if (_cache == null || !_lastVersion.sameVersion(version)) { invalidate() statsClassesInvalidated += 1 _lastVersion = version - _cache = new DesugaredClassCache[List[E]] + _cache = new DesugaredClassCache (_cache, true) } else { statsClassesReused += 1 @@ -929,25 +914,25 @@ final class Emitter[E >: Null <: js.Tree]( } def getMemberMethodCache( - methodName: MethodName): MethodCache[List[E]] = { + methodName: MethodName): MethodCache[List[js.Tree]] = { _memberMethodCache.getOrElseUpdate(methodName, new MethodCache) } def getStaticLikeMethodCache(namespace: MemberNamespace, - methodName: MethodName): MethodCache[List[E]] = { + methodName: MethodName): MethodCache[List[js.Tree]] = { _methodCaches(namespace.ordinal) .getOrElseUpdate(methodName, new MethodCache) } - def getConstructorCache(): MethodCache[List[E]] = { + def getConstructorCache(): MethodCache[List[js.Tree]] = { _constructorCache.getOrElse { - val cache = new MethodCache[List[E]] + val cache = new MethodCache[List[js.Tree]] _constructorCache = Some(cache) cache } } - def getExportedMemberCache(idx: Int): MethodCache[List[E]] = + def getExportedMemberCache(idx: Int): MethodCache[List[js.Tree]] = _exportedMembersCache.getOrElseUpdate(idx, new MethodCache) def getFullClassChangeTracker(): FullClassChangeTracker = { @@ -1015,9 +1000,9 @@ final class Emitter[E >: Null <: js.Tree]( private class FullClassChangeTracker extends knowledgeGuardian.KnowledgeAccessor { private[this] var _lastVersion: Version = Version.Unversioned - private[this] var _lastCtor: WithGlobals[List[E]] = null - private[this] var _lastMemberMethods: List[WithGlobals[List[E]]] = null - private[this] var _lastExportedMembers: List[WithGlobals[List[E]]] = null + private[this] var _lastCtor: WithGlobals[List[js.Tree]] = null + private[this] var _lastMemberMethods: List[WithGlobals[List[js.Tree]]] = null + private[this] var _lastExportedMembers: List[WithGlobals[List[js.Tree]]] = null private[this] var _trackerUsed = false override def invalidate(): Unit = { @@ -1030,9 +1015,9 @@ final class Emitter[E >: Null <: js.Tree]( def startRun(): Unit = _trackerUsed = false - def trackChanged(version: Version, ctor: WithGlobals[List[E]], - memberMethods: List[WithGlobals[List[E]]], - exportedMembers: List[WithGlobals[List[E]]]): Boolean = { + def trackChanged(version: Version, ctor: WithGlobals[List[js.Tree]], + memberMethods: List[WithGlobals[List[js.Tree]]], + exportedMembers: List[WithGlobals[List[js.Tree]]]): Boolean = { @tailrec def allSame[A <: AnyRef](xs: List[A], ys: List[A]): Boolean = { @@ -1074,9 +1059,9 @@ final class Emitter[E >: Null <: js.Tree]( private class CoreJSLibCache extends knowledgeGuardian.KnowledgeAccessor { private[this] var _lastModuleContext: ModuleContext = _ - private[this] var _lib: WithGlobals[CoreJSLib.Lib[List[E]]] = _ + private[this] var _lib: WithGlobals[CoreJSLib.Lib[List[js.Tree]]] = _ - def build(moduleContext: ModuleContext): WithGlobals[CoreJSLib.Lib[List[E]]] = { + def build(moduleContext: ModuleContext): WithGlobals[CoreJSLib.Lib[List[js.Tree]]] = { if (_lib == null || _lastModuleContext != moduleContext) { _lib = CoreJSLib.build(sjsGen, postTransform(_, 0), moduleContext, this) _lastModuleContext = moduleContext @@ -1093,9 +1078,9 @@ final class Emitter[E >: Null <: js.Tree]( object Emitter { /** Result of an emitter run. */ - final class Result[E] private[Emitter]( + final class Result private[Emitter]( val header: String, - val body: Map[ModuleID, (List[E], Boolean)], + val body: Map[ModuleID, (List[js.Tree], Boolean)], val footer: String, val topLevelVarDecls: List[String], val globalRefs: Set[String] @@ -1179,32 +1164,32 @@ object Emitter { new Config(coreSpec.semantics, coreSpec.moduleKind, coreSpec.esFeatures) } - trait PostTransformer[E] { - def transformStats(trees: List[js.Tree], indent: Int): List[E] + trait PostTransformer { + def transformStats(trees: List[js.Tree], indent: Int): List[js.Tree] } object PostTransformer { - object Identity extends PostTransformer[js.Tree] { + object Identity extends PostTransformer { def transformStats(trees: List[js.Tree], indent: Int): List[js.Tree] = trees } } - private final class DesugaredClassCache[E >: Null] { - val privateJSFields = new OneTimeCache[WithGlobals[E]] - val storeJSSuperClass = new OneTimeCache[WithGlobals[E]] - val instanceTests = new OneTimeCache[WithGlobals[E]] - val typeData = new InputEqualityCache[Boolean, WithGlobals[E]] - val setTypeData = new OneTimeCache[E] - val moduleAccessor = new OneTimeCache[WithGlobals[E]] - val staticInitialization = new OneTimeCache[E] - val staticFields = new OneTimeCache[WithGlobals[E]] + private final class DesugaredClassCache { + val privateJSFields = new OneTimeCache[WithGlobals[List[js.Tree]]] + val storeJSSuperClass = new OneTimeCache[WithGlobals[List[js.Tree]]] + val instanceTests = new OneTimeCache[WithGlobals[List[js.Tree]]] + val typeData = new InputEqualityCache[Boolean, WithGlobals[List[js.Tree]]] + val setTypeData = new OneTimeCache[List[js.Tree]] + val moduleAccessor = new OneTimeCache[WithGlobals[List[js.Tree]]] + val staticInitialization = new OneTimeCache[List[js.Tree]] + val staticFields = new OneTimeCache[WithGlobals[List[js.Tree]]] } - private final class GeneratedClass[E]( + private final class GeneratedClass( val className: ClassName, - val main: List[E], - val staticFields: List[E], - val staticInitialization: List[E], + val main: List[js.Tree], + val staticFields: List[js.Tree], + val staticInitialization: List[js.Tree], val trackedGlobalRefs: Set[String], val changed: Boolean ) diff --git a/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala index 50e726106e..450ff7c3fe 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala @@ -137,7 +137,7 @@ class EmitterTest { raw"""Emitter: Method tree cache stats: reused: (\d+) -- invalidated: (\d+)""".r private val EmitterPostTransformStatsMessage = - raw"""Emitter: Post transforms: total: (\d+) -- nested: (\d+) -- nested avoided: (\d+)""".r + raw"""Emitter: Post transforms: (\d+)""".r /** Makes sure that linking a "substantial" program (using `println`) twice * does not invalidate any cache or top-level tree in the second run. @@ -208,21 +208,18 @@ class EmitterTest { // Post transforms - val Seq(postTransforms1, nestedPostTransforms1, _) = + val Seq(postTransforms1) = lines1.assertContainsMatch(EmitterPostTransformStatsMessage).map(_.toInt) - val Seq(postTransforms2, nestedPostTransforms2, _) = + val Seq(postTransforms2) = lines2.assertContainsMatch(EmitterPostTransformStatsMessage).map(_.toInt) - // At the time of writing this test, postTransforms1 reports 216 + // At the time of writing this test, postTransforms1 reports 188 assertTrue( s"Not enough post transforms (got $postTransforms1); extraction must have gone wrong", - postTransforms1 > 200) + postTransforms1 > 180) - assertEquals("Second run must only have nested post transforms", - nestedPostTransforms2, postTransforms2) - assertEquals("Both runs must have the same number of nested post transforms", - nestedPostTransforms1, nestedPostTransforms2) + assertEquals("Second run may not have post transforms", 0, postTransforms2) } } } From 09336f33fc94aef65d60891e45916381042d6fb3 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 24 Mar 2024 13:50:31 +0100 Subject: [PATCH 078/298] Rename postTransform to prePrint in emitter Further, we move the implementations fully into Emitter: At this point its clear, that there won't really be any others (unless we fundamentally change tree printing). --- .../closure/ClosureLinkerBackend.scala | 8 +- .../linker/backend/BasicLinkerBackend.scala | 43 +------- .../linker/backend/emitter/Emitter.scala | 100 ++++++++++++------ .../org/scalajs/linker/EmitterTest.scala | 22 ++-- 4 files changed, 85 insertions(+), 88 deletions(-) diff --git a/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureLinkerBackend.scala b/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureLinkerBackend.scala index 7532e0be47..347465bf72 100644 --- a/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureLinkerBackend.scala +++ b/linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureLinkerBackend.scala @@ -61,12 +61,10 @@ final class ClosureLinkerBackend(config: LinkerBackendImpl.Config) .withTrackAllGlobalRefs(true) .withInternalModulePattern(m => OutputPatternsImpl.moduleName(config.outputPatterns, m.id)) - // Do not apply ClosureAstTransformer eagerly: - // The ASTs used by closure are highly mutable, so re-using them is non-trivial. - // Since closure is slow anyways, we haven't built the optimization. - val postTransformer = Emitter.PostTransformer.Identity + // Do not pre-print trees: We do not want the printed form. + val prePrinter = Emitter.PrePrinter.Off - new Emitter(emitterConfig, postTransformer) + new Emitter(emitterConfig, prePrinter) } val symbolRequirements: SymbolRequirement = emitter.symbolRequirements diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala index 2d6feafa4f..74b4871503 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/BasicLinkerBackend.scala @@ -44,10 +44,10 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) private[this] val fragmentIndex = new SourceMapWriter.Index private[this] val emitter: Emitter = { - val postTransformer = { - if (config.minify) Emitter.PostTransformer.Identity - else if (config.sourceMap) new PostTransformerWithSourceMap(fragmentIndex) - else PostTransformerWithoutSourceMap + val prePrinter = { + if (config.minify) Emitter.PrePrinter.Off + else if (config.sourceMap) new Emitter.PrePrinter.WithSourceMap(fragmentIndex) + else Emitter.PrePrinter.WithoutSourceMap } val emitterConfig = Emitter.Config(config.commonConfig.coreSpec) @@ -55,7 +55,7 @@ final class BasicLinkerBackend(config: LinkerBackendImpl.Config) .withInternalModulePattern(m => OutputPatternsImpl.moduleName(config.outputPatterns, m.id)) .withMinify(config.minify) - new Emitter(emitterConfig, postTransformer) + new Emitter(emitterConfig, prePrinter) } val symbolRequirements: SymbolRequirement = emitter.symbolRequirements @@ -245,37 +245,4 @@ private object BasicLinkerBackend { wasUsed } } - - private object PostTransformerWithoutSourceMap extends Emitter.PostTransformer { - def transformStats(trees: List[js.Tree], indent: Int): List[js.PrintedTree] = { - if (trees.isEmpty) { - Nil // Fast path - } else { - val jsCodeWriter = new ByteArrayWriter() - val printer = new Printers.JSTreePrinter(jsCodeWriter, indent) - - trees.foreach(printer.printStat(_)) - - js.PrintedTree(jsCodeWriter.toByteArray(), SourceMapWriter.Fragment.Empty) :: Nil - } - } - } - - private class PostTransformerWithSourceMap(fragmentIndex: SourceMapWriter.Index) - extends Emitter.PostTransformer { - def transformStats(trees: List[js.Tree], indent: Int): List[js.PrintedTree] = { - if (trees.isEmpty) { - Nil // Fast path - } else { - val jsCodeWriter = new ByteArrayWriter() - val smFragmentBuilder = new SourceMapWriter.FragmentBuilder(fragmentIndex) - val printer = new Printers.JSTreePrinterWithSourceMap(jsCodeWriter, smFragmentBuilder, indent) - - trees.foreach(printer.printStat(_)) - smFragmentBuilder.complete() - - js.PrintedTree(jsCodeWriter.toByteArray(), smFragmentBuilder.result()) :: Nil - } - } - } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala index a5f851cea0..279a2929d0 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Emitter.scala @@ -33,13 +33,13 @@ import EmitterNames._ import GlobalRefUtils._ /** Emits a desugared JS tree to a builder */ -final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransformer) { +final class Emitter(config: Emitter.Config, prePrinter: Emitter.PrePrinter) { import Emitter._ import config._ - require(!config.minify || postTransformer == PostTransformer.Identity, - "When using the 'minify' option, the postTransformer must be Identity.") + require(!config.minify || prePrinter == PrePrinter.Off, + "When using the 'minify' option, the prePrinter must be Off.") private implicit val globalRefTracking: GlobalRefTracking = config.topLevelGlobalRefTracking @@ -64,8 +64,8 @@ final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransfo val classEmitter: ClassEmitter = new ClassEmitter(sjsGen) val everyFileStart: List[js.Tree] = { - // This postTransform does not count in the statistics - postTransformer.transformStats(sjsGen.declarePrototypeVar, 0) + // This prePrint does not count in the statistics + prePrinter.prePrint(sjsGen.declarePrototypeVar, 0) } val coreJSLibCache: CoreJSLibCache = new CoreJSLibCache @@ -86,7 +86,7 @@ final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransfo private[this] var statsClassesInvalidated: Int = 0 private[this] var statsMethodsReused: Int = 0 private[this] var statsMethodsInvalidated: Int = 0 - private[this] var statsPostTransforms: Int = 0 + private[this] var statsPrePrints: Int = 0 val symbolRequirements: SymbolRequirement = Emitter.symbolRequirements(config) @@ -143,7 +143,7 @@ final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransfo statsClassesInvalidated = 0 statsMethodsReused = 0 statsMethodsInvalidated = 0 - statsPostTransforms = 0 + statsPrePrints = 0 // Update GlobalKnowledge. val invalidateAll = knowledgeGuardian.update(moduleSet) @@ -165,7 +165,7 @@ final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransfo logger.debug( s"Emitter: Method tree cache stats: reused: $statsMethodsReused -- "+ s"invalidated: $statsMethodsInvalidated") - logger.debug(s"Emitter: Post transforms: $statsPostTransforms") + logger.debug(s"Emitter: Pre prints: $statsPrePrints") // Inform caches about run completion. state.moduleCaches.filterInPlace((_, c) => c.cleanAfterRun()) @@ -173,13 +173,13 @@ final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransfo } } - private def postTransform(trees: List[js.Tree], indent: Int): List[js.Tree] = { - statsPostTransforms += 1 - postTransformer.transformStats(trees, indent) + private def prePrint(trees: List[js.Tree], indent: Int): List[js.Tree] = { + statsPrePrints += 1 + prePrinter.prePrint(trees, indent) } - private def postTransform(tree: js.Tree, indent: Int): List[js.Tree] = - postTransform(tree :: Nil, indent) + private def prePrint(tree: js.Tree, indent: Int): List[js.Tree] = + prePrint(tree :: Nil, indent) /** Emits all JavaScript code avoiding clashes with global refs. * @@ -248,7 +248,7 @@ final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransfo val moduleImports = extractChangedAndWithGlobals { moduleCache.getOrComputeImports(module.externalDependencies, module.internalDependencies) { - genModuleImports(module).map(postTransform(_, 0)) + genModuleImports(module).map(prePrint(_, 0)) } } @@ -258,7 +258,7 @@ final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransfo */ moduleCache.getOrComputeTopLevelExports(module.topLevelExports) { classEmitter.genTopLevelExports(module.topLevelExports)( - moduleContext, moduleCache).map(postTransform(_, 0)) + moduleContext, moduleCache).map(prePrint(_, 0)) } } @@ -268,7 +268,7 @@ final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransfo WithGlobals.list(initializers.map { initializer => classEmitter.genModuleInitializer(initializer)( moduleContext, moduleCache) - }).map(postTransform(_, 0)) + }).map(prePrint(_, 0)) } } @@ -450,7 +450,7 @@ final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransfo if (kind.isJSClass) { val fieldDefs = classTreeCache.privateJSFields.getOrElseUpdate { classEmitter.genCreatePrivateJSFieldDefsOfJSClass(className)( - moduleContext, classCache).map(postTransform(_, 0)) + moduleContext, classCache).map(prePrint(_, 0)) } main ++= extractWithGlobals(fieldDefs) } @@ -471,7 +471,7 @@ final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransfo main ++= extractWithGlobalsAndChanged(methodCache.getOrElseUpdate(methodDef.version, { classEmitter.genStaticLikeMethod(className, methodDef)(moduleContext, methodCache) - .map(postTransform(_, 0)) + .map(prePrint(_, 0)) })) } } @@ -522,7 +522,7 @@ final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransfo extractWithGlobals(classTreeCache.storeJSSuperClass.getOrElseUpdate({ val jsSuperClass = linkedClass.jsSuperClass.get classEmitter.genStoreJSSuperClass(jsSuperClass)(moduleContext, classCache, linkedClass.pos) - .map(postTransform(_, 1)) + .map(prePrint(_, 1)) })) } else { Nil @@ -552,7 +552,7 @@ final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransfo hasJSSuperClass, // invalidated by class version useESClass, // invalidated by class version jsConstructorDef // part of ctor version - )(moduleContext, ctorCache, linkedClass.pos).map(postTransform(_, memberIndent))) + )(moduleContext, ctorCache, linkedClass.pos).map(prePrint(_, memberIndent))) } else { val ctorVersion = linkedInlineableInit.fold { Version.combine(linkedClass.version) @@ -566,7 +566,7 @@ final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransfo linkedClass.superClass, // invalidated by class version useESClass, // invalidated by class version, linkedInlineableInit // part of ctor version - )(moduleContext, ctorCache, linkedClass.pos).map(postTransform(_, memberIndent))) + )(moduleContext, ctorCache, linkedClass.pos).map(prePrint(_, memberIndent))) } } @@ -620,7 +620,7 @@ final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransfo isJSClass, // invalidated by isJSClassVersion useESClass, // invalidated by isJSClassVersion method // invalidated by method.version - )(moduleContext, methodCache).map(postTransform(_, memberIndent)))) + )(moduleContext, methodCache).map(prePrint(_, memberIndent)))) } // Exported Members @@ -635,7 +635,7 @@ final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransfo isJSClass, // invalidated by isJSClassVersion useESClass, // invalidated by isJSClassVersion member // invalidated by version - )(moduleContext, memberCache).map(postTransform(_, memberIndent)))) + )(moduleContext, memberCache).map(prePrint(_, memberIndent)))) } val hasClassInitializer: Boolean = { @@ -695,7 +695,7 @@ final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransfo if (classEmitter.needInstanceTests(linkedClass)(classCache)) { main ++= extractWithGlobals(classTreeCache.instanceTests.getOrElseUpdate({ classEmitter.genInstanceTests(className, kind)(moduleContext, classCache, linkedClass.pos) - .map(postTransform(_, 0)) + .map(prePrint(_, 0)) })) } @@ -709,14 +709,14 @@ final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransfo linkedClass.ancestors, // invalidated by overall class cache (identity) linkedClass.jsNativeLoadSpec, // invalidated by class version linkedClass.hasDirectInstances // invalidated directly (it is the input to `getOrElseUpdate`) - )(moduleContext, classCache, linkedClass.pos).map(postTransform(_, 0)))) + )(moduleContext, classCache, linkedClass.pos).map(prePrint(_, 0)))) } } if (linkedClass.kind.hasModuleAccessor && linkedClass.hasInstances) { main ++= extractWithGlobals(classTreeCache.moduleAccessor.getOrElseUpdate({ classEmitter.genModuleAccessor(className, isJSClass)(moduleContext, classCache, linkedClass.pos) - .map(postTransform(_, 0)) + .map(prePrint(_, 0)) })) } @@ -727,7 +727,7 @@ final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransfo } else { extractWithGlobals(classTreeCache.staticFields.getOrElseUpdate({ classEmitter.genCreateStaticFieldsOfScalaClass(className)(moduleContext, classCache) - .map(postTransform(_, 0)) + .map(prePrint(_, 0)) })) } @@ -736,7 +736,7 @@ final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransfo val staticInitialization = if (classEmitter.needStaticInitialization(linkedClass)) { classTreeCache.staticInitialization.getOrElseUpdate({ val tree = classEmitter.genStaticInitialization(className)(moduleContext, classCache, linkedClass.pos) - postTransform(tree, 0) + prePrint(tree, 0) }) } else { Nil @@ -1063,7 +1063,7 @@ final class Emitter(config: Emitter.Config, postTransformer: Emitter.PostTransfo def build(moduleContext: ModuleContext): WithGlobals[CoreJSLib.Lib[List[js.Tree]]] = { if (_lib == null || _lastModuleContext != moduleContext) { - _lib = CoreJSLib.build(sjsGen, postTransform(_, 0), moduleContext, this) + _lib = CoreJSLib.build(sjsGen, prePrint(_, 0), moduleContext, this) _lastModuleContext = moduleContext } _lib @@ -1164,13 +1164,45 @@ object Emitter { new Config(coreSpec.semantics, coreSpec.moduleKind, coreSpec.esFeatures) } - trait PostTransformer { - def transformStats(trees: List[js.Tree], indent: Int): List[js.Tree] + sealed trait PrePrinter { + private[Emitter] def prePrint(trees: List[js.Tree], indent: Int): List[js.Tree] } - object PostTransformer { - object Identity extends PostTransformer { - def transformStats(trees: List[js.Tree], indent: Int): List[js.Tree] = trees + object PrePrinter { + object Off extends PrePrinter { + private[Emitter] def prePrint(trees: List[js.Tree], indent: Int): List[js.Tree] = trees + } + + object WithoutSourceMap extends PrePrinter { + private[Emitter] def prePrint(trees: List[js.Tree], indent: Int): List[js.PrintedTree] = { + if (trees.isEmpty) { + Nil // Fast path + } else { + val jsCodeWriter = new ByteArrayWriter() + val printer = new Printers.JSTreePrinter(jsCodeWriter, indent) + + trees.foreach(printer.printStat(_)) + + js.PrintedTree(jsCodeWriter.toByteArray(), SourceMapWriter.Fragment.Empty) :: Nil + } + } + } + + final class WithSourceMap(fragmentIndex: SourceMapWriter.Index) extends PrePrinter { + private[Emitter] def prePrint(trees: List[js.Tree], indent: Int): List[js.PrintedTree] = { + if (trees.isEmpty) { + Nil // Fast path + } else { + val jsCodeWriter = new ByteArrayWriter() + val smFragmentBuilder = new SourceMapWriter.FragmentBuilder(fragmentIndex) + val printer = new Printers.JSTreePrinterWithSourceMap(jsCodeWriter, smFragmentBuilder, indent) + + trees.foreach(printer.printStat(_)) + smFragmentBuilder.complete() + + js.PrintedTree(jsCodeWriter.toByteArray(), smFragmentBuilder.result()) :: Nil + } + } } } diff --git a/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala index 450ff7c3fe..2a473807ec 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/EmitterTest.scala @@ -136,8 +136,8 @@ class EmitterTest { private val EmitterMethodTreeCacheStatsMessage = raw"""Emitter: Method tree cache stats: reused: (\d+) -- invalidated: (\d+)""".r - private val EmitterPostTransformStatsMessage = - raw"""Emitter: Post transforms: (\d+)""".r + private val EmitterPrePrintsStatsMessage = + raw"""Emitter: Pre prints: (\d+)""".r /** Makes sure that linking a "substantial" program (using `println`) twice * does not invalidate any cache or top-level tree in the second run. @@ -206,20 +206,20 @@ class EmitterTest { assertEquals("Second run must reuse all method caches", methodCacheReused2, methodCacheInvalidated1) assertEquals("Second run must not invalidate any method cache", 0, methodCacheInvalidated2) - // Post transforms + // Pre prints - val Seq(postTransforms1) = - lines1.assertContainsMatch(EmitterPostTransformStatsMessage).map(_.toInt) + val Seq(prePrints1) = + lines1.assertContainsMatch(EmitterPrePrintsStatsMessage).map(_.toInt) - val Seq(postTransforms2) = - lines2.assertContainsMatch(EmitterPostTransformStatsMessage).map(_.toInt) + val Seq(prePrints2) = + lines2.assertContainsMatch(EmitterPrePrintsStatsMessage).map(_.toInt) - // At the time of writing this test, postTransforms1 reports 188 + // At the time of writing this test, prePrints1 reports 188 assertTrue( - s"Not enough post transforms (got $postTransforms1); extraction must have gone wrong", - postTransforms1 > 180) + s"Not enough pre prints (got $prePrints1); extraction must have gone wrong", + prePrints1 > 180) - assertEquals("Second run may not have post transforms", 0, postTransforms2) + assertEquals("Second run may not have pre prints", 0, prePrints2) } } } From a7608172be88d121814ef23eac2445bda568fc74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 5 Apr 2024 11:45:31 +0200 Subject: [PATCH 079/298] Make the test about method calls on non-instantiated classes stronger. * Actually run the code and validate that the right exceptions are thrown. * Add a variant where the target of the call is an interface, not a class. --- .../testsuite/compiler/RegressionTest.scala | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/RegressionTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/RegressionTest.scala index 219358cecc..72319844a9 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/RegressionTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/RegressionTest.scala @@ -18,7 +18,7 @@ import org.junit.Test import org.junit.Assert._ import org.junit.Assume._ -import org.scalajs.testsuite.utils.AssertThrows.assertThrows +import org.scalajs.testsuite.utils.AssertThrows.{assertThrows, _} import org.scalajs.testsuite.utils.Platform import org.scalajs.testsuite.utils.Platform._ @@ -268,8 +268,6 @@ class RegressionTest { } @Test def irCheckerDoesNotCheckMethodSignaturesOnClassesWithNoInstance(): Unit = { - assumeTrue("linking only", false) - class Foo // this class will be dropped by base linking class Bar { @@ -281,10 +279,32 @@ class RegressionTest { @noinline def nullBar(): Bar = null + @noinline def nothingBar(): Bar = throw new IllegalStateException() + + // the IR checker must not try to infer the signature of these calls + assertThrowsNPEIfCompliant(nullBar().meth(null)) + assertThrowsNPEIfCompliant((null: Bar).meth(null)) + assertThrows(classOf[IllegalStateException], (nothingBar(): Bar).meth(null)) + } + + @Test def irCheckerDoesNotCheckMethodSignaturesOnInterfacesWithNoInstance(): Unit = { + class Foo // this class will be dropped by base linking + + trait Bar { + /* This method is called, but unreachable because there are no instances + * of `Bar`. It will therefore not make `Foo` reachable. + */ + def meth(foo: Foo): String = foo.toString() + } + + @noinline def nullBar(): Bar = null + + @noinline def nothingBar(): Bar = throw new IllegalStateException() + // the IR checker must not try to infer the signature of these calls - nullBar().meth(null) - (null: Bar).meth(null) - (??? : Bar).meth(null) // scalastyle:ignore + assertThrowsNPEIfCompliant(nullBar().meth(null)) + assertThrowsNPEIfCompliant((null: Bar).meth(null)) + assertThrows(classOf[IllegalStateException], (nothingBar(): Bar).meth(null)) } @Test def orderCtorStatementsWhenInlining_Issue1369(): Unit = { From 40482507304f19369b6c49422bedda12cea2e94b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 5 Apr 2024 13:47:25 +0200 Subject: [PATCH 080/298] Fix #4972: Mark Interfaces with isSubclassInstantiated/hasInstances. --- .../scalajs/linker/analyzer/Analyzer.scala | 29 ++++---- .../org/scalajs/linker/IRCheckerTest.scala | 72 +++++++++++++++++++ 2 files changed, 86 insertions(+), 15 deletions(-) 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 534a30ef85..52a6326a01 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 @@ -1133,23 +1133,22 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, private def subclassInstantiated()(implicit from: From): Unit = { _instantiatedFrom ::= from - if (!(isScalaClass || isJSType)) { - // Ignore - } else if (!_isAnySubclassInstantiated.getAndSet(true)) { - - if (!isNativeJSClass) { - for (clazz <- superClass) { - if (clazz.isNativeJSClass) - clazz.jsNativeLoadSpec.foreach(addLoadSpec(this, _)) - else - addStaticDependency(clazz.className) + if (!_isAnySubclassInstantiated.getAndSet(true)) { + if (!isInterface) { + if (!isNativeJSClass) { + for (clazz <- superClass) { + if (clazz.isNativeJSClass) + clazz.jsNativeLoadSpec.foreach(addLoadSpec(this, _)) + else + addStaticDependency(clazz.className) + } } - } - // Reach exported members - if (!isJSClass) { - for (reachabilityInfo <- data.jsMethodProps) - followReachabilityInfo(reachabilityInfo, this)(FromExports) + // Reach exported members + if (!isJSClass) { + for (reachabilityInfo <- data.jsMethodProps) + followReachabilityInfo(reachabilityInfo, this)(FromExports) + } } } } diff --git a/linker/shared/src/test/scala/org/scalajs/linker/IRCheckerTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/IRCheckerTest.scala index f53e421b43..d9a2760c69 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/IRCheckerTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/IRCheckerTest.scala @@ -90,6 +90,78 @@ class IRCheckerTest { testLinkNoIRError(classDefs, mainModuleInitializers("Test")) } + @Test + def argumentTypeMismatch(): AsyncResult = await { + val A = ClassName("A") + val B = ClassName("B") + val C = ClassName("C") + val D = ClassName("D") + + val fooMethodName = m("foo", List(ClassRef(B)), V) + + val results = for (receiverClassName <- List(A, B, C, D)) yield { + val receiverClassRef = ClassRef(receiverClassName) + val receiverType = ClassType(receiverClassName) + + val testMethodName = m("test", List(receiverClassRef, ClassRef(C), ClassRef(D)), V) + + val newD = New(D, NoArgConstructorName, Nil) + + val classDefs = Seq( + classDef( + "A", + kind = ClassKind.Interface, + interfaces = Nil, + methods = List( + MethodDef(EMF, fooMethodName, NON, + List(paramDef("x", ClassType(B))), NoType, Some(Skip()))(EOH, UNV) + ) + ), + classDef("B", kind = ClassKind.Interface, interfaces = List("A")), + classDef( + "C", + kind = ClassKind.Class, + superClass = Some(ObjectClass), + interfaces = List("A"), + methods = List(trivialCtor("C")) + ), + + classDef( + "D", + kind = ClassKind.Class, + superClass = Some("C"), + interfaces = List("B"), + methods = List( + trivialCtor("D"), + MethodDef( + EMF.withNamespace(MemberNamespace.PublicStatic), + testMethodName, + NON, + List(paramDef("x", receiverType), paramDef("c", ClassType(C)), paramDef("d", ClassType(D))), + NoType, + Some(Block( + Apply(EAF, VarRef("x")(receiverType), fooMethodName, List(VarRef("c")(ClassType(C))))(NoType), + Apply(EAF, VarRef("x")(receiverType), fooMethodName, List(VarRef("d")(ClassType(D))))(NoType) + )) + )(EOH, UNV) + ) + ), + + mainTestClassDef( + ApplyStatic(EAF, D, testMethodName, List(newD, newD, newD))(NoType) + ) + ) + + for (log <- testLinkIRErrors(classDefs, MainTestModuleInitializers)) yield { + log.assertContainsError( + "B expected but C found for tree of type org.scalajs.ir.Trees$VarRef") + log.assertNotContains("B expected but D found") + } + } + + Future.sequence(results) + } + @Test def missingJSNativeLoadSpec(): AsyncResult = await { val classDefs = Seq( From 308236f9a04fd7cc86de101baee088c6ca47d283 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 5 Apr 2024 16:09:26 +0100 Subject: [PATCH 081/298] Implement "parallel" ConcurrentHashMap forEach methods --- .../util/concurrent/ConcurrentHashMap.scala | 18 ++++++ .../concurrent/ConcurrentHashMapTest.scala | 63 +++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/javalib/src/main/scala/java/util/concurrent/ConcurrentHashMap.scala b/javalib/src/main/scala/java/util/concurrent/ConcurrentHashMap.scala index 29847a2acb..49c5ac683e 100644 --- a/javalib/src/main/scala/java/util/concurrent/ConcurrentHashMap.scala +++ b/javalib/src/main/scala/java/util/concurrent/ConcurrentHashMap.scala @@ -12,6 +12,8 @@ package java.util.concurrent +import java.util.function.{BiConsumer, Consumer} + import java.io.Serializable import java.util._ @@ -72,6 +74,22 @@ class ConcurrentHashMap[K, V] private (initialCapacity: Int, loadFactor: Float) new ConcurrentHashMap.KeySetView[K, V](this.inner, mappedValue) } + def forEach(parallelismThreshold: Long, action: BiConsumer[_ >: K, _ >: V]): Unit = { + // Note: It is tempting to simply call inner.forEach here: + // However, this will not have the correct snapshotting behavior. + val i = inner.nodeIterator() + while (i.hasNext()) { + val n = i.next() + action.accept(n.key, n.value) + } + } + + def forEachKey(parallelismThreshold: Long, action: Consumer[_ >: K]): Unit = + inner.keyIterator().forEachRemaining(action) + + def forEachValue(parallelismThreshold: Long, action: Consumer[_ >: V]): Unit = + inner.valueIterator().forEachRemaining(action) + override def values(): Collection[V] = inner.values() diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/concurrent/ConcurrentHashMapTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/concurrent/ConcurrentHashMapTest.scala index 7a98007158..459b7b9926 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/concurrent/ConcurrentHashMapTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/javalib/util/concurrent/ConcurrentHashMapTest.scala @@ -227,6 +227,69 @@ class ConcurrentHashMapTest extends MapTest { val str = keySet.toString assertTrue(s"toString should print keys, but actual: $str", str == "[a, b]" || str == "[b, a]") } + + @Test def forEachPar(): Unit = { + val pairs = List("ONE" -> 1, "TWO" -> 2, "THREE" -> 3) + val map = factory.empty[String, Int] + pairs.foreach(x => map.put(x._1, x._2)) + + val seen = mutable.Set.empty[(String, Int)] + + map.forEach(1L, { (k, v) => + if (k == "TWO") + map.remove("TWO") // check snapshotting behavior. + + seen.synchronized { + seen += k -> v + } + }) + + assertEquals(2, map.size()) + assertFalse(map.containsKey("TWO")) + assertEquals(pairs.toSet, seen) + } + + @Test def forEachKeyPar(): Unit = { + val pairs = List("ONE" -> 1, "TWO" -> 2, "THREE" -> 3) + val map = factory.empty[String, Int] + pairs.foreach(x => map.put(x._1, x._2)) + + val seen = mutable.Set.empty[String] + + map.forEachKey(1L, { k => + if (k == "TWO") + map.remove("TWO") // check snapshotting behavior. + + seen.synchronized { + seen += k + } + }) + + assertEquals(2, map.size()) + assertFalse(map.containsKey("TWO")) + assertEquals(Set("ONE", "TWO", "THREE"), seen) + } + + @Test def forEachValuePar(): Unit = { + val pairs = List("ONE" -> 1, "TWO" -> 2, "THREE" -> 3) + val map = factory.empty[String, Int] + pairs.foreach(x => map.put(x._1, x._2)) + + val seen = mutable.Set.empty[Int] + + map.forEachValue(1L, { v => + if (v == 2) + map.remove("TWO") // check snapshotting behavior. + + seen.synchronized { + seen += v + } + }) + + assertEquals(2, map.size()) + assertFalse(map.containsKey("TWO")) + assertEquals(seen, Set(1, 2, 3)) + } } class ConcurrentHashMapFactory extends ConcurrentMapFactory { From d3c42e1370b8dd1b043061c3df2985a375691494 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sun, 31 Mar 2024 13:25:25 +0200 Subject: [PATCH 082/298] Replace (Par)TrieMap with ConcurrentHashMap in IncOptimizer - Reduces residual retained size of the IncOptimizer on the test suite from 166MB to 144MB. - ~10% batch speedup on the optimizer. --- .../frontend/optimizer/ParCollOps.scala | 38 +--- .../frontend/optimizer/AbsCollOps.scala | 24 +-- .../frontend/optimizer/IncOptimizer.scala | 186 ++++++++++-------- .../frontend/optimizer/SeqCollOps.scala | 38 +--- 4 files changed, 115 insertions(+), 171 deletions(-) diff --git a/linker/jvm/src/main/scala/org/scalajs/linker/frontend/optimizer/ParCollOps.scala b/linker/jvm/src/main/scala/org/scalajs/linker/frontend/optimizer/ParCollOps.scala index e9c36ead8e..30b82b2283 100644 --- a/linker/jvm/src/main/scala/org/scalajs/linker/frontend/optimizer/ParCollOps.scala +++ b/linker/jvm/src/main/scala/org/scalajs/linker/frontend/optimizer/ParCollOps.scala @@ -14,52 +14,20 @@ package org.scalajs.linker.frontend.optimizer import scala.annotation.tailrec -import scala.collection.concurrent.TrieMap -import scala.collection.parallel.mutable.{ParTrieMap, ParArray} +import scala.collection.parallel.mutable.ParArray import scala.collection.parallel._ import java.util.concurrent.atomic._ private[optimizer] object ParCollOps extends AbsCollOps { - type Map[K, V] = TrieMap[K, V] - type ParMap[K, V] = ParTrieMap[K, V] - type AccMap[K, V] = TrieMap[K, Addable[V]] type ParIterable[V] = ParArray[V] type Addable[V] = AtomicReference[List[V]] - def emptyAccMap[K, V]: AccMap[K, V] = TrieMap.empty - def emptyMap[K, V]: Map[K, V] = TrieMap.empty - def emptyParMap[K, V]: ParMap[K, V] = ParTrieMap.empty + def parThreshold: Long = 1 // max parallelism + def emptyParIterable[V]: ParIterable[V] = ParArray.empty def emptyAddable[V]: Addable[V] = new AtomicReference[List[V]](Nil) - // Operations on ParMap - def isEmpty[K, V](map: ParMap[K, V]): Boolean = map.isEmpty - def forceGet[K, V](map: ParMap[K, V], k: K): V = map(k) - def get[K, V](map: ParMap[K, V], k: K): Option[V] = map.get(k) - def put[K, V](map: ParMap[K, V], k: K, v: V): Unit = map.put(k, v) - def remove[K, V](map: ParMap[K, V], k: K): Option[V] = map.remove(k) - - def retain[K, V](map: ParMap[K, V])(p: (K, V) => Boolean): Unit = { - map.foreach { case (k, v) => - if (!p(k, v)) - map.remove(k) - } - } - - def valuesForeach[K, V, U](map: ParMap[K, V])(f: V => U): Unit = - map.values.foreach(f) - - // Operations on AccMap - def acc[K, V](map: AccMap[K, V], k: K, v: V): Unit = - add(map.getOrElseUpdate(k, emptyAddable), v) - - def getAcc[K, V](map: AccMap[K, V], k: K): ParIterable[V] = - map.get(k).fold(emptyParIterable[V])(finishAdd(_)) - - def parFlatMapKeys[A, B](map: AccMap[A, _])(f: A => Option[B]): ParIterable[B] = - map.keys.flatMap(f(_)).toParArray - // Operations on ParIterable def prepAdd[V](it: ParIterable[V]): Addable[V] = new AtomicReference(it.toList) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/AbsCollOps.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/AbsCollOps.scala index 30d915b657..4da8461735 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/AbsCollOps.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/AbsCollOps.scala @@ -14,35 +14,15 @@ package org.scalajs.linker.frontend.optimizer import language.higherKinds -import scala.collection.mutable - private[optimizer] trait AbsCollOps { - type Map[K, V] <: mutable.Map[K, V] - type ParMap[K, V] <: AnyRef - type AccMap[K, V] <: AnyRef type ParIterable[V] <: AnyRef type Addable[V] <: AnyRef - def emptyAccMap[K, V]: AccMap[K, V] - def emptyMap[K, V]: Map[K, V] - def emptyParMap[K, V]: ParMap[K, V] + def parThreshold: Long + def emptyParIterable[V]: ParIterable[V] def emptyAddable[V]: Addable[V] - // Operations on ParMap - def isEmpty[K, V](map: ParMap[K, V]): Boolean - def forceGet[K, V](map: ParMap[K, V], k: K): V - def get[K, V](map: ParMap[K, V], k: K): Option[V] - def put[K, V](map: ParMap[K, V], k: K, v: V): Unit - def remove[K, V](map: ParMap[K, V], k: K): Option[V] - def retain[K, V](map: ParMap[K, V])(p: (K, V) => Boolean): Unit - def valuesForeach[K, V, U](map: ParMap[K, V])(f: V => U): Unit - - // Operations on AccMap - def acc[K, V](map: AccMap[K, V], k: K, v: V): Unit - def getAcc[K, V](map: AccMap[K, V], k: K): ParIterable[V] - def parFlatMapKeys[A, B](map: AccMap[A, _])(f: A => Option[B]): ParIterable[B] - // Operations on ParIterable def prepAdd[V](it: ParIterable[V]): Addable[V] def add[V](addable: Addable[V], v: V): Unit diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala index d092397b51..4533ca7543 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala @@ -16,6 +16,7 @@ import scala.annotation.{switch, tailrec} import scala.collection.mutable +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicBoolean import org.scalajs.ir._ @@ -44,6 +45,16 @@ import OptimizerCore.InlineableFieldBodies.FieldBody * run, based on detecting what parts of the program must be re-optimized, * and keeping optimized results from previous runs for the rest. * + * A general note about use of ConcurrentHashMap[T, Unit] as concurrent sets: + * It would seem better to use ConcurrentHashMap.newKeySet() which is + * specifically designed for this purpose. However, the views alone use up 4 MB + * of shallow size on the test suite at the time of writing. Therefore, we give + * up on the convenience API and use the underlying ConcurrentHashMap directly. + * + * A similar argument applies to usages of keySet(): It appears that the + * implementation holds on to the key set once it is created, resulting in + * unnecessary memory usage. + * * @param semantics Required Scala.js Semantics * @param esLevel ECMAScript level */ @@ -65,15 +76,22 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: private var batchMode: Boolean = false private var objectClass: Class = _ - private val classes = collOps.emptyMap[ClassName, Class] - private val interfaces = collOps.emptyParMap[ClassName, InterfaceType] + private val classes = new ConcurrentHashMap[ClassName, Class] + private val interfaces = new ConcurrentHashMap[ClassName, InterfaceType] private val topLevelExports = new JSTopLevelMethodContainer private var methodsToProcess = collOps.emptyAddable[Processable] @inline private def getInterface(className: ClassName): InterfaceType = - collOps.forceGet(interfaces, className) + interfaces.get(className) + + @inline + private def classOrElse[T >: Class](className: ClassName, default: => T): T = { + val clazz = classes.get(className) + if (clazz != null) clazz + else default + } /** Update the incremental analyzer with a new run. */ def update(unit: LinkingUnit, logger: Logger): List[(ClassDef, Version)] = { @@ -110,7 +128,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: val className = linkedClass.className val interface = getInterface(className) - val publicContainer = classes.get(className).getOrElse { + val publicContainer = classOrElse(className, { /* For interfaces, we need to look at default methods. * For other kinds of classes, the public namespace is necessarily * empty. @@ -120,7 +138,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: linkedClass.kind == ClassKind.Interface || container.methods.isEmpty, linkedClass.className -> linkedClass.kind) container - } + }) val newMethods = for (m <- linkedClass.methods) yield { val namespace = m.flags.namespace @@ -169,14 +187,14 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: } private def updateAndTagClasses(linkedClasses: List[LinkedClass]): Unit = { - val neededInterfaces = collOps.emptyParMap[ClassName, LinkedClass] - val neededClasses = collOps.emptyParMap[ClassName, LinkedClass] + val neededInterfaces = new ConcurrentHashMap[ClassName, LinkedClass] + val neededClasses = new ConcurrentHashMap[ClassName, LinkedClass] for (linkedClass <- linkedClasses) { - collOps.put(neededInterfaces, linkedClass.className, linkedClass) + neededInterfaces.put(linkedClass.className, linkedClass) if (linkedClass.hasInstances && (linkedClass.kind.isClass || linkedClass.kind == ClassKind.HijackedClass)) { - collOps.put(neededClasses, linkedClass.className, linkedClass) + neededClasses.put(linkedClass.className, linkedClass) } } @@ -187,26 +205,26 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: * * Non-batch mode only. */ - assert(!batchMode || collOps.isEmpty(interfaces)) + assert(!batchMode || interfaces.isEmpty()) if (!batchMode) { - collOps.retain(interfaces) { (className, interface) => - collOps.remove(neededInterfaces, className).fold { + interfaces.forEach(collOps.parThreshold, { (className, interface) => + val linkedClass = neededInterfaces.remove(className) + if (linkedClass == null) { interface.delete() - false - } { linkedClass => + interfaces.remove(className) + } else { interface.updateWith(linkedClass) - true } - } + }) } /* Add new interfaces. * Easy, we don't have to notify anyone. */ - collOps.valuesForeach(neededInterfaces) { linkedClass => + neededInterfaces.forEachValue(collOps.parThreshold, { linkedClass => val interface = new InterfaceType(linkedClass) - collOps.put(interfaces, interface.className, interface) - } + interfaces.put(interface.className, interface) + }) if (!batchMode) { /* Class removals: @@ -218,7 +236,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: * Non-batch mode only. */ val objectClassStillExists = - objectClass.walkClassesForDeletions(collOps.get(neededClasses, _)) + objectClass.walkClassesForDeletions(className => Option(neededClasses.get(className))) assert(objectClassStillExists, "Uh oh, java.lang.Object was deleted!") /* Class changes: @@ -228,8 +246,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: * * Non-batch mode only. */ - objectClass.walkForChanges( - collOps.remove(neededClasses, _).get, Set.empty) + objectClass.walkForChanges(neededClasses.remove(_), Set.empty) } /* Class additions: @@ -238,30 +255,36 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: */ // Group children by (immediate) parent - val newChildrenByParent = collOps.emptyAccMap[ClassName, LinkedClass] + val newChildrenByParent = new ConcurrentHashMap[ClassName, collOps.Addable[LinkedClass]] - collOps.valuesForeach(neededClasses) { linkedClass => + neededClasses.forEachValue(collOps.parThreshold, { linkedClass => linkedClass.superClass.fold[Unit] { assert(batchMode, "Trying to add java.lang.Object in incremental mode") objectClass = new Class(None, linkedClass) - classes += linkedClass.className -> objectClass + classes.put(linkedClass.className, objectClass) } { superClassName => - collOps.acc(newChildrenByParent, superClassName.name, linkedClass) + val addable = newChildrenByParent + .computeIfAbsent(superClassName.name, _ => collOps.emptyAddable) + + collOps.add(addable, linkedClass) } - } + }) - val getNewChildren = - (name: ClassName) => collOps.getAcc(newChildrenByParent, name) + val getNewChildren = { (name: ClassName) => + val acc = newChildrenByParent.get(name) + if (acc == null) collOps.emptyParIterable[LinkedClass] + else collOps.finishAdd(acc) + } // Walk the tree to add children if (batchMode) { objectClass.walkForAdditions(getNewChildren) } else { - val existingParents = - collOps.parFlatMapKeys(newChildrenByParent)(classes.get) - collOps.foreach(existingParents) { parent => - parent.walkForAdditions(getNewChildren) - } + newChildrenByParent.forEachKey(1, { parentName => + val parent = classes.get(parentName) + if (parent != null) + parent.walkForAdditions(getNewChildren) + }) } } @@ -293,7 +316,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: def isGetter(classAndMethodName: (ClassName, MethodName)): Boolean = { val (className, methodName) = classAndMethodName - classes(className).lookupMethod(methodName).exists { m => + classes.get(className).lookupMethod(methodName).exists { m => m.originalDef.body match { case Some(Select(This(), _)) => true case _ => false @@ -307,7 +330,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: * - Initialize `elidableConstructorsRemainingDependenciesCount` for `DependentOn` classes * - Initialize the stack with dependency-free classes */ - for (cls <- classes.valuesIterator) { + classes.forEachValue(Long.MaxValue, { cls => cls.elidableConstructorsInfo match { case DependentOn(deps, getterDeps) => if (!getterDeps.forall(isGetter(_))) { @@ -318,7 +341,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: toProcessStack += cls } else { cls.elidableConstructorsRemainingDependenciesCount = deps.size - deps.foreach(dep => classes(dep).elidableConstructorsDependents += cls) + deps.foreach(dep => classes.get(dep).elidableConstructorsDependents += cls) } } case AcyclicElidable => @@ -326,7 +349,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: case NotElidable => () } - } + }) /* Propagate AcyclicElidable * When a class `cls` is on the stack, it is known to be AcyclicElidable. @@ -358,9 +381,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: } // Set the final value of hasElidableConstructors - for (cls <- classes.valuesIterator) { - cls.setHasElidableConstructors() - } + classes.forEachValue(Long.MaxValue, _.setHasElidableConstructors()) } /** Optimizer part: process all methods that need reoptimizing. @@ -493,7 +514,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: /** True if *all* constructors of this class are recursively elidable. */ private var hasElidableConstructors: Boolean = elidableConstructorsInfo != ElidableConstructorsInfo.NotElidable // initial educated guess - private val hasElidableConstructorsAskers = collOps.emptyMap[Processable, Unit] + private val hasElidableConstructorsAskers = new ConcurrentHashMap[Processable, Unit] var fields: List[AnyFieldDef] = linkedClass.fields var fieldsRead: Set[FieldName] = linkedClass.fieldsRead @@ -504,7 +525,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: */ private var inlineableFieldBodies: OptimizerCore.InlineableFieldBodies = computeInlineableFieldBodies(linkedClass) - private val inlineableFieldBodiesAskers = collOps.emptyMap[Processable, Unit] + private val inlineableFieldBodiesAskers = new ConcurrentHashMap[Processable, Unit] setupAfterCreation(linkedClass) @@ -548,7 +569,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: notInstantiatedAnymore() for (method <- methods.values) method.delete() - classes -= className + classes.remove(className) /* Note: no need to tag methods that call *statically* one of the methods * of the deleted classes, since they've got to be invalidated by * themselves. @@ -633,7 +654,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: val newInlineableFieldBodies = computeInlineableFieldBodies(linkedClass) if (inlineableFieldBodies != newInlineableFieldBodies) { inlineableFieldBodies = newInlineableFieldBodies - inlineableFieldBodiesAskers.keysIterator.foreach(_.tag()) + inlineableFieldBodiesAskers.forEachKey(Long.MaxValue, _.tag()) inlineableFieldBodiesAskers.clear() } @@ -657,7 +678,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: if (hasElidableConstructors != newHasElidableConstructors) { hasElidableConstructors = newHasElidableConstructors - hasElidableConstructorsAskers.keysIterator.foreach(_.tag()) + hasElidableConstructorsAskers.forEachKey(Long.MaxValue, _.tag()) hasElidableConstructorsAskers.clear() } @@ -676,7 +697,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: collOps.foreach(getNewChildren(className)) { linkedClass => val cls = new Class(Some(this), linkedClass) collOps.add(subclassAcc, cls) - classes += linkedClass.className -> cls + classes.put(linkedClass.className, cls) cls.walkForAdditions(getNewChildren) } @@ -888,7 +909,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: // Mixin constructor -- test whether its body is entirely empty case ApplyStatically(flags, This(), className, methodName, Nil) - if !flags.isPrivate && !classes.contains(className) => + if !flags.isPrivate && !classes.containsKey(className) => // Since className is not in classes, it must be a default method call. val container = getInterface(className).staticLike(MemberNamespace.Public) @@ -1025,7 +1046,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: // Mixin constructor -- assume it is empty case ApplyStatically(flags, This(), className, methodName, Nil) - if !flags.isPrivate && !classes.contains(className) => + if !flags.isPrivate && !classes.containsKey(className) => fieldBodies // Delegation to another constructor @@ -1221,21 +1242,22 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: val className: ClassName = linkedClass.className - private type MethodCallers = collOps.Map[MethodName, collOps.Map[Processable, Unit]] + private type MethodCallers = + ConcurrentHashMap[MethodName, ConcurrentHashMap[Processable, Unit]] - private val ancestorsAskers = collOps.emptyMap[Processable, Unit] - private val dynamicCallers: MethodCallers = collOps.emptyMap + private val ancestorsAskers = new ConcurrentHashMap[Processable, Unit] + private val dynamicCallers: MethodCallers = new ConcurrentHashMap // ArrayBuffer to avoid need for ClassTag[collOps.Map[_, _]] private val staticCallers = - mutable.ArrayBuffer.fill[MethodCallers](MemberNamespace.Count)(collOps.emptyMap) + mutable.ArrayBuffer.fill[MethodCallers](MemberNamespace.Count)(new ConcurrentHashMap) - private val jsNativeImportsAskers = collOps.emptyMap[Processable, Unit] - private val fieldsReadAskers = collOps.emptyMap[Processable, Unit] + private val jsNativeImportsAskers = new ConcurrentHashMap[Processable, Unit] + private val fieldsReadAskers = new ConcurrentHashMap[Processable, Unit] private var _ancestors: List[ClassName] = linkedClass.ancestors - private val _instantiatedSubclasses = collOps.emptyMap[Class, Unit] + private val _instantiatedSubclasses = new ConcurrentHashMap[Class, Unit] private val staticLikes: Array[StaticLikeNamespace] = { Array.tabulate(MemberNamespace.Count) { ord => @@ -1285,17 +1307,20 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: def askDynamicCallTargets(methodName: MethodName, asker: Processable): List[MethodImpl] = { dynamicCallers - .getOrElseUpdate(methodName, collOps.emptyMap) + .computeIfAbsent(methodName, _ => new ConcurrentHashMap()) .put(asker, ()) asker.registerTo(this) - _instantiatedSubclasses.keys.flatMap(_.lookupMethod(methodName)).toList + + val res = mutable.Set.empty[MethodImpl] + _instantiatedSubclasses.forEachKey(Long.MaxValue, _.lookupMethod(methodName).foreach(res += _)) + res.toList } /** PROCESS PASS ONLY. */ def askStaticCallTarget(namespace: MemberNamespace, methodName: MethodName, asker: Processable): MethodImpl = { staticCallers(namespace.ordinal) - .getOrElseUpdate(methodName, collOps.emptyMap) + .computeIfAbsent(methodName, _ => new ConcurrentHashMap()) .put(asker, ()) asker.registerTo(this) @@ -1303,7 +1328,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: val container = if (namespace != MemberNamespace.Public) inStaticsLike - else classes.getOrElse(className, inStaticsLike) + else classOrElse(className, inStaticsLike) // Method must exist, otherwise it's a bug / invalid IR. container.lookupMethod(methodName).getOrElse { @@ -1317,7 +1342,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: /** UPDATE PASS ONLY. */ def removeInstantiatedSubclass(x: Class): Unit = - _instantiatedSubclasses -= x + _instantiatedSubclasses.remove(x) /** PROCESS PASS ONLY. */ def askAncestors(asker: Processable): List[ClassName] = { @@ -1368,7 +1393,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: // Update ancestors if (linkedClass.ancestors != _ancestors) { _ancestors = linkedClass.ancestors - ancestorsAskers.keysIterator.foreach(_.tag()) + ancestorsAskers.forEachKey(Long.MaxValue, _.tag()) ancestorsAskers.clear() } @@ -1376,7 +1401,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: val newJSNativeImports = computeJSNativeImports(linkedClass) if (jsNativeImports != newJSNativeImports) { jsNativeImports = newJSNativeImports - jsNativeImportsAskers.keysIterator.foreach(_.tag()) + jsNativeImportsAskers.forEachKey(Long.MaxValue, _.tag()) jsNativeImportsAskers.clear() } @@ -1385,7 +1410,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: staticFieldsRead != linkedClass.staticFieldsRead) { fieldsRead = linkedClass.fieldsRead staticFieldsRead = linkedClass.staticFieldsRead - fieldsReadAskers.keysIterator.foreach(_.tag()) + fieldsReadAskers.forEachKey(Long.MaxValue, _.tag()) fieldsReadAskers.clear() } @@ -1412,8 +1437,9 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: * UPDATE PASS ONLY. */ def tagDynamicCallersOf(methodName: MethodName): Unit = { - dynamicCallers.remove(methodName) - .foreach(_.keysIterator.foreach(_.tag())) + val callers = dynamicCallers.remove(methodName) + if (callers != null) + callers.forEachKey(Long.MaxValue, _.tag()) } /** Tag the static-callers of an instance method. @@ -1421,15 +1447,16 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: */ def tagStaticCallersOf(namespace: MemberNamespace, methodName: MethodName): Unit = { - staticCallers(namespace.ordinal).remove(methodName) - .foreach(_.keysIterator.foreach(_.tag())) + val callers = staticCallers(namespace.ordinal).remove(methodName) + if (callers != null) + callers.forEachKey(Long.MaxValue, _.tag()) } /** UPDATE PASS ONLY. */ def unregisterDependee(dependee: Processable): Unit = { ancestorsAskers.remove(dependee) - dynamicCallers.valuesIterator.foreach(_.remove(dependee)) - staticCallers.foreach(_.valuesIterator.foreach(_.remove(dependee))) + dynamicCallers.forEachValue(Long.MaxValue, _.remove(dependee)) + staticCallers.foreach(_.forEachValue(Long.MaxValue, _.remove(dependee))) jsNativeImportsAskers.remove(dependee) } @@ -1468,7 +1495,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: private abstract class Processable { type Def >: scala.Null <: VersionedMemberDef - private[this] val registeredTo = collOps.emptyMap[Unregisterable, Unit] + private[this] val registeredTo = new ConcurrentHashMap[Unregisterable, Unit] private[this] val tagged = new AtomicBoolean(false) private[this] var _deleted: Boolean = false @@ -1510,7 +1537,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: } private def unregisterFromEverywhere(): Unit = { - registeredTo.keysIterator.foreach(_.unregisterDependee(this)) + registeredTo.forEachKey(Long.MaxValue, _.unregisterDependee(this)) registeredTo.clear() } @@ -1560,7 +1587,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: type Def = MethodDef - private val bodyAskers = collOps.emptyMap[Processable, Unit] + private val bodyAskers = new ConcurrentHashMap[Processable, Unit] var attributes: OptimizerCore.MethodAttributes = _ @@ -1578,7 +1605,7 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: /** UPDATE PASS ONLY. */ def tagBodyAskers(): Unit = { - bodyAskers.keysIterator.foreach(_.tag()) + bodyAskers.forEachKey(Long.MaxValue, _.tag()) bodyAskers.clear() } @@ -1741,14 +1768,17 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: getInterface(intfName).askAncestors(asker) protected def hasElidableConstructors(className: ClassName): Boolean = - classes(className).askHasElidableConstructors(asker) + classes.get(className).askHasElidableConstructors(asker) - protected def inlineableFieldBodies(className: ClassName): OptimizerCore.InlineableFieldBodies = - classes.get(className).fold(OptimizerCore.InlineableFieldBodies.Empty)(_.askInlineableFieldBodies(asker)) + protected def inlineableFieldBodies(className: ClassName): OptimizerCore.InlineableFieldBodies = { + val clazz = classes.get(className) + if (clazz == null) OptimizerCore.InlineableFieldBodies.Empty + else clazz.askInlineableFieldBodies(asker) + } protected def tryNewInlineableClass( className: ClassName): Option[OptimizerCore.InlineableClassStructure] = { - classes(className).tryNewInlineable + classes.get(className).tryNewInlineable } protected def getJSNativeImportOf( diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/SeqCollOps.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/SeqCollOps.scala index d5874fbd49..7bdc4a0697 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/SeqCollOps.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/SeqCollOps.scala @@ -12,51 +12,17 @@ package org.scalajs.linker.frontend.optimizer -import scala.collection.{GenTraversableOnce, GenIterable} import scala.collection.mutable -import org.scalajs.ir.Names.{ClassName, MethodName} -import org.scalajs.ir.Trees.MemberNamespace - -import org.scalajs.linker.standard._ -import org.scalajs.linker.CollectionsCompat.MutableMapCompatOps - private[optimizer] object SeqCollOps extends AbsCollOps { - type Map[K, V] = mutable.Map[K, V] - type ParMap[K, V] = mutable.Map[K, V] - type AccMap[K, V] = mutable.Map[K, mutable.ListBuffer[V]] type ParIterable[V] = mutable.ListBuffer[V] type Addable[V] = mutable.ListBuffer[V] - def emptyAccMap[K, V]: AccMap[K, V] = mutable.Map.empty - def emptyMap[K, V]: Map[K, V] = mutable.Map.empty - def emptyParMap[K, V]: ParMap[K, V] = mutable.Map.empty + def parThreshold: Long = Long.MaxValue // no parallelism + def emptyParIterable[V]: ParIterable[V] = mutable.ListBuffer.empty def emptyAddable[V]: Addable[V] = mutable.ListBuffer.empty - // Operations on ParMap - def isEmpty[K, V](map: ParMap[K, V]): Boolean = map.isEmpty - def forceGet[K, V](map: ParMap[K, V], k: K): V = map(k) - def get[K, V](map: ParMap[K, V], k: K): Option[V] = map.get(k) - def put[K, V](map: ParMap[K, V], k: K, v: V): Unit = map.put(k, v) - def remove[K, V](map: ParMap[K, V], k: K): Option[V] = map.remove(k) - - def retain[K, V](map: ParMap[K, V])(p: (K, V) => Boolean): Unit = - map.filterInPlace(p) - - def valuesForeach[K, V, U](map: ParMap[K, V])(f: V => U): Unit = - map.values.foreach(f) - - // Operations on AccMap - def acc[K, V](map: AccMap[K, V], k: K, v: V): Unit = - map.getOrElseUpdate(k, mutable.ListBuffer.empty) += v - - def getAcc[K, V](map: AccMap[K, V], k: K): ParIterable[V] = - map.getOrElse(k, emptyParIterable) - - def parFlatMapKeys[A, B](map: AccMap[A, _])(f: A => Option[B]): ParIterable[B] = - emptyParIterable[B] ++= map.keys.flatMap(f(_)) - // Operations on ParIterable def prepAdd[V](it: ParIterable[V]): Addable[V] = it def add[V](addable: Addable[V], v: V): Unit = addable += v From 175ec62da2c00c540822786aa39bc817564ca791 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 12 Apr 2024 13:15:55 +0100 Subject: [PATCH 083/298] Use Array for byClass reachability info This reduces the retained size on the test suite for the infos as follows: BaseLinker: 26MB -> 24MB Refiner: 22MB -> 20MB --- .../src/main/scala/org/scalajs/linker/analyzer/Infos.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 fc57860d25..9b779b2f37 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 @@ -93,7 +93,7 @@ object Infos { ) final class ReachabilityInfo private[Infos] ( - val byClass: List[ReachabilityInfoInClass], + val byClass: Array[ReachabilityInfoInClass], val globalFlags: ReachabilityInfo.Flags ) @@ -374,7 +374,7 @@ object Infos { setFlag(ReachabilityInfo.FlagUsedExponentOperator) def result(): ReachabilityInfo = - new ReachabilityInfo(byClass.valuesIterator.map(_.result()).toList, flags) + new ReachabilityInfo(byClass.valuesIterator.map(_.result()).toArray, flags) } final class ReachabilityInfoInClassBuilder(val className: ClassName) { From 6dcd1b73dcaa74d18cb7f94eb87ffde4416095b6 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 12 Apr 2024 14:48:21 +0100 Subject: [PATCH 084/298] Move static module dependency tracking to class level info This avoids unnecessary calls to `addStaticDependency`, but more importantly, it will allow us to flatten the structure of `ReachabilityInfoInClass`. --- .../src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala | 4 ---- .../src/main/scala/org/scalajs/linker/analyzer/Infos.scala | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) 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 52a6326a01..77d0ab3880 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 @@ -1448,25 +1448,21 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, } if (!dataInClass.staticFieldsRead.isEmpty) { - moduleUnit.addStaticDependency(className) dataInClass.staticFieldsRead.foreach( clazz._staticFieldsRead.update(_, ())) } if (!dataInClass.staticFieldsWritten.isEmpty) { - moduleUnit.addStaticDependency(className) dataInClass.staticFieldsWritten.foreach( clazz._staticFieldsWritten.update(_, ())) } if (!dataInClass.methodsCalled.isEmpty) { - // Do not add to staticDependencies: We call these on the object. for (methodName <- dataInClass.methodsCalled) clazz.callMethod(methodName) } if (!dataInClass.methodsCalledStatically.isEmpty) { - moduleUnit.addStaticDependency(className) for (methodName <- dataInClass.methodsCalledStatically) clazz.callMethodStatically(methodName) } 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 9b779b2f37..6967326919 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 @@ -400,21 +400,25 @@ object Infos { def addStaticFieldRead(field: FieldName): this.type = { staticFieldsRead += field + setStaticallyReferenced() this } def addStaticFieldWritten(field: FieldName): this.type = { staticFieldsWritten += field + setStaticallyReferenced() this } def addMethodCalled(method: MethodName): this.type = { methodsCalled += method + // Do not call setStaticallyReferenced: We call these methods on the object. this } def addMethodCalledStatically(method: NamespacedMethodName): this.type = { methodsCalledStatically += method + setStaticallyReferenced() this } From a54c022343f5f50c0cffc438f30501bb1e73f033 Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 12 Apr 2024 14:53:33 +0100 Subject: [PATCH 085/298] Add a flag to class level info for dynamic import This allows us to remove a field from `ReachabilityInfoInClass` which reduces the retained size on the test suite for the infos as follows: BaseLinker: 24MB -> 23MB Refiner: 20MB -> 20MB (within rounding error) --- .../org/scalajs/linker/analyzer/Analyzer.scala | 18 +++++++----------- .../org/scalajs/linker/analyzer/Infos.scala | 8 ++++---- 2 files changed, 11 insertions(+), 15 deletions(-) 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 77d0ab3880..48d2379f4c 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 @@ -1432,6 +1432,13 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, if ((flags & ReachabilityInfoInClass.FlagStaticallyReferenced) != 0) { moduleUnit.addStaticDependency(className) } + + if ((flags & ReachabilityInfoInClass.FlagDynamicallyReferenced) != 0) { + if (isNoModule) + _errors ::= DynamicImportWithoutModuleSupport(from) + else + moduleUnit.addDynamicDependency(className) + } } /* Since many of the lists below are likely to be empty, we always @@ -1467,17 +1474,6 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, clazz.callMethodStatically(methodName) } - if (!dataInClass.methodsCalledDynamicImport.isEmpty) { - if (isNoModule) { - _errors ::= DynamicImportWithoutModuleSupport(from) - } else { - moduleUnit.addDynamicDependency(className) - // In terms of reachability, a dynamic import call is just a static call. - for (methodName <- dataInClass.methodsCalledDynamicImport) - clazz.callMethodStatically(methodName) - } - } - if (!dataInClass.jsNativeMembersUsed.isEmpty) { for (member <- dataInClass.jsNativeMembersUsed) clazz.useJSNativeMember(member) 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 6967326919..49b60b6a3c 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 @@ -115,7 +115,6 @@ object Infos { val staticFieldsWritten: List[FieldName], val methodsCalled: List[MethodName], val methodsCalledStatically: List[NamespacedMethodName], - val methodsCalledDynamicImport: List[NamespacedMethodName], val jsNativeMembersUsed: List[MethodName], val flags: ReachabilityInfoInClass.Flags ) @@ -132,6 +131,7 @@ object Infos { final val FlagInstanceTestsUsed = 1 << 2 final val FlagClassDataAccessed = 1 << 3 final val FlagStaticallyReferenced = 1 << 4 + final val FlagDynamicallyReferenced = 1 << 5 } final class ClassInfoBuilder( @@ -384,7 +384,6 @@ object Infos { private val staticFieldsWritten = mutable.Set.empty[FieldName] private val methodsCalled = mutable.Set.empty[MethodName] private val methodsCalledStatically = mutable.Set.empty[NamespacedMethodName] - private val methodsCalledDynamicImport = mutable.Set.empty[NamespacedMethodName] private val jsNativeMembersUsed = mutable.Set.empty[MethodName] private var flags: ReachabilityInfoInClass.Flags = 0 @@ -423,7 +422,9 @@ object Infos { } def addMethodCalledDynamicImport(method: NamespacedMethodName): this.type = { - methodsCalledDynamicImport += method + // In terms of reachability, a dynamic import call is just a static call. + methodsCalledStatically += method + setFlag(ReachabilityInfoInClass.FlagDynamicallyReferenced) this } @@ -461,7 +462,6 @@ object Infos { staticFieldsWritten = toLikelyEmptyList(staticFieldsWritten), methodsCalled = toLikelyEmptyList(methodsCalled), methodsCalledStatically = toLikelyEmptyList(methodsCalledStatically), - methodsCalledDynamicImport = toLikelyEmptyList(methodsCalledDynamicImport), jsNativeMembersUsed = toLikelyEmptyList(jsNativeMembersUsed), flags = flags ) From 0d81e85e50dcb31cd5ecd552facd94314529775f Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Fri, 12 Apr 2024 14:39:14 +0100 Subject: [PATCH 086/298] Store member reachability in a single field This reduces the retained size on the test suite for the infos as follows: BaseLinker: 23MB -> 20MB Refiner: 20MB -> 17MB --- .../scalajs/linker/analyzer/Analyzer.scala | 78 +++++---------- .../org/scalajs/linker/analyzer/Infos.scala | 95 +++++++++++++------ 2 files changed, 91 insertions(+), 82 deletions(-) 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 48d2379f4c..506cde51eb 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 @@ -1079,7 +1079,8 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, } else if (!_isInstantiated.getAndSet(true)) { // TODO: Why is this not in subclassInstantiated()? - referenceFieldClasses(fieldsRead ++ fieldsWritten) + fieldsRead.foreach(referenceFieldClasses(_)) + fieldsWritten.foreach(referenceFieldClasses(_)) if (isScalaClass) { accessData() @@ -1208,12 +1209,6 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, } } - def callMethodStatically(namespacedMethodName: NamespacedMethodName)( - implicit from: From): Unit = { - callMethodStatically(namespacedMethodName.namespace, - namespacedMethodName.methodName) - } - def callMethodStatically(namespace: MemberNamespace, methodName: MethodName)( implicit from: From): Unit = { @@ -1225,16 +1220,14 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, lookupMethod(methodName).reachStatic() } - def readFields(names: List[FieldName])(implicit from: From): Unit = { - names.foreach(_fieldsRead.update(_, ())) - if (isInstantiated) - referenceFieldClasses(names) - } - - def writeFields(names: List[FieldName])(implicit from: From): Unit = { - names.foreach(_fieldsWritten.update(_, ())) + def reachField(info: Infos.FieldReachable)(implicit from: From): Unit = { + val fieldName = info.fieldName + if (info.read) + _fieldsRead.update(fieldName, ()) + if (info.written) + _fieldsWritten.update(fieldName, ()) if (isInstantiated) - referenceFieldClasses(names) + referenceFieldClasses(fieldName) } def useJSNativeMember(name: MethodName)( @@ -1251,8 +1244,7 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, maybeJSNativeLoadSpec } - private def referenceFieldClasses(fieldNames: Iterable[FieldName])( - implicit from: From): Unit = { + private def referenceFieldClasses(fieldName: FieldName)(implicit from: From): Unit = { assert(isInstantiated) /* Reach referenced classes of non-static fields @@ -1261,7 +1253,6 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, * site will not reference the classes in the final JS code. */ for { - fieldName <- fieldNames className <- data.referencedFieldClasses.get(fieldName) } { lookupClass(className)(_ => ()) @@ -1441,43 +1432,26 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, } } - /* Since many of the lists below are likely to be empty, we always - * test `!list.isEmpty` before calling `foreach` or any other - * processing, avoiding closure allocations. - */ + if (dataInClass.memberInfos != null) { + dataInClass.memberInfos.foreach { + case field: Infos.FieldReachable => + clazz.reachField(field) - if (!dataInClass.fieldsRead.isEmpty) { - clazz.readFields(dataInClass.fieldsRead) - } + case Infos.StaticFieldReachable(fieldName, read, written) => + if (read) + clazz._staticFieldsRead.update(fieldName, ()) + if (written) + clazz._staticFieldsWritten.update(fieldName, ()) - if (!dataInClass.fieldsWritten.isEmpty) { - clazz.writeFields(dataInClass.fieldsWritten) - } + case Infos.MethodReachable(methodName) => + clazz.callMethod(methodName) - if (!dataInClass.staticFieldsRead.isEmpty) { - dataInClass.staticFieldsRead.foreach( - clazz._staticFieldsRead.update(_, ())) - } - - if (!dataInClass.staticFieldsWritten.isEmpty) { - dataInClass.staticFieldsWritten.foreach( - clazz._staticFieldsWritten.update(_, ())) - } + case Infos.MethodStaticallyReachable(namespace, methodName) => + clazz.callMethodStatically(namespace, methodName) - if (!dataInClass.methodsCalled.isEmpty) { - for (methodName <- dataInClass.methodsCalled) - clazz.callMethod(methodName) - } - - if (!dataInClass.methodsCalledStatically.isEmpty) { - for (methodName <- dataInClass.methodsCalledStatically) - clazz.callMethodStatically(methodName) - } - - if (!dataInClass.jsNativeMembersUsed.isEmpty) { - for (member <- dataInClass.jsNativeMembersUsed) - clazz.useJSNativeMember(member) - .foreach(addLoadSpec(moduleUnit, _)) + case Infos.JSNativeMemberReachable(methodName) => + clazz.useJSNativeMember(methodName).foreach(addLoadSpec(moduleUnit, _)) + } } } } 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 49b60b6a3c..c031ab3f4f 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 @@ -109,13 +109,12 @@ object Infos { /** Things from a given class that are reached by one method. */ final class ReachabilityInfoInClass private[Infos] ( val className: ClassName, - val fieldsRead: List[FieldName], - val fieldsWritten: List[FieldName], - val staticFieldsRead: List[FieldName], - val staticFieldsWritten: List[FieldName], - val methodsCalled: List[MethodName], - val methodsCalledStatically: List[NamespacedMethodName], - val jsNativeMembersUsed: List[MethodName], + /* We use a single field for all members to reduce memory consumption: + * Typically, there are very few members reached in a single + * ReachabilityInfoInClass, so the overhead of having a field per type + * becomes significant in terms of memory usage. + */ + val memberInfos: Array[MemberReachabilityInfo], // nullable! val flags: ReachabilityInfoInClass.Flags ) @@ -134,6 +133,38 @@ object Infos { final val FlagDynamicallyReferenced = 1 << 5 } + sealed trait MemberReachabilityInfo + + final case class FieldReachable private[Infos] ( + val fieldName: FieldName, + val read: Boolean = false, + val written: Boolean = false + ) extends MemberReachabilityInfo + + final case class StaticFieldReachable private[Infos] ( + val fieldName: FieldName, + val read: Boolean = false, + val written: Boolean = false + ) extends MemberReachabilityInfo + + final case class MethodReachable private[Infos] ( + val methodName: MethodName + ) extends MemberReachabilityInfo + + final case class MethodStaticallyReachable private[Infos] ( + val namespace: MemberNamespace, + val methodName: MethodName + ) extends MemberReachabilityInfo + + object MethodStaticallyReachable { + private[Infos] def apply(m: NamespacedMethodName): MethodStaticallyReachable = + MethodStaticallyReachable(m.namespace, m.methodName) + } + + final case class JSNativeMemberReachable private[Infos] ( + val methodName: MethodName + ) extends MemberReachabilityInfo + final class ClassInfoBuilder( private val className: ClassName, private val kind: ClassKind, @@ -378,33 +409,39 @@ object Infos { } final class ReachabilityInfoInClassBuilder(val className: ClassName) { - private val fieldsRead = mutable.Set.empty[FieldName] - private val fieldsWritten = mutable.Set.empty[FieldName] - private val staticFieldsRead = mutable.Set.empty[FieldName] - private val staticFieldsWritten = mutable.Set.empty[FieldName] + private val fieldsUsed = mutable.Map.empty[FieldName, FieldReachable] + private val staticFieldsUsed = mutable.Map.empty[FieldName, StaticFieldReachable] private val methodsCalled = mutable.Set.empty[MethodName] private val methodsCalledStatically = mutable.Set.empty[NamespacedMethodName] private val jsNativeMembersUsed = mutable.Set.empty[MethodName] private var flags: ReachabilityInfoInClass.Flags = 0 def addFieldRead(field: FieldName): this.type = { - fieldsRead += field + fieldsUsed(field) = fieldsUsed + .getOrElse(field, FieldReachable(field)) + .copy(read = true) this } def addFieldWritten(field: FieldName): this.type = { - fieldsWritten += field + fieldsUsed(field) = fieldsUsed + .getOrElse(field, FieldReachable(field)) + .copy(written = true) this } def addStaticFieldRead(field: FieldName): this.type = { - staticFieldsRead += field + staticFieldsUsed(field) = staticFieldsUsed + .getOrElse(field, StaticFieldReachable(field)) + .copy(read = true) setStaticallyReferenced() this } def addStaticFieldWritten(field: FieldName): this.type = { - staticFieldsWritten += field + staticFieldsUsed(field) = staticFieldsUsed + .getOrElse(field, StaticFieldReachable(field)) + .copy(written = true) setStaticallyReferenced() this } @@ -454,22 +491,20 @@ object Infos { setFlag(ReachabilityInfoInClass.FlagStaticallyReferenced) def result(): ReachabilityInfoInClass = { - new ReachabilityInfoInClass( - className, - fieldsRead = toLikelyEmptyList(fieldsRead), - fieldsWritten = toLikelyEmptyList(fieldsWritten), - staticFieldsRead = toLikelyEmptyList(staticFieldsRead), - staticFieldsWritten = toLikelyEmptyList(staticFieldsWritten), - methodsCalled = toLikelyEmptyList(methodsCalled), - methodsCalledStatically = toLikelyEmptyList(methodsCalledStatically), - jsNativeMembersUsed = toLikelyEmptyList(jsNativeMembersUsed), - flags = flags - ) - } + val memberInfos: Array[MemberReachabilityInfo] = ( + fieldsUsed.valuesIterator ++ + staticFieldsUsed.valuesIterator ++ + methodsCalled.iterator.map(MethodReachable(_)) ++ + methodsCalledStatically.iterator.map(MethodStaticallyReachable(_)) ++ + jsNativeMembersUsed.iterator.map(JSNativeMemberReachable(_)) + ).toArray + + val memberInfosOrNull = + if (memberInfos.isEmpty) null + else memberInfos - private def toLikelyEmptyList[A](set: mutable.Set[A]): List[A] = - if (set.isEmpty) Nil - else set.toList + new ReachabilityInfoInClass(className, memberInfosOrNull, flags) + } } /** Generates the [[MethodInfo]] of a From 2448ab0921f12a328931fff125b970fb5da0440f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 23 Apr 2024 11:25:12 +0200 Subject: [PATCH 087/298] Exclude files from Scala's `library-aux` to prepare for 2.13.14. Since Scala 2.13.14, the stdlib's source jar contains the files of their `library-aux` directory. These files are not meant to be processed by the regular compiler (only by Scaladoc). We must therefore exclude them from the compilation of the scalalib. In the process, we remove the exclusion of `/scala/util/parsing/` which has been dead code since we dropped support for Scala 2.11. --- project/Build.scala | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index 6460bd86bf..0d93e05ec7 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -1681,6 +1681,12 @@ object Build { val s = streams.value + /* Exclude files coming from Scala's `library-aux` directory, as they are not + * meant to be compiled. They are part of the source jar since Scala 2.13.14. + */ + val excludeFiles = + Set("Any.scala", "AnyRef.scala", "Nothing.scala", "Null.scala", "Singleton.scala") + for { srcDir <- sourceDirectories normSrcDir = normPath(srcDir) @@ -1688,10 +1694,10 @@ object Build { } { val normSrc = normPath(src) val path = normSrc.substring(normSrcDir.length) - val useless = + val exclude = path.contains("/scala/collection/parallel/") || - path.contains("/scala/util/parsing/") - if (!useless) { + (src.getParentFile().getName() == "scala" && excludeFiles.contains(src.getName())) + if (!exclude) { if (paths.add(path)) sources += src else From caa3e47c1893ce1536ce50a949cbeb32a829410d Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Wed, 1 May 2024 17:12:23 +0200 Subject: [PATCH 088/298] Remove Infos.ClassInfoBuilder It was a useful abstraction when we were using Traversers to build entire class infos. However, at this point it's mostly boilerplate. Changeing this, will help simplify upcoming changes. --- .../scalajs/linker/analyzer/Analyzer.scala | 11 +-- .../scalajs/linker/analyzer/InfoLoader.scala | 39 ++++------- .../org/scalajs/linker/analyzer/Infos.scala | 68 +++++-------------- 3 files changed, 38 insertions(+), 80 deletions(-) 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 506cde51eb..1db858f237 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 @@ -1507,10 +1507,13 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, if (className == ObjectClass) None else Some(ObjectClass) - new Infos.ClassInfoBuilder(className, ClassKind.Class, - superClass = superClass, interfaces = Nil, jsNativeLoadSpec = None) - .addMethod(makeSyntheticMethodInfo(NoArgConstructorName, MemberNamespace.Constructor)) - .result() + val methods = + List(makeSyntheticMethodInfo(NoArgConstructorName, MemberNamespace.Constructor)) + + new Infos.ClassInfo(className, ClassKind.Class, + superClass = superClass, interfaces = Nil, jsNativeLoadSpec = None, + referencedFieldClasses = Map.empty, methods = methods, + jsNativeMembers = Map.empty, jsMethodProps = Nil, topLevelExports = Nil) } private def makeSyntheticMethodInfo( diff --git a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/InfoLoader.scala b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/InfoLoader.scala index c30a6c9a82..fd8620284c 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/InfoLoader.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/InfoLoader.scala @@ -115,40 +115,27 @@ private[analyzer] object InfoLoader { } private def generateInfos(classDef: ClassDef): Infos.ClassInfo = { - val builder = new Infos.ClassInfoBuilder(classDef.className, - classDef.kind, classDef.superClass.map(_.name), - classDef.interfaces.map(_.name), classDef.jsNativeLoadSpec) + val referencedFieldClasses = Infos.genReferencedFieldClasses(classDef.fields) + val methods = classDef.methods.map(methodsInfoCaches.getInfo(_)) - classDef.fields.foreach { - case FieldDef(flags, FieldIdent(name), _, ftpe) => - if (!flags.namespace.isStatic) - builder.maybeAddReferencedFieldClass(name, ftpe) - - case _: JSFieldDef => - // Nothing to do. - } - - classDef.methods.foreach { method => - builder.addMethod(methodsInfoCaches.getInfo(method)) + val jsMethodProps = { + classDef.jsConstructor.map(jsConstructorInfoCache.getInfo(_)).toList ::: + exportedMembersInfoCaches.getInfos(classDef.jsMethodProps) } - classDef.jsConstructor.foreach { jsConstructor => - builder.addExportedMember(jsConstructorInfoCache.getInfo(jsConstructor)) - } - - for (info <- exportedMembersInfoCaches.getInfos(classDef.jsMethodProps)) - builder.addExportedMember(info) - /* We do not cache top-level exports, because they're quite rare, * and usually quite small when they exist. */ - classDef.topLevelExportDefs.foreach { topLevelExportDef => - builder.addTopLevelExport(Infos.generateTopLevelExportInfo(classDef.name.name, topLevelExportDef)) - } + val topLevelExports = classDef.topLevelExportDefs + .map(Infos.generateTopLevelExportInfo(classDef.name.name, _)) - classDef.jsNativeMembers.foreach(builder.addJSNativeMember(_)) + val jsNativeMembers = classDef.jsNativeMembers + .map(m => m.name.name -> m.jsNativeLoadSpec).toMap - builder.result() + new Infos.ClassInfo(classDef.className, classDef.kind, + classDef.superClass.map(_.name), classDef.interfaces.map(_.name), + classDef.jsNativeLoadSpec, referencedFieldClasses, methods, jsNativeMembers, + jsMethodProps, topLevelExports) } /** Returns true if the cache has been used and should be kept. */ 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 c031ab3f4f..da79bcf17d 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 @@ -43,7 +43,7 @@ object Infos { final case class NamespacedMethodName( namespace: MemberNamespace, methodName: MethodName) - final class ClassInfo private[Infos] ( + final class ClassInfo( val className: ClassName, val kind: ClassKind, val superClass: Option[ClassName], // always None for interfaces @@ -165,57 +165,25 @@ object Infos { val methodName: MethodName ) extends MemberReachabilityInfo - final class ClassInfoBuilder( - private val className: ClassName, - private val kind: ClassKind, - private val superClass: Option[ClassName], - private val interfaces: List[ClassName], - private val jsNativeLoadSpec: Option[JSNativeLoadSpec] - ) { - private val referencedFieldClasses = mutable.Map.empty[FieldName, ClassName] - private val methods = mutable.ListBuffer.empty[MethodInfo] - private val jsNativeMembers = mutable.Map.empty[MethodName, JSNativeLoadSpec] - private val jsMethodProps = mutable.ListBuffer.empty[ReachabilityInfo] - private val topLevelExports = mutable.ListBuffer.empty[TopLevelExportInfo] - - def maybeAddReferencedFieldClass(name: FieldName, tpe: Type): this.type = { - tpe match { - case ClassType(cls) => - referencedFieldClasses.put(name, cls) - case ArrayType(ArrayTypeRef(ClassRef(cls), _)) => - referencedFieldClasses.put(name, cls) - case _ => - } - - this - } - - def addMethod(methodInfo: MethodInfo): this.type = { - methods += methodInfo - this - } - - def addJSNativeMember(member: JSNativeMemberDef): this.type = { - jsNativeMembers.put(member.name.name, member.jsNativeLoadSpec) - this - } - - def addExportedMember(reachabilityInfo: ReachabilityInfo): this.type = { - jsMethodProps += reachabilityInfo - this - } - - def addTopLevelExport(topLevelExportInfo: TopLevelExportInfo): this.type = { - topLevelExports += topLevelExportInfo - this + def genReferencedFieldClasses(fields: List[AnyFieldDef]): Map[FieldName, ClassName] = { + val builder = Map.newBuilder[FieldName, ClassName] + + fields.foreach { + case FieldDef(flags, FieldIdent(name), _, ftpe) => + if (!flags.namespace.isStatic) { + ftpe match { + case ClassType(cls) => + builder += name -> cls + case ArrayType(ArrayTypeRef(ClassRef(cls), _)) => + builder += name -> cls + case _ => + } + } + case _: JSFieldDef => + // Nothing to do. } - def result(): ClassInfo = { - new ClassInfo(className, kind, superClass, - interfaces, jsNativeLoadSpec, referencedFieldClasses.toMap, - methods.toList, jsNativeMembers.toMap, jsMethodProps.toList, - topLevelExports.toList) - } + builder.result() } final class ReachabilityInfoBuilder { From b2cd38f7ff409f239b324f1a9c54eccc6ee550ec Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Wed, 8 May 2024 22:20:16 +0200 Subject: [PATCH 089/298] Remove unused method parameter `instantiatedClasses` --- .../src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 1db858f237..4482ff2f4a 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 @@ -1520,8 +1520,7 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, methodName: MethodName, namespace: MemberNamespace, methodsCalled: List[(ClassName, MethodName)] = Nil, - methodsCalledStatically: List[(ClassName, NamespacedMethodName)] = Nil, - instantiatedClasses: List[ClassName] = Nil + methodsCalledStatically: List[(ClassName, NamespacedMethodName)] = Nil ): Infos.MethodInfo = { val reachabilityInfoBuilder = new Infos.ReachabilityInfoBuilder() for ((className, methodName) <- methodsCalled) From 9264732fb49f224fdfa92df01ed7fc52f34d295f Mon Sep 17 00:00:00 2001 From: Tobias Schlatter Date: Sat, 13 Apr 2024 17:33:54 +0100 Subject: [PATCH 090/298] Optimize Infos for caching in InfoLoader This allows us to use the previously calculated infos themselves as the cached values, reducing memory consumption. Further, we reduce memory consumption by using immutable maps: Because a lot of the maps are empty, we do not spend unecessary memory on empty mutable maps. This reduces the retained size on the test suite for the infos as follows: | Component | Before [MB] | After [MB] | |------------|------------:|-----------:| | BaseLinker | 20 | 15 | | Refiner | 17 | 13 | As a nice side-effect, this allows us to simplify the InfoLoader. The simplification mainly stems from the insight, that we do not need active cache used tracking; control flow is sufficient. Execution times are unaffected, in fact, the incremental case might even be a bit faster (non-significant though). --- .../scalajs/linker/analyzer/Analyzer.scala | 57 +++--- .../scalajs/linker/analyzer/InfoLoader.scala | 172 ++++++------------ .../org/scalajs/linker/analyzer/Infos.scala | 74 ++++---- 3 files changed, 119 insertions(+), 184 deletions(-) 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 4482ff2f4a..3931d2ab58 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 @@ -677,16 +677,15 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, */ private val _instantiatedSubclasses = new GrowingList[ClassInfo] - private val nsMethodInfos = { - val nsMethodInfos = Array.fill(MemberNamespace.Count) { - emptyThreadSafeMap[MethodName, MethodInfo] - } - for (methodData <- data.methods) { - // TODO It would be good to report duplicates as errors at this point - val relevantMap = nsMethodInfos(methodData.namespace.ordinal) - relevantMap(methodData.methodName) = new MethodInfo(this, methodData) - } - nsMethodInfos + private val nsMethodInfos = Array.tabulate(MemberNamespace.Count) { nsOrdinal => + val namespace = MemberNamespace.fromOrdinal(nsOrdinal) + + val m = emptyThreadSafeMap[MethodName, MethodInfo] + + for ((name, data) <- data.methods(nsOrdinal)) + m.put(name, new MethodInfo(this, namespace, name, data)) + + m } def methodInfos( @@ -724,8 +723,8 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, * method exists. */ publicMethodInfos.getOrElseUpdate(methodName, { - val syntheticData = makeSyntheticMethodInfo(methodName, MemberNamespace.Public) - new MethodInfo(this, syntheticData, nonExistent = true) + val syntheticData = makeSyntheticMethodInfo() + new MethodInfo(this, MemberNamespace.Public, methodName, syntheticData, nonExistent = true) }) } @@ -812,11 +811,9 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, val targetOwner = target.owner val syntheticInfo = makeSyntheticMethodInfo( - methodName = methodName, - namespace = MemberNamespace.Public, methodsCalledStatically = List( targetOwner.className -> NamespacedMethodName(MemberNamespace.Public, methodName))) - new MethodInfo(this, syntheticInfo, + new MethodInfo(this, MemberNamespace.Public, methodName, syntheticInfo, syntheticKind = MethodSyntheticKind.DefaultBridge(targetOwner.className)) }) } @@ -1011,10 +1008,8 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, publicMethodInfos.getOrElseUpdate(proxyName, { val syntheticInfo = makeSyntheticMethodInfo( - methodName = proxyName, - namespace = MemberNamespace.Public, methodsCalled = List(this.className -> targetName)) - new MethodInfo(this, syntheticInfo, + new MethodInfo(this, MemberNamespace.Public, proxyName, syntheticInfo, syntheticKind = MethodSyntheticKind.ReflectiveProxy(targetName)) }) } @@ -1024,8 +1019,8 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, assert(namespace != MemberNamespace.Public) methodInfos(namespace).getOrElseUpdate(methodName, { - val syntheticData = makeSyntheticMethodInfo(methodName, namespace) - new MethodInfo(this, syntheticData, nonExistent = true) + val syntheticData = makeSyntheticMethodInfo() + new MethodInfo(this, namespace, methodName, syntheticData, nonExistent = true) }) } @@ -1273,13 +1268,13 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, private class MethodInfo( val owner: ClassInfo, + val namespace: MemberNamespace, + val methodName: MethodName, data: Infos.MethodInfo, val nonExistent: Boolean = false, val syntheticKind: MethodSyntheticKind = MethodSyntheticKind.None ) extends Analysis.MethodInfo { - val methodName = data.methodName - val namespace = data.namespace val isAbstract = data.isAbstract private[this] val _isAbstractReachable = new AtomicBoolean(false) @@ -1357,7 +1352,7 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, } private[this] def doReach(): Unit = - followReachabilityInfo(data.reachabilityInfo, owner)(FromMethod(this)) + followReachabilityInfo(data, owner)(FromMethod(this)) } private class TopLevelExportInfo(val owningClass: ClassName, data: Infos.TopLevelExportInfo) @@ -1507,8 +1502,12 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, if (className == ObjectClass) None else Some(ObjectClass) - val methods = - List(makeSyntheticMethodInfo(NoArgConstructorName, MemberNamespace.Constructor)) + val methods = Array.tabulate[Map[MethodName, Infos.MethodInfo]](MemberNamespace.Count) { nsOrdinal => + if (nsOrdinal == MemberNamespace.Constructor.ordinal) + Map(NoArgConstructorName -> makeSyntheticMethodInfo()) + else + Map.empty + } new Infos.ClassInfo(className, ClassKind.Class, superClass = superClass, interfaces = Nil, jsNativeLoadSpec = None, @@ -1517,18 +1516,16 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean, } private def makeSyntheticMethodInfo( - methodName: MethodName, - namespace: MemberNamespace, methodsCalled: List[(ClassName, MethodName)] = Nil, methodsCalledStatically: List[(ClassName, NamespacedMethodName)] = Nil ): Infos.MethodInfo = { - val reachabilityInfoBuilder = new Infos.ReachabilityInfoBuilder() + val reachabilityInfoBuilder = new Infos.ReachabilityInfoBuilder(ir.Version.Unversioned) + for ((className, methodName) <- methodsCalled) reachabilityInfoBuilder.addMethodCalled(className, methodName) for ((className, methodName) <- methodsCalledStatically) reachabilityInfoBuilder.addMethodCalledStatically(className, methodName) - Infos.MethodInfo(methodName, namespace, isAbstract = false, - reachabilityInfoBuilder.result()) + Infos.MethodInfo(isAbstract = false, reachabilityInfoBuilder.result()) } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/InfoLoader.scala b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/InfoLoader.scala index fd8620284c..7eeb5d197b 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/InfoLoader.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/InfoLoader.scala @@ -64,14 +64,16 @@ private[analyzer] object InfoLoader { case object InitialIRCheck extends IRCheckMode case object InternalIRCheck extends IRCheckMode + private type MethodInfos = Array[Map[MethodName, Infos.MethodInfo]] + private class ClassInfoCache(className: ClassName, irLoader: IRLoader, irCheckMode: InfoLoader.IRCheckMode) { private var cacheUsed: Boolean = false private var version: Version = Version.Unversioned private var info: Future[Infos.ClassInfo] = _ - private val methodsInfoCaches = MethodDefsInfosCache() - private val jsConstructorInfoCache = new JSConstructorDefInfoCache() - private val exportedMembersInfoCaches = JSMethodPropDefsInfosCache() + private var prevMethodInfos: MethodInfos = Array.fill(MemberNamespace.Count)(Map.empty) + private var prevJSCtorInfo: Option[Infos.ReachabilityInfo] = None + private var prevJSMethodPropDefInfos: List[Infos.ReachabilityInfo] = Nil def loadInfo(logger: Logger)(implicit ec: ExecutionContext): Future[Infos.ClassInfo] = synchronized { /* If the cache was already used in this run, the classDef and info are @@ -116,12 +118,13 @@ private[analyzer] object InfoLoader { private def generateInfos(classDef: ClassDef): Infos.ClassInfo = { val referencedFieldClasses = Infos.genReferencedFieldClasses(classDef.fields) - val methods = classDef.methods.map(methodsInfoCaches.getInfo(_)) - val jsMethodProps = { - classDef.jsConstructor.map(jsConstructorInfoCache.getInfo(_)).toList ::: - exportedMembersInfoCaches.getInfos(classDef.jsMethodProps) - } + prevMethodInfos = genMethodInfos(classDef.methods, prevMethodInfos) + prevJSCtorInfo = genJSCtorInfo(classDef.jsConstructor, prevJSCtorInfo) + prevJSMethodPropDefInfos = + genJSMethodPropDefInfos(classDef.jsMethodProps, prevJSMethodPropDefInfos) + + val exportedMembers = prevJSCtorInfo.toList ::: prevJSMethodPropDefInfos /* We do not cache top-level exports, because they're quite rare, * and usually quite small when they exist. @@ -134,139 +137,66 @@ private[analyzer] object InfoLoader { new Infos.ClassInfo(classDef.className, classDef.kind, classDef.superClass.map(_.name), classDef.interfaces.map(_.name), - classDef.jsNativeLoadSpec, referencedFieldClasses, methods, jsNativeMembers, - jsMethodProps, topLevelExports) + classDef.jsNativeLoadSpec, referencedFieldClasses, prevMethodInfos, + jsNativeMembers, exportedMembers, topLevelExports) } /** Returns true if the cache has been used and should be kept. */ def cleanAfterRun(): Boolean = synchronized { val result = cacheUsed cacheUsed = false - if (result) { - // No point in cleaning the inner caches if the whole class disappears - methodsInfoCaches.cleanAfterRun() - jsConstructorInfoCache.cleanAfterRun() - exportedMembersInfoCaches.cleanAfterRun() - } result } } - private final class MethodDefsInfosCache private ( - val caches: Array[mutable.Map[MethodName, MethodDefInfoCache]]) - extends AnyVal { - - def getInfo(methodDef: MethodDef): Infos.MethodInfo = { - val cache = caches(methodDef.flags.namespace.ordinal) - .getOrElseUpdate(methodDef.methodName, new MethodDefInfoCache) - cache.getInfo(methodDef) - } + private def genMethodInfos(methods: List[MethodDef], + prevMethodInfos: MethodInfos): MethodInfos = { - def cleanAfterRun(): Unit = { - caches.foreach(_.filterInPlace((_, cache) => cache.cleanAfterRun())) - } - } - - private object MethodDefsInfosCache { - def apply(): MethodDefsInfosCache = { - new MethodDefsInfosCache( - Array.fill(MemberNamespace.Count)(mutable.Map.empty)) - } - } + val builders = Array.fill(MemberNamespace.Count)(Map.newBuilder[MethodName, Infos.MethodInfo]) - /* For JS method and property definitions, we use their index in the list of - * `linkedClass.exportedMembers` as their identity. We cannot use their name - * because the name itself is a `Tree`. - * - * If there is a different number of exported members than in a previous run, - * we always recompute everything. This is fine because, for any given class, - * either all JS methods and properties are reachable, or none are. So we're - * only missing opportunities for incrementality in the case where JS members - * are added or removed in the original .sjsir, which is not a big deal. - */ - private final class JSMethodPropDefsInfosCache private ( - private var caches: Array[JSMethodPropDefInfoCache]) { - - def getInfos(members: List[JSMethodPropDef]): List[Infos.ReachabilityInfo] = { - if (members.isEmpty) { - caches = null - Nil - } else { - val membersSize = members.size - if (caches == null || membersSize != caches.size) - caches = Array.fill(membersSize)(new JSMethodPropDefInfoCache) - - for ((member, i) <- members.zipWithIndex) yield { - caches(i).getInfo(member) - } - } - } + methods.foreach { method => + val info = prevMethodInfos(method.flags.namespace.ordinal) + .get(method.methodName) + .filter(_.version.sameVersion(method.version)) + .getOrElse(Infos.generateMethodInfo(method)) - def cleanAfterRun(): Unit = { - if (caches != null) - caches.foreach(_.cleanAfterRun()) + builders(method.flags.namespace.ordinal) += method.methodName -> info } - } - private object JSMethodPropDefsInfosCache { - def apply(): JSMethodPropDefsInfosCache = - new JSMethodPropDefsInfosCache(null) + builders.map(_.result()) } - private abstract class AbstractMemberInfoCache[Def <: VersionedMemberDef, Info] { - private var cacheUsed: Boolean = false - private var lastVersion: Version = Version.Unversioned - private var info: Info = _ - - final def getInfo(member: Def): Info = { - update(member) - info - } - - private final def update(member: Def): Unit = { - if (!cacheUsed) { - cacheUsed = true - val newVersion = member.version - if (!lastVersion.sameVersion(newVersion)) { - info = computeInfo(member) - lastVersion = newVersion - } - } - } - - protected def computeInfo(member: Def): Info - - /** Returns true if the cache has been used and should be kept. */ - final def cleanAfterRun(): Boolean = { - val result = cacheUsed - cacheUsed = false - result + private def genJSCtorInfo(jsCtor: Option[JSConstructorDef], + prevJSCtorInfo: Option[Infos.ReachabilityInfo]): Option[Infos.ReachabilityInfo] = { + jsCtor.map { ctor => + prevJSCtorInfo + .filter(_.version.sameVersion(ctor.version)) + .getOrElse(Infos.generateJSConstructorInfo(ctor)) } } - private final class MethodDefInfoCache - extends AbstractMemberInfoCache[MethodDef, Infos.MethodInfo] { - - protected def computeInfo(member: MethodDef): Infos.MethodInfo = - Infos.generateMethodInfo(member) - } - - private final class JSConstructorDefInfoCache - extends AbstractMemberInfoCache[JSConstructorDef, Infos.ReachabilityInfo] { - - protected def computeInfo(member: JSConstructorDef): Infos.ReachabilityInfo = - Infos.generateJSConstructorInfo(member) - } - - private final class JSMethodPropDefInfoCache - extends AbstractMemberInfoCache[JSMethodPropDef, Infos.ReachabilityInfo] { - - protected def computeInfo(member: JSMethodPropDef): Infos.ReachabilityInfo = { - member match { - case methodDef: JSMethodDef => - Infos.generateJSMethodInfo(methodDef) - case propertyDef: JSPropertyDef => - Infos.generateJSPropertyInfo(propertyDef) + private def genJSMethodPropDefInfos(jsMethodProps: List[JSMethodPropDef], + prevJSMethodPropDefInfos: List[Infos.ReachabilityInfo]): List[Infos.ReachabilityInfo] = { + /* For JS method and property definitions, we use their index in the list of + * `linkedClass.exportedMembers` as their identity. We cannot use their name + * because the name itself is a `Tree`. + * + * If there is a different number of exported members than in a previous run, + * we always recompute everything. This is fine because, for any given class, + * either all JS methods and properties are reachable, or none are. So we're + * only missing opportunities for incrementality in the case where JS members + * are added or removed in the original .sjsir, which is not a big deal. + */ + + if (prevJSMethodPropDefInfos.size != jsMethodProps.size) { + // Regenerate everything. + jsMethodProps.map(Infos.generateJSMethodPropDefInfo(_)) + } else { + for { + (prevInfo, member) <- prevJSMethodPropDefInfos.zip(jsMethodProps) + } yield { + if (prevInfo.version.sameVersion(member.version)) prevInfo + else Infos.generateJSMethodPropDefInfo(member) } } } 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 da79bcf17d..956dcb0c15 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 @@ -19,6 +19,7 @@ import org.scalajs.ir.Names._ import org.scalajs.ir.Traversers._ import org.scalajs.ir.Trees._ import org.scalajs.ir.Types._ +import org.scalajs.ir.Version import org.scalajs.linker.backend.emitter.Transients._ import org.scalajs.linker.standard.LinkedTopLevelExport @@ -59,7 +60,7 @@ object Infos { * This happens when they have non-class type (e.g. Int) */ val referencedFieldClasses: Map[FieldName, ClassName], - val methods: List[MethodInfo], + val methods: Array[Map[MethodName, MethodInfo]], val jsNativeMembers: Map[MethodName, JSNativeLoadSpec], val jsMethodProps: List[ReachabilityInfo], val topLevelExports: List[TopLevelExportInfo] @@ -67,22 +68,23 @@ object Infos { override def toString(): String = className.nameString } + /* MethodInfo should contain, not be a ReachbilityInfo + * + * However, since this class is retained over multiple linker runs in the + * cache, the shallow size of the object shows up in memory performance + * profiles. Therefore, we (ab)use inheritance to lower the memory overhead. + */ final class MethodInfo private ( - val methodName: MethodName, - val namespace: MemberNamespace, - val isAbstract: Boolean, - val reachabilityInfo: ReachabilityInfo - ) { - override def toString(): String = methodName.nameString - } + val isAbstract: Boolean, + version: Version, + byClass: Array[ReachabilityInfoInClass], + globalFlags: ReachabilityInfo.Flags + ) extends ReachabilityInfo(version, byClass, globalFlags) object MethodInfo { - def apply( - methodName: MethodName, - namespace: MemberNamespace, - isAbstract: Boolean, - reachabilityInfo: ReachabilityInfo): MethodInfo = { - new MethodInfo(methodName, namespace, isAbstract, reachabilityInfo) + def apply(isAbstract: Boolean, reachabilityInfo: ReachabilityInfo): MethodInfo = { + import reachabilityInfo._ + new MethodInfo(isAbstract, version, byClass, globalFlags) } } @@ -92,9 +94,15 @@ object Infos { val exportName: String ) - final class ReachabilityInfo private[Infos] ( - val byClass: Array[ReachabilityInfoInClass], - val globalFlags: ReachabilityInfo.Flags + sealed class ReachabilityInfo private[Infos] ( + /* The version field does not belong here conceptually. + * However, it helps the InfoLoader re-use previous infos without + * additional data held in memory. + * This reduces the memory we need to cache infos between incremental runs. + */ + val version: Version, + val byClass: Array[ReachabilityInfoInClass], + val globalFlags: ReachabilityInfo.Flags ) object ReachabilityInfo { @@ -186,7 +194,7 @@ object Infos { builder.result() } - final class ReachabilityInfoBuilder { + final class ReachabilityInfoBuilder(version: Version) { private val byClass = mutable.Map.empty[ClassName, ReachabilityInfoInClassBuilder] private var flags: ReachabilityInfo.Flags = 0 @@ -373,7 +381,7 @@ object Infos { setFlag(ReachabilityInfo.FlagUsedExponentOperator) def result(): ReachabilityInfo = - new ReachabilityInfo(byClass.valuesIterator.map(_.result()).toArray, flags) + new ReachabilityInfo(version, byClass.valuesIterator.map(_.result()).toArray, flags) } final class ReachabilityInfoInClassBuilder(val className: ClassName) { @@ -479,38 +487,43 @@ object Infos { * [[org.scalajs.ir.Trees.MethodDef Trees.MethodDef]]. */ def generateMethodInfo(methodDef: MethodDef): MethodInfo = - new GenInfoTraverser().generateMethodInfo(methodDef) + new GenInfoTraverser(methodDef.version).generateMethodInfo(methodDef) /** Generates the [[ReachabilityInfo]] of a * [[org.scalajs.ir.Trees.JSConstructorDef Trees.JSConstructorDef]]. */ def generateJSConstructorInfo(ctorDef: JSConstructorDef): ReachabilityInfo = - new GenInfoTraverser().generateJSConstructorInfo(ctorDef) + new GenInfoTraverser(ctorDef.version).generateJSConstructorInfo(ctorDef) /** Generates the [[ReachabilityInfo]] of a * [[org.scalajs.ir.Trees.JSMethodDef Trees.JSMethodDef]]. */ def generateJSMethodInfo(methodDef: JSMethodDef): ReachabilityInfo = - new GenInfoTraverser().generateJSMethodInfo(methodDef) + new GenInfoTraverser(methodDef.version).generateJSMethodInfo(methodDef) /** Generates the [[ReachabilityInfo]] of a * [[org.scalajs.ir.Trees.JSPropertyDef Trees.JSPropertyDef]]. */ def generateJSPropertyInfo(propertyDef: JSPropertyDef): ReachabilityInfo = - new GenInfoTraverser().generateJSPropertyInfo(propertyDef) + new GenInfoTraverser(propertyDef.version).generateJSPropertyInfo(propertyDef) + + def generateJSMethodPropDefInfo(member: JSMethodPropDef): ReachabilityInfo = member match { + case methodDef: JSMethodDef => generateJSMethodInfo(methodDef) + case propertyDef: JSPropertyDef => generateJSPropertyInfo(propertyDef) + } /** Generates the [[MethodInfo]] for the top-level exports. */ def generateTopLevelExportInfo(enclosingClass: ClassName, topLevelExportDef: TopLevelExportDef): TopLevelExportInfo = { - val info = new GenInfoTraverser().generateTopLevelExportInfo(enclosingClass, - topLevelExportDef) + val info = new GenInfoTraverser(Version.Unversioned) + .generateTopLevelExportInfo(enclosingClass, topLevelExportDef) new TopLevelExportInfo(info, ModuleID(topLevelExportDef.moduleID), topLevelExportDef.topLevelExportName) } - private final class GenInfoTraverser extends Traverser { - private val builder = new ReachabilityInfoBuilder + private final class GenInfoTraverser(version: Version) extends Traverser { + private val builder = new ReachabilityInfoBuilder(version) def generateMethodInfo(methodDef: MethodDef): MethodInfo = { val methodName = methodDef.methodName @@ -521,12 +534,7 @@ object Infos { val reachabilityInfo = builder.result() - MethodInfo( - methodName, - methodDef.flags.namespace, - methodDef.body.isEmpty, - reachabilityInfo - ) + MethodInfo(methodDef.body.isEmpty, reachabilityInfo) } def generateJSConstructorInfo(ctorDef: JSConstructorDef): ReachabilityInfo = { From c48ecc467f21f08f20ae380de5b679a2e32c67e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 13 May 2024 14:34:06 +0200 Subject: [PATCH 091/298] Add tests for floating point remainder operations. They do not correspond to the IEEE-754 notion of remainder. Instead, they corresponding to the common math function `fmod`, which does not have a straightforward implementation. Therefore, it is worth having dedicated tests for them, to make sure that our platforms have consistent behaviors. This is particularly important for the Wasm backend, which does not have built-in access to the semantics of `fmod`. --- .../testsuite/compiler/DoubleTest.scala | 102 +++++++++++++++++ .../testsuite/compiler/FloatTest.scala | 104 ++++++++++++++++++ 2 files changed, 206 insertions(+) diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/DoubleTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/DoubleTest.scala index 1e75f8bb8e..6f158aa23e 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/DoubleTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/DoubleTest.scala @@ -203,6 +203,108 @@ class DoubleTest { test(-3.42e-43f) } + @Test + def testRemainder(): Unit = { + /* Double `%` is atypical. It does not correspond to the IEEE-754 notion + * of remainder/modulo. Instead, it correspond to the common math function + * `fmod`. Therefore, we have dedicated tests for it, to make sure that + * our platforms agree on the semantics. They are not much, but they are + * enough to rule out the naive formula that can sometimes be found on the + * Web, namely `x - trunc(x / y) * y`. + */ + + def test(expected: Double, n: Double, d: Double): Unit = + assertExactEquals(expected, n % d) + + // If n is NaN, return NaN + test(Double.NaN, Double.NaN, Double.NaN) + test(Double.NaN, Double.NaN, Double.PositiveInfinity) + test(Double.NaN, Double.NaN, Double.NegativeInfinity) + test(Double.NaN, Double.NaN, +0.0) + test(Double.NaN, Double.NaN, -0.0) + test(Double.NaN, Double.NaN, 2.1) + test(Double.NaN, Double.NaN, 5.5) + test(Double.NaN, Double.NaN, -151.189) + + // If d is NaN, return NaN + test(Double.NaN, Double.NaN, Double.NaN) + test(Double.NaN, Double.PositiveInfinity, Double.NaN) + test(Double.NaN, Double.NegativeInfinity, Double.NaN) + test(Double.NaN, +0.0, Double.NaN) + test(Double.NaN, -0.0, Double.NaN) + test(Double.NaN, 2.1, Double.NaN) + test(Double.NaN, 5.5, Double.NaN) + test(Double.NaN, -151.189, Double.NaN) + + // If n is PositiveInfinity, return NaN + test(Double.NaN, Double.PositiveInfinity, Double.PositiveInfinity) + test(Double.NaN, Double.PositiveInfinity, Double.NegativeInfinity) + test(Double.NaN, Double.PositiveInfinity, +0.0) + test(Double.NaN, Double.PositiveInfinity, -0.0) + test(Double.NaN, Double.PositiveInfinity, 2.1) + test(Double.NaN, Double.PositiveInfinity, 5.5) + test(Double.NaN, Double.PositiveInfinity, -151.189) + + // If n is NegativeInfinity, return NaN + test(Double.NaN, Double.NegativeInfinity, Double.PositiveInfinity) + test(Double.NaN, Double.NegativeInfinity, Double.NegativeInfinity) + test(Double.NaN, Double.NegativeInfinity, +0.0) + test(Double.NaN, Double.NegativeInfinity, -0.0) + test(Double.NaN, Double.NegativeInfinity, 2.1) + test(Double.NaN, Double.NegativeInfinity, 5.5) + test(Double.NaN, Double.NegativeInfinity, -151.189) + + // If d is PositiveInfinity, return n + test(+0.0, +0.0, Double.PositiveInfinity) + test(-0.0, -0.0, Double.PositiveInfinity) + test(2.1, 2.1, Double.PositiveInfinity) + test(5.5, 5.5, Double.PositiveInfinity) + test(-151.189, -151.189, Double.PositiveInfinity) + + // If d is NegativeInfinity, return n + test(+0.0, +0.0, Double.NegativeInfinity) + test(-0.0, -0.0, Double.NegativeInfinity) + test(2.1, 2.1, Double.NegativeInfinity) + test(5.5, 5.5, Double.NegativeInfinity) + test(-151.189, -151.189, Double.NegativeInfinity) + + // If d is +0.0, return NaN + test(Double.NaN, +0.0, +0.0) + test(Double.NaN, -0.0, +0.0) + test(Double.NaN, 2.1, +0.0) + test(Double.NaN, 5.5, +0.0) + test(Double.NaN, -151.189, +0.0) + + // If d is -0.0, return NaN + test(Double.NaN, +0.0, -0.0) + test(Double.NaN, -0.0, -0.0) + test(Double.NaN, 2.1, -0.0) + test(Double.NaN, 5.5, -0.0) + test(Double.NaN, -151.189, -0.0) + + // If n is +0.0, return n + test(+0.0, +0.0, 2.1) + test(+0.0, +0.0, 5.5) + test(+0.0, +0.0, -151.189) + + // If n is -0.0, return n + test(-0.0, -0.0, 2.1) + test(-0.0, -0.0, 5.5) + test(-0.0, -0.0, -151.189) + + // Non-special values + // { val l = List(2.1, 5.5, -151.189); for (n <- l; d <- l) println(s" test(${n % d}, $n, $d)") } + test(0.0, 2.1, 2.1) + test(2.1, 2.1, 5.5) + test(2.1, 2.1, -151.189) + test(1.2999999999999998, 5.5, 2.1) + test(0.0, 5.5, 5.5) + test(5.5, 5.5, -151.189) + test(-2.0889999999999866, -151.189, 2.1) + test(-2.688999999999993, -151.189, 5.5) + test(-0.0, -151.189, -151.189) + } + @Test def noReverseComparisons_Issue3575(): Unit = { import Double.NaN diff --git a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/FloatTest.scala b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/FloatTest.scala index 755689397c..d4dbdee940 100644 --- a/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/FloatTest.scala +++ b/test-suite/shared/src/test/scala/org/scalajs/testsuite/compiler/FloatTest.scala @@ -53,6 +53,110 @@ class FloatTest { test(-65.67f, -65) } + @Test + def testRemainder(): Unit = { + /* Float `%` is atypical. It does not correspond to the IEEE-754 notion + * of remainder/modulo. Instead, it correspond to the common math function + * `fmod`. Therefore, we have dedicated tests for it, to make sure that + * our platforms agree on the semantics. They are not much, but they are + * enough to rule out the naive formula that can sometimes be found on the + * Web, namely `x - trunc(x / y) * y`. + */ + + def test(expected: Float, x: Float, y: Float): Unit = + assertExactEquals(expected, x % y) + + // If n is NaN, return NaN + test(Float.NaN, Float.NaN, Float.NaN) + test(Float.NaN, Float.NaN, Float.PositiveInfinity) + test(Float.NaN, Float.NaN, Float.NegativeInfinity) + test(Float.NaN, Float.NaN, +0.0f) + test(Float.NaN, Float.NaN, -0.0f) + test(Float.NaN, Float.NaN, 2.1f) + test(Float.NaN, Float.NaN, 5.5f) + test(Float.NaN, Float.NaN, -151.189f) + + // If d is NaN, return NaN + test(Float.NaN, Float.NaN, Float.NaN) + test(Float.NaN, Float.PositiveInfinity, Float.NaN) + test(Float.NaN, Float.NegativeInfinity, Float.NaN) + test(Float.NaN, +0.0f, Float.NaN) + test(Float.NaN, -0.0f, Float.NaN) + test(Float.NaN, 2.1f, Float.NaN) + test(Float.NaN, 5.5f, Float.NaN) + test(Float.NaN, -151.189f, Float.NaN) + + // If n is PositiveInfinity, return NaN + test(Float.NaN, Float.PositiveInfinity, Float.PositiveInfinity) + test(Float.NaN, Float.PositiveInfinity, Float.NegativeInfinity) + test(Float.NaN, Float.PositiveInfinity, +0.0f) + test(Float.NaN, Float.PositiveInfinity, -0.0f) + test(Float.NaN, Float.PositiveInfinity, 2.1f) + test(Float.NaN, Float.PositiveInfinity, 5.5f) + test(Float.NaN, Float.PositiveInfinity, -151.189f) + + // If n is NegativeInfinity, return NaN + test(Float.NaN, Float.NegativeInfinity, Float.PositiveInfinity) + test(Float.NaN, Float.NegativeInfinity, Float.NegativeInfinity) + test(Float.NaN, Float.NegativeInfinity, +0.0f) + test(Float.NaN, Float.NegativeInfinity, -0.0f) + test(Float.NaN, Float.NegativeInfinity, 2.1f) + test(Float.NaN, Float.NegativeInfinity, 5.5f) + test(Float.NaN, Float.NegativeInfinity, -151.189f) + + // If d is PositiveInfinity, return n + test(+0.0f, +0.0f, Float.PositiveInfinity) + test(-0.0f, -0.0f, Float.PositiveInfinity) + test(2.1f, 2.1f, Float.PositiveInfinity) + test(5.5f, 5.5f, Float.PositiveInfinity) + test(-151.189f, -151.189f, Float.PositiveInfinity) + + // If d is NegativeInfinity, return n + test(+0.0f, +0.0f, Float.NegativeInfinity) + test(-0.0f, -0.0f, Float.NegativeInfinity) + test(2.1f, 2.1f, Float.NegativeInfinity) + test(5.5f, 5.5f, Float.NegativeInfinity) + test(-151.189f, -151.189f, Float.NegativeInfinity) + + // If d is +0.0, return NaN + test(Float.NaN, +0.0f, +0.0f) + test(Float.NaN, -0.0f, +0.0f) + test(Float.NaN, 2.1f, +0.0f) + test(Float.NaN, 5.5f, +0.0f) + test(Float.NaN, -151.189f, +0.0f) + + // If d is -0.0, return NaN + test(Float.NaN, +0.0f, -0.0f) + test(Float.NaN, -0.0f, -0.0f) + test(Float.NaN, 2.1f, -0.0f) + test(Float.NaN, 5.5f, -0.0f) + test(Float.NaN, -151.189f, -0.0f) + + // If n is +0.0, return n + test(+0.0f, +0.0f, 2.1f) + test(+0.0f, +0.0f, 5.5f) + test(+0.0f, +0.0f, -151.189f) + + // If n is -0.0, return n + test(-0.0f, -0.0f, 2.1f) + test(-0.0f, -0.0f, 5.5f) + test(-0.0f, -0.0f, -151.189f) + + // Non-special values + // { val l = List(2.1f, 5.5f, -151.189f); for (n <- l; d <- l) println(s" test(${n % d}f, ${n}f, ${d}f)") } + if (hasStrictFloats) { + test(0.0f, 2.1f, 2.1f) + test(2.1f, 2.1f, 5.5f) + test(2.1f, 2.1f, -151.189f) + test(1.3000002f, 5.5f, 2.1f) + test(0.0f, 5.5f, 5.5f) + test(5.5f, 5.5f, -151.189f) + test(-2.0890021f, -151.189f, 2.1f) + test(-2.6889954f, -151.189f, 5.5f) + test(-0.0f, -151.189f, -151.189f) + } + } + @Test def noReverseComparisons_Issue3575(): Unit = { import Float.NaN From 1242fb9128de5f908177c25347ec421fe33f6069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 13 May 2024 14:44:44 +0200 Subject: [PATCH 092/298] Remove SystemJSTest.identityHashCodeIsStableIfObjectIsSealed. This test exploited undefined behavior itself, by trying to seal a Scala object, which is not allowed in general. It was also pointless. If any future object model implementation change would invalidate the current implementation of `systemIdentityHashCode`, we would be able to adjust the latter at the same time. When the test was originally added, back in 2015 in 941d6d3287c228dceb8b2963623ca24d8adcc6d7, `systemIdentityHashCode` was implemented in user space, and therefore had to be forward compatible. This is not the case anymore. The test actually breaks on WebAssembly, since trying to seal a Wasm object actually throws a `TypeError`. --- .../testsuite/javalib/lang/SystemJSTest.scala | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/javalib/lang/SystemJSTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/javalib/lang/SystemJSTest.scala index 867e08de6c..12dc332f64 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/javalib/lang/SystemJSTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/javalib/lang/SystemJSTest.scala @@ -23,25 +23,6 @@ import org.junit.Assume._ class SystemJSTest { - @Test def identityHashCodeIsStableIfObjectIsSealed(): Unit = { - /* This is mostly forward-checking that, should we have an implementation - * that seals Scala.js objects, identityHashCode() survives. - */ - class HasIDHashCodeToBeSealed - - // Seal before the first call to hashCode() - val x1 = new HasIDHashCodeToBeSealed - js.Object.seal(x1.asInstanceOf[js.Object]) - val x1FirstHash = x1.hashCode() - assertEquals(x1FirstHash, x1.hashCode()) - - // Seal after the first call to hashCode() - val x2 = new HasIDHashCodeToBeSealed - val x2FirstHash = x2.hashCode() - js.Object.seal(x2.asInstanceOf[js.Object]) - assertEquals(x2FirstHash, x2.hashCode()) - } - @Test def identityHashCodeForJSObjects(): Unit = { if (Platform.assumeES2015 || js.typeOf(js.Dynamic.global.WeakMap) != "undefined") { /* This test is more restrictive than the spec, but we know our From bc19472b73a2c72870eb8212b5a941cf5083c650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 13 May 2024 14:57:33 +0200 Subject: [PATCH 093/298] Adapt boxValueClassesGivenToJSInteropMethod not to rely on the toString export. The Wasm backend will not support `@JSExport` from Scala classes, including for `toString()`. This commit makes the test pass on Wasm without undermining what it was meat to test. In fact, it is now more similar to the `unbox` test just above. In order not to weaken the overall test, we add dedicated tests for `@JSExport` behavior inside value classes to `ExportsTest` instead. --- .../compiler/InteroperabilityTest.scala | 8 ++++--- .../testsuite/jsinterop/ExportsTest.scala | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/InteroperabilityTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/InteroperabilityTest.scala index 70abe08846..cc8f62fdcc 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/InteroperabilityTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/InteroperabilityTest.scala @@ -630,13 +630,15 @@ class InteroperabilityTest { @Test def boxValueClassesGivenToJSInteropMethod(): Unit = { val obj = js.eval(""" var obj = { - stringOf: function(vc) { return vc.toString(); } + test: function(vc) { return vc; } }; obj; """).asInstanceOf[InteroperabilityTestValueClassParam] val vc = new SomeValueClass(7) - assertEquals("SomeValueClass(7)", obj.stringOf(vc)) + val r = obj.test(vc) + assertTrue(r.isInstanceOf[SomeValueClass]) + assertEquals(7, r.asInstanceOf[SomeValueClass].i) } @Test def doNotUnboxValuesReceivedFromJSMethodInStatementPosition(): Unit = { @@ -818,7 +820,7 @@ object InteroperabilityTest { @js.native trait InteroperabilityTestValueClassParam extends js.Object { - def stringOf(vc: SomeValueClass): String = js.native + def test(vc: SomeValueClass): Any = js.native } @js.native diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/ExportsTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/ExportsTest.scala index 2d2ce36b22..6cb3bf9481 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/ExportsTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/ExportsTest.scala @@ -728,6 +728,20 @@ class ExportsTest { assertEquals(18, bar.method(5, 6, 7)) } + @Test def exportsInsideValueClass(): Unit = { + val obj = new ValueClassWithExports(5).asInstanceOf[js.Dynamic] + + // Explicit export + assertEquals(12, obj.add(7)) + + // Export for toString() inherited from jl.Object + assertEquals("ValueClassWithExports(value = 5)", obj.toString()) + + // Export for toString() visible from JavaScript + val f = new js.Function("obj", "return '' + obj;").asInstanceOf[js.Function1[Any, String]] + assertEquals("ValueClassWithExports(value = 5)", f(obj)) + } + @Test def overloadingWithInheritedExports(): Unit = { class A { @JSExport @@ -1986,6 +2000,13 @@ class ExportedDefaultArgClass(x: Int, y: Int, z: Int) { class SomeValueClass(val i: Int) extends AnyVal +class ValueClassWithExports(val value: Int) extends AnyVal { + @JSExport("add") + def addToValue(x: Int): Int = value + x + + override def toString(): String = s"ValueClassWithExports(value = $value)" +} + object ExportHolder { @JSExportTopLevel("NestedExportedClass") class ExportedClass From 8e06210bd045cd4b87a787b7a79ca4b7ec13f0fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 13 May 2024 17:05:05 +0200 Subject: [PATCH 094/298] Extract tests for `@JSExportTopLevel` in a separate file/class. `@JSExportTopLevel` originally evolved from `@JSExport`. At the time, it made sense to have their tests in the same test class. Nowadays, however, these features have more differences than things in common. These differences will be even more exacerbated with WebAssembly, since it will support `@JSExportTopLevel` but not `@JSExport`. --- .../testsuite/jsinterop/ExportsTest.scala | 632 ----------------- .../jsinterop/TopLevelExportsTest.scala | 662 ++++++++++++++++++ 2 files changed, 662 insertions(+), 632 deletions(-) create mode 100644 test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/TopLevelExportsTest.scala diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/ExportsTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/ExportsTest.scala index 6cb3bf9481..4c03aef61e 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/ExportsTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/ExportsTest.scala @@ -16,32 +16,17 @@ import scala.language.higherKinds import scala.scalajs.js import scala.scalajs.js.annotation._ -import scala.scalajs.js.Dynamic.global import org.scalajs.testsuite.utils.AssertThrows.assertThrows import org.scalajs.testsuite.utils.JSAssert._ -import org.scalajs.testsuite.utils.JSUtils import org.scalajs.testsuite.utils.Platform._ -import scala.annotation.meta - -import scala.concurrent._ -import scala.concurrent.ExecutionContext.Implicits.{global => globalEc} - import org.junit.Assert._ import org.junit.Assume._ import org.junit.Test -import org.scalajs.junit.async._ - class ExportsTest { - /** The namespace in which top-level exports are stored. */ - private lazy val exportsNamespace: Future[js.Dynamic] = - ExportLoopback.exportsNamespace - - // @JSExport - @Test def exportsForMethodsWithImplicitName(): Unit = { class Foo { @JSExport @@ -1093,209 +1078,6 @@ class ExportsTest { assertEquals(3, a.foo(vc1.asInstanceOf[js.Any], vc2.asInstanceOf[js.Any])) } - @Test def toplevelExportsForObjects(): AsyncResult = await { - val objFuture = - if (isNoModule) Future.successful(global.TopLevelExportedObject) - else exportsNamespace.map(_.TopLevelExportedObject) - for (obj <- objFuture) yield { - assertJSNotUndefined(obj) - assertEquals("object", js.typeOf(obj)) - assertEquals("witness", obj.witness) - } - } - - @Test def toplevelExportsForScalaJSDefinedJSObjects(): AsyncResult = await { - val obj1Future = - if (isNoModule) Future.successful(global.SJSDefinedTopLevelExportedObject) - else exportsNamespace.map(_.SJSDefinedTopLevelExportedObject) - for (obj1 <- obj1Future) yield { - assertJSNotUndefined(obj1) - assertEquals("object", js.typeOf(obj1)) - assertEquals("witness", obj1.witness) - - assertSame(obj1, SJSDefinedExportedObject) - } - } - - @Test def toplevelExportsForNestedObjects(): AsyncResult = await { - val objFuture = - if (isNoModule) Future.successful(global.NestedExportedObject) - else exportsNamespace.map(_.NestedExportedObject) - for (obj <- objFuture) yield { - assertJSNotUndefined(obj) - assertEquals("object", js.typeOf(obj)) - assertSame(obj, ExportHolder.ExportedObject) - } - } - - @Test def exportsForObjectsWithConstantFoldedName(): AsyncResult = await { - val objFuture = - if (isNoModule) Future.successful(global.ConstantFoldedObjectExport) - else exportsNamespace.map(_.ConstantFoldedObjectExport) - for (obj <- objFuture) yield { - assertJSNotUndefined(obj) - assertEquals("object", js.typeOf(obj)) - assertEquals("witness", obj.witness) - } - } - - @Test def exportsForProtectedObjects(): AsyncResult = await { - val objFuture = - if (isNoModule) Future.successful(global.ProtectedExportedObject) - else exportsNamespace.map(_.ProtectedExportedObject) - for (obj <- objFuture) yield { - assertJSNotUndefined(obj) - assertEquals("object", js.typeOf(obj)) - assertEquals("witness", obj.witness) - } - } - - @Test def toplevelExportsForClasses(): AsyncResult = await { - val constrFuture = - if (isNoModule) Future.successful(global.TopLevelExportedClass) - else exportsNamespace.map(_.TopLevelExportedClass) - for (constr <- constrFuture) yield { - assertJSNotUndefined(constr) - assertEquals("function", js.typeOf(constr)) - val obj = js.Dynamic.newInstance(constr)(5) - assertEquals(5, obj.x) - } - } - - @Test def toplevelExportsForScalaJSDefinedJSClasses(): AsyncResult = await { - val constrFuture = - if (isNoModule) Future.successful(global.SJSDefinedTopLevelExportedClass) - else exportsNamespace.map(_.SJSDefinedTopLevelExportedClass) - for (constr <- constrFuture) yield { - assertJSNotUndefined(constr) - assertEquals("function", js.typeOf(constr)) - val obj = js.Dynamic.newInstance(constr)(5) - assertTrue((obj: Any).isInstanceOf[SJSDefinedTopLevelExportedClass]) - assertEquals(5, obj.x) - - assertSame(constr, js.constructorOf[SJSDefinedTopLevelExportedClass]) - } - } - - @Test def toplevelExportsForAbstractJSClasses_Issue4117(): AsyncResult = await { - val constrFuture = - if (isNoModule) Future.successful(global.TopLevelExportedAbstractJSClass) - else exportsNamespace.map(_.TopLevelExportedAbstractJSClass) - - for (constr <- constrFuture) yield { - assertEquals("function", js.typeOf(constr)) - - val body = if (useECMAScript2015Semantics) { - """ - class SubClass extends constr { - constructor(x) { - super(x); - } - foo(y) { - return y + this.x; - } - } - return SubClass; - """ - } else { - """ - function SubClass(x) { - constr.call(this, x); - } - SubClass.prototype = Object.create(constr.prototype); - SubClass.prototype.foo = function(y) { - return y + this.x; - }; - return SubClass; - """ - } - - val subclassFun = new js.Function("constr", body) - .asInstanceOf[js.Function1[js.Dynamic, js.Dynamic]] - val subclass = subclassFun(constr) - assertEquals("function", js.typeOf(subclass)) - - val obj = js.Dynamic.newInstance(subclass)(5) - .asInstanceOf[TopLevelExportedAbstractJSClass] - - assertEquals(5, obj.x) - assertEquals(11, obj.foo(6)) - assertEquals(33, obj.bar(6)) - } - } - - @Test def toplevelExportsForNestedClasses(): AsyncResult = await { - val constrFuture = - if (isNoModule) Future.successful(global.NestedExportedClass) - else exportsNamespace.map(_.NestedExportedClass) - for (constr <- constrFuture) yield { - assertJSNotUndefined(constr) - assertEquals("function", js.typeOf(constr)) - val obj = js.Dynamic.newInstance(constr)() - assertTrue((obj: Any).isInstanceOf[ExportHolder.ExportedClass]) - } - } - - @Test def toplevelExportsForNestedSjsDefinedClasses(): AsyncResult = await { - val constrFuture = - if (isNoModule) Future.successful(global.NestedSJSDefinedExportedClass) - else exportsNamespace.map(_.NestedSJSDefinedExportedClass) - for (constr <- constrFuture) yield { - assertJSNotUndefined(constr) - assertEquals("function", js.typeOf(constr)) - val obj = js.Dynamic.newInstance(constr)() - assertTrue((obj: Any).isInstanceOf[ExportHolder.SJSDefinedExportedClass]) - } - } - - @Test def exportsForClassesWithConstantFoldedName(): AsyncResult = await { - val constrFuture = - if (isNoModule) Future.successful(global.ConstantFoldedClassExport) - else exportsNamespace.map(_.ConstantFoldedClassExport) - for (constr <- constrFuture) yield { - assertJSNotUndefined(constr) - assertEquals("function", js.typeOf(constr)) - val obj = js.Dynamic.newInstance(constr)(5) - assertEquals(5, obj.x) - } - } - - @Test def exportsForProtectedClasses(): AsyncResult = await { - val constrFuture = - if (isNoModule) Future.successful(global.ProtectedExportedClass) - else exportsNamespace.map(_.ProtectedExportedClass) - for (constr <- constrFuture) yield { - assertJSNotUndefined(constr) - assertEquals("function", js.typeOf(constr)) - val obj = js.Dynamic.newInstance(constr)(5) - assertEquals(5, obj.x) - } - } - - @Test def exportForClassesWithRepeatedParametersInCtor(): AsyncResult = await { - val constrFuture = - if (isNoModule) Future.successful(global.ExportedVarArgClass) - else exportsNamespace.map(_.ExportedVarArgClass) - for (constr <- constrFuture) yield { - assertEquals("", js.Dynamic.newInstance(constr)().result) - assertEquals("a", js.Dynamic.newInstance(constr)("a").result) - assertEquals("a|b", js.Dynamic.newInstance(constr)("a", "b").result) - assertEquals("a|b|c", js.Dynamic.newInstance(constr)("a", "b", "c").result) - assertEquals("Number: <5>|a", js.Dynamic.newInstance(constr)(5, "a").result) - } - } - - @Test def exportForClassesWithDefaultParametersInCtor(): AsyncResult = await { - val constrFuture = - if (isNoModule) Future.successful(global.ExportedDefaultArgClass) - else exportsNamespace.map(_.ExportedDefaultArgClass) - for (constr <- constrFuture) yield { - assertEquals(6, js.Dynamic.newInstance(constr)(1,2,3).result) - assertEquals(106, js.Dynamic.newInstance(constr)(1).result) - assertEquals(103, js.Dynamic.newInstance(constr)(1,2).result) - } - } - @Test def disambiguateOverloadsInvolvingLongs(): Unit = { class Foo { @@ -1672,332 +1454,12 @@ class ExportsTest { testExposure(getJSObj3()) testExposure(getJSObj4()) } - - // @JSExportTopLevel - - @Test def basicTopLevelExport(): Unit = { - assumeTrue("Assume NoModule", isNoModule) - assertEquals(1, global.TopLevelExport_basic()) - } - - @Test def basicTopLevelExportModule(): AsyncResult = await { - assumeFalse("Assume Module", isNoModule) - for (exp <- exportsNamespace) yield { - assertEquals(1, exp.TopLevelExport_basic()) - } - } - - @Test def overloadedTopLevelExport(): Unit = { - assumeTrue("Assume NoModule", isNoModule) - assertEquals("Hello World", global.TopLevelExport_overload("World")) - assertEquals(2, global.TopLevelExport_overload(2)) - assertEquals(9, global.TopLevelExport_overload(2, 7)) - assertEquals(10, global.TopLevelExport_overload(1, 2, 3, 4)) - } - - @Test def overloadedTopLevelExportModule(): AsyncResult = await { - assumeFalse("Assume Module", isNoModule) - for (exp <- exportsNamespace) yield { - assertEquals("Hello World", exp.TopLevelExport_overload("World")) - assertEquals(2, exp.TopLevelExport_overload(2)) - assertEquals(9, exp.TopLevelExport_overload(2, 7)) - assertEquals(10, exp.TopLevelExport_overload(1, 2, 3, 4)) - } - } - - @Test def defaultParamsTopLevelExport_Issue4052(): Unit = { - assumeTrue("Assume NoModule", isNoModule) - assertEquals(7, global.TopLevelExport_defaultParams(6)) - assertEquals(11, global.TopLevelExport_defaultParams(6, 5)) - } - - @Test def defaultParamsTopLevelExportModule_Issue4052(): AsyncResult = await { - assumeFalse("Assume Module", isNoModule) - for (exp <- exportsNamespace) yield { - assertEquals(7, exp.TopLevelExport_defaultParams(6)) - assertEquals(11, exp.TopLevelExport_defaultParams(6, 5)) - } - } - - @Test def topLevelExportUsesUniqueObject(): Unit = { - assumeTrue("Assume NoModule", isNoModule) - global.TopLevelExport_set(3) - assertEquals(3, TopLevelExports.myVar) - global.TopLevelExport_set(7) - assertEquals(7, TopLevelExports.myVar) - } - - @Test def topLevelExportUsesUniqueObjectModule(): AsyncResult = await { - assumeFalse("Assume Module", isNoModule) - for (exp <- exportsNamespace) yield { - exp.TopLevelExport_set(3) - assertEquals(3, TopLevelExports.myVar) - exp.TopLevelExport_set(7) - assertEquals(7, TopLevelExports.myVar) - } - } - - @Test def topLevelExportFromNestedObject(): Unit = { - assumeTrue("Assume NoModule", isNoModule) - global.TopLevelExport_setNested(28) - assertEquals(28, TopLevelExports.Nested.myVar) - } - - @Test def topLevelExportFromNestedObjectModule(): AsyncResult = await { - assumeFalse("Assume Module", isNoModule) - for (exp <- exportsNamespace) yield { - exp.TopLevelExport_setNested(28) - assertEquals(28, TopLevelExports.Nested.myVar) - } - } - - @Test def topLevelExportWithDoubleUnderscore(): Unit = { - assumeTrue("Assume NoModule", isNoModule) - assertEquals(true, global.__topLevelExportWithDoubleUnderscore) - } - - @Test def topLevelExportWithDoubleUnderscoreModule(): AsyncResult = await { - assumeFalse("Assume Module", isNoModule) - for (exp <- exportsNamespace) yield { - assertEquals(true, exp.__topLevelExportWithDoubleUnderscore) - } - } - - @Test def topLevelExportIsAlwaysReachable(): Unit = { - assumeTrue("Assume NoModule", isNoModule) - assertEquals("Hello World", global.TopLevelExport_reachability()) - } - - @Test def topLevelExportIsAlwaysReachableModule(): AsyncResult = await { - assumeFalse("Assume Module", isNoModule) - for (exp <- exportsNamespace) yield { - assertEquals("Hello World", exp.TopLevelExport_reachability()) - } - } - - // @JSExportTopLevel fields - - @Test def topLevelExportBasicField(): Unit = { - assumeTrue("Assume NoModule", isNoModule) - // Initialization - assertEquals(5, global.TopLevelExport_basicVal) - assertEquals("hello", global.TopLevelExport_basicVar) - - // Scala modifies var - TopLevelFieldExports.basicVar = "modified" - assertEquals("modified", TopLevelFieldExports.basicVar) - assertEquals("modified", global.TopLevelExport_basicVar) - - // Reset var - TopLevelFieldExports.basicVar = "hello" - } - - @Test def topLevelExportBasicFieldModule(): AsyncResult = await { - assumeFalse("Assume Module", isNoModule) - for (exp <- exportsNamespace) yield { - // Initialization - assertEquals(5, exp.TopLevelExport_basicVal) - assertEquals("hello", exp.TopLevelExport_basicVar) - - // Scala modifies var - TopLevelFieldExports.basicVar = "modified" - assertEquals("modified", TopLevelFieldExports.basicVar) - assertEquals("modified", exp.TopLevelExport_basicVar) - - // Reset var - TopLevelFieldExports.basicVar = "hello" - } - } - - @Test def topLevelExportFieldTwice(): Unit = { - assumeTrue("Assume NoModule", isNoModule) - - // Initialization - assertEquals(5, global.TopLevelExport_valExportedTwice1) - assertEquals("hello", global.TopLevelExport_varExportedTwice1) - assertEquals("hello", global.TopLevelExport_varExportedTwice2) - - // Scala modifies var - TopLevelFieldExports.varExportedTwice = "modified" - assertEquals("modified", TopLevelFieldExports.varExportedTwice) - assertEquals("modified", global.TopLevelExport_varExportedTwice1) - assertEquals("modified", global.TopLevelExport_varExportedTwice2) - - // Reset var - TopLevelFieldExports.varExportedTwice = "hello" - } - - @Test def topLevelExportFieldTwiceModule(): AsyncResult = await { - assumeFalse("Assume Module", isNoModule) - for (exp <- exportsNamespace) yield { - // Initialization - assertEquals(5, exp.TopLevelExport_valExportedTwice1) - assertEquals("hello", exp.TopLevelExport_varExportedTwice1) - assertEquals("hello", exp.TopLevelExport_varExportedTwice2) - - // Scala modifies var - TopLevelFieldExports.varExportedTwice = "modified" - assertEquals("modified", TopLevelFieldExports.varExportedTwice) - assertEquals("modified", exp.TopLevelExport_varExportedTwice1) - assertEquals("modified", exp.TopLevelExport_varExportedTwice2) - - // Reset var - TopLevelFieldExports.varExportedTwice = "hello" - } - } - - @Test def topLevelExportWriteValVarCausesTypeerror(): AsyncResult = await { - assumeFalse("Unchecked in Script mode", isNoModule) - - for (exp <- exportsNamespace) yield { - assertThrows(classOf[js.JavaScriptException], { - exp.TopLevelExport_basicVal = 54 - }) - - assertThrows(classOf[js.JavaScriptException], { - exp.TopLevelExport_basicVar = 54 - }) - } - } - - @Test def topLevelExportUninitializedFieldsScala(): Unit = { - assertEquals(0, TopLevelFieldExports.uninitializedVarInt) - assertEquals(0L, TopLevelFieldExports.uninitializedVarLong) - assertEquals(null, TopLevelFieldExports.uninitializedVarString) - assertEquals('\u0000', TopLevelFieldExports.uninitializedVarChar) - } - - @Test def topLevelExportUninitializedFields(): Unit = { - assumeTrue("Assume NoModule", isNoModule) - assertEquals(null, global.TopLevelExport_uninitializedVarInt) - assertEquals(null, global.TopLevelExport_uninitializedVarLong) - assertEquals(null, global.TopLevelExport_uninitializedVarString) - assertEquals(null, global.TopLevelExport_uninitializedVarChar) - } - - @Test def topLevelExportUninitializedFieldsModule(): AsyncResult = await { - assumeFalse("Assume Module", isNoModule) - for (exp <- exportsNamespace) yield { - assertEquals(null, exp.TopLevelExport_uninitializedVarInt) - assertEquals(null, exp.TopLevelExport_uninitializedVarLong) - assertEquals(null, exp.TopLevelExport_uninitializedVarString) - assertEquals(null, exp.TopLevelExport_uninitializedVarChar) - } - } - - @Test def topLevelExportFieldIsAlwaysReachableAndInitialized(): Unit = { - assumeTrue("Assume NoModule", isNoModule) - assertEquals("Hello World", global.TopLevelExport_fieldreachability) - } - - @Test def topLevelExportFieldIsAlwaysReachableAndInitializedModule(): AsyncResult = await { - assumeFalse("Assume Module", isNoModule) - for (exp <- exportsNamespace) yield { - assertEquals("Hello World", exp.TopLevelExport_fieldreachability) - } - } - - @Test def topLevelExportFieldIsWritableAccrossModules(): Unit = { - /* We write to basicVar exported above from a different object to test writing - * of static fields across module boundaries (when module splitting is - * enabled). - */ - - assertEquals("hello", TopLevelFieldExports.inlineVar) - TopLevelFieldExports.inlineVar = "hello modules" - assertEquals("hello modules", TopLevelFieldExports.inlineVar) - - // Reset var - TopLevelFieldExports.inlineVar = "hello" - } - - // @JSExportTopLevel in Script's are `let`s in ES 2015, `var`s in ES 5.1 - - @Test def topLevelExportsNoModuleAreOfCorrectKind(): Unit = { - assumeTrue("relevant only for NoModule", isNoModule) - - val g = JSUtils.globalObject - - // Do we expect to get undefined when looking up the exports in the global object? - val undefinedExpected = useECMAScript2015Semantics - - assertEquals(undefinedExpected, js.isUndefined(g.TopLevelExportedObject)) - assertEquals(undefinedExpected, js.isUndefined(g.SJSDefinedTopLevelExportedObject)) - assertEquals(undefinedExpected, js.isUndefined(g.TopLevelExportedClass)) - assertEquals(undefinedExpected, js.isUndefined(g.SJSDefinedTopLevelExportedClass)) - assertEquals(undefinedExpected, js.isUndefined(g.TopLevelExport_basic)) - assertEquals(undefinedExpected, js.isUndefined(g.TopLevelExport_basicVal)) - assertEquals(undefinedExpected, js.isUndefined(g.TopLevelExport_basicVar)) - } } object ExportNameHolder { - final val className = "ConstantFoldedClassExport" - final val objectName = "ConstantFoldedObjectExport" final val methodName = "myMethod" } -@JSExportTopLevel("TopLevelExportedObject") -@JSExportTopLevel(ExportNameHolder.objectName) -object TopLevelExportedObject { - @JSExport - val witness: String = "witness" -} - -@JSExportTopLevel("SJSDefinedTopLevelExportedObject") -object SJSDefinedExportedObject extends js.Object { - val witness: String = "witness" -} - -@JSExportTopLevel("ProtectedExportedObject") -protected object ProtectedExportedObject { - @JSExport - def witness: String = "witness" -} - -@JSExportTopLevel("TopLevelExportedClass") -@JSExportTopLevel(ExportNameHolder.className) -class TopLevelExportedClass(_x: Int) { - @JSExport - val x = _x -} - -@JSExportTopLevel("SJSDefinedTopLevelExportedClass") -class SJSDefinedTopLevelExportedClass(val x: Int) extends js.Object - -@JSExportTopLevel("TopLevelExportedAbstractJSClass") -abstract class TopLevelExportedAbstractJSClass(val x: Int) extends js.Object { - def foo(y: Int): Int - - def bar(y: Int): Int = 3 * foo(y) -} - -@JSExportTopLevel("ProtectedExportedClass") -protected class ProtectedExportedClass(_x: Int) { - @JSExport - val x = _x -} - -@JSExportTopLevel("ExportedVarArgClass") -class ExportedVarArgClass(x: String*) { - - @JSExportTopLevel("ExportedVarArgClass") - def this(x: Int, y: String) = this(s"Number: <$x>", y) - - @JSExport - def result: String = x.mkString("|") -} - -@JSExportTopLevel("ExportedDefaultArgClass") -class ExportedDefaultArgClass(x: Int, y: Int, z: Int) { - - @JSExportTopLevel("ExportedDefaultArgClass") - def this(x: Int, y: Int = 5) = this(x, y, 100) - - @JSExport - def result: Int = x + y + z -} - class SomeValueClass(val i: Int) extends AnyVal class ValueClassWithExports(val value: Int) extends AnyVal { @@ -2007,100 +1469,6 @@ class ValueClassWithExports(val value: Int) extends AnyVal { override def toString(): String = s"ValueClassWithExports(value = $value)" } -object ExportHolder { - @JSExportTopLevel("NestedExportedClass") - class ExportedClass - - @JSExportTopLevel("NestedExportedObject") - object ExportedObject - - @JSExportTopLevel("NestedSJSDefinedExportedClass") - class SJSDefinedExportedClass extends js.Object -} - -object TopLevelExports { - @JSExportTopLevel("TopLevelExport_basic") - def basic(): Int = 1 - - @JSExportTopLevel("TopLevelExport_overload") - def overload(x: String): String = "Hello " + x - - @JSExportTopLevel("TopLevelExport_overload") - def overload(x: Int, y: Int*): Int = x + y.sum - - @JSExportTopLevel("TopLevelExport_defaultParams") - def defaultParams(x: Int, y: Int = 1): Int = x + y - - var myVar: Int = _ - - @JSExportTopLevel("TopLevelExport_set") - def setMyVar(x: Int): Unit = myVar = x - - object Nested { - var myVar: Int = _ - - @JSExportTopLevel("TopLevelExport_setNested") - def setMyVar(x: Int): Unit = myVar = x - } - - @JSExportTopLevel("__topLevelExportWithDoubleUnderscore") - val topLevelExportWithDoubleUnderscore: Boolean = true -} - -/* This object is only reachable via the top level export to make sure the - * analyzer behaves correctly. - */ -object TopLevelExportsReachability { - private val name = "World" - - @JSExportTopLevel("TopLevelExport_reachability") - def basic(): String = "Hello " + name -} - -object TopLevelFieldExports { - @JSExportTopLevel("TopLevelExport_basicVal") - val basicVal: Int = 5 - - @JSExportTopLevel("TopLevelExport_basicVar") - var basicVar: String = "hello" - - @JSExportTopLevel("TopLevelExport_valExportedTwice1") - @JSExportTopLevel("TopLevelExport_valExportedTwice2") - val valExportedTwice: Int = 5 - - @JSExportTopLevel("TopLevelExport_varExportedTwice1") - @JSExportTopLevel("TopLevelExport_varExportedTwice2") - var varExportedTwice: String = "hello" - - @JSExportTopLevel("TopLevelExport_uninitializedVarInt") - var uninitializedVarInt: Int = _ - - @JSExportTopLevel("TopLevelExport_uninitializedVarLong") - var uninitializedVarLong: Long = _ - - @JSExportTopLevel("TopLevelExport_uninitializedVarString") - var uninitializedVarString: String = _ - - @JSExportTopLevel("TopLevelExport_uninitializedVarChar") - var uninitializedVarChar: Char = _ - - // the export is only to make the field IR-static - @JSExportTopLevel("TopLevelExport_irrelevant") - @(inline @meta.getter @meta.setter) - var inlineVar: String = "hello" -} - -/* This object and its static initializer are only reachable via the top-level - * export of its field, to make sure the analyzer and the static initiliazer - * behave correctly. - */ -object TopLevelFieldExportsReachability { - private val name = "World" - - @JSExportTopLevel("TopLevelExport_fieldreachability") - val greeting = "Hello " + name -} - abstract class AbstractClasstWithPropertyForExport { @JSExport def x: js.Object diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/TopLevelExportsTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/TopLevelExportsTest.scala new file mode 100644 index 0000000000..b0d90209e6 --- /dev/null +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/TopLevelExportsTest.scala @@ -0,0 +1,662 @@ +/* + * 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.testsuite.jsinterop + +import scala.scalajs.js +import scala.scalajs.js.annotation._ +import scala.scalajs.js.Dynamic.global + +import org.scalajs.testsuite.utils.AssertThrows.assertThrows +import org.scalajs.testsuite.utils.JSAssert._ +import org.scalajs.testsuite.utils.JSUtils +import org.scalajs.testsuite.utils.Platform._ + +import scala.annotation.meta + +import scala.concurrent._ +import scala.concurrent.ExecutionContext.Implicits.{global => globalEc} + +import org.junit.Assert._ +import org.junit.Assume._ +import org.junit.Test + +import org.scalajs.junit.async._ + +class TopLevelExportsTest { + + /** The namespace in which top-level exports are stored. */ + private lazy val exportsNamespace: Future[js.Dynamic] = + ExportLoopback.exportsNamespace + + // @JSExportTopLevel classes and objects + + @Test def toplevelExportsForObjects(): AsyncResult = await { + val objFuture = + if (isNoModule) Future.successful(global.TopLevelExportedObject) + else exportsNamespace.map(_.TopLevelExportedObject) + for (obj <- objFuture) yield { + assertJSNotUndefined(obj) + assertEquals("object", js.typeOf(obj)) + assertEquals("witness", obj.witness) + } + } + + @Test def toplevelExportsForScalaJSDefinedJSObjects(): AsyncResult = await { + val obj1Future = + if (isNoModule) Future.successful(global.SJSDefinedTopLevelExportedObject) + else exportsNamespace.map(_.SJSDefinedTopLevelExportedObject) + for (obj1 <- obj1Future) yield { + assertJSNotUndefined(obj1) + assertEquals("object", js.typeOf(obj1)) + assertEquals("witness", obj1.witness) + + assertSame(obj1, SJSDefinedExportedObject) + } + } + + @Test def toplevelExportsForNestedObjects(): AsyncResult = await { + val objFuture = + if (isNoModule) Future.successful(global.NestedExportedObject) + else exportsNamespace.map(_.NestedExportedObject) + for (obj <- objFuture) yield { + assertJSNotUndefined(obj) + assertEquals("object", js.typeOf(obj)) + assertSame(obj, ExportHolder.ExportedObject) + } + } + + @Test def exportsForObjectsWithConstantFoldedName(): AsyncResult = await { + val objFuture = + if (isNoModule) Future.successful(global.ConstantFoldedObjectExport) + else exportsNamespace.map(_.ConstantFoldedObjectExport) + for (obj <- objFuture) yield { + assertJSNotUndefined(obj) + assertEquals("object", js.typeOf(obj)) + assertEquals("witness", obj.witness) + } + } + + @Test def exportsForProtectedObjects(): AsyncResult = await { + val objFuture = + if (isNoModule) Future.successful(global.ProtectedExportedObject) + else exportsNamespace.map(_.ProtectedExportedObject) + for (obj <- objFuture) yield { + assertJSNotUndefined(obj) + assertEquals("object", js.typeOf(obj)) + assertEquals("witness", obj.witness) + } + } + + @Test def toplevelExportsForClasses(): AsyncResult = await { + val constrFuture = + if (isNoModule) Future.successful(global.TopLevelExportedClass) + else exportsNamespace.map(_.TopLevelExportedClass) + for (constr <- constrFuture) yield { + assertJSNotUndefined(constr) + assertEquals("function", js.typeOf(constr)) + val obj = js.Dynamic.newInstance(constr)(5) + assertEquals(5, obj.x) + } + } + + @Test def toplevelExportsForScalaJSDefinedJSClasses(): AsyncResult = await { + val constrFuture = + if (isNoModule) Future.successful(global.SJSDefinedTopLevelExportedClass) + else exportsNamespace.map(_.SJSDefinedTopLevelExportedClass) + for (constr <- constrFuture) yield { + assertJSNotUndefined(constr) + assertEquals("function", js.typeOf(constr)) + val obj = js.Dynamic.newInstance(constr)(5) + assertTrue((obj: Any).isInstanceOf[SJSDefinedTopLevelExportedClass]) + assertEquals(5, obj.x) + + assertSame(constr, js.constructorOf[SJSDefinedTopLevelExportedClass]) + } + } + + @Test def toplevelExportsForAbstractJSClasses_Issue4117(): AsyncResult = await { + val constrFuture = + if (isNoModule) Future.successful(global.TopLevelExportedAbstractJSClass) + else exportsNamespace.map(_.TopLevelExportedAbstractJSClass) + + for (constr <- constrFuture) yield { + assertEquals("function", js.typeOf(constr)) + + val body = if (useECMAScript2015Semantics) { + """ + class SubClass extends constr { + constructor(x) { + super(x); + } + foo(y) { + return y + this.x; + } + } + return SubClass; + """ + } else { + """ + function SubClass(x) { + constr.call(this, x); + } + SubClass.prototype = Object.create(constr.prototype); + SubClass.prototype.foo = function(y) { + return y + this.x; + }; + return SubClass; + """ + } + + val subclassFun = new js.Function("constr", body) + .asInstanceOf[js.Function1[js.Dynamic, js.Dynamic]] + val subclass = subclassFun(constr) + assertEquals("function", js.typeOf(subclass)) + + val obj = js.Dynamic.newInstance(subclass)(5) + .asInstanceOf[TopLevelExportedAbstractJSClass] + + assertEquals(5, obj.x) + assertEquals(11, obj.foo(6)) + assertEquals(33, obj.bar(6)) + } + } + + @Test def toplevelExportsForNestedClasses(): AsyncResult = await { + val constrFuture = + if (isNoModule) Future.successful(global.NestedExportedClass) + else exportsNamespace.map(_.NestedExportedClass) + for (constr <- constrFuture) yield { + assertJSNotUndefined(constr) + assertEquals("function", js.typeOf(constr)) + val obj = js.Dynamic.newInstance(constr)() + assertTrue((obj: Any).isInstanceOf[ExportHolder.ExportedClass]) + } + } + + @Test def toplevelExportsForNestedSjsDefinedClasses(): AsyncResult = await { + val constrFuture = + if (isNoModule) Future.successful(global.NestedSJSDefinedExportedClass) + else exportsNamespace.map(_.NestedSJSDefinedExportedClass) + for (constr <- constrFuture) yield { + assertJSNotUndefined(constr) + assertEquals("function", js.typeOf(constr)) + val obj = js.Dynamic.newInstance(constr)() + assertTrue((obj: Any).isInstanceOf[ExportHolder.SJSDefinedExportedClass]) + } + } + + @Test def exportsForClassesWithConstantFoldedName(): AsyncResult = await { + val constrFuture = + if (isNoModule) Future.successful(global.ConstantFoldedClassExport) + else exportsNamespace.map(_.ConstantFoldedClassExport) + for (constr <- constrFuture) yield { + assertJSNotUndefined(constr) + assertEquals("function", js.typeOf(constr)) + val obj = js.Dynamic.newInstance(constr)(5) + assertEquals(5, obj.x) + } + } + + @Test def exportsForProtectedClasses(): AsyncResult = await { + val constrFuture = + if (isNoModule) Future.successful(global.ProtectedExportedClass) + else exportsNamespace.map(_.ProtectedExportedClass) + for (constr <- constrFuture) yield { + assertJSNotUndefined(constr) + assertEquals("function", js.typeOf(constr)) + val obj = js.Dynamic.newInstance(constr)(5) + assertEquals(5, obj.x) + } + } + + @Test def exportForClassesWithRepeatedParametersInCtor(): AsyncResult = await { + val constrFuture = + if (isNoModule) Future.successful(global.ExportedVarArgClass) + else exportsNamespace.map(_.ExportedVarArgClass) + for (constr <- constrFuture) yield { + assertEquals("", js.Dynamic.newInstance(constr)().result) + assertEquals("a", js.Dynamic.newInstance(constr)("a").result) + assertEquals("a|b", js.Dynamic.newInstance(constr)("a", "b").result) + assertEquals("a|b|c", js.Dynamic.newInstance(constr)("a", "b", "c").result) + assertEquals("Number: <5>|a", js.Dynamic.newInstance(constr)(5, "a").result) + } + } + + @Test def exportForClassesWithDefaultParametersInCtor(): AsyncResult = await { + val constrFuture = + if (isNoModule) Future.successful(global.ExportedDefaultArgClass) + else exportsNamespace.map(_.ExportedDefaultArgClass) + for (constr <- constrFuture) yield { + assertEquals(6, js.Dynamic.newInstance(constr)(1,2,3).result) + assertEquals(106, js.Dynamic.newInstance(constr)(1).result) + assertEquals(103, js.Dynamic.newInstance(constr)(1,2).result) + } + } + + // @JSExportTopLevel methods + + @Test def basicTopLevelExport(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + assertEquals(1, global.TopLevelExport_basic()) + } + + @Test def basicTopLevelExportModule(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + assertEquals(1, exp.TopLevelExport_basic()) + } + } + + @Test def overloadedTopLevelExport(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + assertEquals("Hello World", global.TopLevelExport_overload("World")) + assertEquals(2, global.TopLevelExport_overload(2)) + assertEquals(9, global.TopLevelExport_overload(2, 7)) + assertEquals(10, global.TopLevelExport_overload(1, 2, 3, 4)) + } + + @Test def overloadedTopLevelExportModule(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + assertEquals("Hello World", exp.TopLevelExport_overload("World")) + assertEquals(2, exp.TopLevelExport_overload(2)) + assertEquals(9, exp.TopLevelExport_overload(2, 7)) + assertEquals(10, exp.TopLevelExport_overload(1, 2, 3, 4)) + } + } + + @Test def defaultParamsTopLevelExport_Issue4052(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + assertEquals(7, global.TopLevelExport_defaultParams(6)) + assertEquals(11, global.TopLevelExport_defaultParams(6, 5)) + } + + @Test def defaultParamsTopLevelExportModule_Issue4052(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + assertEquals(7, exp.TopLevelExport_defaultParams(6)) + assertEquals(11, exp.TopLevelExport_defaultParams(6, 5)) + } + } + + @Test def topLevelExportUsesUniqueObject(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + global.TopLevelExport_set(3) + assertEquals(3, TopLevelExports.myVar) + global.TopLevelExport_set(7) + assertEquals(7, TopLevelExports.myVar) + } + + @Test def topLevelExportUsesUniqueObjectModule(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + exp.TopLevelExport_set(3) + assertEquals(3, TopLevelExports.myVar) + exp.TopLevelExport_set(7) + assertEquals(7, TopLevelExports.myVar) + } + } + + @Test def topLevelExportFromNestedObject(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + global.TopLevelExport_setNested(28) + assertEquals(28, TopLevelExports.Nested.myVar) + } + + @Test def topLevelExportFromNestedObjectModule(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + exp.TopLevelExport_setNested(28) + assertEquals(28, TopLevelExports.Nested.myVar) + } + } + + @Test def topLevelExportWithDoubleUnderscore(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + assertEquals(true, global.__topLevelExportWithDoubleUnderscore) + } + + @Test def topLevelExportWithDoubleUnderscoreModule(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + assertEquals(true, exp.__topLevelExportWithDoubleUnderscore) + } + } + + @Test def topLevelExportIsAlwaysReachable(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + assertEquals("Hello World", global.TopLevelExport_reachability()) + } + + @Test def topLevelExportIsAlwaysReachableModule(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + assertEquals("Hello World", exp.TopLevelExport_reachability()) + } + } + + // @JSExportTopLevel fields + + @Test def topLevelExportBasicField(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + // Initialization + assertEquals(5, global.TopLevelExport_basicVal) + assertEquals("hello", global.TopLevelExport_basicVar) + + // Scala modifies var + TopLevelFieldExports.basicVar = "modified" + assertEquals("modified", TopLevelFieldExports.basicVar) + assertEquals("modified", global.TopLevelExport_basicVar) + + // Reset var + TopLevelFieldExports.basicVar = "hello" + } + + @Test def topLevelExportBasicFieldModule(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + // Initialization + assertEquals(5, exp.TopLevelExport_basicVal) + assertEquals("hello", exp.TopLevelExport_basicVar) + + // Scala modifies var + TopLevelFieldExports.basicVar = "modified" + assertEquals("modified", TopLevelFieldExports.basicVar) + assertEquals("modified", exp.TopLevelExport_basicVar) + + // Reset var + TopLevelFieldExports.basicVar = "hello" + } + } + + @Test def topLevelExportFieldTwice(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + + // Initialization + assertEquals(5, global.TopLevelExport_valExportedTwice1) + assertEquals("hello", global.TopLevelExport_varExportedTwice1) + assertEquals("hello", global.TopLevelExport_varExportedTwice2) + + // Scala modifies var + TopLevelFieldExports.varExportedTwice = "modified" + assertEquals("modified", TopLevelFieldExports.varExportedTwice) + assertEquals("modified", global.TopLevelExport_varExportedTwice1) + assertEquals("modified", global.TopLevelExport_varExportedTwice2) + + // Reset var + TopLevelFieldExports.varExportedTwice = "hello" + } + + @Test def topLevelExportFieldTwiceModule(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + // Initialization + assertEquals(5, exp.TopLevelExport_valExportedTwice1) + assertEquals("hello", exp.TopLevelExport_varExportedTwice1) + assertEquals("hello", exp.TopLevelExport_varExportedTwice2) + + // Scala modifies var + TopLevelFieldExports.varExportedTwice = "modified" + assertEquals("modified", TopLevelFieldExports.varExportedTwice) + assertEquals("modified", exp.TopLevelExport_varExportedTwice1) + assertEquals("modified", exp.TopLevelExport_varExportedTwice2) + + // Reset var + TopLevelFieldExports.varExportedTwice = "hello" + } + } + + @Test def topLevelExportWriteValVarCausesTypeerror(): AsyncResult = await { + assumeFalse("Unchecked in Script mode", isNoModule) + + for (exp <- exportsNamespace) yield { + assertThrows(classOf[js.JavaScriptException], { + exp.TopLevelExport_basicVal = 54 + }) + + assertThrows(classOf[js.JavaScriptException], { + exp.TopLevelExport_basicVar = 54 + }) + } + } + + @Test def topLevelExportUninitializedFieldsScala(): Unit = { + assertEquals(0, TopLevelFieldExports.uninitializedVarInt) + assertEquals(0L, TopLevelFieldExports.uninitializedVarLong) + assertEquals(null, TopLevelFieldExports.uninitializedVarString) + assertEquals('\u0000', TopLevelFieldExports.uninitializedVarChar) + } + + @Test def topLevelExportUninitializedFields(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + assertEquals(null, global.TopLevelExport_uninitializedVarInt) + assertEquals(null, global.TopLevelExport_uninitializedVarLong) + assertEquals(null, global.TopLevelExport_uninitializedVarString) + assertEquals(null, global.TopLevelExport_uninitializedVarChar) + } + + @Test def topLevelExportUninitializedFieldsModule(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + assertEquals(null, exp.TopLevelExport_uninitializedVarInt) + assertEquals(null, exp.TopLevelExport_uninitializedVarLong) + assertEquals(null, exp.TopLevelExport_uninitializedVarString) + assertEquals(null, exp.TopLevelExport_uninitializedVarChar) + } + } + + @Test def topLevelExportFieldIsAlwaysReachableAndInitialized(): Unit = { + assumeTrue("Assume NoModule", isNoModule) + assertEquals("Hello World", global.TopLevelExport_fieldreachability) + } + + @Test def topLevelExportFieldIsAlwaysReachableAndInitializedModule(): AsyncResult = await { + assumeFalse("Assume Module", isNoModule) + for (exp <- exportsNamespace) yield { + assertEquals("Hello World", exp.TopLevelExport_fieldreachability) + } + } + + @Test def topLevelExportFieldIsWritableAccrossModules(): Unit = { + /* We write to basicVar exported above from a different object to test writing + * of static fields across module boundaries (when module splitting is + * enabled). + */ + + assertEquals("hello", TopLevelFieldExports.inlineVar) + TopLevelFieldExports.inlineVar = "hello modules" + assertEquals("hello modules", TopLevelFieldExports.inlineVar) + + // Reset var + TopLevelFieldExports.inlineVar = "hello" + } + + // @JSExportTopLevel in Script's are `let`s in ES 2015, `var`s in ES 5.1 + + @Test def topLevelExportsNoModuleAreOfCorrectKind(): Unit = { + assumeTrue("relevant only for NoModule", isNoModule) + + val g = JSUtils.globalObject + + // Do we expect to get undefined when looking up the exports in the global object? + val undefinedExpected = useECMAScript2015Semantics + + assertEquals(undefinedExpected, js.isUndefined(g.TopLevelExportedObject)) + assertEquals(undefinedExpected, js.isUndefined(g.SJSDefinedTopLevelExportedObject)) + assertEquals(undefinedExpected, js.isUndefined(g.TopLevelExportedClass)) + assertEquals(undefinedExpected, js.isUndefined(g.SJSDefinedTopLevelExportedClass)) + assertEquals(undefinedExpected, js.isUndefined(g.TopLevelExport_basic)) + assertEquals(undefinedExpected, js.isUndefined(g.TopLevelExport_basicVal)) + assertEquals(undefinedExpected, js.isUndefined(g.TopLevelExport_basicVar)) + } +} + +object TopLevelExportNameHolder { + final val className = "ConstantFoldedClassExport" + final val objectName = "ConstantFoldedObjectExport" +} + +@JSExportTopLevel("TopLevelExportedObject") +@JSExportTopLevel(TopLevelExportNameHolder.objectName) +object TopLevelExportedObject { + @JSExport + val witness: String = "witness" +} + +@JSExportTopLevel("SJSDefinedTopLevelExportedObject") +object SJSDefinedExportedObject extends js.Object { + val witness: String = "witness" +} + +@JSExportTopLevel("ProtectedExportedObject") +protected object ProtectedExportedObject { + @JSExport + def witness: String = "witness" +} + +@JSExportTopLevel("TopLevelExportedClass") +@JSExportTopLevel(TopLevelExportNameHolder.className) +class TopLevelExportedClass(_x: Int) { + @JSExport + val x = _x +} + +@JSExportTopLevel("SJSDefinedTopLevelExportedClass") +class SJSDefinedTopLevelExportedClass(val x: Int) extends js.Object + +@JSExportTopLevel("TopLevelExportedAbstractJSClass") +abstract class TopLevelExportedAbstractJSClass(val x: Int) extends js.Object { + def foo(y: Int): Int + + def bar(y: Int): Int = 3 * foo(y) +} + +@JSExportTopLevel("ProtectedExportedClass") +protected class ProtectedExportedClass(_x: Int) { + @JSExport + val x = _x +} + +@JSExportTopLevel("ExportedVarArgClass") +class ExportedVarArgClass(x: String*) { + + @JSExportTopLevel("ExportedVarArgClass") + def this(x: Int, y: String) = this(s"Number: <$x>", y) + + @JSExport + def result: String = x.mkString("|") +} + +@JSExportTopLevel("ExportedDefaultArgClass") +class ExportedDefaultArgClass(x: Int, y: Int, z: Int) { + + @JSExportTopLevel("ExportedDefaultArgClass") + def this(x: Int, y: Int = 5) = this(x, y, 100) + + @JSExport + def result: Int = x + y + z +} + +object ExportHolder { + @JSExportTopLevel("NestedExportedClass") + class ExportedClass + + @JSExportTopLevel("NestedExportedObject") + object ExportedObject + + @JSExportTopLevel("NestedSJSDefinedExportedClass") + class SJSDefinedExportedClass extends js.Object +} + +object TopLevelExports { + @JSExportTopLevel("TopLevelExport_basic") + def basic(): Int = 1 + + @JSExportTopLevel("TopLevelExport_overload") + def overload(x: String): String = "Hello " + x + + @JSExportTopLevel("TopLevelExport_overload") + def overload(x: Int, y: Int*): Int = x + y.sum + + @JSExportTopLevel("TopLevelExport_defaultParams") + def defaultParams(x: Int, y: Int = 1): Int = x + y + + var myVar: Int = _ + + @JSExportTopLevel("TopLevelExport_set") + def setMyVar(x: Int): Unit = myVar = x + + object Nested { + var myVar: Int = _ + + @JSExportTopLevel("TopLevelExport_setNested") + def setMyVar(x: Int): Unit = myVar = x + } + + @JSExportTopLevel("__topLevelExportWithDoubleUnderscore") + val topLevelExportWithDoubleUnderscore: Boolean = true +} + +/* This object is only reachable via the top level export to make sure the + * analyzer behaves correctly. + */ +object TopLevelExportsReachability { + private val name = "World" + + @JSExportTopLevel("TopLevelExport_reachability") + def basic(): String = "Hello " + name +} + +object TopLevelFieldExports { + @JSExportTopLevel("TopLevelExport_basicVal") + val basicVal: Int = 5 + + @JSExportTopLevel("TopLevelExport_basicVar") + var basicVar: String = "hello" + + @JSExportTopLevel("TopLevelExport_valExportedTwice1") + @JSExportTopLevel("TopLevelExport_valExportedTwice2") + val valExportedTwice: Int = 5 + + @JSExportTopLevel("TopLevelExport_varExportedTwice1") + @JSExportTopLevel("TopLevelExport_varExportedTwice2") + var varExportedTwice: String = "hello" + + @JSExportTopLevel("TopLevelExport_uninitializedVarInt") + var uninitializedVarInt: Int = _ + + @JSExportTopLevel("TopLevelExport_uninitializedVarLong") + var uninitializedVarLong: Long = _ + + @JSExportTopLevel("TopLevelExport_uninitializedVarString") + var uninitializedVarString: String = _ + + @JSExportTopLevel("TopLevelExport_uninitializedVarChar") + var uninitializedVarChar: Char = _ + + // the export is only to make the field IR-static + @JSExportTopLevel("TopLevelExport_irrelevant") + @(inline @meta.getter @meta.setter) + var inlineVar: String = "hello" +} + +/* This object and its static initializer are only reachable via the top-level + * export of its field, to make sure the analyzer and the static initiliazer + * behave correctly. + */ +object TopLevelFieldExportsReachability { + private val name = "World" + + @JSExportTopLevel("TopLevelExport_fieldreachability") + val greeting = "Hello " + name +} From 4e532afc0e4104bd67da001df7c31053ff68d743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 13 May 2024 17:26:42 +0200 Subject: [PATCH 095/298] Make tests for `@JSExportTopLevel` independent from `@JSExport`. Previously, for top-level exported Scala classes and objects, we used `@JSExport`ed "witness" members to test the results. Because of that, the tests for `@JSExportTopLevel` were dependent on `@JSExport`. That would be problematic for WebAssembly, which is going to support `@JSExportTopLevel` but not `@JSExport`. We now use a `WitnessInterface` trait instead, to access the witness fields/methods without requiring `@JSExport` members. --- .../jsinterop/TopLevelExportsTest.scala | 64 ++++++++++--------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/TopLevelExportsTest.scala b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/TopLevelExportsTest.scala index b0d90209e6..66ae05a678 100644 --- a/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/TopLevelExportsTest.scala +++ b/test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/TopLevelExportsTest.scala @@ -38,6 +38,11 @@ class TopLevelExportsTest { private lazy val exportsNamespace: Future[js.Dynamic] = ExportLoopback.exportsNamespace + def witnessOf(obj: Any): Any = { + assertTrue("" + obj.getClass(), obj.isInstanceOf[WitnessInterface]) + obj.asInstanceOf[WitnessInterface].witness + } + // @JSExportTopLevel classes and objects @Test def toplevelExportsForObjects(): AsyncResult = await { @@ -47,7 +52,7 @@ class TopLevelExportsTest { for (obj <- objFuture) yield { assertJSNotUndefined(obj) assertEquals("object", js.typeOf(obj)) - assertEquals("witness", obj.witness) + assertEquals("witness", witnessOf(obj)) } } @@ -82,7 +87,7 @@ class TopLevelExportsTest { for (obj <- objFuture) yield { assertJSNotUndefined(obj) assertEquals("object", js.typeOf(obj)) - assertEquals("witness", obj.witness) + assertEquals("witness", witnessOf(obj)) } } @@ -93,7 +98,7 @@ class TopLevelExportsTest { for (obj <- objFuture) yield { assertJSNotUndefined(obj) assertEquals("object", js.typeOf(obj)) - assertEquals("witness", obj.witness) + assertEquals("witness", witnessOf(obj)) } } @@ -105,7 +110,7 @@ class TopLevelExportsTest { assertJSNotUndefined(constr) assertEquals("function", js.typeOf(constr)) val obj = js.Dynamic.newInstance(constr)(5) - assertEquals(5, obj.x) + assertEquals(5, witnessOf(obj)) } } @@ -203,7 +208,7 @@ class TopLevelExportsTest { assertJSNotUndefined(constr) assertEquals("function", js.typeOf(constr)) val obj = js.Dynamic.newInstance(constr)(5) - assertEquals(5, obj.x) + assertEquals(5, witnessOf(obj)) } } @@ -215,7 +220,7 @@ class TopLevelExportsTest { assertJSNotUndefined(constr) assertEquals("function", js.typeOf(constr)) val obj = js.Dynamic.newInstance(constr)(5) - assertEquals(5, obj.x) + assertEquals(5, witnessOf(obj)) } } @@ -224,11 +229,11 @@ class TopLevelExportsTest { if (isNoModule) Future.successful(global.ExportedVarArgClass) else exportsNamespace.map(_.ExportedVarArgClass) for (constr <- constrFuture) yield { - assertEquals("", js.Dynamic.newInstance(constr)().result) - assertEquals("a", js.Dynamic.newInstance(constr)("a").result) - assertEquals("a|b", js.Dynamic.newInstance(constr)("a", "b").result) - assertEquals("a|b|c", js.Dynamic.newInstance(constr)("a", "b", "c").result) - assertEquals("Number: <5>|a", js.Dynamic.newInstance(constr)(5, "a").result) + assertEquals("", witnessOf(js.Dynamic.newInstance(constr)())) + assertEquals("a", witnessOf(js.Dynamic.newInstance(constr)("a"))) + assertEquals("a|b", witnessOf(js.Dynamic.newInstance(constr)("a", "b"))) + assertEquals("a|b|c", witnessOf(js.Dynamic.newInstance(constr)("a", "b", "c"))) + assertEquals("Number: <5>|a", witnessOf(js.Dynamic.newInstance(constr)(5, "a"))) } } @@ -237,9 +242,9 @@ class TopLevelExportsTest { if (isNoModule) Future.successful(global.ExportedDefaultArgClass) else exportsNamespace.map(_.ExportedDefaultArgClass) for (constr <- constrFuture) yield { - assertEquals(6, js.Dynamic.newInstance(constr)(1,2,3).result) - assertEquals(106, js.Dynamic.newInstance(constr)(1).result) - assertEquals(103, js.Dynamic.newInstance(constr)(1,2).result) + assertEquals(6, witnessOf(js.Dynamic.newInstance(constr)(1, 2, 3))) + assertEquals(106, witnessOf(js.Dynamic.newInstance(constr)(1))) + assertEquals(103, witnessOf(js.Dynamic.newInstance(constr)(1, 2))) } } @@ -506,10 +511,14 @@ object TopLevelExportNameHolder { final val objectName = "ConstantFoldedObjectExport" } +/** Access to a `witness` property in instances of exported Scala classes. */ +trait WitnessInterface { + def witness: Any +} + @JSExportTopLevel("TopLevelExportedObject") @JSExportTopLevel(TopLevelExportNameHolder.objectName) -object TopLevelExportedObject { - @JSExport +object TopLevelExportedObject extends WitnessInterface { val witness: String = "witness" } @@ -519,16 +528,14 @@ object SJSDefinedExportedObject extends js.Object { } @JSExportTopLevel("ProtectedExportedObject") -protected object ProtectedExportedObject { - @JSExport +protected object ProtectedExportedObject extends WitnessInterface { def witness: String = "witness" } @JSExportTopLevel("TopLevelExportedClass") @JSExportTopLevel(TopLevelExportNameHolder.className) -class TopLevelExportedClass(_x: Int) { - @JSExport - val x = _x +class TopLevelExportedClass(_x: Int) extends WitnessInterface { + val witness = _x } @JSExportTopLevel("SJSDefinedTopLevelExportedClass") @@ -542,29 +549,26 @@ abstract class TopLevelExportedAbstractJSClass(val x: Int) extends js.Object { } @JSExportTopLevel("ProtectedExportedClass") -protected class ProtectedExportedClass(_x: Int) { - @JSExport - val x = _x +protected class ProtectedExportedClass(_x: Int) extends WitnessInterface { + val witness = _x } @JSExportTopLevel("ExportedVarArgClass") -class ExportedVarArgClass(x: String*) { +class ExportedVarArgClass(x: String*) extends WitnessInterface { @JSExportTopLevel("ExportedVarArgClass") def this(x: Int, y: String) = this(s"Number: <$x>", y) - @JSExport - def result: String = x.mkString("|") + def witness: String = x.mkString("|") } @JSExportTopLevel("ExportedDefaultArgClass") -class ExportedDefaultArgClass(x: Int, y: Int, z: Int) { +class ExportedDefaultArgClass(x: Int, y: Int, z: Int) extends WitnessInterface { @JSExportTopLevel("ExportedDefaultArgClass") def this(x: Int, y: Int = 5) = this(x, y, 100) - @JSExport - def result: Int = x + y + z + def witness: Int = x + y + z } object ExportHolder { From 69baff142d96de1afa70bf78971e1756becbca52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sat, 11 May 2024 12:57:00 +0200 Subject: [PATCH 096/298] Fix #4982: Synthesize This nodes with primitive types in hijacked classes. --- .../linker/checker/ClassDefChecker.scala | 25 +++++++++++++++++++ .../linker/frontend/MethodSynthesizer.scala | 10 +++++--- .../org/scalajs/linker/BaseLinkerTest.scala | 25 +++++++++++++++++++ 3 files changed, 56 insertions(+), 4 deletions(-) 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 981d065512..728efa33dc 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 @@ -24,6 +24,7 @@ import org.scalajs.ir.Types._ import org.scalajs.logging._ import org.scalajs.linker.checker.ErrorReporter._ +import org.scalajs.linker.standard.LinkedClass /** Checker for the validity of the IR. */ private final class ClassDefChecker(classDef: ClassDef, @@ -925,6 +926,30 @@ object ClassDefChecker { reporter.errorCount } + def check(linkedClass: LinkedClass, postOptimizer: Boolean, logger: Logger): Int = { + // Rebuild a ClassDef out of the LinkedClass + import linkedClass._ + implicit val pos = linkedClass.pos + val classDef = ClassDef( + name, + OriginalName.NoOriginalName, + kind, + jsClassCaptures, + superClass, + interfaces, + jsSuperClass, + jsNativeLoadSpec, + fields, + methods, + jsConstructorDef, + exportedMembers, + jsNativeMembers, + topLevelExportDefs = Nil + )(optimizerHints) + + check(classDef, postBaseLinker = true, postOptimizer, logger) + } + private class Env( /** Whether there is a valid `new.target` in scope. */ val hasNewTarget: Boolean, diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/MethodSynthesizer.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/MethodSynthesizer.scala index 68ad2893fb..eb50485d57 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/MethodSynthesizer.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/MethodSynthesizer.scala @@ -66,9 +66,10 @@ private[frontend] final class MethodSynthesizer( val targetIdent = targetMDef.name.copy() // for the new pos val proxyIdent = MethodIdent(methodName) val params = targetMDef.args.map(_.copy()) // for the new pos - val currentClassType = ClassType(classInfo.className) + val instanceThisType = + BoxedClassToPrimType.getOrElse(classInfo.className, ClassType(classInfo.className)) - val call = Apply(ApplyFlags.empty, This()(currentClassType), + val call = Apply(ApplyFlags.empty, This()(instanceThisType), targetIdent, params.map(_.ref))(targetMDef.resultType) val body = if (targetName.resultTypeRef == VoidRef) { @@ -100,10 +101,11 @@ private[frontend] final class MethodSynthesizer( val targetIdent = targetMDef.name.copy() // for the new pos val bridgeIdent = targetIdent val params = targetMDef.args.map(_.copy()) // for the new pos - val currentClassType = ClassType(classInfo.className) + val instanceThisType = + BoxedClassToPrimType.getOrElse(classInfo.className, ClassType(classInfo.className)) val body = ApplyStatically( - ApplyFlags.empty, This()(currentClassType), targetInterface, + ApplyFlags.empty, This()(instanceThisType), targetInterface, targetIdent, params.map(_.ref))(targetMDef.resultType) MethodDef(MemberFlags.empty, bridgeIdent, targetMDef.originalName, diff --git a/linker/shared/src/test/scala/org/scalajs/linker/BaseLinkerTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/BaseLinkerTest.scala index b6870d16e5..2f4c74f00a 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/BaseLinkerTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/BaseLinkerTest.scala @@ -22,6 +22,9 @@ import org.scalajs.ir.Types._ import org.scalajs.junit.async._ +import org.scalajs.logging._ + +import org.scalajs.linker.checker.ClassDefChecker import org.scalajs.linker.interface.StandardConfig import org.scalajs.linker.standard._ @@ -67,6 +70,28 @@ class BaseLinkerTest { } } + @Test + def correctThisTypeInHijackedClassReflectiveProxies_Issue4982(): AsyncResult = await { + val compareTo = m("compareTo", List(ClassRef(BoxedIntegerClass)), IntRef) + val compareToReflProxy = + MethodName.reflectiveProxy("compareTo", List(ClassRef(BoxedIntegerClass))) + + val classDefs = Seq( + mainTestClassDef( + consoleLog(Apply(EAF, IntLiteral(5), compareToReflProxy, List(IntLiteral(6)))(AnyType)) + ) + ) + + val config = StandardConfig().withOptimizer(false) + + for (moduleSet <- linkToModuleSet(classDefs, MainTestModuleInitializers, config = config)) yield { + val clazz = findClass(moduleSet, BoxedIntegerClass).get + val errorCount = ClassDefChecker.check(clazz, postOptimizer = false, + new ScalaConsoleLogger(Level.Error)) + assertEquals(0, errorCount) + } + } + private def findClass(moduleSet: ModuleSet, name: ClassName): Option[LinkedClass] = moduleSet.modules.flatMap(_.classDefs).find(_.className == name) } From a62941b1aceeb2a11bd5ecbd4c64232bdfaed88c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 23 May 2024 13:18:45 +0200 Subject: [PATCH 097/298] Make the HTML test runner onLoad event more resilient to loading details. If the `start` method executes after the `DOMContentLoaded` event has fired, the `onLoad` callback was not executed. We are now more robust against that situation, following the pattern recommended on MDN. --- .../org/scalajs/testing/bridge/HTMLRunner.scala | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/test-bridge/src/main/scala/org/scalajs/testing/bridge/HTMLRunner.scala b/test-bridge/src/main/scala/org/scalajs/testing/bridge/HTMLRunner.scala index d2aab7f7dd..0cfd173253 100644 --- a/test-bridge/src/main/scala/org/scalajs/testing/bridge/HTMLRunner.scala +++ b/test-bridge/src/main/scala/org/scalajs/testing/bridge/HTMLRunner.scala @@ -55,8 +55,18 @@ protected[bridge] object HTMLRunner { } } - def start(tests: IsolatedTestSet): Unit = - dom.window.addEventListener("DOMContentLoaded", () => onLoad(tests)) + def start(tests: IsolatedTestSet): Unit = { + // See https://developer.mozilla.org/en-US/docs/Web/API/Document/DOMContentLoaded_event + if (dom.document.readyState == "loading") { + // Loading has not finished yet; register a DOMContentLoaded event + dom.window.addEventListener("DOMContentLoaded", () => onLoad(tests)) + } else { + // `DOMContentLoaded` has already fired; schedule `onLoad` on the next tick + Future { + onLoad(tests) + } + } + } private def onLoad(tests: IsolatedTestSet): Unit = { /* Note: Test filtering is currently done based on the fully qualified name @@ -457,6 +467,7 @@ protected[bridge] object HTMLRunner { @JSGlobal @js.native object document extends js.Object { + def readyState: String = js.native def body: Element = js.native def createElement(tag: String): Element = js.native def createTextNode(tag: String): Node = js.native From c1a4c0bcaf19c9e4c5a78dfa8cef115d3570f509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 30 May 2024 15:49:45 +0200 Subject: [PATCH 098/298] Remove dead code identifying Scala 2.11 trait impl forwarders. Since we do not support Scala 2.11 anymore, it does not make sense to try and identify shapes of "trait impl" forwarders anymore. Technically, the removed code was not dead code, since it could be triggered for methods that happen to delegate to a static method at the user code level. However, that is extremely rare, and even when found, there is virtually no chance that it could take part in multi-inlining. --- .../frontend/optimizer/OptimizerCore.scala | 19 ------------------- 1 file changed, 19 deletions(-) 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 7c484195b5..8cc5b8191b 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 @@ -2093,15 +2093,6 @@ private[optimizer] abstract class OptimizerCore( // TODO? Inline multiple non-forwarders with the exact same body? impls.forall(impl => impl.attributes.isForwarder && impl.attributes.inlineable) && (getMethodBody(impls.head).body.get match { - // Trait impl forwarder - case ApplyStatic(flags, staticCls, MethodIdent(methodName), _) => - impls.tail.forall(getMethodBody(_).body.get match { - case ApplyStatic(`flags`, `staticCls`, MethodIdent(`methodName`), _) => - true - case _ => - false - }) - // Shape of forwards to default methods case ApplyStatically(flags, This(), className, MethodIdent(methodName), args) => impls.tail.forall(getMethodBody(_).body.get match { @@ -6408,16 +6399,6 @@ private[optimizer] object OptimizerCore { val optimizerHints = methodDef.optimizerHints val isForwarder = body match { - // Shape of forwarders to trait impls - case ApplyStatic(_, impl, method, args) => - ((args.size == params.size + 1) && - (args.head.isInstanceOf[This]) && - (args.tail.zip(params).forall { - case (VarRef(LocalIdent(aname)), - ParamDef(LocalIdent(pname), _, _, _)) => aname == pname - case _ => false - })) - // Shape of forwards to default methods case ApplyStatically(_, This(), className, method, args) => args.size == params.size && From 90734e014533af23a2c3371e3914a56ea8b77fe3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Jun 2024 05:50:00 +0000 Subject: [PATCH 099/298] Bump ws from 7.5.9 to 7.5.10 Bumps [ws](https://github.com/websockets/ws) from 7.5.9 to 7.5.10. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/7.5.9...7.5.10) --- updated-dependencies: - dependency-name: ws dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 78045751fb..b7f175e7dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1422,9 +1422,9 @@ } }, "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, "engines": { "node": ">=8.3.0" @@ -2563,9 +2563,9 @@ "dev": true }, "ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, "requires": {} }, From aa84f641c653fbbfc1b14378329cdf89cf1c5564 Mon Sep 17 00:00:00 2001 From: Som Snytt Date: Sun, 23 Jun 2024 21:27:04 -0700 Subject: [PATCH 100/298] Fix name of pickler phase constraint --- .../src/main/scala/org/scalajs/nscplugin/ScalaJSPlugin.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/ScalaJSPlugin.scala b/compiler/src/main/scala/org/scalajs/nscplugin/ScalaJSPlugin.scala index 5273a84b4d..a67adbb948 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/ScalaJSPlugin.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/ScalaJSPlugin.scala @@ -77,7 +77,7 @@ class ScalaJSPlugin(val global: Global) extends NscPlugin { val jsAddons: ScalaJSPlugin.this.jsAddons.type = ScalaJSPlugin.this.jsAddons val scalaJSOpts = ScalaJSPlugin.this.scalaJSOpts override val runsAfter = List("typer") - override val runsBefore = List("pickle") + override val runsBefore = List("pickler") } object ExplicitInnerJSComponent extends ExplicitInnerJS[global.type](global) { From 6fd932263b5f1df0ba25c063ff545068a86cd7e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 31 May 2024 14:23:23 +0200 Subject: [PATCH 101/298] Make the optimizer type-preserving by inserting Casts when necessary. Previously, the optimizer generated IR that was ill-typed, in the sense that it would not pass the IR checker (even if it were generalized to understand intrinsics and record types). This happened when the receiver of a method call had a more generic type than an inlined method. That can be the case when only there is effectively only one possible target method for a virtual call, or when there are multiple targets but we can apply multi-inlining. Producing ill-typed IR is not a big deal for JavaScript, although we had to care about some details like proper unboxing of chars. In order to apply the optimizer to the Wasm backend, however, we need well-typed IR to be able to produce well-typed Wasm. In this commit, we fix the issue by introducing a new transient, `Cast`. It performs a true, unchecked cast, doing nothing but reassigning another type to an expression. It behaves like an unchecked `AsInstanceOf`, except it does not convert `null` to the zero of primitive types. When inlining, we now introduce casts (along with explicit null checks) to adapt the receiver to the target method. For single inlining, it is straightforward. For multi-inlining, choosing a correct target type, and generally producing well-typed IR, required to significantly change the logic. Previously, we picked an arbitrary target among the possible candidates and inlined it in the normal way. We cannot do this anymore, since the `This` type of any one possible target may not represent what is valid for all the targets. Instead, we now deeply inspect the shapes we know how to multi-inlining, and recreate calls on the fly for the method that they call. The explicit introduction of casts at the inlining level, along with explicit null checks, made the logic to refine receiver types in `withBindings` redundant. In order to completely get rid of it without any regression, we had to make `checkNotNull` more powerful to preserve the "it is now not-null" information even when `nullPointers` are unchecked. Interestingly, making the above changes retains more information even within the optimizer. That results in fewer useless temporary variables in the generated code. The changes are therefore beneficial even for the JavaScript backend. Finally, since we have proper casts, we can simplify `AsInstanceOf`s that never require unboxing into `Cast`s, when `asInstanceOfs` are unchecked. This changes almost nothing to the generated code, except replacing some `$uC(expr)` by `expr.c`. --- .../backend/emitter/FunctionEmitter.scala | 18 + .../linker/backend/emitter/Transients.scala | 32 ++ .../frontend/optimizer/OptimizerCore.scala | 375 ++++++++++++------ project/Build.scala | 10 +- 4 files changed, 305 insertions(+), 130 deletions(-) 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 58979155fc..871324daaf 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 @@ -1092,6 +1092,8 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case Transient(AssumeNotNull(obj)) => Transient(AssumeNotNull(rec(obj))) + case Transient(Cast(expr, tpe)) => + Transient(Cast(rec(expr), tpe)) case Transient(ZeroOf(runtimeClass)) => Transient(ZeroOf(rec(runtimeClass))) case Transient(ObjectClassName(obj)) => @@ -1290,6 +1292,8 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { // Transients preserving pureness (modulo NPE) case Transient(AssumeNotNull(obj)) => test(obj) + case Transient(Cast(expr, _)) => + test(expr) case Transient(ZeroOf(runtimeClass)) => test(runtimeClass) // ZeroOf *assumes* that `runtimeClass ne null` case Transient(ObjectClassName(obj)) => @@ -1831,6 +1835,11 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { redo(Transient(AssumeNotNull(newObj)))(env) } + case Transient(Cast(expr, tpe)) => + unnest(expr) { (newExpr, env) => + redo(Transient(Cast(newExpr, tpe)))(env) + } + case Transient(ZeroOf(runtimeClass)) => unnest(runtimeClass) { (newRuntimeClass, env) => redo(Transient(ZeroOf(newRuntimeClass)))(env) @@ -2745,6 +2754,13 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case Transient(AssumeNotNull(obj)) => transformExpr(obj, preserveChar = true) + case Transient(Cast(expr, tpe)) => + val newExpr = transformExpr(expr, preserveChar = true) + if (tpe == CharType && expr.tpe != CharType) + newExpr DOT cpn.c + else + newExpr + case Transient(ZeroOf(runtimeClass)) => js.DotSelect( genSelect(transformExprNoChar(checkNotNull(runtimeClass)), @@ -3197,6 +3213,8 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { def isShapeNotNull(tree: Tree): Boolean = tree match { case Transient(CheckNotNull(_) | AssumeNotNull(_)) => true + case Transient(Cast(expr, _)) => + isShapeNotNull(expr) case _: This => tree.tpe != AnyType case _:New | _:LoadModule | _:NewArray | _:ArrayValue | _:Clone | _:ClassOf => diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Transients.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Transients.scala index c5ee557807..f151112891 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Transients.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Transients.scala @@ -70,6 +70,38 @@ object Transients { } } + /** Casts `expr` to the given `tpe`, without any check. + * + * This operation is only valid if we know that `expr` is indeed a value of + * the given `tpe`. + * + * `Cast` behaves like an unchecked `AsInstanceOf`, except that it does not + * convert `null` to the zero of primitive types. Attempting to cast `null` + * to a primitive type (that is not `NullType`) is undefined behavior. + * + * `Cast` is not always a no-op. In some cases, a `Cast` may still have to + * be implemented using a conversion. For example, casting down from + * `jl.Character` to `char` requires to extract the primitive value from the + * box (although we know that the box is non-null, unlike with + * `AsInstanceOf`). + */ + final case class Cast(expr: Tree, val tpe: Type) extends Transient.Value { + def traverse(traverser: Traverser): Unit = + traverser.traverse(expr) + + def transform(transformer: Transformer, isStat: Boolean)( + implicit pos: Position): Tree = { + Transient(Cast(transformer.transformExpr(expr), tpe)) + } + + def printIR(out: IRTreePrinter): Unit = { + out.print(expr) + out.print(".as![") + out.print(tpe) + out.print("]") + } + } + /** Intrinsic for `System.arraycopy`. * * This node *assumes* that `src` and `dest` are non-null. It is the 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 8cc5b8191b..c7da1c1cb4 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 @@ -976,7 +976,7 @@ private[optimizer] abstract class OptimizerCore( } else if (baseTpe == NullType) { cont(checkNotNull(texpr)) } else if (isSubtype(baseTpe, JavaScriptExceptionClassType)) { - pretransformSelectCommon(AnyType, texpr, + pretransformSelectCommon(AnyType, texpr, optQualDeclaredType = None, FieldIdent(exceptionFieldName), isLhsOfAssign = false)(cont) } else { if (texpr.tpe.isExact || !isSubtype(JavaScriptExceptionClassType, baseTpe)) @@ -1182,13 +1182,14 @@ private[optimizer] abstract class OptimizerCore( implicit scope: Scope): TailRec[Tree] = { val Select(qualifier, field) = tree pretransformExpr(qualifier) { preTransQual => - pretransformSelectCommon(tree.tpe, preTransQual, field, isLhsOfAssign)( + pretransformSelectCommon(tree.tpe, preTransQual, optQualDeclaredType = None, field, isLhsOfAssign)( cont)(scope, tree.pos) } } private def pretransformSelectCommon(expectedType: Type, - preTransQual: PreTransform, field: FieldIdent, isLhsOfAssign: Boolean)( + preTransQual: PreTransform, optQualDeclaredType: Option[Type], + field: FieldIdent, isLhsOfAssign: Boolean)( cont: PreTransCont)( implicit scope: Scope, pos: Position): TailRec[Tree] = { /* Note: Callers are expected to have already removed writes to fields that @@ -1249,7 +1250,13 @@ private[optimizer] abstract class OptimizerCore( case PreTransTree(newQual, newQualType) => val newQual1 = maybeAssumeNotNull(newQual, newQualType) - cont(PreTransTree(Select(newQual1, field)(expectedType), + val newQual2 = optQualDeclaredType match { + case Some(qualDeclaredType) if !isSubtype(newQual1.tpe, qualDeclaredType) => + Transient(Cast(newQual1, qualDeclaredType)) + case _ => + newQual1 + } + cont(PreTransTree(Select(newQual2, field)(expectedType), RefinedType(expectedType))) } } @@ -1707,6 +1714,11 @@ private[optimizer] abstract class OptimizerCore( keepOnlySideEffects(expr) case UnwrapFromThrowable(expr) => checkNotNullStatement(expr)(stat.pos) + + // By definition, a failed cast is always UB, so it cannot have side effects + case Transient(Cast(expr, _)) => + keepOnlySideEffects(expr) + case _ => stat } @@ -1937,6 +1949,9 @@ private[optimizer] abstract class OptimizerCore( case AsInstanceOf(expr, tpe) => rec(expr).mapOrFailed(AsInstanceOf(_, tpe)) + case Transient(Cast(expr, tpe)) => + rec(expr).mapOrKeepGoing(newExpr => Transient(Cast(newExpr, tpe))) + case GetClass(expr) => rec(expr).mapOrKeepGoingIf(GetClass(_))(keepGoingIf = isNotNull(expr)) @@ -2063,24 +2078,16 @@ private[optimizer] abstract class OptimizerCore( } else { val allocationSites = (treceiver :: targs).map(_.tpe.allocationSite) - val shouldMultiInline = { + val shouldTryMultiInline = { impls.nonEmpty && // will fail at runtime. - !impls.exists(impl => scope.implsBeingInlined((allocationSites, impl))) && - canMultiInline(impls) + impls.forall(impl => impl.attributes.isForwarder && impl.attributes.inlineable) && + !impls.exists(impl => scope.implsBeingInlined((allocationSites, impl))) } - if (shouldMultiInline) { - /* When multi-inlining, we cannot use the enclosing class of the - * target method as the declared type of the receiver, since we - * have no guarantee that the receiver is in fact of that - * particular class. It could be of any of the classes that the - * targets belong to. Therefore, we have to keep the receiver's - * static type as a declared type, which is our only safe choice. - */ - val representative = impls.minBy(_.enclosingClassName) // for stability - val receiverType = treceiver.tpe.base - inline(allocationSites, Some((receiverType, treceiver)), targs, - representative, isStat, usePreTransform)(cont) + if (shouldTryMultiInline) { + tryMultiInline(impls, treceiver, targs, isStat, usePreTransform)(cont) { + treeNotInlined + } } else { treeNotInlined } @@ -2089,22 +2096,55 @@ private[optimizer] abstract class OptimizerCore( } } - private def canMultiInline(impls: List[MethodID]): Boolean = { - // TODO? Inline multiple non-forwarders with the exact same body? - impls.forall(impl => impl.attributes.isForwarder && impl.attributes.inlineable) && - (getMethodBody(impls.head).body.get match { - // Shape of forwards to default methods - case ApplyStatically(flags, This(), className, MethodIdent(methodName), args) => - impls.tail.forall(getMethodBody(_).body.get match { + private def tryMultiInline(impls: List[MethodID], treceiver: PreTransform, + targs: List[PreTransform], isStat: Boolean, usePreTransform: Boolean)( + cont: PreTransCont)( + treeNotInlined: => TailRec[Tree])( + implicit scope: Scope, pos: Position): TailRec[Tree] = { + + val referenceMethodDef = getMethodBody(impls.head) + + (referenceMethodDef.body.get match { + // Shape of forwarders to default methods + case body @ ApplyStatically(flags, This(), className, MethodIdent(methodName), args) => + val allTheSame = impls.tail.forall(getMethodBody(_).body.get match { case ApplyStatically(`flags`, This(), `className`, MethodIdent(`methodName`), _) => true case _ => false }) + if (!allTheSame) { + treeNotInlined + } else { + /* In this case, we can directly "splice in" the treceiver and targs + * into a made-up ApplyStatically, as evaluation order is always + * preserved. This potentially avoids useless bindings and keeps + * things simple. + * + * We only need to cast the receiver after checking that it is not null, + * since we need to pass it as the receiver of the `ApplyStatically`, + * which expects a known type. + */ + val treceiverCast = foldCast(checkNotNull(treceiver), ClassType(className)) + + val target = staticCall(className, + MemberNamespace.forNonStaticCall(flags), methodName) + + pretransformSingleDispatch(flags, target, Some(treceiverCast), targs, + isStat, usePreTransform)(cont) { + val newTree = ApplyStatically(flags, + finishTransformExprMaybeAssumeNotNull(treceiverCast), + className, MethodIdent(methodName), + targs.map(finishTransformExpr))( + body.tpe) + cont(newTree.toPreTransform) + } + } + // Bridge method - case Apply(flags, This(), MethodIdent(methodName), referenceArgs) => - impls.tail.forall(getMethodBody(_).body.get match { + case body @ Apply(flags, This(), MethodIdent(methodName), referenceArgs) => + val allTheSame = impls.tail.forall(getMethodBody(_).body.get match { case Apply(`flags`, This(), MethodIdent(`methodName`), implArgs) => referenceArgs.zip(implArgs) forall { case (MaybeUnbox(_, unboxID1), MaybeUnbox(_, unboxID2)) => @@ -2114,6 +2154,71 @@ private[optimizer] abstract class OptimizerCore( false }) + if (!allTheSame) { + treeNotInlined + } else { + /* Interestingly, for this shape of multi-inlining, we do not need to + * cast the receiver. We can keep it as is. Virtual dispatch and + * reachability analysis will be happy to compute the possible targets + * of the generated Apply given the type of our receiver. + * + * It's a good thing too, because it would be quite hard to figure out + * what type to cast the receiver to! + */ + + if (!referenceArgs.exists(_.isInstanceOf[AsInstanceOf])) { + // Common case where where we can splice in; evaluation order is preserved + pretransformApply(flags, treceiver, MethodIdent(methodName), targs, + body.tpe, isStat, usePreTransform)(cont) + } else { + /* If there is at least one unbox, we cannot splice in; we need to + * use actual bindings and resolve the actual Apply tree to apply + * the unboxes in the correct evaluation order. + */ + + /* Generate a new, fake body that we will inline. For + * type-preservation, the type of its `This()` node is the type of + * our receiver. For stability, the parameter names are normalized + * (taking them from `body` would make the result depend on which + * method came up first in the list of targets). + */ + val normalizedParams: List[(LocalName, Type)] = { + referenceMethodDef.args.zipWithIndex.map { + case (referenceParam, i) => (LocalName("x" + i), referenceParam.ptpe) + } + } + val normalizedBody = Apply( + flags, + This()(treceiver.tpe.base), + MethodIdent(methodName), + normalizedParams.zip(referenceArgs).map { + case ((name, ptpe), AsInstanceOf(_, castTpe)) => + AsInstanceOf(VarRef(LocalIdent(name))(ptpe), castTpe) + case ((name, ptpe), _) => + VarRef(LocalIdent(name))(ptpe) + } + )(body.tpe) + + // Construct bindings; need to check null for the receiver to preserve evaluation order + val receiverBinding = + Binding(Binding.This, treceiver.tpe.base, mutable = false, checkNotNull(treceiver)) + val argsBindings = normalizedParams.zip(targs).map { + case ((name, ptpe), targ) => + Binding(Binding.Local(name, NoOriginalName), ptpe, mutable = false, targ) + } + + withBindings(receiverBinding :: argsBindings) { (bodyScope, cont1) => + implicit val scope = bodyScope + if (usePreTransform) { + assert(!isStat, "Cannot use pretransform in statement position") + pretransformExpr(normalizedBody)(cont1) + } else { + cont1(PreTransTree(transform(normalizedBody, isStat))) + } + } (cont) (scope.withEnv(OptEnv.Empty)) + } + } + case body => throw new AssertionError("Invalid forwarder shape: " + body) }) @@ -2546,13 +2651,14 @@ private[optimizer] abstract class OptimizerCore( case This() if args.isEmpty => assert(optReceiver.isDefined, "There was a This(), there should be a receiver") - cont(checkNotNull(optReceiver.get._2)) + cont(foldCast(checkNotNull(optReceiver.get._2), optReceiver.get._1)) case Select(This(), field) if formals.isEmpty => assert(optReceiver.isDefined, "There was a This(), there should be a receiver") - pretransformSelectCommon(body.tpe, optReceiver.get._2, field, - isLhsOfAssign = false)(cont) + pretransformSelectCommon(body.tpe, optReceiver.get._2, + optQualDeclaredType = Some(optReceiver.get._1), + field, isLhsOfAssign = false)(cont) case Assign(lhs @ Select(This(), field), VarRef(LocalIdent(rhsName))) if formals.size == 1 && formals.head.name.name == rhsName => @@ -2567,8 +2673,9 @@ private[optimizer] abstract class OptimizerCore( // Field is never read, discard assign, keep side effects only. cont(PreTransTree(finishTransformArgsAsStat(), RefinedType.NoRefinedType)) } else { - pretransformSelectCommon(lhs.tpe, treceiver, field, - isLhsOfAssign = true) { tlhs => + pretransformSelectCommon(lhs.tpe, treceiver, + optQualDeclaredType = Some(optReceiver.get._1), + field, isLhsOfAssign = true) { tlhs => pretransformAssign(tlhs, args.head)(cont) } } @@ -2588,17 +2695,14 @@ private[optimizer] abstract class OptimizerCore( implicit scope: Scope, pos: Position): TailRec[Tree] = tailcall { val optReceiverBinding = optReceiver map { receiver => - /* If the declaredType is CharType, we must introduce a cast, because we - * must communicate to the emitter that it has to unbox the value. - * For other primitive types, unboxes/casts are not necessary, because - * they would only convert `null` to the zero value of the type. However, - * `null` is ruled out by `checkNotNull` (or because it is UB). + /* Introduce an explicit null check that would be part of the semantics + * of `Apply` or `ApplyStatically`. + * Then, cast the non-null receiver to the expected receiver type. This + * may be required because we found a single potential call target in a + * subclass of the static type of the receiver. */ val (declaredType, value0) = receiver - val value1 = checkNotNull(value0) - val value = - if (declaredType == CharType) foldAsInstanceOf(value1, declaredType) - else value1 + val value = foldCast(checkNotNull(value0), declaredType) Binding(Binding.This, declaredType, false, value) } @@ -4746,12 +4850,52 @@ private[optimizer] abstract class OptimizerCore( private def foldAsInstanceOf(arg: PreTransform, tpe: Type)( implicit pos: Position): PreTransform = { - if (isSubtype(arg.tpe.base, tpe)) + def mayRequireUnboxing: Boolean = + arg.tpe.isNullable && !isNullableType(tpe) + + if (semantics.asInstanceOfs == CheckedBehavior.Unchecked && !mayRequireUnboxing) + foldCast(arg, tpe) + else if (isSubtype(arg.tpe.base, tpe)) arg else AsInstanceOf(finishTransformExpr(arg), tpe).toPreTransform } + private def foldCast(arg: PreTransform, tpe: Type)( + implicit pos: Position): PreTransform = { + def default(arg: PreTransform, newTpe: RefinedType): PreTransform = + PreTransTree(Transient(Cast(finishTransformExpr(arg), tpe)), newTpe) + + def castLocalDef(arg: PreTransform, newTpe: RefinedType): PreTransform = arg match { + case PreTransMaybeBlock(bindingsAndStats, PreTransLocalDef(localDef)) => + val refinedLocalDef = localDef.tryWithRefinedType(newTpe) + if (refinedLocalDef ne localDef) + PreTransBlock(bindingsAndStats, PreTransLocalDef(refinedLocalDef)) + else + default(arg, newTpe) + + case _ => + default(arg, newTpe) + } + + if (isSubtype(arg.tpe.base, tpe)) { + arg + } else { + val castTpe = RefinedType(tpe, isExact = false, + isNullable = arg.tpe.isNullable && isNullableType(tpe), + arg.tpe.allocationSite) + + val isCastFreeAtRunTime = tpe != CharType + + if (isCastFreeAtRunTime) { + // Try to push the cast down to usages of LocalDefs, in order to preserve aliases + castLocalDef(arg, castTpe) + } else { + default(arg, castTpe) + } + } + } + private def foldJSSelect(qualifier: Tree, item: Tree)( implicit pos: Position): Tree = { // !!! Must be in sync with scala.scalajs.runtime.LinkingInfo @@ -4973,16 +5117,31 @@ private[optimizer] abstract class OptimizerCore( } private def checkNotNull(texpr: PreTransform)(implicit pos: Position): PreTransform = { - val tpe = texpr.tpe - - if (!tpe.isNullable || semantics.nullPointers == CheckedBehavior.Unchecked) { + if (!texpr.tpe.isNullable) { texpr - } else { - val refinedType: RefinedType = tpe.base match { - case NullType => RefinedType.Nothing - case baseType => RefinedType(baseType, isExact = tpe.isExact, isNullable = false) + } else if (semantics.nullPointers == CheckedBehavior.Unchecked) { + // If possible, improve the type of the expression to be non-nullable + + val nonNullType = texpr.tpe.toNonNullable + + def rec(texpr: PreTransform): PreTransform = texpr match { + case PreTransBlock(bindingsAndStats, result) => + PreTransBlock(bindingsAndStats, rec(result).asInstanceOf[PreTransResult]) + case PreTransLocalDef(localDef) => + PreTransLocalDef(localDef.tryWithRefinedType(nonNullType))(texpr.pos) + case PreTransTree(tree, tpe) => + PreTransTree(tree, nonNullType) + case _:PreTransUnaryOp | _:PreTransBinaryOp | _:PreTransJSBinaryOp | _:PreTransRecordTree => + // We cannot improve the type of those + texpr } - PreTransTree(Transient(CheckNotNull(finishTransformExpr(texpr))), refinedType) + + if (nonNullType.isNothingType) + texpr // things blow up otherwise + else + rec(texpr) + } else { + PreTransTree(Transient(CheckNotNull(finishTransformExpr(texpr))), texpr.tpe.toNonNullable) } } @@ -5023,18 +5182,20 @@ private[optimizer] abstract class OptimizerCore( } } + private def isNullableType(tpe: Type): Boolean = tpe match { + case NullType => true + case _: PrimType => false + case _ => true + } + private def isNotNull(tree: Tree): Boolean = { // !!! Duplicate code with FunctionEmitter.isNotNull - def isNullableType(tpe: Type): Boolean = tpe match { - case NullType => true - case _: PrimType => false - case _ => true - } - def isShapeNotNull(tree: Tree): Boolean = tree match { case Transient(CheckNotNull(_) | AssumeNotNull(_)) => true + case Transient(Cast(expr, _)) => + isShapeNotNull(expr) case _: This => tree.tpe != AnyType case _:New | _:LoadModule | _:NewArray | _:ArrayValue | _:Clone | _:ClassOf => @@ -5178,48 +5339,6 @@ private[optimizer] abstract class OptimizerCore( } else if (mutable) { withDedicatedVar(RefinedType(declaredType)) } else { - def computeRefinedType(): RefinedType = bindingName match { - case _ if value.tpe.isExact || declaredType == AnyType => - /* If the value's type is exact, or if the declared type is `AnyType`, - * the declared type cannot have any new information to give us, so - * we directly return `value.tpe`. This avoids a useless `isSubtype` - * call, which creates dependencies for incremental optimization. - * - * In addition, for the case `declaredType == AnyType` there is a - * stronger reason: we don't actually know that `this` is non-null in - * that case, since it could be the `this` value of a JavaScript - * function, which can accept `null`. (As of this writing, this is - * theoretical, because the only place where we use a declared type - * of `AnyType` is in `JSFunctionApply`, where the actual value for - * `this` is always `undefined`.) - */ - value.tpe - - case _: Binding.Local => - /* When binding a something else than `this`, we do not receive the - * non-null information. Moreover, there is no situation where the - * declared type would bring any new information, since that would - * not be valid IR in the first place. Therefore, to avoid a useless - * call to `isSubtype`, we directly return `value.tpe`. - */ - value.tpe - - case Binding.This => - /* When binding to `this`, if the declared type is not `AnyType`, - * we are in a situation where - * a) we know the value must be non-null, and - * b) the declaredType may bring more precise information than - * value.tpe.base (typically when inlining a polymorphic method - * that ends up having only one target in a subclass). - * We can refine the type here based on that knowledge. - */ - val improvedBaseType = - if (isSubtype(value.tpe.base, declaredType)) value.tpe.base - else declaredType - val isExact = false // We catch the case value.tpe.isExact earlier - RefinedType(improvedBaseType, isExact, isNullable = false) - } - value match { case PreTransBlock(bindingsAndStats, result) => withNewLocalDef(binding.copy(value = result))(buildInner) { tresult => @@ -5230,41 +5349,19 @@ private[optimizer] abstract class OptimizerCore( /* Attention: the same-name optimization in transformCapturingBody * relies on immutable bindings to var refs not being renamed. */ - - val refinedType = computeRefinedType() - val newLocalDef = if (refinedType == value.tpe) { - localDef - } else { - /* Only adjust if the replacement if ReplaceWithThis or - * ReplaceWithVarRef, because other types have nothing to gain - * (e.g., ReplaceWithConstant) or we want to keep them unwrapped - * because they are examined in optimizations (notably all the - * types with virtualized objects). - */ - localDef.replacement match { - case _:ReplaceWithThis | _:ReplaceWithVarRef => - LocalDef(refinedType, mutable = false, - ReplaceWithOtherLocalDef(localDef)) - case _ => - localDef - } - } - buildInner(newLocalDef, cont) + buildInner(localDef, cont) case PreTransTree(literal: Literal, _) => - /* A `Literal` always has the most precise type it could ever have. - * There is no point using `computeRefinedType()`. - */ buildInner(LocalDef(value.tpe, false, ReplaceWithConstant(literal)), cont) case PreTransTree(VarRef(LocalIdent(refName)), _) if !localIsMutable(refName) => - buildInner(LocalDef(computeRefinedType(), false, + buildInner(LocalDef(value.tpe, false, ReplaceWithVarRef(refName, newSimpleState(UsedAtLeastOnce), None)), cont) case _ => - withDedicatedVar(computeRefinedType()) + withDedicatedVar(value.tpe) } } } @@ -5535,6 +5632,12 @@ private[optimizer] object OptimizerCore { isNullable: Boolean)(val allocationSite: AllocationSite, dummy: Int = 0) { def isNothingType: Boolean = base == NothingType + + def toNonNullable: RefinedType = { + if (!isNullable) this + else if (base == NullType) RefinedType.Nothing + else RefinedType(base, isExact, isNullable = false, allocationSite) + } } private object RefinedType { @@ -5643,7 +5746,11 @@ private[optimizer] object OptimizerCore { * notably because it allows us to run the ClassDefChecker after the * optimizer. */ - localDef.newReplacement + val underlying = localDef.newReplacement + if (underlying.tpe == tpe.base) + underlying + else + Transient(Cast(underlying, tpe.base)) case ReplaceWithConstant(value) => value @@ -5690,6 +5797,23 @@ private[optimizer] object OptimizerCore { false }) } + + def tryWithRefinedType(refinedType: RefinedType): LocalDef = { + /* Only adjust if the replacement if ReplaceWithThis or + * ReplaceWithVarRef, because other types have nothing to gain + * (e.g., ReplaceWithConstant) or we want to keep them unwrapped + * because they are examined in optimizations (notably all the + * types with virtualized objects). + */ + replacement match { + case _:ReplaceWithThis | _:ReplaceWithVarRef => + LocalDef(refinedType, mutable, ReplaceWithOtherLocalDef(this)) + case replacement: ReplaceWithOtherLocalDef => + LocalDef(refinedType, mutable, replacement) + case _ => + this + } + } } private sealed abstract class LocalDefReplacement @@ -6411,6 +6535,7 @@ private[optimizer] object OptimizerCore { // Shape of bridges for generic methods case Apply(_, This(), method, args) => + !method.name.isReflectiveProxy && (args.size == params.size) && args.zip(params).forall { case (MaybeUnbox(VarRef(LocalIdent(aname)), _), diff --git a/project/Build.scala b/project/Build.scala index 0d93e05ec7..dd86f57340 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2021,15 +2021,15 @@ object Build { case `default212Version` => if (!useMinifySizes) { Some(ExpectedSizes( - fastLink = 626000 to 627000, + fastLink = 625000 to 626000, fullLink = 97000 to 98000, fastLinkGz = 75000 to 79000, fullLinkGz = 25000 to 26000, )) } else { Some(ExpectedSizes( - fastLink = 433000 to 434000, - fullLink = 288000 to 289000, + fastLink = 432000 to 433000, + fullLink = 283000 to 284000, fastLinkGz = 62000 to 63000, fullLinkGz = 44000 to 45000, )) @@ -2039,14 +2039,14 @@ object Build { if (!useMinifySizes) { Some(ExpectedSizes( fastLink = 451000 to 452000, - fullLink = 94000 to 95000, + fullLink = 95000 to 96000, fastLinkGz = 58000 to 59000, fullLinkGz = 25000 to 26000, )) } else { Some(ExpectedSizes( fastLink = 308000 to 309000, - fullLink = 265000 to 266000, + fullLink = 263000 to 264000, fastLinkGz = 49000 to 50000, fullLinkGz = 43000 to 44000, )) From 018e5fc4fce8578faf180ed2145a99327870fa60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 23 Jul 2024 11:11:18 +0200 Subject: [PATCH 102/298] Fix #5005: Only emit the int-div helpers if ArithmeticException is instantiated. --- Jenkinsfile | 3 + .../linker/backend/emitter/CoreJSLib.scala | 34 ++++++---- .../backend/emitter/GlobalKnowledge.scala | 3 + .../backend/emitter/KnowledgeGuardian.scala | 65 ++++++++++++++----- 4 files changed, 77 insertions(+), 28 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 9ca49c5e3c..487f96ba53 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -125,6 +125,9 @@ def Tasks = [ ++$scala helloworld$v/run && sbtretry 'set scalaJSLinkerConfig in helloworld.v$v ~= (_.withSemantics(_.withAsInstanceOfs(CheckedBehavior.Unchecked)))' \ ++$scala helloworld$v/run && + sbtretry ++$scala \ + 'set scalaJSLinkerConfig in helloworld.v$v ~= (_.withESFeatures(_.withAllowBigIntsForLongs(true)))' \ + helloworld$v/run && sbtretry ++$scala \ 'set scalaJSLinkerConfig in helloworld.v$v ~= (_.withModuleKind(ModuleKind.CommonJSModule))' \ helloworld$v/run && 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 42862541ae..05ade75ba0 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 @@ -909,7 +909,10 @@ private[emitter] object CoreJSLib { } private def defineArithmeticOps(): List[Tree] = { - val throwDivByZero = { + val isArithmeticExceptionClassInstantiated = + globalKnowledge.isArithmeticExceptionClassInstantiated + + def throwDivByZero: Tree = { Throw(genScalaClassNew(ArithmeticExceptionClass, StringArgConstructorName, str("/ by zero"))) } @@ -917,16 +920,19 @@ private[emitter] object CoreJSLib { def wrapBigInt64(tree: Tree): Tree = Apply(genIdentBracketSelect(BigIntRef, "asIntN"), 64 :: tree :: Nil) - defineFunction2(VarField.intDiv) { (x, y) => - If(y === 0, throwDivByZero, { - Return((x / y) | 0) - }) - } ::: - defineFunction2(VarField.intMod) { (x, y) => - If(y === 0, throwDivByZero, { - Return((x % y) | 0) - }) - } ::: + condDefs(isArithmeticExceptionClassInstantiated)( + defineFunction2(VarField.intDiv) { (x, y) => + If(y === 0, throwDivByZero, { + Return((x / y) | 0) + }) + } ::: + defineFunction2(VarField.intMod) { (x, y) => + If(y === 0, throwDivByZero, { + Return((x % y) | 0) + }) + } ::: + Nil + ) ::: defineFunction1(VarField.doubleToInt) { x => Return(If(x > 2147483647, 2147483647, If(x < -2147483648, -2147483648, x | 0))) } ::: @@ -948,7 +954,7 @@ private[emitter] object CoreJSLib { ) } ) ::: - condDefs(allowBigIntsForLongs)( + condDefs(allowBigIntsForLongs && isArithmeticExceptionClassInstantiated)( defineFunction2(VarField.longDiv) { (x, y) => If(y === bigInt(0), throwDivByZero, { Return(wrapBigInt64(x / y)) @@ -959,7 +965,9 @@ private[emitter] object CoreJSLib { Return(wrapBigInt64(x % y)) }) } ::: - + Nil + ) ::: + condDefs(allowBigIntsForLongs)( defineFunction1(VarField.doubleToLong)(x => Return { If(x < double(-9223372036854775808.0), { // -2^63 bigInt(-9223372036854775808L) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/GlobalKnowledge.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/GlobalKnowledge.scala index 84592f0ec1..b8fec3f3ff 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/GlobalKnowledge.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/GlobalKnowledge.scala @@ -22,6 +22,9 @@ private[emitter] trait GlobalKnowledge { /** Tests whether the `java.lang.Class` class is instantiated. */ def isClassClassInstantiated: Boolean + /** Tests whether the `java.lang.ArithmeticException` class is instantiated. */ + def isArithmeticExceptionClassInstantiated: Boolean + /** Tests whether the parent class data is accessed in the linking unit. */ def isParentDataAccessed: Boolean 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 1e9f26e285..562ad1e519 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 @@ -48,6 +48,7 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { // Object is optional, because the module splitter might remove everything. var objectClass: Option[LinkedClass] = None var classClass: Option[LinkedClass] = None + var arithmeticExceptionClass: Option[LinkedClass] = None val hijackedClasses = Iterable.newBuilder[LinkedClass] // Update classes @@ -81,6 +82,9 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { case ObjectClass => objectClass = Some(linkedClass) + case ArithmeticExceptionClass => + arithmeticExceptionClass = Some(linkedClass) + case name if HijackedClasses(name) => hijackedClasses += linkedClass @@ -93,10 +97,12 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { val invalidateAll = { if (specialInfo == null) { - specialInfo = new SpecialInfo(objectClass, classClass, hijackedClasses.result()) + specialInfo = new SpecialInfo(objectClass, classClass, + arithmeticExceptionClass, hijackedClasses.result()) false } else { - specialInfo.update(objectClass, classClass, hijackedClasses.result()) + specialInfo.update(objectClass, classClass, arithmeticExceptionClass, + hijackedClasses.result()) } } @@ -175,6 +181,9 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { def isClassClassInstantiated: Boolean = specialInfo.askIsClassClassInstantiated(this) + def isArithmeticExceptionClassInstantiated: Boolean = + specialInfo.askIsArithmeticExceptionClassInstantiated(this) + def isInterface(className: ClassName): Boolean = classes(className).askIsInterface(this) @@ -502,10 +511,13 @@ 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 { - private var isClassClassInstantiated = - computeIsClassClassInstantiated(initClassClass) + import SpecialInfo._ + + private var instantiatedSpecialClassBitSet = + computeInstantiatedSpecialClassBitSet(initClassClass, initArithmeticExceptionClass) private var isParentDataAccessed = computeIsParentDataAccessed(initClassClass) @@ -519,18 +531,22 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { private var hijackedDescendants = computeHijackedDescendants(initHijackedClasses) - private val isClassClassInstantiatedAskers = mutable.Set.empty[Invalidatable] + // Askers of isXClassInstantiated -- merged for all X because in practice that's only the CoreJSLib + private val instantiatedSpecialClassAskers = mutable.Set.empty[Invalidatable] + private val methodsInRepresentativeClassesAskers = mutable.Set.empty[Invalidatable] private val methodsInObjectAskers = mutable.Set.empty[Invalidatable] def update(objectClass: Option[LinkedClass], classClass: Option[LinkedClass], + arithmeticExceptionClass: Option[LinkedClass], hijackedClasses: Iterable[LinkedClass]): Boolean = { var invalidateAll = false - val newIsClassClassInstantiated = computeIsClassClassInstantiated(classClass) - if (newIsClassClassInstantiated != isClassClassInstantiated) { - isClassClassInstantiated = newIsClassClassInstantiated - invalidateAskers(isClassClassInstantiatedAskers) + val newInstantiatedSpecialClassBitSet = + computeInstantiatedSpecialClassBitSet(classClass, arithmeticExceptionClass) + if (newInstantiatedSpecialClassBitSet != instantiatedSpecialClassBitSet) { + instantiatedSpecialClassBitSet = newInstantiatedSpecialClassBitSet + invalidateAskers(instantiatedSpecialClassAskers) } val newIsParentDataAccessed = computeIsParentDataAccessed(classClass) @@ -562,8 +578,16 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { invalidateAll } - private def computeIsClassClassInstantiated(classClass: Option[LinkedClass]): Boolean = - classClass.exists(_.hasInstances) + private def computeInstantiatedSpecialClassBitSet( + classClass: Option[LinkedClass], + arithmeticExceptionClass: Option[LinkedClass]): Int = { + var bitSet: Int = 0 + if (classClass.exists(_.hasInstances)) + bitSet |= SpecialClassClass + if (arithmeticExceptionClass.exists(_.hasInstances)) + bitSet |= SpecialClassArithmeticException + bitSet + } private def computeIsParentDataAccessed(classClass: Option[LinkedClass]): Boolean = { def methodExists(linkedClass: LinkedClass, methodName: MethodName): Boolean = { @@ -619,8 +643,14 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { def askIsClassClassInstantiated(invalidatable: Invalidatable): Boolean = { invalidatable.registeredTo(this) - isClassClassInstantiatedAskers += invalidatable - isClassClassInstantiated + instantiatedSpecialClassAskers += invalidatable + (instantiatedSpecialClassBitSet & SpecialClassClass) != 0 + } + + def askIsArithmeticExceptionClassInstantiated(invalidatable: Invalidatable): Boolean = { + invalidatable.registeredTo(this) + instantiatedSpecialClassAskers += invalidatable + (instantiatedSpecialClassBitSet & SpecialClassArithmeticException) != 0 } def askIsParentDataAccessed(invalidatable: Invalidatable): Boolean = @@ -645,19 +675,24 @@ private[emitter] final class KnowledgeGuardian(config: Emitter.Config) { } def unregister(invalidatable: Invalidatable): Unit = { - isClassClassInstantiatedAskers -= invalidatable + instantiatedSpecialClassAskers -= invalidatable methodsInRepresentativeClassesAskers -= invalidatable methodsInObjectAskers -= invalidatable } /** Call this when we invalidate all caches. */ def unregisterAll(): Unit = { - isClassClassInstantiatedAskers.clear() + instantiatedSpecialClassAskers.clear() methodsInRepresentativeClassesAskers.clear() methodsInObjectAskers.clear() } } + private object SpecialInfo { + private final val SpecialClassClass = 1 << 0 + private final val SpecialClassArithmeticException = 1 << 1 + } + private def invalidateAskers(askers: mutable.Set[Invalidatable]): Unit = { /* Calling `invalidate` cause the `Invalidatable` to call `unregister()` in * this class, which will mutate the `askers` set. Therefore, we cannot From 2e8919f11ff030f472efaf08139026bfeaf4dc0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 25 Jul 2024 14:35:32 +0200 Subject: [PATCH 103/298] Remove the case "exact jl.Object" when trying to optimize `eq` as `===`. When performing `x eq y`, the optimizer tries to optimize it as `x === y`. It can do so if it can prove that one of the operands can never be a primitive number. One of the cases that we identified was when it was an *exact* `jl.Object`, typically if it was the result of a `new Object()`. We now remove that specific case. It was only useful for the reference object wrapped in a `NonLocalReturnControl` exception by the codegen for non-local `return`s. For that code, performance is dominated by the throwing and catching of the exception, and using `Object.is` instead of `===` should be inconsequential. Removing that case means that the optimization now only relies on IR `Type`s, rather than the optimizer's `RefinedType`. That will allow us to move the optimization to the JS backend in the following commit. --- .../scalajs/linker/frontend/optimizer/OptimizerCore.scala | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) 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 c7da1c1cb4..3feff5ca2e 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 @@ -3857,13 +3857,7 @@ private[optimizer] abstract class OptimizerCore( case AnyType | ByteType | ShortType | IntType | FloatType | DoubleType => true case ClassType(className) => - /* If `className` is a concrete superclass of a boxed number class, - * then it can be exact, and in that case we know that it cannot be - * a primitive number. In practice this happens only for - * `java.lang.Object`, and especially for code generated for - * non-local returns in Scala. - */ - !tpe.isExact && MaybeHijackedPrimNumberClasses.contains(className) + MaybeHijackedPrimNumberClasses.contains(className) case _ => false } From 03cf1f8396743fec4cad780f90381c4d445bdbcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 25 Jul 2024 15:39:47 +0200 Subject: [PATCH 104/298] Move the `eq`-to-`===` optimization to the JS backend. This simplifies a number of things. In particular, all the infrastructure for intelligently dealing with `JSBinaryOp`s disappears from the optimizer. Since the optimization would be detrimental to Wasm anyway, it makes more sense to move it to the JS backend. --- .../backend/emitter/FunctionEmitter.scala | 59 +++++--- .../frontend/optimizer/OptimizerCore.scala | 142 +++--------------- 2 files changed, 60 insertions(+), 141 deletions(-) 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 871324daaf..343e01605e 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 @@ -2405,23 +2405,23 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { (op: @switch) match { case === | !== => - /** Tests whether an operand receives exemption from the - * `Object.is` treatment. + /* Semantically, this is an `Object.is` test in JS. However, we + * optimize it as a primitive JS strict equality (`===`) when + * possible. * - * If either operand receives an exemption, we use `===` - * instead. + * The optimizer does not do this optimization because: + * + * - it partly relies on the specifics of how hijacked classes are encoded in JS + * (see the `ClassType(ObjectClass)` case in `canBePrimitiveNum`), + * - if handled in the optimizer, `JSBinaryOp`s become frequent + * and require additional infrastructure to optimize common patterns, and + * - it is very specific to *JavaScript*, and is actually detrimental in Wasm. */ - def receivesExemption(tree: js.Tree): Boolean = tree match { - case _:js.Undefined | _:js.Null => - /* An `undefined` operand happens a lot for default - * parameters in exported methods. Because exported methods - * are not optimized, they survive until here. - * - * A `null` operand often happens in the constructor of inner - * JS classes, which are not optimized either. - */ + + def canBePrimitiveNum(tree: Tree): Boolean = tree.tpe match { + case AnyType | ByteType | ShortType | IntType | FloatType | DoubleType => true - case _:js.This => + case ClassType(ObjectClass) => /* Due to how hijacked classes are encoded in JS, we know * that in `java.lang.Object` itself, `this` can never be a * primitive. It will always be a proper Scala.js object. @@ -2429,16 +2429,35 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { * Exempting `this` in `java.lang.Object` is important so * that the body of `Object.equals__O__Z` can be compiled as * `this === that` instead of `Object.is(this, that)`. - * - * This is something that the optimizer is not supposed to be - * able to do, since it doesn't know how hijacked classes are - * encoded. */ - env.enclosingClassName.exists(_ == ObjectClass) + !tree.isInstanceOf[This] + case ClassType(BoxedByteClass | BoxedShortClass | + BoxedIntegerClass | BoxedFloatClass | BoxedDoubleClass) => + true + case ClassType(className) => + globalKnowledge.isAncestorOfHijackedClass(BoxedDoubleClass) + case _ => + false + } + + def isWhole(tree: Tree): Boolean = tree.tpe match { + case ByteType | ShortType | IntType => + true + case ClassType(className) => + className == BoxedByteClass || + className == BoxedShortClass || + className == BoxedIntegerClass case _ => false } - if (receivesExemption(newLhs) || receivesExemption(newRhs)) { + + val canOptimizeAsJSStrictEq = { + !canBePrimitiveNum(lhs) || + !canBePrimitiveNum(rhs) || + (isWhole(lhs) && isWhole(rhs)) + } + + if (canOptimizeAsJSStrictEq) { js.BinaryOp(if (op == ===) JSBinaryOp.=== else JSBinaryOp.!==, newLhs, newRhs) } else { 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 3feff5ca2e..6d973cde36 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 @@ -527,7 +527,7 @@ private[optimizer] abstract class OptimizerCore( val result = { if (isSubtype(texpr.tpe.base, testType)) { if (texpr.tpe.isNullable) - JSBinaryOp(JSBinaryOp.!==, finishTransformExpr(texpr), Null()) + BinaryOp(BinaryOp.!==, finishTransformExpr(texpr), Null()) else Block(finishTransformStat(texpr), BooleanLiteral(true)) } else { @@ -1381,7 +1381,7 @@ private[optimizer] abstract class OptimizerCore( PreTransTree(finishTransformBindings(bindingsAndStats, tree), tpe) } - case _:PreTransUnaryOp | _:PreTransBinaryOp | _:PreTransJSBinaryOp => + case _:PreTransUnaryOp | _:PreTransBinaryOp => PreTransTree(finishTransformExpr(preTrans), preTrans.tpe) case PreTransLocalDef(localDef @ LocalDef(tpe, _, replacement)) => @@ -1424,7 +1424,7 @@ private[optimizer] abstract class OptimizerCore( case PreTransBlock(_, result) => resolveRecordType(result) - case _:PreTransUnaryOp | _:PreTransBinaryOp | _:PreTransJSBinaryOp => + case _:PreTransUnaryOp | _:PreTransBinaryOp => None case PreTransLocalDef(localDef @ LocalDef(tpe, _, replacement)) => @@ -1486,8 +1486,6 @@ private[optimizer] abstract class OptimizerCore( UnaryOp(op, finishTransformExpr(lhs)) case PreTransBinaryOp(op, lhs, rhs) => BinaryOp(op, finishTransformExpr(lhs), finishTransformExpr(rhs)) - case PreTransJSBinaryOp(op, lhs, rhs) => - JSBinaryOp(op, finishTransformExpr(lhs), finishTransformExpr(rhs)) case PreTransLocalDef(localDef) => localDef.newReplacement @@ -1568,11 +1566,6 @@ private[optimizer] abstract class OptimizerCore( finishNoSideEffects } - case PreTransJSBinaryOp(op, lhs, rhs) => - if (op == JSBinaryOp.=== || op == JSBinaryOp.!==) - Block(finishTransformStat(lhs), finishTransformStat(rhs))(stat.pos) - else // other operators can have side effects that we must preserve - finishTransformExpr(stat) case PreTransLocalDef(_) => Skip()(stat.pos) case PreTransRecordTree(tree, _, _) => @@ -3297,9 +3290,9 @@ private[optimizer] abstract class OptimizerCore( * the equals() method has been inlined as a reference * equality test. */ - case (JSBinaryOp(JSBinaryOp.===, VarRef(lhsIdent), Null()), - JSBinaryOp(JSBinaryOp.===, VarRef(rhsIdent), Null()), - JSBinaryOp(JSBinaryOp.===, VarRef(lhsIdent2), VarRef(rhsIdent2))) + case (BinaryOp(BinaryOp.===, VarRef(lhsIdent), Null()), + BinaryOp(BinaryOp.===, VarRef(rhsIdent), Null()), + BinaryOp(BinaryOp.===, VarRef(lhsIdent2), VarRef(rhsIdent2))) if lhsIdent2 == lhsIdent && rhsIdent2 == rhsIdent => elsep @@ -3546,16 +3539,6 @@ private[optimizer] abstract class OptimizerCore( if (newOp == -1) default else PreTransBinaryOp(newOp, l, r) - case PreTransJSBinaryOp(innerOp, l, r) => - val newOp = innerOp match { - case JSBinaryOp.=== => JSBinaryOp.!== - case JSBinaryOp.!== => JSBinaryOp.=== - - case _ => -1 - } - if (newOp == -1) default - else PreTransJSBinaryOp(newOp, l, r) - case _ => default } @@ -3722,17 +3705,15 @@ private[optimizer] abstract class OptimizerCore( * The result is always known statically. * * Bytes, Shorts, Ints, Floats and Doubles all live in the same "space" for - * `===` comparison, since they all upcast as primitive numbers. If - * `isJSStrictEq` is false, they are compared with `equals()` instead of - * `==` so that `NaN === NaN` and `+0.0 !== -0.0`. + * `===` comparison, since they all upcast as primitive numbers. They are + * compared with `equals()` instead of `==` so that `NaN === NaN` and + * `+0.0 !== -0.0`. * * Chars and Longs, however, never compare as `===`, since they are boxed * chars and instances of `RuntimeLong`, respectively---unless we are using * `BigInt`s for `Long`s, in which case those can be `===`. */ - private def literal_===(lhs: Literal, rhs: Literal, - isJSStrictEq: Boolean): Boolean = { - + private def literal_===(lhs: Literal, rhs: Literal): Boolean = { object AnyNumLiteral { def unapply(tree: Literal): Option[Double] = tree match { case ByteLiteral(v) => Some(v.toDouble) @@ -3748,7 +3729,7 @@ private[optimizer] abstract class OptimizerCore( case (BooleanLiteral(l), BooleanLiteral(r)) => l == r case (StringLiteral(l), StringLiteral(r)) => l == r case (ClassOf(l), ClassOf(r)) => l == r - case (AnyNumLiteral(l), AnyNumLiteral(r)) => if (isJSStrictEq) l == r else l.equals(r) + case (AnyNumLiteral(l), AnyNumLiteral(r)) => l.equals(r) case (LongLiteral(l), LongLiteral(r)) => l == r && !useRuntimeLong case (Undefined(), Undefined()) => true case (Null(), Null()) => true @@ -3818,16 +3799,6 @@ private[optimizer] abstract class OptimizerCore( } } - private val MaybeHijackedPrimNumberClasses = { - /* In theory, we could figure out the ancestors from the global knowledge, - * but that would be overkill. - */ - Set(BoxedByteClass, BoxedShortClass, BoxedIntegerClass, BoxedFloatClass, - BoxedDoubleClass, ObjectClass, ClassName("java.lang.CharSequence"), - ClassName("java.io.Serializable"), ClassName("java.lang.Comparable"), - ClassName("java.lang.Number")) - } - private def foldBinaryOp(op: BinaryOp.Code, lhs: PreTransform, rhs: PreTransform)( implicit pos: Position): PreTransform = { @@ -3851,42 +3822,18 @@ private[optimizer] abstract class OptimizerCore( (op: @switch) match { case === | !== => - // Try to optimize as a primitive JS strict equality - - def canBePrimitiveNum(tpe: RefinedType): Boolean = tpe.base match { - case AnyType | ByteType | ShortType | IntType | FloatType | DoubleType => - true - case ClassType(className) => - MaybeHijackedPrimNumberClasses.contains(className) - case _ => - false - } - - def isWhole(tpe: RefinedType): Boolean = tpe.base match { - case ByteType | ShortType | IntType => - true - case ClassType(className) => - className == BoxedByteClass || - className == BoxedShortClass || - className == BoxedIntegerClass - case _ => - false - } - - def canOptimizeAsJSStrictEq(lhsTpe: RefinedType, rhsTpe: RefinedType): Boolean = ( - !canBePrimitiveNum(lhsTpe) || - !canBePrimitiveNum(rhsTpe) || - (isWhole(lhsTpe) && isWhole(rhsTpe)) - ) - (lhs, rhs) match { case (PreTransLit(l), PreTransLit(r)) => - val isSame = literal_===(l, r, isJSStrictEq = false) + val isSame = literal_===(l, r) PreTransLit(BooleanLiteral(if (op == ===) isSame else !isSame)) - case _ if canOptimizeAsJSStrictEq(lhs.tpe, rhs.tpe) => - foldJSBinaryOp( - if (op == ===) JSBinaryOp.=== else JSBinaryOp.!==, - lhs, rhs) + case (PreTransLit(_), _) => + foldBinaryOp(op, rhs, lhs) + + case (_, PreTransLit(Null())) if !lhs.tpe.isNullable => + Block( + finishTransformStat(lhs), + BooleanLiteral(op == !==)).toPreTransform + case _ => default } @@ -4812,36 +4759,6 @@ private[optimizer] abstract class OptimizerCore( } } - private def foldJSBinaryOp(op: JSBinaryOp.Code, lhs: PreTransform, - rhs: PreTransform)( - implicit pos: Position): PreTransform = { - import JSBinaryOp._ - - def default: PreTransform = - PreTransJSBinaryOp(op, lhs, rhs) - - op match { - case JSBinaryOp.=== | JSBinaryOp.!== => - val positive = (op == JSBinaryOp.===) - (lhs, rhs) match { - case (PreTransLit(l), PreTransLit(r)) => - val isEq = literal_===(l, r, isJSStrictEq = true) - PreTransLit(BooleanLiteral(if (positive) isEq else !isEq)) - - case (_, PreTransLit(Null())) if !lhs.tpe.isNullable => - Block( - finishTransformStat(lhs), - BooleanLiteral(!positive)).toPreTransform - - case (PreTransLit(_), _) => foldBinaryOp(op, rhs, lhs) - case _ => default - } - - case _ => - default - } - } - private def foldAsInstanceOf(arg: PreTransform, tpe: Type)( implicit pos: Position): PreTransform = { def mayRequireUnboxing: Boolean = @@ -5125,7 +5042,7 @@ private[optimizer] abstract class OptimizerCore( PreTransLocalDef(localDef.tryWithRefinedType(nonNullType))(texpr.pos) case PreTransTree(tree, tpe) => PreTransTree(tree, nonNullType) - case _:PreTransUnaryOp | _:PreTransBinaryOp | _:PreTransJSBinaryOp | _:PreTransRecordTree => + case _:PreTransUnaryOp | _:PreTransBinaryOp | _:PreTransRecordTree => // We cannot improve the type of those texpr } @@ -5976,8 +5893,6 @@ private[optimizer] object OptimizerCore { lhs.contains(localDef) case PreTransBinaryOp(_, lhs, rhs) => lhs.contains(localDef) || rhs.contains(localDef) - case PreTransJSBinaryOp(_, lhs, rhs) => - lhs.contains(localDef) || rhs.contains(localDef) case PreTransLocalDef(thisLocalDef) => thisLocalDef.contains(localDef) case _: PreTransGenTree => @@ -6118,19 +6033,6 @@ private[optimizer] object OptimizerCore { val tpe: RefinedType = RefinedType(BinaryOp.resultTypeOf(op)) } - /** A `PreTransform` for a `JSBinaryOp`. */ - private final case class PreTransJSBinaryOp(op: JSBinaryOp.Code, - lhs: PreTransform, rhs: PreTransform)(implicit val pos: Position) - extends PreTransResult { - - val tpe: RefinedType = RefinedType(JSBinaryOp.resultTypeOf(op)) - } - - private object PreTransJSBinaryOp { - def isWorthPreTransforming(op: JSBinaryOp.Code): Boolean = - op == JSBinaryOp.=== || op == JSBinaryOp.!== - } - /** A virtual reference to a `LocalDef`. */ private final case class PreTransLocalDef(localDef: LocalDef)( implicit val pos: Position) extends PreTransResult { @@ -6198,8 +6100,6 @@ private[optimizer] object OptimizerCore { PreTransUnaryOp(op, lhs.toPreTransform)(self.pos) case BinaryOp(op, lhs, rhs) => PreTransBinaryOp(op, lhs.toPreTransform, rhs.toPreTransform)(self.pos) - case JSBinaryOp(op, lhs, rhs) if PreTransJSBinaryOp.isWorthPreTransforming(op) => - PreTransJSBinaryOp(op, lhs.toPreTransform, rhs.toPreTransform)(self.pos) case _ => PreTransTree(self) } From cf9e775fe7833c227f30d20464c3cb0da63984bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 22 May 2024 11:16:56 +0200 Subject: [PATCH 105/298] Bump the version to 1.17.0-SNAPSHOT for the upcoming changes. --- ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala index eb920f2071..c32a7d5b2b 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/ScalaJSVersions.scala @@ -17,7 +17,7 @@ import java.util.concurrent.ConcurrentHashMap import scala.util.matching.Regex object ScalaJSVersions extends VersionChecks( - current = "1.16.1-SNAPSHOT", + current = "1.17.0-SNAPSHOT", binaryEmitted = "1.16" ) From 4be30c723db0ca02c80036b005af4e3d19e01165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 23 May 2024 13:20:57 +0200 Subject: [PATCH 106/298] Make `captureJSError` tolerant to sealed `throwable` arguments. If an object is sealed, `captureStackTrace` throws an exception. This will happen for WebAssembly objects. We now detect this case and fall back to instantiating a dedicated `js.Error` object. --- javalib/src/main/scala/java/lang/StackTrace.scala | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/javalib/src/main/scala/java/lang/StackTrace.scala b/javalib/src/main/scala/java/lang/StackTrace.scala index 4dac37591c..76b3d067e7 100644 --- a/javalib/src/main/scala/java/lang/StackTrace.scala +++ b/javalib/src/main/scala/java/lang/StackTrace.scala @@ -61,8 +61,12 @@ private[lang] object StackTrace { * prototypes. */ reference - } else if (js.constructorOf[js.Error].captureStackTrace eq ().asInstanceOf[AnyRef]) { - // Create a JS Error with the current stack trace. + } else if ((js.constructorOf[js.Error].captureStackTrace eq ().asInstanceOf[AnyRef]) || + js.Object.isSealed(throwable.asInstanceOf[js.Object])) { + /* If `captureStackTrace` is not available, or if the `throwable` instance + * is sealed (which notably happens on Wasm), create a JS `Error` with the + * current stack trace. + */ new js.Error() } else { /* V8-specific. From ed255d2e6ff72a21acdab519532f677512a66c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 23 May 2024 13:23:47 +0200 Subject: [PATCH 107/298] Make `ExportLoopback` not dependent on support for multiple modules. We now directly use `import("./main.js")` or `require("./main.js")` rather than relying on the compilation scheme of `js.dynamicImport`. This will allow `ExportLoopback` to work under WebAssembly, although the initial implementation will not support multiple modules. --- project/Build.scala | 4 ++- .../require-commonjs/ExportLoopback.scala | 25 +++++++++++++++++++ .../testsuite/jsinterop/ExportLoopback.scala | 7 +----- 3 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 test-suite/js/src/test/require-commonjs/ExportLoopback.scala rename test-suite/js/src/test/{require-modules => require-esmodule}/org/scalajs/testsuite/jsinterop/ExportLoopback.scala (70%) diff --git a/project/Build.scala b/project/Build.scala index dd86f57340..57cb5d0b12 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2248,7 +2248,9 @@ object Build { includeIf(testDir / "require-dynamic-import", moduleKind == ModuleKind.ESModule) ::: // this is an approximation that works for now includeIf(testDir / "require-esmodule", - moduleKind == ModuleKind.ESModule) + moduleKind == ModuleKind.ESModule) ::: + includeIf(testDir / "require-commonjs", + moduleKind == ModuleKind.CommonJSModule) }, unmanagedResourceDirectories in Test ++= { diff --git a/test-suite/js/src/test/require-commonjs/ExportLoopback.scala b/test-suite/js/src/test/require-commonjs/ExportLoopback.scala new file mode 100644 index 0000000000..aeca2e8864 --- /dev/null +++ b/test-suite/js/src/test/require-commonjs/ExportLoopback.scala @@ -0,0 +1,25 @@ +/* + * 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.testsuite.jsinterop + +import scala.scalajs.js + +import scala.concurrent.Future + +object ExportLoopback { + val exportsNamespace: Future[js.Dynamic] = { + js.Promise.resolve[Unit](()) + .`then`[js.Dynamic](_ => js.Dynamic.global.require("./main.js")) + .toFuture + } +} diff --git a/test-suite/js/src/test/require-modules/org/scalajs/testsuite/jsinterop/ExportLoopback.scala b/test-suite/js/src/test/require-esmodule/org/scalajs/testsuite/jsinterop/ExportLoopback.scala similarity index 70% rename from test-suite/js/src/test/require-modules/org/scalajs/testsuite/jsinterop/ExportLoopback.scala rename to test-suite/js/src/test/require-esmodule/org/scalajs/testsuite/jsinterop/ExportLoopback.scala index 6e8decdc25..b91a1bdbf8 100644 --- a/test-suite/js/src/test/require-modules/org/scalajs/testsuite/jsinterop/ExportLoopback.scala +++ b/test-suite/js/src/test/require-esmodule/org/scalajs/testsuite/jsinterop/ExportLoopback.scala @@ -13,15 +13,10 @@ package org.scalajs.testsuite.jsinterop import scala.scalajs.js -import scala.scalajs.js.annotation._ import scala.concurrent.Future object ExportLoopback { val exportsNamespace: Future[js.Dynamic] = - js.dynamicImport(mainModule).toFuture - - @js.native - @JSImport("./main.js", JSImport.Namespace) - private val mainModule: js.Dynamic = js.native + js.`import`("./main.js").toFuture } From e89e1e48bced9c85ded9749ce7ef3129853a567b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 23 May 2024 15:55:51 +0200 Subject: [PATCH 108/298] Initial implementation of the WebAssembly backend. This commit contains the initial implementation of the WebAssembly backend. This backend is still experimental, in the sense that: * We may remove it in a future Minor version, if we decide that it has a better place elsewhere, and * Newer minor versions may produce WebAssembly code that requires more recent WebAssembly features. The WebAssembly backend silently ignores `@JSExport` and `@JSExportAll` annotations. It is otherwise supposed to support the full Scala.js language semantics. Currently, the backend only supports some configurations of the linker. It requires: * No optimizer, * Unchecked semantics for undefined behaviors, * Strict floats, and * ES modules. Some of those will be relaxed in the future, definitely including the first two. Co-authored-by: Rikito Taniguchi --- Jenkinsfile | 38 + TESTING.md | 19 + .../scala/org/scalajs/ir/UTF8String.scala | 5 +- .../linker/interface/StandardConfig.scala | 58 +- .../backend/LinkerBackendImplPlatform.scala | 2 +- .../backend/LinkerBackendImplPlatform.scala | 2 +- .../linker/backend/LinkerBackendImpl.scala | 26 +- .../backend/WebAssemblyLinkerBackend.scala | 159 + .../linker/backend/javascript/Printers.scala | 6 + .../linker/backend/javascript/Trees.scala | 2 + .../backend/wasmemitter/ClassEmitter.scala | 1288 +++++++ .../backend/wasmemitter/CoreWasmLib.scala | 2214 +++++++++++ .../backend/wasmemitter/DerivedClasses.scala | 151 + .../wasmemitter/EmbeddedConstants.scala | 68 + .../linker/backend/wasmemitter/Emitter.scala | 399 ++ .../backend/wasmemitter/FunctionEmitter.scala | 3374 +++++++++++++++++ .../backend/wasmemitter/LoaderContent.scala | 328 ++ .../backend/wasmemitter/Preprocessor.scala | 473 +++ .../linker/backend/wasmemitter/README.md | 790 ++++ .../linker/backend/wasmemitter/SWasmGen.scala | 137 + .../backend/wasmemitter/SpecialNames.scala | 48 + .../backend/wasmemitter/StringPool.scala | 107 + .../backend/wasmemitter/TypeTransformer.scala | 116 + .../linker/backend/wasmemitter/VarGen.scala | 446 +++ .../backend/wasmemitter/WasmContext.scala | 301 ++ .../backend/webassembly/BinaryWriter.scala | 667 ++++ .../backend/webassembly/FunctionBuilder.scala | 445 +++ .../backend/webassembly/Identitities.scala | 65 + .../backend/webassembly/Instructions.scala | 408 ++ .../backend/webassembly/ModuleBuilder.scala | 95 + .../linker/backend/webassembly/Modules.scala | 137 + .../backend/webassembly/TextWriter.scala | 620 +++ .../linker/backend/webassembly/Types.scala | 187 + .../standard/StandardLinkerBackend.scala | 1 + project/Build.scala | 51 +- .../testsuite/javalib/lang/ClassTestEx.scala | 7 + .../scalajs/testsuite/utils/Platform.scala | 2 + .../resources/SourceMapTestTemplate.scala | 1 + .../compiler/RuntimeTypeTestsJSTest.scala | 20 +- .../javalib/lang/ThrowableJSTest.scala | 3 + .../testsuite/jsinterop/ExportsTest.scala | 10 +- .../testsuite/jsinterop/MiscInteropTest.scala | 1 + .../testsuite/library/LinkingInfoTest.scala | 11 +- .../testsuite/library/StackTraceTest.scala | 1 + .../scalajs/testsuite/utils/Platform.scala | 2 + 45 files changed, 13263 insertions(+), 28 deletions(-) create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/ClassEmitter.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/CoreWasmLib.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/DerivedClasses.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/EmbeddedConstants.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Emitter.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/LoaderContent.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Preprocessor.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/README.md create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/SWasmGen.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/SpecialNames.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/StringPool.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/TypeTransformer.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/VarGen.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/WasmContext.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/BinaryWriter.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/FunctionBuilder.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/Identitities.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/Instructions.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/ModuleBuilder.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/Modules.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/TextWriter.scala create mode 100644 linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/Types.scala diff --git a/Jenkinsfile b/Jenkinsfile index 487f96ba53..70d90e83fe 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -396,6 +396,41 @@ def Tasks = [ ++$scala $testSuite$v/test ''', + "test-suite-webassembly": ''' + setJavaVersion $java + npm install && + sbtretry ++$scala \ + 'set Global/enableWasmEverywhere := true' \ + helloworld$v/run && + sbtretry ++$scala \ + 'set Global/enableWasmEverywhere := true' \ + 'set scalaJSStage in Global := FullOptStage' \ + 'set scalaJSLinkerConfig in helloworld.v$v ~= (_.withPrettyPrint(true))' \ + helloworld$v/run && + sbtretry ++$scala \ + 'set Global/enableWasmEverywhere := true' \ + reversi$v/fastLinkJS \ + reversi$v/fullLinkJS && + sbtretry ++$scala \ + 'set Global/enableWasmEverywhere := true' \ + jUnitTestOutputsJVM$v/test jUnitTestOutputsJS$v/test testBridge$v/test \ + 'set scalaJSStage in Global := FullOptStage' jUnitTestOutputsJS$v/test testBridge$v/test && + sbtretry ++$scala \ + 'set Global/enableWasmEverywhere := true' \ + $testSuite$v/test && + sbtretry ++$scala \ + 'set Global/enableWasmEverywhere := true' \ + 'set scalaJSStage in Global := FullOptStage' \ + $testSuite$v/test && + sbtretry ++$scala \ + 'set Global/enableWasmEverywhere := true' \ + testingExample$v/testHtml && + sbtretry ++$scala \ + 'set Global/enableWasmEverywhere := true' \ + 'set scalaJSStage in Global := FullOptStage' \ + testingExample$v/testHtml + ''', + /* For the bootstrap tests to be able to call * `testSuite/test:fastOptJS`, `scalaJSStage in testSuite` must be * `FastOptStage`, even when `scalaJSStage in Global` is `FullOptStage`. @@ -539,8 +574,11 @@ mainScalaVersions.each { scalaVersion -> quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: mainJavaVersion, testMinify: "false", testSuite: "testSuite"]) quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: mainJavaVersion, testMinify: "true", testSuite: "testSuite"]) quickMatrix.add([task: "test-suite-custom-esversion", scala: scalaVersion, java: mainJavaVersion, esVersion: "ES5_1", testSuite: "testSuite"]) + quickMatrix.add([task: "test-suite-webassembly", scala: scalaVersion, java: mainJavaVersion, testMinify: "false", testSuite: "testSuite"]) + quickMatrix.add([task: "test-suite-webassembly", scala: scalaVersion, java: mainJavaVersion, testMinify: "false", testSuite: "testSuiteEx"]) quickMatrix.add([task: "test-suite-default-esversion", scala: scalaVersion, java: mainJavaVersion, testMinify: "false", testSuite: "scalaTestSuite"]) quickMatrix.add([task: "test-suite-custom-esversion", scala: scalaVersion, java: mainJavaVersion, esVersion: "ES5_1", testSuite: "scalaTestSuite"]) + quickMatrix.add([task: "test-suite-webassembly", scala: scalaVersion, java: mainJavaVersion, testMinify: "false", testSuite: "scalaTestSuite"]) quickMatrix.add([task: "bootstrap", scala: scalaVersion, java: mainJavaVersion]) quickMatrix.add([task: "partest-fastopt", scala: scalaVersion, java: mainJavaVersion]) } diff --git a/TESTING.md b/TESTING.md index d88cbda79c..d26fafe4c3 100644 --- a/TESTING.md +++ b/TESTING.md @@ -25,6 +25,25 @@ $ python3 -m http.server // Open http://localhost:8000/test-suite/js/.2.12/target/scala-2.12/scalajs-test-suite-fastopt-test-html/index.html ``` +## HTML-Test Runner with WebAssembly + +WebAssembly requires modules, so this is manual as well. + +This test currently requires Chrome (or another V8-based browser) with `--wasm-experimental-exnref` enabled. +That option can be configured as "Experimental WebAssembly" at [chrome://flags/#enable-experimental-webassembly-features](chrome://flags/#enable-experimental-webassembly-features). + +``` +$ sbt +> set Global/enableWasmEverywhere := true +> testingExample2_12/testHtml +> testSuite2_12/testHtml +> exit +$ python3 -m http.server + +// Open http://localhost:8000/examples/testing/.2.12/target/scala-2.12/testing-fastopt-test-html/index.html +// Open http://localhost:8000/test-suite/js/.2.12/target/scala-2.12/scalajs-test-suite-fastopt-test-html/index.html +``` + ## Sourcemaps To test source maps, do the following on: diff --git a/ir/shared/src/main/scala/org/scalajs/ir/UTF8String.scala b/ir/shared/src/main/scala/org/scalajs/ir/UTF8String.scala index 00eb0c2f11..8e4fd87a8f 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/UTF8String.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/UTF8String.scala @@ -12,7 +12,7 @@ package org.scalajs.ir -import java.nio.CharBuffer +import java.nio.{ByteBuffer, CharBuffer} import java.nio.charset.CharacterCodingException import java.nio.charset.CodingErrorAction import java.nio.charset.StandardCharsets.UTF_8 @@ -48,6 +48,9 @@ final class UTF8String private (private[ir] val bytes: Array[Byte]) System.arraycopy(that.bytes, 0, result, thisLen, thatLen) new UTF8String(result) } + + def writeTo(buffer: ByteBuffer): Unit = + buffer.put(bytes) } object UTF8String { diff --git a/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/StandardConfig.scala b/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/StandardConfig.scala index 40644b5b9f..14ac9e6a1c 100644 --- a/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/StandardConfig.scala +++ b/linker-interface/shared/src/main/scala/org/scalajs/linker/interface/StandardConfig.scala @@ -63,7 +63,13 @@ final class StandardConfig private ( * On the JavaScript platform, this does not have any effect. */ val closureCompilerIfAvailable: Boolean, - /** Pretty-print the output. */ + /** Pretty-print the output, for debugging purposes. + * + * For the WebAssembly backend, this results in an additional `.wat` file + * next to each produced `.wasm` file with the WebAssembly text format + * representation of the latter. This file is never subsequently used, + * but may be inspected for debugging pruposes. + */ val prettyPrint: Boolean, /** Whether the linker should run in batch mode. * @@ -78,7 +84,9 @@ final class StandardConfig private ( */ val batchMode: Boolean, /** The maximum number of (file) writes executed concurrently. */ - val maxConcurrentWrites: Int + val maxConcurrentWrites: Int, + /** If true, use the experimental WebAssembly backend. */ + val experimentalUseWebAssembly: Boolean ) { private def this() = { this( @@ -97,7 +105,8 @@ final class StandardConfig private ( closureCompilerIfAvailable = false, prettyPrint = false, batchMode = false, - maxConcurrentWrites = 50 + maxConcurrentWrites = 50, + experimentalUseWebAssembly = false ) } @@ -177,6 +186,40 @@ final class StandardConfig private ( def withMaxConcurrentWrites(maxConcurrentWrites: Int): StandardConfig = copy(maxConcurrentWrites = maxConcurrentWrites) + /** Specifies whether to use the experimental WebAssembly backend. + * + * When using this setting, the following settings must also be set: + * + * - `withSemantics(sems)` such that the behaviors of `sems` are all set to + * `CheckedBehavior.Unchecked` + * - `withModuleKind(ModuleKind.ESModule)` + * - `withOptimizer(false)` + * - `withStrictFloats(true)` (this is the default) + * + * These restrictions will be lifted in the future, except for the + * `ModuleKind`. + * + * If any of these restrictions are not met, linking will eventually throw + * an `IllegalArgumentException`. + * + * @note + * Currently, the WebAssembly backend silently ignores `@JSExport` and + * `@JSExportAll` annotations. This behavior may change in the future, + * either by making them warnings or errors, or by adding support for them. + * All other language features are supported. + * + * @note + * This setting is experimental. It may be removed in an upcoming *minor* + * version of Scala.js. Future minor versions may also produce code that + * requires more recent versions of JS engines supporting newer WebAssembly + * standards. + * + * @throws java.lang.UnsupportedOperationException + * In the future, if the feature gets removed. + */ + def withExperimentalUseWebAssembly(experimentalUseWebAssembly: Boolean): StandardConfig = + copy(experimentalUseWebAssembly = experimentalUseWebAssembly) + override def toString(): String = { s"""StandardConfig( | semantics = $semantics, @@ -195,6 +238,7 @@ final class StandardConfig private ( | prettyPrint = $prettyPrint, | batchMode = $batchMode, | maxConcurrentWrites = $maxConcurrentWrites, + | experimentalUseWebAssembly = $experimentalUseWebAssembly, |)""".stripMargin } @@ -214,7 +258,8 @@ final class StandardConfig private ( closureCompilerIfAvailable: Boolean = closureCompilerIfAvailable, prettyPrint: Boolean = prettyPrint, batchMode: Boolean = batchMode, - maxConcurrentWrites: Int = maxConcurrentWrites + maxConcurrentWrites: Int = maxConcurrentWrites, + experimentalUseWebAssembly: Boolean = experimentalUseWebAssembly ): StandardConfig = { new StandardConfig( semantics, @@ -232,7 +277,8 @@ final class StandardConfig private ( closureCompilerIfAvailable, prettyPrint, batchMode, - maxConcurrentWrites + maxConcurrentWrites, + experimentalUseWebAssembly ) } } @@ -263,6 +309,7 @@ object StandardConfig { .addField("prettyPrint", config.prettyPrint) .addField("batchMode", config.batchMode) .addField("maxConcurrentWrites", config.maxConcurrentWrites) + .addField("experimentalUseWebAssembly", config.experimentalUseWebAssembly) .build() } } @@ -290,6 +337,7 @@ object StandardConfig { * - `prettyPrint`: `false` * - `batchMode`: `false` * - `maxConcurrentWrites`: `50` + * - `experimentalUseWebAssembly`: `false` */ def apply(): StandardConfig = new StandardConfig() diff --git a/linker/js/src/main/scala/org/scalajs/linker/backend/LinkerBackendImplPlatform.scala b/linker/js/src/main/scala/org/scalajs/linker/backend/LinkerBackendImplPlatform.scala index 13c3c37784..9db2923d6a 100644 --- a/linker/js/src/main/scala/org/scalajs/linker/backend/LinkerBackendImplPlatform.scala +++ b/linker/js/src/main/scala/org/scalajs/linker/backend/LinkerBackendImplPlatform.scala @@ -15,6 +15,6 @@ package org.scalajs.linker.backend private[backend] object LinkerBackendImplPlatform { import LinkerBackendImpl.Config - def createLinkerBackend(config: Config): LinkerBackendImpl = + def createJSLinkerBackend(config: Config): LinkerBackendImpl = new BasicLinkerBackend(config) } diff --git a/linker/jvm/src/main/scala/org/scalajs/linker/backend/LinkerBackendImplPlatform.scala b/linker/jvm/src/main/scala/org/scalajs/linker/backend/LinkerBackendImplPlatform.scala index 5abeea8403..894028d5ff 100644 --- a/linker/jvm/src/main/scala/org/scalajs/linker/backend/LinkerBackendImplPlatform.scala +++ b/linker/jvm/src/main/scala/org/scalajs/linker/backend/LinkerBackendImplPlatform.scala @@ -17,7 +17,7 @@ import org.scalajs.linker.backend.closure.ClosureLinkerBackend private[backend] object LinkerBackendImplPlatform { import LinkerBackendImpl.Config - def createLinkerBackend(config: Config): LinkerBackendImpl = { + def createJSLinkerBackend(config: Config): LinkerBackendImpl = { if (config.closureCompiler) new ClosureLinkerBackend(config) else diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/LinkerBackendImpl.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/LinkerBackendImpl.scala index 0fc8f5169b..29ded7b1cf 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/LinkerBackendImpl.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/LinkerBackendImpl.scala @@ -38,8 +38,12 @@ abstract class LinkerBackendImpl( } object LinkerBackendImpl { - def apply(config: Config): LinkerBackendImpl = - LinkerBackendImplPlatform.createLinkerBackend(config) + def apply(config: Config): LinkerBackendImpl = { + if (config.experimentalUseWebAssembly) + new WebAssemblyLinkerBackend(config) + else + LinkerBackendImplPlatform.createJSLinkerBackend(config) + } /** Configurations relevant to the backend */ final class Config private ( @@ -62,7 +66,9 @@ object LinkerBackendImpl { /** Pretty-print the output. */ val prettyPrint: Boolean, /** The maximum number of (file) writes executed concurrently. */ - val maxConcurrentWrites: Int + val maxConcurrentWrites: Int, + /** If true, use the experimental WebAssembly backend. */ + val experimentalUseWebAssembly: Boolean ) { private def this() = { this( @@ -74,7 +80,9 @@ object LinkerBackendImpl { minify = false, closureCompilerIfAvailable = false, prettyPrint = false, - maxConcurrentWrites = 50) + maxConcurrentWrites = 50, + experimentalUseWebAssembly = false + ) } def withCommonConfig(commonConfig: CommonPhaseConfig): Config = @@ -106,6 +114,9 @@ object LinkerBackendImpl { def withMaxConcurrentWrites(maxConcurrentWrites: Int): Config = copy(maxConcurrentWrites = maxConcurrentWrites) + def withExperimentalUseWebAssembly(experimentalUseWebAssembly: Boolean): Config = + copy(experimentalUseWebAssembly = experimentalUseWebAssembly) + private def copy( commonConfig: CommonPhaseConfig = commonConfig, jsHeader: String = jsHeader, @@ -115,7 +126,9 @@ object LinkerBackendImpl { minify: Boolean = minify, closureCompilerIfAvailable: Boolean = closureCompilerIfAvailable, prettyPrint: Boolean = prettyPrint, - maxConcurrentWrites: Int = maxConcurrentWrites): Config = { + maxConcurrentWrites: Int = maxConcurrentWrites, + experimentalUseWebAssembly: Boolean = experimentalUseWebAssembly + ): Config = { new Config( commonConfig, jsHeader, @@ -125,7 +138,8 @@ object LinkerBackendImpl { minify, closureCompilerIfAvailable, prettyPrint, - maxConcurrentWrites + maxConcurrentWrites, + experimentalUseWebAssembly ) } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala new file mode 100644 index 0000000000..ef7be8c498 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala @@ -0,0 +1,159 @@ +/* + * 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.backend + +import scala.concurrent.{ExecutionContext, Future} + +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets + +import org.scalajs.logging.Logger + +import org.scalajs.linker._ +import org.scalajs.linker.interface._ +import org.scalajs.linker.interface.unstable._ +import org.scalajs.linker.standard._ + +import org.scalajs.linker.backend.javascript.{ByteArrayWriter, SourceMapWriter} +import org.scalajs.linker.backend.webassembly._ + +import org.scalajs.linker.backend.wasmemitter.Emitter + +final class WebAssemblyLinkerBackend(config: LinkerBackendImpl.Config) + extends LinkerBackendImpl(config) { + + require( + coreSpec.moduleKind == ModuleKind.ESModule, + s"The WebAssembly backend only supports ES modules; was ${coreSpec.moduleKind}." + ) + require( + coreSpec.semantics.asInstanceOfs == CheckedBehavior.Unchecked && + coreSpec.semantics.arrayIndexOutOfBounds == CheckedBehavior.Unchecked && + coreSpec.semantics.arrayStores == CheckedBehavior.Unchecked && + coreSpec.semantics.negativeArraySizes == CheckedBehavior.Unchecked && + coreSpec.semantics.nullPointers == CheckedBehavior.Unchecked && + coreSpec.semantics.stringIndexOutOfBounds == CheckedBehavior.Unchecked && + coreSpec.semantics.moduleInit == CheckedBehavior.Unchecked, + "The WebAssembly backend currently only supports CheckedBehavior.Unchecked semantics; " + + s"was ${coreSpec.semantics}." + ) + require( + coreSpec.semantics.strictFloats, + "The WebAssembly backend only supports strict float semantics." + ) + + val loaderJSFileName = OutputPatternsImpl.jsFile(config.outputPatterns, "__loader") + + private val fragmentIndex = new SourceMapWriter.Index + + private val emitter: Emitter = { + val loaderModuleName = OutputPatternsImpl.moduleName(config.outputPatterns, "__loader") + new Emitter(Emitter.Config(coreSpec, loaderModuleName)) + } + + val symbolRequirements: SymbolRequirement = emitter.symbolRequirements + + override def injectedIRFiles: Seq[IRFile] = emitter.injectedIRFiles + + def emit(moduleSet: ModuleSet, output: OutputDirectory, logger: Logger)( + implicit ec: ExecutionContext): Future[Report] = { + val onlyModule = moduleSet.modules match { + case onlyModule :: Nil => + onlyModule + case modules => + throw new UnsupportedOperationException( + "The WebAssembly backend does not support multiple modules. Found: " + + modules.map(_.id.id).mkString(", ")) + } + val moduleID = onlyModule.id.id + + val emitterResult = emitter.emit(onlyModule, logger) + val wasmModule = emitterResult.wasmModule + + val outputImpl = OutputDirectoryImpl.fromOutputDirectory(output) + + val watFileName = s"$moduleID.wat" + val wasmFileName = s"$moduleID.wasm" + val sourceMapFileName = s"$wasmFileName.map" + val jsFileName = OutputPatternsImpl.jsFile(config.outputPatterns, moduleID) + + val filesToProduce0 = Set( + wasmFileName, + loaderJSFileName, + jsFileName + ) + val filesToProduce1 = + if (config.sourceMap) filesToProduce0 + sourceMapFileName + else filesToProduce0 + val filesToProduce = + if (config.prettyPrint) filesToProduce1 + watFileName + else filesToProduce1 + + def maybeWriteWatFile(): Future[Unit] = { + if (config.prettyPrint) { + val textOutput = TextWriter.write(wasmModule) + val textOutputBytes = textOutput.getBytes(StandardCharsets.UTF_8) + outputImpl.writeFull(watFileName, ByteBuffer.wrap(textOutputBytes)) + } else { + Future.unit + } + } + + def writeWasmFile(): Future[Unit] = { + val emitDebugInfo = !config.minify + + if (config.sourceMap) { + val sourceMapWriter = new ByteArrayWriter + + val wasmFileURI = s"./$wasmFileName" + val sourceMapURI = s"./$sourceMapFileName" + + val smWriter = new SourceMapWriter(sourceMapWriter, wasmFileURI, + config.relativizeSourceMapBase, fragmentIndex) + val binaryOutput = BinaryWriter.writeWithSourceMap( + wasmModule, emitDebugInfo, smWriter, sourceMapURI) + smWriter.complete() + + outputImpl.writeFull(wasmFileName, binaryOutput).flatMap { _ => + outputImpl.writeFull(sourceMapFileName, sourceMapWriter.toByteBuffer()) + } + } else { + val binaryOutput = BinaryWriter.write(wasmModule, emitDebugInfo) + outputImpl.writeFull(wasmFileName, binaryOutput) + } + } + + def writeLoaderFile(): Future[Unit] = + outputImpl.writeFull(loaderJSFileName, ByteBuffer.wrap(emitterResult.loaderContent)) + + def writeJSFile(): Future[Unit] = + outputImpl.writeFull(jsFileName, ByteBuffer.wrap(emitterResult.jsFileContent)) + + for { + existingFiles <- outputImpl.listFiles() + _ <- Future.sequence(existingFiles.filterNot(filesToProduce).map(outputImpl.delete(_))) + _ <- maybeWriteWatFile() + _ <- writeWasmFile() + _ <- writeLoaderFile() + _ <- writeJSFile() + } yield { + val reportModule = new ReportImpl.ModuleImpl( + moduleID, + jsFileName, + None, + coreSpec.moduleKind + ) + new ReportImpl(List(reportModule)) + } + } +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala index 33ec9cc020..aa8045f892 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala @@ -435,6 +435,12 @@ object Printers { print(')') printSeparatorIfStat() + case Await(expr) => + print("(await ") + print(expr) + print(')') + printSeparatorIfStat() + case IncDec(prefix, inc, arg) => val op = if (inc) "++" else "--" print('(') diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala index 0c0b820e82..00405e253e 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala @@ -328,6 +328,8 @@ object Trees { type Code = ir.Trees.JSUnaryOp.Code } + sealed case class Await(expr: Tree)(implicit val pos: Position) extends Tree + /** `++x`, `x++`, `--x` or `x--`. */ sealed case class IncDec(prefix: Boolean, inc: Boolean, arg: Tree)( implicit val pos: Position) 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 new file mode 100644 index 0000000000..7b6026c346 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/ClassEmitter.scala @@ -0,0 +1,1288 @@ +/* + * 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.backend.wasmemitter + +import scala.collection.mutable + +import org.scalajs.ir.{ClassKind, OriginalName, Position, UTF8String} +import org.scalajs.ir.Names._ +import org.scalajs.ir.OriginalName.NoOriginalName +import org.scalajs.ir.Trees._ +import org.scalajs.ir.Types._ + +import org.scalajs.linker.interface.unstable.RuntimeClassNameMapperImpl +import org.scalajs.linker.standard.{CoreSpec, LinkedClass, LinkedTopLevelExport} + +import org.scalajs.linker.backend.webassembly.FunctionBuilder +import org.scalajs.linker.backend.webassembly.{Instructions => wa} +import org.scalajs.linker.backend.webassembly.{Modules => wamod} +import org.scalajs.linker.backend.webassembly.{Identitities => wanme} +import org.scalajs.linker.backend.webassembly.{Types => watpe} + +import EmbeddedConstants._ +import SWasmGen._ +import VarGen._ +import TypeTransformer._ +import WasmContext._ + +class ClassEmitter(coreSpec: CoreSpec) { + import ClassEmitter._ + + def genClassDef(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = { + val classInfo = ctx.getClassInfo(clazz.className) + + if (classInfo.hasRuntimeTypeInfo && !(clazz.kind.isClass && clazz.hasDirectInstances)) { + // Gen typeData -- for concrete Scala classes, we do it as part of the vtable generation instead + val typeDataFieldValues = genTypeDataFieldValues(clazz, Nil) + genTypeDataGlobal(clazz.className, genTypeID.typeData, typeDataFieldValues, Nil) + } + + // Declare static fields + for { + field @ FieldDef(flags, name, _, ftpe) <- clazz.fields + if flags.namespace.isStatic + } { + val origName = makeDebugName(ns.StaticField, name.name) + val global = wamod.Global( + genGlobalID.forStaticField(name.name), + origName, + isMutable = true, + transformType(ftpe), + wa.Expr(List(genZeroOf(ftpe))) + ) + ctx.addGlobal(global) + } + + // Generate method implementations + for (method <- clazz.methods) { + if (method.body.isDefined) + genMethod(clazz, method) + } + + clazz.kind match { + case ClassKind.Class | ClassKind.ModuleClass => + genScalaClass(clazz) + case ClassKind.Interface => + genInterface(clazz) + case ClassKind.JSClass | ClassKind.JSModuleClass => + genJSClass(clazz) + case ClassKind.HijackedClass | ClassKind.AbstractJSType | ClassKind.NativeJSClass | + ClassKind.NativeJSModuleClass => + () // nothing to do + } + } + + /** Generates code for a top-level export. + * + * It is tempting to use Wasm `export`s for top-level exports. However, that + * does not work in several situations: + * + * - for values, an `export`ed `global` is visible in JS as an instance of + * `WebAssembly.Global`, of which we need to extract the `.value` field anyway + * - this in turn causes issues for mutable static fields, since we need to + * republish changes + * - we cannot distinguish mutable static fields from immutable ones, so we + * have to use the same strategy for both + * - exported top-level `def`s must be seen by JS as `function` functions, + * but `export`ed `func`s are JS arrow functions + * + * Overall, the only things for which `export`s would work are for exported + * JS classes and objects. + * + * Instead, we uniformly use the following strategy for all top-level exports: + * + * - the JS code declares a non-initialized `let` for every top-level export, and exports it + * from the module with an ECMAScript `export` + * - the JS code provides a setter function that we import into a Wasm, which allows to set the + * value of that `let` + * - the Wasm code "publishes" every update to top-level exports to the JS code via this + * setter; this happens once in the `start` function for every kind of top-level export (see + * `Emitter.genStartFunction`), and in addition upon each reassignment of a top-level + * exported field (see `FunctionEmitter.genAssign`). + * + * This method declares the import of the setter on the Wasm side, for all kinds of top-level + * exports. In addition, for exported *methods*, it generates the implementation of the method as + * a Wasm function. + * + * The JS code is generated by `Emitter.buildJSFileContent`. Note that for fields, the JS `let`s + * are only "mirrors" of the state. The source of truth for the state remains in the Wasm Global + * for the static field. This is fine because, by spec of ECMAScript modules, JavaScript code + * that *uses* the export cannot mutate it; it can only read it. + * + * The calls to the setters, which actually initialize all the exported `let`s, are performed: + * + * - in the `start` function for all kinds of exports, and + * - in addition on every assignment to an exported mutable static field. + */ + def genTopLevelExport(topLevelExport: LinkedTopLevelExport)( + implicit ctx: WasmContext): Unit = { + genTopLevelExportSetter(topLevelExport.exportName) + topLevelExport.tree match { + case d: TopLevelMethodExportDef => genTopLevelMethodExportDef(d) + case _ => () + } + } + + private def genIsJSClassInstanceFunction(clazz: LinkedClass)( + implicit ctx: WasmContext): Option[wanme.FunctionID] = { + implicit val noPos: Position = Position.NoPosition + + val hasIsJSClassInstance = clazz.kind match { + case ClassKind.NativeJSClass => clazz.jsNativeLoadSpec.isDefined + case ClassKind.JSClass => clazz.jsClassCaptures.isEmpty + case _ => false + } + + if (hasIsJSClassInstance) { + val className = clazz.className + + val fb = new FunctionBuilder( + ctx.moduleBuilder, + genFunctionID.isJSClassInstance(className), + makeDebugName(ns.IsInstance, className), + noPos + ) + val xParam = fb.addParam("x", watpe.RefType.anyref) + fb.setResultType(watpe.Int32) + fb.setFunctionType(genTypeID.isJSClassInstanceFuncType) + + if (clazz.kind == ClassKind.JSClass && !clazz.hasInstances) { + /* We need to constant-fold the instance test, to avoid trying to + * call $loadJSClass.className, since it will not exist at all. + */ + fb += wa.I32Const(0) // false + } else { + fb += wa.LocalGet(xParam) + genLoadJSConstructor(fb, className) + fb += wa.Call(genFunctionID.jsBinaryOps(JSBinaryOp.instanceof)) + fb += wa.Call(genFunctionID.unbox(BooleanRef)) + } + + val func = fb.buildAndAddToModule() + Some(func.id) + } else { + None + } + } + + private def genTypeDataFieldValues(clazz: LinkedClass, + reflectiveProxies: List[ConcreteMethodInfo])( + implicit ctx: WasmContext): List[wa.Instr] = { + val className = clazz.className + val classInfo = ctx.getClassInfo(className) + + val nameStr = RuntimeClassNameMapperImpl.map( + coreSpec.semantics.runtimeClassNameMapper, + className.nameString + ) + val nameDataValue: List[wa.Instr] = + ctx.stringPool.getConstantStringDataInstr(nameStr) + + val kind = className match { + case ObjectClass => KindObject + case BoxedUnitClass => KindBoxedUnit + case BoxedBooleanClass => KindBoxedBoolean + case BoxedCharacterClass => KindBoxedCharacter + case BoxedByteClass => KindBoxedByte + case BoxedShortClass => KindBoxedShort + case BoxedIntegerClass => KindBoxedInteger + case BoxedLongClass => KindBoxedLong + case BoxedFloatClass => KindBoxedFloat + case BoxedDoubleClass => KindBoxedDouble + case BoxedStringClass => KindBoxedString + + case _ => + import ClassKind._ + + clazz.kind match { + case Class | ModuleClass | HijackedClass => + KindClass + case Interface => + KindInterface + case JSClass | JSModuleClass | AbstractJSType | NativeJSClass | NativeJSModuleClass => + KindJSType + } + } + + val strictAncestorsTypeData: List[wa.Instr] = { + val ancestors = clazz.ancestors + + // By spec, the first element of `ancestors` is always the class itself + assert( + ancestors.headOption.contains(className), + s"The ancestors of ${className.nameString} do not start with itself: $ancestors" + ) + val strictAncestors = ancestors.tail + + val elems = for { + ancestor <- strictAncestors + if ctx.getClassInfo(ancestor).hasRuntimeTypeInfo + } yield { + wa.GlobalGet(genGlobalID.forVTable(ancestor)) + } + elems :+ wa.ArrayNewFixed(genTypeID.typeDataArray, elems.size) + } + + val cloneFunction = { + // If the class is concrete and implements the `java.lang.Cloneable`, + // `genCloneFunction` should've generated the clone function + if (!classInfo.isAbstract && clazz.ancestors.contains(CloneableClass)) + wa.RefFunc(genFunctionID.clone(className)) + else + wa.RefNull(watpe.HeapType.NoFunc) + } + + val isJSClassInstance = genIsJSClassInstanceFunction(clazz) match { + case None => wa.RefNull(watpe.HeapType.NoFunc) + case Some(funcID) => wa.RefFunc(funcID) + } + + val reflectiveProxiesInstrs: List[wa.Instr] = { + val elemsInstrs: List[wa.Instr] = reflectiveProxies + .map(proxyInfo => ctx.getReflectiveProxyId(proxyInfo.methodName) -> proxyInfo.tableEntryID) + .sortBy(_._1) // we will perform a binary search on the ID at run-time + .flatMap { case (proxyID, tableEntryID) => + List( + wa.I32Const(proxyID), + wa.RefFunc(tableEntryID), + wa.StructNew(genTypeID.reflectiveProxy) + ) + } + elemsInstrs :+ wa.ArrayNewFixed(genTypeID.reflectiveProxies, reflectiveProxies.size) + } + + nameDataValue ::: + List( + // kind + wa.I32Const(kind), + // specialInstanceTypes + wa.I32Const(classInfo.specialInstanceTypes) + ) ::: ( + // strictAncestors + strictAncestorsTypeData + ) ::: + List( + // componentType - always `null` since this method is not used for array types + wa.RefNull(watpe.HeapType(genTypeID.typeData)), + // name - initially `null`; filled in by the `typeDataName` helper + wa.RefNull(watpe.HeapType.Any), + // the classOf instance - initially `null`; filled in by the `createClassOf` helper + wa.RefNull(watpe.HeapType(genTypeID.ClassStruct)), + // arrayOf, the typeData of an array of this type - initially `null`; filled in by the `arrayTypeData` helper + wa.RefNull(watpe.HeapType(genTypeID.ObjectVTable)), + // clonefFunction - will be invoked from `clone()` method invokaion on the class + cloneFunction, + // isJSClassInstance - invoked from the `isInstance()` helper for JS types + isJSClassInstance + ) ::: + // reflective proxies - used to reflective call on the class at runtime. + // Generated instructions create an array of reflective proxy structs, where each struct + // contains the ID of the reflective proxy and a reference to the actual method implementation. + reflectiveProxiesInstrs + } + + private def genTypeDataGlobal(className: ClassName, typeDataTypeID: wanme.TypeID, + typeDataFieldValues: List[wa.Instr], vtableElems: List[wa.RefFunc])( + implicit ctx: WasmContext): Unit = { + val instrs: List[wa.Instr] = + typeDataFieldValues ::: vtableElems ::: wa.StructNew(typeDataTypeID) :: Nil + ctx.addGlobal( + wamod.Global( + genGlobalID.forVTable(className), + makeDebugName(ns.TypeData, className), + isMutable = false, + watpe.RefType(typeDataTypeID), + wa.Expr(instrs) + ) + ) + } + + /** Generates a Scala class or module class. */ + private def genScalaClass(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = { + val className = clazz.name.name + val typeRef = ClassRef(className) + val classInfo = ctx.getClassInfo(className) + + // generate vtable type, this should be done for both abstract and concrete classes + val vtableTypeID = genVTableType(clazz, classInfo) + + val isAbstractClass = !clazz.hasDirectInstances + + // Generate the vtable and itable for concrete classes + if (!isAbstractClass) { + // Generate an actual vtable, which we integrate into the typeData + val reflectiveProxies = + classInfo.resolvedMethodInfos.valuesIterator.filter(_.methodName.isReflectiveProxy).toList + val typeDataFieldValues = genTypeDataFieldValues(clazz, reflectiveProxies) + val vtableElems = classInfo.tableEntries.map { methodName => + wa.RefFunc(classInfo.resolvedMethodInfos(methodName).tableEntryID) + } + genTypeDataGlobal(className, vtableTypeID, typeDataFieldValues, vtableElems) + + // Generate the itable + genGlobalClassItable(clazz) + } + + // Declare the struct type for the class + val vtableField = watpe.StructField( + genFieldID.objStruct.vtable, + vtableOriginalName, + watpe.RefType(vtableTypeID), + isMutable = false + ) + val itablesField = watpe.StructField( + genFieldID.objStruct.itables, + itablesOriginalName, + watpe.RefType.nullable(genTypeID.itables), + isMutable = false + ) + val fields = classInfo.allFieldDefs.map { field => + watpe.StructField( + genFieldID.forClassInstanceField(field.name.name), + makeDebugName(ns.InstanceField, field.name.name), + transformType(field.ftpe), + isMutable = true // initialized by the constructors, so always mutable at the Wasm level + ) + } + val structTypeID = genTypeID.forClass(className) + val superType = clazz.superClass.map(s => genTypeID.forClass(s.name)) + val structType = watpe.StructType(vtableField :: itablesField :: fields) + val subType = watpe.SubType( + structTypeID, + makeDebugName(ns.ClassInstance, className), + isFinal = false, + superType, + structType + ) + ctx.mainRecType.addSubType(subType) + + // Define the `new` function and possibly the `clone` function, unless the class is abstract + if (!isAbstractClass) { + genNewDefaultFunc(clazz) + if (clazz.ancestors.contains(CloneableClass)) + genCloneFunction(clazz) + } + + // Generate the module accessor + if (clazz.kind == ClassKind.ModuleClass && clazz.hasInstances) { + val heapType = watpe.HeapType(genTypeID.forClass(clazz.className)) + + // global instance + val global = wamod.Global( + genGlobalID.forModuleInstance(className), + makeDebugName(ns.ModuleInstance, className), + isMutable = true, + watpe.RefType.nullable(heapType), + wa.Expr(List(wa.RefNull(heapType))) + ) + ctx.addGlobal(global) + + genModuleAccessor(clazz) + } + } + + private def genVTableType(clazz: LinkedClass, classInfo: ClassInfo)( + implicit ctx: WasmContext): wanme.TypeID = { + val className = classInfo.name + val typeID = genTypeID.forVTable(className) + val vtableFields = + classInfo.tableEntries.map { methodName => + watpe.StructField( + genFieldID.forMethodTableEntry(methodName), + makeDebugName(ns.TableEntry, className, methodName), + watpe.RefType(ctx.tableFunctionType(methodName)), + isMutable = false + ) + } + val superType = clazz.superClass match { + case None => genTypeID.typeData + case Some(s) => genTypeID.forVTable(s.name) + } + val structType = watpe.StructType(CoreWasmLib.typeDataStructFields ::: vtableFields) + val subType = watpe.SubType( + typeID, + makeDebugName(ns.VTable, className), + isFinal = false, + Some(superType), + structType + ) + ctx.mainRecType.addSubType(subType) + typeID + } + + /** Generate type inclusion test for interfaces. + * + * The expression `isInstanceOf[]` will be compiled to a CALL to the function + * generated by this method. + */ + private def genInterfaceInstanceTest(clazz: LinkedClass)( + implicit ctx: WasmContext): Unit = { + assert(clazz.kind == ClassKind.Interface) + + val className = clazz.className + val classInfo = ctx.getClassInfo(className) + + val fb = new FunctionBuilder( + ctx.moduleBuilder, + genFunctionID.instanceTest(className), + makeDebugName(ns.IsInstance, className), + clazz.pos + ) + val exprParam = fb.addParam("expr", watpe.RefType.anyref) + fb.setResultType(watpe.Int32) + + if (!clazz.hasInstances) { + /* Interfaces that do not have instances do not receive an itable index, + * so the codegen below would not work. Return a constant false instead. + */ + fb += wa.I32Const(0) // false + } else { + val itables = fb.addLocal("itables", watpe.RefType.nullable(genTypeID.itables)) + + fb.block(watpe.RefType.anyref) { testFail => + // if expr is not an instance of Object, return false + fb += wa.LocalGet(exprParam) + fb += wa.BrOnCastFail( + testFail, + watpe.RefType.anyref, + watpe.RefType(genTypeID.ObjectStruct) + ) + + // get itables and store + fb += wa.StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.itables) + fb += wa.LocalSet(itables) + + // Dummy return value from the block + fb += wa.RefNull(watpe.HeapType.Any) + + // if the itables is null (no interfaces are implemented) + fb += wa.LocalGet(itables) + fb += wa.BrOnNull(testFail) + + fb += wa.LocalGet(itables) + fb += wa.I32Const(classInfo.itableIdx) + fb += wa.ArrayGet(genTypeID.itables) + fb += wa.RefTest(watpe.RefType(genTypeID.forITable(className))) + fb += wa.Return + } // test fail + + if (classInfo.isAncestorOfHijackedClass) { + /* It could be a hijacked class instance that implements this interface. + * Test whether `jsValueType(expr)` is in the `specialInstanceTypes` bitset. + * In other words, return `((1 << jsValueType(expr)) & specialInstanceTypes) != 0`. + * + * For example, if this class is `Comparable`, + * `specialInstanceTypes == 0b00001111`, since `jl.Boolean`, `jl.String` + * and `jl.Double` implement `Comparable`, but `jl.Void` does not. + * If `expr` is a `number`, `jsValueType(expr) == 3`. We then test whether + * `(1 << 3) & 0b00001111 != 0`, which is true because `(1 << 3) == 0b00001000`. + * If `expr` is `undefined`, it would be `(1 << 4) == 0b00010000`, which + * would give `false`. + */ + val anyRefToVoidSig = watpe.FunctionType(List(watpe.RefType.anyref), Nil) + + val exprNonNullLocal = fb.addLocal("exprNonNull", watpe.RefType.any) + + fb.block(anyRefToVoidSig) { isNullLabel => + // exprNonNull := expr; branch to isNullLabel if it is null + fb += wa.BrOnNull(isNullLabel) + fb += wa.LocalSet(exprNonNullLocal) + + // Load 1 << jsValueType(expr) + fb += wa.I32Const(1) + fb += wa.LocalGet(exprNonNullLocal) + fb += wa.Call(genFunctionID.jsValueType) + fb += wa.I32Shl + + // return (... & specialInstanceTypes) != 0 + fb += wa.I32Const(classInfo.specialInstanceTypes) + fb += wa.I32And + fb += wa.I32Const(0) + fb += wa.I32Ne + fb += wa.Return + } + + fb += wa.I32Const(0) // false + } else { + fb += wa.Drop + fb += wa.I32Const(0) // false + } + } + + fb.buildAndAddToModule() + } + + private def genNewDefaultFunc(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = { + val className = clazz.name.name + val classInfo = ctx.getClassInfo(className) + assert(clazz.hasDirectInstances) + + val structTypeID = genTypeID.forClass(className) + val fb = new FunctionBuilder( + ctx.moduleBuilder, + genFunctionID.newDefault(className), + makeDebugName(ns.NewDefault, className), + clazz.pos + ) + fb.setResultType(watpe.RefType(structTypeID)) + + fb += wa.GlobalGet(genGlobalID.forVTable(className)) + + if (classInfo.classImplementsAnyInterface) + fb += wa.GlobalGet(genGlobalID.forITable(className)) + else + fb += wa.RefNull(watpe.HeapType(genTypeID.itables)) + + classInfo.allFieldDefs.foreach { f => + fb += genZeroOf(f.ftpe) + } + fb += wa.StructNew(structTypeID) + + fb.buildAndAddToModule() + } + + /** Generates the clone function for the given class, if it is concrete and + * implements the Cloneable interface. + * + * The generated clone function will be registered in the typeData of the class (which + * resides in the vtable of the class), and will be invoked for a `Clone` IR tree on + * the class instance. + */ + private def genCloneFunction(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = { + val className = clazz.className + val info = ctx.getClassInfo(className) + + val fb = new FunctionBuilder( + ctx.moduleBuilder, + genFunctionID.clone(className), + makeDebugName(ns.Clone, className), + clazz.pos + ) + val fromParam = fb.addParam("from", watpe.RefType(genTypeID.ObjectStruct)) + fb.setResultType(watpe.RefType(genTypeID.ObjectStruct)) + fb.setFunctionType(genTypeID.cloneFunctionType) + + val structTypeID = genTypeID.forClass(className) + val structRefType = watpe.RefType(structTypeID) + + val fromTypedLocal = fb.addLocal("fromTyped", structRefType) + + // Downcast fromParam to fromTyped + fb += wa.LocalGet(fromParam) + fb += wa.RefCast(structRefType) + fb += wa.LocalSet(fromTypedLocal) + + // Push vtable and itables on the stack (there is at least Cloneable in the itables) + fb += wa.GlobalGet(genGlobalID.forVTable(className)) + fb += wa.GlobalGet(genGlobalID.forITable(className)) + + // Push every field of `fromTyped` on the stack + info.allFieldDefs.foreach { field => + fb += wa.LocalGet(fromTypedLocal) + fb += wa.StructGet(structTypeID, genFieldID.forClassInstanceField(field.name.name)) + } + + // Create the result + fb += wa.StructNew(structTypeID) + + fb.buildAndAddToModule() + } + + private def genModuleAccessor(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = { + assert(clazz.kind == ClassKind.ModuleClass) + + val className = clazz.className + val globalInstanceID = genGlobalID.forModuleInstance(className) + val ctorID = + genFunctionID.forMethod(MemberNamespace.Constructor, className, NoArgConstructorName) + val resultType = watpe.RefType(genTypeID.forClass(className)) + + val fb = new FunctionBuilder( + ctx.moduleBuilder, + genFunctionID.loadModule(clazz.className), + makeDebugName(ns.ModuleAccessor, className), + clazz.pos + ) + fb.setResultType(resultType) + + val instanceLocal = fb.addLocal("instance", resultType) + + fb.block(resultType) { nonNullLabel => + // load global, return if not null + fb += wa.GlobalGet(globalInstanceID) + fb += wa.BrOnNonNull(nonNullLabel) + + // create an instance and call its constructor + fb += wa.Call(genFunctionID.newDefault(className)) + fb += wa.LocalTee(instanceLocal) + fb += wa.Call(ctorID) + + // store it in the global + fb += wa.LocalGet(instanceLocal) + fb += wa.GlobalSet(globalInstanceID) + + // return it + fb += wa.LocalGet(instanceLocal) + } + + fb.buildAndAddToModule() + } + + /** Generates the global instance of the class itable. + * + * Their init value will be an array of null refs of size = number of interfaces. + * They will be initialized in start function. + */ + private def genGlobalClassItable(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = { + val className = clazz.className + + if (ctx.getClassInfo(className).classImplementsAnyInterface) { + val globalID = genGlobalID.forITable(className) + val itablesInit = List( + wa.I32Const(ctx.itablesLength), + wa.ArrayNewDefault(genTypeID.itables) + ) + val global = wamod.Global( + globalID, + makeDebugName(ns.ITable, className), + isMutable = false, + watpe.RefType(genTypeID.itables), + wa.Expr(itablesInit) + ) + ctx.addGlobal(global) + } + } + + private def genInterface(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = { + assert(clazz.kind == ClassKind.Interface) + // gen itable type + val className = clazz.name.name + val classInfo = ctx.getClassInfo(clazz.className) + val itableTypeID = genTypeID.forITable(className) + val itableType = watpe.StructType( + classInfo.tableEntries.map { methodName => + watpe.StructField( + genFieldID.forMethodTableEntry(methodName), + makeDebugName(ns.TableEntry, className, methodName), + watpe.RefType(ctx.tableFunctionType(methodName)), + isMutable = false + ) + } + ) + ctx.mainRecType.addSubType( + itableTypeID, + makeDebugName(ns.ITable, className), + itableType + ) + + if (clazz.hasInstanceTests) + genInterfaceInstanceTest(clazz) + } + + private def genJSClass(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = { + assert(clazz.kind.isJSClass) + + // Define the globals holding the Symbols of private fields + for (fieldDef <- clazz.fields) { + fieldDef match { + case FieldDef(flags, name, _, _) if !flags.namespace.isStatic => + ctx.addGlobal( + wamod.Global( + genGlobalID.forJSPrivateField(name.name), + makeDebugName(ns.PrivateJSField, name.name), + isMutable = true, + watpe.RefType.anyref, + wa.Expr(List(wa.RefNull(watpe.HeapType.Any))) + ) + ) + case _ => + () + } + } + + if (clazz.hasInstances) { + genCreateJSClassFunction(clazz) + + if (clazz.jsClassCaptures.isEmpty) + genLoadJSClassFunction(clazz) + + if (clazz.kind == ClassKind.JSModuleClass) + genLoadJSModuleFunction(clazz) + } + } + + private def genCreateJSClassFunction(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = { + implicit val noPos: Position = Position.NoPosition + + val className = clazz.className + val jsClassCaptures = clazz.jsClassCaptures.getOrElse(Nil) + + /* We need to decompose the body of the constructor into 3 closures. + * Given an IR constructor of the form + * constructor(...params) { + * preSuperStats; + * super(...superArgs); + * postSuperStats; + * } + * We will create closures for `preSuperStats`, `superArgs` and `postSuperStats`. + * + * There is one huge catch: `preSuperStats` can declare `VarDef`s at its top-level, + * and those vars are still visible inside `superArgs` and `postSuperStats`. + * The `preSuperStats` must therefore return a struct with the values of its + * declared vars, which will be given as an additional argument to `superArgs` + * and `postSuperStats`. We call that struct the `preSuperEnv`. + * + * In the future, we should optimize `preSuperEnv` to only store locals that + * are still used by `superArgs` and/or `postSuperArgs`. + */ + + val preSuperStatsFunctionID = genFunctionID.preSuperStats(className) + val superArgsFunctionID = genFunctionID.superArgs(className) + val postSuperStatsFunctionID = genFunctionID.postSuperStats(className) + val ctor = clazz.jsConstructorDef.get + + FunctionEmitter.emitJSConstructorFunctions( + preSuperStatsFunctionID, + superArgsFunctionID, + postSuperStatsFunctionID, + className, + jsClassCaptures, + ctor + ) + + // Build the actual `createJSClass` function + val createJSClassFun = { + val fb = new FunctionBuilder( + ctx.moduleBuilder, + genFunctionID.createJSClassOf(className), + makeDebugName(ns.CreateJSClass, className), + clazz.pos + ) + val classCaptureParams = jsClassCaptures.map { cc => + fb.addParam("cc." + cc.name.name.nameString, transformLocalType(cc.ptpe)) + } + fb.setResultType(watpe.RefType.any) + + val dataStructTypeID = ctx.getClosureDataStructType(jsClassCaptures.map(_.ptpe)) + + val dataStructLocal = fb.addLocal("classCaptures", watpe.RefType(dataStructTypeID)) + val jsClassLocal = fb.addLocal("jsClass", watpe.RefType.any) + + // --- Actual start of instructions of `createJSClass` + + // Bundle class captures in a capture data struct -- leave it on the stack for createJSClass + for (classCaptureParam <- classCaptureParams) + fb += wa.LocalGet(classCaptureParam) + fb += wa.StructNew(dataStructTypeID) + fb += wa.LocalTee(dataStructLocal) + + val classCaptureParamsOfTypeAny: Map[LocalName, wanme.LocalID] = { + jsClassCaptures + .zip(classCaptureParams) + .collect { case (ParamDef(ident, _, AnyType, _), param) => + ident.name -> param + } + .toMap + } + + def genLoadIsolatedTree(tree: Tree): Unit = { + tree match { + case StringLiteral(value) => + // Common shape for all the `nameTree` expressions + fb ++= ctx.stringPool.getConstantStringInstr(value) + + case VarRef(LocalIdent(localName)) if classCaptureParamsOfTypeAny.contains(localName) => + /* Common shape for the `jsSuperClass` value + * We can only deal with class captures of type `AnyType` in this way, + * since otherwise we might need `adapt` to box the values. + */ + fb += wa.LocalGet(classCaptureParamsOfTypeAny(localName)) + + case _ => + // For everything else, put the tree in its own function and call it + val closureFuncID = new JSClassClosureFunctionID(className) + FunctionEmitter.emitFunction( + closureFuncID, + NoOriginalName, + enclosingClassName = None, + Some(jsClassCaptures), + receiverType = None, + paramDefs = Nil, + restParam = None, + tree, + AnyType + ) + fb += wa.LocalGet(dataStructLocal) + fb += wa.Call(closureFuncID) + } + } + + /* Load super constructor; specified by + * https://lampwww.epfl.ch/~doeraene/sjsir-semantics/#sec-sjsir-classdef-runtime-semantics-evaluation + * - if `jsSuperClass` is defined, evaluate it; + * - otherwise load the JS constructor of the declared superClass, + * as if by `LoadJSConstructor`. + */ + clazz.jsSuperClass match { + case None => + genLoadJSConstructor(fb, clazz.superClass.get.name) + case Some(jsSuperClassTree) => + genLoadIsolatedTree(jsSuperClassTree) + } + + // Load the references to the 3 functions that make up the constructor + fb += ctx.refFuncWithDeclaration(preSuperStatsFunctionID) + fb += ctx.refFuncWithDeclaration(superArgsFunctionID) + fb += ctx.refFuncWithDeclaration(postSuperStatsFunctionID) + + // Load the array of field names and initial values + fb += wa.Call(genFunctionID.jsNewArray) + for (fieldDef <- clazz.fields if !fieldDef.flags.namespace.isStatic) { + // Append the name + fieldDef match { + case FieldDef(_, name, _, _) => + fb += wa.GlobalGet(genGlobalID.forJSPrivateField(name.name)) + case JSFieldDef(_, nameTree, _) => + genLoadIsolatedTree(nameTree) + } + fb += wa.Call(genFunctionID.jsArrayPush) + + // Append the boxed representation of the zero of the field + fb += genBoxedZeroOf(fieldDef.ftpe) + fb += wa.Call(genFunctionID.jsArrayPush) + } + + // Call the createJSClass helper to bundle everything + if (ctor.restParam.isDefined) { + fb += wa.I32Const(ctor.args.size) // number of fixed params + fb += wa.Call(genFunctionID.createJSClassRest) + } else { + fb += wa.Call(genFunctionID.createJSClass) + } + + // Store the result, locally in `jsClass` and possibly in the global cache + if (clazz.jsClassCaptures.isEmpty) { + /* Static JS class with a global cache. We must fill the global cache + * before we call the class initializer, later in the current function. + */ + fb += wa.LocalTee(jsClassLocal) + fb += wa.GlobalSet(genGlobalID.forJSClassValue(className)) + } else { + // Local or inner JS class, which is new every time + fb += wa.LocalSet(jsClassLocal) + } + + // Install methods and properties + for (methodOrProp <- clazz.exportedMembers) { + val isStatic = methodOrProp.flags.namespace.isStatic + fb += wa.LocalGet(dataStructLocal) + fb += wa.LocalGet(jsClassLocal) + + val receiverType = if (isStatic) None else Some(watpe.RefType.anyref) + + methodOrProp match { + case JSMethodDef(flags, nameTree, params, restParam, body) => + genLoadIsolatedTree(nameTree) + + val closureFuncID = new JSClassClosureFunctionID(className) + FunctionEmitter.emitFunction( + closureFuncID, + NoOriginalName, // TODO Come up with something here? + Some(className), + Some(jsClassCaptures), + receiverType, + params, + restParam, + body, + AnyType + ) + fb += ctx.refFuncWithDeclaration(closureFuncID) + + fb += wa.I32Const(if (restParam.isDefined) params.size else -1) + if (isStatic) + fb += wa.Call(genFunctionID.installJSStaticMethod) + else + fb += wa.Call(genFunctionID.installJSMethod) + + case JSPropertyDef(flags, nameTree, optGetter, optSetter) => + genLoadIsolatedTree(nameTree) + + optGetter match { + case None => + fb += wa.RefNull(watpe.HeapType.Func) + + case Some(getterBody) => + val closureFuncID = new JSClassClosureFunctionID(className) + FunctionEmitter.emitFunction( + closureFuncID, + NoOriginalName, // TODO Come up with something here? + Some(className), + Some(jsClassCaptures), + receiverType, + paramDefs = Nil, + restParam = None, + getterBody, + resultType = AnyType + ) + fb += ctx.refFuncWithDeclaration(closureFuncID) + } + + optSetter match { + case None => + fb += wa.RefNull(watpe.HeapType.Func) + + case Some((setterParamDef, setterBody)) => + val closureFuncID = new JSClassClosureFunctionID(className) + FunctionEmitter.emitFunction( + closureFuncID, + NoOriginalName, // TODO Come up with something here? + Some(className), + Some(jsClassCaptures), + receiverType, + setterParamDef :: Nil, + restParam = None, + setterBody, + resultType = NoType + ) + fb += ctx.refFuncWithDeclaration(closureFuncID) + } + + if (isStatic) + fb += wa.Call(genFunctionID.installJSStaticProperty) + else + fb += wa.Call(genFunctionID.installJSProperty) + } + } + + // Static fields + for (fieldDef <- clazz.fields if fieldDef.flags.namespace.isStatic) { + // Load class value + fb += wa.LocalGet(jsClassLocal) + + // Load name + fieldDef match { + case FieldDef(_, name, _, _) => + throw new AssertionError( + s"Unexpected private static field ${name.name.nameString} " + + s"in JS class ${className.nameString}" + ) + case JSFieldDef(_, nameTree, _) => + genLoadIsolatedTree(nameTree) + } + + // Generate boxed representation of the zero of the field + fb += genBoxedZeroOf(fieldDef.ftpe) + + /* Note: there is no `installJSStaticField` because it would do the + * same as `installJSField` anyway. + */ + fb += wa.Call(genFunctionID.installJSField) + } + + // Class initializer + if (clazz.methods.exists(_.methodName.isClassInitializer)) { + assert( + clazz.jsClassCaptures.isEmpty, + s"Illegal class initializer in non-static class ${className.nameString}" + ) + val namespace = MemberNamespace.StaticConstructor + fb += wa.Call( + genFunctionID.forMethod(namespace, className, ClassInitializerName) + ) + } + + // Final result + fb += wa.LocalGet(jsClassLocal) + + fb.buildAndAddToModule() + } + } + + private def genLoadJSClassFunction(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = { + require(clazz.jsClassCaptures.isEmpty) + + val className = clazz.className + + val cachedJSClassGlobal = wamod.Global( + genGlobalID.forJSClassValue(className), + makeDebugName(ns.JSClassValueCache, className), + isMutable = true, + watpe.RefType.anyref, + wa.Expr(List(wa.RefNull(watpe.HeapType.Any))) + ) + ctx.addGlobal(cachedJSClassGlobal) + + val fb = new FunctionBuilder( + ctx.moduleBuilder, + genFunctionID.loadJSClass(className), + makeDebugName(ns.JSClassAccessor, className), + clazz.pos + ) + fb.setResultType(watpe.RefType.any) + + fb.block(watpe.RefType.any) { doneLabel => + // Load cached JS class, return if non-null + fb += wa.GlobalGet(cachedJSClassGlobal.id) + fb += wa.BrOnNonNull(doneLabel) + // Otherwise, call createJSClass -- it will also store the class in the cache + fb += wa.Call(genFunctionID.createJSClassOf(className)) + } + + fb.buildAndAddToModule() + } + + private def genLoadJSModuleFunction(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = { + val className = clazz.className + val cacheGlobalID = genGlobalID.forModuleInstance(className) + + ctx.addGlobal( + wamod.Global( + cacheGlobalID, + makeDebugName(ns.ModuleInstance, className), + isMutable = true, + watpe.RefType.anyref, + wa.Expr(List(wa.RefNull(watpe.HeapType.Any))) + ) + ) + + val fb = new FunctionBuilder( + ctx.moduleBuilder, + genFunctionID.loadModule(className), + makeDebugName(ns.ModuleAccessor, className), + clazz.pos + ) + fb.setResultType(watpe.RefType.anyref) + + fb.block(watpe.RefType.anyref) { doneLabel => + // Load cached instance; return if non-null + fb += wa.GlobalGet(cacheGlobalID) + fb += wa.BrOnNonNull(doneLabel) + + // Get the JS class and instantiate it + fb += wa.Call(genFunctionID.loadJSClass(className)) + fb += wa.Call(genFunctionID.jsNewArray) + fb += wa.Call(genFunctionID.jsNew) + + // Store and return the result + fb += wa.GlobalSet(cacheGlobalID) + fb += wa.GlobalGet(cacheGlobalID) + } + + fb.buildAndAddToModule() + } + + /** Generates the function import for a top-level export setter. */ + private def genTopLevelExportSetter(exportedName: String)(implicit ctx: WasmContext): Unit = { + val functionID = genFunctionID.forTopLevelExportSetter(exportedName) + val functionSig = watpe.FunctionType(List(watpe.RefType.anyref), Nil) + val functionType = ctx.moduleBuilder.functionTypeToTypeID(functionSig) + + ctx.moduleBuilder.addImport( + wamod.Import( + "__scalaJSExportSetters", + exportedName, + wamod.ImportDesc.Func( + functionID, + makeDebugName(ns.TopLevelExportSetter, exportedName), + functionType + ) + ) + ) + } + + private def genTopLevelMethodExportDef(exportDef: TopLevelMethodExportDef)( + implicit ctx: WasmContext): Unit = { + implicit val pos = exportDef.pos + + val method = exportDef.methodDef + val exportedName = exportDef.topLevelExportName + val functionID = genFunctionID.forExport(exportedName) + + FunctionEmitter.emitFunction( + functionID, + makeDebugName(ns.TopLevelExport, exportedName), + enclosingClassName = None, + captureParamDefs = None, + receiverType = None, + method.args, + method.restParam, + method.body, + resultType = AnyType + ) + } + + private def genMethod(clazz: LinkedClass, method: MethodDef)( + implicit ctx: WasmContext): Unit = { + implicit val pos = method.pos + + val namespace = method.flags.namespace + val className = clazz.className + val methodName = method.methodName + + val functionID = genFunctionID.forMethod(namespace, className, methodName) + + val namespaceUTF8String = namespace match { + case MemberNamespace.Public => ns.Public + case MemberNamespace.PublicStatic => ns.PublicStatic + case MemberNamespace.Private => ns.Private + case MemberNamespace.PrivateStatic => ns.PrivateStatic + case MemberNamespace.Constructor => ns.Constructor + case MemberNamespace.StaticConstructor => ns.StaticConstructor + } + val originalName = makeDebugName(namespaceUTF8String, className, methodName) + + val isHijackedClass = clazz.kind == ClassKind.HijackedClass + + val receiverType = + if (namespace.isStatic) + None + else if (isHijackedClass) + Some(transformType(BoxedClassToPrimType(className))) + else + Some(transformClassType(className).toNonNullable) + + val body = method.body.getOrElse(throw new Exception("abstract method cannot be transformed")) + + // Emit the function + FunctionEmitter.emitFunction( + functionID, + originalName, + Some(className), + captureParamDefs = None, + receiverType, + method.args, + restParam = None, + body, + method.resultType + ) + + if (namespace == MemberNamespace.Public && !isHijackedClass) { + /* Also generate the bridge that is stored in the table entries. In table + * entries, the receiver type is always `(ref any)`. + * + * TODO: generate this only when the method is actually referred to from + * at least one table. + */ + + val fb = new FunctionBuilder( + ctx.moduleBuilder, + genFunctionID.forTableEntry(className, methodName), + makeDebugName(ns.TableEntry, className, methodName), + pos + ) + val receiverParam = fb.addParam(thisOriginalName, watpe.RefType.any) + val argParams = method.args.map { arg => + val origName = arg.originalName.orElse(arg.name.name) + fb.addParam(origName, TypeTransformer.transformLocalType(arg.ptpe)) + } + fb.setResultTypes(TypeTransformer.transformResultType(method.resultType)) + fb.setFunctionType(ctx.tableFunctionType(methodName)) + + // Load and cast down the receiver + fb += wa.LocalGet(receiverParam) + receiverType match { + case Some(watpe.RefType(_, watpe.HeapType.Any)) => + () // no cast necessary + case Some(receiverType: watpe.RefType) => + fb += wa.RefCast(receiverType) + case _ => + throw new AssertionError(s"Unexpected receiver type $receiverType") + } + + // Load the other parameters + for (argParam <- argParams) + fb += wa.LocalGet(argParam) + + // Call the statically resolved method + fb += wa.ReturnCall(functionID) + + fb.buildAndAddToModule() + } + } + + private def makeDebugName(namespace: UTF8String, exportedName: String): OriginalName = + OriginalName(namespace ++ UTF8String(exportedName)) + + private def makeDebugName(namespace: UTF8String, className: ClassName): OriginalName = + OriginalName(namespace ++ className.encoded) + + private def makeDebugName(namespace: UTF8String, fieldName: FieldName): OriginalName = { + OriginalName( + namespace ++ fieldName.className.encoded ++ dotUTF8String ++ fieldName.simpleName.encoded + ) + } + + private def makeDebugName( + namespace: UTF8String, + className: ClassName, + methodName: MethodName + ): OriginalName = { + // TODO Opt: directly encode the MethodName rather than using nameString + val methodNameUTF8 = UTF8String(methodName.nameString) + OriginalName(namespace ++ className.encoded ++ dotUTF8String ++ methodNameUTF8) + } +} + +object ClassEmitter { + private final class JSClassClosureFunctionID(classNameDebug: ClassName) extends wanme.FunctionID { + override def toString(): String = + s"JSClassClosureFunctionID(${classNameDebug.nameString})" + } + + private val dotUTF8String: UTF8String = UTF8String(".") + + // These particular names are the same as in the JS backend + private object ns { + // Shared with JS backend -- className + methodName + val Public = UTF8String("f.") + val PublicStatic = UTF8String("s.") + val Private = UTF8String("p.") + val PrivateStatic = UTF8String("ps.") + val Constructor = UTF8String("ct.") + val StaticConstructor = UTF8String("sct.") + + // Shared with JS backend -- fieldName + val StaticField = UTF8String("t.") + val PrivateJSField = UTF8String("r.") + + // Shared with JS backend -- className + val ModuleAccessor = UTF8String("m.") + val ModuleInstance = UTF8String("n.") + val JSClassAccessor = UTF8String("a.") + val JSClassValueCache = UTF8String("b.") + val TypeData = UTF8String("d.") + val IsInstance = UTF8String("is.") + + // Shared with JS backend -- string + val TopLevelExport = UTF8String("e.") + val TopLevelExportSetter = UTF8String("u.") + + // Wasm only -- className + methodName + val TableEntry = UTF8String("m.") + + // Wasm only -- fieldName + val InstanceField = UTF8String("f.") + + // Wasm only -- className + val ClassInstance = UTF8String("c.") + val CreateJSClass = UTF8String("c.") + val VTable = UTF8String("v.") + val ITable = UTF8String("it.") + val Clone = UTF8String("clone.") + val NewDefault = UTF8String("new.") + } + + private val thisOriginalName: OriginalName = OriginalName("this") + private val vtableOriginalName: OriginalName = OriginalName("vtable") + private val itablesOriginalName: OriginalName = OriginalName("itables") +} 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 new file mode 100644 index 0000000000..26fb965464 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/CoreWasmLib.scala @@ -0,0 +1,2214 @@ +/* + * 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.backend.wasmemitter + +import org.scalajs.ir.Names._ +import org.scalajs.ir.Trees.{JSUnaryOp, JSBinaryOp, MemberNamespace} +import org.scalajs.ir.Types.{Type => _, ArrayType => _, _} +import org.scalajs.ir.{OriginalName, Position} + +import org.scalajs.linker.backend.webassembly._ +import org.scalajs.linker.backend.webassembly.Instructions._ +import org.scalajs.linker.backend.webassembly.Identitities._ +import org.scalajs.linker.backend.webassembly.Modules._ +import org.scalajs.linker.backend.webassembly.Types._ + +import EmbeddedConstants._ +import VarGen._ +import TypeTransformer._ + +object CoreWasmLib { + import RefType.anyref + + private implicit val noPos: Position = Position.NoPosition + + /** Fields of the `typeData` struct definition. + * + * They are accessible as a public list because they must be repeated in every vtable type + * definition. + * + * @see + * [[VarGen.genFieldID.typeData]], which contains documentation of what is in each field. + */ + val typeDataStructFields: List[StructField] = { + import genFieldID.typeData._ + import RefType.nullable + + def make(id: FieldID, tpe: Type, isMutable: Boolean): StructField = + StructField(id, OriginalName(id.toString()), tpe, isMutable) + + List( + make(nameOffset, Int32, isMutable = false), + make(nameSize, Int32, isMutable = false), + make(nameStringIndex, Int32, isMutable = false), + make(kind, Int32, isMutable = false), + make(specialInstanceTypes, Int32, isMutable = false), + make(strictAncestors, nullable(genTypeID.typeDataArray), isMutable = false), + make(componentType, nullable(genTypeID.typeData), isMutable = false), + make(name, RefType.anyref, isMutable = true), + make(classOfValue, nullable(genTypeID.ClassStruct), isMutable = true), + make(arrayOf, nullable(genTypeID.ObjectVTable), isMutable = true), + make(cloneFunction, nullable(genTypeID.cloneFunctionType), isMutable = false), + make( + isJSClassInstance, + nullable(genTypeID.isJSClassInstanceFuncType), + isMutable = false + ), + make( + reflectiveProxies, + RefType(genTypeID.reflectiveProxies), + isMutable = false + ) + ) + } + + /** 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 + * of `typeData`. + */ + def genPreClasses()(implicit ctx: WasmContext): Unit = { + genPreMainRecTypeDefinitions() + ctx.moduleBuilder.addRecTypeBuilder(ctx.mainRecType) + genCoreTypesInRecType() + + genImports() + + genPrimitiveTypeDataGlobals() + + genHelperDefinitions() + } + + /** Generates definitions that must come *after* the code generated for regular classes. + * + * This notably includes the array class definitions, since they are subtypes of the `jl.Object` + * struct type. + */ + def genPostClasses()(implicit ctx: WasmContext): Unit = { + genArrayClassTypes() + + genBoxedZeroGlobals() + genArrayClassGlobals() + } + + // --- Type definitions --- + + private def genPreMainRecTypeDefinitions()(implicit ctx: WasmContext): Unit = { + val b = ctx.moduleBuilder + + def genUnderlyingArrayType(id: TypeID, elemType: StorageType): Unit = + b.addRecType(id, OriginalName(id.toString()), ArrayType(FieldType(elemType, true))) + + genUnderlyingArrayType(genTypeID.i8Array, Int8) + genUnderlyingArrayType(genTypeID.i16Array, Int16) + genUnderlyingArrayType(genTypeID.i32Array, Int32) + genUnderlyingArrayType(genTypeID.i64Array, Int64) + genUnderlyingArrayType(genTypeID.f32Array, Float32) + genUnderlyingArrayType(genTypeID.f64Array, Float64) + genUnderlyingArrayType(genTypeID.anyArray, anyref) + } + + private def genCoreTypesInRecType()(implicit ctx: WasmContext): Unit = { + def genCoreType(id: TypeID, compositeType: CompositeType): Unit = + ctx.mainRecType.addSubType(id, OriginalName(id.toString()), compositeType) + + genCoreType( + genTypeID.cloneFunctionType, + FunctionType( + List(RefType(genTypeID.ObjectStruct)), + List(RefType(genTypeID.ObjectStruct)) + ) + ) + + genCoreType( + genTypeID.isJSClassInstanceFuncType, + FunctionType(List(RefType.anyref), List(Int32)) + ) + + genCoreType( + genTypeID.typeDataArray, + ArrayType(FieldType(RefType(genTypeID.typeData), isMutable = false)) + ) + genCoreType( + genTypeID.itables, + ArrayType(FieldType(RefType.nullable(HeapType.Struct), isMutable = true)) + ) + genCoreType( + genTypeID.reflectiveProxies, + ArrayType(FieldType(RefType(genTypeID.reflectiveProxy), isMutable = false)) + ) + + ctx.mainRecType.addSubType( + SubType( + genTypeID.typeData, + OriginalName(genTypeID.typeData.toString()), + isFinal = false, + None, + StructType(typeDataStructFields) + ) + ) + + genCoreType( + genTypeID.reflectiveProxy, + StructType( + List( + StructField( + genFieldID.reflectiveProxy.methodID, + OriginalName(genFieldID.reflectiveProxy.methodID.toString()), + Int32, + isMutable = false + ), + StructField( + genFieldID.reflectiveProxy.funcRef, + OriginalName(genFieldID.reflectiveProxy.funcRef.toString()), + RefType(HeapType.Func), + isMutable = false + ) + ) + ) + ) + } + + private def genArrayClassTypes()(implicit ctx: WasmContext): Unit = { + // The vtable type is always the same as j.l.Object + val vtableTypeID = genTypeID.ObjectVTable + val vtableField = StructField( + genFieldID.objStruct.vtable, + OriginalName(genFieldID.objStruct.vtable.toString()), + RefType(vtableTypeID), + isMutable = false + ) + val itablesField = StructField( + genFieldID.objStruct.itables, + OriginalName(genFieldID.objStruct.itables.toString()), + RefType.nullable(genTypeID.itables), + isMutable = false + ) + + val typeRefsWithArrays: List[(TypeID, TypeID)] = + List( + (genTypeID.BooleanArray, genTypeID.i8Array), + (genTypeID.CharArray, genTypeID.i16Array), + (genTypeID.ByteArray, genTypeID.i8Array), + (genTypeID.ShortArray, genTypeID.i16Array), + (genTypeID.IntArray, genTypeID.i32Array), + (genTypeID.LongArray, genTypeID.i64Array), + (genTypeID.FloatArray, genTypeID.f32Array), + (genTypeID.DoubleArray, genTypeID.f64Array), + (genTypeID.ObjectArray, genTypeID.anyArray) + ) + + for ((structTypeID, underlyingArrayTypeID) <- typeRefsWithArrays) { + val origName = OriginalName(structTypeID.toString()) + + val underlyingArrayField = StructField( + genFieldID.objStruct.arrayUnderlying, + OriginalName(genFieldID.objStruct.arrayUnderlying.toString()), + RefType(underlyingArrayTypeID), + isMutable = false + ) + + val superType = genTypeID.ObjectStruct + val structType = StructType( + List(vtableField, itablesField, underlyingArrayField) + ) + val subType = SubType(structTypeID, origName, isFinal = true, Some(superType), structType) + ctx.mainRecType.addSubType(subType) + } + } + + // --- Imports --- + + private def genImports()(implicit ctx: WasmContext): Unit = { + genTagImports() + genGlobalImports() + genHelperImports() + } + + private def genTagImports()(implicit ctx: WasmContext): Unit = { + val exceptionSig = FunctionType(List(RefType.externref), Nil) + val typeID = ctx.moduleBuilder.functionTypeToTypeID(exceptionSig) + ctx.moduleBuilder.addImport( + Import( + "__scalaJSHelpers", + "JSTag", + ImportDesc.Tag( + genTagID.exception, + OriginalName(genTagID.exception.toString()), + typeID + ) + ) + ) + } + + private def genGlobalImports()(implicit ctx: WasmContext): Unit = { + def addGlobalHelperImport(id: genGlobalID.JSHelperGlobalID, tpe: Type): Unit = { + ctx.moduleBuilder.addImport( + Import( + "__scalaJSHelpers", + id.toString(), // import name, guaranteed by JSHelperGlobalID + ImportDesc.Global(id, OriginalName(id.toString()), isMutable = false, tpe) + ) + ) + } + + addGlobalHelperImport(genGlobalID.jsLinkingInfo, RefType.any) + addGlobalHelperImport(genGlobalID.undef, RefType.any) + addGlobalHelperImport(genGlobalID.bFalse, RefType.any) + addGlobalHelperImport(genGlobalID.bZero, RefType.any) + addGlobalHelperImport(genGlobalID.emptyString, RefType.any) + addGlobalHelperImport(genGlobalID.idHashCodeMap, RefType.extern) + } + + private def genHelperImports()(implicit ctx: WasmContext): Unit = { + def addHelperImport(id: genFunctionID.JSHelperFunctionID, + params: List[Type], results: List[Type]): Unit = { + val sig = FunctionType(params, results) + val typeID = ctx.moduleBuilder.functionTypeToTypeID(sig) + ctx.moduleBuilder.addImport( + Import( + "__scalaJSHelpers", + id.toString(), // import name, guaranteed by JSHelperFunctionID + ImportDesc.Func(id, OriginalName(id.toString()), typeID) + ) + ) + } + + addHelperImport(genFunctionID.is, List(anyref, anyref), List(Int32)) + + addHelperImport(genFunctionID.isUndef, List(anyref), List(Int32)) + + for (primRef <- List(BooleanRef, ByteRef, ShortRef, IntRef, FloatRef, DoubleRef)) { + val wasmType = primRef match { + case FloatRef => Float32 + case DoubleRef => Float64 + case _ => Int32 + } + addHelperImport(genFunctionID.box(primRef), List(wasmType), List(anyref)) + addHelperImport(genFunctionID.unbox(primRef), List(anyref), List(wasmType)) + addHelperImport(genFunctionID.typeTest(primRef), List(anyref), List(Int32)) + } + + addHelperImport(genFunctionID.fmod, List(Float64, Float64), List(Float64)) + + addHelperImport( + genFunctionID.closure, + List(RefType.func, anyref), + List(RefType.any) + ) + addHelperImport( + genFunctionID.closureThis, + List(RefType.func, anyref), + List(RefType.any) + ) + addHelperImport( + genFunctionID.closureRest, + List(RefType.func, anyref, Int32), + List(RefType.any) + ) + addHelperImport( + genFunctionID.closureThisRest, + List(RefType.func, anyref, Int32), + List(RefType.any) + ) + + addHelperImport(genFunctionID.makeExportedDef, List(RefType.func), List(RefType.any)) + addHelperImport( + genFunctionID.makeExportedDefRest, + List(RefType.func, Int32), + List(RefType.any) + ) + + addHelperImport(genFunctionID.stringLength, List(RefType.any), List(Int32)) + addHelperImport(genFunctionID.stringCharAt, List(RefType.any, Int32), List(Int32)) + addHelperImport(genFunctionID.jsValueToString, List(RefType.any), List(RefType.any)) + addHelperImport(genFunctionID.jsValueToStringForConcat, List(anyref), List(RefType.any)) + addHelperImport(genFunctionID.booleanToString, List(Int32), List(RefType.any)) + addHelperImport(genFunctionID.charToString, List(Int32), List(RefType.any)) + addHelperImport(genFunctionID.intToString, List(Int32), List(RefType.any)) + addHelperImport(genFunctionID.longToString, List(Int64), List(RefType.any)) + addHelperImport(genFunctionID.doubleToString, List(Float64), List(RefType.any)) + addHelperImport( + genFunctionID.stringConcat, + List(RefType.any, RefType.any), + List(RefType.any) + ) + addHelperImport(genFunctionID.isString, List(anyref), List(Int32)) + + addHelperImport(genFunctionID.jsValueType, List(RefType.any), List(Int32)) + addHelperImport(genFunctionID.bigintHashCode, List(RefType.any), List(Int32)) + addHelperImport( + genFunctionID.symbolDescription, + List(RefType.any), + List(RefType.anyref) + ) + addHelperImport( + genFunctionID.idHashCodeGet, + List(RefType.extern, RefType.any), + List(Int32) + ) + addHelperImport( + genFunctionID.idHashCodeSet, + List(RefType.extern, RefType.any, Int32), + Nil + ) + + addHelperImport(genFunctionID.jsGlobalRefGet, List(RefType.any), List(anyref)) + addHelperImport(genFunctionID.jsGlobalRefSet, List(RefType.any, anyref), Nil) + addHelperImport(genFunctionID.jsGlobalRefTypeof, List(RefType.any), List(RefType.any)) + addHelperImport(genFunctionID.jsNewArray, Nil, List(anyref)) + addHelperImport(genFunctionID.jsArrayPush, List(anyref, anyref), List(anyref)) + addHelperImport( + genFunctionID.jsArraySpreadPush, + List(anyref, anyref), + List(anyref) + ) + addHelperImport(genFunctionID.jsNewObject, Nil, List(anyref)) + addHelperImport( + genFunctionID.jsObjectPush, + List(anyref, anyref, anyref), + List(anyref) + ) + addHelperImport(genFunctionID.jsSelect, List(anyref, anyref), List(anyref)) + addHelperImport(genFunctionID.jsSelectSet, List(anyref, anyref, anyref), Nil) + addHelperImport(genFunctionID.jsNew, List(anyref, anyref), List(anyref)) + addHelperImport(genFunctionID.jsFunctionApply, List(anyref, anyref), List(anyref)) + addHelperImport( + genFunctionID.jsMethodApply, + List(anyref, anyref, anyref), + List(anyref) + ) + addHelperImport(genFunctionID.jsImportCall, List(anyref), List(anyref)) + addHelperImport(genFunctionID.jsImportMeta, Nil, List(anyref)) + addHelperImport(genFunctionID.jsDelete, List(anyref, anyref), Nil) + addHelperImport(genFunctionID.jsForInSimple, List(anyref, anyref), Nil) + addHelperImport(genFunctionID.jsIsTruthy, List(anyref), List(Int32)) + + for ((op, funcID) <- genFunctionID.jsUnaryOps) + addHelperImport(funcID, List(anyref), List(anyref)) + + for ((op, funcID) <- genFunctionID.jsBinaryOps) { + val resultType = + if (op == JSBinaryOp.=== || op == JSBinaryOp.!==) Int32 + else anyref + addHelperImport(funcID, List(anyref, anyref), List(resultType)) + } + + addHelperImport(genFunctionID.newSymbol, Nil, List(anyref)) + addHelperImport( + genFunctionID.createJSClass, + List(anyref, anyref, RefType.func, RefType.func, RefType.func, anyref), + List(RefType.any) + ) + addHelperImport( + genFunctionID.createJSClassRest, + List(anyref, anyref, RefType.func, RefType.func, RefType.func, anyref, Int32), + List(RefType.any) + ) + addHelperImport( + genFunctionID.installJSField, + List(anyref, anyref, anyref), + Nil + ) + addHelperImport( + genFunctionID.installJSMethod, + List(anyref, anyref, anyref, RefType.func, Int32), + Nil + ) + addHelperImport( + genFunctionID.installJSStaticMethod, + List(anyref, anyref, anyref, RefType.func, Int32), + Nil + ) + addHelperImport( + genFunctionID.installJSProperty, + List(anyref, anyref, anyref, RefType.funcref, RefType.funcref), + Nil + ) + addHelperImport( + genFunctionID.installJSStaticProperty, + List(anyref, anyref, anyref, RefType.funcref, RefType.funcref), + Nil + ) + addHelperImport( + genFunctionID.jsSuperSelect, + List(anyref, anyref, anyref), + List(anyref) + ) + addHelperImport( + genFunctionID.jsSuperSelectSet, + List(anyref, anyref, anyref, anyref), + Nil + ) + addHelperImport( + genFunctionID.jsSuperCall, + List(anyref, anyref, anyref, anyref), + List(anyref) + ) + } + + // --- Global definitions --- + + private def genPrimitiveTypeDataGlobals()(implicit ctx: WasmContext): Unit = { + import genFieldID.typeData._ + + val primRefsWithTypeData = List( + VoidRef -> KindVoid, + BooleanRef -> KindBoolean, + CharRef -> KindChar, + ByteRef -> KindByte, + ShortRef -> KindShort, + IntRef -> KindInt, + LongRef -> KindLong, + FloatRef -> KindFloat, + DoubleRef -> KindDouble + ) + + val typeDataTypeID = genTypeID.typeData + + // Other than `name` and `kind`, all the fields have the same value for all primitives + val commonFieldValues = List( + // specialInstanceTypes + I32Const(0), + // strictAncestors + RefNull(HeapType.None), + // componentType + RefNull(HeapType.None), + // name - initially `null`; filled in by the `typeDataName` helper + RefNull(HeapType.None), + // the classOf instance - initially `null`; filled in by the `createClassOf` helper + RefNull(HeapType.None), + // arrayOf, the typeData of an array of this type - initially `null`; filled in by the `arrayTypeData` helper + RefNull(HeapType.None), + // cloneFunction + RefNull(HeapType.NoFunc), + // isJSClassInstance + RefNull(HeapType.NoFunc), + // reflectiveProxies + ArrayNewFixed(genTypeID.reflectiveProxies, 0) + ) + + for ((primRef, kind) <- primRefsWithTypeData) { + val nameDataValue: List[Instr] = + ctx.stringPool.getConstantStringDataInstr(primRef.displayName) + + val instrs: List[Instr] = { + nameDataValue ::: I32Const(kind) :: commonFieldValues ::: + StructNew(genTypeID.typeData) :: Nil + } + + ctx.addGlobal( + Global( + genGlobalID.forVTable(primRef), + OriginalName("d." + primRef.charCode), + isMutable = false, + RefType(genTypeID.typeData), + Expr(instrs) + ) + ) + } + } + + private def genBoxedZeroGlobals()(implicit ctx: WasmContext): Unit = { + val primTypesWithBoxClasses: List[(GlobalID, ClassName, Instr)] = List( + (genGlobalID.bZeroChar, SpecialNames.CharBoxClass, I32Const(0)), + (genGlobalID.bZeroLong, SpecialNames.LongBoxClass, I64Const(0)) + ) + + for ((globalID, boxClassName, zeroValueInstr) <- primTypesWithBoxClasses) { + val boxStruct = genTypeID.forClass(boxClassName) + val instrs: List[Instr] = List( + GlobalGet(genGlobalID.forVTable(boxClassName)), + GlobalGet(genGlobalID.forITable(boxClassName)), + zeroValueInstr, + StructNew(boxStruct) + ) + + ctx.addGlobal( + Global( + globalID, + OriginalName(globalID.toString()), + isMutable = false, + RefType(boxStruct), + Expr(instrs) + ) + ) + } + } + + private def genArrayClassGlobals()(implicit ctx: WasmContext): Unit = { + // Common itable global for all array classes + val itablesInit = List( + I32Const(ctx.itablesLength), + ArrayNewDefault(genTypeID.itables) + ) + ctx.addGlobal( + Global( + genGlobalID.arrayClassITable, + OriginalName(genGlobalID.arrayClassITable.toString()), + isMutable = false, + RefType(genTypeID.itables), + init = Expr(itablesInit) + ) + ) + } + + // --- Function definitions --- + + /** Generates all the helper function definitions of the core Wasm lib. */ + private def genHelperDefinitions()(implicit ctx: WasmContext): Unit = { + genStringLiteral() + genCreateStringFromData() + genTypeDataName() + genCreateClassOf() + genGetClassOf() + genArrayTypeData() + genIsInstance() + genIsAssignableFromExternal() + genIsAssignableFrom() + genCheckCast() + genGetComponentType() + genNewArrayOfThisClass() + genAnyGetClass() + genNewArrayObject() + genIdentityHashCode() + genSearchReflectiveProxy() + genArrayCloneFunctions() + } + + private def newFunctionBuilder(functionID: FunctionID, originalName: OriginalName)( + implicit ctx: WasmContext): FunctionBuilder = { + new FunctionBuilder(ctx.moduleBuilder, functionID, originalName, noPos) + } + + private def newFunctionBuilder(functionID: FunctionID)( + implicit ctx: WasmContext): FunctionBuilder = { + newFunctionBuilder(functionID, OriginalName(functionID.toString())) + } + + private def genStringLiteral()(implicit ctx: WasmContext): Unit = { + val fb = newFunctionBuilder(genFunctionID.stringLiteral) + val offsetParam = fb.addParam("offset", Int32) + val sizeParam = fb.addParam("size", Int32) + val stringIndexParam = fb.addParam("stringIndex", Int32) + fb.setResultType(RefType.any) + + val str = fb.addLocal("str", RefType.any) + + fb.block(RefType.any) { cacheHit => + fb += GlobalGet(genGlobalID.stringLiteralCache) + fb += LocalGet(stringIndexParam) + fb += ArrayGet(genTypeID.anyArray) + + fb += BrOnNonNull(cacheHit) + + // cache miss, create a new string and cache it + fb += GlobalGet(genGlobalID.stringLiteralCache) + fb += LocalGet(stringIndexParam) + + fb += LocalGet(offsetParam) + fb += LocalGet(sizeParam) + fb += ArrayNewData(genTypeID.i16Array, genDataID.string) + fb += Call(genFunctionID.createStringFromData) + fb += LocalTee(str) + fb += ArraySet(genTypeID.anyArray) + + fb += LocalGet(str) + } + + fb.buildAndAddToModule() + } + + /** `createStringFromData: (ref array u16) -> (ref any)` (representing a `string`). */ + private def genCreateStringFromData()(implicit ctx: WasmContext): Unit = { + val dataType = RefType(genTypeID.i16Array) + + val fb = newFunctionBuilder(genFunctionID.createStringFromData) + val dataParam = fb.addParam("data", dataType) + fb.setResultType(RefType.any) + + val lenLocal = fb.addLocal("len", Int32) + val iLocal = fb.addLocal("i", Int32) + val resultLocal = fb.addLocal("result", RefType.any) + + // len := data.length + fb += LocalGet(dataParam) + fb += ArrayLen + fb += LocalSet(lenLocal) + + // i := 0 + fb += I32Const(0) + fb += LocalSet(iLocal) + + // result := "" + fb += GlobalGet(genGlobalID.emptyString) + fb += LocalSet(resultLocal) + + fb.loop() { labelLoop => + // if i == len + fb += LocalGet(iLocal) + fb += LocalGet(lenLocal) + fb += I32Eq + fb.ifThen() { + // then return result + fb += LocalGet(resultLocal) + fb += Return + } + + // result := concat(result, charToString(data(i))) + fb += LocalGet(resultLocal) + fb += LocalGet(dataParam) + fb += LocalGet(iLocal) + fb += ArrayGetU(genTypeID.i16Array) + fb += Call(genFunctionID.charToString) + fb += Call(genFunctionID.stringConcat) + fb += LocalSet(resultLocal) + + // i := i + 1 + fb += LocalGet(iLocal) + fb += I32Const(1) + fb += I32Add + fb += LocalSet(iLocal) + + // loop back to the beginning + fb += Br(labelLoop) + } // end loop $loop + fb += Unreachable + + fb.buildAndAddToModule() + } + + /** `typeDataName: (ref typeData) -> (ref any)` (representing a `string`). + * + * Initializes the `name` field of the given `typeData` if that was not done yet, and returns its + * value. + * + * The computed value is specified by `java.lang.Class.getName()`. See also the documentation on + * [[Names.StructFieldIdx.typeData.name]] for details. + * + * @see + * [[https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Class.html#getName()]] + */ + private def genTypeDataName()(implicit ctx: WasmContext): Unit = { + val typeDataType = RefType(genTypeID.typeData) + val nameDataType = RefType(genTypeID.i16Array) + + val fb = newFunctionBuilder(genFunctionID.typeDataName) + val typeDataParam = fb.addParam("typeData", typeDataType) + fb.setResultType(RefType.any) + + val componentTypeDataLocal = fb.addLocal("componentTypeData", typeDataType) + val componentNameDataLocal = fb.addLocal("componentNameData", nameDataType) + val firstCharLocal = fb.addLocal("firstChar", Int32) + val nameLocal = fb.addLocal("name", RefType.any) + + fb.block(RefType.any) { alreadyInitializedLabel => + // br_on_non_null $alreadyInitialized typeData.name + fb += LocalGet(typeDataParam) + fb += StructGet(genTypeID.typeData, genFieldID.typeData.name) + fb += BrOnNonNull(alreadyInitializedLabel) + + // for the STRUCT_SET typeData.name near the end + fb += LocalGet(typeDataParam) + + // if typeData.kind == KindArray + fb += LocalGet(typeDataParam) + fb += StructGet(genTypeID.typeData, genFieldID.typeData.kind) + fb += I32Const(KindArray) + fb += I32Eq + fb.ifThenElse(RefType.any) { + // it is an array; compute its name from the component type name + + // := "[", for the CALL to stringConcat near the end + fb += I32Const('['.toInt) + fb += Call(genFunctionID.charToString) + + // componentTypeData := ref_as_non_null(typeData.componentType) + fb += LocalGet(typeDataParam) + fb += StructGet( + genTypeID.typeData, + genFieldID.typeData.componentType + ) + fb += RefAsNonNull + fb += LocalSet(componentTypeDataLocal) + + // switch (componentTypeData.kind) + // the result of this switch is the string that must come after "[" + fb.switch(RefType.any) { () => + // scrutinee + fb += LocalGet(componentTypeDataLocal) + fb += StructGet(genTypeID.typeData, genFieldID.typeData.kind) + }( + List(KindBoolean) -> { () => + fb += I32Const('Z'.toInt) + fb += Call(genFunctionID.charToString) + }, + List(KindChar) -> { () => + fb += I32Const('C'.toInt) + fb += Call(genFunctionID.charToString) + }, + List(KindByte) -> { () => + fb += I32Const('B'.toInt) + fb += Call(genFunctionID.charToString) + }, + List(KindShort) -> { () => + fb += I32Const('S'.toInt) + fb += Call(genFunctionID.charToString) + }, + List(KindInt) -> { () => + fb += I32Const('I'.toInt) + fb += Call(genFunctionID.charToString) + }, + List(KindLong) -> { () => + fb += I32Const('J'.toInt) + fb += Call(genFunctionID.charToString) + }, + List(KindFloat) -> { () => + fb += I32Const('F'.toInt) + fb += Call(genFunctionID.charToString) + }, + List(KindDouble) -> { () => + fb += I32Const('D'.toInt) + fb += Call(genFunctionID.charToString) + }, + List(KindArray) -> { () => + // the component type is an array; get its own name + fb += LocalGet(componentTypeDataLocal) + fb += Call(genFunctionID.typeDataName) + } + ) { () => + // default: the component type is neither a primitive nor an array; + // concatenate "L" + + ";" + fb += I32Const('L'.toInt) + fb += Call(genFunctionID.charToString) + fb += LocalGet(componentTypeDataLocal) + fb += Call(genFunctionID.typeDataName) + fb += Call(genFunctionID.stringConcat) + fb += I32Const(';'.toInt) + fb += Call(genFunctionID.charToString) + fb += Call(genFunctionID.stringConcat) + } + + // At this point, the stack contains "[" and the string that must be concatenated with it + fb += Call(genFunctionID.stringConcat) + } { + // it is not an array; its name is stored in nameData + for ( + idx <- List( + genFieldID.typeData.nameOffset, + genFieldID.typeData.nameSize, + genFieldID.typeData.nameStringIndex + ) + ) { + fb += LocalGet(typeDataParam) + fb += StructGet(genTypeID.typeData, idx) + } + fb += Call(genFunctionID.stringLiteral) + } + + // typeData.name := ; leave it on the stack + fb += LocalTee(nameLocal) + fb += StructSet(genTypeID.typeData, genFieldID.typeData.name) + fb += LocalGet(nameLocal) + } + + fb.buildAndAddToModule() + } + + /** `createClassOf: (ref typeData) -> (ref jlClass)`. + * + * Creates the unique `java.lang.Class` instance associated with the given `typeData`, stores it + * in its `classOfValue` field, and returns it. + * + * Must be called only if the `classOfValue` of the typeData is null. All call sites must deal + * with the non-null case as a fast-path. + */ + private def genCreateClassOf()(implicit ctx: WasmContext): Unit = { + val typeDataType = RefType(genTypeID.typeData) + + val fb = newFunctionBuilder(genFunctionID.createClassOf) + val typeDataParam = fb.addParam("typeData", typeDataType) + fb.setResultType(RefType(genTypeID.ClassStruct)) + + val classInstanceLocal = fb.addLocal("classInstance", RefType(genTypeID.ClassStruct)) + + // classInstance := newDefault$java.lang.Class() + // leave it on the stack for the constructor call + 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. + */ + fb += Call(genFunctionID.jsNewObject) + // "__typeData": typeData (TODO hide this better? although nobody will notice anyway) + // (this is used by `isAssignableFromExternal`) + fb ++= ctx.stringPool.getConstantStringInstr("__typeData") + fb += LocalGet(typeDataParam) + fb += Call(genFunctionID.jsObjectPush) + // "name": typeDataName(typeData) + fb ++= ctx.stringPool.getConstantStringInstr("name") + fb += LocalGet(typeDataParam) + fb += Call(genFunctionID.typeDataName) + fb += Call(genFunctionID.jsObjectPush) + // "isPrimitive": (typeData.kind <= KindLastPrimitive) + fb ++= ctx.stringPool.getConstantStringInstr("isPrimitive") + fb += LocalGet(typeDataParam) + fb += StructGet(genTypeID.typeData, genFieldID.typeData.kind) + fb += I32Const(KindLastPrimitive) + fb += I32LeU + fb += Call(genFunctionID.box(BooleanRef)) + fb += Call(genFunctionID.jsObjectPush) + // "isArrayClass": (typeData.kind == KindArray) + fb ++= ctx.stringPool.getConstantStringInstr("isArrayClass") + fb += LocalGet(typeDataParam) + fb += StructGet(genTypeID.typeData, genFieldID.typeData.kind) + fb += I32Const(KindArray) + fb += I32Eq + fb += Call(genFunctionID.box(BooleanRef)) + fb += Call(genFunctionID.jsObjectPush) + // "isInterface": (typeData.kind == KindInterface) + fb ++= ctx.stringPool.getConstantStringInstr("isInterface") + fb += LocalGet(typeDataParam) + fb += StructGet(genTypeID.typeData, genFieldID.typeData.kind) + fb += I32Const(KindInterface) + fb += I32Eq + fb += Call(genFunctionID.box(BooleanRef)) + fb += Call(genFunctionID.jsObjectPush) + // "isInstance": closure(isInstance, typeData) + fb ++= ctx.stringPool.getConstantStringInstr("isInstance") + fb += ctx.refFuncWithDeclaration(genFunctionID.isInstance) + fb += LocalGet(typeDataParam) + fb += Call(genFunctionID.closure) + fb += Call(genFunctionID.jsObjectPush) + // "isAssignableFrom": closure(isAssignableFrom, typeData) + fb ++= ctx.stringPool.getConstantStringInstr("isAssignableFrom") + fb += ctx.refFuncWithDeclaration(genFunctionID.isAssignableFromExternal) + fb += LocalGet(typeDataParam) + fb += Call(genFunctionID.closure) + fb += Call(genFunctionID.jsObjectPush) + // "checkCast": closure(checkCast, typeData) + fb ++= ctx.stringPool.getConstantStringInstr("checkCast") + fb += ctx.refFuncWithDeclaration(genFunctionID.checkCast) + fb += LocalGet(typeDataParam) + fb += Call(genFunctionID.closure) + fb += Call(genFunctionID.jsObjectPush) + // "getComponentType": closure(getComponentType, typeData) + fb ++= ctx.stringPool.getConstantStringInstr("getComponentType") + fb += ctx.refFuncWithDeclaration(genFunctionID.getComponentType) + fb += LocalGet(typeDataParam) + fb += Call(genFunctionID.closure) + fb += Call(genFunctionID.jsObjectPush) + // "newArrayOfThisClass": closure(newArrayOfThisClass, typeData) + fb ++= ctx.stringPool.getConstantStringInstr("newArrayOfThisClass") + fb += ctx.refFuncWithDeclaration(genFunctionID.newArrayOfThisClass) + fb += LocalGet(typeDataParam) + fb += Call(genFunctionID.closure) + fb += Call(genFunctionID.jsObjectPush) + + // Call java.lang.Class::(dataObject) + fb += Call( + genFunctionID.forMethod( + MemberNamespace.Constructor, + ClassClass, + SpecialNames.AnyArgConstructorName + ) + ) + + // typeData.classOfValue := classInstance + fb += LocalGet(typeDataParam) + fb += LocalGet(classInstanceLocal) + fb += StructSet(genTypeID.typeData, genFieldID.typeData.classOfValue) + + // := classInstance for the implicit return + fb += LocalGet(classInstanceLocal) + + fb.buildAndAddToModule() + } + + /** `getClassOf: (ref typeData) -> (ref jlClass)`. + * + * Initializes the `java.lang.Class` instance associated with the given `typeData` if not already + * done, and returns it. + */ + private def genGetClassOf()(implicit ctx: WasmContext): Unit = { + val typeDataType = RefType(genTypeID.typeData) + + val fb = newFunctionBuilder(genFunctionID.getClassOf) + val typeDataParam = fb.addParam("typeData", typeDataType) + fb.setResultType(RefType(genTypeID.ClassStruct)) + + fb.block(RefType(genTypeID.ClassStruct)) { alreadyInitializedLabel => + // fast path + fb += LocalGet(typeDataParam) + fb += StructGet(genTypeID.typeData, genFieldID.typeData.classOfValue) + fb += BrOnNonNull(alreadyInitializedLabel) + // slow path + fb += LocalGet(typeDataParam) + fb += Call(genFunctionID.createClassOf) + } // end bock alreadyInitializedLabel + + fb.buildAndAddToModule() + } + + /** `arrayTypeData: (ref typeData), i32 -> (ref vtable.java.lang.Object)`. + * + * Returns the typeData/vtable of an array with `dims` dimensions over the given typeData. `dims` + * must be be strictly positive. + */ + private def genArrayTypeData()(implicit ctx: WasmContext): Unit = { + val typeDataType = RefType(genTypeID.typeData) + val objectVTableType = RefType(genTypeID.ObjectVTable) + + /* Array classes extend Cloneable, Serializable and Object. + * Filter out the ones that do not have run-time type info at all, as + * we do for other classes. + */ + val strictAncestors = + List(CloneableClass, SerializableClass, ObjectClass) + .filter(name => ctx.getClassInfoOption(name).exists(_.hasRuntimeTypeInfo)) + + val fb = newFunctionBuilder(genFunctionID.arrayTypeData) + val typeDataParam = fb.addParam("typeData", typeDataType) + val dimsParam = fb.addParam("dims", Int32) + fb.setResultType(objectVTableType) + + val arrayTypeDataLocal = fb.addLocal("arrayTypeData", objectVTableType) + + fb.loop() { loopLabel => + fb.block(objectVTableType) { arrayOfIsNonNullLabel => + // br_on_non_null $arrayOfIsNonNull typeData.arrayOf + fb += LocalGet(typeDataParam) + fb += StructGet( + genTypeID.typeData, + genFieldID.typeData.arrayOf + ) + fb += BrOnNonNull(arrayOfIsNonNullLabel) + + // := typeData ; for the .arrayOf := ... later on + fb += LocalGet(typeDataParam) + + // typeData := new typeData(...) + fb += I32Const(0) // nameOffset + fb += I32Const(0) // nameSize + fb += I32Const(0) // nameStringIndex + fb += I32Const(KindArray) // kind = KindArray + fb += I32Const(0) // specialInstanceTypes = 0 + + // strictAncestors + for (strictAncestor <- strictAncestors) + fb += GlobalGet(genGlobalID.forVTable(strictAncestor)) + fb += ArrayNewFixed( + genTypeID.typeDataArray, + strictAncestors.size + ) + + fb += LocalGet(typeDataParam) // componentType + fb += RefNull(HeapType.None) // name + fb += RefNull(HeapType.None) // classOf + fb += RefNull(HeapType.None) // arrayOf + + // clone + fb.switch(RefType(genTypeID.cloneFunctionType)) { () => + fb += LocalGet(typeDataParam) + fb += StructGet(genTypeID.typeData, genFieldID.typeData.kind) + }( + List(KindBoolean) -> { () => + fb += ctx.refFuncWithDeclaration(genFunctionID.cloneArray(BooleanRef)) + }, + List(KindChar) -> { () => + fb += ctx.refFuncWithDeclaration(genFunctionID.cloneArray(CharRef)) + }, + List(KindByte) -> { () => + fb += ctx.refFuncWithDeclaration(genFunctionID.cloneArray(ByteRef)) + }, + List(KindShort) -> { () => + fb += ctx.refFuncWithDeclaration(genFunctionID.cloneArray(ShortRef)) + }, + List(KindInt) -> { () => + fb += ctx.refFuncWithDeclaration(genFunctionID.cloneArray(IntRef)) + }, + List(KindLong) -> { () => + fb += ctx.refFuncWithDeclaration(genFunctionID.cloneArray(LongRef)) + }, + List(KindFloat) -> { () => + fb += ctx.refFuncWithDeclaration(genFunctionID.cloneArray(FloatRef)) + }, + List(KindDouble) -> { () => + fb += ctx.refFuncWithDeclaration(genFunctionID.cloneArray(DoubleRef)) + } + ) { () => + fb += ctx.refFuncWithDeclaration( + genFunctionID.cloneArray(ClassRef(ObjectClass)) + ) + } + + // isJSClassInstance + fb += RefNull(HeapType.NoFunc) + + // reflectiveProxies, empty since all methods of array classes exist in jl.Object + fb += ArrayNewFixed(genTypeID.reflectiveProxies, 0) + + val objectClassInfo = ctx.getClassInfo(ObjectClass) + fb ++= objectClassInfo.tableEntries.map { methodName => + ctx.refFuncWithDeclaration(objectClassInfo.resolvedMethodInfos(methodName).tableEntryID) + } + fb += StructNew(genTypeID.ObjectVTable) + fb += LocalTee(arrayTypeDataLocal) + + // .arrayOf := typeData + fb += StructSet(genTypeID.typeData, genFieldID.typeData.arrayOf) + + // put arrayTypeData back on the stack + fb += LocalGet(arrayTypeDataLocal) + } // end block $arrayOfIsNonNullLabel + + // dims := dims - 1 -- leave dims on the stack + fb += LocalGet(dimsParam) + fb += I32Const(1) + fb += I32Sub + fb += LocalTee(dimsParam) + + // if dims == 0 then + // return typeData.arrayOf (which is on the stack) + fb += I32Eqz + fb.ifThen(FunctionType(List(objectVTableType), List(objectVTableType))) { + fb += Return + } + + // typeData := typeData.arrayOf (which is on the stack), then loop back to the beginning + fb += LocalSet(typeDataParam) + fb += Br(loopLabel) + } // end loop $loop + fb += Unreachable + + fb.buildAndAddToModule() + } + + /** `isInstance: (ref typeData), anyref -> anyref` (a boxed boolean). + * + * Tests whether the given value is a non-null instance of the given type. + * + * Specified by `"isInstance"` at + * [[https://lampwww.epfl.ch/~doeraene/sjsir-semantics/#sec-sjsir-createclassdataof]]. + */ + private def genIsInstance()(implicit ctx: WasmContext): Unit = { + import genFieldID.typeData._ + + val typeDataType = RefType(genTypeID.typeData) + val objectRefType = RefType(genTypeID.ObjectStruct) + + val fb = newFunctionBuilder(genFunctionID.isInstance) + val typeDataParam = fb.addParam("typeData", typeDataType) + val valueParam = fb.addParam("value", RefType.anyref) + fb.setResultType(anyref) + + val valueNonNullLocal = fb.addLocal("valueNonNull", RefType.any) + val specialInstanceTypesLocal = fb.addLocal("specialInstanceTypes", Int32) + + // switch (typeData.kind) + fb.switch(Int32) { () => + fb += LocalGet(typeDataParam) + fb += StructGet(genTypeID.typeData, kind) + }( + // case anyPrimitiveKind => false + (KindVoid to KindLastPrimitive).toList -> { () => + fb += I32Const(0) + }, + // case KindObject => value ne null + List(KindObject) -> { () => + fb += LocalGet(valueParam) + fb += RefIsNull + fb += I32Eqz + }, + // for each boxed class, the corresponding primitive type test + List(KindBoxedUnit) -> { () => + fb += LocalGet(valueParam) + fb += Call(genFunctionID.isUndef) + }, + List(KindBoxedBoolean) -> { () => + fb += LocalGet(valueParam) + fb += Call(genFunctionID.typeTest(BooleanRef)) + }, + List(KindBoxedCharacter) -> { () => + fb += LocalGet(valueParam) + val structTypeID = genTypeID.forClass(SpecialNames.CharBoxClass) + fb += RefTest(RefType(structTypeID)) + }, + List(KindBoxedByte) -> { () => + fb += LocalGet(valueParam) + fb += Call(genFunctionID.typeTest(ByteRef)) + }, + List(KindBoxedShort) -> { () => + fb += LocalGet(valueParam) + fb += Call(genFunctionID.typeTest(ShortRef)) + }, + List(KindBoxedInteger) -> { () => + fb += LocalGet(valueParam) + fb += Call(genFunctionID.typeTest(IntRef)) + }, + List(KindBoxedLong) -> { () => + fb += LocalGet(valueParam) + val structTypeID = genTypeID.forClass(SpecialNames.LongBoxClass) + fb += RefTest(RefType(structTypeID)) + }, + List(KindBoxedFloat) -> { () => + fb += LocalGet(valueParam) + fb += Call(genFunctionID.typeTest(FloatRef)) + }, + List(KindBoxedDouble) -> { () => + fb += LocalGet(valueParam) + fb += Call(genFunctionID.typeTest(DoubleRef)) + }, + List(KindBoxedString) -> { () => + fb += LocalGet(valueParam) + fb += Call(genFunctionID.isString) + }, + // case KindJSType => call typeData.isJSClassInstance(value) or throw if it is null + List(KindJSType) -> { () => + fb.block(RefType.anyref) { isJSClassInstanceIsNull => + // Load value as the argument to the function + fb += LocalGet(valueParam) + + // Load the function reference; break if null + fb += LocalGet(typeDataParam) + fb += StructGet(genTypeID.typeData, isJSClassInstance) + fb += BrOnNull(isJSClassInstanceIsNull) + + // Call the function + fb += CallRef(genTypeID.isJSClassInstanceFuncType) + fb += Call(genFunctionID.box(BooleanRef)) + fb += Return + } + fb += Drop // drop `value` which was left on the stack + + // throw new TypeError("...") + fb ++= ctx.stringPool.getConstantStringInstr("TypeError") + fb += Call(genFunctionID.jsGlobalRefGet) + fb += Call(genFunctionID.jsNewArray) + fb ++= ctx.stringPool.getConstantStringInstr( + "Cannot call isInstance() on a Class representing a JS trait/object" + ) + fb += Call(genFunctionID.jsArrayPush) + fb += Call(genFunctionID.jsNew) + fb += ExternConvertAny + fb += Throw(genTagID.exception) + } + ) { () => + // case _ => + + // valueNonNull := as_non_null value; return false if null + fb.block(RefType.any) { nonNullLabel => + fb += LocalGet(valueParam) + fb += BrOnNonNull(nonNullLabel) + fb += GlobalGet(genGlobalID.bFalse) + fb += Return + } + fb += LocalSet(valueNonNullLocal) + + /* If `typeData` represents an ancestor of a hijacked classes, we have to + * answer `true` if `valueNonNull` is a primitive instance of any of the + * hijacked classes that ancestor class/interface. For example, for + * `Comparable`, we have to answer `true` if `valueNonNull` is a primitive + * boolean, number or string. + * + * To do that, we use `jsValueType` and `typeData.specialInstanceTypes`. + * + * We test whether `jsValueType(valueNonNull)` is in the set represented by + * `specialInstanceTypes`. Since the latter is a bitset where the bit + * indices correspond to the values returned by `jsValueType`, we have to + * test whether + * + * ((1 << jsValueType(valueNonNull)) & specialInstanceTypes) != 0 + * + * Since computing `jsValueType` is somewhat expensive, we first test + * whether `specialInstanceTypes != 0` before calling `jsValueType`. + * + * There is a more elaborated concrete example of this algorithm in + * `genInstanceTest`. + */ + fb += LocalGet(typeDataParam) + fb += StructGet(genTypeID.typeData, specialInstanceTypes) + fb += LocalTee(specialInstanceTypesLocal) + fb += I32Const(0) + fb += I32Ne + fb.ifThen() { + // Load (1 << jsValueType(valueNonNull)) + fb += I32Const(1) + fb += LocalGet(valueNonNullLocal) + fb += Call(genFunctionID.jsValueType) + fb += I32Shl + + // if ((... & specialInstanceTypes) != 0) + fb += LocalGet(specialInstanceTypesLocal) + fb += I32And + fb += I32Const(0) + fb += I32Ne + fb.ifThen() { + // then return true + fb += I32Const(1) + fb += Call(genFunctionID.box(BooleanRef)) + fb += Return + } + } + + // Get the vtable and delegate to isAssignableFrom + + // Load typeData + fb += LocalGet(typeDataParam) + + // Load the vtable; return false if it is not one of our object + fb.block(objectRefType) { ourObjectLabel => + // Try cast to jl.Object + fb += LocalGet(valueNonNullLocal) + fb += BrOnCast(ourObjectLabel, RefType.any, objectRefType) + + // on cast fail, return false + fb += GlobalGet(genGlobalID.bFalse) + fb += Return + } + fb += StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.vtable) + + // Call isAssignableFrom + fb += Call(genFunctionID.isAssignableFrom) + } + + fb += Call(genFunctionID.box(BooleanRef)) + + fb.buildAndAddToModule() + } + + /** `isAssignableFromExternal: (ref typeData), anyref -> i32` (a boolean). + * + * This is the underlying func for the `isAssignableFrom()` closure inside class data objects. + */ + private def genIsAssignableFromExternal()(implicit ctx: WasmContext): Unit = { + val typeDataType = RefType(genTypeID.typeData) + + val fb = newFunctionBuilder(genFunctionID.isAssignableFromExternal) + val typeDataParam = fb.addParam("typeData", typeDataType) + val fromParam = fb.addParam("from", RefType.anyref) + fb.setResultType(anyref) + + // load typeData + fb += LocalGet(typeDataParam) + + // load ref.cast from["__typeData"] (as a JS selection) + fb += LocalGet(fromParam) + fb ++= ctx.stringPool.getConstantStringInstr("__typeData") + fb += Call(genFunctionID.jsSelect) + fb += RefCast(RefType(typeDataType.heapType)) + + // delegate to isAssignableFrom + fb += Call(genFunctionID.isAssignableFrom) + fb += Call(genFunctionID.box(BooleanRef)) + + fb.buildAndAddToModule() + } + + /** `isAssignableFrom: (ref typeData), (ref typeData) -> i32` (a boolean). + * + * Specified by `java.lang.Class.isAssignableFrom(Class)`. + */ + private def genIsAssignableFrom()(implicit ctx: WasmContext): Unit = { + import genFieldID.typeData._ + + val typeDataType = RefType(genTypeID.typeData) + + val fb = newFunctionBuilder(genFunctionID.isAssignableFrom) + val typeDataParam = fb.addParam("typeData", typeDataType) + val fromTypeDataParam = fb.addParam("fromTypeData", typeDataType) + fb.setResultType(Int32) + + val fromAncestorsLocal = fb.addLocal("fromAncestors", RefType(genTypeID.typeDataArray)) + val lenLocal = fb.addLocal("len", Int32) + val iLocal = fb.addLocal("i", Int32) + + // if (fromTypeData eq typeData) + fb += LocalGet(fromTypeDataParam) + fb += LocalGet(typeDataParam) + fb += RefEq + fb.ifThen() { + // then return true + fb += I32Const(1) + fb += Return + } + + // "Tail call" loop for diving into array component types + fb.loop(Int32) { loopForArrayLabel => + // switch (typeData.kind) + fb.switch(Int32) { () => + // typeData.kind + fb += LocalGet(typeDataParam) + fb += StructGet(genTypeID.typeData, kind) + }( + // case anyPrimitiveKind => return false + (KindVoid to KindLastPrimitive).toList -> { () => + fb += I32Const(0) + }, + // case KindArray => check that from is an array, recurse into component types + List(KindArray) -> { () => + fb.block() { fromComponentTypeIsNullLabel => + // fromTypeData := fromTypeData.componentType; jump out if null + fb += LocalGet(fromTypeDataParam) + fb += StructGet(genTypeID.typeData, componentType) + fb += BrOnNull(fromComponentTypeIsNullLabel) + fb += LocalSet(fromTypeDataParam) + + // typeData := ref.as_non_null typeData.componentType (OK because KindArray) + fb += LocalGet(typeDataParam) + fb += StructGet(genTypeID.typeData, componentType) + fb += RefAsNonNull + fb += LocalSet(typeDataParam) + + // loop back ("tail call") + fb += Br(loopForArrayLabel) + } + + // return false + fb += I32Const(0) + }, + // case KindObject => return (fromTypeData.kind > KindLastPrimitive) + List(KindObject) -> { () => + fb += LocalGet(fromTypeDataParam) + fb += StructGet(genTypeID.typeData, kind) + fb += I32Const(KindLastPrimitive) + fb += I32GtU + } + ) { () => + // All other cases: test whether `fromTypeData.strictAncestors` contains `typeData` + + fb.block() { fromAncestorsIsNullLabel => + // fromAncestors := fromTypeData.strictAncestors; go to fromAncestorsIsNull if null + fb += LocalGet(fromTypeDataParam) + fb += StructGet(genTypeID.typeData, strictAncestors) + fb += BrOnNull(fromAncestorsIsNullLabel) + fb += LocalTee(fromAncestorsLocal) + + // if fromAncestors contains typeData, return true + + // len := fromAncestors.length + fb += ArrayLen + fb += LocalSet(lenLocal) + + // i := 0 + fb += I32Const(0) + fb += LocalSet(iLocal) + + // while (i != len) + fb.whileLoop() { + fb += LocalGet(iLocal) + fb += LocalGet(lenLocal) + fb += I32Ne + } { + // if (fromAncestors[i] eq typeData) + fb += LocalGet(fromAncestorsLocal) + fb += LocalGet(iLocal) + fb += ArrayGet(genTypeID.typeDataArray) + fb += LocalGet(typeDataParam) + fb += RefEq + fb.ifThen() { + // then return true + fb += I32Const(1) + fb += Return + } + + // i := i + 1 + fb += LocalGet(iLocal) + fb += I32Const(1) + fb += I32Add + fb += LocalSet(iLocal) + } + } + + // from.strictAncestors is null or does not contain typeData + // return false + fb += I32Const(0) + } + } + + fb.buildAndAddToModule() + } + + /** `checkCast: (ref typeData), anyref -> anyref`. + * + * Casts the given value to the given type; subject to undefined behaviors. + */ + private def genCheckCast()(implicit ctx: WasmContext): Unit = { + val typeDataType = RefType(genTypeID.typeData) + + val fb = newFunctionBuilder(genFunctionID.checkCast) + val typeDataParam = fb.addParam("typeData", typeDataType) + val valueParam = fb.addParam("value", RefType.anyref) + fb.setResultType(RefType.anyref) + + /* Given that we only implement `CheckedBehavior.Unchecked` semantics for + * now, this is always the identity. + */ + + fb += LocalGet(valueParam) + + fb.buildAndAddToModule() + } + + /** `getComponentType: (ref typeData) -> (ref null jlClass)`. + * + * This is the underlying func for the `getComponentType()` closure inside class data objects. + */ + private def genGetComponentType()(implicit ctx: WasmContext): Unit = { + val typeDataType = RefType(genTypeID.typeData) + + val fb = newFunctionBuilder(genFunctionID.getComponentType) + val typeDataParam = fb.addParam("typeData", typeDataType) + 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 += StructGet(genTypeID.typeData, genFieldID.typeData.componentType) + fb += BrOnNull(nullResultLabel) + // Get the corresponding classOf + fb += Call(genFunctionID.getClassOf) + fb += Return + } // end block nullResultLabel + fb += RefNull(HeapType(genTypeID.ClassStruct)) + + fb.buildAndAddToModule() + } + + /** `newArrayOfThisClass: (ref typeData), anyref -> (ref jlObject)`. + * + * This is the underlying func for the `newArrayOfThisClass()` closure inside class data objects. + */ + private def genNewArrayOfThisClass()(implicit ctx: WasmContext): Unit = { + val typeDataType = RefType(genTypeID.typeData) + val i32ArrayType = RefType(genTypeID.i32Array) + + val fb = newFunctionBuilder(genFunctionID.newArrayOfThisClass) + val typeDataParam = fb.addParam("typeData", typeDataType) + val lengthsParam = fb.addParam("lengths", RefType.anyref) + 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 + fb += LocalGet(lengthsParam) + fb ++= ctx.stringPool.getConstantStringInstr("length") + fb += Call(genFunctionID.jsSelect) + fb += Call(genFunctionID.unbox(IntRef)) + fb += LocalTee(lengthsLenLocal) + + // lengthsValues := array.new lengthsLen + fb += ArrayNewDefault(genTypeID.i32Array) + fb += LocalSet(lengthsValuesLocal) + + // i := 0 + fb += I32Const(0) + fb += LocalSet(iLocal) + + // 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) + + fb += LocalGet(lengthsValuesLocal) + fb += LocalGet(iLocal) + + fb += LocalGet(lengthsParam) + fb += LocalGet(iLocal) + fb += RefI31 + fb += Call(genFunctionID.jsSelect) + fb += Call(genFunctionID.unbox(IntRef)) + + fb += ArraySet(genTypeID.i32Array) + + // i += 1 + fb += LocalGet(iLocal) + fb += I32Const(1) + fb += I32Add + fb += LocalSet(iLocal) + } + + // 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() + } + + /** `anyGetClass: (ref any) -> (ref null jlClass)`. + * + * This is the implementation of `value.getClass()` when `value` can be an instance of a hijacked + * class, i.e., a primitive. + * + * For `number`s, the result is based on the actual value, as specified by + * [[https://www.scala-js.org/doc/semantics.html#getclass]]. + */ + private def genAnyGetClass()(implicit ctx: WasmContext): Unit = { + val typeDataType = RefType(genTypeID.typeData) + + val fb = newFunctionBuilder(genFunctionID.anyGetClass) + val valueParam = fb.addParam("value", RefType.any) + fb.setResultType(RefType.nullable(genTypeID.ClassStruct)) + + val typeDataLocal = fb.addLocal("typeData", typeDataType) + val doubleValueLocal = fb.addLocal("doubleValue", Float64) + val intValueLocal = fb.addLocal("intValue", Int32) + val ourObjectLocal = fb.addLocal("ourObject", RefType(genTypeID.ObjectStruct)) + + def getHijackedClassTypeDataInstr(className: ClassName): Instr = + GlobalGet(genGlobalID.forVTable(className)) + + fb.block(RefType.nullable(genTypeID.ClassStruct)) { nonNullClassOfLabel => + fb.block(typeDataType) { gotTypeDataLabel => + fb.block(RefType(genTypeID.ObjectStruct)) { ourObjectLabel => + // if value is our object, jump to $ourObject + fb += LocalGet(valueParam) + fb += BrOnCast( + ourObjectLabel, + RefType.any, + RefType(genTypeID.ObjectStruct) + ) + + // switch(jsValueType(value)) { ... } + fb.switch(typeDataType) { () => + // scrutinee + fb += LocalGet(valueParam) + fb += Call(genFunctionID.jsValueType) + }( + // case JSValueTypeFalse, JSValueTypeTrue => typeDataOf[jl.Boolean] + List(JSValueTypeFalse, JSValueTypeTrue) -> { () => + fb += getHijackedClassTypeDataInstr(BoxedBooleanClass) + }, + // case JSValueTypeString => typeDataOf[jl.String] + List(JSValueTypeString) -> { () => + fb += getHijackedClassTypeDataInstr(BoxedStringClass) + }, + // case JSValueTypeNumber => ... + List(JSValueTypeNumber) -> { () => + /* For `number`s, the result is based on the actual value, as specified by + * [[https://www.scala-js.org/doc/semantics.html#getclass]]. + */ + + // doubleValue := unboxDouble(value) + fb += LocalGet(valueParam) + fb += Call(genFunctionID.unbox(DoubleRef)) + fb += LocalTee(doubleValueLocal) + + // intValue := doubleValue.toInt + fb += I32TruncSatF64S + fb += LocalTee(intValueLocal) + + // if same(intValue.toDouble, doubleValue) -- same bit pattern to avoid +0.0 == -0.0 + fb += F64ConvertI32S + fb += I64ReinterpretF64 + fb += LocalGet(doubleValueLocal) + fb += I64ReinterpretF64 + fb += I64Eq + fb.ifThenElse(typeDataType) { + // then it is a Byte, a Short, or an Integer + + // if intValue.toByte.toInt == intValue + fb += LocalGet(intValueLocal) + fb += I32Extend8S + fb += LocalGet(intValueLocal) + fb += I32Eq + fb.ifThenElse(typeDataType) { + // then it is a Byte + fb += getHijackedClassTypeDataInstr(BoxedByteClass) + } { + // else, if intValue.toShort.toInt == intValue + fb += LocalGet(intValueLocal) + fb += I32Extend16S + fb += LocalGet(intValueLocal) + fb += I32Eq + fb.ifThenElse(typeDataType) { + // then it is a Short + fb += getHijackedClassTypeDataInstr(BoxedShortClass) + } { + // else, it is an Integer + fb += getHijackedClassTypeDataInstr(BoxedIntegerClass) + } + } + } { + // else, it is a Float or a Double + + // if doubleValue.toFloat.toDouble == doubleValue + fb += LocalGet(doubleValueLocal) + fb += F32DemoteF64 + fb += F64PromoteF32 + fb += LocalGet(doubleValueLocal) + fb += F64Eq + fb.ifThenElse(typeDataType) { + // then it is a Float + fb += getHijackedClassTypeDataInstr(BoxedFloatClass) + } { + // else, if it is NaN + fb += LocalGet(doubleValueLocal) + fb += LocalGet(doubleValueLocal) + fb += F64Ne + fb.ifThenElse(typeDataType) { + // then it is a Float + fb += getHijackedClassTypeDataInstr(BoxedFloatClass) + } { + // else, it is a Double + fb += getHijackedClassTypeDataInstr(BoxedDoubleClass) + } + } + } + }, + // case JSValueTypeUndefined => typeDataOf[jl.Void] + List(JSValueTypeUndefined) -> { () => + fb += getHijackedClassTypeDataInstr(BoxedUnitClass) + } + ) { () => + // case _ (JSValueTypeOther) => return null + fb += RefNull(HeapType(genTypeID.ClassStruct)) + fb += Return + } + + fb += Br(gotTypeDataLabel) + } + + /* Now we have one of our objects. Normally we only have to get the + * vtable, but there are two exceptions. If the value is an instance of + * `jl.CharacterBox` or `jl.LongBox`, we must use the typeData of + * `jl.Character` or `jl.Long`, respectively. + */ + fb += LocalTee(ourObjectLocal) + fb += RefTest(RefType(genTypeID.forClass(SpecialNames.CharBoxClass))) + fb.ifThenElse(typeDataType) { + fb += getHijackedClassTypeDataInstr(BoxedCharacterClass) + } { + fb += LocalGet(ourObjectLocal) + fb += RefTest(RefType(genTypeID.forClass(SpecialNames.LongBoxClass))) + fb.ifThenElse(typeDataType) { + fb += getHijackedClassTypeDataInstr(BoxedLongClass) + } { + fb += LocalGet(ourObjectLocal) + fb += StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.vtable) + } + } + } + + fb += Call(genFunctionID.getClassOf) + } + + 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.nullable(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) + * 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) + + // 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()` + * of primitive values when dispatch is required (i.e., when the receiver type is not known to be + * a specific primitive or hijacked class), so it must be consistent with the implementations of + * `hashCode()` in hijacked classes. + * + * For `String` and `Double`, we actually call the hijacked class methods, as they are a bit + * involved. For `Boolean` and `Void`, we hard-code a copy here. + */ + private def genIdentityHashCode()(implicit ctx: WasmContext): Unit = { + import MemberNamespace.Public + import SpecialNames.hashCodeMethodName + import genFieldID.typeData._ + + // A global exclusively used by this function + ctx.addGlobal( + Global( + genGlobalID.lastIDHashCode, + OriginalName(genGlobalID.lastIDHashCode.toString()), + isMutable = true, + Int32, + Expr(List(I32Const(0))) + ) + ) + + val fb = newFunctionBuilder(genFunctionID.identityHashCode) + val objParam = fb.addParam("obj", RefType.anyref) + fb.setResultType(Int32) + + val objNonNullLocal = fb.addLocal("objNonNull", RefType.any) + val resultLocal = fb.addLocal("result", Int32) + + // If `obj` is `null`, return 0 (by spec) + fb.block(RefType.any) { nonNullLabel => + fb += LocalGet(objParam) + fb += BrOnNonNull(nonNullLabel) + fb += I32Const(0) + fb += Return + } + fb += LocalTee(objNonNullLocal) + + // If `obj` is one of our objects, skip all the jsValueType tests + fb += RefTest(RefType(genTypeID.ObjectStruct)) + fb += I32Eqz + fb.ifThen() { + fb.switch() { () => + fb += LocalGet(objNonNullLocal) + fb += Call(genFunctionID.jsValueType) + }( + List(JSValueTypeFalse) -> { () => + fb += I32Const(1237) // specified by jl.Boolean.hashCode() + fb += Return + }, + List(JSValueTypeTrue) -> { () => + fb += I32Const(1231) // specified by jl.Boolean.hashCode() + fb += Return + }, + List(JSValueTypeString) -> { () => + fb += LocalGet(objNonNullLocal) + fb += Call( + genFunctionID.forMethod(Public, BoxedStringClass, hashCodeMethodName) + ) + fb += Return + }, + List(JSValueTypeNumber) -> { () => + fb += LocalGet(objNonNullLocal) + fb += Call(genFunctionID.unbox(DoubleRef)) + fb += Call( + genFunctionID.forMethod(Public, BoxedDoubleClass, hashCodeMethodName) + ) + fb += Return + }, + List(JSValueTypeUndefined) -> { () => + fb += I32Const(0) // specified by jl.Void.hashCode(), Scala.js only + fb += Return + }, + List(JSValueTypeBigInt) -> { () => + fb += LocalGet(objNonNullLocal) + fb += Call(genFunctionID.bigintHashCode) + fb += Return + }, + List(JSValueTypeSymbol) -> { () => + fb.block() { descriptionIsNullLabel => + fb += LocalGet(objNonNullLocal) + fb += Call(genFunctionID.symbolDescription) + fb += BrOnNull(descriptionIsNullLabel) + fb += Call( + genFunctionID.forMethod(Public, BoxedStringClass, hashCodeMethodName) + ) + fb += Return + } + fb += I32Const(0) + fb += Return + } + ) { () => + // JSValueTypeOther -- fall through to using idHashCodeMap + () + } + } + + // If we get here, use the idHashCodeMap + + // Read the existing idHashCode, if one exists + fb += GlobalGet(genGlobalID.idHashCodeMap) + fb += LocalGet(objNonNullLocal) + fb += Call(genFunctionID.idHashCodeGet) + fb += LocalTee(resultLocal) + + // If it is 0, there was no recorded idHashCode yet; allocate a new one + fb += I32Eqz + fb.ifThen() { + // Allocate a new idHashCode + fb += GlobalGet(genGlobalID.lastIDHashCode) + fb += I32Const(1) + fb += I32Add + fb += LocalTee(resultLocal) + fb += GlobalSet(genGlobalID.lastIDHashCode) + + // Store it for next time + fb += GlobalGet(genGlobalID.idHashCodeMap) + fb += LocalGet(objNonNullLocal) + fb += LocalGet(resultLocal) + fb += Call(genFunctionID.idHashCodeSet) + } + + fb += LocalGet(resultLocal) + + fb.buildAndAddToModule() + } + + /** Search for a reflective proxy function with the given `methodId` in the `reflectiveProxies` + * field in `typeData` and returns the corresponding function reference. + * + * `searchReflectiveProxy`: [typeData, i32] -> [(ref func)] + */ + private def genSearchReflectiveProxy()(implicit ctx: WasmContext): Unit = { + import genFieldID.typeData._ + + val typeDataType = RefType(genTypeID.typeData) + + val fb = newFunctionBuilder(genFunctionID.searchReflectiveProxy) + val typeDataParam = fb.addParam("typeData", typeDataType) + val methodIDParam = fb.addParam("methodID", Int32) + fb.setResultType(RefType(HeapType.Func)) + + val reflectiveProxies = + fb.addLocal("reflectiveProxies", Types.RefType(genTypeID.reflectiveProxies)) + val startLocal = fb.addLocal("start", Types.Int32) + val endLocal = fb.addLocal("end", Types.Int32) + val midLocal = fb.addLocal("mid", Types.Int32) + val entryLocal = fb.addLocal("entry", Types.RefType(genTypeID.reflectiveProxy)) + + /* This function implements a binary search. Unlike the typical binary search, + * it does not stop early if it happens to exactly hit the target ID. + * Instead, it systematically reduces the search range until it contains at + * most one element. At that point, it checks whether it is the ID we are + * looking for. + * + * We do this in the name of predictability, in order to avoid performance + * cliffs. It avoids the scenario where a codebase happens to be fast + * because a particular reflective call resolves in Θ(1), but where adding + * or removing something completely unrelated somewhere else in the + * codebase pushes it to a different slot where it resolves in Θ(log n). + * + * This function is therefore intentionally Θ(log n), not merely O(log n). + */ + + fb += LocalGet(typeDataParam) + fb += StructGet(genTypeID.typeData, genFieldID.typeData.reflectiveProxies) + fb += LocalTee(reflectiveProxies) + + // end := reflectiveProxies.length + fb += ArrayLen + fb += LocalSet(endLocal) + + // start := 0 + fb += I32Const(0) + fb += LocalSet(startLocal) + + // while (start + 1 < end) + fb.whileLoop() { + fb += LocalGet(startLocal) + fb += I32Const(1) + fb += I32Add + fb += LocalGet(endLocal) + fb += I32LtU + } { + // mid := (start + end) >>> 1 + fb += LocalGet(startLocal) + fb += LocalGet(endLocal) + fb += I32Add + fb += I32Const(1) + fb += I32ShrU + fb += LocalSet(midLocal) + + // if (methodID < reflectiveProxies[mid].methodID) + fb += LocalGet(methodIDParam) + fb += LocalGet(reflectiveProxies) + fb += LocalGet(midLocal) + fb += ArrayGet(genTypeID.reflectiveProxies) + fb += StructGet(genTypeID.reflectiveProxy, genFieldID.reflectiveProxy.methodID) + fb += I32LtU + fb.ifThenElse() { + // then end := mid + fb += LocalGet(midLocal) + fb += LocalSet(endLocal) + } { + // else start := mid + fb += LocalGet(midLocal) + fb += LocalSet(startLocal) + } + } + + // if (start < end) + fb += LocalGet(startLocal) + fb += LocalGet(endLocal) + fb += I32LtU + fb.ifThen() { + // entry := reflectiveProxies[start] + fb += LocalGet(reflectiveProxies) + fb += LocalGet(startLocal) + fb += ArrayGet(genTypeID.reflectiveProxies) + fb += LocalTee(entryLocal) + + // if (entry.methodID == methodID) + fb += StructGet(genTypeID.reflectiveProxy, genFieldID.reflectiveProxy.methodID) + fb += LocalGet(methodIDParam) + fb += I32Eq + fb.ifThen() { + // return entry.funcRef + fb += LocalGet(entryLocal) + fb += StructGet(genTypeID.reflectiveProxy, genFieldID.reflectiveProxy.funcRef) + fb += Return + } + } + + // throw new TypeError("...") + fb ++= ctx.stringPool.getConstantStringInstr("TypeError") + fb += Call(genFunctionID.jsGlobalRefGet) + fb += Call(genFunctionID.jsNewArray) + // Originally, exception is thrown from JS saying e.g. "obj2.z1__ is not a function" + // TODO Improve the error message to include some information about the missing method + fb ++= ctx.stringPool.getConstantStringInstr("Method not found") + fb += Call(genFunctionID.jsArrayPush) + fb += Call(genFunctionID.jsNew) + fb += ExternConvertAny + fb += Throw(genTagID.exception) + + fb.buildAndAddToModule() + } + + private def genArrayCloneFunctions()(implicit ctx: WasmContext): Unit = { + val baseRefs = List( + BooleanRef, + CharRef, + ByteRef, + ShortRef, + IntRef, + LongRef, + FloatRef, + DoubleRef, + ClassRef(ObjectClass) + ) + + for (baseRef <- baseRefs) + genArrayCloneFunction(baseRef) + } + + /** Generates the clone function for the array class with the given base. */ + private def genArrayCloneFunction(baseRef: NonArrayTypeRef)(implicit ctx: WasmContext): Unit = { + val charCodeForOriginalName = baseRef match { + case baseRef: PrimRef => baseRef.charCode + case _: ClassRef => 'O' + } + val originalName = OriginalName("cloneArray." + charCodeForOriginalName) + + val fb = newFunctionBuilder(genFunctionID.cloneArray(baseRef), originalName) + val fromParam = fb.addParam("from", RefType(genTypeID.ObjectStruct)) + fb.setResultType(RefType(genTypeID.ObjectStruct)) + fb.setFunctionType(genTypeID.cloneFunctionType) + + val arrayTypeRef = ArrayTypeRef(baseRef, 1) + + val arrayStructTypeID = genTypeID.forArrayClass(arrayTypeRef) + val arrayClassType = RefType(arrayStructTypeID) + + val underlyingArrayTypeID = genTypeID.underlyingOf(arrayTypeRef) + val underlyingArrayType = RefType(underlyingArrayTypeID) + + val fromLocal = fb.addLocal("fromTyped", arrayClassType) + val fromUnderlyingLocal = fb.addLocal("fromUnderlying", underlyingArrayType) + val lengthLocal = fb.addLocal("length", Int32) + val resultUnderlyingLocal = fb.addLocal("resultUnderlying", underlyingArrayType) + + // Cast down the from argument + fb += LocalGet(fromParam) + fb += RefCast(arrayClassType) + fb += LocalTee(fromLocal) + + // Load the underlying array + fb += StructGet(arrayStructTypeID, genFieldID.objStruct.arrayUnderlying) + fb += LocalTee(fromUnderlyingLocal) + + // Make a copy of the underlying array + fb += ArrayLen + fb += LocalTee(lengthLocal) + fb += ArrayNewDefault(underlyingArrayTypeID) + fb += LocalTee(resultUnderlyingLocal) // also dest for array.copy + fb += I32Const(0) // destOffset + fb += LocalGet(fromUnderlyingLocal) // src + fb += I32Const(0) // srcOffset + fb += LocalGet(lengthLocal) // length + fb += ArrayCopy(underlyingArrayTypeID, underlyingArrayTypeID) + + // Build the result arrayStruct + fb += LocalGet(fromLocal) + fb += StructGet(arrayStructTypeID, genFieldID.objStruct.vtable) // vtable + fb += GlobalGet(genGlobalID.arrayClassITable) // itable + fb += LocalGet(resultUnderlyingLocal) + fb += StructNew(arrayStructTypeID) + + fb.buildAndAddToModule() + } + +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/DerivedClasses.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/DerivedClasses.scala new file mode 100644 index 0000000000..b7e0a3cf91 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/DerivedClasses.scala @@ -0,0 +1,151 @@ +/* + * 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.backend.wasmemitter + +import scala.concurrent.{ExecutionContext, Future} + +import org.scalajs.ir.ClassKind._ +import org.scalajs.ir.Names._ +import org.scalajs.ir.OriginalName +import org.scalajs.ir.OriginalName.NoOriginalName +import org.scalajs.ir.Position +import org.scalajs.ir.Trees._ +import org.scalajs.ir.Types._ +import org.scalajs.ir.{EntryPointsInfo, Version} + +import org.scalajs.linker.interface.IRFile +import org.scalajs.linker.interface.unstable.IRFileImpl + +import org.scalajs.linker.standard.LinkedClass + +import SpecialNames._ + +/** Derives `CharacterBox` and `LongBox` from `jl.Character` and `jl.Long`. */ +object DerivedClasses { + def deriveClasses(classes: List[LinkedClass]): List[LinkedClass] = { + classes.collect { + case clazz if clazz.className == BoxedCharacterClass || clazz.className == BoxedLongClass => + deriveBoxClass(clazz) + } + } + + /** Generates the accompanying Box class of `Character` or `Long`. + * + * These box classes will be used as the generic representation of `char`s and `long`s when they + * are upcast to `java.lang.Character`/`java.lang.Long` or any of their supertypes. + * + * The generated Box classes mimic the public structure of the corresponding hijacked classes. + * Whereas the hijacked classes instances *are* the primitives (conceptually), the box classes + * contain an explicit `value` field of the primitive type. They delegate all their instance + * methods to the corresponding methods of the hijacked class, applied on the `value` primitive. + * + * For example, given the hijacked class + * + * {{{ + * hijacked class Long extends java.lang.Number with Comparable { + * def longValue;J(): long = this.asInstanceOf[long] + * def toString;T(): string = Long$.toString(this.longValue;J()) + * def compareTo;jlLong;Z(that: java.lang.Long): boolean = + * Long$.compare(this.longValue;J(), that.longValue;J()) + * } + * }}} + * + * we generate + * + * {{{ + * class LongBox extends java.lang.Number with Comparable { + * val value: long + * def (value: long) = { this.value = value } + * def longValue;J(): long = this.value.longValue;J() + * def toString;T(): string = this.value.toString;J() + * def compareTo;jlLong;Z(that: jlLong): boolean = + * this.value.compareTo;jlLong;Z(that) + * } + * }}} + */ + private def deriveBoxClass(clazz: LinkedClass): LinkedClass = { + implicit val pos: Position = clazz.pos + + val EAF = ApplyFlags.empty + val EMF = MemberFlags.empty + val EOH = OptimizerHints.empty + val NON = NoOriginalName + val NOV = Version.Unversioned + + val className = clazz.className + val derivedClassName = className.withSuffix("Box") + val primType = BoxedClassToPrimType(className).asInstanceOf[PrimTypeWithRef] + val derivedClassType = ClassType(derivedClassName) + + val fieldName = FieldName(derivedClassName, valueFieldSimpleName) + val fieldIdent = FieldIdent(fieldName) + + val derivedFields: List[FieldDef] = List( + FieldDef(EMF, fieldIdent, NON, primType) + ) + + val selectField = Select(This()(derivedClassType), fieldIdent)(primType) + + val ctorParamDef = + ParamDef(LocalIdent(fieldName.simpleName.toLocalName), NON, primType, mutable = false) + val derivedCtor = MethodDef( + EMF.withNamespace(MemberNamespace.Constructor), + MethodIdent(MethodName.constructor(List(primType.primRef))), + NON, + List(ctorParamDef), + NoType, + Some(Assign(selectField, ctorParamDef.ref)) + )(EOH, NOV) + + val derivedMethods: List[MethodDef] = for { + method <- clazz.methods if method.flags.namespace == MemberNamespace.Public + } yield { + MethodDef( + method.flags, + method.name, + method.originalName, + method.args, + method.resultType, + Some(Apply(EAF, selectField, method.name, method.args.map(_.ref))(method.resultType)) + )(method.optimizerHints, method.version) + } + + new LinkedClass( + ClassIdent(derivedClassName), + Class, + jsClassCaptures = None, + clazz.superClass, + clazz.interfaces, + jsSuperClass = None, + jsNativeLoadSpec = None, + derivedFields, + derivedCtor :: derivedMethods, + jsConstructorDef = None, + exportedMembers = Nil, + jsNativeMembers = Nil, + EOH, + pos, + ancestors = derivedClassName :: clazz.ancestors.tail, + hasInstances = true, + hasDirectInstances = true, + hasInstanceTests = true, + hasRuntimeTypeInfo = true, + fieldsRead = Set(fieldName), + staticFieldsRead = Set.empty, + staticDependencies = Set.empty, + externalDependencies = Set.empty, + dynamicDependencies = Set.empty, + clazz.version + ) + } +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/EmbeddedConstants.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/EmbeddedConstants.scala new file mode 100644 index 0000000000..58e5f2c82b --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/EmbeddedConstants.scala @@ -0,0 +1,68 @@ +/* + * 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.backend.wasmemitter + +object EmbeddedConstants { + /* Values returned by the `jsValueType` helper. + * + * 0: false + * 1: true + * 2: string + * 3: number + * 4: undefined + * 5: everything else + * + * This encoding has the following properties: + * + * - false and true also return their value as the appropriate i32. + * - the types implementing `Comparable` are consecutive from 0 to 3. + */ + + final val JSValueTypeFalse = 0 + final val JSValueTypeTrue = 1 + final val JSValueTypeString = 2 + final val JSValueTypeNumber = 3 + final val JSValueTypeUndefined = 4 + final val JSValueTypeBigInt = 5 + final val JSValueTypeSymbol = 6 + final val JSValueTypeOther = 7 + + // Values for `typeData.kind` + + final val KindVoid = 0 + final val KindBoolean = 1 + final val KindChar = 2 + final val KindByte = 3 + final val KindShort = 4 + final val KindInt = 5 + final val KindLong = 6 + final val KindFloat = 7 + final val KindDouble = 8 + final val KindArray = 9 + final val KindObject = 10 // j.l.Object + final val KindBoxedUnit = 11 + final val KindBoxedBoolean = 12 + final val KindBoxedCharacter = 13 + final val KindBoxedByte = 14 + final val KindBoxedShort = 15 + final val KindBoxedInteger = 16 + final val KindBoxedLong = 17 + final val KindBoxedFloat = 18 + final val KindBoxedDouble = 19 + final val KindBoxedString = 20 + final val KindClass = 21 + final val KindInterface = 22 + final val KindJSType = 23 + + final val KindLastPrimitive = KindDouble +} 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 new file mode 100644 index 0000000000..0a477f6a59 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Emitter.scala @@ -0,0 +1,399 @@ +/* + * 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.backend.wasmemitter + +import scala.concurrent.{ExecutionContext, Future} + +import org.scalajs.ir.Names._ +import org.scalajs.ir.Types._ +import org.scalajs.ir.OriginalName +import org.scalajs.ir.Position + +import org.scalajs.linker.interface._ +import org.scalajs.linker.interface.unstable._ +import org.scalajs.linker.standard._ +import org.scalajs.linker.standard.ModuleSet.ModuleID + +import org.scalajs.linker.backend.emitter.PrivateLibHolder + +import org.scalajs.linker.backend.javascript.Printers.JSTreePrinter +import org.scalajs.linker.backend.javascript.{Trees => js} + +import org.scalajs.linker.backend.webassembly.FunctionBuilder +import org.scalajs.linker.backend.webassembly.{Instructions => wa} +import org.scalajs.linker.backend.webassembly.{Modules => wamod} +import org.scalajs.linker.backend.webassembly.{Identitities => wanme} +import org.scalajs.linker.backend.webassembly.{Types => watpe} + +import org.scalajs.logging.Logger + +import SpecialNames._ +import VarGen._ +import org.scalajs.linker.backend.javascript.ByteArrayWriter + +final class Emitter(config: Emitter.Config) { + import Emitter._ + + private val classEmitter = new ClassEmitter(config.coreSpec) + + val symbolRequirements: SymbolRequirement = + Emitter.symbolRequirements(config.coreSpec) + + val injectedIRFiles: Seq[IRFile] = PrivateLibHolder.files + + def emit(module: ModuleSet.Module, logger: Logger): Result = { + val wasmModule = emitWasmModule(module) + val loaderContent = LoaderContent.bytesContent + val jsFileContent = buildJSFileContent(module) + + new Result(wasmModule, loaderContent, jsFileContent) + } + + private def emitWasmModule(module: ModuleSet.Module): wamod.Module = { + // Inject the derived linked classes + val allClasses = + DerivedClasses.deriveClasses(module.classDefs) ::: module.classDefs + + /* Sort by ancestor count so that superclasses always appear before + * subclasses, then tie-break by name for stability. + */ + val sortedClasses = allClasses.sortWith { (a, b) => + val cmp = Integer.compare(a.ancestors.size, b.ancestors.size) + if (cmp != 0) cmp < 0 + else a.className.compareTo(b.className) < 0 + } + + val topLevelExports = module.topLevelExports + val moduleInitializers = module.initializers.toList + + implicit val ctx: WasmContext = + Preprocessor.preprocess(sortedClasses, topLevelExports) + + CoreWasmLib.genPreClasses() + genExternalModuleImports(module) + sortedClasses.foreach(classEmitter.genClassDef(_)) + topLevelExports.foreach(classEmitter.genTopLevelExport(_)) + CoreWasmLib.genPostClasses() + + genStartFunction(sortedClasses, moduleInitializers, topLevelExports) + + /* Gen the string pool and the declarative elements at the very end, since + * they depend on what instructions where produced by all the preceding codegen. + */ + ctx.stringPool.genPool() + genDeclarativeElements() + + ctx.moduleBuilder.build() + } + + private def genExternalModuleImports(module: ModuleSet.Module)( + implicit ctx: WasmContext): Unit = { + // Sort for stability + val allImportedModules = module.externalDependencies.toList.sorted + + // Gen imports of external modules on the Wasm side + for (moduleName <- allImportedModules) { + val id = genGlobalID.forImportedModule(moduleName) + val origName = OriginalName("import." + moduleName) + ctx.moduleBuilder.addImport( + wamod.Import( + "__scalaJSImports", + moduleName, + wamod.ImportDesc.Global(id, origName, isMutable = false, watpe.RefType.anyref) + ) + ) + } + } + + private def genStartFunction( + sortedClasses: List[LinkedClass], + moduleInitializers: List[ModuleInitializer.Initializer], + topLevelExportDefs: List[LinkedTopLevelExport] + )(implicit ctx: WasmContext): Unit = { + import org.scalajs.ir.Trees._ + + implicit val pos = Position.NoPosition + + val fb = + new FunctionBuilder(ctx.moduleBuilder, genFunctionID.start, OriginalName("start"), pos) + + // Initialize itables + + def genInitClassITable(classITableGlobalID: wanme.GlobalID, + classInfoForResolving: WasmContext.ClassInfo, ancestors: List[ClassName]): Unit = { + val resolvedMethodInfos = classInfoForResolving.resolvedMethodInfos + + for { + ancestor <- ancestors + // Use getClassInfoOption in case the reachability analysis got rid of those interfaces + interfaceInfo <- ctx.getClassInfoOption(ancestor) + if interfaceInfo.isInterface + } { + fb += wa.GlobalGet(classITableGlobalID) + fb += wa.I32Const(interfaceInfo.itableIdx) + + for (method <- interfaceInfo.tableEntries) + fb += ctx.refFuncWithDeclaration(resolvedMethodInfos(method).tableEntryID) + fb += wa.StructNew(genTypeID.forITable(ancestor)) + fb += wa.ArraySet(genTypeID.itables) + } + } + + // For all concrete, normal classes + for (clazz <- sortedClasses if clazz.kind.isClass && clazz.hasDirectInstances) { + val className = clazz.className + val classInfo = ctx.getClassInfo(className) + if (classInfo.classImplementsAnyInterface) + genInitClassITable(genGlobalID.forITable(className), classInfo, clazz.ancestors) + } + + // For array classes + genInitClassITable(genGlobalID.arrayClassITable, ctx.getClassInfo(ObjectClass), + List(SerializableClass, CloneableClass)) + + // Initialize the JS private field symbols + + for (clazz <- sortedClasses if clazz.kind.isJSClass) { + for (fieldDef <- clazz.fields) { + fieldDef match { + case FieldDef(flags, name, _, _) if !flags.namespace.isStatic => + fb += wa.Call(genFunctionID.newSymbol) + fb += wa.GlobalSet(genGlobalID.forJSPrivateField(name.name)) + case _ => + () + } + } + } + + // Emit the static initializers + + for (clazz <- sortedClasses if clazz.hasStaticInitializer) { + val funcID = genFunctionID.forMethod( + MemberNamespace.StaticConstructor, + clazz.className, + StaticInitializerName + ) + fb += wa.Call(funcID) + } + + // Initialize the top-level exports that require it + + for (tle <- topLevelExportDefs) { + // Load the (initial) exported value on the stack + tle.tree match { + case TopLevelJSClassExportDef(_, exportName) => + fb += wa.Call(genFunctionID.loadJSClass(tle.owningClass)) + case TopLevelModuleExportDef(_, exportName) => + fb += wa.Call(genFunctionID.loadModule(tle.owningClass)) + case TopLevelMethodExportDef(_, methodDef) => + fb += ctx.refFuncWithDeclaration(genFunctionID.forExport(tle.exportName)) + if (methodDef.restParam.isDefined) { + fb += wa.I32Const(methodDef.args.size) + fb += wa.Call(genFunctionID.makeExportedDefRest) + } else { + fb += wa.Call(genFunctionID.makeExportedDef) + } + case TopLevelFieldExportDef(_, _, fieldIdent) => + /* Usually redundant, but necessary if the static field is never + * explicitly set and keeps its default (zero) value instead. In that + * case this initial call is required to publish that zero value (as + * opposed to the default `undefined` value of the JS `let`). + */ + fb += wa.GlobalGet(genGlobalID.forStaticField(fieldIdent.name)) + } + + // Call the export setter + fb += wa.Call(genFunctionID.forTopLevelExportSetter(tle.exportName)) + } + + // Emit the module initializers + + moduleInitializers.foreach { init => + def genCallStatic(className: ClassName, methodName: MethodName): Unit = { + val funcID = genFunctionID.forMethod(MemberNamespace.PublicStatic, className, methodName) + fb += wa.Call(funcID) + } + + ModuleInitializerImpl.fromInitializer(init) match { + case ModuleInitializerImpl.MainMethodWithArgs(className, encodedMainMethodName, args) => + val stringArrayTypeRef = ArrayTypeRef(ClassRef(BoxedStringClass), 1) + SWasmGen.genArrayValue(fb, stringArrayTypeRef, args.size) { + args.foreach(arg => fb ++= ctx.stringPool.getConstantStringInstr(arg)) + } + genCallStatic(className, encodedMainMethodName) + + case ModuleInitializerImpl.VoidMainMethod(className, encodedMainMethodName) => + genCallStatic(className, encodedMainMethodName) + } + } + + // Finish the start function + + fb.buildAndAddToModule() + ctx.moduleBuilder.setStart(genFunctionID.start) + } + + private def genDeclarativeElements()(implicit ctx: WasmContext): Unit = { + // Aggregated Elements + + val funcDeclarations = ctx.getAllFuncDeclarations() + + if (funcDeclarations.nonEmpty) { + /* Functions that are referred to with `ref.func` in the Code section + * must be declared ahead of time in one of the earlier sections + * (otherwise the module does not validate). It can be the Global section + * if they are meaningful there (which is why `ref.func` in the vtables + * work out of the box). In the absence of any other specific place, an + * Element section with the declarative mode is the recommended way to + * introduce these declarations. + */ + val exprs = funcDeclarations.map { funcID => + wa.Expr(List(wa.RefFunc(funcID))) + } + ctx.moduleBuilder.addElement( + wamod.Element(watpe.RefType.funcref, exprs, wamod.Element.Mode.Declarative) + ) + } + } + + private def buildJSFileContent(module: ModuleSet.Module): Array[Byte] = { + implicit val noPos = Position.NoPosition + + // Sort for stability + val importedModules = module.externalDependencies.toList.sorted + + val (moduleImports, importedModulesItems) = (for { + (moduleName, idx) <- importedModules.zipWithIndex + } yield { + val importIdent = js.Ident(s"imported$idx") + val moduleNameStr = js.StringLiteral(moduleName) + val moduleImport = js.ImportNamespace(importIdent, moduleNameStr) + val item = moduleNameStr -> js.VarRef(importIdent) + (moduleImport, item) + }).unzip + + val importedModulesDict = js.ObjectConstr(importedModulesItems) + + val (exportDecls, exportSettersItems) = (for { + exportName <- module.topLevelExports.map(_.exportName) + } yield { + val ident = js.Ident(s"exported$exportName") + val decl = js.Let(ident, mutable = true, None) + val exportStat = js.Export(List(ident -> js.ExportName(exportName))) + val xParam = js.ParamDef(js.Ident("x")) + val setterFun = js.Function(arrow = true, List(xParam), None, { + js.Assign(js.VarRef(ident), xParam.ref) + }) + val setterItem = js.StringLiteral(exportName) -> setterFun + (List(decl, exportStat), setterItem) + }).unzip + + val exportSettersDict = js.ObjectConstr(exportSettersItems) + + val loadFunIdent = js.Ident("__load") + val loaderImport = js.Import( + List(js.ExportName("load") -> loadFunIdent), + js.StringLiteral(config.loaderModuleName) + ) + + val loadCall = js.Apply( + js.VarRef(loadFunIdent), + List( + js.StringLiteral(config.internalWasmFileURIPattern(module.id)), + importedModulesDict, + exportSettersDict + ) + ) + + val fullTree = ( + moduleImports ::: + loaderImport :: + exportDecls.flatten ::: + js.Await(loadCall) :: + Nil + ) + + val writer = new ByteArrayWriter + val printer = new JSTreePrinter(writer) + fullTree.foreach(printer.printStat(_)) + writer.toByteArray() + } +} + +object Emitter { + + /** Configuration for the Emitter. */ + final class Config private ( + val coreSpec: CoreSpec, + val loaderModuleName: String, + val internalWasmFileURIPattern: ModuleID => String + ) { + private def this(coreSpec: CoreSpec, loaderModuleName: String) = { + this( + coreSpec, + loaderModuleName, + internalWasmFileURIPattern = { moduleID => s"./${moduleID.id}.wasm" } + ) + } + + def withInternalWasmFileURIPattern( + internalWasmFileURIPattern: ModuleID => String): Config = { + copy(internalWasmFileURIPattern = internalWasmFileURIPattern) + } + + private def copy( + coreSpec: CoreSpec = coreSpec, + loaderModuleName: String = loaderModuleName, + internalWasmFileURIPattern: ModuleID => String = internalWasmFileURIPattern + ): Config = { + new Config( + coreSpec, + loaderModuleName, + internalWasmFileURIPattern + ) + } + } + + object Config { + def apply(coreSpec: CoreSpec, loaderModuleName: String): Config = + new Config(coreSpec, loaderModuleName) + } + + final class Result( + val wasmModule: wamod.Module, + val loaderContent: Array[Byte], + val jsFileContent: Array[Byte] + ) + + /** Builds the symbol requirements of our back-end. + * + * The symbol requirements tell the LinkerFrontend that we need these symbols to always be + * reachable, even if no "user-land" IR requires them. They are roots for the reachability + * analysis, together with module initializers and top-level exports. If we don't do this, the + * linker frontend will dead-code eliminate our box classes. + */ + private def symbolRequirements(coreSpec: CoreSpec): SymbolRequirement = { + val factory = SymbolRequirement.factory("wasm") + + factory.multiple( + // TODO Ideally we should not require these, but rather adapt to their absence + factory.instantiateClass(ClassClass, AnyArgConstructorName), + factory.instantiateClass(JSExceptionClass, AnyArgConstructorName), + + // See genIdentityHashCode in HelperFunctions + factory.callMethodStatically(BoxedDoubleClass, hashCodeMethodName), + factory.callMethodStatically(BoxedStringClass, hashCodeMethodName) + ) + } + +} 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 new file mode 100644 index 0000000000..d41496cef8 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala @@ -0,0 +1,3374 @@ +/* + * 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.backend.wasmemitter + +import scala.annotation.switch + +import scala.collection.mutable + +import org.scalajs.ir.{ClassKind, OriginalName, Position, UTF8String} +import org.scalajs.ir.Names._ +import org.scalajs.ir.OriginalName.NoOriginalName +import org.scalajs.ir.Trees._ +import org.scalajs.ir.Types._ + +import org.scalajs.linker.backend.webassembly._ +import org.scalajs.linker.backend.webassembly.{Instructions => wa} +import org.scalajs.linker.backend.webassembly.{Identitities => wanme} +import org.scalajs.linker.backend.webassembly.{Types => watpe} +import org.scalajs.linker.backend.webassembly.Types.{FunctionType => Sig} + +import EmbeddedConstants._ +import SWasmGen._ +import VarGen._ +import TypeTransformer._ + +object FunctionEmitter { + + /** Whether to use the legacy `try` instruction to implement `TryCatch`. + * + * Support for catching JS exceptions was only added to `try_table` in V8 12.5 from April 2024. + * While waiting for Node.js to catch up with V8, we use `try` to implement our `TryCatch`. + * + * We use this "fixed configuration option" to keep the code that implements `TryCatch` using + * `try_table` in the codebase, as code that is actually compiled, so that refactorings apply to + * it as well. It also makes it easier to manually experiment with the new `try_table` encoding, + * which is available in Chrome since v125. + * + * Note that we use `try_table` regardless to implement `TryFinally`. Its `catch_all_ref` handler + * is perfectly happy to catch and rethrow JavaScript exception in Node.js 22. Duplicating that + * implementation for `try` would be a nightmare, given how complex it is already. + */ + private final val UseLegacyExceptionsForTryCatch = true + + def emitFunction( + functionID: wanme.FunctionID, + originalName: OriginalName, + enclosingClassName: Option[ClassName], + captureParamDefs: Option[List[ParamDef]], + receiverType: Option[watpe.Type], + paramDefs: List[ParamDef], + restParam: Option[ParamDef], + body: Tree, + resultType: Type + )(implicit ctx: WasmContext, pos: Position): Unit = { + val emitter = prepareEmitter( + functionID, + originalName, + enclosingClassName, + captureParamDefs, + preSuperVarDefs = None, + hasNewTarget = false, + receiverType, + paramDefs ::: restParam.toList, + transformResultType(resultType) + ) + emitter.genBody(body, resultType) + emitter.fb.buildAndAddToModule() + } + + def emitJSConstructorFunctions( + preSuperStatsFunctionID: wanme.FunctionID, + superArgsFunctionID: wanme.FunctionID, + postSuperStatsFunctionID: wanme.FunctionID, + enclosingClassName: ClassName, + jsClassCaptures: List[ParamDef], + ctor: JSConstructorDef + )(implicit ctx: WasmContext): Unit = { + implicit val pos = ctor.pos + + val allCtorParams = ctor.args ::: ctor.restParam.toList + val ctorBody = ctor.body + + // Compute the pre-super environment + val preSuperDecls = ctorBody.beforeSuper.collect { case varDef: VarDef => + varDef + } + + // Build the `preSuperStats` function + locally { + val preSuperEnvStructTypeID = ctx.getClosureDataStructType(preSuperDecls.map(_.vtpe)) + val preSuperEnvType = watpe.RefType(preSuperEnvStructTypeID) + + val emitter = prepareEmitter( + preSuperStatsFunctionID, + OriginalName(UTF8String("preSuperStats.") ++ enclosingClassName.encoded), + Some(enclosingClassName), + Some(jsClassCaptures), + preSuperVarDefs = None, + hasNewTarget = true, + receiverType = None, + allCtorParams, + List(preSuperEnvType) + ) + + emitter.genBlockStats(ctorBody.beforeSuper) { + // Build and return the preSuperEnv struct + for (varDef <- preSuperDecls) { + val localID = (emitter.lookupLocal(varDef.name.name): @unchecked) match { + case VarStorage.Local(localID) => localID + } + emitter.fb += wa.LocalGet(localID) + } + emitter.fb += wa.StructNew(preSuperEnvStructTypeID) + } + + emitter.fb.buildAndAddToModule() + } + + // Build the `superArgs` function + locally { + val emitter = prepareEmitter( + superArgsFunctionID, + OriginalName(UTF8String("superArgs.") ++ enclosingClassName.encoded), + Some(enclosingClassName), + Some(jsClassCaptures), + Some(preSuperDecls), + hasNewTarget = true, + receiverType = None, + allCtorParams, + List(watpe.RefType.anyref) // a js.Array + ) + emitter.genBody(JSArrayConstr(ctorBody.superCall.args), AnyType) + emitter.fb.buildAndAddToModule() + } + + // Build the `postSuperStats` function + locally { + val emitter = prepareEmitter( + postSuperStatsFunctionID, + OriginalName(UTF8String("postSuperStats.") ++ enclosingClassName.encoded), + Some(enclosingClassName), + Some(jsClassCaptures), + Some(preSuperDecls), + hasNewTarget = true, + receiverType = Some(watpe.RefType.anyref), + allCtorParams, + List(watpe.RefType.anyref) + ) + emitter.genBody(Block(ctorBody.afterSuper), AnyType) + emitter.fb.buildAndAddToModule() + } + } + + private def prepareEmitter( + functionID: wanme.FunctionID, + originalName: OriginalName, + enclosingClassName: Option[ClassName], + captureParamDefs: Option[List[ParamDef]], + preSuperVarDefs: Option[List[VarDef]], + hasNewTarget: Boolean, + receiverType: Option[watpe.Type], + paramDefs: List[ParamDef], + resultTypes: List[watpe.Type] + )(implicit ctx: WasmContext, pos: Position): FunctionEmitter = { + val fb = new FunctionBuilder(ctx.moduleBuilder, functionID, originalName, pos) + + def addCaptureLikeParamListAndMakeEnv( + captureParamName: String, + captureLikes: List[(LocalName, Type)] + ): Env = { + val dataStructTypeID = ctx.getClosureDataStructType(captureLikes.map(_._2)) + val param = fb.addParam(captureParamName, watpe.RefType(dataStructTypeID)) + val env: List[(LocalName, VarStorage)] = for { + ((name, _), idx) <- captureLikes.zipWithIndex + } yield { + val storage = VarStorage.StructField( + param, + dataStructTypeID, + genFieldID.captureParam(idx) + ) + name -> storage + } + env.toMap + } + + val captureParamsEnv: Env = captureParamDefs match { + case None => + Map.empty + case Some(defs) => + addCaptureLikeParamListAndMakeEnv("__captureData", + defs.map(p => p.name.name -> p.ptpe)) + } + + val preSuperEnvEnv: Env = preSuperVarDefs match { + case None => + Map.empty + case Some(defs) => + addCaptureLikeParamListAndMakeEnv("__preSuperEnv", + defs.map(p => p.name.name -> p.vtpe)) + } + + val newTargetStorage = if (!hasNewTarget) { + None + } else { + val newTargetParam = fb.addParam(newTargetOriginalName, watpe.RefType.anyref) + Some(VarStorage.Local(newTargetParam)) + } + + val receiverStorage = receiverType.map { tpe => + val receiverParam = fb.addParam(receiverOriginalName, tpe) + VarStorage.Local(receiverParam) + } + + val normalParamsEnv: Env = paramDefs.map { paramDef => + val param = fb.addParam( + paramDef.originalName.orElse(paramDef.name.name), + transformLocalType(paramDef.ptpe) + ) + paramDef.name.name -> VarStorage.Local(param) + }.toMap + + val fullEnv: Env = captureParamsEnv ++ preSuperEnvEnv ++ normalParamsEnv + + fb.setResultTypes(resultTypes) + + new FunctionEmitter( + fb, + enclosingClassName, + newTargetStorage, + receiverStorage, + fullEnv + ) + } + + private val ObjectRef = ClassRef(ObjectClass) + private val BoxedStringRef = ClassRef(BoxedStringClass) + private val toStringMethodName = MethodName("toString", Nil, BoxedStringRef) + private val equalsMethodName = MethodName("equals", List(ObjectRef), BooleanRef) + private val compareToMethodName = MethodName("compareTo", List(ObjectRef), IntRef) + + private val CharSequenceClass = ClassName("java.lang.CharSequence") + private val ComparableClass = ClassName("java.lang.Comparable") + private val JLNumberClass = ClassName("java.lang.Number") + + private val newTargetOriginalName = OriginalName("new.target") + private val receiverOriginalName = OriginalName("this") + + private sealed abstract class VarStorage + + private object VarStorage { + final case class Local(localID: wanme.LocalID) extends VarStorage + + final case class StructField(structLocalID: wanme.LocalID, + structTypeID: wanme.TypeID, fieldID: wanme.FieldID) + extends VarStorage + } + + private type Env = Map[LocalName, VarStorage] + + private final class ClosureFunctionID(debugName: OriginalName) extends wanme.FunctionID { + override def toString(): String = s"ClosureFunctionID(${debugName.toString()})" + } +} + +private class FunctionEmitter private ( + val fb: FunctionBuilder, + enclosingClassName: Option[ClassName], + _newTargetStorage: Option[FunctionEmitter.VarStorage.Local], + _receiverStorage: Option[FunctionEmitter.VarStorage.Local], + paramsEnv: FunctionEmitter.Env +)(implicit ctx: WasmContext) { + import FunctionEmitter._ + + private var closureIdx: Int = 0 + private var currentEnv: Env = paramsEnv + + private def newTargetStorage: VarStorage.Local = + _newTargetStorage.getOrElse(throw new Error("Cannot access new.target in this context.")) + + private def receiverStorage: VarStorage.Local = + _receiverStorage.getOrElse(throw new Error("Cannot access to the receiver in this context.")) + + private def withNewLocal[A](name: LocalName, originalName: OriginalName, tpe: watpe.Type)( + body: wanme.LocalID => A + ): A = { + val savedEnv = currentEnv + val local = fb.addLocal(originalName.orElse(name), tpe) + currentEnv = currentEnv.updated(name, VarStorage.Local(local)) + try body(local) + finally currentEnv = savedEnv + } + + private def lookupLocal(name: LocalName): VarStorage = { + currentEnv.getOrElse( + name, { + throw new AssertionError(s"Cannot find binding for '${name.nameString}'") + } + ) + } + + private def addSyntheticLocal(tpe: watpe.Type): wanme.LocalID = + fb.addLocal(NoOriginalName, tpe) + + private def genClosureFuncOriginalName(): OriginalName = { + if (fb.functionOriginalName.isEmpty) { + NoOriginalName + } else { + val innerName = OriginalName(fb.functionOriginalName.get ++ UTF8String("__c" + closureIdx)) + closureIdx += 1 + innerName + } + } + + private def markPosition(pos: Position): Unit = + fb += wa.PositionMark(pos) + + private def markPosition(tree: Tree): Unit = + markPosition(tree.pos) + + def genBody(tree: Tree, expectedType: Type): Unit = + genTree(tree, expectedType) + + def genTreeAuto(tree: Tree): Unit = + genTree(tree, tree.tpe) + + def genTree(tree: Tree, expectedType: Type): Unit = { + val generatedType: Type = tree match { + case t: Literal => genLiteral(t, expectedType) + case t: UnaryOp => genUnaryOp(t) + case t: BinaryOp => genBinaryOp(t) + case t: VarRef => genVarRef(t) + case t: LoadModule => genLoadModule(t) + case t: StoreModule => genStoreModule(t) + case t: This => genThis(t) + case t: ApplyStatically => genApplyStatically(t) + case t: Apply => genApply(t) + case t: ApplyStatic => genApplyStatic(t) + case t: ApplyDynamicImport => genApplyDynamicImport(t) + case t: IsInstanceOf => genIsInstanceOf(t) + case t: AsInstanceOf => genAsInstanceOf(t) + case t: GetClass => genGetClass(t) + case t: Block => genBlock(t, expectedType) + case t: Labeled => unwinding.genLabeled(t, expectedType) + case t: Return => unwinding.genReturn(t) + case t: Select => genSelect(t) + case t: SelectStatic => genSelectStatic(t) + case t: Assign => genAssign(t) + case t: VarDef => genVarDef(t) + case t: New => genNew(t) + case t: If => genIf(t, expectedType) + case t: While => genWhile(t) + case t: ForIn => genForIn(t) + case t: TryCatch => genTryCatch(t, expectedType) + case t: TryFinally => unwinding.genTryFinally(t, expectedType) + case t: Throw => genThrow(t) + case t: Match => genMatch(t, expectedType) + case t: Debugger => NoType // ignore + case t: Skip => NoType + case t: Clone => genClone(t) + case t: IdentityHashCode => genIdentityHashCode(t) + case t: WrapAsThrowable => genWrapAsThrowable(t) + case t: UnwrapFromThrowable => genUnwrapFromThrowable(t) + + // JavaScript expressions + case t: JSNew => genJSNew(t) + case t: JSSelect => genJSSelect(t) + case t: JSFunctionApply => genJSFunctionApply(t) + case t: JSMethodApply => genJSMethodApply(t) + case t: JSImportCall => genJSImportCall(t) + case t: JSImportMeta => genJSImportMeta(t) + case t: LoadJSConstructor => genLoadJSConstructor(t) + case t: LoadJSModule => genLoadJSModule(t) + case t: SelectJSNativeMember => genSelectJSNativeMember(t) + case t: JSDelete => genJSDelete(t) + case t: JSUnaryOp => genJSUnaryOp(t) + case t: JSBinaryOp => genJSBinaryOp(t) + case t: JSArrayConstr => genJSArrayConstr(t) + case t: JSObjectConstr => genJSObjectConstr(t) + case t: JSGlobalRef => genJSGlobalRef(t) + case t: JSTypeOfGlobalRef => genJSTypeOfGlobalRef(t) + case t: JSLinkingInfo => genJSLinkingInfo(t) + case t: Closure => genClosure(t) + + // array + case t: ArrayLength => genArrayLength(t) + case t: NewArray => genNewArray(t) + case t: ArraySelect => genArraySelect(t) + case t: ArrayValue => genArrayValue(t) + + // Non-native JS classes + case t: CreateJSClass => genCreateJSClass(t) + case t: JSPrivateSelect => genJSPrivateSelect(t) + case t: JSSuperSelect => genJSSuperSelect(t) + case t: JSSuperMethodCall => genJSSuperMethodCall(t) + case t: JSNewTarget => genJSNewTarget(t) + + case _: RecordSelect | _: RecordValue | _: Transient | _: JSSuperConstructorCall => + throw new AssertionError(s"Invalid tree: $tree") + } + + genAdapt(generatedType, expectedType) + } + + private def genAdapt(generatedType: Type, expectedType: Type): Unit = { + (generatedType, expectedType) match { + case _ if generatedType == expectedType => + () + case (NothingType, _) => + () + case (_, NoType) => + fb += wa.Drop + case (primType: PrimTypeWithRef, _) => + // box + primType match { + case NullType => + () + case CharType => + /* `char` and `long` are opaque to JS in the Scala.js semantics. + * We implement them with real Wasm classes following the correct + * vtable. Upcasting wraps a primitive into the corresponding class. + */ + genBox(watpe.Int32, SpecialNames.CharBoxClass) + case LongType => + genBox(watpe.Int64, SpecialNames.LongBoxClass) + case NoType | NothingType => + throw new AssertionError(s"Unexpected adaptation from $primType to $expectedType") + case _ => + /* Calls a `bX` helper. Most of them are of the form + * bX: (x) => x + * at the JavaScript level, but with a primType->anyref Wasm type. + * For example, for `IntType`, `bI` has type `i32 -> anyref`. This + * asks the JS host to turn a primitive `i32` into its generic + * representation, which we can store in an `anyref`. + */ + fb += wa.Call(genFunctionID.box(primType.primRef)) + } + case _ => + () + } + } + + private def genAssign(tree: Assign): Type = { + val Assign(lhs, rhs) = tree + + lhs match { + case Select(qualifier, field) => + val className = field.name.className + val classInfo = ctx.getClassInfo(className) + + // For Select, the receiver can never be a hijacked class, so we can use genTreeAuto + genTreeAuto(qualifier) + + if (!classInfo.hasInstances) { + /* The field may not exist in that case, and we cannot look it up. + * However we necessarily have a `null` receiver if we reach this + * point, so we can trap as NPE. + */ + markPosition(tree) + fb += wa.Unreachable + } else { + genTree(rhs, lhs.tpe) + markPosition(tree) + fb += wa.StructSet( + genTypeID.forClass(className), + genFieldID.forClassInstanceField(field.name) + ) + } + + case SelectStatic(field) => + val fieldName = field.name + val globalID = genGlobalID.forStaticField(fieldName) + + genTree(rhs, lhs.tpe) + markPosition(tree) + fb += wa.GlobalSet(globalID) + + // Update top-level export mirrors + val classInfo = ctx.getClassInfo(fieldName.className) + val mirrors = classInfo.staticFieldMirrors.getOrElse(fieldName, Nil) + for (exportedName <- mirrors) { + fb += wa.GlobalGet(globalID) + fb += wa.Call(genFunctionID.forTopLevelExportSetter(exportedName)) + } + + case ArraySelect(array, index) => + genTreeAuto(array) + array.tpe match { + case ArrayType(arrayTypeRef) => + // Get the underlying array; implicit trap on null + markPosition(tree) + fb += wa.StructGet( + genTypeID.forArrayClass(arrayTypeRef), + genFieldID.objStruct.arrayUnderlying + ) + genTree(index, IntType) + genTree(rhs, lhs.tpe) + markPosition(tree) + fb += wa.ArraySet(genTypeID.underlyingOf(arrayTypeRef)) + case NothingType => + // unreachable + () + case NullType => + markPosition(tree) + fb += wa.Unreachable + case _ => + throw new IllegalArgumentException( + s"ArraySelect.array must be an array type, but has type ${array.tpe}") + } + + case JSPrivateSelect(qualifier, field) => + genTree(qualifier, AnyType) + fb += wa.GlobalGet(genGlobalID.forJSPrivateField(field.name)) + genTree(rhs, AnyType) + markPosition(tree) + fb += wa.Call(genFunctionID.jsSelectSet) + + case JSSelect(qualifier, item) => + genTree(qualifier, AnyType) + genTree(item, AnyType) + genTree(rhs, AnyType) + markPosition(tree) + fb += wa.Call(genFunctionID.jsSelectSet) + + case JSSuperSelect(superClass, receiver, item) => + genTree(superClass, AnyType) + genTree(receiver, AnyType) + genTree(item, AnyType) + genTree(rhs, AnyType) + markPosition(tree) + fb += wa.Call(genFunctionID.jsSuperSelectSet) + + case JSGlobalRef(name) => + markPosition(tree) + fb ++= ctx.stringPool.getConstantStringInstr(name) + genTree(rhs, AnyType) + markPosition(tree) + fb += wa.Call(genFunctionID.jsGlobalRefSet) + + case VarRef(ident) => + lookupLocal(ident.name) match { + case VarStorage.Local(local) => + genTree(rhs, lhs.tpe) + markPosition(tree) + fb += wa.LocalSet(local) + case VarStorage.StructField(structLocal, structTypeID, fieldID) => + markPosition(tree) + fb += wa.LocalGet(structLocal) + genTree(rhs, lhs.tpe) + markPosition(tree) + fb += wa.StructSet(structTypeID, fieldID) + } + + case lhs: RecordSelect => + throw new AssertionError(s"Invalid tree: $tree") + } + + NoType + } + + private def genApply(tree: Apply): Type = { + val Apply(flags, receiver, method, args) = tree + + receiver.tpe match { + case NothingType => + genTree(receiver, NothingType) + // nothing else to do; this is unreachable + NothingType + + case NullType => + genTree(receiver, NullType) + fb += wa.Unreachable // trap + NothingType + + case _ if method.name.isReflectiveProxy => + genReflectiveCall(tree) + + case _ => + val receiverClassName = receiver.tpe match { + case prim: PrimType => PrimTypeToBoxedClass(prim) + case ClassType(cls) => cls + case AnyType => ObjectClass + case ArrayType(_) => ObjectClass + case tpe: RecordType => throw new AssertionError(s"Invalid receiver type $tpe") + } + val receiverClassInfo = ctx.getClassInfo(receiverClassName) + + /* If possible, "optimize" this Apply node as an ApplyStatically call. + * We can do this if the receiver's class is a hijacked class or an + * array type (because they are known to be final) or if the target + * method is effectively final. + * + * The latter condition is nothing but an optimization, and should be + * done by the optimizer instead. We will remove it once we can run the + * optimizer with Wasm. + * + * The former condition (being a hijacked class or an array type) will + * also never happen after we have the optimizer. But if we do not have + * the optimizer, we must still do it now because the preconditions of + * `genApplyWithDispatch` would not be met. + */ + val canUseStaticallyResolved = { + receiverClassInfo.kind == ClassKind.HijackedClass || + receiver.tpe.isInstanceOf[ArrayType] || + receiverClassInfo.resolvedMethodInfos.get(method.name).exists(_.isEffectivelyFinal) + } + if (canUseStaticallyResolved) { + genApplyStatically(ApplyStatically( + flags, receiver, receiverClassName, method, args)(tree.tpe)(tree.pos)) + } else { + genApplyWithDispatch(tree, receiverClassInfo) + } + } + } + + private def genReflectiveCall(tree: Apply): Type = { + val Apply(flags, receiver, MethodIdent(methodName), args) = tree + + assert(methodName.isReflectiveProxy) + + val receiverLocalForDispatch = + addSyntheticLocal(watpe.RefType.any) + + val proxyId = ctx.getReflectiveProxyId(methodName) + val funcTypeID = ctx.tableFunctionType(methodName) + + /* We only need to handle calls on non-hijacked classes. For hijacked + * classes, the compiler already emits the appropriate dispatch at the IR + * level. + */ + + // Load receiver and arguments + genTree(receiver, AnyType) + fb += wa.RefAsNonNull + fb += wa.LocalTee(receiverLocalForDispatch) + genArgs(args, methodName) + + // Looks up the method to be (reflectively) called + markPosition(tree) + fb += wa.LocalGet(receiverLocalForDispatch) + fb += wa.RefCast(watpe.RefType(genTypeID.ObjectStruct)) // see above: cannot be a hijacked class + fb += wa.StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.vtable) + fb += wa.I32Const(proxyId) + // `searchReflectiveProxy`: [typeData, i32] -> [(ref func)] + fb += wa.Call(genFunctionID.searchReflectiveProxy) + + fb += wa.RefCast(watpe.RefType(watpe.HeapType(funcTypeID))) + fb += wa.CallRef(funcTypeID) + + tree.tpe + } + + /** Generates the code for an `Apply` tree that requires dynamic dispatch. + * + * In that case, there is always at least a vtable/itable-based dispatch. It may also contain + * primitive-based dispatch if the receiver's type is an ancestor of a hijacked class. + * + * This method must not be used if the receiver's type is a primitive, a + * hijacked class or an array type. Hijacked classes do not have dispatch + * tables, so the methods that are not available in any superclass/interface + * cannot be called through a table dispatch. Array types share their vtable + * with jl.Object, but methods called directly on an array type are not + * registered as called on jl.Object by the Analyzer. In all these cases, + * we must use a statically resolved call instead. + */ + private def genApplyWithDispatch(tree: Apply, + receiverClassInfo: WasmContext.ClassInfo): Type = { + + val Apply(flags, receiver, MethodIdent(methodName), args) = tree + + val receiverClassName = receiverClassInfo.name + + /* Similar to transformType(t.receiver.tpe), but: + * - it is non-null, + * - ancestors of hijacked classes are not treated specially, + * - array types are treated as j.l.Object. + * + * This is used in the code paths where we have already ruled out `null` + * values and primitive values (that implement hijacked classes). + */ + val refTypeForDispatch: watpe.RefType = { + if (receiverClassInfo.isInterface) + watpe.RefType(genTypeID.ObjectStruct) + else + watpe.RefType(genTypeID.forClass(receiverClassName)) + } + + // A local for a copy of the receiver that we will use to resolve dispatch + val receiverLocalForDispatch = addSyntheticLocal(refTypeForDispatch) + + /* Gen loading of the receiver and check that it is non-null. + * After this codegen, the non-null receiver is on the stack. + */ + def genReceiverNotNull(): Unit = { + genTreeAuto(receiver) + fb += wa.RefAsNonNull + } + + /* Generates a resolved call to a method of a hijacked class. + * Before this code gen, the stack must contain the receiver and the args. + * After this code gen, the stack contains the result. + */ + def genHijackedClassCall(hijackedClass: ClassName): Unit = { + val funcID = genFunctionID.forMethod(MemberNamespace.Public, hijackedClass, methodName) + fb += wa.Call(funcID) + } + + if (!receiverClassInfo.hasInstances) { + /* If the target class info does not have any instance, the only possible + * value for the receiver is `null`. We can therefore immediately trap for + * an NPE. It is important to short-cut this path because the reachability + * analysis may have entirely dead-code eliminated the target method, + * which means we do not know its signature and therefore cannot emit the + * corresponding vtable/itable calls. + */ + genTreeAuto(receiver) + markPosition(tree) + fb += wa.Unreachable // NPE + } else if (!receiverClassInfo.isAncestorOfHijackedClass) { + // Standard dispatch codegen + genReceiverNotNull() + fb += wa.LocalTee(receiverLocalForDispatch) + genArgs(args, methodName) + + markPosition(tree) + genTableDispatch(receiverClassInfo, methodName, receiverLocalForDispatch) + } else { + /* Here the receiver's type is an ancestor of a hijacked class (or `any`, + * which is treated as `jl.Object`). + * + * We must emit additional dispatch for the possible primitive values. + * + * The overall structure of the generated code is as follows: + * + * block resultType $done + * block (ref any) $notOurObject + * load non-null receiver and args and store into locals + * reload copy of receiver + * br_on_cast_fail (ref any) (ref $targetRealClass) $notOurObject + * reload args + * generate standard table-based dispatch + * br $done + * end $notOurObject + * choose an implementation of a single hijacked class, or a JS helper + * reload args + * call the chosen implementation + * end $done + */ + + assert(receiverClassInfo.kind != ClassKind.HijackedClass, receiverClassName) + + val resultType = transformResultType(tree.tpe) + + fb.block(resultType) { labelDone => + def pushArgs(argsLocals: List[wanme.LocalID]): Unit = + argsLocals.foreach(argLocal => fb += wa.LocalGet(argLocal)) + + /* First try the case where the value is one of our objects. + * We load the receiver and arguments inside the block `notOurObject`. + * This helps producing good code for the no-args case, in which we do + * not need to store the receiver in a local at all. + * For the case with the args, it does not hurt either way. We could + * move it out, but that would make for a less consistent codegen. + */ + val argsLocals = fb.block(watpe.RefType.any) { labelNotOurObject => + // Load receiver and arguments and store them in temporary variables + genReceiverNotNull() + val argsLocals = if (args.isEmpty) { + /* When there are no arguments, we can leave the receiver directly on + * the stack instead of going through a local. We will still need a + * local for the table-based dispatch, though. + */ + Nil + } else { + /* When there are arguments, we need to store them in temporary + * variables. This is not required for correctness of the evaluation + * order. It is only necessary so that we do not duplicate the + * codegen of the arguments. If the arguments are complex, doing so + * could lead to exponential blow-up of the generated code. + */ + val receiverLocal = addSyntheticLocal(watpe.RefType.any) + + fb += wa.LocalSet(receiverLocal) + val argsLocals: List[wanme.LocalID] = + for ((arg, typeRef) <- args.zip(methodName.paramTypeRefs)) yield { + val tpe = ctx.inferTypeFromTypeRef(typeRef) + genTree(arg, tpe) + val localID = addSyntheticLocal(transformLocalType(tpe)) + fb += wa.LocalSet(localID) + localID + } + fb += wa.LocalGet(receiverLocal) + argsLocals + } + + markPosition(tree) // main position marker for the entire hijacked class dispatch branch + + fb += wa.BrOnCastFail(labelNotOurObject, watpe.RefType.any, refTypeForDispatch) + fb += wa.LocalTee(receiverLocalForDispatch) + pushArgs(argsLocals) + genTableDispatch(receiverClassInfo, methodName, receiverLocalForDispatch) + fb += wa.Br(labelDone) + + argsLocals + } // end block labelNotOurObject + + /* Now we have a value that is not one of our objects, so it must be + * a JavaScript value whose representative class extends/implements the + * receiver class. It may be a primitive instance of a hijacked class, or + * any other value (whose representative class is therefore `jl.Object`). + * + * It is also *not* `char` or `long`, since those would reach + * `genApplyNonPrim` in their boxed form, and therefore they are + * "ourObject". + * + * The (ref any) is still on the stack. + */ + + if (methodName == toStringMethodName) { + // By spec, toString() is special + assert(argsLocals.isEmpty) + fb += wa.Call(genFunctionID.jsValueToString) + } else if (receiverClassName == JLNumberClass) { + // the value must be a `number`, hence we can unbox to `double` + genUnbox(DoubleType) + pushArgs(argsLocals) + genHijackedClassCall(BoxedDoubleClass) + } else if (receiverClassName == CharSequenceClass) { + // the value must be a `string`; it already has the right type + pushArgs(argsLocals) + genHijackedClassCall(BoxedStringClass) + } else if (methodName == compareToMethodName) { + /* The only method of jl.Comparable. Here the value can be a boolean, + * a number or a string. We use `jsValueType` to dispatch to Wasm-side + * implementations because they have to perform casts on their arguments. + */ + assert(argsLocals.size == 1) + + val receiverLocal = addSyntheticLocal(watpe.RefType.any) + fb += wa.LocalTee(receiverLocal) + + val jsValueTypeLocal = addSyntheticLocal(watpe.Int32) + fb += wa.Call(genFunctionID.jsValueType) + fb += wa.LocalTee(jsValueTypeLocal) + + fb.switch(Sig(List(watpe.Int32), Nil), Sig(Nil, List(watpe.Int32))) { () => + // scrutinee is already on the stack + }( + // case JSValueTypeFalse | JSValueTypeTrue => + List(JSValueTypeFalse, JSValueTypeTrue) -> { () => + /* The jsValueTypeLocal is the boolean value, thanks to the chosen encoding. + * This trick avoids an additional unbox. + */ + fb += wa.LocalGet(jsValueTypeLocal) + pushArgs(argsLocals) + genHijackedClassCall(BoxedBooleanClass) + }, + // case JSValueTypeString => + List(JSValueTypeString) -> { () => + fb += wa.LocalGet(receiverLocal) + // no need to unbox for string + pushArgs(argsLocals) + genHijackedClassCall(BoxedStringClass) + } + ) { () => + // case _ (JSValueTypeNumber) => + fb += wa.LocalGet(receiverLocal) + genUnbox(DoubleType) + pushArgs(argsLocals) + genHijackedClassCall(BoxedDoubleClass) + } + } else { + /* It must be a method of j.l.Object and it can be any value. + * hashCode() and equals() are overridden in all hijacked classes. + * We use `identityHashCode` for `hashCode` and `Object.is` for `equals`, + * as they coincide with the respective specifications (on purpose). + * The other methods are never overridden and can be statically + * resolved to j.l.Object. + */ + pushArgs(argsLocals) + methodName match { + case SpecialNames.hashCodeMethodName => + fb += wa.Call(genFunctionID.identityHashCode) + case `equalsMethodName` => + fb += wa.Call(genFunctionID.is) + case _ => + genHijackedClassCall(ObjectClass) + } + } + } // end block labelDone + } + + if (tree.tpe == NothingType) + fb += wa.Unreachable + + tree.tpe + } + + /** Generates a vtable- or itable-based dispatch. + * + * Before this code gen, the stack must contain the receiver and the args of the target method. + * In addition, the receiver must be available in the local `receiverLocalForDispatch`. The two + * occurrences of the receiver must have the type for dispatch. + * + * After this code gen, the stack contains the result. If the result type is `NothingType`, + * `genTableDispatch` leaves the stack in an arbitrary state. It is up to the caller to insert an + * `unreachable` instruction when appropriate. + */ + def genTableDispatch(receiverClassInfo: WasmContext.ClassInfo, + methodName: MethodName, receiverLocalForDispatch: wanme.LocalID): Unit = { + // Generates an itable-based dispatch. + def genITableDispatch(): Unit = { + fb += wa.LocalGet(receiverLocalForDispatch) + fb += wa.StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.itables) + fb += wa.I32Const(receiverClassInfo.itableIdx) + fb += wa.ArrayGet(genTypeID.itables) + fb += wa.RefCast(watpe.RefType(genTypeID.forITable(receiverClassInfo.name))) + fb += wa.StructGet( + genTypeID.forITable(receiverClassInfo.name), + genFieldID.forMethodTableEntry(methodName) + ) + fb += wa.CallRef(ctx.tableFunctionType(methodName)) + } + + // Generates a vtable-based dispatch. + def genVTableDispatch(): Unit = { + val receiverClassName = receiverClassInfo.name + + fb += wa.LocalGet(receiverLocalForDispatch) + fb += wa.StructGet( + genTypeID.forClass(receiverClassName), + genFieldID.objStruct.vtable + ) + fb += wa.StructGet( + genTypeID.forVTable(receiverClassName), + genFieldID.forMethodTableEntry(methodName) + ) + fb += wa.CallRef(ctx.tableFunctionType(methodName)) + } + + if (receiverClassInfo.isInterface) + genITableDispatch() + else + genVTableDispatch() + } + + private def genApplyStatically(tree: ApplyStatically): Type = { + val ApplyStatically(flags, receiver, className, MethodIdent(methodName), args) = tree + + receiver.tpe match { + case NothingType => + genTree(receiver, NothingType) + // nothing else to do; this is unreachable + NothingType + + case NullType => + genTree(receiver, NullType) + markPosition(tree) + fb += wa.Unreachable // trap + NothingType + + case _ => + val namespace = MemberNamespace.forNonStaticCall(flags) + val targetClassName = { + val classInfo = ctx.getClassInfo(className) + if (!classInfo.isInterface && namespace == MemberNamespace.Public) + classInfo.resolvedMethodInfos(methodName).ownerClass + else + className + } + + BoxedClassToPrimType.get(targetClassName) match { + case None => + genTree(receiver, ClassType(targetClassName)) + fb += wa.RefAsNonNull + + case Some(primReceiverType) => + if (receiver.tpe == primReceiverType) { + genTreeAuto(receiver) + } else { + genTree(receiver, AnyType) + fb += wa.RefAsNonNull + genUnbox(primReceiverType) + } + } + + genArgs(args, methodName) + + markPosition(tree) + val funcID = genFunctionID.forMethod(namespace, targetClassName, methodName) + fb += wa.Call(funcID) + if (tree.tpe == NothingType) + fb += wa.Unreachable + tree.tpe + } + } + + private def genApplyStatic(tree: ApplyStatic): Type = { + val ApplyStatic(flags, className, MethodIdent(methodName), args) = tree + + genArgs(args, methodName) + val namespace = MemberNamespace.forStaticCall(flags) + val funcID = genFunctionID.forMethod(namespace, className, methodName) + markPosition(tree) + fb += wa.Call(funcID) + if (tree.tpe == NothingType) + fb += wa.Unreachable + tree.tpe + } + + private def genApplyDynamicImport(tree: ApplyDynamicImport): Type = { + // As long as we do not support multiple modules, this cannot happen + throw new AssertionError( + s"Unexpected $tree at ${tree.pos}; multiple modules are not supported yet") + } + + private def genArgs(args: List[Tree], methodName: MethodName): Unit = { + for ((arg, paramTypeRef) <- args.zip(methodName.paramTypeRefs)) { + val paramType = ctx.inferTypeFromTypeRef(paramTypeRef) + genTree(arg, paramType) + } + } + + private def genLiteral(tree: Literal, expectedType: Type): Type = { + if (expectedType == NoType) { + /* Since all literals are pure, we can always get rid of them. + * This is mostly useful for the argument of `Return` nodes that target a + * `Labeled` in statement position, since they must have a non-`void` + * type in the IR but they get a `void` expected type. + */ + expectedType + } else { + markPosition(tree) + + tree match { + case BooleanLiteral(v) => fb += wa.I32Const(if (v) 1 else 0) + case ByteLiteral(v) => fb += wa.I32Const(v) + case ShortLiteral(v) => fb += wa.I32Const(v) + case IntLiteral(v) => fb += wa.I32Const(v) + case CharLiteral(v) => fb += wa.I32Const(v) + case LongLiteral(v) => fb += wa.I64Const(v) + case FloatLiteral(v) => fb += wa.F32Const(v) + case DoubleLiteral(v) => fb += wa.F64Const(v) + + case Undefined() => + fb += wa.GlobalGet(genGlobalID.undef) + case Null() => + fb += wa.RefNull(watpe.HeapType.None) + + case StringLiteral(v) => + fb ++= ctx.stringPool.getConstantStringInstr(v) + + case ClassOf(typeRef) => + genLoadTypeData(fb, typeRef) + fb += wa.Call(genFunctionID.getClassOf) + } + + tree.tpe + } + } + + private def genSelect(tree: Select): Type = { + val Select(qualifier, FieldIdent(fieldName)) = tree + + val className = fieldName.className + val classInfo = ctx.getClassInfo(className) + + // For Select, the receiver can never be a hijacked class, so we can use genTreeAuto + genTreeAuto(qualifier) + + markPosition(tree) + + if (!classInfo.hasInstances) { + /* The field may not exist in that case, and we cannot look it up. + * However we necessarily have a `null` receiver if we reach this point, + * so we can trap as NPE. + */ + fb += wa.Unreachable + } else { + fb += wa.StructGet( + genTypeID.forClass(className), + genFieldID.forClassInstanceField(fieldName) + ) + } + + tree.tpe + } + + private def genSelectStatic(tree: SelectStatic): Type = { + val SelectStatic(FieldIdent(fieldName)) = tree + + markPosition(tree) + fb += wa.GlobalGet(genGlobalID.forStaticField(fieldName)) + tree.tpe + } + + private def genStoreModule(tree: StoreModule): Type = { + val className = enclosingClassName.getOrElse { + throw new AssertionError(s"Cannot emit $tree at ${tree.pos} without enclosing class name") + } + + genTreeAuto(This()(ClassType(className))(tree.pos)) + + markPosition(tree) + fb += wa.GlobalSet(genGlobalID.forModuleInstance(className)) + NoType + } + + private def genLoadModule(tree: LoadModule): Type = { + val LoadModule(className) = tree + + markPosition(tree) + fb += wa.Call(genFunctionID.loadModule(className)) + tree.tpe + } + + private def genUnaryOp(tree: UnaryOp): Type = { + import UnaryOp._ + + val UnaryOp(op, lhs) = tree + + genTreeAuto(lhs) + + markPosition(tree) + + (op: @switch) match { + case Boolean_! => + fb += wa.I32Eqz + + // Widening conversions + case CharToInt | ByteToInt | ShortToInt => + /* These are no-ops because they are all represented as i32's with the + * right mathematical value. + */ + () + case IntToLong => + fb += wa.I64ExtendI32S + case IntToDouble => + fb += wa.F64ConvertI32S + case FloatToDouble => + fb += wa.F64PromoteF32 + + // Narrowing conversions + case IntToChar => + fb += wa.I32Const(0xFFFF) + fb += wa.I32And + case IntToByte => + fb += wa.I32Extend8S + case IntToShort => + fb += wa.I32Extend16S + case LongToInt => + fb += wa.I32WrapI64 + case DoubleToInt => + fb += wa.I32TruncSatF64S + case DoubleToFloat => + fb += wa.F32DemoteF64 + + // Long <-> Double (neither widening nor narrowing) + case LongToDouble => + fb += wa.F64ConvertI64S + case DoubleToLong => + fb += wa.I64TruncSatF64S + + // Long -> Float (neither widening nor narrowing) + case LongToFloat => + fb += wa.F32ConvertI64S + + // String.length + case String_length => + fb += wa.Call(genFunctionID.stringLength) + } + + tree.tpe + } + + private def genBinaryOp(tree: BinaryOp): Type = { + import BinaryOp._ + + val BinaryOp(op, lhs, rhs) = tree + + def genLongShiftOp(shiftInstr: wa.Instr): Type = { + genTree(lhs, LongType) + genTree(rhs, IntType) + markPosition(tree) + fb += wa.I64ExtendI32S + fb += shiftInstr + LongType + } + + (op: @switch) match { + case === | !== => + genEq(tree) + + case String_+ => + genStringConcat(tree) + + case Int_/ => + rhs match { + case IntLiteral(rhsValue) => + genDivModByConstant(tree, isDiv = true, rhsValue, wa.I32Const(_), wa.I32Sub, wa.I32DivS) + case _ => + genDivMod(tree, isDiv = true, wa.I32Const(_), wa.I32Eqz, wa.I32Eq, wa.I32Sub, wa.I32DivS) + } + case Int_% => + rhs match { + case IntLiteral(rhsValue) => + genDivModByConstant(tree, isDiv = false, rhsValue, wa.I32Const(_), wa.I32Sub, wa.I32RemS) + case _ => + genDivMod(tree, isDiv = false, wa.I32Const(_), wa.I32Eqz, wa.I32Eq, wa.I32Sub, wa.I32RemS) + } + case Long_/ => + rhs match { + case LongLiteral(rhsValue) => + genDivModByConstant(tree, isDiv = true, rhsValue, wa.I64Const(_), wa.I64Sub, wa.I64DivS) + case _ => + genDivMod(tree, isDiv = true, wa.I64Const(_), wa.I64Eqz, wa.I64Eq, wa.I64Sub, wa.I64DivS) + } + case Long_% => + rhs match { + case LongLiteral(rhsValue) => + genDivModByConstant(tree, isDiv = false, rhsValue, wa.I64Const(_), wa.I64Sub, wa.I64RemS) + case _ => + genDivMod(tree, isDiv = false, wa.I64Const(_), wa.I64Eqz, wa.I64Eq, wa.I64Sub, wa.I64RemS) + } + + case Long_<< => + genLongShiftOp(wa.I64Shl) + case Long_>>> => + genLongShiftOp(wa.I64ShrU) + case Long_>> => + genLongShiftOp(wa.I64ShrS) + + /* Floating point remainders are specified by + * https://262.ecma-international.org/#sec-numeric-types-number-remainder + * which says that it is equivalent to the C library function `fmod`. + * For `Float`s, we promote and demote to `Double`s. + * `fmod` seems quite hard to correctly implement, so we delegate to a + * JavaScript Helper. + * (The naive function `x - trunc(x / y) * y` that we can find on the + * Web does not work.) + */ + case Float_% => + genTree(lhs, FloatType) + fb += wa.F64PromoteF32 + genTree(rhs, FloatType) + fb += wa.F64PromoteF32 + markPosition(tree) + fb += wa.Call(genFunctionID.fmod) + fb += wa.F32DemoteF64 + FloatType + case Double_% => + genTree(lhs, DoubleType) + genTree(rhs, DoubleType) + markPosition(tree) + fb += wa.Call(genFunctionID.fmod) + DoubleType + + case String_charAt => + genTree(lhs, StringType) + genTree(rhs, IntType) + markPosition(tree) + fb += wa.Call(genFunctionID.stringCharAt) + CharType + + case _ => + genTreeAuto(lhs) + genTreeAuto(rhs) + markPosition(tree) + fb += getElementaryBinaryOpInstr(op) + tree.tpe + } + } + + private def genEq(tree: BinaryOp): Type = { + import BinaryOp.{===, !==} + + val BinaryOp(op, lhs, rhs) = tree + assert(op == === || op == !==) + + // TODO Optimize this when the operands have a better type than `any` + + genTree(lhs, AnyType) + genTree(rhs, AnyType) + + markPosition(tree) + + fb += wa.Call(genFunctionID.is) + + if (op == !==) + fb += wa.I32Eqz + + BooleanType + } + + private def getElementaryBinaryOpInstr(op: BinaryOp.Code): wa.Instr = { + import BinaryOp._ + + (op: @switch) match { + case Boolean_== => wa.I32Eq + case Boolean_!= => wa.I32Ne + case Boolean_| => wa.I32Or + case Boolean_& => wa.I32And + + case Int_+ => wa.I32Add + case Int_- => wa.I32Sub + case Int_* => wa.I32Mul + case Int_| => wa.I32Or + case Int_& => wa.I32And + case Int_^ => wa.I32Xor + case Int_<< => wa.I32Shl + case Int_>>> => wa.I32ShrU + case Int_>> => wa.I32ShrS + case Int_== => wa.I32Eq + case Int_!= => wa.I32Ne + case Int_< => wa.I32LtS + case Int_<= => wa.I32LeS + case Int_> => wa.I32GtS + case Int_>= => wa.I32GeS + + case Long_+ => wa.I64Add + case Long_- => wa.I64Sub + case Long_* => wa.I64Mul + case Long_| => wa.I64Or + case Long_& => wa.I64And + case Long_^ => wa.I64Xor + + case Long_== => wa.I64Eq + case Long_!= => wa.I64Ne + case Long_< => wa.I64LtS + case Long_<= => wa.I64LeS + case Long_> => wa.I64GtS + case Long_>= => wa.I64GeS + + case Float_+ => wa.F32Add + case Float_- => wa.F32Sub + case Float_* => wa.F32Mul + case Float_/ => wa.F32Div + + case Double_+ => wa.F64Add + case Double_- => wa.F64Sub + case Double_* => wa.F64Mul + case Double_/ => wa.F64Div + + case Double_== => wa.F64Eq + case Double_!= => wa.F64Ne + case Double_< => wa.F64Lt + case Double_<= => wa.F64Le + case Double_> => wa.F64Gt + case Double_>= => wa.F64Ge + } + } + + private def genStringConcat(tree: BinaryOp): Type = { + val BinaryOp(op, lhs, rhs) = tree + assert(op == BinaryOp.String_+) + + lhs match { + case StringLiteral("") => + // Common case where we don't actually need a concatenation + genToStringForConcat(rhs) + + case _ => + genToStringForConcat(lhs) + genToStringForConcat(rhs) + markPosition(tree) + fb += wa.Call(genFunctionID.stringConcat) + } + + StringType + } + + private def genToStringForConcat(tree: Tree): Unit = { + def genWithDispatch(isAncestorOfHijackedClass: Boolean): Unit = { + /* Somewhat duplicated from genApplyNonPrim, but specialized for + * `toString`, and where the handling of `null` is different. + * + * We need to return the `"null"` string in two special cases: + * - if the value itself is `null`, or + * - if the value's `toString(): String` method returns `null`! + */ + + // A local for a copy of the receiver that we will use to resolve dispatch + val receiverLocalForDispatch = + addSyntheticLocal(watpe.RefType(genTypeID.ObjectStruct)) + + val objectClassInfo = ctx.getClassInfo(ObjectClass) + + if (!isAncestorOfHijackedClass) { + /* Standard dispatch codegen, with dedicated null handling. + * + * The overall structure of the generated code is as follows: + * + * block (ref any) $done + * block $isNull + * load receiver as (ref null java.lang.Object) + * br_on_null $isNull + * generate standard table-based dispatch + * br_on_non_null $done + * end $isNull + * gen "null" + * end $done + */ + + fb.block(watpe.RefType.any) { labelDone => + fb.block() { labelIsNull => + genTreeAuto(tree) + markPosition(tree) + fb += wa.BrOnNull(labelIsNull) + fb += wa.LocalTee(receiverLocalForDispatch) + genTableDispatch(objectClassInfo, toStringMethodName, receiverLocalForDispatch) + fb += wa.BrOnNonNull(labelDone) + } + + fb ++= ctx.stringPool.getConstantStringInstr("null") + } + } else { + /* Dispatch where the receiver can be a JS value. + * + * The overall structure of the generated code is as follows: + * + * block (ref any) $done + * block anyref $notOurObject + * load receiver + * br_on_cast_fail anyref (ref $java.lang.Object) $notOurObject + * generate standard table-based dispatch + * br_on_non_null $done + * ref.null any + * end $notOurObject + * call the JS helper, also handles `null` + * end $done + */ + + fb.block(watpe.RefType.any) { labelDone => + // First try the case where the value is one of our objects + fb.block(watpe.RefType.anyref) { labelNotOurObject => + // Load receiver + genTreeAuto(tree) + + markPosition(tree) + + fb += wa.BrOnCastFail( + labelNotOurObject, + watpe.RefType.anyref, + watpe.RefType(genTypeID.ObjectStruct) + ) + fb += wa.LocalTee(receiverLocalForDispatch) + genTableDispatch(objectClassInfo, toStringMethodName, receiverLocalForDispatch) + fb += wa.BrOnNonNull(labelDone) + fb += wa.RefNull(watpe.HeapType.Any) + } // end block labelNotOurObject + + // Now we have a value that is not one of our objects; the anyref is still on the stack + fb += wa.Call(genFunctionID.jsValueToStringForConcat) + } // end block labelDone + } + } + + tree.tpe match { + case primType: PrimType => + genTreeAuto(tree) + + markPosition(tree) + + primType match { + case StringType => + () // no-op + case BooleanType => + fb += wa.Call(genFunctionID.booleanToString) + case CharType => + fb += wa.Call(genFunctionID.charToString) + case ByteType | ShortType | IntType => + fb += wa.Call(genFunctionID.intToString) + case LongType => + fb += wa.Call(genFunctionID.longToString) + case FloatType => + fb += wa.F64PromoteF32 + fb += wa.Call(genFunctionID.doubleToString) + case DoubleType => + fb += wa.Call(genFunctionID.doubleToString) + case NullType | UndefType => + fb += wa.Call(genFunctionID.jsValueToStringForConcat) + case NothingType => + () // unreachable + case NoType => + throw new AssertionError( + s"Found expression of type void in String_+ at ${tree.pos}: $tree") + } + + case ClassType(BoxedStringClass) => + // Common case for which we want to avoid the hijacked class dispatch + genTreeAuto(tree) + markPosition(tree) + fb += wa.Call(genFunctionID.jsValueToStringForConcat) // for `null` + + case ClassType(className) => + genWithDispatch(ctx.getClassInfo(className).isAncestorOfHijackedClass) + + case AnyType => + genWithDispatch(isAncestorOfHijackedClass = true) + + case ArrayType(_) => + genWithDispatch(isAncestorOfHijackedClass = false) + + case tpe: RecordType => + throw new AssertionError( + s"Invalid type $tpe for String_+ at ${tree.pos}: $tree") + } + } + + private def genDivModByConstant[T](tree: BinaryOp, isDiv: Boolean, + rhsValue: T, const: T => wa.Instr, sub: wa.Instr, mainOp: wa.Instr)( + implicit num: Numeric[T]): Type = { + /* When we statically know the value of the rhs, we can avoid the + * dynamic tests for division by zero and overflow. This is quite + * common in practice. + */ + + import BinaryOp._ + + val BinaryOp(op, lhs, rhs) = tree + assert(op == Int_/ || op == Int_% || op == Long_/ || op == Long_%) + + val tpe = tree.tpe + + if (rhsValue == num.zero) { + genTree(lhs, tpe) + markPosition(tree) + genThrowArithmeticException()(tree.pos) + NothingType + } else if (isDiv && rhsValue == num.fromInt(-1)) { + /* MinValue / -1 overflows; it traps in Wasm but we need to wrap. + * We rewrite as `0 - lhs` so that we do not need any test. + */ + markPosition(tree) + fb += const(num.zero) + genTree(lhs, tpe) + markPosition(tree) + fb += sub + tpe + } else { + genTree(lhs, tpe) + markPosition(rhs) + fb += const(rhsValue) + markPosition(tree) + fb += mainOp + tpe + } + } + + private def genDivMod[T](tree: BinaryOp, isDiv: Boolean, const: T => wa.Instr, + eqz: wa.Instr, eqInstr: wa.Instr, sub: wa.Instr, mainOp: wa.Instr)( + implicit num: Numeric[T]): Type = { + /* Here we perform the same steps as in the static case, but using + * value tests at run-time. + */ + + import BinaryOp._ + + val BinaryOp(op, lhs, rhs) = tree + assert(op == Int_/ || op == Int_% || op == Long_/ || op == Long_%) + + val tpe = tree.tpe + val wasmType = transformType(tpe) + + val lhsLocal = addSyntheticLocal(wasmType) + val rhsLocal = addSyntheticLocal(wasmType) + genTree(lhs, tpe) + fb += wa.LocalSet(lhsLocal) + genTree(rhs, tpe) + fb += wa.LocalTee(rhsLocal) + + markPosition(tree) + + fb += eqz + fb.ifThen() { + genThrowArithmeticException()(tree.pos) + } + if (isDiv) { + // Handle the MinValue / -1 corner case + fb += wa.LocalGet(rhsLocal) + fb += const(num.fromInt(-1)) + fb += eqInstr + fb.ifThenElse(wasmType) { + // 0 - lhs + fb += const(num.zero) + fb += wa.LocalGet(lhsLocal) + fb += sub + } { + // lhs / rhs + fb += wa.LocalGet(lhsLocal) + fb += wa.LocalGet(rhsLocal) + fb += mainOp + } + } else { + // lhs % rhs + fb += wa.LocalGet(lhsLocal) + fb += wa.LocalGet(rhsLocal) + fb += mainOp + } + + tpe + } + + private def genThrowArithmeticException()(implicit pos: Position): Unit = { + val ctorName = MethodName.constructor(List(ClassRef(BoxedStringClass))) + genNewScalaClass(ArithmeticExceptionClass, ctorName) { + fb ++= ctx.stringPool.getConstantStringInstr("/ by zero") + } + fb += wa.ExternConvertAny + fb += wa.Throw(genTagID.exception) + } + + private def genIsInstanceOf(tree: IsInstanceOf): Type = { + val IsInstanceOf(expr, testType) = tree + + genTree(expr, AnyType) + + markPosition(tree) + + def genIsPrimType(testType: PrimType): Unit = testType match { + case UndefType => + fb += wa.Call(genFunctionID.isUndef) + case StringType => + fb += wa.Call(genFunctionID.isString) + case CharType => + val structTypeID = genTypeID.forClass(SpecialNames.CharBoxClass) + fb += wa.RefTest(watpe.RefType(structTypeID)) + case LongType => + val structTypeID = genTypeID.forClass(SpecialNames.LongBoxClass) + fb += wa.RefTest(watpe.RefType(structTypeID)) + case NoType | NothingType | NullType => + throw new AssertionError(s"Illegal isInstanceOf[$testType]") + case testType: PrimTypeWithRef => + fb += wa.Call(genFunctionID.typeTest(testType.primRef)) + } + + testType match { + case testType: PrimType => + genIsPrimType(testType) + + case AnyType | ClassType(ObjectClass) => + fb += wa.RefIsNull + fb += wa.I32Eqz + + case ClassType(JLNumberClass) => + /* Special case: the only non-Object *class* that is an ancestor of a + * hijacked class. We need to accept `number` primitives here. + */ + val tempLocal = addSyntheticLocal(watpe.RefType.anyref) + fb += wa.LocalTee(tempLocal) + fb += wa.RefTest(watpe.RefType(genTypeID.forClass(JLNumberClass))) + fb.ifThenElse(watpe.Int32) { + fb += wa.I32Const(1) + } { + fb += wa.LocalGet(tempLocal) + fb += wa.Call(genFunctionID.typeTest(DoubleRef)) + } + + case ClassType(testClassName) => + BoxedClassToPrimType.get(testClassName) match { + case Some(primType) => + genIsPrimType(primType) + case None => + if (ctx.getClassInfo(testClassName).isInterface) + fb += wa.Call(genFunctionID.instanceTest(testClassName)) + else + fb += wa.RefTest(watpe.RefType(genTypeID.forClass(testClassName))) + } + + case ArrayType(arrayTypeRef) => + arrayTypeRef match { + case ArrayTypeRef(ClassRef(ObjectClass) | _: PrimRef, 1) => + // For primitive arrays and exactly Array[Object], a wa.RefTest is enough + val structTypeID = genTypeID.forArrayClass(arrayTypeRef) + fb += wa.RefTest(watpe.RefType(structTypeID)) + + case _ => + /* Non-Object reference array types need a sophisticated type test + * based on assignability of component types. + */ + import watpe.RefType.anyref + + fb.block(Sig(List(anyref), List(watpe.Int32))) { doneLabel => + fb.block(Sig(List(anyref), List(anyref))) { notARefArrayLabel => + // Try and cast to the generic representation first + val refArrayStructTypeID = genTypeID.forArrayClass(arrayTypeRef) + fb += wa.BrOnCastFail( + notARefArrayLabel, + watpe.RefType.anyref, + watpe.RefType(refArrayStructTypeID) + ) + + // refArrayValue := the generic representation + val refArrayValueLocal = + addSyntheticLocal(watpe.RefType(refArrayStructTypeID)) + fb += wa.LocalSet(refArrayValueLocal) + + // Load typeDataOf(arrayTypeRef) + genLoadArrayTypeData(fb, arrayTypeRef) + + // Load refArrayValue.vtable + fb += wa.LocalGet(refArrayValueLocal) + fb += wa.StructGet(refArrayStructTypeID, genFieldID.objStruct.vtable) + + // Call isAssignableFrom and return its result + fb += wa.Call(genFunctionID.isAssignableFrom) + fb += wa.Br(doneLabel) + } + + // Here, the value is not a reference array type, so return false + fb += wa.Drop + fb += wa.I32Const(0) + } + } + + case testType: RecordType => + throw new AssertionError(s"Illegal type in IsInstanceOf: $testType") + } + + BooleanType + } + + private def genAsInstanceOf(tree: AsInstanceOf): Type = { + val AsInstanceOf(expr, targetTpe) = tree + + val sourceTpe = expr.tpe + + if (sourceTpe == NothingType) { + // We cannot call transformType for NothingType, so we have to handle this case separately. + genTree(expr, NothingType) + NothingType + } else { + // By IR checker rules, targetTpe is none of NothingType, NullType, NoType or RecordType + + val sourceWasmType = transformType(sourceTpe) + val targetWasmType = transformType(targetTpe) + + if (sourceWasmType == targetWasmType) { + /* Common case where no cast is necessary at the Wasm level. + * Note that this is not *obviously* correct. It is only correct + * because, under our choices of representation and type translation + * rules, there is no pair `(sourceTpe, targetTpe)` for which the Wasm + * types are equal but a valid cast would require a *conversion*. + */ + genTreeAuto(expr) + } else { + genTree(expr, AnyType) + + markPosition(tree) + + targetTpe match { + case targetTpe: PrimType => + // TODO Opt: We could do something better for things like double.asInstanceOf[int] + genUnbox(targetTpe) + + case _ => + targetWasmType match { + case watpe.RefType(true, watpe.HeapType.Any) => + () // nothing to do + case targetWasmType: watpe.RefType => + fb += wa.RefCast(targetWasmType) + case _ => + throw new AssertionError(s"Unexpected type in AsInstanceOf: $targetTpe") + } + } + } + + targetTpe + } + } + + /** Unbox the `anyref` on the stack to the target `PrimType`. + * + * `targetTpe` must not be `NothingType`, `NullType` nor `NoType`. + * + * The type left on the stack is non-nullable. + */ + private def genUnbox(targetTpe: PrimType): Unit = { + targetTpe match { + case UndefType => + fb += wa.Drop + fb += wa.GlobalGet(genGlobalID.undef) + + case StringType => + fb += wa.RefAsNonNull + + case CharType | LongType => + // Extract the `value` field (the only field) out of the box class. + + val boxClass = + if (targetTpe == CharType) SpecialNames.CharBoxClass + else SpecialNames.LongBoxClass + val fieldName = FieldName(boxClass, SpecialNames.valueFieldSimpleName) + val resultType = transformType(targetTpe) + + fb.block(Sig(List(watpe.RefType.anyref), List(resultType))) { doneLabel => + fb.block(Sig(List(watpe.RefType.anyref), Nil)) { isNullLabel => + fb += wa.BrOnNull(isNullLabel) + val structTypeID = genTypeID.forClass(boxClass) + fb += wa.RefCast(watpe.RefType(structTypeID)) + fb += wa.StructGet( + structTypeID, + genFieldID.forClassInstanceField(fieldName) + ) + fb += wa.Br(doneLabel) + } + fb += genZeroOf(targetTpe) + } + + case NothingType | NullType | NoType => + throw new IllegalArgumentException(s"Illegal type in genUnbox: $targetTpe") + + case targetTpe: PrimTypeWithRef => + fb += wa.Call(genFunctionID.unbox(targetTpe.primRef)) + } + } + + private def genGetClass(tree: GetClass): Type = { + /* Unlike in `genApply` or `genStringConcat`, here we make no effort to + * optimize known-primitive receivers. In practice, such cases would be + * useless. + */ + + val GetClass(expr) = tree + + val needHijackedClassDispatch = expr.tpe match { + case ClassType(className) => + ctx.getClassInfo(className).isAncestorOfHijackedClass + case ArrayType(_) | NothingType | NullType => + false + case _ => + true + } + + if (!needHijackedClassDispatch) { + val typeDataLocal = addSyntheticLocal(watpe.RefType(genTypeID.typeData)) + + genTreeAuto(expr) + markPosition(tree) + fb += wa.StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.vtable) // implicit trap on null + fb += wa.Call(genFunctionID.getClassOf) + } else { + genTree(expr, AnyType) + markPosition(tree) + fb += wa.RefAsNonNull + fb += wa.Call(genFunctionID.anyGetClass) + } + + tree.tpe + } + + private def genReadStorage(storage: VarStorage): Unit = { + storage match { + case VarStorage.Local(localID) => + fb += wa.LocalGet(localID) + case VarStorage.StructField(structLocal, structTypeID, fieldID) => + fb += wa.LocalGet(structLocal) + fb += wa.StructGet(structTypeID, fieldID) + } + } + + private def genVarRef(tree: VarRef): Type = { + val VarRef(LocalIdent(name)) = tree + + markPosition(tree) + if (tree.tpe == NothingType) + fb += wa.Unreachable + else + genReadStorage(lookupLocal(name)) + tree.tpe + } + + private def genThis(tree: This): Type = { + markPosition(tree) + genReadStorage(receiverStorage) + tree.tpe + } + + private def genVarDef(tree: VarDef): Type = { + /* This is an isolated VarDef that is not in a Block. + * Its scope is empty by construction, and therefore it need not be stored. + */ + val VarDef(_, _, _, _, rhs) = tree + genTree(rhs, NoType) + NoType + } + + private def genIf(tree: If, expectedType: Type): Type = { + val If(cond, thenp, elsep) = tree + + val ty = transformResultType(expectedType) + genTree(cond, BooleanType) + + markPosition(tree) + + elsep match { + case Skip() => + assert(expectedType == NoType) + fb.ifThen() { + genTree(thenp, expectedType) + } + case _ => + fb.ifThenElse(ty) { + genTree(thenp, expectedType) + } { + genTree(elsep, expectedType) + } + } + + if (expectedType == NothingType) + fb += wa.Unreachable + + expectedType + } + + private def genWhile(tree: While): Type = { + val While(cond, body) = tree + + cond match { + case BooleanLiteral(true) => + // infinite loop that must be typed as `nothing`, i.e., unreachable + markPosition(tree) + fb.loop() { label => + genTree(body, NoType) + markPosition(tree) + fb += wa.Br(label) + } + fb += wa.Unreachable + NothingType + + case _ => + // normal loop typed as `void` + markPosition(tree) + fb.loop() { label => + genTree(cond, BooleanType) + markPosition(tree) + fb.ifThen() { + genTree(body, NoType) + markPosition(tree) + fb += wa.Br(label) + } + } + NoType + } + } + + private def genForIn(tree: ForIn): Type = { + /* This is tricky. In general, the body of a ForIn can be an arbitrary + * statement, which can refer to the enclosing scope and its locals, + * including for mutations. Unfortunately, there is no way to implement a + * ForIn other than actually doing a JS `for (var key in obj) { body }` + * loop. That means we need to pass the `body` as a JS closure. + * + * That is problematic for our backend because we basically need to perform + * lambda lifting: identifying captures ourselves, and turn references to + * local variables into accessing the captured environment. + * + * We side-step this issue for now by exploiting the known shape of `ForIn` + * generated by the Scala.js compiler. This is fine as long as we do not + * support the Scala.js optimizer. We will have to revisit this code when + * we add that support. + */ + + val ForIn(obj, LocalIdent(keyVarName), _, body) = tree + + body match { + case JSFunctionApply(fVarRef: VarRef, List(VarRef(argIdent))) + if fVarRef.ident.name != keyVarName && argIdent.name == keyVarName => + genTree(obj, AnyType) + genTree(fVarRef, AnyType) + markPosition(tree) + fb += wa.Call(genFunctionID.jsForInSimple) + + case _ => + throw new NotImplementedError(s"Unsupported shape of ForIn node at ${tree.pos}: $tree") + } + + NoType + } + + private def genTryCatch(tree: TryCatch, expectedType: Type): Type = { + val TryCatch(block, LocalIdent(errVarName), errVarOrigName, handler) = tree + + val resultType = transformResultType(expectedType) + + if (UseLegacyExceptionsForTryCatch) { + markPosition(tree) + fb += wa.Try(fb.sigToBlockType(Sig(Nil, resultType))) + genTree(block, expectedType) + markPosition(tree) + fb += wa.Catch(genTagID.exception) + withNewLocal(errVarName, errVarOrigName, watpe.RefType.anyref) { exceptionLocal => + fb += wa.AnyConvertExtern + fb += wa.LocalSet(exceptionLocal) + genTree(handler, expectedType) + } + fb += wa.End + } else { + markPosition(tree) + fb.block(resultType) { doneLabel => + fb.block(watpe.RefType.externref) { catchLabel => + /* We used to have `resultType` as result of the try_table, with the + * `wa.BR(doneLabel)` outside of the try_table. Unfortunately it seems + * V8 cannot handle try_table with a result type that is `(ref ...)`. + * The current encoding with `externref` as result type (to match the + * enclosing block) and the `br` *inside* the `try_table` works. + */ + fb.tryTable(watpe.RefType.externref)( + List(wa.CatchClause.Catch(genTagID.exception, catchLabel)) + ) { + genTree(block, expectedType) + markPosition(tree) + fb += wa.Br(doneLabel) + } + } // end block $catch + withNewLocal(errVarName, errVarOrigName, watpe.RefType.anyref) { exceptionLocal => + fb += wa.AnyConvertExtern + fb += wa.LocalSet(exceptionLocal) + genTree(handler, expectedType) + } + } // end block $done + } + + if (expectedType == NothingType) + fb += wa.Unreachable + + expectedType + } + + private def genThrow(tree: Throw): Type = { + val Throw(expr) = tree + + genTree(expr, AnyType) + markPosition(tree) + fb += wa.ExternConvertAny + fb += wa.Throw(genTagID.exception) + + NothingType + } + + private def genBlock(tree: Block, expectedType: Type): Type = { + val Block(stats) = tree + + genBlockStats(stats.init) { + genTree(stats.last, expectedType) + } + expectedType + } + + final def genBlockStats(stats: List[Tree])(inner: => Unit): Unit = { + val savedEnv = currentEnv + + for (stat <- stats) { + stat match { + case VarDef(LocalIdent(name), originalName, vtpe, _, rhs) => + genTree(rhs, vtpe) + markPosition(stat) + val local = fb.addLocal(originalName.orElse(name), transformLocalType(vtpe)) + currentEnv = currentEnv.updated(name, VarStorage.Local(local)) + fb += wa.LocalSet(local) + case _ => + genTree(stat, NoType) + } + } + + inner + + currentEnv = savedEnv + } + + private def genNew(tree: New): Type = { + val New(className, MethodIdent(ctorName), args) = tree + + genNewScalaClass(className, ctorName) { + genArgs(args, ctorName) + } (tree.pos) + + tree.tpe + } + + private def genNewScalaClass(cls: ClassName, ctor: MethodName)( + genCtorArgs: => Unit)(implicit pos: Position): Unit = { + + /* Do not use transformType here, because we must get the struct type even + * if the given class is an ancestor of hijacked classes (which in practice + * is only the case for j.l.Object). + */ + val instanceLocal = addSyntheticLocal(watpe.RefType(genTypeID.forClass(cls))) + + markPosition(pos) + fb += wa.Call(genFunctionID.newDefault(cls)) + fb += wa.LocalTee(instanceLocal) + genCtorArgs + markPosition(pos) + fb += wa.Call(genFunctionID.forMethod(MemberNamespace.Constructor, cls, ctor)) + fb += wa.LocalGet(instanceLocal) + } + + /** Codegen to box a primitive `char`/`long` into a `CharacterBox`/`LongBox`. */ + private def genBox(primType: watpe.SimpleType, boxClassName: ClassName): Type = { + val primLocal = addSyntheticLocal(primType) + + /* We use a direct `StructNew` instead of the logical call to `newDefault` + * plus constructor call. We can do this because we know that this is + * what the constructor would do anyway (so we're basically inlining it). + */ + + fb += wa.LocalSet(primLocal) + fb += wa.GlobalGet(genGlobalID.forVTable(boxClassName)) + fb += wa.GlobalGet(genGlobalID.forITable(boxClassName)) + fb += wa.LocalGet(primLocal) + fb += wa.StructNew(genTypeID.forClass(boxClassName)) + + ClassType(boxClassName) + } + + private def genIdentityHashCode(tree: IdentityHashCode): Type = { + val IdentityHashCode(expr) = tree + + // TODO Avoid dispatch when we know a more precise type than any + genTree(expr, AnyType) + + markPosition(tree) + fb += wa.Call(genFunctionID.identityHashCode) + + IntType + } + + private def genWrapAsThrowable(tree: WrapAsThrowable): Type = { + val WrapAsThrowable(expr) = tree + + val nonNullThrowableType = watpe.RefType(genTypeID.ThrowableStruct) + val jsExceptionType = watpe.RefType(genTypeID.JSExceptionStruct) + + fb.block(nonNullThrowableType) { doneLabel => + genTree(expr, AnyType) + + markPosition(tree) + + // if expr.isInstanceOf[Throwable], then br $done + fb += wa.BrOnCast(doneLabel, watpe.RefType.anyref, nonNullThrowableType) + + // otherwise, wrap in a new JavaScriptException + + val exprLocal = addSyntheticLocal(watpe.RefType.anyref) + val instanceLocal = addSyntheticLocal(jsExceptionType) + + fb += wa.LocalSet(exprLocal) + fb += wa.Call(genFunctionID.newDefault(SpecialNames.JSExceptionClass)) + fb += wa.LocalTee(instanceLocal) + fb += wa.LocalGet(exprLocal) + fb += wa.Call( + genFunctionID.forMethod( + MemberNamespace.Constructor, + SpecialNames.JSExceptionClass, + SpecialNames.AnyArgConstructorName + ) + ) + fb += wa.LocalGet(instanceLocal) + } + + tree.tpe + } + + private def genUnwrapFromThrowable(tree: UnwrapFromThrowable): Type = { + val UnwrapFromThrowable(expr) = tree + + fb.block(watpe.RefType.anyref) { doneLabel => + genTree(expr, ClassType(ThrowableClass)) + + markPosition(tree) + + fb += wa.RefAsNonNull + + // if !expr.isInstanceOf[js.JavaScriptException], then br $done + fb += wa.BrOnCastFail( + doneLabel, + watpe.RefType(genTypeID.ThrowableStruct), + watpe.RefType(genTypeID.JSExceptionStruct) + ) + + // otherwise, unwrap the JavaScriptException by reading its field + fb += wa.StructGet( + genTypeID.JSExceptionStruct, + genFieldID.forClassInstanceField(SpecialNames.exceptionFieldName) + ) + } + + AnyType + } + + private def genJSNew(tree: JSNew): Type = { + val JSNew(ctor, args) = tree + + genTree(ctor, AnyType) + genJSArgsArray(args) + markPosition(tree) + fb += wa.Call(genFunctionID.jsNew) + AnyType + } + + private def genJSSelect(tree: JSSelect): Type = { + val JSSelect(qualifier, item) = tree + + genTree(qualifier, AnyType) + genTree(item, AnyType) + markPosition(tree) + fb += wa.Call(genFunctionID.jsSelect) + AnyType + } + + private def genJSFunctionApply(tree: JSFunctionApply): Type = { + val JSFunctionApply(fun, args) = tree + + genTree(fun, AnyType) + genJSArgsArray(args) + markPosition(tree) + fb += wa.Call(genFunctionID.jsFunctionApply) + AnyType + } + + private def genJSMethodApply(tree: JSMethodApply): Type = { + val JSMethodApply(receiver, method, args) = tree + + genTree(receiver, AnyType) + genTree(method, AnyType) + genJSArgsArray(args) + markPosition(tree) + fb += wa.Call(genFunctionID.jsMethodApply) + AnyType + } + + private def genJSImportCall(tree: JSImportCall): Type = { + val JSImportCall(arg) = tree + + genTree(arg, AnyType) + markPosition(tree) + fb += wa.Call(genFunctionID.jsImportCall) + AnyType + } + + private def genJSImportMeta(tree: JSImportMeta): Type = { + markPosition(tree) + fb += wa.Call(genFunctionID.jsImportMeta) + AnyType + } + + private def genLoadJSConstructor(tree: LoadJSConstructor): Type = { + val LoadJSConstructor(className) = tree + + markPosition(tree) + SWasmGen.genLoadJSConstructor(fb, className) + AnyType + } + + private def genLoadJSModule(tree: LoadJSModule): Type = { + val LoadJSModule(className) = tree + + markPosition(tree) + + ctx.getClassInfo(className).jsNativeLoadSpec match { + case Some(loadSpec) => + genLoadJSFromSpec(fb, loadSpec) + case None => + // This is a non-native JS module + fb += wa.Call(genFunctionID.loadModule(className)) + } + + AnyType + } + + private def genSelectJSNativeMember(tree: SelectJSNativeMember): Type = { + val SelectJSNativeMember(className, MethodIdent(memberName)) = tree + + val info = ctx.getClassInfo(className) + val jsNativeLoadSpec = info.jsNativeMembers.getOrElse(memberName, { + throw new AssertionError( + s"Found $tree for non-existing JS native member at ${tree.pos}") + }) + markPosition(tree) + genLoadJSFromSpec(fb, jsNativeLoadSpec) + AnyType + } + + private def genJSDelete(tree: JSDelete): Type = { + val JSDelete(qualifier, item) = tree + + genTree(qualifier, AnyType) + genTree(item, AnyType) + markPosition(tree) + fb += wa.Call(genFunctionID.jsDelete) + NoType + } + + private def genJSUnaryOp(tree: JSUnaryOp): Type = { + val JSUnaryOp(op, lhs) = tree + + genTree(lhs, AnyType) + markPosition(tree) + fb += wa.Call(genFunctionID.jsUnaryOps(op)) + AnyType + } + + private def genJSBinaryOp(tree: JSBinaryOp): Type = { + val JSBinaryOp(op, lhs, rhs) = tree + + op match { + case JSBinaryOp.|| | JSBinaryOp.&& => + /* Here we need to implement the short-circuiting behavior, with a + * condition based on the truthy value of the left-hand-side. + */ + val lhsLocal = addSyntheticLocal(watpe.RefType.anyref) + genTree(lhs, AnyType) + markPosition(tree) + fb += wa.LocalTee(lhsLocal) + fb += wa.Call(genFunctionID.jsIsTruthy) + if (op == JSBinaryOp.||) { + fb.ifThenElse(watpe.RefType.anyref) { + fb += wa.LocalGet(lhsLocal) + } { + genTree(rhs, AnyType) + markPosition(tree) + } + } else { + fb.ifThenElse(watpe.RefType.anyref) { + genTree(rhs, AnyType) + markPosition(tree) + } { + fb += wa.LocalGet(lhsLocal) + } + } + + case _ => + genTree(lhs, AnyType) + genTree(rhs, AnyType) + markPosition(tree) + fb += wa.Call(genFunctionID.jsBinaryOps(op)) + } + + tree.tpe + } + + private def genJSArrayConstr(tree: JSArrayConstr): Type = { + val JSArrayConstr(items) = tree + + markPosition(tree) + genJSArgsArray(items) + AnyType + } + + private def genJSObjectConstr(tree: JSObjectConstr): Type = { + val JSObjectConstr(fields) = tree + + markPosition(tree) + fb += wa.Call(genFunctionID.jsNewObject) + for ((prop, value) <- fields) { + genTree(prop, AnyType) + genTree(value, AnyType) + fb += wa.Call(genFunctionID.jsObjectPush) + } + AnyType + } + + private def genJSGlobalRef(tree: JSGlobalRef): Type = { + val JSGlobalRef(name) = tree + + markPosition(tree) + fb ++= ctx.stringPool.getConstantStringInstr(name) + fb += wa.Call(genFunctionID.jsGlobalRefGet) + AnyType + } + + private def genJSTypeOfGlobalRef(tree: JSTypeOfGlobalRef): Type = { + val JSTypeOfGlobalRef(JSGlobalRef(name)) = tree + + markPosition(tree) + fb ++= ctx.stringPool.getConstantStringInstr(name) + fb += wa.Call(genFunctionID.jsGlobalRefTypeof) + AnyType + } + + private def genJSArgsArray(args: List[TreeOrJSSpread]): Unit = { + fb += wa.Call(genFunctionID.jsNewArray) + for (arg <- args) { + arg match { + case arg: Tree => + genTree(arg, AnyType) + fb += wa.Call(genFunctionID.jsArrayPush) + case JSSpread(items) => + genTree(items, AnyType) + fb += wa.Call(genFunctionID.jsArraySpreadPush) + } + } + } + + private def genJSLinkingInfo(tree: JSLinkingInfo): Type = { + markPosition(tree) + fb += wa.GlobalGet(genGlobalID.jsLinkingInfo) + AnyType + } + + private def genArrayLength(tree: ArrayLength): Type = { + val ArrayLength(array) = tree + + genTreeAuto(array) + + markPosition(tree) + + array.tpe match { + case ArrayType(arrayTypeRef) => + // Get the underlying array; implicit trap on null + fb += wa.StructGet( + genTypeID.forArrayClass(arrayTypeRef), + genFieldID.objStruct.arrayUnderlying + ) + // Get the length + fb += wa.ArrayLen + IntType + + case NothingType => + // unreachable + NothingType + case NullType => + fb += wa.Unreachable + NothingType + case _ => + throw new IllegalArgumentException( + s"ArraySelect.array must be an array type, but has type ${tree.array.tpe}") + } + } + + 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}") + } + + markPosition(tree) + + if (lengths.size == 1) { + genLoadVTableAndITableForArray(fb, arrayTypeRef) + + // Create the underlying array + genTree(lengths.head, IntType) + markPosition(tree) + + 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) + + fb += wa.Call(genFunctionID.newArrayObject) + } + + tree.tpe + } + + private def genArraySelect(tree: ArraySelect): Type = { + val ArraySelect(array, index) = tree + + genTreeAuto(array) + + markPosition(tree) + + array.tpe match { + case ArrayType(arrayTypeRef) => + // Get the underlying array; implicit trap on null + fb += wa.StructGet( + genTypeID.forArrayClass(arrayTypeRef), + genFieldID.objStruct.arrayUnderlying + ) + + // Load the index + genTree(index, IntType) + + markPosition(tree) + + // Use the appropriate variant of array.get for sign extension + val typeIdx = genTypeID.underlyingOf(arrayTypeRef) + arrayTypeRef match { + case ArrayTypeRef(BooleanRef | CharRef, 1) => + fb += wa.ArrayGetU(typeIdx) + case ArrayTypeRef(ByteRef | ShortRef, 1) => + fb += wa.ArrayGetS(typeIdx) + case _ => + fb += wa.ArrayGet(typeIdx) + } + + /* If it is a reference array type whose element type does not translate + * to `anyref`, we must cast down the result. + */ + arrayTypeRef match { + case ArrayTypeRef(_: PrimRef, 1) => + // a primitive array always has the correct type + () + case _ => + transformType(tree.tpe) match { + case watpe.RefType.anyref => + // nothing to do + () + case refType: watpe.RefType => + fb += wa.RefCast(refType) + case otherType => + throw new AssertionError(s"Unexpected result type for reference array: $otherType") + } + } + + tree.tpe + + case NothingType => + // unreachable + NothingType + case NullType => + fb += wa.Unreachable + NothingType + case _ => + throw new IllegalArgumentException( + s"ArraySelect.array must be an array type, but has type ${array.tpe}") + } + } + + private def genArrayValue(tree: ArrayValue): Type = { + val ArrayValue(arrayTypeRef, elems) = tree + + val expectedElemType = arrayTypeRef match { + case ArrayTypeRef(base: PrimRef, 1) => base.tpe + case _ => AnyType + } + + // Mark the position for the header of `genArrayValue` + markPosition(tree) + + SWasmGen.genArrayValue(fb, arrayTypeRef, elems.size) { + // Create the underlying array + elems.foreach(genTree(_, expectedElemType)) + + // Re-mark the position for the footer of `genArrayValue` + markPosition(tree) + } + + tree.tpe + } + + private def genClosure(tree: Closure): Type = { + val Closure(arrow, captureParams, params, restParam, body, captureValues) = tree + + val hasThis = !arrow + val hasRestParam = restParam.isDefined + val dataStructTypeID = ctx.getClosureDataStructType(captureParams.map(_.ptpe)) + + // Define the function where captures are reified as a `__captureData` argument. + val closureFuncOrigName = genClosureFuncOriginalName() + val closureFuncID = new ClosureFunctionID(closureFuncOrigName) + emitFunction( + closureFuncID, + closureFuncOrigName, + enclosingClassName = None, + Some(captureParams), + receiverType = if (!hasThis) None else Some(watpe.RefType.anyref), + params, + restParam, + body, + resultType = AnyType + )(ctx, tree.pos) + + markPosition(tree) + + // Put a reference to the function on the stack + fb += ctx.refFuncWithDeclaration(closureFuncID) + + // Evaluate the capture values and instantiate the capture data struct + for ((param, value) <- captureParams.zip(captureValues)) + genTree(value, param.ptpe) + markPosition(tree) + fb += wa.StructNew(dataStructTypeID) + + /* If there is a ...rest param, the helper requires as third argument the + * number of regular arguments. + */ + if (hasRestParam) + fb += wa.I32Const(params.size) + + // Call the appropriate helper + val helper = (hasThis, hasRestParam) match { + case (false, false) => genFunctionID.closure + case (true, false) => genFunctionID.closureThis + case (false, true) => genFunctionID.closureRest + case (true, true) => genFunctionID.closureThisRest + } + fb += wa.Call(helper) + + AnyType + } + + private def genClone(tree: Clone): Type = { + val Clone(expr) = tree + + expr.tpe match { + case NothingType => + genTree(expr, NothingType) + NothingType + + case NullType => + genTree(expr, NullType) + fb += wa.Unreachable // trap for NPE + NothingType + + case exprType => + val exprLocal = addSyntheticLocal(watpe.RefType(genTypeID.ObjectStruct)) + + genTree(expr, ClassType(CloneableClass)) + + markPosition(tree) + + fb += wa.RefAsNonNull + fb += wa.LocalTee(exprLocal) + + fb += wa.LocalGet(exprLocal) + fb += wa.StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.vtable) + fb += wa.StructGet(genTypeID.typeData, genFieldID.typeData.cloneFunction) + // cloneFunction: (ref jl.Object) -> (ref jl.Object) + fb += wa.CallRef(genTypeID.cloneFunctionType) + + // cast the (ref jl.Object) back down to the result type + transformType(exprType) match { + case watpe.RefType(_, watpe.HeapType.Type(genTypeID.ObjectStruct)) => + // no need to cast to (ref null? jl.Object) + case wasmType: watpe.RefType => + fb += wa.RefCast(wasmType.toNonNullable) + case wasmType => + // Since no hijacked class extends jl.Cloneable, this case cannot happen + throw new AssertionError( + s"Unexpected type for Clone: $exprType (Wasm: $wasmType)") + } + + exprType + } + } + + private def genMatch(tree: Match, expectedType: Type): Type = { + val Match(selector, cases, defaultBody) = tree + + val selectorLocal = addSyntheticLocal(transformType(selector.tpe)) + + genTreeAuto(selector) + + markPosition(tree) + + fb += wa.LocalSet(selectorLocal) + + fb.block(transformResultType(expectedType)) { doneLabel => + fb.block() { defaultLabel => + val caseLabels = cases.map(c => c._1 -> fb.genLabel()) + for (caseLabel <- caseLabels) + fb += wa.Block(wa.BlockType.ValueType(), Some(caseLabel._2)) + + for { + (matchableLiterals, label) <- caseLabels + matchableLiteral <- matchableLiterals + } { + markPosition(matchableLiteral) + fb += wa.LocalGet(selectorLocal) + matchableLiteral match { + case IntLiteral(value) => + fb += wa.I32Const(value) + fb += wa.I32Eq + fb += wa.BrIf(label) + case StringLiteral(value) => + fb ++= ctx.stringPool.getConstantStringInstr(value) + fb += wa.Call(genFunctionID.is) + fb += wa.BrIf(label) + case Null() => + fb += wa.RefIsNull + fb += wa.BrIf(label) + } + } + fb += wa.Br(defaultLabel) + + for { + (caseLabel, (_, caseBody)) <- caseLabels.zip(cases).reverse + } { + markPosition(caseBody) + fb += wa.End + genTree(caseBody, expectedType) + fb += wa.Br(doneLabel) + } + } + genTree(defaultBody, expectedType) + } + + if (expectedType == NothingType) + fb += wa.Unreachable + + expectedType + } + + private def genCreateJSClass(tree: CreateJSClass): Type = { + val CreateJSClass(className, captureValues) = tree + + val classInfo = ctx.getClassInfo(className) + val jsClassCaptures = classInfo.jsClassCaptures.getOrElse { + throw new AssertionError( + s"Illegal CreateJSClass of top-level class ${className.nameString}") + } + + for ((captureValue, captureParam) <- captureValues.zip(jsClassCaptures)) + genTree(captureValue, captureParam.ptpe) + + markPosition(tree) + + fb += wa.Call(genFunctionID.createJSClassOf(className)) + + AnyType + } + + private def genJSPrivateSelect(tree: JSPrivateSelect): Type = { + val JSPrivateSelect(qualifier, FieldIdent(fieldName)) = tree + + genTree(qualifier, AnyType) + + markPosition(tree) + + fb += wa.GlobalGet(genGlobalID.forJSPrivateField(fieldName)) + fb += wa.Call(genFunctionID.jsSelect) + + AnyType + } + + private def genJSSuperSelect(tree: JSSuperSelect): Type = { + val JSSuperSelect(superClass, receiver, item) = tree + + genTree(superClass, AnyType) + genTree(receiver, AnyType) + genTree(item, AnyType) + + markPosition(tree) + + fb += wa.Call(genFunctionID.jsSuperSelect) + + AnyType + } + + private def genJSSuperMethodCall(tree: JSSuperMethodCall): Type = { + val JSSuperMethodCall(superClass, receiver, method, args) = tree + + genTree(superClass, AnyType) + genTree(receiver, AnyType) + genTree(method, AnyType) + genJSArgsArray(args) + + markPosition(tree) + + fb += wa.Call(genFunctionID.jsSuperCall) + + AnyType + } + + private def genJSNewTarget(tree: JSNewTarget): Type = { + markPosition(tree) + + genReadStorage(newTargetStorage) + + AnyType + } + + /*--------------------------------------------------------------------* + * HERE BE DRAGONS --- Handling of TryFinally, Labeled and Return --- * + *--------------------------------------------------------------------*/ + + /* From this point onwards, and until the end of the file, you will find + * the infrastructure required to handle TryFinally and Labeled/Return pairs. + * + * Independently, TryFinally and Labeled/Return are not very difficult to + * handle. The dragons come when they interact, and in particular when a + * TryFinally stands in the middle of a Labeled/Return pair. + * + * For example: + * + * val foo: int = alpha[int]: { + * val bar: string = try { + * if (somethingHappens) + * return@alpha 5 + * "bar" + * } finally { + * doTheFinally() + * } + * someOtherThings(bar) + * } + * + * In that situation, if we naively translate the `return@alpha` into + * `br $alpha`, we bypass the `finally` block, which goes against the spec. + * + * Instead, we must stash the result 5 in a local and jump to the finally + * block. The issue is that, at the end of `doTheFinally()`, we need to keep + * propagating further up (instead of executing `someOtherThings()`). + * + * That means that there are 3 possible outcomes after the `finally` block: + * + * - Rethrow the exception if we caught one. + * - Reload the stashed result and branch further to `alpha`. + * - Otherwise keep going to do `someOtherThings()`. + * + * Now what if there are *several* labels for which we cross that + * `try..finally`? Well we need to deal with all the possible labels. This + * means that, in general, we in fact have `2 + n` possible outcomes, where + * `n` is the number of labels for which we found a `Return` that crosses the + * boundary. + * + * In order to know whether we need to rethrow, we look at a nullable + * `exnref`. For the remaining cases, we use a separate `destinationTag` + * local. Every label gets assigned a distinct tag > 0. Fall-through is + * always represented by 0. Before branching to a `finally` block, we set the + * appropriate value to the `destinationTag` value. + * + * Since the various labels can have different result types, and since they + * can be different from the result of the regular flow of the `try` block, + * we cannot use the stack for the `try_table` itself: each label has a + * dedicated local for its result if it comes from such a crossing `return`. + * + * Two more complications: + * + * - If the `finally` block itself contains another `try..finally`, they may + * need a `destinationTag` concurrently. Therefore, every `try..finally` + * gets its own `destinationTag` local. We do not need this for another + * `try..finally` inside a `try` (or elsewhere in the function), so this is + * not an optimal allocation; we do it this way not to complicate this + * further. + * - If the `try` block contains another `try..finally`, so that there are + * two (or more) `try..finally` in the way between a `Return` and a + * `Labeled`, we must forward to the next `finally` in line (and its own + * `destinationTag` local) so that the whole chain gets executed before + * reaching the `Labeled`. + * + * --- + * + * As an evil example of everything that can happen, consider: + * + * alpha[double]: { // allocated destinationTag = 1 + * val foo: int = try { // declare local destinationTagOuter + * beta[int]: { // allocated destinationTag = 2 + * val bar: int = try { // declare local destinationTagInner + * if (A) return@alpha 5 + * if (B) return@beta 10 + * 56 + * } finally { + * doTheFinally() + * // not shown: there is another try..finally here using a third + * // local destinationTagThird, since destinationTagOuter and + * // destinationTagInner are alive at the same time. + * } + * someOtherThings(bar) + * } + * } finally { + * doTheOuterFinally() + * } + * moreOtherThings(foo) + * } + * + * The whole compiled code is too overwhelming to be useful, so we show the + * important aspects piecemiel, from the bottom up. + * + * First, the compiled code for `return@alpha 5`: + * + * i32.const 5 ; eval the argument of the return + * local.set $alphaResult ; store it in $alphaResult because we are cross a try..finally + * i32.const 1 ; the destination tag of alpha + * local.set $destinationTagInner ; store it in the destinationTag local of the inner try..finally + * br $innerCross ; branch to the cross label of the inner try..finally + * + * Second, we look at the shape generated for the inner try..finally: + * + * block $innerDone (result i32) + * block $innerCatch (result exnref) + * block $innerCross + * try_table (catch_all_ref $innerCatch) + * ; [...] body of the try + * + * local.set $innerTryResult + * end ; try_table + * + * ; set destinationTagInner := 0 to mean fall-through + * i32.const 0 + * local.set $destinationTagInner + * end ; block $innerCross + * + * ; no exception thrown + * ref.null exn + * end ; block $innerCatch + * + * ; now we have the common code with the finally + * + * ; [...] body of the finally + * + * ; maybe re-throw + * block $innerExnIsNull (param exnref) + * br_on_null $innerExnIsNull + * throw_ref + * end + * + * ; re-dispatch after the outer finally based on $destinationTagInner + * + * ; first transfer our destination tag to the outer try's destination tag + * local.get $destinationTagInner + * local.set $destinationTagOuter + * + * ; now use a br_table to jump to the appropriate destination + * ; if 0, fall-through + * ; if 1, go the outer try's cross label because it is still on the way to alpha + * ; if 2, go to beta's cross label + * ; default to fall-through (never used but br_table needs a default) + * br_table $innerDone $outerCross $betaCross $innerDone + * end ; block $innerDone + * + * We omit the shape of beta and of the outer try. There are similar to the + * shape of alpha and inner try, respectively. + * + * We conclude with the shape of the alpha block: + * + * block $alpha (result f64) + * block $alphaCross + * ; begin body of alpha + * + * ; [...] ; the try..finally + * local.set $foo ; val foo = + * moreOtherThings(foo) + * + * ; end body of alpha + * + * br $alpha ; if alpha finished normally, jump over `local.get $alphaResult` + * end ; block $alphaCross + * + * ; if we returned from alpha across a try..finally, fetch the result from the local + * local.get $alphaResult + * end ; block $alpha + */ + + /** This object namespaces everything related to unwinding, so that we don't pollute too much the + * overall internal scope of `FunctionEmitter`. + */ + private object unwinding { + + /** The number of enclosing `Labeled` and `TryFinally` blocks. + * + * For `TryFinally`, it is only enclosing if we are in the `try` branch, not the `finally` + * branch. + * + * Invariant: + * {{{ + * currentUnwindingStackDepth == enclosingTryFinallyStack.size + enclosingLabeledBlocks.size + * }}} + */ + private var currentUnwindingStackDepth: Int = 0 + + private var enclosingTryFinallyStack: List[TryFinallyEntry] = Nil + + private var enclosingLabeledBlocks: Map[LabelName, LabeledEntry] = Map.empty + + private def innermostTryFinally: Option[TryFinallyEntry] = + enclosingTryFinallyStack.headOption + + private def enterTryFinally(entry: TryFinallyEntry)(body: => Unit): Unit = { + assert(entry.depth == currentUnwindingStackDepth) + enclosingTryFinallyStack ::= entry + currentUnwindingStackDepth += 1 + try { + body + } finally { + currentUnwindingStackDepth -= 1 + enclosingTryFinallyStack = enclosingTryFinallyStack.tail + } + } + + private def enterLabeled(entry: LabeledEntry)(body: => Unit): Unit = { + assert(entry.depth == currentUnwindingStackDepth) + val savedLabeledBlocks = enclosingLabeledBlocks + enclosingLabeledBlocks = enclosingLabeledBlocks.updated(entry.irLabelName, entry) + currentUnwindingStackDepth += 1 + try { + body + } finally { + currentUnwindingStackDepth -= 1 + enclosingLabeledBlocks = savedLabeledBlocks + } + } + + /** The last destination tag that was allocated to a LabeledEntry. */ + private var lastDestinationTag: Int = 0 + + private def allocateDestinationTag(): Int = { + lastDestinationTag += 1 + lastDestinationTag + } + + /** Information about an enclosing `TryFinally` block. */ + private final class TryFinallyEntry(val depth: Int) { + import TryFinallyEntry._ + + private var _crossInfo: Option[CrossInfo] = None + + def isInside(labeledEntry: LabeledEntry): Boolean = + this.depth > labeledEntry.depth + + def wasCrossed: Boolean = _crossInfo.isDefined + + def requireCrossInfo(): CrossInfo = { + _crossInfo.getOrElse { + val info = CrossInfo(addSyntheticLocal(watpe.Int32), fb.genLabel()) + _crossInfo = Some(info) + info + } + } + } + + private object TryFinallyEntry { + /** Cross info for a `TryFinally` entry. + * + * @param destinationTagLocal + * The destinationTag local variable for this `TryFinally`. + * @param crossLabel + * The cross label for this `TryFinally`. + */ + sealed case class CrossInfo( + val destinationTagLocal: wanme.LocalID, + val crossLabel: wanme.LabelID + ) + } + + /** Information about an enclosing `Labeled` block. */ + private final class LabeledEntry(val depth: Int, + val irLabelName: LabelName, val expectedType: Type) { + + import LabeledEntry._ + + /** The regular label for this `Labeled` block, used for `Return`s that + * do not cross a `TryFinally`. + */ + val regularWasmLabel: wanme.LabelID = fb.genLabel() + + private var _crossInfo: Option[CrossInfo] = None + + def wasCrossUsed: Boolean = _crossInfo.isDefined + + def requireCrossInfo(): CrossInfo = { + _crossInfo.getOrElse { + val destinationTag = allocateDestinationTag() + val resultTypes = transformResultType(expectedType) + val resultLocals = resultTypes.map(addSyntheticLocal(_)) + val crossLabel = fb.genLabel() + val info = CrossInfo(destinationTag, resultLocals, crossLabel) + _crossInfo = Some(info) + info + } + } + } + + private object LabeledEntry { + /** Cross info for a `LabeledEntry`. + * + * @param destinationTag + * The destination tag allocated to this label, used by the `finally` + * blocks to keep propagating to the right destination. Destination + * tags are always `> 0`. The value `0` is reserved for fall-through. + * @param resultLocals + * The locals in which to store the result of the label if we have to + * cross a `try..finally`. + * @param crossLabel + * An additional Wasm label that has a `[]` result, and which will get + * its result from the `resultLocal` instead of expecting it on the stack. + */ + sealed case class CrossInfo( + destinationTag: Int, + resultLocals: List[wanme.LocalID], + crossLabel: wanme.LabelID + ) + } + + def genLabeled(tree: Labeled, expectedType: Type): Type = { + val Labeled(LabelIdent(labelName), tpe, body) = tree + + val entry = new LabeledEntry(currentUnwindingStackDepth, labelName, expectedType) + + val ty = transformResultType(expectedType) + + markPosition(tree) + + // Manual wa.Block here because we have a specific `label` + fb += wa.Block(fb.sigToBlockType(Sig(Nil, ty)), Some(entry.regularWasmLabel)) + + /* Remember the position in the instruction stream, in case we need to + * come back and insert the wa.Block for the cross handling. + */ + val instrsBlockBeginIndex = fb.markCurrentInstructionIndex() + + // Emit the body + enterLabeled(entry) { + genTree(body, expectedType) + } + + markPosition(tree) + + // Deal with crossing behavior + if (entry.wasCrossUsed) { + assert(expectedType != NothingType, + "The tryFinallyCrossLabel should not have been used for label " + + s"${labelName.nameString} of type nothing") + + /* In this case we need to handle situations where we receive the value + * from the label's `result` local, branching out of the label's + * `crossLabel`. + * + * Instead of the standard shape + * + * block $labeled (result t) + * body + * end + * + * We need to amend the shape to become + * + * block $labeled (result t) + * block $crossLabel + * body ; inside the body, jumps to this label after a + * ; `finally` are compiled as `br $crossLabel` + * br $labeled + * end + * local.get $label.resultLocals ; (0 to many) + * end + */ + + val LabeledEntry.CrossInfo(_, resultLocals, crossLabel) = + entry.requireCrossInfo() + + // Go back and insert the `block $crossLabel` right after `block $labeled` + fb.insert(instrsBlockBeginIndex, wa.Block(wa.BlockType.ValueType(), Some(crossLabel))) + + // Add the `br`, `end` and `local.get` at the current position, as usual + fb += wa.Br(entry.regularWasmLabel) + fb += wa.End + for (local <- resultLocals) + fb += wa.LocalGet(local) + } + + fb += wa.End + + if (expectedType == NothingType) + fb += wa.Unreachable + + expectedType + } + + def genTryFinally(tree: TryFinally, expectedType: Type): Type = { + val TryFinally(tryBlock, finalizer) = tree + + val entry = new TryFinallyEntry(currentUnwindingStackDepth) + + val resultType = transformResultType(expectedType) + val resultLocals = resultType.map(addSyntheticLocal(_)) + + markPosition(tree) + + fb.block() { doneLabel => + fb.block(watpe.RefType.exnref) { catchLabel => + /* Remember the position in the instruction stream, in case we need + * to come back and insert the wa.BLOCK for the cross handling. + */ + val instrsBlockBeginIndex = fb.markCurrentInstructionIndex() + + fb.tryTable()(List(wa.CatchClause.CatchAllRef(catchLabel))) { + // try block + enterTryFinally(entry) { + genTree(tryBlock, expectedType) + } + + markPosition(tree) + + // store the result in locals during the finally block + for (resultLocal <- resultLocals.reverse) + fb += wa.LocalSet(resultLocal) + } + + /* If this try..finally was crossed by a `Return`, we need to amend + * the shape of our try part to + * + * block $catch (result exnref) + * block $cross + * try_table (catch_all_ref $catch) + * body + * set_local $results ; 0 to many + * end + * i32.const 0 ; 0 always means fall-through + * local.set $destinationTag + * end + * ref.null exn + * end + */ + if (entry.wasCrossed) { + val TryFinallyEntry.CrossInfo(destinationTagLocal, crossLabel) = + entry.requireCrossInfo() + + // Go back and insert the `block $cross` right after `block $catch` + fb.insert( + instrsBlockBeginIndex, + wa.Block(wa.BlockType.ValueType(), Some(crossLabel)) + ) + + // And the other amendments normally + fb += wa.I32Const(0) + fb += wa.LocalSet(destinationTagLocal) + fb += wa.End // of the inserted wa.BLOCK + } + + // on success, push a `null_ref exn` on the stack + fb += wa.RefNull(watpe.HeapType.Exn) + } // end block $catch + + // finally block (during which we leave the `(ref null exn)` on the stack) + genTree(finalizer, NoType) + + markPosition(tree) + + if (!entry.wasCrossed) { + // If the `exnref` is non-null, rethrow it + fb += wa.BrOnNull(doneLabel) + fb += wa.ThrowRef + } else { + /* If the `exnref` is non-null, rethrow it. + * Otherwise, stay within the `$done` block. + */ + fb.block(Sig(List(watpe.RefType.exnref), Nil)) { exnrefIsNullLabel => + fb += wa.BrOnNull(exnrefIsNullLabel) + fb += wa.ThrowRef + } + + /* Otherwise, use a br_table to dispatch to the right destination + * based on the value of the try..finally's destinationTagLocal, + * which is set by `Return` or to 0 for fall-through. + */ + + // The order does not matter here because they will be "re-sorted" by emitBRTable + val possibleTargetEntries = + enclosingLabeledBlocks.valuesIterator.filter(_.wasCrossUsed).toList + + val nextTryFinallyEntry = innermostTryFinally // note that we're out of ourselves already + .filter(nextTry => possibleTargetEntries.exists(nextTry.isInside(_))) + + /* Build the destination table for `br_table`. Target Labeled's that + * are outside of the next try..finally in line go to the latter; + * for other `Labeled`'s, we go to their cross label. + */ + val brTableDests: List[(Int, wanme.LabelID)] = possibleTargetEntries.map { targetEntry => + val LabeledEntry.CrossInfo(destinationTag, _, crossLabel) = + targetEntry.requireCrossInfo() + val label = nextTryFinallyEntry.filter(_.isInside(targetEntry)) match { + case None => crossLabel + case Some(nextTry) => nextTry.requireCrossInfo().crossLabel + } + destinationTag -> label + } + + fb += wa.LocalGet(entry.requireCrossInfo().destinationTagLocal) + for (nextTry <- nextTryFinallyEntry) { + // Transfer the destinationTag to the next try..finally in line + fb += wa.LocalTee(nextTry.requireCrossInfo().destinationTagLocal) + } + emitBRTable(brTableDests, doneLabel) + } + } // end block $done + + // reload the result onto the stack + for (resultLocal <- resultLocals) + fb += wa.LocalGet(resultLocal) + + if (expectedType == NothingType) + fb += wa.Unreachable + + expectedType + } + + private def emitBRTable(dests: List[(Int, wanme.LabelID)], + defaultLabel: wanme.LabelID): Unit = { + dests match { + case Nil => + fb += wa.Drop + fb += wa.Br(defaultLabel) + + case (singleDestValue, singleDestLabel) :: Nil => + /* Common case (as far as getting here in the first place is concerned): + * All the `Return`s that cross the current `TryFinally` have the same + * target destination (namely the enclosing `def` in the original program). + */ + fb += wa.I32Const(singleDestValue) + fb += wa.I32Eq + fb += wa.BrIf(singleDestLabel) + fb += wa.Br(defaultLabel) + + case _ :: _ => + // `max` is safe here because the list is non-empty + val table = Array.fill(dests.map(_._1).max + 1)(defaultLabel) + for (dest <- dests) + table(dest._1) = dest._2 + fb += wa.BrTable(table.toList, defaultLabel) + } + } + + def genReturn(tree: Return): Type = { + val Return(expr, LabelIdent(labelName)) = tree + + val targetEntry = enclosingLabeledBlocks(labelName) + + genTree(expr, targetEntry.expectedType) + + markPosition(tree) + + if (targetEntry.expectedType != NothingType) { + innermostTryFinally.filter(_.isInside(targetEntry)) match { + case None => + // Easy case: directly branch out of the block + fb += wa.Br(targetEntry.regularWasmLabel) + + case Some(tryFinallyEntry) => + /* Here we need to branch to the innermost enclosing `finally` block, + * while remembering the destination label and the result value. + */ + val LabeledEntry.CrossInfo(destinationTag, resultLocals, _) = + targetEntry.requireCrossInfo() + val TryFinallyEntry.CrossInfo(destinationTagLocal, crossLabel) = + tryFinallyEntry.requireCrossInfo() + + // 1. Store the result in the label's result locals. + for (local <- resultLocals.reverse) + fb += wa.LocalSet(local) + + // 2. Store the label's destination tag into the try..finally's destination local. + fb += wa.I32Const(destinationTag) + fb += wa.LocalSet(destinationTagLocal) + + // 3. Branch to the enclosing `finally` block's cross label. + fb += wa.Br(crossLabel) + } + } + + NothingType + } + } +} 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 new file mode 100644 index 0000000000..48bfae78d9 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/LoaderContent.scala @@ -0,0 +1,328 @@ +/* + * 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.backend.wasmemitter + +import java.nio.charset.StandardCharsets + +import org.scalajs.ir.ScalaJSVersions + +import EmbeddedConstants._ + +/** Contents of the `__loader.js` file that we emit in every output. */ +object LoaderContent { + val bytesContent: Array[Byte] = + stringContent.getBytes(StandardCharsets.UTF_8) + + private def stringContent: String = { + raw""" +// This implementation follows no particular specification, but is the same as the JS backend. +// It happens to coincide with java.lang.Long.hashCode() for common values. +function bigintHashCode(x) { + var res = 0; + if (x < 0n) + x = ~x; + while (x !== 0n) { + res ^= Number(BigInt.asIntN(32, x)); + x >>= 32n; + } + return res; +} + +// JSSuperSelect support -- directly copied from the output of the JS backend +function resolveSuperRef(superClass, propName) { + var getPrototypeOf = Object.getPrototyeOf; + var getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; + var superProto = superClass.prototype; + while (superProto !== null) { + var desc = getOwnPropertyDescriptor(superProto, propName); + if (desc !== (void 0)) { + return desc; + } + superProto = getPrototypeOf(superProto); + } +} +function superSelect(superClass, self, propName) { + var desc = resolveSuperRef(superClass, propName); + if (desc !== (void 0)) { + var getter = desc.get; + return getter !== (void 0) ? getter.call(self) : getter.value; + } +} +function superSelectSet(superClass, self, propName, value) { + var desc = resolveSuperRef(superClass, propName); + if (desc !== (void 0)) { + var setter = desc.set; + if (setter !== (void 0)) { + setter.call(self, value); + return; + } + } + throw new TypeError("super has no setter '" + propName + "'."); +} + +function installJSField(instance, name, value) { + Object.defineProperty(instance, name, { + value, + configurable: true, + enumerable: true, + writable: true, + }); +} + +// FIXME We need to adapt this to the correct values +const linkingInfo = Object.freeze({ + "esVersion": 6, + "assumingES6": true, + "productionMode": false, + "linkerVersion": "${ScalaJSVersions.current}", + "fileLevelThis": this +}); + +const scalaJSHelpers = { + // JSTag + JSTag: WebAssembly.JSTag, + + // BinaryOp.=== + is: Object.is, + + // undefined + undef: void 0, + isUndef: (x) => x === (void 0), + + // Zero boxes + bFalse: false, + bZero: 0, + + // Boxes (upcast) -- most are identity at the JS level but with different types in Wasm + bZ: (x) => x !== 0, + bB: (x) => x, + bS: (x) => x, + bI: (x) => x, + bF: (x) => x, + bD: (x) => x, + + // Unboxes (downcast, null is converted to the zero of the type as part of ToWebAssemblyValue) + uZ: (x) => x, // ToInt32 turns false into 0 and true into 1, so this is also an identity + uB: (x) => x, + uS: (x) => x, + uI: (x) => x, + uF: (x) => x, + uD: (x) => x, + + // Type tests + tZ: (x) => typeof x === 'boolean', + tB: (x) => typeof x === 'number' && Object.is((x << 24) >> 24, x), + tS: (x) => typeof x === 'number' && Object.is((x << 16) >> 16, x), + tI: (x) => typeof x === 'number' && Object.is(x | 0, x), + tF: (x) => typeof x === 'number' && (Math.fround(x) === x || x !== x), + tD: (x) => typeof x === 'number', + + // fmod, to implement Float_% and Double_% (it is apparently quite hard to implement fmod otherwise) + fmod: (x, y) => x % y, + + // Closure + closure: (f, data) => f.bind(void 0, data), + closureThis: (f, data) => function(...args) { return f(data, this, ...args); }, + closureRest: (f, data, n) => ((...args) => f(data, ...args.slice(0, n), args.slice(n))), + closureThisRest: (f, data, n) => function(...args) { return f(data, this, ...args.slice(0, n), args.slice(n)); }, + + // Top-level exported defs -- they must be `function`s but have no actual `this` nor `data` + makeExportedDef: (f) => function(...args) { return f(...args); }, + makeExportedDefRest: (f, n) => function(...args) { return f(...args.slice(0, n), args.slice(n)); }, + + // Strings + emptyString: "", + stringLength: (s) => s.length, + stringCharAt: (s, i) => s.charCodeAt(i), + jsValueToString: (x) => (x === void 0) ? "undefined" : x.toString(), + jsValueToStringForConcat: (x) => "" + x, + booleanToString: (b) => b ? "true" : "false", + charToString: (c) => String.fromCharCode(c), + intToString: (i) => "" + i, + longToString: (l) => "" + l, // l must be a bigint here + doubleToString: (d) => "" + d, + stringConcat: (x, y) => ("" + x) + y, // the added "" is for the case where x === y === null + isString: (x) => typeof x === 'string', + + // Get the type of JS value of `x` in a single JS helper call, for the purpose of dispatch. + jsValueType: (x) => { + if (typeof x === 'number') + return $JSValueTypeNumber; + if (typeof x === 'string') + return $JSValueTypeString; + if (typeof x === 'boolean') + return x | 0; // JSValueTypeFalse or JSValueTypeTrue + if (typeof x === 'undefined') + return $JSValueTypeUndefined; + if (typeof x === 'bigint') + return $JSValueTypeBigInt; + if (typeof x === 'symbol') + return $JSValueTypeSymbol; + return $JSValueTypeOther; + }, + + // Identity hash code + bigintHashCode, + symbolDescription: (x) => { + var desc = x.description; + return (desc === void 0) ? null : desc; + }, + idHashCodeGet: (map, obj) => map.get(obj) | 0, // undefined becomes 0 + idHashCodeSet: (map, obj, value) => map.set(obj, value), + + // JS interop + jsGlobalRefGet: (globalRefName) => (new Function("return " + globalRefName))(), + jsGlobalRefSet: (globalRefName, v) => { + var argName = globalRefName === 'v' ? 'w' : 'v'; + (new Function(argName, globalRefName + " = " + argName))(v); + }, + jsGlobalRefTypeof: (globalRefName) => (new Function("return typeof " + globalRefName))(), + jsNewArray: () => [], + jsArrayPush: (a, v) => (a.push(v), a), + jsArraySpreadPush: (a, vs) => (a.push(...vs), a), + jsNewObject: () => ({}), + jsObjectPush: (o, p, v) => (o[p] = v, o), + jsSelect: (o, p) => o[p], + jsSelectSet: (o, p, v) => o[p] = v, + jsNew: (constr, args) => new constr(...args), + jsFunctionApply: (f, args) => f(...args), + jsMethodApply: (o, m, args) => o[m](...args), + jsImportCall: (s) => import(s), + jsImportMeta: () => import.meta, + jsDelete: (o, p) => { delete o[p]; }, + jsForInSimple: (o, f) => { for (var k in o) f(k); }, + jsIsTruthy: (x) => !!x, + jsLinkingInfo: linkingInfo, + + // Excruciating list of all the JS operators + jsUnaryPlus: (a) => +a, + jsUnaryMinus: (a) => -a, + jsUnaryTilde: (a) => ~a, + jsUnaryBang: (a) => !a, + jsUnaryTypeof: (a) => typeof a, + jsStrictEquals: (a, b) => a === b, + jsNotStrictEquals: (a, b) => a !== b, + jsPlus: (a, b) => a + b, + jsMinus: (a, b) => a - b, + jsTimes: (a, b) => a * b, + jsDivide: (a, b) => a / b, + jsModulus: (a, b) => a % b, + jsBinaryOr: (a, b) => a | b, + jsBinaryAnd: (a, b) => a & b, + jsBinaryXor: (a, b) => a ^ b, + jsShiftLeft: (a, b) => a << b, + jsArithmeticShiftRight: (a, b) => a >> b, + jsLogicalShiftRight: (a, b) => a >>> b, + jsLessThan: (a, b) => a < b, + jsLessEqual: (a, b) => a <= b, + jsGreaterThan: (a, b) => a > b, + jsGreaterEqual: (a, b) => a >= b, + jsIn: (a, b) => a in b, + jsInstanceof: (a, b) => a instanceof b, + jsExponent: (a, b) => a ** b, + + // Non-native JS class support + newSymbol: Symbol, + createJSClass: (data, superClass, preSuperStats, superArgs, postSuperStats, fields) => { + // fields is an array where even indices are field names and odd indices are initial values + return class extends superClass { + constructor(...args) { + var preSuperEnv = preSuperStats(data, new.target, ...args); + super(...superArgs(data, preSuperEnv, new.target, ...args)); + for (var i = 0; i != fields.length; i = (i + 2) | 0) + installJSField(this, fields[i], fields[(i + 1) | 0]); + postSuperStats(data, preSuperEnv, new.target, this, ...args); + } + }; + }, + createJSClassRest: (data, superClass, preSuperStats, superArgs, postSuperStats, fields, n) => { + // fields is an array where even indices are field names and odd indices are initial values + return class extends superClass { + constructor(...args) { + var fixedArgs = args.slice(0, n); + var restArg = args.slice(n); + var preSuperEnv = preSuperStats(data, new.target, ...fixedArgs, restArg); + super(...superArgs(data, preSuperEnv, new.target, ...fixedArgs, restArg)); + for (var i = 0; i != fields.length; i = (i + 2) | 0) + installJSField(this, fields[i], fields[(i + 1) | 0]); + postSuperStats(data, preSuperEnv, new.target, this, ...fixedArgs, restArg); + } + }; + }, + installJSField, + installJSMethod: (data, jsClass, name, func, fixedArgCount) => { + var closure = fixedArgCount < 0 + ? (function(...args) { return func(data, this, ...args); }) + : (function(...args) { return func(data, this, ...args.slice(0, fixedArgCount), args.slice(fixedArgCount))}); + jsClass.prototype[name] = closure; + }, + installJSStaticMethod: (data, jsClass, name, func, fixedArgCount) => { + var closure = fixedArgCount < 0 + ? (function(...args) { return func(data, ...args); }) + : (function(...args) { return func(data, ...args.slice(0, fixedArgCount), args.slice(fixedArgCount))}); + jsClass[name] = closure; + }, + installJSProperty: (data, jsClass, name, getter, setter) => { + var getterClosure = getter + ? (function() { return getter(data, this) }) + : (void 0); + var setterClosure = setter + ? (function(arg) { setter(data, this, arg) }) + : (void 0); + Object.defineProperty(jsClass.prototype, name, { + get: getterClosure, + set: setterClosure, + configurable: true, + }); + }, + installJSStaticProperty: (data, jsClass, name, getter, setter) => { + var getterClosure = getter + ? (function() { return getter(data) }) + : (void 0); + var setterClosure = setter + ? (function(arg) { setter(data, arg) }) + : (void 0); + Object.defineProperty(jsClass, name, { + get: getterClosure, + set: setterClosure, + configurable: true, + }); + }, + jsSuperSelect: superSelect, + jsSuperSelectSet: superSelectSet, + jsSuperCall: (superClass, receiver, method, args) => { + return superClass.prototype[method].apply(receiver, args); + }, +} + +export async function load(wasmFileURL, importedModules, exportSetters) { + const myScalaJSHelpers = { ...scalaJSHelpers, idHashCodeMap: new WeakMap() }; + const importsObj = { + "__scalaJSHelpers": myScalaJSHelpers, + "__scalaJSImports": importedModules, + "__scalaJSExportSetters": exportSetters, + }; + const resolvedURL = new URL(wasmFileURL, import.meta.url); + if (resolvedURL.protocol === 'file:') { + const { fileURLToPath } = await import("node:url"); + const { readFile } = await import("node:fs/promises"); + const wasmPath = fileURLToPath(resolvedURL); + const body = await readFile(wasmPath); + return WebAssembly.instantiate(body, importsObj); + } else { + return await WebAssembly.instantiateStreaming(fetch(resolvedURL), importsObj); + } +} + """ + } +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Preprocessor.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Preprocessor.scala new file mode 100644 index 0000000000..5c2d76f190 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Preprocessor.scala @@ -0,0 +1,473 @@ +/* + * 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.backend.wasmemitter + +import scala.collection.mutable + +import org.scalajs.ir.Names._ +import org.scalajs.ir.Trees._ +import org.scalajs.ir.Types._ +import org.scalajs.ir.{ClassKind, Traversers} + +import org.scalajs.linker.standard.{LinkedClass, LinkedTopLevelExport} + +import EmbeddedConstants._ +import WasmContext._ + +object Preprocessor { + def preprocess(classes: List[LinkedClass], tles: List[LinkedTopLevelExport]): WasmContext = { + val staticFieldMirrors = computeStaticFieldMirrors(tles) + + val specialInstanceTypes = computeSpecialInstanceTypes(classes) + + val abstractMethodCalls = + AbstractMethodCallCollector.collectAbstractMethodCalls(classes, tles) + + val (itableBucketCount, itableBucketAssignments) = + computeItableBuckets(classes) + + val classInfosBuilder = mutable.HashMap.empty[ClassName, ClassInfo] + val definedReflectiveProxyNames = mutable.HashSet.empty[MethodName] + + for (clazz <- classes) { + val classInfo = preprocess( + clazz, + staticFieldMirrors.getOrElse(clazz.className, Map.empty), + specialInstanceTypes.getOrElse(clazz.className, 0), + itableBucketAssignments.getOrElse(clazz.className, -1), + clazz.superClass.map(sup => classInfosBuilder(sup.name)) + ) + classInfosBuilder += clazz.className -> classInfo + + // For Scala classes, collect the reflective proxy method names that it defines + if (clazz.kind.isClass || clazz.kind == ClassKind.HijackedClass) { + for (method <- clazz.methods if method.methodName.isReflectiveProxy) + definedReflectiveProxyNames += method.methodName + } + } + + val classInfos = classInfosBuilder.toMap + + // sort for stability + val reflectiveProxyIDs = definedReflectiveProxyNames.toList.sorted.zipWithIndex.toMap + + for (clazz <- classes) { + classInfos(clazz.className).buildMethodTable( + abstractMethodCalls.getOrElse(clazz.className, Set.empty)) + } + + new WasmContext(classInfos, reflectiveProxyIDs, itableBucketCount) + } + + private def computeStaticFieldMirrors( + tles: List[LinkedTopLevelExport]): Map[ClassName, Map[FieldName, List[String]]] = { + + var result = Map.empty[ClassName, Map[FieldName, List[String]]] + for (tle <- tles) { + tle.tree match { + case TopLevelFieldExportDef(_, exportName, FieldIdent(fieldName)) => + val className = tle.owningClass + val mirrors = result.getOrElse(className, Map.empty) + val newExportNames = exportName :: mirrors.getOrElse(fieldName, Nil) + val newMirrors = mirrors.updated(fieldName, newExportNames) + result = result.updated(className, newMirrors) + + case _ => + } + } + result + } + + private def computeSpecialInstanceTypes( + classes: List[LinkedClass]): Map[ClassName, Int] = { + + val result = mutable.AnyRefMap.empty[ClassName, Int] + + for { + clazz <- classes + if clazz.kind == ClassKind.HijackedClass + } { + val specialInstanceTypes = clazz.className match { + case BoxedBooleanClass => (1 << JSValueTypeFalse) | (1 << JSValueTypeTrue) + case BoxedStringClass => 1 << JSValueTypeString + case BoxedDoubleClass => 1 << JSValueTypeNumber + case BoxedUnitClass => 1 << JSValueTypeUndefined + case _ => 0 + } + + if (specialInstanceTypes != 0) { + for (ancestor <- clazz.ancestors.tail) + result(ancestor) = result.getOrElse(ancestor, 0) | specialInstanceTypes + } + } + + result.toMap + } + + private def preprocess( + clazz: LinkedClass, + staticFieldMirrors: Map[FieldName, List[String]], + specialInstanceTypes: Int, + itableIdx: Int, + superClass: Option[ClassInfo] + ): ClassInfo = { + val className = clazz.className + val kind = clazz.kind + + val allFieldDefs: List[FieldDef] = { + if (kind.isClass) { + val inheritedFields = + superClass.fold[List[FieldDef]](Nil)(_.allFieldDefs) + val myFieldDefs = clazz.fields.collect { + case fd: FieldDef if !fd.flags.namespace.isStatic => + fd + case fd: JSFieldDef => + throw new AssertionError(s"Illegal $fd in Scala class $className") + } + inheritedFields ::: myFieldDefs + } else { + Nil + } + } + + // Does this Scala class implement any interface? + val classImplementsAnyInterface = { + (kind.isClass || kind == ClassKind.HijackedClass) && + (clazz.interfaces.nonEmpty || superClass.exists(_.classImplementsAnyInterface)) + } + + /* Should we emit a vtable/typeData global for this class? + * + * There are essentially three reasons for which we need them: + * + * - Because there is a `classOf[C]` somewhere in the program; if that is + * true, then `clazz.hasRuntimeTypeInfo` is true. + * - Because it is the vtable of a class with direct instances; in that + * case `clazz.hasRuntimeTypeInfo` is also true, as guaranteed by the + * Scala.js frontend analysis. + * - Because we generate a test of the form `isInstanceOf[Array[C]]`. In + * that case, `clazz.hasInstanceTests` is true. + * + * `clazz.hasInstanceTests` is also true if there is only `isInstanceOf[C]`, + * in the program, so that is not *optimal*, but it is correct. + */ + val hasRuntimeTypeInfo = clazz.hasRuntimeTypeInfo || clazz.hasInstanceTests + + val resolvedMethodInfos: Map[MethodName, ConcreteMethodInfo] = { + if (kind.isClass || kind == ClassKind.HijackedClass) { + val inherited = + superClass.fold[Map[MethodName, ConcreteMethodInfo]](Map.empty)(_.resolvedMethodInfos) + + val concretePublicMethodNames = for { + m <- clazz.methods + if m.body.isDefined && m.flags.namespace == MemberNamespace.Public + } yield { + m.methodName + } + + for (methodName <- concretePublicMethodNames) + inherited.get(methodName).foreach(_.markOverridden()) + + concretePublicMethodNames.foldLeft(inherited) { (prev, methodName) => + prev.updated(methodName, new ConcreteMethodInfo(className, methodName)) + } + } else { + Map.empty + } + } + + new ClassInfo( + className, + kind, + clazz.jsClassCaptures, + allFieldDefs, + superClass, + classImplementsAnyInterface, + clazz.hasInstances, + !clazz.hasDirectInstances, + hasRuntimeTypeInfo, + clazz.jsNativeLoadSpec, + clazz.jsNativeMembers.map(m => m.name.name -> m.jsNativeLoadSpec).toMap, + staticFieldMirrors, + specialInstanceTypes, + resolvedMethodInfos, + itableIdx + ) + } + + /** Collects virtual and interface method calls. + * + * That information will be used to decide what entries are necessary in + * vtables and itables. + * + * TODO Arguably this is a job for the `Analyzer`. + */ + private object AbstractMethodCallCollector { + def collectAbstractMethodCalls(classes: List[LinkedClass], + tles: List[LinkedTopLevelExport]): Map[ClassName, Set[MethodName]] = { + + val collector = new AbstractMethodCallCollector + for (clazz <- classes) + collector.collectAbstractMethodCalls(clazz) + for (tle <- tles) + collector.collectAbstractMethodCalls(tle) + collector.result() + } + } + + private class AbstractMethodCallCollector private () extends Traversers.Traverser { + private val builder = new mutable.AnyRefMap[ClassName, mutable.HashSet[MethodName]] + + private def registerCall(className: ClassName, methodName: MethodName): Unit = + builder.getOrElseUpdate(className, new mutable.HashSet) += methodName + + def collectAbstractMethodCalls(clazz: LinkedClass): Unit = { + for (method <- clazz.methods) + traverseMethodDef(method) + for (jsConstructor <- clazz.jsConstructorDef) + traverseJSConstructorDef(jsConstructor) + for (export <- clazz.exportedMembers) + traverseJSMethodPropDef(export) + } + + def collectAbstractMethodCalls(tle: LinkedTopLevelExport): Unit = { + tle.tree match { + case TopLevelMethodExportDef(_, jsMethodDef) => + traverseJSMethodPropDef(jsMethodDef) + case _ => + () + } + } + + def result(): Map[ClassName, Set[MethodName]] = + builder.toMap.map(kv => kv._1 -> kv._2.toSet) + + override def traverse(tree: Tree): Unit = { + super.traverse(tree) + + tree match { + case Apply(flags, receiver, MethodIdent(methodName), _) if !methodName.isReflectiveProxy => + receiver.tpe match { + case ClassType(className) => + registerCall(className, methodName) + case AnyType => + registerCall(ObjectClass, methodName) + case _ => + // For all other cases, including arrays, we will always perform a static dispatch + () + } + + case _ => + () + } + } + } + + /** Group interface types and types that implement any interfaces into buckets, + * ensuring that no two types in the same bucket have common subtypes. + * + * For example, given the following type hierarchy (with upper types as + * supertypes), types will be assigned to the following buckets: + * + * {{{ + * A __ + * / |\ \ + * / | \ \ + * B C E G + * | /| / + * |/ |/ + * D F + * }}} + * + * - bucket0: [A] + * - bucket1: [B, C, G] + * - bucket2: [D, F] + * - bucket3: [E] + * + * In the original paper, within each bucket, types are given unique indices + * that are local to each bucket. A gets index 0. B, C, and G are assigned + * 0, 1, and 2 respectively. Similarly, D=0, F=1, and E=0. + * + * This method (called packed encoding) compresses the interface tables + * compared to a global 1-1 mapping from interface to index. With the 1-1 + * mapping strategy, the length of the itables would be 7 (for interfaces + * A-G). In contrast, using a packed encoding strategy, the length of the + * interface tables is reduced to the number of buckets, which is 4 in this + * case. + * + * Each element in the interface tables array corresponds to the interface + * table of the type in the respective bucket that the object implements. + * For example, an object that implements G (and A) would have an interface + * table structured as: [(itable of A), (itable of G), null, null], because + * A is in bucket 0 and G is in bucket 1. + * + * {{{ + * Object implements G + * | + * +----------+---------+ + * | ...class metadata | + * +--------------------+ 1-1 mapping strategy version + * | vtable | +----> [(itable of A), null, null, null, null, null, (itable of G)] + * +--------------------+ / + * | itables +/ + * +--------------------+\ packed encoding version + * | ... + +-----> [(itable of A), (itable of G), null, null] + * +--------------------+ + * }}} + * + * To perform an interface dispatch, we can use bucket IDs and indices to + * locate the appropriate interface table. For instance, suppose we need to + * dispatch for interface G. Knowing that G belongs to bucket 1, we retrieve + * the itable for G from i-th element of the itables. + * + * @note + * Why the types in the example are assigned to the buckets like that? + * - bucket0: [A] + * - A is placed alone in the first bucket. + * - It cannot be grouped with any of its subtypes as that would violate + * the "no common subtypes" rule. + * - bucket1: [B, C, G] + * - B, C, and G cannot be in the same bucket with A since they are all + * direct subtypes of A. + * - They are grouped together because they do not share any common subtype. + * - bucket2: [D, F] + * - D cannot be assigned to neither bucket 0 or 1 because it shares the + * same subtype (D itself) with A (in bucket 0) and C (in bucket 1). + * - D and F are grouped together because they do not share any common subtype. + * - bucket3: [E] + * - E shares its subtype with all the other buckets, so it gets assigned + * to a new bucket. + * + * @return + * The total number of buckets and a map from interface name to + * (the index of) the bucket it was assigned to. + * + * @see + * The algorithm is based on the "packed encoding" presented in the paper + * "Efficient Type Inclusion Tests" + * [[https://www.researchgate.net/publication/2438441_Efficient_Type_Inclusion_Tests]] + */ + private def computeItableBuckets( + allClasses: List[LinkedClass]): (Int, Map[ClassName, Int]) = { + + /* Since we only have to assign itable indices to interfaces with + * instances, we can filter out all parts of the hierarchy that are not + * Scala types with instances. + */ + val classes = allClasses.filter(c => !c.kind.isJSType && c.hasInstances) + + /* The algorithm separates the type hierarchy into three disjoint subsets: + * + * - join types: types with multiple parents (direct supertypes) that have + * only single subtyping descendants: + * `join(T) = {x ∈ multis(T) | ∄ y ∈ multis(T) : y <: x}` + * where multis(T) means types with multiple direct supertypes. + * - spine types: all ancestors of join types: + * `spine(T) = {x ∈ T | ∃ y ∈ join(T) : x ∈ ancestors(y)}` + * - plain types: types that are neither join nor spine types + * + * Now observe that: + * + * - we only work with types that have instances, + * - the only way an *interface* `I` can have instances is if there is a + * *class* with instances that implements it, + * - there must exist such a class `C` that is a join type: one that + * extends another *class* and also at least one interface that has `I` + * in its ancestors (note that `jl.Object` implements no interface), + * - therefore, `I` must be a spine type! + * + * The bucket assignment process consists of two parts: + * + * **1. Assign buckets to spine types** + * + * Two spine types can share the same bucket only if they do not have any + * common join type descendants. + * + * Visit spine types in reverse topological order (from leaves to root) + * because when assigning a spine type to a bucket, the algorithm already + * has complete information about the join/spine type descendants of that + * spine type. + * + * Assign a bucket to a spine type if adding it does not violate the bucket + * assignment rule, namely: two spine types can share a bucket only if they + * do not have any common join type descendants. If no existing bucket + * satisfies the rule, create a new bucket. + * + * **2. Assign buckets to non-spine types (plain and join types)** + * + * Since we only need to assign itable indices to interfaces, and we + * observed that interfaces are all spine types, we can entirely skip this + * phase of the paper's algorithm. + */ + + val buckets = new mutable.ListBuffer[Bucket]() + val resultBuilder = Map.newBuilder[ClassName, Int] + + def findOrCreateBucketSuchThat(p: Bucket => Boolean): Bucket = { + buckets.find(p).getOrElse { + val newBucket = new Bucket(index = buckets.size) + buckets += newBucket + newBucket + } + } + + /* All join type descendants of the class. + * Invariant: sets are non-empty when present. + */ + val joinsOf = new mutable.HashMap[ClassName, mutable.HashSet[ClassName]]() + + // Phase 1: Assign buckets to spine types + for (clazz <- classes.reverseIterator) { + val className = clazz.className + val parents = (clazz.superClass.toList ::: clazz.interfaces.toList).map(_.name) + + joinsOf.get(className) match { + case Some(joins) => + // This type is a spine type + assert(joins.nonEmpty, s"Found empty joins set for $className") + + /* If the spine type is an interface, look for an existing bucket to + * add it to. Two spine types can share a bucket only if they don't + * have any common join type descendants. + */ + if (clazz.kind == ClassKind.Interface) { + val bucket = findOrCreateBucketSuchThat(!_.joins.exists(joins)) + resultBuilder += className -> bucket.index + bucket.joins ++= joins + } + + for (parent <- parents) + joinsOf.getOrElseUpdate(parent, new mutable.HashSet()) ++= joins + + case None if parents.length > 1 => + // This type is a join type: add to joins map + for (parent <- parents) + joinsOf.getOrElseUpdate(parent, new mutable.HashSet()) += className + + case None => + // This type is a plain type. Do nothing. + } + } + + // No Phase 2 :-) + + // Build the result + (buckets.size, resultBuilder.result()) + } + + private final class Bucket(val index: Int) { + /** A set of join types that are descendants of the types assigned to that bucket */ + val joins = new mutable.HashSet[ClassName]() + } + +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/README.md b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/README.md new file mode 100644 index 0000000000..f03a0452bf --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/README.md @@ -0,0 +1,790 @@ +# WebAssembly Emitter + +This directory contains the WebAssembly Emitter, which takes linked IR and produces WebAssembly files. + +The entry point is the class `Emitter`. +Overall, this organization of this backend is similar to that of the JavaScript backend. + +This readme gives an overview of the compilation scheme. + +## WebAssembly features that we use + +* The [GC extension](https://github.com/WebAssembly/gc) +* The [exception handling proposal](https://github.com/WebAssembly/exception-handling) + +All our heap values are allocated as GC data structures. +We do not use the linear memory of WebAssembly at all. + +## Type representation + +Since WebAssembly is strongly statically typed, we have to convert IR types into Wasm types. +The full compilation pipeline must be type-preserving: a well-typed IR program compiles into a well-typed Wasm module. + +In most cases, we also preserve subtyping: if `S <: T` at the IR level, then `transform(S) <: transform(T)` at the Wasm level. +This is however not true when `S` is a primitive type, or when `T = void`. + +* When `T = void` and `S ≠ void`, we have to `drop` the value of type `S` from the stack. +* When `S` is a primitive and `T` is a reference type (which must be an ancestor of a hijacked class), we have to "box" the primitive. + We will come back to this in the [hijacked classes](#hijacked-classes) section. + +### Primitive types + +| IR type | Wasm type | Value representation (if non-obvious) | +|-----------------|-------------|-----------------------------------------| +| `void` | no type | no value on the stack | +| `boolean` | `i32` | `0` for `false`, 1 for `true` | +| `byte`, `short` | `i32` | the `.toInt` value, i.e., sign extended | +| `char` | `i32` | the `.toInt` value, i.e., 0-extended | +| `int` | `i32` | | +| `long` | `i64` | | +| `float` | `f32` | | +| `double` | `f64` | | +| `undef` | `(ref any)` | the global JavaScript value `undefined` | +| `string` | `(ref any)` | a JavaScript `string` | + +### Reference types + +We will describe more precisely the representation of reference types in the coming sections. +This table is for reference. + +| IR type | Wasm type | +|-----------------------------------|----------------------------------| +| `C`, a Scala class | `(ref null $c.C)` | | +| `I`, a Scala interface | `(ref null $c.jl.Object)` | +| all ancestors of hijacked classes | `(ref null any)` (aka `anyref`) | +| `PT[]`, a primitive array | `(ref null $PTArray)` | +| `RT[]`, any reference array type | `(ref null $ObjectArray)` | + +### Nothing + +Wasm does not have a bottom type that we can express at the "user level". +That means we cannot transform `nothing` into any single Wasm type. +However, Wasm has a well-defined notion of [*stack polymorphism*](https://webassembly.github.io/gc/core/valid/instructions.html#polymorphism). +As far as we are concerned, we can think of a stack polymorphic context as officially dead code. +After a *stack-polymorphic instruction*, such as `br` (an unconditional branch), we have dead code which can automatically adapt its type (to be precise: the type of the elements on the stack) to whatever is required to typecheck the following instructions. + +A stack-polymorphic context is as close as Wasm gets to our notion of `nothing`. +Our "type representation" for `nothing` is therefore to make sure that we are in a stack-polymorphic context. + +## Object model + +### Basic structure + +We use GC `struct`s to represent instances of classes. +The structs start with a `vtable` reference and an `itables` reference, which are followed by user-defined fields. +The declared supertypes of those `struct`s follow the *class* hierarchy (ignoring interfaces). + +The `vtable` and `itables` references are immutable. +User-defined fields are always mutable as the WebAssembly level, since they are mutated in the constructors. + +For example, given the following IR classes: + +```scala +class A extends jl.Object { + val x: int +} + +class B extends A { + var y: double +} +``` + +We define the following GC structs: + +```wat +(type $c.A (sub $c.java.lang.Object (struct + (field $vtable (ref $v.A)) + (field $itables (ref null $itables)) + (field $f.A.x (mut i32))) +)) + +(type $c.B (sub $c.A (struct + (field $vtable (ref $v.B)) + (field $itables (ref null $itables)) + (field $f.A.x (mut i32)) + (field $f.B.y (mut f64))) +)) +``` + +As required in Wasm structs, all fields are always repeated in substructs. +Declaring a parent struct type does not imply inheritance of the fields. + +### Methods and statically resolved calls + +Methods are compiled into Wasm functions in a straightforward way, given the type transformations presented above. +When present, the receiver comes as a first argument. + +Statically resolved calls are also compiled straightforwardly as: + +1. Push the receiver, if any, on the stack +2. Push the arguments on the stack +3. `call` the target function + +Constructors are considered instance methods with a `this` receiver for this purpose, and are always statically resolved. + +For example, given the IR + +```scala +class A extends java.lang.Object { + val A::x: int + def x;I(): int = { + this.A::x + } + def plus;I;I(y: int): int = { + (this.x;I() +[int] y) + } + constructor def ;I;V(x: int) { + this.A::x = x; + this.java.lang.Object::;V() + } +} +``` + +We get the following implementing functions, assuming all method calls are statically resolved. + +```wat +;; getter for x +(func $f.A.x_I + (param $this (ref $c.A)) (result i32) + ;; field selection: push the object than `struct.get` + local.get $this + struct.get $c.A $f.A.x + ;; there is always an implicit `return` at the end of a Wasm function +) + +;; method plus +(func $f.A.plus_I_I + (param $this (ref $c.A)) (param $y i32) (result i32) + ;; call to the getter: push receiver, cast null away, then `call` + local.get $this + ref.as_non_null + call $f.A.x_I + ;; add `y` to the stack and `i32.add` to add it to the result of the call + local.get $y + i32.add +) + +;; constructor +(func $ct.A._I_V + (param $this (ref $c.A)) (param $x i32) + ;; this.x = x + local.get $this + local.get $x + struct.set $c.A $f.A.x + ;; call Object.(this) + local.get $this + ref.as_non_null + call $ct.java.lang.Object._V +) +``` + +In theory, the call to the getter should have been a virtual call in this case. +In practice, the backend contains an analysis of virtual calls to methods that are never overridden, and statically resolves them instead. +In the future, we will probably transfer this optimization to the `Optimizer`, as it already contains all the required logic to efficiently do this. +In the absence of the optimizer, however, this one optimization was important to get decent code size. + +### typeData + +Metadata about IR classes are reified at run-time as values of the struct type `(ref typeData)`. +Documentation for the meaning of each field can be found in `VarGen.genFieldID.typeData`. + +### vtable and virtual method calls + +The vtable of our object model follows a standard layout: + +* The class meta data, then +* Function pointers for the virtual methods, from `jl.Object` down to the current class. + +vtable structs form a subtyping hierarchy that mirrors the class hierarchy, so that `$v.B` is a subtype of `$v.A`. +This is required for `$c.B` to be a valid subtype of `$c.A`, since their first field is of the corresponding vtable types. + +The vtable of `jl.Object` is a subtype of `typeData`, which allows to generically manipulate `typeData`s even when they are not full vtables. +For example, the `typeData` of JS types and Scala interfaces do not have a corresponding vtable. + +An alternative would have been to make the vtables *contain* the `(ref typeData)` as a first field. +That would however require an additional pointer indirection on every access to the `typeData`, for no benefit in memory usage or code size. +WebAssembly does not have a notion of "flattened" inner structs: a struct cannot contain another struct; it can only contain a *reference* to another struct. + +Given + +```scala +class A extends Object { + def foo(x: int): int = x +} + +class B extends A { + val field: int + + def bar(x: double): double = x + override def foo(x: int): int = x + this.field +} +``` + +we get + +```wat +(type $v.A (sub $v.java.lang.Object (struct + ;; ... class metadata + ;; ... methods of jl.Object + (field $m.foo_I_I (ref $4)) +))) + +(type $v.helloworld.B (sub $v.A (struct + ;; ... class metadata + ;; ... methods of jl.Object + (field $m.foo_I_I (ref $4)) + (field $m.bar_D_D (ref $6)) +))) + +(type $4 (func (param (ref any)) (param i32) (result i32))) +(type $6 (func (param (ref any)) (param f64) (result f64))) +``` + +Note that the declared type of `this` in the function types is always `(ref any)`. +If we used the enclosing class type, the type of `$m.foo_I_I` would have incompatible types in the two vtables: + +* In `$v.A`, it would have type `(func (param (ref $c.A)) ...)` +* In `$v.B`, it would have type `(func (param (ref $c.B)) ...)` + +Since the latter is not a subtype of the former, `$v.B` cannot be a subtype of `$v.A` (recall from earlier that we need that subtyping relationship to hold). + +Because we use `(ref any)`, we cannot directly put a reference to the implementing functions (e.g., `$f.A.foo_I_I`) in the vtables: their receiver has a precise type. +Instead, we generate bridge forwarders (the `forTableEntry` methods) which: + +1. take a receiver of type `(ref any)`, +2. cast it down to the precise type, and +3. call the actual implementation function (with a tail call, because why not) + +The table entry forwarder for `A.foo` looks as follows: + +```wat +;; this function has an explicit `(type $4)` which ensures it can be put in the vtables +(func $m.A.foo_I_I (type $4) + (param $this (ref any)) (param $x i32) (result i32) + ;; get the receiver and cast it down to the precise type + local.get $this + ref.cast (ref $c.A) + ;; load the other arguments and call the actual implementation function + local.get $x + return_call $f.A.foo_I_I ;; return_call is a guaranteed tail call +) +``` + +A virtual call to `a.foo(1)` is compiled as you would expect: lookup the function reference in the vtable and call it. + +### itables and interface method calls + +The itables field contains the method tables for interface call dispatch. +It is an instance of the following array type: + +```wat +(type $itables (array (mut structref))) +``` + +As a first approximation, we assign a distinct index to every interface in the program. +It is used to index into the itables array of the instance. +At the index of a given interface `Intf`, we find a `(ref $it.Intf)` whose fields are the method table entries of `Intf`. +Like for vtables, we use the "table entry bridges" in the itables, i.e., the functions where the receiver is of type `(ref any)`. + +For example, given + +```scala +interface Intf { + def foo(x: int): int + def bar(x: double): double +} + +class A extends Intf { + def foo(x: int): int = x + def bar(x: double): double = x +} +``` + +the struct type for `Intf` is defined as + +```wat +(type $it.Intf (struct + (field $m.Intf.bar_D_D (ref $6)) + (field $m.Intf.foo_I_I (ref $4)) +)) + +(type $4 (func (param (ref any)) (param i32) (result i32))) +(type $6 (func (param (ref any)) (param f64) (result f64))) +``` + +In practice, allocating one slot for every interface in the program is wasteful. +We can use the same slot for a set of interfaces that have no concrete class in common. +This slot allocation is implemented in `Preprocessor.assignBuckets`. + +Since Wasm structs only support single inheritance in their subtyping relationships, we have to transform every interface type as `(ref null jl.Object)` (the common supertype of all interfaces). +This does not turn out to be a problem for interface method calls, since they pass through the `itables` array anyway, and use the table entry bridges which take `(ref any)` as argument. + +Given the above structure, an interface method call to `intf.foo(1)` is compiled as expected: lookup the function reference in the appropriate slot of the `itables` array, then call it. + +### Reflective calls + +Calls to reflective proxies use yet another strategy. +Instead of building arrays or structs where each reflective proxy appears at a compile-time-constant slot, we use a search-based strategy. + +Each reflective proxy name found in the program is allocated a unique integer ID. +The reflective proxy table of a class is an array of pairs `(id, funcRef)`, stored in the class' `typeData`. +In order to call a reflective proxy, we perform the following steps: + + +1. Load the `typeData` of the receiver. +2. Search the reflective proxy ID in `$reflectiveProxies` (using the `searchReflectiveProxy` helper). +3. Call it (using `call_ref`). + +This strategy trades off efficiency for space. +It is slow, but that corresponds to the fact that reflective calls are slow on the JVM as well. +In order to have fixed slots for reflective proxy methods, we would need an `m*n` matrix where `m` is the number of concrete classes and `n` the number of distinct reflective proxy names in the entire program. +With the compilation scheme we use, we only need an array containing the actually implemented reflective proxies per class, but we pay an `O(log n)` run-time cost for lookup (instead of `O(1)`). + +## Hijacked classes + +Due to our strong interoperability guarantees with JavaScript, the universal (boxed) representation of hijacked classes must be the appropriate JavaScript values. +For example, a boxed `int` must be a JavaScript `number`. +The only Wasm type that can store references to both GC structs and arbitrary JavaScript `number`s is `anyref` (an alias of `(ref null any)`). +That is why we transform the types of ancestors of hijacked classes to the Wasm type `anyref`. + +### Boxing + +When an `int` is upcast to `jl.Integer` or higher, we must *adapt* the `i32` into `anyref`. +Doing so is not free, since `i32` is not a subtype of `anyref`. +Even worse, no Wasm-only instruction sequence is able to perform that conversion in a way that we always get a JavaScript `number`. + +Instead, we ask JavaScript for help. +We use the following JavaScript helper function, which is defined in `LoaderContent`: + +```js +__scalaJSHelpers: { + bI: (x) => x, +} +``` + +Huh!? That's an identity function. +How does it help? + +The magic is to import it into Wasm with a non-identity type. +We import it as + +```wat +(import "__scalaJSHelpers" "bI" (func $bI (param i32) (result anyref))) +``` + +The actual conversion happens at the boundary between Wasm and JavaScript and back. +Conversions are specified in the [Wasm JS Interface](https://webassembly.github.io/gc/js-api/index.html). +The relevant internal functions are [`ToJSValue`](https://webassembly.github.io/gc/js-api/index.html#tojsvalue) and [`ToWebAssemblyValue`](https://webassembly.github.io/gc/js-api/index.html#towebassemblyvalue). + +When calling `$bI` with an `i32` value as argument, on the Wasm spec side of things, it is an `i32.const u32` value (Wasm values carry their type from a spec point of view). +`ToJSValue` then specifies that: + +> * If `w` is of the form `i32.const u32`, +> * Let `i32` be `signed_32(u32)`. +> * Return 𝔽(`i32` interpreted as a mathematical value). + +where 𝔽 is the JS spec function that creates a `number` from a mathematical value. + +When that `number` *returns* from the JavaScript "identity" function and flows back into Wasm, the spec invokes `ToWebAssemblyValue(v, anyref)`, which specifies: + +> * If `type` is of the form `ref null heaptype` (here `heaptype = any`), +> * [...] +> * Else, +> 1. Let `map` be the surrounding agent's associated host value cache. +> 2. If a host address `hostaddr` exists such that `map[hostaddr]` is the same as `v`, +> * Return `ref.host hostaddr`. +> 3. Let host address `hostaddr` be the smallest address such that `map[hostaddr]` exists is `false`. +> 4. Set `map[hostaddr]` to `v`. +> 5. Let `r` be `ref.host hostaddr`. + +Therefore, from a spec point of view, we receive back a `ref.host hostaddr` for which the engine remembers that it maps to `v`. +That `ref.host` value is a valid value of type `anyref`, and therefore we can carry it around inside Wasm. + +### Unboxing + +When we *unbox* an IR `any` into a primitive `int`, we perform perform the inverse operations. +We also use an identity function at the JavaScript for unboxing an `int`: + +```js +__scalaJSHelpers: { + uI: (x) => x, +} +``` + +However, we swap the Wasm types of parameter and result: + +```wat +(import "__scalaJSHelpers" "uI" (func $uI (param anyref) (result i32))) +``` + +When the `ref.host hostaddr` enters JavaScript, `ToJSValue` specifies: + +> * If `w` is of the form `ref.host hostaddr`, +> * Let `map` be the surrounding agent's associated host value cache. +> * Assert: `map[hostaddr]` exists. +> * Return `map[hostaddr]`. + +This recovers the JavaScript `number` value we started with. +When it comes back into WebAssembly, the spec invokes `ToWebAssemblyValue(v, i32)`, which specifies: + +> * If `type` is `i32`, +> * Let `i32` be ? `ToInt32(v)`. +> * Let `u32` be the unsigned integer such that `i32` is `signed_32(u32)`. +> * Return `i32.const u32`. + +Overall, we use `bI`/`uI` as a pair of round-trip functions that perform a lossless conversion from `i32` to `anyref` and back, in a way that JavaScript code would always see the appropriate `number` value. + +Note: conveniently, `ToInt32(v)` also takes care of converting `null` into 0, which is a spec trivia we also exploit in the JS backend. + +### Efficiency + +How is the above not terribly inefficient? +Because implementations do not actually use a "host value cache" map. +Instead, they pass pointer values as is through the boundary. + +Concretely, `ToWebAssemblyValue(v, anyref)` and `ToJSValue(ref.host x)` are no-ops. +The conversions involving `i32` are not free, but they are as efficient as it gets for the target JS engines. + +### Method dispatch + +When the receiver of a method call is a primitive or a hijacked class, the call can always be statically resolved by construction, hence no dispatch is necessary. +For strict ancestors of hijacked classes, we must use a type-test-based dispatch similar to what we do in `$dp_` dispatchers in the JavaScript backend. + +## Arrays + +Like the JS backend, we define a separate `struct` type for each primitive array type: `$IntArray`, `$FloatArray`, etc. +Unlike the JS backend, we merge all the reference array types in a single `struct` type `$ObjectArray`. +We do not really have a choice, since there is a (practically) unbounded amount of them, and we cannot create new `struct` types at run-time. + +All array "classes" follow the same structure: + +* They actually extend `jl.Object` +* Their vtable type is the same as `jl.Object` +* They each have their own vtable value for the differing metadata, although the method table entries are the same as in `jl.Object` + * This is also true for reference types: the vtables are dynamically created at run-time on first use (they are values and share the same type, so that we can do) +* Their `itables` field points to a common itables array with entries for `jl.Cloneable` and `j.io.Serializable` +* They have a unique "user-land" field `$underlyingArray`, which is a Wasm array of its values: + * For primitives, they are primitive arrays, such as `(array mut i32)` + * For references, they are all the same type `(array mut anyref)` + +Concretely, here are the relevant Wasm definitions: + +```wat +(type $i8Array (array (mut i8))) +(type $i16Array (array (mut i16))) +(type $i32Array (array (mut i32))) +(type $i64Array (array (mut i64))) +(type $f32Array (array (mut f32))) +(type $f64Array (array (mut f64))) +(type $anyArray (array (mut anyref))) + +(type $BooleanArray (sub final $c.java.lang.Object (struct + (field $vtable (ref $v.java.lang.Object)) + (field $itables (ref null $itables)) + (field $arrayUnderlying (ref $i8Array)) +))) +(type $CharArray (sub final $c.java.lang.Object (struct + (field $vtable (ref $v.java.lang.Object)) + (field $itables (ref null $itables)) + (field $arrayUnderlying (ref $i16Array)) +))) +... +(type $ObjectArray (sub final $c.java.lang.Object (struct + (field $vtable (ref $v.java.lang.Object)) + (field $itables (ref null $itables)) + (field $arrayUnderlying (ref $anyArray)) +))) +``` + +Given the above layout, reading and writing length and elements is straightforward. +The only catch is reading an element of a reference type that is more specific than `jl.Object[]`. +In that case, we must `ref.cast` the element down to its transformed Wasm type to preserve typing. +This is not great, but given the requirement that reference array types be (unsoundly) covariant in their element type, it seems to be the only viable encoding. + +The indirection to get at `$arrayUnderlying` elements is not ideal either, but is no different than what we do in the JS backend with the `u` field. +In the future, Wasm might provide the ability to [nest an array in a flat layout at the end of a struct](https://github.com/WebAssembly/gc/blob/main/proposals/gc/Post-MVP.md#nested-data-structures). + +## Order of definitions in the Wasm module + +For most definitions, Wasm does not care in what order things are defined in a module. +In particular, all functions are declared ahead of time, so that the order in which they are defined is irrelevant. + +There are however some exceptions. +The ones that are relevant to our usage of Wasm are the following: + +* In a given recursive type group, type definitions can only refer to types defined in that group or in previous groups (recall that all type definitions are part of recursive type groups, even if they are alone). +* Even within a recursive type group, the *supertype* of a type definition must be defined before it. +* The initialization code of `global` definitions can only refer to other global definitions that are defined before. + +For type definitions, we use the following ordering: + +1. Definitions of the underlying array types (e.g., `(type $i8Array (array (mut i8)))`) +2. The big recursive type group, with: + 1. Some types referred to from `$typeData`, in no particular order. + 2. The `$typeData` struct definition (it is a supertype of the vtable types, so it must come early). + 3. For each Scala class or interface in increasing order of ancestor count (the same order we use in the JS backend), if applicable: + 1. Its vtable type (e.g., `$v.java.lang.Object`) + 2. Its object struct type (e.g., `$c.java.lang.Object`) + 3. Its itable type (e.g., `$it.java.lang.Comparable`) + 4. Function types appearing in vtables and itables, interspersed with the above in no particular order. + 5. The `$XArray` struct definitions (e.g., `$BooleanArray`), which are subtypes of `$c.java.lang.Object`. +3. All the other types, in no particular order, among which: + * Function types that do not appear in vtables and itables, including the method implementation types and auto-generated function types for block types + * Closure data struct types + +For global definitions, we use the following ordering: + +1. The typeData of the primitive types (e.g., `$d.I`) +2. For each linked class, in the same ancestor count-based order: + 1. In no particular order, if applicable: + * Its typeData/vtable global (e.g., `$d.java.lang.Object`), which may refer to the typeData of ancestors, so the order between classes is important + * Its itables global (e.g., `$it.java.lang.Class`) + * Static field definitions + * Definitions of `Symbol`s for the "names" of private JS fields + * The module instance + * The cached JS class value +3. Cached values of boxed zero values (such as `$bZeroChar`), which refer to the vtable and itables globals of the box classes +4. The itables global of array classes (namely, `$arrayClassITable`) + +## Miscellaneous + +### Object instantiation + +An IR `New(C, ctor, args)` embeds two steps: + +1. Allocate a new instance of `C` with all fields initialized to their zero +2. Call the given `ctor` on the new instance + +The second step follows the compilation scheme of a statically resolved method call, which we saw above. +The allocation itself is performed by a `$new.C` function, which we generate for every concrete class. +It looks like the following: + +```wat +(func $new.C + (result (ref $c.C)) + + global.get $d.C ;; the global vtable for class C + global.get $it.C ;; the global itables for class C + i32.const 0 ;; zero of type int + f64.const 0.0 ;; zero of type double + struct.new $c.C ;; allocate a $c.C initialized with all of the above +) +``` + +It would be nice to do the following instead: + +1. Allocate a `$c.C` entirely initialized with zeros, using `struct.new_default` +2. Set the `$vtable` and `$itables` fields + +This would have a constant code size cost, irrespective of the amount of fields in `C`. +Unfortunately, we cannot do this because the `$vtable` field is immutable. + +We cannot make it mutable since we rely on covariance (which only applies for immutable fields) for class subtyping. +Abandoning this would have much worse consequences. + +Wasm may evolve to have [a more flexible `struct.new_default`](https://github.com/WebAssembly/gc/blob/main/proposals/gc/Post-MVP.md#handle-nondefaultable-fields-in-structnew_default), which would solve this trade-off. + +### Clone + +The IR node `Clone` takes an arbitrary instance of `jl.Cloneable` and returns a shallow copy of it. +Wasm does not have any generic way to clone a reference to a `struct`. +We must statically know what type of `struct` we want to clone instead. + +To solve this issue, we add a "magic" `$clone` function pointer in every vtable. +It is only populated for classes that actually extend `jl.Cloneable`. +We then compile a `Clone` node similarly to any virtual method call. + +Each concrete implementation `$clone.C` statically knows its corresponding `$c.C` struct type. +It can therefore allocate a new instance and copy all the fields. + +### Identity hash code + +We implement `IdentityHashCode` in the same way as the JS backend: + +* We allocate one global `WeakMap` to store the identity hash codes (`idHashCodeMap`) +* We allocate identity hash codes themselves by incrementing a global counter (`lastIDHashCode`) +* For primitives, which we cannot put in a `WeakMap`, we use their normal `hashCode()` method + +This is implemented in the function `identityHashCode` in `CoreWasmLib`. + +### Strings + +As mentioned above, strings are represented as JS `string`s. +All the primitive operations on strings, including string concatenation (which embeds conversion to string) are performed by helper JS functions. + +String constants are gathered from the entire program and their raw bytes stored in a data segment. +We deduplicate strings so that we do not store the same string several times, but otherwise do not attempt further compression (such as reusing prefixes). +Since creating string values from the data segment is expensive, we cache the constructed strings in a global array. + +At call site, we emit the following instruction sequence: + +```wat +i32.const 84 ;; start of the string content in the data segment, in bytes +i32.const 10 ;; string length, in chars +i32.const 9 ;; index into the cache array for that string +call $stringLiteral +``` + +In the future, we may want to use one of the following two Wasm proposals to improve efficiency of strings: + +* [JS String Builtins](https://github.com/WebAssembly/js-string-builtins) +* [Reference-Typed Strings, aka `stringref`](https://github.com/WebAssembly/stringref) + +Even before that, an alternative for string literals would be to create them upfront from the JS loader and pass them to Wasm as `import`s. + +## JavaScript interoperability + +The most difficult aspects of JavaScript interoperability are related to hijacked classes, which we already mentioned. +Other than that, we have: + +* a number of IR nodes with JS operation semantics (starting with `JS...`), +* closures, and +* non-native JS classes. + +### JS operation IR nodes + +We use a series of helper JS functions that directly embed the operation semantics. +For example, `JSMethodApply` is implemented as a call to the following helper: + +```js +__scalaJSHelpers: { + jsMethodApply: (o, m, args) => o[m](...args), +} +``` + +The `args` are passed a JS array, which is built one element at a time, using the following helpers: + +```js +__scalaJSHelpers: { + jsNewArray: () => [], + jsArrayPush: (a, v) => (a.push(v), a), + jsArraySpreadPush: (a, vs) => (a.push(...vs), a), +} +``` + +This is of course far from being ideal. +In the future, we will likely want to generate a JS helper for each call site, so that it can be specialized for the method name and shape of argument list. + +### Closures + +Wasm can create a function reference to any Wasm function with `ref.func`. +Such a function reference can be passed to JavaScript and will be seen as a JS function. +However, it is not possible to create *closures*; all the arguments to the Wasm function must always be provided. + +In order to create closures, we reify captures as a `__captureData` argument to the Wasm function. +It is a reference to a `struct` with values for all the capture params of the IR `Closure` node. +We allocate that struct when creating the `Closure`, then pass it to a JS helper, along with the function reference. +The JS helper then creates an actual closure from the JS side and returns it to Wasm. + +To accomodate the combination of `function`/`=>` and `...rest`/no-rest, we use the following four helpers: + +```js +__scalaJSHelpers: { + closure: (f, data) => f.bind(void 0, data), + closureThis: (f, data) => function(...args) { return f(data, this, ...args); }, + closureRest: (f, data, n) => ((...args) => f(data, ...args.slice(0, n), args.slice(n))), + closureThisRest: (f, data, n) => function(...args) { return f(data, this, ...args.slice(0, n), args.slice(n)); }, +} +``` + +The `n` parameter is the number of non-rest parameters to the function. + +They are imported into Wasm with the following signatures: + +```wat +(import "__scalaJSHelpers" "closure" + (func $closure (param (ref func)) (param anyref) (result (ref any)))) +(import "__scalaJSHelpers" "closureThis" + (func $closureThis (param (ref func)) (param anyref) (result (ref any)))) +(import "__scalaJSHelpers" "closureRest" + (func $closureRest (param (ref func)) (param anyref) (param i32) (result (ref any)))) +(import "__scalaJSHelpers" "closureThisRest" + (func $closureThisRest (param (ref func)) (param anyref) (param i32) (result (ref any)))) +``` + +### Non-native JS classes + +For non-native JS classes, we take the above approach to another level. +We use a unique JS helper function to create arbitrary JavaScript classes. +It reads as follows: + +```js +__scalaJSHelpers: { + createJSClass: (data, superClass, preSuperStats, superArgs, postSuperStats, fields) => { + // fields is an array where even indices are field names and odd indices are initial values + return class extends superClass { + constructor(...args) { + var preSuperEnv = preSuperStats(data, new.target, ...args); + super(...superArgs(data, preSuperEnv, new.target, ...args)); + for (var i = 0; i != fields.length; i = (i + 2) | 0) { + Object.defineProperty(this, fields[i], { + value: fields[(i + 1) | 0], + configurable: true, + enumerable: true, + writable: true, + }); + } + postSuperStats(data, preSuperEnv, new.target, this, ...args); + } + }; + }, +} +``` + +Since the `super()` call must lexically appear in the `constructor` of the class, we have to decompose the body of the constructor into 3 functions: + +* `preSuperStats` contains the statements before the super call, and returns an environment of the locally declared variables as a `struct` (much like capture data), +* `superArgs` computes an array of the arguments to the super call, and +* `postSuperStats` contains the statements after the super call. + +The latter two take the `preSuperEnv` environment computed by `preSuperStats` as parameter. +All functions also receive the class captures `data` and the value of `new.target`. + +The helper also takes the `superClass` as argument, as well as an array describing what `fields` should be created. +The `fields` array contains an even number of elements: + +* even indices are field names, +* odd indices are the initial value of the corresponding field. + +The method `ClassEmitter.genCreateJSClassFunction` is responsible for generating the code that calls `createJSClass`. +After that call, it uses more straightforward helpers to install the instance methods/properties and static methods/properties. +Those are created as `function` closures, which mimics the run-time spec behavior of the `class` construct. + +In the future, we may also want to generate a specialized version of `createJSClass` for each declared non-native JS class. +It could specialize the shape of constructor parameters, the shape of the arguments to the super constructor, and the fields. + +## Exceptions + +In Wasm, exceptions consist of a *tag* and a *payload*. +The tag defines the signature of the payload, and must be declared upfront (either imported or defined within Wasm). +Typically, each language defines a unique tag with a payload that matches its native exception type. +For example, a Java-to-Wasm compiler would define a tag `$javaException` with type `[(ref jl.Throwable)]`, indicating that its payload is a unique reference to a non-null instance of `java.lang.Throwable`. + +In order to throw an exception, the Wasm `throw` instruction takes a tag and arguments that match its payload type. +Exceptions can be caught in two ways: + +* A specific `catch` with a given tag: it only catches exceptions thrown with that tag, and extracts the payload value. +* A catch-all: it catches all exceptions, but the payloads cannot be observed. + +Each of those cases comes with a variant that captures an `exnref`, which can be used to re-throw the exception with `throw_ref`. + +For Scala.js, our exception model says that we can throw and catch arbitrary values, i.e., `anyref`. +Moreover, our exceptions can be caught by JavaScript, and JavaScript exceptions can be caught from Scala.js. + +JavaScript exceptions are reified in Wasm as exceptions with a special tag, namely `WebAssembly.JSTag`, defined in the JS API. +Wasm itself does not know that tag, but it can be `import`ed. +Its payload signature is a single `externref`, which is isomorphic to `anyref` (there is a pair of Wasm instructions to losslessly convert between them). + +Instead of defining our own exception tag, we exclusively use `JSTag`, both for throwing and catching. +That makes our exceptions directly interoperable with JavaScript at no extra cost. +The import reads as + +```wat +(import "__scalaJSHelpers" "JSTag" (tag $exception (param externref))) +``` + +Given the above, `Throw` and `TryCatch` have a straightforward implementation. + +For `TryFinally`, we have to compile it down to a try-catch-all, because Wasm does not have any notion of `try..finally`. +That compilation scheme is very complicated. +It deserves an entire dedicated explanation, which is covered by the big comment in `FunctionEmitter` starting with `HERE BE DRAGONS`. diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/SWasmGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/SWasmGen.scala new file mode 100644 index 0000000000..a1ef630952 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/SWasmGen.scala @@ -0,0 +1,137 @@ +/* + * 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.backend.wasmemitter + +import org.scalajs.ir.Names._ +import org.scalajs.ir.Trees.JSNativeLoadSpec +import org.scalajs.ir.Types._ + +import org.scalajs.linker.backend.webassembly._ +import org.scalajs.linker.backend.webassembly.Instructions._ + +import VarGen._ + +/** Scala.js-specific Wasm generators that are used across the board. */ +object SWasmGen { + + def genZeroOf(tpe: Type)(implicit ctx: WasmContext): Instr = { + tpe match { + case BooleanType | CharType | ByteType | ShortType | IntType => + I32Const(0) + + case LongType => I64Const(0L) + case FloatType => F32Const(0.0f) + case DoubleType => F64Const(0.0) + case StringType => GlobalGet(genGlobalID.emptyString) + case UndefType => GlobalGet(genGlobalID.undef) + + case AnyType | ClassType(_) | ArrayType(_) | NullType => + RefNull(Types.HeapType.None) + + case NoType | NothingType | _: RecordType => + throw new AssertionError(s"Unexpected type for field: ${tpe.show()}") + } + } + + def genBoxedZeroOf(tpe: Type)(implicit ctx: WasmContext): Instr = { + tpe match { + case BooleanType => + GlobalGet(genGlobalID.bFalse) + case CharType => + GlobalGet(genGlobalID.bZeroChar) + case ByteType | ShortType | IntType | FloatType | DoubleType => + GlobalGet(genGlobalID.bZero) + case LongType => + GlobalGet(genGlobalID.bZeroLong) + case AnyType | ClassType(_) | ArrayType(_) | StringType | UndefType | NullType => + RefNull(Types.HeapType.None) + + case NoType | NothingType | _: RecordType => + throw new AssertionError(s"Unexpected type for field: ${tpe.show()}") + } + } + + def genLoadTypeData(fb: FunctionBuilder, typeRef: TypeRef): Unit = typeRef match { + case typeRef: NonArrayTypeRef => genLoadNonArrayTypeData(fb, typeRef) + case typeRef: ArrayTypeRef => genLoadArrayTypeData(fb, typeRef) + } + + def genLoadNonArrayTypeData(fb: FunctionBuilder, typeRef: NonArrayTypeRef): Unit = { + fb += GlobalGet(genGlobalID.forVTable(typeRef)) + } + + def genLoadArrayTypeData(fb: FunctionBuilder, arrayTypeRef: ArrayTypeRef): Unit = { + genLoadNonArrayTypeData(fb, arrayTypeRef.base) + fb += I32Const(arrayTypeRef.dimensions) + fb += Call(genFunctionID.arrayTypeData) + } + + /** Gen code to load the vtable and the itable of the given array type. */ + def genLoadVTableAndITableForArray(fb: FunctionBuilder, arrayTypeRef: ArrayTypeRef): Unit = { + // Load the typeData of the resulting array type. It is the vtable of the resulting object. + genLoadArrayTypeData(fb, arrayTypeRef) + + // Load the itables for the array type + fb += GlobalGet(genGlobalID.arrayClassITable) + } + + def genArrayValue(fb: FunctionBuilder, arrayTypeRef: ArrayTypeRef, length: Int)( + genElems: => Unit): Unit = { + genLoadVTableAndITableForArray(fb, arrayTypeRef) + + // Create the underlying array + genElems + val underlyingArrayType = genTypeID.underlyingOf(arrayTypeRef) + fb += ArrayNewFixed(underlyingArrayType, length) + + // Create the array object + fb += StructNew(genTypeID.forArrayClass(arrayTypeRef)) + } + + def genLoadJSConstructor(fb: FunctionBuilder, className: ClassName)( + implicit ctx: WasmContext): Unit = { + val info = ctx.getClassInfo(className) + + info.jsNativeLoadSpec match { + case None => + // This is a non-native JS class + fb += Call(genFunctionID.loadJSClass(className)) + + case Some(loadSpec) => + genLoadJSFromSpec(fb, loadSpec) + } + } + + def genLoadJSFromSpec(fb: FunctionBuilder, loadSpec: JSNativeLoadSpec)( + implicit ctx: WasmContext): Unit = { + def genFollowPath(path: List[String]): Unit = { + for (prop <- path) { + fb ++= ctx.stringPool.getConstantStringInstr(prop) + fb += Call(genFunctionID.jsSelect) + } + } + + loadSpec match { + case JSNativeLoadSpec.Global(globalRef, path) => + fb ++= ctx.stringPool.getConstantStringInstr(globalRef) + fb += Call(genFunctionID.jsGlobalRefGet) + genFollowPath(path) + case JSNativeLoadSpec.Import(module, path) => + fb += GlobalGet(genGlobalID.forImportedModule(module)) + genFollowPath(path) + case JSNativeLoadSpec.ImportWithGlobalFallback(importSpec, _) => + genLoadJSFromSpec(fb, importSpec) + } + } + +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/SpecialNames.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/SpecialNames.scala new file mode 100644 index 0000000000..9a060bc3b4 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/SpecialNames.scala @@ -0,0 +1,48 @@ +/* + * 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.backend.wasmemitter + +import org.scalajs.ir.Names._ +import org.scalajs.ir.Types._ + +object SpecialNames { + // Class names + + /* Our back-end-specific box classes for the generic representation of + * `char` and `long`. These classes are not part of the classpath. They are + * generated automatically by `DerivedClasses`. + */ + val CharBoxClass = BoxedCharacterClass.withSuffix("Box") + val LongBoxClass = BoxedLongClass.withSuffix("Box") + + val CharBoxCtor = MethodName.constructor(List(CharRef)) + val LongBoxCtor = MethodName.constructor(List(LongRef)) + + // js.JavaScriptException, for WrapAsThrowable and UnwrapFromThrowable + val JSExceptionClass = ClassName("scala.scalajs.js.JavaScriptException") + + // Field names + + val valueFieldSimpleName = SimpleFieldName("value") + + val exceptionFieldName = FieldName(JSExceptionClass, SimpleFieldName("exception")) + + // Method names + + val AnyArgConstructorName = MethodName.constructor(List(ClassRef(ObjectClass))) + + val hashCodeMethodName = MethodName("hashCode", Nil, IntRef) + + /** A unique simple method name to map all method *signatures* into `MethodName`s. */ + val normalizedSimpleMethodName = SimpleMethodName("m") +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/StringPool.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/StringPool.scala new file mode 100644 index 0000000000..12450488cc --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/StringPool.scala @@ -0,0 +1,107 @@ +/* + * 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.backend.wasmemitter + +import scala.collection.mutable + +import org.scalajs.ir.OriginalName + +import org.scalajs.linker.backend.webassembly.Instructions._ +import org.scalajs.linker.backend.webassembly.Modules._ +import org.scalajs.linker.backend.webassembly.Types._ + +import VarGen._ + +private[wasmemitter] final class StringPool { + import StringPool._ + + private val registeredStrings = new mutable.AnyRefMap[String, StringData] + private val rawData = new mutable.ArrayBuffer[Byte]() + private var nextIndex: Int = 0 + + // Set to true by `genPool()`. When true, registering strings is illegal. + private var poolWasGenerated: Boolean = false + + /** Registers the given constant string and returns its allocated data. */ + private def register(str: String): StringData = { + if (poolWasGenerated) + throw new IllegalStateException("The string pool was already generated") + + registeredStrings.getOrElseUpdate(str, { + // Compute the new entry before changing the state + val data = StringData(nextIndex, offset = rawData.size) + + // Write the actual raw data and update the next index + rawData ++= str.toCharArray.flatMap { char => + Array((char & 0xFF).toByte, (char >> 8).toByte) + } + nextIndex += 1 + + data + }) + } + + /** Returns the list of instructions that load the given constant string. + * + * The resulting list is *not* a Wasm constant expression, since it includes + * a `call` to the helper function `stringLiteral`. + */ + def getConstantStringInstr(str: String): List[Instr] = + getConstantStringDataInstr(str) :+ Call(genFunctionID.stringLiteral) + + /** Returns the list of 3 constant integers that must be passed to `stringLiteral`. + * + * The resulting list is a Wasm constant expression, and hence can be used + * in the initializer of globals. + */ + def getConstantStringDataInstr(str: String): List[I32Const] = { + val data = register(str) + List( + I32Const(data.offset), + I32Const(str.length()), + I32Const(data.constantStringIndex) + ) + } + + def genPool()(implicit ctx: WasmContext): Unit = { + poolWasGenerated = true + + ctx.moduleBuilder.addData( + Data( + genDataID.string, + OriginalName("stringPool"), + rawData.toArray, + Data.Mode.Passive + ) + ) + + ctx.addGlobal( + Global( + genGlobalID.stringLiteralCache, + OriginalName("stringLiteralCache"), + isMutable = false, + RefType(genTypeID.anyArray), + Expr( + List( + I32Const(nextIndex), // number of entries in the pool + ArrayNewDefault(genTypeID.anyArray) + ) + ) + ) + ) + } +} + +private[wasmemitter] object StringPool { + private final case class StringData(constantStringIndex: Int, offset: Int) +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/TypeTransformer.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/TypeTransformer.scala new file mode 100644 index 0000000000..55101a98b3 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/TypeTransformer.scala @@ -0,0 +1,116 @@ +/* + * 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.backend.wasmemitter + +import org.scalajs.ir.Names._ +import org.scalajs.ir.Types._ + +import org.scalajs.linker.backend.webassembly.{Types => watpe} + +import VarGen._ + +object TypeTransformer { + + /** Transforms an IR type for a local definition (including parameters). + * + * `void` is not a valid input for this method. It is rejected by the + * `ClassDefChecker`. + * + * `nothing` translates to `i32` in this specific case, because it is a valid + * type for a `ParamDef` or `VarDef`. Obviously, assigning a value to a local + * of type `nothing` (either locally or by calling the method for a param) + * can never complete, and therefore reading the value of such a local is + * always unreachable. It is up to the reading codegen to handle this case. + */ + def transformLocalType(tpe: Type)(implicit ctx: WasmContext): watpe.Type = { + tpe match { + case NothingType => watpe.Int32 + case _ => transformType(tpe) + } + } + + /** Transforms an IR type to the Wasm result types of a function or block. + * + * `void` translates to an empty result type list, as expected. + * + * `nothing` translates to an empty result type list as well, because Wasm does + * not have a bottom type (at least not one that can expressed at the user level). + * A block or function call that returns `nothing` should typically be followed + * by an extra `unreachable` statement to recover a stack-polymorphic context. + * + * @see + * https://webassembly.github.io/spec/core/syntax/types.html#result-types + */ + def transformResultType(tpe: Type)(implicit ctx: WasmContext): List[watpe.Type] = { + tpe match { + case NoType => Nil + case NothingType => Nil + case _ => List(transformType(tpe)) + } + } + + /** Transforms a value type to a unique Wasm type. + * + * This method cannot be used for `void` and `nothing`, since they have no corresponding Wasm + * value type. + */ + def transformType(tpe: Type)(implicit ctx: WasmContext): watpe.Type = { + tpe match { + case AnyType => watpe.RefType.anyref + case ClassType(className) => transformClassType(className) + case StringType | UndefType => watpe.RefType.any + case tpe: PrimTypeWithRef => transformPrimType(tpe) + + case tpe: ArrayType => + watpe.RefType.nullable(genTypeID.forArrayClass(tpe.arrayTypeRef)) + + case RecordType(fields) => + throw new AssertionError(s"Unexpected record type $tpe") + } + } + + def transformClassType(className: ClassName)(implicit ctx: WasmContext): watpe.RefType = { + ctx.getClassInfoOption(className) match { + case Some(info) => + if (info.isAncestorOfHijackedClass) + watpe.RefType.anyref + else if (!info.hasInstances) + watpe.RefType.nullref + else if (info.isInterface) + watpe.RefType.nullable(genTypeID.ObjectStruct) + else + watpe.RefType.nullable(genTypeID.forClass(className)) + + case None => + watpe.RefType.nullref + } + } + + private def transformPrimType(tpe: PrimTypeWithRef): watpe.Type = { + tpe match { + case BooleanType => watpe.Int32 + case ByteType => watpe.Int32 + case ShortType => watpe.Int32 + case IntType => watpe.Int32 + case CharType => watpe.Int32 + case LongType => watpe.Int64 + case FloatType => watpe.Float32 + case DoubleType => watpe.Float64 + case NullType => watpe.RefType.nullref + + case NoType | NothingType => + throw new IllegalArgumentException( + s"${tpe.show()} does not have a corresponding Wasm type") + } + } +} 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 new file mode 100644 index 0000000000..7d26398a25 --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/VarGen.scala @@ -0,0 +1,446 @@ +/* + * 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.backend.wasmemitter + +import org.scalajs.ir.Names._ +import org.scalajs.ir.Trees.{JSUnaryOp, JSBinaryOp, MemberNamespace} +import org.scalajs.ir.Types._ + +import org.scalajs.linker.backend.webassembly.Identitities._ + +/** Manages generation of non-local IDs. + * + * `LocalID`s and `LabelID`s are directly managed by `FunctionBuilder` instead. + */ +object VarGen { + + object genGlobalID { + final case class forImportedModule(moduleName: String) extends GlobalID + final case class forModuleInstance(className: ClassName) extends GlobalID + final case class forJSClassValue(className: ClassName) extends GlobalID + + final case class forVTable(typeRef: NonArrayTypeRef) extends GlobalID + + object forVTable { + def apply(className: ClassName): forVTable = + forVTable(ClassRef(className)) + } + + final case class forITable(className: ClassName) extends GlobalID + final case class forStaticField(fieldName: FieldName) extends GlobalID + final case class forJSPrivateField(fieldName: FieldName) extends GlobalID + + case object bZeroChar extends GlobalID + case object bZeroLong extends GlobalID + case object stringLiteralCache extends GlobalID + case object arrayClassITable extends GlobalID + case object lastIDHashCode extends GlobalID + + /** A `GlobalID` for a JS helper global. + * + * Its `toString()` is guaranteed to correspond to the import name of the helper. + */ + sealed abstract class JSHelperGlobalID extends GlobalID + + case object jsLinkingInfo extends JSHelperGlobalID + case object undef extends JSHelperGlobalID + case object bFalse extends JSHelperGlobalID + case object bZero extends JSHelperGlobalID + case object emptyString extends JSHelperGlobalID + case object idHashCodeMap extends JSHelperGlobalID + } + + object genFunctionID { + final case class forMethod(namespace: MemberNamespace, + className: ClassName, methodName: MethodName) + extends FunctionID + + final case class forTableEntry(className: ClassName, methodName: MethodName) + extends FunctionID + + final case class forExport(exportedName: String) extends FunctionID + final case class forTopLevelExportSetter(exportedName: String) extends FunctionID + + final case class loadModule(className: ClassName) extends FunctionID + final case class newDefault(className: ClassName) extends FunctionID + final case class instanceTest(className: ClassName) extends FunctionID + final case class clone(className: ClassName) extends FunctionID + final case class cloneArray(arrayBaseRef: NonArrayTypeRef) extends FunctionID + + final case class isJSClassInstance(className: ClassName) extends FunctionID + final case class loadJSClass(className: ClassName) extends FunctionID + final case class createJSClassOf(className: ClassName) extends FunctionID + final case class preSuperStats(className: ClassName) extends FunctionID + final case class superArgs(className: ClassName) extends FunctionID + final case class postSuperStats(className: ClassName) extends FunctionID + + case object start extends FunctionID + + // JS helpers + + /** A `FunctionID` for a JS helper function. + * + * Its `toString()` is guaranteed to correspond to the import name of the helper. + */ + sealed abstract class JSHelperFunctionID extends FunctionID + + case object is extends JSHelperFunctionID + + case object isUndef extends JSHelperFunctionID + + final case class box(primRef: PrimRef) extends JSHelperFunctionID { + override def toString(): String = "b" + primRef.charCode + } + + final case class unbox(primRef: PrimRef) extends JSHelperFunctionID { + override def toString(): String = "u" + primRef.charCode + } + + final case class typeTest(primRef: PrimRef) extends JSHelperFunctionID { + override def toString(): String = "t" + primRef.charCode + } + + case object fmod extends JSHelperFunctionID + + case object closure extends JSHelperFunctionID + case object closureThis extends JSHelperFunctionID + case object closureRest extends JSHelperFunctionID + case object closureThisRest extends JSHelperFunctionID + + case object makeExportedDef extends JSHelperFunctionID + case object makeExportedDefRest extends JSHelperFunctionID + + case object stringLength extends JSHelperFunctionID + case object stringCharAt extends JSHelperFunctionID + case object jsValueToString extends JSHelperFunctionID // for actual toString() call + case object jsValueToStringForConcat extends JSHelperFunctionID + case object booleanToString extends JSHelperFunctionID + case object charToString extends JSHelperFunctionID + case object intToString extends JSHelperFunctionID + case object longToString extends JSHelperFunctionID + case object doubleToString extends JSHelperFunctionID + case object stringConcat extends JSHelperFunctionID + case object isString extends JSHelperFunctionID + + case object jsValueType extends JSHelperFunctionID + case object bigintHashCode extends JSHelperFunctionID + case object symbolDescription extends JSHelperFunctionID + case object idHashCodeGet extends JSHelperFunctionID + case object idHashCodeSet extends JSHelperFunctionID + + case object jsGlobalRefGet extends JSHelperFunctionID + case object jsGlobalRefSet extends JSHelperFunctionID + case object jsGlobalRefTypeof extends JSHelperFunctionID + case object jsNewArray extends JSHelperFunctionID + case object jsArrayPush extends JSHelperFunctionID + case object jsArraySpreadPush extends JSHelperFunctionID + case object jsNewObject extends JSHelperFunctionID + case object jsObjectPush extends JSHelperFunctionID + case object jsSelect extends JSHelperFunctionID + case object jsSelectSet extends JSHelperFunctionID + case object jsNew extends JSHelperFunctionID + case object jsFunctionApply extends JSHelperFunctionID + case object jsMethodApply extends JSHelperFunctionID + case object jsImportCall extends JSHelperFunctionID + case object jsImportMeta extends JSHelperFunctionID + case object jsDelete extends JSHelperFunctionID + case object jsForInSimple extends JSHelperFunctionID + case object jsIsTruthy extends JSHelperFunctionID + + final case class jsUnaryOp(name: String) extends JSHelperFunctionID { + override def toString(): String = name + } + + val jsUnaryOps: Map[JSUnaryOp.Code, jsUnaryOp] = { + Map( + JSUnaryOp.+ -> jsUnaryOp("jsUnaryPlus"), + JSUnaryOp.- -> jsUnaryOp("jsUnaryMinus"), + JSUnaryOp.~ -> jsUnaryOp("jsUnaryTilde"), + JSUnaryOp.! -> jsUnaryOp("jsUnaryBang"), + JSUnaryOp.typeof -> jsUnaryOp("jsUnaryTypeof") + ) + } + + final case class jsBinaryOp(name: String) extends JSHelperFunctionID { + override def toString(): String = name + } + + val jsBinaryOps: Map[JSBinaryOp.Code, jsBinaryOp] = { + Map( + JSBinaryOp.=== -> jsBinaryOp("jsStrictEquals"), + JSBinaryOp.!== -> jsBinaryOp("jsNotStrictEquals"), + JSBinaryOp.+ -> jsBinaryOp("jsPlus"), + JSBinaryOp.- -> jsBinaryOp("jsMinus"), + JSBinaryOp.* -> jsBinaryOp("jsTimes"), + JSBinaryOp./ -> jsBinaryOp("jsDivide"), + JSBinaryOp.% -> jsBinaryOp("jsModulus"), + JSBinaryOp.| -> jsBinaryOp("jsBinaryOr"), + JSBinaryOp.& -> jsBinaryOp("jsBinaryAnd"), + JSBinaryOp.^ -> jsBinaryOp("jsBinaryXor"), + JSBinaryOp.<< -> jsBinaryOp("jsShiftLeft"), + JSBinaryOp.>> -> jsBinaryOp("jsArithmeticShiftRight"), + JSBinaryOp.>>> -> jsBinaryOp("jsLogicalShiftRight"), + JSBinaryOp.< -> jsBinaryOp("jsLessThan"), + JSBinaryOp.<= -> jsBinaryOp("jsLessEqual"), + JSBinaryOp.> -> jsBinaryOp("jsGreaterThan"), + JSBinaryOp.>= -> jsBinaryOp("jsGreaterEqual"), + JSBinaryOp.in -> jsBinaryOp("jsIn"), + JSBinaryOp.instanceof -> jsBinaryOp("jsInstanceof"), + JSBinaryOp.** -> jsBinaryOp("jsExponent") + ) + } + + case object newSymbol extends JSHelperFunctionID + case object createJSClass extends JSHelperFunctionID + case object createJSClassRest extends JSHelperFunctionID + case object installJSField extends JSHelperFunctionID + case object installJSMethod extends JSHelperFunctionID + case object installJSStaticMethod extends JSHelperFunctionID + case object installJSProperty extends JSHelperFunctionID + case object installJSStaticProperty extends JSHelperFunctionID + case object jsSuperSelect extends JSHelperFunctionID + case object jsSuperSelectSet extends JSHelperFunctionID + case object jsSuperCall extends JSHelperFunctionID + + // Wasm internal helpers + + case object createStringFromData extends FunctionID + case object stringLiteral extends FunctionID + case object typeDataName extends FunctionID + case object createClassOf extends FunctionID + case object getClassOf extends FunctionID + case object arrayTypeData extends FunctionID + case object isInstance extends FunctionID + case object isAssignableFromExternal extends FunctionID + case object isAssignableFrom extends FunctionID + case object checkCast extends FunctionID + case object getComponentType extends FunctionID + case object newArrayOfThisClass extends FunctionID + case object anyGetClass extends FunctionID + case object newArrayObject extends FunctionID + case object identityHashCode extends FunctionID + case object searchReflectiveProxy extends FunctionID + } + + object genFieldID { + final case class forClassInstanceField(name: FieldName) extends FieldID + final case class forMethodTableEntry(methodName: MethodName) extends FieldID + final case class captureParam(i: Int) extends FieldID + + object objStruct { + case object vtable extends FieldID + case object itables extends FieldID + case object arrayUnderlying extends FieldID + } + + object reflectiveProxy { + case object methodID extends FieldID + case object funcRef extends FieldID + } + + /** Fields of the typeData structs. */ + object typeData { + + /** The name data as the 3 arguments to `stringLiteral`. + * + * It is only meaningful for primitives and for classes. For array types, they are all 0, as + * array types compute their `name` from the `name` of their component type. + */ + case object nameOffset extends FieldID + + /** See `nameOffset`. */ + case object nameSize extends FieldID + + /** See `nameOffset`. */ + case object nameStringIndex extends FieldID + + /** The kind of type data, an `i32`. + * + * Possible values are the the `KindX` constants in `EmbeddedConstants`. + */ + case object kind extends FieldID + + /** A bitset of special (primitive) instance types that are instances of this type, an `i32`. + * + * From 0 to 5, the bits correspond to the values returned by the helper `jsValueType`. In + * addition, bits 6 and 7 represent `char` and `long`, respectively. + */ + case object specialInstanceTypes extends FieldID + + /** Array of the strict ancestor classes of this class. + * + * This is `null` for primitive and array types. For all other types, including JS types, it + * contains an array of the typeData of their ancestors that: + * + * - are not themselves (hence the *strict* ancestors), + * - have typeData to begin with. + */ + case object strictAncestors extends FieldID + + /** The typeData of a component of this array type, or `null` if this is not an array type. + * + * For example: + * + * - the `componentType` for class `Foo` is `null`, + * - the `componentType` for the array type `Array[Foo]` is the `typeData` of `Foo`. + */ + case object componentType extends FieldID + + /** The name as nullable string (`anyref`), lazily initialized from the nameData. + * + * This field is initialized by the `typeDataName` helper. + * + * The contents of this value is specified by `java.lang.Class.getName()`. In particular, for + * array types, it obeys the following rules: + * + * - `Array[prim]` where `prim` is a one of the primitive types with `charCode` `X` is + * `"[X"`, for example, `"[I"` for `Array[Int]`. + * - `Array[pack.Cls]` where `Cls` is a class is `"[Lpack.Cls;"`. + * - `Array[nestedArray]` where `nestedArray` is an array type with name `nested` is + * `"[nested"`, for example `"⟦I"` for `Array[Array[Int]]` and `"⟦Ljava.lang.String;"` + * for `Array[Array[String]]`.¹ + * + * ¹ We use the Unicode character `⟦` to represent two consecutive `[` characters in order + * not to confuse Scaladoc. + */ + case object name extends FieldID + + /** The `classOf` value, a nullable `java.lang.Class`, lazily initialized from this typeData. + * + * This field is initialized by the `createClassOf` helper. + */ + case object classOfValue extends FieldID + + /** The typeData/vtable of an array of this type, a nullable `typeData`, lazily initialized. + * + * This field is initialized by the `arrayTypeData` helper. + * + * For example, once initialized, + * + * - in the `typeData` of class `Foo`, it contains the `typeData` of `Array[Foo]`, + * - in the `typeData` of `Array[Int]`, it contains the `typeData` of `Array[Array[Int]]`. + */ + case object arrayOf extends FieldID + + /** The function to clone the object of this type, a nullable function reference. + * + * This field is initialized only with the classes that implement java.lang.Cloneable. + */ + case object cloneFunction extends FieldID + + /** `isInstance` func ref for top-level JS classes. */ + case object isJSClassInstance extends FieldID + + /** The reflective proxies in this type, used for reflective call on the class at runtime. + * + * This field contains an array of reflective proxy structs, where each struct contains the + * ID of the reflective proxy and a reference to the actual method implementation. Reflective + * call site should walk through the array to look up a method to call. + * + * See `genSearchReflectivePRoxy` in `HelperFunctions` + */ + case object reflectiveProxies extends FieldID + } + } + + object genTypeID { + final case class forClass(className: ClassName) extends TypeID + final case class captureData(index: Int) extends TypeID + final case class forVTable(className: ClassName) extends TypeID + final case class forITable(className: ClassName) extends TypeID + final case class forFunction(index: Int) extends TypeID + final case class forTableFunctionType(methodName: MethodName) extends TypeID + + val ObjectStruct = forClass(ObjectClass) + val ClassStruct = forClass(ClassClass) + val ThrowableStruct = forClass(ThrowableClass) + val JSExceptionStruct = forClass(SpecialNames.JSExceptionClass) + + val ObjectVTable: TypeID = forVTable(ObjectClass) + + case object typeData extends TypeID + case object reflectiveProxy extends TypeID + + // Array types -- they extend j.l.Object + case object BooleanArray extends TypeID + case object CharArray extends TypeID + case object ByteArray extends TypeID + case object ShortArray extends TypeID + case object IntArray extends TypeID + case object LongArray extends TypeID + case object FloatArray extends TypeID + case object DoubleArray extends TypeID + case object ObjectArray extends TypeID + + def forArrayClass(arrayTypeRef: ArrayTypeRef): TypeID = { + if (arrayTypeRef.dimensions > 1) { + ObjectArray + } else { + arrayTypeRef.base match { + case BooleanRef => BooleanArray + case CharRef => CharArray + case ByteRef => ByteArray + case ShortRef => ShortArray + case IntRef => IntArray + case LongRef => LongArray + case FloatRef => FloatArray + case DoubleRef => DoubleArray + case _ => ObjectArray + } + } + } + + case object typeDataArray extends TypeID + case object itables extends TypeID + case object reflectiveProxies extends TypeID + + // primitive array types, underlying the Array[T] classes + case object i8Array extends TypeID + case object i16Array extends TypeID + case object i32Array extends TypeID + case object i64Array extends TypeID + case object f32Array extends TypeID + case object f64Array extends TypeID + case object anyArray extends TypeID + + def underlyingOf(arrayTypeRef: ArrayTypeRef): TypeID = { + if (arrayTypeRef.dimensions > 1) { + anyArray + } else { + arrayTypeRef.base match { + case BooleanRef => i8Array + case CharRef => i16Array + case ByteRef => i8Array + case ShortRef => i16Array + case IntRef => i32Array + case LongRef => i64Array + case FloatRef => f32Array + case DoubleRef => f64Array + case _ => anyArray + } + } + } + + case object cloneFunctionType extends TypeID + case object isJSClassInstanceFuncType extends TypeID + } + + object genTagID { + case object exception extends TagID + } + + object genDataID { + case object string extends DataID + } + +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/WasmContext.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/WasmContext.scala new file mode 100644 index 0000000000..c6c2aee99a --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/WasmContext.scala @@ -0,0 +1,301 @@ +/* + * 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.backend.wasmemitter + +import scala.annotation.tailrec + +import scala.collection.mutable +import scala.collection.mutable.LinkedHashMap + +import org.scalajs.ir.ClassKind +import org.scalajs.ir.Names._ +import org.scalajs.ir.OriginalName.NoOriginalName +import org.scalajs.ir.Trees.{FieldDef, ParamDef, JSNativeLoadSpec} +import org.scalajs.ir.Types._ + +import org.scalajs.linker.interface.ModuleInitializer +import org.scalajs.linker.interface.unstable.ModuleInitializerImpl +import org.scalajs.linker.standard.LinkedTopLevelExport +import org.scalajs.linker.standard.LinkedClass + +import org.scalajs.linker.backend.webassembly.ModuleBuilder +import org.scalajs.linker.backend.webassembly.{Instructions => wa} +import org.scalajs.linker.backend.webassembly.{Modules => wamod} +import org.scalajs.linker.backend.webassembly.{Identitities => wanme} +import org.scalajs.linker.backend.webassembly.{Types => watpe} + +import VarGen._ +import org.scalajs.ir.OriginalName + +final class WasmContext( + classInfo: Map[ClassName, WasmContext.ClassInfo], + reflectiveProxies: Map[MethodName, Int], + val itablesLength: Int +) { + import WasmContext._ + + private val functionTypes = LinkedHashMap.empty[watpe.FunctionType, wanme.TypeID] + private val tableFunctionTypes = mutable.HashMap.empty[MethodName, wanme.TypeID] + private val closureDataTypes = LinkedHashMap.empty[List[Type], wanme.TypeID] + + val moduleBuilder: ModuleBuilder = { + new ModuleBuilder(new ModuleBuilder.FunctionTypeProvider { + def functionTypeToTypeID(sig: watpe.FunctionType): wanme.TypeID = { + functionTypes.getOrElseUpdate( + sig, { + val typeID = genTypeID.forFunction(functionTypes.size) + moduleBuilder.addRecType(typeID, NoOriginalName, sig) + typeID + } + ) + } + }) + } + + private var nextClosureDataTypeIndex: Int = 1 + + private val _funcDeclarations: mutable.LinkedHashSet[wanme.FunctionID] = + new mutable.LinkedHashSet() + + val stringPool: StringPool = new StringPool + + /** The main `rectype` containing the object model types. */ + val mainRecType: ModuleBuilder.RecTypeBuilder = new ModuleBuilder.RecTypeBuilder + + def getClassInfoOption(name: ClassName): Option[ClassInfo] = + classInfo.get(name) + + def getClassInfo(name: ClassName): ClassInfo = + classInfo.getOrElse(name, throw new Error(s"Class not found: $name")) + + def inferTypeFromTypeRef(typeRef: TypeRef): Type = typeRef match { + case PrimRef(tpe) => + tpe + case ClassRef(className) => + if (className == ObjectClass || getClassInfo(className).kind.isJSType) + AnyType + else + ClassType(className) + case typeRef: ArrayTypeRef => + ArrayType(typeRef) + } + + /** Retrieves a unique identifier for a reflective proxy with the given name. + * + * If no class defines a reflective proxy with the given name, returns `-1`. + */ + def getReflectiveProxyId(name: MethodName): Int = + reflectiveProxies.getOrElse(name, -1) + + /** Adds or reuses a function type for a table function. + * + * Table function types are part of the main `rectype`, and have names derived from the + * `methodName`. + */ + def tableFunctionType(methodName: MethodName): wanme.TypeID = { + // Project all the names with the same *signatures* onto a normalized `MethodName` + val normalizedName = MethodName( + SpecialNames.normalizedSimpleMethodName, + methodName.paramTypeRefs, + methodName.resultTypeRef, + methodName.isReflectiveProxy + ) + + tableFunctionTypes.getOrElseUpdate( + normalizedName, { + val typeID = genTypeID.forTableFunctionType(normalizedName) + val regularParamTyps = normalizedName.paramTypeRefs.map { typeRef => + TypeTransformer.transformLocalType(inferTypeFromTypeRef(typeRef))(this) + } + val resultType = TypeTransformer.transformResultType( + inferTypeFromTypeRef(normalizedName.resultTypeRef))(this) + mainRecType.addSubType( + typeID, + NoOriginalName, + watpe.FunctionType(watpe.RefType.any :: regularParamTyps, resultType) + ) + typeID + } + ) + } + + def getClosureDataStructType(captureParamTypes: List[Type]): wanme.TypeID = { + closureDataTypes.getOrElseUpdate( + captureParamTypes, { + val fields: List[watpe.StructField] = { + for ((tpe, i) <- captureParamTypes.zipWithIndex) yield { + watpe.StructField( + genFieldID.captureParam(i), + NoOriginalName, + TypeTransformer.transformLocalType(tpe)(this), + isMutable = false + ) + } + } + val structTypeID = genTypeID.captureData(nextClosureDataTypeIndex) + nextClosureDataTypeIndex += 1 + val structType = watpe.StructType(fields) + moduleBuilder.addRecType(structTypeID, NoOriginalName, structType) + structTypeID + } + ) + } + + def refFuncWithDeclaration(funcID: wanme.FunctionID): wa.RefFunc = { + _funcDeclarations += funcID + wa.RefFunc(funcID) + } + + def addGlobal(g: wamod.Global): Unit = + moduleBuilder.addGlobal(g) + + def getAllFuncDeclarations(): List[wanme.FunctionID] = + _funcDeclarations.toList +} + +object WasmContext { + final class ClassInfo( + val name: ClassName, + val kind: ClassKind, + val jsClassCaptures: Option[List[ParamDef]], + val allFieldDefs: List[FieldDef], + superClass: Option[ClassInfo], + val classImplementsAnyInterface: Boolean, + val hasInstances: Boolean, + val isAbstract: Boolean, + val hasRuntimeTypeInfo: Boolean, + val jsNativeLoadSpec: Option[JSNativeLoadSpec], + val jsNativeMembers: Map[MethodName, JSNativeLoadSpec], + val staticFieldMirrors: Map[FieldName, List[String]], + _specialInstanceTypes: Int, // should be `val` but there is a large Scaladoc for it below + val resolvedMethodInfos: Map[MethodName, ConcreteMethodInfo], + _itableIdx: Int + ) { + override def toString(): String = + s"ClassInfo(${name.nameString})" + + /** For a class or interface, its table entries in definition order. */ + private var _tableEntries: List[MethodName] = null + + /** Returns the index of this interface's itable in the classes' interface tables. + * + * Only interfaces that have instances get an itable index. + */ + def itableIdx: Int = { + if (_itableIdx < 0) { + val isInterface = kind == ClassKind.Interface + if (isInterface && hasInstances) { + // it should have received an itable idx + throw new IllegalStateException( + s"$this was not assigned an itable index although it needs one.") + } else { + throw new IllegalArgumentException( + s"Trying to ask the itable idx for $this, which is not supposed to have one " + + s"(isInterface = $isInterface; hasInstances = $hasInstances).") + } + } + _itableIdx + } + + /** A bitset of the `jsValueType`s corresponding to hijacked classes that extend this class. + * + * This value is used for instance tests against this class. A JS value `x` is an instance of + * this type iff `jsValueType(x)` is a member of this bitset. Because of how a bitset works, + * this means testing the following formula: + * + * {{{ + * ((1 << jsValueType(x)) & specialInstanceTypes) != 0 + * }}} + * + * For example, if this class is `Comparable`, we want the bitset to contain the values for + * `boolean`, `string` and `number` (but not `undefined`), because `jl.Boolean`, `jl.String` + * and `jl.Double` implement `Comparable`. + * + * This field is initialized with 0, and augmented during preprocessing by calls to + * `addSpecialInstanceType`. + * + * This technique is used both for static `isInstanceOf` tests as well as reflective tests + * through `Class.isInstance`. For the latter, this value is stored in + * `typeData.specialInstanceTypes`. For the former, it is embedded as a constant in the + * generated code. + * + * See the `isInstance` and `genInstanceTest` helpers. + * + * Special cases: this value remains 0 for all the numeric hijacked classes except `jl.Double`, + * since `jsValueType(x) == JSValueTypeNumber` is not enough to deduce that + * `x.isInstanceOf[Int]`, for example. + */ + val specialInstanceTypes: Int = _specialInstanceTypes + + /** Is this class an ancestor of any hijacked class? + * + * This includes but is not limited to the hijacked classes themselves, as well as `jl.Object`. + */ + def isAncestorOfHijackedClass: Boolean = + specialInstanceTypes != 0 || kind == ClassKind.HijackedClass + + def isInterface: Boolean = + kind == ClassKind.Interface + + def buildMethodTable(methodsCalledDynamically0: Set[MethodName]): Unit = { + if (_tableEntries != null) + throw new IllegalStateException(s"Duplicate call to buildMethodTable() for $name") + + val methodsCalledDynamically: List[MethodName] = + if (hasInstances) methodsCalledDynamically0.toList + else Nil + + kind match { + case ClassKind.Class | ClassKind.ModuleClass | ClassKind.HijackedClass => + val superTableEntries = superClass.fold[List[MethodName]](Nil)(_.tableEntries) + val superTableEntrySet = superTableEntries.toSet + + /* When computing the table entries to add for this class, exclude: + * - methods that are already in the super class' table entries, and + * - methods that are effectively final, since they will always be + * statically resolved instead of using the table dispatch. + */ + val newTableEntries = methodsCalledDynamically + .filter(!superTableEntrySet.contains(_)) + .filterNot(m => resolvedMethodInfos.get(m).exists(_.isEffectivelyFinal)) + .sorted // for stability + + _tableEntries = superTableEntries ::: newTableEntries + + case ClassKind.Interface => + _tableEntries = methodsCalledDynamically.sorted // for stability + + case _ => + _tableEntries = Nil + } + } + + def tableEntries: List[MethodName] = { + if (_tableEntries == null) + throw new IllegalStateException(s"Table not yet built for $name") + _tableEntries + } + } + + final class ConcreteMethodInfo(val ownerClass: ClassName, val methodName: MethodName) { + val tableEntryID = genFunctionID.forTableEntry(ownerClass, methodName) + + private var effectivelyFinal: Boolean = true + + /** For use by `Preprocessor`. */ + private[wasmemitter] def markOverridden(): Unit = + effectivelyFinal = false + + def isEffectivelyFinal: Boolean = effectivelyFinal + } +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/BinaryWriter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/BinaryWriter.scala new file mode 100644 index 0000000000..1c79f6daea --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/BinaryWriter.scala @@ -0,0 +1,667 @@ +/* + * 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.backend.webassembly + +import scala.annotation.tailrec + +import java.nio.{ByteBuffer, ByteOrder} + +import org.scalajs.ir.{Position, UTF8String} +import org.scalajs.linker.backend.javascript.SourceMapWriter + +import Instructions._ +import Identitities._ +import Modules._ +import Types._ + +private sealed class BinaryWriter(module: Module, emitDebugInfo: Boolean) { + import BinaryWriter._ + + /** The big output buffer. */ + private[BinaryWriter] val buf = new Buffer() + + private val typeIdxValues: Map[TypeID, Int] = + module.types.flatMap(_.subTypes).map(_.id).zipWithIndex.toMap + + private val dataIdxValues: Map[DataID, Int] = + module.datas.map(_.id).zipWithIndex.toMap + + private val funcIdxValues: Map[FunctionID, Int] = { + val importedFunctionIDs = module.imports.collect { + case Import(_, _, ImportDesc.Func(id, _, _)) => id + } + val allIDs = importedFunctionIDs ::: module.funcs.map(_.id) + allIDs.zipWithIndex.toMap + } + + private val tagIdxValues: Map[TagID, Int] = { + val importedTagIDs = module.imports.collect { case Import(_, _, ImportDesc.Tag(id, _, _)) => + id + } + val allIDs = importedTagIDs ::: module.tags.map(_.id) + allIDs.zipWithIndex.toMap + } + + private val globalIdxValues: Map[GlobalID, Int] = { + val importedGlobalIDs = module.imports.collect { + case Import(_, _, ImportDesc.Global(id, _, _, _)) => id + } + val allIDs = importedGlobalIDs ::: module.globals.map(_.id) + allIDs.zipWithIndex.toMap + } + + private val fieldIdxValues: Map[TypeID, Map[FieldID, Int]] = { + (for { + recType <- module.types + SubType(typeID, _, _, _, StructType(fields)) <- recType.subTypes + } yield { + typeID -> fields.map(_.id).zipWithIndex.toMap + }).toMap + } + + private var localIdxValues: Option[Map[LocalID, Int]] = None + + /** A stack of the labels in scope (innermost labels are on top of the stack). */ + private var labelsInScope: List[Option[LabelID]] = Nil + + private def withLocalIdxValues(values: Map[LocalID, Int])(f: => Unit): Unit = { + val saved = localIdxValues + localIdxValues = Some(values) + try f + finally localIdxValues = saved + } + + protected def emitStartFuncPosition(pos: Position): Unit = () + protected def emitPosition(pos: Position): Unit = () + protected def emitEndFuncPosition(): Unit = () + protected def emitSourceMapSection(): Unit = () + + def write(): ByteBuffer = { + // magic header: null char + "asm" + buf.byte(0) + buf.byte('a') + buf.byte('s') + buf.byte('m') + + // version + buf.byte(1) + buf.byte(0) + buf.byte(0) + buf.byte(0) + + writeSection(SectionType)(writeTypeSection()) + writeSection(SectionImport)(writeImportSection()) + writeSection(SectionFunction)(writeFunctionSection()) + writeSection(SectionTag)(writeTagSection()) + writeSection(SectionGlobal)(writeGlobalSection()) + writeSection(SectionExport)(writeExportSection()) + if (module.start.isDefined) + writeSection(SectionStart)(writeStartSection()) + writeSection(SectionElement)(writeElementSection()) + if (module.datas.nonEmpty) + writeSection(SectionDataCount)(writeDataCountSection()) + writeSection(SectionCode)(writeCodeSection()) + writeSection(SectionData)(writeDataSection()) + + if (emitDebugInfo) + writeCustomSection("name")(writeNameCustomSection()) + + emitSourceMapSection() + + buf.result() + } + + private def writeSection(sectionID: Byte)(sectionContent: => Unit): Unit = { + buf.byte(sectionID) + buf.byteLengthSubSection(sectionContent) + } + + protected final def writeCustomSection(customSectionName: String)( + sectionContent: => Unit): Unit = { + writeSection(SectionCustom) { + buf.name(customSectionName) + sectionContent + } + } + + private def writeTypeSection(): Unit = { + buf.vec(module.types) { recType => + recType.subTypes match { + case singleSubType :: Nil => + writeSubType(singleSubType) + case subTypes => + buf.byte(0x4E) // `rectype` + buf.vec(subTypes)(writeSubType(_)) + } + } + } + + private def writeSubType(subType: SubType): Unit = { + subType match { + case SubType(_, _, true, None, compositeType) => + writeCompositeType(compositeType) + case _ => + buf.byte(if (subType.isFinal) 0x4F else 0x50) + buf.opt(subType.superType)(writeTypeIdx(_)) + writeCompositeType(subType.compositeType) + } + } + + private def writeCompositeType(compositeType: CompositeType): Unit = { + def writeFieldType(fieldType: FieldType): Unit = { + writeType(fieldType.tpe) + buf.boolean(fieldType.isMutable) + } + + compositeType match { + case ArrayType(fieldType) => + buf.byte(0x5E) // array + writeFieldType(fieldType) + case StructType(fields) => + buf.byte(0x5F) // struct + buf.vec(fields)(field => writeFieldType(field.fieldType)) + case FunctionType(params, results) => + buf.byte(0x60) // func + writeResultType(params) + writeResultType(results) + } + } + + private def writeImportSection(): Unit = { + buf.vec(module.imports) { imprt => + buf.name(imprt.module) + buf.name(imprt.name) + + imprt.desc match { + case ImportDesc.Func(_, _, typeID) => + buf.byte(0x00) // func + writeTypeIdx(typeID) + case ImportDesc.Global(_, _, isMutable, tpe) => + buf.byte(0x03) // global + writeType(tpe) + buf.boolean(isMutable) + case ImportDesc.Tag(_, _, typeID) => + buf.byte(0x04) // tag + buf.byte(0x00) // exception kind (that is the only valid kind for now) + writeTypeIdx(typeID) + } + } + } + + private def writeFunctionSection(): Unit = { + buf.vec(module.funcs) { fun => + writeTypeIdx(fun.typeID) + } + } + + private def writeTagSection(): Unit = { + buf.vec(module.tags) { tag => + buf.byte(0x00) // exception kind (that is the only valid kind for now) + writeTypeIdx(tag.typeID) + } + } + + private def writeGlobalSection(): Unit = { + buf.vec(module.globals) { global => + writeType(global.tpe) + buf.boolean(global.isMutable) + writeExpr(global.init) + } + } + + private def writeExportSection(): Unit = { + buf.vec(module.exports) { exp => + buf.name(exp.name) + exp.desc match { + case ExportDesc.Func(id) => + buf.byte(0x00) + writeFuncIdx(id) + case ExportDesc.Global(id) => + buf.byte(0x03) + writeGlobalIdx(id) + } + } + } + + private def writeStartSection(): Unit = { + writeFuncIdx(module.start.get) + } + + private def writeElementSection(): Unit = { + buf.vec(module.elems) { element => + element.mode match { + case Element.Mode.Declarative => buf.u32(7) + } + writeType(element.tpe) + buf.vec(element.init) { expr => + writeExpr(expr) + } + } + } + + private def writeDataSection(): Unit = { + buf.vec(module.datas) { data => + data.mode match { + case Data.Mode.Passive => buf.u32(1) + } + buf.vec(data.bytes)(buf.byte) + } + } + + private def writeDataCountSection(): Unit = + buf.u32(module.datas.size) + + private def writeCodeSection(): Unit = { + buf.vec(module.funcs) { func => + buf.byteLengthSubSection(writeFunc(func)) + } + } + + private def writeNameCustomSection(): Unit = { + // Currently, we only emit the function names + + val importFunctionNames = module.imports.collect { + case Import(_, _, ImportDesc.Func(id, origName, _)) if origName.isDefined => + id -> origName + } + val definedFunctionNames = + module.funcs.filter(_.originalName.isDefined).map(f => f.id -> f.originalName) + val allFunctionNames = importFunctionNames ::: definedFunctionNames + + buf.byte(0x01) // function names + buf.byteLengthSubSection { + buf.vec(allFunctionNames) { elem => + writeFuncIdx(elem._1) + buf.name(elem._2.get) + } + } + } + + private def writeFunc(func: Function): Unit = { + emitStartFuncPosition(func.pos) + + buf.vec(func.locals) { local => + buf.u32(1) + writeType(local.tpe) + } + + withLocalIdxValues((func.params ::: func.locals).map(_.id).zipWithIndex.toMap) { + writeExpr(func.body) + } + + emitEndFuncPosition() + } + + private def writeType(tpe: StorageType): Unit = { + tpe match { + case tpe: SimpleType => buf.byte(tpe.binaryCode) + case tpe: PackedType => buf.byte(tpe.binaryCode) + + case RefType(true, heapType: HeapType.AbsHeapType) => + buf.byte(heapType.binaryCode) + + case RefType(nullable, heapType) => + buf.byte(if (nullable) 0x63 else 0x64) + writeHeapType(heapType) + } + } + + private def writeHeapType(heapType: HeapType): Unit = { + heapType match { + case HeapType.Type(typeID) => writeTypeIdxs33(typeID) + case heapType: HeapType.AbsHeapType => buf.byte(heapType.binaryCode) + } + } + + private def writeResultType(resultType: List[Type]): Unit = + buf.vec(resultType)(writeType(_)) + + private def writeTypeIdx(typeID: TypeID): Unit = + buf.u32(typeIdxValues(typeID)) + + private def writeFieldIdx(typeID: TypeID, fieldID: FieldID): Unit = + buf.u32(fieldIdxValues(typeID)(fieldID)) + + private def writeDataIdx(dataID: DataID): Unit = + buf.u32(dataIdxValues(dataID)) + + private def writeTypeIdxs33(typeID: TypeID): Unit = + buf.s33OfUInt(typeIdxValues(typeID)) + + private def writeFuncIdx(funcID: FunctionID): Unit = + buf.u32(funcIdxValues(funcID)) + + private def writeTagIdx(tagID: TagID): Unit = + buf.u32(tagIdxValues(tagID)) + + private def writeGlobalIdx(globalID: GlobalID): Unit = + buf.u32(globalIdxValues(globalID)) + + private def writeLocalIdx(localID: LocalID): Unit = { + localIdxValues match { + case Some(values) => buf.u32(values(localID)) + case None => throw new IllegalStateException("Local name table is not available") + } + } + + private def writeLabelIdx(labelID: LabelID): Unit = { + val relativeNumber = labelsInScope.indexOf(Some(labelID)) + if (relativeNumber < 0) + throw new IllegalStateException(s"Cannot find $labelID in scope") + buf.u32(relativeNumber) + } + + private def writeExpr(expr: Expr): Unit = { + for (instr <- expr.instr) + writeInstr(instr) + buf.byte(0x0B) // end + } + + private def writeInstr(instr: Instr): Unit = { + instr match { + case PositionMark(pos) => + emitPosition(pos) + + case _ => + val opcode = instr.opcode + if (opcode <= 0xFF) { + buf.byte(opcode.toByte) + } else { + assert(opcode <= 0xFFFF, + s"cannot encode an opcode longer than 2 bytes yet: ${opcode.toHexString}") + buf.byte((opcode >>> 8).toByte) + buf.byte(opcode.toByte) + } + + writeInstrImmediates(instr) + + instr match { + case instr: StructuredLabeledInstr => + // We must register even the `None` labels, because they contribute to relative numbering + labelsInScope ::= instr.label + case End => + labelsInScope = labelsInScope.tail + case _ => + () + } + } + } + + private def writeInstrImmediates(instr: Instr): Unit = { + def writeBrOnCast(labelIdx: LabelID, from: RefType, to: RefType): Unit = { + val castFlags = ((if (from.nullable) 1 else 0) | (if (to.nullable) 2 else 0)).toByte + buf.byte(castFlags) + writeLabelIdx(labelIdx) + writeHeapType(from.heapType) + writeHeapType(to.heapType) + } + + instr match { + // Convenience categories + + case instr: SimpleInstr => + () + case instr: BlockTypeLabeledInstr => + writeBlockType(instr.blockTypeArgument) + case instr: LabelInstr => + writeLabelIdx(instr.labelArgument) + case instr: FuncInstr => + writeFuncIdx(instr.funcArgument) + case instr: TypeInstr => + writeTypeIdx(instr.typeArgument) + case instr: TagInstr => + writeTagIdx(instr.tagArgument) + case instr: LocalInstr => + writeLocalIdx(instr.localArgument) + case instr: GlobalInstr => + writeGlobalIdx(instr.globalArgument) + case instr: HeapTypeInstr => + writeHeapType(instr.heapTypeArgument) + case instr: RefTypeInstr => + writeHeapType(instr.refTypeArgument.heapType) + case instr: StructFieldInstr => + writeTypeIdx(instr.structTypeID) + writeFieldIdx(instr.structTypeID, instr.fieldID) + + // Specific instructions with unique-ish shapes + + case I32Const(v) => buf.i32(v) + case I64Const(v) => buf.i64(v) + case F32Const(v) => buf.f32(v) + case F64Const(v) => buf.f64(v) + + case BrTable(labelIdxVector, defaultLabelIdx) => + buf.vec(labelIdxVector)(writeLabelIdx(_)) + writeLabelIdx(defaultLabelIdx) + + case TryTable(blockType, clauses, _) => + writeBlockType(blockType) + buf.vec(clauses)(writeCatchClause(_)) + + case ArrayNewData(typeIdx, dataIdx) => + writeTypeIdx(typeIdx) + writeDataIdx(dataIdx) + + case ArrayNewFixed(typeIdx, length) => + writeTypeIdx(typeIdx) + buf.u32(length) + + case ArrayCopy(destType, srcType) => + writeTypeIdx(destType) + writeTypeIdx(srcType) + + case BrOnCast(labelIdx, from, to) => + writeBrOnCast(labelIdx, from, to) + case BrOnCastFail(labelIdx, from, to) => + writeBrOnCast(labelIdx, from, to) + + case PositionMark(pos) => + throw new AssertionError(s"Unexpected $instr") + } + } + + private def writeCatchClause(clause: CatchClause): Unit = { + buf.byte(clause.opcode.toByte) + clause.tag.foreach(tag => writeTagIdx(tag)) + writeLabelIdx(clause.label) + } + + private def writeBlockType(blockType: BlockType): Unit = { + blockType match { + case BlockType.ValueType(None) => buf.byte(0x40) + case BlockType.ValueType(Some(tpe)) => writeType(tpe) + case BlockType.FunctionType(typeID) => writeTypeIdxs33(typeID) + } + } +} + +object BinaryWriter { + private final val SectionCustom = 0x00 + private final val SectionType = 0x01 + private final val SectionImport = 0x02 + private final val SectionFunction = 0x03 + private final val SectionTable = 0x04 + private final val SectionMemory = 0x05 + private final val SectionGlobal = 0x06 + private final val SectionExport = 0x07 + private final val SectionStart = 0x08 + private final val SectionElement = 0x09 + private final val SectionCode = 0x0A + private final val SectionData = 0x0B + private final val SectionDataCount = 0x0C + private final val SectionTag = 0x0D + + def write(module: Module, emitDebugInfo: Boolean): ByteBuffer = + new BinaryWriter(module, emitDebugInfo).write() + + def writeWithSourceMap(module: Module, emitDebugInfo: Boolean, + sourceMapWriter: SourceMapWriter, sourceMapURI: String): ByteBuffer = { + new WithSourceMap(module, emitDebugInfo, sourceMapWriter, sourceMapURI).write() + } + + private[BinaryWriter] final class Buffer { + private var buf: ByteBuffer = + ByteBuffer.allocate(1024 * 1024).order(ByteOrder.LITTLE_ENDIAN) + + private def ensureRemaining(requiredRemaining: Int): Unit = { + if (buf.remaining() < requiredRemaining) { + buf.flip() + val newCapacity = Integer.highestOneBit(buf.capacity() + requiredRemaining) << 1 + val newBuf = ByteBuffer.allocate(newCapacity).order(ByteOrder.LITTLE_ENDIAN) + newBuf.put(buf) + buf = newBuf + } + } + + def currentGlobalOffset: Int = buf.position() + + def result(): ByteBuffer = { + buf.flip() + buf + } + + def byte(b: Byte): Unit = { + ensureRemaining(1) + buf.put(b) + } + + def rawByteArray(array: Array[Byte]): Unit = { + ensureRemaining(array.length) + buf.put(array) + } + + def boolean(b: Boolean): Unit = + byte(if (b) 1 else 0) + + def u32(value: Int): Unit = unsignedLEB128(Integer.toUnsignedLong(value)) + + def s32(value: Int): Unit = signedLEB128(value.toLong) + + def i32(value: Int): Unit = s32(value) + + def s33OfUInt(value: Int): Unit = signedLEB128(Integer.toUnsignedLong(value)) + + def u64(value: Long): Unit = unsignedLEB128(value) + + def s64(value: Long): Unit = signedLEB128(value) + + def i64(value: Long): Unit = s64(value) + + def f32(value: Float): Unit = { + ensureRemaining(4) + buf.putFloat(value) + } + + def f64(value: Double): Unit = { + ensureRemaining(8) + buf.putDouble(value) + } + + def vec[A](elems: Iterable[A])(op: A => Unit): Unit = { + u32(elems.size) + for (elem <- elems) + op(elem) + } + + def opt[A](elemOpt: Option[A])(op: A => Unit): Unit = + vec(elemOpt.toList)(op) + + def name(s: String): Unit = + name(UTF8String(s)) + + def name(utf8: UTF8String): Unit = { + val len = utf8.length + u32(len) + ensureRemaining(len) + utf8.writeTo(buf) + } + + def byteLengthSubSection(subSectionContent: => Unit): Unit = { + // Reserve 4 bytes at the current offset to store the byteLength later + val byteLengthOffset = buf.position() + ensureRemaining(4) + val startOffset = buf.position() + 4 + buf.position(startOffset) // do not write the 4 bytes for now + + subSectionContent + + // Compute byteLength + val endOffset = buf.position() + val byteLength = endOffset - startOffset + + /* Because we limited ourselves to 4 bytes, we cannot represent a size + * greater than 2^(4*7). + */ + assert(byteLength < (1 << 28), + s"Implementation restriction: Cannot write a subsection that large: $byteLength") + + /* Write the byteLength in the reserved slot. Note that we *always* use + * 4 bytes to store the byteLength, even when less bytes are necessary in + * the unsigned LEB encoding. The WebAssembly spec specifically calls out + * this choice as valid. We leverage it to have predictable total offsets + * when we write the code section, which is important to efficiently + * generate source maps. + */ + buf.put(byteLengthOffset, ((byteLength & 0x7F) | 0x80).toByte) + buf.put(byteLengthOffset + 1, (((byteLength >>> 7) & 0x7F) | 0x80).toByte) + buf.put(byteLengthOffset + 2, (((byteLength >>> 14) & 0x7F) | 0x80).toByte) + buf.put(byteLengthOffset + 3, ((byteLength >>> 21) & 0x7F).toByte) + } + + @tailrec + private def unsignedLEB128(value: Long): Unit = { + val next = value >>> 7 + if (next == 0) { + byte(value.toByte) + } else { + byte(((value.toInt & 0x7F) | 0x80).toByte) + unsignedLEB128(next) + } + } + + @tailrec + private def signedLEB128(value: Long): Unit = { + val chunk = value.toInt & 0x7F + val next = value >> 7 + if (next == (if ((chunk & 0x40) != 0) -1 else 0)) { + byte(chunk.toByte) + } else { + byte((chunk | 0x80).toByte) + signedLEB128(next) + } + } + } + + private final class WithSourceMap(module: Module, emitDebugInfo: Boolean, + sourceMapWriter: SourceMapWriter, sourceMapURI: String) + extends BinaryWriter(module, emitDebugInfo) { + + override protected def emitStartFuncPosition(pos: Position): Unit = + sourceMapWriter.startNode(buf.currentGlobalOffset, pos) + + override protected def emitPosition(pos: Position): Unit = { + sourceMapWriter.endNode(buf.currentGlobalOffset) + sourceMapWriter.startNode(buf.currentGlobalOffset, pos) + } + + override protected def emitEndFuncPosition(): Unit = + sourceMapWriter.endNode(buf.currentGlobalOffset) + + override protected def emitSourceMapSection(): Unit = { + // See https://github.com/WebAssembly/tool-conventions/blob/main/Debugging.md#source-maps + writeCustomSection("sourceMappingURL") { + buf.name(sourceMapURI) + } + } + } +} diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/FunctionBuilder.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/FunctionBuilder.scala new file mode 100644 index 0000000000..fff0a74acd --- /dev/null +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/FunctionBuilder.scala @@ -0,0 +1,445 @@ +/* + * 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.backend.webassembly + +import scala.collection.mutable + +import org.scalajs.ir.{OriginalName, Position} + +import Instructions._ +import Identitities._ +import Modules._ +import Types._ + +final class FunctionBuilder( + moduleBuilder: ModuleBuilder, + val functionID: FunctionID, + val functionOriginalName: OriginalName, + functionPos: Position +) { + import FunctionBuilder._ + + private var labelIdx = 0 + + private val params = mutable.ListBuffer.empty[Local] + private val locals = mutable.ListBuffer.empty[Local] + private var resultTypes: List[Type] = Nil + + private var specialFunctionType: Option[TypeID] = None + + /** The instructions buffer. */ + private val instrs: mutable.ListBuffer[Instr] = mutable.ListBuffer.empty + + // Signature building + + /** Adds one parameter to the function with the given orignal name and type. + * + * Returns the `LocalID` of the new parameter. + * + * @note + * This follows a builder pattern to easily and safely correlate the + * definition of a parameter and extracting its `LocalID`. + */ + def addParam(originalName: OriginalName, tpe: Type): LocalID = { + val id = new ParamIDImpl(params.size, originalName) + params += Local(id, originalName, tpe) + id + } + + /** Adds one parameter to the function with the given orignal name and type. + * + * Returns the `LocalID` of the new parameter. + * + * @note + * This follows a builder pattern to easily and safely correlate the + * definition of a parameter and extracting its `LocalID`. + */ + def addParam(name: String, tpe: Type): LocalID = + addParam(OriginalName(name), tpe) + + /** Sets the list of result types of the function to build. + * + * By default, the list of result types is `Nil`. + * + * @note + * This follows a builder pattern to be consistent with `addParam`. + */ + def setResultTypes(tpes: List[Type]): Unit = + resultTypes = tpes + + /** Sets the list of result types to a single type. + * + * This method is equivalent to + * {{{ + * setResultTypes(tpe :: Nil) + * }}} + * + * @note + * This follows a builder pattern to be consistent with `addParam`. + */ + def setResultType(tpe: Type): Unit = + setResultTypes(tpe :: Nil) + + /** Specifies the function type to use for the function. + * + * If this method is not called, a default function type will be + * automatically generated. Generated function types are always alone in a + * recursive type group, without supertype, and final. + * + * Use `setFunctionType` if the function must conform to a specific function + * type, such as one that is defined within a recursive type group, or that + * is a subtype of other function types. + * + * The given function type must be consistent with the params created with + * `addParam` and with the result types specified by `setResultType(s)`. + * Using `setFunctionType` does not implicitly set any result type or create + * any parameter (it cannot, since it cannot *resolve* the `typeID` to a + * `FunctionType`). + */ + def setFunctionType(typeID: TypeID): Unit = + specialFunctionType = Some(typeID) + + // Local definitions + + def genLabel(): LabelID = { + val label = new LabelIDImpl(labelIdx) + labelIdx += 1 + label + } + + def addLocal(originalName: OriginalName, tpe: Type): LocalID = { + val id = new LocalIDImpl(locals.size, originalName) + locals += Local(id, originalName, tpe) + id + } + + def addLocal(name: String, tpe: Type): LocalID = + addLocal(OriginalName(name), tpe) + + // Instructions + + def +=(instr: Instr): Unit = + instrs += instr + + def ++=(instrs: Iterable[Instr]): Unit = + this.instrs ++= instrs + + def markCurrentInstructionIndex(): InstructionIndex = + new InstructionIndex(instrs.size) + + def insert(index: InstructionIndex, instr: Instr): Unit = + instrs.insert(index.value, instr) + + // Helpers to build structured control flow + + def sigToBlockType(sig: FunctionType): BlockType = sig match { + case FunctionType(Nil, Nil) => + BlockType.ValueType() + case FunctionType(Nil, resultType :: Nil) => + BlockType.ValueType(resultType) + case _ => + BlockType.FunctionType(moduleBuilder.functionTypeToTypeID(sig)) + } + + private def toBlockType[BT: BlockTypeLike](blockType: BT): BlockType = + implicitly[BlockTypeLike[BT]].toBlockType(this, blockType) + + /* Work around a bug in the Scala compiler. + * + * We force it to see `ForResultTypes` here, so that it actually typechecks + * it and realizes that it is a valid implicit instance of + * `BlockTypeLike[ForResultTypes]`. I guess this is because it appears later + * in the same file. + * + * If we remove this line, the invocations with `()` in this file, which + * desugar to `(Nil)` due to the default value, do not find the implicit value. + */ + BlockTypeLike.ForResultTypes + + def ifThenElse[BT: BlockTypeLike](blockType: BT = Nil)(thenp: => Unit)(elsep: => Unit): Unit = { + instrs += If(toBlockType(blockType)) + thenp + instrs += Else + elsep + instrs += End + } + + def ifThen[BT: BlockTypeLike](blockType: BT = Nil)(thenp: => Unit): Unit = { + instrs += If(toBlockType(blockType)) + thenp + instrs += End + } + + def block[BT: BlockTypeLike, A](blockType: BT = Nil)(body: LabelID => A): A = { + val label = genLabel() + instrs += Block(toBlockType(blockType), Some(label)) + val result = body(label) + instrs += End + result + } + + def loop[BT: BlockTypeLike, A](blockType: BT = Nil)(body: LabelID => A): A = { + val label = genLabel() + instrs += Loop(toBlockType(blockType), Some(label)) + val result = body(label) + instrs += End + result + } + + def whileLoop()(cond: => Unit)(body: => Unit): Unit = { + loop() { loopLabel => + cond + ifThen() { + body + instrs += Br(loopLabel) + } + } + } + + def tryTable[BT: BlockTypeLike, A](blockType: BT = Nil)( + clauses: List[CatchClause])(body: => A): A = { + instrs += TryTable(toBlockType(blockType), clauses) + val result = body + instrs += End + result + } + + /** Builds a `switch` over a scrutinee using a `br_table` instruction. + * + * This function produces code that encodes the following control-flow: + * + * {{{ + * switch (scrutinee) { + * case clause0_alt0 | ... | clause0_altN => clause0_body + * ... + * case clauseM_alt0 | ... | clauseM_altN => clauseM_body + * case _ => default + * } + * }}} + * + * All the alternative values must be non-negative and distinct, but they need not be + * consecutive. The highest one must be strictly smaller than 128, as a safety precaution against + * generating unexpectedly large tables. + * + * @param scrutineeSig + * The signature of the `scrutinee` block, *excluding* the i32 result that will be switched + * over. + * @param clauseSig + * The signature of every `clauseI_body` block and of the `default` block. The clauses' params + * must consume at least all the results of the scrutinee. + */ + def switch(scrutineeSig: FunctionType, clauseSig: FunctionType)( + scrutinee: () => Unit)( + clauses: (List[Int], () => Unit)*)( + default: () => Unit): Unit = { + + // Check prerequisites + + require(clauseSig.params.size >= scrutineeSig.results.size, + "The clauses of a switch must consume all the results of the scrutinee " + + s"(scrutinee results: ${scrutineeSig.results}; clause params: ${clauseSig.params})") + + val numCases = clauses.map(_._1.max).max + 1 + require(numCases <= 128, s"Too many cases for switch: $numCases") + + // Allocate all the labels we will use + val doneLabel = genLabel() + val defaultLabel = genLabel() + val clauseLabels = clauses.map(_ => genLabel()) + + // Build the dispatch vector, i.e., the array of caseValue -> target clauseLabel + val dispatchVector = { + val dv = Array.fill(numCases)(defaultLabel) + for { + ((caseValues, _), clauseLabel) <- clauses.zip(clauseLabels) + caseValue <- caseValues + } { + require(dv(caseValue) == defaultLabel, s"Duplicate case value for switch: $caseValue") + dv(caseValue) = clauseLabel + } + dv.toList + } + + // Input parameter to the overall switch "instruction" + val switchInputParams = + clauseSig.params.drop(scrutineeSig.results.size) ::: scrutineeSig.params + + // Compute the BlockType's we will need + val doneBlockType = sigToBlockType(FunctionType(switchInputParams, clauseSig.results)) + val clauseBlockType = sigToBlockType(FunctionType(switchInputParams, clauseSig.params)) + + // Open done block + instrs += Block(doneBlockType, Some(doneLabel)) + // Open case and default blocks (in reverse order: default block is outermost!) + for (label <- (defaultLabel +: clauseLabels.reverse)) { + instrs += Block(clauseBlockType, Some(label)) + } + + // Load the scrutinee and dispatch + scrutinee() + instrs += BrTable(dispatchVector, defaultLabel) + + // Close all the case blocks and emit their respective bodies + for ((_, caseBody) <- clauses) { + instrs += End // close the block whose label is the corresponding label for this clause + caseBody() // emit the body of that clause + instrs += Br(doneLabel) // jump to done + } + + // Close the default block and emit its body (no jump to done necessary) + instrs += End + default() + + instrs += End // close the done block + } + + def switch(clauseSig: FunctionType)(scrutinee: () => Unit)( + clauses: (List[Int], () => Unit)*)(default: () => Unit): Unit = { + switch(FunctionType.NilToNil, clauseSig)(scrutinee)(clauses: _*)(default) + } + + def switch(resultType: Type)(scrutinee: () => Unit)( + clauses: (List[Int], () => Unit)*)(default: () => Unit): Unit = { + switch(FunctionType(Nil, List(resultType)))(scrutinee)(clauses: _*)(default) + } + + def switch()(scrutinee: () => Unit)( + clauses: (List[Int], () => Unit)*)(default: () => Unit): Unit = { + switch(FunctionType.NilToNil)(scrutinee)(clauses: _*)(default) + } + + // Final result + + def buildAndAddToModule(): Function = { + val functionTypeID = specialFunctionType.getOrElse { + val sig = FunctionType(params.toList.map(_.tpe), resultTypes) + moduleBuilder.functionTypeToTypeID(sig) + } + + val dcedInstrs = localDeadCodeEliminationOfInstrs() + + val func = Function( + functionID, + functionOriginalName, + functionTypeID, + params.toList, + resultTypes, + locals.toList, + Expr(dcedInstrs), + functionPos + ) + moduleBuilder.addFunction(func) + func + } + + /** Performs local dead code elimination and produces the final list of instructions. + * + * After a stack-polymorphic instruction, the rest of the block is unreachable. In theory, + * WebAssembly specifies that the rest of the block should be type-checkeable no matter the + * contents of the stack. In practice, however, it seems V8 cannot handle `throw_ref` in such a + * context. It reports a validation error of the form "invalid type for throw_ref: expected + * exnref, found ". + * + * We work around this issue by forcing a pass of local dead-code elimination. This is in fact + * straightforwrd: after every stack-polymorphic instruction, ignore all instructions until the + * next `Else` or `End`. The only tricky bit is that if we encounter nested + * `StructuredLabeledInstr`s during that process, must jump over them. That means we need to + * track the level of nesting at which we are. + */ + private def localDeadCodeEliminationOfInstrs(): List[Instr] = { + val resultBuilder = List.newBuilder[Instr] + + val iter = instrs.iterator + while (iter.hasNext) { + // Emit the current instruction + val instr = iter.next() + resultBuilder += instr + + /* If it is a stack-polymorphic instruction, dead-code eliminate until the + * end of the current block. + */ + if (instr.isInstanceOf[StackPolymorphicInstr]) { + var nestingLevel = 0 + + while (nestingLevel >= 0 && iter.hasNext) { + val deadCodeInstr = iter.next() + deadCodeInstr match { + case End | Else | _: Catch if nestingLevel == 0 => + /* We have reached the end of the original block of dead code. + * Actually emit this END or ELSE and then drop `nestingLevel` + * below 0 to end the dead code processing loop. + */ + resultBuilder += deadCodeInstr + nestingLevel = -1 // acts as a `break` instruction + + case End => + nestingLevel -= 1 + + case _: StructuredLabeledInstr => + nestingLevel += 1 + + case _ => + () + } + } + } + } + + resultBuilder.result() + } +} + +object FunctionBuilder { + private final class ParamIDImpl(index: Int, originalName: OriginalName) extends LocalID { + override def toString(): String = + if (originalName.isDefined) originalName.get.toString() + else s"" + } + + private final class LocalIDImpl(index: Int, originalName: OriginalName) extends LocalID { + override def toString(): String = + if (originalName.isDefined) originalName.get.toString() + else s"" + } + + private final class LabelIDImpl(index: Int) extends LabelID { + override def toString(): String = s"