use std::borrow::Cow;
use std::collections::BTreeSet;
use super::*;
use crate::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program, UnOp};
use crate::validate::fusion_safety::validate_fusion_alias_hazards;
use crate::validate::self_composition::validate_self_composition;
use proptest::prelude::*;
fn validate_with_options_legacy(
program: &Program,
options: ValidationOptions<'_>,
) -> ValidationReport {
let mut report = ValidationReport {
errors: Vec::with_capacity(program.buffers().len() + program.entry().len()),
warnings: Vec::new(),
trace: Vec::new(),
};
if let Some(message) = program.top_level_region_violation_cause() {
report.errors.push(err(
"V105",
ValidationPhase::Program,
ValidationLocation::Program,
message,
"construct runnable programs with Program::wrapped or add one top-level Region",
));
}
for (axis, &size) in program.workgroup_size.iter().enumerate() {
if size == 0 {
report.errors.push(err("V106", ValidationPhase::Program, ValidationLocation::WorkgroupAxis(axis as u8), format!("workgroup_size[{axis}] is 0"), format!("all workgroup dimensions must be >= 1.")));
}
}
let mut seen_names = FxHashSet::default();
let mut seen_bindings = FxHashSet::default();
for buf in program.buffers() {
if !seen_names.insert(&buf.name) {
report.errors.push(err("V107", ValidationPhase::Program, ValidationLocation::Buffer(Cow::Owned(buf.name.to_string())), format!(
"duplicate buffer name `{}`",
buf.name
), "each buffer must have a unique name"));
}
if buf.access != BufferAccess::Workgroup && !seen_bindings.insert(buf.binding) {
report.errors.push(err("V108", ValidationPhase::Program, ValidationLocation::Buffer(Cow::Owned(buf.name.to_string())), format!(
"duplicate binding slot {} (buffer `{}`)",
buf.binding, buf.name
), "each buffer must have a unique binding"));
}
if buf.access == BufferAccess::Workgroup && buf.count == 0 {
report.errors.push(err("V109", ValidationPhase::Program, ValidationLocation::Buffer(Cow::Owned(buf.name.to_string())), format!(
"workgroup buffer `{}` has count 0",
buf.name
), "declare a positive element count"));
}
validate_output_buffer_contract(buf, &mut report.errors);
}
validate_output_markers(program.buffers(), &mut report.errors);
let mut buffer_map: FxHashMap<&str, &crate::ir_inner::model::program::BufferDecl> =
FxHashMap::default();
buffer_map.reserve(program.buffers().len());
buffer_map.extend(program.buffers().iter().map(|b| (b.name.as_ref(), b)));
let mut scope = FxHashMap::default();
let mut limits = depth::LimitState::default();
nodes::validate_nodes(
program.entry(),
&buffer_map,
&mut scope,
false,
0,
&mut limits,
options,
&mut report,
);
validate_fusion_alias_hazards(program.entry(), &mut report.errors);
validate_self_composition(program.entry(), &mut report.errors);
report
}
use crate::transform::visit::{
fixtures::arb_program, walk_nodes_and_exprs, ExprVisitor, NodeVisitor,
};
#[derive(Default)]
struct BufferNamesReached(BTreeSet<String>);
impl NodeVisitor for BufferNamesReached {
fn visit_node(&mut self, node: &Node) {
let mut record = |name: &crate::ir::Ident| {
self.0.insert(name.as_str().to_string());
};
match node {
Node::Store { buffer, .. }
| Node::AllReduce { buffer, .. }
| Node::Broadcast { buffer, .. } => record(buffer),
Node::IndirectDispatch { count_buffer, .. } => record(count_buffer),
Node::AsyncLoad {
source,
destination,
..
}
| Node::AsyncStore {
source,
destination,
..
}
| Node::AllGather {
input: source,
output: destination,
..
}
| Node::ReduceScatter {
input: source,
output: destination,
..
} => {
record(source);
record(destination);
}
_ => {}
}
}
}
impl ExprVisitor for BufferNamesReached {
fn visit_expr(&mut self, expr: &Expr) {
match expr {
Expr::Load { buffer, .. }
| Expr::BufLen { buffer }
| Expr::BufferRef { buffer }
| Expr::Atomic { buffer, .. } => {
self.0.insert(buffer.as_str().to_string());
}
_ => {}
}
}
}
fn unknown_buffer_name(message: &str) -> Option<String> {
let tail = message.split("unknown buffer `").nth(1)?;
tail.split('`').next().map(str::to_string)
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 128,
..ProptestConfig::default()
})]
#[test]
fn validator_walk_reaches_the_same_buffers_as_the_public_visitor(program in arb_program()) {
let probe = program.with_rewritten_buffers(Vec::new());
let mut by_visitor = BufferNamesReached::default();
walk_nodes_and_exprs(&probe, &mut by_visitor);
let report = validate_with_options(&probe, ValidationOptions::default());
let by_validator: BTreeSet<String> = report
.errors
.iter()
.filter_map(|issue| unknown_buffer_name(&issue.message()))
.collect();
prop_assert_eq!(by_visitor.0, by_validator);
}
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 50,
..ProptestConfig::default()
})]
#[test]
fn single_pass_validator_matches_legacy(program in arb_program()) {
let legacy = validate_with_options_legacy(&program, ValidationOptions::default());
let modern = validate_with_options(&program, ValidationOptions::default());
let mut legacy_errors = legacy.errors;
let mut modern_errors = modern.errors;
legacy_errors.sort_by(|a, b| a.message().cmp(&b.message()));
modern_errors.sort_by(|a, b| a.message().cmp(&b.message()));
for issue in &mut legacy_errors {
issue.set_location(ValidationLocation::Program);
}
for issue in &mut modern_errors {
issue.set_location(ValidationLocation::Program);
}
prop_assert_eq!(
legacy_errors, modern_errors,
"error mismatch between legacy and single-pass validator"
);
let mut legacy_warnings = legacy.warnings;
let mut modern_warnings = modern.warnings;
legacy_warnings.sort_by(|a, b| a.message.cmp(&b.message));
modern_warnings.sort_by(|a, b| a.message.cmp(&b.message));
prop_assert_eq!(
legacy_warnings, modern_warnings,
"warning mismatch between legacy and single-pass validator"
);
}
}
#[test]
fn call_result_binding_unknown_type_does_not_produce_false_v045() {
let program = Program::wrapped(
vec![BufferDecl::output("out", 0, DataType::F32).with_count(1)],
[1, 1, 1],
vec![
Node::Let {
name: "x".into(),
value: Expr::Call {
op_id: "unknown.dialect.op".into(),
args: vec![],
},
},
Node::Assign {
name: "x".into(),
value: Expr::LitF32(1.0),
},
Node::Store {
buffer: "out".into(),
index: Expr::u32(0),
value: Expr::var("x"),
},
],
);
let report = validate_with_options(&program, ValidationOptions::default());
let v045: Vec<_> = report
.errors
.iter()
.filter(|e| e.code().as_str() == "V045")
.collect();
assert!(
v045.is_empty(),
"false V045 fired on call-result binding with unknown type: {:?}",
v045
);
assert!(
report.errors.iter().any(|e| e.code().as_str() == "V016"),
"expected V016 for call with no lookup, got: {:?}",
report.errors
);
}
#[test]
fn fma_f32_violations_flags_integer_fma_with_actionable_message() {
let program = Program::wrapped(
vec![BufferDecl::output("out", 0, DataType::U32)],
[1, 1, 1],
vec![Node::let_bind(
"bad_fma",
Expr::Fma {
a: Box::new(Expr::u32(1)),
b: Box::new(Expr::u32(2)),
c: Box::new(Expr::u32(3)),
},
)],
);
let violations = fma_f32_violations(&program);
assert_eq!(
violations.len(),
3,
"every non-f32 Fma operand (a, b, c) must be reported, got: {violations:?}"
);
for violation in &violations {
assert!(
violation.code().as_str() == "V028",
"fma_f32_violations must only return V028 errors, got: {}",
violation.message()
);
assert!(
violation.message().contains("Fma requires three f32 operands")
&& violation.message().contains("must be `f32`")
&& violation.message().contains("Fix:"),
"V028 message must name the f32 contract and a fix, got: {}",
violation.message()
);
}
}
#[test]
fn fma_f32_violations_empty_for_all_f32_operands() {
let program = Program::wrapped(
vec![BufferDecl::output("out", 0, DataType::F32).with_count(1)],
[1, 1, 1],
vec![Node::store(
"out",
Expr::u32(0),
Expr::Fma {
a: Box::new(Expr::LitF32(2.0)),
b: Box::new(Expr::LitF32(3.0)),
c: Box::new(Expr::LitF32(4.0)),
},
)],
);
assert!(
fma_f32_violations(&program).is_empty(),
"f32 Fma is valid and must not be flagged"
);
}
#[test]
fn fma_f32_violations_ignores_unrelated_validation_errors() {
let program = Program::wrapped(
vec![BufferDecl::output("out", 0, DataType::U32)],
[0, 1, 1],
Vec::new(),
);
assert!(
!validate(&program).is_empty(),
"zero workgroup dimension must be a validation error (guards the test premise)"
);
assert!(
fma_f32_violations(&program).is_empty(),
"non-Fma validation errors must be filtered out by fma_f32_violations"
);
}
#[test]
fn validate_recognizes_integer_unpack_ops() {
let program = Program::wrapped(
vec![BufferDecl::output("out", 0, DataType::U32).with_count(1)],
[1, 1, 1],
vec![Node::store(
"out",
Expr::u32(0),
Expr::UnOp {
op: UnOp::Unpack8High,
operand: Box::new(Expr::u32(0xDEAD_BEEF)),
},
)],
);
let errors = validate(&program);
assert!(
!errors.iter().any(|e| e.message().contains("is not recognized")),
"integer unpack op must be recognized, got: {errors:?}"
);
assert!(
!errors.iter().any(|e| e.message().contains("unpack ops require")),
"a u32 operand is valid for unpack ops, got: {errors:?}"
);
}
#[test]
fn validate_rejects_non_integer_unpack_operand_on_type_not_existence() {
let program = Program::wrapped(
vec![BufferDecl::output("out", 0, DataType::U32).with_count(1)],
[1, 1, 1],
vec![Node::store(
"out",
Expr::u32(0),
Expr::UnOp {
op: UnOp::Unpack4Low,
operand: Box::new(Expr::LitF32(1.5)),
},
)],
);
let errors = validate(&program);
assert!(
errors.iter().any(|e| e
.message()
.contains("unpack ops require a 32-bit integer")
&& e.message().contains("Fix:")),
"f32 unpack operand must be rejected with the integer-word contract, got: {errors:?}"
);
assert!(
!errors.iter().any(|e| e.message().contains("is not recognized")),
"unpack op must be rejected on operand type, not treated as unrecognized, got: {errors:?}"
);
}
#[test]
fn store_signed_remainder_into_i32_buffer_validates() {
let program = Program::wrapped(
vec![
BufferDecl::output("out", 0, DataType::I32).with_count(4),
BufferDecl::read("a", 1, DataType::I32).with_count(4),
BufferDecl::read("b", 2, DataType::I32).with_count(4),
],
[1, 1, 1],
vec![Node::store(
"out",
Expr::u32(0),
Expr::rem(Expr::load("a", Expr::u32(0)), Expr::load("b", Expr::u32(0))),
)],
);
let errors = validate(&program);
assert!(
!errors.iter().any(|e| e.code().as_str() == "V045"
|| e.message().contains("value has type")),
"store of a same-width int (rem result, u32-typed) into an i32 buffer must \
validate (bit-exact reinterpret), got: {errors:?}"
);
}
#[test]
fn store_signed_div_into_u32_buffer_validates() {
let program = Program::wrapped(
vec![
BufferDecl::output("out", 0, DataType::U32).with_count(4),
BufferDecl::read("a", 1, DataType::I32).with_count(4),
BufferDecl::read("b", 2, DataType::I32).with_count(4),
],
[1, 1, 1],
vec![Node::store(
"out",
Expr::u32(0),
Expr::div(Expr::load("a", Expr::u32(0)), Expr::load("b", Expr::u32(0))),
)],
);
let errors = validate(&program);
assert!(
!errors.iter().any(|e| e.code().as_str() == "V045"
|| e.message().contains("value has type")),
"store of an i32-typed value into a u32 buffer must validate, got: {errors:?}"
);
}
#[test]
fn store_float_into_int_buffer_still_rejected() {
let program = Program::wrapped(
vec![
BufferDecl::output("out", 0, DataType::I32).with_count(4),
BufferDecl::read("f", 1, DataType::F32).with_count(4),
],
[1, 1, 1],
vec![Node::store(
"out",
Expr::u32(0),
Expr::load("f", Expr::u32(0)),
)],
);
let errors = validate(&program);
assert!(
errors.iter().any(|e| e.message().contains("Node::Store")
&& e.message().contains("element type")),
"storing an f32 value into an i32 buffer must still be rejected (no int/float \
coercion), got: {errors:?}"
);
}
#[test]
fn assign_signed_remainder_to_i32_buffer_validates() {
let program = Program::wrapped(
vec![
BufferDecl::read_write("buf", 0, DataType::I32).with_count(4),
BufferDecl::read("a", 1, DataType::I32).with_count(4),
BufferDecl::read("b", 2, DataType::I32).with_count(4),
],
[1, 1, 1],
vec![Node::assign(
"buf",
Expr::rem(Expr::load("a", Expr::u32(0)), Expr::load("b", Expr::u32(0))),
)],
);
let errors = validate(&program);
assert!(
!errors.iter().any(|e| e.code().as_str() == "V045"),
"assigning a same-width int (rem result) to an i32 buffer must validate, got: {errors:?}"
);
}
#[test]
fn store_bool_comparison_result_into_u32_buffer_validates() {
let program = Program::wrapped(
vec![
BufferDecl::output("out", 0, DataType::U32).with_count(4),
BufferDecl::read("a", 1, DataType::I32).with_count(4),
BufferDecl::read("b", 2, DataType::I32).with_count(4),
],
[1, 1, 1],
vec![Node::store(
"out",
Expr::u32(0),
Expr::lt(Expr::load("a", Expr::u32(0)), Expr::load("b", Expr::u32(0))),
)],
);
let errors = validate(&program);
assert!(
!errors.iter().any(|e| e.code().as_str() == "V045"
|| e.message().contains("value has type")),
"storing a bool comparison result into a u32 buffer must validate, got: {errors:?}"
);
}