feat(parsing): port libyaml input reader to Swift

Stage 3 foundation of the faithful libyaml scanner port. Translates libyaml's input buffer and position tracking (the buffer/mark fields and the PEEK/SKIP/SKIP_LINE macros) into PureYAML.Parsing.LibYAML.Reader. It iterates Unicode scalars rather than grapheme clusters so a CR and an LF are distinct, matching libyaml's byte-oriented scanning (a CR+LF pair is still one line break), and tracks the mark in libyaml's 0-based line and column convention. Unit-tested for peek/skip column tracking and LF and CRLF line breaks. This is the input layer the token fetchers consume.
This commit is contained in:
Mihaela Mihaljevic 2026-06-24 03:04:08 +02:00
parent 348af8b77b
commit 7f94e17e2f
2 changed files with 103 additions and 0 deletions

View File

@ -0,0 +1,57 @@
extension PureYAML.Parsing.LibYAML {
/// Port of libyaml's input buffer and position tracking (the `buffer`/`mark`
/// fields with the `PEEK`, `SKIP`, and `SKIP_LINE` macros). It iterates
/// `Unicode.Scalar`s rather than grapheme clusters so a CR and an LF are
/// distinct, matching libyaml's byte-oriented scanning, and tracks the mark in
/// libyaml's 0-based line and column convention; conversion to the package's
/// 1-based `Mark` happens at token emission during integration.
struct Reader {
private let scalars: [Unicode.Scalar]
private(set) var index = 0
private(set) var line = 0
private(set) var column = 0
init(_ string: String) {
scalars = Array(string.unicodeScalars)
}
/// `PEEK_TOKEN` / `CHECK_AT`: the scalar `offset` ahead, or nil past the
/// end (libyaml's `\0`).
func peek(_ offset: Int = 0) -> Unicode.Scalar? {
let position = index + offset
return position < scalars.count ? scalars[position] : nil
}
var isAtEnd: Bool {
index >= scalars.count
}
/// The current position. Column is 0-based, as in libyaml.
var mark: PureYAML.Parsing.Mark {
PureYAML.Parsing.Mark(line: line, column: column, index: index)
}
/// `SKIP`: consume one non-break scalar, advancing the column.
mutating func skip() {
guard index < scalars.count else {
return
}
index += 1
column += 1
}
/// `SKIP_LINE`: consume a line break and start a new line. A CR+LF pair is
/// one break, matching libyaml.
mutating func skipLine() {
if peek() == "\r", peek(1) == "\n" {
index += 2
} else if CharacterClass.isBreak(peek()) {
index += 1
} else {
return
}
line += 1
column = 0
}
}
}

View File

@ -0,0 +1,46 @@
@testable import PureYAML
import Testing
/// Verifies the ported reader iterates Unicode scalars with libyaml's mark and
/// `SKIP` / `SKIP_LINE` semantics: CR and LF are distinct scalars, a CR+LF pair
/// is one line break, and the column is 0-based.
@Suite("LibYAML Reader")
struct LibYAMLReaderTests {
@Test("peek and skip advance the column scalar by scalar")
func test_peekAndSkip() {
var reader = PureYAML.Parsing.LibYAML.Reader("ab")
#expect(reader.peek() == "a")
#expect(reader.peek(1) == "b")
#expect(reader.peek(2) == nil)
reader.skip()
#expect(reader.peek() == "b")
#expect(reader.column == 1)
#expect(reader.line == 0)
reader.skip()
#expect(reader.isAtEnd)
#expect(reader.peek() == nil)
}
@Test("skipLine starts a new line and resets the column")
func test_skipLineLF() {
var reader = PureYAML.Parsing.LibYAML.Reader("a\nb")
reader.skip()
reader.skipLine()
#expect(reader.line == 1)
#expect(reader.column == 0)
#expect(reader.peek() == "b")
}
@Test("a CR+LF pair is a single line break")
func test_skipLineCRLF() {
var reader = PureYAML.Parsing.LibYAML.Reader("a\r\nb")
reader.skip()
// CR and LF are distinct scalars but one break.
#expect(reader.peek() == "\r")
#expect(reader.peek(1) == "\n")
reader.skipLine()
#expect(reader.line == 1)
#expect(reader.column == 0)
#expect(reader.peek() == "b")
}
}