Skip to content

Commit

Permalink
Added scala 2 annotation equivalent, but 2.12 needs some love
Browse files Browse the repository at this point in the history
  • Loading branch information
j-mie6 committed Apr 23, 2024
1 parent e2d3d60 commit 65d97a2
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 17 deletions.
32 changes: 18 additions & 14 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -102,27 +102,31 @@ 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",
"2.12" -> "4.5.0",
"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",
Expand Down
74 changes: 71 additions & 3 deletions parsley-debug/shared/src/main/scala-2/parsley/debuggable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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("<init>")
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)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright 2020 Parsley Contributors <https://github.com/j-mie6/Parsley/graphs/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
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2020 Parsley Contributors <https://github.com/j-mie6/Parsley/graphs/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)
}
24 changes: 24 additions & 0 deletions parsley-debug/shared/src/test/scala/parsley/AnnotationTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2020 Parsley Contributors <https://github.com/j-mie6/Parsley/graphs/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
}
}

0 comments on commit 65d97a2

Please sign in to comment.