plotnik_lib/query/
graph_optimize.rs

1//! Epsilon elimination optimization pass.
2//!
3//! Reduces graph size by removing unnecessary epsilon transitions.
4//!
5//! # Safety Rules (from ADR-0005)
6//!
7//! An epsilon node CANNOT be eliminated if:
8//! - It has a `RefMarker` (Enter/Exit)
9//! - It has multiple successors (branch point)
10//! - Its successor already has a `RefMarker`
11//! - Both have non-Stay `Nav` that can't be merged
12
13use std::collections::{HashMap, HashSet};
14
15use crate::ir::{Nav, NavKind};
16
17use super::Query;
18use super::graph::{BuildGraph, BuildMatcher, NodeId};
19
20/// Statistics from epsilon elimination.
21#[derive(Debug, Default)]
22pub struct OptimizeStats {
23    pub epsilons_eliminated: usize,
24    pub epsilons_kept: usize,
25}
26
27impl Query<'_> {
28    /// Run epsilon elimination on the graph.
29    ///
30    /// Populates `dead_nodes` with eliminated node IDs.
31    pub(super) fn optimize_graph(&mut self) {
32        let (dead, _stats) = eliminate_epsilons(&mut self.graph);
33        self.dead_nodes = dead;
34    }
35}
36
37/// Run epsilon elimination on a BuildGraph.
38///
39/// Returns the set of dead node IDs that should be skipped during emission.
40pub fn eliminate_epsilons(graph: &mut BuildGraph) -> (HashSet<NodeId>, OptimizeStats) {
41    let mut stats = OptimizeStats::default();
42    let mut dead_nodes: HashSet<NodeId> = HashSet::new();
43
44    let mut predecessors = build_predecessor_map(graph);
45
46    // Process nodes in reverse order to handle chains
47    let node_count = graph.len() as NodeId;
48    for id in (0..node_count).rev() {
49        if dead_nodes.contains(&id) {
50            continue;
51        }
52
53        let node = graph.node(id);
54        if !is_eliminable_epsilon(node, graph, &predecessors) {
55            if node.is_epsilon() {
56                stats.epsilons_kept += 1;
57            }
58            continue;
59        }
60
61        let successor_id = node.successors[0];
62        let effects_to_prepend = graph.node(id).effects.clone();
63        let nav_to_transfer = graph.node(id).nav;
64        let preds = predecessors.get(&id).cloned().unwrap_or_default();
65
66        // Prepend effects to successor
67        if !effects_to_prepend.is_empty() {
68            let succ = graph.node_mut(successor_id);
69            let mut new_effects = effects_to_prepend;
70            new_effects.append(&mut succ.effects);
71            succ.effects = new_effects;
72        }
73
74        // Transfer or merge nav
75        let successor_nav = graph.node(successor_id).nav;
76        if !nav_to_transfer.is_stay() {
77            if successor_nav.is_stay() {
78                graph.node_mut(successor_id).nav = nav_to_transfer;
79            } else if can_merge_up(nav_to_transfer, successor_nav) {
80                let merged = Nav::up(nav_to_transfer.level + successor_nav.level);
81                graph.node_mut(successor_id).nav = merged;
82            }
83        }
84
85        // Redirect predecessors to successor
86        for pred_id in &preds {
87            if dead_nodes.contains(pred_id) {
88                continue;
89            }
90            let pred = graph.node_mut(*pred_id);
91            for succ in &mut pred.successors {
92                if *succ == id {
93                    *succ = successor_id;
94                }
95            }
96            // Update predecessor map: pred is now a predecessor of successor
97            predecessors.entry(successor_id).or_default().push(*pred_id);
98        }
99        // Remove eliminated node from successor's predecessors
100        if let Some(succ_preds) = predecessors.get_mut(&successor_id) {
101            succ_preds.retain(|&p| p != id);
102        }
103
104        redirect_definitions(graph, id, successor_id);
105
106        dead_nodes.insert(id);
107        stats.epsilons_eliminated += 1;
108    }
109
110    (dead_nodes, stats)
111}
112
113fn is_eliminable_epsilon(
114    node: &super::graph::BuildNode,
115    graph: &BuildGraph,
116    predecessors: &HashMap<NodeId, Vec<NodeId>>,
117) -> bool {
118    if !matches!(node.matcher, BuildMatcher::Epsilon) {
119        return false;
120    }
121
122    if node.ref_marker.is_some() {
123        return false;
124    }
125
126    if node.successors.len() != 1 {
127        return false;
128    }
129
130    let successor_id = node.successors[0];
131    let successor = graph.node(successor_id);
132
133    if !node.nav.is_stay() && !successor.nav.is_stay() && !can_merge_up(node.nav, successor.nav) {
134        return false;
135    }
136
137    // Don't eliminate if node has nav and successor is a join point.
138    // Different paths may need different navigation (e.g., first iteration vs loop re-entry).
139    if !node.nav.is_stay() {
140        let succ_pred_count = predecessors.get(&successor_id).map_or(0, |p| p.len());
141        if succ_pred_count > 1 {
142            return false;
143        }
144    }
145
146    if !node.effects.is_empty() && successor.ref_marker.is_some() {
147        return false;
148    }
149
150    // Don't eliminate if epsilon has effects and successor has navigation.
151    // Effects must execute BEFORE successor's nav/match, but prepending to effects list
152    // would execute them AFTER nav/match.
153    if !node.effects.is_empty() && !successor.nav.is_stay() {
154        return false;
155    }
156
157    // Don't eliminate if node has effects and successor is a join point.
158    // Merging effects onto a join point changes execution count (e.g., loop entry vs per-iteration).
159    if !node.effects.is_empty() {
160        let succ_pred_count = predecessors.get(&successor_id).map_or(0, |p| p.len());
161        if succ_pred_count > 1 {
162            return false;
163        }
164    }
165
166    true
167}
168
169fn build_predecessor_map(graph: &BuildGraph) -> HashMap<NodeId, Vec<NodeId>> {
170    let mut predecessors: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
171
172    for (id, node) in graph.iter() {
173        for &succ in &node.successors {
174            predecessors.entry(succ).or_default().push(id);
175        }
176    }
177
178    predecessors
179}
180
181fn can_merge_up(a: Nav, b: Nav) -> bool {
182    a.kind == NavKind::Up && b.kind == NavKind::Up
183}
184
185fn redirect_definitions(graph: &mut BuildGraph, old_id: NodeId, new_id: NodeId) {
186    let updates: Vec<_> = graph
187        .definitions()
188        .filter(|(_, entry)| *entry == old_id)
189        .map(|(name, _)| name)
190        .collect();
191
192    for name in updates {
193        graph.add_definition(name, new_id);
194    }
195}