Metaprogramming

in Dotty

Nicolas Stucki

LAMP/EPFL

Scala 2.x Macros

  • 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
  • Inline: As a metaprogramming feature
  • Match Types: Computing new types
  • Macros
    • Quotes & Splices (simple/high-level)
    • TASTy Reflect (typed AST API)

Overview

Inline

As a metaprogramming feature

inline def log[T](msg: String)(thunk: => T): T = ...

Inline definitions

  • 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
  }
}

Inlining code

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
  }
}

Inlining code with private references

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) 
  }
}

Recursive inline

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

Calling a recursive inline method

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)
    }
  }
}

Calling a recursive inline method... until when?

...
...
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 parameter

Inline conditional

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)))

Inline match

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

Specializing inline

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()

Specializing inline

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]
  }

Inline - Implicit match

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._

Match Types

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

Match Types

Elem[X] will have the type of
the first case that matches X

def addEventListener(tpe: String)(action: dom.Event => Any): Unit = js.native

Match type example

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
}

Match type example

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) => ... }

Example: Tuples

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

Example: Tuples

type Concat[X <: Tuple, +Y <: Tuple] <: Tuple = X match {
  case Unit => Y
  case head *: tail => head *: Concat[tail, Y]
}

Recursive Match Type

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]]
}

Successor type

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)
    }
  }
  ...
}

Constant values from types

inline def constValue[T]: T
inline def constValueOpt[T]: Option[T]
Value of the literal type

Macros

With Quotes & Splices

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

Syntax

import scala.quoted._

Macro definition through inline

  • 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
    

Comparison with inline

  • 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._

Code generation

powerCode generates an Expr containing the generated code

Well-typed macros cannot go wrong

"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._

Passing known values into a quote

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] }
  }
}

Passing known values into a quote

Whitebox macro

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

Inspecting Quotes

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

Inspecting Quotes

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

Inspecting Quotes ... with Quotes

StringContext("hello ", "!").S(world)
Analyzing arguments to extract constants

Macros

With TASTy Reflection

(low-level tree API)

TASTy (Typed ASTs)

  • TASTy file format is the standard encoding for all Scala 3 typed trees
    • Typed ASTs with source positions
    • Scaladoc comments
    • Extensible
  • TASTy Reflect provides an API over these typed trees
    • Inspection and construction of trees
    • Inspection of positions and Scaladoc comments
    • Stability given by the stability of the file format

TASTy Reflect in a macro (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

TASTy Reflect in a macro

  • 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

Prototypes

Thank you

https://slides.com/nicolasstucki/scala-days-2019

Questions?

Made with Slides.com