Recipes¶
A worked tour of every parser construction style the library offers, from custom lexical rules through Pratt tables to operator chains. Start with Getting Started if you have not tokenized anything yet, and see Diagnostics for rendering failures.
Strings, comments, and DSL-style identifier boundaries¶
(let* ((identifier-char-p
(lambda (char)
(or (alpha-char-p char)
(digit-char-p char)
(char= char #\_)
(char= char #\$)
(char= char #\?))))
(tokenizer
(cl-parser-kit:make-tokenizer
:rules (list (cl-parser-kit:make-whitespace-rule :skip-p t)
(cl-parser-kit:make-line-comment-rule :skip-p t)
(cl-parser-kit:make-block-comment-rule :skip-p t)
(cl-parser-kit:make-string-rule :escape-char #\\)
(cl-parser-kit:make-keyword-rule
:if "if"
:identifier-char-predicate identifier-char-p)
(cl-parser-kit:make-identifier-rule
:start-predicate identifier-char-p
:continue-predicate identifier-char-p)))))
(cl-parser-kit:tokenize-string
"if $value /* note */ \"ok\" ; trailing comment
if?"
tokenizer))
The lexical helpers are intentionally customizable. If your language allows
identifier sigils or suffix markers such as $value or tail?, use
make-identifier-rule with custom :start-predicate / :continue-predicate,
and pass the same boundary logic into make-keyword-rule so reserved words
do not match inside those identifiers.
Pratt parsing¶
(let* ((table (cl-parser-kit:make-pratt-table))
(tokens (vector (cl-parser-kit:make-token :type :number :text "1" :value 1)
(cl-parser-kit:make-token :type :plus :text "+")
(cl-parser-kit:make-token :type :number :text "2" :value 2)
(cl-parser-kit:make-token :type :bang :text "!"))))
(cl-parser-kit:register-prefix-operator
table :number 0
(lambda (token stream next table)
(declare (ignore stream table))
(values t (cl-parser-kit:token-value token) next nil)))
(cl-parser-kit:register-infix-operator
table :plus 10 11
(lambda (left op right next table)
(declare (ignore op table))
(values t (list :add left right) next nil)))
(cl-parser-kit:register-postfix-operator
table :bang 30
(lambda (left op stream next table)
(declare (ignore op stream table))
(values t (list :fact left) next nil)))
(cl-parser-kit:parse-pratt-all tokens table))
Use :position when the expression begins later in an existing token
stream, and :min-binding-power when a caller needs Pratt parsing to stop
before lower-precedence operators.
The source-oriented Pratt entry point, parse-pratt-source, accepts the
same keywords after tokenization.
Seq helpers for delimited grammars¶
For small delimited grammars, the seq helpers remove most of the parser loop boilerplate:
(let* ((tokenizer (cl-parser-kit:make-tokenizer
:rules (list (cl-parser-kit:make-whitespace-rule :skip-p t)
(cl-parser-kit:make-keyword-rule :let "let")
(cl-parser-kit:make-literal-rule :lparen "(")
(cl-parser-kit:make-literal-rule :rparen ")")
(cl-parser-kit:make-literal-rule :comma ",")
(cl-parser-kit:make-literal-rule :semicolon ";")
(cl-parser-kit:make-identifier-rule))))
(parser (cl-parser-kit:seq
(cl-parser-kit:preceded-by
(cl-parser-kit:literal "let" :type :let)
(cl-parser-kit:delimited-sep-by1
(cl-parser-kit:literal "(" :type :lparen)
(cl-parser-kit:type-token :identifier)
(cl-parser-kit:literal "," :type :comma)
(cl-parser-kit:literal ")" :type :rparen)))
(cl-parser-kit:opt
(cl-parser-kit:literal ";" :type :semicolon))
(cl-parser-kit:end-of-input))))
(cl-parser-kit:parse-source parser "let (answer, result);" tokenizer))
If you want parser results to contain raw strings and values instead of
token objects, combine terminated-by with the projection helpers. Swap
delimited-sep-by for delimited-sep-end-by when a trailing separator like
(answer, result,) should still parse:
(let* ((tokenizer (cl-parser-kit:make-tokenizer
:rules (list (cl-parser-kit:make-whitespace-rule :skip-p t)
(cl-parser-kit:make-literal-rule :lparen "(")
(cl-parser-kit:make-literal-rule :rparen ")")
(cl-parser-kit:make-literal-rule :comma ",")
(cl-parser-kit:make-literal-rule :semicolon ";")
(cl-parser-kit:make-identifier-rule))))
(group-parser
(cl-parser-kit:terminated-by
(cl-parser-kit:delimited-sep-by
(cl-parser-kit:literal "(" :type :lparen)
(cl-parser-kit:type-token-text :identifier)
(cl-parser-kit:literal "," :type :comma)
(cl-parser-kit:literal ")" :type :rparen))
(cl-parser-kit:literal ";" :type :semicolon)))
(binding-parser
(cl-parser-kit:map-parser
(cl-parser-kit:seq
(cl-parser-kit:type-token-text :identifier)
(cl-parser-kit:literal-value "=" :type :equals)
(cl-parser-kit:terminated-by
(cl-parser-kit:type-token-value :number)
(cl-parser-kit:literal-text ";" :type :semicolon))
(cl-parser-kit:end-of-input))
(lambda (parts)
(let ((identifier (first parts))
(operator (second parts))
(value (third parts))
(end-of-input (fourth parts)))
(declare (ignore end-of-input))
(list identifier operator value))))))
(list (cl-parser-kit:parse-source group-parser "(answer, result);" tokenizer)
(cl-parser-kit:parse-tokens
binding-parser
(vector (cl-parser-kit:make-token :type :identifier :text "answer")
(cl-parser-kit:make-token :type :equals
:text "="
:value :assign)
(cl-parser-kit:make-token :type :number
:text "42"
:value 42)
(cl-parser-kit:make-token :type :semicolon :text ";")))))
The seq helpers differ in where they commit failure:
preceded-byreturns only the inner parser value, but any committed failure from the prefix or inner parser is propagated as-is.terminated-byreturns only the main parser value; once that main parser has consumed input, a missing suffix remains a hard failure instead of being silently ignored.betweenis the same contract applied to open/body/close delimiters, so a missing close delimiter stays committed after the body has started.sep-by/delimited-sep-bystop before a separator that does not match, but reject a trailing separator because the following item parser has already become mandatory.sep-end-by/delimited-sep-end-bykeep the same committed-item contract, yet recover from a final separator when the next item parser fails without consuming input.
Pratt handlers use the same success/failure contract as the rest of the parser:
- prefix/postfix handlers return
(values t value next nil)on success - infix handlers return
(values t value next nil)on success - handlers may return
(values nil nil next failure)for domain-specific validation failures
Working directly with tokens¶
If your pipeline already has tokens, use parse-tokens or parse-all
directly:
(let* ((tokens (vector (cl-parser-kit:make-token :type :identifier :text "answer")
(cl-parser-kit:make-token :type :equals :text "=")
(cl-parser-kit:make-token :type :number :text "42" :value 42)))
(parser (cl-parser-kit:seq
(cl-parser-kit:type-token :identifier)
(cl-parser-kit:type-token :equals)
(cl-parser-kit:type-token :number))))
(cl-parser-kit:parse-tokens parser tokens))
For custom token contracts and manual cursor inspection, combine
satisfies-token with peek-token, next-token, and eof-token-p:
(let* ((tokens (vector (cl-parser-kit:make-token :type :identifier :text "answer")
(cl-parser-kit:make-token :type :equals :text "=")
(cl-parser-kit:make-token :type :number :text "42" :value 42)))
(parser
(cl-parser-kit:map-parser
(cl-parser-kit:seq
(cl-parser-kit:satisfies-token
(lambda (token)
(and (eql (cl-parser-kit:token-type token) :identifier)
(> (length (cl-parser-kit:token-text token)) 3)))
:expected-name :long-identifier)
(cl-parser-kit:type-token :equals)
(cl-parser-kit:type-token-value :number)
(cl-parser-kit:end-of-input))
(lambda (parts)
(list (cl-parser-kit:token-text (first parts))
(third parts))))))
(multiple-value-bind (first next)
(cl-parser-kit:next-token tokens 0)
(list :peek (cl-parser-kit:token-text (cl-parser-kit:peek-token tokens 0))
:next (list (cl-parser-kit:token-text first) next)
:eof-before (cl-parser-kit:eof-token-p tokens next)
:parse (multiple-value-list
(cl-parser-kit:parse-tokens parser tokens))
:eof-after (cl-parser-kit:eof-token-p tokens (length tokens)))))
If those tokens come from an external lexer, diagnostics can still recover
line/column locations and source excerpts without token-span as long as
each token carries token-start, token-end, and (:source <string>) in
token-metadata:
(let* ((source "answer
+")
(tokens (vector (cl-parser-kit:make-token :type :identifier
:text "answer"
:start 0
:end 6
:metadata (list :source source))
(cl-parser-kit:make-token :type :plus
:text "+"
:start 7
:end 8
:metadata (list :source source))))
(parser (cl-parser-kit:type-token :identifier)))
(multiple-value-bind (ok value next failure)
(cl-parser-kit:parse-all parser tokens)
(declare (ignore value next))
(if ok
:ok
(cl-parser-kit:parse-failure->string failure))))
Lowering into a CST¶
When downstream tooling needs a stable tree shape, lower parsed output into a small CST:
(let* ((span (cl-parser-kit:make-span :start 0 :end 3))
(cst (cl-parser-kit:make-cst-node
:type :binding
:children (list (cl-parser-kit:make-cst-node
:type :identifier
:value "answer"
:span span)))))
(cl-parser-kit:cst-node->sexp cst :include-span t))
Lookahead and negative lookahead¶
To assert what may or may not come next without consuming extra input,
combine lookahead and not-followed-by:
(let* ((tokens (vector (cl-parser-kit:make-token :type :identifier :text "foo")
(cl-parser-kit:make-token :type :plus :text "+")))
(parser (cl-parser-kit:seq
(cl-parser-kit:lookahead
(cl-parser-kit:seq
(cl-parser-kit:type-token :identifier)
(cl-parser-kit:type-token :plus)))
(cl-parser-kit:type-token :identifier)
(cl-parser-kit:not-followed-by
(cl-parser-kit:type-token :identifier))
(cl-parser-kit:type-token :plus))))
(cl-parser-kit:parse-tokens parser tokens))
Naming grammar terms with label¶
When a grammar term matters more than a raw token type, wrap the parser
with label so failures report that domain term instead:
(let ((failure
(multiple-value-bind (ok value next failure)
(cl-parser-kit:parse-tokens
(cl-parser-kit:seq
(cl-parser-kit:alt
(cl-parser-kit:seq
(cl-parser-kit:literal "let" :type :let)
(cl-parser-kit:type-token :identifier))
(cl-parser-kit:seq
(cl-parser-kit:literal "const" :type :const)
(cl-parser-kit:label
(cl-parser-kit:type-token :identifier)
:binding-name)
(cl-parser-kit:literal "=" :type :equals)
(cl-parser-kit:type-token :number)))
(cl-parser-kit:end-of-input))
(vector (cl-parser-kit:make-token :type :const :text "const")
(cl-parser-kit:make-token :type :equals :text "=")))
(declare (ignore value next))
(unless ok
failure))))
(list (cl-parser-kit:parse-failure-position failure)
(cl-parser-kit:parse-failure-expected failure)
(cl-parser-kit:parse-failure-committed-p failure)
(cl-parser-kit:token-type
(cl-parser-kit:parse-failure-actual failure))))
Operand/operator chains with chainl1 / chainr1¶
When a grammar is just a repeated operand/operator pair, chainl1 and
chainr1 encode associativity directly without hand-written parser loops:
(let* ((tokenizer (cl-parser-kit:make-tokenizer
:rules (list (cl-parser-kit:make-whitespace-rule :skip-p t)
(cl-parser-kit:make-literal-rule :minus "-")
(cl-parser-kit:make-literal-rule :caret "^")
(cl-parser-kit:make-number-rule))))
(number-parser
(cl-parser-kit:map-parser
(cl-parser-kit:type-token :number)
#'cl-parser-kit:token-value))
(subtract-parser
(cl-parser-kit:chainl1
number-parser
(cl-parser-kit:operator-parser
(cl-parser-kit:literal "-" :type :minus)
(lambda (left right)
(- left right)))))
(power-parser
(cl-parser-kit:chainr1
number-parser
(cl-parser-kit:operator-parser
(cl-parser-kit:literal "^" :type :caret)
(lambda (left right)
(expt left right))))))
(list (cl-parser-kit:parse-source subtract-parser "10 - 3 - 2" tokenizer)
(cl-parser-kit:parse-source power-parser "2 ^ 3 ^ 2" tokenizer)))
operator-parser is the thin wrapper for the common "match a token, ignore
its payload, return a binary combiner" pattern that shows up around
chainl1 and chainr1.