PureYAML/Sources/Parsing/Scanner.FlowSimpleKey.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

106 lines
4.5 KiB
Swift

extension PureYAML.Parsing.Scanner {
func scanFlowToken(
_ character: Character,
state: inout State,
) -> Bool {
switch character {
case "[":
openFlowCollection(kind: .sequence, token: .flowSequenceStart, state: &state)
return true
case "]":
closeFlowCollection(token: .flowSequenceEnd, state: &state)
return true
case "{":
openFlowCollection(kind: .mapping, token: .flowMappingStart, state: &state)
return true
case "}":
closeFlowCollection(token: .flowMappingEnd, state: &state)
return true
case ",":
// A flow entry separator abandons any pending simple key and reopens the
// door for a fresh one on the next entry (libyaml fetch_flow_entry).
removeFlowSimpleKey(&state)
scanFixedToken(.flowEntry, state: &state)
state.simpleKeyAllowed = true
if state.flowContexts.last?.kind == .mapping {
state.flowContexts[state.flowContexts.count - 1].isExpectingKey = true
}
return true
default:
return false
}
}
/// Opens a flow collection (`[` or `{`). The opener itself may be a simple key
/// at the enclosing level, so a key is saved before the flow level rises; a
/// fresh simple-key slot is then pushed and a key is allowed inside.
func openFlowCollection(
kind: PureYAML.Parsing.ScannerFlowContextKind,
token: PureYAML.Parsing.TokenKind,
state: inout State,
) {
saveFlowSimpleKey(&state)
state.flowDepth += 1
state.flowContexts.append(.init(kind: kind, isExpectingKey: kind == .mapping))
state.flowSimpleKeys.append(nil)
scanFixedToken(token, state: &state)
state.simpleKeyAllowed = true
}
/// Closes a flow collection (`]` or `}`): the pending simple key on this level is
/// dropped, the level is popped, and no simple key may immediately follow the
/// closing indicator (libyaml fetch_flow_collection_end).
func closeFlowCollection(
token: PureYAML.Parsing.TokenKind,
state: inout State,
) {
removeFlowSimpleKey(&state)
state.flowDepth = max(0, state.flowDepth - 1)
scanFixedToken(token, state: &state)
_ = state.flowContexts.popLast()
_ = state.flowSimpleKeys.popLast()
state.simpleKeyAllowed = false
}
/// Records a potential simple key at the current reader position for the
/// innermost flow level, if a simple key is allowed there. Mirrors libyaml's
/// `yaml_parser_save_simple_key`: the saved key remembers the index of the
/// next token to be appended, so a later `:` can splice a ``mappingKey`` in
/// front of the node that starts here. No-op outside flow context.
func saveFlowSimpleKey(_ state: inout State) {
guard state.flowDepth > 0, state.simpleKeyAllowed, !state.flowSimpleKeys.isEmpty else {
return
}
state.flowSimpleKeys[state.flowSimpleKeys.count - 1] = PureYAML.Parsing.ScannerFlowSimpleKey(
tokenIndex: state.tokens.count,
mark: state.reader.mark,
)
}
/// Discards any potential simple key on the innermost flow level. Mirrors
/// libyaml's `yaml_parser_remove_simple_key` for the flow context, where a
/// simple key is never *required*, so abandoning it is not an error.
func removeFlowSimpleKey(_ state: inout State) {
guard !state.flowSimpleKeys.isEmpty else {
return
}
state.flowSimpleKeys[state.flowSimpleKeys.count - 1] = nil
}
/// Promotes a pending flow simple key to an actual mapping key by inserting a
/// ``TokenKind/mappingKey`` token at the saved index, mirroring libyaml's
/// `fetch_value` splicing a KEY token before a simple key. Returns true when a
/// key was inserted so the caller can avoid emitting a redundant one.
@discardableResult
func promoteFlowSimpleKey(_ state: inout State) -> Bool {
guard !state.flowSimpleKeys.isEmpty, let key = state.flowSimpleKeys[state.flowSimpleKeys.count - 1] else {
return false
}
let token = PureYAML.Parsing.Token(kind: .mappingKey, mark: key.mark, endMark: key.mark)
let index = min(key.tokenIndex, state.tokens.count)
state.tokens.insert(token, at: index)
state.flowSimpleKeys[state.flowSimpleKeys.count - 1] = nil
return true
}
}