Module: Ast::Merge::TrailingGroups::Core
- Included in:
- DestIterate
- Defined in:
- lib/ast/merge/trailing_groups/core.rb
Overview
Core primitives for position-aware template-only node interleaving.
This module provides three stateless methods that form the backbone of the trailing-groups algorithm. They operate on plain data structures (hashes, sets, arrays) and yield to caller-supplied blocks for format-specific emission — no emitter API coupling.
Algorithm overview
-
Build — Walk template nodes in template order. Each node is classified as matched (present in destination) or template-only via a caller-supplied predicate. Consecutive template-only nodes are grouped under the index of the preceding matched node (
:prefixfor nodes before the first match). -
Flush — After each destination node is consumed during the dest iteration loop, check whether any interior trailing groups are now ready. A group anchored at template index K is ready when all matched template indices ≤ K have been consumed. This deferred-flush approach handles destination reordering correctly.
-
Emit remaining — After the dest loop, emit any trailing groups that were never flushed (tail groups, safety net for edge cases).
Instance Method Summary collapse
-
#build_trailing_groups(template_nodes:, matched_predicate:, entry_builder: nil) ⇒ Array(Hash{Symbol,Integer => Array<Hash>}, Set<Integer>)
Build a map of trailing groups for position-aware insertion.
-
#emit_remaining_trailing_groups(trailing_groups:, consumed_indices:) {|info| ... } ⇒ void
Emit any trailing groups not yet flushed (tail + safety net).
-
#flush_ready_trailing_groups(trailing_groups:, matched_indices:, consumed_indices:) {|info| ... } ⇒ void
Flush interior trailing groups whose prerequisites are met.
Instance Method Details
#build_trailing_groups(template_nodes:, matched_predicate:, entry_builder: nil) ⇒ Array(Hash{Symbol,Integer => Array<Hash>}, Set<Integer>)
Build a map of trailing groups for position-aware insertion.
Walks template_nodes in order. For each node the matched_predicate
is called with (node, index). Matched nodes become group anchors;
consecutive unmatched nodes accumulate in the current group.
Each group entry is a Hash with at least :node and :index keys.
Callers may supply entry_builder to add extra keys (e.g. :item
for sequence items in psych-merge).
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/ast/merge/trailing_groups/core.rb', line 61 def build_trailing_groups(template_nodes:, matched_predicate:, entry_builder: nil) groups = {} matched_indices = ::Set.new current_anchor = :prefix current_buffer = [] template_nodes.each_with_index do |node, idx| if matched_predicate.call(node, idx) matched_indices << idx groups[current_anchor] = current_buffer unless current_buffer.empty? current_anchor = idx current_buffer = [] else entry = entry_builder ? entry_builder.call(node, idx) : { node: node, index: idx } current_buffer << entry end end groups[current_anchor] = current_buffer unless current_buffer.empty? [groups, matched_indices] end |
#emit_remaining_trailing_groups(trailing_groups:, consumed_indices:) {|info| ... } ⇒ void
This method returns an undefined value.
Emit any trailing groups not yet flushed (tail + safety net).
This is called after the destination iteration loop completes.
Groups are emitted in ascending anchor order, skipping :prefix
(which is always handled before the loop).
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/ast/merge/trailing_groups/core.rb', line 139 def emit_remaining_trailing_groups(trailing_groups:, consumed_indices:, &emit_block) sorted_anchors(trailing_groups).each do |anchor| next if anchor == :prefix # already emitted before the loop group = trailing_groups[anchor] next unless group group.each do |info| next if consumed_indices.include?(info[:index]) emit_block.call(info) consumed_indices << info[:index] end end end |
#flush_ready_trailing_groups(trailing_groups:, matched_indices:, consumed_indices:) {|info| ... } ⇒ void
This method returns an undefined value.
Flush interior trailing groups whose prerequisites are met.
An interior group is one whose anchor is strictly less than the largest matched template index (tail groups are deferred to #emit_remaining_trailing_groups).
A group anchored at template index K is ready when every matched template index in the range 0..K has been consumed. This prevents premature emission when the destination reorders matched items relative to the template.
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/ast/merge/trailing_groups/core.rb', line 101 def flush_ready_trailing_groups(trailing_groups:, matched_indices:, consumed_indices:, &emit_block) return if matched_indices.empty? last_matched = matched_indices.max sorted_anchors(trailing_groups).each do |anchor| next if anchor == :prefix next if anchor >= last_matched # tail group — defer to remaining pass group = trailing_groups[anchor] next if group.nil? || group.all? { |info| consumed_indices.include?(info[:index]) } # Check if all matched template indices 0..anchor have been consumed ready = matched_indices .select { |idx| idx <= anchor } .all? { |idx| consumed_indices.include?(idx) } next unless ready group.each do |info| next if consumed_indices.include?(info[:index]) emit_block.call(info) consumed_indices << info[:index] end end end |