Class: RedQuilt::Arena
- Inherits:
-
Object
- Object
- RedQuilt::Arena
- Defined in:
- lib/red_quilt/arena.rb
Overview
Parallel-array storage for AST nodes.
Each node has a single integer id (its position in the columns). All structural and payload fields are stored as columns keyed by id:
structural parent, first_child, last_child, next_sibling, prev_sibling
source span source_start (byte offset, -1 = no span), source_len
payload int1..int3, str1, str2 (per-NodeType conventions)
Invariants:
-
id starts at 0 and grows monotonically with #add_node. Ids are never reassigned and never reclaimed; a #detach’ed node keeps its row in the columns and stays addressable (its parent / siblings are reset to NO_NODE) but is no longer reachable from the tree. This means the arena’s memory is monotone non-decreasing for the lifetime of a parse — a deliberate trade for allocation simplicity.
-
NO_NODE (= -1) is the sentinel value used for “no parent”, “no sibling”, and as a source_start to mean “this node has no span; its content is materialized in str1 instead”.
-
@source is the original document string. It is treated as immutable: callers must not mutate it after constructing the arena, since byteslice positions stored in source_start/source_len refer to it directly.
Defined Under Namespace
Classes: IntegrityError
Constant Summary collapse
- NO_NODE =
-1
Instance Attribute Summary collapse
-
#source ⇒ Object
readonly
Returns the value of attribute source.
Instance Method Summary collapse
-
#add_node(type, source_start: NO_NODE, source_len: 0, int1: 0, int2: 0, int3: 0, str1: nil, str2: nil) ⇒ Object
Appends a fresh node to the arena and returns its id.
- #append_child(parent_id, child_id) ⇒ Object
-
#check_integrity!(root_id) ⇒ Object
Verifies the structural invariants of the tree rooted at root_id.
-
#child_ids(id) ⇒ Object
Returns an Enumerator yielding each child id.
-
#detach(child_id) ⇒ Object
Removes child_id from its current parent.
-
#each_child(id) ⇒ Object
Yields each child id of ‘id` in order.
-
#initialize(source) ⇒ Arena
constructor
A new instance of Arena.
-
#insert_before(parent_id, ref_id, new_id) ⇒ Object
Inserts new_id immediately before ref_id in parent_id’s child list.
- #int1(id) ⇒ Object
- #int2(id) ⇒ Object
- #int3(id) ⇒ Object
- #raw_first_child_id(id) ⇒ Object
- #raw_last_child_id(id) ⇒ Object
- #raw_next_sibling_id(id) ⇒ Object
-
#raw_parent_id(id) ⇒ Object
Structural id accessors.
- #raw_prev_sibling_id(id) ⇒ Object
-
#reparent(new_parent_id, first_id, last_id) ⇒ Object
Moves a contiguous sibling range [first_id .. last_id] (both inclusive, walking #next_sibling from first to last) under new_parent_id, replacing any existing children there.
-
#source_end(id) ⇒ Object
Byte offset one past the node’s source span (start + len).
- #source_len(id) ⇒ Object
-
#source_span(id) ⇒ Object
Returns a SourceSpan for the node, or nil when the node has no span (source_start < 0, meaning the content is held in str1).
- #source_start(id) ⇒ Object
- #str1(id) ⇒ Object
- #str2(id) ⇒ Object
-
#text(id) ⇒ Object
Returns the node’s textual content.
- #type(id) ⇒ Object
- #type_name(id) ⇒ Object
- #update_int3(id, value) ⇒ Object
- #update_span(id, start_byte, end_byte) ⇒ Object
- #update_str1(id, value) ⇒ Object
Constructor Details
#initialize(source) ⇒ Arena
Returns a new instance of Arena.
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
# File 'lib/red_quilt/arena.rb', line 35 def initialize(source) @source = source @type = [] @parent = [] @first_child = [] @last_child = [] @next_sibling = [] @prev_sibling = [] @source_start = [] @source_len = [] @int1 = [] @int2 = [] @int3 = [] @str1 = [] @str2 = [] end |
Instance Attribute Details
#source ⇒ Object (readonly)
Returns the value of attribute source.
33 34 35 |
# File 'lib/red_quilt/arena.rb', line 33 def source @source end |
Instance Method Details
#add_node(type, source_start: NO_NODE, source_len: 0, int1: 0, int2: 0, int3: 0, str1: nil, str2: nil) ⇒ Object
Appends a fresh node to the arena and returns its id. The node starts detached (parent = first_child = … = NO_NODE).
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/red_quilt/arena.rb', line 54 def add_node(type, source_start: NO_NODE, source_len: 0, int1: 0, int2: 0, int3: 0, str1: nil, str2: nil) id = @type.length @type[id] = type @parent[id] = NO_NODE @first_child[id] = NO_NODE @last_child[id] = NO_NODE @next_sibling[id] = NO_NODE @prev_sibling[id] = NO_NODE @source_start[id] = source_start @source_len[id] = source_len @int1[id] = int1 @int2[id] = int2 @int3[id] = int3 @str1[id] = str1 @str2[id] = str2 id end |
#append_child(parent_id, child_id) ⇒ Object
72 73 74 75 76 77 78 79 80 81 82 83 84 |
# File 'lib/red_quilt/arena.rb', line 72 def append_child(parent_id, child_id) @parent[child_id] = parent_id if @first_child[parent_id] == NO_NODE @first_child[parent_id] = child_id @last_child[parent_id] = child_id else last = @last_child[parent_id] @next_sibling[last] = child_id @prev_sibling[child_id] = last @last_child[parent_id] = child_id end child_id end |
#check_integrity!(root_id) ⇒ Object
Verifies the structural invariants of the tree rooted at root_id. Raises IntegrityError on the first violation, including the offending node id(s) and a description of the broken rule.
Checked invariants:
-
root has parent = NO_NODE
-
for every reachable node ‘n` and its first_child / last_child `fc` / `lc`:
* fc and lc are both NO_NODE, or both not NO_NODE * walking next_sibling from fc reaches lc and only lc * for each child `c`, @parent[c] == n * for each child `c`, @prev_sibling[c] equals the previously visited sibling (or NO_NODE for the first) -
no node is reached twice (no shared subtrees, no cycles)
Intended for development / debugging. Not called by the production parse / render path.
312 313 314 315 316 317 318 319 320 321 |
# File 'lib/red_quilt/arena.rb', line 312 def check_integrity!(root_id) raise IntegrityError, "root_id #{root_id} has no row" if root_id >= @type.length if @parent[root_id] != NO_NODE raise IntegrityError, "root #{root_id} has non-NO_NODE parent #{@parent[root_id]}" end visited = {} walk_for_integrity(root_id, NO_NODE, visited) self end |
#child_ids(id) ⇒ Object
Returns an Enumerator yielding each child id. Kept for the external NodeRef API where Enumerator chaining (map, select, …) is convenient.
272 273 274 275 276 277 278 279 280 |
# File 'lib/red_quilt/arena.rb', line 272 def child_ids(id) Enumerator.new do |yielder| child_id = @first_child[id] until child_id == NO_NODE yielder << child_id child_id = @next_sibling[child_id] end end end |
#detach(child_id) ⇒ Object
Removes child_id from its current parent. The node’s row stays in the arena (its payload columns are untouched) but parent / siblings are reset to NO_NODE, so the node is no longer reachable through any tree walk. Detached rows are not reused by subsequent #add_node calls.
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/red_quilt/arena.rb', line 106 def detach(child_id) parent_id = @parent[child_id] prev_id = @prev_sibling[child_id] next_id = @next_sibling[child_id] if prev_id == NO_NODE @first_child[parent_id] = next_id else @next_sibling[prev_id] = next_id end if next_id == NO_NODE @last_child[parent_id] = prev_id else @prev_sibling[next_id] = prev_id end @parent[child_id] = NO_NODE @prev_sibling[child_id] = NO_NODE @next_sibling[child_id] = NO_NODE child_id end |
#each_child(id) ⇒ Object
Yields each child id of ‘id` in order. Block form is preferred over #child_ids on hot paths (renderer, builder) because it avoids the Enumerator allocation.
260 261 262 263 264 265 266 267 |
# File 'lib/red_quilt/arena.rb', line 260 def each_child(id) child_id = @first_child[id] until child_id == NO_NODE yield child_id child_id = @next_sibling[child_id] end self end |
#insert_before(parent_id, ref_id, new_id) ⇒ Object
Inserts new_id immediately before ref_id in parent_id’s child list.
87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/red_quilt/arena.rb', line 87 def insert_before(parent_id, ref_id, new_id) @parent[new_id] = parent_id prev_ref = @prev_sibling[ref_id] @prev_sibling[new_id] = prev_ref @next_sibling[new_id] = ref_id @prev_sibling[ref_id] = new_id if prev_ref == NO_NODE @first_child[parent_id] = new_id else @next_sibling[prev_ref] = new_id end new_id end |
#int1(id) ⇒ Object
214 215 216 |
# File 'lib/red_quilt/arena.rb', line 214 def int1(id) @int1[id] end |
#int2(id) ⇒ Object
218 219 220 |
# File 'lib/red_quilt/arena.rb', line 218 def int2(id) @int2[id] end |
#int3(id) ⇒ Object
222 223 224 |
# File 'lib/red_quilt/arena.rb', line 222 def int3(id) @int3[id] end |
#raw_first_child_id(id) ⇒ Object
185 186 187 |
# File 'lib/red_quilt/arena.rb', line 185 def raw_first_child_id(id) @first_child[id] end |
#raw_last_child_id(id) ⇒ Object
189 190 191 |
# File 'lib/red_quilt/arena.rb', line 189 def raw_last_child_id(id) @last_child[id] end |
#raw_next_sibling_id(id) ⇒ Object
193 194 195 |
# File 'lib/red_quilt/arena.rb', line 193 def raw_next_sibling_id(id) @next_sibling[id] end |
#raw_parent_id(id) ⇒ Object
Structural id accessors. The ‘raw_` prefix flags that these return raw column values that may be the NO_NODE sentinel, and the `_id` suffix flags that the returned integer is a node id (suitable for feeding back into other Arena methods).
181 182 183 |
# File 'lib/red_quilt/arena.rb', line 181 def raw_parent_id(id) @parent[id] end |
#raw_prev_sibling_id(id) ⇒ Object
197 198 199 |
# File 'lib/red_quilt/arena.rb', line 197 def raw_prev_sibling_id(id) @prev_sibling[id] end |
#reparent(new_parent_id, first_id, last_id) ⇒ Object
Moves a contiguous sibling range [first_id .. last_id] (both inclusive, walking #next_sibling from first to last) under new_parent_id, replacing any existing children there. The walk assumes the range is well-formed; passing nodes from different parents or a last_id not reachable from first_id is undefined behavior.
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/red_quilt/arena.rb', line 135 def reparent(new_parent_id, first_id, last_id) return if first_id == NO_NODE || last_id == NO_NODE original_parent = @parent[first_id] prev_of_first = @prev_sibling[first_id] next_of_last = @next_sibling[last_id] if prev_of_first == NO_NODE @first_child[original_parent] = next_of_last else @next_sibling[prev_of_first] = next_of_last end if next_of_last == NO_NODE @last_child[original_parent] = prev_of_first else @prev_sibling[next_of_last] = prev_of_first end @prev_sibling[first_id] = NO_NODE @next_sibling[last_id] = NO_NODE id = first_id loop do @parent[id] = new_parent_id break if id == last_id id = @next_sibling[id] end @first_child[new_parent_id] = first_id @last_child[new_parent_id] = last_id end |
#source_end(id) ⇒ Object
Byte offset one past the node’s source span (start + len).
210 211 212 |
# File 'lib/red_quilt/arena.rb', line 210 def source_end(id) @source_start[id] + @source_len[id] end |
#source_len(id) ⇒ Object
205 206 207 |
# File 'lib/red_quilt/arena.rb', line 205 def source_len(id) @source_len[id] end |
#source_span(id) ⇒ Object
Returns a SourceSpan for the node, or nil when the node has no span (source_start < 0, meaning the content is held in str1).
236 237 238 239 240 241 |
# File 'lib/red_quilt/arena.rb', line 236 def source_span(id) start_byte = @source_start[id] return nil if start_byte.nil? || start_byte.negative? SourceSpan.new(start_byte, start_byte + @source_len[id]) end |
#source_start(id) ⇒ Object
201 202 203 |
# File 'lib/red_quilt/arena.rb', line 201 def source_start(id) @source_start[id] end |
#str1(id) ⇒ Object
226 227 228 |
# File 'lib/red_quilt/arena.rb', line 226 def str1(id) @str1[id] end |
#str2(id) ⇒ Object
230 231 232 |
# File 'lib/red_quilt/arena.rb', line 230 def str2(id) @str2[id] end |
#text(id) ⇒ Object
Returns the node’s textual content. Prefers str1 (the literal form, e.g. an entity decoded to its character, or a reassembled blockquote line). Falls back to a byteslice of @source when only a span is recorded. Returns nil if neither is available.
247 248 249 250 251 252 253 254 255 |
# File 'lib/red_quilt/arena.rb', line 247 def text(id) literal = @str1[id] return literal unless literal.nil? start_byte = @source_start[id] return nil if start_byte.nil? || start_byte.negative? @source.byteslice(start_byte, @source_len[id]) end |
#type(id) ⇒ Object
169 170 171 |
# File 'lib/red_quilt/arena.rb', line 169 def type(id) @type[id] end |
#type_name(id) ⇒ Object
173 174 175 |
# File 'lib/red_quilt/arena.rb', line 173 def type_name(id) NodeType.name_for(@type[id]) end |
#update_int3(id, value) ⇒ Object
286 287 288 |
# File 'lib/red_quilt/arena.rb', line 286 def update_int3(id, value) @int3[id] = value end |
#update_span(id, start_byte, end_byte) ⇒ Object
290 291 292 293 |
# File 'lib/red_quilt/arena.rb', line 290 def update_span(id, start_byte, end_byte) @source_start[id] = start_byte @source_len[id] = end_byte - start_byte end |
#update_str1(id, value) ⇒ Object
282 283 284 |
# File 'lib/red_quilt/arena.rb', line 282 def update_str1(id, value) @str1[id] = value end |