From 7f94e17e2fd83b12aaa5eee59ceaa2443cc338b9 Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Wed, 24 Jun 2026 03:04:08 +0200 Subject: [PATCH] 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. --- Sources/Parsing/LibYAML.Reader.swift | 57 ++++++++++++++++++++++++++++ Tests/LibYAMLReaderTests.swift | 46 ++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 Sources/Parsing/LibYAML.Reader.swift create mode 100644 Tests/LibYAMLReaderTests.swift diff --git a/Sources/Parsing/LibYAML.Reader.swift b/Sources/Parsing/LibYAML.Reader.swift new file mode 100644 index 0000000..611af5b --- /dev/null +++ b/Sources/Parsing/LibYAML.Reader.swift @@ -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 + } + } +} diff --git a/Tests/LibYAMLReaderTests.swift b/Tests/LibYAMLReaderTests.swift new file mode 100644 index 0000000..1ac8f50 --- /dev/null +++ b/Tests/LibYAMLReaderTests.swift @@ -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") + } +}