Skip to content

Rename Liftable to ToExpr and Unliftable to FromExpr #10618

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion community-build/community-projects/utest
20 changes: 10 additions & 10 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -809,16 +809,16 @@ class Definitions {
@tu lazy val QuoteMatching_ExprMatch: Symbol = QuoteMatchingClass.requiredMethod("ExprMatch")
@tu lazy val QuoteMatching_TypeMatch: Symbol = QuoteMatchingClass.requiredMethod("TypeMatch")

@tu lazy val LiftableModule: Symbol = requiredModule("scala.quoted.Liftable")
@tu lazy val LiftableModule_BooleanLiftable: Symbol = LiftableModule.requiredMethod("BooleanLiftable")
@tu lazy val LiftableModule_ByteLiftable: Symbol = LiftableModule.requiredMethod("ByteLiftable")
@tu lazy val LiftableModule_ShortLiftable: Symbol = LiftableModule.requiredMethod("ShortLiftable")
@tu lazy val LiftableModule_IntLiftable: Symbol = LiftableModule.requiredMethod("IntLiftable")
@tu lazy val LiftableModule_LongLiftable: Symbol = LiftableModule.requiredMethod("LongLiftable")
@tu lazy val LiftableModule_FloatLiftable: Symbol = LiftableModule.requiredMethod("FloatLiftable")
@tu lazy val LiftableModule_DoubleLiftable: Symbol = LiftableModule.requiredMethod("DoubleLiftable")
@tu lazy val LiftableModule_CharLiftable: Symbol = LiftableModule.requiredMethod("CharLiftable")
@tu lazy val LiftableModule_StringLiftable: Symbol = LiftableModule.requiredMethod("StringLiftable")
@tu lazy val ToExprModule: Symbol = requiredModule("scala.quoted.ToExpr")
@tu lazy val ToExprModule_BooleanToExpr: Symbol = ToExprModule.requiredMethod("BooleanToExpr")
@tu lazy val ToExprModule_ByteToExpr: Symbol = ToExprModule.requiredMethod("ByteToExpr")
@tu lazy val ToExprModule_ShortToExpr: Symbol = ToExprModule.requiredMethod("ShortToExpr")
@tu lazy val ToExprModule_IntToExpr: Symbol = ToExprModule.requiredMethod("IntToExpr")
@tu lazy val ToExprModule_LongToExpr: Symbol = ToExprModule.requiredMethod("LongToExpr")
@tu lazy val ToExprModule_FloatToExpr: Symbol = ToExprModule.requiredMethod("FloatToExpr")
@tu lazy val ToExprModule_DoubleToExpr: Symbol = ToExprModule.requiredMethod("DoubleToExpr")
@tu lazy val ToExprModule_CharToExpr: Symbol = ToExprModule.requiredMethod("CharToExpr")
@tu lazy val ToExprModule_StringToExpr: Symbol = ToExprModule.requiredMethod("StringToExpr")

@tu lazy val QuotedRuntimeModule: Symbol = requiredModule("scala.quoted.runtime.Expr")
@tu lazy val QuotedRuntime_exprQuote : Symbol = QuotedRuntimeModule.requiredMethod("quote")
Expand Down
1 change: 0 additions & 1 deletion compiler/src/dotty/tools/dotc/core/StdNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,6 @@ object StdNames {
val thisPrefix : N = "thisPrefix"
val throw_ : N = "throw"
val toArray: N = "toArray"
val toExpr: N = "toExpr"
val toList: N = "toList"
val toObjectArray : N = "toObjectArray"
val toSeq: N = "toSeq"
Expand Down
35 changes: 24 additions & 11 deletions compiler/src/dotty/tools/dotc/transform/PickleQuotes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -173,23 +173,36 @@ class PickleQuotes extends MacroTransform {
Lambda(lambdaTpe, mkConst).withSpan(body.span)
}

/** Encode quote using Reflection.Literal
*
* Generate the code
* ```scala
* qctx => scala.quoted.ToExpr.{BooleanToExpr,ShortToExpr, ...}.apply(<literalValue>)(qctx)
* ```
* this closure is always applied directly to the actual context and the BetaReduce phase removes it.
*/
def liftedValue(lit: Literal, lifter: Symbol) =
val exprType = defn.QuotedExprClass.typeRef.appliedTo(body.tpe)
val lambdaTpe = MethodType(defn.QuotesClass.typeRef :: Nil, exprType)
def mkToExprCall(ts: List[Tree]) =
ref(lifter).appliedToType(originalTp).select(nme.apply).appliedTo(lit).appliedTo(ts.head)
Lambda(lambdaTpe, mkToExprCall).withSpan(body.span)

def pickleAsValue(lit: Literal) = {
// TODO should all constants be pickled as Literals?
// Should examime the generated bytecode size to decide and performance
def liftedValue(lifter: Symbol) =
ref(lifter).appliedToType(originalTp).select(nme.toExpr).appliedTo(lit)
lit.const.tag match {
case Constants.NullTag => pickleAsLiteral(lit)
case Constants.UnitTag => pickleAsLiteral(lit)
case Constants.BooleanTag => liftedValue(defn.LiftableModule_BooleanLiftable)
case Constants.ByteTag => liftedValue(defn.LiftableModule_ByteLiftable)
case Constants.ShortTag => liftedValue(defn.LiftableModule_ShortLiftable)
case Constants.IntTag => liftedValue(defn.LiftableModule_IntLiftable)
case Constants.LongTag => liftedValue(defn.LiftableModule_LongLiftable)
case Constants.FloatTag => liftedValue(defn.LiftableModule_FloatLiftable)
case Constants.DoubleTag => liftedValue(defn.LiftableModule_DoubleLiftable)
case Constants.CharTag => liftedValue(defn.LiftableModule_CharLiftable)
case Constants.StringTag => liftedValue(defn.LiftableModule_StringLiftable)
case Constants.BooleanTag => liftedValue(lit, defn.ToExprModule_BooleanToExpr)
case Constants.ByteTag => liftedValue(lit, defn.ToExprModule_ByteToExpr)
case Constants.ShortTag => liftedValue(lit, defn.ToExprModule_ShortToExpr)
case Constants.IntTag => liftedValue(lit, defn.ToExprModule_IntToExpr)
case Constants.LongTag => liftedValue(lit, defn.ToExprModule_LongToExpr)
case Constants.FloatTag => liftedValue(lit, defn.ToExprModule_FloatToExpr)
case Constants.DoubleTag => liftedValue(lit, defn.ToExprModule_DoubleToExpr)
case Constants.CharTag => liftedValue(lit, defn.ToExprModule_CharToExpr)
case Constants.StringTag => liftedValue(lit, defn.ToExprModule_StringToExpr)
}
}

Expand Down
2 changes: 1 addition & 1 deletion docs/docs/reference/changed-features/numeric-literals.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ method in the `FromDigits` given instance. That method is defined in terms of a
implementation method `fromDigitsImpl`. Here is its definition:
```scala
private def fromDigitsImpl(digits: Expr[String])(using ctx: Quotes): Expr[BigFloat] =
digits.unlift match {
digits.value match {
case Some(ds) =>
try {
val BigFloat(m, e) = apply(ds)
Expand Down
50 changes: 26 additions & 24 deletions docs/docs/reference/metaprogramming/macros.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,33 +271,33 @@ package quoted

object Expr {
...
def apply[T: Liftable](x: T)(using Quotes): Expr[T] = summon[Liftable[T]].toExpr(x)
def apply[T: ToExpr](x: T)(using Quotes): Expr[T] = summon[ToExpr[T]].toExpr(x)
...
}
```
This method says that values of types implementing the `Liftable` type class can be
converted ("lifted") to `Expr` values using `Expr.apply`.
This method says that values of types implementing the `ToExpr` type class can be
converted to `Expr` values using `Expr.apply`.

Dotty comes with given instances of `Liftable` for
Dotty comes with given instances of `ToExpr` for
several types including `Boolean`, `String`, and all primitive number
types. For example, `Int` values can be converted to `Expr[Int]`
values by wrapping the value in a `Literal` tree node. This makes use
of the underlying tree representation in the compiler for
efficiency. But the `Liftable` instances are nevertheless not _magic_
efficiency. But the `ToExpr` instances are nevertheless not _magic_
in the sense that they could all be defined in a user program without
knowing anything about the representation of `Expr` trees. For
instance, here is a possible instance of `Liftable[Boolean]`:
instance, here is a possible instance of `ToExpr[Boolean]`:
```scala
given Liftable[Boolean] {
given ToExpr[Boolean] {
def toExpr(b: Boolean) =
if (b) '{ true } else '{ false }
}
```
Once we can lift bits, we can work our way up. For instance, here is a
possible implementation of `Liftable[Int]` that does not use the underlying
possible implementation of `ToExpr[Int]` that does not use the underlying
tree machinery:
```scala
given Liftable[Int] {
given ToExpr[Int] {
def toExpr(n: Int) = n match {
case Int.MinValue => '{ Int.MinValue }
case _ if n < 0 => '{ - ${ toExpr(-n) } }
Expand All @@ -307,16 +307,16 @@ given Liftable[Int] {
}
}
```
Since `Liftable` is a type class, its instances can be conditional. For example,
Since `ToExpr` is a type class, its instances can be conditional. For example,
a `List` is liftable if its element type is:
```scala
given [T: Liftable : Type]: Liftable[List[T]] with
given [T: ToExpr : Type]: ToExpr[List[T]] with
def toExpr(xs: List[T]) = xs match {
case head :: tail => '{ ${ Expr(head) } :: ${ toExpr(tail) } }
case Nil => '{ Nil: List[T] }
}
```
In the end, `Liftable` resembles very much a serialization
In the end, `ToExpr` resembles very much a serialization
framework. Like the latter it can be derived systematically for all
collections, case classes and enums. Note also that the synthesis
of _type-tag_ values of type `Type[T]` is essentially the type-level
Expand Down Expand Up @@ -433,7 +433,7 @@ either a constant or is a parameter that will be a constant when instantiated. T
aspect is also important for macro expansion.

To get values out of expressions containing constants `Expr` provides the method
`unlift` (or `unliftOrError`). This will convert the `Expr[T]` into a `Some[T]` (or `T`) when the
`value` (or `valueOrError`). This will convert the `Expr[T]` into a `Some[T]` (or `T`) when the
expression contains value. Otherwise it will retrun `None` (or emit an error).
To avoid having incidental val bindings generated by the inlining of the `def`
it is recommended to use an inline parameter. To illustrate this, consider an
Expand All @@ -442,7 +442,7 @@ implementation of the `power` function that makes use of a statically known expo
inline def power(x: Double, inline n: Int) = ${ powerCode('x, 'n) }

private def powerCode(x: Expr[Double], n: Expr[Int])(using Quotes): Expr[Double] =
n.unlift match
n.value match
case Some(m) => powerCode(x, m)
case None => '{ Math.pow($x, $y) }

Expand Down Expand Up @@ -604,7 +604,7 @@ inline method that can calculate either a value of type `Int` or a value of type
transparent inline def defaultOf(inline str: String) = ${ defaultOfImpl('str) }

def defaultOfImpl(strExpr: Expr[String])(using Quotes): Expr[Any] =
strExpr.unliftOrError match
strExpr.valueOrError match
case "int" => '{1}
case "string" => '{"a"}

Expand Down Expand Up @@ -632,20 +632,22 @@ It is possible to deconstruct or extract values out of `Expr` using pattern matc

`scala.quoted` contains objects that can help extracting values from `Expr`.

* `scala.quoted.Unlifted`: matches an expression of a value (or list of values) and returns the value (or list of values).
* `scala.quoted.Const`/`scala.quoted.Consts`: Same as `Unlifted` but only works on primitive values.
* `scala.quoted.Expr`/`scala.quoted.Exprs`: matches an expression of a value (or list of values) and returns the value (or list of values).
* `scala.quoted.Const`/`scala.quoted.Consts`: Same as `Expr`/`Exprs` but only works on primitive values.
* `scala.quoted.Varargs`: matches an explicit sequence of expressions and returns them. These sequences are useful to get individual `Expr[T]` out of a varargs expression of type `Expr[Seq[T]]`.


These could be used in the following way to optimize any call to `sum` that has statically known values.
```scala
inline def sum(inline args: Int*): Int = ${ sumExpr('args) }
private def sumExpr(argsExpr: Expr[Seq[Int]])(using Quotes): Expr[Int] = argsExpr match {
case Varargs(Unlifted(args)) => // args is of type Seq[Int]
Expr(args.sum) // precompute result of sum
case Varargs(args @ Exprs(argValues)) =>
// args is of type Seq[Expr[Int]]
// argValues is of type Seq[Int]
Expr(argValues.sum) // precompute result of sum
case Varargs(argExprs) => // argExprs is of type Seq[Expr[Int]]
val staticSum: Int = argExprs.map(_.unlift.getOrElse(0))
val dynamicSum: Seq[Expr[Int]] = argExprs.filter(_.unlift.isEmpty)
val staticSum: Int = argExprs.map(_.value.getOrElse(0))
val dynamicSum: Seq[Expr[Int]] = argExprs.filter(_.value.isEmpty)
dynamicSum.foldLeft(Expr(staticSum))((acc, arg) => '{ $acc + $arg })
case _ =>
'{ $argsExpr.sum }
Expand Down Expand Up @@ -682,8 +684,8 @@ private def sumExpr(args1: Seq[Expr[Int]])(using Quotes): Expr[Int] = {
case arg => Seq(arg)
}
val args2 = args1.flatMap(flatSumArgs)
val staticSum: Int = args2.map(_.unlift.getOrElse(0)).sum
val dynamicSum: Seq[Expr[Int]] = args2.filter(_.unlift.isEmpty)
val staticSum: Int = args2.map(_.value.getOrElse(0)).sum
val dynamicSum: Seq[Expr[Int]] = args2.filter(_.value.isEmpty)
dynamicSum.foldLeft(Expr(staticSum))((acc, arg) => '{ $acc + $arg })
}
```
Expand Down Expand Up @@ -759,7 +761,7 @@ private def evalExpr(e: Expr[Int])(using Quotes): Expr[Int] = {
// body: Expr[Int => Int] where the argument represents references to y
evalExpr(Expr.betaReduce(body)(evalExpr(x)))
case '{ ($x: Int) * ($y: Int) } =>
(x.unlift, y.unlift) match
(x.value, y.value) match
case (Some(a), Some(b)) => Expr(a * b)
case _ => e
case _ => e
Expand Down
27 changes: 21 additions & 6 deletions library/src-bootstrapped/scala/quoted/Expr.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,26 @@ object Expr {
Block(statements.map(Term.of), Term.of(expr)).asExpr.asInstanceOf[Expr[T]]
}

/** Lift a value into an expression containing the construction of that value */
def apply[T](x: T)(using lift: Liftable[T])(using Quotes): Expr[T] =
lift.toExpr(x)
/** Creates an expression that will construct the value `x` */
def apply[T](x: T)(using ToExpr[T])(using Quotes): Expr[T] =
scala.Predef.summon[ToExpr[T]].apply(x)

/** Lifts this sequence of expressions into an expression of a sequence
/** Get `Some` of a copy of the value if the expression contains a literal constant or constructor of `T`.
* Otherwise returns `None`.
*
* Usage:
* ```
* case '{ ... ${expr @ Expr(value)}: T ...} =>
* // expr: Expr[T]
* // value: T
* ```
*
* To directly get the value of an expression `expr: Expr[T]` consider using `expr.value`/`expr.valueOrError` insead.
*/
def unapply[T](x: Expr[T])(using FromExpr[T])(using Quotes): Option[T] =
scala.Predef.summon[FromExpr[T]].unapply(x)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to keep this general extractor, a more informative name like Value might be better.

Copy link
Contributor Author

@nicolasstucki nicolasstucki Dec 3, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that one. The issue is that it would either fragment concepts into Expr.apply/Value.unapply or we would need to duplicate some logic between Expr and Value which might lead to confusion.


/** Creates an expression that will construct a copy of this sequence
*
* Transforms a sequence of expression
* `Seq(e1, e2, ...)` where `ei: Expr[T]`
Expand All @@ -43,7 +58,7 @@ object Expr {
def ofSeq[T](xs: Seq[Expr[T]])(using Type[T])(using Quotes): Expr[Seq[T]] =
Varargs(xs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now we have @targetName feature, may be we can rename Expr.ofSeq to Expr.apply? Just to open the discussion, honestly I find Expr.ofSeq is more informative and easy to find.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That won't work, the name of the method is not to distinguish the arguments but the target type. Expr(seq) should this return a Expr[Seq[T]] or a Expr[List[T]]? Also, Exprs might be a better place for it.

Let's keep this for a follow-up PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can only have Value.apply and Value.unapply. This would imply that Expr(v) will not work anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part of the API clearly needs a deeper analysis. I will try it in a separate PR and we can keep this one only to remove the Liftable/Unliftable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Values seems to be a much better name as it captures much better the semantics of the operation in the names. The only downside is that it would affect 99% of the macros.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can only have Value.apply and Value.unapply. This would imply that Expr(v) will not work anymore.

It seems to be Expr(v) is better than Value.apply, as we are constructing a value of Expr[T]. Could you please elaborate?

Copy link
Contributor Author

@nicolasstucki nicolasstucki Dec 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid confusions we need to have Expr.apply/Expr.unapply or Value.apply/Value.unapply.

Would you understand if you relate the Some concept if your code would be like the following?

Some(2) match
  case Just(n) =>
// or
Just(2) match
  case Some(n) =>

I would find it extremely confusing and would probably assume they are different concepts. Even though logically they are the dual of each other.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a difference here: for Trees, we expect there are different cases correspond to different syntactic forms, but for Option we don't have the expectation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be true for trees but here we are on the Expr abstraction which hides those details. In the expression world you don't distinguish '{1}, '{{1}}, '{{{1}}}, ... . Those are only differences that can be seen on Trees.


/** Lifts this list of expressions into an expression of a list
/** Creates an expression that will construct a copy of this list
*
* Transforms a list of expression
* `List(e1, e2, ...)` where `ei: Expr[T]`
Expand All @@ -53,7 +68,7 @@ object Expr {
def ofList[T](xs: Seq[Expr[T]])(using Type[T])(using Quotes): Expr[List[T]] =
if (xs.isEmpty) Expr(Nil) else '{ List(${Varargs(xs)}: _*) }

/** Lifts this sequence of expressions into an expression of a tuple
/** Creates an expression that will construct a copy of this tuple
*
* Transforms a sequence of expression
* `Seq(e1, e2, ...)` where `ei: Expr[Any]`
Expand Down
Loading