Scala:引用表达式报错,等价AST正常的原因与相关问题
在Scala 3.7.0环境下,使用引用表达式'{ ... }调用trait Criteria中的透明内联方法discriminator时,编译器会抛出错误:"Deferred inline method discriminator in trait Criteria cannot be invoked",但通过手动构建AST的等价代码却能正常编译运行。
代码示例
// 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 causes the compiler error // '{ $criteriaExpr.discriminator[P] } // AST version: Works correctly Select.unique(criteriaExpr.asTerm, "discriminator").appliedToType(TypeRepr.of[P]).asExprOf[Int] } }
核心问题
- 为何引用表达式执行失败,而AST版本却能正常工作?
- 手动构建的AST是否与引用表达式完全等价?
- 当引用表达式无法编译时,如何查看其生成的AST结构?
- 有没有办法让引用表达式在该场景下正常运行?
测试用例
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) }
使用AST版本时,测试用例可正常编译运行,catDiscriminator被推导为常量,说明宏展开阶段已完成求值,但引用表达式的编译器检查逻辑仍判定discriminator调用为延迟内联方法。
问题解答
1. 引用表达式失败、AST版本正常的原因
引用表达式'{ $criteriaExpr.discriminator[P] }会触发Scala编译器的内联方法合法性检查:在引用表达式的上下文里,编译器看到的是trait Criteria中未实现的transparent inline def discriminator(属于延迟内联方法),此时编译器尚未关联到具体的given实例实现,因此直接抛出错误。
手动构建AST时,绕过了引用表达式的静态检查逻辑——直接生成的Select节点会在宏展开阶段被替换为具体given实例的内联方法实现,此时编译器能看到完整的内联代码,从而完成求值,不会触发延迟内联方法的调用错误。
2. 手动AST与引用表达式是否完全等价?
不完全等价。虽然两者最终生成的运行时代码可能一致,但编译期的处理路径不同:
- 引用表达式会先经过Scala的前端语法检查、类型校验,包括对延迟内联方法的调用限制;
- 手动AST构建直接生成抽象语法树节点,跳过了前端的部分静态检查,直接进入宏展开阶段的代码替换逻辑。
不过在这个场景下,当引用表达式能正常编译时,两者生成的AST最终是一致的。
3. 查看引用表达式生成的AST结构
可以通过两种方式查看:
- 在宏实现中打印AST:临时添加代码,将引用表达式转换为Term并输出,例如:
注释掉返回该表达式的代码,先编译打印AST,即可看到结构。val quotedTerm = '{ $criteriaExpr.discriminator[P] }.asTerm report.info(quotedTerm.show) - 启用编译器打印选项:编译时添加
-Xprint:typer参数,编译器会输出经过类型检查后的AST结构,从中可找到引用表达式对应的节点。
4. 让引用表达式正常运行的方法
通过强制编译器关联具体的given实例类型解决,修改引用表达式的写法,明确告知编译器使用的是具体实现类的方法,而非trait中的延迟内联方法:
import quotes.reflect.* // 获取given实例的具体类型并转换Expr val givenType = criteriaExpr.asTerm.tpe val typedCriteriaExpr = criteriaExpr.asExprOf[givenType.type] '{ $typedCriteriaExpr.discriminator[P] }
这种方式让编译器在引用表达式的上下文里看到的是具体实现类的内联方法,而非trait中的延迟方法,从而通过检查。
内容的提问来源于stack exchange,提问作者Readren

