From 11cd58daa73211fc3bffe9d8b36b060ee61c3bdb Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Mon, 3 Feb 2025 03:53:19 -0500 Subject: [PATCH 1/2] Implement Def.inputTaskDyn **Problem/Solution** This implements Def.inputTaskDyn using Scala 3 macros. --- main-settings/src/main/scala/sbt/Def.scala | 3 + .../main/scala/sbt/std/InputTaskMacro.scala | 155 ++++++++---------- .../src/test/scala/sbt/std/UsageTest.scala | 14 +- main/src/main/scala/sbt/Defaults.scala | 8 +- main/src/main/scala/sbt/Keys.scala | 3 +- .../sbt-test/actions/input-task-dyn/build.sbt | 6 +- .../actions/input-task-dyn/{pending => test} | 0 7 files changed, 97 insertions(+), 92 deletions(-) rename sbt-app/src/sbt-test/actions/input-task-dyn/{pending => test} (100%) diff --git a/main-settings/src/main/scala/sbt/Def.scala b/main-settings/src/main/scala/sbt/Def.scala index 0abccd91d..e020c9998 100644 --- a/main-settings/src/main/scala/sbt/Def.scala +++ b/main-settings/src/main/scala/sbt/Def.scala @@ -319,6 +319,9 @@ object Def extends BuildSyntax with Init with InitializeImplicits: inline def inputTask[A1](inline a: A1): Def.Initialize[InputTask[A1]] = ${ InputTaskMacro.inputTaskMacroImpl[A1]('a) } + inline def inputTaskDyn[A1](inline a: Def.Initialize[Task[A1]]): Def.Initialize[InputTask[A1]] = + ${ InputTaskMacro.inputTaskDynMacroImpl[A1]('a) } + inline def taskIf[A1](inline a: A1): Def.Initialize[Task[A1]] = ${ TaskMacro.taskIfImpl[A1]('a, cached = true) } diff --git a/main-settings/src/main/scala/sbt/std/InputTaskMacro.scala b/main-settings/src/main/scala/sbt/std/InputTaskMacro.scala index e091d0df9..31dc687f8 100644 --- a/main-settings/src/main/scala/sbt/std/InputTaskMacro.scala +++ b/main-settings/src/main/scala/sbt/std/InputTaskMacro.scala @@ -11,6 +11,7 @@ package std import sbt.internal.util.Types.Id import sbt.internal.util.complete.Parser import scala.quoted.* +import scala.collection.mutable.ListBuffer object InputTaskMacro: import TaskMacro.ContSyntax.* @@ -20,8 +21,10 @@ object InputTaskMacro: ): Expr[Def.Initialize[InputTask[A1]]] = inputTaskMacro0[A1](tree) - // def inputTaskDynMacroImpl[A1: Type](t: c.Expr[Initialize[Task[A1]]])(using qctx: Quotes): c.Expr[Initialize[InputTask[A1]]] = - // inputTaskDynMacro0[A1](c)(t) + def inputTaskDynMacroImpl[A1: Type](tree: Expr[Def.Initialize[Task[A1]]])(using + qctx: Quotes + ): Expr[Def.Initialize[InputTask[A1]]] = + inputTaskDynMacro0[A1](tree) private def inputTaskMacro0[A1: Type](tree: Expr[A1])(using Quotes @@ -110,92 +113,78 @@ object InputTaskMacro: private def iTaskMacro[A1: Type](tree: Expr[A1])(using qctx: Quotes): Expr[Task[A1]] = val convert1 = new TaskConvert(qctx, 2000) convert1.contMapN[A1, Task, Id](tree, convert1.appExpr, None) - /* + private def inputTaskDynMacro0[A1: Type]( expr: Expr[Def.Initialize[Task[A1]]] - )(using qctx: Quotes): Expr[Def.Initialize[InputTask[A1]]] = { - import qctx.reflect.{ Apply => ApplyTree, * } - // import internal.decorators._ - val tag: Type[A1] = summon[Type[A1]] - // val util = ContextUtil[c.type](c) - val convert1 = new InitParserConvert(qctx) - import convert1.Converted - - // val it = Ident(convert1.singleton(InputTask)) - val isParserWrapper = new InitParserConvert(qctx).asPredicate - val isTaskWrapper = new FullConvert(qctx).asPredicate - val isAnyWrapper = - (n: String, tpe: TypeRepr, tr: Term) => - isParserWrapper(n, tpe, tr) || isTaskWrapper(n, tpe, tr) - val ttree = expr.asTerm - val defs = convert1.collectDefs(ttree, isAnyWrapper) - val checkQual = - util.checkReferences(defs, isAnyWrapper, weakTypeOf[Def.Initialize[InputTask[Any]]]) - - // the Symbol for the anonymous function passed to the appropriate Instance.map/flatMap/pure method - // this Symbol needs to be known up front so that it can be used as the owner of synthetic vals - - // val functionSym = util.functionSymbol(ttree.pos) - var result: Option[(Term, TypeRepr, ValDef)] = None - - // original is the Tree being replaced. It is needed for preserving attributes. - def subWrapper(tpe: TypeRepr, qual: Term, original: Term): Tree = - if result.isDefined then - report.errorAndAbort( - "implementation restriction: a dynamic InputTask can only have a single input parser.", - qual.pos, + )(using qctx: Quotes): Expr[Def.Initialize[InputTask[A1]]] = + // convert1 detects x.parsed where x is Parser[a], State => Parser[a], Initialize[Parser[a]], or Initialize[State => Parser[a]] + // val it6a = Def.inputTaskDyn { + // val d3 = dummy3.parsed + // val i = d3._2 + // Def.task { tk.value + i } + // } + val convert1 = InitParserConvert(qctx, 0) + import convert1.qctx.reflect.* + def expandTask[A2: Type](dyn: Boolean, tree: Tree): Expr[Def.Initialize[Task[A2]]] = + if dyn then + TaskMacro.taskDynMacroImpl[A2]( + tree.asExprOf[Def.Initialize[Task[A2]]] ) - Literal(UnitConstant()) - else { - // qual.foreach(checkQual) - val vd = util.freshValDef(tpe, qual.symbol.pos, functionSym) // val $x: - result = Some((qual, tpe, vd)) - val tree = util.refVal(original, vd) // $x - tree.setPos( - qual.pos - ) // position needs to be set so that wrapKey passes the position onto the wrapper - assert(tree.tpe != null, "Null type: " + tree) - tree.setType(tpe) - tree + else TaskMacro.taskMacroImpl[A2](tree.asExprOf[A2], false) + val inputBuf = ListBuffer[(String, TypeRepr, Term, Term)]() + val record = [a] => + (name: String, tpe: Type[a], qual: Term, oldTree: Term) => + given t: Type[a] = tpe + convert1 + .convert[a](name, qual) + .transform { (replacement: Term) => + inputBuf.append((name, TypeRepr.of[a](using tpe), qual, replacement)) + oldTree + } + def genCreateFree(body: Term) = + val init = expandTask[A1](true, body) + '{ + InputTask.createFree($init) } - // Tree for InputTask.[, ](arg1)(arg2) - def inputTaskCreate(name: String, tpeA: Type, tpeB: Type, arg1: Tree, arg2: Tree) = { - val typedApp = TypeApply(util.select(it, name), TypeTree(tpeA) :: TypeTree(tpeB) :: Nil) - val app = ApplyTree(ApplyTree(typedApp, arg1 :: Nil), arg2 :: Nil) - Expr[Def.Initialize[InputTask[A1]]](app) - } - // Tree for InputTask.createFree[](arg1) - def inputTaskCreateFree(tpe: Type, arg: Tree) = { - val typedApp = TypeApply(util.select(it, InputTaskCreateFreeName), TypeTree(tpe) :: Nil) - val app = ApplyTree(typedApp, arg :: Nil) - Expr[Def.Initialize[InputTask[A1]]](app) - } - def expandTask[I: Type](dyn: Boolean, tx: Tree): c.Expr[Initialize[Task[I]]] = - if dyn then taskDynMacroImpl[I](c)(c.Expr[Initialize[Task[I]]](tx)) - else taskMacroImpl[I](c)(c.Expr[I](tx)) - def wrapTag[I: Type]: Type[Initialize[Task[I]]] = weakTypeTag - - def sub(name: String, tpe: TypeRepr, qual: Term, oldTree: Term): Converted = - convert1.convert[A1](name, qual) transform { (tree: Term) => - subWrapper(tpe, tree, oldTree) + // This is roughly based on getMap in Cont.scala + def genCreateDyn[Arg: Type](parser: Term, body: Term) = + val param = parser.asExprOf[Def.Initialize[State => Parser[Arg]]] + val tpe = + MethodType(List("$p"))( + _ => List(TypeRepr.of[Arg]), + _ => TypeRepr.of[Def.Initialize[Task[A1]]] + ) + val lambda = Lambda( + owner = Symbol.spliceOwner, + tpe = tpe, + rhsFn = (sym, params) => { + val param = params.head.asInstanceOf[Term] + val substitute = [a] => + (name: String, tpe: Type[a], qual: Term, replace: Term) => + given t: Type[a] = tpe + convert1 + .convert[a](name, qual) + .transform { _ => Ref(param.symbol) } + val modifiedBody = + convert1 + .transformWrappers(body.changeOwner(sym), substitute, sym) + modifiedBody + } + ) + val action = expandTask[Arg => Def.Initialize[Task[A1]]](false, lambda) + '{ + InputTask.createDyn[Arg, A1](p = $param)(action = $action) } - - val tx = - convert1.transformWrappers(expr.asTerm, sub, Symbol.spliceOwner) - result match { - case Some((p, tpe, param)) => - val fCore = util.createFunction(param :: Nil, tx, functionSym) - val bodyTpe = wrapTag(tag).tpe - val fTpe = util.functionType(tpe :: Nil, bodyTpe) - val fTag = Type[Any](fTpe) // don't know the actual type yet, so use Any - val fInit = expandTask(false, fCore)(fTag).tree - inputTaskCreate(InputTaskCreateDynName, tpe, tag.tpe, p, fInit) - case None => - val init = expandTask[A1](true, tx).tree - inputTaskCreateFree(tag.tpe, init) - } - } - */ + val body = convert1.transformWrappers(expr.asTerm, record, Symbol.spliceOwner) + inputBuf.toList match + case Nil => genCreateFree(body) + case (_, tpe, _, paramTree) :: Nil => + tpe.asType match + case '[a] => genCreateDyn[a](paramTree, body) + case xs => + report.errorAndAbort("a dynamic InputTask can only have a single input parser.") + ??? + end inputTaskDynMacro0 def parserGenInputTaskMacroImpl[A1: Type, A2: Type]( parserGen: Expr[ParserGen[A1]], diff --git a/main-settings/src/test/scala/sbt/std/UsageTest.scala b/main-settings/src/test/scala/sbt/std/UsageTest.scala index 40661e194..e4d03f321 100644 --- a/main-settings/src/test/scala/sbt/std/UsageTest.scala +++ b/main-settings/src/test/scala/sbt/std/UsageTest.scala @@ -129,11 +129,15 @@ object Assign { dummys.parsed } - // val it6 = Def.inputTaskDyn { - // val d3 = dummy3.parsed - // val i = d3._2 - // Def.task { tk.value + i } - // } + val it6a = Def.inputTaskDyn { + val d3 = dummy3.parsed + val i = d3._2 + Def.task { tk.value + i } + } + + val it6b = Def.inputTaskDyn { + Def.task { 1 } + } val it7 = Def.inputTask { it5.parsed diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 915d4525d..2c38e8454 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -1038,7 +1038,13 @@ object Defaults extends BuildCommon { val r = run.evaluated val service = bgJobService.value service.waitForTry(r.handle).get - r + () + }, + runMainBlock := { + val r = runMain.evaluated + val service = bgJobService.value + service.waitForTry(r.handle).get + () }, fgRun := runTask(fullClasspath, (run / mainClass), (run / runner)).evaluated, fgRunMain := runMainTask(fullClasspath, (run / runner)).evaluated, diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 2cc159c89..26ef3c1e5 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -316,8 +316,9 @@ object Keys { val selectMainClass = taskKey[Option[String]]("Selects the main class to run.").withRank(BMinusTask) val mainClass = taskKey[Option[String]]("Defines the main class for packaging or running.").withRank(BPlusTask) val run = inputKey[EmulateForeground]("Runs a main class, passing along arguments provided on the command line.").withRank(APlusTask) - val runBlock = inputKey[EmulateForeground]("Runs a main class, and blocks until it's done.").withRank(DTask) + val runBlock = inputKey[Unit]("Runs a main class, and blocks until it's done.").withRank(DTask) val runMain = inputKey[EmulateForeground]("Runs the main class selected by the first argument, passing the remaining arguments to the main method.").withRank(ATask) + val runMainBlock = inputKey[Unit]("Runs the main class selected by the first argument, passing the remaining arguments to the main method.").withRank(DTask) val discoveredMainClasses = taskKey[Seq[String]]("Auto-detects main classes.").withRank(BMinusTask) val runner = taskKey[ScalaRun]("Implementation used to run a main class.").withRank(DTask) val trapExit = settingKey[Boolean]("If true, enables exit trapping and thread management for 'run'-like tasks. This was removed in sbt 1.6.0 due to JDK 17 deprecating Security Manager.").withRank(CSetting) diff --git a/sbt-app/src/sbt-test/actions/input-task-dyn/build.sbt b/sbt-app/src/sbt-test/actions/input-task-dyn/build.sbt index d87e3f2d0..34d2ef5ec 100644 --- a/sbt-app/src/sbt-test/actions/input-task-dyn/build.sbt +++ b/sbt-app/src/sbt-test/actions/input-task-dyn/build.sbt @@ -5,12 +5,14 @@ import complete.Parser val runFoo = inputKey[Unit]("Runs Foo with passed arguments") val check = taskKey[Unit]("") +scalaVersion := "3.6.3" + lazy val root = (project in file(".")). settings( name := "run-test", runFoo := Def.inputTaskDyn { - val args = Def.spaceDelimited().parsed - (Compile / runMain).toTask(s" Foo " + args.mkString(" ")) + val args = Def.spaceDelimited().parsed + (Compile / runMainBlock).toTask(s" Foo " + args.mkString(" ")) }.evaluated, check := { val x = runFoo.toTask(" hi ho").value diff --git a/sbt-app/src/sbt-test/actions/input-task-dyn/pending b/sbt-app/src/sbt-test/actions/input-task-dyn/test similarity index 100% rename from sbt-app/src/sbt-test/actions/input-task-dyn/pending rename to sbt-app/src/sbt-test/actions/input-task-dyn/test From 711df2cbf2ded91b13588918b29d2ce6b4f7e813 Mon Sep 17 00:00:00 2001 From: Adrien Piquerez Date: Fri, 7 Feb 2025 12:02:16 +0100 Subject: [PATCH 2/2] Internal usage of inputTaskDyn --- main/src/main/scala/sbt/Defaults.scala | 26 +++++++++---------- .../internal/server/BuildServerProtocol.scala | 19 +++++++------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 2c38e8454..a58d454d2 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -4705,22 +4705,20 @@ trait BuildExtra extends BuildCommon with DefExtra { baseArguments: String* ): Vector[Setting[?]] = { Vector( - scoped := Def - .input(_ => Def.spaceDelimited()) - .flatMapTask { result => - initScoped( - scoped.scopedKey, - ClassLoaders.runner.mapReferenced(Project.mapScope(_.rescope(config))), - ).zipWith(Def.task { - ((config / fullClasspath).value, streams.value, fileConverter.value, result) - }) { (rTask, t) => - (t, rTask) mapN { case ((cp, s, converter, args), r) => - given FileConverter = converter - r.run(mainClass, cp.files, baseArguments ++ args, s.log).get - } + scoped := Def.inputTaskDyn { + val result = Def.spaceDelimited().parsed + initScoped( + scoped.scopedKey, + ClassLoaders.runner.mapReferenced(Project.mapScope(_.rescope(config))), + ).zipWith(Def.task { + ((config / fullClasspath).value, streams.value, fileConverter.value, result) + }) { (rTask, t) => + (t, rTask) mapN { case ((cp, s, converter, args), r) => + given FileConverter = converter + r.run(mainClass, cp.files, baseArguments ++ args, s.log).get } } - .evaluated + }.evaluated ) ++ inTask(scoped)((config / forkOptions) := forkOptionsTask.value) } diff --git a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala index 1144d8f61..9ad019378 100644 --- a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala @@ -747,13 +747,12 @@ object BuildServerProtocol { private inline def bspInputTask[T]( inline taskImpl: (BspFullWorkspace, ScopeFilter) => T ): Def.Initialize[InputTask[T]] = - Def - .input(_ => targetIdentifierParser) - .flatMapTask { targets => - val workspace: BspFullWorkspace = bspFullWorkspace.value.filter(targets) - val filter = ScopeFilter.in(workspace.scopes.values.toList) - Def.task(taskImpl(workspace, filter)) - } + Def.inputTaskDyn { + val targets = targetIdentifierParser.parsed + val workspace: BspFullWorkspace = bspFullWorkspace.value.filter(targets) + val filter = ScopeFilter.in(workspace.scopes.values.toList) + Def.task(taskImpl(workspace, filter)) + } private def jvmEnvironmentItem(): Initialize[Task[JvmEnvironmentItem]] = Def.task { val target = Keys.bspTargetIdentifier.value @@ -866,7 +865,8 @@ object BuildServerProtocol { .map(JsonParser.parseFromString) private def bspRunTask: Def.Initialize[InputTask[Unit]] = - Def.input(_ => jsonParser).flatMapTask { json => + Def.inputTaskDyn { + val json = jsonParser.parsed val runParams = json.flatMap(Converter.fromJson[RunParams]).get val defaultClass = Keys.mainClass.value val defaultJvmOptions = Keys.javaOptions.value @@ -909,7 +909,8 @@ object BuildServerProtocol { } private def bspTestTask: Def.Initialize[InputTask[Unit]] = - Def.input(_ => jsonParser).flatMapTask { json => + Def.inputTaskDyn { + val json = jsonParser.parsed val testParams = json.flatMap(Converter.fromJson[TestParams]).get val workspace = bspFullWorkspace.value