What kinds of macros should Scala 3 support?

Quill propagates refinement types along with their quoted SQL snippets in order to perform SQL construction & optimization at compile time. Greatly simplified, it basically takes

val x = quote{1}
val y = quote{x + 2}
val z = quote{y + 3}

And the quote refines the types being returned so the contents of each call is visible in the type as an annotation:

val x: Quote[Int] @ast("1") = quote{1}
val y: Quote[Int] @ast("1 + 2") = quote{x + 2}
val z: Quote[Int] @ast("1 + 2 + 3") = quote{y + 3}

And then proceeds to perform optimizations at compile-time based on the annotated AST:

val x: Quote[Int] @ast("1") = Quote(1)
val y: Quote[Int] @ast("1 + 2") = Quote(3)
val z: Quote[Int] @ast("1 + 2 + 3") = Quote(6)

Quill doesn’t need all the power of whitebox macros. The annotations it uses to pass data around between invocations is “side channel”-ish: they never affect the “primary” type e.g. Quote[Int], but only the contents of downstream annotations.

Without the ability to pass annotations between calls via their types, Quill falls back to performing the optimisations and transformations at runtime. This works, but pushes computation-overhead and failure-reporting to runtime, whereas with the annotations present it can do all that and report any errors before you ever run any code. This would be the case if whitebox macros did not exist (blackbox macros cannot refine the types they return based on the AST captured).

5 Likes