PureYAML's scanner is a hand-rolled approximation of libyaml's algorithm; its indentation and simple-key bugs are the gap between the two. Rather than keep patching the approximation, re-derive the scanner as a faithful, function-by-function Swift port of libyaml's scanner.c (parser.c to follow), so correctness is by construction. The no-C-runtime constraint is preserved: this translates the algorithm into pure Swift, it does not bind to libyaml. First piece: the indentation engine under PureYAML.Parsing.LibYAML, a line-for-line port of yaml_parser_roll_indent and yaml_parser_unroll_indent. A block collection opens only when content is strictly more indented than the current level, which fixes the '- a:' / same-column 'b:' mis-nesting by construction. Unit-tested against that exact case (sibling vs nested vs flow-inert). Lands as a parallel implementation verified against libyaml built from the CYaml source; swapped in only once it matches the reference with no regression. docs/LIBYAML_PORT.md records the plan. No public behavior changed yet.
59 lines
2.4 KiB
Swift
59 lines
2.4 KiB
Swift
@testable import PureYAML
|
|
import Testing
|
|
|
|
/// Verifies the ported indentation engine reproduces libyaml's `roll_indent` /
|
|
/// `unroll_indent` behavior, in particular the rule that fixes the block-mapping
|
|
/// bug: a block collection opens only when content is strictly more indented than
|
|
/// the current level.
|
|
@Suite("LibYAML Indent Engine")
|
|
struct LibYAMLIndentEngineTests {
|
|
private let mark = PureYAML.Parsing.Mark.start
|
|
|
|
@Test("a same-column key after an empty value is a sibling, not nested")
|
|
func test_sameColumnKeyIsSibling() {
|
|
// `- a:` then ` b: c`: the `-` opens a sequence at column 0; the key `a`
|
|
// opens a mapping at column 2; `a` has no value; `b` arrives at column 2.
|
|
var engine = PureYAML.Parsing.LibYAML.IndentEngine()
|
|
engine.rollIndent(column: 0, number: -1, type: .blockSequenceStart, mark: mark)
|
|
engine.rollIndent(column: 2, number: -1, type: .blockMappingStart, mark: mark)
|
|
engine.unrollIndent(column: 2, mark: mark)
|
|
engine.rollIndent(column: 2, number: -1, type: .blockMappingStart, mark: mark)
|
|
engine.unrollIndent(column: -1, mark: mark)
|
|
|
|
// One sequence, one mapping (b is a sibling of a, not nested under it).
|
|
#expect(engine.tokens.map(\.type) == [
|
|
.blockSequenceStart,
|
|
.blockMappingStart,
|
|
.blockEnd,
|
|
.blockEnd,
|
|
])
|
|
}
|
|
|
|
@Test("a more-indented value opens a nested mapping")
|
|
func test_moreIndentedValueNests() {
|
|
// `a:` then ` b: c`: the value is more indented, so it nests.
|
|
var engine = PureYAML.Parsing.LibYAML.IndentEngine()
|
|
engine.rollIndent(column: 0, number: -1, type: .blockMappingStart, mark: mark)
|
|
engine.unrollIndent(column: 2, mark: mark)
|
|
engine.rollIndent(column: 2, number: -1, type: .blockMappingStart, mark: mark)
|
|
engine.unrollIndent(column: -1, mark: mark)
|
|
|
|
#expect(engine.tokens.map(\.type) == [
|
|
.blockMappingStart,
|
|
.blockMappingStart,
|
|
.blockEnd,
|
|
.blockEnd,
|
|
])
|
|
}
|
|
|
|
@Test("flow level makes indentation inert")
|
|
func test_flowLevelIsInert() {
|
|
var engine = PureYAML.Parsing.LibYAML.IndentEngine()
|
|
engine.flowLevel = 1
|
|
engine.rollIndent(column: 4, number: -1, type: .blockMappingStart, mark: mark)
|
|
engine.unrollIndent(column: 0, mark: mark)
|
|
#expect(engine.tokens.isEmpty)
|
|
#expect(engine.indent == -1)
|
|
}
|
|
}
|