Skip to content

Commit

Permalink
Rewrite and adapt to Scala 3 features (#51)
Browse files Browse the repository at this point in the history
Rewrite and adapt to Scala 3 features
  • Loading branch information
hamzaremmal authored Mar 25, 2024
2 parents 1600462 + 0540802 commit 71c6c4b
Show file tree
Hide file tree
Showing 13 changed files with 190 additions and 193 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ lazy val root = project

scalaVersion := dottyVersion,
scalacOptions ++= Seq(
"-deprecation"
"-deprecation", "-feature", "-language:implicitConversions"
),
libraryDependencies ++= Seq(
"org.scala-lang.modules" % "scala-parser-combinators_2.13" % "1.1.2",
Expand Down
104 changes: 48 additions & 56 deletions src/main/scala/dotty/xml/interpolator/internal/Expand.scala
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
package dotty.xml.interpolator
package internal

import scala.language.implicitConversions
import scala.quoted._

import dotty.xml.interpolator.internal.Tree._
import Tree.*

object Expand {
object Expand:

def apply(nodes: Seq[Node])(implicit ctx: XmlContext, q: Quotes): Expr[scala.xml.Node | scala.xml.NodeBuffer] = {
if (nodes.size == 1) expandNode(nodes.head).asInstanceOf[Expr[scala.xml.Node]]
def apply(nodes: Seq[Node])(using XmlContext, Quotes): Expr[scala.xml.Node | scala.xml.NodeBuffer] =
if nodes.size == 1 then expandNode(nodes.head).asInstanceOf[Expr[scala.xml.Node]]
else expandNodes(nodes)
}

private def expandNode(node: Node)(implicit ctx: XmlContext, q: Quotes): Expr[Any] = {
node match {
private def expandNode(node: Node)(using XmlContext, Quotes): Expr[Any] =
node match
case group: Group => expandGroup(group)
case elem: Elem => expandElem(elem)
case text: Text => expandText(text)
Expand All @@ -24,33 +22,32 @@ object Expand {
case procInstr: ProcInstr => expandProcInstr(procInstr)
case entityRef: EntityRef => expandEntityRef(entityRef)
case unparsed: Unparsed => expandUnparsed(unparsed)
}
}
end expandNode

private def expandNodes(nodes: Seq[Node])(implicit ctx: XmlContext, q: Quotes): Expr[scala.xml.NodeBuffer] = {
nodes.foldLeft('{ new _root_.scala.xml.NodeBuffer() })((expr, node) => '{ $expr &+ ${expandNode(node)} } )
}
private def expandNodes(nodes: Seq[Node])(using XmlContext, Quotes): Expr[scala.xml.NodeBuffer] =
nodes.foldLeft('{ scala.xml.NodeBuffer() }): (expr, node) =>
'{ $expr &+ ${ expandNode(node) } }

private def expandGroup(group: Group)(implicit ctx: XmlContext, q: Quotes): Expr[scala.xml.Group] =
'{ new _root_.scala.xml.Group(${expandNodes(group.nodes)}) }
private def expandGroup(group: Group)(using XmlContext, Quotes): Expr[scala.xml.Group] =
'{ scala.xml.Group(${ expandNodes(group.nodes) }) }

private def expandElem(elem: Elem)(implicit ctx: XmlContext, q: Quotes): Expr[scala.xml.Elem] = {
private def expandElem(elem: Elem)(using ctx: XmlContext, q: Quotes): Expr[scala.xml.Elem] =
val (namespaces, attributes) = elem.attributes.partition(_.isNamespace)
val prefix = if (elem.prefix.nonEmpty) Expr(elem.prefix) else '{ null: String }
val prefix = if elem.prefix.nonEmpty then Expr(elem.prefix) else '{ null }
val label = Expr(elem.label)
val attributes1 = expandAttributes(attributes)
val scope = expandNamespaces(namespaces)
val empty = Expr(elem.end.isEmpty)
val child = expandNodes(elem.children)(new XmlContext(ctx.args, scope), q)
if (elem.children.isEmpty)
'{ new _root_.scala.xml.Elem($prefix, $label, $attributes1, $scope, $empty) }
val child = expandNodes(elem.children)(using new XmlContext(ctx.args, scope), q)
if elem.children.isEmpty then
'{ new scala.xml.Elem($prefix, $label, $attributes1, $scope, $empty) }
else
'{ new _root_.scala.xml.Elem($prefix, $label, $attributes1, $scope, $empty, _root_.scala.xml.NodeSeq.seqToNodeSeq($child): _*) }
}
'{ new scala.xml.Elem($prefix, $label, $attributes1, $scope, $empty, scala.xml.NodeSeq.seqToNodeSeq($child)*) }
end expandElem

private def expandAttributes(attributes: Seq[Attribute])(implicit ctx: XmlContext, q: Quotes): Expr[scala.xml.MetaData] = {
private def expandAttributes(attributes: Seq[Attribute])(using XmlContext, Quotes): Expr[scala.xml.MetaData] =
import quotes.reflect._
attributes.foldRight('{ _root_.scala.xml.Null }: Expr[scala.xml.MetaData])((attribute, rest) => {
attributes.foldRight('{ _root_.scala.xml.Null }: Expr[scala.xml.MetaData]): (attribute, rest) =>
val value = attribute.value match {
case Seq(v) => expandNode(v)
case vs => expandNodes(vs)
Expand All @@ -71,57 +68,52 @@ object Expand {
*/

val term = value.asTerm
if (term.tpe <:< TypeRepr.of[String]) {
if term.tpe <:< TypeRepr.of[String] then
val value = term.asExprOf[String]
if (attribute.prefix.isEmpty) '{ new _root_.scala.xml.UnprefixedAttribute(${Expr(attribute.key)}, $value, $rest) }
else '{ new _root_.scala.xml.PrefixedAttribute(${Expr(attribute.prefix)}, ${Expr(attribute.key)}, $value, $rest) }
} else if (term.tpe <:< TypeRepr.of[collection.Seq[scala.xml.Node]]) {
if attribute.prefix.isEmpty then '{ scala.xml.UnprefixedAttribute(${ Expr(attribute.key) }, $value, $rest) }
else '{ scala.xml.PrefixedAttribute(${ Expr(attribute.prefix) }, ${ Expr(attribute.key) }, $value, $rest) }
else if term.tpe <:< TypeRepr.of[collection.Seq[scala.xml.Node]] then
val value = term.asExprOf[collection.Seq[scala.xml.Node]]
if (attribute.prefix.isEmpty) '{ new _root_.scala.xml.UnprefixedAttribute(${Expr(attribute.key)}, $value, $rest) }
else '{ new _root_.scala.xml.PrefixedAttribute(${Expr(attribute.prefix)}, ${Expr(attribute.key)}, $value, $rest) }
} else {
if attribute.prefix.isEmpty then '{ scala.xml.UnprefixedAttribute(${ Expr(attribute.key) }, $value, $rest) }
else '{ scala.xml.PrefixedAttribute(${ Expr(attribute.prefix) }, ${ Expr(attribute.key) }, $value, $rest) }
else
val value = term.asExprOf[Option[collection.Seq[scala.xml.Node]]]
if (attribute.prefix.isEmpty) '{ new _root_.scala.xml.UnprefixedAttribute(${Expr(attribute.key)}, $value, $rest) }
else '{ new _root_.scala.xml.PrefixedAttribute(${Expr(attribute.prefix)}, ${Expr(attribute.key)}, $value, $rest) }
}
})
}
if attribute.prefix.isEmpty then '{ scala.xml.UnprefixedAttribute(${ Expr(attribute.key) }, $value, $rest) }
else '{ scala.xml.PrefixedAttribute(${ Expr(attribute.prefix) }, ${ Expr(attribute.key) }, $value, $rest) }
end expandAttributes

private def expandNamespaces(namespaces: Seq[Attribute])(implicit ctx: XmlContext, q: Quotes): Expr[scala.xml.NamespaceBinding] = {
private def expandNamespaces(namespaces: Seq[Attribute])(using XmlContext, Quotes): Expr[Scope] =
import quotes.reflect._
namespaces.foldLeft(ctx.scope)((rest, namespace) => {
val prefix = if (namespace.prefix.nonEmpty) Expr(namespace.key) else '{ null: String }
val uri = (namespace.value.head: @unchecked) match {
namespaces.foldLeft(ctx.scope): (rest, namespace) =>
val prefix = if namespace.prefix.nonEmpty then Expr(namespace.key) else '{ null }
val uri = (namespace.value.head: @unchecked) match
case Text(text) => Expr(text)
case Placeholder(id) =>
val call = '{ ${ctx.args(id)}(using _root_.scala.xml.TopScope) }
val call = '{ ${ctx.args(id)}(using scala.xml.TopScope) }
Expr.betaReduce(call).asExprOf[String]
}
'{ new _root_.scala.xml.NamespaceBinding($prefix, $uri, $rest) }
})
}
'{ scala.xml.NamespaceBinding($prefix, $uri, $rest) }
end expandNamespaces

private def expandText(text: Text)(using Quotes): Expr[scala.xml.Text] =
'{ new _root_.scala.xml.Text(${Expr(text.text)}) }
'{ scala.xml.Text(${ Expr(text.text) }) }

private def expandComment(comment: Comment)(using Quotes): Expr[scala.xml.Comment] =
'{ new _root_.scala.xml.Comment(${Expr(comment.text)}) }
'{ scala.xml.Comment(${ Expr(comment.text) }) }

private def expandPlaceholder(placeholder: Placeholder)(implicit ctx: XmlContext, q: Quotes): Expr[Any] = {
private def expandPlaceholder(placeholder: Placeholder)(using XmlContext, Quotes): Expr[Any] =
val arg = ctx.args(placeholder.id)
val scope = ctx.scope
Expr.betaReduce('{ $arg(using $scope) })
}
Expr.betaReduce('{ $arg(using ${ ctx.scope }) })

private def expandPCData(pcdata: PCData)(using Quotes): Expr[scala.xml.PCData] =
'{ new _root_.scala.xml.PCData(${Expr(pcdata.data)}) }
'{ scala.xml.PCData(${ Expr(pcdata.data) }) }

private def expandProcInstr(instr: ProcInstr)(using Quotes): Expr[scala.xml.ProcInstr] =
'{ new _root_.scala.xml.ProcInstr(${Expr(instr.target)}, ${Expr(instr.proctext)}) }
'{ scala.xml.ProcInstr(${ Expr(instr.target) }, ${ Expr(instr.proctext) }) }

private def expandEntityRef(ref: EntityRef)(using Quotes): Expr[scala.xml.EntityRef] =
'{ new _root_.scala.xml.EntityRef(${Expr(ref.name)}) }
'{ scala.xml.EntityRef(${ Expr(ref.name) }) }

private def expandUnparsed(unparsed: Unparsed)(using Quotes): Expr[scala.xml.Unparsed] =
'{ new _root_.scala.xml.Unparsed(${Expr(unparsed.data)}) }
}
'{ scala.xml.Unparsed(${ Expr(unparsed.data) }) }

end Expand
5 changes: 2 additions & 3 deletions src/main/scala/dotty/xml/interpolator/internal/Hole.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
package dotty.xml.interpolator
package internal

object Hole {
object Hole:
val HoleStart = 0xE000.toChar.toString
val HoleChar = 0xE001.toChar.toString
def encode(i: Int) = HoleStart + HoleChar * i
}
def encode(i: Int) = HoleStart + HoleChar * i
96 changes: 48 additions & 48 deletions src/main/scala/dotty/xml/interpolator/internal/Macro.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,43 @@ import scala.quoted._
import scala.collection.mutable.ArrayBuffer
import scala.language.implicitConversions

object Macro {
object Macro:

def impl(strCtxExpr: Expr[StringContext], argsExpr: Expr[Seq[Scope ?=> Any]], scope: Expr[Scope])(using qctx: Quotes): Expr[scala.xml.Node | scala.xml.NodeBuffer] = {
((strCtxExpr, argsExpr): @unchecked) match {
/** ??? */
def impl(strCtxExpr: Expr[StringContext], argsExpr: Expr[Seq[Scope ?=> Any]], scope: Expr[Scope])
(using Quotes): Expr[scala.xml.Node | scala.xml.NodeBuffer] =

(strCtxExpr, argsExpr) match
case ('{ StringContext(${Varargs(parts)}: _*) }, Varargs(args)) =>

val (xmlStr, offsets) = encode(parts)
implicit val ctx: XmlContext = new XmlContext(args, scope)
implicit val reporter: Reporter = new Reporter {
import quotes.reflect._

def error(msg: String, idx: Int): Unit = {
val (part, offset) = Reporter.from(idx, offsets, parts)
val pos = part.asTerm.pos
val (srcF, start) = (pos.sourceFile, pos.start)
report.error(msg, Position(srcF, start + offset, start + offset + 1))
}
given XmlContext = new XmlContext(args, scope)
given Reporter = new Reporter {
import quotes.reflect.*

def error(msg: String, idx: Int): Unit = {
val (part, offset) = Reporter.from(idx, offsets, parts)
val pos = part.asTerm.pos
val (srcF, start) = (pos.sourceFile, pos.start)
report.error(msg, Position(srcF, start + offset, start + offset + 1))
}

def error(msg: String, expr: Expr[Any]): Unit =
report.error(msg, expr)
}

def error(msg: String, expr: Expr[Any]): Unit = {
report.error(msg, expr)
}
}
implCore(xmlStr)
}
}
end impl

def implErrors(strCtxExpr: Expr[StringContext], argsExpr: Expr[Seq[Scope ?=> Any]], scope: Expr[Scope])(using qctx: Quotes): Expr[List[(Int, String)]] = {
((strCtxExpr, argsExpr): @unchecked) match {
def implErrors(strCtxExpr: Expr[StringContext], argsExpr: Expr[Seq[Scope ?=> Any]], scope: Expr[Scope])
(using Quotes): Expr[List[(Int, String)]] =
(strCtxExpr, argsExpr) match
case ('{ StringContext(${Varargs(parts)}: _*) }, Varargs(args)) =>
val errors = List.newBuilder[Expr[(Int, String)]]
val (xmlStr, offsets) = encode(parts)
implicit val ctx: XmlContext = new XmlContext(args, scope)
implicit val reporter: Reporter = new Reporter {
given XmlContext = new XmlContext(args, scope)
given Reporter = new Reporter {
import quotes.reflect._

def error(msg: String, idx: Int): Unit = {
Expand All @@ -53,48 +58,43 @@ object Macro {
}
implCore(xmlStr)
Expr.ofList(errors.result())
}
}
end implErrors

private def implCore(xmlStr: String)(using XmlContext, Reporter, Quotes): Expr[scala.xml.Node | scala.xml.NodeBuffer] = {
private def implCore(xmlStr: String)(using XmlContext, Reporter, Quotes): Expr[scala.xml.Node | scala.xml.NodeBuffer] =

import Parse.{apply => parse}
import Transform.{apply => transform}
import Validate.{apply => validate}
import TypeCheck.{apply => typecheck}
import Expand.{apply => expand}
import Parse.apply as parse
import Transform.apply as transform
import Validate.apply as validate
import TypeCheck.apply as typecheck
import Expand.apply as expand

val interpolate = (
parse
andThen transform
andThen validate
andThen typecheck
andThen expand
)
val interpolate =
parse andThen
transform andThen
validate andThen
typecheck andThen
expand

interpolate(xmlStr)
}
end implCore

private def encode(parts: Seq[Expr[String]])(using Quotes): (String, Array[Int]) = {
private def encode(parts: Seq[Expr[String]])(using Quotes): (String, Array[Int]) =
val sb = new StringBuilder()
val bf = ArrayBuffer.empty[Int]

def appendPart(part: Expr[String]) = {
def appendPart(part: Expr[String]) =
bf += sb.length
sb ++= part.valueOrAbort
bf += sb.length
}

def appendHole(index: Int) = {
sb ++= Hole.encode(index)
}
def appendHole(index: Int) = sb ++= Hole.encode(index)

for ((part, index) <- parts.init.zipWithIndex) {
for (part, index) <- parts.init.zipWithIndex do
appendPart(part)
appendHole(index)
}
appendPart(parts.last)

(sb.toString, bf.toArray)
}
}
end encode

end Macro
10 changes: 4 additions & 6 deletions src/main/scala/dotty/xml/interpolator/internal/Reporter.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package dotty.xml.interpolator
package internal

import scala.quoted._
import scala.quoted.*

trait Reporter {
trait Reporter:
def error(msg: String, idx: Int): Unit
def error(msg: String, expr: Expr[Any]): Unit
}

object Reporter {
object Reporter:
def from(idx: Int, offsets: Array[Int], parts: Seq[Expr[String]]): (Expr[String], Int) = {
val index = offsets.lastIndexWhere(idx >= _)
val isWithinHoleOrAtTheEnd = index % 2 != 0
Expand All @@ -17,5 +16,4 @@ object Reporter {
case false => (index / 2, idx - offsets(index))
}
(parts(partIndex), offset)
}
}
}
17 changes: 9 additions & 8 deletions src/main/scala/dotty/xml/interpolator/internal/Transform.scala
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package dotty.xml.interpolator
package internal

import dotty.xml.interpolator.internal.Tree._
import Tree.*

object Transform {
def apply(nodes: Seq[Node]): Seq[Node] = {
nodes.map {
object Transform:

def apply(nodes: Seq[Node]): Seq[Node] =
nodes.map:
case elem : Elem =>
val children = apply(elem.children)
if (elem.name == "xml:group" && !elem.end.isEmpty) Group(elem.children).setPos(elem.pos)
if elem.name == "xml:group" && !elem.end.isEmpty then Group(elem.children).setPos(elem.pos)
else elem.copy(children = children).setPos(elem.pos)
case node => node
}
}
}
end apply

end Transform
Loading

0 comments on commit 71c6c4b

Please sign in to comment.