diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 385e143bb0be..22cafaf8bcc8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -155,7 +155,10 @@ jobs: - name: Scala.js Test run: | - ./project/scripts/sbt ";sjsSandbox/run ;sjsSandbox/test ;sjsJUnitTests/test ;set sjsJUnitTests/scalaJSLinkerConfig ~= switchToESModules ;sjsJUnitTests/test ;sjsCompilerTests/test" + ./project/scripts/sbt ";sjsSandbox/run ;sjsSandbox/test ;sjsJUnitTests/test ;set sjsJUnitTests/scalaJSLinkerConfig ~= switchToESModules ;sjsJUnitTests/test" + ./project/scripts/sbt ";sjsJUnitTests/clean ;set sjsJUnitTests/scalaJSLinkerConfig ~= switchToLatestESVersion ;sjsJUnitTests/test" + ./project/scripts/sbt ";sjsJUnitTests/clean ;set sjsJUnitTests/scalaJSLinkerConfig ~= switchToLatestESVersion ;set Global/enableWebAssembly := true; sjsJUnitTests/test" + ./project/scripts/sbt ";sjsCompilerTests/test" test_windows_fast: runs-on: [self-hosted, Windows] diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index 8ffc9637a001..d9fe9cd232b8 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -13,7 +13,7 @@ import Contexts.* import Decorators.* import Flags.* import Names.* -import NameKinds.DefaultGetterName +import NameKinds.{AdaptedClosureName, DefaultGetterName, UniqueName} import Types.* import Symbols.* import Phases.* @@ -70,11 +70,14 @@ class JSCodeGen()(using genCtx: Context) { // Some state -------------------------------------------------------------- + private val anonFunctionsAccessedFromAnonFunClasses = mutable.Set.empty[Symbol] private val lazilyGeneratedAnonClasses = new MutableSymbolMap[TypeDef] private val generatedClasses = mutable.ListBuffer.empty[js.ClassDef] private val generatedStaticForwarderClasses = mutable.ListBuffer.empty[(Symbol, js.ClassDef)] val currentClassSym = new ScopedVar[Symbol] + private val delambdafyTargetDefDefs = new ScopedVar[MutableSymbolMap[DefDef]] + private val methodsAllowingJSAwait = new ScopedVar[mutable.Set[Symbol]] private val currentMethodSym = new ScopedVar[Symbol] private val localNames = new ScopedVar[LocalNameGenerator] private val thisLocalVarName = new ScopedVar[Option[LocalName]] @@ -88,6 +91,8 @@ class JSCodeGen()(using genCtx: Context) { private def resetAllScopedVars[T](body: => T): T = { withScopedVars( currentClassSym := null, + delambdafyTargetDefDefs := null, + methodsAllowingJSAwait := null, currentMethodSym := null, localNames := null, thisLocalVarName := null, @@ -98,10 +103,12 @@ class JSCodeGen()(using genCtx: Context) { } } - private def withPerMethodBodyState[A](methodSym: Symbol)(body: => A): A = { + private def withPerMethodBodyState[A](methodSym: Symbol, + initThisLocalVarName: Option[LocalName] = None)( + body: => A): A = { withScopedVars( currentMethodSym := methodSym, - thisLocalVarName := None, + thisLocalVarName := initThisLocalVarName, isModuleInitialized := new ScopedVar.VarBox(false), undefinedDefaultParams := mutable.Set.empty, ) { @@ -171,6 +178,7 @@ class JSCodeGen()(using genCtx: Context) { try { genCompilationUnit(ctx.compilationUnit) } finally { + anonFunctionsAccessedFromAnonFunClasses.clear() generatedClasses.clear() generatedStaticForwarderClasses.clear() } @@ -223,6 +231,24 @@ class JSCodeGen()(using genCtx: Context) { } } + /* Record all the anonfun functions that are called from anon classes. + * These are the bodies of SAM-expanded classes. They must not be treated + * as delambdafy targets + */ + for typeDef <- allTypeDefs if typeDef.symbol.isAnonymousClass do + new TreeTraverser { + def traverse(tree: Tree)(using Context): Unit = + traverseChildren(tree) + tree match + case tree @ Apply(fun, _) => + val sym = fun.symbol + if isDelambdafyTargetCandidate(sym) && sym.owner != typeDef.symbol then + anonFunctionsAccessedFromAnonFunClasses += sym + case _ => + () + }.traverse(typeDef) + end for + val (anonJSClassTypeDefs, otherTypeDefs) = allTypeDefs.partition(td => td.symbol.isAnonymousClass && td.symbol.isJSType) @@ -241,7 +267,9 @@ class JSCodeGen()(using genCtx: Context) { if (!isPrimitive) { withScopedVars( - currentClassSym := sym + currentClassSym := sym, + delambdafyTargetDefDefs := MutableSymbolMap(), + methodsAllowingJSAwait := mutable.Set.empty, ) { val tree = if (sym.isJSType) { if (!sym.is(Trait) && sym.isNonNativeJSClass) @@ -315,6 +343,52 @@ class JSCodeGen()(using genCtx: Context) { dir.fileNamed(filename + suffix) } + private def isDelambdafyTargetCandidate(sym: Symbol): Boolean = + def isAdaptedAnonFunName(name: Name): Boolean = name match + case UniqueName(underlying, _) => isAdaptedAnonFunName(underlying) + case _ => name.is(AdaptedClosureName) + + sym.isAnonymousFunction || isAdaptedAnonFunName(sym.name) + end isDelambdafyTargetCandidate + + private def isDelambdafyTarget(sym: Symbol): Boolean = + isDelambdafyTargetCandidate(sym) && !anonFunctionsAccessedFromAnonFunClasses.contains(sym) + + private def collectValOrDefDefs(impl: Template): List[ValOrDefDef] = { + val b = List.newBuilder[ValOrDefDef] + + for (stat <- (impl.constr :: impl.body)) { + stat match { + case stat: ValDef => + b += stat + + case stat: DefDef => + val sym = stat.symbol + if isDelambdafyTarget(sym) then + delambdafyTargetDefDefs(sym) = stat + else + b += stat + + case EmptyTree => + () + + case _ => + throw new FatalError(i"Unexpected tree in template: $stat at ${stat.sourcePos}") + } + } + + b.result() + } + + private def consumeDelambdafyTarget(sym: Symbol): DefDef = { + delambdafyTargetDefDefs.remove(sym) match { + case null => + throw new FatalError(i"Cannot resolve delambdafy target $sym at ${sym.sourcePos}") + case defDef => + defDef + } + } + // Generate a class -------------------------------------------------------- /** Gen the IR ClassDef for a Scala class definition (maybe a module class). @@ -365,11 +439,8 @@ class JSCodeGen()(using genCtx: Context) { val methodsBuilder = List.newBuilder[js.MethodDef] val jsNativeMembersBuilder = List.newBuilder[js.JSNativeMemberDef] - val tpl = td.rhs.asInstanceOf[Template] - for (tree <- tpl.constr :: tpl.body) { + for (tree <- collectValOrDefDefs(td.rhs.asInstanceOf[Template])) { tree match { - case EmptyTree => () - case vd: ValDef => // fields are added via genClassFields(), but we need to generate the JS native members val sym = vd.symbol @@ -383,9 +454,6 @@ class JSCodeGen()(using genCtx: Context) { jsNativeMembersBuilder += genJSNativeMemberDef(dd) else methodsBuilder ++= genMethod(dd) - - case _ => - throw new FatalError("Illegal tree in body of genScalaClass(): " + tree) } } @@ -526,11 +594,8 @@ class JSCodeGen()(using genCtx: Context) { val generatedMethods = new mutable.ListBuffer[js.MethodDef] val dispatchMethodNames = new mutable.ListBuffer[JSName] - val tpl = td.rhs.asInstanceOf[Template] - for (tree <- tpl.constr :: tpl.body) { + for (tree <- collectValOrDefDefs(td.rhs.asInstanceOf[Template])) { tree match { - case EmptyTree => () - case _: ValDef => () // fields are added via genClassFields() @@ -555,9 +620,6 @@ class JSCodeGen()(using genCtx: Context) { dispatchMethodNames += sym.jsName } } - - case _ => - throw new FatalError("Illegal tree in gen of genNonNativeJSClass(): " + tree) } } @@ -680,12 +742,11 @@ class JSCodeGen()(using genCtx: Context) { val generatedMethods = new mutable.ListBuffer[js.MethodDef] - val tpl = td.rhs.asInstanceOf[Template] - for (tree <- tpl.constr :: tpl.body) { + for (tree <- collectValOrDefDefs(td.rhs.asInstanceOf[Template])) { tree match { - case EmptyTree => () - case dd: DefDef => generatedMethods ++= genMethod(dd) - case _ => + case dd: DefDef => + generatedMethods ++= genMethod(dd) + case tree: ValDef => throw new FatalError( i"""Illegal tree in gen of genInterface(): $tree |class = $td @@ -1494,7 +1555,9 @@ class JSCodeGen()(using genCtx: Context) { * * Other (normal) methods are emitted with `genMethodBody()`. */ - private def genMethodWithCurrentLocalNameScope(dd: DefDef): Option[js.MethodDef] = { + private def genMethodWithCurrentLocalNameScope(dd: DefDef, + initThisLocalVarName: Option[LocalName] = None): Option[js.MethodDef] = { + implicit val pos = dd.span val sym = dd.symbol val vparamss = dd.termParamss @@ -1547,7 +1610,7 @@ class JSCodeGen()(using genCtx: Context) { } } - withPerMethodBodyState(sym) { + withPerMethodBodyState(sym, initThisLocalVarName) { assert(vparamss.isEmpty || vparamss.tail.isEmpty, "Malformed parameter list: " + vparamss) val params = if (vparamss.isEmpty) Nil else vparamss.head.map(_.symbol) @@ -2362,7 +2425,9 @@ class JSCodeGen()(using genCtx: Context) { val typeDef = consumeLazilyGeneratedAnonClass(sym) val originalClassDef = resetAllScopedVars { withScopedVars( - currentClassSym := sym + currentClassSym := sym, + delambdafyTargetDefDefs := MutableSymbolMap(), + methodsAllowingJSAwait := mutable.Set.empty, ) { genNonNativeJSClass(typeDef) } @@ -3084,7 +3149,10 @@ class JSCodeGen()(using genCtx: Context) { case _ => false } - if (isMethodStaticInIR(sym)) { + if (isDelambdafyTarget(sym)) { + // Force inlining at compile-time + genApplyInline(consumeDelambdafyTarget(sym), receiver, args) + } else if (isMethodStaticInIR(sym)) { genApplyStatic(sym, genActualArgs(sym, args)) } else if (sym.owner.isJSType) { if (!sym.owner.isNonNativeJSClass || sym.isJSExposed) @@ -3098,6 +3166,48 @@ class JSCodeGen()(using genCtx: Context) { } } + private def genApplyInline(targetDefDef: DefDef, receiver: Tree, args: List[Tree])(using Position): js.Tree = { + val target = targetDefDef.symbol + val isTargetStatic = isMethodStaticInIR(target) + + // Gen the receiver and arguments + val genReceiver = + if isTargetStatic then None + else Some(genExpr(receiver)) + val genArgs = args.map(genExpr(_)) + val allActualArgs = genReceiver.toList ::: genArgs + + // Generate the inlined method body + val initThisLocalVarName = + if isTargetStatic then None + else Some(freshLocalIdent("this").name) + val genMethodDef = genMethodWithCurrentLocalNameScope(targetDefDef, initThisLocalVarName).get + + val js.MethodDef(methodFlags, _, _, methodParams, _, methodBody) = genMethodDef + + /* Add the receiver in the param defs, if the generated method is not static. + * This happens when isTargetStatic is false *and* the method is not a + * non-exposed method of a JS class. Since isTargetStatic must be false in + * that situation, we know `genReceiver` and `initThisLocalVarName` are defined. + */ + val allParamDefs = + if methodFlags.namespace.isStatic then + methodParams + else + val receiverParamDef = js.ParamDef(js.LocalIdent(initThisLocalVarName.get), + thisOriginalName, encodeClassType(target.owner), mutable = false) + receiverParamDef :: methodParams + + /* Generate bindings for the params. + */ + val paramVarDefs = + for (paramDef, actualArg) <- allParamDefs.zip(allActualArgs) yield + js.VarDef(paramDef.name, paramDef.originalName, paramDef.ptpe, paramDef.mutable, actualArg) + + // Put everything together + js.Block(paramVarDefs, methodBody.get) + } + /** Gen JS code for a call to a JS method (of a subclass of `js.Any`). * * Basically it boils down to calling the method as a `JSBracketSelect`, @@ -3437,13 +3547,13 @@ class JSCodeGen()(using genCtx: Context) { * * Input: a `Closure` tree of the form * {{{ - * Closure(env, call, functionalInterface) + * Closure(env, targetTree, functionalInterface) * }}} * representing the pseudo-syntax * {{{ - * { (p1, ..., pm) => call(env1, ..., envn, p1, ..., pm) }: functionInterface + * { (p1, ..., pm) => targetTree(env1, ..., envn, p1, ..., pm) }: functionInterface * }}} - * where `envi` are identifiers in the local scope. The qualifier of `call` + * where `envi` are identifiers in the local scope. The qualifier of `targetTree` * is also implicitly captured. * * Output: a `js.Closure` tree of the form @@ -3452,16 +3562,16 @@ class JSCodeGen()(using genCtx: Context) { * }}} * representing the pseudo-syntax * {{{ - * lambda( - * formalParam1, ..., formalParamM) = body - * }}} - * where the `actualCaptures` and `body` are, in general, arbitrary - * expressions. But in this case, `actualCaptures` will be identifiers from - * `env`, and the `body` will be of the form - * {{{ - * call(formalCapture1.ref, ..., formalCaptureN.ref, - * formalParam1.ref, ...formalParamM.ref) + * arrow-lambda<_this = this, formalCapture1 = actualCapture1, ..., formalCaptureN = actualCaptureN>( + * formalParam1: any, ..., formalParamM: any): any = { + * val formalParam1Unboxed: T1 = formalParam1.asInstanceOf[T1]; + * ... + * val formapParamNUnboxed: TN = formalParamM.asInstanceOf[TN]; + * // inlined body of `targetTree`, boxed + * } * }}} + * where the `actualCaptures` are, in general, arbitrary expressions. + * But in this case, `actualCaptures` will be identifiers from `env`. * * When the `js.Closure` node is evaluated, i.e., when the closure value is * created, the expressions of the `actualCaptures` are evaluated, and the @@ -3476,37 +3586,11 @@ class JSCodeGen()(using genCtx: Context) { */ private def genClosure(tree: Closure): js.Tree = { implicit val pos = tree.span - val Closure(env, call, functionalInterface) = tree + val Closure(env, targetTree, functionalInterface) = tree val envSize = env.size - val (fun, args) = call match { - // case Apply(fun, args) => (fun, args) // Conjectured not to happen - case t @ Select(_, _) => (t, Nil) - case t @ Ident(_) => (t, Nil) - } - val sym = fun.symbol - val isStaticCall = isMethodStaticInIR(sym) - - val qualifier = qualifierOf(fun) - val allCaptureValues = - if (isStaticCall) env - else qualifier :: env - - val formalAndActualCaptures = allCaptureValues.map { value => - implicit val pos = value.span - val (formalIdent, originalName) = value match { - case Ident(name) => (freshLocalIdent(name.toTermName), OriginalName(name.toString)) - case This(_) => (freshLocalIdent("this"), thisOriginalName) - case _ => (freshLocalIdent(), NoOriginalName) - } - val formalCapture = js.ParamDef(formalIdent, originalName, - toIRType(value.tpe), mutable = false) - val actualCapture = genExpr(value) - (formalCapture, actualCapture) - } - val (formalCaptures, actualCaptures) = formalAndActualCaptures.unzip - + // Extract information about the SAM type we are implementing val funInterfaceSym = functionalInterface.tpe.typeSymbol val hasRepeatedParam = { funInterfaceSym.exists && { @@ -3518,70 +3602,114 @@ class JSCodeGen()(using genCtx: Context) { val isFunctionXXL = funInterfaceSym.name == tpnme.FunctionXXL && funInterfaceSym.owner == defn.ScalaRuntimePackageClass - val formalParamNames = sym.info.paramNamess.flatten.drop(envSize) - val formalParamTypes = sym.info.paramInfoss.flatten.drop(envSize) - val formalParamRepeateds = - if (hasRepeatedParam) (0 until (formalParamTypes.size - 1)).map(_ => false) :+ true - else (0 until formalParamTypes.size).map(_ => false) - - val formalAndActualParams = formalParamNames.lazyZip(formalParamTypes).lazyZip(formalParamRepeateds).map { - (name, tpe, repeated) => - val formalTpe = - if (isFunctionXXL) jstpe.ArrayType(ObjectArrayTypeRef, nullable = true) - else jstpe.AnyType - val formalParam = js.ParamDef(freshLocalIdent(name), - OriginalName(name.toString), formalTpe, mutable = false) - val actualParam = - if (repeated) genJSArrayToVarArgs(formalParam.ref)(tree.sourcePos) - else unbox(formalParam.ref, tpe) - (formalParam, actualParam) - } - val (formalAndRestParams, actualParams) = formalAndActualParams.unzip - - val (formalParams, restParam) = - if (hasRepeatedParam) (formalAndRestParams.init, Some(formalAndRestParams.last)) - else (formalAndRestParams, None) - - val genBody = { - val call = if (isStaticCall) { - genApplyStatic(sym, formalCaptures.map(_.ref) ::: actualParams) - } else { - val thisCaptureRef :: argCaptureRefs = formalCaptures.map(_.ref): @unchecked - if (!sym.owner.isNonNativeJSClass || sym.isJSExposed) - genApplyMethodMaybeStatically(thisCaptureRef, sym, argCaptureRefs ::: actualParams) + val target = targetTree.symbol + val isTargetStatic = isMethodStaticInIR(target) + + val allCaptureValues = + if (isTargetStatic) env + else qualifierOf(targetTree) :: env + + // Gen actual captures in the local name scope of the enclosing method + val actualCaptures: List[js.Tree] = allCaptureValues.map(genExpr(_)) + + val closure: js.Closure = withNewLocalNameScope { + // Gen the inlined target method body + val initThisLocalVarName = + if isTargetStatic then None + else Some(freshLocalIdent("this").name) + val genMethodDef: js.MethodDef = + genMethodWithCurrentLocalNameScope(consumeDelambdafyTarget(target), initThisLocalVarName).get + val js.MethodDef(methodFlags, _, _, allMethodParams, _, methodBody) = genMethodDef + + // Add a ParamDef for the receiver, if the generated method is not static + val allMethodParamsWithReceiver = + if methodFlags.namespace.isStatic then + allMethodParams else - genApplyJSClassMethod(thisCaptureRef, sym, argCaptureRefs ::: actualParams) + val receiverParamDef = js.ParamDef(js.LocalIdent(initThisLocalVarName.get), + thisOriginalName, encodeClassType(target.owner), mutable = false) + receiverParamDef :: allMethodParams + + // Extract capture params + val (formalCaptures, methodParams) = + allMethodParamsWithReceiver.splitAt(if isTargetStatic then envSize else envSize + 1) + + // Construct the ParamDefs of the js.Closure, and adapt their references to the target's param types + + val formalParamNames = target.info.paramNamess.flatten.drop(envSize) + val formalParamTypes = target.info.paramInfoss.flatten.drop(envSize) + val formalParamRepeateds = + if (hasRepeatedParam) (0 until (formalParamTypes.size - 1)).map(_ => false) :+ true + else (0 until formalParamTypes.size).map(_ => false) + + val formalAndActualParams = formalParamNames.lazyZip(formalParamTypes).lazyZip(formalParamRepeateds).map { + (name, tpe, repeated) => + val formalTpe = + if (isFunctionXXL) jstpe.ArrayType(ObjectArrayTypeRef, nullable = true) + else jstpe.AnyType + val formalParam = js.ParamDef(freshLocalIdent(name), + OriginalName(name.toString), formalTpe, mutable = false) + val actualParam = + if (repeated) genJSArrayToVarArgs(formalParam.ref)(tree.sourcePos) + else unbox(formalParam.ref, tpe) + (formalParam, actualParam) + } + val (formalAndRestParams, adaptedParamValues) = formalAndActualParams.unzip + + val (formalParams, restParam) = + if (hasRepeatedParam) (formalAndRestParams.init, Some(formalAndRestParams.last)) + else (formalAndRestParams, None) + + // At this point, the adapted args had better match the method params + assert(methodParams.size == adaptedParamValues.size, + s"Arity mismatch: $methodParams <-> $adaptedParamValues at $pos") + + // Declare each method param as a VarDef, initialized to the corresponding adapted arg + val methodParamsAsVarDefs = for ((methodParam, adaptedParamValue) <- methodParams.zip(adaptedParamValues)) yield { + js.VarDef(methodParam.name, methodParam.originalName, methodParam.ptpe, + methodParam.mutable, adaptedParamValue) } - box(call, sym.info.finalResultType) + + // Adapt the body's result + val patchedBodyWithBox = box(methodBody.get, target.info.finalResultType) + + // Finally, assemble all the pieces + val fullClosureBody = js.Block(methodParamsAsVarDefs, patchedBodyWithBox) + js.Closure( + js.ClosureFlags.typed, + formalCaptures, + formalParams, + restParam, + resultType = jstpe.AnyType, + fullClosureBody, + actualCaptures + ) } val isThisFunction = funInterfaceSym.isSubClass(jsdefn.JSThisFunctionClass) && { - val ok = formalParams.nonEmpty + val ok = closure.params.nonEmpty if (!ok) report.error("The SAM or apply method for a js.ThisFunction must have a leading non-varargs parameter", tree) ok } if (isThisFunction) { - val thisParam :: otherParams = formalParams: @unchecked + val thisParam :: otherParams = closure.params: @unchecked js.Closure( js.ClosureFlags.function, - formalCaptures, + closure.captureParams, otherParams, - restParam, - jstpe.AnyType, + closure.restParam, + closure.resultType, js.Block( js.VarDef(thisParam.name, thisParam.originalName, thisParam.ptpe, mutable = false, js.This()(thisParam.ptpe)(thisParam.pos))(thisParam.pos), - genBody), - actualCaptures) + closure.body), + closure.captureValues) } else { - val closure = js.Closure(js.ClosureFlags.typed, formalCaptures, - formalParams, restParam, jstpe.AnyType, genBody, actualCaptures) - if (!funInterfaceSym.exists || defn.isFunctionClass(funInterfaceSym)) { - val formalCount = formalParams.size + val formalCount = closure.params.size val descriptor = js.NewLambda.Descriptor( superClass = encodeClassName(defn.AbstractFunctionClass(formalCount)), interfaces = Nil, @@ -3949,6 +4077,54 @@ class JSCodeGen()(using genCtx: Context) { // js.import.meta js.JSImportMeta() + case JS_ASYNC => + // js.async(arg) + assert(args.size == 1, + s"Expected exactly 1 argument for JS primitive $code but got " + + s"${args.size} at $pos") + + def extractStatsAndClosure(arg: Tree): (List[Tree], Closure) = (arg: @unchecked) match + case arg: Closure => + (Nil, arg) + case Block(outerStats, expr) => + val (innerStats, closure) = extractStatsAndClosure(expr) + (outerStats ::: innerStats, closure) + + val (stats, fun @ Closure(_, target, _)) = extractStatsAndClosure(args.head) + methodsAllowingJSAwait += target.symbol + val genStats = stats.map(genStat(_)) + val asyncExpr = genClosure(fun) match { + case js.NewLambda(_, closure: js.Closure) + if closure.params.isEmpty && closure.resultType == jstpe.AnyType => + val newFlags = closure.flags.withTyped(false).withAsync(true) + js.JSFunctionApply(closure.copy(flags = newFlags), Nil) + case other => + throw FatalError( + s"Unexpected tree generated for the Function0 argument to js.async at ${tree.sourcePos}: $other") + } + js.Block(genStats, asyncExpr) + + case JS_AWAIT => + // js.await(arg) + val (arg, permitValue) = genArgs2 + if (!methodsAllowingJSAwait.contains(currentMethodSym)) { + // This is an orphan await + if (!(args(1).tpe <:< jsdefn.WasmJSPI_allowOrphanJSAwaitModuleClassRef)) { + report.error( + "Illegal use of js.await().\n" + + "It can only be used inside a js.async {...} block, without any lambda,\n" + + "by-name argument or nested method in-between.\n" + + "If you compile for WebAssembly, you can allow arbitrary js.await()\n" + + "calls by adding the following import:\n" + + "import scala.scalajs.js.wasm.JSPI.allowOrphanJSAwait", + tree.sourcePos) + } + } + /* In theory we should evaluate `permit` after `arg` but before the `JSAwait`. + * It *should* always be side-effect-free, though, so we just discard it. + */ + js.JSAwait(arg) + case DYNAMIC_IMPORT => // runtime.dynamicImport assert(args.size == 1, diff --git a/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala b/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala index 2f6591763fdd..81b7e21cb3bc 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala @@ -42,6 +42,10 @@ final class JSDefinitions()(using Context) { def JSPackage_undefined(using Context) = JSPackage_undefinedR.symbol @threadUnsafe lazy val JSPackage_dynamicImportR = ScalaJSJSPackageClass.requiredMethodRef("dynamicImport") def JSPackage_dynamicImport(using Context) = JSPackage_dynamicImportR.symbol + @threadUnsafe lazy val JSPackage_asyncR = ScalaJSJSPackageClass.requiredMethodRef("async") + def JSPackage_async(using Context) = JSPackage_asyncR.symbol + @threadUnsafe lazy val JSPackage_awaitR = ScalaJSJSPackageClass.requiredMethodRef("await") + def JSPackage_await(using Context) = JSPackage_awaitR.symbol @threadUnsafe lazy val JSNativeAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.native") def JSNativeAnnot(using Context) = JSNativeAnnotType.symbol.asClass @@ -210,6 +214,10 @@ final class JSDefinitions()(using Context) { @threadUnsafe lazy val Special_unwrapFromThrowableR = SpecialPackageClass.requiredMethodRef("unwrapFromThrowable") def Special_unwrapFromThrowable(using Context) = Special_unwrapFromThrowableR.symbol + @threadUnsafe lazy val WasmJSPIModuleRef = requiredModuleRef("scala.scalajs.js.wasm.JSPI") + @threadUnsafe lazy val WasmJSPI_allowOrphanJSAwaitModuleRef = requiredModuleRef("scala.scalajs.js.wasm.JSPI.allowOrphanJSAwait") + def WasmJSPI_allowOrphanJSAwaitModuleClassRef(using Context) = WasmJSPIModuleRef.select(WasmJSPI_allowOrphanJSAwaitModuleRef.symbol) + @threadUnsafe lazy val WrappedArrayType: TypeRef = requiredClassRef("scala.scalajs.js.WrappedArray") def WrappedArrayClass(using Context) = WrappedArrayType.symbol.asClass diff --git a/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala b/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala index 41e62094b04f..52171b40ec93 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala @@ -30,7 +30,10 @@ object JSPrimitives { inline val JS_IMPORT = JS_NEW_TARGET + 1 // js.import.apply(specifier) inline val JS_IMPORT_META = JS_IMPORT + 1 // js.import.meta - inline val CONSTRUCTOROF = JS_IMPORT_META + 1 // runtime.constructorOf(clazz) + inline val JS_ASYNC = JS_IMPORT_META + 1 // js.async + inline val JS_AWAIT = JS_ASYNC + 1 // js.await + + inline val CONSTRUCTOROF = JS_AWAIT + 1 // runtime.constructorOf(clazz) inline val CREATE_INNER_JS_CLASS = CONSTRUCTOROF + 1 // runtime.createInnerJSClass inline val CREATE_LOCAL_JS_CLASS = CREATE_INNER_JS_CLASS + 1 // runtime.createLocalJSClass inline val WITH_CONTEXTUAL_JS_CLASS_VALUE = CREATE_LOCAL_JS_CLASS + 1 // runtime.withContextualJSClassValue @@ -110,6 +113,8 @@ class JSPrimitives(ictx: Context) extends DottyPrimitives(ictx) { addPrimitive(jsdefn.JSPackage_typeOf, TYPEOF) addPrimitive(jsdefn.JSPackage_native, JS_NATIVE) + addPrimitive(jsdefn.JSPackage_async, JS_ASYNC) + addPrimitive(jsdefn.JSPackage_await, JS_AWAIT) addPrimitive(defn.BoxedUnit_UNIT, UNITVAL) diff --git a/compiler/src/dotty/tools/dotc/transform/Dependencies.scala b/compiler/src/dotty/tools/dotc/transform/Dependencies.scala index a4c3550441f5..730b52f9e147 100644 --- a/compiler/src/dotty/tools/dotc/transform/Dependencies.scala +++ b/compiler/src/dotty/tools/dotc/transform/Dependencies.scala @@ -195,6 +195,18 @@ abstract class Dependencies(root: ast.tpd.Tree, @constructorOnly rootContext: Co val encClass = local.owner.enclosingClass // When to prefer the enclosing class over the enclosing package: val preferEncClass = + ctx.settings.scalajs.value + // In Scala.js, never hoist anything. This is particularly important for: + // - members of DynamicImportThunk subclasses: moving code across the + // boundaries of a DynamicImportThunk changes the dynamic and static + // dependencies between ES modules, which is forbidden by spec; and + // - anonymous function defs (and their adapted variants): the backend + // must be able to find them in the same class as the corresponding + // Closure nodes, because it forcibly inlines them in the generated + // js.Closure's. + // We let the Scala.js optimizer deal with removing unneeded captured + // references, such as `this` pointers. + || encClass.isStatic // If class is not static, we try to hoist the method out of // the class to avoid the outer pointer. @@ -216,13 +228,6 @@ abstract class Dependencies(root: ast.tpd.Tree, @constructorOnly rootContext: Co // object or class constructor to be static since that can cause again deadlocks // by its interaction with class initialization. See run/deadlock.scala, which works // in Scala 3 but deadlocks in Scala 2. - || - /* Scala.js: Never move any member beyond the boundary of a DynamicImportThunk. - * DynamicImportThunk subclasses are boundaries between the eventual ES modules - * that can be dynamically loaded. Moving members across that boundary changes - * the dynamic and static dependencies between ES modules, which is forbidden. - */ - ctx.settings.scalajs.value && encClass.isSubClass(jsdefn.DynamicImportThunkClass) logicOwner(sym) = if preferEncClass then encClass else local.enclosingPackageClass diff --git a/compiler/src/dotty/tools/dotc/transform/LambdaLift.scala b/compiler/src/dotty/tools/dotc/transform/LambdaLift.scala index af168b563048..e8d3f7897ea4 100644 --- a/compiler/src/dotty/tools/dotc/transform/LambdaLift.scala +++ b/compiler/src/dotty/tools/dotc/transform/LambdaLift.scala @@ -2,6 +2,7 @@ package dotty.tools.dotc package transform import MegaPhase.* +import core.Annotations.Annotation import core.Denotations.NonSymSingleDenotation import core.DenotTransformers.* import core.Symbols.* @@ -110,6 +111,14 @@ object LambdaLift: else // Add Final when a method is lifted into a class. initFlags = initFlags | Final + + // If local was a local method inside an @static method, mark the lifted local as @static as well + def isLocalToScalaStatic(sym: Symbol): Boolean = + val owner = sym.owner + owner.is(Method) && (owner.hasAnnotation(defn.ScalaStaticAnnot) || isLocalToScalaStatic(owner)) + if isLocalToScalaStatic(local) then + local.addAnnotation(Annotation(defn.ScalaStaticAnnot, local.span)) + local.copySymDenotation( owner = newOwner, name = newName(local), diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala b/compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala index 87ee2be91465..3801e1f75907 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala @@ -164,9 +164,9 @@ object JSSymUtils { sym.getAnnotation(jsdefn.JSNameAnnot).fold[JSName] { JSName.Literal(defaultJSName) } { annotation => - annotation.arguments.head match { - case Literal(Constant(name: String)) => JSName.Literal(name) - case tree => JSName.Computed(tree.symbol) + annotation.argumentConstantString(0) match { + case Some(name) => JSName.Literal(name) + case None => JSName.Computed(annotation.arguments.head.symbol) } } } diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala b/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala index 13fcbe542448..08ee07c0dff6 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/PrepJSInterop.scala @@ -154,7 +154,7 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP } checkJSNameAnnots(sym) - constFoldJSExportTopLevelAndStaticAnnotations(sym) + constantFoldJSAnnotations(sym) markExposedIfRequired(tree.symbol) @@ -1089,17 +1089,18 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP } } - /** Constant-folds arguments to `@JSExportTopLevel` and `@JSExportStatic`. + /** Constant-folds arguments to `@JSName`, `@JSExportTopLevel` and `@JSExportStatic`. * * Unlike scalac, dotc does not constant-fold expressions in annotations. * Our back-end needs to have access to the arguments to those two * annotations as literal strings, so we specifically constant-fold them * here. */ - private def constFoldJSExportTopLevelAndStaticAnnotations(sym: Symbol)(using Context): Unit = { + private def constantFoldJSAnnotations(sym: Symbol)(using Context): Unit = { val annots = sym.annotations val newAnnots = annots.mapConserve { annot => - if (annot.symbol == jsdefn.JSExportTopLevelAnnot || annot.symbol == jsdefn.JSExportStaticAnnot) { + if (annot.symbol == jsdefn.JSExportTopLevelAnnot || annot.symbol == jsdefn.JSExportStaticAnnot || + annot.symbol == jsdefn.JSNameAnnot) { annot.tree match { case app @ Apply(fun, args) => val newArgs = args.mapConserve { arg => @@ -1109,7 +1110,7 @@ class PrepJSInterop extends MacroTransform with IdentityDenotTransformer { thisP case _ => arg.tpe.widenTermRefExpr.normalized match { case ConstantType(c) => Literal(c).withSpan(arg.span) - case _ => arg // PrepJSExports will emit an error for those cases + case _ => arg } } } diff --git a/project/Build.scala b/project/Build.scala index 24526b5cac00..0cdc70905509 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -33,6 +33,8 @@ import com.typesafe.tools.mima.plugin.MimaPlugin.autoImport._ import org.scalajs.sbtplugin.ScalaJSPlugin import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ +import org.scalajs.linker.interface.ESVersion + import sbtbuildinfo.BuildInfoPlugin import sbtbuildinfo.BuildInfoPlugin.autoImport._ import sbttastymima.TastyMiMaPlugin @@ -2772,7 +2774,9 @@ object Build { case FullOptStage => (Test / fullLinkJS / scalaJSLinkerConfig).value } - if (linkerConfig.moduleKind != ModuleKind.NoModule && !linkerConfig.closureCompiler) + val isWebAssembly = linkerConfig.experimentalUseWebAssembly + + if (linkerConfig.moduleKind != ModuleKind.NoModule && !linkerConfig.closureCompiler && !isWebAssembly) Seq(baseDirectory.value / "test-require-multi-modules") else Nil @@ -2800,6 +2804,8 @@ object Build { val moduleKind = linkerConfig.moduleKind val hasModules = moduleKind != ModuleKind.NoModule + val hasAsyncAwait = linkerConfig.esFeatures.esVersion >= ESVersion.ES2017 + val isWebAssembly = linkerConfig.experimentalUseWebAssembly def conditionally(cond: Boolean, subdir: String): Seq[File] = if (!cond) Nil @@ -2829,9 +2835,12 @@ object Build { ++ conditionally(!hasModules, "js/src/test/require-no-modules") ++ conditionally(hasModules, "js/src/test/require-modules") - ++ conditionally(hasModules && !linkerConfig.closureCompiler, "js/src/test/require-multi-modules") + ++ conditionally(hasModules && !linkerConfig.closureCompiler && !isWebAssembly, "js/src/test/require-multi-modules") ++ conditionally(moduleKind == ModuleKind.ESModule, "js/src/test/require-dynamic-import") ++ conditionally(moduleKind == ModuleKind.ESModule, "js/src/test/require-esmodule") + + ++ conditionally(hasAsyncAwait, "js/src/test/require-async-await") + ++ conditionally(hasAsyncAwait && isWebAssembly, "js/src/test/require-orphan-await") ) }, diff --git a/project/DottyJSPlugin.scala b/project/DottyJSPlugin.scala index 89a876c21e66..756b093c3d80 100644 --- a/project/DottyJSPlugin.scala +++ b/project/DottyJSPlugin.scala @@ -6,20 +6,34 @@ import sbt.Keys.* import org.scalajs.sbtplugin.ScalaJSPlugin import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ -import org.scalajs.linker.interface.StandardConfig +import org.scalajs.linker.interface.{ESVersion, StandardConfig} + +import org.scalajs.jsenv.nodejs.NodeJSEnv object DottyJSPlugin extends AutoPlugin { object autoImport { val switchToESModules: StandardConfig => StandardConfig = config => config.withModuleKind(ModuleKind.ESModule) + + val switchToLatestESVersion: StandardConfig => StandardConfig = + config => config.withESFeatures(_.withESVersion(ESVersion.ES2021)) + + val enableWebAssembly: SettingKey[Boolean] = + settingKey("enable all the configuration items required for WebAssembly") } + import autoImport._ + val writePackageJSON = taskKey[Unit]( "Write package.json to configure module type for Node.js") override def requires: Plugins = ScalaJSPlugin + override def globalSettings: Seq[Setting[_]] = Def.settings( + enableWebAssembly := false, + ) + override def projectSettings: Seq[Setting[_]] = Def.settings( /* #11709 Remove the dependency on scala3-library that ScalaJSPlugin adds. @@ -40,6 +54,31 @@ object DottyJSPlugin extends AutoPlugin { // Typecheck the Scala.js IR found on the classpath scalaJSLinkerConfig ~= (_.withCheckIR(true)), + // Maybe configure WebAssembly + scalaJSLinkerConfig := { + val prev = scalaJSLinkerConfig.value + if (enableWebAssembly.value) { + prev + .withModuleKind(ModuleKind.ESModule) + .withExperimentalUseWebAssembly(true) + } else { + prev + } + }, + jsEnv := { + val baseConfig = NodeJSEnv.Config() + val config = if (enableWebAssembly.value) { + baseConfig.withArgs(List( + "--experimental-wasm-exnref", + "--experimental-wasm-imported-strings", // for JS string builtins + "--experimental-wasm-jspi", // for JSPI, used by async/await + )) + } else { + baseConfig + } + new NodeJSEnv(config) + }, + Compile / jsEnvInput := (Compile / jsEnvInput).dependsOn(writePackageJSON).value, Test / jsEnvInput := (Test / jsEnvInput).dependsOn(writePackageJSON).value,