Skip to content

Commit b22f9c5

Browse files
committed
Resolve #171.
Don't compare source names when deciding "longer match" in mergeError. #175 would be a better fix, but that would require a major bump.
1 parent 088590b commit b22f9c5

File tree

6 files changed

+79
-1
lines changed

6 files changed

+79
-1
lines changed

ChangeLog.md

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
Drop `Stream` constraint requirement.
55
- Implement `Alternative.many/some` using `Text.Parsec.Prim.many/many1`,
66
instead of default implementation.
7+
- Change the position comparison in `mergeError` to not compare source names.
8+
This doesn't alter reported error positions when only a single source is parsed.
9+
This fixes performance issue caused by long source names.
710

811
### 3.1.16.0
912

parsec.cabal

+14
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,17 @@ test-suite parsec-issue127
141141
main-is: issue127.hs
142142
hs-source-dirs: test
143143
build-depends: base, parsec
144+
145+
test-suite parsec-issue171
146+
default-language: Haskell2010
147+
type: exitcode-stdio-1.0
148+
main-is: issue171.hs
149+
hs-source-dirs: test
150+
build-depends: base, tasty, tasty-hunit, deepseq, parsec
151+
152+
test-suite parsec-issue175
153+
default-language: Haskell2010
154+
type: exitcode-stdio-1.0
155+
main-is: issue175.hs
156+
hs-source-dirs: test
157+
build-depends: base, tasty, tasty-hunit, parsec

src/Text/Parsec/Error.hs

+6-1
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,17 @@ mergeError e1@(ParseError pos1 msgs1) e2@(ParseError pos2 msgs2)
145145
| null msgs2 && not (null msgs1) = e1
146146
| null msgs1 && not (null msgs2) = e2
147147
| otherwise
148-
= case pos1 `compare` pos2 of
148+
-- perfectly we'd compare the consumed token count
149+
-- https://github.com/haskell/parsec/issues/175
150+
= case compareErrorPos pos1 pos2 of
149151
-- select the longest match
150152
EQ -> ParseError pos1 (msgs1 ++ msgs2)
151153
GT -> e1
152154
LT -> e2
153155

156+
compareErrorPos :: SourcePos -> SourcePos -> Ordering
157+
compareErrorPos x y = mappend (compare (sourceLine x) (sourceLine y)) (compare (sourceColumn x) (sourceColumn y))
158+
154159
instance Show ParseError where
155160
show err
156161
= show (errorPos err) ++ ":" ++

test/issue127.hs

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
-- this should run in constant memory
12
module Main (main) where
23

34
import Text.Parsec

test/issue171.hs

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
-- this should be fast
2+
module Main (main) where
3+
4+
import Control.DeepSeq (NFData (..))
5+
import System.CPUTime (getCPUTime)
6+
import Text.Printf (printf)
7+
import Test.Tasty (defaultMain)
8+
import Test.Tasty.HUnit (testCaseSteps, assertBool)
9+
10+
import Text.Parsec
11+
import Text.Parsec.String (Parser)
12+
13+
main :: IO ()
14+
main = defaultMain $ testCaseSteps "issue-171" $ \info -> do
15+
time0 <- getCPUTime
16+
check $ concat $ replicate 100000 "a "
17+
time1 <- getCPUTime
18+
let diff = (time1 - time0) `div` 1000000000
19+
info $ printf "%d milliseconds\n" diff
20+
assertBool "" (diff < 100)
21+
22+
parser :: Parser [String]
23+
parser = many (char 'a' <|> char 'b') `sepBy` char ' '
24+
25+
check :: String -> IO ()
26+
check s = putStrLn $ either onError (const "") $ parse parser {- important: pass input as SourceName -} s s
27+
28+
onError :: ParseError -> String
29+
onError err = rnf (show err) `seq` "error"

test/issue175.hs

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module Main (main) where
2+
3+
import Text.Parsec
4+
import Text.Parsec.Error
5+
import Text.Parsec.String (Parser)
6+
import Text.Parsec.Pos (newPos)
7+
8+
import Test.Tasty (defaultMain)
9+
import Test.Tasty.HUnit (assertFailure, testCaseSteps, (@?=))
10+
11+
main :: IO ()
12+
main = defaultMain $ testCaseSteps "issue175" $ \info -> do
13+
case parse p "" "x" of
14+
Right _ -> assertFailure "Unexpected success"
15+
-- with setPosition the "longest match" is arbitrary
16+
-- megaparsec tracks consumed tokens separately, but we don't.
17+
-- so our position is arbitrary.
18+
Left err -> do
19+
info $ show err
20+
errorPos err @?= newPos "aaa" 9 1 -- can be arbitrary
21+
length (errorMessages err) @?= 2
22+
23+
p :: Parser Char
24+
p = p1 <|> p2 where
25+
p1 = setPosition (newPos "aaa" 9 1) >> char 'a'
26+
p2 = setPosition (newPos "zzz" 1 1) >> char 'b'

0 commit comments

Comments
 (0)