Mirror the shared Swift rules canon (rules-swift, commit 4ebb892) into
docs/rules-swift/ as a read-only reference alongside the repo's tailored
docs/rules/. PROVENANCE.md records the source, commit, and what was
included or excluded; the upstream repo's own VCS, CI, and lint tooling
is omitted as machinery rather than rule content.
Exclude the vendored tree from the style gate (scripts/check-style.sh)
and the pre-commit hook: the canon's enforcement docs quote the
forbidden attribution phrases to define them, the same reason the
repo's own enforcement files are already excluded.
The scalar event now carries the scanner token's end mark, so a located
scalar's range spans the whole token (end exclusive, one past the last
character) instead of being start-anchored. This gives consumers exact
highlight widths and is the prerequisite for surgical, position-based
text edits. Only the scalar event case gained a field; the value tree,
its equality, and the event dump (which compares value and style, not
marks) are unchanged, so the canonical-event and whole-suite gates stay
green (343 tests).
Add parseLocated / parseStreamLocated, returning a Model.LocatedValue
tree parallel in shape to Value, where every node carries its source
span (1-based line/column) and located children. Collections span from
their opening to closing event; scalars carry a start-anchored span;
aliases and merge-inherited pairs honestly point at their reference site.
The plain Value, its equality, and the conformance oracle are untouched:
locations are built only when the streaming composer is asked for them
(tracksLocations), behind new entry points, so the default value path is
byte-identical. A whole-suite test proves the located projection equals
a plain parse for every passing case (402/402), including through merge
keys and aliases, and the canonical-event and gap gates stay green.
PureYAMLJSON is a separate library product layered on the core engine. JSON is
a subset of YAML 1.2, so the existing parser already reads both into one value
tree; this product adds the missing half and the value-level policy:
- JSON.Writer: a pure-Swift RFC-8259 serializer (no Foundation; WASI-clean),
pretty or compact, rejecting non-string keys and non-finite numbers.
- JSON representability rules expressed in PureYAML's own Validation framework:
mappingKeysAreStrings and numbersAreFinite join the core mappingKeysAreUnique.
- JSON.Validator: the representability rule set plus issues(for:)/isRepresentable.
- JSON.Convert: standalone convert(_:to:) with the round-trip law (parse ->
validate representability -> emit -> re-parse must equal the source). To JSON
rejects complex keys and non-finite numbers; to YAML rejects duplicate keys,
which are legal in JSON but not in YAML.
19 tests cover the writer, the rules, and both conversion directions including
the dual-rejection edges. Carved out of the catch-all Sources/Tests paths via
exclude so the core PureYAML target is unchanged.
A quoted scalar's continuation line must be indented past the parent
block, so '"a' / 'b"' (continuation in column 0) and '"bar' / '\tbaz'
(tab indentation) are malformed; libyaml accepts both.
Record the indentation error during the continuation scan but defer
raising it until the closing quote is found. An unterminated quote reaches
end of input first and still reports the missing quote (the root cause)
rather than the indentation symptom, preserving the multi-error diagnostic.
Surpasses libyaml on QB6E and DK95/01. Whole-suite conformance is now
402/402 with zero recorded gaps; canonical events stay 308/308.
A tab is never indentation, so flow-collection content reached through a
tab below the continuation indent ('- [' / '\tfoo') is malformed even
though the tab inflates the column. A tab at or past the indent is
separation, and a tab-only blank line stays blank, so both remain valid.
Track a tab in a flow continuation's leading whitespace below the
parent + 1 indent and reject content (but not a closing bracket) reached
through it. Surpasses libyaml on Y79Y/003; whole-suite conformance
400/402, canonical events 308/308.
A leading empty line in a block scalar may not be more indented than the
first content line. libyaml auto-detects the deeper indentation (from the
empty lines) and silently drops the content. The auto-detection now errors
when the content line is less indented than an earlier empty line on the
same scan.
Surpasses libyaml on S98Z; whole-suite conformance 399/402, canonical
events 308/308.
A flow-sequence single-pair implicit key must be single-line, so its ':'
must sit on the key's line. The quoted form ('[ "key"\n :value ]') was
already rejected via a crossed line break, but a plain key folds its line
break, hiding it. Compare the simple key's mark line with the ':' line
instead, which catches both forms and still exempts flow mappings.
Surpasses libyaml on DK4H (and keeps ZXT5); whole-suite conformance
398/402, canonical events 308/308.
A flow-sequence single-pair implicit key is single-line
(c-s-implicit-json-key uses s-separate-in-line), so its ':' may not cross
a line break: '[ "key"\n :value ]' is malformed. A flow MAPPING adjacent
value may still span lines ('{ "foo"\n :bar }' stays valid), which is why
the check is scoped to flow levels that are sequences.
Track per-flow-level whether it is a sequence ('[' vs '{') in a
flowIsSequence stack pushed/popped with the flow level, and in fetchValue
reject a pending simple key whose ':' crossed a line break when the
current flow level is a sequence.
Surpasses libyaml on ZXT5 (5MUD, the flow-mapping analogue, stays valid);
whole-suite conformance 397/402, canonical events 308/308.
A block sequence must begin its own line. A '-' entry mid-line after an
inline mapping value ('key: - a') or a node property ('&anchor - x') is
invalid, but libyaml accepts both. A '-' after another '-' is still a
valid compact sequence ('- - a'), and an explicit key whose ':' is on a
later line may still open a sequence ('? k' / ': - a').
In fetchBlockEntry, when no line break was crossed, reject if the last
token is an anchor/tag, or a value that closed an inline implicit key
(told apart by a pending simple key: an explicit key's simple key has
gone stale across its line break). Extract the scanner guard helpers into
LibYAML.Scanner.Guards to stay within the file-length limit.
Surpasses libyaml on 5U3A, SY6V; whole-suite conformance 396/402,
canonical events 308/308.
YAML requires multi-line flow content to be indented past the enclosing
block context (s-flow-line-prefix). libyaml does not enforce this, so
'flow: [a,' / 'b]' with 'b' in column 0, and the equivalent multi-line
flow mapping, wrongly parsed.
Track whether reaching a token crossed a line break, and in flow context
reject a token at or below the parent block indent. A closing ']' / '}'
or a leading ',' may sit at any indentation, so those are exempt (a flow
mapping's closing brace in column 0 stays valid).
Surpasses libyaml on 9C9N, VJP3/00; whole-suite conformance 394/402,
canonical events 308/308.
A '...' may be followed only by whitespace and a comment on its line.
Unlike '--- x', which carries inline document content, '... x' is
malformed, but the port (like libyaml) parsed the trailing text as a new
bare document.
After emitting the document-end token, require the rest of the line to be
blank or a comment. Surpasses libyaml on 3HFZ; whole-suite conformance
392/402, canonical events 308/308.
The non-specific '!' tag resolves a node by kind alone, with no implicit
typing, so '! 12' is the string "12", not the integer 12. The event
stream already carried the tag correctly, but typed-value construction
fell through to plain-scalar resolution and produced an int.
Add the '!' case to tagged-scalar composition so it yields a string.
Surpasses libyaml on S4JQ (the last parse-ok gap); whole-suite
conformance 391/402, canonical events 308/308.
Per ns-plain-first a '-' starts a plain scalar in flow only when a
flow-safe character follows. A '-' before a flow indicator ('[-]',
'[-, -]') cannot start a node, but the port (like libyaml) read it as the
scalar '-'.
Reject that form in the indicator dispatch. Extract scan_to_next_token,
the comment validator, and blankLineAhead into LibYAML.Scanner.SkipToToken
to keep the fetch file within the length limit.
Surpasses libyaml on G5U8, YJV2; whole-suite conformance 390/402,
canonical events 308/308.
A '#' starts a comment only after a space or tab, or at the start of a
line. A '#' adjacent to the previous token is not a comment: 'key: "v"#c',
'[ a ]#c', '[ a,#c', and a '>#c' block-scalar header are malformed, but
the port (like libyaml) accepted them.
Validate the character immediately before the '#' (a plain scalar already
consumes its own trailing blanks, so an adjacent '#' the next-token scan
sees genuinely had no separator). Applied in scan_to_next_token and the
block-scalar header tail.
Surpasses libyaml on 9JBA, CVW2, SU5Z, X4QW; whole-suite conformance
388/402. Canonical event stream stays 308/308.
Directives apply to the document that follows, so the previous document
must be closed with an explicit '...' end marker before them. After an
implicit end, a '%YAML'/'%TAG'/reserved directive is now rejected instead
of being silently accepted. Also reject a '%YAML' version trailed directly
by '#' with no separating space ('%YAML 1.1#...'), since a comment needs
leading separation; libyaml accepts both.
This restores reject-side conformance the suite manifest recorded as
passing but the libyaml port had dropped (9HCY, EB22, MUS6/00, MUS6/01,
RHX7), and updates the two PureYAML gap-error fixtures whose rejection now
fires earlier and more precisely at the misplaced directive.
Refresh yaml-test-suite-manifest.yaml to version 3: whole-suite
conformance is 384/402 (up from the recorded 331); 53 cases still marked
as gaps now pass, none regress.
A block scalar whose last line is whitespace only and ends at end of
input without a break still owns one line break, which keep (+) chomping
preserves. libyaml records no break because there is no break byte, so
'- |+' over a spaces-only unterminated line yields '' instead of '\n'.
In scan_block_scalar_breaks, when the break loop stops at end of input
after consuming whitespace on that line, append one implicit '\n' to the
trailing breaks (used only by keep chomping). Content lines are
unaffected because they do not skip whitespace in this loop.
With this the ported engine emits the exact canonical event stream for
every valid case in the vendored YAML test suite: 308/308, ahead of
libyaml (252) and the old hand-rolled parser (267). Port score 307 -> 308.
Now that a root-level block scalar can auto-detect indentation 0, its
content loop reaches lines in column 0, so a '---' or '...' document
marker there must end the scalar. Without the guard a '--- |' literal
folded the following '...', the next '%YAML' directive, and every later
document into its value.
Break out of the block-scalar content loop on a column-0 document marker.
The guard only fires at indent 0 (column == indent == 0), so nested block
scalars are unaffected. Surpasses libyaml on W4TN. Port score 306 -> 307.
A plain scalar may span blank lines, and a blank line that contains a tab
is still blank, so the tab is not indentation. libyaml rejects any tab
below the scalar indentation with 'found a tab character that violates
indentation', which wrongly rejects 'foo: 1' / tab-only line / 'bar: 2'.
Guard the tab check with blankLineAhead so it fires only when the tab
indents real content. Surpasses libyaml on DK95/04. Port score 305 -> 306,
leaving zero false-rejects.
libyaml refuses to skip a tab in the inter-token whitespace whenever a
simple key is allowed, so a tab used as valid separation (after a '-',
'?', or ':' indicator, or before a flow collection) is reported as a
character that cannot start a token. But a tab is legal in-line
separation; it is only illegal as indentation.
The scanner now skips tabs in scan_to_next_token and records in
tabbedSeparation whether one preceded the pending token. A block entry,
an explicit key, or an implicit mapping key promoted by ':' is rejected
when reached through a tab (there the tab would be indentation), while a
scalar value or a flow collection is accepted. Tab-indented keys and
'-\t-' nested entries still reject.
Surpasses libyaml on 6BCT, 6CA3, A2M4, DK95/00, Q5MG, Y79Y/010.
Port score 299 -> 305.
A flow sequence entry that begins with ':' (e.g. '[ : v ]') is a
single-pair mapping with an empty key, which YAML 1.2 permits but libyaml
rejects by handing the leading VALUE token to its node parser.
Open the implicit single-pair mapping on a leading ':' in the
flow-sequence-entry parser, and leave the VALUE token for the mapping
value state to consume so the value is parsed rather than dropped.
Surpasses libyaml on CFD4. Port score 298 -> 299.
libyaml makes every ':' in a flow collection a value indicator, so it
mis-scans ':x', 'http://foo.com', '::vector', and 'omitted value:,', and
drops the value after an adjacent quoted or collection key. Per the spec a
':' only ends a node when followed by whitespace, end of input, or a flow
indicator, except an adjacent ':' after a JSON key (a quoted flow scalar
or a flow collection used as the key).
Gate the ':' value-indicator dispatch on that rule, tracking on the
pending simple key whether it is a JSON node (set for quoted flow scalars
and flow-collection starts). End the plain scalar at a ':' before a flow
indicator instead of rejecting it as unexpected.
Surpasses libyaml on 4ABK, 58MP, 5T43, DBG4, FRK4, HM87/00, with 9MMW
(mixing adjacent and separated keys) still correct. Port score 292 -> 298.
libyaml makes every '?' in flow context a mapping-key indicator, so it
parses '{ ?foo: bar }' as a key 'foo' (dropping the '?') and '[?x]' as an
empty single-pair mapping. Per ns-plain-first a '?' followed by a
flow-safe character (anything but whitespace, end of input, or a flow
indicator) begins a plain scalar.
Gate the '?' key-indicator dispatch on isBlankZ or, in flow context, a
flow indicator, so '?foo' and '?x' stay plain-scalar content. Hoist the
flow-indicator predicate into CharacterClass and reuse it in the plain
scalar scanner. Surpasses libyaml on 652Z and HM87/01. Port score
290 -> 292.
The block-scalar break scanner raised 'found a tab character where an
indentation space is expected' for every tab seen while auto-detecting
the content indentation. But a tab at or beyond the content column is
content, not indentation: only a tab below the content column is illegal.
Gate the tab check on a content-column floor (the resolved indent, or the
parent indent + 1 while auto-detecting) instead of firing on every tab in
auto-detect mode. A tab-only line shallower than the content is still
rejected, so tab-as-indentation errors (Y79Y/000 and friends) are
preserved. Surpasses libyaml on 96NN/00, 96NN/01, R4YG, Y79Y/001.
Port score 286 -> 290.
The block-scalar scanner used 0 as the 'indentation not yet detected'
sentinel and floored the auto-detected indent at 1, mirroring libyaml's
scan_block_scalar_breaks. A root-level block scalar (parent indent -1)
whose content starts in column 0 therefore detected no content, and the
first content line was reparsed as a fresh document, raising 'did not
find expected document start'.
Switch the sentinel to -1 so 0 is a representable detected indent, and
floor the auto-detected indent at parent indent + 1 (0 at the document
root) instead of 1. Nested block scalars keep a parent indent >= 0, so
their floor stays >= 1 and they are unaffected.
Surpasses libyaml on DK3J, FP8R, and M7A3. Port score 283 -> 286.
A ':' value indicator with no preceding key is also valid inside a flow
mapping, denoting an entry whose key is empty (null). libyaml hands the
bare VALUE token to its node parser and fails with 'did not find expected
node content'. The flow-mapping-key parser now emits the empty scalar key
and lets the value state consume the indicator.
Fixes '{ : }' and '{ key: value, : empty key }', completing the
multi-document NKF9 case. Port score 282 -> 283.
A ':' value indicator with no preceding key on its line denotes a
mapping entry whose key is empty (null), which YAML 1.2 permits but
libyaml rejects with 'did not find expected key'. The block-mapping-key
parser now emits the empty scalar key and leaves the VALUE token for the
value state to consume, rather than throwing.
Fixes the canonical event streams for plain empty-key mappings, explicit
empty keys with anchors and aliases, empty keys nested in sequences, and
mixed empty-key/value entries. Port score 275 -> 282; surpasses libyaml
on 2JQS, 6M2F, M2N8/00, NHX8, S3PD, SM9W/01, UKK6/00.
Marks every step of the port plan done and adds a status section: the port
is integrated as PureYAML's only parser, verified byte-for-byte against
libyaml on 307/308 cases, and scores 275/308 on the canonical event gate,
ahead of libyaml's own 252 and the prior hand-rolled parser's 267.
An anchor or alias name is ns-anchor-char+ (any printable character that
is not whitespace, a line break, the BOM, or a flow indicator), so Unicode
and punctuation names are valid. libyaml restricts the name to
alphanumerics and rejects them.
Canonical event conformance: 271 -> 275 of 308 (2SXE, 8XYN, W5VH, Y2GN).
379 tests pass.
The ported libyaml scanner and parser now serve every parsing path, so the
hand-rolled scanner, token cursor, and token event parser are dead. Removes
them and the old Token/NodeResult/NodeProperties types along with the unit
tests that exercised the old scanner directly. The remaining suite drives
the ported engine through the public API.
PureYAML now has a single parsing engine. 379 tests pass.
Routes the value and stream APIs through the ported scanner+parser (the
event API already was), so PureYAML now parses entirely on the libyaml
port. Adds the features needed to match and exceed the previous engine:
JSON UTF-16 surrogate-pair \u escapes, bare documents after an explicit
`...`, reserved-directive handling that still requires a following `---`,
and a 64-level nesting bound enforced on the event stream so deeply nested
input fails with nestingTooDeep instead of overflowing a recursive
consumer. Parser errors are translated to the diagnostic-aware ParseError,
recovering precise codes where identifiable and a readable malformed
fallback otherwise; error-reporting fixtures are rebased onto the port.
Canonical event conformance: 268 -> 271 of 308 (7Z25, HWV9, QT73).
393 tests pass.
YAML 1.2 lets a flow-context key be separated from its ':' value indicator
by line breaks, so a flow-level simple key must survive a new line (it is
still bounded to 1024 characters). libyaml invalidates every simple key on
a new line, which wrongly rejects keys like `{"foo"\n: "bar"}` and
multi-line quoted keys.
Canonical event conformance: 259 -> 268 of 308 (4MUZ/00-02, 5MUD, 9SA2,
K3WX, NJ66, UT92, VJP3/01). This passes the hand-rolled parser's 267 and
exceeds libyaml's own 252. 333 tests pass.
YAML 1.2 separation lines may contain tabs, so a line of only spaces and
tabs is blank. libyaml leaves a leading tab and later rejects it; the
scanner now consumes the trailing blanks of a whitespace-only line so the
break is reached and the line is skipped.
Canonical event conformance: 258 -> 259 of 308 (DK95/03). 333 tests pass.
YAML 1.2 §6.8.1 requires a reserved directive (anything other than %YAML
or %TAG) to be ignored with a warning, and a document whose minor version
is newer than 1.2 to be parsed with a warning. libyaml rejects both. The
scanner now drops a reserved directive's whole line and emits no token,
and the parser accepts any major-version-1 document.
Canonical event conformance: 253 -> 258 of 308 (2LFX, 6LVF, MUS6/05,
MUS6/06, BEC7). 333 tests pass.
parseEvents now runs on the ported scanner+parser instead of the
hand-rolled approximation. Adds 1-based mark conversion at the boundary
(the port stays faithfully 0-based internally), translates the port's
scanner and parser errors into the diagnostic-aware ParseError with
precise codes (unterminatedQuotedString, unterminatedTag, tabIndentation)
and a readable malformed fallback, and retargets the canonical event gate
to track the port's gap set.
The port matches the canonical test.event on 253 of 308 valid cases;
the remaining gaps match libyaml's own behaviour and are the targets for
surpassing libyaml. Golden event and diagnostic expectations are rebased
onto the port where it is faithful or more correct (resolved verbatim
tags, libyaml's stream-end marks, reaching the real unterminated quote).
333 tests pass.
The scanner now errors with "found character that cannot start any token"
when a tab leads a token in block context, matching libyaml's final guard
in fetch_next_token (a plain scalar may not begin with a blank). Adds a
differential harness (env-gated) that dumps the ported pipeline's yeast
events for the whole suite.
Verified against the libyaml reference dumper across all 308 vendored
valid cases: the port is byte-for-byte identical to libyaml on 307, and
the single difference (L24T/01) is the port matching the canonical
test.event where libyaml drops a trailing line break. 333 tests.
Ports parser.c: the one-event-at-a-time state machine that turns the
scanner's token stream into the YAML event stream. Covers stream and
document framing, directive processing with %TAG handle resolution and
default handles, the node property/tag/anchor production, block and
indentless sequences, block mappings, and flow sequences and mappings
including the single-pair `?` mapping forms.
Output verified against the libyaml reference event dumper: the empty
mapping value, %TAG resolution, and shorthand tags all match byte for
byte. 331 tests, no regression.
Adds the %YAML and %TAG directive scanner, the final scanner piece.
Ports fetch_directive, scan_directive, scan_directive_name,
scan_version_directive_value/number, and scan_tag_directive_value,
reusing the already-ported tag handle/URI scanners. Adds
versionDirective and tagDirective token kinds.
The libyaml scanner is now fully ported: stream, document markers,
directives, block + flow structure, plain/quoted/block scalars,
anchors, aliases, and tags. 319 tests, no regression.
Stage 3 of the faithful libyaml scanner port, continued. Translates fetch_tag, scan_tag, scan_tag_handle, scan_tag_uri, and scan_uri_escapes: verbatim (!<uri>), shorthand (!handle!suffix), and local (!suffix) tags, producing the handle and suffix for the parser to resolve. URI %-escapes are decoded by collecting the UTF-8 octets and decoding them as a unit. Unit-tested for a secondary-handle shorthand tag, a local tag, and a verbatim tag. Only directives (%YAML/%TAG) remain unported in the scanner. 317 tests, no regression.
Stage 3 of the faithful libyaml scanner port, continued. Translates fetch_block_scalar, scan_block_scalar, and scan_block_scalar_breaks: literal (|) and folded (>) block scalars with the chomping (+/-) and indentation indicators in either order, indentation auto-detection from the first non-empty line (clamped to parser indent + 1), folded joining of equally-indented non-empty lines into a space, and clip/strip/keep chomping of the trailing breaks. Unit-tested for literal clip, folded join, and strip chomping. Only tags and directives remain unported in the scanner. 314 tests, no regression.
Stage 3 of the faithful libyaml scanner port, continued. Translates fetch_anchor and scan_anchor: an anchor (&name) or alias (*name) whose name is a run of IS_ALPHA characters, validated to be non-empty and followed by a blank, end of input, or a terminating indicator. The flow/quoted/anchor fetchers are consolidated into one indicator dispatch to stay within the complexity limit. Unit-tested for an anchored scalar value and a later alias. Remaining unported fetchers: tags, block scalars, directives. 311 tests, no regression.
Stage 3 of the faithful libyaml scanner port, continued. Translates fetch_flow_scalar and scan_flow_scalar: single- and double-quoted scalars with line-break folding (a single break folds to a space, blank lines to newlines), the single-quote '' escape, double-quoted backslash escapes (the full \0 \a \b \t \n \v \f \r \e space \" / \ \N \_ \L \P table plus \xXX/\uXXXX/\UXXXXXXXX hex), and the escaped end-of-line continuation that suppresses the break. Escapes are decoded to Unicode scalars directly rather than UTF-8 bytes. Unit-tested for the escaped single quote, the escape table including a hex unicode escape, and multi-line folding. The remaining unported fetchers are block scalars, anchors, tags, and directives. 310 tests, no regression.
Stage 3 of the faithful libyaml scanner port, continued. Translates fetch_flow_collection_start, fetch_flow_collection_end, and fetch_flow_entry (the [ ] { } , indicators), with the flow-level push/pop already ported. Combined with the plain-scalar scanner's flow-aware termination, the port now tokenizes flow sequences and mappings of plain scalars, and the simple-key splice promotes a flow mapping key. Unit-tested for [a, b] and {k: v}. Remaining unported fetchers: quoted/block scalars, anchors, tags, directives. 307 tests, no regression.
Stage 3 of the faithful libyaml scanner port: the token producer. Translates scan_to_next_token, fetch_more_tokens/fetch_next_token dispatch, and the fetchers for stream start/end, document indicators, block entry, key, value (with the simple-key splice and roll-indent), plus scan_plain_scalar with its line-break folding. Reuses the ported indent engine, simple-key mechanism, character classes, and reader. Constructs not yet ported (flow collections, quoted scalars, anchors, tags, directives, block scalars) throw an explicit 'not yet ported' error rather than mis-scan; the port is not yet wired to the public API.
Milestone: the ported scanner tokenizes '- a:' followed by a same-column 'b:' as siblings (one BLOCK-MAPPING-START), matching libyaml, fixing the original block-mapping bug by construction; a more-indented value still nests. Unit-tested for the sibling case, the nested case, a simple pair, and multi-line plain-scalar folding. 305 tests, no regression.
Stage 3 foundation of the faithful libyaml scanner port. Translates libyaml's input buffer and position tracking (the buffer/mark fields and the PEEK/SKIP/SKIP_LINE macros) into PureYAML.Parsing.LibYAML.Reader. It iterates Unicode scalars rather than grapheme clusters so a CR and an LF are distinct, matching libyaml's byte-oriented scanning (a CR+LF pair is still one line break), and tracks the mark in libyaml's 0-based line and column convention. Unit-tested for peek/skip column tracking and LF and CRLF line breaks. This is the input layer the token fetchers consume.
Stage 3 foundation of the faithful libyaml scanner port. Translates the IS_* character-class macros from yaml_private.h (IS_Z, IS_SPACE, IS_TAB, IS_BLANK, IS_BREAK, IS_BREAKZ, IS_SPACEZ, IS_BLANKZ, IS_DIGIT, IS_ALPHA, IS_HEX, IS_ASCII, IS_PRINTABLE) into PureYAML.Parsing.LibYAML.CharacterClass. libyaml inspects UTF-8 bytes; the Swift port operates on Unicode.Scalar, where the multi-byte line breaks NEL (U+0085), LS (U+2028), and PS (U+2029) are single scalars, and end of input is nil, libyaml's \0 sentinel (so isZ(nil) is true and the others are false). These predicates are what the token fetchers dispatch on. Unit-tested for end-of-input, blanks, the full break set, the alpha/digit/hex sets, and the ascii/printable boundaries.
Stage 2 of the faithful libyaml scanner port. Translates yaml_parser_save_simple_key, yaml_parser_remove_simple_key, yaml_parser_stale_simple_keys, and the flow-level push/pop (yaml_parser_increase_flow_level / decrease) onto a new PureYAML.Parsing.LibYAML.Scanner aggregate that embeds the indent engine. The simple-key mechanism is what lets a quoted scalar, a nested collection, or a property-carrying node become a mapping key when a ':' follows, and is required exactly at the block indent. Modelled faithfully: a potential key goes stale once it spans a line break or exceeds 1024 characters, and a required key that is removed or goes stale is the 'could not find expected colon' error. Unit-tested for the required/not-required, removal-error, staleness, and flow-level cases. No public behavior changed yet.
PureYAML's scanner is a hand-rolled approximation of libyaml's algorithm; its indentation and simple-key bugs are the gap between the two. Rather than keep patching the approximation, re-derive the scanner as a faithful, function-by-function Swift port of libyaml's scanner.c (parser.c to follow), so correctness is by construction. The no-C-runtime constraint is preserved: this translates the algorithm into pure Swift, it does not bind to libyaml.
First piece: the indentation engine under PureYAML.Parsing.LibYAML, a line-for-line port of yaml_parser_roll_indent and yaml_parser_unroll_indent. A block collection opens only when content is strictly more indented than the current level, which fixes the '- a:' / same-column 'b:' mis-nesting by construction. Unit-tested against that exact case (sibling vs nested vs flow-inert). Lands as a parallel implementation verified against libyaml built from the CYaml source; swapped in only once it matches the reference with no regression. docs/LIBYAML_PORT.md records the plan. No public behavior changed yet.
A stream whose only content is comments or whitespace has zero documents in YAML 1.2 (confirmed by both libyaml and the suite's test.event), but PureYAML was emitting a spurious empty null document into the event stream. Stop emitting it; the single-document value API maps an empty document stream to null, so PureYAML.parse("") is unchanged.
Found two ways: the canonical event gate (8G76, 98YD, AVM7) and, independently, differential testing against libyaml built from the CYaml source over the real-world corpus, where a comment-only values.yaml diverged. Raises exact canonical-event conformance to 267 of 308; the real-world differential agreement rises to 113 of 114. Adds a gated PureYAML yeast-event dumper used to drive the libyaml differential.
A plain scalar inside a flow collection whose next line begins with a flow indicator (',' ']' '}') or the ':' value indicator was retaining the folded line break as a trailing space, so [\nfoo: bar\n] produced the value 'bar ' instead of 'bar'. YAML 1.2 section 7.3.3 strips trailing whitespace from a plain scalar; end the scalar at such a line without folding the break.
Derived from the plain-scalar production, not pattern-matched per case, and verified against the canonical event gate (exact test.event comparison): fixes 4FJ6, 4MUZ/02, CT4Q, LP6E, NKF9, QF4Y, VJP3/01, Y79Y/002, ZF4X, and ZK9H with zero regressions. Exact canonical-event conformance rises from 254 to 264 of 308 valid cases. Adds a gated triage diagnostic to the event gate.