on master help sign in
Added Lexer and Parser libraries to contrib

These are libraries to support writing total lexers and parsers. They are not yet very sophisticated (in that they only have the basic combinators) but they do work.

commit180fc75336577d329d7e03a05d81f9d74c8e6000 authorEdwin Brady <ecb10@st-andrews.ac.uk> dateMon 19 Jun 2017, 12:33 (9 years ago) parent8897f64
4 files changed+475
CHANGELOG.md+6 libs/contrib/Text/Lexer.idr+238 libs/contrib/Text/Parser.idr+229 libs/contrib/contrib.ipkg+2
CHANGELOG.md+6
@@ -1,3 +1,9 @@
1+# New in 1.0.1
2+
3+## Library Updates
4+
5++ Added `Text.Lexer` and `Text.Parser` to `contrib`. These are small libraries
6+ for implementing total lexical analysers and parsers.
17
28## Tool Updates
39
libs/contrib/Text/Lexer.idrnew file+238
@@ -0,0 +1,238 @@
1+module Text.Lexer
2+
3+%default total
4+
5+||| A language of token recognisers.
6+||| The `consumes` flag is True is the recogniser is guaranteed to consume
7+||| at least one character
8+export
9+data Recognise : (consumes : Bool) -> Type where
10+ Empty : Recognise False
11+ Fail : Recognise c
12+ Pred : (Char -> Bool) -> Recognise True
13+ SeqEat : Recognise True -> Inf (Recognise e) -> Recognise True
14+ SeqEmpty : Recognise e1 -> Recognise e2 -> Recognise (e1 || e2)
15+ Alt : Recognise e1 -> Recognise e2 -> Recognise (e1 && e2)
16+
17+||| A token recogniser. Guaranteed to consume at least one character.
18+public export
19+Lexer : Type
20+Lexer = Recognise True
21+
22+public export
23+inf : Bool -> Type -> Type
24+inf True t = Inf t
25+inf False t = t
26+
27+||| Sequence two recognisers. If either consumes a character, the sequence
28+||| is guaranteed to consume a character.
29+export %inline
30+(<+>) : {c1 : Bool} ->
31+ Recognise c1 -> inf c1 (Recognise c2) -> Recognise (c1 || c2)
32+(<+>) {c1 = False} = SeqEmpty
33+(<+>) {c1 = True} = SeqEat
34+
35+||| Alternative recognisers. If both consume, the combination is guaranteed
36+||| to consumer a character.
37+export
38+(<|>) : Recognise c1 -> Recognise c2 -> Recognise (c1 && c2)
39+(<|>) = Alt
40+
41+||| Recognise a specific character
42+export
43+is : Char -> Lexer
44+is x = Pred (==x)
45+
46+||| Recognise anything but the given character
47+export
48+isNot : Char -> Lexer
49+isNot x = Pred (/=x)
50+
51+||| Recognise a sequence of at least one sub-lexers
52+export
53+some : Lexer -> Lexer
54+some l = l <+> (some l <|> Empty)
55+
56+||| Recognise a sequence of at zero or more sub-lexers. This is not
57+||| guaranteed to consume input
58+export
59+many : Lexer -> Recognise False
60+many l = some l <|> Empty
61+
62+||| Recognise any character
63+export
64+any : Lexer
65+any = Pred (const True)
66+
67+||| Recognise no input (doesn't consume any input)
68+export
69+empty : Recognise False
70+empty = Empty
71+
72+||| Recognise a character that matches a predicate
73+export
74+pred : (Char -> Bool) -> Lexer
75+pred = Pred
76+
77+||| Recognise any of the characters in the given string
78+export
79+oneOf : String -> Lexer
80+oneOf cs = pred (\x => x `elem` unpack cs)
81+
82+data StrLen : Type where
83+ MkStrLen : String -> Nat -> StrLen
84+
85+getString : StrLen -> String
86+getString (MkStrLen str n) = str
87+
88+strIndex : StrLen -> Nat -> Maybe Char
89+strIndex (MkStrLen str len) i
90+ = if i >= len then Nothing
91+ else Just (assert_total (prim__strIndex str (cast i)))
92+
93+mkStr : String -> StrLen
94+mkStr str = MkStrLen str (length str)
95+
96+strTail : Nat -> StrLen -> StrLen
97+strTail start (MkStrLen str len)
98+ = MkStrLen (substr start len str) (minus len start)
99+
100+-- If the string is recognised, returns the index at which the token
101+-- ends
102+scan : Recognise c -> Nat -> StrLen -> Maybe Nat
103+scan Empty idx str = pure idx
104+scan Fail idx str = Nothing
105+scan (Pred f) idx str
106+ = do c <- strIndex str idx
107+ if f c
108+ then Just (idx + 1)
109+ else Nothing
110+scan (SeqEat r1 r2) idx str
111+ = do idx' <- scan r1 idx str
112+ -- TODO: Can we prove totality instead by showing idx has increased?
113+ assert_total (scan r2 idx' str)
114+scan (SeqEmpty r1 r2) idx str
115+ = do idx' <- scan r1 idx str
116+ scan r2 idx' str
117+scan (Alt r1 r2) idx str
118+ = case scan r1 idx str of
119+ Nothing => scan r2 idx str
120+ Just idx => Just idx
121+
122+takeToken : Lexer -> StrLen -> Maybe (String, StrLen)
123+takeToken lex str
124+ = do i <- scan lex 0 str -- i must be > 0 if successful
125+ pure (substr 0 i (getString str), strTail i str)
126+
127+||| Recognise a digit 0-9
128+export
129+digits : Lexer
130+digits = some (Pred isDigit)
131+
132+||| Recognise a specific string
133+export
134+exact : String -> Lexer
135+exact str with (unpack str)
136+ exact str | [] = Fail -- Not allowed, Lexer has to consume
137+ exact str | (x :: xs)
138+ = foldl SeqEmpty (is x) (map is xs)
139+
140+||| Recognise a whitespace character
141+export
142+space : Lexer
143+space = some (pred isSpace)
144+
145+||| Recognise a non-alphanumeric, non-whitespace character
146+export
147+symbol : Lexer
148+symbol = some (pred (\x => not (isAlphaNum x) && not (isSpace x)))
149+
150+strChar : Lexer
151+strChar = (is '\\' <+> any) <|> isNot '"'
152+
153+||| Recognise a string literal, including escaped characters.
154+||| (Note: doesn't yet handle escape sequences such as \123)
155+export
156+stringLit : Lexer
157+stringLit = is '"' <+> many strChar <+> is '"'
158+
159+||| Recognise a character literal, including escaped characters.
160+||| (Note: doesn't yet handle escape sequences such as \123)
161+export
162+charLit : Lexer
163+charLit = is '\'' <+> strChar <+> is '\''
164+
165+||| Recognise an integer literal (possibly with a '-' prefix)
166+export
167+intLit : Lexer
168+intLit = (is '-' <|> Empty) <+> digits
169+
170+||| A mapping from lexers to the tokens they produce.
171+||| This is a list of pairs `(Lexer, String -> tokenType)`
172+||| For each Lexer in the list, if a substring in the input matches, run
173+||| the associated function to produce a token of type `tokenType`
174+public export
175+TokenMap : (tokenType : Type) -> Type
176+TokenMap tokenType = List (Lexer, String -> tokenType)
177+
178+||| A token, and the line and column where it was in the input
179+public export
180+record TokenData a where
181+ constructor MkToken
182+ line : Int
183+ col : Int
184+ tok : a
185+
186+fspanEnd : Nat -> (Char -> Bool) -> String -> (Nat, String)
187+fspanEnd k p "" = (k, "")
188+fspanEnd k p xxs
189+ = assert_total $
190+ let x = prim__strHead xxs
191+ xs = prim__strTail xxs in
192+ if p x then fspanEnd (S k) p xs
193+ else (k, xxs)
194+
195+-- Faster version of 'span' from the prelude (avoids unpacking)
196+export
197+fspan : (Char -> Bool) -> String -> (String, String)
198+fspan p xs
199+ = let (end, rest) = fspanEnd 0 p xs in
200+ (substr 0 end xs, rest)
201+
202+tokenise : (line : Int) -> (col : Int) ->
203+ List (TokenData a) -> TokenMap a ->
204+ StrLen -> (List (TokenData a), (Int, Int, StrLen))
205+tokenise line col acc tmap str
206+ = case getFirstToken tmap str of
207+ Just (tok, line', col', rest) =>
208+ -- assert total because getFirstToken must consume something
209+ assert_total (tokenise line' col' (tok :: acc) tmap rest)
210+ Nothing => (reverse acc, (line, col, str))
211+ where
212+ countNLs : List Char -> Nat
213+ countNLs str = List.length (filter (== '\n') str)
214+
215+ getCols : String -> Int -> Int
216+ getCols x c
217+ = case fspan (/= '\n') (reverse x) of
218+ (incol, "") => c + cast (length incol)
219+ (incol, _) => cast (length incol)
220+
221+ getFirstToken : TokenMap a -> StrLen -> Maybe (TokenData a, Int, Int, StrLen)
222+ getFirstToken [] str = Nothing
223+ getFirstToken ((lex, fn) :: ts) str
224+ = case takeToken lex str of
225+ Just (tok, rest) => Just (MkToken line col (fn tok),
226+ line + cast (countNLs (unpack tok)),
227+ getCols tok col, rest)
228+ Nothing => getFirstToken ts str
229+
230+||| Given a mapping from lexers to token generating functions (the
231+||| TokenMap a) and an input string, return a list of recognised tokens,
232+||| and the line, column, and remainder of the input at the first point in the
233+||| string where there are no recognised tokens.
234+export
235+lex : TokenMap a -> String -> (List (TokenData a), (Int, Int, String))
236+lex tmap str = let (ts, (l, c, str')) = tokenise 0 0 [] tmap (mkStr str) in
237+ (ts, (l, c, getString str'))
238+
libs/contrib/Text/Parser.idrnew file+229
@@ -0,0 +1,229 @@
1+module Text.Parser
2+
3+%default total
4+
5+-- TODO: Add some primitives for helping with error messages.
6+-- e.g. perhaps set a string to state what we're currently trying to
7+-- parse, or to say what the next expected token is in words
8+
9+||| Description of a language's grammar. The `tok` parameter is the type
10+||| of tokens, and the `consumes` flag is True if the language is guaranteed
11+||| to be non-empty - that is, successfully parsing the language is guaranteed
12+||| to consume some input.
13+export
14+data Grammar : (tok : Type) -> (consumes : Bool) -> Type -> Type where
15+ Empty : (val : ty) -> Grammar tok False ty
16+ Terminal : (tok -> Maybe a) -> Grammar tok True a
17+ NextIs : (tok -> Bool) -> Grammar tok False tok
18+ EOF : Grammar tok False ()
19+
20+ Fail : String -> Grammar tok c ty
21+ Commit : Grammar tok False ()
22+
23+ SeqEat : Grammar tok True a -> Inf (a -> Grammar tok c2 b) ->
24+ Grammar tok True b
25+ SeqEmpty : {c1, c2 : Bool} ->
26+ Grammar tok c1 a -> (a -> Grammar tok c2 b) ->
27+ Grammar tok (c1 || c2) b
28+ Alt : {c1, c2 : Bool} ->
29+ Grammar tok c1 ty -> Grammar tok c2 ty ->
30+ Grammar tok (c1 && c2) ty
31+
32+public export
33+inf : Bool -> Type -> Type
34+inf True t = Inf t
35+inf False t = t
36+
37+||| Sequence two grammars. If either consumes some input, the sequence is
38+||| guaranteed to consume some input. If the first one consumes input, the
39+||| second is allowed to be recursive (because it means some input has been
40+||| consumed and therefore the input is smaller)
41+export %inline
42+(>>=) : {c1 : Bool} ->
43+ Grammar tok c1 a -> inf c1 (a -> Grammar tok c2 b) ->
44+ Grammar tok (c1 || c2) b
45+(>>=) {c1 = False} = SeqEmpty
46+(>>=) {c1 = True} = SeqEat
47+
48+||| Give two alternative grammars. If both consume, the combination is
49+||| guaranteed to consume.
50+export
51+(<|>) : Grammar tok c1 ty -> Grammar tok c2 ty ->
52+ Grammar tok (c1 && c2) ty
53+(<|>) = Alt
54+
55+export
56+pure : (val : ty) -> Grammar tok False ty
57+pure = Empty
58+
59+||| Check whether the next token satisfies a predicate
60+export
61+nextIs : (tok -> Bool) -> Grammar tok False tok
62+nextIs = NextIs
63+
64+||| Look at the next token in the input
65+export
66+peek : Grammar tok False tok
67+peek = nextIs (const True)
68+
69+||| Succeeds if running the predicate on the next token returns Just x,
70+||| returning x. Otherwise fails.
71+export
72+terminal : (tok -> Maybe a) -> Grammar tok True a
73+terminal = Terminal
74+
75+||| Always fail with a message
76+export
77+fail : String -> Grammar tok c ty
78+fail = Fail
79+
80+||| Succeed if the input is empty
81+export
82+eof : Grammar tok False ()
83+eof = EOF
84+
85+||| Commit to an alternative; if the current branch of an alternative
86+||| fails to parse, no more branches will be tried
87+export
88+commit : Grammar tok False ()
89+commit = Commit
90+
91+data ParseResult : List tok -> (consumes : Bool) -> Type -> Type where
92+ Failure : {xs : List tok} ->
93+ (committed : Bool) ->
94+ (err : String) -> (rest : List tok) -> ParseResult xs c ty
95+ EmptyRes : (committed : Bool) ->
96+ (val : ty) -> (more : List tok) -> ParseResult more False ty
97+ NonEmptyRes : (committed : Bool) ->
98+ (val : ty) -> (more : List tok) ->
99+ ParseResult (x :: xs ++ more) c ty
100+
101+weakenRes : {whatever, c : Bool} -> {xs : List tok} ->
102+ ParseResult xs c ty -> ParseResult xs (whatever && c) ty
103+weakenRes (Failure com msg ts) = Failure com msg ts
104+weakenRes {whatever=True} (EmptyRes com val xs) = EmptyRes com val xs
105+weakenRes {whatever=False} (EmptyRes com val xs) = EmptyRes com val xs
106+weakenRes (NonEmptyRes com val more) = NonEmptyRes com val more
107+
108+shorter : (more : List tok) -> .(ys : List tok) ->
109+ LTE (S (length more)) (S (length (ys ++ more)))
110+shorter more [] = lteRefl
111+shorter more (x :: xs) = LTESucc (lteSuccLeft (shorter more xs))
112+
113+doParse : {c : Bool} ->
114+ (commit : Bool) -> (xs : List tok) -> (act : Grammar tok c ty) ->
115+ ParseResult xs c ty
116+doParse com xs act with (smallerAcc xs)
117+ doParse com xs (Empty val) | sml = EmptyRes com val xs
118+ doParse com [] (Fail str) | sml = Failure com str []
119+ doParse com (x :: xs) (Fail str) | sml = Failure com str (x :: xs)
120+ doParse com xs Commit | sml = EmptyRes True () xs
121+
122+ doParse com [] (Terminal f) | sml = Failure com "End of input" []
123+ doParse com (x :: xs) (Terminal f) | sml
124+ = maybe
125+ (Failure com "Unrecognised token" (x :: xs))
126+ (\a => NonEmptyRes com {xs=[]} a xs)
127+ (f x)
128+ doParse com [] EOF | sml = EmptyRes com () []
129+ doParse com (x :: xs) EOF | sml
130+ = Failure com "Expected end of input" (x :: xs)
131+ doParse com [] (NextIs f) | sml = Failure com "End of input" []
132+ doParse com (x :: xs) (NextIs f) | sml
133+ = if f x
134+ then EmptyRes com x (x :: xs)
135+ else Failure com "Unrecognised token" (x :: xs)
136+ doParse com xs (Alt x y) | sml with (doParse False xs x | sml)
137+ doParse com xs (Alt x y) | sml | Failure com' msg ts
138+ = if com' -- If the alternative had committed, don't try the
139+ -- other branch (and reset commit flag)
140+ then Failure com msg ts
141+ else weakenRes (doParse False xs y | sml)
142+ -- Successfully parsed the first option, so use the outer commit flag
143+ doParse com xs (Alt x y) | sml | (EmptyRes _ val xs)
144+ = EmptyRes com val xs
145+ doParse com (z :: (ys ++ more)) (Alt x y) | sml | (NonEmptyRes _ val more)
146+ = NonEmptyRes com val more
147+ doParse com xs (SeqEmpty act next) | (Access morerec)
148+ = case doParse com xs act | Access morerec of
149+ Failure com msg ts => Failure com msg ts
150+ EmptyRes com val xs =>
151+ case doParse com xs (next val) | (Access morerec) of
152+ Failure com' msg ts => Failure com' msg ts
153+ EmptyRes com' val xs => EmptyRes com' val xs
154+ NonEmptyRes com' val more => NonEmptyRes com' val more
155+ NonEmptyRes {x} {xs=ys} com val more =>
156+ case (doParse com more (next val) | morerec _ (shorter more ys)) of
157+ Failure com' msg ts => Failure com' msg ts
158+ EmptyRes com' val _ => NonEmptyRes com' val more
159+ NonEmptyRes {x=x1} {xs=xs1} com' val more' =>
160+ rewrite appendAssociative (x :: ys) (x1 :: xs1) more' in
161+ NonEmptyRes com' val more'
162+ doParse com xs (SeqEat act next) | sml with (doParse com xs act | sml)
163+ doParse com xs (SeqEat act next) | sml | Failure com' msg ts
164+ = Failure com' msg ts
165+ doParse com (x :: (ys ++ more)) (SeqEat act next) | (Access morerec) | (NonEmptyRes com' val more)
166+ = case doParse com' more (next val) | morerec _ (shorter more ys) of
167+ Failure com' msg ts => Failure com' msg ts
168+ EmptyRes com' val _ => NonEmptyRes com' val more
169+ NonEmptyRes {x=x1} {xs=xs1} com' val more' =>
170+ rewrite appendAssociative (x :: ys) (x1 :: xs1) more' in
171+ NonEmptyRes com' val more'
172+ -- This next line is not strictly necessary, but it stops the coverage
173+ -- checker taking a really long time and eating lots of memory...
174+ doParse _ _ _ | sml = Failure True "Help the coverage checker!" []
175+
176+public export
177+data ParseError tok = Error String (List tok)
178+
179+||| Parse a list of tokens according to the given grammar. If successful,
180+||| returns a pair of the parse result and the unparsed tokens (the remaining
181+||| input).
182+export
183+parse : (xs : List tok) -> (act : Grammar tok c ty) ->
184+ Either (ParseError tok) (ty, List tok)
185+parse xs act
186+ = case doParse False xs act of
187+ Failure _ msg ts => Left (Error msg ts)
188+ EmptyRes _ val rest => pure (val, rest)
189+ NonEmptyRes _ val rest => pure (val, rest)
190+
191+||| Parse one or more things
192+export
193+some : Grammar tok True a ->
194+ Grammar tok True (List a)
195+some p = do x <- p
196+ (do xs <- some p
197+ pure (x :: xs)) <|> pure [x]
198+
199+||| Parse zero or more things (may match the empty input)
200+export
201+many : Grammar tok True a ->
202+ Grammar tok False (List a)
203+many p = some p
204+ <|> pure []
205+
206+||| Parse one or more things, separated by another thing
207+export
208+sepBy1 : Grammar tok True () -> Grammar tok True a ->
209+ Grammar tok True (List a)
210+sepBy1 sep p = do x <- p
211+ (do sep
212+ xs <- sepBy1 sep p
213+ pure (x :: xs)) <|> pure [x]
214+
215+||| Parse zero or more things, separated by another thing. May
216+||| match the empty input.
217+export
218+sepBy : Grammar tok True () -> Grammar tok True a ->
219+ Grammar tok False (List a)
220+sepBy sep p = sepBy1 sep p <|> pure []
221+
222+||| Optionally parse a thing, with a default value if the grammar doesn't
223+||| match. May match the empty input.
224+export
225+optional : Grammar tok True a -> (ifNothing : a) ->
226+ Grammar tok False a
227+optional p def = p <|> pure def
228+
229+
libs/contrib/contrib.ipkg+2
@@ -30,4 +30,6 @@ modules = CFFI, CFFI.Types, CFFI.Memory,
3030
3131 System.Concurrency.Process,
3232
33+ Text.Lexer, Text.Parser,
34+
3335 Data.Fin.Extra