From 5826b58610dc13b511aff9fc315b6425330895bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 2 Sep 2025 10:47:27 +0200 Subject: [PATCH 1/3] Scala.js: Handle `@JSName` annots with constant-folded arguments. Previously, if an `@JSName` annotation had an argument that was not a literal, but a reference to a constant expression (such as a `final val`), it would not be constant-folded in the generated Scala.js IR. This produced worse code than necessary. For Wasm, it was particularly bad, as the names must then be evaluated on the Wasm side instead of being pushed to the custom JS helpers. --- .../dotty/tools/dotc/transform/sjs/JSSymUtils.scala | 6 +++--- .../tools/dotc/transform/sjs/PrepJSInterop.scala | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala b/compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala index a8ebb05e394e..6042d06d2cf8 100644 --- a/compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala +++ b/compiler/src/dotty/tools/dotc/transform/sjs/JSSymUtils.scala @@ -170,9 +170,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 } } } From eef3fe24ab2c756133a6bad17afb31f80d7b57df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: 2025年8月30日 18:08:20 +0200 Subject: [PATCH 2/3] Scala.js: Inline the target of Closure nodes in their js.Closures. This is forward port of the Scala.js commit https://github.com/scala-js/scala-js/commit/0d16b42e54d823dbd79a06fb6247730a01206831 The body of `Closure` nodes always has a simple shape that calls a helper method. We previously generated that call in the body of the `js.Closure`, and marked the target method `@inline` so that the optimizer would always inline it. Instead, we now directly "inline" it from the codegen, by generating the `js.MethodDef` right inside the `js.Closure` scope. As is, this does not change the generated code. However, it may speed up (cold) linker runs, since it will have less work to do. Notably, it performs two fewer knowledge queries to find and inline the target method. It also reduces the total amount of methods to manipulate in the incremental analysis. More importantly, this will be necessary later if we want to add support for `async/await` or `function*/yield`. Indeed, for those, we will need `await`/`yield` expressions to be lexically scoped in the body of their enclosing closure. That won't work if they are in the body of a separate helper method. --- .../dotty/tools/backend/sjs/JSCodeGen.scala | 346 ++++++++++++------ .../tools/dotc/transform/Dependencies.scala | 19 +- .../tools/dotc/transform/LambdaLift.scala | 9 + 3 files changed, 256 insertions(+), 118 deletions(-) diff --git a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala index edaaca86ce67..db9acb377870 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,13 @@ 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 currentMethodSym = new ScopedVar[Symbol] private val localNames = new ScopedVar[LocalNameGenerator] private val thisLocalVarName = new ScopedVar[Option[LocalName]] @@ -88,6 +90,7 @@ class JSCodeGen()(using genCtx: Context) { private def resetAllScopedVars[T](body: => T): T = { withScopedVars( currentClassSym := null, + delambdafyTargetDefDefs := null, currentMethodSym := null, localNames := null, thisLocalVarName := null, @@ -98,10 +101,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 +176,7 @@ class JSCodeGen()(using genCtx: Context) { try { genCompilationUnit(ctx.compilationUnit) } finally { + anonFunctionsAccessedFromAnonFunClasses.clear() generatedClasses.clear() generatedStaticForwarderClasses.clear() } @@ -223,6 +229,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 +265,8 @@ class JSCodeGen()(using genCtx: Context) { if (!isPrimitive) { withScopedVars( - currentClassSym := sym + currentClassSym := sym, + delambdafyTargetDefDefs := MutableSymbolMap(), ) { val tree = if (sym.isJSType) { if (!sym.is(Trait) && sym.isNonNativeJSClass) @@ -315,6 +340,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 +436,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 +451,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 +591,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 +617,6 @@ class JSCodeGen()(using genCtx: Context) { dispatchMethodNames += sym.jsName } } - - case _ => - throw new FatalError("Illegal tree in gen of genNonNativeJSClass(): " + tree) } } @@ -680,12 +739,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 +1552,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 +1607,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 +2422,8 @@ class JSCodeGen()(using genCtx: Context) { val typeDef = consumeLazilyGeneratedAnonClass(sym) val originalClassDef = resetAllScopedVars { withScopedVars( - currentClassSym := sym + currentClassSym := sym, + delambdafyTargetDefDefs := MutableSymbolMap(), ) { genNonNativeJSClass(typeDef) } @@ -3084,7 +3145,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 +3162,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`, @@ -3459,13 +3565,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 @@ -3474,16 +3580,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 @@ -3498,37 +3604,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 && { @@ -3540,70 +3620,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) } - box(call, sym.info.finalResultType) + 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) + } + + // 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, 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), From c58cceae4b4789552fb66967a856914981ef2210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: 2025年8月30日 18:11:53 +0200 Subject: [PATCH 3/3] Scala.js: Support `js.async` and `js.await`, including JSPI on Wasm. This is forward port of the compiler changes in the two commits of the Scala.js PR https://github.com/scala-js/scala-js/pull/5130 --- We add support for a new pair of primitive methods, `js.async` and `js.await`. They correspond to JavaScript `async` functions and `await` expressions. `js.await(p)` awaits a `Promise`, but it must be directly scoped within a `js.async { ... }` block. At the IR level, `js.await(p)` directly translates to a dedicated IR node `JSAwait(arg)`. `js.async` blocks don't have a direct representation. Instead, the IR models `async function`s and `async =>`functions, as `Closure`s with an additionnal `async` flag. This corresponds to the JavaScript model for `async/await`. A `js.async { body }` block therefore corresponds to an immediately-applied `async Closure`, which in JavaScript would be written as `(async () => body)()`. --- We then optionally allow orphan `js.await(p)` on WebAssembly. With the JavaScript Promise Integration (JSPI), there can be as many frames as we want between a `WebAssembly.promising` function and the corresponding `WebAssembly.Suspending` calls. The former is introduced by our `js.async` blocks, and the latter by calls to `js.await`. Normally, `js.await` must be directly enclosed within a `js.async` block. This ensures that it can be compiled to a JavaScript `async` function and `await` expression. We introduce a sort of "language import" to allow "orphan awaits". This way, we can decouple the `js.await` calls from their `js.async` blocks. The generated IR will then only link when targeting WebAssembly. Technically, `js.await` requires an implicit `js.AwaitPermit`. The default one only allows `js.await` directly inside `js.async`, and we can import a stronger one that allows orphan awaits. There must still be a `js.async` block *dynamically* enclosing any `js.await` call (on the call stack), without intervening JS frame. If that dynamic property is not satisfied, a `WebAssembly.SuspendError` gets thrown. This last point is not yet implemented in Node.js at the time of this commit; instead such a situation traps. The corresponding test is therefore currently ignored. --- Since these features fundamentally depend on a) the target ES version and b) the WebAssembly backend, we add more configurations of Scala.js to the Scala 3 CI. --- .github/workflows/ci.yaml | 5 +- .../dotty/tools/backend/sjs/JSCodeGen.scala | 52 +++++++++++++++++++ .../tools/backend/sjs/JSDefinitions.scala | 8 +++ .../tools/backend/sjs/JSPrimitives.scala | 7 ++- project/Build.scala | 13 ++++- project/DottyJSPlugin.scala | 41 ++++++++++++++- 6 files changed, 121 insertions(+), 5 deletions(-) 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 db9acb377870..f6b84ff6c41a 100644 --- a/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala +++ b/compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala @@ -77,6 +77,7 @@ class JSCodeGen()(using genCtx: Context) { 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]] @@ -91,6 +92,7 @@ class JSCodeGen()(using genCtx: Context) { withScopedVars( currentClassSym := null, delambdafyTargetDefDefs := null, + methodsAllowingJSAwait := null, currentMethodSym := null, localNames := null, thisLocalVarName := null, @@ -267,6 +269,7 @@ class JSCodeGen()(using genCtx: Context) { withScopedVars( currentClassSym := sym, delambdafyTargetDefDefs := MutableSymbolMap(), + methodsAllowingJSAwait := mutable.Set.empty, ) { val tree = if (sym.isJSType) { if (!sym.is(Trait) && sym.isNonNativeJSClass) @@ -2424,6 +2427,7 @@ class JSCodeGen()(using genCtx: Context) { withScopedVars( currentClassSym := sym, delambdafyTargetDefDefs := MutableSymbolMap(), + methodsAllowingJSAwait := mutable.Set.empty, ) { genNonNativeJSClass(typeDef) } @@ -4095,6 +4099,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 ebec841125b4..e42502fe715b 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 @@ -212,6 +216,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 606916ca4b6e..1d0198a46ec1 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 @@ -112,6 +115,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/project/Build.scala b/project/Build.scala index 1bfdbac3e107..622d049d8ed8 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,

AltStyle によって変換されたページ (->オリジナル) /