-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathApplicativeParser.hs
More file actions
113 lines (98 loc) · 3.14 KB
/
ApplicativeParser.hs
File metadata and controls
113 lines (98 loc) · 3.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
-- | Demonstration of static analysis with an Applicative parser.
--
-- + This is a toy example showing tracking of keywords.
module Scratch.ApplicativeParser where
import qualified Data.Set as Set
import qualified Data.Text as Text
import Scratch.Prelude
import Test.Hspec
data Parser a = Parser
{ keywords :: Set Keyword,
runParserWithKeywords :: Set Keyword -> Text -> Maybe (Text, a)
}
newtype Keyword
= Keyword Text
deriving (Eq, Ord, Show)
runParser ::
Parser a ->
-- | Input
Text ->
-- | Unconsumed input and result
Maybe (Text, a)
runParser Parser {keywords, runParserWithKeywords} =
runParserWithKeywords keywords
instance Functor Parser where
fmap :: (a -> b) -> Parser a -> Parser b
fmap f p@(Parser _ runP) =
p {runParserWithKeywords = (fmap . fmap . fmap) f . runP}
lift2Parser :: forall a b c. (a -> b -> c) -> Parser a -> Parser b -> Parser c
lift2Parser f (Parser k1 p1) (Parser k2 p2) =
Parser
{ keywords = k1 <> k2,
runParserWithKeywords = runP
}
where
runP :: Set Keyword -> Text -> Maybe (Text, c)
runP finalKeywords input = do
(remaining, a) <- p1 finalKeywords input
(remaining2, b) <- p2 finalKeywords remaining
Just (remaining2, f a b)
instance Applicative Parser where
pure :: a -> Parser a
pure a =
Parser
{ keywords = mempty,
runParserWithKeywords = \_ input -> Just (input, a)
}
liftA2 = lift2Parser
-- If we wanted to make this module really useful
-- we'd also write an Alternative instance,
-- which we could do and still keep static analysis (unlike Monad).
parseKeyword :: Text -> Parser ()
parseKeyword keyword =
Parser
{ keywords = Set.singleton (Keyword keyword),
runParserWithKeywords = \_ input -> do
remaining <- Text.stripPrefix keyword input
Just (Text.dropWhile (== ' ') remaining, ())
}
-- * Example use
data EqualityEquation
= EqualityEquation Text Text
deriving (Eq, Ord, Show)
parseVariable :: Parser Text
parseVariable =
Parser
{ keywords = mempty,
runParserWithKeywords = runP
}
where
-- If we want to forbid the keywords of our language
-- from being used as variables, normally we'd have
-- to maintain a list of them, and keep it in sync
-- with the parser code.
--
-- But here we pull it out of THIN. AIR.
runP :: Set Keyword -> Text -> Maybe (Text, Text)
runP finalKeywords input = do
let (candidateVar, remaining) = Text.span (/= ' ') input
guard (not (Text.null candidateVar))
if Set.member (Keyword candidateVar) finalKeywords
then Nothing
else Just (Text.dropWhile (== ' ') remaining, candidateVar)
exampleParser :: Parser EqualityEquation
exampleParser =
(\() v1 () v2 -> EqualityEquation v1 v2)
<$> parseKeyword "assert"
<*> parseVariable
<*> parseKeyword "=="
<*> parseVariable
spec :: Spec
spec =
describe "applicative parser" $ do
it "fails if a keyword is used as a variable" $ do
runParser exampleParser "assert assert == b"
`shouldBe` Nothing
it "succeeds" $ do
runParser exampleParser "assert a == b"
`shouldBe` Just ("", EqualityEquation "a" "b")