diff --git a/build.sbt b/build.sbt index 5cb2ff359..e9e074867 100644 --- a/build.sbt +++ b/build.sbt @@ -102,6 +102,24 @@ lazy val parsleyDebug = crossProject(JSPlatform, JVMPlatform, NativePlatform) .settings( name := "parsley-debug", commonSettings, + scalacOptions ++= { + scalaVersion.value match { + case Scala213 => Seq("-Ymacro-annotations") + case Scala3 => Seq.empty + case Scala212 => Seq.empty + } + }, + libraryDependencies ++= { + // Reflection library choice per Scala version. + scalaVersion.value match { + case v@Scala212 => Seq( + "org.scala-lang" % "scala-reflect" % v, + compilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full), + ) + case v@Scala213 => Seq("org.scala-lang" % "scala-reflect" % v) + case _ => Seq() + } + }, tlVersionIntroduced := Map( "2.13" -> "4.5.0", @@ -109,20 +127,6 @@ lazy val parsleyDebug = crossProject(JSPlatform, JVMPlatform, NativePlatform) "3" -> "4.5.0", ), ) - .jvmSettings( - libraryDependencies ++= { - // Reflection library choice per Scala version. - CrossVersion.partialVersion(Keys.scalaVersion.value) match { - case Some((2, 12)) => - Seq("org.scala-lang" % "scala-reflect" % Scala212) - case Some((2, 13)) => - Seq("org.scala-lang" % "scala-reflect" % Scala213) - case _ => - // No Scala library for any other version (2.11, 3, etc.). - Seq() - } - } - ) def testCoverageJob(cacheSteps: List[WorkflowStep]) = WorkflowJob( id = "coverage", diff --git a/parsley-debug/shared/src/main/scala-2/parsley/debuggable.scala b/parsley-debug/shared/src/main/scala-2/parsley/debuggable.scala index bc26011e7..953a69dec 100644 --- a/parsley-debug/shared/src/main/scala-2/parsley/debuggable.scala +++ b/parsley-debug/shared/src/main/scala-2/parsley/debuggable.scala @@ -5,7 +5,75 @@ */ package parsley -import scala.annotation.StaticAnnotation +import scala.annotation.{StaticAnnotation, compileTimeOnly} +import scala.reflect.macros.blackbox -// TODO: implementation! -class debuggable extends StaticAnnotation +@compileTimeOnly("macros need to be enabled to use this functionality: -Ymacro-annotations in 2.13, or use \"Macro Paradise\" for 2.12") +class debuggable extends StaticAnnotation { + def macroTransform(annottees: Any*): Any = macro debuggable.impl +} + +private object debuggable { + def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = { + import c.universe._ + // to accurately model the Scala 3 equivalent, we are treated like a black-box + // macro: only the first annottee is relevant, and this must be a class or an object + // anything else is returned as is. + val inputs = annottees.toList + val outputs = inputs match { + case ClassDef(mods, clsName, tyParams, Template(parents, self, body)) :: rest => + collect(c)(body, body => ClassDef(mods, clsName, tyParams, Template(parents, self, body))) :: rest + case ModuleDef(mods, objName, Template(parents, self, body)) :: rest => + collect(c)(body, body => ModuleDef(mods, objName, Template(parents, self, body))) :: rest + case _ => + c.error(c.enclosingPosition, "only classes/objects containing parsers can be annotated for debugging") + inputs + } + q"{..$outputs}" + } + + private def collect(c: blackbox.Context)(defs: List[c.Tree], recon: List[c.Tree] => c.Tree): c.Tree = { + import c.universe._ + val parsleyTy = c.typeOf[Parsley[_]].typeSymbol + // can't typecheck constructors in a stand-alone block + // FIXME: in addition, on 2.12, we need to remove `paramaccessor` modifiers on constructor arguments + val noConDefs = defs.filter { + case DefDef(_, name, _, _, _, _) => name != TermName("") + case _ => true + } + val typeMap = c.typecheck(q"..${noConDefs}", c.TERMmode, silent = true) match { + // in this case, we want to use the original tree (it's still untyped, as required) + // but we can process typedDefs into a map from identifiers to inferred types + case Block(typedDefs, _) => + typedDefs.collect { + case ValDef(_, name, tpt, _) => name -> ((Nil, tpt.tpe)) + case DefDef(_, name, _, args, ret, _) => name -> ((args.flatten.map(_.tpt.tpe), ret.tpe)) + }.toMap + // in this case, we can extract those with annotated type signatures + case _ => Map.empty[TermName, (List[Type], Type)] + } + // filter the definitions based on their type from the type map: + def filterParsley(t: Tree) = t match { + case dfn: ValOrDefDef => typeMap.get(dfn.name) match { + case Some((Nil, ty)) if ty.typeSymbol == parsleyTy => Some(dfn.name) + case _ => None + } + case _ => None + } + val parsers = defs.collect { + case t if filterParsley(t).isDefined => filterParsley(t).get // I hate you 2.12 + } + // the idea is we inject a call to Collector.registerNames with a constructed + // map from these identifiers to their compile-time names + val listOfParsers = q"List(..${parsers.map(tr => q"${Ident(tr)} -> ${tr.toString}")})" + val registration = q"parsley.debugger.util.Collector.registerNames($listOfParsers.toMap)" + + /*println(registration) + println(typeMap) + assert(parsers.nonEmpty)*/ + // add the registration as the last statement in the object + // TODO: in future, we want to modify all `def`s with a top level `opaque` combinator + // that will require a bit more modification of the overall `body`, unfortunately + recon(defs :+ registration) + } +} diff --git a/parsley-debug/shared/src/test/scala-2/scala/annotation/experimental.scala b/parsley-debug/shared/src/test/scala-2/scala/annotation/experimental.scala new file mode 100644 index 000000000..b349aac50 --- /dev/null +++ b/parsley-debug/shared/src/test/scala-2/scala/annotation/experimental.scala @@ -0,0 +1,9 @@ +/* + * Copyright 2020 Parsley Contributors + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package scala.annotation + +// this fills in the gap for 2.13 and 2.12: we can't use 3.4's -experimental flag +class experimental extends StaticAnnotation diff --git a/parsley-debug/shared/src/test/scala/annotation/AnnotationTestObjects.scala b/parsley-debug/shared/src/test/scala/annotation/AnnotationTestObjects.scala new file mode 100644 index 000000000..d0a8842e6 --- /dev/null +++ b/parsley-debug/shared/src/test/scala/annotation/AnnotationTestObjects.scala @@ -0,0 +1,27 @@ +/* + * Copyright 2020 Parsley Contributors + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package annotation +import parsley.Parsley +import parsley.quick._ + +import scala.annotation.experimental + +@experimental @parsley.debuggable +object otherParsers { + val a = char('b') +} + +@experimental @parsley.debuggable +class parsers(val x: Int) { + val p = char('a') + val q = p ~> p + lazy val r: Parsley[Char] = ~r ~> q + def s = otherParsers.a + val y = 7 + + def this(f: Float) = this(f.toInt) + def many[A](p: Parsley[A]): Parsley[List[A]] = Parsley.many(p) +} diff --git a/parsley-debug/shared/src/test/scala/parsley/AnnotationTest.scala b/parsley-debug/shared/src/test/scala/parsley/AnnotationTest.scala new file mode 100644 index 000000000..fa1a8fdb6 --- /dev/null +++ b/parsley-debug/shared/src/test/scala/parsley/AnnotationTest.scala @@ -0,0 +1,24 @@ +/* + * Copyright 2020 Parsley Contributors + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package parsley + +import scala.annotation.experimental +import parsley.debugger.internal.Renamer + +@experimental +class AnnotationTest extends ParsleyTest { + "parsley.debuggable" should "fill in the names for objects" in { + Renamer.nameOf(None, annotation.otherParsers.a.internal) shouldBe "a" + } + + it should "fill in the names for classes" in { + val parsers = new annotation.parsers(6) + Renamer.nameOf(None, parsers.p.internal) shouldBe "p" + Renamer.nameOf(None, parsers.q.internal) shouldBe "q" + Renamer.nameOf(None, parsers.r.internal) shouldBe "r" + Renamer.nameOf(None, parsers.s.internal) should not be ("char") // see the objects lol + } +}