PureYAML/Sources/Parsing/EventComposer.swift
Mihaela Mihaljevic a8950e118c Merge conformance/yaml-test-suite-gaps: the libyaml port becomes the engine on main
The ported engine wins every conflict: main's post-fork patches to the
hand-rolled Scanner and TokenEventParser land on deleted files and are
dropped; their behaviors are carried by the port and guarded by the
whole-suite conformance gate (402/402 passing).

Merge adaptations, all gates green (style, namespacing, forbidden
patterns, changelog, roadmap, swiftformat, swiftlint strict, build,
full test suite, whole-suite conformance sweep):

- PureYAMLJSON representability rules move onto the Check algebra,
  replacing the retired legacy Rule type.
- The permissive tag-handle pre-pass is removed; the port rejects an
  undefined handle at parse time (suite case QLJ7) and the validation
  report carries it as a parse diagnostic.
- EventStreamComposer's nested types split one-per-file per the
  namespacing gate.
- Tests pinned to old-engine behavior were re-verified against the
  reference and updated: libyaml rejects an explicit-key continuation
  ending at a value colon, and suite case QB6E requires rejecting a
  quoted continuation at column 0.
- CHANGELOG unreleased entries from both lines are preserved in two
  groups with a provenance note.

Closes #1, closes #3
2026-07-05 00:23:48 +02:00

249 lines
7.9 KiB
Swift

extension PureYAML.Parsing {
struct EventComposer {
var events: [Event]
var index: Int
var anchors: [String: PureYAML.Model.Value]
let scalarParser: Parser
init(
events: [Event],
scalarParser: Parser,
) {
self.events = events
index = 0
anchors = [:]
self.scalarParser = scalarParser
}
mutating func compose() throws -> PureYAML.Model.Value {
_ = try expect("stream start") { event in
if case .streamStart = event {
return true
}
return false
}
// An empty or comment-only stream has no document events; the
// single-document value API maps that to null.
if current?.isStreamEnd == true {
_ = try expectStreamEnd()
return .null
}
_ = try expect("document start") { event in
if case .documentStart = event {
return true
}
return false
}
let value = try composeNode()
_ = try expect("document end") { event in
if case .documentEnd = event {
return true
}
return false
}
if case let .documentStart(_, mark)? = current {
throw PureYAML.Parsing.ParseError.unsupportedMultiDocumentStream(line: mark.line)
}
_ = try expectStreamEnd()
return value
}
mutating func composeStream() throws -> [PureYAML.Stream.Document] {
_ = try expect("stream start") { event in
if case .streamStart = event {
return true
}
return false
}
var documents: [PureYAML.Stream.Document] = []
while current?.isStreamEnd != true {
_ = try expect("document start") { event in
if case .documentStart = event {
return true
}
return false
}
anchors = [:]
let value = try composeNode()
_ = try expect("document end") { event in
if case .documentEnd = event {
return true
}
return false
}
documents.append(.init(index: documents.count, value: value))
}
_ = try expectStreamEnd()
return documents
}
}
}
extension PureYAML.Parsing.EventComposer {
mutating func composeNode() throws -> PureYAML.Model.Value {
guard let event = current else {
throw unexpectedEvent(expected: "YAML node")
}
switch event {
case let .alias(anchor, mark):
index += 1
guard let value = anchors[anchor] else {
throw PureYAML.Parsing.ParseError.undefinedAlias(
anchor: anchor,
line: mark.line,
column: mark.column,
)
}
return value
case let .mappingStart(anchor, _, _, _):
let value = try composeMapping()
store(value, anchor: anchor)
return value
case let .scalar(value, anchor, tag, style, mark, _):
index += 1
let model = try composeScalar(
value,
tag: tag,
style: style,
mark: mark,
)
store(model, anchor: anchor)
return model
case let .sequenceStart(anchor, _, _, _):
let value = try composeSequence()
store(value, anchor: anchor)
return value
default:
throw unexpectedEvent(expected: "YAML node")
}
}
mutating func composeSequence() throws -> PureYAML.Model.Value {
_ = try expect("sequence start") { event in
if case .sequenceStart = event {
return true
}
return false
}
var values: [PureYAML.Model.Value] = []
while current?.isSequenceEnd != true {
try values.append(composeNode())
}
_ = try expect("sequence end") { $0.isSequenceEnd }
return .sequence(values)
}
mutating func composeMapping() throws -> PureYAML.Model.Value {
_ = try expect("mapping start") { event in
if case .mappingStart = event {
return true
}
return false
}
var mergeSources: [[PureYAML.Model.Pair]] = []
var localPairs: [PureYAML.Model.Pair] = []
while current?.isMappingEnd != true {
let key = try composeMappingKey()
if key.isMergeKey {
try mergeSources.append(contentsOf: composeMergeSources())
} else {
let value = try composeNode()
localPairs.append(PureYAML.Model.Pair(keyNode: key.keyNode, value: value))
}
}
_ = try expect("mapping end") { $0.isMappingEnd }
let pairs = PureYAML.Parsing.MergeResolution.mergedPairs(from: mergeSources, localPairs: localPairs)
return .mapping(PureYAML.Model.Mapping(pairs))
}
mutating func composeMappingKey() throws -> ComposedMappingKey {
guard let event = current else {
throw unexpectedEvent(expected: "mapping key")
}
guard case let .scalar(value, anchor, tag, style, _, _) = event else {
let value = try composeNode()
return ComposedMappingKey(
keyNode: PureYAML.Model.Key(value: value),
scalarValue: nil,
tag: nil,
style: nil,
)
}
index += 1
if let anchor {
let model = try composeScalar(value, tag: tag, style: style, mark: .start)
store(model, anchor: anchor)
}
return ComposedMappingKey(
keyNode: .string(value),
scalarValue: value,
tag: scalarParser.normalizedTag(tag),
style: style,
)
}
func composeScalar(
_ value: String,
tag: String?,
style: PureYAML.Parsing.ScalarStyle,
mark: PureYAML.Parsing.Mark,
) throws -> PureYAML.Model.Value {
try scalarParser.composeScalarValue(value, tag: tag, style: style, mark: mark)
}
}
extension PureYAML.Parsing.EventComposer {
var current: PureYAML.Parsing.Event? {
guard events.indices.contains(index) else {
return nil
}
return events[index]
}
mutating func store(
_ value: PureYAML.Model.Value,
anchor: String?,
) {
guard let anchor else {
return
}
anchors[anchor] = value
}
mutating func expect(
_ expected: String,
matching: (PureYAML.Parsing.Event) -> Bool,
) throws -> PureYAML.Parsing.Event {
guard let event = current else {
throw unexpectedEvent(expected: expected)
}
guard matching(event) else {
throw unexpectedEvent(expected: expected)
}
index += 1
return event
}
func unexpectedEvent(expected: String) -> PureYAML.Parsing.ParseError {
let mark = current?.mark ?? events.last?.mark ?? .start
return PureYAML.Parsing.ParseError.unexpectedEvent(
expected: expected,
actual: current?.description ?? "end of events",
line: mark.line,
column: mark.column,
)
}
mutating func expectStreamEnd() throws -> PureYAML.Parsing.Event {
try expect("stream end") { event in
if case .streamEnd = event {
return true
}
return false
}
}
}