Skip to content

Commit 793e8eb

Browse files
committed
make flatMap consume whitespace, introduce flatMapX that doesn't
1 parent 19e0fd0 commit 793e8eb

File tree

7 files changed

+68
-20
lines changed

7 files changed

+68
-20
lines changed

fastparse/src/fastparse/internal/MacroImpls.scala

+25-1
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ object MacroImpls {
163163
}
164164

165165

166-
def flatMapMacro[T: c.WeakTypeTag, V: c.WeakTypeTag]
166+
def flatMapXMacro[T: c.WeakTypeTag, V: c.WeakTypeTag]
167167
(c: Context)
168168
(f: c.Expr[T => ParsingRun[V]]): c.Expr[ParsingRun[V]] = {
169169
import c.universe._
@@ -176,6 +176,30 @@ object MacroImpls {
176176
}
177177
}
178178

179+
def flatMapMacro[T: c.WeakTypeTag, V: c.WeakTypeTag]
180+
(c: Context)
181+
(f: c.Expr[T => ParsingRun[V]])
182+
(whitespace: c.Expr[ParsingRun[Any] => ParsingRun[Unit]]): c.Expr[ParsingRun[V]] = {
183+
import c.universe._
184+
185+
val lhs0 = c.prefix.asInstanceOf[c.Expr[EagerOps[T]]]
186+
reify {
187+
val lhs = lhs0.splice.parse0
188+
whitespace.splice match{ case ws =>
189+
if (!lhs.isSuccess) lhs.asInstanceOf[ParsingRun[V]]
190+
else {
191+
val oldCapturing = lhs.noDropBuffer
192+
val successValue = lhs.successValue
193+
lhs.noDropBuffer = true
194+
ws(lhs)
195+
lhs.noDropBuffer = oldCapturing
196+
if (!lhs.isSuccess && lhs.cut) lhs.asInstanceOf[ParsingRun[V]]
197+
else f.splice(successValue.asInstanceOf[T])
198+
}
199+
}
200+
}
201+
}
202+
179203
def eitherMacro[T: c.WeakTypeTag, V >: T: c.WeakTypeTag]
180204
(c: Context)
181205
(other: c.Expr[ParsingRun[V]])

fastparse/src/fastparse/package.scala

+17-8
Original file line numberDiff line numberDiff line change
@@ -177,12 +177,21 @@ package object fastparse {
177177
(implicit ctx: P[Any]): P[T] = macro MacroImpls.filterMacro[T]
178178
/**
179179
* Transforms the result of this parser using the given function into a
180-
* new parser which is applied. Useful for doing dependent parsing, e.g.
181-
* when parsing JSON you may first parse a character to see if it's a `[`,
182-
* `{`, or `"`, and then deciding whether you next want to parse an array,
183-
* dictionary or string.
180+
* new parser which is applied (after whitespace). Useful for doing
181+
* dependent parsing, e.g. when parsing JSON you may first parse a
182+
* character to see if it's a `[`, `{`, or `"`, and then deciding whether
183+
* you next want to parse an array, dictionary or string.
184184
*/
185-
def flatMap[V](f: T => P[V]): P[V] = macro MacroImpls.flatMapMacro[T, V]
185+
def flatMap[V](f: T => P[V])
186+
(implicit whitespace: P[Any] => P[Unit]): P[V] = macro MacroImpls.flatMapMacro[T, V]
187+
/**
188+
* Transforms the result of this parser using the given function into a
189+
* new parser which is applied (without consuming whitespace). Useful for
190+
* doing dependent parsing, e.g. when parsing JSON you may first parse a
191+
* character to see if it's a `[`, `{`, or `"`, and then deciding whether
192+
* you next want to parse an array, dictionary or string.
193+
*/
194+
def flatMapX[V](f: T => P[V]): P[V] = macro MacroImpls.flatMapXMacro[T, V]
186195

187196
/**
188197
* Either-or operator: tries to parse the left-hand-side, and if that
@@ -195,7 +204,7 @@ package object fastparse {
195204
/**
196205
* Capture operator; makes the parser return the span of input it parsed
197206
* as a [[String]], which can then be processed further using [[~]],
198-
* [[map]] or [[flatMap]]
207+
* [[map]] or [[flatMapX]]
199208
*/
200209
def !(implicit ctx: P[Any]): P[String] = macro MacroImpls.captureMacro
201210

@@ -590,9 +599,9 @@ package object fastparse {
590599

591600
/**
592601
* Like [[AnyChar]], but returns the single character it parses. Useful
593-
* together with [[EagerOps.flatMap]] to provide one-character-lookahead
602+
* together with [[EagerOps.flatMapX]] to provide one-character-lookahead
594603
* style parsing: [[SingleChar]] consumes the single character, and then
595-
* [[EagerOps.flatMap]] can `match` on that single character and decide
604+
* [[EagerOps.flatMapX]] can `match` on that single character and decide
596605
* which downstream parser you wish to invoke
597606
*/
598607
def SingleChar(implicit ctx: P[_]): P[Char] = {

fastparse/test/src/fastparse/IndentationTests.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ object IndentationTests extends TestSuite{
2323
def number[_: P]: P[Int] = P( CharIn("0-9").rep(1).!.map(_.toInt) )
2424

2525
def deeper[_: P]: P[Int] = P( " ".rep(indent + 1).!.map(_.length) )
26-
def blockBody[_: P]: P[Seq[Int]] = "\n" ~ deeper.flatMap(i =>
26+
def blockBody[_: P]: P[Seq[Int]] = "\n" ~ deeper.flatMapX(i =>
2727
new Parser(indent = i).factor.rep(1, sep = ("\n" + " " * i)./)
2828
)
2929
def block[_: P]: P[Int] = P( CharIn("+\\-*/").! ~/ blockBody).map(eval)

fastparse/test/src/fastparse/ParsingTests.scala

+15-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package test.fastparse
22
import fastparse._
33
import utest._
4-
import NoWhitespace._
4+
55
object ParsingTests extends TestSuite{
66

77

@@ -33,7 +33,7 @@ object ParsingTests extends TestSuite{
3333

3434
}
3535
val tests = Tests {
36-
36+
import NoWhitespace._
3737

3838
'literal - {
3939
checkFail(implicit c => "Hello WOrld!", ("Hello", 0), 0)
@@ -185,7 +185,8 @@ object ParsingTests extends TestSuite{
185185
checkFail(implicit c => ("Hello" ~/ "Bye").?, ("HelloBoo", 0), 5)
186186
}
187187
'flatMap - {
188-
checkFlatmap()
188+
checkFail(implicit c => ("Hello" ~/ "Boo").flatMapX(_ => Fail).?, ("HelloBoo", 0), 8)
189+
checkFail(implicit c => (("Hello" ~/ "Boo").flatMapX(_ => Pass) ~ Fail).?, ("HelloBoo", 0), 8)
189190
}
190191
'filter - {
191192
checkFail(implicit c => ("Hello" ~/ "Boo").filter(_ => false) | "", ("HelloBoo", 0), 8)
@@ -212,11 +213,17 @@ object ParsingTests extends TestSuite{
212213
val msg = f.trace().msg
213214
msg ==> """Expected "hello" | "world":1:1, found "cow" """.trim
214215
}
216+
'whitespaceFlatMap{
217+
checkWhitespaceFlatMap()
218+
}
215219
}
216-
// Broken out of the TestSuite block to avoid problems in our 2.10.x
217-
// build due to https://issues.scala-lang.org/browse/SI-7987
218-
def checkFlatmap() = {
219-
checkFail(implicit c => ("Hello" ~/ "Boo").flatMap(_ => Fail).?, ("HelloBoo", 0), 8)
220-
checkFail(implicit c => (("Hello" ~/ "Boo").flatMap(_ => Pass) ~ Fail).?, ("HelloBoo", 0), 8)
220+
221+
def checkWhitespaceFlatMap() = {
222+
import fastparse._, SingleLineWhitespace._
223+
def parser[_: P] = P( CharsWhileIn("a").!.flatMap{n => "b" * n.length} ~ End )
224+
val Parsed.Success(_, _) = parse("aaa bbb", parser(_))
225+
val Parsed.Success(_, _) = parse("aa bb", parser(_))
226+
val Parsed.Failure(_, _, _) = parse("aaa bb", parser(_))
227+
val Parsed.Failure(_, _, _) = parse("aaa b", parser(_))
221228
}
222229
}

pythonparse/src/pythonparse/Statements.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ class Statements(indent: Int){
189189
_.collectFirst{ case (s, None) => s}
190190
}.filter(_.isDefined).map(_.get)
191191
}
192-
def indented = P( deeper.flatMap{ nextIndent =>
192+
def indented = P( deeper.flatMapX{ nextIndent =>
193193
new Statements(nextIndent).stmt.repX(1, spaces.repX(1) ~~ (" " * nextIndent | "\t" * nextIndent)).map(_.flatten)
194194
} )
195195
P( indented | " ".rep ~ simple_stmt )

readme/Changelog.scalatex

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
user-facing interface; binary and source incompatible with
88
@sect.ref{1.0.0}
99
@li
10-
3-5x performance improvements on most benchmarks
10+
3-4x performance improvements on most benchmarks
1111
@li
1212
Parsers are no longer immutable objects, but just methods taking
1313
and returning @code{fastparse.ParsingRun} instances
@@ -25,6 +25,9 @@
2525
@li
2626
Dropped support for Byte Parsers (due to low uptake), Scala 2.10,
2727
Scala-Native (until 0.4.0 is out)
28+
@li
29+
@code{.flatMap} now consumes whitespace between the first and
30+
second parsers; use @code{.flatMapX} if you want to avoid this.
2831
@sect{1.0.0}
2932
@ul
3033
@li

readme/WritingParsers.scalatex

+5
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,11 @@
196196
@p
197197
Which is equivalent and behaves exactly the same.
198198

199+
@p
200+
Note that @code{.flatMap} consumes whitespace between the first
201+
and second parsers; in cases where you do not want to do this,
202+
use @code{.flatMapX}
203+
199204
@sect{Filter}
200205
@hl.ref(tests/"ExampleTests.scala", Seq("'filter", ""))
201206

0 commit comments

Comments
 (0)