Skip to content

Commit

Permalink
Merge pull request #18 from kasiaMarek/singletons
Browse files Browse the repository at this point in the history
Singletons
  • Loading branch information
susuro authored Jan 11, 2024
2 parents c5cd614 + 313f045 commit 4ea3c47
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 11 deletions.
3 changes: 3 additions & 0 deletions src/main/scala/net/marek/tyre/Tyre.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ sealed trait Tyre[+R]:
def <*>[S](re: Tyre[S]): Tyre[(R, S)] = And(this, re)
def <|>[S](re: Tyre[S]): Tyre[Either[R, S]] = Or(this, re)
def map[S](f: R => S): Tyre[S] = Conv(this, f)
def cast[S]: Tyre[S] = map(_.asInstanceOf[S])

private case class Pred(f: Char => Boolean) extends Tyre[Char]

Expand All @@ -30,8 +31,10 @@ object Tyre:
def epsilon: Tyre[Unit] = Epsilon

object Pred:
def single(s: Char & Singleton): Tyre[s.type] = char(s).map(_.asInstanceOf[s.type])
def pred(f: Char => Boolean): Tyre[Char] = Pred(f)
def any: Tyre[Char] = Pred(_ => true)
def empty: Tyre[Nothing] = Pred(_ => false).map(_ => throw new RuntimeException("impossible"))
def in(cs: List[Range]): Tyre[Char] = Pred(c => cs.exists(r => r.from <= c && r.to >= c))
def notIn(cs: List[Range]): Tyre[Char] = Pred(c => cs.forall(r => r.from > c || r.to < c))
def char(c: Char): Tyre[Char] = Pred(_ == c)
Expand Down
8 changes: 7 additions & 1 deletion src/main/scala/net/marek/tyre/pattern/Re.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ sealed private trait Re
private case object ReAny extends Re:
override def toString(): String = "."

private case class ReSingle(s: Char & Singleton) extends Re:
override def toString(): String = s"$s"

private case class ReIn(cs: List[Range]) extends Re:
override def toString(): String = cs
.map:
Expand Down Expand Up @@ -49,8 +52,11 @@ private case object ReEpsilon extends Re:
private case class ReHole(index: Int) extends Re:
override def toString(): String = s"@$index"

private case class ReLiteralConv(re: ReIn) extends Re:
override def toString(): String = s"$re!l"

private object Re:
def char(c: Char): ReIn = ReIn(List(Range(c, c)))
def char(c: Char & Singleton): ReSingle = ReSingle(c)

private enum CastOp(val symbol: Char):
case Stringify extends CastOp('s')
22 changes: 22 additions & 0 deletions src/main/scala/net/marek/tyre/pattern/StringInterpolator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ private def tyreImpl(sc: Expr[StringContext], args: Expr[Seq[Any]])(using Quotes
def toTyre(re: Re)(using Quotes): Expr[Tyre[?]] = re match
case ReEpsilon => '{ Epsilon }
case ReAny => '{ Pred.any }
case ReSingle(s) => '{ Pred.single(${ Expr(s) }) }
case ReIn(cs) => '{ Pred.in(${ Expr(cs) }) }
case ReNotIn(cs) => '{ Pred.notIn(${ Expr(cs) }) }
case ReAnd(re1, re2) =>
Expand Down Expand Up @@ -74,6 +75,23 @@ private def tyreImpl(sc: Expr[StringContext], args: Expr[Seq[Any]])(using Quotes
case ReOpt(re) =>
toTyre(re) match
case '{ $ree: Tyre[t1] } => '{ Opt(${ ree }) }
case ReLiteralConv(ReIn(cs)) =>
assert(cs.map(_.size).sum <= 16, "Character class to large to create union type")
val allChars = cs.flatMap(_.getChars)

def loop(cs: List[Char]): Expr[?] =
cs match
case Nil => '{ ??? }
case c :: Nil => '{ Union.single(${ Expr(c) }) }
case c :: tail =>
loop(tail) match
case '{ $acce: t } =>
'{ Union[t](${ Expr(c) }) }

loop(allChars) match
case '{ $acce: t } =>
'{ Pred.in(${ Expr(cs) }).cast[t] }

case ReCast(re, cast) =>
toTyre(re) match
case '{ $ree: Tyre[t1] } => '{ Cast(${ ree }, ${ Expr(cast) }) }
Expand All @@ -85,3 +103,7 @@ private def tyreImpl(sc: Expr[StringContext], args: Expr[Seq[Any]])(using Quotes
private given ToExpr[CastOp] with
def apply(c: CastOp)(using Quotes) = c match
case CastOp.Stringify => '{ CastOp.Stringify }

private object Union:
def single(c: Char & Singleton): c.type = c
def apply[R2](c: Char & Singleton): c.type | R2 = c
23 changes: 14 additions & 9 deletions src/main/scala/net/marek/tyre/pattern/StringParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,25 @@ private object TyreParser extends Parsers:
escape ~> accept("predef class", { case el: Char if CharClass.hasNeg(el) => CharClass.negs(el) })
private val inBracketSpecial =
accept("not escaped special", { case el: Char if !Reserved.isEscapedInBrackets(el) => el })
private val literalCastOp = accept("literal cast operator", { case 'l' => 'l' })

private val literalInBracket = literal | inBracketSpecial

private val rangeOrLiteralInBrackets =
literal ~ opt(dash ~> literal) ^^ {
literalInBracket ~ opt(dash ~> literalInBracket) ^^ {
case li1 ~ None => List(Range(li1, li1))
case start ~ Some(end) => List(Range(start, end))
} | inBracketSpecial ^^ { case li => List(Range(li, li)) }
| charClassIn | charClassNotIn
}

private val rangeOrLiteralOrCharClassInBrackets =
rangeOrLiteralInBrackets | charClassIn | charClassNotIn

private val any =
hole ^^ ReHole.apply
| lBracket ~> opt(caret) ~ rep1(rangeOrLiteralInBrackets) <~ rBracket ^^ {
| lBracket ~> rep1(rangeOrLiteralInBrackets) <~ rBracket ~ exclamation ~ literalCastOp ^^ { case list =>
ReLiteralConv(ReIn(list.flatten))
}
| lBracket ~> opt(caret) ~ rep1(rangeOrLiteralOrCharClassInBrackets) <~ rBracket ^^ {
case Some(_) ~ list => ReNotIn(list.flatten)
case None ~ list => ReIn(list.flatten)
}
Expand All @@ -102,9 +110,6 @@ private object TyreParser extends Parsers:
private val repetition: Parser[Rep] =
star ^^ { _ => Rep.Star } | plus ^^ { _ => Rep.Plus } | questionMark ^^ { _ => Rep.QuestionMark }

private val conversion: Parser[CastOp] =
exclamation ~> CastOp.Stringify

private val expr0: Parser[Re] =
consumingExpr ~ opt(repetition) ^^ {
case r ~ Some(Rep.Star) => ReStar(r)
Expand All @@ -114,8 +119,8 @@ private object TyreParser extends Parsers:
}

private val expr1: Parser[Re] =
expr0 ~ opt(conversion) ^^ {
case r ~ Some(CastOp.Stringify) => ReCast(r, CastOp.Stringify)
expr0 ~ opt(exclamation ~> CastOp.Stringify) ^^ {
case r ~ Some(cast) => ReCast(r, cast)
case r ~ None => r
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/net/marek/tyre/pattern/TyreOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ private object Opt:
Conv(Or(re, Epsilon), conv)

private object OrM:
def merge[R1, R2](e: Either[R1, R2]): R1 | R2 = e match
private def merge[R1, R2](e: Either[R1, R2]): R1 | R2 = e match
case Left(v) => v
case Right(v) => v
def apply[R1, R2](left: Tyre[R1], right: Tyre[R2]): Tyre[R1 | R2] =
Expand Down
2 changes: 2 additions & 0 deletions src/main/scala/net/marek/tyre/utils/Range.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import scala.quoted.ToExpr

private[tyre] case class Range(from: Char, to: Char):
def contains(c: Char): Boolean = c >= from && c <= to
def getChars: List[Char] = (from to to).toList
def size = to - from + 1

private[tyre] object Range:
def apply(char: Char): Range = Range(char, char)
Expand Down
21 changes: 21 additions & 0 deletions src/test/scala/net/marek/tyre/ConversionTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package net.marek.tyre

import org.scalatest.funsuite.AnyFunSuite

import scala.annotation.unused

class ConversionTest extends AnyFunSuite:

test("Singleton types"):
@unused val _: Tyre[('a', List[Char])] = tyre"a.*"
@unused val _: Tyre[('a', List[Char])] = tyre"[a]!l.*"
@unused val _: Tyre[('a' | 'b', List[Char])] = tyre"[ab]!l.*"
@unused val _: Tyre[('a' | 'b' | 'c', List[Char])] = tyre"[a-c]!l.*"
@unused val _: Tyre[(List['a' | 'b' | 'c' | 'w'], List[Char])] = tyre"[a-cw]!l*.*"
assertDoesNotCompile("""tyre"[a-\u3333]!l.*"""") // to many literal types in union

test("Singleton regex"):
val t = tyre"[a-c]!l.*q"
val m = t.compile()
val res: Option[('a' | 'b' | 'c', List[Char], 'q')] = m.run("bxxq")
assertResult(Some(('b', List('x', 'x'), 'q')))(res)

0 comments on commit 4ea3c47

Please sign in to comment.