Nicolas Stucki
LAMP/EPFL
Scala 2.x macros
Coupled with scalac internals
Portability problem
Dotty macros
Portable (TASTy)
Simpler and safer
Dotty language features
Achieve the same without macros
As a metaprogramming feature
inline def log[T](msg: String)(thunk: => T): T = ...
Guaranteed inline
Potentially recursive
Potentially type specializing return type
Potentially macro entry point
object Logger {
var indent = 0
inline def log[T](msg: String)(thunk: => T): T =
println(s"${" " * indent}start $msg")
indent += 1
val result = thunk
indent -= 1
println(s"${" " * indent}$msg = $result")
result
}
}
Logger.log("123L^5") {
power(123L, 5)
}
val msg = "123L^5"
println(s"${" " * indent}start $msg")
Logger.indent += 1
val result = power(123L, 5)
Logger.indent -= 1
println(s"${" " * indent}$msg = $result")
result
By-value parameter is placed in a val
By-name parameters are inlined directly
Special handling for private references
expands to
object Logger {
private var indent = 0
inline def log[T](msg: String)(op: => T): T = {
println(s"${" " * indent}start $msg")
indent += 1
val result = op
indent -= 1
println(s"${" " * indent}$msg = $result")
result
}
}
object Logger {
private var indent = 0
def inline$indent: Int = indent
def inline$indent_=(x$0: Int): Unit = indent = x$0
inline def log[T](msg: String)(op: => T): T = {
println(s"${" " * indent}start $msg")
inline$indent = inline$indent + 1
val result = op
inline$indent = inline$indent - 1
println(s"${" " * indent}$msg = $result")
result
}
}
Accessors will be generated for unacessible references
inline def power(x: Long, n: Int): Long = {
if (n == 0)
1L
else if (n % 2 == 1)
x * power(x, n - 1)
else {
val y: Long = x * x
power(y, n / 2)
}
}
We can call recursively an inline method
val x = expr
if (10 == 0)
// 1L
else if (10 % 2 == 1)
// x * power(x, 10 - 1)
else {
val y = x * x
power(y, 10 / 2)
}
val x = expr
val y = x * x
power(y, 5)
inline the code while simplifying
resulting in
val x = expr
val y = x * x
y * {
val y2 = y * y
val y3 = y2 * y2
y3 * 1L
}
at the end the code will be
power(x, 10)
recursively inline
def badPower(x: Long, n: Int): Int = {
power(x, n)
}
What if n is not statically known?
def badPower(x: Long, n: Int): Int = {
if (n == 0)
1L
else if (n % 2 == 1)
x * {
if (n - 1 == 0)
1L
else if ((n-1) % 2 == 1)
x * power(x, (n-1) - 1)
else {
val y = x * x
power(y, (n-1) / 2)
}
}
else {
val y = x * x
if (n - 1 == 0)
1L
else if ((n/1) % 2 == 1)
y * power(y, (n/2) - 1)
else {
val y2 = y * y
power(y2, (n/2) / 2)
}
}
}
...
...
def badPower(x: Long, n: Int): Int = {
power(x, n) // error: n is not a known constant
}
inline def power(x: Long, inline n: Int): Int = ...
The argument must be a known constant value
Primitive values: Boolean, Int, Double, String, ....
Some case classes: Option, ...
inline def power(x: Long, n: Int): Long = {
inline if (n == 0)
1L
else inline if (n % 2 == 1)
x * power(x, n - 1)
else {
val y: Long = x * x
power(y, n / 2)
}
}
def badPower(x: Long, n: Int): Int = {
power(x, n)
// error: cannot reduce inline if
// its condition n == 0 is not a constant value
// This location is in code that was inlined at ...
}
condition must be reduced
only one branch will remain
inline def toInt(n: Nat): Int =
inline n match {
case Zero => 0
case Succ(n1) => toInt(n1) + 1
}
val natTwo = toInt(Succ(Succ(Zero)))
trait Nat
case object Zero extends Nat
case class Succ[N <: Nat](n: N) extends Nat
one case must match the scrutinee
only one case will remain
Peano number as ADT
val natTwo: Int = 2
inlined as
Return type is going to be specialized
to a more precise type upon expansion
inline def toInt(n: Nat) <: Int = ...
val natTwo: 2 = 2
val natTwo = toInt(Succ(Succ(Zero)))
inlined as
val natTwo: Int = 2
instead of
class A
class B extends A {
def meth(): Unit = ...
}
val a: A = choose(true)
val b: B = choose(false)
// error: meth() not defined on A
choose(true).meth()
// Ok
choose(false).meth()
inline def choose(b: Boolean) <: A =
inline if (b) new A()
else new B()
The result type of choose is a subtype of A
but not necessarily A
inline def setFor[T]: Set[T] =
implicit match {
case ord: Ordering[T] => new TreeSet[T]
case _ => new HashSet[T]
}
setFor[String] // new TreeSet(scala.math.Ordering.String)
setFor[Object] // new HashSet
for each case try to find an implicit of that type
if found use that branch
import scala.collection.immutable._
Computing New Types
type Elem[X] = X match {
case String => Char
case Array[t] => t
case Iterable[t] => t
}
Elem[String] =:= Char
Elem[Array[Int]] =:= Int
Elem[List[Float]] =:= Float
Elem[Nil.type] =:= Nothing
Elem[X] will have the type of the first case that matches X
def addEventListener(tpe: String)(action: dom.Event => Any): Unit = js.native
but the type is "click" the event will be of type dom.MouseEvent
In Scala.js we can define the interface
addEventListener("click") { (e: dom.Event) => ... }
which will be used as
type EventTypeOf[Tp <: String] <: dom.Event = Tp match {
case "click" => dom.MouseEvent
...
case _ => dom.Event
}
def addEventListener[Tp <: String, Ev <: EventTypeOf[Tp]](tpe: Tp)(e: Ev => Any): Unit = js.native
if Tp is a know singleton string we can refine the type of the event
addEventListener("click") { (e: dom.MouseEvent) => ... }
T1 *: T2 *: ... *: Tn *: Unit
type Unit <: Tuple
type *:[H, T <: Tuple] <: Tuple
A tuple type
x1 *: x2 *: ... *: xn *: ()
A tuple term
type Unit <: Tuple
type *:[H, T <: Tuple] <: Tuple
trait Tuple {
inline def *: [H](x: H): H *: this.type = ...
inline def ++ [That <: Tuple](that: That): Concat[this.type, That] = ...
inline def size: Size[this.type] = ...
...
}
Some operation on Tuples
type Concat[X <: Tuple, +Y <: Tuple] <: Tuple = X match {
case Unit => Y
case head *: tail => head *: Concat[tail, Y]
}
just like concat on a list
def concat(x: List[Int], y: List[Int]): List[Int] = x match {
case Nil => y
case head :: tail => head :: concat(tail, y)
}
import scala.compiletime.S
type Size[X <: Tuple] <: Int = X match {
case Unit => 0
case x *: xs => S[Size[xs]]
}
type S[N <: Int] <: Int = N match {
case 0 => 1
case 1 => 2
case 2 => 3
...
}
Successor of a literal Int
Intrinsified in the compiler
import scala.compiletime.constValueOpt
trait Tuple {
inline def size: Size[this.type] = {
inline constValueOpt[Size[this.type]] match {
case Some(n) => n
case _ => computeSizeAtRuntimeOn(this)
}
}
...
}
inline def constValue[T]: T
inline def constValueOpt[T]: Option[T]
Value of the literal type
With Quotes & Splices
Quoting expressions ...
val expr: Expr[T] = '{ e }
val t: Type[T] = '[ T ]
'{
val e: T = ${ expr }
}
Quoting types...
and splicing them inside quotes
'{
val e2: ${ t } = e
}
and splicing them inside quotes
import scala.quoted.{Expr, Type}
val x: String = ...
def f(x: String): String = ...
s"println($x)"
s"println(${ f(x) })"
val x: Expr[String] = ...
def f(x: Expr[String]): Expr[String] = ...
'{ println($x) }
'{ println(${ f(x) }) }
Syntax follows similar rules
of string interpolators
import scala.quoted.{Expr, Type}
def f(x: Expr[String]): Expr[String] = ...
'{
val x: String = ...
println(${ f('x) })
}
Code inside quotes
can still be referred inside
import scala.quoted._
Code that will run while compiling
Code that the user will write
${...} outside a '{...} only as body of inline method
val x = 3L
val res = power(x, 10)
inline def power(x: Long, inline n: Int): Long =
${ powerExpr('x, n) }
private def powerExpr(x: Expr[Long], n: Int): Expr[Long] = ...
inline def power(x: Long, inline n: Int): Long = {
inline if (n == 0)
1L
else inline if (n % 2 == 1)
x * power(x, n - 1)
else {
val y: Long = x * x
power(y, n / 2)
}
}
def powerExpr(x: Expr[Long], n: Int): Expr[Long] = {
if (n == 0)
'{ 1L }
else if (n % 2 == 1)
'{ $x * ${ powerExpr(x, n - 1) } }
else '{
val y: Long = $x * $x
${ powerExpr('y, n / 2) }
}
}
import scala.quoted._
Conditions can be constant folded by user code
Arbitrary compiled code
Conditions must be constant foldable by the compiler
Low syntactic overhead
def powerCode(x: Expr[Long], n: Int): Expr[Long] = {
if (n == 0)
'{ 1L }
else if (n % 2 == 1)
'{ $x * ${ powerCode(x, n - 1) } }
else '{
val y: Long = $x * $x
${ powerCode('y, n / 2) }
}
}
powerCode('{x}, 0)
// generates the code
'{ 1L }
powerCode('{x}, 2)
// generates the code
'{ val y = x * x; y }
powerCode('{x}, 4)
// generates the code
'{ val y = x * x; y * y }
import scala.quoted._
powerCode generates an Expr containing the generated code
"For any free variable reference, the number of quoted scopes and the number of spliced scopes between the reference and its definition must be equal"
def powerExpr(x: Expr[Long], n: Int): Expr[Long] = {
if (n == 0)
'{ 1L }
else if (n % 2 == 1)
'{ $x * ${ powerExpr(x, n - 1) } }
else '{
val y: Long = $x * $x
${ powerExpr('y, n / 2) }
}
}
import scala.quoted._
def (x: T) toExpr[T: Liftable]: Expr[T] = ...
val one: Expr[Int] = 1.toExpr
val hello: Expr[String] = "hello".toExpr
import scala.quoted._
Liftable type class
Extensible in libraries
implied for Liftable[Boolean] {
def toExpr(bool: Boolean): Expr[Boolean] =
if (bool) '{true} else '{false}
}
implied [T: Liftable: Type] for Liftable[List[T]] {
def toExpr(list: List[T]): Expr[List[T]] = list match {
case x :: xs => '{ ${x.toExpr} :: ${toExpr(xs)} }
case Nil => '{ List.empty[T] }
}
}
inline def whiteboxMacro(...) <: X = ${ ... }
Simply combination
specialized return type
macro $
import scala.quoted.Expr
import scala.quoted.matching.Const
import scala.tasty.Reflection
Analyzing contents of Expr
inline def swap(tuple: =>(Int, Long)): (Long, Int) =
${ swapExpr('tuple) }
val x = (1, 2L)
swap(x) // inlines: x.swap
swap((3, 4L)) // inlines: (4L, 3)
def swapExpr(tuple: Expr[(Int, Long)]) given Reflection: Expr[(Long, Int)] = {
tuple match {
case '{ ($x1, $x2) } => '{ ($x2, $x1) }
case _ => '{ $tuple.swap }
}
}
import scala.quoted.Expr
import scala.quoted.matching.Const
import scala.tasty.Reflection
// vanilla power implementation
def power(x: Long, n: Int): Int = ...
// power optimized for n
def powerExpr(x: Expr[Long], n: Int): Expr[Int] = ...
Analyzing contents of Expr
def powerExpr2(x: Expr[Long], n: Expr[Int]) given Reflection: Expr[Long] = {
(x, n) match {
case (Const(y), Const(k)) => power(y, k).toExpr
case ( _, Const(k)) => powerExpr(x, k)
case _ => '{ power($x, $n) }
}
}
inline def (self: => StringContext) S (args: => String*): String =
${ sExpr('self, 'args) }
def sExpr(self: Expr[StringContext], args: Expr[Seq[String]]) given Reflection: Expr[String] = {
self match {
case '{ StringContext(${ConstSeq(parts)}: _*) } =>
val upprerParts: List[String] = parts.toList.map(_.toUpperCase)
val upprerPartsExpr: Expr[List[String]] = upprerParts.map(_.toExpr).toExprOfList
'{ StringContext($upprerPartsExpr: _*).s($args: _*) }
case _ => ...
}
}
Example from a string interpolation macro
import scala.quoted.Expr
import scala.quoted.matching.Const
import scala.tasty.Reflection
StringContext("hello ", "!").S(world)
Analyzing arguments to extract constants
With TASTy Reflection
(low-level tree API)
import scala.tasty.Reflection
object Const {
def unapply[T](expr: Expr[T]) given (refl: Reflection): Option[T] = {
import refl._
def rec(tree: Term): Option[T] = tree match {
case Literal(c) => Some(c.value.asInstanceOf[T])
case Block(Nil, e) => rec(e)
case _ => None
}
rec(expr.unseal)
}
}
Look into the typed AST of the expression
Types not statically know
A macro can pass an given instance of scala.tasty.Reflection
Reflection is used by the '{ ... } and Const(...) paterns
def powerExpr2(x: Expr[Long], n: Expr[Int]) given Reflection: Expr[Long] = {
(x, n) match {
case (Const(y), Const(k)) => power(y, k).toExpr
case ( _, Const(k)) => powerExpr(x, k)
case _ => '{ power($x, $n) }
}
}
import scala.quoted.Expr
import scala.quoted.matching.Const
import scala.tasty.Reflection
Shapeless 3
Features used: specializing inline def, inline matches, implicit match
scala.Tuple
Features used: Match types, inline
Scalatest
Features used: macros
f-interpolator and xml-interpolator
Features used: macros, specializing inline def
https://slides.com/nicolasstucki/scala-days-2019