use vyre_foundation::ir::{
inline_calls_with_resolver, AtomicOp, BufferAccess, BufferDecl, DataType, Expr, Node, Program,
};
use vyre_foundation::memory_model::MemoryOrdering;
fn table_lookup_callee() -> Program {
Program::wrapped(
vec![
BufferDecl::storage("table", 0, BufferAccess::ReadOnly, DataType::U32).with_count(16),
BufferDecl::storage("i", 1, BufferAccess::ReadOnly, DataType::U32).with_count(1),
BufferDecl::output("result", 2, DataType::U32).with_count(1),
],
[1, 1, 1],
vec![Node::store(
"result",
Expr::u32(0),
Expr::Load {
buffer: "table".into(),
index: Box::new(Expr::add(
Expr::Load {
buffer: "i".into(),
index: Box::new(Expr::u32(0)),
},
Expr::u32(3),
)),
},
)],
)
}
fn resolver(op_id: &str) -> Option<Program> {
match op_id {
"table_lookup" => Some(table_lookup_callee()),
"table_len" => Some(buflen_callee()),
"table_bump" => Some(atomic_callee()),
_ => None,
}
}
fn buflen_callee() -> Program {
Program::wrapped(
vec![
BufferDecl::storage("table", 0, BufferAccess::ReadOnly, DataType::U32).with_count(16),
BufferDecl::output("result", 1, DataType::U32).with_count(1),
],
[1, 1, 1],
vec![Node::store(
"result",
Expr::u32(0),
Expr::BufLen {
buffer: "table".into(),
},
)],
)
}
fn atomic_callee() -> Program {
Program::wrapped(
vec![
BufferDecl::storage("table", 0, BufferAccess::ReadOnly, DataType::U32).with_count(16),
BufferDecl::output("result", 1, DataType::U32).with_count(1),
],
[1, 1, 1],
vec![Node::store(
"result",
Expr::u32(0),
Expr::Atomic {
op: AtomicOp::Add,
buffer: "table".into(),
index: Box::new(Expr::u32(0)),
expected: None,
value: Box::new(Expr::u32(1)),
ordering: MemoryOrdering::Relaxed,
},
)],
)
}
fn caller_calling(op_id: &str, args: Vec<Expr>) -> Program {
Program::wrapped(
vec![
BufferDecl::storage("data", 0, BufferAccess::ReadOnly, DataType::U32).with_count(64),
BufferDecl::storage("idx", 1, BufferAccess::ReadOnly, DataType::U32).with_count(1),
BufferDecl::output("out", 2, DataType::U32).with_count(1),
],
[1, 1, 1],
vec![Node::store(
"out",
Expr::u32(0),
Expr::Call {
op_id: op_id.into(),
args,
},
)],
)
}
fn loads(program: &Program) -> Vec<(String, String)> {
let mut found = Vec::new();
walk_nodes(program.entry(), &mut |expr| {
if let Expr::Load { buffer, index } = expr {
found.push((buffer.to_string(), format!("{index:?}")));
}
});
found
}
fn atomic_buffers(program: &Program) -> Vec<String> {
let mut found = Vec::new();
walk_nodes(program.entry(), &mut |expr| {
if let Expr::Atomic { buffer, .. } = expr {
found.push(buffer.to_string());
}
});
found
}
fn buflens(program: &Program) -> Vec<String> {
let mut found = Vec::new();
walk_nodes(program.entry(), &mut |expr| {
if let Expr::BufLen { buffer } = expr {
found.push(buffer.to_string());
}
});
found
}
fn walk_nodes(nodes: &[Node], visit: &mut impl FnMut(&Expr)) {
for node in nodes {
match node {
Node::Let { value, .. } | Node::Assign { value, .. } => walk_expr(value, visit),
Node::Store { index, value, .. } => {
walk_expr(index, visit);
walk_expr(value, visit);
}
Node::If {
cond,
then,
otherwise,
} => {
walk_expr(cond, visit);
walk_nodes(then, visit);
walk_nodes(otherwise, visit);
}
Node::Loop { from, to, body, .. } => {
walk_expr(from, visit);
walk_expr(to, visit);
walk_nodes(body, visit);
}
Node::Block(inner) => walk_nodes(inner, visit),
Node::Region { body, .. } => walk_nodes(body, visit),
_ => {}
}
}
}
fn walk_expr(expr: &Expr, visit: &mut impl FnMut(&Expr)) {
visit(expr);
match expr {
Expr::Load { index, .. } => walk_expr(index, visit),
Expr::BinOp { left, right, .. } => {
walk_expr(left, visit);
walk_expr(right, visit);
}
Expr::UnOp { operand, .. } => walk_expr(operand, visit),
Expr::Cast { value, .. } => walk_expr(value, visit),
Expr::Fma { a, b, c } => {
walk_expr(a, visit);
walk_expr(b, visit);
walk_expr(c, visit);
}
Expr::Select {
cond,
true_val,
false_val,
} => {
walk_expr(cond, visit);
walk_expr(true_val, visit);
walk_expr(false_val, visit);
}
Expr::Atomic {
index,
expected,
value,
..
} => {
walk_expr(index, visit);
if let Some(expected) = expected {
walk_expr(expected, visit);
}
walk_expr(value, visit);
}
Expr::Call { args, .. } => {
for arg in args {
walk_expr(arg, visit);
}
}
_ => {}
}
}
#[test]
fn buffer_argument_retargets_the_load_and_preserves_the_index() {
let caller = caller_calling(
"table_lookup",
vec![
Expr::BufferRef {
buffer: "data".into(),
},
Expr::Load {
buffer: "idx".into(),
index: Box::new(Expr::u32(0)),
},
],
);
let inlined = inline_calls_with_resolver(&caller, resolver).expect("inline");
let loads = loads(&inlined);
let data_loads: Vec<&(String, String)> =
loads.iter().filter(|(buf, _)| buf == "data").collect();
assert_eq!(
data_loads.len(),
1,
"expected exactly one retargeted load of `data`, got {loads:?}"
);
let index = &data_loads[0].1;
assert!(
index.contains("LitU32(3)"),
"the callee's `+ 3` offset must survive retargeting, got index {index}"
);
assert!(
index.contains("idx"),
"the scalar argument must be substituted into the index, got index {index}"
);
}
#[test]
fn the_callees_parameter_buffer_never_leaks_into_the_caller() {
let caller = caller_calling(
"table_lookup",
vec![
Expr::BufferRef {
buffer: "data".into(),
},
Expr::Load {
buffer: "idx".into(),
index: Box::new(Expr::u32(0)),
},
],
);
let inlined = inline_calls_with_resolver(&caller, resolver).expect("inline");
for (buffer, _) in loads(&inlined) {
assert!(
buffer == "data" || buffer == "idx",
"inlined program reads callee-local buffer `{buffer}`; only the caller's own buffers may survive"
);
}
let names: Vec<String> = inlined
.buffers()
.iter()
.map(|b| b.name().to_string())
.collect();
assert_eq!(
names,
vec!["data".to_string(), "idx".to_string(), "out".to_string()],
"inlining must not add the callee's buffers to the caller"
);
}
#[test]
fn scalar_argument_still_substitutes_the_value_rather_than_retargeting() {
let caller = caller_calling(
"table_lookup",
vec![
Expr::BufferRef {
buffer: "data".into(),
},
Expr::u32(7),
],
);
let inlined = inline_calls_with_resolver(&caller, resolver).expect("inline");
let loads = loads(&inlined);
assert_eq!(
loads.len(),
1,
"the scalar argument must not produce a load, got {loads:?}"
);
assert_eq!(loads[0].0, "data");
assert!(
loads[0].1.contains("LitU32(7)"),
"the literal argument must appear in the index, got {}",
loads[0].1
);
}
#[test]
fn buflen_of_a_buffer_argument_becomes_the_caller_buffers_length() {
let caller = caller_calling(
"table_len",
vec![Expr::BufferRef {
buffer: "data".into(),
}],
);
let inlined = inline_calls_with_resolver(&caller, resolver).expect("inline");
assert_eq!(
buflens(&inlined),
vec!["data".to_string()],
"BufLen must retarget at the caller's buffer, not collapse to a literal"
);
}
#[test]
fn buflen_of_a_scalar_argument_stays_one() {
let caller = caller_calling("table_len", vec![Expr::u32(7)]);
let inlined = inline_calls_with_resolver(&caller, resolver).expect("inline");
assert!(
buflens(&inlined).is_empty(),
"a scalar argument has length 1 and must fold, got {:?}",
buflens(&inlined)
);
let dump = format!("{:?}", inlined.entry());
assert!(
dump.contains("LitU32(1)"),
"the folded length literal 1 must appear in the inlined program"
);
}
#[test]
fn atomic_on_a_buffer_argument_retargets_to_the_caller_buffer() {
let caller = caller_calling(
"table_bump",
vec![Expr::BufferRef {
buffer: "data".into(),
}],
);
let inlined = inline_calls_with_resolver(&caller, resolver).expect("inline");
assert_eq!(
atomic_buffers(&inlined),
vec!["data".to_string()],
"the atomic must retarget at the caller's buffer"
);
}
#[test]
fn a_buffer_reference_outside_a_call_argument_is_rejected() {
let program = Program::wrapped(
vec![
BufferDecl::storage("data", 0, BufferAccess::ReadOnly, DataType::U32).with_count(64),
BufferDecl::output("out", 1, DataType::U32).with_count(1),
],
[1, 1, 1],
vec![Node::store(
"out",
Expr::u32(0),
Expr::BufferRef {
buffer: "data".into(),
},
)],
);
let report = vyre_foundation::validate::validate(&program);
assert!(
report.iter().any(|e| e.to_string().contains("V051")),
"storing a buffer reference must raise V051, got {:?}",
report
);
}