use super::lex::split_top_level;
use std::collections::HashMap;
pub(super) fn promote_entry_allocas_and_fold_aggregates(san_ll: &str) -> String {
let lines: Vec<&str> = san_ll.lines().collect();
let mut out: Vec<String> = Vec::with_capacity(lines.len());
let mut i = 0;
while i < lines.len() {
let line = lines[i];
if line.starts_with("define") && line.trim_end().ends_with('{') {
let start = i + 1;
let mut j = start;
while j < lines.len() && lines[j] != "}" {
j += 1;
}
out.push(line.to_string());
let body: Vec<String> = lines[start..j].iter().map(|s| s.to_string()).collect();
out.extend(rewrite_body_to_fixpoint(body));
if j < lines.len() {
out.push(lines[j].to_string());
}
i = j + 1;
} else {
out.push(line.to_string());
i += 1;
}
}
let mut result = out.join("\n");
if san_ll.ends_with('\n') {
result.push('\n');
}
result
}
fn rewrite_body_to_fixpoint(mut body: Vec<String>) -> Vec<String> {
for _ in 0..64 {
let (next, changed_a) = fold_aggregates_once(body.clone());
let (next, changed_c) = dce_pure_once(next);
let (next, changed_b) = promote_allocas_once(next);
let (next, changed_d) = lower_pointer_array_index_once(next);
body = next;
if !changed_a && !changed_b && !changed_c && !changed_d {
break;
}
}
body
}
struct Def {
name: String,
opcode: String,
rest: String,
}
fn parse_def(line: &str) -> Option<Def> {
let t = line.trim();
let eq = t.find(" = ")?;
let name = t[..eq].trim();
if !name.starts_with('%') {
return None;
}
let rhs = t[eq + 3..].trim();
let opcode = rhs.split_whitespace().next()?;
let rest = rhs[opcode.len()..].trim();
Some(Def {
name: name.to_string(),
opcode: opcode.to_string(),
rest: rest.to_string(),
})
}
fn subst_value_token(line: &str, old: &str, new_tok: &str) -> String {
debug_assert!(old.starts_with('%'));
let bytes = line.as_bytes();
let mut out = String::with_capacity(line.len());
let mut i = 0;
while i < line.len() {
if line[i..].starts_with(old) {
let after = i + old.len();
let next_ok = after >= line.len() || {
let c = bytes[after] as char;
!(c.is_ascii_alphanumeric() || c == '_' || c == '.')
};
let prev_ok = i == 0 || {
let c = bytes[i - 1] as char;
!(c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '%')
};
if next_ok && prev_ok {
out.push_str(new_tok);
i = after;
continue;
}
}
let ch = line[i..].chars().next().unwrap();
out.push(ch);
i += ch.len_utf8();
}
out
}
fn apply_substitutions(body: &mut [String], subs: &HashMap<String, String>) {
if subs.is_empty() {
return;
}
for line in body.iter_mut() {
for (old, new_tok) in subs {
if line.contains(old.as_str()) {
*line = subst_value_token(line, old, new_tok);
}
}
}
}
fn parse_insertvalue(rest: &str) -> Option<(String, String, Vec<u32>)> {
let parts = split_top_level(rest, ',');
if parts.len() < 3 {
return None;
}
let agg = last_token(parts[0].trim())?;
let val = last_token(parts[1].trim())?;
let mut path = Vec::new();
for p in &parts[2..] {
path.push(p.trim().parse::<u32>().ok()?);
}
Some((agg, val, path))
}
fn parse_extractvalue(rest: &str) -> Option<(String, Vec<u32>)> {
let parts = split_top_level(rest, ',');
if parts.len() < 2 {
return None;
}
let agg = last_token(parts[0].trim())?;
let mut path = Vec::new();
for p in &parts[1..] {
path.push(p.trim().parse::<u32>().ok()?);
}
Some((agg, path))
}
fn last_token(s: &str) -> Option<String> {
s.split_whitespace().last().map(|t| t.to_string())
}
fn fold_aggregates_once(mut body: Vec<String>) -> (Vec<String>, bool) {
let mut inserts: HashMap<String, (String, Vec<u32>, String)> = HashMap::new();
for line in &body {
if let Some(def) = parse_def(line) {
if def.opcode == "insertvalue" {
if let Some((agg, val, path)) = parse_insertvalue(&def.rest) {
inserts.insert(def.name.clone(), (val, path, agg));
}
}
}
}
let mut subs: HashMap<String, String> = HashMap::new();
for line in &body {
let Some(def) = parse_def(line) else { continue };
if def.opcode != "extractvalue" {
continue;
}
let Some((agg, path)) = parse_extractvalue(&def.rest) else {
continue;
};
if let Some(val) = resolve_aggregate_path(&agg, &path, &inserts) {
if val != "poison" && val != "undef" && val != "zeroinitializer" {
subs.insert(def.name.clone(), val);
}
}
}
if subs.is_empty() {
return (body, false);
}
apply_substitutions(&mut body, &subs);
body.retain(|line| {
if let Some(def) = parse_def(line) {
if def.opcode == "extractvalue" && subs.contains_key(&def.name) {
return false;
}
}
true
});
(body, true)
}
fn resolve_aggregate_path(
agg: &str,
path: &[u32],
inserts: &HashMap<String, (String, Vec<u32>, String)>,
) -> Option<String> {
let mut cur = agg.to_string();
for _ in 0..256 {
let (val, ipath, parent) = inserts.get(&cur)?;
if ipath == path {
return Some(val.clone());
}
if is_prefix(ipath, path) || is_prefix(path, ipath) {
return None;
}
cur = parent.clone();
}
None
}
fn is_prefix(a: &[u32], b: &[u32]) -> bool {
a.len() < b.len() && b[..a.len()] == *a
}
fn dce_pure_once(mut body: Vec<String>) -> (Vec<String>, bool) {
const PURE: [&str; 4] = ["insertvalue", "extractvalue", "getelementptr", "bitcast"];
let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
for line in &body {
let operand_text = match line.find(" = ") {
Some(eq) => &line[eq + 3..],
None => line.as_str(),
};
collect_value_tokens(operand_text, &mut used);
}
let before = body.len();
body.retain(|line| {
if let Some(def) = parse_def(line) {
let dead = !used.contains(&def.name);
if dead && (PURE.contains(&def.opcode.as_str()) || def.opcode == "alloca") {
return false;
}
}
true
});
let changed = body.len() != before;
(body, changed)
}
fn collect_value_tokens(s: &str, out: &mut std::collections::HashSet<String>) {
let bytes = s.as_bytes();
let mut i = 0;
while i < s.len() {
if bytes[i] == b'%' {
let start = i;
i += 1;
while i < s.len() {
let c = bytes[i] as char;
if c.is_ascii_alphanumeric() || c == '_' || c == '.' {
i += 1;
} else {
break;
}
}
if i > start + 1 {
out.insert(s[start..i].to_string());
}
} else {
i += 1;
}
}
}
#[derive(Clone)]
enum ElemIdx {
Const(u32),
Dyn(String, String), }
fn lower_pointer_array_index_once(body: Vec<String>) -> (Vec<String>, bool) {
let entry_end = body
.iter()
.position(|l| is_block_label(l))
.unwrap_or(body.len());
let arrays: Vec<(String, u32, String)> = body[..entry_end]
.iter()
.filter_map(|line| {
let def = parse_def(line)?;
if def.opcode != "alloca" {
return None;
}
let ty = def.rest.split(',').next().unwrap_or("").trim();
let (n, elem) = parse_array_type(ty)?;
Some((def.name, n, elem))
})
.collect();
for (aname, n, elem) in &arrays {
if let Some(nb) = try_lower_array(&body, entry_end, aname, *n, elem) {
return (nb, true);
}
}
(body, false)
}
fn parse_array_type(ty: &str) -> Option<(u32, String)> {
let t = ty.trim();
let inner = t.strip_prefix('[')?.strip_suffix(']')?.trim();
let (n, rest) = inner.split_once(" x ")?;
Some((n.trim().parse::<u32>().ok()?, rest.trim().to_string()))
}
fn try_lower_array(
body: &[String],
entry_end: usize,
aname: &str,
n: u32,
elem: &str,
) -> Option<Vec<String>> {
let mut elem_idx: HashMap<String, ElemIdx> = HashMap::new();
let mut progressed = true;
while progressed {
progressed = false;
for line in body {
let Some(def) = parse_def(line) else { continue };
if def.opcode != "getelementptr" || elem_idx.contains_key(&def.name) {
continue;
}
let parts = split_top_level(&def.rest, ',');
if parts.len() < 2 {
continue;
}
let base = last_token(parts[1].trim())?;
if base == aname {
if parts.len() == 4 && last_token(parts[2].trim())? == "0" {
let idx = parse_index_operand(parts[3].trim())?;
elem_idx.insert(def.name.clone(), idx);
progressed = true;
}
} else if let Some(base_idx) = elem_idx.get(&base).cloned() {
if parts.len() == 3 {
let add = parse_index_operand(parts[2].trim())?;
let combined = add_elem_idx(&base_idx, &add)?;
elem_idx.insert(def.name.clone(), combined);
progressed = true;
}
}
}
}
let mut family: std::collections::HashSet<String> = elem_idx.keys().cloned().collect();
family.insert(aname.to_string());
let mut vals: Vec<Option<String>> = vec![None; n as usize];
let mut store_count: Vec<usize> = vec![0; n as usize];
let mut load_rewrites: Vec<(usize, String, ElemIdx)> = Vec::new(); let mut dead_ptrs: std::collections::HashSet<String> = std::collections::HashSet::new();
for (li, line) in body.iter().enumerate() {
let t = line.trim();
if is_block_label(line) {
continue;
}
if let Some(def) = parse_def(line) {
if family.contains(&def.name)
&& (def.opcode == "alloca" || def.opcode == "getelementptr")
{
continue; }
if def.opcode == "load" {
let parts = split_top_level(&def.rest, ',');
if let Some(ptr) = parts.get(1).and_then(|p| last_token(p.trim())) {
if let Some(idx) = elem_idx.get(&ptr) {
load_rewrites.push((li, def.name.clone(), idx.clone()));
continue;
}
if ptr == aname {
load_rewrites.push((li, def.name.clone(), ElemIdx::Const(0)));
continue;
}
}
}
}
if let Some(inner) = t.strip_prefix("store ") {
let parts = split_top_level(inner, ',');
if parts.len() >= 2 {
let val = last_token(parts[0].trim())?;
let ptr = last_token(parts[1].trim())?;
if let Some(ElemIdx::Const(c)) = elem_idx.get(&ptr) {
if (*c as usize) < vals.len() {
if li >= entry_end {
return None;
}
store_count[*c as usize] += 1;
if store_count[*c as usize] > 1 {
return None;
}
vals[*c as usize] = Some(val.clone());
dead_ptrs.insert(ptr.clone());
continue;
}
}
if family.contains(&val) {
return None; }
if family.contains(&ptr) {
return None; }
}
continue;
}
if (t.starts_with("call ") || t.starts_with("tail call ")) && is_ignorable_intrinsic_call(t)
{
continue;
}
let code = line.split(';').next().unwrap_or(line);
let code = strip_phi_block_operands(code);
if mentions_family(&code, &family) {
return None;
}
}
if vals.iter().any(|v| v.is_none()) || load_rewrites.is_empty() {
return None;
}
let vals: Vec<String> = vals.into_iter().map(|v| v.unwrap()).collect();
let prefix = ".psa";
if body.iter().any(|l| l.contains(prefix)) {
return None;
}
let mut counter = 0u32;
let rewrite_map: HashMap<usize, (String, ElemIdx)> = load_rewrites
.into_iter()
.map(|(li, res, idx)| (li, (res, idx)))
.collect();
let mut out: Vec<String> = Vec::with_capacity(body.len());
let mut load_subs: HashMap<String, String> = HashMap::new();
for (li, line) in body.iter().enumerate() {
if let Some((res, idx)) = rewrite_map.get(&li) {
match idx {
ElemIdx::Const(c) => {
let v = vals.get(*c as usize)?;
load_subs.insert(res.clone(), v.clone());
}
ElemIdx::Dyn(tok, ity) => {
if vals.len() == 1 {
load_subs.insert(res.clone(), vals[0].clone());
} else {
let lines =
emit_select_cascade(res, tok, ity, &vals, elem, prefix, &mut counter);
out.extend(lines);
}
}
}
continue; }
if let Some(def) = parse_def(line) {
if family.contains(&def.name)
&& (def.opcode == "alloca" || def.opcode == "getelementptr")
{
continue;
}
}
let t = line.trim();
if let Some(inner) = t.strip_prefix("store ") {
let parts = split_top_level(inner, ',');
if let Some(ptr) = parts.get(1).and_then(|p| last_token(p.trim())) {
if dead_ptrs.contains(&ptr) {
continue;
}
}
}
if (t.starts_with("call ") || t.starts_with("tail call "))
&& is_ignorable_intrinsic_call(t)
&& mentions_family(line, &family)
{
continue;
}
out.push(line.clone());
}
apply_substitutions(&mut out, &load_subs);
Some(out)
}
fn parse_index_operand(op: &str) -> Option<ElemIdx> {
let toks: Vec<&str> = op.split_whitespace().collect();
let tok = toks.last()?.to_string();
let ty = toks
.get(toks.len().wrapping_sub(2))
.unwrap_or(&"i64")
.to_string();
if let Ok(c) = tok.parse::<u32>() {
Some(ElemIdx::Const(c))
} else if tok.starts_with('%') {
Some(ElemIdx::Dyn(tok, ty))
} else {
None
}
}
fn add_elem_idx(base: &ElemIdx, add: &ElemIdx) -> Option<ElemIdx> {
match (base, add) {
(ElemIdx::Const(0), a) => Some(a.clone()),
(ElemIdx::Const(b), ElemIdx::Const(a)) => Some(ElemIdx::Const(b + a)),
_ => None, }
}
fn emit_select_cascade(
res: &str,
idx_tok: &str,
idx_ty: &str,
vals: &[String],
elem: &str,
prefix: &str,
counter: &mut u32,
) -> Vec<String> {
let n = vals.len();
let mut acc = vals[n - 1].clone();
let mut lines: Vec<String> = Vec::new();
for i in (0..n - 1).rev() {
let cmp = format!("{prefix}.c{}", *counter);
*counter += 1;
lines.push(format!(" {cmp} = icmp eq {idx_ty} {idx_tok}, {i}"));
let sel_name = if i == 0 {
res.to_string()
} else {
let s = format!("{prefix}.s{}", *counter);
*counter += 1;
s
};
lines.push(format!(
" {sel_name} = select i1 {cmp}, {elem} {}, {elem} {acc}",
vals[i]
));
acc = sel_name;
}
lines
}
struct BlockDoms {
line_block: Vec<usize>,
dom: Vec<std::collections::HashSet<usize>>,
}
impl BlockDoms {
fn build(body: &[String]) -> BlockDoms {
let mut block_start: Vec<usize> = vec![0];
let mut label_to_block: HashMap<String, usize> = HashMap::new();
for (i, line) in body.iter().enumerate() {
if is_block_label(line) {
let name = line.trim();
let name = name.split(':').next().unwrap_or("").trim();
label_to_block.insert(format!("%{name}"), block_start.len());
block_start.push(i);
}
}
let nblocks = block_start.len();
let mut line_block = vec![0usize; body.len()];
for b in 0..nblocks {
let start = block_start[b];
let end = if b + 1 < nblocks {
block_start[b + 1]
} else {
body.len()
};
line_block[start..end].fill(b);
}
let mut succ: Vec<Vec<usize>> = vec![Vec::new(); nblocks];
for b in 0..nblocks {
let start = block_start[b];
let end = if b + 1 < nblocks {
block_start[b + 1]
} else {
body.len()
};
for l in (start..end).rev() {
let t = body[l].trim();
if t.starts_with("br ") || t.starts_with("switch ") {
let toks: Vec<&str> = t.split_whitespace().collect();
for w in 0..toks.len() {
if toks[w].trim_end_matches(',') == "label" {
if let Some(name) = toks.get(w + 1) {
let name = name.trim_end_matches(',');
if let Some(&tb) = label_to_block.get(name) {
if !succ[b].contains(&tb) {
succ[b].push(tb);
}
}
}
}
}
break;
}
if t.starts_with("ret") || t.starts_with("unreachable") {
break;
}
}
}
let mut preds: Vec<Vec<usize>> = vec![Vec::new(); nblocks];
for (b, succ_b) in succ.iter().enumerate() {
for &s in succ_b {
preds[s].push(b);
}
}
let mut reachable = vec![false; nblocks];
let mut stack = vec![0usize];
reachable[0] = true;
while let Some(b) = stack.pop() {
for &s in &succ[b] {
if !reachable[s] {
reachable[s] = true;
stack.push(s);
}
}
}
let reachable_set: std::collections::HashSet<usize> =
(0..nblocks).filter(|&b| reachable[b]).collect();
let mut dom: Vec<std::collections::HashSet<usize>> = vec![Default::default(); nblocks];
for b in 0..nblocks {
if reachable[b] {
dom[b] = reachable_set.clone();
}
}
dom[0] = std::collections::HashSet::from([0]);
let mut changed = true;
while changed {
changed = false;
for b in 1..nblocks {
if !reachable[b] {
continue;
}
let mut new_dom: Option<std::collections::HashSet<usize>> = None;
for &p in &preds[b] {
if !reachable[p] {
continue;
}
new_dom = Some(match new_dom {
None => dom[p].clone(),
Some(acc) => acc.intersection(&dom[p]).copied().collect(),
});
}
let mut new_dom = new_dom.unwrap_or_default();
new_dom.insert(b);
if new_dom != dom[b] {
dom[b] = new_dom;
changed = true;
}
}
}
BlockDoms { line_block, dom }
}
fn store_dominates_load(&self, store_line: usize, load_line: usize) -> bool {
let sb = self.line_block[store_line];
let lb = self.line_block[load_line];
if sb == lb {
return store_line < load_line;
}
if sb == 0 {
return true;
}
self.dom[lb].contains(&sb)
}
}
fn promote_allocas_once(body: Vec<String>) -> (Vec<String>, bool) {
let doms = BlockDoms::build(&body);
let mut allocas: Vec<(String, String)> = Vec::new(); for line in &body {
if let Some(def) = parse_def(line) {
if def.opcode == "alloca" {
let ty = def.rest.split(',').next().unwrap_or("").trim().to_string();
allocas.push((def.name.clone(), ty));
}
}
}
if allocas.is_empty() {
return (body, false);
}
for (aname, _ty) in &allocas {
if let Some((subs, dead_defs)) = try_promote_alloca(&body, &doms, aname) {
let mut nb = body.clone();
nb.retain(|line| {
if let Some(def) = parse_def(line) {
if dead_defs.contains(&def.name) {
return false;
}
}
if is_dead_store(line, &dead_defs) {
return false;
}
let t = line.trim();
if (t.starts_with("call ") || t.starts_with("tail call "))
&& is_ignorable_intrinsic_call(t)
&& dead_defs
.iter()
.any(|d| !d.starts_with("store@") && mentions_name(line, d))
{
return false;
}
true
});
apply_substitutions(&mut nb, &subs);
return (nb, true);
}
}
(body, false)
}
fn try_promote_alloca(
body: &[String],
doms: &BlockDoms,
aname: &str,
) -> Option<(HashMap<String, String>, std::collections::HashSet<String>)> {
let mut gep_path: HashMap<String, String> = HashMap::new();
let mut family: std::collections::HashSet<String> = std::collections::HashSet::new();
family.insert(aname.to_string());
let mut progressed = true;
while progressed {
progressed = false;
for line in body {
let Some(def) = parse_def(line) else { continue };
if def.opcode != "getelementptr" {
continue;
}
if gep_path.contains_key(&def.name) {
continue;
}
if let Some((base, idxpath)) = parse_gep_base_and_path(&def.rest) {
if family.contains(&base) {
let base_path = if base == aname {
String::new()
} else {
gep_path.get(&base)?.clone()
};
let full = join_path(&base_path, &idxpath);
gep_path.insert(def.name.clone(), full);
family.insert(def.name.clone());
progressed = true;
}
}
}
}
let mut slot_store: HashMap<String, (String, usize, usize, String)> = HashMap::new();
let mut loads: Vec<(String, String, usize)> = Vec::new(); let mut dead: std::collections::HashSet<String> = std::collections::HashSet::new();
for (li, line) in body.iter().enumerate() {
let t = line.trim();
if let Some(def) = parse_def(line) {
if family.contains(&def.name)
&& (def.opcode == "alloca" || def.opcode == "getelementptr")
{
continue;
}
}
if let Some(inner) = t.strip_prefix("store ") {
let parts = split_top_level(inner, ',');
if parts.len() < 2 {
if mentions_family(line, &family) {
return None;
}
continue;
}
let val = last_token(parts[0].trim())?;
let ptr = last_token(parts[1].trim())?;
if family.contains(&ptr) {
let key = slot_key(&ptr, aname, &gep_path);
let entry =
slot_store
.entry(key.clone())
.or_insert((val.clone(), 0, li, ptr.clone()));
entry.0 = val.clone();
entry.1 += 1;
entry.2 = li;
entry.3 = ptr.clone();
} else if family.contains(&val) {
return None;
}
continue;
}
if let Some(def) = parse_def(line) {
if def.opcode == "load" {
let parts = split_top_level(&def.rest, ',');
if let Some(pp) = parts.get(1) {
if let Some(ptr) = last_token(pp.trim()) {
if family.contains(&ptr) {
let key = slot_key(&ptr, aname, &gep_path);
loads.push((def.name.clone(), key, li));
continue;
}
}
}
}
if def.opcode == "getelementptr" && family.contains(&def.name) {
continue; }
}
if (t.starts_with("call ") || t.starts_with("tail call ")) && is_ignorable_intrinsic_call(t)
{
continue;
}
if mentions_family_as_operand(line, &family, aname) {
return None;
}
}
let mut subs: HashMap<String, String> = HashMap::new();
let mut slot_loads: HashMap<String, usize> = HashMap::new();
let mut slot_forwarded: HashMap<String, usize> = HashMap::new();
for (_res, key, _li) in &loads {
*slot_loads.entry(key.clone()).or_insert(0) += 1;
}
for (res, key, load_li) in &loads {
if let Some((val, count, store_li, _ptr)) = slot_store.get(key) {
if *count == 1 && doms.store_dominates_load(*store_li, *load_li) {
subs.insert(res.clone(), val.clone());
dead.insert(res.clone());
*slot_forwarded.entry(key.clone()).or_insert(0) += 1;
}
}
}
if subs.is_empty() {
return None; }
for (key, (_val, count, _store_li, ptr)) in &slot_store {
if *count == 1
&& slot_forwarded.get(key).copied().unwrap_or(0)
== slot_loads.get(key).copied().unwrap_or(0)
&& slot_loads.get(key).copied().unwrap_or(0) > 0
{
dead.insert(store_marker(ptr));
}
}
Some((subs, dead))
}
fn store_marker(ptr: &str) -> String {
format!("store@{ptr}")
}
fn is_ignorable_intrinsic_call(t: &str) -> bool {
t.contains("@llvm.lifetime.start")
|| t.contains("@llvm.lifetime.end")
|| t.contains("@llvm.dbg.")
|| t.contains("@llvm.assume")
|| t.contains("@llvm.invariant.start")
|| t.contains("@llvm.invariant.end")
}
fn mentions_name(line: &str, name: &str) -> bool {
subst_value_token(line, name, "\0") != *line
}
fn is_dead_store(line: &str, dead: &std::collections::HashSet<String>) -> bool {
let t = line.trim();
if !t.starts_with("store ") {
return false;
}
let inner = &t[6..];
let parts = split_top_level(inner, ',');
if parts.len() < 2 {
return false;
}
if let Some(ptr) = last_token(parts[1].trim()) {
return dead.contains(&store_marker(&ptr));
}
false
}
fn slot_key(ptr: &str, aname: &str, gep_path: &HashMap<String, String>) -> String {
if ptr == aname {
String::new()
} else {
gep_path
.get(ptr)
.cloned()
.unwrap_or_else(|| ptr.to_string())
}
}
fn strip_phi_block_operands(line: &str) -> String {
let t = line.trim_start();
let is_phi = t
.split_once('=')
.map(|(_, r)| r.trim_start().starts_with("phi "))
.unwrap_or(false);
if !is_phi {
return line.to_string();
}
let mut out = String::with_capacity(line.len());
let bytes = line.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'[' {
let start = i;
let mut j = i + 1;
while j < bytes.len() && bytes[j] != b']' {
j += 1;
}
let inner = &line[start + 1..j.min(line.len())];
let val = split_top_level(inner, ',')
.into_iter()
.next()
.unwrap_or_default();
out.push('[');
out.push_str(val.trim());
out.push(']');
i = (j + 1).min(bytes.len());
} else {
out.push(bytes[i] as char);
i += 1;
}
}
out
}
fn mentions_family(line: &str, family: &std::collections::HashSet<String>) -> bool {
family
.iter()
.any(|f| subst_value_token(line, f, "\0") != *line)
}
fn mentions_family_as_operand(
line: &str,
family: &std::collections::HashSet<String>,
_aname: &str,
) -> bool {
mentions_family(line, family)
}
fn is_block_label(line: &str) -> bool {
let t = line.trim();
if t.is_empty() || t.starts_with(';') {
return false;
}
let head = t.split_whitespace().next().unwrap_or("");
head.ends_with(':') && !head.starts_with('"')
}
fn parse_gep_base_and_path(rest: &str) -> Option<(String, String)> {
let parts = split_top_level(rest, ',');
if parts.len() < 2 {
return None;
}
let base = last_token(parts[1].trim())?;
let idxs: Vec<String> = parts[2..].iter().map(|p| p.trim().to_string()).collect();
Some((base, idxs.join(", ")))
}
fn join_path(base: &str, add: &str) -> String {
if base.is_empty() {
add.to_string()
} else if add.is_empty() {
base.to_string()
} else {
format!("{base} | {add}")
}
}
#[cfg(test)]
mod tests {
use super::*;
fn run(src: &str) -> String {
promote_entry_allocas_and_fold_aggregates(src)
}
#[test]
fn no_op_module_is_byte_identical() {
let src = "define void @k() {\n ret void\n}\n";
assert_eq!(run(src), src);
}
#[test]
fn extractvalue_of_insertvalue_folds() {
let src = "\
define void @k(ptr addrspace(1) %0) {
%a = insertvalue { ptr } poison, ptr %0, 0
%b = extractvalue { ptr } %a, 0
store ptr %b, ptr %0, align 8
ret void
}
";
let out = run(src);
assert!(
!out.contains("extractvalue"),
"extract should fold away:\n{out}"
);
assert!(
out.contains("store ptr %0, ptr %0"),
"use of %b -> %0:\n{out}"
);
}
#[test]
fn entry_single_store_load_forwards_and_removes_alloca() {
let src = "\
define void @k(ptr addrspace(1) %0, ptr addrspace(1) %out) {
%s = alloca [1 x ptr addrspace(1)], align 8
%p = getelementptr inbounds [1 x ptr addrspace(1)], ptr %s, i64 0, i64 0
store ptr addrspace(1) %0, ptr %p, align 8
%q = getelementptr inbounds [1 x ptr addrspace(1)], ptr %s, i64 0, i64 0
%v = load ptr addrspace(1), ptr %q, align 8
store ptr addrspace(1) %v, ptr %out, align 8
ret void
}
";
let out = run(src);
assert!(!out.contains("alloca"), "alloca promoted away:\n{out}");
assert!(!out.contains("= load"), "load forwarded:\n{out}");
assert!(
out.contains("store ptr addrspace(1) %0, ptr %out"),
"forwarded %0:\n{out}"
);
}
#[test]
fn struct_then_array_fixpoint_resolves_pointer() {
let src = "\
define void @k(ptr addrspace(1) %0, ptr addrspace(1) %out) {
%arr = alloca [1 x ptr addrspace(1)], align 8
%e0 = getelementptr inbounds [1 x ptr addrspace(1)], ptr %arr, i64 0, i64 0
store ptr addrspace(1) %0, ptr %e0, align 8
%ap = getelementptr inbounds [1 x ptr addrspace(1)], ptr %arr, i64 0, i64 0
%st = alloca { ptr }, align 8
%sp = getelementptr inbounds { ptr }, ptr %st, i64 0, i32 0
store ptr %ap, ptr %sp, align 8
%spl = getelementptr inbounds { ptr }, ptr %st, i64 0, i32 0
%ld = load ptr, ptr %spl, align 8
%agg = insertvalue { ptr } poison, ptr %ld, 0
%m = extractvalue { ptr } %agg, 0
%dev = load ptr addrspace(1), ptr %m, align 8
%ev = getelementptr inbounds float, ptr addrspace(1) %dev, i64 0
%val = load float, ptr addrspace(1) %ev, align 4
store float %val, ptr addrspace(1) %out, align 4
ret void
}
";
let out = run(src);
assert!(!out.contains("alloca"), "all allocas promoted:\n{out}");
assert!(!out.contains("insertvalue"), "aggregate folded:\n{out}");
assert!(!out.contains("extractvalue"), "extract folded:\n{out}");
assert!(
out.contains("getelementptr inbounds float, ptr addrspace(1) %0"),
"device ptr resolved to %0:\n{out}"
);
}
#[test]
fn escaping_alloca_is_left_untouched() {
let src = "\
define void @k(ptr addrspace(1) %0, ptr %esc) {
%arr = alloca [1 x ptr addrspace(1)], align 8
%e0 = getelementptr inbounds [1 x ptr addrspace(1)], ptr %arr, i64 0, i64 0
store ptr addrspace(1) %0, ptr %e0, align 8
store ptr %e0, ptr %esc, align 8
ret void
}
";
let out = run(src);
assert!(out.contains("alloca"), "escaping alloca kept:\n{out}");
}
#[test]
fn multi_store_slot_is_not_forwarded() {
let src = "\
define void @k(ptr addrspace(1) %0, ptr addrspace(1) %1, ptr addrspace(1) %out) {
%s = alloca [1 x ptr addrspace(1)], align 8
%p = getelementptr inbounds [1 x ptr addrspace(1)], ptr %s, i64 0, i64 0
store ptr addrspace(1) %0, ptr %p, align 8
store ptr addrspace(1) %1, ptr %p, align 8
%v = load ptr addrspace(1), ptr %p, align 8
store ptr addrspace(1) %v, ptr %out, align 8
ret void
}
";
let out = run(src);
assert!(
out.contains("alloca"),
"multi-store alloca not promoted:\n{out}"
);
}
#[test]
fn subst_token_respects_word_boundary() {
let l = "%10 = add i32 %1, %11";
assert_eq!(subst_value_token(l, "%1", "%99"), "%10 = add i32 %99, %11");
}
#[test]
fn non_entry_dominating_store_forwards() {
let src = "\
define void @k(ptr addrspace(1) %tex, ptr addrspace(1) %out) {
br label %h
h:
%s = alloca { ptr addrspace(1) }, align 8
%p = getelementptr inbounds { ptr addrspace(1) }, ptr %s, i64 0, i32 0
store ptr addrspace(1) %tex, ptr %p, align 8
br label %use
use:
%pp = getelementptr inbounds { ptr addrspace(1) }, ptr %s, i64 0, i32 0
%dev = load ptr addrspace(1), ptr %pp, align 8
%w = call i32 @air.get_width_texture_2d(ptr addrspace(1) %dev, i32 0)
ret void
}
";
let out = run(src);
assert!(
!out.contains("= alloca") || !out.contains("store ptr addrspace(1) %tex"),
"non-entry dominating store should forward + drop:\n{out}"
);
assert!(
out.contains("@air.get_width_texture_2d(ptr addrspace(1) %tex"),
"load must resolve to %tex:\n{out}"
);
}
#[test]
fn non_dominating_store_is_not_forwarded() {
let src = "\
define void @k(ptr addrspace(1) %tex, i1 %c, ptr addrspace(1) %out) {
%s = alloca { ptr addrspace(1) }, align 8
br i1 %c, label %set, label %merge
set:
%p = getelementptr inbounds { ptr addrspace(1) }, ptr %s, i64 0, i32 0
store ptr addrspace(1) %tex, ptr %p, align 8
br label %merge
merge:
%pp = getelementptr inbounds { ptr addrspace(1) }, ptr %s, i64 0, i32 0
%dev = load ptr addrspace(1), ptr %pp, align 8
%w = call i32 @air.get_width_texture_2d(ptr addrspace(1) %dev, i32 0)
ret void
}
";
let out = run(src);
assert!(
out.contains("load ptr addrspace(1), ptr %pp"),
"non-dominating store must NOT be forwarded (byte-safety):\n{out}"
);
}
}