Module: Rigor::Source::NodeChildren
- Defined in:
- lib/rigor/source/node_children.rb
Overview
Allocation-free replacement for the node.compact_child_nodes.each { … } idiom.
Prism::Node#compact_child_nodes allocates a fresh Array on every call — for the ~43 leaf node classes
(IntegerNode, LocalVariableReadNode, NilNode, …) it is literally def compact_child_nodes; []; end.
Rigor's tree walkers call it unconditionally on every node of every walk, so one full-tree walk allocates one
Array per node visited. On leaf-heavy sources (a Ragel-generated parser has hundreds of thousands of
integer-literal leaves) these throwaway arrays are the single largest allocation source in a run — over half
of all allocations on mail's lib.
Loading this file compiles a #rigor_each_child method onto every Prism::*Node class (additive reopening of
a dependency's class, the Cache::RbsEnvironmentMarshalPatch precedent; the rigor_ prefix keeps it
collision-free). It yields the same child nodes, in the same order, without materialising the intermediate
array: each child-bearing field is read directly in field declaration order — exactly the order
compact_child_nodes emits — and a NodeListField's already-materialised array is iterated in place (the
reader returns the stored array, not a copy). Nil optional children and nil list elements are skipped,
mirroring compact_child_nodes's "compact" semantics. Leaf classes compile to an empty method.
A method compiled onto the node class is the fastest dispatch available for this shape — one virtual send on
the receiver, the same mechanism compact_child_nodes itself uses, with zero allocation. The reflective
alternatives were measured and rejected: a public_send-per-field loop is ~45 % slower than
compact_child_nodes on a full-tree walk (dynamic dispatch per field swamps the allocation win), and a
central generated-method table dispatched via Module#send + a per-class Hash lookup still regressed
Rails-corpus wall ~5 % under YJIT. Walkers therefore call node.rigor_each_child { … } directly.
The field map is derived once at load from Prism::Reflection, so it tracks whatever prism version resolves
at runtime (ADR-79) instead of a hand-maintained table. spec/rigor/source/node_children_spec.rb is the
binding contract: over a corpus of real source it asserts — for every node reached — that both
#rigor_each_child and NodeChildren.each_child yield output element-for-element identical (object identity and order) to
compact_child_nodes.
Constant Summary collapse
- NODE_CLASSES =
The concrete
Prism::*Nodeclasses (< Prism::Node). Prism.constants.grep(/Node\z/).filter_map do |name| const = Prism.const_get(name) const if const.is_a?(Class) && const < Prism::Node end.freeze
- EMPTY =
Shared frozen empty entry for leaf classes. Identity (
equal?) distinguishes a leaf entry from a childless-in-practice non-leaf. [].freeze
- CHILD_READERS =
node class => frozen Array of
[reader_symbol, :node | :node_optional | :list]pairs, in field declaration order (the ordercompact_child_nodesemits its children). Leaf classes map to EMPTY. This is the source of truth the per-class methods are compiled from, exposed for introspection and the equivalence spec. NODE_CLASSES.each_with_object({}) do |klass, map| pairs = Prism::Reflection.fields_for(klass).filter_map do |field| case field when Prism::Reflection::OptionalNodeField [field.name, :node_optional].freeze when Prism::Reflection::NodeField [field.name, :node].freeze when Prism::Reflection::NodeListField [field.name, :list].freeze end end map[klass] = pairs.empty? ? EMPTY : pairs.freeze end.freeze
- LEAF_CLASSES =
The leaf classes — no child-bearing field, so
compact_child_nodesis always[]and#rigor_each_childcompiles to an empty method. Exposed for the equivalence spec and introspection. NODE_CLASSES.select { |klass| CHILD_READERS[klass].equal?(EMPTY) }.to_set.freeze
Class Method Summary collapse
-
.each_child(node) {|child| ... } ⇒ Object
Yield each direct child
Prism::Nodeofnodeincompact_child_nodesorder, without allocating an intermediate array.
Class Method Details
.each_child(node) {|child| ... } ⇒ Object
Yield each direct child Prism::Node of node in compact_child_nodes order, without allocating an
intermediate array. Nil / non-node input yields nothing (an optional field can be nil, so a caller may pass
e.g. node.body). Pure and re-entrant; break / next / return in the block behave exactly as they
would with compact_child_nodes.each. Walkers holding a known Prism::Node call #rigor_each_child
directly; this wrapper is the nil-tolerant entry.
111 112 113 |
# File 'lib/rigor/source/node_children.rb', line 111 def each_child(node, &) node.rigor_each_child(&) if node.is_a?(Prism::Node) end |