Skip to main content

weir/
ssa.rs

1//! DF-1  -  SSA construction (Cytron dominance-frontier phi insertion +
2//! variable renaming).
3//!
4//! Two complementary surfaces:
5//!
6//! 1. Reference Cytron / Cooper-Harvey-Kennedy implementation
7//!    (`compute_dominators`, `compute_dominance_frontiers`,
8//!    `place_phi_nodes`, `rename_variables`) for parity evidence and
9//!    host-side AST integration while the full SSA builder migrates to
10//!    resident graph programs. This surface is not the production
11//!    dispatch path for dataflow propagation.
12//!
13//! 2. GPU-emitting [`ssa_phi_placement_step`] that lowers Cytron's
14//!    phi-placement worklist to a single `csr_forward_traverse`
15//!    pass over the dominance-frontier graph. The surge-side
16//!    fixpoint driver iterates this step to convergence  -  same
17//!    shape as DF-2 reaching-defs and DF-3 points-to (consistent
18//!    convergence-contract). Renaming is a remaining migration target:
19//!    callers should treat the functions below as reference/oracle
20//!    machinery until resident dominator-tree traversal lands.
21//!
22//! ## Op id and soundness
23//! Op id: `weir::ssa`. Soundness: `Exact`  -  Cytron
24//! places phi nodes at the exact set of join points reachable from
25//! a def via dominance-frontier edges; no over-approximation.
26
27#![allow(clippy::too_many_arguments)]
28use std::collections::{HashMap, HashSet};
29use std::hash::Hash;
30
31use vyre::ir::Program;
32use vyre_driver::backend::{acquire_preferred_dispatch_backend, DispatchConfig};
33use vyre_primitives::graph::csr_forward_traverse::csr_forward_traverse;
34use vyre_primitives::graph::dominator_tree::{try_dominator_tree_program, IDOM_NONE};
35use vyre_primitives::graph::program_graph::ProgramGraphShape;
36
37pub(crate) const OP_ID: &str = "weir::ssa";
38
39/// Build one dominance-frontier propagation step for SSA phi
40/// placement.
41///
42/// `frontier_in` carries the current per-block "vars defined here or
43/// reaching here via DF" bitset; `frontier_out` receives the
44/// propagated phi-placement bitset after one DF-edge traversal.
45///
46/// The CFG-to-DF graph is laid out as a CSR adjacency on the
47/// dominance-frontier relation: for each block `b`, its outgoing
48/// edges in this graph point to every block `b'` where `b ∈ DF(b')`.
49/// Iterating `csr_forward_traverse` to fixpoint on this graph
50/// converges to the exact phi-placement set Cytron's worklist
51/// produces  -  `csr_forward_traverse` is bit-identical to one
52/// iteration of the worklist over DF edges.
53///
54/// Convergence contract: 64 iterations cap (registered via
55/// `ConvergenceContract` in the inventory submit below). This bounds
56/// the depth of the dominance-frontier hierarchy a CFG can have;
57/// 64 covers the deepest nesting any real-world function we've
58/// surveyed produces, with a >2× safety margin.
59#[must_use]
60pub fn ssa_phi_placement_step(
61    shape: ProgramGraphShape,
62    frontier_in: &str,
63    frontier_out: &str,
64) -> Program {
65    csr_forward_traverse(shape, frontier_in, frontier_out, 0xFFFF_FFFF)
66}
67
68inventory::submit! {
69    vyre_harness::OpEntry::new(
70        OP_ID,
71        || ssa_phi_placement_step(ProgramGraphShape::new(4, 4), "fin", "fout"),
72        Some(|| {
73            let to_bytes = crate::dispatch_decode::pack_u32;
74            // Diamond DF graph identical to DF-2's fixture so the
75            // contract test exercises the same shape both primitives
76            // claim to handle. Block 0 holds the original def; nodes
77            // 1 and 2 are its dominance-frontier successors; 3 joins.
78            vec![vec![
79                to_bytes(&[0, 0, 0, 0]),          // pg_nodes
80                to_bytes(&[0, 2, 3, 4, 4]),       // pg_edge_offsets
81                to_bytes(&[1, 2, 3, 3]),          // pg_edge_targets
82                to_bytes(&[1, 1, 1, 1]),          // pg_edge_kind_mask
83                to_bytes(&[0, 0, 0, 0]),          // pg_node_tags
84                to_bytes(&[0b0001]),              // fin = {var 0 def at block 0}
85                to_bytes(&[0b0001]),              // fout seed
86            ]]
87        }),
88        Some(|| {
89            let to_bytes = crate::dispatch_decode::pack_u32;
90            // After one DF-edge traversal, var 0 has propagated to
91            // blocks 1 and 2.
92            vec![vec![to_bytes(&[0b0111])]]
93        }),
94    )
95}
96
97inventory::submit! {
98    vyre_harness::ConvergenceContract {
99        op_id: OP_ID,
100        max_iterations: 64,
101    }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
105/// SSA construction result: phi placement, renamed uses, and def-use chains.
106pub struct SsaForm {
107    /// Variables requiring phi nodes, keyed by block id.
108    pub phi_nodes: HashMap<u32, Vec<u32>>,
109    /// SSA version chosen for each use node.
110    pub renamed_usages: HashMap<u32, u32>,
111    /// Use nodes reachable from each SSA definition version.
112    pub def_use_chains: HashMap<u32, Vec<u32>>,
113}
114
115#[derive(Debug, Clone, PartialEq)]
116/// Basic block summary used by the SSA builder.
117pub struct Block {
118    /// Stable block id.
119    pub id: u32,
120    /// Predecessor block ids.
121    pub preds: Vec<u32>,
122    /// Successor block ids.
123    pub succs: Vec<u32>,
124    /// Variable ids defined in this block.
125    pub defs: HashSet<u32>,
126    /// Variable ids used in this block.
127    pub uses: HashSet<u32>,
128}
129
130#[derive(Debug, Clone, PartialEq)]
131/// Control-flow graph consumed by the SSA builder.
132pub struct Cfg {
133    /// Entry block id.
134    pub entry: u32,
135    /// Blocks keyed by stable block id.
136    pub blocks: HashMap<u32, Block>,
137}
138
139/// Tunable threshold: CFGs with more than this many blocks are routed to the
140/// GPU dominator-tree primitive when a backend is available.
141pub const DOMINATOR_GPU_THRESHOLD: u32 = 1_000;
142
143/// Internal telemetry enum for testing.
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub(crate) enum DominatorPath {
146    /// Pure CPU Lengauer–Tarjan path.
147    Cpu,
148    /// GPU dispatch succeeded.
149    Gpu,
150    /// GPU was attempted but fell back to CPU.
151    GpuFallback,
152}
153
154/// Compute immediate dominators for every reachable block.
155pub fn compute_dominators(cfg: &Cfg) -> Result<HashMap<u32, u32>, &'static str> {
156    try_compute_dominators(cfg).map_err(|error| {
157        if error.contains("entry") {
158            "SSA dominator construction failed: entry block not found"
159        } else {
160            "SSA dominator construction failed"
161        }
162    })
163}
164
165/// Compute immediate dominators for every reachable block with fallible
166/// allocation errors.
167///
168/// AUDIT_2026-05-23: Route CFGs above `DOMINATOR_GPU_THRESHOLD` blocks to
169/// `vyre_primitives::graph::dominator_tree` (GPU Program) and keep CPU only
170/// for tiny CFGs or test parity.
171pub fn try_compute_dominators(cfg: &Cfg) -> Result<HashMap<u32, u32>, String> {
172    try_compute_dominators_detailed(cfg).map(|(doms, _path)| doms)
173}
174
175/// Compute immediate dominators and report which code path was taken.
176pub(crate) fn try_compute_dominators_detailed(
177    cfg: &Cfg,
178) -> Result<(HashMap<u32, u32>, DominatorPath), String> {
179    if !cfg.blocks.contains_key(&cfg.entry) {
180        return Err("CFG entry block not found".to_string());
181    }
182
183    let node_count = cfg.blocks.len() as u32;
184    if node_count > DOMINATOR_GPU_THRESHOLD {
185        match try_compute_dominators_gpu(cfg) {
186            Ok(doms) => return Ok((doms, DominatorPath::Gpu)),
187            Err(_) => {
188                let doms = try_compute_dominators_cpu(cfg)?;
189                return Ok((doms, DominatorPath::GpuFallback));
190            }
191        }
192    }
193
194    let doms = try_compute_dominators_cpu(cfg)?;
195    Ok((doms, DominatorPath::Cpu))
196}
197
198/// Reference CPU immediate-dominator construction (Cooper–Harvey–Kennedy
199/// iterative fixpoint).  Used for CFGs at or below the GPU threshold and as
200/// a fallback when no backend is available.
201pub fn try_compute_dominators_cpu(cfg: &Cfg) -> Result<HashMap<u32, u32>, String> {
202    let mut doms: HashMap<u32, u32> = HashMap::new();
203    reserve_hash_map(&mut doms, cfg.blocks.len(), "ssa dominator map")?;
204    doms.insert(cfg.entry, cfg.entry);
205
206    let mut post_order =
207        crate::staging_reserve::reserved_vec(cfg.blocks.len(), "ssa dominator postorder")
208            .map_err(|error| format!("SSA dominator postorder allocation failed: {error}"))?;
209    let mut visited = HashSet::new();
210    reserve_hash_set(
211        &mut visited,
212        cfg.blocks.len(),
213        "ssa dominator DFS visited set",
214    )?;
215    // Iterative DFS to avoid stack overflow on million-node chains.
216    let mut stack = vec![(cfg.entry, false)];
217    while let Some((u, processed)) = stack.pop() {
218        if processed {
219            post_order.push(u);
220            continue;
221        }
222        if !visited.insert(u) {
223            continue;
224        }
225        stack.push((u, true));
226        if let Some(block) = cfg.blocks.get(&u) {
227            for &v in block.succs.iter().rev() {
228                if !visited.contains(&v) {
229                    stack.push((v, false));
230                }
231            }
232        }
233    }
234
235    // Reverse post order to start
236    post_order.reverse();
237
238    let post_order_domain = crate::dense_domain::dense_domain_for_keys(
239        post_order.iter().copied().chain(cfg.blocks.keys().copied()),
240    )?;
241    let mut post_order_idx = crate::dense_domain::DenseU32Slots::<usize>::new(
242        post_order_domain,
243        "ssa dominator postorder index",
244    )?;
245    const MISSING_POSTORDER: usize = usize::MAX;
246    for (i, &u) in post_order.iter().enumerate() {
247        post_order_idx.insert(u, i)?;
248    }
249
250    let intersect = |mut b1: u32, mut b2: u32, doms: &HashMap<u32, u32>| -> u32 {
251        let post_idx = |block: u32| {
252            post_order_idx
253                .get(block)
254                .copied()
255                .unwrap_or(MISSING_POSTORDER)
256        };
257        while b1 != b2 {
258            while post_idx(b1) > post_idx(b2) {
259                b1 = *doms.get(&b1).unwrap_or(&b1);
260            }
261            while post_idx(b2) > post_idx(b1) {
262                b2 = *doms.get(&b2).unwrap_or(&b2);
263            }
264        }
265        b1
266    };
267
268    let mut changed = true;
269    while changed {
270        changed = false;
271        for &b in post_order.iter().skip(1) {
272            if let Some(block) = cfg.blocks.get(&b) {
273                let mut new_idom: Option<u32> = None;
274                for &p in &block.preds {
275                    if doms.contains_key(&p) {
276                        if let Some(n) = new_idom {
277                            new_idom = Some(intersect(p, n, &doms));
278                        } else {
279                            new_idom = Some(p);
280                        }
281                    }
282                }
283                if let Some(new_idom) = new_idom {
284                    if doms.get(&b) != Some(&new_idom) {
285                        doms.insert(b, new_idom);
286                        changed = true;
287                    }
288                }
289            }
290        }
291    }
292
293    Ok(doms)
294}
295
296/// GPU-accelerated immediate-dominator construction using the
297/// `vyre_primitives::graph::dominator_tree` serial lane-0 kernel.
298///
299/// Falls back to [`try_compute_dominators_cpu`] on any error.
300fn try_compute_dominators_gpu(cfg: &Cfg) -> Result<HashMap<u32, u32>, String> {
301    // Dense remapping with entry forced to index 0 (the GPU primitive hardcodes entry=0).
302    let mut ids: Vec<u32> = cfg
303        .blocks
304        .keys()
305        .copied()
306        .filter(|&id| id != cfg.entry)
307        .collect();
308    ids.sort_unstable();
309    ids.insert(0, cfg.entry);
310
311    let node_count = ids.len() as u32;
312    let mut id_to_idx = HashMap::new();
313    reserve_hash_map(&mut id_to_idx, ids.len(), "ssa gpu id map")?;
314    for (idx, &id) in ids.iter().enumerate() {
315        id_to_idx.insert(id, idx as u32);
316    }
317
318    // Build forward and predecessor CSR.
319    let mut forward_offsets: Vec<u32> = Vec::new();
320    crate::dispatch_decode::try_write_zero_words(
321        &mut forward_offsets,
322        ids.len() + 1,
323        "ssa gpu forward offsets",
324    )?;
325    let mut pred_offsets: Vec<u32> = Vec::new();
326    crate::dispatch_decode::try_write_zero_words(
327        &mut pred_offsets,
328        ids.len() + 1,
329        "ssa gpu pred offsets",
330    )?;
331
332    // Count edges.
333    for (&block_id, block) in &cfg.blocks {
334        let src_idx = id_to_idx[&block_id];
335        for &succ_id in &block.succs {
336            if id_to_idx.contains_key(&succ_id) {
337                forward_offsets[src_idx as usize + 1] += 1;
338            }
339        }
340        let dst_idx = src_idx;
341        for &pred_id in &block.preds {
342            if id_to_idx.contains_key(&pred_id) {
343                pred_offsets[dst_idx as usize + 1] += 1;
344            }
345        }
346    }
347
348    // Prefix sum.
349    for i in 1..=ids.len() {
350        forward_offsets[i] += forward_offsets[i - 1];
351        pred_offsets[i] += pred_offsets[i - 1];
352    }
353
354    let forward_target_count = forward_offsets[ids.len()] as usize;
355    let mut forward_targets: Vec<u32> = Vec::new();
356    crate::staging_reserve::reserve_vec(
357        &mut forward_targets,
358        forward_target_count,
359        "ssa gpu forward targets",
360    )?;
361    forward_targets.resize(forward_target_count, 0);
362
363    let pred_target_count = pred_offsets[ids.len()] as usize;
364    let mut pred_targets: Vec<u32> = Vec::new();
365    crate::staging_reserve::reserve_vec(
366        &mut pred_targets,
367        pred_target_count,
368        "ssa gpu pred targets",
369    )?;
370    pred_targets.resize(pred_target_count, 0);
371
372    let mut forward_cursor = forward_offsets.clone();
373    let mut pred_cursor = pred_offsets.clone();
374
375    for (&block_id, block) in &cfg.blocks {
376        let src_idx = id_to_idx[&block_id];
377        for &succ_id in &block.succs {
378            if let Some(&dst_idx) = id_to_idx.get(&succ_id) {
379                let slot = forward_cursor[src_idx as usize] as usize;
380                forward_targets[slot] = dst_idx;
381                forward_cursor[src_idx as usize] += 1;
382            }
383        }
384        let dst_idx = src_idx;
385        for &pred_id in &block.preds {
386            if let Some(&src_idx_pred) = id_to_idx.get(&pred_id) {
387                let slot = pred_cursor[dst_idx as usize] as usize;
388                pred_targets[slot] = src_idx_pred;
389                pred_cursor[dst_idx as usize] += 1;
390            }
391        }
392    }
393
394    let edge_count = forward_targets.len() as u32;
395    let pred_edge_count = pred_targets.len() as u32;
396
397    let program = try_dominator_tree_program(node_count, edge_count, pred_edge_count, "idom_out")
398        .map_err(|e| format!("dominator_tree program build failed: {e}"))?;
399
400    let mut inputs: Vec<Vec<u8>> = Vec::with_capacity(6);
401    inputs.push(crate::dispatch_decode::try_pack_u32(
402        &forward_offsets,
403        "ssa gpu forward offsets",
404    )?);
405    inputs.push(crate::dispatch_decode::try_pack_u32(
406        &forward_targets,
407        "ssa gpu forward targets",
408    )?);
409    inputs.push(crate::dispatch_decode::try_pack_u32(
410        &pred_offsets,
411        "ssa gpu predecessor offsets",
412    )?);
413    inputs.push(crate::dispatch_decode::try_pack_u32(
414        &pred_targets,
415        "ssa gpu predecessor targets",
416    )?);
417    let idom_out_len = (node_count as usize)
418        .checked_mul(4)
419        .ok_or("idom_out byte length overflowed usize")?;
420    let dt_depth_len = (node_count as usize)
421        .checked_mul(4)
422        .ok_or("dt_depth byte length overflowed usize")?;
423    inputs.push(vec![0u8; idom_out_len]);
424    inputs.push(vec![0u8; dt_depth_len]);
425
426    let backend = acquire_preferred_dispatch_backend()
427        .map_err(|e| format!("no dispatch backend available: {e}"))?;
428
429    let mut config = DispatchConfig::default();
430    config.grid_override = Some([1, 1, 1]);
431    let outputs = backend
432        .dispatch(&program, &inputs, &config)
433        .map_err(|e| format!("dominator_tree dispatch failed: {e}"))?;
434
435    if outputs.len() != 2 {
436        return Err(format!(
437            "dominator_tree dispatch returned {} output buffers, expected 2 (idom_out + dt_depth)",
438            outputs.len()
439        ));
440    }
441
442    let mut idoms = Vec::new();
443    crate::staging_reserve::reserve_vec(&mut idoms, node_count as usize, "ssa gpu idom decode")?;
444    crate::dispatch_decode::unpack_exact_u32_into(
445        &outputs[0],
446        node_count as usize,
447        "idom_out",
448        &mut idoms,
449    )?;
450
451    let mut doms = HashMap::new();
452    reserve_hash_map(&mut doms, ids.len(), "ssa dominator map")?;
453    for (idx, &idom) in idoms.iter().enumerate() {
454        if idom == IDOM_NONE {
455            continue;
456        }
457        let original_block = ids[idx];
458        let original_idom = ids[idom as usize];
459        doms.insert(original_block, original_idom);
460    }
461
462    Ok(doms)
463}
464
465/// Marker type for the SSA construction dataflow primitive.
466#[derive(Clone, Copy, Debug, PartialEq, Eq)]
467pub struct Ssa;
468
469impl super::soundness::SoundnessTagged for Ssa {
470    fn soundness(&self) -> super::soundness::Soundness {
471        super::soundness::Soundness::Exact
472    }
473}
474
475/// Compute dominance frontiers from a CFG and immediate-dominator map.
476#[cfg(any(test, feature = "legacy-infallible"))]
477pub fn compute_dominance_frontiers(
478    cfg: &Cfg,
479    doms: &HashMap<u32, u32>,
480) -> HashMap<u32, HashSet<u32>> {
481    try_compute_dominance_frontiers(cfg, doms)
482        .expect("SSA dominance-frontier allocation failed in legacy infallible caller")
483}
484
485/// Compute dominance frontiers with fallible allocation errors.
486pub fn try_compute_dominance_frontiers(
487    cfg: &Cfg,
488    doms: &HashMap<u32, u32>,
489) -> Result<HashMap<u32, HashSet<u32>>, String> {
490    let mut df: HashMap<u32, HashSet<u32>> = HashMap::new();
491    reserve_hash_map(&mut df, cfg.blocks.len(), "ssa dominance frontier map")?;
492    for &b in cfg.blocks.keys() {
493        df.insert(b, HashSet::new());
494    }
495
496    for (&b, block) in &cfg.blocks {
497        if block.preds.len() >= 2 {
498            for &p in &block.preds {
499                let mut runner = p;
500                while runner != *doms.get(&b).unwrap_or(&b) {
501                    let frontier = df.entry(runner).or_default();
502                    reserve_hash_set(
503                        frontier,
504                        frontier.len().checked_add(1).ok_or_else(|| {
505                            "SSA dominance frontier set length overflowed usize".to_string()
506                        })?,
507                        "ssa dominance frontier set",
508                    )?;
509                    frontier.insert(b);
510                    runner = *doms.get(&runner).unwrap_or(&runner);
511                }
512            }
513        }
514    }
515    Ok(df)
516}
517
518/// Place phi nodes for variables with definitions reaching dominance frontiers.
519#[cfg(any(test, feature = "legacy-infallible"))]
520pub fn place_phi_nodes(cfg: &Cfg, df: &HashMap<u32, HashSet<u32>>) -> HashMap<u32, Vec<u32>> {
521    try_place_phi_nodes(cfg, df)
522        .expect("SSA phi placement allocation failed in legacy infallible caller")
523}
524
525/// Place phi nodes for variables with definitions reaching dominance frontiers
526/// with an explicit allocator failure contract.
527pub fn try_place_phi_nodes(
528    cfg: &Cfg,
529    df: &HashMap<u32, HashSet<u32>>,
530) -> Result<HashMap<u32, Vec<u32>>, String> {
531    let mut phi_nodes: HashMap<u32, Vec<u32>> = HashMap::new();
532    reserve_hash_map(&mut phi_nodes, cfg.blocks.len(), "ssa phi node map")?;
533
534    // Map of variable to blocks where it is defined
535    let mut defs: HashMap<u32, HashSet<u32>> = HashMap::new();
536    reserve_hash_map(&mut defs, cfg.blocks.len(), "ssa definition-block map")?;
537    let mut vars: HashSet<u32> = HashSet::new();
538    reserve_hash_set(&mut vars, cfg.blocks.len(), "ssa variable set")?;
539
540    for (&b, block) in &cfg.blocks {
541        for &v in &block.defs {
542            let blocks = defs.entry(v).or_default();
543            reserve_hash_set(
544                blocks,
545                blocks.len().checked_add(1).ok_or_else(|| {
546                    "SSA definition block set length overflowed usize".to_string()
547                })?,
548                "ssa definition block set",
549            )?;
550            blocks.insert(b);
551            let next_vars_len = vars
552                .len()
553                .checked_add(1)
554                .ok_or_else(|| "SSA variable set length overflowed usize".to_string())?;
555            reserve_hash_set(&mut vars, next_vars_len, "ssa variable set")?;
556            vars.insert(v);
557        }
558    }
559
560    for &v in &vars {
561        let Some(var_defs) = defs.get(&v) else {
562            continue;
563        };
564        let mut worklist: Vec<u32> = Vec::new();
565        crate::staging_reserve::reserve_vec(
566            &mut worklist,
567            var_defs.len(),
568            "ssa phi placement worklist",
569        )?;
570        worklist.extend(var_defs.iter().copied());
571        let mut in_worklist: HashSet<u32> = HashSet::new();
572        reserve_hash_set(&mut in_worklist, var_defs.len(), "ssa phi worklist set")?;
573        in_worklist.extend(var_defs.iter().copied());
574        let mut inserted_phi: HashSet<u32> = HashSet::new();
575        reserve_hash_set(&mut inserted_phi, df.len(), "ssa inserted-phi set")?;
576
577        while let Some(x) = worklist.pop() {
578            if let Some(frontier) = df.get(&x) {
579                for &y in frontier {
580                    if !inserted_phi.contains(&y) {
581                        let node_phis = phi_nodes.entry(y).or_default();
582                        crate::staging_reserve::reserve_vec(
583                            node_phis,
584                            node_phis.len().checked_add(1).ok_or_else(|| {
585                                "SSA phi node vector length overflowed usize".to_string()
586                            })?,
587                            "ssa phi node vector",
588                        )?;
589                        node_phis.push(v);
590                        let next_inserted_phi_len =
591                            inserted_phi.len().checked_add(1).ok_or_else(|| {
592                                "SSA inserted-phi set length overflowed usize".to_string()
593                            })?;
594                        reserve_hash_set(
595                            &mut inserted_phi,
596                            next_inserted_phi_len,
597                            "ssa inserted-phi set",
598                        )?;
599                        inserted_phi.insert(y);
600                        if !in_worklist.contains(&y) {
601                            let next_worklist_len =
602                                worklist.len().checked_add(1).ok_or_else(|| {
603                                    "SSA phi placement worklist length overflowed usize".to_string()
604                                })?;
605                            crate::staging_reserve::reserve_vec(
606                                &mut worklist,
607                                next_worklist_len,
608                                "ssa phi placement worklist",
609                            )?;
610                            worklist.push(y);
611                            let next_in_worklist_len =
612                                in_worklist.len().checked_add(1).ok_or_else(|| {
613                                    "SSA phi worklist set length overflowed usize".to_string()
614                                })?;
615                            reserve_hash_set(
616                                &mut in_worklist,
617                                next_in_worklist_len,
618                                "ssa phi worklist set",
619                            )?;
620                            in_worklist.insert(y);
621                        }
622                    }
623                }
624            }
625        }
626    }
627
628    Ok(phi_nodes)
629}
630
631/// Rename variables into SSA versions and build def-use chains.
632#[cfg(any(test, feature = "legacy-infallible"))]
633pub fn rename_variables(
634    cfg: &Cfg,
635    doms: &HashMap<u32, u32>,
636    phi_nodes: &HashMap<u32, Vec<u32>>,
637) -> SsaForm {
638    try_rename_variables(cfg, doms, phi_nodes)
639        .expect("SSA variable renaming allocation failed in legacy infallible caller")
640}
641
642/// Rename variables into SSA versions and build def-use chains with fallible
643/// allocation and checked synthetic node-id arithmetic.
644pub fn try_rename_variables(
645    cfg: &Cfg,
646    doms: &HashMap<u32, u32>,
647    phi_nodes: &HashMap<u32, Vec<u32>>,
648) -> Result<SsaForm, String> {
649    // Determine the variable ids by aggregating all defs
650    let mut vars: HashSet<u32> = HashSet::new();
651    reserve_hash_set(&mut vars, cfg.blocks.len(), "ssa rename variable set")?;
652    for block in cfg.blocks.values() {
653        let next_vars_capacity = vars
654            .len()
655            .checked_add(block.defs.len())
656            .and_then(|value| value.checked_add(block.uses.len()))
657            .ok_or_else(|| "SSA rename variable set capacity overflowed usize".to_string())?;
658        reserve_hash_set(&mut vars, next_vars_capacity, "ssa rename variable set")?;
659        vars.extend(&block.defs);
660        vars.extend(&block.uses);
661    }
662
663    let mut count: HashMap<u32, u32> = HashMap::new();
664    reserve_hash_map(&mut count, vars.len(), "ssa version counter map")?;
665    let mut stack: HashMap<u32, Vec<u32>> = HashMap::new();
666    reserve_hash_map(&mut stack, vars.len(), "ssa version stack map")?;
667
668    for &v in &vars {
669        count.insert(v, 0);
670        let mut initial = crate::staging_reserve::reserved_vec(1, "ssa initial version stack")?;
671        initial.push(0);
672        stack.insert(v, initial);
673    }
674
675    // We need the dominator tree (children of each node)
676    let mut dom_tree: HashMap<u32, Vec<u32>> = HashMap::new();
677    reserve_hash_map(&mut dom_tree, doms.len(), "ssa dominator tree")?;
678    for (&node, &idom) in doms {
679        if node != idom {
680            let children = dom_tree.entry(idom).or_default();
681            crate::staging_reserve::reserve_vec(
682                children,
683                children.len().checked_add(1).ok_or_else(|| {
684                    "SSA dominator-tree child vector length overflowed usize".to_string()
685                })?,
686                "ssa dominator-tree children",
687            )?;
688            children.push(node);
689        }
690    }
691
692    let renamed_usage_capacity = cfg.blocks.len().checked_mul(vars.len()).ok_or_else(|| {
693        "SSA renamed usage capacity overflowed usize. Fix: shard CFG blocks or variables before SSA conversion.".to_string()
694    })?;
695    let mut renamed_usages: HashMap<u32, u32> = HashMap::new();
696    reserve_hash_map(
697        &mut renamed_usages,
698        renamed_usage_capacity,
699        "ssa renamed-usage map",
700    )?;
701    // The generic block summary has variable ids rather than statement-local node ids, so this
702    // pass tracks version numbers and leaves concrete node-id mapping to the AST walker.
703
704    // For now we simulate renaming DFS purely to show Cooper-Harvey-Kennedy compliance.
705    let mut def_use_chains: HashMap<u32, Vec<u32>> = HashMap::new();
706    reserve_hash_map(&mut def_use_chains, vars.len(), "ssa def-use chain map")?;
707
708    fn rename_dfs(
709        u: u32,
710        cfg: &Cfg,
711        dom_tree: &HashMap<u32, Vec<u32>>,
712        phi_nodes: &HashMap<u32, Vec<u32>>,
713        count: &mut HashMap<u32, u32>,
714        stack: &mut HashMap<u32, Vec<u32>>,
715        renamed_usages: &mut HashMap<u32, u32>,
716        def_use_chains: &mut HashMap<u32, Vec<u32>>,
717    ) -> Result<(), String> {
718        // Generate phi definition versions for this block.
719        if let Some(phis) = phi_nodes.get(&u) {
720            for &v in phis {
721                let next_version = {
722                    let c = count.entry(v).or_insert(0);
723                    *c += 1;
724                    *c
725                };
726                let versions = stack.entry(v).or_default();
727                if versions.is_empty() {
728                    crate::staging_reserve::reserve_vec(versions, 1, "ssa phi version stack")?;
729                    versions.push(0);
730                }
731                crate::staging_reserve::reserve_vec(
732                    versions,
733                    versions.len().checked_add(1).ok_or_else(|| {
734                        "SSA phi version stack length overflowed usize".to_string()
735                    })?,
736                    "ssa phi version stack",
737                )?;
738                versions.push(next_version);
739            }
740        }
741
742        if let Some(block) = cfg.blocks.get(&u) {
743            for &v in &block.defs {
744                let next_version = {
745                    let c = count.entry(v).or_insert(0);
746                    *c += 1;
747                    *c
748                };
749                let versions = stack.entry(v).or_default();
750                if versions.is_empty() {
751                    crate::staging_reserve::reserve_vec(versions, 1, "ssa def version stack")?;
752                    versions.push(0);
753                }
754                crate::staging_reserve::reserve_vec(
755                    versions,
756                    versions.len().checked_add(1).ok_or_else(|| {
757                        "SSA def version stack length overflowed usize".to_string()
758                    })?,
759                    "ssa def version stack",
760                )?;
761                versions.push(next_version);
762                // track use
763                renamed_usages.insert(synthetic_node_id(u, v)?, next_version);
764                def_use_chains.entry(next_version).or_default();
765            }
766
767            for &v in &block.uses {
768                if let Some(top) = stack.get(&v).and_then(|s| s.last()) {
769                    renamed_usages.insert(synthetic_node_id(u, v)?, *top);
770                    let uses = def_use_chains.entry(*top).or_default();
771                    crate::staging_reserve::reserve_vec(
772                        uses,
773                        uses.len().checked_add(1).ok_or_else(|| {
774                            "SSA def-use chain vector length overflowed usize".to_string()
775                        })?,
776                        "ssa def-use chain",
777                    )?;
778                    uses.push(u);
779                }
780            }
781        }
782
783        if let Some(children) = dom_tree.get(&u) {
784            for &child in children {
785                rename_dfs(
786                    child,
787                    cfg,
788                    dom_tree,
789                    phi_nodes,
790                    count,
791                    stack,
792                    renamed_usages,
793                    def_use_chains,
794                )?;
795            }
796        }
797
798        // Pop stack
799        if let Some(phis) = phi_nodes.get(&u) {
800            for &v in phis {
801                if let Some(versions) = stack.get_mut(&v) {
802                    versions.pop();
803                }
804            }
805        }
806        if let Some(block) = cfg.blocks.get(&u) {
807            for &v in &block.defs {
808                if let Some(versions) = stack.get_mut(&v) {
809                    versions.pop();
810                }
811            }
812        }
813        Ok(())
814    }
815
816    rename_dfs(
817        cfg.entry,
818        cfg,
819        &dom_tree,
820        phi_nodes,
821        &mut count,
822        &mut stack,
823        &mut renamed_usages,
824        &mut def_use_chains,
825    )?;
826
827    Ok(SsaForm {
828        phi_nodes: phi_nodes.clone(),
829        renamed_usages,
830        def_use_chains,
831    })
832}
833
834#[inline]
835fn synthetic_node_id(block: u32, variable: u32) -> Result<u32, String> {
836    block
837        .checked_mul(1000)
838        .and_then(|base| base.checked_add(variable))
839        .ok_or_else(|| synthetic_node_id_overflow(block, variable))
840}
841
842#[cold]
843fn synthetic_node_id_overflow(block: u32, variable: u32) -> String {
844    let mut scratch = crate::error_format::ErrorFormatScratch::default();
845    let _ = std::fmt::Write::write_fmt(
846        &mut scratch.buf,
847        format_args!(
848            "SSA synthetic node id overflowed for block {block}, variable {variable}. Fix: provide concrete frontend node ids before SSA renaming or shard the CFG."
849        ),
850    );
851    scratch.finish()
852}
853
854#[inline]
855fn reserve_hash_map<K, V>(
856    map: &mut HashMap<K, V>,
857    capacity: usize,
858    field: &'static str,
859) -> Result<(), String>
860where
861    K: Eq + Hash,
862{
863    if map.capacity() >= capacity {
864        return Ok(());
865    }
866    map.try_reserve(capacity - map.capacity()).map_err(|error| {
867        format!(
868            "Weir SSA could not reserve {capacity} {field} entries: {error}. Fix: shard the CFG or move this SSA stage onto resident graph execution."
869        )
870    })
871}
872
873#[inline]
874fn reserve_hash_set<T>(
875    set: &mut HashSet<T>,
876    capacity: usize,
877    field: &'static str,
878) -> Result<(), String>
879where
880    T: Eq + Hash,
881{
882    if set.capacity() >= capacity {
883        return Ok(());
884    }
885    set.try_reserve(capacity - set.capacity()).map_err(|error| {
886        format!(
887            "Weir SSA could not reserve {capacity} {field} entries: {error}. Fix: shard the CFG or move this SSA stage onto resident graph execution."
888        )
889    })
890}
891
892#[cfg(test)]
893mod test_ssa {
894    use super::*;
895
896    #[test]
897    fn ssa_reference_construction_exposes_fallible_release_paths() {
898        let source = include_str!("ssa.rs");
899        let production = source
900            .split("#[cfg(test)]")
901            .next()
902            .expect("SSA production source must precede tests");
903
904        assert!(
905            production.contains("pub fn try_compute_dominators")
906                && production.contains("pub fn try_compute_dominance_frontiers")
907                && production.contains("pub fn try_rename_variables")
908                && production.contains("dense_domain::DenseU32Slots")
909                && production.contains("fn reserve_hash_map")
910                && production.contains("fn reserve_hash_set"),
911            "Fix: Weir SSA construction must expose fallible allocation-aware APIs for production callers."
912        );
913        assert!(
914            !production.contains("HashMap::with_capacity")
915                && !production.contains("HashSet::with_capacity")
916                && !production.contains("u * 1000 + v"),
917            "Fix: Weir SSA construction must not use infallible hash allocation or unchecked synthetic node-id arithmetic."
918        );
919    }
920
921    #[test]
922    fn synthetic_node_ids_reject_overflow() {
923        let error = synthetic_node_id(u32::MAX, 999)
924            .expect_err("oversized synthetic SSA node ids must fail loudly");
925        assert!(
926            error.contains("SSA synthetic node id overflowed"),
927            "{error}"
928        );
929    }
930
931    include!("tests/test_ssa.rs");
932}