From 15936a5f8d70511711a5ed7b088dbf4203cf46c0 Mon Sep 17 00:00:00 2001 From: Taus Date: Fri, 8 May 2026 12:48:10 +0000 Subject: [PATCH] yeast: Take fields by ownership in apply_rules_inner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, apply_rules_inner snapshotted a node's fields by cloning the BTreeMap into a Vec<(FieldId, Vec)>, then built a fresh BTreeMap of new_fields for the rewritten Ids. For a node with N fields, this allocated 2N+1 things per visit (the snapshot Vec, N cloned children Vecs, the new BTreeMap entries) — even when nothing in the subtree was rewritten. Use std::mem::take to swap the parent's fields out by ownership: the recursion can mutate the AST (including pushing new nodes from rule firings) without any conflict, since we hold the owned BTreeMap locally. Iterate values_mut() and only allocate a fresh children Vec on the first divergence (lazy alloc): unchanged children stay in the existing slot. When done, swap the fields back. For a subtree with no rewrites, this is now zero allocations per node (modulo the recursion itself). For nodes with rewrites, it's one Vec allocation per field that contains a rewritten child, instead of two plus the BTreeMap rebuild. --- shared/yeast/src/lib.rs | 51 ++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/shared/yeast/src/lib.rs b/shared/yeast/src/lib.rs index 0e554a3cf43..281f44a98b2 100644 --- a/shared/yeast/src/lib.rs +++ b/shared/yeast/src/lib.rs @@ -591,35 +591,40 @@ fn apply_rules_inner( } } - // Collect fields before recursing (avoids borrowing ast immutably during mutation) - let field_entries: Vec<(FieldId, Vec)> = ast.nodes[id] - .fields - .iter() - .map(|(&fid, children)| (fid, children.clone())) - .collect(); - - // recursively descend into all the fields + // Take the parent's fields by ownership: the recursion will rewrite + // each child Id, and we'll write the (possibly mutated) field map back + // when we're done. Avoids cloning the whole BTreeMap and its child + // Vecs on entry. Each child Vec is only re-allocated if a rewrite + // actually changes its contents. + // // Child traversal does not increment rewrite depth and starts fresh // (no rule is skipped on child subtrees). - let mut changed = false; - let mut new_fields = BTreeMap::new(); - for (field_id, children) in field_entries { - let mut new_children = Vec::new(); - for child_id in children { + let mut fields = std::mem::take(&mut ast.nodes[id].fields); + for children in fields.values_mut() { + let mut new_children: Option> = None; + for (i, &child_id) in children.iter().enumerate() { let result = apply_rules_inner(index, ast, child_id, fresh, rewrite_depth, None)?; - if result.len() != 1 || result[0] != child_id { - changed = true; + let unchanged = result.len() == 1 && result[0] == child_id; + match (&mut new_children, unchanged) { + (None, true) => {} // unchanged so far, no allocation needed + (None, false) => { + // First divergence — copy already-processed Ids and + // start collecting the rewritten sequence. + let mut new = Vec::with_capacity(children.len()); + new.extend_from_slice(&children[..i]); + new.extend(result); + new_children = Some(new); + } + (Some(new), _) => { + new.extend(result); + } } - new_children.extend(result); } - new_fields.insert(field_id, new_children); + if let Some(new) = new_children { + *children = new; + } } - - if !changed { - return Ok(vec![id]); - } - - ast.nodes[id].fields = new_fields; + ast.nodes[id].fields = fields; Ok(vec![id]) }