PureYAML/Sources/Parsing/TokenEventParser.Flow.swift
Mihaela M d4ba594012 feat(parsing): opt-in located parse on the hand-rolled engine
Port the located value tree onto the hand-rolled engine, reusing the
engine-agnostic located layer (Model.LocatedValue, LocatedPair,
SourcePosition, SourceRange, and the located composer helper) authored
on the conformance branch. The streaming composer now builds a parallel
located tree behind an opt-in tracksLocations flag, exposed as
parseLocated / parseStreamLocated; the plain Value path stays
byte-identical, gated behind the flag.

The scalar event carries the scanner token's end mark, so a located
scalar spans the whole token (end exclusive, one past the last
character) rather than being start-anchored. The hand-rolled scanner
already tracked token end marks, so this surfaces information the engine
was already computing.

Also stop emitting a spurious null document for a comment-only or empty
stream: YAML 1.2 gives such a stream zero documents, while the
single-document value API still maps it to null, so parse("") is
unchanged.

A whole-suite test proves the located projection equals a plain parse
for every passing case; the value, event-dump, and conformance suites
stay green (507 tests).
2026-06-29 07:14:42 +02:00

171 lines
7.7 KiB
Swift

extension PureYAML.Parsing.TokenEventParser {
mutating func parseFlowSequence(
properties: PureYAML.Parsing.NodeProperties,
) throws -> PureYAML.Parsing.NodeResult {
let start = try expect("flow sequence start") { kind in
if case .flowSequenceStart = kind {
return true
}
return false
}
let startMark = properties.mark ?? start.mark
var endMark = start.endMark
try emit(.sequenceStart(
anchor: properties.anchor,
tag: properties.tag,
style: .flow,
mark: startMark,
))
var hasEntry = false
while cursor.current != nil {
if case .flowSequenceEnd = cursor.current?.kind {
let end = try expect("flow sequence end") { kind in
if case .flowSequenceEnd = kind {
return true
}
return false
}
endMark = end.endMark
try emit(.sequenceEnd(mark: endMark))
return PureYAML.Parsing.NodeResult(kind: .sequence, endMark: endMark)
}
if case .flowEntry = cursor.current?.kind {
// A comma separates entries, so one must precede it: a leading or repeated comma
// is an empty entry, which is not allowed. A single trailing comma before the
// closing bracket is fine, since the close is handled above regardless of `hasEntry`.
guard hasEntry else {
throw unexpectedToken(expected: "flow sequence entry or flow sequence end")
}
hasEntry = false
_ = try cursor.advance()
continue
}
// A flow sequence element that begins with `:` is a single-pair mapping with an empty
// key, e.g. `[ : value ]`; an element that begins with a node and is then followed by `:`
// already becomes a mapping through the scalar key's implicit-key marker (`[ a: b ]`).
if case .mappingValue = cursor.current?.kind {
endMark = try parseEmptyKeyFlowMappingElement().endMark
} else {
endMark = try parseNode().endMark
}
hasEntry = true
if cursor.current?.kind.isFlowTerminator == false {
throw unexpectedToken(expected: "flow entry or flow sequence end")
}
}
throw unexpectedToken(expected: "flow sequence end")
}
/// Emits a single-pair flow mapping with an empty (null) key for a flow sequence element that
/// begins with `:`. The value follows the `:` and may itself be empty before the next separator.
mutating func parseEmptyKeyFlowMappingElement() throws -> PureYAML.Parsing.NodeResult {
let mark = cursor.current?.mark ?? .start
try emit(.mappingStart(anchor: nil, tag: nil, style: .flow, mark: mark))
try emit(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: mark, endMark: mark))
let valueIndicator = try expect("mapping value") { kind in
if case .mappingValue = kind {
return true
}
return false
}
let properties = try consumeProperties()
let endMark: PureYAML.Parsing.Mark
if properties == .none, cursor.current?.kind.isFlowTerminator != false {
endMark = valueIndicator.endMark
try emit(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: endMark, endMark: endMark))
} else {
endMark = try parseNode(properties: properties).endMark
}
try emit(.mappingEnd(mark: endMark))
return PureYAML.Parsing.NodeResult(kind: .mapping, endMark: endMark)
}
mutating func parseFlowMapping(
properties: PureYAML.Parsing.NodeProperties,
) throws -> PureYAML.Parsing.NodeResult {
let start = try expect("flow mapping start") { kind in
if case .flowMappingStart = kind {
return true
}
return false
}
let startMark = properties.mark ?? start.mark
var endMark = start.endMark
try emit(.mappingStart(
anchor: properties.anchor,
tag: properties.tag,
style: .flow,
mark: startMark,
))
var hasEntry = false
while cursor.current != nil {
if case .flowMappingEnd = cursor.current?.kind {
let end = try expect("flow mapping end") { kind in
if case .flowMappingEnd = kind {
return true
}
return false
}
endMark = end.endMark
try emit(.mappingEnd(mark: endMark))
return PureYAML.Parsing.NodeResult(kind: .mapping, endMark: endMark)
}
if case .flowEntry = cursor.current?.kind {
// As in a flow sequence, a comma separates entries and so requires one before it;
// a leading or repeated comma is an empty entry. A trailing comma is fine.
guard hasEntry else {
throw unexpectedToken(expected: "flow mapping entry or flow mapping end")
}
hasEntry = false
_ = try cursor.advance()
continue
}
endMark = try parseFlowMappingPair().endMark
hasEntry = true
if cursor.current?.kind.isFlowTerminator == false {
throw unexpectedToken(expected: "flow entry or flow mapping end")
}
}
throw unexpectedToken(expected: "flow mapping end")
}
mutating func parseFlowMappingPair() throws -> PureYAML.Parsing.NodeResult {
if cursor.current?.kind.isMappingKey == true {
return try parseMappingPair()
}
// A flow mapping key may be empty (`{ : v }`) or carry only properties (`{ !!str : v }`):
// both resolve to a null key, so emit one rather than requiring a node before the `:`. This is
// valid only in flow mapping key position, so the empty-node allowance stays local here.
let keyProperties = try consumeProperties()
let keyEndMark: PureYAML.Parsing.Mark
if case .mappingValue = cursor.current?.kind {
let mark = keyProperties.mark ?? cursor.current?.mark ?? .start
try emit(.scalar(value: "", anchor: keyProperties.anchor, tag: keyProperties.tag, style: .plain, mark: mark, endMark: mark))
keyEndMark = mark
} else {
keyEndMark = try parseNode(properties: keyProperties).endMark
}
// A flow mapping entry may omit its `:` value indicator: `{ a }` and `{ a, b }` are sets whose
// keys map to null. A value parses only when a `:` follows the key.
guard case .mappingValue = cursor.current?.kind else {
try emit(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: keyEndMark, endMark: keyEndMark))
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: keyEndMark)
}
let valueIndicator = try expect("mapping value") { kind in
if case .mappingValue = kind {
return true
}
return false
}
let properties = try consumeProperties()
guard properties != .none || cursor.current?.kind.isFlowTerminator == false else {
let mark = valueIndicator.endMark
try emit(.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: mark, endMark: mark))
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: mark)
}
return try parseNode(properties: properties)
}
}