In the following example, using a quoted expression ('{ ... }
) raises a compiler error, while the equivalent AST construction works fine. Why does this happen?
// Using Scala 3.7.0
trait Criteria[S] {
transparent inline def discriminator[P <: S]: Int
}
transparent inline def discriminatorOf[S, P <: S]: Int = ${discriminatorOfImpl[S, P]}
private def discriminatorOfImpl[S: Type, P <: S: Type](using quotes: Quotes): Expr[Int] = {
import quotes.reflect.*
Expr.summon[Criteria[S]] match {
case None =>
report.errorAndAbort(s"No given of ${Type.show[Criteria[S]]} found")
case Some(criteriaExpr) =>
// Quoted version: Uncommenting it (the next line) would cause the compiler to raise the error: Deferred inline method discriminator in trait Criteria cannot be invoked
// '{ $criteriaExpr.discriminator[P] }
// AST version: supposedly equivalent to the quote in the previous line
Select.unique(criteriaExpr.asTerm, "discriminator").appliedToType(TypeRepr.of[P]).asExprOf[Int]
}
}
The error I get with the quoted version is:
"Deferred inline method discriminator
in trait Criteria
cannot be invoked"
Questions:
sealed trait Animal
case class Dog(dogField: Int) extends Animal
case class Cat(catField: String) extends Animal
given Criteria[Animal]:
override transparent inline def discriminator[P <: Animal]: Int = {
inline erasedValue[P] match {
case _: Dog => 0
case _: Cat => 1
}
}
@main def runSummon(): Unit = {
val catDiscriminator: 1 = discriminatorOf[Animal, Cat]
println(catDiscriminator)
}
The above test case also compiles (with the AST version) and runs as expected.
Note that catDiscriminator
is a constant, which tells that the Select.unique(criteriaExpr.asTerm, "discriminator").appliedToType(TypeRepr.of[P]).asExprOf[Int]
expression was reduced to a constant. Therefore, the expression can and is evaluated eagerly during the macro expansion. Then why does the quote checker consider the call to the discriminator
inline method as deferred?
There are different times:
Scala 2 reify {...}
/.splice
expressions-trees, Scala 3 quotations '{...}
, '[...]
/${...}
are typechecked at the time 1 (and then at the time 2 once again, upon expansion).
Scala 2 quasiquotes q"..."
, tq"..."
/$...
, ..$...
, ...$...
, c.parse
-parsed trees, manually constructed trees, Scala 3 manually constructed trees are typechecked at the time 2.
This makes a difference for
- Why does the quoted version fail when the AST version works?
When you write the tree '{ $criteriaExpr.discriminator[P] }
it should be typechecked at the time 1 (static type). When you write .asExprOf[Int]
this should be checked at the time 2 (dynamic type). What static type should criteriaExpr
have in '{ $criteriaExpr.discriminator[P] }
? It can't have no type. There's nothing different from Criteria[S]
and you have Deferred inline method cannot be invoked
because of that: How to call an inline method from within a scala 3.6.4 macro? Probably compiler could postpone inferring type (? <: Criteria[S]
) for this specific node of the whole program AST, keeping in mind that the type of implicit can be finer than Criteria[S]
but apparently compiler doesn't. So we opened a ticket https://github.com/scala/scala3/issues/23134
- How can I inspect what AST a quoted expression would generate when it does not compile?
In Scala 2 there was setting scalacOptions += "-Ymacro-debug-lite"
(or "-Ymacro-debug-verbose"
). In Scala 3 it's absent, so you should print specific tree manually:
val expr = '{ $criteriaExpr.discriminator[P] }
println(expr.asTerm.show(using Printer.TreeStructure))
expr
val expr = Select.unique(criteriaExpr.asTerm, "discriminator").appliedToType(TypeRepr.of[P]).asExprOf[Int]
println(expr.asTerm.show(using Printer.TreeStructure))
expr
Also you can inspect the whole dump after corresponding compilation stage with
scalacOptions ++= Seq("-Xprint-types", "-Vprint:typer")
scalacOptions ++= Seq("-Xprint-types", "-Vprint:posttyper")
scalacOptions ++= Seq("-Xprint-types", "-Vprint:inlining")
scalacOptions ++= Seq("-Xprint-types", "-Vprint:postInlining")
scalacOptions ++= Seq("-Xprint-types", "-Vprint:inlineVals")
You can see the whole list of compilation stages for a current version of compiler with scalacOptions += "-Xshow-phases"
.
https://dotty.epfl.ch/docs/contributing/architecture/phases.html
parser scan and parse sources
typer type the trees
checkUnusedPostTyper check for unused elements
checkShadowing check for elements shadowing other elements in scope
inlinedPositions check inlined positions
sbt-deps sends information on classes' dependencies to sbt
extractSemanticDBExtractSemanticInfo
extract info into .semanticdb files
posttyper additional checks and cleanups after type checking
unrollDefs generates forwarders for methods annotated with @unroll
prepjsinterop additional checks and transformations for Scala.js
SetRootTree set the rootTreeOrProvider on class symbols
pickler generates TASTy info
sbt-api sends a representation of the API of classes to sbt
inlining inline and execute macros
postInlining add mirror support for inlined code
staging check staging levels and heal staged types
splicing splicing
pickleQuotes turn quoted trees into explicit run-time data
structures
checkUnusedPostInlining check for unused elements
instrumentCoverage instrument code for coverage checking
crossVersionChecks check issues related to deprecated and experimental
firstTransform some transformations to put trees into a canonical form
checkReentrant check no data races involving global vars
elimPackagePrefixes eliminate references to package prefixes in Select
nodes
cookComments cook the comments: expand variables, doc, etc.
checkLoopingImplicits check that implicit defs do not call themselves in an
infinite loop
betaReduce reduce closure applications
inlineVals check right hand-sides of an `inline val`s
expandSAMs expand SAM closures to anonymous classes
elimRepeated rewrite vararg parameters and arguments
refchecks checks related to abstract members and overriding
dropForMap Drop unused trailing map calls in for comprehensions
extractSemanticDBAppendDiagnostics
extract info into .semanticdb files
initChecker check initialization of objects
protectedAccessors add accessors for protected members
extmethods expand methods of value classes with extension methods
uncacheGivenAliases avoid caching RHS of simple parameterless given aliases
checkStatic check restrictions that apply to @static members
elimByName map by-name parameters to functions
hoistSuperArgs hoist complex arguments of supercalls to enclosing
scope
forwardDepChecks ensure no forward references to local vals
specializeApplyMethods adds specialized methods to FunctionN
tryCatchPatterns compile cases in try/catch
patternMatcher compile pattern matches
preRecheck preRecheck
recheck recheck
setupCC prepare compilation unit for capture checking
cc capture checking
elimOpaque turn opaque into normal aliases
explicitJSClasses make all JS classes explicit
explicitOuter add accessors to outer classes from nested ones
explicitSelf make references to non-trivial self types explicit as
casts
interpolators optimize s, f, and raw string interpolators
dropBreaks replace local Break throws by labeled returns
pruneErasedDefs drop erased definitions and simplify erased expressions
uninitialized eliminates `compiletime.uninitialized`
inlinePatterns remove placeholders of inlined patterns
vcInlineMethods inlines calls to value class methods
seqLiterals express vararg arguments as arrays
intercepted rewrite universal `!=`, `##` methods
getters replace non-private vals and vars with getter defs
specializeFunctions specialize Function{0,1,2} by replacing super with
specialized super
specializeTuples replaces tuple construction and selection trees
collectNullableFields collect fields that can be nulled out after use in lazy
initialization
elimOuterSelect expand outer selections
resolveSuper implement super accessors
functionXXLForwarders add forwarders for FunctionXXL apply methods
paramForwarding add forwarders for aliases of superclass parameters
genericTuples optimize generic operations on tuples
letOverApply lift blocks from receivers of applications
arrayConstructors intercept creation of (non-generic) arrays and
intrinsify
erasure rewrite types to JVM model
elimErasedValueType expand erased value types to their underlying
implementation types
pureStats remove pure statements in blocks
vcElideAllocations peep-hole optimization to eliminate unnecessary value
class allocations
etaReduce reduce eta expansions of pure paths
arrayApply optimize `scala.Array.apply`
addLocalJSFakeNews adds fake new invocations to local JS classes in calls
to `createLocalJSClass`
elimPolyFunction rewrite PolyFunction subclasses to FunctionN subclasses
tailrec rewrite tail recursion to loops
completeJavaEnums fill in constructors for Java enums
mixin expand trait fields and trait initializers
lazyVals expand lazy vals
memoize add private fields to getters and setters
nonLocalReturns expand non-local returns
capturedVars represent vars captured by closures as heap objects
constructors collect initialization code in primary constructors
instrumentation count calls and allocations under -Yinstrument
lambdaLift lifts out nested functions to class scope
elimStaticThis replace This references to static objects by global
identifiers
countOuterAccesses identify outer accessors that can be dropped
dropOuterAccessors drop unused outer accessors
dropParentRefinements drop parent refinements from a template
checkNoSuperThis check that supercalls don't contain references to This
flatten lift all inner classes to package scope
transformWildcards replace wildcards with default values
moveStatic move static methods from companion to the class itself
expandPrivate widen private definitions accessed from nested classes
restoreScopes repair rendered invalid scopes
selectStatic get rid of selects that would be compiled into
GetStatic
junitBootstrappers generate JUnit-specific bootstrapper classes for
Scala.js
Collect entry points collect all entry points and save them in the context
collectSuperCalls find classes that are called with super
repeatableAnnotations aggregate repeatable annotations
genSJSIR generate .sjsir files for Scala.js
genBCode generate JVM bytecode
- Is my AST construction actually equivalent to the quoted version?
Obviously they are not equivalent since the the former compiles while the latter doesn't :) Or you should better define "equivalence".
If we temporarily comment out transparent inline
trait Criteria[S] {
/*transparent inline*/ def discriminator[P <: S]: Int
}
given Criteria[Animal]:
override /*transparent inline*/ def discriminator[P <: Animal]: Int = {
???
// inline erasedValue[P] match {
// case _: Dog => 0
// case _: Cat => 1
// }
}
then with .show(using Printer.TreeStructure)
we can see
// quotation '{...}
Inlined(Some(TypeIdent("Macros$")), Nil, TypeApply(Select(Inlined(None, Nil, Ident("given_Criteria_Animal")), "discriminator"), List(Inferred())))
// manual AST
TypeApply(Select(Ident("given_Criteria_Animal"), "discriminator"), List(Inferred()))
that the trees are similar (although not identical because of Inlined
wrapping trees). So the difference in behavior is mostly because of the typechecking at different times (1 or 2), not because of completely different tree structure.
- Is there a way to make the quoted version work in this case?
I can't see such option. How should criteriaExpr
be typed in '{ $criteriaExpr.discriminator[P] }
is something that should be implemented in compiler. Statically it's Criteria[S]
, dynamically something finer. We would prefer this to be something finer than Criteria[S]
statically. Alas.
There are always programs that can't fail at runtime but are rejected by compiler. For example if true then 1 else "a"
is dynamically Int
but val x: Int = if true then 1 else "a"
doesn't compile.