use crate::ir_inner::model::node::Node;
use crate::memory_model::MemoryOrdering;
use crate::validate::{err, ValidationError};
#[inline]
pub(crate) fn check_barrier(
divergent: bool,
ordering: MemoryOrdering,
errors: &mut Vec<ValidationError>,
) {
if divergent {
errors.push(err(
"V010: barrier may be reached by only part of a workgroup. Fix: move the barrier to uniform control flow."
.to_string(),
));
}
if !ordering.is_valid_for_barrier() {
errors.push(err(format!(
"V043: barrier uses memory ordering `{ordering:?}`, but barriers must synchronize memory. Fix: use Acquire, Release, AcqRel, or SeqCst; use no barrier at all for Relaxed."
)));
}
}
pub(crate) fn check_loop_back_edge(body: &[Node], errors: &mut Vec<ValidationError>) {
let mut steps: Vec<&Node> = Vec::new();
splice_straight_line(body, &mut steps);
if !steps.iter().any(|node| contains_barrier_anywhere(node)) {
return;
}
let Some(last_exit) = steps.iter().rposition(|node| can_return(node)) else {
return;
};
if steps[last_exit + 1..]
.iter()
.any(|node| matches!(node, Node::Barrier { .. }))
{
return;
}
errors.push(err(
"V055: an invocation can return from a synchronizing loop body after its last barrier, \
so the exit and the next iteration's writes are unordered across the back edge. One \
invocation can take the back edge and write while a sibling has not yet reached the \
exit; the sibling then leaves the kernel while the rest keep iterating, freezing the \
data it owns partway through. Nothing hangs, because a barrier does not count \
invocations that already returned, so this costs answers and not liveness, and one \
workgroup is enough to hit it. Fix: put a barrier after the early exit, as the last \
node of the loop body, so the exit is ordered against the back edge."
.to_string(),
));
}
fn splice_straight_line<'a>(nodes: &'a [Node], out: &mut Vec<&'a Node>) {
for node in nodes {
match node {
Node::Block(inner) => splice_straight_line(inner, out),
Node::Region { body, .. } => splice_straight_line(body, out),
other => out.push(other),
}
}
}
fn contains_barrier_anywhere(node: &Node) -> bool {
match node {
Node::Barrier { .. } => true,
Node::If {
then, otherwise, ..
} => {
then.iter().any(contains_barrier_anywhere)
|| otherwise.iter().any(contains_barrier_anywhere)
}
Node::Loop { body, .. } => body.iter().any(contains_barrier_anywhere),
Node::Block(nodes) => nodes.iter().any(contains_barrier_anywhere),
Node::Region { body, .. } => body.iter().any(contains_barrier_anywhere),
_ => false,
}
}
fn can_return(node: &Node) -> bool {
match node {
Node::Return => true,
Node::If {
then, otherwise, ..
} => then.iter().any(can_return) || otherwise.iter().any(can_return),
Node::Loop { body, .. } => body.iter().any(can_return),
Node::Block(nodes) => nodes.iter().any(can_return),
Node::Region { body, .. } => body.iter().any(can_return),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir_inner::model::expr::Expr;
#[test]
fn divergent_barrier_emits_v010() {
let mut errors = Vec::new();
check_barrier(true, MemoryOrdering::SeqCst, &mut errors);
assert_eq!(errors.len(), 1);
assert!(errors[0].message().contains("V010"));
}
#[test]
fn uniform_barrier_is_valid() {
let mut errors = Vec::new();
check_barrier(false, MemoryOrdering::SeqCst, &mut errors);
assert!(errors.is_empty());
}
#[test]
fn relaxed_barrier_is_rejected() {
let mut errors = Vec::new();
check_barrier(false, MemoryOrdering::Relaxed, &mut errors);
assert!(errors.iter().any(|error| error.message().contains("V043")));
}
fn barrier() -> Node {
Node::Barrier {
ordering: MemoryOrdering::SeqCst,
}
}
#[test]
fn exit_after_the_last_barrier_emits_v055() {
let mut errors = Vec::new();
check_loop_back_edge(
&[
barrier(),
Node::If {
cond: Expr::bool(true),
then: vec![Node::Return],
otherwise: Vec::new(),
},
],
&mut errors,
);
assert!(
errors.iter().any(|error| error.message().contains("V055")),
"an early exit after the loop's last barrier must be refused"
);
}
#[test]
fn a_barrier_after_the_exit_is_accepted() {
let mut errors = Vec::new();
check_loop_back_edge(
&[
barrier(),
Node::If {
cond: Expr::bool(true),
then: vec![Node::Return],
otherwise: Vec::new(),
},
barrier(),
],
&mut errors,
);
assert!(
errors.is_empty(),
"a barrier on the back edge discharges the obligation: {errors:?}"
);
}
#[test]
fn an_exit_in_a_loop_with_no_barrier_is_accepted() {
let mut errors = Vec::new();
check_loop_back_edge(
&[Node::If {
cond: Expr::bool(true),
then: vec![Node::Return],
otherwise: Vec::new(),
}],
&mut errors,
);
assert!(
errors.is_empty(),
"a loop with no barrier has no collective contract to break: {errors:?}"
);
}
#[test]
fn a_nested_exit_after_the_last_barrier_emits_v055() {
let mut errors = Vec::new();
check_loop_back_edge(
&[
barrier(),
Node::Loop {
var: "inner".into(),
from: Expr::u32(0),
to: Expr::u32(4),
body: vec![Node::If {
cond: Expr::bool(true),
then: vec![Node::Return],
otherwise: Vec::new(),
}],
},
],
&mut errors,
);
assert!(
errors.iter().any(|error| error.message().contains("V055")),
"a nested early exit still leaves the outer loop's barriers"
);
}
}
#[cfg(test)]
mod back_edge_depth_tests {
use super::*;
use crate::ir_inner::model::expr::Expr;
fn barrier() -> Node {
Node::Barrier {
ordering: MemoryOrdering::SeqCst,
}
}
fn exit_guard() -> Node {
Node::If {
cond: Expr::bool(true),
then: vec![Node::Return],
otherwise: Vec::new(),
}
}
#[test]
fn a_barrier_inside_a_block_still_makes_the_loop_collective() {
let mut errors = Vec::new();
check_loop_back_edge(&[Node::Block(vec![barrier()]), exit_guard()], &mut errors);
assert!(
errors.iter().any(|error| error.message().contains("V055")),
"a barrier nested in a Block must still trigger the back-edge check"
);
}
#[test]
fn a_guarding_barrier_inside_a_block_is_credited() {
let mut errors = Vec::new();
check_loop_back_edge(
&[barrier(), exit_guard(), Node::Block(vec![barrier()])],
&mut errors,
);
assert!(
errors.is_empty(),
"a Block executes unconditionally, so a barrier inside one orders the back edge: \
{errors:?}"
);
}
#[test]
fn a_guarding_barrier_inside_an_if_is_not_credited() {
let mut errors = Vec::new();
check_loop_back_edge(
&[
barrier(),
exit_guard(),
Node::If {
cond: Expr::bool(true),
then: vec![barrier()],
otherwise: Vec::new(),
},
],
&mut errors,
);
assert!(
errors.iter().any(|error| error.message().contains("V055")),
"a barrier only reached on one branch does not order the back edge"
);
}
#[test]
fn a_barrier_only_inside_a_nested_loop_triggers_but_never_guards() {
let nested = Node::Loop {
var: "inner".into(),
from: Expr::u32(0),
to: Expr::u32(0),
body: vec![barrier()],
};
let mut errors = Vec::new();
check_loop_back_edge(&[exit_guard(), nested], &mut errors);
assert!(
errors.iter().any(|error| error.message().contains("V055")),
"a nested-loop barrier makes the loop collective but cannot guard the back edge"
);
}
}