PureYAML/Sources/Parsing/TokenEventParser.Flow.swift
Mihaela Mihaljevic 16da7248c7 fix(parsing): complete flow-collection key and empty-node handling
Implement libyaml's simple-key mechanism for flow context: the scanner records a potential key position before a plain or quoted scalar, an anchor/tag, or a [/{ opener, and a later ':' splices a mappingKey token retroactively in front of that node. This lets a quoted scalar ("k":v), a nested collection ({k}:v), or a property-carrying node (&c c: d) become a flow mapping key. In flow context ':' is always a value indicator and '?' always an explicit-key indicator regardless of the next character (the block-only following-blank lookahead is disabled), so adjacent forms and trailing '?' parse, and multi-line plain keys fold before the ':' is read. The flow parser is rebuilt around the flow_sequence_entry / flow_mapping_key / flow_mapping_value productions, composing empty scalars for absent keys and values. The streaming token cursor now buffers an open flow collection's tokens until it closes so the retroactive key splice stays valid.

Flips 10 official YAML test suite cases (348/402): 4FJ6, 8KB6, 9BXH, 9MMW, CN3R, DFF7, FRK4, NJ66, UT92, CT4Q. Flow collections used as block mapping keys (LX3P, 6BFJ) need the splice in block context and stay tracked. Updates manifest, fixtures, counts, and roadmap.
2026-06-23 23:18:59 +02:00

197 lines
8.7 KiB
Swift

extension PureYAML.Parsing.TokenEventParser {
/// Parses a flow sequence, mirroring libyaml's `flow_sequence_entry`
/// productions. Each entry is either a flow node or, when introduced by a
/// `mappingKey` (`?`) or recognized as a single-pair `key: value`, a one-entry
/// flow mapping nested directly in the sequence.
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,
))
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 {
_ = try cursor.advance()
continue
}
endMark = try parseFlowSequenceEntry().endMark
if cursor.current?.kind.isFlowTerminator == false {
throw unexpectedToken(expected: "flow entry or flow sequence end")
}
}
throw unexpectedToken(expected: "flow sequence end")
}
/// Parses a single flow-sequence entry. A `mappingKey` (`?`, or one spliced in
/// by the simple-key mechanism before a `key: value` pair) produces a
/// single-pair flow mapping wrapping the entry; otherwise the entry is a plain
/// flow node.
mutating func parseFlowSequenceEntry() throws -> PureYAML.Parsing.NodeResult {
if cursor.current?.kind.isMappingKey == true {
return try parseFlowSequenceSinglePair()
}
return try parseNode()
}
/// Wraps an explicit-`?` single-pair entry of a flow sequence in a one-entry
/// flow mapping, mirroring libyaml's `flow_sequence_entry_mapping_*` states.
mutating func parseFlowSequenceSinglePair() throws -> PureYAML.Parsing.NodeResult {
let keyIndicator = try expect("mapping key") { $0.isMappingKey }
try emit(.mappingStart(anchor: nil, tag: nil, style: .flow, mark: keyIndicator.mark))
try parseFlowSinglePairKeyValue(keyIndicator: keyIndicator)
let mark = cursor.current?.mark ?? keyIndicator.endMark
try emit(.mappingEnd(mark: mark))
return PureYAML.Parsing.NodeResult(kind: .mapping, endMark: mark)
}
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,
))
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 {
_ = try cursor.advance()
continue
}
endMark = try parseFlowMappingPair().endMark
if cursor.current?.kind.isFlowTerminator == false {
throw unexpectedToken(expected: "flow entry or flow mapping end")
}
}
throw unexpectedToken(expected: "flow mapping end")
}
/// Parses one flow-mapping entry. With a `mappingKey` (`?`) indicator the key
/// follows the explicit form; without one the entry is a `flow_node` that is
/// itself the key (carrying an empty value unless a `:` provides one).
mutating func parseFlowMappingPair() throws -> PureYAML.Parsing.NodeResult {
if cursor.current?.kind.isMappingKey == true {
let keyIndicator = try expect("mapping key") { $0.isMappingKey }
return try parseFlowSinglePairKeyValue(keyIndicator: keyIndicator)
}
try parseFlowMappingImplicitKey()
return try parseFlowMappingValue()
}
/// Parses the key and value of a flow pair introduced by an explicit `?`,
/// where each side may be absent (composing an empty scalar). Shared by flow
/// mappings and single-pair entries nested in a flow sequence.
@discardableResult
mutating func parseFlowSinglePairKeyValue(
keyIndicator: PureYAML.Parsing.Token,
) throws -> PureYAML.Parsing.NodeResult {
if flowKeyIsEmpty() {
try emitEmptyScalar(properties: .none, mark: keyIndicator.endMark)
} else {
_ = try parseNode()
}
return try parseFlowMappingValue()
}
/// Parses the key of an implicit flow-mapping entry (`key : value` with no
/// explicit `?`). The key may be absent entirely (`{ : v }`, `{ , : v }`)
/// or be only a tag/anchor with no content, both composing to an empty
/// scalar key rather than routing into a block mapping.
mutating func parseFlowMappingImplicitKey() throws {
let properties = try consumeProperties()
if properties == .none, cursor.current?.kind.isMappingValue == true {
let mark = cursor.current?.mark ?? cursor.previous?.endMark ?? .start
try emitEmptyScalar(properties: .none, mark: mark)
return
}
if properties != .none, cursor.current?.kind.isMappingValue == true || cursor.current?.kind.isFlowTerminator == true {
let mark = cursor.current?.mark ?? cursor.previous?.endMark ?? .start
try emitEmptyScalar(properties: properties, mark: mark)
return
}
// An alias as a flow-mapping key (`{*a: b}`) is emitted directly so it is
// not re-routed into a block mapping by the alias-as-key check in
// `parseNode`, which only applies to block context.
if properties == .none, cursor.current?.kind.isAlias == true, try cursor.peek()?.kind.isMappingValue == true {
try emitAliasKey()
return
}
_ = try parseNode(properties: properties)
}
/// Parses the value half of a flow-mapping entry. A `:` consumes its node (or an
/// empty scalar when the value is absent); without a `:` the entry has an
/// implicit empty value (libyaml's `flow_mapping_value` with `empty == 1`).
mutating func parseFlowMappingValue() throws -> PureYAML.Parsing.NodeResult {
guard cursor.current?.kind.isMappingValue == true else {
let mark = cursor.current?.mark ?? cursor.previous?.endMark ?? .start
try emitEmptyScalar(properties: .none, mark: mark)
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: mark)
}
let valueIndicator = try expect("mapping value") { kind in
if case .mappingValue = kind {
return true
}
return false
}
let properties = try consumeProperties()
guard properties != .none || canBeginNode(cursor.current), cursor.current?.kind.isFlowTerminator == false else {
let mark = valueIndicator.endMark
try emitEmptyScalar(properties: properties, mark: mark)
return PureYAML.Parsing.NodeResult(kind: .scalar, endMark: mark)
}
return try parseNode(properties: properties)
}
/// True when the key position right after a `?` indicator is empty, i.e. the
/// next token is a `:` value indicator, a flow terminator, or the end of input,
/// so an empty scalar key must be composed (libyaml's `flow_mapping_key`).
func flowKeyIsEmpty() -> Bool {
guard let current = cursor.current else {
return true
}
return current.kind.isMappingValue || current.kind.isFlowTerminator
}
}