Commit Graph

31 Commits

Author SHA1 Message Date
b45b7a4b7d fix(parsing): accept an alias node as an implicit mapping key
An alias may stand where a mapping key is expected (`*a : value`). The scanner
emits the implicit-key indicator before a scalar key that a `:` follows, but
not before an alias, so the parser, which already accepts an alias node in key
position, never entered the pair and rejected the alias as an unexpected token.

The scanner now emits the same indicator before an alias that a `:` (after
optional spaces) follows, in block context. A `:` written directly against the
alias stays part of its name, since `:` is a valid anchor-name character, so
only a space-separated `:` marks the alias as a key.

Fixes yaml-test-suite E76Z and 26DV; raises parse-level conformance to 310/333
(93.1%) and value-level to 203/203 (100.0%), with the should-error rate
unchanged.
2026-06-29 03:35:56 +02:00
4b06f6e5fc fix(parsing): end an inline item value at a sibling block entry
A block entry with an inline node recorded only the node's indent (one past
the dash) as a valid implicit level, not the dash's own indent. So a plain
scalar that is an inline sequence item value could not tell a sibling entry
that returns to the sequence indent from its own content, and a compact nested
sequence as an explicit key's value folded into one scalar: `? a` / `: - b` /
`  - - c` / `   - d` became `{a: [b -  - c - d]}` instead of `{a: [b, [c, d]]}`.

Record the entry's own indent as a sibling level too, so the continuation
stops there. Skip it under a pending explicit key, whose own sequence key
drives the implicit indentation differently (an explicit key whose key is a
sequence, `? - x`, must keep parsing). A block entry more indented than its
container is still plain content, so a wrapped value beginning a line with
`- ` folds in. The block-entry recording moved to a helper to keep the file
within its length limit.

Brings yaml-test-suite value-level conformance to 197/197 (100.0%) with the
parse-level and should-error rejection rates unchanged. 485 tests pass.
2026-06-29 02:09:57 +02:00
3b504a9c3a fix(parsing): keep a tab on a leading blank of a block scalar
A blank line within a block scalar keeps any whitespace past the content
indentation as content, but the leading blank lines, those before the first
content line, were only counted and emitted as empty. So an otherwise-blank
`tab` line under a one-space content indent lost its tab, and a folded scalar
beginning with such a line dropped the leading tab.

Record each leading blank line's whitespace verbatim instead of a count, and
once the content indentation is established emit the surplus past it, the same
way an interior blank line does. A leading blank with no surplus still emits an
empty line, and an empty block scalar (no content indentation) still emits
empty lines for its blanks.

Raises yaml-test-suite value-level conformance to 196/197 (99.5%) with the
parse-level and should-error rejection rates unchanged. 482 tests pass.
2026-06-29 01:52:58 +02:00
95c5ff3966 fix(parsing): parse an explicit key inside a flow collection
A flow `?` did not mark an explicit key, so the scalar after it synthesized
its own implicit key: `{? foo : bar}` parsed as `{{foo: bar}: null}` instead of
`{foo: bar}`, and `[? foo : bar]` the same way.

Track the explicit-key state per flow level on the flow context: set it at a
flow `?`, clear it at that level's `:` or a `,`, and discard it with the
context when the collection closes. The key scalar then is not turned into an
implicit key, the pair's `:` is its value indicator, and a nested collection's
`:` does not affect an outer key (`{? {a: b} : c}`). The continuation fold for
a multi-line explicit key now also consults the flow context, restoring
`[? foo\n bar : baz]` to the single key `foo bar`.

The block explicit-key flag is left untouched, since it is not cleared between
valueless `? a` / `? b` entries and folding it into this check regressed
block sets; only the flow path is changed here.

Raises yaml-test-suite value-level conformance to 192/194 (99.0%) with the
parse-level and should-error rejection rates unchanged. 472 tests pass.
2026-06-29 01:17:26 +02:00
8bc855d7a0 fix(parsing): base an inline-mapping indent indicator on the mapping
An explicit block scalar indentation indicator is the content indentation
relative to the block scalar's parent node. For an inline mapping value
(`- aaa: |2`) the parent is the mapping, but a compact inline mapping never
pushes its indentation onto the main stack, so the indicator was added to the
stack level (0) rather than the mapping's (2). The content indentation then
came out too small and the next mapping key was pulled into the scalar.

Capture at the header whether the block scalar is an inline mapping value
(a mapping value was seen on its line). When it is, fold the deepest
applicable implicit indentation into the indicator's base; a direct sequence
item (`- |1`), which has no mapping value on its line, keeps the sequence's
own level, so the explicit indicators on sequence items stay correct.

Raises yaml-test-suite value-level conformance to 191/194 (98.5%) with the
parse-level and should-error rejection rates unchanged. 463 tests pass.
2026-06-28 23:24:21 +02:00
4b0dc9375e fix(parsing): keep tabs in block scalar content verbatim
A block scalar is indented with spaces only, so a tab past the content
indentation is literal content. The indentation scan measured the leading
whitespace as a tab-expanded width and rebuilt the surplus beyond the
indentation as spaces, so a line like `␣\ttext` under a one-space indent came
out `   text` rather than `\ttext`: the tab became three spaces.

Record the literal leading-whitespace characters during the indentation scan
and reconstruct the surplus from them, walking columns the way the width scan
does and keeping the remainder verbatim. A tab stays a tab; surplus spaces
stay spaces; the blank-line surplus path uses the same reconstruction.

Raises yaml-test-suite value-level conformance to 180/194 (92.8%) with the
parse-level and should-error rejection rates unchanged. 438 tests pass.
2026-06-28 20:53:40 +02:00
67d72c8b15 fix(parsing): skip tabs as mid-line separation white space
skipSeparation consumed only spaces between tokens, so a tab after an
indicator leaked into the following node: an explicit key written `?\tkey`
became the key `\tkey` rather than `key`. A tab is separation white space in
YAML, forbidden only as indentation, which the line-start scan already handles
before this point.

Skip tabs there too. This closes the post-indicator tab leak left open when
the indicator boundary itself was changed, and handles a run of tabs between a
`:` and its value. The test-suite rates are unchanged, since no comparable
case isolates this, but the parser now matches the spec on tab separation.

435 tests pass.
2026-06-28 20:35:04 +02:00
5a0e6c732e fix(parsing): parse a flow collection before a colon as a mapping key
A flow collection followed by a same-line `: ` indicator is an implicit block
mapping key, e.g. `[a, b]: value` or `{a: 1}: value`, but the parser took the
flow node as the document's root value and then rejected the trailing `:` with
"expected document boundary".

The marker that tells the parser a node is a key is emitted before the node,
so for a flow key it must precede the flow tokens, which are already drained
by the time the `:` is seen (the streaming cursor drains after each token).
Resolve it in the scanner with a character lookahead from the flow opener to
its matching close, skipping quoted scalars so their brackets and colons do
not miscount the nesting; when a same-line `:` boundary follows, emit the same
implicit-key marker a scalar key gets, so the flow node parses as the key
through the ordinary mapping path with no parser change.

A block implicit key must lie on one line, so a flow collection spanning a
line break before its `:` (`[23 / ]: 42`) is rejected, which a first cut
wrongly accepted until the lookahead was made to stop at a line break. A flow
node that no `:` follows, or that is a mapping value, is unaffected.

Raises yaml-test-suite parse-level to 294/333 (88.3%) with the should-error
rejection rate unchanged. 416 tests pass.
2026-06-28 18:34:11 +02:00
a5b8986d5e fix(parsing): fold root block scalars with column-0 content
A block scalar that is the document's own root node (`>`, `|`, or after
`---` on the same line) may begin its content at column 0: the document node's
indentation is -1, so column-0 content is still more indented than its parent.
The scanner read the base level as 0 and required content strictly deeper than
it, so the lines fell through indentation handling and were mis-scanned as a
stray plain scalar after the document, raising "expected document boundary".

A root block scalar and an under-indented nested one (`- |` then unindented
content) present identical indentation state at the first content line, so the
two cannot be told apart there. Capture the distinction at header-scan time
instead: a root block scalar has the base indentation and no mapping value,
explicit key, block-entry implicit indent, or flow context introducing it on
its line. Only then is its parent the document; a sequence item or mapping
value keeps its parent's indent as the floor, so its under-indented content is
still rejected, unchanged.

Raises yaml-test-suite parse-level to 291/333 (87.4%) and value-level to
173/191 (90.6%). A regression suite covers the root cases (including a leading
'#' as literal content) and the nested under-indented cases that must error.
410 tests pass.
2026-06-28 17:42:38 +02:00
4941eac711 fix(parsing): parse implicit null mapping keys
A ':' that opens a line with no key before it is a mapping entry with a
null key (': value', a bare ':', or a null-keyed entry among others). The
scanner emitted the ':' with no preceding key, so the parser rejected it
as a missing node.

The scanner now synthesizes the empty key and its node for such a ':'. To
avoid mistaking an explicit key's value indicator for a null key, it tracks
a pending block-level '?' key: a ':' opening a line is that key's value
when one is pending, and only a synthesized null key otherwise. The flag
is set and cleared only at flow depth zero, so a ':' inside a flow complex
key ('? {a: b}' then ': c') does not disturb the pending block key.

Measured against the official YAML test-suite, parse-level conformance
rises from 283/333 (85.0%) to 287/333 (86.2%) with no regression.
2026-06-28 02:53:45 +02:00
d79df8c4bc fix(parsing): reject a block entry after a flow collection on a line
A block sequence entry cannot follow a flow collection that closed earlier
on the same line ('- { y: z }- invalid', '- [a] - b'): the entry would
begin a sequence inline after a complete node. The scanner accepted it,
producing extra sequence items.

The scanner now records when a flow collection closes at the top level on
the current line and rejects a following block entry. The flag resets at
each line break, so a flow collection on its own line is unaffected, and a
nested block entry ('- -') has no preceding flow close, so it stays valid.

Measured against the official YAML test-suite, parse-level conformance
rises from 276/333 (82.9%) to 277/333 (83.2%) with no regression.
2026-06-28 01:34:55 +02:00
30dbd9316b fix(parsing): reject a lone dash before a flow indicator
In a flow collection a '-' that begins a plain scalar must be followed by
a non-indicator character. Immediately before a flow indicator ('[-]',
'[-, -]', '{a: -}') the dash is not a valid scalar, but the scanner
accepted it as the scalar '-'.

scanToken now rejects a '-' in flow context when the next character is a
flow indicator, before falling through to plain-scalar scanning. A dash
followed by ordinary content stays valid: negative numbers, a dash inside
a scalar, a dash starting a word, and a quoted dash are unaffected.

Measured against the official YAML test-suite, parse-level conformance
rises from 273/333 (82.0%) to 275/333 (82.6%) with no regression.
2026-06-28 01:00:57 +02:00
f1db1344da fix(parsing): reject an over-indented leading line in a block scalar
A block scalar's leading empty lines must not be more indented than its
first non-empty line, so a blank line wider than the content (a 5-space
blank above 2-space content, or 2,3-space blanks above 1-space content) is
malformed. The scanner buffered the leading blanks without comparing their
indentation to the content, folding such input into the value.

The scanner now records the widest leading blank and rejects the scalar
when it exceeds the content indentation, established for both auto-detected
and explicit indicators. Indentation is counted in leading spaces, not
tab-expanded width, so a tab on a blank line ('␣\t') is one space rather
than a jump to the next tab stop, keeping such valid block scalars intact.

Measured against the official YAML test-suite, parse-level conformance
rises from 270/333 (81.1%) to 273/333 (82.0%) with no regression.
2026-06-28 00:21:57 +02:00
b0dc31922b fix(parsing): reject a block sequence inline as a mapping value
A block sequence cannot be an implicit key's value on the same line as its
':' (key: - a); the value must begin on a following line, or use flow
style. The scanner accepted the inline block entry and parsed it as the
key's sequence value.

The block-entry case now rejects a '-' that starts a sequence when the
line already holds an implicit-key mapping value, reusing the same
same-line-pair flag as the inline nested mapping rule. As there, an
explicit- or null-key ':' opens its line and does not set the flag, so a
compact sequence value after it stays valid, as do a sequence on the next
line, a nested sequence, a flow sequence, and a negative number (key: -1).

Measured against the official YAML test-suite, parse-level conformance
rises from 260/333 (78.1%) to 261/333 (78.4%) with no regression.
2026-06-27 18:35:58 +02:00
735767ced9 fix(parsing): reject inline nested block mappings
Block style permits only one mapping pair per line, so a scalar followed
by ':' after a block-level value on the same line (a: b: c, a: 'b': c,
x: { y: z }in: valid, or trailing content after a quoted value) is not a
new key but a forbidden inline nested mapping. The scanner synthesized an
implicit key for any scalar followed by ':', regardless of position, so
these parsed as nested mappings instead of failing.

The scanner now records when a line already holds a block-level mapping
pair and suppresses a further implicit key on that line, in plain and
quoted scalars alike. The flag is set only for an implicit-key ':' (one
preceded by its key on the same line): a ':' that opens its line is an
explicit- or null-key value indicator, whose value may be a compact
mapping (? k then : a: 1), so that, flow mappings, next-line nesting, and
colons inside scalars stay valid.

Measured against the official YAML test-suite, parse-level conformance
rises from 256/333 (76.9%) to 260/333 (78.1%) with no regression.
2026-06-27 18:28:57 +02:00
27c423dbb9 fix(parsing): require an explicit document after a directive
The YAML grammar requires every directive to head an explicit document,
so a directive must be followed by a '---' marker. The scanner accepted a
directive with no document at all: '%YAML 1.2' alone, or '%YAML 1.2' then
a '...' document-end marker, parsed without error.

The scanner now records the line of a directive awaiting its document,
clears it at a document start, and rejects a still-pending directive at a
document-end marker and at stream end. Both scan drivers finalize through
the same stream-end path, so the check lives in one helper called from
each. The rule applies to any directive, including an unrecognized one
(which is otherwise still ignored); the directive test that relied on an
unrecognized directive parsing without a '---' is corrected to include
one, since that leniency is not permitted by the grammar.

Measured against the official YAML test-suite, parse-level conformance
rises from 253/333 (76.0%) to 255/333 (76.6%) with no regression.
2026-06-27 17:18:47 +02:00
4c77b17f9f fix(parsing): validate %YAML directive parameters and reject duplicates
The %YAML directive takes exactly one parameter, the version, and at most
one may appear per document. The scanner checked only that a valid version
was present: it ignored any further tokens, so '%YAML 1.2 foo' was
accepted, and it did not track that a %YAML directive had already been
seen, so a repeated directive was accepted too. It now rejects an extra
parameter (allowing only a trailing comment) and a second %YAML directive,
resetting the per-document flag at each document boundary. A well-formed
directive, with or without a trailing comment, is unaffected.

Measured against the official YAML test-suite, parse-level conformance
rises from 251/333 (75.4%) to 253/333 (76.0%) with no regression.
2026-06-27 16:33:03 +02:00
1d07fce635 fix(parsing): reject a comment glued to earlier content on its line
A '#' that begins a comment must be preceded by whitespace or open the
line; glued to earlier same-line content (e.g. "value"# c, [ a, b ]#c,
>#c, a flow entry then ,#c) it is neither a comment nor, in those
positions, plain-scalar content, so the document is malformed. Such input
was previously accepted as a comment.

A '#' that opens a line, follows a space, or sits inside a plain scalar
(a#b, where it stays literal content and never reaches this check) is
unaffected. The scanner tracks whether a real token has been scanned on
the current physical line, reset on each line break, rather than comparing
token line numbers: the streaming tokenizer drains its token buffer
between scans, and a block scalar's blank line emits an empty scalar
attributed to the next line, either of which would misread as content.

Measured against the official YAML test-suite, parse-level conformance
rises from 243/333 (73.0%) to 247/333 (74.2%): four wrongly-accepted
invalid documents are now rejected, with no valid document regressing.
The scalar-scanning routines move to Scanner.Scalars.swift to keep the
file within the length limit.
2026-06-27 15:53:16 +02:00
ceec6cca84 fix(parsing): allow a space before the colon of a quoted mapping key
A quoted mapping key followed by a space and then ':' ('"key" : value')
failed to parse: the scanner synthesizes the implicit mapping-key token by
checking the character immediately after the key, but a quoted scalar
stops at its closing quote, leaving the separating space, whereas a plain
scalar consumes that space into itself and ends at the colon. The quoted
path now looks past spaces for the colon, which the spec allows between an
implicit key and its value indicator.

Quoted keys with no space, quoted values, and plain keys (which already
worked with a space) are unaffected. Measured against the official YAML
test-suite, parse-level conformance rises from 238/333 (71.5%) to
240/333 (72.1%), with no regression.
2026-06-27 01:19:28 +02:00
d90a091ab0 fix(parsing): fold a plain scalar value that begins on its own line
A multi-line plain scalar whose value sits one line below its mapping key
stopped after the first line: the continuation check compared the next
line's indent against the value's own indent, which had been pushed onto
the indentation stack, so a continuation at the same indent failed the
strict '>'. It now compares against the parent block's indent (the level
below the value's, when the scalar begins exactly at the indentation top),
matching the spec: a continuation must exceed the mapping's indentation,
not the value's own. Inline values are unaffected, since their top-of-
stack already is the parent block's indent.

Measured against the official YAML test-suite, parse-level conformance
rises from 211/333 (63.4%) to 221/333 (66.4%) with no regression across
the existing 306 tests. The +10 covers the multi-line plain scalar family
the harness flagged, not just the case investigated.
2026-06-26 18:52:09 +02:00
ceb12295a4 fix(parsing): preserve leading blank lines in block scalars
Blank lines between a block scalar header and its first content line come
before the indent token, so the earlier blank-line capture skipped them.
Buffer leading blanks while the block indentation is still pending and
flush them as empty content once the indent is established, in the shared
indentation path used by both scan loops. Also fix folded bodies to emit
a break for a leading blank rather than folding it into a leading space.
This closes the last block-scalar blank-line edge from #57.
2026-06-26 08:42:16 +02:00
d3ff3823a5 fix(parsing): preserve blank lines in block scalars
The scanner dropped every blank line within and after a block scalar, so
a literal block lost internal blank lines, a folded block turned a blank
line into a space rather than a newline, and the keep indicator could not
preserve trailing blank lines. Capture blank lines as empty content in
both the batch scan loop and the streaming TokenCursor loop, which had
drifted as duplicate copies. Rework blockScalarValue to apply literal
versus folded line semantics and clip/strip/keep chomping over the
captured lines; content without blank lines is unchanged. Re-pin the
multiline-scalar test whose expectation encoded the old dropped-blank
behavior, and add a chomping-and-folding test matrix.
2026-06-26 08:09:45 +02:00
fadde56a05 fix(parsing): classify the keep block-scalar chomping indicator
The scanner consumed the `+` indicator of a `|+` / `>+` block scalar but
mapped it to nothing, so keep was silently treated as clip, and a scanner
test baked that in. Add a `keep` case to BlockScalarChomping, recognize
`+` in the header scanner, and correct the test. Full keep value semantics
(preserving trailing blank lines) depend on scanner blank-line capture and
are tracked in #57; this commit fixes the token classification only.
2026-06-26 07:50:07 +02:00
208b892019 feat(parser): harden diagnostics and streaming
Add structured validation diagnostics, JSON and Yams comparison gates, and lazy token/event parsing so production behavior is measured without retaining full intermediate parser arrays.
2026-06-06 07:57:48 +02:00
bc54528353 Harden parser against real YAML fixtures 2026-06-05 19:34:30 +02:00
8be6e68a23 feat(parser): add multi-document stream parsing 2026-06-05 14:55:54 +02:00
84a2de9ea1 test(compatibility): document unsupported yaml gaps 2026-06-05 13:29:22 +02:00
6197c23e6f feat(emitting): add literal block scalar output 2026-06-05 11:06:19 +02:00
7a71110bb4 feat(parsing): support directives and scalar tags 2026-06-05 10:16:34 +02:00
bee7683ee2 feat(parsing): parse token stream into events 2026-06-05 09:56:18 +02:00
11e7382ab1 feat(parsing): add reader scanner core 2026-06-05 07:40:23 +02:00