# yaml.module
YAML decode + encode for authored engine content. Pure Luau, no engine
dependencies — a general parsing library, not data-system plumbing.
```luau
local Yaml = require("@builtin::modules.yaml")
local doc = Yaml.decode(vfs.read(path))
vfs.write(path, Yaml.encode(doc))
```
## Supported subset
- Block mappings (`key: value`) and block sequences (`- item`), nested
to any depth.
- Compact sequence-of-mapping entries (`- path: x` / ` kind: y`).
- Flow collections — `{ a: 1, b: 2 }` and `[1, 2, 3]` — single line.
- Plain scalars with type inference: `~` / `null` → nil, `true` /
`false` → boolean, numeric literals → number, everything else →
string.
- Single- and double-quoted strings (`''` escapes a literal quote
inside a single-quoted string; `\n` `\t` `\r` `\\` `\"` inside a
double-quoted one). A quote opens a quoted scalar where a scalar
begins — after `key:`, after a `- ` entry marker, or inside a flow
collection — so a plain scalar carries an apostrophe as data
(`note: the world's rules`). Quoted keys are read in flow mappings.
- `#` comments — honored outside quotes, so a `#` inside a quoted
string is kept as data.
- Literal (`|`) and folded (`>`) block scalars — the bare indicators
only. A block scalar's content is literal text: quotes (balanced or
not), `#`, `key: value` and `---` inside it are data.
- An optional leading `---` document marker.
## Loud-error contract
Constructs outside the subset above raise instead of misparsing:
anchors/aliases (`&`, `*`), tags (`!`), directives (`%`),
multi-document streams (a second `---`), block-scalar
chomping/indentation indicators (`|-`, `|+`, `>-`, `|2`, ...), tab
indentation, and inconsistent indentation (both block structure and
block-scalar bodies). Every error carries the offending line number
(`yaml: line N: ...`), so a bad config points straight at the line to
fix.
Tab indentation is rejected everywhere — including block-scalar body
lines whose leading whitespace contains a tab. A tab after the first
non-space character of a line is content and passes through.
## API
- `Yaml.decode(text: string): any` — decode a YAML document into a
Luau value (table, scalar, or nil for an empty document). Raises on
malformed input or an unsupported construct.
- `Yaml.decodeWithComments(text: string): (any, boolean)` — decode as
`decode` does, and report whether the text carries a comment by the
decoder's own rule: a `#` at a line's start or after whitespace, outside
a quoted scalar and outside a block scalar. Re-encoding the value drops
such comments, so an editor that writes a document back reads this first.
- `Yaml.encode(value: table): string` — encode a Luau table as YAML
(block style, two-space indent, alphabetically sorted keys). Raises
on values YAML can't represent (functions, userdata, non-string
mapping keys).
A top-level empty table encodes to an empty document, which decodes
back to `nil` (the YAML empty-document ambiguity).
# content_version
Per-path content-version counters — a cheap, synchronous "has this file
changed?" token. An assetType behavior memoizes its parse in the ref's
`runtime` keyed by the counter `get(path)` returned when it parsed, then serves
every later read as an integer compare instead of re-reading and re-parsing the
file.
## Why it exists
A `.data()` behavior that re-reads and re-parses its source on every call is
pure overhead when the file hasn't changed. This module gives it a version
token to gate the rebuild on:
- `get(path)` when the parse ran, stored alongside the parsed value.
- On every later call, compare `get(path)` to the stored value — equal means
the cache is still valid (no `vfs.read`, no parse); a bump means rebuild.
## Exports
- `M.get(path: string) -> number` — the path's current version counter (`0` if
never written this VM).
- `M.bump(path: string)` — bump `path`'s counter, invalidating every reader
memoized against its previous value. Content code rarely calls this directly.
## What bumps a counter
Two write surfaces feed it, so the token reflects a change no matter where it
originated:
- `vfs.write` / `vfs.move` / `vfs.remove` bump **synchronously**, in the same
call — so a script that writes a file and reads it back in the same tick sees
the new content immediately (the asset-change `onChange` dispatch fires a
frame later, too late for a same-tick read).
- the generic asset-change dispatcher bumps on every source write it routes,
including peer-synced and engine-originated writes that never pass through the
Luau `vfs.*` surface — with the engine's normalized path, the canonical form a
reader keys on.
## Per-path, not a global epoch
Counters are per **path**, not one global epoch. The per-frame dirty-entity
writer churns scene-dirty paths every frame during play; a global epoch would
let that churn invalidate every unrelated cache. Per-path isolation means only a
change to the file a reader depends on rebuilds it.
The counter map lives on `_G` (`__zero_content_versions`), installed once before
the global table is sealed at boot, so a single map is shared across every copy
of this module that a require-cache reset (`vfs.reload`) might create. A
module-upvalue map would let a bumper and a reader that landed on different
module copies diverge, and the reader would serve stale content. Per-VM.