From 05fc8c2c1449764c853a136323e1c170e77d32e4 Mon Sep 17 00:00:00 2001 From: Will Lillis Date: Fri, 21 Aug 2026 17:46:01 -0400 Subject: [PATCH] perf(generate): flatten `Choice`s directly into the children tail --- crates/generate/src/rules.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/crates/generate/src/rules.rs b/crates/generate/src/rules.rs index 0ca0d8527..9fe5d687b 100644 --- a/crates/generate/src/rules.rs +++ b/crates/generate/src/rules.rs @@ -174,6 +174,8 @@ pub struct RulePool { children: Vec, params: Vec, str_pool: StrPool, + /// Reusable walk scratch for choice flattening + scratch: Vec, } impl RulePool { @@ -333,19 +335,29 @@ impl RulePool { /// Flatten nested choices and de-dup structurally, keeping a `Choice` node /// even for a single element pub fn choice(&mut self, ids: &[RuleId]) -> RuleId { - let mut elements: Vec = Vec::with_capacity(ids.len()); - let mut stack: Vec = Vec::with_capacity(ids.len()); + // Elements build directly at the children tail. The walk only reads + // existing nodes, so nothing else appends while the range grows + let start = self.children.len(); + let mut stack = std::mem::take(&mut self.scratch); stack.extend(ids.iter().rev()); while let Some(id) = stack.pop() { if let Rule::Choice(range) = self.node(id) { let base = stack.len(); stack.extend_from_slice(self.child_slice(range)); stack[base..].reverse(); - } else if !elements.iter().any(|&e| self.subtree_eq(e, id)) { - elements.push(id); + } else if !self.children[start..] + .iter() + .copied() + .any(|e| self.subtree_eq(e, id)) + { + self.children.push(id); } } - let range = self.push_children(&elements); + self.scratch = stack; + let range = RuleIdRange { + start: start as u32, + len: (self.children.len() - start) as u32, + }; self.push_node(Rule::Choice(range)) }