on master help sign in
Clean up and document the stricter parsing functions.
commite44ddd98bd71039bc6fcaf24e02f48f416b919e2 authorBryan O'Sullivan <bos@serpentine.com> dateWed 30 Nov 2011, 21:21 (15 years ago) parent4666c3b
7 files changed+341−197
Data/Aeson.hs+17−7 Data/Aeson/Generic.hs+17−7 Data/Aeson/Parser.hs+33−182 Data/Aeson/Parser/Internal.hs+249 aeson.cabal+1 benchmarks/bench-parse.py+6−1 release-notes.markdown+18
Data/Aeson.hs+17−7
@@ -14,6 +14,7 @@ module Data.Aeson
1414 (
1515 -- * Encoding and decoding
1616 decode
17+ , decode'
1718 , encode
1819 -- * Core JSON types
1920 , Value(..)
@@ -34,21 +35,30 @@ module Data.Aeson
3435 , object
3536 -- * Parsing
3637 , json
38+ , json'
3739 ) where
3840
3941import Data.Aeson.Encode (encode)
40−import Data.Aeson.Parser (json)
42+import Data.Aeson.Parser.Internal (decodeWith, json, json')
4143import Data.Aeson.Types
4244import qualified Data.ByteString.Lazy as L
43−import qualified Data.Attoparsec.Lazy as L
4445
4546-- | Efficiently deserialize a JSON value from a lazy 'L.ByteString'.
4647-- If this fails due to incomplete or invalid input, 'Nothing' is
4748-- returned.
49+--
50+-- This function parses immediately, but defers conversion. See
51+-- 'json' for details.
4852decode :: (FromJSON a) => L.ByteString -> Maybe a
49−decode s = case L.parse json s of
50− L.Done _ v -> case fromJSON v of
51− Success a -> Just a
52− _ -> Nothing
53− _ -> Nothing
53+decode = decodeWith json fromJSON
5454{-# INLINE decode #-}
55+
56+-- | Efficiently deserialize a JSON value from a lazy 'L.ByteString'.
57+-- If this fails due to incomplete or invalid input, 'Nothing' is
58+-- returned.
59+--
60+-- This function parses and performs conversion immediately. See
61+-- 'json'' for details.
62+decode' :: (FromJSON a) => L.ByteString -> Maybe a
63+decode' = decodeWith json' fromJSON
64+{-# INLINE decode' #-}
Data/Aeson/Generic.hs+17−7
@@ -18,6 +18,7 @@ module Data.Aeson.Generic
1818 (
1919 -- * Decoding and encoding
2020 decode
21+ , decode'
2122 , encode
2223 -- * Lower-level conversion functions
2324 , fromJSON
@@ -39,11 +40,10 @@ import Data.Text (Text, pack, unpack)
3940import Data.Text.Encoding (encodeUtf8)
4041import Data.Time.Clock (UTCTime)
4142import Data.Word (Word, Word8, Word16, Word32, Word64)
42−import Data.Aeson.Parser (json)
43+import Data.Aeson.Parser.Internal (decodeWith, json, json')
4344import qualified Data.Aeson.Encode as E
4445import qualified Data.Aeson.Functions as F
4546import qualified Data.Aeson.Types as T
46−import qualified Data.Attoparsec.Lazy as L
4747import qualified Data.ByteString as B
4848import qualified Data.ByteString.Lazy as L
4949import qualified Data.HashMap.Strict as H
@@ -62,12 +62,22 @@ encode = E.encode . toJSON
6262-- | Efficiently deserialize a JSON value from a lazy 'L.ByteString'.
6363-- If this fails due to incomplete or invalid input, 'Nothing' is
6464-- returned.
65+--
66+-- This function parses immediately, but defers conversion. See
67+-- 'json' for details.
6568decode :: (Data a) => L.ByteString -> Maybe a
66−decode s = case L.parse json s of
67− L.Done _ v -> case fromJSON v of
68− Success a -> Just a
69− _ -> Nothing
70− _ -> Nothing
69+decode = decodeWith json fromJSON
70+{-# INLINE decode #-}
71+
72+-- | Efficiently deserialize a JSON value from a lazy 'L.ByteString'.
73+-- If this fails due to incomplete or invalid input, 'Nothing' is
74+-- returned.
75+--
76+-- This function parses and performs conversion immediately. See
77+-- 'json'' for details.
78+decode' :: (Data a) => L.ByteString -> Maybe a
79+decode' = decodeWith json' fromJSON
80+{-# INLINE decode' #-}
7181
7282type T a = a -> Value
7383
Data/Aeson/Parser.hs+33−182
@@ -10,196 +10,47 @@
1010--
1111-- Efficiently and correctly parse a JSON string. The string must be
1212-- encoded as UTF-8.
13+--
14+-- It can be useful to think of parsing as occurring in two phases:
15+--
16+-- * Identification of the textual boundaries of a JSON value. This
17+-- is always strict, so that an invalid JSON document can be
18+-- rejected as soon as possible.
19+--
20+-- * Conversion of a JSON value to a Haskell value. This may be
21+-- either immediate (strict) or deferred (lazy); see below for
22+-- details.
1323
1424module Data.Aeson.Parser
1525 (
26+ -- * Lazy parsers
27+ -- $lazy
1628 json
17− , json'
1829 , value
19− , value'
2030 , jstring
31+ -- * Strict parsers
32+ -- $strict
33+ , json'
34+ , value'
2135 ) where
2236
23−import Blaze.ByteString.Builder (fromByteString, toByteString)
24−import Blaze.ByteString.Builder.Char.Utf8 (fromChar)
25−import Blaze.ByteString.Builder.Word (fromWord8)
26−import Control.Applicative as A
27−import Data.Aeson.Types (Value(..))
28−import Data.Attoparsec.Char8
29−import Data.Bits ((.|.), shiftL)
30−import Data.ByteString as B
31−import Data.Char (chr)
32−import Data.Monoid (mappend, mempty)
33−import Data.Text as T
34−import Data.Text.Encoding (decodeUtf8)
35−import Data.Vector as Vector hiding ((++))
36−import Data.Word (Word8)
37−import qualified Data.Attoparsec as A
38−import qualified Data.Attoparsec.Zepto as Z
39−import qualified Data.ByteString.Char8 as B8
40−import qualified Data.ByteString.Unsafe as B
41−import qualified Data.HashMap.Strict as H
42−
43−-- | Parse a top-level JSON value. This must be either an object or
44−-- an array.
45−json :: Parser Value
46−json = json_ object_ array_
37+import Data.Aeson.Parser.Internal (json, json', jstring, value, value')
4738
48−-- | Parse a top-level JSON value. This must be either an object or
49−-- an array.
39+-- $lazy
5040--
51−-- This is a strict parser version of 'json' which avoids
52−-- building up thunks during parsing. Prefer this version if most of
53−-- the JSON data needs to be accessed.
54−json' :: Parser Value
55−json' = json_ object_' array_'
56−
57−json_ :: Parser Value -> Parser Value -> Parser Value
58−json_ obj ary = do
59− w <- skipSpace *> A.satisfy (\w -> w == 123 || w == 91)
60− if w == 123
61− then obj
62− else ary
63−{-# INLINE json_ #-}
64−
65−object_ :: Parser Value
66−object_ = {-# SCC "object_" #-} Object <$> objectValues value
67−
68−object_' :: Parser Value
69−object_' = {-# SCC "object_'" #-} do
70− !vals <- objectValues value'
71− return (Object vals)
72−
73−objectValues :: Parser Value -> Parser (H.HashMap Text Value)
74−objectValues val = do
75− skipSpace
76− let pair = do
77− a <- jstring <* skipSpace
78− b <- char ':' *> skipSpace *> val
79− return (a,b)
80− vals <- ((pair <* skipSpace) `sepBy` (char ',' *> skipSpace)) <* char '}'
81− return (H.fromList vals)
82−{-# INLINE objectValues #-}
83−
84−array_ :: Parser Value
85−array_ = {-# SCC "array_" #-} Array <$> arrayValues value
86−
87−array_' :: Parser Value
88−array_' = {-# SCC "array_'" #-} do
89− !vals <- arrayValues value'
90− return (Array vals)
91−
92−arrayValues :: Parser Value -> Parser (Vector Value)
93−arrayValues val = do
94− skipSpace
95− vals <- ((val <* skipSpace) `sepBy` (char ',' *> skipSpace)) <* char ']'
96− return (Vector.fromList vals)
97−{-# INLINE arrayValues #-}
98−
99−-- | Parse any JSON value. Use 'json' in preference to this function
100−-- if you are parsing data from an untrusted source.
101−value :: Parser Value
102−value = most <|> (Number <$> number)
103− where
104− most = do
105− c <- satisfy (`B8.elem` "{[\"ftn")
106− case c of
107− '{' -> object_
108− '[' -> array_
109− '"' -> String <$> jstring_
110− 'f' -> string "alse" *> pure (Bool False)
111− 't' -> string "rue" *> pure (Bool True)
112− 'n' -> string "ull" *> pure Null
113− _ -> error "attoparsec panic! the impossible happened!"
114−
115−-- | Strict version of 'value'. See also 'json''.
116−value' :: Parser Value
117−value' = most <|> num
118− where
119− most = do
120− c <- satisfy (`B8.elem` "{[\"ftn")
121− case c of
122− '{' -> object_'
123− '[' -> array_'
124− '"' -> do
125− !s <- jstring_
126− return (String s)
127− 'f' -> string "alse" *> pure (Bool False)
128− 't' -> string "rue" *> pure (Bool True)
129− 'n' -> string "ull" *> pure Null
130− _ -> error "attoparsec panic! the impossible happened!"
131− num = do
132− !n <- number
133− return (Number n)
134−
135−doubleQuote, backslash :: Word8
136−doubleQuote = 34
137−backslash = 92
138−{-# INLINE backslash #-}
139−{-# INLINE doubleQuote #-}
140−
141−jstring :: Parser Text
142−jstring = A.word8 doubleQuote *> jstring_
143−
144−-- | Parse a string without a leading quote.
145−jstring_ :: Parser Text
146−jstring_ = {-# SCC "jstring_" #-} do
147− s <- A.scan False $ \s c -> if s then Just False
148− else if c == doubleQuote
149− then Nothing
150− else Just (c == backslash)
151− _ <- A.word8 doubleQuote
152− if backslash `B.elem` s
153− then case Z.parse unescape s of
154− Right r -> return (decodeUtf8 r)
155− Left err -> fail err
156− else return (decodeUtf8 s)
157−{-# INLINE jstring_ #-}
158−
159−unescape :: Z.Parser ByteString
160−unescape = toByteString <$> go mempty where
161− go acc = do
162− h <- Z.takeWhile (/=backslash)
163− let rest = do
164− start <- Z.take 2
165− let !slash = B.unsafeHead start
166− !t = B.unsafeIndex start 1
167− escape = case B.findIndex (==t) "\"\\/ntbrfu" of
168− Just i -> i
169− _ -> 255
170− if slash /= backslash || escape == 255
171− then fail "invalid JSON escape sequence"
172− else do
173− let cont m = go (acc `mappend` fromByteString h `mappend` m)
174− {-# INLINE cont #-}
175− if t /= 117 -- 'u'
176− then cont (fromWord8 (B.unsafeIndex mapping escape))
177− else do
178− a <- hexQuad
179− if a < 0xd800 || a > 0xdfff
180− then cont (fromChar (chr a))
181− else do
182− b <- Z.string "\\u" *> hexQuad
183− if a <= 0xdbff && b >= 0xdc00 && b <= 0xdfff
184− then let !c = ((a - 0xd800) `shiftL` 10) +
185− (b - 0xdc00) + 0x10000
186− in cont (fromChar (chr c))
187− else fail "invalid UTF-16 surrogates"
188− done <- Z.atEnd
189− if done
190− then return (acc `mappend` fromByteString h)
191− else rest
192− mapping = "\"\\/\n\t\b\r\f"
41+-- The 'json' and 'value' parsers decouple identification from
42+-- conversion. Identification occurs immediately (so that an invalid
43+-- JSON document can be rejected as early as possible), but conversion
44+-- to a Haskell value is deferred until that value is needed.
45+--
46+-- This decoupling can be time-efficient if only a smallish subset of
47+-- elements in a JSON value need to be inspected, since the cost of
48+-- conversion is zero for uninspected elements. The trade off is an
49+-- increase in memory usage, due to allocation of thunks for values
50+-- that have not yet been converted.
19351
194−hexQuad :: Z.Parser Int
195−hexQuad = do
196− s <- Z.take 4
197− let hex n | w >= 48 && w <= 57 = w - 48
198− | w >= 97 && w <= 122 = w - 87
199− | w >= 65 && w <= 90 = w - 55
200− | otherwise = 255
201− where w = fromIntegral $ B.unsafeIndex s n
202− a = hex 0; b = hex 1; c = hex 2; d = hex 3
203− if (a .|. b .|. c .|. d) /= 255
204− then return $! d .|. (c `shiftL` 4) .|. (b `shiftL` 8) .|. (a `shiftL` 12)
205− else fail "invalid hex escape"
52+-- $strict
53+--
54+-- The 'json'' and 'value'' parsers combine identification with
55+-- conversion. They consume more CPU cycles up front, but have a
56+-- smaller memory footprint.
Data/Aeson/Parser/Internal.hsnew file+249
@@ -0,0 +1,249 @@
1+{-# LANGUAGE BangPatterns, OverloadedStrings #-}
2+
3+-- |
4+-- Module: Data.Aeson.Parser.Internal
5+-- Copyright: (c) 2011 MailRank, Inc.
6+-- License: Apache
7+-- Maintainer: Bryan O'Sullivan <bos@serpentine.com>
8+-- Stability: experimental
9+-- Portability: portable
10+--
11+-- Efficiently and correctly parse a JSON string. The string must be
12+-- encoded as UTF-8.
13+
14+module Data.Aeson.Parser.Internal
15+ (
16+ -- * Lazy parsers
17+ json
18+ , value
19+ , jstring
20+ -- * Strict parsers
21+ , json'
22+ , value'
23+ -- * Helpers
24+ , decodeWith
25+ ) where
26+
27+import Blaze.ByteString.Builder (fromByteString, toByteString)
28+import Blaze.ByteString.Builder.Char.Utf8 (fromChar)
29+import Blaze.ByteString.Builder.Word (fromWord8)
30+import Control.Applicative as A
31+import Data.Aeson.Types (Result(..), Value(..))
32+import Data.Attoparsec.Char8 hiding (Result)
33+import Data.Bits ((.|.), shiftL)
34+import Data.ByteString as B
35+import Data.Char (chr)
36+import Data.Monoid (mappend, mempty)
37+import Data.Text as T
38+import Data.Text.Encoding (decodeUtf8)
39+import Data.Vector as Vector hiding ((++))
40+import Data.Word (Word8)
41+import qualified Data.Attoparsec as A
42+import qualified Data.Attoparsec.Lazy as L
43+import qualified Data.Attoparsec.Zepto as Z
44+import qualified Data.ByteString.Char8 as B8
45+import qualified Data.ByteString.Lazy as L
46+import qualified Data.ByteString.Unsafe as B
47+import qualified Data.HashMap.Strict as H
48+
49+-- | Parse a top-level JSON value. This must be either an object or
50+-- an array.
51+--
52+-- The conversion of parsed values to Haskell values is deferred.
53+-- This may improve performance if not all of the results of
54+-- conversions are needed, but at a cost in thunk allocation.
55+json :: Parser Value
56+json = json_ object_ array_
57+
58+-- | Parse a top-level JSON value. This must be either an object or
59+-- an array.
60+--
61+-- This is a strict version of 'json' which avoids building up thunks
62+-- during parsing; it performs all conversions immediately. Prefer
63+-- this version if most of the JSON data needs to be accessed.
64+json' :: Parser Value
65+json' = json_ object_' array_'
66+
67+json_ :: Parser Value -> Parser Value -> Parser Value
68+json_ obj ary = do
69+ w <- skipSpace *> A.satisfy (\w -> w == 123 || w == 91)
70+ if w == 123
71+ then obj
72+ else ary
73+{-# INLINE json_ #-}
74+
75+object_ :: Parser Value
76+object_ = {-# SCC "object_" #-} Object <$> objectValues jstring value
77+
78+object_' :: Parser Value
79+object_' = {-# SCC "object_'" #-} do
80+ !vals <- objectValues jstring' value'
81+ return (Object vals)
82+ where
83+ jstring' = do
84+ !s <- jstring
85+ return s
86+
87+objectValues :: Parser Text -> Parser Value -> Parser (H.HashMap Text Value)
88+objectValues str val = do
89+ skipSpace
90+ let pair = do
91+ a <- str <* skipSpace
92+ b <- char ':' *> skipSpace *> val
93+ return (a,b)
94+ vals <- ((pair <* skipSpace) `sepBy` (char ',' *> skipSpace)) <* char '}'
95+ return (H.fromList vals)
96+{-# INLINE objectValues #-}
97+
98+array_ :: Parser Value
99+array_ = {-# SCC "array_" #-} Array <$> arrayValues value
100+
101+array_' :: Parser Value
102+array_' = {-# SCC "array_'" #-} do
103+ !vals <- arrayValues value'
104+ return (Array vals)
105+
106+arrayValues :: Parser Value -> Parser (Vector Value)
107+arrayValues val = do
108+ skipSpace
109+ vals <- ((val <* skipSpace) `sepBy` (char ',' *> skipSpace)) <* char ']'
110+ return (Vector.fromList vals)
111+{-# INLINE arrayValues #-}
112+
113+-- | Parse any JSON value. You should usually 'json' in preference to
114+-- this function. This is only safe to use if you are parsing data
115+-- from an untrusted source.
116+value :: Parser Value
117+value = most <|> (Number <$> number)
118+ where
119+ most = do
120+ c <- satisfy (`B8.elem` "{[\"ftn")
121+ case c of
122+ '{' -> object_
123+ '[' -> array_
124+ '"' -> String <$> jstring_
125+ 'f' -> string "alse" *> pure (Bool False)
126+ 't' -> string "rue" *> pure (Bool True)
127+ 'n' -> string "ull" *> pure Null
128+ _ -> error "attoparsec panic! the impossible happened!"
129+
130+-- | Strict version of 'value'. See also 'json''.
131+value' :: Parser Value
132+value' = most <|> num
133+ where
134+ most = do
135+ c <- satisfy (`B8.elem` "{[\"ftn")
136+ case c of
137+ '{' -> object_'
138+ '[' -> array_'
139+ '"' -> do
140+ !s <- jstring_
141+ return (String s)
142+ 'f' -> string "alse" *> pure (Bool False)
143+ 't' -> string "rue" *> pure (Bool True)
144+ 'n' -> string "ull" *> pure Null
145+ _ -> error "attoparsec panic! the impossible happened!"
146+ num = do
147+ !n <- number
148+ return (Number n)
149+
150+doubleQuote, backslash :: Word8
151+doubleQuote = 34
152+backslash = 92
153+{-# INLINE backslash #-}
154+{-# INLINE doubleQuote #-}
155+
156+-- | Parse a quoted JSON string.
157+jstring :: Parser Text
158+jstring = A.word8 doubleQuote *> jstring_
159+
160+-- | Parse a string without a leading quote.
161+jstring_ :: Parser Text
162+jstring_ = {-# SCC "jstring_" #-} do
163+ s <- A.scan False $ \s c -> if s then Just False
164+ else if c == doubleQuote
165+ then Nothing
166+ else Just (c == backslash)
167+ _ <- A.word8 doubleQuote
168+ if backslash `B.elem` s
169+ then case Z.parse unescape s of
170+ Right r -> return (decodeUtf8 r)
171+ Left err -> fail err
172+ else return (decodeUtf8 s)
173+{-# INLINE jstring_ #-}
174+
175+unescape :: Z.Parser ByteString
176+unescape = toByteString <$> go mempty where
177+ go acc = do
178+ h <- Z.takeWhile (/=backslash)
179+ let rest = do
180+ start <- Z.take 2
181+ let !slash = B.unsafeHead start
182+ !t = B.unsafeIndex start 1
183+ escape = case B.findIndex (==t) "\"\\/ntbrfu" of
184+ Just i -> i
185+ _ -> 255
186+ if slash /= backslash || escape == 255
187+ then fail "invalid JSON escape sequence"
188+ else do
189+ let cont m = go (acc `mappend` fromByteString h `mappend` m)
190+ {-# INLINE cont #-}
191+ if t /= 117 -- 'u'
192+ then cont (fromWord8 (B.unsafeIndex mapping escape))
193+ else do
194+ a <- hexQuad
195+ if a < 0xd800 || a > 0xdfff
196+ then cont (fromChar (chr a))
197+ else do
198+ b <- Z.string "\\u" *> hexQuad
199+ if a <= 0xdbff && b >= 0xdc00 && b <= 0xdfff
200+ then let !c = ((a - 0xd800) `shiftL` 10) +
201+ (b - 0xdc00) + 0x10000
202+ in cont (fromChar (chr c))
203+ else fail "invalid UTF-16 surrogates"
204+ done <- Z.atEnd
205+ if done
206+ then return (acc `mappend` fromByteString h)
207+ else rest
208+ mapping = "\"\\/\n\t\b\r\f"
209+
210+hexQuad :: Z.Parser Int
211+hexQuad = do
212+ s <- Z.take 4
213+ let hex n | w >= 48 && w <= 57 = w - 48
214+ | w >= 97 && w <= 122 = w - 87
215+ | w >= 65 && w <= 90 = w - 55
216+ | otherwise = 255
217+ where w = fromIntegral $ B.unsafeIndex s n
218+ a = hex 0; b = hex 1; c = hex 2; d = hex 3
219+ if (a .|. b .|. c .|. d) /= 255
220+ then return $! d .|. (c `shiftL` 4) .|. (b `shiftL` 8) .|. (a `shiftL` 12)
221+ else fail "invalid hex escape"
222+
223+decodeWith :: Parser Value -> (Value -> Result a) -> L.ByteString -> Maybe a
224+decodeWith p to s =
225+ case L.parse p s of
226+ L.Done _ v -> case to v of
227+ Success a -> Just a
228+ _ -> Nothing
229+ _ -> Nothing
230+{-# INLINE decodeWith #-}
231+
232+-- $lazy
233+--
234+-- The 'json' and 'value' parsers decouple identification from
235+-- conversion. Identification occurs immediately (so that an invalid
236+-- JSON document can be rejected as early as possible), but conversion
237+-- to a Haskell value is deferred until that value is needed.
238+--
239+-- This decoupling can be time-efficient if only a smallish subset of
240+-- elements in a JSON value need to be inspected, since the cost of
241+-- conversion is zero for uninspected elements. The trade off is an
242+-- increase in memory usage, due to allocation of thunks for values
243+-- that have not yet been converted.
244+
245+-- $strict
246+--
247+-- The 'json'' and 'value'' parsers combine identification with
248+-- conversion. They consume more CPU cycles up front, but have a
249+-- smaller memory footprint.
aeson.cabal+1
@@ -120,6 +120,7 @@ library
120120
121121 other-modules:
122122 Data.Aeson.Functions
123+ Data.Aeson.Parser.Internal
123124 Data.Aeson.Types.Class
124125 Data.Aeson.Types.Internal
125126
benchmarks/bench-parse.py+6−1
@@ -4,9 +4,14 @@ import os, re, subprocess, sys
44
55result_re = re.compile(r'^\s*(\d+) good, (\d+\.\d+)s$', re.M)
66
7+if len(sys.argv) > 1:
8+ parser_exe = sys.argv[1]
9+else:
10+ parser_exe = './AesonParse'
11+
712def run(count, filename):
813 print ' %s :: %s times' % (filename, count)
9− p = subprocess.Popen(['./AesonParse', str(count), filename],
14+ p = subprocess.Popen([parser_exe, str(count), filename],
1015 stdout=subprocess.PIPE)
1116 output = p.stdout.read()
1217 p.wait()
release-notes.markdown+18
@@ -48,3 +48,21 @@ how to use SYB generics, refer to
4848
4949* Instances of `ToJSON` and `FromJSON` for tuples are between 45% and
5050 70% faster than in 0.3.
51+
52+
53+## Evaluation control
54+
55+This version of aeson makes explicit the decoupling between
56+*identifying* an element of a JSON document and *converting* it to
57+Haskell. See the
58+[`Data.Aeson.Parser`](http://hackage.haskell.org/packages/archive/aeson/latest/doc/html/Data-Aeson-Parser.html)
59+documentation for details.
60+
61+The normal aeson `decode` function performs identification strictly,
62+but defers conversion until needed. This can result in improved
63+performance (e.g. if the results of some conversions are never
64+needed), but at a cost in increased memory consumption.
65+
66+The new `decode'` function performs identification and conversion
67+immediately. This incurs an up-front cost in CPU cycles, but reduces
68+reduce memory consumption.