feat(parsing): port libyaml character-class predicates to Swift

Stage 3 foundation of the faithful libyaml scanner port. Translates the IS_* character-class macros from yaml_private.h (IS_Z, IS_SPACE, IS_TAB, IS_BLANK, IS_BREAK, IS_BREAKZ, IS_SPACEZ, IS_BLANKZ, IS_DIGIT, IS_ALPHA, IS_HEX, IS_ASCII, IS_PRINTABLE) into PureYAML.Parsing.LibYAML.CharacterClass. libyaml inspects UTF-8 bytes; the Swift port operates on Unicode.Scalar, where the multi-byte line breaks NEL (U+0085), LS (U+2028), and PS (U+2029) are single scalars, and end of input is nil, libyaml's \0 sentinel (so isZ(nil) is true and the others are false). These predicates are what the token fetchers dispatch on. Unit-tested for end-of-input, blanks, the full break set, the alpha/digit/hex sets, and the ascii/printable boundaries.
This commit is contained in:
Mihaela Mihaljevic 2026-06-24 03:00:28 +02:00
parent 9ce57e86e5
commit 348af8b77b
2 changed files with 159 additions and 0 deletions

View File

@ -0,0 +1,94 @@
extension PureYAML.Parsing.LibYAML {
/// Port of libyaml's character-class macros (`IS_*` in `yaml_private.h`).
/// libyaml inspects UTF-8 bytes; this operates on `Unicode.Scalar`, where the
/// multi-byte line breaks NEL (`U+0085`), LS (`U+2028`), and PS (`U+2029`) are
/// single scalars. End of input is `nil`, which is libyaml's `\0` sentinel, so
/// `isZ(nil)` is true and every other predicate is false for `nil`.
enum CharacterClass {
/// `IS_Z`: the end of the input (libyaml's `\0`).
static func isZ(_ scalar: Unicode.Scalar?) -> Bool {
scalar == nil
}
/// `IS_SPACE`.
static func isSpace(_ scalar: Unicode.Scalar?) -> Bool {
scalar == " "
}
/// `IS_TAB`.
static func isTab(_ scalar: Unicode.Scalar?) -> Bool {
scalar == "\t"
}
/// `IS_BLANK`: space or tab.
static func isBlank(_ scalar: Unicode.Scalar?) -> Bool {
isSpace(scalar) || isTab(scalar)
}
/// `IS_BREAK`: CR, LF, NEL, LS, or PS.
static func isBreak(_ scalar: Unicode.Scalar?) -> Bool {
switch scalar {
case "\r", "\n", "\u{85}", "\u{2028}", "\u{2029}": true
default: false
}
}
/// `IS_BREAKZ`: a line break or the end of input.
static func isBreakZ(_ scalar: Unicode.Scalar?) -> Bool {
isBreak(scalar) || isZ(scalar)
}
/// `IS_SPACEZ`: a space, a line break, or the end of input.
static func isSpaceZ(_ scalar: Unicode.Scalar?) -> Bool {
isSpace(scalar) || isBreakZ(scalar)
}
/// `IS_BLANKZ`: blank (space or tab), a line break, or the end of input.
static func isBlankZ(_ scalar: Unicode.Scalar?) -> Bool {
isBlank(scalar) || isBreakZ(scalar)
}
/// `IS_DIGIT`.
static func isDigit(_ scalar: Unicode.Scalar?) -> Bool {
guard let scalar else { return false }
return scalar >= "0" && scalar <= "9"
}
/// `IS_ALPHA`: a digit, ASCII letter, `_`, or `-` (the directive, anchor,
/// and tag-handle name characters).
static func isAlpha(_ scalar: Unicode.Scalar?) -> Bool {
guard let scalar else { return false }
return (scalar >= "0" && scalar <= "9")
|| (scalar >= "A" && scalar <= "Z")
|| (scalar >= "a" && scalar <= "z")
|| scalar == "_"
|| scalar == "-"
}
/// `IS_HEX`: a hexadecimal digit.
static func isHex(_ scalar: Unicode.Scalar?) -> Bool {
guard let scalar else { return false }
return (scalar >= "0" && scalar <= "9")
|| (scalar >= "A" && scalar <= "F")
|| (scalar >= "a" && scalar <= "f")
}
/// `IS_ASCII`.
static func isASCII(_ scalar: Unicode.Scalar?) -> Bool {
guard let scalar else { return false }
return scalar.value <= 0x7F
}
/// `IS_PRINTABLE`: the YAML `c-printable` set.
static func isPrintable(_ scalar: Unicode.Scalar?) -> Bool {
guard let scalar else { return false }
let value = scalar.value
return value == 0x09 || value == 0x0A || value == 0x0D
|| (value >= 0x20 && value <= 0x7E)
|| value == 0x85
|| (value >= 0xA0 && value <= 0xD7FF)
|| (value >= 0xE000 && value <= 0xFFFD)
|| (value >= 0x10000 && value <= 0x10FFFF)
}
}
}

View File

@ -0,0 +1,65 @@
@testable import PureYAML
import Testing
/// Verifies the ported character-class predicates match libyaml's `IS_*` macros,
/// including end-of-input as `nil` and the multi-byte line breaks.
@Suite("LibYAML Character Class")
struct LibYAMLCharacterClassTests {
private typealias Classify = PureYAML.Parsing.LibYAML.CharacterClass
@Test("end of input is Z and a break, nothing else")
func test_endOfInput() {
#expect(Classify.isZ(nil))
#expect(Classify.isBreakZ(nil))
#expect(Classify.isBlankZ(nil))
#expect(!Classify.isBreak(nil))
#expect(!Classify.isBlank(nil))
#expect(!Classify.isAlpha(nil))
}
@Test("blanks are space and tab only")
func test_blank() {
#expect(Classify.isSpace(" "))
#expect(Classify.isTab("\t"))
#expect(Classify.isBlank(" "))
#expect(Classify.isBlank("\t"))
#expect(!Classify.isBlank("\n"))
#expect(!Classify.isBlank("x"))
}
@Test("line breaks include CR, LF, NEL, LS, PS")
func test_break() {
for scalar in ["\r", "\n", "\u{85}", "\u{2028}", "\u{2029}"] as [Unicode.Scalar] {
#expect(Classify.isBreak(scalar))
#expect(Classify.isBreakZ(scalar))
#expect(Classify.isBlankZ(scalar))
}
#expect(!Classify.isBreak(" "))
#expect(!Classify.isBreak("x"))
}
@Test("alpha is alnum plus underscore and hyphen")
func test_alpha() {
for scalar in ["0", "9", "A", "Z", "a", "z", "_", "-"] as [Unicode.Scalar] {
#expect(Classify.isAlpha(scalar))
}
#expect(!Classify.isAlpha("."))
#expect(!Classify.isAlpha(" "))
#expect(Classify.isDigit("5"))
#expect(!Classify.isDigit("a"))
#expect(Classify.isHex("f"))
#expect(Classify.isHex("9"))
#expect(!Classify.isHex("g"))
}
@Test("ascii and printable boundaries")
func test_asciiPrintable() {
#expect(Classify.isASCII("\u{7F}"))
#expect(!Classify.isASCII("\u{80}"))
#expect(Classify.isPrintable("\t"))
#expect(Classify.isPrintable("A"))
#expect(Classify.isPrintable("\u{85}"))
#expect(!Classify.isPrintable("\u{0}"))
#expect(!Classify.isPrintable("\u{1F}"))
}
}