Upgrade to Pro — share decks privately, control downloads, hide ads and more …

WartremoverのScala 3対応

WartremoverのScala 3対応

ScalaMatsuri 2022発表資料

kenji yoshida

March 20, 2022
Tweet

More Decks by kenji yoshida

Other Decks in Programming

Transcript

  1. Wartremover History 2013 initial commit by @puffnfresh 2016 to 2018

    ? second maintainer @ClaireNeveu 2019 ~ @xuwei-k 5 / 71
  2. Scalafix は2 種類 SyntacticRule 型情報扱えない。あくまでparse した結果のみ SemanticRule Syntactic と比較したら追加で型情報のようなものが扱え る?

    完全な型ではないらしい?( よくわかってない) 重い、実行に時間がかかるので、個人的にあまり書いてない 13 / 71
  3. Wartremover 2 におけるtest val result = WartTestTraverser(SizeIs) { List(3).size ==

    1 Vector(3).size <= 1 Iterable(3).size >= 1 Map.empty[Int, String].size > 1 Set.empty[Boolean].size < 1 } assertErrors(result)( "Maybe you can use `sizeIs` instead of `size`", 5 ) collection のsize だったらsizeIs に置き換え可能 なことを教えてくれるルールのtest 21 / 71
  4. val result = WartTestTraverser(SizeIs) { Array(2).size == 1 Array(true).length <=

    3 "foo".size == 2 "foo".length == 3 (null: java.util.List[String]).size > 4 } assertEmpty(result) Array やString は警告出さないか? 22 / 71
  5. Wartremover 2 におけるtest 用macro macro input source code macro output

    warnings: List[String] errors: List[String] 26 / 71
  6. Scala 2 wartremover 関連用語集 WartUniverse Scala 2 のUniverse のwapper かつその他error

    と warning メソッド定義など WartTraverser 個々のrule を定義する時に継承するtrait WartUniverse を受け取って、Traverser を返す Traverser scala-reflect 2 内部のclass Global compile 時に必要な情報を全て持っていて引回すScala 2 compiler 内部のclass 28 / 71
  7. Scala 2 wartremover 定義例 object SizeIs extends WartTraverser { override

    def apply(u: WartUniverse): u.Traverser = { import u.universe._ new Traverser { override def traverse(tree: Tree): Unit = { tree match { case t if hasWartAnnotation(u)(t) => case Apply( Select( Select( IsScalaCollection(), a @ (TermName("size") | TermName("leng ), Method() ), List(_) ) if !isSynthetic(u)(tree) => error(u)( tree.pos, s"Maybe you can use `${a.decodedName}Is` ins ) 30 / 71
  8. Scala 3 wartremover 用語集 WartUniverse とWartTraverser 同じようなもの作る予定? Traverser scala3-library のものを使う?(

    後述) Scala 2 compiler のGlobal 3 では3 では3 ではContext というものになってる Quotes Scala 3 でmacro 書く時に必ず渡ってくる、必要になる何か 31 / 71
  9. Scala 3 におけるtest 用macro の作り方講座 1: Scala 2 と同じようなシグネチャのmacro を

    定義 2: 渡ってきたQuotes を scala.quoted.runtime.impl.QuotesImpl に無理矢理 asInstanceOf 3: QuotesImpl はContext を内包しているの で、それを取り出して独自Reporter をset 32 / 71
  10. Scala 3 におけるtest 用macro の作り方講座 4: 細かい処理中略 5: 検査対象のExpr を引数で受け取った

    Traverser でtraverse 6: 設定した独自Reporter からwarnings と errors 取り出してmacro の戻り値として返す 33 / 71
  11. Scala 3 におけるtest 用macro inline def apply[A <: WartTraverser](inline t:

    A)(inline a def applyImpl[A <: WartTraverser: Type]( t: Expr[A], expr: Expr[Any] )(using q1: Quotes): Expr[Result] = { val q2 = q1.asInstanceOf[QuotesImpl] val reporter = new WartReporter q2.ctx.asInstanceOf[FreshContext].setReporter(reporter) val wart = { val clazz = Class.forName(t.show + NameTransformer.MOD clazz.getField(NameTransformer.MODULE_INSTANCE_NAME).g } val universe = new WartUniverse(q1, wart, false, LogLeve val x: universe.Traverser = wart.apply(universe) val term = x.q.reflect.asTerm(expr) x.traverseTree(term)(term.symbol) val result1 = reporter.result val warnings = result1.collect { case a if a.level() == val errors = result1.collect { case a if a.level() == Di Expr(Result(errors = errors, warnings = warnings)) } 34 / 71
  12. compiler plugin の辛さ scalafix と比較してTree が綺麗ではない? Scala 3 の方が多少まし?だが圧倒的にscalafix 使いやす

    い 慣れの問題?個人の感想? compiler 内部に依存してしまうとcompiler が 互換壊すと辛い library と違い互換はあまり保証されない 35 / 71
  13. compiler plugin の辛さ typer phase 後だと自動生成部分まで渡ってく る case class のcopy

    PartialFunction(applyOrElse やisDefinedAt に展開) xml literal が展開後( 後述) その他色々。Scala 2 も3 も同様 typer の前のparser phase 後で受け取れるの か謎 やってみたけどよくわからない macro から展開されたcode か?なども考慮する 必要あり 36 / 71
  14. val x: PartialFunction[Int, Int] = { case x if x

    % 2 == 0 => x.toString } scala -Xprint:typer -e " ここにコード" 37 / 71
  15. val x: PartialFunction[Int,Int] = ({ @SerialVersionUID(value = 0) final <synthetic>

    class $an def <init>(): <$anon: Int => Int> = { $anonfun.super.<init>(); () }; final override def applyOrElse[A1 <: Int, B1 >: Int](x case (x @ _) if x.%(2).==(0) => x.toString() case (defaultCase$ @ _) => default.apply(x1) }; final def isDefinedAt(x1: Int): Boolean = ((x1.asInsta case (x @ _) if x.%(2).==(0) => true case (defaultCase$ @ _) => false } }; new $anonfun() }: PartialFunction[Int,Int]); 38 / 71
  16. $ scala -Xprint:typer -e "case class A(x: Int)" case class

    A extends AnyRef with Product with Serializable { private[this] val x: Int = _; def x: Int = A.this.x; def (x: Int): this.A = { A.super.(); () }; def copy(x: Int = x): this.A = new A(x); def copy$default$1: Int = A.this.x; override def productPrefix: String = "A"; def productArity: Int = 1; def productElement(x$1: Int): Any = x$1 match { case 0 => A.this.x case _ => scalaAny(x$1) }; override def productIterator: Iterator[Any] = scalaAny(A.this); def canEqual(x$1: Any): Boolean = x$1.$isInstanceOf[this.A](); override def productElementName(x$1: Int): String = x$1 match { case 0 => "x" case _ => scalaString(x$1) }; override def hashCode(): Int = { var acc: Int = -889275714; acc = scala.runtime.Statics.mix(acc, A.this.productPrefix.hashCode()); acc = scala.runtime.Statics.mix(acc, x); scala.runtime.Statics.finalizeHash(acc, 1) }; override def toString(): String = scala.runtime.ScalaRunTime._toString(A.this); override def equals(x$1: Any): Boolean = A.this.eq(x$1Object).||(x$1 match { case (_: this.A) => true case _ => false }.&&({ val A$1: this.A = x$1this.A; A.this.x.==(A$1.x).&&(A$1.canEqual(A.this)) })) }; private object A extends scalaInt,this.A with java.io.Serializable { def (): this.A.type = { A.super.(); () }; final override def toString(): String = "A"; case def apply(x: Int): this.A = new A(x); case def unapply(x$0: this.A): Option[Int] = if (x$0.eq(null)) scala.None else SomeInt(x$0.x) } 39 / 71
  17. Scala 3.1.1 compiler phases $ scala -Xshow-phases parser typer inlinedPositions

    sbt-deps extractSemanticDB posttyper prepjsinterop sbt-api SetRootTree pickler inlining postInlining staging pickleQuotes {firstTransform, checkReentrant, elimPackagePrefixes, cookComments, checkStatic, checkLoopingImplicits, betaReduce, initChecker {elimRepeated, protectedAccessors, extmethods, uncacheGivenAliases, byNameClosures, hoistSuperArgs, specializeApplyMe {elimOpaque, explicitJSClasses, explicitOuter, explicitSelf, elimByName, stringInterpolatorOpt} {pruneErasedDefs, uninitializedDefs, inlinePatterns, vcInlineMethods, seqLiterals, intercepted, getters, specializeF erasure {elimErasedValueType, pureStats, vcElideAllocations, arrayApply, addLocalJSFakeNews, elimPolyFunction, tailrec, comp {constructors, instrumentation} {lambdaLift, elimStaticThis, countOuterAccesses} {dropOuterAccessors, checkNoSuperThis, flatten, transformWildcards, moveStatic, expandPrivate, restoreScopes, select genSJSIR genBCode 40 / 71
  18. Scala 2.13.8 compiler phases $ scala -Xshow-phases phase name id

    description ---------- -- ----------- parser 1 parse source into ASTs, perform simple desugaring namer 2 resolve names, attach symbols to named trees packageobjects 3 load package objects typer 4 the meat and potatoes: type the trees superaccessors 5 add super accessors in traits and nested classes extmethods 6 add extension methods for inline classes pickler 7 serialize symbol tables refchecks 8 reference/override checking, translate nested objects patmat 9 translate match expressions uncurry 10 uncurry, translate function values to anonymous classes fields 11 synthesize accessors and fields, add bitmaps for lazy vals tailcalls 12 replace tail calls by jumps specialize 13 @specialized-driven class and method specialization explicitouter 14 this refs to outer pointers erasure 15 erase types, add interfaces for traits posterasure 16 clean up erased inline classes lambdalift 17 move nested functions to top level constructors 18 move field definitions into constructors flatten 19 eliminate inner classes mixin 20 mixin composition cleanup 21 platform-specific cleanups, generate reflective calls delambdafy 22 remove lambdas jvm 23 generate JVM bytecode terminal 24 the last phase during a compilation run 41 / 71
  19. What is Typer? 以下はScala 2 と3 でおそらく大体同じ? 型付け含め色々やるらしい 詳細知らない implicit

    探索などもおそらくここだし、大抵こ こが遅い? 大抵のcompiler plugin 書く場合はこの後? 42 / 71
  20. compiler plugin の辛さ 他のcompiler plugin( 例: scoverage) と組 み合わせるとあり得ないTree 渡ってくるので特

    殊処理必要 compiler 内部はmutable(Scala 2 も3 も) error が出た場合の原因が慣れないとわかりにく い? 43 / 71
  21. $ scala -version Scala code runner version 2.12.15 -- Copyright

    2002-2021, $ scala -Xprint:typer -e "<a />" new scala.xml.Elem( null, "a", scala.xml.Null, scala.xml.TopScope, true ) 44 / 71
  22. compiler plugin の良いとこ ろ compiler 内部の完全な型にアクセス可能 compile 途中のphase に挟み込めば原理上一番 無駄がない

    中間データ的なものが一番少なくて済むはず? Scala 3 だとExpr を直接pattern match 出来 て便利! ただし割とまだbug もある 46 / 71
  23. Expr のmatch 楽しい!便利!最高! t.asExpr match { case '{ ($x1: Iterable[t]).size

    < ($x2: Int) } => error(u)(tree.pos, sizeMessage) case '{ ($x1: Iterable[t]).size == ($x2: Int) } => error(u)(tree.pos, sizeMessage) case '{ ($x1: Iterable[t]).size <= ($x2: Int) } => error(u)(tree.pos, sizeMessage) ただし、あくまでExpr だけであって、 全てがこれで書けるわけではない 47 / 71
  24. t.asExpr match { case '{ ($x: List[t]).toList } => error(u)(tree.pos,

    "redundant toList conversion") case '{ ($x: Vector[t]).toVector } => error(u)(tree.pos, "redundant toVector conversion") case '{ ($x: Set[t]).toSet } => // ここでなぜか死ぬ??? error(u)(tree.pos, "redundant toSet conversion") 48 / 71
  25. 正解はこちら Expr のpattern match 内部で型自体を変数として 扱う場合の書き方 case '{ type t1

    type t2 >: `t1` ($x: Set[`t1`]).toSet[`t2`] } => error(u)(tree.pos, "redundant toSet conversion") 50 / 71
  26. 両方の変数を参照出来るけど何これ? 🤔 import scala.quoted.* object Macros { def fooImpl(a: Expr[Any])(using

    Quotes): Expr[Any] = a m case '{ $x1: String } | '{ $x2: Int } => println((x1, x2)) a } inline def foo(a: Any) = ${fooImpl('a)} } 51 / 71
  27. 報告したbug その2 https://github.com/lampepfl/dotty/issues/146 java.lang.VerifyError: Bad local variable type Exception Details:

    Location: example/Macros$.fooImpl(Lscala/quoted/Expr;Lscala/quot Reason: Type top (current frame, locals[8]) is not assignable Current Frame: bci: @179 flags: { } locals: { 'example/Macros$', 'scala/quoted/Expr', 'sca stack: { 'scala/Predef$', 'scala/Tuple2$' } 52 / 71
  28. Wartremover のcompiler plugin 側実装予定コード import dotty.tools.dotc.plugins.PluginPhase import dotty.tools.dotc.plugins.StandardPlugin class WartremoverPlugin

    extends StandardPlugin { override def name = "wartremover" override def description = "wartremover" override def init(options: List[String]): List[PluginPhase] = { // ここで引数parse する val newPhase = new WartremoverPhase( // parse した引数から生成したもの渡す ) newPhase :: Nil } 53 / 71
  29. Wartremover のcompiler のphase 実装予定コード class WartremoverPhase( errorWarts: List[WartTraverser], warningWarts: List[WartTraverser]

    // その他の引数省略 ) extends PluginPhase { override def phaseName = "wartremover" // Typer の後に実行して!という指定 override val runsAfter = Set(TyperPhase.name) // 他にも色々あるが、おそらくこれだけで原理上全部のTree 辿れる? override def prepareForUnit(tree: Tree)(using c: Context) = { // Context と新しいQuotes を生成 val c2 = QuotesCache.init(c.fresh) val q = scala.quoted.runtime.impl.QuotesImpl()(using c2) def runWart(w: WartTraverser, onlyWarning: Boolean): Unit = { val universe = new WartUniverse( quotes = q, traverser = w, onlyWarning = onlyWarning, // その他引数 ) val traverser = w.apply(universe) // compiler 内部のTree をlibrary 側のTree に無理矢理キャスト! val t = treetraverser.q.reflect.Tree try { traverser.traverseTree(t)(t.symbol) } catch { case NonFatal(e) => // 場合によってログ出す } } errorWarts.foreach(w => runWart(w = w, onlyWarning = false)) warningWarts.foreach(w => runWart(w = w, onlyWarning = true)) c } 54 / 71
  30. scala-reflect 2 のAST が抽 象化されてない例 A labelled expression. Not expressible

    in language syntax, but generated by the compiler to simulate while/do- while loops, and also by the pattern matcher. 56 / 71
  31. scala-reflect 2 のAST が抽 象化されてない例 type LabelDef >: Null <:

    LabelDefApi with DefTree with TermTree Scala 3 のQuotes API には出てこない? 隠されてるのか、根本的になくなったのかよくわ かってない 57 / 71
  32. Scala 3 対応の進捗 6 〜7 割のテストがそのまま通った? 残りが色々辛い そもそもScala 3 で原理的に解決されてるもの

    Product やSerializable に推論されなくなった implicit に型書くのが必須化 implicit conversion 禁止 必ずfalse になる比較がcompile error 近日中に3.0.0-RC1 出す・・・? 60 / 71
  33. ( 少し頑張れば) こういうのもscalafix の SyntacticRule で? default arguments 禁止 implicit

    conversion 禁止 type class ではない( 型引数持たな い)context 引回し用implicit param 禁止 sealed 継承したら必ずfinal にしないとダメだ ぞ! 62 / 71
  34. TATSy をparse する案 TASTy とはScala 3 がcompile 時に吐き出す、 型含めた全ての情報が入ったもの Scala

    2 とは保存方法も別、入ってる情報量や種類も別 Scala 2 の場合はScalaSignature というアノテーションに 付加情報入っていた Scala 2 の方が情報が少ない。あくまでシグネチャのみ 66 / 71