Skip to content

How an edit is made

yamldoc has its own YAML engine. Knowing roughly how it works explains why the guarantees in What is preserved can be as strong as they are, and why the API has the shape it has.

Source, syntax, semantics

Parsing keeps three things apart.

The source is the bytes you handed in, decoded from UTF-8, UTF-16 or UTF-32 but never rewritten; every later structure points into it by byte span.

The syntax is an immutable tree of occurrences over that source: scalars, mappings, sequences and aliases, each with its style, tag, anchor and exact location, plus the directives and every token in between, comments included. It says nothing about meaning; yes is a plain scalar spelt yes.

The semantics is a graph composed from the syntax under a schema profile: the YAML version's rules for what a plain scalar means, which keys are equal, what an alias binds to. Composition is lazy, cached per revision, and can fail in part without taking the syntax with it, which is how Validate can point at a dangling alias in a file that still round-trips exactly.

A Snapshot is a syntax revision plus its lazily composed semantics. It is immutable, which is what lets one be read while another is being written.

A transaction

Edit opens a transaction against the current revision. Each command inside it does four things:

  1. States its intention independently of the source: "this occurrence now has this value", "this entry is gone", "this subtree corresponds to that one". The statement lives in its own model, built from the semantic graph, not from the bytes.
  2. Plans the new source for the edit's footprint alone, in the style of the source around it, and records which existing bytes the result still depends on.
  3. Applies the plan to a private working copy of the syntax, without re-parsing, and produces the next working revision. Its semantics are derived from the previous graph when only one scalar's value changed, and composed afresh otherwise. Every Node from the previous working revision is now stale, which is why the API asks you to select again.
  4. Records which comment belongs to which owner.

When the callback returns, publication re-parses the emitted bytes from scratch, composes them, and compares the result against every statement from step 1: the values, the correspondence between old and new occurrences, the retained bytes from step 2, and the comment ownership from step 4. Only if all of it agrees is the new revision installed. The check takes nothing from the writer but its bytes: they are parsed as a fresh file would be, and compared with a statement built before the writer ran, so a bug in the writer produces an error, not a corrupted file. What the check cannot catch is a bug the parser shares with it; the test suite's independent expectations cover that side (spec 0008, D10).

Why the limits

Everything the engine does is charged against a budget: source bytes, nodes, depth, comment count, work per transaction. A library fed untrusted files has to be bounded, and a bounded engine has to say what its bounds are. The zero Options uses defaults sized for configuration files; the numbers are provisional and will be replaced by measured ones.

Why not goccy

The first version of this library was built on goccy/go-yaml, the best parser and concrete syntax tree available in Go. It could round-trip a file but not edit one faithfully: comment alignment collapsed, multi-line flow collections reflowed, CRLF became LF, and several corruption cases had to be detected and refused rather than handled, because a tree that is re-emitted cannot promise to leave any byte alone. The decision to own the engine, and what it had to do, is spec 0008 on the wiki; the investigation that led to it is the report it cites.