The gap-only manifest stopped guarding a case once it was fixed, so later work silently regressed previously-fixed cases (caught only by an external full-suite run, not the gated sweep or full corpus). Replace it with a full manifest of all 402 official cases (Tests/Fixtures/yaml-test-suite-manifest.yaml), each carrying its conformant expectation and a passing/gap status, with every case's files vendored. The gated sweep now asserts every passing case still passes (the regression guard) and every gap still reproduces, so any regression fails the build. The honest gate-enforced count is 329/402 passing (the earlier 348 came from a more lenient one-time oracle). Repairs three regressions the new gate revealed: the bucket-6 tab rule over-rejected tabs in block-scalar content and as separation before a scalar value (root cause: the batch scanner and the incremental TokenCursor had divergent line-start loops; unified onto a single scanStep, fixing 96NN/00, 96NN/01, R4YG, Y79Y/001, DK95/00, DK95/03, DK95/07, 6CA3, Q5MG, J3BT); an anchor on a prior line before an alias key wrongly rejected 26DV; and a tag-only flow key mis-parsed WZ62. Genuine tab-indentation errors (4EJS, Y79Y/000) still reject.
24 KiB
Changelog
All notable changes to PureYAML are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Fixed
-
Make the YAML test suite conformance gate comprehensive and repair the regressions it revealed. The gap-only manifest (which deleted a case once fixed, so nothing guarded it afterward and later work could silently regress it) is replaced by a full manifest of all 402 official cases (
Tests/Fixtures/yaml-test-suite-manifest.yaml), each with itsid,name,localPath, conformantexpectedbehavior,oracle,byteCount,lineCount, and astatus(passing|gap). Every case's files are vendored underTests/Fixtures/yaml-test-suite/. The gated sweep (PUREYAML_RUN_YAML_TEST_SUITE=1) now asserts everypassingcase still passes and everygapstill reproduces, so a regression in any passing case fails the sweep. Result at the pinned commit: 329 of 402 passing, 73 gap. -
Stop over-rejecting tabs. A tab is invalid only as block-structure indentation (
scanner.cscan_to_next_token/scan_block_scalar_breaks/scan_plain_scalar). Block indentation is now measured in spaces only; a tab is left for the token scanner, which allows it inside block-scalar content once a leading space fixes the content column (96NN/00, 96NN/01, R4YG, Y79Y/001), as separation before a scalar value (DK95/00) or a flow node (6CA3, Q5MG, J3BT), and on a whitespace-only blank line (DK95/03, DK95/07), while still rejecting genuine tab indentation (4EJS, Y79Y/000, Y79Y/004-008, DK95/06). The batch scanner and the incremental token cursor were unified onto a singlescanStep, since their divergence previously let the streaming parser miss a tab error the batch scanner caught. -
Restore anchors/aliases as mapping keys for
26DV: an anchor on akey:value line followed by an indented*alias :key now anchors the nested mapping rather than the alias key (which cannot carry properties);&b *aliason one line is still rejected (SU74). -
Restore flow bare-tag/anchor keys for
WZ62({ foo : !!str, !!str : bar, }to{"foo":"", "":"bar"}): a flow key introduced by a splicedmappingKeyuses the same key parser as the implicit form, so a bare tag before the:composes an empty tagged scalar key instead of a mapping-as-key. -
Complete flow-collection (
[...],{...}) parsing for keys, empty nodes, and multi-line scalars, fixing 10 official YAML test suite cases and removing them from the conformance gap manifest. The scanner now implements libyaml's simple-key mechanism inside flow context: a potential key position is recorded before a plain or quoted scalar, an anchor or tag, or a nested[/{opener, and a later:value indicator splices amappingKeytoken in front of that node so a collection or property-carrying node can be a flow-mapping key. In flow context a:is now always a value indicator and a?always an explicit key indicator regardless of the following character (matching libyaml'sflow_level || IS_BLANKZ_ATchecks), so"k":v,{k}:v, and a trailing?parse correctly; the implicit-key forward lookahead is restricted to block context. A multi-line plain key folds in full before its:is read. The flow parser was rebuilt around libyaml's flow productions (flow_sequence_entry,flow_mapping_key/value), composing empty scalar keys and values where absent. The streaming token cursor holds an open flow collection's tokens until it closes so the retroactive key splice stays valid. Fixed false-rejects 4FJ6, 8KB6, 9BXH, 9MMW, CN3R, DFF7, FRK4, NJ66, UT92 and corrected values for CT4Q. The flow-node-as-block-mapping-key cases LX3P and 6BFJ remain tracked (they need the block-context simple-key splice, a separate change). -
Parse multi-line plain scalars and block scalars at the document root, fixing 35 official YAML test suite cases and removing them from the conformance gap manifest. The scanner now tracks the enclosing block-collection indent (libyaml's
parser->indent) separately from per-line indentation, defaulting to -1 at the bare document root, so a plain scalar's continuation lines fold using the spec rule "more indented than the enclosing block": a root scalar folds its column-0 continuations (single break to space, blank line to newline, per-line whitespace stripped) while a scalar inside a collection still ends at a sibling line. A---document-start marker may now be followed by the document's root node on the same line (--- foo,--- [1, 2],--- |,--- >), and a block scalar whose body sits at the document root's column 0 captures that body and ends at the next---/...marker. Fixed false-rejects 35KP, 4CQQ, 4Q9F, 6JQW, 82AN, 93WF, 96L6, 9MQT/00, 9YRD, B3HG, DK3J, EX5H, EXG3, FP8R, RZT7, T26H, UGM3, XLQ9; corrected values for 27NA, 2G84/02, 2G84/03, 4WA9, 6FWR, 6LVF, A2M4, K54U, KSS4, L383, T5N4, W4TN; and started correctly rejecting the invalid 9KBC, ZVH3, 9MQT/01. The still-imperfect M7A3 (multi-document...splitting) and NB6Z (tabs on empty plain-scalar lines) now parse but with mismatched values and stay tracked. Added root multi-line plain scalar regression tests. -
Escape carriage returns and line feeds in double-quoted output per unicode scalar rather than per Character. A CR+LF pair forms a single Swift extended grapheme cluster, so the previous Character-level escape switch matched neither
\rnor\nand emitted the pair unescaped; the folding parser then read it back differently, breaking parse/dump/parse stability for values containing CRLF (observed on the Zoom OpenAPI corpus seed). Added a dumper regression test. -
Tightened error detection so 15 more official YAML test suite cases are handled correctly, repairing four error-detection regressions and removing them from the conformance gap manifest. Tabs are now rejected when they would supply block-structure indentation: a tab at the start of a block line, a tab in a block scalar's indentation, and a tab in separation position that precedes a nested block node (a
-entry, a?key, or a compact mapping key) all fail, while a tab used purely as in-line separation before a scalar value stays valid (4EJS, Y79Y/000, Y79Y/004, Y79Y/005, Y79Y/006, Y79Y/008, Y79Y/009 now reject; 6BCT and Y79Y/010 now parse with matching values; DC7X and J3BT no longer falsely reject). A---/...document marker at column 0 is now rejected when it appears inside an open flow collection (N782) or inside an open quoted scalar (RXY3, 5TRB) rather than silently ending the document. A second%YAML/%TAGdirective section that opens a new document without an intervening...suffix is rejected (MUS6/01). An anchor or tag attached to an alias node, and an anchor-plus-alias on one node, are rejected (SU74, SR86). Remaining accepted-invalid cases that need----line content scanning or full simple-key tracking (for example 2G84/00, 2G84/01, 9MQT/01, ZCZ6) are left tracked in the gap manifest. -
Added YAML directive handling and completed document-boundary support, fixing three root causes. First, a
%now begins a directive only in directive position (before any document content and before the---that starts a document, including immediately after a...end marker); everywhere else, and in any flow context,%is ordinary plain text rather than a misfiring directive. Second,%YAMLis parsed leniently per the spec: a1.xversion is accepted (a higher minor is treated as 1.2 with a warning rather than an error), while an unsupported major version, a duplicate%YAMLin one document, or extra non-comment fields are rejected; reserved and unknown directives (and%TAGhandle definitions) are accepted and ignored, and a directive must be followed by a---document start. Third, a...end marker now closes the current document so that following content begins a fresh document in the multi-document stream parser (parseStream), while the single-documentparsestill rejects multi-document input. This makes 11 official YAML test suite cases parse with matching values (2LFX, 5TYM, 6WLZ, 6ZKB, 7Z25, 9DXL, 9WXW, BEC7, MUS6/03, MUS6/05, MUS6/06), correctly rejects four previously accepted-invalid cases (9MMA, B63P, H7TQ, SF5V), and unblocks 6LVF and W4TN from parse errors to value comparisons that now depend on separate scalar gaps (----line content and literal block chomping). -
Rewrote literal (
|) and folded (>) block scalar content handling to follow YAML 1.2 §8.1 semantics, fixing four root causes: interior blank lines are no longer dropped from literal scalars; folded scalars now apply true §8.1.3 line folding (a single break between equally indented non-empty lines folds to a space, blank lines stay literal newlines, and more-indented lines keep both their leading whitespace and their surrounding breaks unfolded); tabs inside block scalar content are preserved instead of being rewritten as spaces (only the block indentation columns are stripped); and the+keep chomping indicator is now supported alongside clip and strip. The block scalar is now scanned as a single unit (header line plus all content lines) with indentation auto-detection, mirroring the reference scanner, rather than reassembled from a lossy per-line token array. This makes 24 additional official YAML test suite cases pass: 4QFQ, 5LLU, 6VJK, 7T8X, 96NN/00, 96NN/01, DWX9, F6MC, F8F9, H2RW, JEF9/00, JEF9/01, K527, K858, L24T/00, L24T/01, M29M, M9B4, MJS9, P2AD, R4YG, TS54, W9L4, and Y79Y/001. -
Implemented YAML 1.2 flow (quoted) scalar line folding for both single (
') and double (") quoted scalars, mirroring the reference scanner'syaml_parser_scan_flow_scalar. Multi-line quoted scalars now fold correctly: trailing whitespace before a break is stripped, leading whitespace on a continuation line is stripped, a single break between non-empty lines folds to one space, and N consecutive breaks yield N-1 literal line feeds (the first and last lines keep their own leading and trailing spaces as content). A double quoted backslash at end of line now suppresses the break entirely (no space, no newline). Completed the double quoted escape set with\xXX(2 hex digits) and\UXXXXXXXX(8 hex digits), and added the previously missing\0 \a \v \e, literal\<space>, and the named breaks\N \_ \L \P, alongside the existing\uXXXX(including surrogate pairs). This makes 30 additional official YAML test suite cases pass: 3RLN/00, 3RLN/01, 3RLN/02, 3RLN/03, 3RLN/04, 3RLN/05, 4ZYM, 565N, 5GBF, 6WPF, 7A4E, 8UDB, 9SA2, 9TFX, DE56/00, DE56/01, DE56/02, DE56/03, DE56/04, DE56/05, DK95/02, DK95/08, G4RS, NAT4, NP9H, PRH3, Q8AD, T4YY, TL85, and XV9V. -
Composed an empty (null) plain scalar wherever a node is grammatically required but absent, instead of raising "expected a YAML node". This single production mirrors
parser.c's empty-scalar fallback and covers every position a node can go missing: after a tag or anchor property with no following node, an absent mapping value, an absent mapping key (both the plain: valueform and the explicit?form), and the empty key and value forms inside flow collections. An empty node carrying an anchor or tag is explicit; a bare absent node with no property in a grammatically-required position is still a parse error, matching the reference parser. The empty scalar resolves through the normal scalar resolver against an empty value, so!!stryields"",!!nulland a bare empty yield null, and an empty key resolves like any other empty scalar. Implemented inSources/Parsing/TokenEventParser.EmptyNode.swiftwith the flow and block call sites in the siblingTokenEventParser.*files. This makes 20 additional official YAML test suite cases pass: 2JQS, 2XXW, 5WE3, 6M2F, 7W2P, BU8L, CFD4, JTV5, KK5P, LE5A, M2N8/00, NHX8, NKF9, PW8X, RZP5, S3PD, SM9W/01, UKK6/02, WZ62, and XW4D. CT4Q now parses but still mismatches its expected JSON (a separate flow multi-line plain-scalar folding gap) and is re-recorded asjson-mismatch. -
Accepted node properties (anchor, tag) and aliases in mapping-key position, so a mapping key is parsed as the full node it is: it may carry
&anchorand/or!tagproperties, and it may itself be an*alias. Fixed four root causes. (1) The scanner now terminates an anchor or alias name at the first character outside libyaml'sIS_ALPHAset (ASCII letters, digits,-,_), mirroringyaml_parser_scan_anchor, so*alias:reads the aliasaliasfollowed by a:mapping indicator instead of swallowing the colon into the name. (2) The block-mapping parser now begins a pair at leading properties or an alias, not only at the key indicator the scanner emits after them, and properties on the same line as the first key attach to that key node rather than to the mapping (&a a: banchors the keya, while&aalone on its own line anchors the map). (3) An anchor or tag at value position with no node following on the same line now composes an empty (null) value that still carries the property, soa: &anchorregistersanchoras null and a later*anchorresolves. (4) An anchored scalar now registers its resolved value (so an empty anchored scalar registers null, not""). The implicit-key scanner also now recognizes a key with trailing spaces before its:("top1" :). Implemented inSources/Parsing/Scanner.swift,Scanner.Helpers.swift,TokenEventParser.swift,TokenEventParser.EmptyNode.swift,TokenEventParser.Helpers.swift,TokenEventParser.Block.swift,TokenEventParser.Flow.swift,TokenCursor.swift, and the anchor registration inEventStreamComposer.swiftandEventComposer.swift. This makes 10 additional official YAML test suite cases pass: 26DV, 6KGN, 74H7, 87E4, 9KAX, E76Z, G9HC, HMQ5, LQZ7, and ZWK4 (G9HC is an invalid-anchor case that is now correctly rejected). 2SXE now parses but still mismatches its expected JSON (a separate compact block-mapping structural gap) and is re-recorded asjson-mismatch.
Added
- Track conformance against the official YAML test suite
(github.com/yaml/yaml-test-suite,
databranch). The 182 cases PureYAML does not yet handle correctly are vendored underTests/Fixtures/yaml-test-suite/with a machine-readable manifest (Tests/Fixtures/yaml-test-suite-gaps.yaml) recording each gap's category and current behavior. A new opt-in sweep (bash scripts/check-yaml-test-suite.sh, gated behindPUREYAML_RUN_YAML_TEST_SUITE=1) reproduces every recorded gap so future fixes must update the manifest.docs/YAML_TEST_SUITE_CONFORMANCE.mddocuments the measured result (220/402 passing) and the ordered plan for closing the gaps.
[0.1.5] - 2026-06-23
Documentation
- Document why
maxNestingDepthis the necessary and sufficient safety bound for deeply nested input: aModel.Valueis a recursive tree whose every operation (compose, emit, compare, hash, and especially deallocate) recurses with nesting, and Swift cannot deallocate a recursive value type iteratively, so bounding the tree's depth at parse time is the only complete protection. Records the measured headroom, why the bound cannot be raised without an iterative parser (and would be pointless past the ~5,000deinitlimit), and the caveat that trees built by encoding a deepCodablevalue bypass it.
[0.1.4] - 2026-06-23
Fixed
- Reject documents nested beyond a fixed depth (
maxNestingDepth) with anestingTooDeepparse error instead of overflowing the recursive-descent parser's stack. A pathologically nested input (for example hundreds of nested[[[...]]]or deeply indented mappings) previously crashed the process; it now fails cleanly. The limit is well above the nesting of real-world specs, which are unaffected.
[0.1.3] - 2026-06-23
Fixed
- Parse a leading
#in block scalar content (>,>-,|,|-) as literal text instead of a comment. A block scalar whose first content line began with#, such as an OpenAPI$ref: >-folded onto#/components/..., was previously dropped to an empty string.
[0.1.2] - 2026-06-06
Added
- Document the real-world YAML corpus, fuzz/property gate, and CI artifact contract for production hardening.
- Add machine-readable parser diagnostic codes to validation report payloads and cover malformed production-shaped YAML with 10+ structured diagnostics.
- Vendor a 114-file real-world YAML corpus with a machine-readable manifest, default metadata checks, and an opt-in full parse/validate/round-trip gate.
- Add large public APIs.guru Stripe and Zoom OpenAPI YAML specs to the opt-in corpus gate.
- Add a reduced synthetic ACME Zephyr fixture set for Stitcher-style multi-file OpenAPI inputs and bundled output.
- Add a full renamed Birch-style ACME fixture catalog with 594 YAML files, preserving module/file structure while replacing private organization names, domains, and documentation URLs.
- Add YAML 1.2 JSON-subset compatibility for compact flow mappings such as
{"openapi":"3.0.0"}, including regression coverage for colons inside flow mapping values. - Add JSON compatibility tests and artifacts that compare PureYAML against Swift's JSON decoder across JSON roots, escapes, Unicode surrogate pairs, exponent numbers, nested values, and whitespace.
- Preserve round-trip fidelity for mapping keys that require quotes, including trailing-space keys found in the large Zoom OpenAPI corpus seed.
- Add an opt-in Yams differential script that compares parse behavior for the checked-in real corpus without adding Yams to the PureYAML package manifest.
- Add an opt-in Yams diagnostic comparison script that records PureYAML structured malformed-input diagnostics alongside Yams parser errors.
- Add an opt-in Yams validation comparison script proving structured duplicate key validation on YAML that both parsers can load.
- Add an opt-in Yams throughput script and scheduled/manual CI artifact for release-mode parser performance comparison on representative corpus seeds.
- Add an opt-in PureYAML phase profiler that times scanner, event parser, composer, lazy parse, validation, and dumping phases on slow corpus seeds.
- Compose normal parser output from events as they are emitted, avoiding a full
intermediate event array while keeping
parseEventsavailable for explicit event materialization. - Lazily scan tokens for normal
parseandparseStreamcalls, avoiding a retained full token array on the main parser path. - Track reader positions with a UTF-8 cursor so ASCII-heavy scanner advancement avoids per-character string allocation while preserving byte-accurate marks.
- Add deterministic generated corpus properties for valid YAML round trips and malformed real-seed mutations with structured diagnostics.
- Write corpus run artifacts under
.build/pureyaml-artifacts/, including real seed summaries, generated mutation summaries, seed metadata, and environment details. - Add a scheduled/manual CI corpus job that runs the corpus and Yams differential gates and uploads the generated artifact bundle.
[0.1.1] - 2026-06-06
Added
- Add platform CI badges and an active production-hardening roadmap to the README.
Changed
- Add release-mode build and test gates to the macOS, Linux, Windows, and WASM verification scripts.
- Update GitHub Actions checkout steps to
actions/checkout@v6so CI uses the current Node 24 action runtime.
Fixed
- Treat more-indented plain scalar continuation lines that start with
-as text unless they appear at a known sequence-entry indentation. This fixes OpenAPI-styleallOfdescriptions while preserving nested block sequences such as- - 123. - Parse explicit scalar keys whose
:value starts a same-line mapping and continues with indented mapping siblings, matching large OpenAPI schema keys.
[0.1.0] - 2026-06-05
Added
- Bootstrap PureYAML as a root Swift package with one
PureYAMLlibrary product, no external SwiftPM dependencies, no C sources, no generated parser targets, and no Foundation requirement in the library target. - Add the core pure-Swift YAML model with ordered mappings, sequences, scalars,
duplicate-key preservation, and first-class complex mapping keys through
Model.Pair.keyNode. - Add parser support for block mappings, block sequences, comments, flow collections, quoted strings, literal and folded block scalars, anchors, aliases, YAML directives, document markers, merge keys, complex mapping keys, multi-document streams, and selected explicit built-in scalar tags.
- Add deterministic YAML dumping with block output by default, optional flow collections, conservative plain scalars, conservative literal block scalars, complex mapping key dumping, and multi-document stream dumping.
- Add tag-preserving parsing with
PureYAML.parseTagged(_:)andPureYAML.parseTaggedStream(_:), plus tagged validation rules for unsupported built-in tags and tags applied to the wrong node kind. - Add
PureYAML.Tagged.Constructorfor caller-owned tagged construction with exact missing-handler and kind-mismatch errors, recursive path context, and a model-value fallback that preserves mapping order and duplicate keys. - Add path-aware validation with duplicate-key checks, custom rules, rule predicates, strict and non-strict behavior, warning collection, stream document indexes, deterministic validation paths, and direct value-tree validation for states ordinary loaders may collapse.
- Add diagnostic-first validation reports, raw-source preflight scanning,
Validation.ReportError, and Markdown, YAML, and JSON report rendering for application-owned files or error bodies. - Add scalar, keyed, unkeyed, nested, dynamic-key, dictionary-like, and
super-coder
Codableconversion APIs with exact path-aware errors. - Add fixture-backed compatibility coverage for scalar spellings, explicit tags, collections, anchors, unsupported YAML gaps, literal block emission, downstream documents, and deliberately failing validation fixtures.
- Add a real-world YAML fixture corpus with representative short, medium, large, and very large documents, including OpenAPI, Kubernetes, GitHub Actions, Docker Compose, Prometheus, and cert-manager YAML.
- Add macOS, Linux, Windows, and WASM verification gates, hosted CI, a full local verification script, a pre-push hook, usage documentation, migration boundaries, attribution, and release notes.
Changed
- Lower the package tools version to Swift 6.1 for hosted macOS CI compatibility.
- Harden duplicate-key validation to use set membership while preserving exact diagnostics.
- Make validation rules immutable after construction.
- Preserve keyed super-encoder field order even when delayed child encoders are written in a different order than they were requested.
- Make keyed default
superEncoder()andsuperDecoder()use the standardsupermapping key instead of the current mapping. - Resolve Yams-compatible plain scalar spellings for
yesandnobooleans, radix-prefixed integers, numeric separators, and.infand.nanfloats. - Broaden opt-in literal block emission while preserving exact parser round-trips for every emitted block scalar.
- Harden block mapping, multiline plain-scalar parsing, merge expansion, tagged parsing, lower-column mapping siblings, indented root fragments, JSON-style flow mapping pairs, tab-indented corpus files, and empty collection emission.