From 8d5355d274320ca02b1e51e071f4db9dc7e8850b Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 12 Dec 2021 22:59:18 -0500 Subject: [PATCH] Port Applicative-do macro to Scala 3 See https://eed3si9n.com/sudori-part3/ for details --- .../sbt/internal/util/appmacro/Instance.scala | 231 ------------------ .../internal/util/appmacro/KListBuilder.scala | 85 ------- .../internal/util/appmacro/MixedBuilder.scala | 28 --- .../internal/util/appmacro/TupleBuilder.scala | 70 ------ .../util/appmacro/TupleNBuilder.scala | 67 ----- .../sbt/internal/util/appmacro/Cont.scala | 223 +++++++++++++++++ .../internal/util/appmacro/ContextUtil.scala | 36 +-- .../test/scala/sbt/internal/ContTest.scala | 42 ++++ .../scala/sbt/internal/ContTestMacro.scala | 21 ++ .../src/main/scala/sbt/util/Applicative.scala | 1 + .../src/main/scala/sbt/util/Apply.scala | 1 + .../src/main/scala/sbt/util/FlatMap.scala | 1 + .../src/main/scala/sbt/util/Functor.scala | 1 + .../main/scala/sbt/util/ListInstances.scala | 1 + .../src/main/scala/sbt/util/Monad.scala | 1 + .../main/scala/sbt/util/OptionInstances.scala | 2 + 16 files changed, 306 insertions(+), 505 deletions(-) delete mode 100644 core-macros/src/main/scala-2/sbt/internal/util/appmacro/Instance.scala delete mode 100644 core-macros/src/main/scala-2/sbt/internal/util/appmacro/KListBuilder.scala delete mode 100644 core-macros/src/main/scala-2/sbt/internal/util/appmacro/MixedBuilder.scala delete mode 100644 core-macros/src/main/scala-2/sbt/internal/util/appmacro/TupleBuilder.scala delete mode 100644 core-macros/src/main/scala-2/sbt/internal/util/appmacro/TupleNBuilder.scala create mode 100644 core-macros/src/main/scala/sbt/internal/util/appmacro/Cont.scala create mode 100644 core-macros/src/test/scala/sbt/internal/ContTest.scala create mode 100644 core-macros/src/test/scala/sbt/internal/ContTestMacro.scala diff --git a/core-macros/src/main/scala-2/sbt/internal/util/appmacro/Instance.scala b/core-macros/src/main/scala-2/sbt/internal/util/appmacro/Instance.scala deleted file mode 100644 index 96b282f5f..000000000 --- a/core-macros/src/main/scala-2/sbt/internal/util/appmacro/Instance.scala +++ /dev/null @@ -1,231 +0,0 @@ -/* - * sbt - * Copyright 2011 - 2018, Lightbend, Inc. - * Copyright 2008 - 2010, Mark Harrah - * Licensed under Apache License 2.0 (see LICENSE) - */ - -package sbt.internal.util -package appmacro - -import sbt.internal.util.Classes.Applicative -import sbt.internal.util.Types.Id - -/** - * The separate hierarchy from Applicative/Monad is for two reasons. - * - * 1. The type constructor is represented as an abstract type because a TypeTag cannot represent a - * type constructor directly. 2. The applicative interface is uncurried. - */ -trait Instance { - type M[x] - def app[K[L[x]], Z](in: K[M], f: K[Id] => Z)(implicit a: AList[K]): M[Z] - def map[S, T](in: M[S], f: S => T): M[T] - def pure[T](t: () => T): M[T] -} - -trait MonadInstance extends Instance { - def flatten[T](in: M[M[T]]): M[T] -} - -import scala.reflect.macros._ - -object Instance { - type Aux[M0[_]] = Instance { type M[x] = M0[x] } - type Aux2[M0[_], N[_]] = Instance { type M[x] = M0[N[x]] } - - final val ApplyName = "app" - final val FlattenName = "flatten" - final val PureName = "pure" - final val MapName = "map" - final val InstanceTCName = "M" - - final class Input[U <: Universe with Singleton]( - val tpe: U#Type, - val expr: U#Tree, - val local: U#ValDef - ) - trait Transform[C <: blackbox.Context with Singleton, N[_]] { - def apply(in: C#Tree): C#Tree - } - def idTransform[C <: blackbox.Context with Singleton]: Transform[C, Id] = in => in - - /** - * Implementation of a macro that provides a direct syntax for applicative functors and monads. It - * is intended to be used in conjunction with another macro that conditions the inputs. - * - * This method processes the Tree `t` to find inputs of the form `wrap[T]( input )` This form is - * typically constructed by another macro that pretends to be able to get a value of type `T` from - * a value convertible to `M[T]`. This `wrap(input)` form has two main purposes. First, it - * identifies the inputs that should be transformed. Second, it allows the input trees to be - * wrapped for later conversion into the appropriate `M[T]` type by `convert`. This wrapping is - * necessary because applying the first macro must preserve the original type, but it is useful to - * delay conversion until the outer, second macro is called. The `wrap` method accomplishes this - * by allowing the original `Tree` and `Type` to be hidden behind the raw `T` type. This method - * will remove the call to `wrap` so that it is not actually called at runtime. - * - * Each `input` in each expression of the form `wrap[T]( input )` is transformed by `convert`. - * This transformation converts the input Tree to a Tree of type `M[T]`. The original wrapped - * expression `wrap(input)` is replaced by a reference to a new local `val x: T`, where `x` is a - * fresh name. These converted inputs are passed to `builder` as well as the list of these - * synthetic `ValDef`s. The `TupleBuilder` instance constructs a tuple (Tree) from the inputs and - * defines the right hand side of the vals that unpacks the tuple containing the results of the - * inputs. - * - * The constructed tuple of inputs and the code that unpacks the results of the inputs are then - * passed to the `i`, which is an implementation of `Instance` that is statically accessible. An - * Instance defines a applicative functor associated with a specific type constructor and, if it - * implements MonadInstance as well, a monad. Typically, it will be either a top-level module or a - * stable member of a top-level module (such as a val or a nested module). The `with Singleton` - * part of the type verifies some cases at macro compilation time, while the full check for static - * accessibility is done at macro expansion time. Note: Ideally, the types would verify that `i: - * MonadInstance` when `t.isRight`. With the various dependent types involved, this is not worth - * it. - * - * The `t` argument is the argument of the macro that will be transformed as described above. If - * the macro that calls this method is for a multi-input map (app followed by map), `t` should be - * the argument wrapped in Left. If this is for multi-input flatMap (app followed by flatMap), - * this should be the argument wrapped in Right. - */ - def contImpl[T, N[_]]( - c: blackbox.Context, - i: Instance with Singleton, - convert: Convert, - builder: TupleBuilder, - linter: LinterDSL - )( - t: Either[c.Expr[T], c.Expr[i.M[T]]], - inner: Transform[c.type, N] - )(implicit - tt: c.WeakTypeTag[T], - nt: c.WeakTypeTag[N[T]], - it: c.TypeTag[i.type] - ): c.Expr[i.M[N[T]]] = { - import c.universe.{ Apply => ApplyTree, _ } - - val util = ContextUtil[c.type](c) - val mTC: Type = util.extractTC(i, InstanceTCName) - val mttpe: Type = appliedType(mTC, nt.tpe :: Nil).dealias - - // the tree for the macro argument - val (tree, treeType) = t match { - case Left(l) => (l.tree, nt.tpe.dealias) - case Right(r) => (r.tree, mttpe) - } - // 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(tree.pos) - - val instanceSym = util.singleton(i) - // A Tree that references the statically accessible Instance that provides the actual implementations of map, flatMap, ... - val instance = Ident(instanceSym) - - val isWrapper: (String, Type, Tree) => Boolean = convert.asPredicate(c) - - // Local definitions `defs` in the macro. This is used to ensure references are to M instances defined outside of the macro call. - // Also `refCount` is the number of references, which is used to create the private, synthetic method containing the body - val defs = util.collectDefs(tree, isWrapper) - val checkQual: Tree => Unit = util.checkReferences(defs, isWrapper, mttpe.erasure) - - type In = Input[c.universe.type] - var inputs = List[In]() - - // transforms the original tree into calls to the Instance functions pure, map, ..., - // resulting in a value of type M[T] - def makeApp(body: Tree): Tree = - inputs match { - case Nil => pure(body) - case x :: Nil => single(body, x) - case xs => arbArity(body, xs) - } - - // no inputs, so construct M[T] via Instance.pure or pure+flatten - def pure(body: Tree): Tree = { - val typeApplied = TypeApply(util.select(instance, PureName), TypeTree(treeType) :: Nil) - val f = util.createFunction(Nil, body, functionSym) - val p = ApplyTree(typeApplied, f :: Nil) - if (t.isLeft) p else flatten(p) - } - // m should have type M[M[T]] - // the returned Tree will have type M[T] - def flatten(m: Tree): Tree = { - val typedFlatten = TypeApply(util.select(instance, FlattenName), TypeTree(tt.tpe) :: Nil) - ApplyTree(typedFlatten, m :: Nil) - } - - // calls Instance.map or flatmap directly, skipping the intermediate Instance.app that is unnecessary for a single input - def single(body: Tree, input: In): Tree = { - val variable = input.local - val param = - treeCopy.ValDef(variable, util.parameterModifiers, variable.name, variable.tpt, EmptyTree) - val typeApplied = - TypeApply(util.select(instance, MapName), variable.tpt :: (TypeTree(treeType): Tree) :: Nil) - val f = util.createFunction(param :: Nil, body, functionSym) - val mapped = ApplyTree(typeApplied, input.expr :: f :: Nil) - if (t.isLeft) mapped else flatten(mapped) - } - - // calls Instance.app to get the values for all inputs and then calls Instance.map or flatMap to evaluate the body - def arbArity(body: Tree, inputs: List[In]): Tree = { - val result = builder.make(c)(mTC, inputs) - val param = util.freshMethodParameter(appliedType(result.representationC, util.idTC :: Nil)) - val bindings = result.extract(param) - val f = util.createFunction(param :: Nil, Block(bindings, body), functionSym) - val ttt = TypeTree(treeType) - val typedApp = - TypeApply(util.select(instance, ApplyName), TypeTree(result.representationC) :: ttt :: Nil) - val app = - ApplyTree(ApplyTree(typedApp, result.input :: f :: Nil), result.alistInstance :: Nil) - if (t.isLeft) app else flatten(app) - } - - // Called when transforming the tree to add an input. - // For `qual` of type M[A], and a `selection` qual.value, - // the call is addType(Type A, Tree qual) - // The result is a Tree representing a reference to - // the bound value of the input. - def addType(tpe: Type, qual: Tree, selection: Tree): Tree = { - qual.foreach(checkQual) - val vd = util.freshValDef(tpe, qual.pos, functionSym) - inputs ::= new Input(tpe, qual, vd) - util.refVal(selection, vd) - } - def sub(name: String, tpe: Type, qual: Tree, replace: Tree): Converted[c.type] = { - val tag = c.WeakTypeTag[T](tpe) - convert[T](c)(name, qual)(tag) transform { tree => - addType(tpe, tree, replace) - } - } - - // applies the transformation - linter.runLinter(c)(tree) - val tx = util.transformWrappers(tree, (n, tpe, t, replace) => sub(n, tpe, t, replace)) - // resetting attributes must be: a) local b) done here and not wider or else there are obscure errors - val tr = makeApp(inner(tx)) - val noWarn = q"""($tr: @_root_.scala.annotation.nowarn("cat=other-pure-statement"))""" - c.Expr[i.M[N[T]]](noWarn) - } - - import Types._ - - implicit def applicativeInstance[A[_]](implicit ap: Applicative[A]): Instance.Aux[A] = - new Instance { - type M[x] = A[x] - def app[K[L[x]], Z](in: K[A], f: K[Id] => Z)(implicit a: AList[K]) = a.apply[A, Z](in, f) - def map[S, T](in: A[S], f: S => T) = ap.map(f, in) - def pure[S](s: () => S): M[S] = ap.pure(s()) - } - - def compose[A[_], B[_]](implicit a: Aux[A], b: Aux[B]): Instance.Aux2[A, B] = - new Composed[A, B](a, b) - // made a public, named, unsealed class because of trouble with macros and inference when the Instance is not an object - class Composed[A[_], B[_]](a: Aux[A], b: Aux[B]) extends Instance { - type M[x] = A[B[x]] - def pure[S](s: () => S): A[B[S]] = a.pure(() => b.pure(s)) - def map[S, T](in: M[S], f: S => T): M[T] = a.map(in, (bv: B[S]) => b.map(bv, f)) - def app[K[L[x]], Z](in: K[M], f: K[Id] => Z)(implicit alist: AList[K]): A[B[Z]] = { - val g: K[B] => B[Z] = in => b.app[K, Z](in, f) - a.app[AList.SplitK[K, B]#l, B[Z]](in, g)(AList.asplit(alist)) - } - } -} diff --git a/core-macros/src/main/scala-2/sbt/internal/util/appmacro/KListBuilder.scala b/core-macros/src/main/scala-2/sbt/internal/util/appmacro/KListBuilder.scala deleted file mode 100644 index 067b05601..000000000 --- a/core-macros/src/main/scala-2/sbt/internal/util/appmacro/KListBuilder.scala +++ /dev/null @@ -1,85 +0,0 @@ -/* - * sbt - * Copyright 2011 - 2018, Lightbend, Inc. - * Copyright 2008 - 2010, Mark Harrah - * Licensed under Apache License 2.0 (see LICENSE) - */ - -package sbt.internal.util -package appmacro - -import scala.reflect._ -import macros._ - -/** A `TupleBuilder` that uses a KList as the tuple representation. */ -object KListBuilder extends TupleBuilder { - def make( - c: blackbox.Context - )(mt: c.Type, inputs: Inputs[c.universe.type]): BuilderResult[c.type] = - new BuilderResult[c.type] { - val ctx: c.type = c - val util = ContextUtil[c.type](c) - import c.universe.{ Apply => ApplyTree, _ } - import util._ - - val knilType = c.typeOf[KNil] - val knil = Ident(knilType.typeSymbol.companion) - val kconsTpe = c.typeOf[KCons[Int, KNil, List]] - val kcons = kconsTpe.typeSymbol.companion - val mTC: Type = mt.asInstanceOf[c.universe.Type] - val kconsTC: Type = kconsTpe.typeConstructor - - /** This is the L in the type function [L[x]] ... */ - val tcVariable: TypeSymbol = newTCVariable(util.initialOwner) - - /** Instantiates KCons[h, t <: KList[L], L], where L is the type constructor variable */ - def kconsType(h: Type, t: Type): Type = - appliedType(kconsTC, h :: t :: refVar(tcVariable) :: Nil) - - def bindKList(prev: ValDef, revBindings: List[ValDef], params: List[ValDef]): List[ValDef] = - params match { - case (x @ ValDef(mods, name, tpt, _)) :: xs => - val rhs = select(Ident(prev.name), "head") - val head = treeCopy.ValDef(x, mods, name, tpt, rhs) - util.setSymbol(head, x.symbol) - val tail = localValDef(TypeTree(), select(Ident(prev.name), "tail")) - val base = head :: revBindings - bindKList(tail, if (xs.isEmpty) base else tail :: base, xs) - case Nil => revBindings.reverse - } - - private[this] def makeKList( - revInputs: Inputs[c.universe.type], - klist: Tree, - klistType: Type - ): Tree = - revInputs match { - case in :: tail => - val next = ApplyTree( - TypeApply( - Ident(kcons), - TypeTree(in.tpe) :: TypeTree(klistType) :: TypeTree(mTC) :: Nil - ), - in.expr :: klist :: Nil - ) - makeKList(tail, next, appliedType(kconsTC, in.tpe :: klistType :: mTC :: Nil)) - case Nil => klist - } - - /** The input trees combined in a KList */ - val klist = makeKList(inputs.reverse, knil, knilType) - - /** - * The input types combined in a KList type. The main concern is tracking the heterogeneous - * types. The type constructor is tcVariable, so that it can be applied to [X] X or M later. - * When applied to `M`, this type gives the type of the `input` KList. - */ - val klistType: Type = inputs.foldRight(knilType)((in, klist) => kconsType(in.tpe, klist)) - - val representationC = internal.polyType(tcVariable :: Nil, klistType) - val input = klist - val alistInstance: ctx.universe.Tree = - TypeApply(select(Ident(alist), "klist"), TypeTree(representationC) :: Nil) - def extract(param: ValDef) = bindKList(param, Nil, inputs.map(_.local)) - } -} diff --git a/core-macros/src/main/scala-2/sbt/internal/util/appmacro/MixedBuilder.scala b/core-macros/src/main/scala-2/sbt/internal/util/appmacro/MixedBuilder.scala deleted file mode 100644 index efc7f2596..000000000 --- a/core-macros/src/main/scala-2/sbt/internal/util/appmacro/MixedBuilder.scala +++ /dev/null @@ -1,28 +0,0 @@ -/* - * sbt - * Copyright 2011 - 2018, Lightbend, Inc. - * Copyright 2008 - 2010, Mark Harrah - * Licensed under Apache License 2.0 (see LICENSE) - */ - -package sbt.internal.util -package appmacro - -import scala.reflect._ -import macros._ - -/** - * A builder that uses `TupleN` as the representation for small numbers of inputs (up to - * `TupleNBuilder.MaxInputs`) and `KList` for larger numbers of inputs. This builder cannot handle - * fewer than 2 inputs. - */ -object MixedBuilder extends TupleBuilder { - def make( - c: blackbox.Context - )(mt: c.Type, inputs: Inputs[c.universe.type]): BuilderResult[c.type] = { - val delegate = - if (inputs.size > TupleNBuilder.MaxInputs) (KListBuilder: TupleBuilder) - else (TupleNBuilder: TupleBuilder) - delegate.make(c)(mt, inputs) - } -} diff --git a/core-macros/src/main/scala-2/sbt/internal/util/appmacro/TupleBuilder.scala b/core-macros/src/main/scala-2/sbt/internal/util/appmacro/TupleBuilder.scala deleted file mode 100644 index 7ecdddc01..000000000 --- a/core-macros/src/main/scala-2/sbt/internal/util/appmacro/TupleBuilder.scala +++ /dev/null @@ -1,70 +0,0 @@ -/* - * sbt - * Copyright 2011 - 2018, Lightbend, Inc. - * Copyright 2008 - 2010, Mark Harrah - * Licensed under Apache License 2.0 (see LICENSE) - */ - -package sbt.internal.util -package appmacro - -import scala.reflect._ -import macros._ - -/** - * A `TupleBuilder` abstracts the work of constructing a tuple data structure such as a `TupleN` or - * `KList` and extracting values from it. The `Instance` macro implementation will (roughly) - * traverse the tree of its argument and ultimately obtain a list of expressions with type `M[T]` - * for different types `T`. The macro constructs an `Input` value for each of these expressions that - * contains the `Type` for `T`, the `Tree` for the expression, and a `ValDef` that will hold the - * value for the input. - * - * `TupleBuilder.apply` is provided with the list of `Input`s and is expected to provide three - * values in the returned BuilderResult. First, it returns the constructed tuple data structure Tree - * in `input`. Next, it provides the type constructor `representationC` that, when applied to M, - * gives the type of tuple data structure. For example, a builder that constructs a `Tuple3` for - * inputs `M[Int]`, `M[Boolean]`, and `M[String]` would provide a Type representing `[L[x]] (L[Int], - * L[Boolean], L[String])`. The `input` method would return a value whose type is that type - * constructor applied to M, or `(M[Int], M[Boolean], M[String])`. - * - * Finally, the `extract` method provides a list of vals that extract information from the applied - * input. The type of the applied input is the type constructor applied to `Id` (`[X] X`). The - * returned list of ValDefs should be the ValDefs from `inputs`, but with non-empty right-hand - * sides. - */ -trait TupleBuilder { - - /** A convenience alias for a list of inputs (associated with a Universe of type U). */ - type Inputs[U <: Universe with Singleton] = List[Instance.Input[U]] - - /** Constructs a one-time use Builder for Context `c` and type constructor `tcType`. */ - def make( - c: blackbox.Context - )(tcType: c.Type, inputs: Inputs[c.universe.type]): BuilderResult[c.type] -} - -trait BuilderResult[C <: blackbox.Context with Singleton] { - val ctx: C - import ctx.universe._ - - /** - * Represents the higher-order type constructor `[L[x]] ...` where `...` is the type of the data - * structure containing the added expressions, except that it is abstracted over the type - * constructor applied to each heterogeneous part of the type . - */ - def representationC: PolyType - - /** - * The instance of AList for the input. For a `representationC` of `[L[x]]`, this `Tree` should - * have a `Type` of `AList[L]` - */ - def alistInstance: Tree - - /** Returns the completed value containing all expressions added to the builder. */ - def input: Tree - - /* The list of definitions that extract values from a value of type `$representationC[Id]`. - * The returned value should be identical to the `ValDef`s provided to the `TupleBuilder.make` method but with - * non-empty right hand sides. Each `ValDef` may refer to `param` and previous `ValDef`s in the list.*/ - def extract(param: ValDef): List[ValDef] -} diff --git a/core-macros/src/main/scala-2/sbt/internal/util/appmacro/TupleNBuilder.scala b/core-macros/src/main/scala-2/sbt/internal/util/appmacro/TupleNBuilder.scala deleted file mode 100644 index 349e90065..000000000 --- a/core-macros/src/main/scala-2/sbt/internal/util/appmacro/TupleNBuilder.scala +++ /dev/null @@ -1,67 +0,0 @@ -/* - * sbt - * Copyright 2011 - 2018, Lightbend, Inc. - * Copyright 2008 - 2010, Mark Harrah - * Licensed under Apache License 2.0 (see LICENSE) - */ - -package sbt.internal.util -package appmacro - -import scala.tools.nsc.Global -import scala.reflect._ -import macros._ - -/** - * A builder that uses a TupleN as the tuple representation. It is limited to tuples of size 2 to - * `MaxInputs`. - */ -object TupleNBuilder extends TupleBuilder { - - /** The largest number of inputs that this builder can handle. */ - final val MaxInputs = 11 - final val TupleMethodName = "tuple" - - def make( - c: blackbox.Context - )(mt: c.Type, inputs: Inputs[c.universe.type]): BuilderResult[c.type] = - new BuilderResult[c.type] { - val util = ContextUtil[c.type](c) - import c.universe._ - import util._ - - val global: Global = c.universe.asInstanceOf[Global] - - val ctx: c.type = c - val representationC: PolyType = { - val tcVariable: Symbol = newTCVariable(util.initialOwner) - val tupleTypeArgs = inputs.map(in => - internal.typeRef(NoPrefix, tcVariable, in.tpe :: Nil).asInstanceOf[global.Type] - ) - val tuple = global.definitions.tupleType(tupleTypeArgs) - internal.polyType(tcVariable :: Nil, tuple.asInstanceOf[Type]) - } - - val input: Tree = mkTuple(inputs.map(_.expr)) - val alistInstance: Tree = { - val selectTree = select(Ident(alist), TupleMethodName + inputs.size.toString) - TypeApply(selectTree, inputs.map(in => TypeTree(in.tpe))) - } - def extract(param: ValDef): List[ValDef] = bindTuple(param, Nil, inputs.map(_.local), 1) - - def bindTuple( - param: ValDef, - revBindings: List[ValDef], - params: List[ValDef], - i: Int - ): List[ValDef] = - params match { - case (x @ ValDef(mods, name, tpt, _)) :: xs => - val rhs = select(Ident(param.name), "_" + i.toString) - val newVal = treeCopy.ValDef(x, mods, name, tpt, rhs) - util.setSymbol(newVal, x.symbol) - bindTuple(param, newVal :: revBindings, xs, i + 1) - case Nil => revBindings.reverse - } - } -} diff --git a/core-macros/src/main/scala/sbt/internal/util/appmacro/Cont.scala b/core-macros/src/main/scala/sbt/internal/util/appmacro/Cont.scala new file mode 100644 index 000000000..09b50914f --- /dev/null +++ b/core-macros/src/main/scala/sbt/internal/util/appmacro/Cont.scala @@ -0,0 +1,223 @@ +package sbt +package internal +package util +package appmacro + +import scala.collection.mutable.ListBuffer +import scala.reflect.TypeTest +import scala.quoted.* +import sbt.util.Applicative +import sbt.util.Monad + +/** + * Implementation of a macro that provides a direct syntax for applicative functors and monads. It + * is intended to be used in conjunction with another macro that conditions the inputs. + */ +trait Cont: + final val InstanceTCName = "F" + + extension [C <: Quotes & Singleton](conv: Convert[C]) + /** + * Implementation of a macro that provides a direct syntax for applicative functors. It is + * intended to be used in conjunction with another macro that conditions the inputs. + */ + def contMapN[A: Type, F[_], Effect[_]: Type]( + tree: Expr[A], + inner: conv.TermTransform[Effect] + )(using + iftpe: Type[F], + eatpe: Type[Effect[A]], + ): Expr[F[Effect[A]]] = + contImpl[A, F, Effect](Left(tree), inner) + + /** + * Implementation of a macro that provides a direct syntax for applicative functors. It is + * intended to be used in conjunction with another macro that conditions the inputs. + */ + def contFlatMap[A: Type, F[_], Effect[_]: Type]( + tree: Expr[F[A]], + inner: conv.TermTransform[Effect] + )(using + iftpe: Type[F], + eatpe: Type[Effect[A]], + ): Expr[F[Effect[A]]] = + contImpl[A, F, Effect](Right(tree), inner) + + /** + * Implementation of a macro that provides a direct syntax for applicative functors and monads. + * It is intended to be used in conjunction with another macro that conditions the inputs. + * + * This method processes the Term `t` to find inputs of the form `wrap[A]( input )` This form is + * typically constructed by another macro that pretends to be able to get a value of type `A` + * from a value convertible to `F[A]`. This `wrap(input)` form has two main purposes. First, it + * identifies the inputs that should be transformed. Second, it allows the input trees to be + * wrapped for later conversion into the appropriate `F[A]` type by `convert`. This wrapping is + * necessary because applying the first macro must preserve the original type, but it is useful + * to delay conversion until the outer, second macro is called. The `wrap` method accomplishes + * this by allowing the original `Term` and `Type` to be hidden behind the raw `A` type. This + * method will remove the call to `wrap` so that it is not actually called at runtime. + * + * Each `input` in each expression of the form `wrap[A]( input )` is transformed by `convert`. + * This transformation converts the input Term to a Term of type `F[A]`. The original wrapped + * expression `wrap(input)` is replaced by a reference to a new local `val x: A`, where `x` is a + * fresh name. These converted inputs are passed to `builder` as well as the list of these + * synthetic `ValDef`s. The `TupleBuilder` instance constructs a tuple (Tree) from the inputs + * and defines the right hand side of the vals that unpacks the tuple containing the results of + * the inputs. + * + * The constructed tuple of inputs and the code that unpacks the results of the inputs are then + * passed to the `i`, which is an implementation of `Instance` that is statically accessible. An + * Instance defines a applicative functor associated with a specific type constructor and, if it + * implements MonadInstance as well, a monad. Typically, it will be either a top-level module or + * a stable member of a top-level module (such as a val or a nested module). The `with + * Singleton` part of the type verifies some cases at macro compilation time, while the full + * check for static accessibility is done at macro expansion time. Note: Ideally, the types + * would verify that `i: MonadInstance` when `t.isRight`. With the various dependent types + * involved, this is not worth it. + * + * The `eitherTree` argument is the argument of the macro that will be transformed as described + * above. If the macro that calls this method is for a multi-input map (app followed by map), + * `in` should be the argument wrapped in Left. If this is for multi-input flatMap (app followed + * by flatMap), this should be the argument wrapped in Right. + */ + def contImpl[A: Type, F[_], Effect[_]: Type]( + eitherTree: Either[Expr[A], Expr[F[A]]], + inner: conv.TermTransform[Effect] + )(using + iftpe: Type[F], + eatpe: Type[Effect[A]], + ): Expr[F[Effect[A]]] = + import conv.* + import qctx.reflect.* + given qctx.type = qctx + + val fTypeCon = TypeRepr.of[F] + val faTpe = fTypeCon.appliedTo(TypeRepr.of[Effect[A]]) + val (expr, treeType) = eitherTree match + case Left(l) => (l, TypeRepr.of[Effect[A]]) + case Right(r) => (r, faTpe) + + // we can extract i out of i.type + val instanceExpr = Expr.summon[Applicative[F]].get + val inputBuf = ListBuffer[Input]() + + def makeApp(body: Term, inputs: List[Input]): Expr[F[Effect[A]]] = inputs match + case Nil => pure(body) + case x :: Nil => genMap(body, x) + case xs => genMapN(body, xs) + + // no inputs, so construct F[A] via Instance.pure or pure+flatten + def pure(body: Term): Expr[F[Effect[A]]] = + def pure0[A1: Type](body: Expr[A1]): Expr[F[A1]] = + '{ + $instanceExpr.pure[A1] { $body } + } + eitherTree match + case Left(_) => pure0[Effect[A]](body.asExprOf[Effect[A]]) + case Right(_) => + flatten(pure0[F[Effect[A]]](body.asExprOf[F[Effect[A]]])) + + // m should have type F[F[A]] + // the returned Tree will have type F[A] + def flatten(m: Expr[F[F[Effect[A]]]]): Expr[F[Effect[A]]] = + '{ + { + val i1 = $instanceExpr.asInstanceOf[Monad[F]] + i1.flatten[Effect[A]]($m.asInstanceOf[F[F[Effect[A]]]]) + } + } + + def genMap(body: Term, input: Input): Expr[F[Effect[A]]] = + def genMap0[A1: Type](body: Expr[A1]): Expr[F[A1]] = + input.tpe.asType match + case '[a] => + val tpe = + MethodType(List(input.name))(_ => List(TypeRepr.of[a]), _ => TypeRepr.of[A1]) + val lambda = Lambda( + owner = Symbol.spliceOwner, + tpe = tpe, + rhsFn = (sym, params) => { + val param = params.head.asInstanceOf[Term] + // Called when transforming the tree to add an input. + // For `qual` of type F[A], and a `selection` qual.value, + // the call is addType(Type A, Tree qual) + // The result is a Tree representing a reference to + // the bound value of the input. + def substitute(name: String, tpe: TypeRepr, qual: Term, replace: Term) = + convert[A](name, qual) transform { (tree: Term) => + typed[a](Ref(param.symbol)) + } + transformWrappers(body.asTerm.changeOwner(sym), substitute, sym) + } + ).asExprOf[a => A1] + val expr = input.term.asExprOf[F[a]] + typed[F[A1]]( + '{ + $instanceExpr.map[a, A1]($expr.asInstanceOf[F[a]])($lambda) + }.asTerm + ).asExprOf[F[A1]] + eitherTree match + case Left(_) => + genMap0[Effect[A]](body.asExprOf[Effect[A]]) + case Right(_) => + flatten(genMap0[F[Effect[A]]](body.asExprOf[F[Effect[A]]])) + + def genMapN(body: Term, inputs: List[Input]): Expr[F[Effect[A]]] = + def genMapN0[A1: Type](body: Expr[A1]): Expr[F[A1]] = + val br = makeTuple(inputs) + val lambdaTpe = + MethodType(List("$p0"))(_ => List(br.inputTupleTypeRepr), _ => TypeRepr.of[A1]) + val lambda = Lambda( + owner = Symbol.spliceOwner, + tpe = lambdaTpe, + rhsFn = (sym, params) => { + val p0 = params.head.asInstanceOf[Term] + // Called when transforming the tree to add an input. + // For `qual` of type F[A], and a `selection` qual.value, + // the call is addType(Type A, Tree qual) + // The result is a Tree representing a reference to + // the bound value of the input. + def substitute(name: String, tpe: TypeRepr, qual: Term, replace: Term) = + convert[A](name, qual) transform { (tree: Term) => + val idx = inputs.indexWhere(input => input.term == qual) + Select + .unique(Ref(p0.symbol), "apply") + .appliedToTypes(List(br.inputTupleTypeRepr)) + .appliedToArgs(List(Literal(IntConstant(idx)))) + } + transformWrappers(body.asTerm.changeOwner(sym), substitute, sym) + } + ) + val tupleMapRepr = TypeRepr + .of[Tuple.Map] + .appliedTo(List(br.inputTupleTypeRepr, TypeRepr.of[F])) + tupleMapRepr.asType match + case '[tupleMap] => + br.inputTupleTypeRepr.asType match + case '[inputTypeTpe] => + '{ + given Applicative[F] = $instanceExpr + AList.tuple.mapN[F, A1, inputTypeTpe & Tuple](${ + br.tupleExpr.asInstanceOf[Expr[Tuple.Map[inputTypeTpe & Tuple, F]]] + })( + ${ lambda.asExprOf[inputTypeTpe => A1] } + ) + } + + eitherTree match + case Left(_) => + genMapN0[Effect[A]](body.asExprOf[Effect[A]]) + case Right(_) => + flatten(genMapN0[F[Effect[A]]](body.asExprOf[F[Effect[A]]])) + + // Called when transforming the tree to add an input. + // For `qual` of type F[A], and a `selection` qual.value. + def record(name: String, tpe: TypeRepr, qual: Term, replace: Term) = + convert[A](name, qual) transform { (tree: Term) => + inputBuf += Input(tpe, qual, freshName("q")) + replace + } + val tx = transformWrappers(expr.asTerm, record, Symbol.spliceOwner) + val tr = makeApp(inner(tx), inputBuf.toList) + tr +end Cont diff --git a/core-macros/src/main/scala/sbt/internal/util/appmacro/ContextUtil.scala b/core-macros/src/main/scala/sbt/internal/util/appmacro/ContextUtil.scala index 9969c0df4..7c2503c02 100644 --- a/core-macros/src/main/scala/sbt/internal/util/appmacro/ContextUtil.scala +++ b/core-macros/src/main/scala/sbt/internal/util/appmacro/ContextUtil.scala @@ -9,30 +9,6 @@ trait ContextUtil[C <: Quotes & scala.Singleton](val qctx: C): import qctx.reflect.* given qctx.type = qctx - /** - * Returns a Type representing the type constructor tcp.. For example, given `object Demo { - * type M[x] = List[x] }`, the call `extractTypeCon(Demo, "M")` will return a type representing - * the type constructor `[x] List[x]`. - */ - def extractTypeCon(tcp: AnyRef & scala.Singleton, name: String)(using - tcpt: Type[tcp.type] - ): TypeRepr = - val tcpTpe = TypeRepr.of[tcp.type] - val fSym = tcpTpe.typeSymbol.declaredType(name).head - val typeConTpe: TypeRepr = tcpTpe.memberType(fSym) - val hiRepr = typeConTpe match - case TypeBounds(low, TypeLambda(_, _, AppliedType(tc, _))) => tc - hiRepr - - /** - * Returns a reference given a singleton/termref - */ - def extractSingleton[A: Type]: Expr[A] = - def termRef(r: TypeRepr)(using rtt: TypeTest[TypeRepr, TermRef]): Ref = r match - case rtt(ref) => Ref.term(ref) - case _ => sys.error(s"expected termRef but got $r") - termRef(TypeRepr.of[A]).asExprOf[A] - private var counter: Int = -1 def freshName(prefix: String): String = counter = counter + 1 @@ -58,6 +34,18 @@ trait ContextUtil[C <: Quotes & scala.Singleton](val qctx: C): def typed[A: Type](value: Term): Term = Typed(value, TypeTree.of[A]) + def makeTuple(inputs: List[Input]): BuilderResult = + new BuilderResult: + override def inputTupleTypeRepr: TypeRepr = + tupleTypeRepr(inputs.map(_.tpe)) + override def tupleExpr: Expr[Tuple] = + Expr.ofTupleFromSeq(inputs.map(_.term.asExpr)) + + trait BuilderResult: + def inputTupleTypeRepr: TypeRepr + def tupleExpr: Expr[Tuple] + end BuilderResult + def tupleTypeRepr(param: List[TypeRepr]): TypeRepr = param match case x :: xs => TypeRepr.of[scala.*:].appliedTo(List(x, tupleTypeRepr(xs))) diff --git a/core-macros/src/test/scala/sbt/internal/ContTest.scala b/core-macros/src/test/scala/sbt/internal/ContTest.scala new file mode 100644 index 000000000..357ae990c --- /dev/null +++ b/core-macros/src/test/scala/sbt/internal/ContTest.scala @@ -0,0 +1,42 @@ +package sbt.internal + +import sbt.internal.util.appmacro.* +import verify.* +import ContTestMacro.* +import sbt.util.Applicative + +object ContTest extends BasicTestSuite: + test("pure") { + given Applicative[List] = sbt.util.ListInstances.listMonad + val actual = contMapNMacro[List, Int](12) + assert(actual == List(12)) + } + + test("getMap") { + given Applicative[List] = sbt.util.ListInstances.listMonad + val actual = contMapNMacro[List, Int](ContTest.wrapInit(List(1)) + 2) + assert(actual == List(3)) + } + + test("getMapN") { + given Applicative[List] = sbt.util.ListInstances.listMonad + val actual = contMapNMacro[List, Int]( + ContTest.wrapInit(List(1)) + + ContTest.wrapInit(List(2)) + 3 + ) + assert(actual == List(6)) + } + + test("getMapN2") { + given Applicative[List] = sbt.util.ListInstances.listMonad + val actual = contMapNMacro[List, Int]({ + val x = ContTest.wrapInit(List(1)) + val y = ContTest.wrapInit(List(2)) + x + y + 3 + }) + assert(actual == List(6)) + } + + // This compiles away + def wrapInit[A](a: List[A]): A = ??? +end ContTest diff --git a/core-macros/src/test/scala/sbt/internal/ContTestMacro.scala b/core-macros/src/test/scala/sbt/internal/ContTestMacro.scala new file mode 100644 index 000000000..1bd76c168 --- /dev/null +++ b/core-macros/src/test/scala/sbt/internal/ContTestMacro.scala @@ -0,0 +1,21 @@ +package sbt.internal + +import sbt.internal.util.Types.Id +import sbt.internal.util.appmacro.* +import sbt.util.Applicative +import scala.quoted.* +import ConvertTestMacro.InputInitConvert + +object ContTestMacro: + inline def contMapNMacro[F[_]: Applicative, A](inline expr: A): List[A] = + ${ contMapNMacroImpl[F, A]('expr) } + + def contMapNMacroImpl[F[_]: Type, A: Type](expr: Expr[A])(using + qctx: Quotes + ): Expr[List[A]] = + object ContSyntax extends Cont + import ContSyntax.* + val convert1: Convert[qctx.type] = new InputInitConvert(qctx) + convert1.contMapN[A, List, Id](expr, convert1.idTransform) + +end ContTestMacro diff --git a/util-collection/src/main/scala/sbt/util/Applicative.scala b/util-collection/src/main/scala/sbt/util/Applicative.scala index 39752157a..340e43b72 100644 --- a/util-collection/src/main/scala/sbt/util/Applicative.scala +++ b/util-collection/src/main/scala/sbt/util/Applicative.scala @@ -16,4 +16,5 @@ end Applicative object Applicative: given Applicative[Option] = OptionInstances.optionMonad + given Applicative[List] = ListInstances.listMonad end Applicative diff --git a/util-collection/src/main/scala/sbt/util/Apply.scala b/util-collection/src/main/scala/sbt/util/Apply.scala index ea5ec6ccf..0bcd35e25 100644 --- a/util-collection/src/main/scala/sbt/util/Apply.scala +++ b/util-collection/src/main/scala/sbt/util/Apply.scala @@ -16,4 +16,5 @@ end Apply object Apply: given Apply[Option] = OptionInstances.optionMonad + given Apply[List] = ListInstances.listMonad end Apply diff --git a/util-collection/src/main/scala/sbt/util/FlatMap.scala b/util-collection/src/main/scala/sbt/util/FlatMap.scala index 9e644b403..310c9492b 100644 --- a/util-collection/src/main/scala/sbt/util/FlatMap.scala +++ b/util-collection/src/main/scala/sbt/util/FlatMap.scala @@ -19,4 +19,5 @@ end FlatMap object FlatMap: given FlatMap[Option] = OptionInstances.optionMonad + given FlatMap[List] = ListInstances.listMonad end FlatMap diff --git a/util-collection/src/main/scala/sbt/util/Functor.scala b/util-collection/src/main/scala/sbt/util/Functor.scala index 983954f02..ffbc175c6 100644 --- a/util-collection/src/main/scala/sbt/util/Functor.scala +++ b/util-collection/src/main/scala/sbt/util/Functor.scala @@ -13,4 +13,5 @@ end Functor object Functor: given Functor[Option] = OptionInstances.optionMonad + given Functor[List] = ListInstances.listMonad end Functor diff --git a/util-collection/src/main/scala/sbt/util/ListInstances.scala b/util-collection/src/main/scala/sbt/util/ListInstances.scala index 32c6904d5..d88b85c6b 100644 --- a/util-collection/src/main/scala/sbt/util/ListInstances.scala +++ b/util-collection/src/main/scala/sbt/util/ListInstances.scala @@ -10,6 +10,7 @@ package sbt.util private[sbt] object ListInstances: lazy val listMonad: Monad[List] = new Monad[List]: + type F[a] = List[a] def pure[A](x: A): List[A] = List(x) def ap[A, B](ff: List[A => B])(fa: List[A]): List[B] = for diff --git a/util-collection/src/main/scala/sbt/util/Monad.scala b/util-collection/src/main/scala/sbt/util/Monad.scala index 64403e368..ab199dbca 100644 --- a/util-collection/src/main/scala/sbt/util/Monad.scala +++ b/util-collection/src/main/scala/sbt/util/Monad.scala @@ -16,4 +16,5 @@ end Monad object Monad: given Monad[Option] = OptionInstances.optionMonad + given Monad[List] = ListInstances.listMonad end Monad diff --git a/util-collection/src/main/scala/sbt/util/OptionInstances.scala b/util-collection/src/main/scala/sbt/util/OptionInstances.scala index 3dadfff95..774cebeee 100644 --- a/util-collection/src/main/scala/sbt/util/OptionInstances.scala +++ b/util-collection/src/main/scala/sbt/util/OptionInstances.scala @@ -10,6 +10,8 @@ package sbt.util private[sbt] object OptionInstances: lazy val optionMonad: Monad[Option] = new Monad[Option]: + type F[a] = Option[a] + def pure[A](x: A): Option[A] = Some(x) def ap[A, B](ff: Option[A => B])(fa: Option[A]): Option[B] = if ff.isDefined && fa.isDefined then Some(ff.get(fa.get))