Semantics-Preserving Inlining for Metaprogramming

Nicolas Stucki, Aggelos Biboudis, Sébastien Doeraene, Martin Odersky

LAMP

Overview

  • Syntactic / Semantic Inlining
  • Inlining & Metaprogramming in Scala
  • Semantics preservation
  • Inlining & OO
  • Transparent inlining

Syntactic

  • Macro preprocessors
  • Inlines and then types checks
  • Extremely flexible
  • Hard to reason about semantics
#define getmax(a,b) ((a)>(b)?(a):(b))

getmax(i, j) // ((i)>(j)?(i):(j))

getmax("a", 2) // (("a")>(2)?("a"):(2))

Semantic

  • Type checks and then inlines
  • Equivalent to a normal function call
  • Inline semantics β-reduction
  • Guaranteed or best effort
inline def max(a: Int, b: Int): Int = 
  if (a > b) a else b
  
max(i, j) // if (x > y) x else y

max("a", 2) // error

Why inline for metaprogramming?

// Scala 3 macro
inline def power(x: D, n: D): Int =
  ${ powerCode('x, 'n) }

def powerCode(using Ctx)(x: Expr[D], n: Expr[I])(...): Expr[D] = 
  ...
// Scala 2 macro
def power(x: D, n: I): D = 
  macro impl

def impl(c: Ctx)(x: c.Expr[D], n: c.Expr[I]): c.Expr[D] = 
  ...

Inlining a function

inline def max(a: Int, b: Int): Int =
  if (a > b) a else b
  
  
max(x, y)
// becomes
val a = x
val b = y
if (a > b) a else b
  • val ensures correct call semantics
  • may be optimized away

Other parameters

inline def max(a: => Int, inline b: Int): Int =
  if (a > b) a else b
  
  
max(x, y)
// becomes
def a = x
if (a > y) a else y
  • Semantically equivalent
  • by-name: guaranties no duplication of argument
  • inline: fine grained control of generated code

Recursion

inline def power(x: Double, inline n: Int): Double =
  inline if (n == 0) 1.0
  else inline if (n > 0) x * power(x, n - 1)
  else error("n was not a know positive integer")

power(f(5), 3)
// becomes
val x = f(5)
x * x * x * 1.0
  • Recursive call to power
  • Constant folded conditions
  • inline if: ensures one branch is eliminated
  • error: if not eliminated emit error message

Overloading

  • Initial code elaboration does not change
    • Overloads
    • Implicit resolution
    • Implicit converisons
  • Can perform devirtualization
def log(x: Int): Unit = println("Log Int: " + x)
def log[T](x: T): Unit = println("Log: " + x)

inline def ilog[T](inline x: T): Unit = log[T](x)

ilog(3) // log[Int](3)

Private access

  • Accessors automatically generated
class Num(a: Int) {
  inline def max(b: Int): Int =
    if (a > b) a else b
    
  def a$inline: Int = a
}

num.max(4)
// becomes 
val b = 3
if (num.a$inline > b) num.a$inline else b

​Can we implement or override a method?

trait Num {
  def max(n: Int): Int
}

trait Nat extends Num {
  inline def max(n: Int): Int = ...
}
  • Yes, but we need to retain Num.max
  • Nat.max is inlined
  • Num.max called at runtime

Overriding and Abstracting

​Can we override an inline method?

trait Num {
  inline def max(n: Int): Int = ...
}

trait Nat extends Num {
  override def max(n: Int): Int = ...
}
  • No
  • Num.max would never call Nat.max
  • Inline definitions are effectively final

Overriding and Abstracting

Rules

  • Inline methods are final
  • If a methods implements/overrides a normal method then it must be retained
  • Retained methods cannot have inline parameters
  • Abstract inline methods can only be implemented by inline methods

Overriding and Abstracting

Transparent Inline

val map1: TreeSet[Int] = setFor[Int]
val map2: HashSet[Int => Int] = setFor[Int => Int]

transparent inline def setFor[T]: Set[T] =
  summonFrom {
    case ord: Ordering[T] => new TreeSet[T](ord)
    case _ => new HashSet[T]
  }
  
  • Provides a way to influence typing at call-site
  • transparent refines the type after inlining
  • summonFrom summons a value after inlining

Thank you

Semantics-Preserving Inlining for Metaprogramming

By Nicolas Stucki

Semantics-Preserving Inlining for Metaprogramming

  • 487