//! Tests for the WGSL front end. #![cfg(feature = "wgsl-in")] fn check(input: &str, snapshot: &str) { let output = naga::front::wgsl::parse_str(input) .expect_err("expected parser error") .emit_to_string(input); if output != snapshot { for diff in diff::lines(&output, snapshot) { match diff { diff::Result::Left(l) => println!("-{}", l), diff::Result::Both(l, _) => println!(" {}", l), diff::Result::Right(r) => println!("+{}", r), } } panic!("Error snapshot failed"); } } #[test] fn function_without_identifier() { check( "fn () {}", r###"error: expected identifier, found '(' ┌─ wgsl:1:4 │ 1 │ fn () {} │ ^ expected identifier "###, ); } #[test] fn invalid_integer() { check( "fn foo([location(1.)] x: i32) {}", r###"error: expected identifier, found '[' ┌─ wgsl:1:8 │ 1 │ fn foo([location(1.)] x: i32) {} │ ^ expected identifier "###, ); } #[test] fn invalid_float() { check( "let scale: f32 = 1.1.;", r###"error: expected ';', found '.' ┌─ wgsl:1:21 │ 1 │ let scale: f32 = 1.1.; │ ^ expected ';' "###, ); } #[test] fn invalid_texture_sample_type() { check( "let x: texture_2d;", r###"error: texture sample type must be one of f32, i32 or u32, but found f16 ┌─ wgsl:1:19 │ 1 │ let x: texture_2d; │ ^^^ must be one of f32, i32 or u32 "###, ); } #[test] fn unknown_identifier() { check( r###" fn f(x: f32) -> f32 { return x * schmoo; } "###, r###"error: no definition in scope for identifier: 'schmoo' ┌─ wgsl:3:30 │ 3 │ return x * schmoo; │ ^^^^^^ unknown identifier "###, ); } #[test] fn negative_index() { check( r#" fn main() -> f32 { let a = array(0., 1., 2.); return a[-1]; } "#, r#"error: expected unsigned integer constant expression, found `-1` ┌─ wgsl:4:26 │ 4 │ return a[-1]; │ ^^ expected unsigned integer "#, ); } #[test] fn bad_texture() { check( r#" [[group(0), binding(0)]] var sampler : sampler; [[stage(fragment)]] fn main() -> [[location(0)]] vec4 { let a = 3; return textureSample(a, sampler, vec2(0.0)); } "#, r#"error: expected an image, but found 'a' which is not an image ┌─ wgsl:7:38 │ 7 │ return textureSample(a, sampler, vec2(0.0)); │ ^ not an image "#, ); } #[test] fn bad_type_cast() { check( r#" fn x() -> i32 { return i32(vec2(0.0)); } "#, r#"error: cannot cast a vec2 to a i32 ┌─ wgsl:3:27 │ 3 │ return i32(vec2(0.0)); │ ^^^^^^^^^^^^^^^^ cannot cast a vec2 to a i32 "#, ); } #[test] fn bad_texture_sample_type() { check( r#" [[group(0), binding(0)]] var sampler : sampler; [[group(0), binding(1)]] var texture : texture_2d; [[stage(fragment)]] fn main() -> [[location(0)]] vec4 { return textureSample(texture, sampler, vec2(0.0)); } "#, r#"error: texture sample type must be one of f32, i32 or u32, but found bool ┌─ wgsl:3:63 │ 3 │ [[group(0), binding(1)]] var texture : texture_2d; │ ^^^^ must be one of f32, i32 or u32 "#, ); } #[test] fn bad_for_initializer() { check( r#" fn x() { for ({};;) {} } "#, r#"error: for(;;) initializer is not an assignment or a function call: '{}' ┌─ wgsl:3:22 │ 3 │ for ({};;) {} │ ^^ not an assignment or function call "#, ); } #[test] fn unknown_storage_class() { check( r#" [[group(0), binding(0)]] var texture: texture_2d; "#, r#"error: unknown storage class: 'bad' ┌─ wgsl:2:42 │ 2 │ [[group(0), binding(0)]] var texture: texture_2d; │ ^^^ unknown storage class "#, ); } #[test] fn unknown_attribute() { check( r#" [[a]] fn x() {} "#, r#"error: unknown attribute: 'a' ┌─ wgsl:2:15 │ 2 │ [[a]] │ ^ unknown attribute "#, ); } #[test] fn unknown_built_in() { check( r#" fn x([[builtin(unknown_built_in)]] y: u32) {} "#, r#"error: unknown builtin: 'unknown_built_in' ┌─ wgsl:2:28 │ 2 │ fn x([[builtin(unknown_built_in)]] y: u32) {} │ ^^^^^^^^^^^^^^^^ unknown builtin "#, ); } #[test] fn unknown_access() { check( r#" var x: array; "#, r#"error: unknown access: 'unknown_access' ┌─ wgsl:2:25 │ 2 │ var x: array; │ ^^^^^^^^^^^^^^ unknown access "#, ); } #[test] fn unknown_shader_stage() { check( r#" [[stage(geometry)]] fn main() {} "#, r#"error: unknown shader stage: 'geometry' ┌─ wgsl:2:21 │ 2 │ [[stage(geometry)]] fn main() {} │ ^^^^^^^^ unknown shader stage "#, ); } #[test] fn unknown_ident() { check( r#" fn main() { let a = b; } "#, r#"error: no definition in scope for identifier: 'b' ┌─ wgsl:3:25 │ 3 │ let a = b; │ ^ unknown identifier "#, ); } #[test] fn unknown_scalar_type() { check( r#" let a: vec2; "#, r#"error: unknown scalar type: 'something' ┌─ wgsl:2:25 │ 2 │ let a: vec2; │ ^^^^^^^^^ unknown scalar type │ = note: Valid scalar types are f16, f32, f64, i8, i16, i32, i64, u8, u16, u32, u64, bool "#, ); } #[test] fn unknown_type() { check( r#" let a: Vec; "#, r#"error: unknown type: 'Vec' ┌─ wgsl:2:20 │ 2 │ let a: Vec; │ ^^^ unknown type "#, ); } #[test] fn unknown_storage_format() { check( r#" let storage: texture_storage_1d; "#, r#"error: unknown storage format: 'rgba' ┌─ wgsl:2:45 │ 2 │ let storage: texture_storage_1d; │ ^^^^ unknown storage format "#, ); } #[test] fn unknown_conservative_depth() { check( r#" [[early_depth_test(abc)]] fn main() {} "#, r#"error: unknown conservative depth: 'abc' ┌─ wgsl:2:32 │ 2 │ [[early_depth_test(abc)]] fn main() {} │ ^^^ unknown conservative depth "#, ); } #[test] fn zero_array_stride() { check( r#" type zero = [[stride(0)]] array; "#, r#"error: array stride must not be zero ┌─ wgsl:2:34 │ 2 │ type zero = [[stride(0)]] array; │ ^ array stride must not be zero "#, ); } #[test] fn struct_member_zero_size() { check( r#" struct Bar { [[size(0)]] data: array; }; "#, r#"error: struct member size or alignment must not be 0 ┌─ wgsl:3:24 │ 3 │ [[size(0)]] data: array; │ ^ struct member size or alignment must not be 0 "#, ); } #[test] fn struct_member_zero_align() { check( r#" struct Bar { [[align(0)]] data: array; }; "#, r#"error: struct member size or alignment must not be 0 ┌─ wgsl:3:25 │ 3 │ [[align(0)]] data: array; │ ^ struct member size or alignment must not be 0 "#, ); } #[test] fn inconsistent_binding() { check( r#" fn foo([[builtin(vertex_index), location(0)]] x: u32) {} "#, r#"error: input/output binding is not consistent ┌─ wgsl:2:16 │ 2 │ fn foo([[builtin(vertex_index), location(0)]] x: u32) {} │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ input/output binding is not consistent "#, ); } #[test] fn unknown_local_function() { check( r#" fn x() { for (a();;) {} } "#, r#"error: unknown local function `a` ┌─ wgsl:3:22 │ 3 │ for (a();;) {} │ ^ unknown local function "#, ); } #[test] fn let_type_mismatch() { check( r#" let x: i32 = 1.0; "#, r#"error: the type of `x` is expected to be [1] ┌─ wgsl:2:17 │ 2 │ let x: i32 = 1.0; │ ^ definition of `x` "#, ); } #[test] fn local_var_type_mismatch() { check( r#" fn foo() { var x: f32 = 1; } "#, r#"error: the type of `x` is expected to be [1] ┌─ wgsl:3:21 │ 3 │ var x: f32 = 1; │ ^ definition of `x` "#, ); } #[test] fn local_var_missing_type() { check( r#" fn foo() { var x; } "#, r#"error: variable `x` needs a type ┌─ wgsl:3:21 │ 3 │ var x; │ ^ definition of `x` "#, ); } #[test] fn postfix_pointers() { check( r#" fn main() { var v: vec4 = vec4(1.0, 1.0, 1.0, 1.0); let pv = &v; let a = *pv[3]; // Problematic line } "#, r#"error: the value indexed by a `[]` subscripting expression must not be a pointer ┌─ wgsl:5:26 │ 5 │ let a = *pv[3]; // Problematic line │ ^^ expression is a pointer "#, ); check( r#" struct S { m: i32; }; fn main() { var s: S = S(42); let ps = &s; let a = *ps.m; // Problematic line } "#, r#"error: the value accessed by a `.member` expression must not be a pointer ┌─ wgsl:6:26 │ 6 │ let a = *ps.m; // Problematic line │ ^^ expression is a pointer "#, ); } macro_rules! check_validation_error { // We want to support an optional guard expression after the pattern, so // that we can check values we can't match against, like strings. // Unfortunately, we can't simply include `$( if $guard:expr )?` in the // pattern, because Rust treats `?` as a repetition operator, and its count // (0 or 1) will not necessarily match `$source`. ( $( $source:literal ),* : $pattern:pat ) => { check_validation_error!( @full $( $source ),* : $pattern if true ; ""); }; ( $( $source:literal ),* : $pattern:pat if $guard:expr ) => { check_validation_error!( @full $( $source ),* : $pattern if $guard ; stringify!( $guard ) ); }; ( @full $( $source:literal ),* : $pattern:pat if $guard:expr ; $guard_string:expr ) => { $( let error = validation_error($source); if ! matches!(&error, $pattern if $guard) { eprintln!("validation error does not match pattern:\n\ source code: {}\n\ \n\ actual result:\n\ {:#?}\n\ \n\ expected match for pattern:\n\ {}{}", stringify!($source), error, stringify!($pattern), $guard_string); panic!("validation error does not match pattern"); } )* }; } fn validation_error(source: &str) -> Result { let module = match naga::front::wgsl::parse_str(source) { Ok(module) => module, Err(err) => { eprintln!("WGSL parse failed:"); panic!("{}", err.emit_to_string(source)); } }; naga::valid::Validator::new( naga::valid::ValidationFlags::all(), naga::valid::Capabilities::empty(), ) .validate(&module) .map_err(|e| e.into_inner()) // TODO: Add tests for spans, too? } #[test] fn invalid_arrays() { check_validation_error! { "type Bad = array, 4>;", "type Bad = array;", "type Bad = array, 4>;": Err(naga::valid::ValidationError::Type { error: naga::valid::TypeError::InvalidArrayBaseType(_), .. }) } check_validation_error! { r#" [[block]] struct Block { value: f32; }; type Bad = array; "#: Err(naga::valid::ValidationError::Type { error: naga::valid::TypeError::NestedTopLevel, .. }) } check_validation_error! { r#" type Bad = [[stride(2)]] array; "#: Err(naga::valid::ValidationError::Type { error: naga::valid::TypeError::InsufficientArrayStride { stride: 2, base_size: 4 }, .. }) } check_validation_error! { "type Bad = array;", r#" let length: f32 = 2.718; type Bad = array; "#: Err(naga::valid::ValidationError::Type { error: naga::valid::TypeError::InvalidArraySizeConstant(_), .. }) } check_validation_error! { "type Bad = array;", "type Bad = array;": Err(naga::valid::ValidationError::Type { error: naga::valid::TypeError::NonPositiveArrayLength(_), .. }) } } #[test] fn invalid_structs() { check_validation_error! { "struct Bad { data: sampler; };", "struct Bad { data: texture_2d; };": Err(naga::valid::ValidationError::Type { error: naga::valid::TypeError::InvalidData(_), .. }) } check_validation_error! { "struct Bad { data: array; other: f32; };": Err(naga::valid::ValidationError::Type { error: naga::valid::TypeError::InvalidDynamicArray(_, _), .. }) } } #[test] fn invalid_functions() { check_validation_error! { "fn unacceptable_unsized(arg: array) { }", "fn unacceptable_unsized(arg: ptr>) { }", " struct Unsized { data: array; }; fn unacceptable_unsized(arg: Unsized) { } ": Err(naga::valid::ValidationError::Function { name: function_name, error: naga::valid::FunctionError::InvalidArgumentType { index: 0, name: argument_name, }, .. }) if function_name == "unacceptable_unsized" && argument_name == "arg" } check_validation_error! { " struct Unsized { data: array; }; fn acceptable_pointer_to_unsized(arg: ptr) { } ": Ok(_) } check_validation_error! { " struct Unsized { data: array; }; fn unacceptable_uniform_class(arg: ptr) { } ": Err(naga::valid::ValidationError::Function { name: function_name, error: naga::valid::FunctionError::InvalidArgumentPointerClass { index: 0, name: argument_name, class: naga::StorageClass::Uniform, }, .. }) if function_name == "unacceptable_uniform_class" && argument_name == "arg" } } #[test] fn pointer_type_equivalence() { check_validation_error! { r#" fn f(pv: ptr>, pf: ptr) { } fn g() { var m: mat2x2; let pv: ptr> = &m.x; let pf: ptr = &m.x.x; f(pv, pf); } "#: Ok(_) } } #[test] fn missing_bindings() { check_validation_error! { " [[stage(vertex)]] fn vertex(input: vec4) -> [[location(0)]] vec4 { return input; } ": Err(naga::valid::ValidationError::EntryPoint { stage: naga::ShaderStage::Vertex, error: naga::valid::EntryPointError::Argument( 0, naga::valid::VaryingError::MissingBinding, ), .. }) } check_validation_error! { " [[stage(vertex)]] fn vertex([[location(0)]] input: vec4, more_input: f32) -> [[location(0)]] vec4 { return input + more_input; } ": Err(naga::valid::ValidationError::EntryPoint { stage: naga::ShaderStage::Vertex, error: naga::valid::EntryPointError::Argument( 1, naga::valid::VaryingError::MissingBinding, ), .. }) } check_validation_error! { " [[stage(vertex)]] fn vertex([[location(0)]] input: vec4) -> vec4 { return input; } ": Err(naga::valid::ValidationError::EntryPoint { stage: naga::ShaderStage::Vertex, error: naga::valid::EntryPointError::Result( naga::valid::VaryingError::MissingBinding, ), .. }) } check_validation_error! { " struct VertexIn { [[location(0)]] pos: vec4; uv: vec2; }; [[stage(vertex)]] fn vertex(input: VertexIn) -> [[location(0)]] vec4 { return input.pos; } ": Err(naga::valid::ValidationError::EntryPoint { stage: naga::ShaderStage::Vertex, error: naga::valid::EntryPointError::Argument( 0, naga::valid::VaryingError::MemberMissingBinding(1), ), .. }) } } #[test] fn invalid_access() { check_validation_error! { " fn array_by_value(a: array, i: i32) -> i32 { return a[i]; } ", " fn matrix_by_value(m: mat4x4, i: i32) -> vec4 { return m[i]; } ": Err(naga::valid::ValidationError::Function { error: naga::valid::FunctionError::Expression { error: naga::valid::ExpressionError::IndexMustBeConstant(_), .. }, .. }) } check_validation_error! { r#" fn main() -> f32 { let a = array(0., 1., 2.); return a[3]; } "#: Err(naga::valid::ValidationError::Function { error: naga::valid::FunctionError::Expression { error: naga::valid::ExpressionError::IndexOutOfBounds(_, _), .. }, .. }) } } #[test] fn valid_access() { check_validation_error! { " fn vector_by_value(v: vec4, i: i32) -> i32 { return v[i]; } ", " fn matrix_dynamic(m: mat4x4, i: i32, j: i32) -> f32 { var temp: mat4x4 = m; // Dynamically indexing the column vector applies // `Access` to a `ValuePointer`. return temp[i][j]; } ", " fn main() { var v: vec4 = vec4(1.0, 1.0, 1.0, 1.0); let pv = &v; let a = (*pv)[3]; } ": Ok(_) } } #[test] fn invalid_local_vars() { check_validation_error! { " struct Unsized { data: array; }; fn local_ptr_dynamic_array(okay: ptr) { var not_okay: ptr> = &(*okay).data; } ": Err(naga::valid::ValidationError::Function { error: naga::valid::FunctionError::LocalVariable { name: local_var_name, error: naga::valid::LocalVariableError::InvalidType(_), .. }, .. }) if local_var_name == "not_okay" } } #[test] fn dead_code() { check_validation_error! { " fn dead_code_after_if(condition: bool) -> i32 { if (condition) { return 1; } else { return 2; } return 3; } ": Ok(_) } check_validation_error! { " fn dead_code_after_block() -> i32 { { return 1; } return 2; } ": Err(naga::valid::ValidationError::Function { error: naga::valid::FunctionError::InstructionsAfterReturn, .. }) } } #[test] fn invalid_runtime_sized_arrays() { // You can't have structs whose last member is an unsized struct. An unsized // array may only appear as the last member of a struct used directly as a // variable's store type. check_validation_error! { " struct Unsized { arr: array; }; [[block]] struct Outer { legit: i32; unsized: Unsized; }; [[group(0), binding(0)]] var outer: Outer; fn fetch(i: i32) -> f32 { return outer.unsized.arr[i]; } ": Err(naga::valid::ValidationError::Type { name: struct_name, error: naga::valid::TypeError::InvalidDynamicArray(member_name, _), .. }) if struct_name == "Outer" && member_name == "unsized" } } #[test] fn select() { check_validation_error! { " fn select_pointers(which: bool) -> i32 { var x: i32 = 1; var y: i32 = 2; let ptr = select(&x, &y, which); return *ptr; } ", " fn select_arrays(which: bool) -> i32 { var x: array; var y: array; let s = select(x, y, which); return s[0]; } ", " struct S { member: i32; }; fn select_structs(which: bool) -> S { var x: S = S(1); var y: S = S(2); let s = select(x, y, which); return s; } ": Err( naga::valid::ValidationError::Function { name, error: naga::valid::FunctionError::Expression { error: naga::valid::ExpressionError::InvalidSelectTypes, .. }, .. }, ) if name.starts_with("select_") } }