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.
75 lines
3.3 KiB
Swift
75 lines
3.3 KiB
Swift
extension PureYAML.Parsing.LibYAML {
|
|
/// Port of the indentation stack from `yaml_parser_t` plus
|
|
/// `yaml_parser_roll_indent` and `yaml_parser_unroll_indent`. libyaml tracks a
|
|
/// single `indent` column (the current block collection's indentation, -1 at
|
|
/// the document root) and a stack of the enclosing levels. A block collection
|
|
/// is opened only when content is strictly more indented than the current
|
|
/// level, which is exactly why `- a:` followed by a same-column `b:` keeps `b`
|
|
/// a sibling and leaves `a` an empty value.
|
|
struct IndentEngine {
|
|
/// `parser->indent`. -1 at the document root, matching libyaml.
|
|
private(set) var indent = -1
|
|
/// `parser->indents`, the stack of enclosing indentation levels.
|
|
private(set) var indents: [Int] = []
|
|
/// `parser->flow_level`; indentation is inert inside flow collections.
|
|
var flowLevel = 0
|
|
/// The token queue (`parser->tokens`) the engine appends to or, for
|
|
/// `roll_indent`, inserts a start token into at a recorded position.
|
|
private(set) var tokens: [Token] = []
|
|
/// `parser->tokens_parsed`, the count already dequeued, used to turn an
|
|
/// absolute token number into an index into the live queue.
|
|
var tokensParsed = 0
|
|
|
|
/// Port of `yaml_parser_roll_indent`. In flow context it does nothing.
|
|
/// When the column is greater than the current indent, it pushes the
|
|
/// current indent, adopts the new column, and emits the block start token.
|
|
/// `number == -1` appends; otherwise the token is inserted at the recorded
|
|
/// queue position so a `BLOCK-MAPPING-START` precedes a simple key that was
|
|
/// already queued.
|
|
mutating func rollIndent(
|
|
column: Int,
|
|
number: Int,
|
|
type: TokenType,
|
|
mark: PureYAML.Parsing.Mark,
|
|
) {
|
|
if flowLevel != 0 {
|
|
return
|
|
}
|
|
if indent < column {
|
|
indents.append(indent)
|
|
indent = column
|
|
let token = Token(type: type, startMark: mark, endMark: mark)
|
|
if number == -1 {
|
|
tokens.append(token)
|
|
} else {
|
|
tokens.insert(token, at: number - tokensParsed)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Port of `yaml_parser_unroll_indent`. In flow context it does nothing.
|
|
/// While the current indent is deeper than the column, it emits a
|
|
/// `BLOCK-END` and pops the enclosing level, closing every block collection
|
|
/// that the dedent ends.
|
|
mutating func unrollIndent(column: Int, mark: PureYAML.Parsing.Mark) {
|
|
if flowLevel != 0 {
|
|
return
|
|
}
|
|
while indent > column {
|
|
tokens.append(Token(type: .blockEnd, startMark: mark, endMark: mark))
|
|
indent = indents.removeLast()
|
|
}
|
|
}
|
|
|
|
/// Dequeue the front token, advancing `tokensParsed` like libyaml's
|
|
/// `yaml_parser_scan` does when it hands a token to the parser.
|
|
mutating func dequeue() -> Token? {
|
|
guard !tokens.isEmpty else {
|
|
return nil
|
|
}
|
|
tokensParsed += 1
|
|
return tokens.removeFirst()
|
|
}
|
|
}
|
|
}
|