Skip to content

CPS internals for better performance and stack safety #154

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Mar 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ Notable changes to this project are documented in this file. The format is based
Breaking changes:
- Update project and deps to PureScript v0.15.0 (#160 by @JordanMartinez)
- Drop deprecated `MonadZero` instance (#160 by @JordanMartinez)
- New optimized internals. `ParserT` now has a more efficient representation,
resulting in (up to) 20x performance improvement. In addition to the performance,
all parser execution is always stack-safe, even monadically, obviating the need
to run parsers with `Trampoline` as the base Monad or to explicitly use `MonadRec`.

Code that was parametric over the underlying Monad no longer needs to propagate a
Monad constraint.

Code that constructs parsers via the underlying representation will need to be updated,
but otherwise the interface is unchanged and parsers should just enjoy the speed boost.

(#154 by @natefaubion)
- Make `<??>` right-associative (#164 by @JordanMartinez)
- Drop `<?>` and `<~?>` prec from 3 to 4 (#163, #164 by @JordanMartinez)

Expand Down
21 changes: 21 additions & 0 deletions bench/Json/Common.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module Bench.Json.Common where

import Prelude

import Data.Generic.Rep (class Generic)
import Data.List (List)
import Data.Show.Generic (genericShow)
import Data.Tuple (Tuple)

data Json
= JsonNull
| JsonNumber Number
| JsonString String
| JsonBoolean Boolean
| JsonArray (List Json)
| JsonObject (List (Tuple String Json))

derive instance Generic Json _

instance Show Json where
show a = genericShow a
58 changes: 58 additions & 0 deletions bench/Json/Parsing.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
module Bench.Json.Parsing where

import Prelude hiding (between)

import Bench.Json.Common (Json(..))
import Control.Lazy (defer)
import Data.List (List)
import Data.Maybe (Maybe(..))
import Data.Number as Number
import Data.Tuple (Tuple(..))
import Text.Parsing.Parser (Parser, fail)
import Text.Parsing.Parser.Combinators (between, choice, sepBy, try)
import Text.Parsing.Parser.String (regex, skipSpaces, string)

json :: Parser String Json
json = defer \_ ->
skipSpaces *> choice
[ JsonObject <$> jsonObject
, JsonArray <$> jsonArray
, JsonString <$> jsonString
, JsonNumber <$> jsonNumber
, JsonBoolean <$> jsonBoolean
, JsonNull <$ jsonNull
]

jsonObject :: Parser String (List (Tuple String Json))
jsonObject = defer \_ ->
between (string "{") (skipSpaces *> string "}") do
skipSpaces *> jsonObjectPair `sepBy` (try (skipSpaces *> string ","))

jsonObjectPair :: Parser String (Tuple String Json)
jsonObjectPair = defer \_ ->
Tuple <$> (skipSpaces *> jsonString <* skipSpaces <* string ":") <*> json

jsonArray :: Parser String (List Json)
jsonArray = defer \_ ->
between (string "[") (skipSpaces *> string "]") do
json `sepBy` (try (skipSpaces *> string ","))

jsonString :: Parser String String
jsonString = between (string "\"") (string "\"") do
regex {} """\\"|[^"]*"""

jsonNumber :: Parser String Number
jsonNumber = do
n <- regex {} """(\+|-)?(\d+(\.\d*)?|\d*\.\d+)([eE](\+|-)?\d+)?"""
case Number.fromString n of
Just n' -> pure n'
Nothing -> fail "Expected number"

jsonBoolean :: Parser String Boolean
jsonBoolean = choice
[ true <$ string "true"
, false <$ string "false"
]

jsonNull :: Parser String String
jsonNull = string "null"
58 changes: 58 additions & 0 deletions bench/Json/StringParsers.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
module Bench.Json.StringParser where

import Prelude hiding (between)

import Bench.Json.Common (Json(..))
import Control.Lazy (defer)
import Data.List (List)
import Data.Maybe (Maybe(..))
import Data.Number as Number
import Data.Tuple (Tuple(..))
import StringParser (Parser, fail, try)
import StringParser.CodePoints (regex, skipSpaces, string)
import StringParser.Combinators (between, choice, sepBy)

json :: Parser Json
json = defer \_ ->
skipSpaces *> choice
[ JsonObject <$> jsonObject
, JsonArray <$> jsonArray
, JsonString <$> jsonString
, JsonNumber <$> jsonNumber
, JsonBoolean <$> jsonBoolean
, JsonNull <$ jsonNull
]

jsonObject :: Parser (List (Tuple String Json))
jsonObject = defer \_ ->
between (string "{") (skipSpaces *> string "}") do
skipSpaces *> jsonObjectPair `sepBy` (try (skipSpaces *> string ","))

jsonObjectPair :: Parser (Tuple String Json)
jsonObjectPair = defer \_ ->
Tuple <$> (skipSpaces *> jsonString <* skipSpaces <* string ":") <*> json

jsonArray :: Parser (List Json)
jsonArray = defer \_ ->
between (string "[") (skipSpaces *> string "]") do
json `sepBy` (try (skipSpaces *> string ","))

jsonString :: Parser String
jsonString = between (string "\"") (string "\"") do
regex """\\"|[^"]*"""

jsonNumber :: Parser Number
jsonNumber = do
n <- regex """(\+|-)?(\d+(\.\d*)?|\d*\.\d+)([eE](\+|-)?\d+)?"""
case Number.fromString n of
Just n' -> pure n'
Nothing -> fail "Expected number"

jsonBoolean :: Parser Boolean
jsonBoolean = choice
[ true <$ string "true"
, false <$ string "false"
]

jsonNull :: Parser String
jsonNull = string "null"
27 changes: 27 additions & 0 deletions bench/Json/TestData.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module Bench.Json.TestData where

import Prelude

import Data.Array (replicate)
import Data.String (joinWith)

jsonProps :: String
jsonProps =
"""
"some_number": 42.00009
, "some_string": "foobarbazquux"
, "some_null": null
, "some_boolean": true
, "some_other_boolean": false
, "some_array": [ 1, 2, "foo", true, 99 ]
, "some_object": { "foo": 42, "bar": "wat", "baz": false }
"""

smallJson :: String
smallJson = "{" <> jsonProps <> "}"

mediumJson :: String
mediumJson = "{" <> joinWith ", " (replicate 30 jsonProps) <> "}"

largeJson :: String
largeJson = "[" <> joinWith ", " (replicate 100 smallJson) <> "]"
37 changes: 32 additions & 5 deletions bench/Main.purs
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,12 @@ module Bench.Main where

import Prelude

import Bench.Json.Parsing as BenchParsing
import Bench.Json.StringParser as BenchStringParser
import Bench.Json.TestData (largeJson, mediumJson, smallJson)
import Data.Array (fold, replicate)
import Data.Either (either)
import Data.List (manyRec)
import Data.List (many, manyRec)
import Data.List.Types (List)
import Data.String.Regex (Regex, regex)
import Data.String.Regex as Regex
Expand All @@ -56,9 +59,9 @@ import Performance.Minibench (benchWith)
import Text.Parsing.Parser (Parser, runParser)
import Text.Parsing.Parser.String (string)
import Text.Parsing.Parser.String.Basic (digit)
import Text.Parsing.StringParser as StringParser
import Text.Parsing.StringParser.CodePoints as StringParser.CodePoints
import Text.Parsing.StringParser.CodeUnits as StringParser.CodeUnits
import StringParser as StringParser
import StringParser.CodePoints as StringParser.CodePoints
import StringParser.CodeUnits as StringParser.CodeUnits

string23 :: String
string23 = "23"
Expand Down Expand Up @@ -100,7 +103,7 @@ pattern23 = either (unsafePerformEffect <<< throw) identity
}

parseSkidoo :: Parser String (List String)
parseSkidoo = manyRec $ string "skidoo"
parseSkidoo = many $ string "skidoo"

patternSkidoo :: Regex
patternSkidoo = either (unsafePerformEffect <<< throw) identity
Expand Down Expand Up @@ -138,3 +141,27 @@ main = do
log "Regex.match patternSkidoo"
benchWith 200
$ \_ -> Regex.match patternSkidoo stringSkidoo_10000

log "runParser json smallJson"
benchWith 1000
$ \_ -> runParser smallJson BenchParsing.json

log "StringParser.runParser json smallJson"
benchWith 1000
$ \_ -> StringParser.runParser BenchStringParser.json smallJson

log "runParser json mediumJson"
benchWith 500
$ \_ -> runParser mediumJson BenchParsing.json

log "StringParser.runParser json mediumJson"
benchWith 500
$ \_ -> StringParser.runParser BenchStringParser.json mediumJson

log "runParser json largeJson"
benchWith 100
$ \_ -> runParser largeJson BenchParsing.json

log "StringParser.runParser json largeJson"
benchWith 100
$ \_ -> StringParser.runParser BenchStringParser.json largeJson
1 change: 1 addition & 0 deletions spago-dev.dhall
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ in conf //
, dependencies = conf.dependencies #
[ "assert"
, "console"
, "enums"
, "effect"
, "psci-support"
, "minibench"
Expand Down
5 changes: 4 additions & 1 deletion spago.dhall
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@
[ "arrays"
, "control"
, "either"
, "enums"
, "foldable-traversable"
, "functions"
, "identity"
, "integers"
, "lazy"
, "lists"
, "math"
, "maybe"
, "newtype"
, "numbers"
, "partial"
, "prelude"
, "record"
, "strings"
Expand All @@ -21,7 +25,6 @@
, "tuples"
, "unfoldable"
, "unicode"
, "unsafe-coerce"
]
, packages = ./packages.dhall
, sources = [ "src/**/*.purs" ]
Expand Down
Loading