2 Parsers and Combinators
| (require parser-combinator/parse) | |
| package: parser-combinator | |
> (parse (lit "hi") "hi") "hi"
> (parse (lit "hi") "bye") bad input, got error
> (parse (lit "hi") "high") bad input, had leftover text "gh"
2.1 Data Definitions
Value : any/c
Result : (or/c 'error result?)
'error, meaning the parser did not match
(result value remaining), meaning the parser matched a prefix of its input, producing value, and left the string remaining unconsumed
Parser : (-> string? (or/c 'error result?))
A Parser is a function that consumes a string and produces a Result. A parser only ever matches a prefix of its input. It is up to parse to insist that a match covers the whole string.
Parsers are functions. A parser can be applied directly to a string:
The parsers made by this library pass positions in the input to one another rather than copying it, so applying a parser copies the input only once, for the remaining string of its Result. See Run Parsers to write parsers that work the same way.
2.2 Defining Parsers
syntax
(define-parser id parser-expr)
Unlike define, define-parser does not evaluate parser-expr until id is first applied to a string. This lets a parser refer to itself. It can also refer to parsers that are defined later in the module. After that first application, id keeps the parser that parser-expr produced, so parser-expr is evaluated only once, rather than every time the parser runs.
2.3 Primitive Parsers
Note that emp is a function that produces a parser, not a parser itself, so it is used as (emp).
> ((emp) "abc") (result '() "abc")
> ((lit "-") "-1.35") (result "-" "1.35")
> ((lit "hi") "high") (result "hi" "gh")
> ((lit "high") "hi") 'error
> ((lit "") "walrus") (result "" "walrus")
> ((rx #rx"[^ab]") "cab") (result "c" "ab")
> ((rx #rx"[^ab]") "bad") 'error
> ((rx #px"\\d+") "42x") (result "42" "x")
> ((rx #rx"a|b") "xb") 'error
> ((span char-alphabetic?) "abc123") (result "abc" "123")
> ((span char-numeric?) "abc") 'error
A span matches the same text as an rx for a character class, such as (rx #rx"[a-z]+"), but costs less when the runs are short. Matching a regexp against a string has a cost every time, even when it matches one character or none, and span only checks characters. Because it always takes the longest run, it can’t match a fixed number of characters, which takes an rx such as (rx #px"[0-9]{4}").
2.4 Combinators
> ((seq list (lit "a") (lit "b")) "abc") (result '("a" "b") "c")
> ((seq string-append (lit "a") (lit "b")) "abc") (result "ab" "c")
> ((seq list (lit "a") (lit "b")) "acb") 'error
procedure
build : procedure? p : Parser
build must accept as many arguments as there are ps. If it does not, seq* raises exn:fail:contract when it makes the parser, before any input is parsed. A constructor can keep only some of the Values, which is useful for dropping delimiters. For the common case of keeping only the middle of three, see unwrap.
> (parse (seq* list (lit "a") (lit "b") (lit "c")) "abc") '("a" "b" "c")
> (parse (seq* (lambda (l x r) (string->symbol x)) (lit "(") (rx #rx"[a-z]+") (lit ")")) "(abc)") 'abc
> (seq* (lambda (a b) a) (lit "a") (lit "b") (lit "c")) seq*: contract violation
expected: (procedure-arity-includes/c 3)
given: #<procedure>
The choice is ordered and final: once p1 matches, alt never goes back to try p2, even if the rest of the grammar then fails. When one alternative is a prefix of another, list the longer one first.
> (parse (seq list (alt (lit "a") (lit "ab")) (lit "c")) "abc") bad input, got error
> (parse (seq list (alt (lit "ab") (lit "a")) (lit "c")) "abc") '("ab" "c")
> (define SIGNED (switch [(#\+ #\-) (rx #px"[+-]\\d+")] [else (rx #px"\\d+")])) > (SIGNED "-12") (result "-12" "")
> (SIGNED "12") (result "12" "")
Unlike alt, switch commits to the clause it chooses. If that parser fails, so does switch, without trying any other clause. It behaves like alt* when each alternative can only start with the characters listed for it, and then it costs one case on the next character, instead of trying each alternative in turn.
> ((alt (lit "ab") (lit "ac")) "ac") (result "ac" "")
> ((switch [(#\a) (lit "ab")] [else (lit "ac")]) "ac") 'error
Like alt, star never gives back a match, so (seq list (star (lit "a")) (lit "a")) matches nothing at all. It stops at the first match of p that consumes no input, leaving that match out, so a p that can match the empty string, such as (opt q), can’t make it repeat forever.
> (define NUMBER (seq* string->number (rx #px"\\d+"))) > (define BYTE (where NUMBER byte?)) > (parse BYTE "200") 200
> (parse BYTE "300") bad input, got error
It is the way to restrict a match by its Value without writing a Parser as a plain function, which would be given a copy of the rest of the input every time it runs. Written as a plain function, where would be:
> (define (plain-where p ok?) (lambda (s) (let ([r (p s)]) (if (and (result? r) (ok? (result-value r))) r 'error))))
2.5 Run Parsers
| (require (submod parser-combinator/parse positions)) | |
| package: parser-combinator | |
The parsers made by parser-combinator/parse each hold a Run, which matches the input from a position, and call each other’s Runs rather than copying the input. This module provides those pieces, for a parser that can’t be built from the combinators, such as one whose grammar depends on what it has already matched.
A Parser written as a plain function also works with the combinators, but it is given a copy of the rest of the input every time it runs, so inside a loop like star it makes parsing quadratic. A parser written with this module doesn’t copy.
Match : (or/c 'error matched?)
'error, meaning the parser did not match
(matched value end), meaning the parser matched, producing value, and the match ended just before position end of the input
Run : (-> string? exact-nonnegative-integer? (or/c 'error matched?))
A Run matches its string starting at the given position, and produces a Match.
struct
(struct matched (value end) #:transparent) value : Value end : exact-nonnegative-integer?
; Char-Predicate -> Parser ; matches one character that `ok?` accepts
> (define (char-where ok?) (parser (lambda (s i) (if (and (< i (string-length s)) (ok? (string-ref s i))) (matched (string (string-ref s i)) (add1 i)) 'error)))) > (parse (plus (char-where char-alphabetic?)) "abc") '("a" "b" "c")
> ((char-where char-numeric?) "7up") (result "7" "up")
procedure
p : Parser str : string? i : exact-nonnegative-integer?
; Parser -> Parser ; matches a digit n, then n of `p`, producing their list
> (define (counted p) (parser (lambda (s i) (let ([n (run (rx #rx"[0-9]") s i)]) (if (symbol? n) 'error (let loop ([k (string->number (matched-value n))] [i (matched-end n)] [vs '()]) (if (zero? k) (matched (reverse vs) i) (let ([m (run p s i)]) (if (symbol? m) 'error (loop (sub1 k) (matched-end m) (cons (matched-value m) vs))))))))))) > (run (counted (lit "ab")) "x2ababab" 1) (matched '("ab" "ab") 6)
> (parse (counted (lit "ab")) "3ababab") '("ab" "ab" "ab")
> (parse (counted (lit "ab")) "2ababab") bad input, had leftover text "ab"