From 348af8b77bcd1e789f19278e254183e35d0177c0 Mon Sep 17 00:00:00 2001 From: Mihaela Mihaljevic Date: Wed, 24 Jun 2026 03:00:28 +0200 Subject: [PATCH] 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. --- Sources/Parsing/LibYAML.CharacterClass.swift | 94 ++++++++++++++++++++ Tests/LibYAMLCharacterClassTests.swift | 65 ++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 Sources/Parsing/LibYAML.CharacterClass.swift create mode 100644 Tests/LibYAMLCharacterClassTests.swift diff --git a/Sources/Parsing/LibYAML.CharacterClass.swift b/Sources/Parsing/LibYAML.CharacterClass.swift new file mode 100644 index 0000000..77153de --- /dev/null +++ b/Sources/Parsing/LibYAML.CharacterClass.swift @@ -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) + } + } +} diff --git a/Tests/LibYAMLCharacterClassTests.swift b/Tests/LibYAMLCharacterClassTests.swift new file mode 100644 index 0000000..6f08c19 --- /dev/null +++ b/Tests/LibYAMLCharacterClassTests.swift @@ -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}")) + } +}