| 1 |
{-# LANGUAGE RecordWildCards #-}
|
| 2 |
{-# LANGUAGE ViewPatterns #-}
|
| 3 |
{-# LANGUAGE OverloadedStrings #-}
|
| 4 |
|
1 | 5 |
module Data.SCargot.General
|
2 | 6 |
( -- * SExprSpec
|
3 | 7 |
SExprSpec
|
4 | 8 |
, mkSpec
|
5 | 9 |
, convertSpec
|
6 | 10 |
, addReader
|
7 | |
, addCommentType
|
| 11 |
, addComment
|
| 12 |
-- * Specific SExprSpec Conversions
|
8 | 13 |
, asRich
|
9 | 14 |
, asWellFormed
|
10 | |
-- * A Few Standard Reader Macros
|
11 | |
, quote
|
12 | |
, vector
|
| 15 |
, withSemicolonComments
|
| 16 |
, withQuote
|
13 | 17 |
-- * Using a SExprSpec
|
14 | |
, parseSExpr
|
15 | |
, serializeSExpr
|
| 18 |
, decode
|
| 19 |
, decodeOne
|
| 20 |
, encode
|
| 21 |
-- * Useful Type Aliases
|
| 22 |
, Reader
|
| 23 |
, Comment
|
| 24 |
, Serializer
|
16 | 25 |
) where
|
17 | 26 |
|
18 | |
import Control.Applicative
|
| 27 |
import Control.Applicative ((<*))
|
| 28 |
import Control.Monad ((>=>))
|
19 | 29 |
import Data.Attoparsec.Text
|
20 | |
import Data.Map.String (Map)
|
21 | |
import qualified Data.Map.String as M
|
| 30 |
import Data.Char (isAlpha)
|
| 31 |
import Data.Map.Strict (Map)
|
| 32 |
import qualified Data.Map.Strict as M
|
| 33 |
import Data.Text (Text)
|
| 34 |
|
| 35 |
import Prelude hiding (takeWhile)
|
22 | 36 |
|
23 | 37 |
import Data.SCargot.Repr
|
24 | 38 |
|
25 | 39 |
type ReaderMacroMap atom = Map Char (Reader atom)
|
26 | |
type CommentMap = Map Char (Parser ())
|
| 40 |
type CommentMap = Map Char Comment
|
| 41 |
|
| 42 |
-- | A 'Reader' represents a reader macro: it takes a parser for
|
| 43 |
-- the S-Expression type and performs as much or as little
|
| 44 |
-- parsing as it would like, and then returns an S-expression.
|
27 | 45 |
type Reader atom = (Parser (SExpr atom) -> Parser (SExpr atom))
|
| 46 |
|
| 47 |
-- | A 'Comment' represents any kind of skippable comment.
|
| 48 |
type Comment = Parser ()
|
| 49 |
|
| 50 |
-- | A 'Serializer' is any function which can serialize an Atom
|
| 51 |
-- to 'Text'.
|
28 | 52 |
type Serializer atom = atom -> Text
|
29 | 53 |
|
30 | 54 |
-- | A 'SExprSpec' describes a parser and emitter for a particular
|
|
37 | 61 |
{ sesPAtom :: Parser atom
|
38 | 62 |
, sesSAtom :: Serializer atom
|
39 | 63 |
, readerMap :: ReaderMacroMap atom
|
40 | |
, commentMap :: CommentMap
|
| 64 |
, comment :: Comment
|
41 | 65 |
, postparse :: SExpr atom -> Either String carrier
|
42 | 66 |
, preserial :: carrier -> SExpr atom
|
43 | 67 |
}
|
44 | 68 |
|
45 | |
-- | This creates a basic 'SExprSpec' when given a parser and serializer
|
| 69 |
-- | Create a basic 'SExprSpec' when given a parser and serializer
|
46 | 70 |
-- for an atom type.
|
47 | 71 |
mkSpec :: Parser atom -> Serializer atom -> SExprSpec atom (SExpr atom)
|
48 | 72 |
mkSpec p s = SExprSpec
|
49 | |
{ sesPAtom = p
|
50 | |
, sesSAtom = s
|
51 | |
, rmMap = M.empty
|
52 | |
, postparse = return
|
53 | |
, preserial = id
|
| 73 |
{ sesPAtom = p
|
| 74 |
, sesSAtom = s
|
| 75 |
, readerMap = M.empty
|
| 76 |
, commentMap = skipSpace
|
| 77 |
, postparse = return
|
| 78 |
, preserial = id
|
54 | 79 |
}
|
55 | 80 |
|
56 | |
-- | This is used to modify the carrier type for a 'SExprSpec'. This is
|
| 81 |
-- | Modify the carrier type for a 'SExprSpec'. This is
|
57 | 82 |
-- used internally to convert between various 'SExpr' representations,
|
58 | 83 |
-- but could also be used externally to add an extra conversion layer
|
59 | 84 |
-- onto a 'SExprSpec', e.g. for a custom Lisp-like language:
|
|
64 | 89 |
convertSpec :: (b -> Either String c) -> (c -> b) -> SExprSpec a b -> SExprSpec a c
|
65 | 90 |
convertSpec f g spec = spec
|
66 | 91 |
{ postparse = postparse spec >=> f
|
67 | |
, preserial = g . preserial spec
|
| 92 |
, preserial = preserial spec . g
|
68 | 93 |
}
|
69 | 94 |
|
70 | |
addReader :: Char -> Reader a -> SExprSpec a c -> SExprSpec a c
|
71 | |
addReader c reader spec = spec { rmMap = insert c reader (rmMap spec) }
|
72 | |
|
73 | |
addCommentType :: Char -> Comment -> SExprSpec a c -> SExprSpec a c
|
74 | |
addCommentType c comment spec = spec { }
|
75 | |
|
76 | |
quote :: atom -> Reader atom
|
77 | |
quote q parse = go <$> parse
|
78 | |
where go v = SCons q (SCons v SNil)
|
79 | |
|
| 95 |
-- | Convert the final output representation from the 'SExpr' type
|
| 96 |
-- to the 'RichSExpr' type.
|
80 | 97 |
asRich :: SExprSpec a (SExpr b) -> SExprSpec a (RichSExpr b)
|
81 | 98 |
asRich = convertSpec (return . toRich) fromRich
|
82 | 99 |
|
| 100 |
-- | Convert the final output representation from the 'SExpr' type
|
| 101 |
-- to the 'WellFormedSExpr' type.
|
83 | 102 |
asWellFormed :: SExprSpec a (SExpr b) -> SExprSpec a (WellFormedSExpr b)
|
84 | 103 |
asWellFormed = convertSpec toWellFormed fromWellFormed
|
85 | 104 |
|
86 | |
parseGenericSExpr :: Parser atom -> ReaderMacroMap atom -> CommentMap -> Parser (SExpr atom)
|
87 | |
parseGenericSExpr atom reader comment =
|
88 | |
char '(' *>
|
89 | |
|
90 | |
-- |
|
91 | |
parseSExpr :: SExprSpec atom carrier -> Text -> Either String carrier
|
92 | |
parseSExpr spec = undefined
|
93 | |
|
94 | |
-- | blah
|
95 | |
serializeSExpr :: SExprSpec atom carrier -> carrier -> Text
|
96 | |
serializeSExpr spec = serializeGenericSExpr ses . preserial
|
| 105 |
-- | Add the ability to execute some particular reader macro, as
|
| 106 |
-- defined by its initial character and the 'Parser' which returns
|
| 107 |
-- the parsed S-Expression. The 'Reader' is passed a 'Parser' which
|
| 108 |
-- can be recursively called to parse more S-Expressions, and begins
|
| 109 |
-- parsing after the reader character has been removed from the
|
| 110 |
-- stream.
|
| 111 |
addReader :: Char -> Reader a -> SExprSpec a c -> SExprSpec a c
|
| 112 |
addReader c reader spec = spec
|
| 113 |
{ readerMap = M.insert c reader (readerMap spec) }
|
| 114 |
|
| 115 |
-- | Add the ability to ignore some kind of comment. If the comment
|
| 116 |
-- parser overlaps with a reader macro or the atom parser, then the
|
| 117 |
-- former will be tried first.
|
| 118 |
setComment :: Comment -> SExprSpec a c -> SExprSpec a c
|
| 119 |
setComment c spec = spec { comment = c }
|
| 120 |
|
| 121 |
-- | Add the ability to skip line comments beginning with a semicolon.
|
| 122 |
withSemicolonComments :: SExprSpec a c -> SExprSpec a c
|
| 123 |
withSemicolonComments = addComment ';' (skipWhile (\ c -> c /= '\n'))
|
| 124 |
|
| 125 |
-- | Add the ability to understand a quoted S-Expression. In general,
|
| 126 |
-- many Lisps use @'sexpr@ as sugar for @(quote sexpr)@. This is
|
| 127 |
-- a convenience function which allows you to easily add quoted
|
| 128 |
-- expressions to a 'SExprSpec', provided that you supply which
|
| 129 |
-- atom you want substituted in for the symbol @quote@.
|
| 130 |
withQuote :: a -> SExprSpec a (SExpr a) -> SExprSpec a (SExpr a)
|
| 131 |
withQuote q = addReader '\'' prs
|
| 132 |
where prs p = go `fmap` p
|
| 133 |
go s = SCons (SAtom q) (SCons s SNil)
|
| 134 |
|
| 135 |
parseGenericSExpr ::
|
| 136 |
Parser atom -> ReaderMacroMap atom -> Parser () -> Parser (SExpr atom)
|
| 137 |
parseGenericSExpr atom reader skip = do
|
| 138 |
let sExpr = parseGenericSExpr atom reader skip
|
| 139 |
skip
|
| 140 |
c <- peekChar
|
| 141 |
r <- case c of
|
| 142 |
Nothing -> fail "Unexpected end of input"
|
| 143 |
Just '(' -> char '(' >> skip >> parseList sExpr skip
|
| 144 |
Just (flip M.lookup reader -> Just r) -> anyChar >> r sExpr
|
| 145 |
_ -> SAtom `fmap` atom
|
| 146 |
skip
|
| 147 |
return r
|
| 148 |
|
| 149 |
parseList :: Parser (SExpr atom) -> Parser () -> Parser (SExpr atom)
|
| 150 |
parseList sExpr skip = do
|
| 151 |
i <- peekChar
|
| 152 |
case i of
|
| 153 |
Nothing -> fail "Unexpected end of input"
|
| 154 |
Just ')' -> char ')' >> return SNil
|
| 155 |
_ -> do
|
| 156 |
car <- sExpr
|
| 157 |
skip
|
| 158 |
c <- peekChar
|
| 159 |
case c of
|
| 160 |
Just '.' -> do
|
| 161 |
char '.'
|
| 162 |
cdr <- sExpr
|
| 163 |
skip
|
| 164 |
char ')'
|
| 165 |
skip
|
| 166 |
return (SCons car cdr)
|
| 167 |
Just ')' -> do
|
| 168 |
char ')'
|
| 169 |
skip
|
| 170 |
return (SCons car SNil)
|
| 171 |
_ -> do
|
| 172 |
cdr <- parseList sExpr skip
|
| 173 |
return (SCons car cdr)
|
| 174 |
|
| 175 |
-- | Given a CommentMap, create the corresponding parser to
|
| 176 |
-- skip those comments (if they exist).
|
| 177 |
buildSkip :: CommentMap -> Parser ()
|
| 178 |
buildSkip m = skipSpace >> comments >> skipSpace
|
| 179 |
where comments = do
|
| 180 |
c <- peekChar
|
| 181 |
case c of
|
| 182 |
Nothing -> return ()
|
| 183 |
Just c' -> case M.lookup c' m of
|
| 184 |
Just p -> anyChar >> p
|
| 185 |
Nothing -> return ()
|
| 186 |
|
| 187 |
(#) :: a -> (a -> b) -> b
|
| 188 |
(#) = flip ($)
|
| 189 |
|
| 190 |
testSpec :: SExprSpec Text (SExpr Text)
|
| 191 |
testSpec = mkSpec (takeWhile1 isAlpha) id
|
| 192 |
# withQuote "quote"
|
| 193 |
# addReader '#' (\ p -> SCons (SAtom "vector") `fmap` p)
|
| 194 |
|
| 195 |
-- | Decode a single S-expression. If any trailing input is left after
|
| 196 |
-- the S-expression (ignoring comments or whitespace) then this
|
| 197 |
-- will fail: for those cases, use 'decode', which returns a list of
|
| 198 |
-- all the S-expressions found at the top level.
|
| 199 |
decodeOne :: SExprSpec atom carrier -> Text -> Either String carrier
|
| 200 |
decodeOne SExprSpec { .. } = parseOnly (parser <* endOfInput) >=> postparse
|
| 201 |
where parser = parseGenericSExpr sesPAtom readerMap (buildSkip commentMap)
|
| 202 |
|
| 203 |
-- | Decode several S-expressions according to a given 'SExprSpec'. This
|
| 204 |
-- will return a list of every S-expression that appears at the top-level
|
| 205 |
-- of the document.
|
| 206 |
decode :: SExprSpec atom carrier -> Text -> Either String [carrier]
|
| 207 |
decode SExprSpec { .. } =
|
| 208 |
parseOnly (many1 parser <* endOfInput) >=> mapM postparse
|
| 209 |
where parser = parseGenericSExpr sesPAtom readerMap (buildSkip commentMap)
|
| 210 |
|
| 211 |
-- | Emit an S-Expression in a machine-readable way. This
|
| 212 |
encode :: SExprSpec atom carrier -> carrier -> Text
|
| 213 |
encode SExprSpec { .. } = undefined
|