PureYAML/Sources/Parsing/LibYAML.Parser.swift
Mihaela Mihaljevic 521062d974 feat(parsing): port libyaml parser state machine to Swift
Ports parser.c: the one-event-at-a-time state machine that turns the
scanner's token stream into the YAML event stream. Covers stream and
document framing, directive processing with %TAG handle resolution and
default handles, the node property/tag/anchor production, block and
indentless sequences, block mappings, and flow sequences and mappings
including the single-pair `?` mapping forms.

Output verified against the libyaml reference event dumper: the empty
mapping value, %TAG resolution, and shorthand tags all match byte for
byte. 331 tests, no regression.
2026-06-24 17:05:59 +02:00

145 lines
5.9 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)] = []
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(implicit: true)
case .documentStart:
try parseDocumentStart(implicit: false)
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
}