byte-machine
byte-machine builds fast, resumable byte parsers for Chez Scheme, in the manner of llparse.
You describe a parser as a state machine over bytes, using ordinary Scheme procedures. byte-machine checks the machine and writes it out as a plain R6RS library. That library parses input in pieces: feed it bytes as they arrive, split anywhere, and it calls your functions to say what it found.
The generated library needs only (rnrs) and one small runtime library,
(byte-machine runtime). Your application does not load the builder, the
checker or the code generator.
💡 These pages were generated from Markdown with chez-cmark-gfm.
A taste
A machine that reads lowercase words separated by spaces, ending at a newline. Each word is a span: its bytes are handed to a callback as they arrive.
(define words
(make-machine 'words 'between '()
(list (make-callback-declaration 'on-word 'span-data #t '() #f #f)
(make-callback-declaration 'on-word-end 'span-complete #t '() #f #f))
(list (make-span-declaration 'word 'on-word 'on-word-end #f))
(list (make-error-declaration 'bad 'unexpected-byte "expected a letter, a space or a newline" #f)
(make-error-declaration 'eof 'unexpected-eof "input ended before the newline" #f))
(list
(make-byte-dispatch-node 'between
(list (make-byte-case is-letter (make-edge 'open 'reexecute) #f)
(make-byte-case is-space (make-edge 'between 'advance) #f)
(make-byte-case is-newline (make-edge 'done 'advance) #f))
(make-edge 'reject 'reexecute) #f #f)
(make-span-start-node 'open 'word 'in-word #f)
(make-byte-dispatch-node 'in-word
(list (make-byte-case is-letter (make-edge 'in-word 'advance) #f))
(make-edge 'close 'reexecute) #f #f)
(make-span-end-node 'close 'word 'after-word #f)
(make-byte-dispatch-node 'after-word
(list (make-byte-case is-space (make-edge 'between 'advance) #f)
(make-byte-case is-newline (make-edge 'done 'advance) #f))
(make-edge 'reject 'reexecute) #f #f)
(make-stop-node 'done #f)
(make-error-node 'reject 'bad #f))
'eof #f))
(is-letter, is-space and is-newline are byte matchers, defined in the
full program.) Generate it once, at build time. Then feed it one line split
across two reads, in the middle of a word:
(feed! "hello wor")
(feed! "ld\n")
on-word "hello"
on-word-end
on-word "wor"
feed "hello wor" -> need-input, consumed 9
on-word "ld"
on-word-end
feed "ld\n" -> stopped, consumed 3
The parser kept no copy of "hello wor". It remembered only that it was
inside a word, and carried on from there when "ld\n" arrived.
The complete program is in examples/words/, and
Usage walks through it line by line.
What you get
- Resumable parsing. Every state survives a feed boundary, so a token split across any number of feeds parses exactly as it does in one buffer.
- No input buffering. Spans arrive as ranges into the bytevector you fed. The runtime keeps no reference to it once the feed returns, and the generated per-byte path allocates nothing, unless a 64-bit field's value reaches 2^60 and becomes a bignum.
- The pieces protocol parsers need. Byte dispatch, byte classes, fixed sequences, keyword tables, unsigned integer fields with overflow checks, counted reads ("the next N bytes are the body"), callbacks that can pause the parser, and spans.
- Precise errors. An error names its code, the node it happened at, the stream offset, and whether the fault was at a byte, between two bytes, or at the end of input.
- A checked machine. Every machine passes a verifier before any code is written. It rejects unreachable states, loops that make no progress, and ends of input that land in the wrong place.
- A second implementation to test against. A reference interpreter runs the same machine. Run both on the same input and compare: a code-generator bug shows up as a disagreement.
Contents
- Installing: what you need, and how to put the libraries on Chez's path.
- Usage: build, check, generate and run the words parser, step by step.
- Building machines: every node kind, matcher and edge.
- Fields and counts: numbers in the machine, read from the input or set by your application.
- Callbacks and spans: what your functions receive, and how long they may keep it.
- Running a parser: feeding, finishing, pausing, resetting, and what each result means.
- Errors: input errors, callback failures, and programming mistakes.
- The verifier: what it checks, and every diagnostic code.
- Generated code: what the generator writes, and how to ship it.
- Testing a grammar: compare the generated parser with the interpreter.
- Why llparse-style parsers: where the design comes from, and why it suits HTTP and URI parsing in servers.
- API reference: every exported binding.
- Building: build and test byte-machine itself, and this site.