From 7d8a6d0a752417083c37dfea424bdda551a5c971 Mon Sep 17 00:00:00 2001 From: Raymond Dodge Date: Wed, 31 Jan 2024 05:43:51 -0500 Subject: [PATCH 1/5] [base] Create a prebuilt Repeated for concatinating Expr[String] values --- .../typeclass/VersionSpecificRepeated.scala | 62 +++++++++++++++++++ .../typeclass/VersionSpecificRepeated.scala | 48 ++++++++++++++ Base/src/main/scala/typeclass/Repeat.scala | 2 +- JsonParser/src/main/scala-2/MacroImpl.scala | 42 +------------ JsonParser/src/main/scala-3/MacroImpl.scala | 16 +---- UriParser/src/main/scala-2/MacroImpl.scala | 5 +- UriParser/src/main/scala-3/MacroImpl.scala | 5 +- 7 files changed, 117 insertions(+), 63 deletions(-) diff --git a/Base/src/main/scala-2/typeclass/VersionSpecificRepeated.scala b/Base/src/main/scala-2/typeclass/VersionSpecificRepeated.scala index 3b8efbd..d42eb76 100644 --- a/Base/src/main/scala-2/typeclass/VersionSpecificRepeated.scala +++ b/Base/src/main/scala-2/typeclass/VersionSpecificRepeated.scala @@ -4,6 +4,68 @@ package typeclass import scala.collection.mutable.Builder import scala.reflect.macros.blackbox.Context +private[typeclass] +trait VersionSpecificRepeated { + def concatenateExprString(c:Context):Repeated[c.Expr[String], c.Expr[String]] = { + import c.universe.Tree + import c.universe.Quasiquote + val ttString0 = c.universe.typeTag[String] + final class ConcatenateExprString extends Repeated[c.Expr[String], c.Expr[String]] { + val accumulatorName = c.freshName(c.universe.TermName("accumulator$")) + val accumulatorTypeTree = c.universe.TypeTree( + c.universe.rootMirror.staticClass("scala.collection.mutable.StringBuilder").asType.toTypeConstructor + ) + val accumulatorIdent = c.universe.Ident(accumulatorName) + val accumulatorValDef = c.universe.ValDef( + c.universe.NoMods, + accumulatorName, + accumulatorTypeTree, + q"new $accumulatorTypeTree()", + ) + + implicit val ttString: c.TypeTag[String] = ttString0 + + sealed trait Acc + final object AccZero extends Acc + final class AccOne(val elem: c.Expr[String]) extends Acc + final class AccMany extends Acc { + val builder: Builder[Tree, c.Expr[String]] = List.newBuilder.mapResult(stat => + c.Expr[String]( + c.universe.Block( + stat, + q"$accumulatorIdent.toString" + ) + ) + ) + } + + def init():Acc = AccZero + def append(acc:Acc, elem:c.Expr[String]):Acc = acc match { + case AccZero => new AccOne(elem) + case accOne: AccOne => { + val retval = new AccMany() + retval.builder += accumulatorValDef + retval.builder += q"$accumulatorIdent.append(${accOne.elem})" + retval.builder += q"$accumulatorIdent.append($elem)" + retval + } + case accMany: AccMany => { + accMany.builder += q"$accumulatorIdent.append($elem)" + accMany + } + } + def result(acc:Acc):c.Expr[String] = { + acc match { + case AccZero => c.Expr[String](c.universe.Literal(c.universe.Constant(""))) + case accOne: AccOne => accOne.elem + case accMany: AccMany => accMany.builder.result() + } + } + } + new ConcatenateExprString() + } +} + private[typeclass] trait VersionSpecificContraRepeated { trait ContraRepeateds[Expr[_], Type[_]] { diff --git a/Base/src/main/scala-3/typeclass/VersionSpecificRepeated.scala b/Base/src/main/scala-3/typeclass/VersionSpecificRepeated.scala index 991cecc..0a10a37 100644 --- a/Base/src/main/scala-3/typeclass/VersionSpecificRepeated.scala +++ b/Base/src/main/scala-3/typeclass/VersionSpecificRepeated.scala @@ -2,8 +2,56 @@ package name.rayrobdod.stringContextParserCombinator package typeclass import scala.collection.mutable.Builder +import scala.collection.mutable.StringBuilder import scala.quoted.* +private[typeclass] +trait VersionSpecificRepeated { + def quotedConcatenateExprString(using Quotes):Repeated[Expr[String], Expr[String]] = { + final class ConcatenateExprString extends Repeated[Expr[String], Expr[String]] { + sealed trait Acc + object AccZero extends Acc + final class AccOne(val elem: Expr[String]) extends Acc + final class AccMany extends Acc { + val builder: Builder[Expr[String], Expr[String]] = List.newBuilder.mapResult(stat => + val accumulator:Expr[StringBuilder] = '{new scala.collection.mutable.StringBuilder} + import quotes.reflect.* + val retval = ValDef.let(Symbol.spliceOwner, "builder$", accumulator.asTerm): accumulatorRef => + val accumulatorRefExpr = accumulatorRef.asExprOf[StringBuilder] + Block( + stat.map(addend => '{$accumulatorRefExpr.append($addend)}.asTerm), + Apply(Select.unique(accumulatorRef, "result"), Nil), + ) + retval.asExprOf[String] + ) + } + + def init():Acc = AccZero + def append(acc:Acc, elem:Expr[String]):Acc = acc match { + case AccZero => new AccOne(elem) + case accOne: AccOne => { + val retval = new AccMany() + retval.builder += accOne.elem + retval.builder += elem + retval + } + case accMany: AccMany => { + accMany.builder += elem + accMany + } + } + def result(acc:Acc):Expr[String] = { + acc match { + case AccZero => Expr[String]("") + case accOne: AccOne => accOne.elem + case accMany: AccMany => accMany.builder.result() + } + } + } + new ConcatenateExprString() + } +} + private[typeclass] trait VersionSpecificContraRepeated { given quotedUnit(using Quotes):ContraRepeated[Expr, Unit, Unit] = BiRepeated.quotedUnit diff --git a/Base/src/main/scala/typeclass/Repeat.scala b/Base/src/main/scala/typeclass/Repeat.scala index 938f60c..f5dfc7f 100644 --- a/Base/src/main/scala/typeclass/Repeat.scala +++ b/Base/src/main/scala/typeclass/Repeat.scala @@ -85,7 +85,7 @@ trait BiRepeated[Expr[_], A, Z] with ContraRepeated[Expr, A, Z] /** Predefined implicit implementations of Repeated */ -object Repeated extends LowPrioRepeated { +object Repeated extends VersionSpecificRepeated with LowPrioRepeated { /** * Repeated units results in a unit */ diff --git a/JsonParser/src/main/scala-2/MacroImpl.scala b/JsonParser/src/main/scala-2/MacroImpl.scala index 7320b21..d1426f9 100644 --- a/JsonParser/src/main/scala-2/MacroImpl.scala +++ b/JsonParser/src/main/scala-2/MacroImpl.scala @@ -1,50 +1,11 @@ package name.rayrobdod.stringContextParserCombinatorExample.json -import scala.collection.immutable.Seq import scala.math.BigDecimal import scala.reflect.macros.whitebox.Context import org.json4s._ import name.rayrobdod.stringContextParserCombinator._ final class MacroImpl(val c:Context {type PrefixType = JsonStringContext}) { - /** - * Creates an Expr that represents the concatenation of the component Exprs - */ - private[this] def concatenateStrings(strings:List[c.Expr[String]]):c.Expr[String] = { - strings match { - case Seq() => c.Expr[String](c.universe.Literal(c.universe.Constant(""))) - case Seq(x) => x - case _ => { - val accumulatorName = c.universe.TermName("accumulator$") - val accumulatorType = c.universe.typeTag[scala.collection.mutable.StringBuilder] - val accumulatorTypeTree = c.universe.TypeTree(accumulatorType.tpe) - val accumulatorExpr = c.Expr(c.universe.Ident(accumulatorName))(accumulatorType) - val stats = scala.collection.mutable.Buffer[c.universe.Tree]( - c.universe.ValDef( - c.universe.NoMods, - accumulatorName, - accumulatorTypeTree, - c.universe.Apply( - c.universe.Select( - c.universe.New(accumulatorTypeTree), - c.universe.termNames.CONSTRUCTOR - ), - List() - ) - ) - ) - strings.foreach(x => stats += c.universe.reify(accumulatorExpr.splice.append(x.splice)).tree) - - c.Expr[String]( - c.universe.Block( - stats.toList, - c.universe.reify(accumulatorExpr.splice.toString).tree - ) - ) - } - } - } - private[this] def assembleCollection[A](parts:List[Either[c.Expr[A], c.Expr[List[A]]]])(implicit builderType: c.universe.TypeTag[scala.collection.mutable.Builder[A, List[A]]]):c.Expr[List[A]] = { val builderName = c.freshName(c.universe.TermName("builder")) val builderTypeTree = c.universe.TypeTree(builderType.tpe) @@ -196,8 +157,7 @@ final class MacroImpl(val c:Context {type PrefixType = JsonStringContext}) { val content:Parser[c.Expr[String]] = paired( (jCharsLifted orElse jCharsImmediate) .toInterpolator - .repeat[c.Expr[Any], List[c.Expr[String]]](strategy = RepeatStrategy.Possessive) - .map(strs => concatenateStrings(strs)) + .repeat(strategy = RepeatStrategy.Possessive)(typeclass.Repeated.concatenateExprString(c)) , (jCharsImmediate).toExtractor ) diff --git a/JsonParser/src/main/scala-3/MacroImpl.scala b/JsonParser/src/main/scala-3/MacroImpl.scala index b1b3d20..a70ab80 100644 --- a/JsonParser/src/main/scala-3/MacroImpl.scala +++ b/JsonParser/src/main/scala-3/MacroImpl.scala @@ -7,19 +7,6 @@ import org.json4s._ import name.rayrobdod.stringContextParserCombinator._ object MacroImpl { - /** - * Creates an Expr that represents the concatenation of the component Exprs - */ - private def concatenateStrings(strings:Seq[Expr[String]])(using Quotes):Expr[String] = { - strings match { - case Seq() => '{ "" } - case Seq(x) => x - case _ => '{ - ${strings.foldLeft('{new _root_.java.lang.StringBuilder()})({(builder, part) => '{$builder.append($part)}})}.toString - } - } - } - private def assembleCollection[A] (parts:List[Either[Expr[A], Expr[List[A]]]]) (using Type[A], Quotes) @@ -169,8 +156,7 @@ object MacroImpl { val content:Parser[Expr[String]] = paired( (jCharsLifted orElse jCharsImmediate) .toInterpolator - .repeat(strategy = RepeatStrategy.Possessive) - .map(strs => concatenateStrings(strs)) + .repeat(strategy = RepeatStrategy.Possessive)(using typeclass.Repeated.quotedConcatenateExprString) , (jCharsImmediate).toExtractor ) diff --git a/UriParser/src/main/scala-2/MacroImpl.scala b/UriParser/src/main/scala-2/MacroImpl.scala index 00e1420..30d394b 100644 --- a/UriParser/src/main/scala-2/MacroImpl.scala +++ b/UriParser/src/main/scala-2/MacroImpl.scala @@ -170,7 +170,7 @@ object MacroImpl { val opaquePart:Interpolator[c.Expr[String]] = { val variable:Interpolator[c.Expr[String]] = ofType[String] val literal:Interpolator[c.Expr[String]] = (uriNoSlashChar andThen uriChar.repeat()).mapToExpr - (variable orElse literal).repeat().map(xs => concatenateStrings(c)(xs)) + (variable orElse literal).repeat()(typeclass.Repeated.concatenateExprString(c)) } @@ -181,8 +181,7 @@ object MacroImpl { val fragmentOrQueryString:Interpolator[c.Expr[String]] = { val Arbitrary = (ofType[String] orElse uriChar.repeat(1).mapToExpr) - .repeat() - .map(xs => concatenateStrings(c)(xs)) + .repeat()(typeclass.Repeated.concatenateExprString(c)) val Mapping = { implicit def AndThenElemElem:typeclass.Sequenced[c.Expr[String], c.Expr[String], List[c.Expr[String]]] = (a:c.Expr[String],b:c.Expr[String]) => a :: b :: Nil implicit def AndThenElemList:typeclass.Sequenced[c.Expr[String], List[c.Expr[String]], List[c.Expr[String]]] = (a:c.Expr[String], b:List[c.Expr[String]]) => a +: b diff --git a/UriParser/src/main/scala-3/MacroImpl.scala b/UriParser/src/main/scala-3/MacroImpl.scala index b5a7b70..71eb0d9 100644 --- a/UriParser/src/main/scala-3/MacroImpl.scala +++ b/UriParser/src/main/scala-3/MacroImpl.scala @@ -133,7 +133,7 @@ object MacroImpl { private def opaquePart(using Quotes):Interpolator[Expr[String]] = { val variable:Interpolator[Expr[String]] = ofType[String] val literal:Interpolator[Expr[String]] = (uriNoSlashChar andThen uriChar.repeat()).mapToExpr - (variable orElse literal).repeat().map(xs => concatenateStrings(xs)) + (variable orElse literal).repeat()(using typeclass.Repeated.quotedConcatenateExprString) } @@ -144,8 +144,7 @@ object MacroImpl { private def fragmentOrQueryString(using Quotes):Interpolator[Expr[String]] = { val Arbitrary = (ofType[String] orElse uriChar.repeat(1).mapToExpr) - .repeat() - .map(xs => concatenateStrings(xs)) + .repeat()(using typeclass.Repeated.quotedConcatenateExprString) val Mapping = { given typeclass.Sequenced[Expr[String], Expr[String], List[Expr[String]]] = (a, b) => a :: b :: Nil given typeclass.Sequenced[Expr[String], List[Expr[String]], List[Expr[String]]] = (a, b) => a +: b From b89dec0f082075263b7b9ac7121ce1941623e37c Mon Sep 17 00:00:00 2001 From: Raymond Dodge Date: Wed, 31 Jan 2024 08:07:44 -0500 Subject: [PATCH 2/5] [uri] use `addString` when splicing a map into a fragment or query string when the string would have been placed in a string builder anyway. If the map is the only part of the query string, continue to use `mkString` Improve names while modifying that area Remove the `concatenateStrings` method, completing the point of the parent commit --- UriParser/src/main/scala-2/MacroImpl.scala | 156 ++++++++++++--------- UriParser/src/main/scala-3/MacroImpl.scala | 113 ++++++++++----- 2 files changed, 162 insertions(+), 107 deletions(-) diff --git a/UriParser/src/main/scala-2/MacroImpl.scala b/UriParser/src/main/scala-2/MacroImpl.scala index 30d394b..d3ba9fe 100644 --- a/UriParser/src/main/scala-2/MacroImpl.scala +++ b/UriParser/src/main/scala-2/MacroImpl.scala @@ -10,45 +10,6 @@ import name.rayrobdod.stringContextParserCombinator.RepeatStrategy._ import name.rayrobdod.stringContextParserCombinatorExample.uri.ConcatenateStringImplicits._ object MacroImpl { - /** - * Creates an Expr that represents the concatenation of the component Exprs - */ - def concatenateStrings(c:Context)(strings:Seq[c.Expr[String]]):c.Expr[String] = { - import c.universe.Quasiquote - strings match { - case Seq() => c.Expr[String](c.universe.Literal(c.universe.Constant(""))) - case Seq(x) => x - case _ => { - val accumulatorName = c.universe.TermName("accumulator$") - val accumulatorType = c.universe.typeTag[scala.collection.mutable.StringBuilder] - val accumulatorTypeTree = c.universe.TypeTree(accumulatorType.tpe) - val accumulatorExpr = c.Expr(c.universe.Ident(accumulatorName))(accumulatorType) - val stats = scala.collection.mutable.Buffer[c.universe.Tree]( - c.universe.ValDef( - c.universe.NoMods, - accumulatorName, - accumulatorTypeTree, - c.universe.Apply( - c.universe.Select( - c.universe.New(accumulatorTypeTree), - c.universe.termNames.CONSTRUCTOR - ), - List() - ) - ) - ) - strings.foreach(x => stats += q"$accumulatorExpr.append($x)") - - c.Expr[String]( - c.universe.Block( - stats.toList, - q"$accumulatorExpr.toString" - ) - ) - } - } - } - def stringContext_uri(c:Context {type PrefixType = UriStringContext})(args:c.Expr[Any]*):c.Expr[URI] = { val LeafParsers = Interpolator.contextInterpolators(c) import LeafParsers._ @@ -183,38 +144,95 @@ object MacroImpl { val Arbitrary = (ofType[String] orElse uriChar.repeat(1).mapToExpr) .repeat()(typeclass.Repeated.concatenateExprString(c)) val Mapping = { - implicit def AndThenElemElem:typeclass.Sequenced[c.Expr[String], c.Expr[String], List[c.Expr[String]]] = (a:c.Expr[String],b:c.Expr[String]) => a :: b :: Nil - implicit def AndThenElemList:typeclass.Sequenced[c.Expr[String], List[c.Expr[String]], List[c.Expr[String]]] = (a:c.Expr[String], b:List[c.Expr[String]]) => a +: b - implicit def AndThenListElem:typeclass.Sequenced[List[c.Expr[String]], c.Expr[String], List[c.Expr[String]]] = (a:List[c.Expr[String]], b:c.Expr[String]) => a :+ b - implicit def AndThenListList:typeclass.Sequenced[List[c.Expr[String]], List[c.Expr[String]], List[c.Expr[String]]] = (a:List[c.Expr[String]], b:List[c.Expr[String]]) => a ++: b - final class ListRepeatTypes[A] extends typeclass.Repeated[List[A], List[A]] { - type Acc = scala.collection.mutable.Builder[A, List[A]] - def init():Acc = List.newBuilder - def append(acc:Acc, elem:List[A]):Acc = {acc ++= elem} - def result(acc:Acc):List[A] = acc.result() + val accumulatorName = c.freshName(c.universe.TermName("accumulator$")) + val accumulatorTypeTree = c.universe.TypeTree( + c.universe.rootMirror.staticClass("scala.collection.mutable.StringBuilder").asType.toTypeConstructor + ) + val accumulatorIdent = c.universe.Ident(accumulatorName) + val accumulatorValDef = c.universe.ValDef( + c.universe.NoMods, + accumulatorName, + accumulatorTypeTree, + q"new $accumulatorTypeTree()", + ) + + class StringExpr private (val isEmpty: Boolean, private val direct: Option[c.Expr[String]], private val accStats: List[c.Tree]) { + def ++(other: StringExpr): StringExpr = { + if (this.isEmpty) { + other + } else if (other.isEmpty) { + this + } else { + new StringExpr(false, None, this.accStats ++: other.accStats) + } + } + def result: c.Expr[String] = { + this.direct match { + case Some(x) => x + case None => { + c.Expr[String]( + c.universe.Block( + accumulatorValDef :: accStats, + q"$accumulatorIdent.toString" + ) + ) + } + } + } + } + object StringExpr { + def empty: StringExpr = new StringExpr(true, Option(constExpr("")), Nil) + def single(direct: c.Expr[String]): StringExpr = new StringExpr(false, Some(direct), List(q"""$accumulatorIdent.append($direct)""")) + def single(direct: c.Expr[String], accStats: List[c.Tree]): StringExpr = new StringExpr(false, Some(direct), accStats) + def multiple(accStats: List[c.Tree]): StringExpr = new StringExpr(false, None, accStats) + } + + implicit def AndThenStringExpr: typeclass.Sequenced[StringExpr, StringExpr, StringExpr] = (a:StringExpr, b:StringExpr) => a ++ b + final class RepeatStringExpr extends typeclass.Repeated[StringExpr, c.Expr[String]] { + type Acc = StringExpr + def init():Acc = StringExpr.empty + def append(acc:Acc, elem:StringExpr):Acc = { + if (acc.isEmpty) { + elem + } else { + acc ++ StringExpr.single(constExpr("&")) ++ elem + } + } + def result(acc:Acc):c.Expr[String] = acc.result } - implicit def ListRepeatTypes[A]:typeclass.Repeated[List[A], List[A]] = new ListRepeatTypes[A] - val EqualsChar = codePointIn("=").map(_.toString).mapToExpr - val AndChar = codePointIn("&").map(_.toString).mapToExpr - - val tupleConcatFun = q""" {ab:(String, String) => ab._1 + "=" + ab._2} """ - val lit:Interpolator[c.Expr[String]] = (escapedChar orElse unreservedChar orElse codePointIn(";?:@+$,")).repeat().mapToExpr - val str:Interpolator[c.Expr[String]] = ofType[String] - val str2:Interpolator[c.Expr[String]] = str orElse lit - val pair:Interpolator[List[c.Expr[String]]] = ofType(c.typeTag[scala.Tuple2[String, String]]) - .map(x => List( - c.Expr[String](q"$x._1"), - constExpr("="), - c.Expr[String](q"$x._2") + implicit def RepeatStringExpr: typeclass.Repeated[StringExpr, c.Expr[String]] = new RepeatStringExpr + + val EqualsChar = isString("=").map(_ => StringExpr.single(constExpr("="))) + val AndChar = isString("&") + + val tupleConcatFun = q""" {pair:(String, String) => pair._1 + "=" + pair._2} """ + + val literalString:Interpolator[c.Expr[String]] = (escapedChar orElse unreservedChar orElse codePointIn(";?:@+$,")).repeat(1).mapToExpr + val holeString:Interpolator[c.Expr[String]] = ofType[String] + val string:Interpolator[StringExpr] = (holeString orElse literalString).map(s => StringExpr.single(s)) + + val holePair:Interpolator[StringExpr] = ofType(c.typeTag[scala.Tuple2[String, String]]) + .map(x => + StringExpr.multiple( + List( + q"$accumulatorIdent.append($x._1)", + q"""$accumulatorIdent.append("=")""", + q"$accumulatorIdent.append($x._2)", + ) + ) + ) + val literalPair: Interpolator[StringExpr] = (string andThen EqualsChar andThen string) + val pair:Interpolator[StringExpr] = holePair orElse literalPair + + val map:Interpolator[StringExpr] = ofType(c.typeTag[scala.collection.Map[String, String]]) + .map(m => StringExpr.single( + c.Expr[String](q"""$m.map($tupleConcatFun).mkString("&")"""), + List(q"""$m.map($tupleConcatFun).addString($accumulatorIdent, "&")"""), )) - val pair2:Interpolator[List[c.Expr[String]]] = pair orElse (str2 andThen EqualsChar andThen str2) - val map:Interpolator[List[c.Expr[String]]] = ofType(c.typeTag[scala.collection.Map[String, String]]) - .map(x => c.Expr[List[String]](q"$x.map($tupleConcatFun)")) - .map(x => List(c.Expr[String](q""" $x.mkString("&") """))) - val mapOrPair:Interpolator[List[c.Expr[String]]] = map orElse pair2 - - (mapOrPair andThen (AndChar andThen mapOrPair).repeat()) - .map(xs => concatenateStrings(c)(xs)) + + val mapOrPair:Interpolator[StringExpr] = map orElse pair + + mapOrPair.repeat(min = 1, delimiter = AndChar)(using RepeatStringExpr) } Mapping.attempt orElse Arbitrary } diff --git a/UriParser/src/main/scala-3/MacroImpl.scala b/UriParser/src/main/scala-3/MacroImpl.scala index 71eb0d9..6b8c825 100644 --- a/UriParser/src/main/scala-3/MacroImpl.scala +++ b/UriParser/src/main/scala-3/MacroImpl.scala @@ -7,17 +7,6 @@ import name.rayrobdod.stringContextParserCombinator.RepeatStrategy._ import name.rayrobdod.stringContextParserCombinatorExample.uri.ConcatenateStringImplicits.{given} object MacroImpl { - /** - * Creates an Expr that represents the concatenation of the component Exprs - */ - private def concatenateStrings(strings:Seq[Expr[String]])(using Quotes):Expr[String] = { - strings match { - case Seq() => '{ "" } - case Seq(x) => x - case _ => '{ ${Expr.ofSeq(strings)}.mkString } - } - } - import name.rayrobdod.stringContextParserCombinator.Interpolator._ private def parseByteHex(x:(Char, Char)):Int = java.lang.Integer.parseInt(s"${x._1}${x._2}", 16) @@ -146,38 +135,86 @@ object MacroImpl { val Arbitrary = (ofType[String] orElse uriChar.repeat(1).mapToExpr) .repeat()(using typeclass.Repeated.quotedConcatenateExprString) val Mapping = { - given typeclass.Sequenced[Expr[String], Expr[String], List[Expr[String]]] = (a, b) => a :: b :: Nil - given typeclass.Sequenced[Expr[String], List[Expr[String]], List[Expr[String]]] = (a, b) => a +: b - given typeclass.Sequenced[List[Expr[String]], Expr[String], List[Expr[String]]] = (a, b) => a :+ b - given typeclass.Sequenced[List[Expr[String]], List[Expr[String]], List[Expr[String]]] = (a, b) => a ++: b - given [A]:typeclass.Repeated[List[A], List[A]] = new typeclass.Repeated[List[A], List[A]] { - type Acc = scala.collection.mutable.Builder[A, List[A]] - def init():Acc = List.newBuilder - def append(acc:Acc, elem:List[A]):Acc = {acc ++= elem} - def result(acc:Acc):List[A] = acc.result + class StringExpr private (val isEmpty: Boolean, private val direct: Option[Expr[String]], private val accStats: List[Expr[StringBuilder] => Expr[Unit]]) { + def ++(other: StringExpr): StringExpr = { + if (this.isEmpty) { + other + } else if (other.isEmpty) { + this + } else { + new StringExpr(false, None, this.accStats ++: other.accStats) + } + } + def result: Expr[String] = { + this.direct match { + case Some(x) => x + case None => { + val accumulator:Expr[StringBuilder] = '{new scala.collection.mutable.StringBuilder} + import quotes.reflect.* + val retval = ValDef.let(Symbol.spliceOwner, "builder$", accumulator.asTerm): accumulatorRef => + val accumulatorRefExpr = accumulatorRef.asExprOf[StringBuilder] + Block( + accStats.map(accStat => accStat(accumulatorRefExpr).asTerm), + Apply(Select.unique(accumulatorRef, "toString"), Nil), + ) + retval.asExprOf[String] + } + } + } + } + object StringExpr { + def empty: StringExpr = new StringExpr(true, Option(Expr("")), Nil) + def single(direct: Expr[String]): StringExpr = new StringExpr(false, Some(direct), List(acc => '{$acc.append($direct); ()})) + def single(direct: Expr[String], accStats: List[Expr[StringBuilder] => Expr[Unit]]): StringExpr = new StringExpr(false, Some(direct), accStats) + def multiple(accStats: List[Expr[StringBuilder] => Expr[Unit]]): StringExpr = new StringExpr(false, None, accStats) } - val EqualsChar = codePointIn("=").map(x => Expr.apply(x.toString)) - val AndChar = codePointIn("&").map(x => Expr.apply(x.toString)) + implicit def AndThenStringExpr: typeclass.Sequenced[StringExpr, StringExpr, StringExpr] = (a:StringExpr, b:StringExpr) => a ++ b + final class RepeatStringExpr extends typeclass.Repeated[StringExpr, Expr[String]] { + type Acc = StringExpr + def init():Acc = StringExpr.empty + def append(acc:Acc, elem:StringExpr):Acc = { + if (acc.isEmpty) { + elem + } else { + acc ++ StringExpr.single(Expr("&")) ++ elem + } + } + def result(acc:Acc):Expr[String] = acc.result + } + implicit def RepeatStringExpr: typeclass.Repeated[StringExpr, Expr[String]] = new RepeatStringExpr + + val EqualsChar = isString("=").map(_ => StringExpr.single(Expr("="))) + val AndChar = isString("&") val tupleConcatFun = '{ {(ab:Tuple2[String, String]) => ab._1 + "=" + ab._2} } - val lit:Interpolator[Expr[String]] = (escapedChar orElse unreservedChar orElse codePointIn(";?:@+$,")).repeat().mapToExpr - val str:Interpolator[Expr[String]] = ofType[String] - val str2:Interpolator[Expr[String]] = str orElse lit - val pair:Interpolator[List[Expr[String]]] = ofType[scala.Tuple2[String, String]] - .map(x => List( - '{ $x._1 }, - Expr.apply("="), - '{ $x._2 }, + + val literalString:Interpolator[Expr[String]] = (escapedChar orElse unreservedChar orElse codePointIn(";?:@+$,")).repeat().mapToExpr + val holeString:Interpolator[Expr[String]] = ofType[String] + val string:Interpolator[StringExpr] = (holeString orElse literalString).map(s => StringExpr.single(s)) + + val holePair:Interpolator[StringExpr] = ofType[scala.Tuple2[String, String]] + .map(x => + StringExpr.multiple( + List( + (sb: Expr[StringBuilder]) => '{ $sb.append($x._1); () }, + (sb: Expr[StringBuilder]) => '{ $sb.append("="); () }, + (sb: Expr[StringBuilder]) => '{ $sb.append($x._2); () }, + ) + ) + ) + val literalPair:Interpolator[StringExpr] = (string andThen EqualsChar andThen string) + val pair:Interpolator[StringExpr] = holePair orElse literalPair + + val map:Interpolator[StringExpr] = ofType[scala.collection.Map[String, String]] + .map(m => StringExpr.single( + '{ $m.map($tupleConcatFun).mkString("&") }, + List((sb: Expr[StringBuilder]) => '{$m.map($tupleConcatFun).addString($sb, "&"); ()}) )) - val pair2:Interpolator[List[Expr[String]]] = pair orElse (str2 andThen EqualsChar andThen str2) - val map:Interpolator[List[Expr[String]]] = ofType[scala.collection.Map[String, String]] - .map(x => '{$x.map($tupleConcatFun)}) - .map(x => List('{ $x.mkString("&") })) - val mapOrPair:Interpolator[List[Expr[String]]] = map orElse pair2 - - (mapOrPair andThen (AndChar andThen mapOrPair).repeat()) - .map(xs => concatenateStrings(xs)) + + val mapOrPair:Interpolator[StringExpr] = map orElse pair + + mapOrPair.repeat(min = 1, delimiter = AndChar)(using RepeatStringExpr) } Mapping.attempt orElse Arbitrary } From ab1c03ca1ea9885dbda6c70ba584c4fc5f0cafd3 Mon Sep 17 00:00:00 2001 From: Raymond Dodge Date: Fri, 23 Feb 2024 09:23:39 -0500 Subject: [PATCH 3/5] [base] Use a more consistent name for the new repeateds --- .../main/scala-2/typeclass/VersionSpecificRepeated.scala | 6 +++--- .../main/scala-3/typeclass/VersionSpecificRepeated.scala | 6 +++--- JsonParser/src/main/scala-2/MacroImpl.scala | 2 +- JsonParser/src/main/scala-3/MacroImpl.scala | 2 +- UriParser/src/main/scala-2/MacroImpl.scala | 4 ++-- UriParser/src/main/scala-3/MacroImpl.scala | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Base/src/main/scala-2/typeclass/VersionSpecificRepeated.scala b/Base/src/main/scala-2/typeclass/VersionSpecificRepeated.scala index d42eb76..fd7f04c 100644 --- a/Base/src/main/scala-2/typeclass/VersionSpecificRepeated.scala +++ b/Base/src/main/scala-2/typeclass/VersionSpecificRepeated.scala @@ -6,11 +6,11 @@ import scala.reflect.macros.blackbox.Context private[typeclass] trait VersionSpecificRepeated { - def concatenateExprString(c:Context):Repeated[c.Expr[String], c.Expr[String]] = { + def forContextConcatenateString(c:Context):Repeated[c.Expr[String], c.Expr[String]] = { import c.universe.Tree import c.universe.Quasiquote val ttString0 = c.universe.typeTag[String] - final class ConcatenateExprString extends Repeated[c.Expr[String], c.Expr[String]] { + final class ConcatenateString extends Repeated[c.Expr[String], c.Expr[String]] { val accumulatorName = c.freshName(c.universe.TermName("accumulator$")) val accumulatorTypeTree = c.universe.TypeTree( c.universe.rootMirror.staticClass("scala.collection.mutable.StringBuilder").asType.toTypeConstructor @@ -62,7 +62,7 @@ trait VersionSpecificRepeated { } } } - new ConcatenateExprString() + new ConcatenateString() } } diff --git a/Base/src/main/scala-3/typeclass/VersionSpecificRepeated.scala b/Base/src/main/scala-3/typeclass/VersionSpecificRepeated.scala index 0a10a37..a42f3c6 100644 --- a/Base/src/main/scala-3/typeclass/VersionSpecificRepeated.scala +++ b/Base/src/main/scala-3/typeclass/VersionSpecificRepeated.scala @@ -7,8 +7,8 @@ import scala.quoted.* private[typeclass] trait VersionSpecificRepeated { - def quotedConcatenateExprString(using Quotes):Repeated[Expr[String], Expr[String]] = { - final class ConcatenateExprString extends Repeated[Expr[String], Expr[String]] { + def quotedConcatenateString(using Quotes):Repeated[Expr[String], Expr[String]] = { + final class ConcatenateString extends Repeated[Expr[String], Expr[String]] { sealed trait Acc object AccZero extends Acc final class AccOne(val elem: Expr[String]) extends Acc @@ -48,7 +48,7 @@ trait VersionSpecificRepeated { } } } - new ConcatenateExprString() + new ConcatenateString() } } diff --git a/JsonParser/src/main/scala-2/MacroImpl.scala b/JsonParser/src/main/scala-2/MacroImpl.scala index d1426f9..ca4c29d 100644 --- a/JsonParser/src/main/scala-2/MacroImpl.scala +++ b/JsonParser/src/main/scala-2/MacroImpl.scala @@ -157,7 +157,7 @@ final class MacroImpl(val c:Context {type PrefixType = JsonStringContext}) { val content:Parser[c.Expr[String]] = paired( (jCharsLifted orElse jCharsImmediate) .toInterpolator - .repeat(strategy = RepeatStrategy.Possessive)(typeclass.Repeated.concatenateExprString(c)) + .repeat(strategy = RepeatStrategy.Possessive)(typeclass.Repeated.forContextConcatenateString(c)) , (jCharsImmediate).toExtractor ) diff --git a/JsonParser/src/main/scala-3/MacroImpl.scala b/JsonParser/src/main/scala-3/MacroImpl.scala index a70ab80..8668e26 100644 --- a/JsonParser/src/main/scala-3/MacroImpl.scala +++ b/JsonParser/src/main/scala-3/MacroImpl.scala @@ -156,7 +156,7 @@ object MacroImpl { val content:Parser[Expr[String]] = paired( (jCharsLifted orElse jCharsImmediate) .toInterpolator - .repeat(strategy = RepeatStrategy.Possessive)(using typeclass.Repeated.quotedConcatenateExprString) + .repeat(strategy = RepeatStrategy.Possessive)(using typeclass.Repeated.quotedConcatenateString) , (jCharsImmediate).toExtractor ) diff --git a/UriParser/src/main/scala-2/MacroImpl.scala b/UriParser/src/main/scala-2/MacroImpl.scala index d3ba9fe..4762a28 100644 --- a/UriParser/src/main/scala-2/MacroImpl.scala +++ b/UriParser/src/main/scala-2/MacroImpl.scala @@ -131,7 +131,7 @@ object MacroImpl { val opaquePart:Interpolator[c.Expr[String]] = { val variable:Interpolator[c.Expr[String]] = ofType[String] val literal:Interpolator[c.Expr[String]] = (uriNoSlashChar andThen uriChar.repeat()).mapToExpr - (variable orElse literal).repeat()(typeclass.Repeated.concatenateExprString(c)) + (variable orElse literal).repeat()(typeclass.Repeated.forContextConcatenateString(c)) } @@ -142,7 +142,7 @@ object MacroImpl { val fragmentOrQueryString:Interpolator[c.Expr[String]] = { val Arbitrary = (ofType[String] orElse uriChar.repeat(1).mapToExpr) - .repeat()(typeclass.Repeated.concatenateExprString(c)) + .repeat()(typeclass.Repeated.forContextConcatenateString(c)) val Mapping = { val accumulatorName = c.freshName(c.universe.TermName("accumulator$")) val accumulatorTypeTree = c.universe.TypeTree( diff --git a/UriParser/src/main/scala-3/MacroImpl.scala b/UriParser/src/main/scala-3/MacroImpl.scala index 6b8c825..e7dda48 100644 --- a/UriParser/src/main/scala-3/MacroImpl.scala +++ b/UriParser/src/main/scala-3/MacroImpl.scala @@ -122,7 +122,7 @@ object MacroImpl { private def opaquePart(using Quotes):Interpolator[Expr[String]] = { val variable:Interpolator[Expr[String]] = ofType[String] val literal:Interpolator[Expr[String]] = (uriNoSlashChar andThen uriChar.repeat()).mapToExpr - (variable orElse literal).repeat()(using typeclass.Repeated.quotedConcatenateExprString) + (variable orElse literal).repeat()(using typeclass.Repeated.quotedConcatenateString) } @@ -133,7 +133,7 @@ object MacroImpl { private def fragmentOrQueryString(using Quotes):Interpolator[Expr[String]] = { val Arbitrary = (ofType[String] orElse uriChar.repeat(1).mapToExpr) - .repeat()(using typeclass.Repeated.quotedConcatenateExprString) + .repeat()(using typeclass.Repeated.quotedConcatenateString) val Mapping = { class StringExpr private (val isEmpty: Boolean, private val direct: Option[Expr[String]], private val accStats: List[Expr[StringBuilder] => Expr[Unit]]) { def ++(other: StringExpr): StringExpr = { From c8b9995c9a8a96f48c6f2fea78a296ee9e10a013 Mon Sep 17 00:00:00 2001 From: Raymond Dodge Date: Fri, 23 Feb 2024 23:46:39 -0500 Subject: [PATCH 4/5] [base] Add an id variant of the string concatenator repeated Also, add some unit tests for the new repeateds --- Base/src/main/scala/typeclass/Repeat.scala | 22 ++++++++++++++ .../VersonSpecificRepeatedTest.scala | 28 +++++++++++++++++ .../VersonSpecificRepeatedTestImpls.scala | 30 +++++++++++++++++++ .../test/scala/typeclass/RepeatedTest.scala | 21 +++++++++++++ 4 files changed, 101 insertions(+) create mode 100644 Base/src/test/scala-3/typeclass/VersonSpecificRepeatedTest.scala create mode 100644 Base/src/test/scala-3/typeclass/VersonSpecificRepeatedTestImpls.scala create mode 100644 Base/src/test/scala/typeclass/RepeatedTest.scala diff --git a/Base/src/main/scala/typeclass/Repeat.scala b/Base/src/main/scala/typeclass/Repeat.scala index f5dfc7f..f99af5f 100644 --- a/Base/src/main/scala/typeclass/Repeat.scala +++ b/Base/src/main/scala/typeclass/Repeat.scala @@ -86,6 +86,20 @@ trait BiRepeated[Expr[_], A, Z] /** Predefined implicit implementations of Repeated */ object Repeated extends VersionSpecificRepeated with LowPrioRepeated { + private def apply[A, Acc, Z]( + initFn: () => Acc, + appendFn: (Acc, A) => Acc, + resultFn: Acc => Z, + ): Repeated[A, Z] = { + type Acc2 = Acc + new Repeated[A, Z] { + type Acc = Acc2 + def init():Acc = initFn() + def append(acc:Acc, elem:A):Acc = appendFn(acc, elem) + def result(acc:Acc):Z = resultFn(acc) + } + } + /** * Repeated units results in a unit */ @@ -124,6 +138,14 @@ object Repeated extends VersionSpecificRepeated with LowPrioRepeated { } new RepeatedCodepoint() } + + def idConcatenateString:Repeated[String, String] = { + Repeated.apply( + () => new StringBuilder, + (acc:StringBuilder, elem:String) => acc ++= elem, + (acc:StringBuilder) => acc.toString, + ) + } } private[typeclass] trait LowPrioRepeated { diff --git a/Base/src/test/scala-3/typeclass/VersonSpecificRepeatedTest.scala b/Base/src/test/scala-3/typeclass/VersonSpecificRepeatedTest.scala new file mode 100644 index 0000000..c411be7 --- /dev/null +++ b/Base/src/test/scala-3/typeclass/VersonSpecificRepeatedTest.scala @@ -0,0 +1,28 @@ +package name.rayrobdod.stringContextParserCombinator +package typeclass +package repeated + +import scala.quoted.* +import munit.Location + +final class QuotedConcatenateStringTest extends munit.FunSuite { + inline def assertParseSuccess( + inline sc: StringContext, + inline args: Any*)( + expecting:String)( + using loc:Location + ):Unit = ${ + QuotedConcatenateStringTestImpls.assertParseSuccessImpl( + 'this, 'sc, 'args, 'expecting, 'loc) + } + + test ("0") { + assertParseSuccess(StringContext(""))("") + } + test ("1") { + assertParseSuccess(StringContext("ab"))("ab") + } + test ("many") { + assertParseSuccess(StringContext("ab", "cd", "ef"), "12", "34")("ab12cd34ef") + } +} diff --git a/Base/src/test/scala-3/typeclass/VersonSpecificRepeatedTestImpls.scala b/Base/src/test/scala-3/typeclass/VersonSpecificRepeatedTestImpls.scala new file mode 100644 index 0000000..76c4487 --- /dev/null +++ b/Base/src/test/scala-3/typeclass/VersonSpecificRepeatedTestImpls.scala @@ -0,0 +1,30 @@ +package name.rayrobdod.stringContextParserCombinator +package typeclass +package repeated + +import scala.quoted.* +import munit.Location +import Interpolator.{charWhere, ofType, end} + +object QuotedConcatenateStringTestImpls { + def assertParseSuccessImpl( + self: Expr[munit.FunSuite], + sc: Expr[StringContext], + args: Expr[Seq[Any]], + expecting: Expr[String], + loc: Expr[Location])( + using Quotes + ):Expr[Unit] = { + + val dut = (charWhere(_ => true).repeat(1).mapToExpr orElse ofType[String]) + .repeat()(using Repeated.quotedConcatenateString) + .andThen(end) + + val actual = dut.interpolate(sc, args) + + '{ + given Location = ${loc} + $self.assertEquals($actual, $expecting) + } + } +} diff --git a/Base/src/test/scala/typeclass/RepeatedTest.scala b/Base/src/test/scala/typeclass/RepeatedTest.scala new file mode 100644 index 0000000..c7c3bd4 --- /dev/null +++ b/Base/src/test/scala/typeclass/RepeatedTest.scala @@ -0,0 +1,21 @@ +package name.rayrobdod.stringContextParserCombinator +package typeclass +package repeated + +import Interpolator.idInterpolators.{charWhere, ofType, end} + +final class IdConcatenateString extends BaseInterpolatorSuite { + val dut = (charWhere(_ => true).repeat(1) orElse ofType[String]) + .repeat()(using Repeated.idConcatenateString) + .andThen(end) + + test ("0") { + assertParseSuccess(dut, ("" :: Nil, Nil), "") + } + test ("1") { + assertParseSuccess(dut, ("ab" :: Nil, Nil), "ab") + } + test ("many") { + assertParseSuccess(dut, ("ab" :: "cd" :: "ef" :: Nil, "12" :: "34" :: Nil), "ab12cd34ef") + } +} From 4e3b25a4d48d985be8cf72a0ba1d3eb52d5ae005 Mon Sep 17 00:00:00 2001 From: Raymond Dodge Date: Sat, 2 Mar 2024 14:58:24 -0500 Subject: [PATCH 5/5] [base] Add change notes and @since tags to the new concatenateString methods --- .../scala-2/typeclass/VersionSpecificRepeated.scala | 4 ++++ .../scala-3/typeclass/VersionSpecificRepeated.scala | 4 ++++ Base/src/main/scala/typeclass/Repeat.scala | 4 ++++ CHANGES.md | 10 ++++++++++ 4 files changed, 22 insertions(+) create mode 100644 CHANGES.md diff --git a/Base/src/main/scala-2/typeclass/VersionSpecificRepeated.scala b/Base/src/main/scala-2/typeclass/VersionSpecificRepeated.scala index fd7f04c..0300856 100644 --- a/Base/src/main/scala-2/typeclass/VersionSpecificRepeated.scala +++ b/Base/src/main/scala-2/typeclass/VersionSpecificRepeated.scala @@ -6,6 +6,10 @@ import scala.reflect.macros.blackbox.Context private[typeclass] trait VersionSpecificRepeated { + /** + * Creates an Expr[String] consisting of the concatenation of the component Expr[String]s + * @since 0.1.1 + */ def forContextConcatenateString(c:Context):Repeated[c.Expr[String], c.Expr[String]] = { import c.universe.Tree import c.universe.Quasiquote diff --git a/Base/src/main/scala-3/typeclass/VersionSpecificRepeated.scala b/Base/src/main/scala-3/typeclass/VersionSpecificRepeated.scala index a42f3c6..2db3b23 100644 --- a/Base/src/main/scala-3/typeclass/VersionSpecificRepeated.scala +++ b/Base/src/main/scala-3/typeclass/VersionSpecificRepeated.scala @@ -7,6 +7,10 @@ import scala.quoted.* private[typeclass] trait VersionSpecificRepeated { + /** + * Creates an Expr[String] consisting of the concatenation of the component Expr[String]s + * @since 0.1.1 + */ def quotedConcatenateString(using Quotes):Repeated[Expr[String], Expr[String]] = { final class ConcatenateString extends Repeated[Expr[String], Expr[String]] { sealed trait Acc diff --git a/Base/src/main/scala/typeclass/Repeat.scala b/Base/src/main/scala/typeclass/Repeat.scala index f99af5f..ae7ed44 100644 --- a/Base/src/main/scala/typeclass/Repeat.scala +++ b/Base/src/main/scala/typeclass/Repeat.scala @@ -139,6 +139,10 @@ object Repeated extends VersionSpecificRepeated with LowPrioRepeated { new RepeatedCodepoint() } + /** + * Creates a String consisting of the concatenation of the component strings + * @since 0.1.1 + */ def idConcatenateString:Repeated[String, String] = { Repeated.apply( () => new StringBuilder, diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..d1c56ba --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,10 @@ +# Changelog + +## [Unreleased] +* Add built-in but explicit Repeated instances for concatenating strings + * `idConcatenateString` for id context + * `forContextConcatenateString` for scala-2 macro context + * `quotedConcatenateString` for scala-3 quoted context + +## [0.1.0] 2024-02-01 +Initial tagged version