feat(parsing): port libyaml simple-key mechanism to Swift
Stage 2 of the faithful libyaml scanner port. Translates yaml_parser_save_simple_key, yaml_parser_remove_simple_key, yaml_parser_stale_simple_keys, and the flow-level push/pop (yaml_parser_increase_flow_level / decrease) onto a new PureYAML.Parsing.LibYAML.Scanner aggregate that embeds the indent engine. The simple-key mechanism is what lets a quoted scalar, a nested collection, or a property-carrying node become a mapping key when a ':' follows, and is required exactly at the block indent. Modelled faithfully: a potential key goes stale once it spans a line break or exceeds 1024 characters, and a required key that is removed or goes stale is the 'could not find expected colon' error. Unit-tested for the required/not-required, removal-error, staleness, and flow-level cases. No public behavior changed yet.
This commit is contained in:
parent
c3033c356d
commit
9ce57e86e5
92
Sources/Parsing/LibYAML.Scanner.swift
Normal file
92
Sources/Parsing/LibYAML.Scanner.swift
Normal file
@ -0,0 +1,92 @@
|
||||
extension PureYAML.Parsing.LibYAML {
|
||||
/// Aggregate scanner state, mirroring the fields of `yaml_parser_t` that the
|
||||
/// ported functions read and write. It is built up as the port proceeds;
|
||||
/// currently it holds the indentation engine and the simple-key mechanism.
|
||||
/// Column values follow libyaml's 0-based convention internally; conversion to
|
||||
/// the package's 1-based `Mark` happens at token emission during integration.
|
||||
struct Scanner {
|
||||
/// The indentation stack and `roll_indent` / `unroll_indent` (also the
|
||||
/// owner of `flow_level` and the token queue).
|
||||
var indentEngine = IndentEngine()
|
||||
/// `parser->simple_keys`. Always holds at least the block-context level.
|
||||
var simpleKeys: [SimpleKey] = [.empty]
|
||||
/// `parser->simple_key_allowed`. True at stream start.
|
||||
var simpleKeyAllowed = true
|
||||
/// `parser->mark`, the current position (column is 0-based here).
|
||||
var mark: PureYAML.Parsing.Mark = .start
|
||||
|
||||
/// `parser->flow_level`, kept on the indent engine.
|
||||
var flowLevel: Int {
|
||||
indentEngine.flowLevel
|
||||
}
|
||||
|
||||
/// Port of `yaml_parser_stale_simple_keys`. A potential simple key is
|
||||
/// invalid once it is on an earlier line or more than 1024 characters back
|
||||
/// (a simple key is single-line and bounded); a required one going stale is
|
||||
/// an error.
|
||||
mutating func staleSimpleKeys() throws {
|
||||
for index in simpleKeys.indices {
|
||||
let key = simpleKeys[index]
|
||||
guard key.possible, key.mark.line < mark.line || key.mark.index + 1024 < mark.index else {
|
||||
continue
|
||||
}
|
||||
if key.required {
|
||||
throw ScannerError(
|
||||
context: "while scanning a simple key",
|
||||
problem: "could not find expected ':'",
|
||||
mark: key.mark,
|
||||
)
|
||||
}
|
||||
simpleKeys[index].possible = false
|
||||
}
|
||||
}
|
||||
|
||||
/// Port of `yaml_parser_save_simple_key`. Record the current position as a
|
||||
/// potential simple key for this flow level when one is allowed. A key is
|
||||
/// required (a `:` must follow) when it sits exactly at the block indent.
|
||||
mutating func saveSimpleKey() throws {
|
||||
let required = flowLevel == 0 && indentEngine.indent == mark.column
|
||||
guard simpleKeyAllowed else {
|
||||
return
|
||||
}
|
||||
let key = SimpleKey(
|
||||
possible: true,
|
||||
required: required,
|
||||
tokenNumber: indentEngine.tokensParsed + indentEngine.tokens.count,
|
||||
mark: mark,
|
||||
)
|
||||
try removeSimpleKey()
|
||||
simpleKeys[simpleKeys.count - 1] = key
|
||||
}
|
||||
|
||||
/// Port of `yaml_parser_remove_simple_key`. Drop the potential key at the
|
||||
/// current flow level; removing a required key is an error.
|
||||
mutating func removeSimpleKey() throws {
|
||||
let top = simpleKeys.count - 1
|
||||
if simpleKeys[top].possible, simpleKeys[top].required {
|
||||
throw ScannerError(
|
||||
context: "while scanning a simple key",
|
||||
problem: "could not find expected ':'",
|
||||
mark: simpleKeys[top].mark,
|
||||
)
|
||||
}
|
||||
simpleKeys[top].possible = false
|
||||
}
|
||||
|
||||
/// Port of `yaml_parser_increase_flow_level`. Push an empty simple key for
|
||||
/// the new flow level.
|
||||
mutating func increaseFlowLevel() {
|
||||
simpleKeys.append(.empty)
|
||||
indentEngine.flowLevel += 1
|
||||
}
|
||||
|
||||
/// Port of `yaml_parser_decrease_flow_level`. Pop the flow level's simple
|
||||
/// key.
|
||||
mutating func decreaseFlowLevel() {
|
||||
if indentEngine.flowLevel > 0 {
|
||||
indentEngine.flowLevel -= 1
|
||||
simpleKeys.removeLast()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
Sources/Parsing/LibYAML.ScannerError.swift
Normal file
10
Sources/Parsing/LibYAML.ScannerError.swift
Normal file
@ -0,0 +1,10 @@
|
||||
extension PureYAML.Parsing.LibYAML {
|
||||
/// A scanning error, mirroring libyaml's `yaml_parser_set_scanner_error`
|
||||
/// (`context` plus `problem` at a mark). It is mapped to the package's public
|
||||
/// `ParseError` when the port is integrated.
|
||||
struct ScannerError: Swift.Error, Equatable {
|
||||
var context: String
|
||||
var problem: String
|
||||
var mark: PureYAML.Parsing.Mark
|
||||
}
|
||||
}
|
||||
20
Sources/Parsing/LibYAML.SimpleKey.swift
Normal file
20
Sources/Parsing/LibYAML.SimpleKey.swift
Normal file
@ -0,0 +1,20 @@
|
||||
extension PureYAML.Parsing.LibYAML {
|
||||
/// Port of `yaml_simple_key_t`. A potential implicit ("simple") mapping key:
|
||||
/// a node that may turn out to be a key once a `:` is seen on the same line.
|
||||
/// `tokenNumber` records where its `KEY` token must be spliced into the queue.
|
||||
struct SimpleKey: Equatable {
|
||||
/// Whether a simple key is still possible at this flow level.
|
||||
var possible: Bool
|
||||
/// Whether a `:` is required (a key sitting exactly at the block indent).
|
||||
var required: Bool
|
||||
/// The absolute token number where the `KEY` token is inserted.
|
||||
var tokenNumber: Int
|
||||
/// The position the potential key started at.
|
||||
var mark: PureYAML.Parsing.Mark
|
||||
|
||||
/// The empty entry pushed for a new flow level (`{ 0, 0, 0, {0,0,0} }`).
|
||||
static var empty: Self {
|
||||
Self(possible: false, required: false, tokenNumber: 0, mark: .start)
|
||||
}
|
||||
}
|
||||
}
|
||||
66
Tests/LibYAMLSimpleKeyTests.swift
Normal file
66
Tests/LibYAMLSimpleKeyTests.swift
Normal file
@ -0,0 +1,66 @@
|
||||
@testable import PureYAML
|
||||
import Testing
|
||||
|
||||
/// Verifies the ported simple-key mechanism (`save_simple_key`,
|
||||
/// `remove_simple_key`, `stale_simple_keys`, flow-level push/pop) reproduces
|
||||
/// libyaml's behavior.
|
||||
@Suite("LibYAML Simple Key")
|
||||
struct LibYAMLSimpleKeyTests {
|
||||
private func mark(line: Int, column: Int, index: Int) -> PureYAML.Parsing.Mark {
|
||||
PureYAML.Parsing.Mark(line: line, column: column, index: index)
|
||||
}
|
||||
|
||||
@Test("saves a possible key, required when it sits at the block indent")
|
||||
func test_saveSimpleKeyRequiredAtIndent() throws {
|
||||
var scanner = PureYAML.Parsing.LibYAML.Scanner()
|
||||
scanner.indentEngine.rollIndent(column: 0, number: -1, type: .blockMappingStart, mark: .start)
|
||||
scanner.mark = mark(line: 1, column: 0, index: 0)
|
||||
try scanner.saveSimpleKey()
|
||||
#expect(scanner.simpleKeys.last?.possible == true)
|
||||
#expect(scanner.simpleKeys.last?.required == true)
|
||||
}
|
||||
|
||||
@Test("a key not at the block indent is possible but not required")
|
||||
func test_saveSimpleKeyNotRequired() throws {
|
||||
var scanner = PureYAML.Parsing.LibYAML.Scanner()
|
||||
scanner.indentEngine.rollIndent(column: 0, number: -1, type: .blockMappingStart, mark: .start)
|
||||
scanner.mark = mark(line: 1, column: 4, index: 4)
|
||||
try scanner.saveSimpleKey()
|
||||
#expect(scanner.simpleKeys.last?.possible == true)
|
||||
#expect(scanner.simpleKeys.last?.required == false)
|
||||
}
|
||||
|
||||
@Test("removing a required possible key is an error")
|
||||
func test_removeRequiredKeyThrows() throws {
|
||||
var scanner = PureYAML.Parsing.LibYAML.Scanner()
|
||||
scanner.indentEngine.rollIndent(column: 0, number: -1, type: .blockMappingStart, mark: .start)
|
||||
scanner.mark = mark(line: 1, column: 0, index: 0)
|
||||
try scanner.saveSimpleKey()
|
||||
#expect(throws: PureYAML.Parsing.LibYAML.ScannerError.self) {
|
||||
try scanner.removeSimpleKey()
|
||||
}
|
||||
}
|
||||
|
||||
@Test("a non-required key on an earlier line goes stale without error")
|
||||
func test_staleNonRequiredKey() throws {
|
||||
var scanner = PureYAML.Parsing.LibYAML.Scanner()
|
||||
scanner.indentEngine.rollIndent(column: 0, number: -1, type: .blockMappingStart, mark: .start)
|
||||
scanner.mark = mark(line: 1, column: 4, index: 4)
|
||||
try scanner.saveSimpleKey()
|
||||
scanner.mark = mark(line: 2, column: 0, index: 10)
|
||||
try scanner.staleSimpleKeys()
|
||||
#expect(scanner.simpleKeys.last?.possible == false)
|
||||
}
|
||||
|
||||
@Test("flow level pushes and pops a simple-key slot")
|
||||
func test_flowLevelStack() {
|
||||
var scanner = PureYAML.Parsing.LibYAML.Scanner()
|
||||
#expect(scanner.simpleKeys.count == 1)
|
||||
scanner.increaseFlowLevel()
|
||||
#expect(scanner.flowLevel == 1)
|
||||
#expect(scanner.simpleKeys.count == 2)
|
||||
scanner.decreaseFlowLevel()
|
||||
#expect(scanner.flowLevel == 0)
|
||||
#expect(scanner.simpleKeys.count == 1)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user