PureYAML/Sources/Parsing/EventComposer.swift
Mihaela M d4ba594012 feat(parsing): opt-in located parse on the hand-rolled engine
Port the located value tree onto the hand-rolled engine, reusing the
engine-agnostic located layer (Model.LocatedValue, LocatedPair,
SourcePosition, SourceRange, and the located composer helper) authored
on the conformance branch. The streaming composer now builds a parallel
located tree behind an opt-in tracksLocations flag, exposed as
parseLocated / parseStreamLocated; the plain Value path stays
byte-identical, gated behind the flag.

The scalar event carries the scanner token's end mark, so a located
scalar spans the whole token (end exclusive, one past the last
character) rather than being start-anchored. The hand-rolled scanner
already tracked token end marks, so this surfaces information the engine
was already computing.

Also stop emitting a spurious null document for a comment-only or empty
stream: YAML 1.2 gives such a stream zero documents, while the
single-document value API still maps it to null, so parse("") is
unchanged.

A whole-suite test proves the located projection equals a plain parse
for every passing case; the value, event-dump, and conformance suites
stay green (507 tests).
2026-06-29 07:14:42 +02:00

246 lines
7.8 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
store(.string(value), 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
}
}
}