Skip to content

Commit 51d2843

Browse files
authored
Merge pull request #154 from natefaubion/cps-internals
CPS internals for better performance and stack safety
2 parents 54525df + fac60b5 commit 51d2843

17 files changed

+662
-252
lines changed

CHANGELOG.md

+12
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ Notable changes to this project are documented in this file. The format is based
77
Breaking changes:
88
- Update project and deps to PureScript v0.15.0 (#160 by @JordanMartinez)
99
- Drop deprecated `MonadZero` instance (#160 by @JordanMartinez)
10+
- New optimized internals. `ParserT` now has a more efficient representation,
11+
resulting in (up to) 20x performance improvement. In addition to the performance,
12+
all parser execution is always stack-safe, even monadically, obviating the need
13+
to run parsers with `Trampoline` as the base Monad or to explicitly use `MonadRec`.
14+
15+
Code that was parametric over the underlying Monad no longer needs to propagate a
16+
Monad constraint.
17+
18+
Code that constructs parsers via the underlying representation will need to be updated,
19+
but otherwise the interface is unchanged and parsers should just enjoy the speed boost.
20+
21+
(#154 by @natefaubion)
1022
- Make `<??>` right-associative (#164 by @JordanMartinez)
1123
- Drop `<?>` and `<~?>` prec from 3 to 4 (#163, #164 by @JordanMartinez)
1224

bench/Json/Common.purs

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module Bench.Json.Common where
2+
3+
import Prelude
4+
5+
import Data.Generic.Rep (class Generic)
6+
import Data.List (List)
7+
import Data.Show.Generic (genericShow)
8+
import Data.Tuple (Tuple)
9+
10+
data Json
11+
= JsonNull
12+
| JsonNumber Number
13+
| JsonString String
14+
| JsonBoolean Boolean
15+
| JsonArray (List Json)
16+
| JsonObject (List (Tuple String Json))
17+
18+
derive instance Generic Json _
19+
20+
instance Show Json where
21+
show a = genericShow a

bench/Json/Parsing.purs

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
module Bench.Json.Parsing where
2+
3+
import Prelude hiding (between)
4+
5+
import Bench.Json.Common (Json(..))
6+
import Control.Lazy (defer)
7+
import Data.List (List)
8+
import Data.Maybe (Maybe(..))
9+
import Data.Number as Number
10+
import Data.Tuple (Tuple(..))
11+
import Text.Parsing.Parser (Parser, fail)
12+
import Text.Parsing.Parser.Combinators (between, choice, sepBy, try)
13+
import Text.Parsing.Parser.String (regex, skipSpaces, string)
14+
15+
json :: Parser String Json
16+
json = defer \_ ->
17+
skipSpaces *> choice
18+
[ JsonObject <$> jsonObject
19+
, JsonArray <$> jsonArray
20+
, JsonString <$> jsonString
21+
, JsonNumber <$> jsonNumber
22+
, JsonBoolean <$> jsonBoolean
23+
, JsonNull <$ jsonNull
24+
]
25+
26+
jsonObject :: Parser String (List (Tuple String Json))
27+
jsonObject = defer \_ ->
28+
between (string "{") (skipSpaces *> string "}") do
29+
skipSpaces *> jsonObjectPair `sepBy` (try (skipSpaces *> string ","))
30+
31+
jsonObjectPair :: Parser String (Tuple String Json)
32+
jsonObjectPair = defer \_ ->
33+
Tuple <$> (skipSpaces *> jsonString <* skipSpaces <* string ":") <*> json
34+
35+
jsonArray :: Parser String (List Json)
36+
jsonArray = defer \_ ->
37+
between (string "[") (skipSpaces *> string "]") do
38+
json `sepBy` (try (skipSpaces *> string ","))
39+
40+
jsonString :: Parser String String
41+
jsonString = between (string "\"") (string "\"") do
42+
regex {} """\\"|[^"]*"""
43+
44+
jsonNumber :: Parser String Number
45+
jsonNumber = do
46+
n <- regex {} """(\+|-)?(\d+(\.\d*)?|\d*\.\d+)([eE](\+|-)?\d+)?"""
47+
case Number.fromString n of
48+
Just n' -> pure n'
49+
Nothing -> fail "Expected number"
50+
51+
jsonBoolean :: Parser String Boolean
52+
jsonBoolean = choice
53+
[ true <$ string "true"
54+
, false <$ string "false"
55+
]
56+
57+
jsonNull :: Parser String String
58+
jsonNull = string "null"

bench/Json/StringParsers.purs

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
module Bench.Json.StringParser where
2+
3+
import Prelude hiding (between)
4+
5+
import Bench.Json.Common (Json(..))
6+
import Control.Lazy (defer)
7+
import Data.List (List)
8+
import Data.Maybe (Maybe(..))
9+
import Data.Number as Number
10+
import Data.Tuple (Tuple(..))
11+
import StringParser (Parser, fail, try)
12+
import StringParser.CodePoints (regex, skipSpaces, string)
13+
import StringParser.Combinators (between, choice, sepBy)
14+
15+
json :: Parser Json
16+
json = defer \_ ->
17+
skipSpaces *> choice
18+
[ JsonObject <$> jsonObject
19+
, JsonArray <$> jsonArray
20+
, JsonString <$> jsonString
21+
, JsonNumber <$> jsonNumber
22+
, JsonBoolean <$> jsonBoolean
23+
, JsonNull <$ jsonNull
24+
]
25+
26+
jsonObject :: Parser (List (Tuple String Json))
27+
jsonObject = defer \_ ->
28+
between (string "{") (skipSpaces *> string "}") do
29+
skipSpaces *> jsonObjectPair `sepBy` (try (skipSpaces *> string ","))
30+
31+
jsonObjectPair :: Parser (Tuple String Json)
32+
jsonObjectPair = defer \_ ->
33+
Tuple <$> (skipSpaces *> jsonString <* skipSpaces <* string ":") <*> json
34+
35+
jsonArray :: Parser (List Json)
36+
jsonArray = defer \_ ->
37+
between (string "[") (skipSpaces *> string "]") do
38+
json `sepBy` (try (skipSpaces *> string ","))
39+
40+
jsonString :: Parser String
41+
jsonString = between (string "\"") (string "\"") do
42+
regex """\\"|[^"]*"""
43+
44+
jsonNumber :: Parser Number
45+
jsonNumber = do
46+
n <- regex """(\+|-)?(\d+(\.\d*)?|\d*\.\d+)([eE](\+|-)?\d+)?"""
47+
case Number.fromString n of
48+
Just n' -> pure n'
49+
Nothing -> fail "Expected number"
50+
51+
jsonBoolean :: Parser Boolean
52+
jsonBoolean = choice
53+
[ true <$ string "true"
54+
, false <$ string "false"
55+
]
56+
57+
jsonNull :: Parser String
58+
jsonNull = string "null"

bench/Json/TestData.purs

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
module Bench.Json.TestData where
2+
3+
import Prelude
4+
5+
import Data.Array (replicate)
6+
import Data.String (joinWith)
7+
8+
jsonProps :: String
9+
jsonProps =
10+
"""
11+
"some_number": 42.00009
12+
, "some_string": "foobarbazquux"
13+
, "some_null": null
14+
, "some_boolean": true
15+
, "some_other_boolean": false
16+
, "some_array": [ 1, 2, "foo", true, 99 ]
17+
, "some_object": { "foo": 42, "bar": "wat", "baz": false }
18+
"""
19+
20+
smallJson :: String
21+
smallJson = "{" <> jsonProps <> "}"
22+
23+
mediumJson :: String
24+
mediumJson = "{" <> joinWith ", " (replicate 30 jsonProps) <> "}"
25+
26+
largeJson :: String
27+
largeJson = "[" <> joinWith ", " (replicate 100 smallJson) <> "]"

bench/Main.purs

+32-5
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,12 @@ module Bench.Main where
4141

4242
import Prelude
4343

44+
import Bench.Json.Parsing as BenchParsing
45+
import Bench.Json.StringParser as BenchStringParser
46+
import Bench.Json.TestData (largeJson, mediumJson, smallJson)
4447
import Data.Array (fold, replicate)
4548
import Data.Either (either)
46-
import Data.List (manyRec)
49+
import Data.List (many, manyRec)
4750
import Data.List.Types (List)
4851
import Data.String.Regex (Regex, regex)
4952
import Data.String.Regex as Regex
@@ -56,9 +59,9 @@ import Performance.Minibench (benchWith)
5659
import Text.Parsing.Parser (Parser, runParser)
5760
import Text.Parsing.Parser.String (string)
5861
import Text.Parsing.Parser.String.Basic (digit)
59-
import Text.Parsing.StringParser as StringParser
60-
import Text.Parsing.StringParser.CodePoints as StringParser.CodePoints
61-
import Text.Parsing.StringParser.CodeUnits as StringParser.CodeUnits
62+
import StringParser as StringParser
63+
import StringParser.CodePoints as StringParser.CodePoints
64+
import StringParser.CodeUnits as StringParser.CodeUnits
6265

6366
string23 :: String
6467
string23 = "23"
@@ -100,7 +103,7 @@ pattern23 = either (unsafePerformEffect <<< throw) identity
100103
}
101104

102105
parseSkidoo :: Parser String (List String)
103-
parseSkidoo = manyRec $ string "skidoo"
106+
parseSkidoo = many $ string "skidoo"
104107

105108
patternSkidoo :: Regex
106109
patternSkidoo = either (unsafePerformEffect <<< throw) identity
@@ -138,3 +141,27 @@ main = do
138141
log "Regex.match patternSkidoo"
139142
benchWith 200
140143
$ \_ -> Regex.match patternSkidoo stringSkidoo_10000
144+
145+
log "runParser json smallJson"
146+
benchWith 1000
147+
$ \_ -> runParser smallJson BenchParsing.json
148+
149+
log "StringParser.runParser json smallJson"
150+
benchWith 1000
151+
$ \_ -> StringParser.runParser BenchStringParser.json smallJson
152+
153+
log "runParser json mediumJson"
154+
benchWith 500
155+
$ \_ -> runParser mediumJson BenchParsing.json
156+
157+
log "StringParser.runParser json mediumJson"
158+
benchWith 500
159+
$ \_ -> StringParser.runParser BenchStringParser.json mediumJson
160+
161+
log "runParser json largeJson"
162+
benchWith 100
163+
$ \_ -> runParser largeJson BenchParsing.json
164+
165+
log "StringParser.runParser json largeJson"
166+
benchWith 100
167+
$ \_ -> StringParser.runParser BenchStringParser.json largeJson

spago-dev.dhall

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ in conf //
1212
, dependencies = conf.dependencies #
1313
[ "assert"
1414
, "console"
15+
, "enums"
1516
, "effect"
1617
, "psci-support"
1718
, "minibench"

spago.dhall

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@
55
[ "arrays"
66
, "control"
77
, "either"
8+
, "enums"
89
, "foldable-traversable"
10+
, "functions"
911
, "identity"
1012
, "integers"
13+
, "lazy"
1114
, "lists"
1215
, "math"
1316
, "maybe"
1417
, "newtype"
1518
, "numbers"
19+
, "partial"
1620
, "prelude"
1721
, "record"
1822
, "strings"
@@ -21,7 +25,6 @@
2125
, "tuples"
2226
, "unfoldable"
2327
, "unicode"
24-
, "unsafe-coerce"
2528
]
2629
, packages = ./packages.dhall
2730
, sources = [ "src/**/*.purs" ]

0 commit comments

Comments
 (0)