Parser combinators with restartable parser errors for error recovery
https://florida.moe/phosphate
- Scheme 100%
| lib | finish tutorial, check in tests that I forgot | |
| tests | finish tutorial, check in tests that I forgot | |
| .gitignore | 0.1 | |
| phosphate.egg | 0.1 | |
| phosphate.release-info | 0.1 | |
| README.tsm | 0.1 | |
head| meta| @| charset| utf-8
link| @| href| https://florida.moe/assets/treeside-simple.css
rel| stylesheet
type| text/css
nh1| Phosphate — Parser Combinators with Error Recovery
blockquote|
Thy gardens and thy phosphate mines,{br}
Florida, my Florida
— Chastain V. Waugh
Current version: 0.1
{href `https://codeberg.org/phm/phosphate` Git repository (master)}
0.1:
{href `https://codeberg.org/phm/phosphate/src/tag/0.1` git},
{href `https://florida.moe/phosphate/0.1.html` manual},
{href `https://florida.moe/phosphate/phosphate-0.1.0-5.tgz` tarball}
Phosphate is a parser-combinator library with error recovering inspired by
algebraic effect handlers.
Parsers are procedures of three arguments: an {SRFI 225} DTO, a dictionary
of global state, and a dictionary of dynamic state.
Parsers return the new global state and zero or more values.
Global state is passed along successful parses and contains the input
state of the text to parse. The global state can also be used to store
parse-wide settings, such as case folding.
The dynamic state is modified and passed around as parsers are composed
like Scheme parameters and is used to store the handlers and information
such as nesting depth. The dynamic state is not the same as the dynamic
extent, although it acts in an analagous way. Unlike the `parameterize`
form of R7RS, {idref parameterize/p} is guaranteed to tail-call its
parser argument.
Parse failures and parse errors are handled using a system like
algebraic effect handlers (except they use `call/cc` instead of
delimited continuations). A system parse failure is usually handled
by the library, while a user-defined parse {em error} is handled by a
user-defined handler. The error handlers get a continuation that allows them
to execute code and return values in the raised-from context.
If you port this library to a system with real delimited continuations
(either Racket or a future R{sup N}RS), you should use the name
`orthophosphate`.
Inspired by Medeiros and Mascarenhas,
{href `https://arxiv.org/abs/1806.11150v1` "Syntax Error Recovery in Parsing Expression Grammars"},
2018, and {href `https://docs.racket-lang.org/megaparsack` MegaParsack}.
Phosphate is written in R{sup 7}RS and is tested to work in Chibi Scheme 0.11 and CHICKEN 5.
Phosphate only requires {SRFI 225} to run (requires {SRFI 64} for tests).
{b WARNING!} The API is unlikely to change, but this is an unstable release,
so it might.
the-table-of-contents|
nh2| Tutorial
This tutorial will walk through parsing an
{href `https://www.rfc-editor.org/rfc/rfc4180` RFC 4180} comma separated
values file using all of the major features of Phosphate.
{small If you are looking for an actual CSV parsing library, Wolfgang Corcoran-Mathe
has {href `https://www.sigwinch.xyz/scheme/csv.html` one} for R{sup 6}RS.}
A CSV file is a sequence of records separated by CRLF, optionally followed
by CRLF followed by EOF. The {idref many-until/p} parser is perfect for this.
It parses a parser repeatedly until it parses some end parser, and returns
the results as a list.
code| pre|
(define csv/p
(many-until/p record/p
csv-eof/p
\`((sep/p . ,crlf/p))))
The {idref many-until/p} parser takes keyword arguments in the form of an
alist, which will be modified later as we get into error handling.
The `crlf/p` parser just checks for `\r\n` in the feed. This can be accomplished
using the {idref ==seq/p} parser, which checks for a string, list, or vector in
the stream.
code| pre|
(define crlf/p
(==seq/p "\\r\\n"))
The `csv-eof/p` procedure just checks for an optional CRLF followed by EOF. This can
be handled using the previously defined `crlf/p`, {idref eof/p} (checks that the sequence is at EOF),
{idref or/p} (disjunction over parsers) and
{idref and/p} (composes parsers together, returning the results of the final
parser).
code| pre|
(define csv-eof/p
(or/p eof/p (and/p crlf/p eof/p)))
The `record/p` field parses text data separated by commas. It is terminated
by CRLF, but it cannot consume it because `csv/p` consumes it. We can check
for CRLF without consuming input using {idref lookahead/p}.
code| pre|
(define record/p
(many-until/p field/p
(lookahead/p (or/p crlf/p eof/p))
\`((sep/p . ,comma/p))))
The {idref many-until/p} and the related {idref many/p} parsers will
insert the values returne by the separator parser into the list of
parsed results. This was not a problem for `crlf/p` because it used
{idref ==seq/p} because it returns no results. We could use that parser
again, but for the sake of education I will use the parser {idref ==/p},
which does return a value. Since we don't want the commas in the returned
list of fields, we can use {idref discard/p} to discard the parsed values.
code| pre|
(define comma/p
(discard/p (==/p #\,円)))
A `field/p` is either escaped or unescaped. We could use the {idref or/p}
combinator here to try one, backtrack when it does not work, and then
try another, but this is wasteful. The backtrack state will be active
throughout the entire parse even when we have parsed `"`, which cannot
be in an unescaped field. Instead we will use {idref cond/p}, which
will tail-call a parser when one parser succeeds. In this case, we wish
to look ahead one character to see if the next character is `"`, and if
so parse the quoted character without pushing backtracking state.
Otherwise we parse the unescaped field.
code| pre|
(define field/p
(cond/p
((lookahead/p (==/p #\\")) escaped/p)
(else unescaped/p)))
Unescaped text does not allowed control characters, double quotes,
commas, or DEL. As an extension to the spec, all non-ASCII Unicode
characters are supported. To apply a Scheme predicate to the current
character, use {idref satisfy/p} (which when successful will return the
character it matched against), inside {idref many/p}
to capture zero or more of the valid characters. Since we want to
return a string, we will capture the returned value of {idref many/p}
using {idref lv/p} (short for {em let-values/p}) and then use
{idref return/p} to convert the list of characters into a string.
code| pre|
(define unescaped-char/p
(satisfy/p (lambda (x)
(and (not (char<? x #\\x20))
(not (char=? x #\\x22))
(not (char=? x #\\x2C))
(not (char=? x #\\x7F))))))
(define unescaped/p
(lv/p (((lst) (many/p unescaped-char/p)))
(return/p (list->string lst))))
The use of {idref return/p} here wraps the returned values such that
other parses can read the value along with the current parse state
returned from {idref many/p}.
Escaped text starts with a double-quote and ends with a double quote.
The escaped text can have two double-quotes in it, which turn into a
single double quote.
We can accomplish this using {idref advance/p} (return any value at
the current input point, and advance input) and {idref not/p}
(attempt to parse the parser passed to it, and fail if it succeeds).
The {idref not/p} parser is a lookahead parser, so if it succeeds the
parse state is at the same point as the entry to {idref not/p}.
code| pre|
(define escaped-char/p
(cond/p
((==/p #\\") (==/p #\\"))
(else advance/p)))
(define escaped/p
(and/p (==/p #\\")
(lv/p (((lst)
(many-until/p escaped-char/p
(and/p (==/p #\\")
(not/p (==/p #\\"))))))
(return/p (list->string lst)))))
Note that in `escaped-char/p`, the value returned by the first `(==/p #\")`
is discarded. The parser then tail-calls the second `(==/p #\")`, which
will return a value if the second character is `"`, and fail otherwise.
To actually parse input, we use {idref parse}. This requires the
DTO that describes the operations on the input stream,
the input stream as a dictionary, the dynamic state as a dictionary,
the parser, and any error handlers. There are no named error handlers
so we handle the catch-all error `#t` and just return `#f` on error.
code| pre|
(define (parse-csv str)
(parse (phosphate-dto)
(char-sequence->dict "<stdin>" str)
(phosphate-empty-dict)
csv/p
(#t (lambda (k dto dict dyn mark . rest)
(values dict #f)))))
The parser will return two values: the input dictionary at whatever point
the parser stopped at, and the parsed CSV file or `#f` on error.
This completes our simple CSV parser. Since the parser is made up of
procedures, we need to rearrange the definitions to make it work.
A complete script is:
code| pre|
(import (scheme base) (phosphate))
(define comma/p
(discard/p (==/p #\,円)))
(define crlf/p
(==seq/p "\\r\\n"))
(define escaped-char/p
(cond/p
((==/p #\\") (==/p #\\"))
(else advance/p)))
(define escaped/p
(and/p (==/p #\\")
(lv/p (((lst)
(many-until/p escaped-char/p
(and/p (==/p #\\")
(not/p (==/p #\\"))))))
(return/p (list->string lst)))))
(define unescaped-char/p
(satisfy/p (lambda (x)
(and (not (char<? x #\\x20))
(not (char=? x #\\x22))
(not (char=? x #\\x2C))
(not (char=? x #\\x7F))))))
(define unescaped/p
(lv/p (((lst) (many/p unescaped-char/p)))
(return/p (list->string lst))))
(define field/p
(cond/p
((lookahead/p (==/p #\\")) escaped/p)
(else unescaped/p)))
(define record/p
(many-until/p field/p
(lookahead/p (or/p crlf/p eof/p))
\`((sep/p . ,comma/p))))
(define csv-eof/p
(or/p eof/p (and/p crlf/p eof/p)))
(define csv/p
(many-until/p record/p
csv-eof/p
\`((sep/p . ,crlf/p))))
(define (parse-csv str)
(parse (phosphate-dto)
(char-sequence->dict "<stdin>" str)
(phosphate-empty-dict)
csv/p
(#t (lambda (k dto dict dyn mark . rest)
(values dict #f)))))
You can try some examples:
code| verb|
(parse-csv "a,b,c") ; ⇒ (("a" "b" "c"))
(parse-csv "h,e,llo\r\nw,o,rld") ; ⇒ (("h" "e" "llo") ("w" "o" "rld"))
(parse-csv "\"This is\r\n\"\"escaped\"\"\",this is not") ; ⇒ (("This is\r\n\"escaped\" "this is not"))
This parser works, but when a parse error occurs, the parser just
gives up and says it failed. There is no description of the failure, and
no way to recover from the failure.
code| verb|
(parse-csv "\"truncated") ; ⇒ #f
(parse-csv "invalid \"text\"") ; → #f
Phosphate allows us to write a parser that raises named parse errors with
continuations that allows the parser to recover and continue. Where to
put error handlers and how many errors handlers to install will depend
on what you need. With parser combinators, it is easy to write a parser,
so there is no need to write the most general parser possible.
Phosphate differentiates between parse {em failures}, which have the mark
`#f`, and parse {em errors}, which have any other mark. Parse errors
are meant to be handled or signalled, while parse failures are meant
to backtrack. Parse failures can be converted into parse errors using
{idref as-error/p}. Our current parser only has parse failures and no
parse errors.
Lets start adding errors at `escaped/p`. The {idref many-until/p} allows
us to specify certain error behavior. The parser does not use {var min}
or {var max} to specify size constraints, so {var too-little-error}
and {var expected-end-error} are not useful. The escaped text does
not have a separator, so {var expected-after-sep-error} is not useful
either. The error we are looking for is {var expected-sep-or-end-error},
which is raised when the end parser and the char parser fail. If this
error is raied, it means that the parser hits EOF while trying to parse
the escaped text.
So as a first pass we can write
code| pre|
(define escaped/p
(and/p (==/p #\\")
(lv/p (((lst)
(many-until/p escaped-char/p
(and/p (==/p #\\")
(not/p (==/p #\\"))
'((expected-sep-or-end-error
. EOF-in-escaped-text)))))
(return/p (list->string lst)))))
This will raise `'EOF-in-escaped-text` when the parser hits EOF while
parsing escaped text. The arguments that the handler receives are
documented in the {idref many-until/p} entry.
We also want to raise an error when garbage data is found after the
closing quote mark. We can do this by adding another parser at the
end that looks ahead for delimiters.
code| pre|
(define escaped/p
(and/p (==/p #\\")
(lv/p (((lst)
(many-until/p escaped-char/p
(and/p (==/p #\\")
(not/p (==/p #\\")))
'((expected-sep-or-end-error
. EOF-in-escaped-text)))))
(let ((str (if (list? lst) (list->string lst) lst)))
(cond/p
((or/p eof/p
(lookahead/p comma/p)
(lookahead/p crlf/p))
(return/p str))
(else (error/p 'garbage-after-escaped str)))))))
Here {idref error/p} will raise an error with mark `'garbage-after-escaped`
with the parsed string as an argument. The continuation that is passed to
the handler is the continuation of the parse of {idref error/p}. Note that
the body of {idref lv/p} is just an expression, so we can put a regular
`let` in there. The `if` expression is used to allow for non-list returns
from error handlers.
Similar considerations apply to `unescaped/p`:
code| pre|
(define unescaped/p
(lv/p (((lst) (many/p unescaped-char/p)))
(let ((str (if (list? lst) (list->string lst) lst)))
(cond/p
((or/p eof/p (lookahead/p comma/p) (lookahead/p crlf/p))
(return/p str))
(else (error/p 'garbage-after-unescaped str))))))
There is nothing to be done for `field/p`, as it is just a wrapper for
`escaped/p` and `unescaped/p`, which do the error handling.
At `record/p` there is not much error handling to be done, because errors
regarding missing CRLF or commas are done in the parsers we just did.
However, if parsing of a field fails, we may want to throw out the whole
row instead of trying to recover an individual parse. We can do this by
installing a handler whenever `record/p` is parsed using
{idref handle-errors/p}.
code| pre|
(define record/p
(handle-errors/p
(many-until/p field/p
(lookahead/p (or/p crlf/p eof/p))
\`((sep/p . ,comma/p)))
('return-from-record (lambda (k dto dict dyn mark value)
(values dict value)))))
During the parse of `record/p`, a handler named `'return-from-record`
is installed. When it is invoked (for instance, with `(error/p
'return-from-record value)`, the passed value is returned from the parse
of `record/p`. This is because the lambda is evaluated in the continuation
of the invocation of {idref handle-errors/p}.
As an artifical example, we will write a parser entry procedure that will
abort a record when a malformed unescaped field is found, but will recover
for a malformed escaped field.
When there is garbage after a field, we can skip over the characters in
that field until we get to a synchronization point, which is `,` CRLF,
or EOF.
When we want to abort a record, we have to parse fields until the end of
a record is found, but discarding all data accumulated.
code| pre|
(define abort-after-field/p
(skip/p (and/p (not/p (or/p comma/p crlf/p eof/p))
advance/p)))
(define abort-field/p
(cond/p
((==/p #\\") (and/p (skip/p (cond/p
((==/p #\\") (==/p #\\"))
(else advance/p)))
(or/p (==/p #\\") (return/p))
abort-after-field/p))
(else abort-after-field/p)))
(define abort-record/p
(many-until/p abort-field/p
(lookahead/p (or/p eof/p crlf/p))
\`((sep/p . ,comma/p))))
(define (parse-csv str)
(parse (phosphate-dto)
(char-sequence->dict "<stdin>" str)
(phosphate-empty-dict)
csv/p
('garbage-after-escaped
(lambda (k dto dict dyn mark str)
(k (lambda ()
((and/p abort-after-field/p
(return/p str))
dto dict dyn)))))
('garbage-after-unescaped
(lambda (k dto dict dyn mark str)
(k (lambda ()
((and/p abort-after-field/p
abort-record/p
(error/p 'return-from-record #f))
dto dict dyn)))))
('EOF-in-escaped-text
(lambda (k dto dict dyn mark . rest)
(k (lambda ()
(values dict #f)))))))
The {idref parse} form uses {idref handle-errors/p} to install handlers,
so the same considerations apply to both. The previous use of {idref
handle-errors/p} returned values in the continuation that the handler
was invoked in, and directly returned values without further parsing.
Here we see parsers that invoke the continuation {var k}.
The continuation passed to an error handler accepts a single value, a
thunk, and will evaluate that thunk in that continuation. Hence each
handler will re-enter the continuation. In the case of the first two
handlers, it will re-start parsing by passing values manually to a
new set of combinators. After skipping tokens to get to a synchronization
point, the parsers either return values to their continuation (in the
cased of `'garbage-after-escaped`).
With this we have written our parser with error recovery. The complete
script is:
code| pre|
(import (scheme base) (phosphate))
(define comma/p
(discard/p (==/p #\,円)))
(define crlf/p
(==seq/p "\\r\\n"))
(define escaped-char/p
(cond/p
((==/p #\\") (==/p #\\"))
(else advance/p)))
(define escaped/p
(and/p (==/p #\\")
(lv/p (((lst)
(many-until/p escaped-char/p
(and/p (==/p #\\")
(not/p (==/p #\\")))
'((expected-sep-or-end-error
. EOF-in-escaped-text)))))
(let ((str (if (list? lst) (list->string lst) lst)))
(cond/p
((or/p eof/p
(lookahead/p comma/p)
(lookahead/p crlf/p))
(return/p str))
(else (error/p 'garbage-after-escaped str)))))))
(define unescaped-char/p
(satisfy/p (lambda (x)
(and (not (char<? x #\\x20))
(not (char=? x #\\x22))
(not (char=? x #\\x2C))
(not (char=? x #\\x7F))))))
(define unescaped/p
(lv/p (((lst) (many/p unescaped-char/p)))
(let ((str (if (list? lst) (list->string lst) lst)))
(cond/p
((or/p eof/p (lookahead/p comma/p) (lookahead/p crlf/p))
(return/p str))
(else (error/p 'garbage-after-unescaped str))))))
(define field/p
(cond/p
((lookahead/p (==/p #\\")) escaped/p)
(else unescaped/p)))
(define record/p
(handle-errors/p
(many-until/p field/p
(lookahead/p (or/p crlf/p eof/p))
\`((sep/p . ,comma/p)))
('return-from-record (lambda (k dto dict dyn mark value)
(values dict value)))))
(define csv-eof/p
(or/p eof/p (and/p crlf/p eof/p)))
(define csv/p
(many-until/p record/p
csv-eof/p
\`((sep/p . ,crlf/p))))
(define abort-after-field/p
(skip/p (and/p (not/p (or/p comma/p crlf/p eof/p))
advance/p)))
(define abort-field/p
(cond/p
((==/p #\\") (and/p (skip/p (cond/p
((==/p #\\") (==/p #\\"))
(else advance/p)))
(or/p (==/p #\\") (return/p))
abort-after-field/p))
(else abort-after-field/p)))
(define abort-record/p
(many-until/p abort-field/p
(lookahead/p (or/p eof/p crlf/p))
\`((sep/p . ,comma/p))))
(define (parse-csv str)
(parse (phosphate-dto)
(char-sequence->dict "<stdin>" str)
(phosphate-empty-dict)
csv/p
('garbage-after-escaped
(lambda (k dto dict dyn mark str)
(k (lambda ()
((and/p abort-after-field/p
(return/p str))
dto dict dyn)))))
('garbage-after-unescaped
(lambda (k dto dict dyn mark str)
(k (lambda ()
((and/p abort-after-field/p
abort-record/p
(error/p 'return-from-record #f))
dto dict dyn)))))
('EOF-in-escaped-text
(lambda (k dto dict dyn mark . rest)
(k (lambda ()
(values dict #f)))))))
Normal code is fine:
code| verb|
(parse-csv "a,b,c") ; ⇒ (("a" "b" "c"))
(parse-csv "h,e,llo\r\nw,o,rld") ; ⇒ (("h" "e" "llo") ("w" "o" "rld"))
(parse-csv "\"This is\r\n\"\"escaped\"\"\",this is not") ; ⇒ (("This is\r\n\"escaped\" "this is not"))
CSV files with garbage after escaped work as expected:
code| verb|
(parse-csv "field,\"my \"\"escaped\"\" field\" with \"garbage,next field\r\nother, field\r\n")
; ⇒ (("field "my \"escaped\" field" "next field") ("other field"))
Garbage after unescaped will handle even very mangled files:
code| verb|
(parse-csv "multiple, garbage \n, and \"this has a \r\n\
in it, causing another row\", extra stuff\r\n\
this, is OK")
; ⇒ (#f #f ("this" "is OK"))
CSV files with unterminated escaped text will return `#f`:
code| verb|
(parse-csv "a,record,with,\"EOF") ; ⇒ (("a" "record" "with" #f))
This parser will work, but what if we are parsing untrusted data? Then we
might read a very large file that will cause our program to run out of
memory. This is more of an issue for file formats like JSON with arbitrary
nesting, but we might still want to impose limits on the length of fields,
records, and files.
We can use {idref parameterize/p} for this, unlike R{sup 7}RS's `parameterize`,
the parser that is being parameterized will always be tail-called. This is
because parser parameters are carried around in the `dyn` dictionary.
We will add the parameters `field-max`, `record-max`, and `file-max`. These
are either `#f` or exact positive integers. The {idref many-until/p} and
{idref many/p} parsers accept a `'max` keyword that will limit the text to
a maximum length, and they also can emit errors when this maximum length is
hit without a corresponding limiter.
code| pre|
(define escaped/p
(and/p (==/p #\\")
(lv/p (((max) (ref-parameter/p 'field-max))
((lst)
(many-until/p escaped-char/p
(and/p (==/p #\\")
(not/p (==/p #\\")))
\`((expected-sep-or-end-error
. EOF-in-escaped-text)
(max . ,max)
(expected-end-error . escaped-too-big)))))
(let ((str (if (list? lst) (list->string lst) lst)))
(cond/p
((or/p eof/p
(lookahead/p comma/p)
(lookahead/p crlf/p))
(return/p str))
(else (error/p 'garbage-after-escaped str)))))))
(define unescaped/p
(lv/p (((max) (ref-parameter/p 'field-max))
((lst) (many/p unescaped-char/p
\`((max . ,max)))))
(let ((str (if (list? lst) (list->string lst) lst)))
(cond/p
((or/p eof/p (lookahead/p comma/p) (lookahead/p crlf/p))
(return/p str))
((lookahead/p unescaped-char/p)
(error/p 'unescaped-too-big))
(else (error/p 'garbage-after-unescaped str))))))
(define record/p
(lv/p (((max) (ref-parameter/p 'record-max)))
(handle-errors/p
(many-until/p field/p
(lookahead/p (or/p crlf/p eof/p))
\`((sep/p . ,comma/p)
(max . ,max)
(expected-end-error . record-too-big)))
('return-from-record (lambda (k dto dict dyn mark value)
(values dict value))))))
(define csv/p
(lv/p (((max) (ref-parameter/p 'file-max)))
(many-until/p record/p
csv-eof/p
\`((sep/p . ,crlf/p)
(max . ,max)
(expected-end-error . file-too-big)))))
Here we use the {idref ref-parameter/p} parser, which will return the value
associated with the key given in its argument.
The `parse-csv` procedure now takes limits.
code| pre|
(define (parse-csv str file-max record-max field-max)
(parse (phosphate-dto)
(char-sequence->dict "<stdin>" str)
(phosphate-empty-dict)
(parameterize/p csv/p
'file-max file-max
'record-max record-max
'field-max field-max)
('garbage-after-escaped
(lambda (k dto dict dyn mark str)
(k (lambda ()
((and/p abort-after-field/p
(return/p str))
dto dict dyn)))))
('garbage-after-unescaped
(lambda (k dto dict dyn mark str)
(k (lambda ()
((and/p abort-after-field/p
abort-record/p
(error/p 'return-from-record #f))
dto dict dyn)))))
('EOF-in-escaped-text
(lambda (k dto dict dyn mark . rest)
(k (lambda ()
(values dict #f)))))
(#t (lambda (k dto dict dyn mark . rest)
(values dict mark)))))
The parser is set up to fail as soon as a limit as reached. Exercise for
the reader: modify the parser to allow for sensible error recovery after
a limit has been reached.
The complete script looks like:
code| pre|
(import (scheme base) (phosphate))
(define comma/p
(discard/p (==/p #\,円)))
(define crlf/p
(==seq/p "\\r\\n"))
(define escaped-char/p
(cond/p
((==/p #\\") (==/p #\\"))
(else advance/p)))
(define escaped/p
(and/p (==/p #\\")
(lv/p (((max) (ref-parameter/p 'field-max))
((lst)
(many-until/p escaped-char/p
(and/p (==/p #\\")
(not/p (==/p #\\")))
\`((expected-sep-or-end-error
. EOF-in-escaped-text)
(max . ,max)
(expected-end-error . escaped-too-big)))))
(let ((str (if (list? lst) (list->string lst) lst)))
(cond/p
((or/p eof/p
(lookahead/p comma/p)
(lookahead/p crlf/p))
(return/p str))
(else (error/p 'garbage-after-escaped str)))))))
(define unescaped-char/p
(satisfy/p (lambda (x)
(and (not (char<? x #\\x20))
(not (char=? x #\\x22))
(not (char=? x #\\x2C))
(not (char=? x #\\x7F))))))
(define unescaped/p
(lv/p (((max) (ref-parameter/p 'field-max))
((lst) (many/p unescaped-char/p
\`((max . ,max)))))
(let ((str (if (list? lst) (list->string lst) lst)))
(cond/p
((or/p eof/p (lookahead/p comma/p) (lookahead/p crlf/p))
(return/p str))
((lookahead/p unescaped-char/p)
(error/p 'unescaped-too-big))
(else (error/p 'garbage-after-unescaped str))))))
(define field/p
(cond/p
((lookahead/p (==/p #\\")) escaped/p)
(else unescaped/p)))
(define record/p
(lv/p (((max) (ref-parameter/p 'record-max)))
(handle-errors/p
(many-until/p field/p
(lookahead/p (or/p crlf/p eof/p))
\`((sep/p . ,comma/p)
(max . ,max)
(expected-end-error . record-too-big)))
('return-from-record (lambda (k dto dict dyn mark value)
(values dict value))))))
(define csv-eof/p
(or/p eof/p (and/p crlf/p eof/p)))
(define csv/p
(lv/p (((max) (ref-parameter/p 'file-max)))
(many-until/p record/p
csv-eof/p
\`((sep/p . ,crlf/p)
(max . ,max)
(expected-end-error . file-too-big)))))
(define abort-after-field/p
(skip/p (and/p (not/p (or/p comma/p crlf/p eof/p))
advance/p)))
(define abort-field/p
(cond/p
((==/p #\\") (and/p (skip/p (cond/p
((==/p #\\") (==/p #\\"))
(else advance/p)))
(or/p (==/p #\\") (return/p))
abort-after-field/p))
(else abort-after-field/p)))
(define abort-record/p
(many-until/p abort-field/p
(lookahead/p (or/p eof/p crlf/p))
\`((sep/p . ,comma/p))))
(define (parse-csv str file-max record-max field-max)
(parse (phosphate-dto)
(char-sequence->dict "<stdin>" str)
(phosphate-empty-dict)
(parameterize/p csv/p
'file-max file-max
'record-max record-max
'field-max field-max)
('garbage-after-escaped
(lambda (k dto dict dyn mark str)
(k (lambda ()
((and/p abort-after-field/p
(return/p str))
dto dict dyn)))))
('garbage-after-unescaped
(lambda (k dto dict dyn mark str)
(k (lambda ()
((and/p abort-after-field/p
abort-record/p
(error/p 'return-from-record #f))
dto dict dyn)))))
('EOF-in-escaped-text
(lambda (k dto dict dyn mark . rest)
(k (lambda ()
(values dict #f)))))
(#t (lambda (k dto dict dyn mark . rest)
(values dict mark)))))
You can try some of the previous examples and see that they work.
code| verb|
(parse-csv "\"This is\r\n\"\"escaped\"\"\",this is not" #f #f #f) ; ⇒ (("This is\r\n\"escaped\" "this is not"))
We can also try some limited parses, and see them fail:
code| verb|
(parse-csv "one,two,three" #f #f 4 ) ; ⇒ 'unescaped-too-big
(parse-csv "this,record,is,too,big" #f 4 #f ) ; ⇒ 'record-too-big
(parse-csv "this,file\r\nis not too\r\nbig" 3 #f #f ) ; ⇒ (("this" "file") ("is not too") ("big"))
(parse-csv "this,file\r\nis too\r\nbig" 2 #f #f ) ; ⇒ 'file-too-big
This covers all the major features of Phosphate.
nh2| API
{index {l {e `dict`}} {ref `dict` argument parameter definition}}
All procedure arguments that start with {label `dict` `dict`} or are described as dictionaries must
be objects that are {code (dictionary? dto dict)} when
passed to the procedure.
nh3| Dictonaries
feature|
library| (phosphate)
procedure| name| phosphate-dto
returns| name| dto?
Default DTO.
feature|
library| (phosphate)
procedure| name| phosphate-empty-dict
returns| name| dict
Default empty dictionary.
feature|
library| (phosphate)
procedure| name| dict-invoke
args| name| dto
pred| `dict` dict
name| key
rest| name| arguments
returns| name| *
Invoke the procedure associated with {var key} in {var dict} with
{var arguments}. Raises an error when there is no mapping for {var key}.
nh3| Parser Entry
feature|
library| (phosphate)
syntax| {name parse} {non-terminal dto} {non-terminal dict} {non-terminal dyn} {non-terminal parser} {gstar ({non-terminal error} {non-terminal handler})}
Start a parser with {non-terminal dict} as the input dictionary,
{non-terminal dyn} as the initial dynamic state, {non-terminal dto}
as the DTO for both, {non-terminal parser} as the parser, and
{non-terminal handler} as the handler for each {non-terminal error}.
No handlers are installed by default, even for `#f`, so make sure
to install one or the parser will raise an exception when the parse
fails.
nh3| Fundamental Parsers
{index {l {e `parser`}} {ref `parser` argument parameter definition}}
{index {l {e `/p`}} {ref `slash-p` convention}}
All procedure arguments that start with {label `parser` `parser`},
or are described as parsers must be procedures that take
three arguments (a DTO, the global {ref `dict` dictionary}, and the dynamic
dictionary). A procedure that ends
in {label `slash-p` `/p`} is either a parser, a procedure that makes a
parser, or a macro that makes a parser.
The number of return values of a parser is the number of values returned by
the procedure minus the return dictionary: so a parser that "returns one value"
will return two values: the return dictionary and some Scheme value.
feature|
library| (phosphate)
procedure| name| return/p
args| rest| name| values
returns| pred| `parser` parser
Creates a parser that returns its input dictionary unchanged and the
{var values} passed to it.
feature|
library| (phosphate)
syntax| {name delay/p} {non-terminal expr}
Returns a parser that returns two values: its input dictionary unchanged,
and the {non-terminal expr} evaluated at the time of calling the parser.
feature|
library| (phosphate)
procedure| name| ref/p
args| name| key
returns| pred| `parser` parser
Creates a parser that returns the global dictionary unchanged, with the
second value being the value associated with {var key} in the global
dictionary.
feature|
library| (phosphate)
procedure| name| expose/p
args| name| dto
pred| `dict` dict
pred| `dict` dyn
returns| pred| `dict` dict
name| dto
pred| `dict` global-dict
pred| `dict` dyn
A parser that returns the global dictionary unchanged, with the dto,
global dictionary, and dynamic dictionary as values after it.
feature|
library| (phosphate)
procedure| name| test/p
args| name| obj
returns| pred| parser
Returns a parser that returns no values if {var obj} is true, and fails
if {var obj} is false.
feature|
library| (phosphate)
procedure| name| inject/p
args| name| obj
returns| pred| parser
Returns a parser that will inject {var obj} as the next object that will
be parsed. This object does not increase the line or character number.
The next object returned by the parser is the object that would be
returned if {idref inject/p} had not been parsed.
feature|
library| (phosphate)
procedure| name| discard/p
args| pred| parser
returns| pred| parser
Returns a parser that parses {var parser} and discards the return values
(but keeps the dictionary).
feature|
library| (phosphate)
procedure| name| eof/p
args| name| dto
pred| `dict` dict
pred| `dict` dyn
returns| pred| `dict` dict
Returns no values when the parser is at EOF, and fails otherwise.
nh3| Errors and Parse Failures
{index {l {e `fail mark`}} {ref `fail mark` definition}}
The {label `fail mark` {dfn fail mark}} is `#f`, and is used to denote a
parse failure that is not necessarily a reportable parse error. This is
used for backtracking.
{index {l {e `fallback mark`}} {ref `fallback mark` definition}}
The {label `fallback mark` {dfn fallback mark}} is `#t`.
When an error is raised and there is no handler for it, but there is
a handler for the fallback mark, then the handler for the fallback mark
is invoked.
feature|
library| (phosphate)
syntax| {name if/p}
{non-terminal conditional/p}
{non-terminal subsequent/p}
{non-terminal alternative/p}
Each expression must evaluate to a parser.
Returns a parser that, when parsed, will evaluate the passed parsers.
The parser then parses {var conditional/p} with a dynamic state
handling the fail mark.
If that fails, then parse {var alternative/p} in the tail-context of
the call to {var if/p} and the input dictionary.
Otherwise, if {var conditional/p} returns the dictionary {var dict{sub 2}}
and values {var vals}, then parse {code (apply subsequent/p vals)} with
the dictionary {var dict{sub 2}}.
This is a macro and not a procedure in order to delay the evaluation of
the subsequent and alternative so that it matches the behavior of Scheme's
`if`.
feature|
library| (phosphate)
syntax| {name cond/p}
{gstar {non-terminal clause}}
{gopt (else {non-terminal parser})}
indexed-ebnf|
r-name| clause
r-prod| ({non-terminal expr})
r-prod| ({non-terminal expr{sub 1}} {non-terminal expr{sub 2}})
r-prod| ({non-terminal expr{sub 1}} => {non-terminal formals}
{non-terminal expr{sub 2}})
Evaluates to a parser that will try the left hand side of each {ntref clause}
in order. If one matches, then it binds its results to {non-terminal formals}
(or just discards them), and then tail-parses the right hand side.
If no parser matches and there is a final `else` clause, then that parser
is tail-parsed. Otherwise the match fails.
feature|
library| (phosphate)
procedure| name| as-error/p
args| name| mark
pred| `parser` parser
rest| name| irritants
returns| pred| `parser` parser
Returns a parser that will parse {var parser} with a dynamic state handling
the fail mark. If the parser returns normally, then return the return dictionary
and values. If the parser fails with the fail mark, then raise an error with
{var mark}, the continuation of the call to {idref as-error/p}, DTO, and dictionary, with the {var irritants}
passed after it.
feature|
library| (phosphate)
procedure| name| error/p
args| name| mark
rest| name| irritants
returns| pred| `parser`
Returns a parser that aborts to the first handler marked with {var mark},
passing as arguments:
1| A continuation that takes a single argument, a thunk. The thunk is
called with the continuation of {idref error/p}.
2| The DTO.
3| The global state.
4| The dynamic state.
5| The mark that the error was raised with.
along with {var irritants} as rest arguments.
feature|
library| (phosphate)
procedure| name| fail/p
args| name| dto
pred| `dict` dict
pred| `dict` dyn
returns| pred| `dict` dict
Equivalent to {code (({idref error/p} #f) dto dict)}.
feature|
library| (phosphate)
syntax| {name handle-errors/p}
{non-terminal parser}
{gstar (({non-terminal name} {non-terminal formals})
{non-terminal handler})}
Parse {non-terminal parser} with each the evaluation of {non-terminal
name} assigned in the dynamic state to a handler with formals
{non-terminal formals} and body {non-terminal handler} in order of
inclusion. The handler is called with the continuation of
{idref handle-errors/p}.
The arguments to a handler depend
on how the handler was invoked. In all cases with the included procedures,
the handler is invoked using {idref error/p}, and so the next arguments
are those arguments.
nh3| Backtracking Parsers
The {idref or/p} combinator implements infinite lookahead. To use limited
lookahead, use {idref if/p} or {idref cond/p}. Unlike MegaParsack or other
PEG implementations, this does not track whether or not a branch of
{idref or/p} has consumed input. (This was the behavior of early versions
of Phosphate.) This is done to make parts of the parser that look ahead
explicit to make it easier to understand where error recovery will occur.
This also gets rid of dead stack frames and allows for idiomatic code to
iterate using tail-recursion.
feature|
library| (phosphate)
procedure| name| or/p
args| rest| pred| `parser`
returns| pred| `parser`
Returns a parser that will try each {var parser} in order. If that
succeeds, return its values.
Otherwise try the next parser, failing if all parsers fail.
feature|
library| (phosphate)
procedure| name| lookahead/p
args| pred| `parser`
returns| pred| `parser`
Returns a parser that parses {var parser}. If the parser succeeds, then
the returned dictionary is the input dictionary with no values. If it
fails it fails as normal.
Failure will return a continuation that is inside the dynamic extent
of {idref lookahead/p}.
feature|
library| (phosphate)
procedure| name| not/p
args| pred| `parser`
returns| pred| `parser`
Returns a parser that parses {var parser}. If that parse succeeds,
raise a parse failure with the returned dictionary. Otherwise, return
zero values and the input dictionary.
Failure will return the continuation of the invocation of {idref not/p}.
nh3| Predicates
feature|
library| (phosphate)
procedure| name| advance/p
args| name| dto
pred| `dict` dict
pred| `dict` dyn
returns| pred| `dict` dict
name| value
Returns a parser that returns the value at this point and a dictionary advanced this point if
there is a value at this point. EOF is a parse failure.
feature|
library| (phosphate)
procedure| name| satisfy/p
args| name| obj
name| predicate
returns| pred| parser
Returns a parser that returns a dictionary that is advanced one element
from this point if the value at this point if it satisfies
`predicate`, and fails otherwise.
feature|
library| (phosphate)
procedure| name| ==/p
args| name| obj
opt| name| equal?
returns| pred| parser
Returns a parser that returns a dictionary that is advanced one element
from this point if the value at this point if it is
`equal?` (defaults to {R7RS}'s `equal?`), to `obj`, and fails otherwise.
feature|
library| (phosphate)
procedure| name| !==/p
args| name| obj
opt| name| equal?
returns| pred| parser
Returns a parser that returns a dictionary that is advanced one element
from this point if the value at this point if it {em does not} satisfies
`equal?` (defaults to {R7RS}'s `equal?`), and fails otherwise.
feature|
library| (phosphate)
procedure| name| ==seq/p
args| name| sequence
opt| name| equal?
returns| pred| parser
{var sequence} is either a list, or a string or a vector, which are
converted to lists.
Returns a parser that matches each element in {var sequence} with the input stream.
If successful, it will return a dictionary increment as many positions as
there are elements of {var sequence}, and raises a parse failure otherwise.
feature|
library| (phosphate)
procedure| name| one-of/p
args| name| sequence
opt| name| equal?
returns| pred| parser
{var sequence} is a list.
Returns a parser that will return the dictionary advanced one past the
current element and the element, if it is `equal?` (defaults to {R7RS}
`equal?`) to any one of the elements of {var sequence}.
nh3| Sequencing
feature|
library| (phosphate)
syntax| {name lv/p}
({gstar ({non-terminal formal} {non-terminal parser})})
{non-terminal parser{sub final}}
Expands to a {ref `parser`} that parses each {non-terminal parser} in
order, binding the returned values (without the dictionary) to the
{non-terminal formal}s interpeted as multiple-values bindings. The
returned dictionary from the previous parsers is used for future
parsers. Then the {non-terminal parser{sub final}} is parsed, and
its continuation is the continuation of the invocation of
{idref lv/p}.
feature|
library| (phosphate)
procedure| name| values/p
args| rest| pred| parser
returns| pred| parser
Returns a parser that parses each parser in sequence like in
{idref lv/p}. The values are returned, appended from this
parser.
feature|
library| (phosphate)
procedure| name| and/p
args| rest| pred| parser
returns| pred| parser
Returns a parser that parses each parser in order, discarding all
return results except the final parser, which is tail-called.
nh3| Repetition
feature|
library| (phosphate)
procedure| name| many/p
args| pred| parser
opt| name| kw
returns| pred| parser
The keywords argument is an association list that accepts keys
`'min`, `'max`, `'sep/p`, `'too-little-error`, and
`'expected-after-sep-error`.
Returns a parser that parses {var parser} between {var min} (defaults
to `0`) to {var max} (defaults to `#f`, which is equivalent to `+inf.0`).
If {var sep/p} is `#f` (the default), then the parser will detect the
end of the repetition when {var parser} fails to parse.
If {var sep/p} is {em not} `#f`, then it must be a parser. If the
parse of {var sep/p} fails, then the parser will halt repetitions.
After the first parse, the returned parser attempts to parse {var
sep/p}. If the parse of {var sep/p} succeeds, then it will try to
parse {var parser}. If parsing {var parser} fails, then an error
with mark {var expected-after-sep-error} (defaults to `#f`) is raised
with arguments:
1| the number of elements matched,
2| the minimum number of elements that can be matched,
3| the maximum number of elements that can be matched,
4| a procedure of two arguments (an integer and a list), that returns a
parser that will continue to parse, where the number of
successful parses is the first argument, and the accumulated list of
values in reverse is the second element,
5| the values returned by the parse of {var sep/p}.
The continuation of this error returns from the parse of {idref many/p}.
If the parser fails due to matching too little elements, then it will
raise an error with error {var too-little-error}, which defaults to `#f`.
The arguments passed to {var too-little-error} are
1| the number of elements matched,
2| the minimum number of elements that can be matched,
3| the maximum number of elements that can be matched,
4| a procedure of two arguments (an integer and a list), that returns a
parser that will continue to parse, where the number of
successful parses is the first argument, and the accumulated list of
values in reverse is the second element,
The continuation returns
the accumulated list to whatever parsed the call to {idref many/p}.
The parser will return a list of values, appended, from each parse of
`sep/p` and `parser/p`.
feature|
library| (phosphate)
procedure| name| many-until/p
args| pred| parser
pred| `parser` end/p
opt| name| kw
returns| pred| parser
Returns a parser that parses {var parser} repeatedly until it parses
{var end/p}.
The keywords argument is an association list that accepts keys
`'min`, `'max`, `'sep/p`, `'too-little-error`,
`'expected-after-sep-error`, `'expected-sep-or-end-error`, and
`'expected-end-error`.
The returned parser will try to parse {var end/p}. If that succeeds,
then its values are discarded and the parse either ends with
{var too-little-error} as in {idref many/p} (i.e. if {var min}
elements are not matched), or the list of elements parsed is
returned.
If the maximum number of elements has been parsed, then the parser
will try to parse {var end/p}, failing with {var expected-end-error}
(defaults to `#f`) if it could not.
If it does not parse {var end/p}, then it will parse {var sep/p}
like in {idref many/p}.
If neither works, then the parser raises an error of mark
{var expected-sep-or-end-error} (defaults to `#f`), with the
same continuation and values passed to it as {var too-little-error}.
feature|
procedure| name| skip/p
args| pred| parser
returns| pred| parser
Returns a parser that matches {var parser} zero or more times, discarding
the results and returning no values.
nh3| Parameterization
feature|
library| (phosphate)
procedure| name| parameterize/p
args| pred| parser
rest| name| key
name| val
returns| pred| parser
Returns a parser that tail-calls {var parser} with each {var key} mapped to
its respective {var val} in the dynamic state of the call.
feature|
library| (phosphate)
procedure| name| parameterize-update/p
args| name| key
name| updater
pred| parser
returns| pred| parser
Returns a parser that tail-calls {var parser} with {var key} mapped to
`(dict-ref dto dyn key updater)` in the dynamic extent of the call.
feature|
library| (phosphate)
procedure| name| ref-parameter/p
args| name| key
returns| pred| parser
Returns a parser that returns its global dictionary unchanged with a
single extra value, the value that {var key} is mapped to in the dynamic
state.
nh3| Making Dictionaries
A dictionary has (generally) the following keys:
li| `here`: The current element to match against.
li| `eof?`: Is the parser at EOF?
li| `next`: A procedure that takes a DTO and a dictionary, and returns a
dictionary that is at the next element in the sequence. This must
memoize the result, so that streams are lazy.
li| `line-number`: The line number (when parsing chars).
li| `offset`: The character offset (when parsing chars).
li| `filename`: Filename.
feature|
library| (phosphate)
procedure| name| parser-init
args| name| dto
pred| `dict` dict
returns| pred| `dict` dict
Advances {var dict} if `here` is not a key.
feature|
library| (phosphate)
procedure| name| char-sequence->dict
args| name| filename
name| input
returns| pred| `dict` dict
Input must be either a list of chars, a string, a vector of chars, or
a textual input port.
Returns a dictionary that parses each character in the sequence, with
line numbers and offsets.
feature|
library| (phosphate)
procedure| name| char-position-wrapper
args| name| dto
pred| `dict` dict
name| filename
Returns a dictionary with the {var filename}, tracking line and offset
numbers starting from the `line-number` and `offset` in the dictionary,
which defaults to 1 and 0 respectively.
feature|
library| (phosphate)
procedure| name| port-like->dict
args| name| dto
pred| `dict` dict
name| accessor
name| port
returns| pred| `dict` dict
Returns a dictionary that parses input by calling {var accessor} on
{var port}.
feature|
library| (phosphate)
procedure| name| char-port->dict
args| name| dto
pred| `dict` dict
name| port
returns| pred| `dict` dict
Returns a dictionary that parses input by calling {var read-char} on
{var port}.
feature|
library| (phosphate)
procedure| name| u8-port->dict
args| name| dto
pred| `dict` dict
name| port
returns| pred| `dict` dict
Returns a dictionary that parses input by calling {var read-u8} on
{var port}.
feature|
library| (phosphate)
procedure| name| generator->dict
args| name| dto
pred| `dict` dict
name| generator
returns| pred| `dict` dict
Returns a dictionary that parses input by invoking {var generator} with
no arguments.
feature|
library| (phosphate)
procedure| name| list->dict!
args| name| dto
pred| `dict` dict
name| list
returns| pred| `dict` dict
Returns a dictionary that parses input by reading from the list in
sequence. Mutating the list will cause undefined behavior.
feature|
library| (phosphate)
procedure| name| list->dict
args| name| dto
pred| `dict` dict
name| list
returns| pred| `dict` dict
Returns a dictionary that parses input by reading from the list in
sequence. The list is copied beforehand.
feature|
library| (phosphate)
procedure| name| string->dict
args| name| dto
pred| `dict` dict
name| string
returns| pred| `dict` dict
Returns a dictionary that parses input by reading from the string in
sequence.
nh2| JSON Parser
This is a set of parser combinators for
{href `https://www.rfc-editor.org/rfc/rfc8259` RFC 8259} JSON. Instead of
offering a parser that parses a single JSON document, this library offers
a set of JSON parsers that can validate the input structure during parsing.
For instance:
code| pre|
({idref object*/p} ({idref string/p})
"first_name" ({idref string/p})
"last_name" ({idref string/p})
"age" ({idref number/p}))
will create a parser that will only accept objects with exactly three
keys, `"first_name"`, `"last_name"`, and `"age"` (in any order), that
take a string, a string, and a number respectively.
nh3| Atoms
feature|
library| (phosphate RFC8259)
procedure|
name| whitespace/p
args| name| dto
pred| `dict` dict
pred| `dict` dyn
returns| pred| `dict` dict
Skips internal whitespace and returns no values.
feature|
library| (phosphate RFC8259)
procedure|
name| true/p
args| name| dto
pred| `dict` dict
pred| `dict` dyn
returns| pred| `dict` dict
name| #t
Reads `true` and returns `#t` if the parse is successful.
feature|
library| (phosphate RFC8259)
procedure|
name| false/p
args| name| dto
pred| `dict` dict
pred| `dict` dyn
returns| pred| `dict` dict
name| #f
Reads `false` and returns `#f` if the parse is successful.
feature|
library| (phosphate RFC8259)
procedure|
name| null/p
args| name| dto
pred| `dict` dict
pred| `dict` dyn
returns| pred| `dict` dict
name| 'null
Reads `null` and returns `'null` if the parse is successful.
nh3| Numbers
feature|
library| (phosphate RFC8259)
procedure|
name| number/p
args| opt| name| max-of-part
returns| pred| parser
Returns a parser that reads JSON numbers, and returns a number parsed
using `number->string`. The maximum number of characters for each part
of the number is {var max-of-part} (defaults to infinite).
The parser will raise the following errors:
dt| `'no-digits-after-frac`
dd| Raised when `.` is parsed, but no digits follow it. The continuation
of this error expects a list of ASCII digits as characters.
dt| `'no-digits-after-e`
dd| Raised when `e` is parsed, but no digits follow it. The continuation
of this error expects a list of ASCII digits as characters.
feature|
library| (phosphate RFC8259)
procedure|
name| start-number/p
args| name| dto
pred| `dict` dict
pred| `dict` dyn
returns| pred| `dict` dict
Looks ahead to see if the input stream is at a character that starts a
number, which is an ASCII digit or `-`.
feature|
library| (phosphate RFC8259)
procedure|
name| delimits-number/p
args| name| dto
pred| `dict` dict
pred| `dict` dyn
returns| pred| `dict` dict
Looks ahead to see if the input stream is at a character that can follow
a well formed number. This is a whitespace character, the start/end of an
object, or `,`.
nh3| Strings
feature|
library| (phosphate RFC8259)
procedure|
name| as-string/p
args| name| string
returns| pred| parser
Returns a parser that parses a JSON string that is equal to {var string}.
The parser will parse a string that has the exact length of the
input string, and will return a newly allocated string if the string is
`equal?` to the {var string}.
feature|
library| (phosphate RFC8259)
procedure|
name| UTF16/p
args| name| dto
pred| `dict` dict
pred| `dict` dyn
returns| pred| `dict` dict
Parses a UTF-16 escape, starting with `\u`.
1| If an escape is not a surrogate pair, the parser parses one
`\u` escape and returns the character.
2| If the escape is a high surrogate, and the next character is an
escape for a low surrogate, then the parser will consume both
escapes and returns the character reprsented by the pair.
3| If the first `\u` is not a valid unicode sequence (it is not
four hexadecimal characters), then the parser raises a
`'invalid-unicode-escape` error, whose continuation accepts
one value, an integer representing the parsed codepoint. The
recommended method for handling this error is to return `#xFFFD`.
({em This is an integer, not a character.})
4| If the first `\u` is a low surrogate, or it is a high surrogate that
is not followed by a low surrogate sequence, then the parser raises
a `'lone-surrogate` error with the first codepoint as an integer
parsed. The continuation is the continuation of the parse of
`UTF-16/p`. The recommended method for handling this error is to
return `#\xFFFD`. ({em This is the character, not an integer.})
feature|
library| (phosphate RFC8259)
procedure|
name| unescaped/p
args| name| dto
pred| `dict` dict
pred| `dict` dyn
returns| pred| `dict` dict
Parses a character that is allowed to appear unescaped in a string.
feature|
library| (phosphate RFC8259)
procedure|
name| string/p
args| opt| name| min
opt| name| max
returns| pred| parser
Returns a parser that parses a string with minimum length
{var min} (default 0) and maximum length {var max}.
The parser will raise the same errors as {idref UTF16/p}. In addition,
it will return
dt| `'expected-end-of-string`
dd| The end double quote `"` was not found, either because of EOF or
because an unescaped character was found. The string parsed
up to that point is passed. The continuation of this error returns
the string that is returned by the parse of {idref string/p}.
dt| `'unknown-escape`
dd| An unknown escape character was found. The parser is at the unknown
escape character. The continuation of this error returns a
character that is inserted into the string.
nh3| Objects
feature|
library| (phosphate RFC8259)
procedure|
name| object/p
args| pred| `parser` key/p
pred| `parser` value/p
opt| name| min
opt| name| max
returns| pred| `parser`
Returns a parser that reads a JSON object, where all keys match
{var key/p} and all values match {var value/p}. The object matched has
length between {var min} (default `0`) and {var max} (default infinite).
The returned value is an alist.
The following errors are raised by returned parser:
dt| `'expected-key-separator`
dd| The character that separates the key and value in the object is
missing. The continuatuion takes no values and tries to parse the
value.
dt| `'expected-value`
dd| The value parser failed. The key is passed as an argument.
The continuation takes one value, which is the value for the key.
dt| `'object-too-small`
dd| The object parsed was smaller than the length limit. The arguments
are the minimum length, maximum length, and the accumulated alist
(in reverse). The continuation of this error accepts the whole alist
for the parser, and tries to match `}`.
dt| `'expected-end-of-object`
dd| An invalid char or EOF was found in the middle of the object. The
alist is passed as an argument. The continuation of this error accepts
the whole alist.
feature|
library| (phosphate RFC8259)
procedure|
name| object-dict/p
args| pred| `parser` string/p
name| key-dto
name| key-dict
returns| pred| `parser`
Parses a JSON object that has the same keys as the keys of {var key-dict}
(in the sense of `equal?`). The parsed values are parsed according to
the parser associated with each key in {var key-dict}.
The errors raised by the returned parser include the errors returned by
{idref object/p}, and in addition
dt| `unexpected-key`
dd| The object matched a key that is either not in the dictionary, or
was already matched. The handler is passed the key, and the accumulated
alist (in reverse). The
continuation of this call returns the object.
dt| `unexpected-value`
dd| A parse of the value for the key raised a parse failure. The handle
is passed the key. The continuation of this error returns the value
that was to be parsed.
feature|
library| (phosphate RFC8259)
procedure|
name| object*/p
args| pred| `parser` string/p
rest| name| string
pred| parser
returns| pred| `parser`
Equivalent to calling {idref object-dict/p} with a dictionary that
maps each {var string} to each {var parser}.
nh3| Arrays
feature|
library| (phosphate RFC8259)
procedure|
name| array/p
args| pred| parser
opt| name| min
opt| name| max
returns| pred| parser
Create a parser that parses a JSON array. Each element in the array is
{var parser}, and the array must be between {var min} elements
(default 0) and {var max} elements (default infinity).
The errors raised by the returned parser are the errors raised by
{var parser}, and
dt| `'array-too-small`
dd| The array parsed was smaller than the length limit. The arguments
are the minimum length, maximum length, and the accumulated array
(as a list in reverse). The continuation of this error accepts
the whole list (in regular order as a vector) for the parser,
and tries to match `]`.
dt| `expected-end-of-array`
dd| An unexpected character or EOF was found. The arguments to this is
the array (as a vector). The continuation of this
error accepts the array as a vector.
feature|
library| (phosphate RFC8259)
procedure|
name| array*/p
args| pred| parser
rest| name| value/p
returns| pred| parser
Create a parser that parses a JSON array with the same number of elements
as arguments to this procedure, and where the {var n}th element is parsed
by the {var n}th parser.
The errors raised by the returned parse are the errors raised by the
parsers, the errors raised by {idref array/p}, and in addition
dt| `'unexpected-array-value`
dd| The parser that parses the value in this part of the array failed.
The continuation accepts one value, the value in place of the failed
parse.
nh3| A JSON File
feature|
library| (phosphate RFC8259)
procedure|
name| RFC8259/p
args| name| dto
pred| `dict` dict
pred| `dict` dyn
returns| pred| `dict` dict
Parses a JSON file. The errors raised by this parser is the union of
the errors raised from {idref number/p}, {idref object/p},
{idref array/p}, and {idref string/p}.
nh2| Copyright
© Peter McGoron 2026
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
nh2| Index
the-index|