[yaml, yaml_edit] Add token stream to package:yaml and rewrite yaml_edit using token-driven CST - #2593
[yaml, yaml_edit] Add token stream to package:yaml and rewrite yaml_edit using token-driven CST#2593sigurdm wants to merge 9 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a Concrete Syntax Tree (CST) layout engine to represent non-semantic source elements like comments and whitespace, allowing the editor to support updating explicit keys without colons and resolve previous crash test failures. Feedback focuses on improving the robustness of finding list item hyphens by scanning forward rather than using lastIndexOf, and optimizing performance in map mutations by avoiding redundant .toList() conversions on map keys.
| var hyphenOffset = -1; | ||
| if (list.style == CollectionStyle.BLOCK) { | ||
| hyphenOffset = yaml.lastIndexOf('-', trueSpan.start.offset); | ||
| } |
There was a problem hiding this comment.
Using yaml.lastIndexOf to find the hyphen offset can incorrectly match a hyphen inside a comment or a string value that precedes the current element. Since we parse sequentially, we can scan forward from prevLineEnd to trueSpan.start.offset while skipping comments and whitespace to reliably find the actual list item hyphen.
var hyphenOffset = -1;
if (list.style == CollectionStyle.BLOCK) {
var scan = prevLineEnd;
while (scan < trueSpan.start.offset) {
final c = yaml.codeUnitAt(scan);
if (YamlChar.isWhitespace(c) || YamlChar.isLineBreak(c)) {
scan++;
} else if (c == YamlChar.hash) {
scan++;
while (scan < yaml.length &&
!YamlChar.isLineBreak(yaml.codeUnitAt(scan))) {
scan++;
}
} else if (c == 0x2D /* - */) {
hyphenOffset = scan;
break;
} else {
scan++;
}
}
}There was a problem hiding this comment.
Resolved: replaced backwards scan with forward scan from prevLineEnd that skips comments and whitespace to locate the block sequence hyphen indicator.
| final keyAtIndex = map.nodes.keys.toList()[insertionIndex] as YamlNode; | ||
| final keySpanStart = keyAtIndex.span.start.offset; | ||
| final prevNewLineIndex = yaml.lastIndexOf('\n', keySpanStart); | ||
| final int minOffset; | ||
| if (insertionIndex > 0) { | ||
| final prevKey = map.nodes.keys.toList()[insertionIndex - 1]; |
There was a problem hiding this comment.
Calling map.nodes.keys.toList() repeatedly inside this block creates redundant list copies. We can optimize this by converting the keys to a list once and reusing it.
| final keyAtIndex = map.nodes.keys.toList()[insertionIndex] as YamlNode; | |
| final keySpanStart = keyAtIndex.span.start.offset; | |
| final prevNewLineIndex = yaml.lastIndexOf('\n', keySpanStart); | |
| final int minOffset; | |
| if (insertionIndex > 0) { | |
| final prevKey = map.nodes.keys.toList()[insertionIndex - 1]; | |
| final keys = map.nodes.keys.toList(); | |
| final keyAtIndex = keys[insertionIndex] as YamlNode; | |
| final keySpanStart = keyAtIndex.span.start.offset; | |
| final int minOffset; | |
| if (insertionIndex > 0) { | |
| final prevKey = keys[insertionIndex - 1]; |
There was a problem hiding this comment.
Resolved: cached keys list locally and used Iterable.elementAt in _addToFlowMap to avoid redundant allocations. Also rewound scan past leading indentation spaces so entry insertion at index 0 preserves sibling column indentation.
|
This would effectively introduce a new YAML parser, correct? Might it not be wiser to add additional metadata to the existing just an idea, ofcourse we'd want to avoid extra cost for parsing YAML for people who don't care. |
|
Yeah - I think we basically need to bring this all the way to a full parser... maybe we extend package:yaml we should try both directions to see what the costs are.... |
…limiter spans Preserve layout elements (tokens, whitespace, comments, delimiters) during YAML scanning and parsing when retainLayout is enabled. Expose CST layout accessors (openSpan, closeSpan, colonSpan, dashSpan, entrySpan, commaSpan, anchorSpan, aliasSpan, leadingLayout, trailingLayout) on YamlNode, YamlMap, and YamlList. TAG=agy CONV=eabffc54-0045-490f-beb6-67e4668362d1
…fy algebraic laws - Use colonSpan, dashSpan, openSpan, closeSpan, and entrySpan directly for precise mutation offsets without heuristic character scanning. - Preserve trailing comments on block scalar replacements. - Fix flow collection indentation stripping when updating items in lists. - Add formal laws test suite with property-based fuzzing validating: * Law 1: PutGet / Roundtrip invariant * Law 2: GetPut / Idempotence invariant * Law 3: PutPut / Sequential overwrite invariant * Law 4: Disjoint path commutativity invariant * Law 5: Comment and layout frame condition * Law 6: Offside rule and indentation monotonicity TAG=agy CONV=eabffc54-0045-490f-beb6-67e4668362d1
7eae564 to
e18c238
Compare
Revert experimental package:yaml layout retention changes and replace package:yaml_edit heuristic scanning with a standalone concrete syntax tree (CST) parser and lossless slot-directed mutation system. The CST partitions the source document into disjoint, verifiable slots guaranteeing comment, indentation, and structure preservation by construction.
Adds optional token stream retention to
package:yamland rewritespackage:yaml_edit's modification logic around a lossless token-driven Concrete Syntax Tree (CST) and slot-directed mutations.Motivation
package:yamlproduces a value tree: it describes what a document means, but not how it is written. Comments, blank lines, indentation, delimiter positions, and collection styles are absent from the AST. Previously,YamlEditorrecovered this information during edits by scanning the source string with ad-hoc heuristics (indexOf,lastIndexOf, regex matching) to locate colons, commas, brackets, and indentation levels. This made edge cases (comments containing delimiter characters, multiline comments, nested flow collections, and mixed indentation) prone to offset miscalculations and syntax corruption.This PR replaces ad-hoc scanning with a tiling Concrete Syntax Tree (CST) constructed once from
package:yaml's AST and token stream:CstDocument.parseenforces this invariant on every document it builds, verifying that every structural indicator matches its expected character and every interstitial gap contains only whitespace and comments.Changes
package:yaml(3.2.0-wip)retainTokens: true):retainTokensparameter toloadYamlDocumentto retain scanner tokens onYamlDocument.tokens.Token,TokenType,ScalarToken,CommentToken,TagToken,AnchorToken,AliasToken, and directive token classes viapackage:yaml/tokens.dart.CommentTokenfor all comments and ensuresYamlDocument.tokenscontains strictly positive-length, non-overlapping real tokens sorted in source order.|+,>+) block scalar token spans to include trailing empty lines consumed by the scalar.package:yaml_editlib/src/cst.dart):package:yaml's token stream (loadYamlDocument(..., retainTokens: true)), avoiding custom character scanning inyaml_edit.CstNodewith exact character spans._checkTiling) ensuring the CST tiles the input string with zero gaps and zero overlaps.lib/src/cst_mutations.dart):list_mutations.dartandmap_mutations.dartwith syntax-directed CST operations.{...},[...]) and comma tokens directly from the CST while preserving leading and sibling trailing comments.yaml-test-suite(test/cst_tiling_test.dart).test/structured_fuzz_test.dart) verifying CST tiling, structural locality frame bounds, comment conservation laws, algebraic lens laws (PutGet,GetPut,PutPut), insert-remove cancellation, and byte-for-byte disjoint path commutativity.