Skip to content

Commit f9dc7fc

Browse files
committed
Start an example parser for arbitrary case classes
loosely inspired by the Aggregate Literal pre-sip
1 parent 5c65913 commit f9dc7fc

File tree

4 files changed

+177
-0
lines changed

4 files changed

+177
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package name.rayrobdod.stringContextParserCombinatorExample.aggregateLiteral
2+
3+
import scala.quoted.*
4+
import name.rayrobdod.stringContextParserCombinator.{Interpolator => _, *}
5+
import name.rayrobdod.stringContextParserCombinator.Interpolator.quotedInterpolators.*
6+
7+
enum Sign {
8+
case Positive
9+
case Negative
10+
11+
def *(x:Int):Int = {
12+
this match {
13+
case Positive => x
14+
case Negative => -x
15+
}
16+
}
17+
}
18+
19+
object MacroImpl {
20+
private val whitespace: Interpolator[Unit] = charIn(" \t\r\n").repeat().void.hide
21+
22+
private val int: Interpolator[Expr[Int]] = {
23+
val sign = isString("-").optionally()(using new typeclass.Optionally[Any, Unit, Sign] {
24+
def none(implicit ctx: Any):Sign = Sign.Positive
25+
def some(elem:Unit)(implicit ctx: Any):Sign = Sign.Negative
26+
})
27+
28+
val digits = charIn("0").map(_ => 0) <|>
29+
(charIn('1' to '9') <~> charIn(('0' to '9') :+ '_').repeat())
30+
.map({(ht) =>
31+
val (h, t2) = ht
32+
val t = t2.filter(_ != '_')
33+
s"$h$t".toInt
34+
})
35+
36+
(sign <~> digits).map((sd) => sd._1 * sd._2).mapToExpr <~ whitespace
37+
}
38+
39+
private val string: Interpolator[Expr[String]] = {
40+
def charFlatCollect[A](pf: PartialFunction[Char, Interpolator[A]]):Interpolator[A] =
41+
charWhere((x) => pf.isDefinedAt(x)).flatMap((x) => pf(x))
42+
43+
val charImmediate = charWhere(x => x != '\"' && x != '\\' && x != '\n' && x != '\r')
44+
val charEscaped = (
45+
(isString("\\") ~> charFlatCollect({
46+
case '\\' => pass.map(_ => '\\')
47+
case '"' => pass.map(_ => '"')
48+
case '\'' => pass.map(_ => '\'')
49+
case 'n' => pass.map(_ => '\n')
50+
case 'r' => pass.map(_ => '\r')
51+
case 'b' => pass.map(_ => '\b')
52+
case 'f' => pass.map(_ => '\f')
53+
case 't' => pass.map(_ => '\t')
54+
case 'u' => charIn(('0' to '9') ++ ('a' to 'f') ++ ('A' to 'F')).repeat(4,4).map(x => Integer.parseInt(x, 16).toChar)
55+
}))
56+
)
57+
58+
isString("\"") ~> (charImmediate <|> charEscaped).repeat().mapToExpr <~ isString("\"") <~ whitespace
59+
}
60+
61+
private def caseClassField(quotes: Quotes)(symbolA: quotes.reflect.Symbol): Interpolator[Expr[?]] = {
62+
(isString(symbolA.name) ~> whitespace ~> isString("=") ~> whitespace).optionally() ~> obj(quotes)(symbolA.typeRef.baseClasses(0).typeRef)
63+
}
64+
65+
private def caseClass[Z](quotes: Quotes)(tpeZ: quotes.reflect.TypeRepr): Interpolator[Expr[Z]] = {
66+
import quotes.reflect.*
67+
val symbolZ = tpeZ.typeSymbol
68+
69+
val paramParsers =
70+
symbolZ.caseFields
71+
.map(caseClassField(quotes))
72+
.reverse
73+
74+
val paramsParser =
75+
paramParsers.tail
76+
.foldLeft[Interpolator[List[Expr[?]]]](paramParsers.head.map(x => List(x)))({(folding, value) =>
77+
(value <~ isString(",") <~ whitespace) `<::>` folding
78+
}).map(params =>
79+
New(TypeIdent(symbolZ)).select(symbolZ.primaryConstructor).appliedToArgs(params.map(_.asTerm)).asExpr.asInstanceOf[Expr[Z]]
80+
)
81+
82+
isString("[") ~> whitespace ~> paramsParser <~ whitespace <~ isString("]") <~ whitespace
83+
}
84+
85+
86+
private def obj[Z](quotes: Quotes)(tpeZ: quotes.reflect.TypeRepr): Interpolator[Expr[Z]] = {
87+
import quotes.reflect.*
88+
given Quotes = quotes
89+
90+
val interpolation = (
91+
ofType[Z](using new {
92+
def createType(using Quotes): Type[Z] = tpeZ.asType.asInstanceOf[Type[Z]]
93+
})
94+
)
95+
96+
val literal = (
97+
if (tpeZ <:< TypeRepr.of[Int]) {
98+
Option(int.asInstanceOf[Interpolator[Expr[Z]]])
99+
} else
100+
if (tpeZ <:< TypeRepr.of[String]) {
101+
Option(string.asInstanceOf[Interpolator[Expr[Z]]])
102+
} else
103+
if (tpeZ.typeSymbol.caseFields.nonEmpty) {
104+
Option(caseClass(quotes)(tpeZ))
105+
} else
106+
{
107+
println(Printer.TypeReprCode.show(tpeZ))
108+
Option.empty
109+
}
110+
)
111+
112+
literal.map(_ <|> interpolation).getOrElse(interpolation)
113+
}
114+
115+
def stringContext_cc[Z](sc:Expr[scala.StringContext], args:Expr[Seq[Any]])(using Type[Z], Quotes):Expr[Z] = {
116+
val retval = (whitespace ~> obj[Z](quotes)(quotes.reflect.TypeRepr.of[Z]) <~ end).interpolate(sc, args)
117+
println(retval.show)
118+
retval
119+
}
120+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package name.rayrobdod.stringContextParserCombinatorExample.aggregateLiteral
2+
3+
extension (inline sc:scala.StringContext)
4+
// A parameter using `Z` is needed to force scala to decide that the `Z` is not `Nothing`
5+
inline def cc[Z](inline args: Any*)(using scala.reflect.ClassTag[Z]):Z =
6+
${MacroImpl.stringContext_cc[Z]('sc, 'args)}
7+
8+
case class Point(x: Int, y: Int)
9+
case class Circle(center: Point, radius: Int, fill: String)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package name.rayrobdod.stringContextParserCombinatorExample.aggregateLiteralTest
2+
3+
import name.rayrobdod.stringContextParserCombinatorExample.aggregateLiteral._
4+
5+
final class CaseClassTest extends munit.FunSuite {
6+
case class Point(x: Int, y: Int)
7+
case class Triangle(p1: Point, p2: Point, p3: Point)
8+
9+
test("Simple") {
10+
val exp = Point(12, 23)
11+
val res: Point = cc"[12, 23]"
12+
assertEquals(res, exp)
13+
}
14+
test("Simple with names") {
15+
val exp = Point(34, 45)
16+
val res: Point = cc"[x = 34, y = 45]"
17+
assertEquals(res, exp)
18+
}
19+
test("Simple with names and interpolations") {
20+
val exp = Point(56, 67)
21+
val res: Point = cc"[x = ${exp.x}, y = ${exp.y}]"
22+
assertEquals(res, exp)
23+
}
24+
25+
test("Nested case classes") {
26+
val exp = Triangle(Point(12, 23), Point(34,45), Point(56,67))
27+
val res: Triangle = cc"[[12,23], p2 = [x = 34, y = 45], ${exp.p3}]"
28+
assertEquals(res, exp)
29+
}
30+
}

build.sbt

+18
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,23 @@ lazy val xml = (projectMatrix in file("XmlParser"))
301301
scala3Ver,
302302
))
303303

304+
lazy val aggregateLiteral = (projectMatrix in file("AggregateLiteralParser"))
305+
.dependsOn(base)
306+
.disablePlugins(IfDefPlugin)
307+
.disablePlugins(MimaPlugin)
308+
.disablePlugins(TastyMiMaPlugin)
309+
.settings(sharedSettings)
310+
.settings(
311+
name := "aggregateLiteral",
312+
publish / skip := true,
313+
console / initialCommands := """
314+
import name.rayrobdod.stringContextParserCombinatorExample.aggregateLiteral._
315+
""",
316+
)
317+
.jvmPlatform(scalaVersions = Seq(
318+
scala3Ver,
319+
))
320+
304321

305322
lazy val jvms = (project in file(".sbt/matrix/jvms"))
306323
.disablePlugins(JvmPlugin)
@@ -312,6 +329,7 @@ lazy val jvms = (project in file(".sbt/matrix/jvms"))
312329
.aggregate(time.jvm.get.map(Project.projectToRef):_*)
313330
.aggregate(uri.jvm.get.map(Project.projectToRef):_*)
314331
.aggregate(xml.jvm.get.map(Project.projectToRef):_*)
332+
.aggregate(aggregateLiteral.jvm.get.map(Project.projectToRef):_*)
315333

316334
disablePlugins(MimaPlugin)
317335
disablePlugins(TastyMiMaPlugin)

0 commit comments

Comments
 (0)