Skip to content

Commit 22b34f1

Browse files
authored
Add Parser.caret (#301)
* Add Parser.caret * actually add Caret * fix 2.11 compilation * respond to review
1 parent 55a146e commit 22b34f1

File tree

5 files changed

+150
-21
lines changed

5 files changed

+150
-21
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright (c) 2021 Typelevel
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy of
5+
* this software and associated documentation files (the "Software"), to deal in
6+
* the Software without restriction, including without limitation the rights to
7+
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8+
* the Software, and to permit persons to whom the Software is furnished to do so,
9+
* subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16+
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17+
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18+
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19+
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
*/
21+
22+
package cats.parse
23+
24+
import cats.Order
25+
26+
/** This is a pointer to a zero based row, column, and total offset.
27+
*/
28+
case class Caret(row: Int, col: Int, offset: Int)
29+
30+
object Caret {
31+
val Start: Caret = Caret(0, 0, 0)
32+
33+
implicit val caretOrder: Order[Caret] =
34+
new Order[Caret] {
35+
def compare(left: Caret, right: Caret): Int = {
36+
val c0 = Integer.compare(left.row, right.row)
37+
if (c0 != 0) c0
38+
else {
39+
val c1 = Integer.compare(left.col, right.col)
40+
if (c1 != 0) c1
41+
else Integer.compare(left.offset, right.offset)
42+
}
43+
}
44+
}
45+
46+
implicit val caretOrdering: Ordering[Caret] =
47+
caretOrder.toOrdering
48+
}

core/shared/src/main/scala/cats/parse/LocationMap.scala

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,28 +60,34 @@ class LocationMap(val input: String) {
6060
*/
6161
def lineCount: Int = lines.length
6262

63+
def isValidOffset(offset: Int): Boolean =
64+
(0 <= offset && offset <= input.length)
65+
6366
/** Given a string offset return the line and column If input.length is given (EOF) we return the
6467
* same value as if the string were one character longer (i.e. if we have appended a non-newline
6568
* character at the EOF)
6669
*/
6770
def toLineCol(offset: Int): Option[(Int, Int)] =
68-
if (offset < 0 || offset > input.length) None
69-
else if (offset == input.length) {
71+
if (isValidOffset(offset)) {
72+
val Caret(_, row, col) = toCaretUnsafeImpl(offset)
73+
Some((row, col))
74+
} else None
75+
76+
// This does not do bounds checking because we
77+
// don't want to check twice. Callers to this need to
78+
// do bounds check
79+
private def toCaretUnsafeImpl(offset: Int): Caret =
80+
if (offset == input.length) {
7081
// this is end of line
71-
if (offset == 0) Some((0, 0))
82+
if (offset == 0) Caret.Start
7283
else {
73-
toLineCol(offset - 1)
74-
.map { case (line, col) =>
75-
if (endsWithNewLine) (line + 1, 0)
76-
else (line, col + 1)
77-
}
84+
val Caret(_, line, col) = toCaretUnsafeImpl(offset - 1)
85+
if (endsWithNewLine) Caret(offset, line + 1, 0)
86+
else Caret(offset, line, col + 1)
7887
}
7988
} else {
8089
val idx = Arrays.binarySearch(firstPos, offset)
81-
if (idx == firstPos.length) {
82-
// greater than all elements
83-
None
84-
} else if (idx < 0) {
90+
if (idx < 0) {
8591
// idx = (~(insertion pos) - 1)
8692
// The insertion point is defined as the point at which the key would be
8793
// inserted into the array: the index of the first element greater than
@@ -92,13 +98,25 @@ class LocationMap(val input: String) {
9298
// so we are pointing into a row
9399
val rowStart = firstPos(row)
94100
val col = offset - rowStart
95-
Some((row, col))
101+
Caret(offset, row, col)
96102
} else {
97103
// idx is exactly the right value because offset is beginning of a line
98-
Some((idx, 0))
104+
Caret(offset, idx, 0)
99105
}
100106
}
101107

108+
/** Convert an offset to a Caret.
109+
* @throws IllegalArgumentException
110+
* if offset is longer than input
111+
*/
112+
def toCaretUnsafe(offset: Int): Caret =
113+
if (isValidOffset(offset)) toCaretUnsafeImpl(offset)
114+
else throw new IllegalArgumentException(s"offset = $offset exceeds ${input.length}")
115+
116+
def toCaret(offset: Int): Option[Caret] =
117+
if (isValidOffset(offset)) Some(toCaretUnsafeImpl(offset))
118+
else None
119+
102120
/** return the line without a newline
103121
*/
104122
def getLine(i: Int): Option[String] =

core/shared/src/main/scala/cats/parse/Parser.scala

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1629,7 +1629,7 @@ object Parser {
16291629
case str if Impl.matchesString(str) => str.asInstanceOf[Parser0[String]]
16301630
case _ =>
16311631
Impl.unmap0(pa) match {
1632-
case Impl.Pure(_) | Impl.Index => emptyStringParser0
1632+
case Impl.Pure(_) | Impl.Index | Impl.GetCaret => emptyStringParser0
16331633
case notEmpty => Impl.StringP0(notEmpty)
16341634
}
16351635
}
@@ -1683,6 +1683,11 @@ object Parser {
16831683
*/
16841684
def index: Parser0[Int] = Impl.Index
16851685

1686+
/** return the current Caret (offset, line, column) this is a bit more expensive that just the
1687+
* index
1688+
*/
1689+
def caret: Parser0[Caret] = Impl.GetCaret
1690+
16861691
/** succeeds when we are at the start
16871692
*/
16881693
def start: Parser0[Unit] = Impl.StartParser
@@ -1717,7 +1722,7 @@ object Parser {
17171722
case p1: Parser[_] => as(p1, b)
17181723
case _ =>
17191724
Impl.unmap0(pa) match {
1720-
case Impl.Pure(_) | Impl.Index => pure(b)
1725+
case Impl.Pure(_) | Impl.Index | Impl.GetCaret => pure(b)
17211726
case notPure =>
17221727
Impl.Void0(notPure).map(Impl.ConstFn(b))
17231728
}
@@ -1837,6 +1842,10 @@ object Parser {
18371842
var offset: Int = 0
18381843
var error: Eval[Chain[Expectation]] = null
18391844
var capture: Boolean = true
1845+
1846+
// This is lazy because we don't want to trigger it
1847+
// unless someone uses GetCaret
1848+
lazy val locationMap: LocationMap = LocationMap(str)
18401849
}
18411850

18421851
// invariant: input must be sorted
@@ -1885,8 +1894,9 @@ object Parser {
18851894
final def doesBacktrack(p: Parser0[Any]): Boolean =
18861895
p match {
18871896
case Backtrack0(_) | Backtrack(_) | AnyChar | CharIn(_, _, _) | Str(_) | IgnoreCase(_) |
1888-
Length(_) | StartParser | EndParser | Index | Pure(_) | Fail() | FailWith(_) | Not(_) |
1889-
StringIn(_) =>
1897+
Length(_) | StartParser | EndParser | Index | GetCaret | Pure(_) | Fail() | FailWith(
1898+
_
1899+
) | Not(_) | StringIn(_) =>
18901900
true
18911901
case Map0(p, _) => doesBacktrack(p)
18921902
case Map(p, _) => doesBacktrack(p)
@@ -1916,7 +1926,7 @@ object Parser {
19161926
// and by construction, a oneOf0 never always succeeds
19171927
final def alwaysSucceeds(p: Parser0[Any]): Boolean =
19181928
p match {
1919-
case Index | Pure(_) => true
1929+
case Index | GetCaret | Pure(_) => true
19201930
case Map0(p, _) => alwaysSucceeds(p)
19211931
case SoftProd0(a, b) => alwaysSucceeds(a) && alwaysSucceeds(b)
19221932
case Prod0(a, b) => alwaysSucceeds(a) && alwaysSucceeds(b)
@@ -1934,7 +1944,7 @@ object Parser {
19341944
def unmap0(pa: Parser0[Any]): Parser0[Any] =
19351945
pa match {
19361946
case p1: Parser[Any] => unmap(p1)
1937-
case Pure(_) | Index => Parser.unit
1947+
case GetCaret | Index | Pure(_) => Parser.unit
19381948
case s if alwaysSucceeds(s) => Parser.unit
19391949
case Map0(p, _) =>
19401950
// we discard any allocations done by fn
@@ -2172,6 +2182,12 @@ object Parser {
21722182
override def parseMut(state: State): Int = state.offset
21732183
}
21742184

2185+
case object GetCaret extends Parser0[Caret] {
2186+
override def parseMut(state: State): Caret =
2187+
// This unsafe call is safe because the offset can never go too far
2188+
state.locationMap.toCaretUnsafe(state.offset)
2189+
}
2190+
21752191
final def backtrack[A](pa: Parser0[A], state: State): A = {
21762192
val offset = state.offset
21772193
val a = pa.parseMut(state)

core/shared/src/test/scala/cats/parse/LocationMapTest.scala

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,4 +216,36 @@ class LocationMapTest extends munit.ScalaCheckSuite {
216216
assert(s.endsWith(lm.getLine(lm.lineCount - 1).get))
217217
}
218218
}
219+
220+
property("toLineCol and toCaret are consistent") {
221+
forAll { (s: String, other: Int) =>
222+
val lm = LocationMap(s)
223+
(0 to s.length).foreach { offset =>
224+
val c = lm.toCaretUnsafe(offset)
225+
val oc = lm.toCaret(offset)
226+
val lc = lm.toLineCol(offset)
227+
228+
assertEquals(oc, Some(c))
229+
assertEquals(lc, oc.map { case Caret(_, r, c) => (r, c) })
230+
}
231+
232+
if (other < 0 || s.length < other) {
233+
assert(scala.util.Try(lm.toCaretUnsafe(other)).isFailure)
234+
assertEquals(lm.toCaret(other), None)
235+
assertEquals(lm.toLineCol(other), None)
236+
}
237+
}
238+
}
239+
240+
property("Caret ordering matches offset ordering") {
241+
forAll { (s: String, o1: Int, o2: Int) =>
242+
val lm = LocationMap(s)
243+
val c1 = lm.toCaret(o1)
244+
val c2 = lm.toCaret(o2)
245+
246+
if (c1.isDefined && c2.isDefined) {
247+
assertEquals(Ordering[Option[Caret]].compare(c1, c2), Integer.compare(o1, o2))
248+
}
249+
}
250+
}
219251
}

core/shared/src/test/scala/cats/parse/ParserTest.scala

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ object ParserGen {
7070
def map[A, B](ga: Gen[A])(fn: A => B) = ga.map(fn)
7171
}
7272

73+
implicit val cogenCaret: Cogen[Caret] =
74+
Cogen { caret: Caret =>
75+
(caret.offset.toLong << 32) | (caret.col.toLong << 16) | (caret.row.toLong)
76+
}
77+
7378
def arbGen[A: Arbitrary: Cogen]: GenT[Gen] =
7479
GenT(Arbitrary.arbitrary[A])
7580

@@ -516,7 +521,7 @@ object ParserGen {
516521
(5, expect0),
517522
(1, ignoreCase0),
518523
(5, charIn0),
519-
(1, Gen.oneOf(GenT(Parser.start), GenT(Parser.end), GenT(Parser.index))),
524+
(1, Gen.oneOf(GenT(Parser.start), GenT(Parser.end), GenT(Parser.index), GenT(Parser.caret))),
520525
(1, fail),
521526
(1, failWith),
522527
(1, rec.map(void0(_))),
@@ -2479,4 +2484,14 @@ class ParserTest extends munit.ScalaCheckSuite {
24792484
assertEquals(v1.void, v1)
24802485
}
24812486
}
2487+
2488+
property("P.caret is the same as index + toCaretUnsafe") {
2489+
forAll(ParserGen.gen, Arbitrary.arbitrary[String]) { (p, input) =>
2490+
val v1 = p.fa.void
2491+
val lm = LocationMap(input)
2492+
val left = (v1 *> Parser.index).map(lm.toCaretUnsafe(_)).parse(input)
2493+
val right = (v1 *> Parser.caret).parse(input)
2494+
assertEquals(left, right)
2495+
}
2496+
}
24822497
}

0 commit comments

Comments
 (0)