An anchor on an empty mapping value (`a: &anchor`) failed to register, so a
later `*anchor` errored as an undefined alias. Two bugs compounded:
- The parser dispatched the anchor-only value position to a nested block
mapping, absorbing the sibling key as the anchored node's child instead of
emitting the empty anchored node. It now emits an empty anchored node when
only a sibling mapping key follows, since the indented and inline-nested
cases are already handled by earlier branches.
- The stream composer registered an anchor by its raw scalar text, so an
anchor on an empty scalar came back as "" and a typed scalar (`&a 42`) as
text. It now registers the composed value, fixing a latent typed-anchor bug.
Fixes yaml-test-suite 6KGN; raises parse-level conformance to 306/333 (91.9%)
and value-level to 199/199 (100.0%), with the should-error rate unchanged.
258 lines
9.1 KiB
Swift
258 lines
9.1 KiB
Swift
extension PureYAML.Parsing {
|
|
struct EventStreamComposer {
|
|
let scalarParser: Parser
|
|
let mode: EventStreamComposerMode
|
|
var anchors: [String: PureYAML.Model.Value] = [:]
|
|
var containers: [EventStreamContainer] = []
|
|
var documentValues: [PureYAML.Model.Value] = []
|
|
var currentDocumentValue: PureYAML.Model.Value?
|
|
var didSeeStreamStart = false
|
|
var didSeeStreamEnd = false
|
|
|
|
init(
|
|
scalarParser: Parser,
|
|
mode: EventStreamComposerMode,
|
|
) {
|
|
self.scalarParser = scalarParser
|
|
self.mode = mode
|
|
}
|
|
|
|
mutating func consume(_ event: PureYAML.Parsing.Event) throws {
|
|
switch event {
|
|
case .streamStart:
|
|
didSeeStreamStart = true
|
|
case .streamEnd:
|
|
didSeeStreamEnd = true
|
|
case let .documentStart(mark):
|
|
try startDocument(mark: mark)
|
|
case let .documentEnd(mark):
|
|
try endDocument(mark: mark)
|
|
case let .scalar(value, anchor, tag, style, mark):
|
|
try consumeScalar(value, anchor: anchor, tag: tag, style: style, mark: mark)
|
|
case let .alias(anchor, mark):
|
|
try consumeAlias(anchor, mark: mark)
|
|
case let .sequenceStart(anchor, _, _, mark):
|
|
containers.append(.init(kind: .sequence, anchor: anchor, mark: mark))
|
|
case let .sequenceEnd(mark):
|
|
try completeSequence(mark: mark)
|
|
case let .mappingStart(anchor, _, _, mark):
|
|
containers.append(.init(kind: .mapping, anchor: anchor, mark: mark))
|
|
case let .mappingEnd(mark):
|
|
try completeMapping(mark: mark)
|
|
}
|
|
}
|
|
|
|
mutating func composedValue() throws -> PureYAML.Model.Value {
|
|
guard didSeeStreamStart, didSeeStreamEnd else {
|
|
throw unexpectedEvent(expected: "complete YAML stream", mark: .start)
|
|
}
|
|
guard documentValues.count == 1, let value = documentValues.first else {
|
|
throw unexpectedEvent(expected: "single YAML document", mark: .start)
|
|
}
|
|
return value
|
|
}
|
|
|
|
mutating func composedStream() throws -> [PureYAML.Stream.Document] {
|
|
guard didSeeStreamStart, didSeeStreamEnd else {
|
|
throw unexpectedEvent(expected: "complete YAML stream", mark: .start)
|
|
}
|
|
return documentValues.enumerated().map { index, value in
|
|
PureYAML.Stream.Document(index: index, value: value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension PureYAML.Parsing.EventStreamComposer {
|
|
mutating func startDocument(mark: PureYAML.Parsing.Mark) throws {
|
|
guard containers.isEmpty else {
|
|
throw unexpectedEvent(expected: "document content end", mark: mark)
|
|
}
|
|
if mode == .singleDocument, !documentValues.isEmpty {
|
|
throw PureYAML.Parsing.ParseError.unsupportedMultiDocumentStream(line: mark.line)
|
|
}
|
|
anchors = [:]
|
|
currentDocumentValue = nil
|
|
}
|
|
|
|
mutating func endDocument(mark: PureYAML.Parsing.Mark) throws {
|
|
guard containers.isEmpty else {
|
|
throw unexpectedEvent(expected: "document content end", mark: mark)
|
|
}
|
|
documentValues.append(currentDocumentValue ?? .null)
|
|
currentDocumentValue = nil
|
|
}
|
|
|
|
mutating func consumeScalar(
|
|
_ value: String,
|
|
anchor: String?,
|
|
tag: String?,
|
|
style: PureYAML.Parsing.ScalarStyle,
|
|
mark: PureYAML.Parsing.Mark,
|
|
) throws {
|
|
let model = try scalarParser.composeScalarValue(value, tag: tag, style: style, mark: mark)
|
|
if let anchor {
|
|
// Register the resolved node, not the raw text: an alias resolves to the anchored node's
|
|
// value, so an empty scalar anchors null and a typed scalar (`&a 42`) anchors that value.
|
|
anchors[anchor] = model
|
|
}
|
|
try complete(.init(
|
|
value: model,
|
|
mappingKey: .init(
|
|
keyNode: .string(value),
|
|
scalarValue: value,
|
|
tag: scalarParser.normalizedTag(tag),
|
|
style: style,
|
|
),
|
|
mark: mark,
|
|
))
|
|
}
|
|
|
|
mutating func consumeAlias(
|
|
_ anchor: String,
|
|
mark: PureYAML.Parsing.Mark,
|
|
) throws {
|
|
guard let value = anchors[anchor] else {
|
|
throw PureYAML.Parsing.ParseError.undefinedAlias(
|
|
anchor: anchor,
|
|
line: mark.line,
|
|
column: mark.column,
|
|
)
|
|
}
|
|
try complete(.init(
|
|
value: value,
|
|
mappingKey: .init(
|
|
keyNode: PureYAML.Model.Key(value: value),
|
|
scalarValue: nil,
|
|
tag: nil,
|
|
style: nil,
|
|
),
|
|
mark: mark,
|
|
))
|
|
}
|
|
|
|
mutating func completeSequence(mark: PureYAML.Parsing.Mark) throws {
|
|
guard let container = containers.popLast(), container.kind == .sequence else {
|
|
throw unexpectedEvent(expected: "sequence start", mark: mark)
|
|
}
|
|
let value = PureYAML.Model.Value.sequence(container.values.map(\.value))
|
|
store(value, anchor: container.anchor)
|
|
try complete(.init(
|
|
value: value,
|
|
mappingKey: .init(
|
|
keyNode: PureYAML.Model.Key(value: value),
|
|
scalarValue: nil,
|
|
tag: nil,
|
|
style: nil,
|
|
),
|
|
mark: container.mark,
|
|
sequenceItems: container.values,
|
|
))
|
|
}
|
|
|
|
mutating func completeMapping(mark: PureYAML.Parsing.Mark) throws {
|
|
guard let container = containers.popLast(), container.kind == .mapping else {
|
|
throw unexpectedEvent(expected: "mapping start", mark: mark)
|
|
}
|
|
guard container.pendingKey == nil else {
|
|
throw unexpectedEvent(expected: "mapping value", mark: mark)
|
|
}
|
|
let pairs = PureYAML.Parsing.MergeResolution.mergedPairs(
|
|
from: container.mergeSources,
|
|
localPairs: container.localPairs,
|
|
)
|
|
let value = PureYAML.Model.Value.mapping(.init(pairs))
|
|
store(value, anchor: container.anchor)
|
|
try complete(.init(
|
|
value: value,
|
|
mappingKey: .init(
|
|
keyNode: PureYAML.Model.Key(value: value),
|
|
scalarValue: nil,
|
|
tag: nil,
|
|
style: nil,
|
|
),
|
|
mark: container.mark,
|
|
))
|
|
}
|
|
|
|
mutating func complete(_ node: PureYAML.Parsing.EventStreamComposedNode) throws {
|
|
guard !containers.isEmpty else {
|
|
currentDocumentValue = node.value
|
|
return
|
|
}
|
|
|
|
switch containers[containers.count - 1].kind {
|
|
case .sequence:
|
|
containers[containers.count - 1].values.append(node)
|
|
case .mapping:
|
|
try completeMappingNode(node)
|
|
}
|
|
}
|
|
|
|
mutating func completeMappingNode(_ node: PureYAML.Parsing.EventStreamComposedNode) throws {
|
|
let index = containers.count - 1
|
|
guard let key = containers[index].pendingKey else {
|
|
containers[index].pendingKey = node.mappingKey
|
|
return
|
|
}
|
|
|
|
if key.isMergeKey {
|
|
try containers[index].mergeSources.append(contentsOf: mergeSources(
|
|
from: node,
|
|
))
|
|
} else {
|
|
containers[index].localPairs.append(.init(keyNode: key.keyNode, value: node.value))
|
|
}
|
|
containers[index].pendingKey = nil
|
|
}
|
|
|
|
mutating func store(
|
|
_ value: PureYAML.Model.Value,
|
|
anchor: String?,
|
|
) {
|
|
guard let anchor else {
|
|
return
|
|
}
|
|
anchors[anchor] = value
|
|
}
|
|
|
|
func mergeSources(
|
|
from node: PureYAML.Parsing.EventStreamComposedNode,
|
|
) throws -> [[PureYAML.Model.Pair]] {
|
|
switch node.value {
|
|
case let .mapping(mapping):
|
|
return [mapping.pairs]
|
|
case let .sequence(values):
|
|
guard !node.sequenceItems.isEmpty else {
|
|
return try values.map { value in
|
|
guard case let .mapping(mapping) = value else {
|
|
throw PureYAML.Parsing.MergeResolution.invalidMergeValue(mark: node.mark)
|
|
}
|
|
return mapping.pairs
|
|
}
|
|
}
|
|
return try values.enumerated().map { index, value in
|
|
let mark = node.sequenceItems[index].mark
|
|
guard case let .mapping(mapping) = value else {
|
|
throw PureYAML.Parsing.MergeResolution.invalidMergeValue(mark: mark)
|
|
}
|
|
return mapping.pairs
|
|
}
|
|
case .bool, .double, .int, .null, .string:
|
|
throw PureYAML.Parsing.MergeResolution.invalidMergeValue(mark: node.mark)
|
|
}
|
|
}
|
|
|
|
func unexpectedEvent(
|
|
expected: String,
|
|
mark: PureYAML.Parsing.Mark,
|
|
) -> PureYAML.Parsing.ParseError {
|
|
PureYAML.Parsing.ParseError.unexpectedEvent(
|
|
expected: expected,
|
|
actual: "streaming event",
|
|
line: mark.line,
|
|
column: mark.column,
|
|
)
|
|
}
|
|
}
|