Overview
The bjodat value, its reader and its writer: Bjolang’s shapes read as data.
(import (text bjodat-core))
(parse-bjodat "{:a 1 :b [2 3]}") ; (Ok (BjoMap ...))
(parse-bjodat-all "1 2 3") ; (Ok (list (BjoInt 1) ...)), a file is a stream
(bjodat->string v) ; the canonical spelling, maps flat
bjodat is Bjolang’s shapes read as data: no evaluation, no macro expansion, no environment. A file is what it says it is. It is what a package manifest, a lockfile or a config file is written in.
This module is the machinery, not the interface. A program that declares
records imports (text bjodat): its def/bjodat-type reads the
text straight into a record, with no Bjodat value in between, and
it re-exports Bjodat, its cases, Reader, the one-pass
driver and bjodat->string, so that one import covers the
generator, the traits and the writer. Import this module by name when you
want the tree: parse-bjodat, read-bjodat,
bjodat-ref and the entry accessors, for data whose shape is not
known until run time.
The format. Two differences from source, and they are the whole of it:
- A bare identifier is a
Symbol. It is already quoted, because there is nothing here to evaluate and so nothing for a quote to protect it from.'foois refused: the mark would be a second spelling of one value. {...}is a map. In source a brace opens a comprehension, and a comprehension is a loop, and a loop is not data.
42 -100 3.14159 1e10 0x2A 0b101010 ;; integer or double, and nothing else
+inf.0 -inf.0 +nan.0 ;; the doubles that are not numbers, as Scheme spells them
#t #f ;; the booleans; `true` is a symbol
#\a #\newline #\space ;; characters, as in source
"a \"quoted\" word\n" ;; strings, escaped as source escapes
"""a "raw" word""" ;; raw strings, as source writes them
:key #:key ;; a keyword, either spelling
a-symbol bjolang/http net8.0 ;; symbols; `.` and `/` are in a name
(a list "of" :anything) ;; a list
["a" "vec" "of" "strings"] ;; a vec, the one sequence type
{:name "http" :version "1.2"} ;; a map, flat
{(:name "http") (:version "1.2")} ;; a map, parenthesised
;; a comment, to the end of the line
The escapes are those source has: \n, \t, \r,
\\, \", \0 and \uXXXX. The named characters
are #\newline, #\space, #\tab, #\return and
#\nul. A byte order mark at the start of the text is skipped.
A raw string follows the rules in Syntax.org, “Raw strings”,
margin and all. The writer still writes every string escaped and on one
line: a raw string written back out would need a margin to fit into the
text around it, and bjodat->string writes a value on its own.
A number has no width suffix. 21uy and 21L name a .NET
type, and which type a field holds is the record’s to say, not the file’s.
A number is a long unless it has a fraction or an exponent, or is
too wide for a long, and then it is a double. - is a
symbol of its own, and the start of a number when a digit follows it.
+inf.0, -inf.0 and +nan.0 are the infinities and
NaN, spelled as Scheme spells them; -nan.0 reads as NaN too.
There is no #[...]. Source splits vec from array because one is
immutable and the other is a .NET array; data is neither, so a second
spelling would read the same and mean nothing.
A comma is not whitespace. Source lexes , as unquote, so a stray
one is a token rather than a decoration, and it is refused. Separate with
spaces.
Maps. Two spellings, and no third. Which one a map is, is
decided by the character after the brace: a ( means the
parenthesised form, anything else means the flat one. They do not mix,
and a map that starts one way and continues the other is refused.
That rule is why a list may not be a key. If it could,
{(a b) 1} would be both a flat map keyed by the list
(a b) and a parenthesised map whose first entry is a -> b.
A key is a name, and a name is a keyword, a symbol, a string, a number
or a char, never a list. A flat map with an odd number of forms is the
other refusal.
Entries keep the order they were written in, and writing one back out writes them in that order. A manifest that round-trips through a record comes back looking like what a person wrote, not sorted by hash.
A file is a stream. Nothing requires a file to hold exactly one
value, which is what makes a log or a lockfile writable by appending.
parse-bjodat asks for exactly one and refuses trailing text;
parse-bjodat-all takes them all, and an empty file is an empty
list rather than an error.
The tree. A BjoMap holds a flat (Vec Bjodat) of
alternating key and value, in written order, not a trie. Building a trie
was about seventy percent of what reading a keyed document cost, and a
record decoded from one reads every field once and throws it away. Reach
into it with bjodat-entry-count, bjodat-entry-key,
bjodat-entry-value and bjodat-ref. If you want a
dictionary, build one from the entries.
Refusals. Every reader answers (Result string _). A
refusal while reading carries the line and column where it was found:
line 1, column 12: Package: this map is written flat, so a '(' here is an entry of the other form
The one-pass machinery. Reader and the functions from
open-map! to parse-one-pass-all are what a decoder
generated by def/bjodat-type drives to read a record without a
Bjodat tree standing between the text and the fields. They are
small and awkward on purpose: the caller is generated code that already
knows its field names, and drives the loop itself rather than handing in
a table, because a table would be the thing this is trying not to build.
They are not an API for people, but the entry points
(parse-one-pass and the rest) are how a generated
bjodat-read-<Name> is run.
See also: Bjodat, parse-bjodat, bjodat->string, bjodat-ref
Reference
bjodat->stringbjodat-entry-countbjodat-entry-keybjodat-entry-valuebjodat-refclause-values!end-clause!end-entry!field-valuekey=?next-field!next-key!open-form!open-map!parse-bjodatparse-bjodat-allparse-one-passparse-one-pass-allread-bjodatread-bjodat-allread-one-passread-one-pass-allskip-clause!skip-value!
Types
Bjodat
union
A bjodat value: what a document reads as, as a tree.
BjoBool#tor#f.BjoInt- An integer, always a
long. BjoFloat- A number with a fraction or an exponent, or one too wide for a
long; or+inf.0,-inf.0,+nan.0. BjoStr- A string, raw or escaped.
BjoChar- A character,
#\aor#\newline. BjoSym- A bare identifier. It is data, so it needs no quote.
BjoKey- A keyword, written
:keyor#:key. BjoList- A
(...). BjoVec- A
[...]. BjoMap- A
{...}: key, value, key, value, an even length, in written order, whichever spelling it was written in.
(match (parse-bjodat "{:name \"http\"}")
((Ok (BjoMap entries)) (bjodat-ref entries :name)) ; (Some (BjoStr "http"))
(_ None))
A BjoMap is not a trie or a dictionary but a flat vec of
alternating key and value. Read it with bjodat-entry-count,
bjodat-entry-key, bjodat-entry-value and
bjodat-ref.
See also: parse-bjodat, bjodat->string, bjodat-ref
Reader
record
A reader over a port, as a generated one-pass decoder drives it. Not an API for people.
port- What it reads from.
held- The one character of lookahead, as a code; -1 at the end of the port.
row- The line the held character is on, from 1.
col- Its column, from 1.
scratch- One buffer for every string, symbol, number and key name in the document.
stack- One array for every list, vec and map in the document.
top- How much of the stack is in use.
names- A small cache of the names read, so that a repeated key does not build a new string.
Made by read-one-pass and its neighbours and handed to the
decoder they are given. A program has no reason to touch its fields.
See also: read-one-pass, parse-one-pass
Functions
read-bjodat
function
(: read-bjodat (-> TextInputPort (Result string Bjodat)))
(read-bjodat port)
Reads exactly one value from a port, and then the end.
port- The port to read from, to its end.
- returns
(Ok value), or(Err message)naming the line and column. Text after the value is refused.
See also: parse-bjodat, read-bjodat-all
parse-bjodat
function
(: parse-bjodat (-> string (Result string Bjodat)))
(parse-bjodat text)
Reads exactly one value from a string.
text- The document.
- returns
(Ok value), or(Err message)naming the line and column. Text after the value is refused.
(parse-bjodat "{:a 1 :b [2 3]}") ; (Ok (BjoMap ...))
(parse-bjodat "1 2") ; (Err "line 1, column 3: the value ended here, ...")
See also: read-bjodat, parse-bjodat-all, bjodat->string
read-bjodat-all
function
(: read-bjodat-all (-> TextInputPort (Result string (List Bjodat))))
(read-bjodat-all port)
Reads every value from a port, in order.
port- The port to read from, to its end.
- returns
(Ok values), or the first refusal. An empty document is the empty list.
A file is a stream of values, which is what makes a log or a lockfile writable by appending.
See also: parse-bjodat-all, read-bjodat
parse-bjodat-all
function
(: parse-bjodat-all (-> string (Result string (List Bjodat))))
(parse-bjodat-all text)
Reads every value in a string, in order.
text- The document.
- returns
(Ok values), or the first refusal. An empty document is the empty list.
See also: read-bjodat-all, parse-bjodat
bjodat->string
function
(: bjodat->string (-> Bjodat string))
(bjodat->string v)
Writes a value in the canonical spelling.
v- The value.
- returns
- Its text: maps flat, entries in the order they are held, strings escaped and on one line.
(bjodat->string (BjoVec [(BjoInt 1) (BjoSym 'a)])) ; "[1 a]"
Strings are always written escaped, never raw: a raw string written back
out would need a margin to fit into the text around it. A double is
written in the shortest form that reads back as the same double, and
always with a fraction or an exponent, so that it reads back as a
BjoFloat: 2.0 is written 2.0, and 1e20
1E+20. NaN and the infinities are written +nan.0,
+inf.0 and -inf.0. Symbols
and keywords are written as their names, unchecked.
See also: parse-bjodat
bjodat-entry-count
function
(: bjodat-entry-count (-> (Vec Bjodat) int))
(bjodat-entry-count entries)
How many entries a map’s vec holds.
entries- What a
BjoMapholds. - returns
- Half its length.
See also: bjodat-entry-key, bjodat-entry-value
bjodat-entry-key
function
(: bjodat-entry-key (-> (Vec Bjodat) int Bjodat))
(bjodat-entry-key entries i)
The key of a map’s i-th entry.
entries- What a
BjoMapholds. i- The entry, from 0.
- returns
- Its key, which may be any value but a list.
See also: bjodat-entry-value, bjodat-entry-count
bjodat-entry-value
function
(: bjodat-entry-value (-> (Vec Bjodat) int Bjodat))
(bjodat-entry-value entries i)
The value of a map’s i-th entry.
entries- What a
BjoMapholds. i- The entry, from 0.
- returns
- Its value.
See also: bjodat-entry-key, bjodat-entry-count
bjodat-ref
function
(: bjodat-ref (-> (Vec Bjodat) Keyword (Option Bjodat)))
(bjodat-ref entries k)
The first value under a keyword in a map.
entries- What a
BjoMapholds. k- The keyword.
- returns
(Some value)for the first entry underk, in written order, orNone.
(bjodat-ref entries :version)
A linear scan comparing interned keywords by reference. That is the
right shape for a record’s handful of fields and the wrong one for a map
with thousands of keys used as a lookup table: if you want a dictionary,
build one from the entries. Only a BjoKey key can match, so an
entry under a symbol or a string is never found. A duplicate key: the
first wins.
See also: bjodat-entry-count
open-map!
function
(: open-map! (-> Reader string (Result string bool)))
(open-map! rd owner)
Steps over the brace that opens a map, and says which spelling it is in. Not an API for people.
rd- The reader.
owner- The record's name, for messages.
- returns
(Ok #t)when the entries are parenthesised,(Ok #f)when flat; a refusal when there is no map here.
Decided by the first thing inside the brace, as the tree reader decides it; every entry after has to agree.
See also: next-key!, end-entry!
next-key!
function
(: next-key! (-> Reader string bool (Result string bool)))
(next-key! rd owner paired)
Takes the next key’s name into the buffer, without interning it. Not an API for people.
rd- The reader.
owner- The record's name, for messages.
paired- What
open-map!answered. - returns
(Ok #t)when a key is in the buffer and the reader stands at its value,(Ok #f)when the map closed.
A key that is not a keyword is read and dropped, and leaves the buffer
empty, which no field name can equal: it takes the path an unknown key
takes, as in the tree decoder, where bjodat-ref only matches a
BjoKey.
See also: key=?, end-entry!, skip-value!
end-entry!
function
(: end-entry! (-> Reader string bool (Result string bool)))
(end-entry! rd owner paired)
Steps over the ) of a parenthesised entry; nothing in the flat form. Not an API for people.
rd- The reader.
owner- The record's name, for messages.
paired- What
open-map!answered. - returns
(Ok #t), or a refusal when the entry holds more than a key and a value.
See also: next-key!
key=?
function
(: key=? (-> Reader string bool))
(key=? rd name)
Whether the name in the reader’s buffer is this one. Not an API for people.
rd- The reader.
name- A field name or tag, without its colon.
- returns
- #t when they are equal.
The comparison the one-pass decoder exists for: no string is built and nothing is interned, for a key, a clause name or a tag.
See also: next-key!, next-field!
field-value
function
(: field-value (-> Reader (Result string Bjodat)))
(field-value rd)
Reads a field’s value, as a Bjodat for its type’s instance to convert. Not an API for people.
rd- The reader, standing at the value.
- returns
- The value, or a refusal.
The value is still read as a Bjodat and converted by the field
type’s own bjodat-> instance. That keeps every instance, including
one a user wrote, working, and every message identical to the tree
decoder’s. The entry is what the one-pass decoder saves, not the value.
See also: skip-value!
skip-value!
function
(: skip-value! (-> Reader (Result string bool)))
(skip-value! rd)
Reads an unknown key’s value and drops it. Not an API for people.
rd- The reader, standing at the value.
- returns
(Ok #t), or a refusal.
It has to be read: a value is not a token, and finding where it ends means parsing it.
See also: field-value, skip-clause!
open-form!
function
(: open-form! (-> Reader string string (Result string bool)))
(open-form! rd owner tag)
Steps over ( and a tagged form’s head, refusing any head but the tag. Not an API for people.
rd- The reader.
owner- The record's name, for messages.
tag- The head the record is written under.
- returns
(Ok #t), or a refusal such asexpected (package ...), and this is (banana ...).
The tag is matched against the buffer, so a form costs no string for its own name; one is built only on the way to refusing.
See also: next-field!, skip-clause!
next-field!
function
(: next-field! (-> Reader string (Result string bool)))
(next-field! rd owner)
Takes the next clause’s field name into the buffer. Not an API for people.
rd- The reader.
owner- The record's name, for messages.
- returns
(Ok #t)when a name is in the buffer and the reader stands at the clause's first value,(Ok #f)when the form closed.
See also: key=?, end-clause!, skip-clause!, clause-values!
end-clause!
function
(: end-clause! (-> Reader string string (Result string bool)))
(end-clause! rd owner name)
Steps over the ) of a clause holding one value. Not an API for people.
rd- The reader.
owner- The record's name, for messages.
name- The field's name, for messages.
- returns
(Ok #t), or a refusal when the clause holds more than one value.
The owner and the field name arrive apart and are joined only on the way to refusing: joined by the caller they would be a string built per field per record.
See also: next-field!
skip-clause!
function
(: skip-clause! (-> Reader (Result string bool)))
(skip-clause! rd)
Reads an unknown clause to its ), whatever it holds, and drops it. Not an API for people.
rd- The reader, after the clause's name.
- returns
(Ok #t), or a refusal.
Any arity, and that is the point of it. A map’s unknown key is one value and done, but a clause may hold none, one or five, and a reader that insisted on one would make every new clause a breaking change for every older tool. This is what keeps a manifest forward-compatible.
See also: skip-value!
clause-values!
function
(: clause-values! (-> Reader (Result string (List Bjodat))))
(clause-values! rd)
Reads every value of a clause, for a #:rest field, and its ). Not an API for people.
rd- The reader, after the clause's name.
- returns
- The values in written order, or a refusal.
The values are read as Bjodat and converted by the element type’s
own instance. read-value carries the depth budget, so a manifest
nesting (package (depends (package (depends ...)))) five hundred
deep is refused by bjodat-max-depth rather than by the stack. A
#:rest that recursed straight back into a generated decoder would
have no budget at all.
See also: next-field!, field-value
read-one-pass
function
(: read-one-pass (-> TextInputPort (-?-> Reader (Result string %a)) (Result string %a)))
(read-one-pass port decode)
Runs a one-pass decoder over a port: one record, and then the end.
port- The port to read from, to its end.
decode- A generated
bjodat-read-<Name>. - returns
- What the decoder answered; text after the record is refused.
The decoder is typed -?->, so that a bjoroutine calling this gets
the decoder’s suspending copy rather than the one that parks the thread.
Pass it by name, not wrapped in a (fun (rd) ...): a lambda body is
sealed to the sync copy of what it calls.
See also: parse-one-pass, read-one-pass-all
parse-one-pass
function
(: parse-one-pass (-> string (-?-> Reader (Result string %a)) (Result string %a)))
(parse-one-pass text decode)
Runs a one-pass decoder over a string: one record, and then the end.
text- The document.
decode- A generated
bjodat-read-<Name>, passed by name. - returns
- What the decoder answered; text after the record is refused.
A generated bjodat-parse-<Name> is exactly this with its own
decoder.
See also: read-one-pass, parse-one-pass-all
read-one-pass-all
function
(: read-one-pass-all (-> TextInputPort (-?-> Reader (Result string %a)) (Result string (List %a))))
(read-one-pass-all port decode)
Runs a one-pass decoder over each record of a […] read from a port.
port- The port to read from.
decode- A generated
bjodat-read-<Name>, passed by name. - returns
- The records in order, or the first refusal. The document has to be a vec.
A List rather than a Vec: it is built by consing and
turned round once, and a caller that folds over it once, which is most
of them, should not pay for a tree it does not index into.
See also: parse-one-pass-all, read-one-pass
parse-one-pass-all
function
(: parse-one-pass-all (-> string (-?-> Reader (Result string %a)) (Result string (List %a))))
(parse-one-pass-all text decode)
Runs a one-pass decoder over each record of a […] in a string.
text- The document, a vec of records.
decode- A generated
bjodat-read-<Name>, passed by name. - returns
- The records in order, or the first refusal.
(parse-one-pass-all text bjodat-read-Dep) ; (Result string (List Dep))
See also: read-one-pass-all, parse-one-pass
Values
bjodat-max-depth
value
(: bjodat-max-depth int)
How deep a value may nest before the reader refuses it: 200.
It applies to nesting in the reader, so a hostile document nested a thousand deep is refused with a message rather than by the stack. Each record a generated one-pass decoder reads starts a fresh budget, so a chain of nested record types is bounded by the types rather than by this; the values inside a field are bounded normally.
See also: read-bjodat