PureYAML/Sources/Parsing/LibYAML.Parser.swift
Mihaela Mihaljevic 0588996506 feat(parsing): run the whole library on the ported engine
Routes the value and stream APIs through the ported scanner+parser (the
event API already was), so PureYAML now parses entirely on the libyaml
port. Adds the features needed to match and exceed the previous engine:
JSON UTF-16 surrogate-pair \u escapes, bare documents after an explicit
`...`, reserved-directive handling that still requires a following `---`,
and a 64-level nesting bound enforced on the event stream so deeply nested
input fails with nestingTooDeep instead of overflowing a recursive
consumer. Parser errors are translated to the diagnostic-aware ParseError,
recovering precise codes where identifiable and a readable malformed
fallback otherwise; error-reporting fixtures are rebased onto the port.

Canonical event conformance: 268 -> 271 of 308 (7Z25, HWV9, QT73).
393 tests pass.
2026-06-24 18:02:22 +02:00

149 lines
6.1 KiB
Swift

extension PureYAML.Parsing.LibYAML {
/// A faithful port of libyaml's `parser.c`: a one-event-at-a-time state
/// machine that consumes the scanner's token stream and produces the YAML
/// event stream. The token queue is materialized up front by the scanner, so
/// `peekToken`/`skipToken` index into a fixed array rather than pulling lazily,
/// which is equivalent because simple-key resolution lives entirely in the
/// scanner.
struct Parser {
let tokens: [Token]
var cursor = 0
var state: ParserState = .streamStart
var states: [ParserState] = []
var marks: [Mark] = []
var tagDirectives: [(handle: String, prefix: String)] = []
/// Whether a bare (implicit) document may begin here: true for the first
/// document and after an explicit `...` end marker, false otherwise. YAML
/// 1.2 allows a bare document to follow `...`, which libyaml rejects.
var allowBareDocument = true
init(_ input: String = "") throws {
var scanner = Scanner(input)
tokens = try scanner.scanAll()
}
init(tokens: [Token]) {
self.tokens = tokens
}
/// Drive the state machine until STREAM-END, collecting every event.
mutating func parse() throws -> [Event] {
var events: [Event] = []
while state != .end {
try events.append(stateMachine())
}
return events
}
/// The head of the token queue. STREAM-END is always the final token and
/// the machine halts once it has been consumed, so the clamp is never the
/// value actually parsed; it only avoids an out-of-range index.
func peekToken() -> Token {
tokens[min(cursor, tokens.count - 1)]
}
mutating func skipToken() {
cursor += 1
}
/// Port of `yaml_parser_state_machine`: dispatch on the current state. The
/// flat 24-way switch is split into document/block/flow groups to keep each
/// dispatcher's branching within the complexity budget.
mutating func stateMachine() throws -> Event {
switch state {
case .streamStart, .implicitDocumentStart, .documentStart, .documentContent, .documentEnd:
try documentStateMachine()
case .blockNode, .blockNodeOrIndentlessSequence, .flowNode,
.blockSequenceFirstEntry, .blockSequenceEntry, .indentlessSequenceEntry,
.blockMappingFirstKey, .blockMappingKey, .blockMappingValue:
try nodeStateMachine()
case .flowSequenceFirstEntry, .flowSequenceEntry, .flowSequenceEntryMappingKey,
.flowSequenceEntryMappingValue, .flowSequenceEntryMappingEnd,
.flowMappingFirstKey, .flowMappingKey, .flowMappingValue, .flowMappingEmptyValue:
try flowStateMachine()
case .end:
throw endError()
}
}
private mutating func documentStateMachine() throws -> Event {
switch state {
case .streamStart:
try parseStreamStart()
case .implicitDocumentStart:
try parseDocumentStart()
case .documentStart:
try parseDocumentStart()
case .documentContent:
try parseDocumentContent()
default:
try parseDocumentEnd()
}
}
private mutating func nodeStateMachine() throws -> Event {
switch state {
case .blockNode:
try parseNode(block: true, indentlessSequence: false)
case .blockNodeOrIndentlessSequence:
try parseNode(block: true, indentlessSequence: true)
case .flowNode:
try parseNode(block: false, indentlessSequence: false)
case .blockSequenceFirstEntry:
try parseBlockSequenceEntry(first: true)
case .blockSequenceEntry:
try parseBlockSequenceEntry(first: false)
case .indentlessSequenceEntry:
try parseIndentlessSequenceEntry()
case .blockMappingFirstKey:
try parseBlockMappingKey(first: true)
case .blockMappingKey:
try parseBlockMappingKey(first: false)
default:
try parseBlockMappingValue()
}
}
private mutating func flowStateMachine() throws -> Event {
switch state {
case .flowSequenceFirstEntry:
try parseFlowSequenceEntry(first: true)
case .flowSequenceEntry:
try parseFlowSequenceEntry(first: false)
case .flowSequenceEntryMappingKey:
try parseFlowSequenceEntryMappingKey()
case .flowSequenceEntryMappingValue:
try parseFlowSequenceEntryMappingValue()
case .flowSequenceEntryMappingEnd:
try parseFlowSequenceEntryMappingEnd()
case .flowMappingFirstKey:
try parseFlowMappingKey(first: true)
case .flowMappingKey:
try parseFlowMappingKey(first: false)
case .flowMappingValue:
try parseFlowMappingValue(empty: false)
default:
try parseFlowMappingValue(empty: true)
}
}
private func endError() -> ParserError {
ParserError(context: nil, problem: "parsed past the end of the stream", mark: peekToken().startMark)
}
/// Port of `yaml_parser_process_empty_scalar`: an empty plain scalar event
/// stands in for an absent node.
func processEmptyScalar(at mark: Mark) -> Event {
.scalar(value: "", anchor: nil, tag: nil, style: .plain, mark: mark)
}
}
}
extension PureYAML.Parsing.LibYAML.Parser {
typealias Token = PureYAML.Parsing.LibYAML.Token
typealias Mark = PureYAML.Parsing.Mark
typealias Event = PureYAML.Parsing.Event
typealias ParserError = PureYAML.Parsing.LibYAML.ParserError
typealias ParserState = PureYAML.Parsing.LibYAML.ParserState
}