PureYAML/Sources/Parsing/LibYAML.Scanner.Streaming.swift
Mihaela Mihaljevic 18fdf9b69a feat(parsing): add a resumable scanner increment and broaden the mutation suite
Add Scanner.scanStep(), the first increment of true chunked input streaming
(#4): Reader gains isFinal/append(_:)/markFinal(), and peek(_:) flags ranDry
instead of asserting end of input when the buffer runs out mid-fetch and more
chunks may still arrive. scanStep() snapshots the scanner before each
fetchNextToken() and restores it whenever a fetch runs dry, so a construct
spanning a chunk boundary is retried after more input lands rather than
misread as an early end of input. Every existing call site keeps the default
isFinal: true, so scanAll() and batch parsing are unchanged; ranDry can never
be set when isFinal is true, by construction. A token already queued is not
always safe to release, since libyaml's simple-key mechanism can still splice
a KEY token in front of it, so scanStep() ports libyaml's
yaml_parser_fetch_more_tokens release condition.

Proof: StreamingScannerDifferentialTests.swift drives scanStep() at several
chunk sizes over representative cases always, and over the entire
yaml-test-suite manifest (402 cases) behind PUREYAML_RUN_YAML_TEST_SUITE=1,
asserting the token sequence matches scanAll() exactly. That proof caught a
real bug: lastTokenForbidsInlineSequence() read indentEngine.tokens.last,
which goes empty once tokens are released incrementally, silently breaking
the key: - a rejection. Fixed by tracking IndentEngine.lastToken explicitly,
kept in sync with tokens.last so batch behavior (which never dequeues) is
unchanged.

Not yet wired into PureYAMLStreaming; scanStep() and its supporting types stay
internal until that integration is designed.

Also broaden the real-seed mutation property suite (#6) with the three
mutation classes it named but didn't yet cover: invalidControlCharacter,
mixedTabSpaceIndentation, and truncatedFlowCollection. The test now asserts
diagnostic quality, not just pass/fail: line must fall within the mutated
document, and column, when present, must be 1-based (some ParseError cases,
like tabIndentation, are line-only by design).
2026-07-09 23:38:39 +02:00

60 lines
2.6 KiB
Swift

extension PureYAML.Parsing.LibYAML.Scanner {
/// The result of one `scanStep()` call.
enum ScanOutcome: Equatable {
/// A token is safe to release to the caller now.
case token(PureYAML.Parsing.LibYAML.Token)
/// The buffered input was exhausted mid-fetch; append more input (via
/// `reader.append(_:)`) and call `scanStep()` again. The scanner's state is
/// unchanged from before this call, as if it had not been called.
case needMoreInput
/// STREAM-END has been released and no further tokens remain.
case done
}
/// Resumable, one-token-at-a-time counterpart to `scanAll()`. Where `scanAll()`
/// assumes the full input is already buffered, `scanStep()` may be driven across
/// a `Reader` still receiving chunks (`isFinal == false`): a fetch that runs off
/// the buffered end is rolled back in full (as if it had not been attempted)
/// rather than mistaken for true end of input.
///
/// A token already queued is not always safe to hand out immediately: libyaml's
/// simple-key mechanism can still splice a KEY (and a BLOCK-MAPPING-START) in
/// front of it once a later `:` is found (`saveSimpleKey`/`insert(_:atTokenNumber:)`
/// in `LibYAML.Scanner.swift`/`LibYAML.IndentEngine.swift`). Port of libyaml's
/// `yaml_parser_fetch_more_tokens` release condition: the token at the queue
/// front (position `tokensParsed`) is safe to release only when no currently
/// possible simple key (at any flow level) is still recorded at that exact
/// position; such a key could still resolve into a splice targeting that slot.
mutating func scanStep() throws -> ScanOutcome {
while true {
if isFrontSafeToRelease, let token = indentEngine.dequeue() {
return .token(token)
}
if streamEndProduced {
return .done
}
let checkpoint = self
do {
try fetchNextToken()
} catch {
if !reader.isFinal, reader.ranDry {
self = checkpoint
return .needMoreInput
}
throw error
}
if !reader.isFinal, reader.ranDry {
self = checkpoint
return .needMoreInput
}
}
}
private var isFrontSafeToRelease: Bool {
guard !indentEngine.tokens.isEmpty else {
return false
}
return !simpleKeys.contains { $0.possible && $0.tokenNumber == indentEngine.tokensParsed }
}
}