[2.0.x] Fix Scala 3 .previous expansion for unstable path (#9041) (#9075)

**problem**
.previous was implemented as an inline expansion to wrapInitTask(Previous.runtime(in)(<instance of JsonFormat[A1]))

For example, the .previous on taskKey[String] gets inlined like
this, and then sbt's macro moves `Previous.runtime(...)(using StringJsonFormat)` into task input.

lazy val fingerprints = taskKey[String]("...")
val previousFingerprints = fingerprints.previous

// inlined
val previousFingerprints: Option[String] = {
  InputWrapper.wrapInitTask(
    Previous.runtime(...)(using StringJsonFormat)
  )
}

- 6c8ee6ea37/main-settings/src/main/scala/sbt/Def.scala (L410-L412)
- 6c8ee6ea37/core-macros/src/main/scala/sbt/internal/util/appmacro/Cont.scala (L468)

However, if the return type of task is a bit more complicated like
Seq[String], it doesn't work:

Scala 3 Compiler's inliner creates a synthetic proxy symbol if the
inlining tree is an application, whose arguments are unstable path.

For example,

lazy val fingerprints = taskKey[Seq[String]]("...")
val previousFingerprints = fingerprints.previous

// inline to
val previousFingerprints: Option[Seq[String]] = {
  val x$2$proxy1 = immSeqFormat(StringJsonFormat)
  InputWrapper.wrapInitTask(
    Previous.runtime(...)(using x$2$proxy1)
  )
}

cc7d6db700/compiler/src/dotty/tools/dotc/inlines/Inliner.scala (L324-L329)

However, sbt2's Cont macro captures only `Previous.runtime(...)(using x$2$proxy1)`, while it doesn't capture the proxy definition. Consequently, while sbt macro moves the `Previous.runtime(...)` application as a task input, the proxy definition is left in the task body.

mapN(
  (
    link / fingerprints,
    Previous.runtime(...)(using x$2$proxy1) // here x$2$proxy1 can't be found
  )
) {
  ...
  val x$2$proxy1 = ...
}

Then we get:

-- Error: /.../build.sbt:14:59
14 |  val previousFingerprints = (link / fingerprints).previous
   |                                                           ^
   |While expanding a macro, a reference to value x$2$proxy1 was used outside the scope where it was defined

**How this PR fixed**
This commit fixes the problem by defining a dedicated Scala3 macro for .previous that summon JsonFormat[A1] inside the macro before constructing the wrapped previous.runtime(...)(using ...) by inliner. The macro insert the found given value tree directly into the previous.runtime(...)(using $found).

This way, Cont macro always moves the Previous.runtime tree along with it's given argument, without leaking compiler-generated inline proxies across scopes.

Co-authored-by: Rikito Taniguchi <rikitotaniguchi@proton.me>
This commit is contained in:
eugene yokota 2026-04-12 06:26:19 -04:00 committed by GitHub
parent 879ef86567
commit 53b36840bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 37 additions and 2 deletions

View File

@ -404,8 +404,16 @@ object Def extends BuildSyntax with Init with InitializeImplicits:
extension [A1](inline in: TaskKey[A1])
// implicit def macroPrevious[T](@deprecated("unused", "") in: TaskKey[T]): MacroPrevious[T] = ???
inline def previous(using JsonFormat[A1]): Option[A1] =
InputWrapper.`wrapInitTask_\u2603\u2603`[Option[A1]](Previous.runtime[A1](in))
// Previously, we implemented `.previous` as a plain inline expansion of
// `wrapInitTask(Previous.runtime(in))`. For composite types such as `Seq[String]`, the
// compiler can rewrite the format argument into a synthetic local like
// `{ val x$2$proxy1 = immSeqFormat(StringJsonFormat); Previous.runtime(in)(using x$2$proxy1) }`.
// The outer task macro later hoists only `Previous.runtime(...)` as an input dependency,
// leaving `x$2$proxy1` behind and causing "used outside the scope where it was defined".
// Now we implement `.previous` as a macro so the wrapped tree is already self-contained.
// See https://github.com/sbt/sbt/issues/9037.
inline def previous(using @scala.annotation.unused format: JsonFormat[A1]): Option[A1] =
${ TaskMacro.previousImpl[A1]('in) }
// The following conversions enable the types Parser[T], Initialize[Parser[T]], and
// Initialize[State => Parser[T]] to be used in the inputTask macro as an input with an ultimate

View File

@ -17,6 +17,7 @@ import sbt.internal.util.{ SourcePosition, SourcePositionImpl }
import language.experimental.macros
import scala.quoted.*
import sbt.util.BuildWideCacheConfiguration
import sjsonnew.JsonFormat
object TaskMacro:
final val AssignInitName = "set"
@ -120,6 +121,15 @@ object TaskMacro:
val convert1 = new FullConvert(qctx, 1000)
convert1.contFlatMap[A1, F, Id](t, convert1.appExpr, None)
def previousImpl[A1: Type](t: Expr[TaskKey[A1]])(using qctx: Quotes): Expr[Option[A1]] =
import qctx.reflect.*
Expr.summon[JsonFormat[A1]] match
case Some(ev) =>
'{
InputWrapper.`wrapInitTask_\u2603\u2603`[Option[A1]](Previous.runtime[A1]($t)(using $ev))
}
case _ => report.errorAndAbort(s"JsonFormat[${Type.show[A1]}] missing")
/** Implementation of := macro for settings. */
def settingAssignMacroImpl[A1: Type](rec: Expr[Scoped.DefinableSetting[A1]], v: Expr[A1])(using
qctx: Quotes

View File

@ -106,6 +106,23 @@ class TaskPosSpec {
}
}
locally {
// .previous should compile for task with complex return type like `Seq[String]`
// https://github.com/sbt/sbt/issues/9037
import sbt.*, Def.*
import sjsonnew.BasicJsonProtocol.given
val link = taskKey[Int]("")
val fingerprints = taskKey[Seq[String]]("")
Def.taskDyn[Int] {
val currentFingerprints = (link / fingerprints).value
val previousFingerprints = (link / fingerprints).previous
Def.task {
if previousFingerprints.exists(_ != currentFingerprints) then currentFingerprints.size
else 0
}
}
}
locally {
// missing .value error should not happen inside task dyn
import sbt.*, Def.*