PureYAML/Sources/Model/LocatedValue.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

59 lines
2.7 KiB
Swift

public extension PureYAML.Model {
/// A value tree annotated with source spans, parallel in shape to ``Value``: every node
/// carries its own ``SourceRange`` and its located children. It is produced by the opt-in
/// ``PureYAML/parseLocated(_:)`` and ``PureYAML/parseStreamLocated(_:)`` entry points; the
/// plain ``Value`` API is unchanged, so source positions never affect value equality, the
/// conformance oracle, or any existing consumer.
///
/// `value` projects the position-free ``Value`` (identical to what a plain parse returns),
/// and `range` is the node's own span, so a consumer can keep operating on ``Value`` and
/// consult the located tree only when it needs a location.
indirect enum LocatedValue: Equatable, Sendable {
/// A leaf scalar (`null`, `bool`, `int`, `double`, or `string`) with its start-anchored span.
case scalar(Value, SourceRange)
/// A sequence: its located items and the span from its opening to its closing.
case sequence([LocatedValue], SourceRange)
/// A mapping: its located pairs and the span from its opening to its closing.
case mapping([LocatedPair], SourceRange)
}
}
public extension PureYAML.Model.LocatedValue {
/// The span of this node.
var range: PureYAML.Model.SourceRange {
switch self {
case let .scalar(_, range), let .sequence(_, range), let .mapping(_, range):
range
}
}
/// The position-free value this located node projects to, identical to a plain parse.
var value: PureYAML.Model.Value {
switch self {
case let .scalar(value, _):
value
case let .sequence(items, _):
.sequence(items.map(\.value))
case let .mapping(pairs, _):
.mapping(PureYAML.Model.Mapping(pairs.map(\.pair)))
}
}
/// Wrap a plain ``Value`` so every node shares one span. Used where a real per-node span is
/// not available (an alias expansion or a merge-inherited pair, whose content is defined
/// elsewhere): the whole subtree honestly points at its single reference site.
static func synthesized(from value: PureYAML.Model.Value, at range: PureYAML.Model.SourceRange) -> Self {
switch value {
case .null, .bool, .int, .double, .string:
return .scalar(value, range)
case let .sequence(items):
return .sequence(items.map { synthesized(from: $0, at: range) }, range)
case let .mapping(mapping):
let pairs = mapping.pairs.map {
PureYAML.Model.LocatedPair(keyNode: $0.keyNode, keyRange: range, value: synthesized(from: $0.value, at: range))
}
return .mapping(pairs, range)
}
}
}