PureYAML/Sources/Parsing/EventStreamComposer.swift
Mihaela Mihaljevic e8987e4cfd feat(parsing): add authoritative canonical-event conformance gate
The conformance number was measured against a lossy oracle: the JSON
projection (in.json) discards anchors, tags, and scalar styles, and exists
for only about half the cases, so "passing" often meant only "parse did not
throw". Measured against the official suite's own test.event event streams,
the true exact conformance is 254 of 308 valid cases (40 emit a wrong event
stream, 14 false-reject), well below the JSON-proxy figure.

Add a canonical-event gate (Tests/CanonicalEventConformanceTests.swift) that
serializes the parser's events into the yaml-test-suite yeast format and
compares them exactly to each case's test.event, checking anchors, tags,
scalar styles, flow vs block, and explicit document markers, for every valid
case. The gate records the gap set and fails on any regression or unrecorded
fix. All 402 test.event files are vendored alongside their inputs.

To make the comparison exact, the parser Event model now records whether a
document boundary was explicit (documentStart/documentEnd carry an `explicit`
flag), distinguishing `---`/`...` from an implicit boundary. This resolves 89
cases that were previously correct but unverifiable without normalization.

No parser behavior changed; this is measurement and model fidelity. The 40
wrong-event and 14 false-reject cases are the real remaining gaps, to be fixed
against this gate.
2026-06-24 01:24:24 +02:00

319 lines
11 KiB
Swift

extension PureYAML.Parsing {
enum EventStreamComposerMode {
case singleDocument
case stream
}
struct EventStreamComposedNode {
var value: PureYAML.Model.Value
var mappingKey: PureYAML.Parsing.EventComposer.ComposedMappingKey
var mark: PureYAML.Parsing.Mark
var sequenceItems: [EventStreamComposedNode] = []
}
enum EventStreamContainerKind {
case sequence
case mapping
}
struct EventStreamContainer {
var kind: EventStreamContainerKind
var anchor: String?
var values: [EventStreamComposedNode] = []
var mergeSources: [[PureYAML.Model.Pair]] = []
var localPairs: [PureYAML.Model.Pair] = []
var pendingKey: PureYAML.Parsing.EventComposer.ComposedMappingKey?
var mark: PureYAML.Parsing.Mark
}
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 {
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 = 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 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 invalidMergeValue(mark: mark)
}
return mapping.pairs
}
case .bool, .double, .int, .null, .string:
throw invalidMergeValue(mark: node.mark)
}
}
func mergedPairs(
from mergeSources: [[PureYAML.Model.Pair]],
localPairs: [PureYAML.Model.Pair],
) -> [PureYAML.Model.Pair] {
guard !mergeSources.isEmpty else {
return localPairs
}
let localKeys = Set(localPairs.map(\.keyNode))
var inheritedKeys = Set<PureYAML.Model.Key>()
var inheritedPairs: [PureYAML.Model.Pair] = []
for source in mergeSources {
var sourceKeys = Set<PureYAML.Model.Key>()
for pair in source {
guard !localKeys.contains(pair.keyNode),
!inheritedKeys.contains(pair.keyNode)
else {
continue
}
inheritedPairs.append(pair)
sourceKeys.insert(pair.keyNode)
}
inheritedKeys.formUnion(sourceKeys)
}
return inheritedPairs + localPairs
}
func invalidMergeValue(
mark: PureYAML.Parsing.Mark,
) -> PureYAML.Parsing.ParseError {
PureYAML.Parsing.ParseError.invalidMergeValue(
line: mark.line,
column: mark.column,
)
}
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,
)
}
}