PureYAML/Sources/Parsing/TokenCursor.swift
Mihaela Mihaljevic ce0585f77e test(conformance): track all 402 suite cases; fix 3 regressions
The gap-only manifest stopped guarding a case once it was fixed, so later
work silently regressed previously-fixed cases (caught only by an external
full-suite run, not the gated sweep or full corpus). Replace it with a full
manifest of all 402 official cases (Tests/Fixtures/yaml-test-suite-manifest.yaml),
each carrying its conformant expectation and a passing/gap status, with every
case's files vendored. The gated sweep now asserts every passing case still
passes (the regression guard) and every gap still reproduces, so any regression
fails the build.

The honest gate-enforced count is 329/402 passing (the earlier 348 came from a
more lenient one-time oracle).

Repairs three regressions the new gate revealed: the bucket-6 tab rule
over-rejected tabs in block-scalar content and as separation before a scalar
value (root cause: the batch scanner and the incremental TokenCursor had
divergent line-start loops; unified onto a single scanStep, fixing 96NN/00,
96NN/01, R4YG, Y79Y/001, DK95/00, DK95/03, DK95/07, 6CA3, Q5MG, J3BT); an anchor
on a prior line before an alias key wrongly rejected 26DV; and a tag-only flow
key mis-parsed WZ62. Genuine tab-indentation errors (4EJS, Y79Y/000) still reject.
2026-06-24 00:16:28 +02:00

219 lines
6.0 KiB
Swift

extension PureYAML.Parsing {
private final class TokenScannerStateBox {
var state: Scanner.State
init(state: Scanner.State) {
self.state = state
}
}
struct TokenCursor {
private var tokens: [Token]
private(set) var index: Int
private var previousToken: Token?
private var scanner: Scanner?
private var scannerBox: TokenScannerStateBox?
init(tokens: [Token]) {
self.tokens = tokens.filter { !$0.kind.isComment }
index = 0
previousToken = nil
scanner = nil
scannerBox = nil
}
init(yaml: String, scanner: Scanner = .init()) {
var state = Scanner.State(reader: Reader(yaml))
state.append(.streamStart)
tokens = state.tokens.filter { !$0.kind.isComment }
state.tokens.removeAll(keepingCapacity: true)
index = 0
previousToken = nil
self.scanner = scanner
scannerBox = TokenScannerStateBox(state: state)
}
var current: Token? {
guard tokens.indices.contains(index) else {
return nil
}
return tokens[index]
}
var previous: Token? {
previousToken
}
mutating func peek(offset: Int = 1) throws -> Token? {
try ensureAvailable(offset: offset)
let targetIndex = index + offset
guard tokens.indices.contains(targetIndex) else {
return nil
}
return tokens[targetIndex]
}
mutating func advance() throws -> Token? {
try ensureAvailable(offset: 0)
guard tokens.indices.contains(index) else {
return nil
}
let token = tokens[index]
previousToken = token
index += 1
compactConsumedTokens()
try ensureAvailable(offset: 0)
return token
}
private mutating func ensureAvailable(offset: Int) throws {
while tokens.count <= index + offset, scannerBox != nil {
try scanMoreTokens()
}
}
private mutating func scanMoreTokens() throws {
guard let scanner, let scannerBox else {
return
}
// Scan at least one token, and keep scanning without draining while a
// flow collection is open: a flow `:` may splice a `mappingKey` into an
// already-scanned earlier token (the simple-key mechanism), so the whole
// flow collection's tokens must stay in `state.tokens` until it closes
// for the retroactive index to remain valid.
while scannerBox.state.tokens.isEmpty || scannerBox.state.flowDepth > 0 {
if scannerBox.state.reader.isAtEnd {
if scannerBox.state.hasPendingDirective {
throw PureYAML.Parsing.ParseError.directiveWithoutDocument(
line: scannerBox.state.reader.mark.line,
)
}
scanner.closeIndentation(&scannerBox.state)
scannerBox.state.append(.streamEnd)
drainScannedTokens(from: &scannerBox.state)
self.scannerBox = nil
return
}
// Drive the same scanning step the batch scanner uses, so the two
// paths share one source of truth for line breaks, blank lines,
// indentation, and `simpleKeyAllowed` handling.
let step = try scanner.scanStep(&scannerBox.state)
if case .scannedMarker = step, !scannerBox.state.tokens.isEmpty {
break
}
}
drainScannedTokens(from: &scannerBox.state)
}
private mutating func drainScannedTokens(from state: inout Scanner.State) {
tokens.append(contentsOf: state.tokens.filter { !$0.kind.isComment })
state.tokens.removeAll(keepingCapacity: true)
}
private mutating func compactConsumedTokens() {
guard index > 4096 else {
return
}
tokens.removeFirst(index)
index = 0
}
}
}
extension PureYAML.Parsing.TokenKind {
var isAlias: Bool {
if case .alias = self {
return true
}
return false
}
var isNodeProperty: Bool {
switch self {
case .anchor, .tag:
true
default:
false
}
}
var isBlockEntry: Bool {
if case .blockEntry = self {
return true
}
return false
}
var isComment: Bool {
if case .comment = self {
return true
}
return false
}
var isDedent: Bool {
if case .dedent = self {
return true
}
return false
}
var isDocumentEnd: Bool {
if case .documentEnd = self {
return true
}
return false
}
var isDocumentStart: Bool {
if case .documentStart = self {
return true
}
return false
}
var isFlowTerminator: Bool {
switch self {
case .flowEntry, .flowMappingEnd, .flowSequenceEnd:
true
default:
false
}
}
var isIndent: Bool {
if case .indent = self {
return true
}
return false
}
var isMappingKey: Bool {
if case .mappingKey = self {
return true
}
return false
}
var isMappingValue: Bool {
if case .mappingValue = self {
return true
}
return false
}
var isStreamEnd: Bool {
if case .streamEnd = self {
return true
}
return false
}
var isTerminator: Bool {
isDedent || isDocumentEnd || isDocumentStart || isFlowTerminator || isStreamEnd
}
}