on master help sign in
Add a faster, custom impl of double parser
commitb085507aec0b037fa434ece0cee6d3feddda789b authorHarendra Kumar <harendra@composewell.com> dateWed 26 Jul 2023, 23:12 (3 years ago) parent2e489ea
3 files changed+308−101
benchmark/Streamly/Benchmark/Unicode/Parser.hs+36−19 core/src/Streamly/Internal/Unicode/Parser.hs+148−5 test/Streamly/Test/Unicode/Parser.hs+124−77
benchmark/Streamly/Benchmark/Unicode/Parser.hs+36−19
@@ -10,6 +10,11 @@
1010{-# LANGUAGE ScopedTypeVariables #-}
1111{-# OPTIONS_GHC -Wno-orphans #-}
1212
13+#undef FUSION_CHECK
14+#ifdef FUSION_CHECK
15+{-# OPTIONS_GHC -ddump-simpl -ddump-to-file -dsuppress-all #-}
16+#endif
17+
1318module Main
1419 (
1520 main
@@ -22,28 +27,18 @@ import Streamly.Internal.Data.Stream (Stream)
2227import Prelude hiding
2328 (any, all, take, sequence, sequence_, sequenceA, takeWhile, dropWhile)
2429
25−import qualified Streamly.Internal.Data.Fold as Fold
30+import qualified Streamly.Data.Array as Array
2631import qualified Streamly.Data.Stream as Stream
27−import qualified Streamly.Data.Unfold as Unfold
2832import qualified Streamly.Internal.Unicode.Parser as PRU
2933
3034import Gauge hiding (env)
3135import Streamly.Benchmark.Common
32−import Streamly.Benchmark.Common.Handle
33−
34−{-# INLINE sourceUnfoldrM #-}
35−sourceUnfoldrM :: Monad m => Int -> Int -> Stream m Int
36−sourceUnfoldrM value n = Stream.unfoldrM step n
37− where
38− step cnt =
39− if cnt > n + value
40− then return Nothing
41− else return (Just (cnt, cnt + 1))
4236
4337runParser :: Int -> (Stream IO Char -> IO a) -> IO ()
4438runParser count p = do
4539 let v = "+123456789.123456789e-123"
46− let s = Stream.unfold Unfold.fromList v
40+ let !(arr :: Array.Array Char) = Array.fromListN (length v) v
41+ let s = Stream.unfold Array.reader arr
4742 replicateM_ count (p s)
4843
4944-- | Takes a fold method, and uses it with a default source.
@@ -51,10 +46,23 @@ runParser count p = do
5146benchIOSink :: Int -> String -> (Stream IO Char -> IO b) -> Benchmark
5247benchIOSink value name f = bench name $ nfIO $ runParser value f
5348
49+{-# INLINE doubleParser #-}
50+doubleParser :: Monad m => Stream m Char -> m (Either ParseError (Int, Int))
51+doubleParser = Stream.parse PRU.doubleParser
52+
53+{-# INLINE number #-}
54+number :: Monad m => Stream m Char -> m (Either ParseError (Integer, Int))
55+number = Stream.parse PRU.number
56+
5457{-# INLINE double #-}
5558double :: Monad m => Stream m Char -> m (Either ParseError Double)
5659double = Stream.parse PRU.double
5760
61+{-# INLINE numberDouble #-}
62+numberDouble :: Monad m => Stream m Char -> m (Either ParseError Double)
63+numberDouble = Stream.parse p
64+ where p = uncurry PRU.mkDouble <$> PRU.number
65+
5866-------------------------------------------------------------------------------
5967-- Benchmarks
6068-------------------------------------------------------------------------------
@@ -69,7 +77,10 @@ instance NFData ParseError where
6977o_n_heap_serial :: Int -> [Benchmark]
7078o_n_heap_serial value =
7179 [
72− benchIOSink value "double" double
80+ benchIOSink value "doubleParser" doubleParser
81+ , benchIOSink value "number" number
82+ , benchIOSink value "double (doubleParser)" double
83+ , benchIOSink value "double (number)" numberDouble
7384 ]
7485
7586-------------------------------------------------------------------------------
@@ -78,13 +89,19 @@ o_n_heap_serial value =
7889
7990main :: IO ()
8091main = do
81− env <- mkHandleBenchEnv
82− runWithCLIOptsEnv defaultStreamSize alloc (allBenchmarks env)
92+#ifndef FUSION_CHECK
93+ runWithCLIOpts defaultStreamSize allBenchmarks
8394
8495 where
8596
86− alloc value = Stream.fold Fold.toList $ Stream.chunksOf 100 $ sourceUnfoldrM value 0
87−
88− allBenchmarks _ _ value =
97+ allBenchmarks value =
8998 [ bgroup (o_n_heap_prefix moduleName) (o_n_heap_serial value)
9099 ]
100+#else
101+ -- Enable FUSION_CHECK macro at the beginning of the file
102+ -- Enable one benchmark below, and run the benchmark
103+ -- Check the .dump-simpl output
104+ let value = 100000
105+ runParser value double
106+ return ()
107+#endif
core/src/Streamly/Internal/Unicode/Parser.hs+148−5
@@ -48,6 +48,7 @@ module Streamly.Internal.Unicode.Parser
4848 -- * Numeric
4949 , signed
5050 , number
51+ , doubleParser
5152 , double
5253 , decimal
5354 , hexadecimal
@@ -58,7 +59,7 @@ module Streamly.Internal.Unicode.Parser
5859where
5960
6061import Control.Applicative (Alternative(..))
61−import Data.Bits (Bits, (.|.), shiftL)
62+import Data.Bits (Bits, (.|.), shiftL, (.&.))
6263import Data.Char (ord)
6364import Data.Ratio ((%))
6465import Fusion.Plugin.Types (Fuse(..))
@@ -269,6 +270,7 @@ hexadecimal = Parser.takeWhile1 isHexDigit (Fold.foldl' step 0)
269270signed :: (Num a, Monad m) => Parser Char m a -> Parser Char m a
270271signed p = (negate <$> (char '-' *> p)) <|> (char '+' *> p) <|> p
271272
273+-- XXX Change Multiplier to Sign
272274type Multiplier = Int
273275
274276-- XXX We can use Int instead of Integer to make it twice as fast. But then we
@@ -376,12 +378,12 @@ number = Parser (\s a -> return $ step s a) initial (return . extract)
376378 let num = ord val - 48
377379 if num >= 0 && num <= 9
378380 then Partial 0 $ SPAfterExponent multiplier buf decimalPlaces 1 num
379− else Error $ exitSPInitial $ show val
381+ else Done 2 $ exitSPAfterDot multiplier buf decimalPlaces
380382 step (SPExponentWithSign mult buf decimalPlaces powerMult) val =
381383 let num = ord val - 48
382384 in if num >= 0 && num <= 9
383385 then Partial 0 $ SPAfterExponent mult buf decimalPlaces powerMult num
384− else Error $ exitSPSign $ show val
386+ else Done 3 $ exitSPAfterDot mult buf decimalPlaces
385387 step (SPAfterExponent mult num decimalPlaces powerMult buf) val =
386388 let n = ord val - 48
387389 in if n >= 0 && n <= 9
@@ -407,9 +409,150 @@ number = Parser (\s a -> return $ step s a) initial (return . extract)
407409 extract (SPAfterExponent mult num decimalPlaces powerMult powerNum) =
408410 Done 0 $ exitSPAfterExponent mult num decimalPlaces powerMult powerNum
409411
412+type MantissaInt = Int
413+type OverflowPower = Int
414+
415+{-# ANN type DoubleParseState Fuse #-}
416+data DoubleParseState
417+ = DPInitial
418+ | DPSign !Multiplier
419+ | DPAfterSign !Multiplier !MantissaInt !OverflowPower
420+ | DPDot !Multiplier !MantissaInt !OverflowPower
421+ | DPAfterDot !Multiplier !MantissaInt !OverflowPower
422+ | DPExponent !Multiplier !MantissaInt !OverflowPower
423+ | DPExponentWithSign !Multiplier !MantissaInt !OverflowPower !PowerMultiplier
424+ | DPAfterExponent !Multiplier !MantissaInt !OverflowPower !PowerMultiplier !Power
425+
426+-- | A fast, custom parser for double precision flaoting point numbers. Returns
427+-- (mantissa, exponent) tuple. This is much faster than 'number' because it
428+-- assumes the number will fit in a 'Double' type and uses 'Int' representation
429+-- to store mantissa.
430+--
431+-- Number larger than 'Double' may overflow. Int overflow is not checked in the
432+-- exponent.
433+--
434+{-# INLINE doubleParser #-}
435+doubleParser :: Monad m => Parser Char m (Int, Int)
436+doubleParser = Parser (\s a -> return $ step s a) initial (return . extract)
437+
438+ where
439+
440+ -- XXX Assuming Int = Int64
441+
442+ -- Up to 58 bits Int won't overflow
443+ -- ghci> (2^59-1)*10+9 :: Int
444+ -- 5764607523034234879
445+ mask :: Word
446+ mask = 0x7c00000000000000 -- 58 bits, ignore the sign bit
447+
448+ {-# INLINE combineNum #-}
449+ combineNum :: Int -> Int -> Int -> (Int, Int)
450+ combineNum mantissa power num =
451+ if fromIntegral mantissa .&. mask == 0
452+ then (mantissa * 10 + num, power)
453+ else (mantissa, power + 1)
454+
455+ {-# INLINE initial #-}
456+ initial = pure $ IPartial DPInitial
457+
458+ exitDPInitial msg =
459+ "number: expecting sign or decimal digit, got " ++ msg
460+ exitDPSign msg =
461+ "number: expecting decimal digit, got " ++ msg
462+ exitDPAfterSign multiplier num opower = (fromIntegral multiplier * num, opower)
463+ exitDPAfterDot multiplier num opow =
464+ (fromIntegral multiplier * num , opow)
465+ exitDPAfterExponent mult num opow powerMult powerNum =
466+ (fromIntegral mult * num, opow + powerMult * powerNum)
467+
468+ {-# INLINE step #-}
469+ step DPInitial val =
470+ case val of
471+ '+' -> Continue 0 (DPSign 1)
472+ '-' -> Continue 0 $ (DPSign (-1))
473+ _ -> do
474+ let num = ord val - 48
475+ if num >= 0 && num <= 9
476+ then Partial 0 $ DPAfterSign 1 num 0
477+ else Error $ exitDPInitial $ show val
478+ step (DPSign multiplier) val =
479+ let num = ord val - 48
480+ in if num >= 0 && num <= 9
481+ then Partial 0 $ DPAfterSign multiplier num 0
482+ else Error $ exitDPSign $ show val
483+ step (DPAfterSign multiplier buf opower) val =
484+ case val of
485+ '.' -> Continue 0 $ DPDot multiplier buf opower
486+ 'e' -> Continue 0 $ DPExponent multiplier buf opower
487+ 'E' -> Continue 0 $ DPExponent multiplier buf opower
488+ _ ->
489+ let num = ord val - 48
490+ in if num >= 0 && num <= 9
491+ then
492+ let (buf1, power1) = combineNum buf opower num
493+ in Partial 0
494+ $ DPAfterSign multiplier buf1 power1
495+ else Done 1 $ exitDPAfterSign multiplier buf opower
496+ step (DPDot multiplier buf opower) val =
497+ let num = ord val - 48
498+ in if num >= 0 && num <= 9
499+ then
500+ let (buf1, power1) = combineNum buf opower num
501+ in Partial 0 $ DPAfterDot multiplier buf1 (power1 - 1)
502+ else Done 2 $ exitDPAfterSign multiplier buf opower
503+ step (DPAfterDot multiplier buf opower) val =
504+ case val of
505+ 'e' -> Continue 0 $ DPExponent multiplier buf opower
506+ 'E' -> Continue 0 $ DPExponent multiplier buf opower
507+ _ ->
508+ let num = ord val - 48
509+ in if num >= 0 && num <= 9
510+ then
511+ let (buf1, power1) = combineNum buf opower num
512+ in Partial 0 $ DPAfterDot multiplier buf1 (power1 - 1)
513+ else Done 1 $ exitDPAfterDot multiplier buf opower
514+ step (DPExponent multiplier buf opower) val =
515+ case val of
516+ '+' -> Continue 0 (DPExponentWithSign multiplier buf opower 1)
517+ '-' -> Continue 0 (DPExponentWithSign multiplier buf opower (-1))
518+ _ -> do
519+ let num = ord val - 48
520+ if num >= 0 && num <= 9
521+ then Partial 0 $ DPAfterExponent multiplier buf opower 1 num
522+ else Done 2 $ exitDPAfterDot multiplier buf opower
523+ step (DPExponentWithSign mult buf opower powerMult) val =
524+ let num = ord val - 48
525+ in if num >= 0 && num <= 9
526+ then Partial 0 $ DPAfterExponent mult buf opower powerMult num
527+ else Done 3 $ exitDPAfterDot mult buf opower
528+ step (DPAfterExponent mult num opower powerMult buf) val =
529+ let n = ord val - 48
530+ in if n >= 0 && n <= 9
531+ then
532+ Partial 0
533+ $ DPAfterExponent mult num opower powerMult (buf * 10 + n)
534+ else Done 1 $ exitDPAfterExponent mult num opower powerMult buf
535+
536+ {-# INLINE extract #-}
537+ extract DPInitial = Error $ exitDPInitial "end of input"
538+ extract (DPSign _) = Error $ exitDPSign "end of input"
539+ extract (DPAfterSign mult num opow) = Done 0 $ exitDPAfterSign mult num opow
540+ extract (DPDot mult num opow) = Done 1 $ exitDPAfterSign mult num opow
541+ extract (DPAfterDot mult num opow) =
542+ Done 0 $ exitDPAfterDot mult num opow
543+ extract (DPExponent mult num opow) =
544+ Done 1 $ exitDPAfterDot mult num opow
545+ extract (DPExponentWithSign mult num opow _) =
546+ Done 2 $ exitDPAfterDot mult num opow
547+ extract (DPAfterExponent mult num opow powerMult powerNum) =
548+ Done 0 $ exitDPAfterExponent mult num opow powerMult powerNum
549+
410550-- XXX We can have a `realFloat` parser instead to parse any RealFloat value.
411551-- And a integral parser to read any integral value.
412552
553+-- XXX This is very expensive, takes much more time than the rest of the
554+-- parsing. Need to look into fromRational.
555+
413556-- | @mkDouble mantissa exponent@ converts a mantissa and exponent to a
414557-- 'Double' value equivalent to @mantissa * 10^exponent@. It does not check for
415558-- overflow, powers more than 308 will overflow.
@@ -417,7 +560,7 @@ number = Parser (\s a -> return $ step s a) initial (return . extract)
417560mkDouble :: Integer -> Int -> Double
418561mkDouble mantissa power =
419562 if power > 0
420− then fromRational (fromIntegral (mantissa * 10 ^ power))
563+ then fromRational ((mantissa * 10 ^ power) % 1)
421564 else fromRational (mantissa % 10 ^ (-power))
422565
423566-- | Parse a decimal 'Double' value. This parser accepts an optional sign (+ or
@@ -473,4 +616,4 @@ mkDouble mantissa power =
473616--
474617{-# INLINE double #-}
475618double :: Monad m => Parser Char m Double
476−double = uncurry mkDouble <$> number
619+double = fmap (\(m,e) -> mkDouble (fromIntegral m) e) doubleParser
test/Streamly/Test/Unicode/Parser.hs+124−77
@@ -6,7 +6,7 @@ where
66
77import Control.Monad.Identity (Identity(runIdentity))
88import Debug.Trace (trace)
9−import Streamly.Internal.Data.Parser (ParseError (..))
9+import Streamly.Internal.Data.Parser (ParseError (..), Parser)
1010import Streamly.Test.Common (chooseDouble)
1111import Test.Hspec.QuickCheck (prop)
1212import Test.QuickCheck
@@ -89,103 +89,150 @@ double s d = monadicIO $ do
8989 case x of
9090 Right val -> if val == d
9191 then assert (val == d)
92− else trace ("Expectedc = " ++ show d ++ " Got = "++ show val) (assert (val == d))
92+ else trace ("Expected = " ++ show d ++ " Got = "++ show val) (assert (val == d))
9393 Left (ParseError _) -> assert False
9494
95−doubleErr :: String -> String -> Property
96−doubleErr s msg = monadicIO $ do
97− x <- run $ doubleParser s
95+numberP :: Monad m => Parser Char m Double
96+numberP = uncurry Parser.mkDouble <$> Parser.number
97+
98+numberParser :: String -> IO (Either ParseError Double)
99+numberParser = Stream.parse numberP . Stream.fromList
100+
101+number :: String -> Double -> Property
102+number s d = monadicIO $ do
103+ x <- run $ numberParser s
104+ case x of
105+ Right val -> if val == d
106+ then assert (val == d)
107+ else trace ("Expected = " ++ show d ++ " Got = "++ show val) (assert (val == d))
108+ Left (ParseError _) -> assert False
109+
110+doubleErr :: (String -> IO (Either ParseError Double)) -> String -> String -> Property
111+doubleErr f s msg = monadicIO $ do
112+ x <- run $ f s
98113 case x of
99114 Right _ -> assert False
100115 Left (ParseError err) -> if err == msg
101116 then assert (err == msg)
102117 else trace err (assert (err == msg))
103118
104−remainingStream :: String -> [String]
105−remainingStream x =
119+remainingStreamDouble :: String -> [String]
120+remainingStreamDouble x =
106121 let f = Stream.parseBreak Unicode.double . Stream.fromList
107122 in concatMap (Stream.toList . snd) (f x)
108123
109−afterParse :: String -> String -> Property
110−afterParse si so = monadicIO $ do
111− let x = remainingStream si
112− in assert (x == [so])
113−
114−moduleName :: String
115−moduleName = "Unicode.Parser"
124+remainingStreamNumber :: String -> [String]
125+remainingStreamNumber x =
126+ let f = Stream.parseBreak numberP . Stream.fromList
127+ in concatMap (Stream.toList . snd) (f x)
116128
117−main :: IO ()
118−main = do
119− H.hspec
120− $ H.describe moduleName $ do
121− H.describe "Unicode Parser" $ do
122− prop "double \"4\" 4.0" $ double "4" 4.0
123− prop "double \"00.3\" 0.3" $ double "00.3" 0.3
124− prop "double \"0.003\" 3.0e-3" $ double "0.003" 3.0e-3
125− prop "double \"44.56.67\" 44.56" $ double "44.56.67" 44.56
126− prop "double \"0.0.00\" 0.0" $ double "0.0.00" 0.0
127− prop "double \"44.56-78\" 44.56" $ double "44.56-78" 44.56
128− prop "double \"44-\" 44.0" $ double "44-" 44.0
129− prop "double \"44-56\" 44.0" $ double "44-56" 44.0
130− prop "double \"44+\" 44.0" $ double "44+" 44.0
131− prop "double \"44+56\" 44.0" $ double "44+56" 44.0
132− prop "double \"44u\" 44.0" $ double "44u" 44.0
133− prop "double \"+123.345\" 123.345" $ double "+123.345" 123.345
134− prop "double \"-123.345\" (-123.345)" $ double "-123.345" (-123.345)
135− prop "double \"1e\"" $ double "1e" 1.0
136− prop "double \"1e+\"" $ double "1e+" 1.0
137− prop "double \"1e+0\"" $ double "1e+0" 1.0
138− prop "double \"1e-0\"" $ double "1e-0" 1.0
139− prop "double \"1.0e-1\"" $ double "1.0e-1" 0.1
140− prop "double \"1.0e+1\"" $ double "1.0e+1" 10.0
141− prop "double \"1.0e1\"" $ double "1.0e1" 10.0
142− prop "double \"1.0e+309\"" $ double "1.0e+309" 1.0e309
143− prop "double \"1.0e-309\"" $ double "1.0e-309" 1.0e-309
144− prop "double \"1.0e1.\"" $ double "1.0e1." 10.0
145− prop "double \"1.0e1+\"" $ double "1.0e1+" 10.0
146− prop "double \"1.0e1-\"" $ double "1.0e1-" 10.0
147− prop "double \"1.0e1e\"" $ double "1.0e1e" 10.0
148− prop "double \"1.0E+1\"" $ double "1.0E+1" 10.0
129+afterParse :: (String -> [String]) -> String -> String -> Property
130+afterParse f si so = monadicIO $ do
131+ let x = f si
132+ in assert (x == [so])
149133
134+testParser :: String -> (String -> Double -> Property) -> H.SpecWith ()
135+testParser desc f = do
136+ -- XXX We can combine the parser tests and remaining stream tests in the
137+ -- same test.
138+ H.describe ("Unicode Parser " ++ desc) $ do
139+ prop "double \"00.00\"" $ f "00.00" 0.0
140+ prop "double \"00.00e00\"" $ f "00.00e00" 0.0
141+ prop "double \"4\" 4.0" $ f "4" 4.0
142+ prop "double \"00.3\" 0.3" $ f "00.3" 0.3
143+ prop "double \"0.003\" 3.0e-3" $ f "0.003" 3.0e-3
144+ prop "double \"44.56.67\" 44.56" $ f "44.56.67" 44.56
145+ prop "double \"0.0.00\" 0.0" $ f "0.0.00" 0.0
146+ prop "double \"44.56-78\" 44.56" $ f "44.56-78" 44.56
147+ prop "double \"44-\" 44.0" $ f "44-" 44.0
148+ prop "double \"44-56\" 44.0" $ f "44-56" 44.0
149+ prop "double \"44+\" 44.0" $ f "44+" 44.0
150+ prop "double \"44+56\" 44.0" $ f "44+56" 44.0
151+ prop "double \"44u\" 44.0" $ f "44u" 44.0
152+ prop "double \"+123.345\" 123.345" $ f "+123.345" 123.345
153+ prop "double \"-123.345\" (-123.345)" $ f "-123.345" (-123.345)
154+ prop "double \"1e\"" $ f "1e" 1.0
155+ prop "double \"1e+\"" $ f "1e+" 1.0
156+ prop "double \"1ex\"" $ f "1ex" 1.0
157+ prop "double \"1e+x\"" $ f "1e+x" 1.0
158+ prop "double \"1e+0\"" $ f "1e+0" 1.0
159+ prop "double \"1e-0\"" $ f "1e-0" 1.0
160+ prop "double \"1.0e-1\"" $ f "1.0e-1" 0.1
161+ prop "double \"1.0e+1\"" $ f "1.0e+1" 10.0
162+ prop "double \"1.0e1\"" $ f "1.0e1" 10.0
163+ prop "double \"1.0e+309\"" $ f "1.0e+309" 1.0e309
164+ prop "double \"1.0e-309\"" $ f "1.0e-309" 1.0e-309
165+ prop "double \"1.0e1.\"" $ f "1.0e1." 10.0
166+ prop "double \"1.0e1+\"" $ f "1.0e1+" 10.0
167+ prop "double \"1.0e1-\"" $ f "1.0e1-" 10.0
168+ prop "double \"1.0e1e\"" $ f "1.0e1e" 10.0
169+ prop "double \"1.0E+1\"" $ f "1.0E+1" 10.0
170+ prop "double \"9223372036854775806\"" $ f "9223372036854775806" 9.223372036854776e18
171+ -- maxBound :: Int
172+ prop "double \"9223372036854775807\"" $ f "9223372036854775807" 9.223372036854776e18
173+ prop "double \"9223372036854775808\"" $ f "9223372036854775808" 9.223372036854776e18
174+
175+testParseErr :: String -> (String -> IO (Either ParseError Double)) -> H.SpecWith ()
176+testParseErr desc f = do
177+ H.describe ("Unicode Parser Error " ++ desc) $ do
150178 prop "double \"\" Error"
151− $ doubleErr "" "number: expecting sign or decimal digit, got end of input"
179+ $ doubleErr f "" "number: expecting sign or decimal digit, got end of input"
152180 prop "double \"a\" Error"
153− $ doubleErr "a" "number: expecting sign or decimal digit, got 'a'"
181+ $ doubleErr f "a" "number: expecting sign or decimal digit, got 'a'"
154182 prop "double \".4\" Error"
155− $ doubleErr ".4" "number: expecting sign or decimal digit, got '.'"
183+ $ doubleErr f ".4" "number: expecting sign or decimal digit, got '.'"
156184 prop "double \".\" Error"
157− $ doubleErr "." "number: expecting sign or decimal digit, got '.'"
185+ $ doubleErr f "." "number: expecting sign or decimal digit, got '.'"
158186 prop "double \"..\" Error"
159− $ doubleErr ".." "number: expecting sign or decimal digit, got '.'"
187+ $ doubleErr f ".." "number: expecting sign or decimal digit, got '.'"
160188 prop "double \"-\" Error"
161− $ doubleErr "-" "number: expecting decimal digit, got end of input"
189+ $ doubleErr f "-" "number: expecting decimal digit, got end of input"
162190 prop "double \"+\" Error"
163− $ doubleErr "+" "number: expecting decimal digit, got end of input"
191+ $ doubleErr f "+" "number: expecting decimal digit, got end of input"
164192 prop "double \"++\" Error"
165− $ doubleErr "++" "number: expecting decimal digit, got '+'"
166−
167− prop "afterParse \"4.\" \".\"" $ afterParse "4." "."
168− prop "afterParse \"4..\" \"..\"" $ afterParse "4.." ".."
169− prop "afterParse \"4-\" \"-\"" $ afterParse "4-" "-"
170− prop "afterParse \"4--\" \"--\"" $ afterParse "4--" "--"
171− prop "afterParse \"4.9abc\" \"abc\"" $ afterParse "4.9abc" "abc"
172− prop "afterParse \"4.9\" \"\"" $ afterParse "4.9" ""
173− prop "afterParse \"+4.9\" \"\"" $ afterParse "+4.9" ""
174− prop "afterParse \"-4.9\" \"\"" $ afterParse "-4.9" ""
175− prop "afterParse \"4.9.\" \".\"" $ afterParse "4.9." "."
176− prop "afterParse \"4.9..\" \"..\"" $ afterParse "4.9.." ".."
177− prop "afterParse \"4.9...\" \"...\"" $ afterParse "4.9..." "..."
178− prop "afterParse \"4.9+\" \"+\"" $ afterParse "4.9+" "+"
179− prop "afterParse \"4.9++\" \"++\"" $ afterParse "4.9++" "++"
180− prop "afterParse \"4.9+++\" \"+++\"" $ afterParse "4.9+++" "+++"
181− prop "afterParse \"4.9-\" \"-\"" $ afterParse "4.9-" "-"
182− prop "afterParse \"4.9--\" \"--\"" $ afterParse "4.9--" "--"
183− prop "afterParse \"4.9---\" \"---\"" $ afterParse "4.9---" "---"
184− prop "afterParse \"\" \"\"" $ afterParse "" ""
185− prop "afterParse \".\" \".\"" $ afterParse "." "."
186− prop "afterParse \"..\" \"..\"" $ afterParse ".." ".."
187− prop "afterParse \"+\" \"+\"" $ afterParse "+" "+"
188− prop "afterParse \"++\" \"++\"" $ afterParse "++" "++"
193+ $ doubleErr f "++" "number: expecting decimal digit, got '+'"
194+
195+testLeftOver :: String -> (String -> [String]) -> H.SpecWith ()
196+testLeftOver desc f = do
197+ H.describe ("Unicode Parser leftover " ++ desc) $ do
198+ prop "afterParse \"4.\" \".\"" $ afterParse f "4." "."
199+ prop "afterParse \"4..\" \"..\"" $ afterParse f "4.." ".."
200+ prop "afterParse \"4-\" \"-\"" $ afterParse f "4-" "-"
201+ prop "afterParse \"4--\" \"--\"" $ afterParse f "4--" "--"
202+ prop "afterParse \"4.9abc\" \"abc\"" $ afterParse f "4.9abc" "abc"
203+ prop "afterParse \"4.9ex\"" $ afterParse f "4.9ex" "ex"
204+ prop "afterParse \"4.9e+x\"" $ afterParse f "4.9e+x" "e+x"
205+ prop "afterParse \"4.9\" \"\"" $ afterParse f "4.9" ""
206+ prop "afterParse \"+4.9\" \"\"" $ afterParse f "+4.9" ""
207+ prop "afterParse \"-4.9\" \"\"" $ afterParse f "-4.9" ""
208+ prop "afterParse \"4.9.\" \".\"" $ afterParse f "4.9." "."
209+ prop "afterParse \"4.9..\" \"..\"" $ afterParse f "4.9.." ".."
210+ prop "afterParse \"4.9...\" \"...\"" $ afterParse f "4.9..." "..."
211+ prop "afterParse \"4.9+\" \"+\"" $ afterParse f "4.9+" "+"
212+ prop "afterParse \"4.9++\" \"++\"" $ afterParse f "4.9++" "++"
213+ prop "afterParse \"4.9+++\" \"+++\"" $ afterParse f "4.9+++" "+++"
214+ prop "afterParse \"4.9-\" \"-\"" $ afterParse f "4.9-" "-"
215+ prop "afterParse \"4.9--\" \"--\"" $ afterParse f "4.9--" "--"
216+ prop "afterParse \"4.9---\" \"---\"" $ afterParse f "4.9---" "---"
217+ prop "afterParse \"\" \"\"" $ afterParse f "" ""
218+ prop "afterParse \".\" \".\"" $ afterParse f "." "."
219+ prop "afterParse \"..\" \"..\"" $ afterParse f ".." ".."
220+ prop "afterParse \"+\" \"+\"" $ afterParse f "+" "+"
221+ prop "afterParse \"++\" \"++\"" $ afterParse f "++" "++"
222+
223+moduleName :: String
224+moduleName = "Unicode.Parser"
225+
226+main :: IO ()
227+main = do
228+ H.hspec
229+ $ H.describe moduleName $ do
230+ testParser "double" double
231+ testParser "number" number
232+ testParseErr "double" doubleParser
233+ testParseErr "number" numberParser
234+ testLeftOver "double" remainingStreamDouble
235+ testLeftOver "number" remainingStreamNumber
189236 H.describe "Scientific parser property test" $ do
190237 prop "Exponent format" scientificExpFP
191238 prop "Decimal format" scientificFixFP