Skip to content

[yaml, yaml_edit] Add token stream to package:yaml and rewrite yaml_edit using token-driven CST - #2593

Draft
sigurdm wants to merge 9 commits into
dart-lang:mainfrom
sigurdm:cst-formal-model
Draft

sigurdm wants to merge 9 commits into
dart-lang:mainfrom
sigurdm:cst-formal-model

Conversation

@sigurdm

@sigurdm sigurdm commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Adds optional token stream retention to package:yaml and rewrites package:yaml_edit's modification logic around a lossless token-driven Concrete Syntax Tree (CST) and slot-directed mutations.

Motivation

package:yaml produces 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, YamlEditor recovered 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:

  • Exact Tiling Invariant: Every character of the source belongs to exactly one CST slot, slots are contiguous and disjoint, and concatenating all slots reproduces the source byte for byte. CstDocument.parse enforces 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.
  • Structural Locality: Because slots are disjoint, replacing the source range of any slot is guaranteed to leave every other character of the document untouched. Mutations never search the source string for delimiters—every offset needed for an edit is already a verified slot boundary.

Changes

package:yaml (3.2.0-wip)

  • Optional Token Stream (retainTokens: true):
    • Adds retainTokens parameter to loadYamlDocument to retain scanner tokens on YamlDocument.tokens.
    • Exports Token, TokenType, ScalarToken, CommentToken, TagToken, AnchorToken, AliasToken, and directive token classes via package:yaml/tokens.dart.
    • Emits CommentToken for all comments and ensures YamlDocument.tokens contains strictly positive-length, non-overlapping real tokens sorted in source order.
    • Updates keep-chomped (|+, >+) block scalar token spans to include trailing empty lines consumed by the scalar.

package:yaml_edit

  • Lossless Token-Driven CST (lib/src/cst.dart):
    • Builds a concrete syntax tree directly from package:yaml's token stream (loadYamlDocument(..., retainTokens: true)), avoiding custom character scanning in yaml_edit.
    • Represents every token, delimiter, comment, newline, and whitespace segment as a CstNode with exact character spans.
    • Includes an invariant verifier (_checkTiling) ensuring the CST tiles the input string with zero gaps and zero overlaps.
  • Slot-Directed Mutations (lib/src/cst_mutations.dart):
    • Replaces heuristic string searching in list_mutations.dart and map_mutations.dart with syntax-directed CST operations.
    • Mapping insertions and updates target exact key/colon/value slots and compute indentation from sibling key columns.
    • Flow collection mutations locate enclosing delimiters ({...}, [...]) and comma tokens directly from the CST while preserving leading and sibling trailing comments.
    • Block sequence additions and removals operate directly on dash tokens and entry spans.
  • Testing:
    • Adds CST tiling verification across yaml-test-suite (test/cst_tiling_test.dart).
    • Adds structured property-based CST fuzzing (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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread pkgs/yaml_edit/lib/src/cst.dart Outdated
Comment on lines +273 to +276
var hyphenOffset = -1;
if (list.style == CollectionStyle.BLOCK) {
hyphenOffset = yaml.lastIndexOf('-', trueSpan.start.offset);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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++;
          }
        }
      }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved: replaced backwards scan with forward scan from prevLineEnd that skips comments and whitespace to locate the block sequence hyphen indicator.

Comment on lines +81 to +85
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];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@jonasfj

jonasfj commented Sep 9, 2026

Copy link
Copy Markdown
Member

This would effectively introduce a new YAML parser, correct?

Might it not be wiser to add additional metadata to the existing YamlNode objects?

just an idea, ofcourse we'd want to avoid extra cost for parsing YAML for people who don't care.

@sigurdm

sigurdm commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

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....

@sigurdm sigurdm changed the title [yaml_edit] Implement Concrete Syntax Tree (CST) and layout engine for lossless editing [yaml, yaml_edit] CST layout retention in package:yaml and lossless syntax-directed editing in package:yaml_edit Sep 10, 2026
…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
@sigurdm
sigurdm changed the base branch from handle-aliases to main September 10, 2026 12:52
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.
@sigurdm sigurdm changed the title [yaml, yaml_edit] CST layout retention in package:yaml and lossless syntax-directed editing in package:yaml_edit [yaml_edit] Rewrite using concrete syntax tree and slot-directed mutations Sep 14, 2026
@sigurdm sigurdm changed the title [yaml_edit] Rewrite using concrete syntax tree and slot-directed mutations [yaml, yaml_edit] Add token stream to package:yaml and rewrite yaml_edit using token-driven CST Sep 17, 2026
@github-actions github-actions Bot added the type-infra A repository infrastructure change or enhancement label Sep 17, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package:yaml_edit package:yaml type-infra A repository infrastructure change or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants