Steven Gilham
Based on material developed in collaboration with Will Harwood and Laurence Jordan and presented in 2008
|
Course Overview
|
A standard application of the techniques
|
Lexers and Parsers
|
NOT a LISP course in your FP course!
Expressions are of the form
(mult (add x 5) factor)
(bind ((x. 1) (y . 2) (z . 3)) (add x y z))
(defun append (x y)
(if (null x) y
(cons (head x) (append (tail x) y))))
Lexer takes character data and presents a stream of tokens
Parser takes the token stream and creates a syntax tree
A state machine
"abcde" -> [ 'a'; 'b'; 'c'; 'd'; 'e' ]
"abcde" [ 'a' ] "bcde" [ 'a'; 'b' ] "cde" ... [ 'a'; 'b'; 'c'; 'd'; 'e' ] "" |
|
If we had written
let rec toList i = …
we would get the following from e.g i.Length
Lookup on object of indeterminate type based on information prior to this program point.
A type annotation may be needed prior to this program point to constrain the type of the object.
This may allow the lookup to be resolved.
= The type of i cannot be inferred.
We are, in this lecture, working from first principles throughout, rather than going to library code — the availability of mature facilities such as fslex/fsyacc or FParsec would reduce this example into just the incantations to initialise those.
This toList implementation is intended as a reminder of the technique used,
without diverting into implementation detail. We can see that the repeated
creation of temporary strings, for example, would be a potential performance
issue in practice. Indeed, what one might write in practice would be more like
let toList s = (s :> char seq) |> Seq.toList;;
but objects and modules come later in the series.
type lexical = BRA | KET | DOT | SYMATOM of char list | NUMATOM of int ;;
|
|
An Algebraic Data Type
enum on steroidsTest what we have found
| Is Member of List? | |
| Is Stopcase? | |
| Define digits | |
| Is Digit? | |
| Is Alpha? | |
char → int | char list → int |
|---|---|
|
|
| Symbol lexer | |
| Number or Symbol | |
Assembling the pieces
|
|
Write a lexer for .ica/.ini files (see also Exercise 2.3 below)
Define the grammar it obeys
SEXPR = ATOM | BRA SEXPR DOT SEXPR KET | BRA SEXPR* KETATOM = SYMBOL | NUMBERSEXPR = SYMBOL | NUMBER | ( SEXPR . SEXPR ) | ( SEXPR* )Translate the grammar directly into a program by
let parseAtom s = ((parseSym >* ATOMACTION) || (parseNum >* ATOMACTION)) s ;;
let rec parseSEXPR s =
(parseAtom ||
(bra >> parseSEXPR >> dot >> parseSEXPR >> ket >* CONSPAIR) ||
(bra >> (ITERATE parseSEXPR) >* CONSLIST >> ket)
) s;;
| Capture the concept of success or failure | |
| A simple parser that takes a list of lexed items and either returns a new parse state or fails | |
| Example (mult 1 2) where '(' has been read... | |
Maybe is a standard patternnull or need for nulltype Either = Left of 'error
| Right of 'result| Again, but for numbers | |
| Helper function for constants – a higher order parser that takes the token to match as a parameter | |
| Parsers for the lexical constants BRA, KET and DOT. | |
Partial function application
val parseConst :
x:'a -> s:('a list * 'b) Maybe -> ('a list * 'c list) Maybe
when 'a : equality
leads to
val bra : x:(lexical list * 'a) Maybe ->
(lexical list * 'b list) Maybe
"Either X or Y"
Try f. If it fails, try g |
|
| Make it an infix | |
"After X then Y"
Try f. If it fails, the parse fails.If it succeeds, try g on the new parse state; if that fails, the parse fails. |
|
| Make it an infix | |
"As many X as are available"
Try f. If it fails, the parse is a no-op.If it succeeds, recurse on the new parse state. |
|
| Not an infix |
Write the operator OPT such that OPT A matches zero or one occurrence of the parser A
Write the parser PLUS such that PLUS A matches one or more occurrences of A
This builds the syntax tree
The parsers only check the structure of the expression
We also want them to build an abstract structure corresponding to the string!
SEXPR = ATOM → ATOM-ACTION |
BRA SEXPR DOT SEXPR KET → PAIR-ACTION |
BRA SEXPR* KET → LIST-ACTION
ATOM = SYMBOL → SYMBOL-ACTION |
NUMBER → NUMBER-ACTION
Transforms take a parse state and build a parse state of a different type
(unparsed input, parse items list) -> (unparsed input, LISP Tree list)
LISP trees are built from ATOM, NIL and CONS elements
type SEXPR = SYMBOL of char list | NUMBER of int | NIL | CONS of SEXPR * SEXPR ;;
Parse input using f,
then transform according to h. |
|
| Make it an infix | |
The representation we seek is
SEXPR = ATOM |
BRA SEXPR DOT SEXPR KET → CONSPAIR |
BRA SEXPR* → CONSLIST KET
ATOM = SYMBOL → ATOM-ACTION |
NUMBER → ATOM-ACTION| CONSPAIR – produce a singleton list containing the CONS of the items. | |
| CONSLIST – produce a CONS representing a list | |
| ATOMACTION – translate base cases between data types | |
Recapitulating
let parseAtom s = ((parseSym >* ATOMACTION) || (parseNum >* ATOMACTION)) s ;;
let rec parseSEXPR s =
(parseAtom ||
(bra >> parseSEXPR >> dot >> parseSEXPR >> ket >* CONSPAIR) ||
(bra >> (ITERATE parseSEXPR) >* CONSLIST >> ket)
) s;;Building a usable API
let parse str =
let r = parseSEXPR (Return (lexer (toList str), []))
match r with
| Return(_ , result::_) -> result
| Return(_, []) -> NIL
| Fail -> NIL ;;
Test Case
parse "( ((ABC)) . (123. 456))";;
Result
val it : SEXPR =
CONS
(CONS (CONS (SYMBOL ['A'; 'B'; 'C'],NIL),NIL),CONS (NUMBER 123,NUMBER 456))Define a grammar
File = Line*
Line = WHITESPACE | CMD | 'NL'
WHITESPACE = ('NL' | 'TAB' | 'SPACE' )
CMD = SectionHeader | BINDING | COMMENT
SectionHeader = '[' HeaderString ']'
BINDING = BindString '=' BindString
HeaderString = Any but ('[' | ']' | 'NL')
BindString = Quote | Any but ('='| 'NL' )
Quote = '"' (Anything but '"') '"'
COMMENT = ';' (Any but 'NL') 'NL'
Write a Lexer for the grammar
Write a Parser for it
Extend it, using the full INI file format definition.
What real-world performance implications are there in the use of this sort of parser? Re-implement one of the parsers to avoid the limitations of the strictly evaluated list datatype.