use crate::ir::Ident;
use crate::ir_inner::model::node::Node;
use crate::ir_inner::model::program::Program;
use crate::visit::bound_names::count_bound_names;
use rustc_hash::FxHashMap;
use smallvec::SmallVec;
use std::sync::Arc;
pub const DEFAULT_INLINE_THRESHOLD: usize = 64;
enum Staged {
FlatRegion(Vec<Node>),
Keep(Node),
}
#[must_use]
#[inline]
pub fn run(program: Program) -> Program {
run_with_threshold(program, DEFAULT_INLINE_THRESHOLD)
}
#[must_use]
pub fn run_with_threshold(program: Program, threshold: usize) -> Program {
program.map_entry(|owned_entry| {
let mut entry = Vec::with_capacity(owned_entry.len());
inline_nodes_into(owned_entry, threshold, &mut entry);
entry
})
}
fn inline_nodes_into(nodes: Vec<Node>, threshold: usize, out: &mut Vec<Node>) {
let mut staged: Vec<Staged> = Vec::with_capacity(nodes.len());
for node in nodes {
match node {
Node::Region {
body,
generator,
source_region,
} => {
let count = count_nodes_capped(&body, threshold);
let body_vec = match Arc::try_unwrap(body) {
Ok(v) => v,
Err(arc) => (*arc).clone(),
};
if count <= threshold {
let mut inlined = Vec::with_capacity(body_vec.len());
inline_nodes_into(body_vec, threshold, &mut inlined);
staged.push(Staged::FlatRegion(inlined));
} else {
let mut new_body = Vec::with_capacity(body_vec.len());
inline_nodes_into(body_vec, threshold, &mut new_body);
staged.push(Staged::Keep(Node::Region {
generator,
source_region,
body: Arc::new(new_body),
}));
}
}
Node::Block(children) => {
let mut new_children = Vec::with_capacity(children.len());
inline_nodes_into(children, threshold, &mut new_children);
staged.push(Staged::Keep(Node::Block(new_children)));
}
Node::Loop {
var,
from,
to,
body,
} => {
let mut new_body = Vec::with_capacity(body.len());
inline_nodes_into(body, threshold, &mut new_body);
staged.push(Staged::Keep(Node::Loop {
var,
from,
to,
body: new_body,
}));
}
Node::If {
cond,
then,
otherwise,
} => {
let mut new_then = Vec::with_capacity(then.len());
let mut new_otherwise = Vec::with_capacity(otherwise.len());
inline_nodes_into(then, threshold, &mut new_then);
inline_nodes_into(otherwise, threshold, &mut new_otherwise);
staged.push(Staged::Keep(Node::If {
cond,
then: new_then,
otherwise: new_otherwise,
}));
}
other => staged.push(Staged::Keep(other)),
}
}
let mut bound_counts: FxHashMap<Ident, usize> = FxHashMap::default();
for item in &staged {
match item {
Staged::FlatRegion(body) => count_bound_names(body, &mut bound_counts),
Staged::Keep(node) => count_bound_names(std::slice::from_ref(node), &mut bound_counts),
}
}
for item in staged {
match item {
Staged::FlatRegion(mut body) => {
let collides = body.iter().any(|node| match node {
Node::Let { name, .. } => {
bound_counts.get(name.as_str()).copied().unwrap_or(0) >= 2
}
_ => false,
});
if collides {
out.push(Node::Block(body));
} else {
out.append(&mut body);
}
}
Staged::Keep(node) => out.push(node),
}
}
}
fn count_nodes_capped(nodes: &[Node], threshold: usize) -> usize {
let cap = threshold.saturating_add(1);
let mut count = 0usize;
let mut stack: SmallVec<[&[Node]; 16]> = SmallVec::new();
stack.push(nodes);
while let Some(nodes) = stack.pop() {
for node in nodes {
count = count.saturating_add(1);
if count >= cap {
return cap;
}
match node {
Node::Block(children) | Node::Loop { body: children, .. } => {
stack.push(children);
}
Node::If {
then, otherwise, ..
} => {
stack.push(otherwise);
stack.push(then);
}
Node::Region { body, .. } => {
stack.push(body);
}
_ => {}
}
}
}
count
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::{BufferDecl, DataType, Expr, Program};
#[test]
fn small_region_inlines() {
let body = vec![Node::store("out", Expr::u32(0), Expr::u32(42))];
let region = Node::Region {
generator: "test".into(),
source_region: None,
body: std::sync::Arc::new(body),
};
let prog = Program::wrapped(
vec![BufferDecl::read_write("out", 0, DataType::U32)],
[1, 1, 1],
vec![region],
);
let optimized = run(prog);
assert!(
!matches!(&optimized.entry()[0], Node::Region { .. }),
"small Region must inline"
);
assert!(matches!(&optimized.entry()[0], Node::Store { .. }));
}
#[test]
fn large_region_stays_wrapped() {
let body: Vec<Node> = (0..100)
.map(|i| Node::store("out", Expr::u32(i), Expr::u32(i)))
.collect();
let region = Node::Region {
generator: "test".into(),
source_region: None,
body: std::sync::Arc::new(body),
};
let prog = Program::wrapped(
vec![BufferDecl::read_write("out", 0, DataType::U32)],
[1, 1, 1],
vec![region],
);
let optimized = run_with_threshold(prog, 64);
assert!(
matches!(&optimized.entry()[0], Node::Region { .. }),
"large Region must stay wrapped"
);
}
#[test]
fn generated_large_region_count_is_capped_at_inline_threshold() {
let body: Vec<Node> = (0..4096)
.map(|i| Node::store("out", Expr::u32(i), Expr::u32(i)))
.collect();
assert_eq!(
count_nodes_capped(&body, 64),
65,
"Fix: region-inline must stop counting once a generated body exceeds the inline threshold."
);
}
#[test]
fn nested_small_regions_all_inline() {
let inner = Node::Region {
generator: "inner".into(),
source_region: None,
body: std::sync::Arc::new(vec![Node::store("out", Expr::u32(0), Expr::u32(1))]),
};
let outer = Node::Region {
generator: "outer".into(),
source_region: None,
body: std::sync::Arc::new(vec![inner]),
};
let prog = Program::wrapped(
vec![BufferDecl::read_write("out", 0, DataType::U32)],
[1, 1, 1],
vec![outer],
);
let optimized = run(prog);
assert_eq!(optimized.entry().len(), 1);
assert!(matches!(&optimized.entry()[0], Node::Store { .. }));
}
#[test]
fn colliding_sibling_lets_are_each_block_scoped() {
let mk = |gen: &str| Node::Region {
generator: gen.into(),
source_region: None,
body: Arc::new(vec![
Node::let_bind("u_re_s1_b0_k0", Expr::u32(1)),
Node::store("out", Expr::u32(0), Expr::var("u_re_s1_b0_k0")),
]),
};
let prog = Program::wrapped(
vec![BufferDecl::read_write("out", 0, DataType::U32)],
[1, 1, 1],
vec![mk("fft_stage_a"), mk("fft_stage_b")],
);
let entry = run(prog).into_entry_vec();
assert!(
!entry.iter().any(|n| matches!(n, Node::Region { .. })),
"both small regions must inline, got {entry:?}"
);
assert!(
!entry
.iter()
.any(|n| matches!(n, Node::Let { name, .. } if name == "u_re_s1_b0_k0")),
"the shared name must not appear at top level (would collide), got {entry:?}"
);
let blocks: Vec<&Vec<Node>> = entry
.iter()
.filter_map(|n| match n {
Node::Block(b) => Some(b),
_ => None,
})
.collect();
assert_eq!(blocks.len(), 2, "each colliding sibling gets its own Block");
for block_body in blocks {
assert!(
matches!(&block_body[0], Node::Let { name, .. } if name == "u_re_s1_b0_k0"),
"each Block scopes one copy of the shared let, got {block_body:?}"
);
}
}
#[test]
fn regions_inside_loops_also_inline() {
let region = Node::Region {
generator: "inner".into(),
source_region: None,
body: std::sync::Arc::new(vec![Node::store("out", Expr::var("i"), Expr::u32(1))]),
};
let loop_node = Node::loop_for("i", Expr::u32(0), Expr::u32(4), vec![region]);
let prog = Program::wrapped(
vec![BufferDecl::read_write("out", 0, DataType::U32)],
[1, 1, 1],
vec![loop_node],
);
let optimized = run(prog);
let Node::Loop { body, .. } = &optimized.entry()[0] else {
panic!("expected Loop");
};
assert_eq!(body.len(), 1);
assert!(
matches!(&body[0], Node::Store { .. }),
"Region inside Loop must inline to just the Store"
);
}
#[test]
fn region_with_nested_sibling_binder_is_block_scoped() {
let region = Node::Region {
generator: "stage".into(),
source_region: None,
body: Arc::new(vec![
Node::let_bind("x", Expr::u32(1)),
Node::store("out", Expr::u32(0), Expr::var("x")),
]),
};
let sibling = Node::If {
cond: Expr::eq(Expr::u32(0), Expr::u32(0)),
then: vec![
Node::let_bind("x", Expr::u32(2)),
Node::store("out", Expr::u32(0), Expr::var("x")),
],
otherwise: vec![],
};
let mut out = Vec::new();
inline_nodes_into(vec![region, sibling], DEFAULT_INLINE_THRESHOLD, &mut out);
assert_eq!(out.len(), 2, "Region wrapped + If kept, got {out:?}");
let Node::Block(block_body) = &out[0] else {
panic!(
"flattened Region must be re-wrapped in a Block, got {:?}",
out[0]
);
};
assert!(
matches!(&block_body[0], Node::Let { name, .. } if name == "x"),
"the Block scopes the Region's `let x`, got {block_body:?}"
);
assert!(
matches!(&out[1], Node::If { .. }),
"sibling If preserved, got {:?}",
out[1]
);
assert!(
!out.iter()
.any(|n| matches!(n, Node::Let { name, .. } if name == "x")),
"no bare top-level `let x` may leak into the parent scope, got {out:?}"
);
}
}