1
1
Fork
You've already forked phosphate
0
Parser combinators with restartable parser errors for error recovery https://florida.moe/phosphate
  • Scheme 100%
Find a file
2026年02月07日 20:54:58 -05:00
lib finish tutorial, check in tests that I forgot 2026年02月07日 20:11:33 -05:00
tests finish tutorial, check in tests that I forgot 2026年02月07日 20:11:33 -05:00
.gitignore 0.1 2026年02月07日 20:54:58 -05:00
phosphate.egg 0.1 2026年02月07日 20:54:58 -05:00
phosphate.release-info 0.1 2026年02月07日 20:54:58 -05:00
README.tsm 0.1 2026年02月07日 20:54:58 -05:00

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|