YAML · MoonBit · Apache-2.0
YAML, read as a value and as a document.
Five versions of the language — 1.2.2, 1.1, 1.0, the strict dialect, and one that reads the version a file asks for — from a single scanner. A version is a resolver here, not a fork. And every one of them can be written back, comments and all.
// one document, read four ways
let config = @moonyaml.loads(
#|server:
#| host: 0.0.0.0
#| port: 12000
#|limits: &shared { files: 1000 }
#|upload: *shared
)
@moonyaml.dumps(config) // back to YAML
@moonyaml.loads_all(text) // every document
@moonyaml.parse_all(text) // comments kept
01 · why
YAML is not a JSON dialect.
It has its own specification, its own versions that disagree about
what a word means, and constructs — anchors, tags, block scalars,
document streams — with no JSON counterpart. It reads into a
Json tree because that is what a caller wants, not
because it is the same language. Pretending otherwise is how a
configuration file comes to mean something other than what it looks
like.
Versions are resolvers
The grammar is the same in every version; what differs is what a scalar means. So the scanner is shared, the resolver is a record, and a version is a value you can pass to a function.
Value or document
loads gives a Json. parse
gives a Node that remembers the comment above a key,
the flow style of a short list, the anchor, the tag and the
quoting — so emit can write the file back.
Ambiguity is an error
Everything that cannot be resolved to one reading is refused with a line and a column, rather than guessed at. A tag that is unknown is not dropped: a document that asked for a type and did not get it has not been read.
Alias expansion, bounded
An alias may name a node that holds aliases, so a dozen lines can expand to billions of nodes. Here the expansion is counted and capped, and going over is an error with the count in it.
02 · the same file, five readings
Where the versions part company.
enabled: yes
mask: 014
starts: 12:30
1.2.2 {"enabled":"yes", "mask":14, "starts":"12:30"}
1.1 {"enabled":true, "mask":12, "starts":750}
1.0 {"enabled":true, "mask":12, "starts":750}
strict {"enabled":"yes", "mask":"014", "starts":"12:30"}
Four readings of one file, and not one of them is a bug. yes
is a boolean in 1.1 and a string in 1.2; 014 is octal
twelve in 1.1 and decimal fourteen in 1.2; 12:30 is a
sexagesimal only in 1.1. Which of those you meant is a property of the
document, not of the reader — so it is a package you choose, not a
flag that guesses.
03 · the document
Write the file back the way it was written.
# the deployment
name: web
defaults: &defaults
replicas: 3
front:
<<: *defaults
image: nginx:1.27-alpine # the only change
ports: [80, 443]
motd: |
one
two
parse → emit# the deployment
name: web
defaults: &defaults
replicas: 3
front:
<<: *defaults
image: nginx:1.27-alpine # the only change
ports: [80, 443]
motd: |
one
two
The comment above the document, the comment beside the image line,
the anchor on defaults, the alias that stands for its
value, the flow style of ports, the block scalar's
chomping indicator, the quoting of a string that had to be quoted,
the --- a document opened with — all of it is in the
tree, and all of it comes back. to_json is there for the
caller who wants the value instead, with the merge key folded in and
the alias resolved.
A mapping in that tree is a list of pairs rather than a map: order is
part of a document, a key is a node rather than a string, and a file
may have a sequence as a key or the same key twice. None of those
fits a Json, and all of them fit a document.
04 · the parse
What it saw, before what it means.
A value loses the shape of the document: a quoted scalar and a plain
one are the same string, a flow collection and a block one are the
same list, and a repeated key is gone. Node::events
reports the parse instead — one line per event, the notation the
YAML Test Suite writes a parse tree in.
That is what a caller compares with when two documents should be the same document: an edit that was supposed to change one value either changed one value or it did not, and the events say which.
+STR
+DOC
+MAP
=VAL :name
=VAL :web
-MAP
-DOC
-STR
05 · refusing
What it will not guess at.
A reader that is permissive where the specification is ambiguous is
a reader that will one day silently read something else. This one
raises Refused — a suberror with a position, not a
string — and the message says which rule it was.
The check is a property of the parse, not a lint afterwards: nothing is read into a tree that has already been guessed at.
a: [1, 2 1:4 ends mid-document
a: 1
a: 2 2:1 the key `a` is written twice
a: &x 1
b: *y 2:4 no anchor named `y`
06 · next
Start where you are.
The guide
Reading, writing, the five versions, the document tree, every knob and why its default is what it is.
The API
Every public type, value and function, with the signature the compiler produces and the comment that explains it.
Three programs
A tour of everything it reads, one document read five ways, and a round trip that keeps its comments — with their real output.