mirror of
https://github.com/gfx-rs/wgpu.git
synced 2024-11-25 08:13:27 +00:00
ed3006ccc6
Improve handling of `Access` expressions whose base is an array or matrix (not a pointer to such), and whose index is not known at compile time. SPIR-V does not have instructions that can do this directly, so spill such values to temporary variables, and perform the accesses using `OpAccessChain` instructions applied to the temporaries. When performing chains of accesses like `a[i].x[j]`, do not reify intermediate values; generate a single `OpAccessChain` for the entire thing. Remove special cases for arrays; the same code now handles arrays and matrices. Update validation to permit dynamic indexing of matrices. For details, see the comments on the new tracking structures in `naga:🔙:spv::Function`. Add snapshot test `index-by-value.wgsl`. Fixes #6358. Fixes #4337. Alternative to #6362.
2432 lines
56 KiB
Rust
2432 lines
56 KiB
Rust
/*!
|
|
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(snapshot, &output) {
|
|
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 very_negative_integers() {
|
|
// wgpu#4492
|
|
check(
|
|
"const i32min = -0x80000000i;",
|
|
r###"error: numeric literal not representable by target type: `0x80000000i`
|
|
┌─ wgsl:1:17
|
|
│
|
|
1 │ const i32min = -0x80000000i;
|
|
│ ^^^^^^^^^^^ numeric literal not representable by target type
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn reserved_identifier_prefix() {
|
|
check(
|
|
"var __bad;",
|
|
r###"error: Identifier starts with a reserved prefix: '__bad'
|
|
┌─ wgsl:1:5
|
|
│
|
|
1 │ var __bad;
|
|
│ ^^^^^ invalid identifier
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[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(
|
|
"const scale: f32 = 1.1.;",
|
|
r###"error: expected identifier, found ';'
|
|
┌─ wgsl:1:24
|
|
│
|
|
1 │ const scale: f32 = 1.1.;
|
|
│ ^ expected identifier
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_texture_sample_type() {
|
|
check(
|
|
"const x: texture_2d<bool>;",
|
|
r###"error: texture sample type must be one of f32, i32 or u32, but found bool
|
|
┌─ wgsl:1:21
|
|
│
|
|
1 │ const x: texture_2d<bool>;
|
|
│ ^^^^ 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 bad_texture() {
|
|
check(
|
|
r#"
|
|
@group(0) @binding(0) var sampler1 : sampler;
|
|
|
|
@fragment
|
|
fn main() -> @location(0) vec4<f32> {
|
|
let a = 3;
|
|
return textureSample(a, sampler1, vec2<f32>(0.0));
|
|
}
|
|
"#,
|
|
r#"error: expected an image, but found 'a' which is not an image
|
|
┌─ wgsl:7:38
|
|
│
|
|
7 │ return textureSample(a, sampler1, vec2<f32>(0.0));
|
|
│ ^ not an image
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn bad_type_cast() {
|
|
check(
|
|
r#"
|
|
fn x() -> i32 {
|
|
return i32(vec2<f32>(0.0));
|
|
}
|
|
"#,
|
|
r#"error: cannot cast a vec2<f32> to a i32
|
|
┌─ wgsl:3:28
|
|
│
|
|
3 │ return i32(vec2<f32>(0.0));
|
|
│ ^^^^^^^^^^^^^^ cannot cast a vec2<f32> to a i32
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn type_not_constructible() {
|
|
check(
|
|
r#"
|
|
fn x() {
|
|
_ = atomic<i32>(0);
|
|
}
|
|
"#,
|
|
r#"error: type `atomic` is not constructible
|
|
┌─ wgsl:3:21
|
|
│
|
|
3 │ _ = atomic<i32>(0);
|
|
│ ^^^^^^ type is not constructible
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn type_not_inferable() {
|
|
check(
|
|
r#"
|
|
fn x() {
|
|
_ = vec2();
|
|
}
|
|
"#,
|
|
r#"error: type can't be inferred
|
|
┌─ wgsl:3:21
|
|
│
|
|
3 │ _ = vec2();
|
|
│ ^^^^ type can't be inferred
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unexpected_constructor_parameters() {
|
|
check(
|
|
r#"
|
|
fn x() {
|
|
_ = i32(0, 1);
|
|
}
|
|
"#,
|
|
r#"error: unexpected components
|
|
┌─ wgsl:3:28
|
|
│
|
|
3 │ _ = i32(0, 1);
|
|
│ ^ unexpected components
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn constructor_parameter_type_mismatch() {
|
|
check(
|
|
r#"
|
|
fn x() {
|
|
_ = mat2x2<f32>(array(0, 1), vec2(2, 3));
|
|
}
|
|
"#,
|
|
r#"error: automatic conversions cannot convert `array<{AbstractInt}, 2>` to `vec2<f32>`
|
|
┌─ wgsl:3:21
|
|
│
|
|
3 │ _ = mat2x2<f32>(array(0, 1), vec2(2, 3));
|
|
│ ^^^^^^^^^^^ ^^^^^^^^^^^ this expression has type array<{AbstractInt}, 2>
|
|
│ │
|
|
│ a value of type vec2<f32> is required here
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn bad_texture_sample_type() {
|
|
check(
|
|
r#"
|
|
@group(0) @binding(0) var sampler1 : sampler;
|
|
@group(0) @binding(1) var texture : texture_2d<bool>;
|
|
|
|
@fragment
|
|
fn main() -> @location(0) vec4<f32> {
|
|
return textureSample(texture, sampler1, vec2<f32>(0.0));
|
|
}
|
|
"#,
|
|
r#"error: texture sample type must be one of f32, i32 or u32, but found bool
|
|
┌─ wgsl:3:60
|
|
│
|
|
3 │ @group(0) @binding(1) var texture : texture_2d<bool>;
|
|
│ ^^^^ 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<bad> texture: texture_2d<f32>;
|
|
"#,
|
|
r#"error: unknown address space: 'bad'
|
|
┌─ wgsl:2:39
|
|
│
|
|
2 │ @group(0) @binding(0) var<bad> texture: texture_2d<f32>;
|
|
│ ^^^ unknown address space
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_attribute() {
|
|
check(
|
|
r#"
|
|
@a
|
|
fn x() {}
|
|
"#,
|
|
r#"error: unknown attribute: 'a'
|
|
┌─ wgsl:2:14
|
|
│
|
|
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:27
|
|
│
|
|
2 │ fn x(@builtin(unknown_built_in) y: u32) {}
|
|
│ ^^^^^^^^^^^^^^^^ unknown builtin
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_access() {
|
|
check(
|
|
r#"
|
|
var<storage,unknown_access> x: array<u32>;
|
|
"#,
|
|
r#"error: unknown access: 'unknown_access'
|
|
┌─ wgsl:2:25
|
|
│
|
|
2 │ var<storage,unknown_access> x: array<u32>;
|
|
│ ^^^^^^^^^^^^^^ unknown access
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[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#"
|
|
const a = vec2<vec2f>();
|
|
"#,
|
|
r#"error: unknown scalar type: 'vec2f'
|
|
┌─ wgsl:2:28
|
|
│
|
|
2 │ const a = vec2<vec2f>();
|
|
│ ^^^^^ unknown scalar type
|
|
│
|
|
= note: Valid scalar types are f32, f64, i32, u32, bool
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_type() {
|
|
check(
|
|
r#"
|
|
const a: Vec = 10;
|
|
"#,
|
|
r#"error: unknown type: 'Vec'
|
|
┌─ wgsl:2:22
|
|
│
|
|
2 │ const a: Vec = 10;
|
|
│ ^^^ unknown type
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_storage_format() {
|
|
check(
|
|
r#"
|
|
const storage1: texture_storage_1d<rgba>;
|
|
"#,
|
|
r#"error: unknown storage format: 'rgba'
|
|
┌─ wgsl:2:48
|
|
│
|
|
2 │ const storage1: texture_storage_1d<rgba>;
|
|
│ ^^^^ unknown storage format
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_conservative_depth() {
|
|
check(
|
|
r#"
|
|
@early_depth_test(abc) fn main() {}
|
|
"#,
|
|
r#"error: unknown conservative depth: 'abc'
|
|
┌─ wgsl:2:31
|
|
│
|
|
2 │ @early_depth_test(abc) fn main() {}
|
|
│ ^^^ unknown conservative depth
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn struct_member_size_too_low() {
|
|
check(
|
|
r#"
|
|
struct Bar {
|
|
@size(0) data: array<f32>
|
|
}
|
|
"#,
|
|
r#"error: struct member size must be at least 4
|
|
┌─ wgsl:3:23
|
|
│
|
|
3 │ @size(0) data: array<f32>
|
|
│ ^ must be at least 4
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn struct_member_align_too_low() {
|
|
check(
|
|
r#"
|
|
struct Bar {
|
|
@align(8) data: vec3<f32>
|
|
}
|
|
"#,
|
|
r#"error: struct member alignment must be at least 16
|
|
┌─ wgsl:3:24
|
|
│
|
|
3 │ @align(8) data: vec3<f32>
|
|
│ ^ must be at least 16
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn struct_member_non_po2_align() {
|
|
check(
|
|
r#"
|
|
struct Bar {
|
|
@align(7) data: array<f32>
|
|
}
|
|
"#,
|
|
r#"error: struct member alignment must be a power of 2
|
|
┌─ wgsl:3:24
|
|
│
|
|
3 │ @align(7) data: array<f32>
|
|
│ ^ must be a power of 2
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[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: no definition in scope for identifier: 'a'
|
|
┌─ wgsl:3:22
|
|
│
|
|
3 │ for (a();;) {}
|
|
│ ^ unknown identifier
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn let_type_mismatch() {
|
|
check(
|
|
r#"
|
|
const x: i32 = 1.0;
|
|
"#,
|
|
r#"error: the type of `x` is expected to be `i32`, but got `{AbstractFloat}`
|
|
┌─ wgsl:2:19
|
|
│
|
|
2 │ const x: i32 = 1.0;
|
|
│ ^ definition of `x`
|
|
|
|
"#,
|
|
);
|
|
|
|
check(
|
|
r#"
|
|
fn foo() {
|
|
let x: f32 = true;
|
|
}
|
|
"#,
|
|
r#"error: the type of `x` is expected to be `f32`, but got `bool`
|
|
┌─ wgsl:3:21
|
|
│
|
|
3 │ let x: f32 = true;
|
|
│ ^ definition of `x`
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn var_type_mismatch() {
|
|
check(
|
|
r#"
|
|
fn foo() {
|
|
var x: f32 = 1u;
|
|
}
|
|
"#,
|
|
r#"error: the type of `x` is expected to be `f32`, but got `u32`
|
|
┌─ wgsl:3:21
|
|
│
|
|
3 │ var x: f32 = 1u;
|
|
│ ^ definition of `x`
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn local_var_missing_type() {
|
|
check(
|
|
r#"
|
|
fn foo() {
|
|
var x;
|
|
}
|
|
"#,
|
|
r#"error: declaration of `x` needs a type specifier or initializer
|
|
┌─ wgsl:3:21
|
|
│
|
|
3 │ var x;
|
|
│ ^ needs a type specifier or initializer
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn postfix_pointers() {
|
|
check(
|
|
r#"
|
|
fn main() {
|
|
var v: vec4<f32> = vec4<f32>(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
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn reserved_keyword() {
|
|
// global var
|
|
check(
|
|
r#"
|
|
var bool: bool = true;
|
|
"#,
|
|
r###"error: name `bool` is a reserved keyword
|
|
┌─ wgsl:2:17
|
|
│
|
|
2 │ var bool: bool = true;
|
|
│ ^^^^ definition of `bool`
|
|
|
|
"###,
|
|
);
|
|
|
|
// global constant
|
|
check(
|
|
r#"
|
|
const break: bool = true;
|
|
fn foo() {
|
|
var foo = break;
|
|
}
|
|
"#,
|
|
r###"error: name `break` is a reserved keyword
|
|
┌─ wgsl:2:19
|
|
│
|
|
2 │ const break: bool = true;
|
|
│ ^^^^^ definition of `break`
|
|
|
|
"###,
|
|
);
|
|
|
|
// local let
|
|
check(
|
|
r#"
|
|
fn foo() {
|
|
let atomic: f32 = 1.0;
|
|
}
|
|
"#,
|
|
r###"error: name `atomic` is a reserved keyword
|
|
┌─ wgsl:3:21
|
|
│
|
|
3 │ let atomic: f32 = 1.0;
|
|
│ ^^^^^^ definition of `atomic`
|
|
|
|
"###,
|
|
);
|
|
|
|
// local var
|
|
check(
|
|
r#"
|
|
fn foo() {
|
|
var sampler: f32 = 1.0;
|
|
}
|
|
"#,
|
|
r###"error: name `sampler` is a reserved keyword
|
|
┌─ wgsl:3:21
|
|
│
|
|
3 │ var sampler: f32 = 1.0;
|
|
│ ^^^^^^^ definition of `sampler`
|
|
|
|
"###,
|
|
);
|
|
|
|
// fn name
|
|
check(
|
|
r#"
|
|
fn break() {}
|
|
"#,
|
|
r###"error: name `break` is a reserved keyword
|
|
┌─ wgsl:2:16
|
|
│
|
|
2 │ fn break() {}
|
|
│ ^^^^^ definition of `break`
|
|
|
|
"###,
|
|
);
|
|
|
|
// struct
|
|
check(
|
|
r#"
|
|
struct array {}
|
|
"#,
|
|
r###"error: name `array` is a reserved keyword
|
|
┌─ wgsl:2:20
|
|
│
|
|
2 │ struct array {}
|
|
│ ^^^^^ definition of `array`
|
|
|
|
"###,
|
|
);
|
|
|
|
// struct member
|
|
check(
|
|
r#"
|
|
struct Foo { sampler: f32 }
|
|
"#,
|
|
r###"error: name `sampler` is a reserved keyword
|
|
┌─ wgsl:2:26
|
|
│
|
|
2 │ struct Foo { sampler: f32 }
|
|
│ ^^^^^^^ definition of `sampler`
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn module_scope_identifier_redefinition() {
|
|
// const
|
|
check(
|
|
r#"
|
|
const foo: bool = true;
|
|
const foo: bool = true;
|
|
"#,
|
|
r###"error: redefinition of `foo`
|
|
┌─ wgsl:2:19
|
|
│
|
|
2 │ const foo: bool = true;
|
|
│ ^^^ previous definition of `foo`
|
|
3 │ const foo: bool = true;
|
|
│ ^^^ redefinition of `foo`
|
|
|
|
"###,
|
|
);
|
|
// var
|
|
check(
|
|
r#"
|
|
var foo: bool = true;
|
|
var foo: bool = true;
|
|
"#,
|
|
r###"error: redefinition of `foo`
|
|
┌─ wgsl:2:17
|
|
│
|
|
2 │ var foo: bool = true;
|
|
│ ^^^ previous definition of `foo`
|
|
3 │ var foo: bool = true;
|
|
│ ^^^ redefinition of `foo`
|
|
|
|
"###,
|
|
);
|
|
|
|
// let and var
|
|
check(
|
|
r#"
|
|
var foo: bool = true;
|
|
const foo: bool = true;
|
|
"#,
|
|
r###"error: redefinition of `foo`
|
|
┌─ wgsl:2:17
|
|
│
|
|
2 │ var foo: bool = true;
|
|
│ ^^^ previous definition of `foo`
|
|
3 │ const foo: bool = true;
|
|
│ ^^^ redefinition of `foo`
|
|
|
|
"###,
|
|
);
|
|
|
|
// function
|
|
check(
|
|
r#"fn foo() {}
|
|
fn bar() {}
|
|
fn foo() {}"#,
|
|
r###"error: redefinition of `foo`
|
|
┌─ wgsl:1:4
|
|
│
|
|
1 │ fn foo() {}
|
|
│ ^^^ previous definition of `foo`
|
|
2 │ fn bar() {}
|
|
3 │ fn foo() {}
|
|
│ ^^^ redefinition of `foo`
|
|
|
|
"###,
|
|
);
|
|
|
|
// let and function
|
|
check(
|
|
r#"
|
|
const foo: bool = true;
|
|
fn foo() {}
|
|
"#,
|
|
r###"error: redefinition of `foo`
|
|
┌─ wgsl:2:19
|
|
│
|
|
2 │ const foo: bool = true;
|
|
│ ^^^ previous definition of `foo`
|
|
3 │ fn foo() {}
|
|
│ ^^^ redefinition of `foo`
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn matrix_with_bad_type() {
|
|
check(
|
|
r#"
|
|
fn main() {
|
|
let m = mat2x2<i32>();
|
|
}
|
|
"#,
|
|
r#"error: matrix scalar type must be floating-point, but found `i32`
|
|
┌─ wgsl:3:32
|
|
│
|
|
3 │ let m = mat2x2<i32>();
|
|
│ ^^^ must be floating-point (e.g. `f32`)
|
|
|
|
"#,
|
|
);
|
|
|
|
check(
|
|
r#"
|
|
fn main() {
|
|
var m: mat3x3<i32>;
|
|
}
|
|
"#,
|
|
r#"error: matrix scalar type must be floating-point, but found `i32`
|
|
┌─ wgsl:3:31
|
|
│
|
|
3 │ var m: mat3x3<i32>;
|
|
│ ^^^ must be floating-point (e.g. `f32`)
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn matrix_constructor_inferred() {
|
|
check(
|
|
r#"
|
|
const m: mat2x2<f64> = mat2x2<f32>(vec2(0), vec2(1));
|
|
"#,
|
|
r#"error: the type of `m` is expected to be `mat2x2<f64>`, but got `mat2x2<f32>`
|
|
┌─ wgsl:2:19
|
|
│
|
|
2 │ const m: mat2x2<f64> = mat2x2<f32>(vec2(0), vec2(1));
|
|
│ ^ definition of `m`
|
|
|
|
"#,
|
|
);
|
|
}
|
|
|
|
/// Check the result of validating a WGSL program against a pattern.
|
|
///
|
|
/// Unless you are generating code programmatically, the
|
|
/// `check_validation_error` macro will probably be more convenient to
|
|
/// use.
|
|
macro_rules! check_one_validation {
|
|
( $source:expr, $pattern:pat $( if $guard:expr )? ) => {
|
|
let source = $source;
|
|
let error = validation_error($source, naga::valid::Capabilities::default());
|
|
#[allow(clippy::redundant_pattern_matching)]
|
|
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\
|
|
{}",
|
|
&source,
|
|
error,
|
|
stringify!($pattern));
|
|
$( eprintln!("if {}", stringify!($guard)); )?
|
|
panic!("validation error does not match pattern");
|
|
}
|
|
};
|
|
( $source:expr, $pattern:pat $( if $guard:expr )?, $capabilities:expr ) => {
|
|
let source = $source;
|
|
let error = validation_error($source, $capabilities);
|
|
#[allow(clippy::redundant_pattern_matching)]
|
|
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\
|
|
{}",
|
|
&source,
|
|
error,
|
|
stringify!($pattern));
|
|
$( eprintln!("if {}", stringify!($guard)); )?
|
|
panic!("validation error does not match pattern");
|
|
}
|
|
}
|
|
}
|
|
|
|
macro_rules! check_validation {
|
|
// 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_one_validation!($source, $pattern);
|
|
)*
|
|
};
|
|
( $( $source:literal ),* : $pattern:pat, $capabilities:expr ) => {
|
|
$(
|
|
check_one_validation!($source, $pattern, $capabilities);
|
|
)*
|
|
};
|
|
( $( $source:literal ),* : $pattern:pat if $guard:expr ) => {
|
|
$(
|
|
check_one_validation!($source, $pattern if $guard);
|
|
)*
|
|
};
|
|
( $( $source:literal ),* : $pattern:pat if $guard:expr, $capabilities:expr ) => {
|
|
$(
|
|
check_one_validation!($source, $pattern if $guard, $capabilities);
|
|
)*
|
|
}
|
|
}
|
|
|
|
fn validation_error(
|
|
source: &str,
|
|
caps: naga::valid::Capabilities,
|
|
) -> Result<naga::valid::ModuleInfo, naga::valid::ValidationError> {
|
|
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(), caps)
|
|
.validate(&module)
|
|
.map_err(|e| e.into_inner()) // TODO: Add tests for spans, too?
|
|
}
|
|
|
|
#[test]
|
|
fn int64_capability() {
|
|
check_validation! {
|
|
"var input: u64;",
|
|
"var input: i64;":
|
|
Err(naga::valid::ValidationError::Type {
|
|
source: naga::valid::TypeError::WidthError(naga::valid::WidthError::MissingCapability {flag: "SHADER_INT64",..}),
|
|
..
|
|
})
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_arrays() {
|
|
check_validation! {
|
|
"alias Bad = array<array<f32>, 4>;",
|
|
"alias Bad = array<sampler, 4>;",
|
|
"alias Bad = array<texture_2d<f32>, 4>;":
|
|
Err(naga::valid::ValidationError::Type {
|
|
source: naga::valid::TypeError::InvalidArrayBaseType(_),
|
|
..
|
|
})
|
|
}
|
|
|
|
check_validation! {
|
|
"var<uniform> input: array<u64, 2>;",
|
|
"var<uniform> input: array<vec2<u32>, 2>;":
|
|
Err(naga::valid::ValidationError::GlobalVariable {
|
|
source: naga::valid::GlobalVariableError::Alignment(naga::AddressSpace::Uniform,_,_),
|
|
..
|
|
}),
|
|
naga::valid::Capabilities::SHADER_INT64
|
|
}
|
|
|
|
check_validation! {
|
|
r#"
|
|
fn main() -> f32 {
|
|
let a = array<f32, 3>(0., 1., 2.);
|
|
return a[-1];
|
|
}
|
|
"#:
|
|
Err(
|
|
naga::valid::ValidationError::Function {
|
|
name,
|
|
source: naga::valid::FunctionError::Expression {
|
|
source: naga::valid::ExpressionError::NegativeIndex(_),
|
|
..
|
|
},
|
|
..
|
|
}
|
|
)
|
|
if name == "main"
|
|
}
|
|
|
|
check(
|
|
"alias Bad = array<f32, true>;",
|
|
r###"error: must be a const-expression that resolves to a concrete integer scalar (u32 or i32)
|
|
┌─ wgsl:1:24
|
|
│
|
|
1 │ alias Bad = array<f32, true>;
|
|
│ ^^^^ must resolve to u32 or i32
|
|
|
|
"###,
|
|
);
|
|
|
|
check(
|
|
r#"
|
|
const length: f32 = 2.718;
|
|
alias Bad = array<f32, length>;
|
|
"#,
|
|
r###"error: must be a const-expression that resolves to a concrete integer scalar (u32 or i32)
|
|
┌─ wgsl:3:36
|
|
│
|
|
3 │ alias Bad = array<f32, length>;
|
|
│ ^^^^^^ must resolve to u32 or i32
|
|
|
|
"###,
|
|
);
|
|
|
|
check(
|
|
"alias Bad = array<f32, 0>;",
|
|
r###"error: array element count must be positive (> 0)
|
|
┌─ wgsl:1:24
|
|
│
|
|
1 │ alias Bad = array<f32, 0>;
|
|
│ ^ must be positive
|
|
|
|
"###,
|
|
);
|
|
|
|
check(
|
|
"alias Bad = array<f32, -1>;",
|
|
r###"error: array element count must be positive (> 0)
|
|
┌─ wgsl:1:24
|
|
│
|
|
1 │ alias Bad = array<f32, -1>;
|
|
│ ^^ must be positive
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn discard_in_wrong_stage() {
|
|
check_validation! {
|
|
"@compute @workgroup_size(1)
|
|
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
|
|
if global_id.x == 3u {
|
|
discard;
|
|
}
|
|
}":
|
|
Err(naga::valid::ValidationError::EntryPoint {
|
|
stage: naga::ShaderStage::Compute,
|
|
source: naga::valid::EntryPointError::ForbiddenStageOperations,
|
|
..
|
|
})
|
|
}
|
|
|
|
check_validation! {
|
|
"@vertex
|
|
fn main() -> @builtin(position) vec4<f32> {
|
|
if true {
|
|
discard;
|
|
}
|
|
return vec4<f32>();
|
|
}":
|
|
Err(naga::valid::ValidationError::EntryPoint {
|
|
stage: naga::ShaderStage::Vertex,
|
|
source: naga::valid::EntryPointError::ForbiddenStageOperations,
|
|
..
|
|
})
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_structs() {
|
|
check_validation! {
|
|
"struct Bad { data: sampler }",
|
|
"struct Bad { data: texture_2d<f32> }":
|
|
Err(naga::valid::ValidationError::Type {
|
|
source: naga::valid::TypeError::InvalidData(_),
|
|
..
|
|
})
|
|
}
|
|
|
|
check_validation! {
|
|
"struct Bad { data: array<f32>, other: f32, }":
|
|
Err(naga::valid::ValidationError::Type {
|
|
source: naga::valid::TypeError::InvalidDynamicArray(_, _),
|
|
..
|
|
})
|
|
}
|
|
|
|
check_validation! {
|
|
"struct Empty {}":
|
|
Err(naga::valid::ValidationError::Type {
|
|
source: naga::valid::TypeError::EmptyStruct,
|
|
..
|
|
})
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_functions() {
|
|
check_validation! {
|
|
"fn unacceptable_unsized(arg: array<f32>) { }",
|
|
"
|
|
struct Unsized { data: array<f32> }
|
|
fn unacceptable_unsized(arg: Unsized) { }
|
|
":
|
|
Err(naga::valid::ValidationError::Function {
|
|
name: function_name,
|
|
source: naga::valid::FunctionError::InvalidArgumentType {
|
|
index: 0,
|
|
name: argument_name,
|
|
},
|
|
..
|
|
})
|
|
if function_name == "unacceptable_unsized" && argument_name == "arg"
|
|
}
|
|
|
|
// Pointer's address space cannot hold unsized data.
|
|
check_validation! {
|
|
"fn unacceptable_unsized(arg: ptr<workgroup, array<f32>>) { }",
|
|
"
|
|
struct Unsized { data: array<f32> }
|
|
fn unacceptable_unsized(arg: ptr<workgroup, Unsized>) { }
|
|
":
|
|
Err(naga::valid::ValidationError::Type {
|
|
source: naga::valid::TypeError::InvalidPointerToUnsized {
|
|
base: _,
|
|
space: naga::AddressSpace::WorkGroup { .. },
|
|
},
|
|
..
|
|
})
|
|
}
|
|
|
|
// Pointers of these address spaces cannot be passed as arguments.
|
|
check_validation! {
|
|
"fn unacceptable_ptr_space(arg: ptr<storage, array<f32>>) { }":
|
|
Err(naga::valid::ValidationError::Function {
|
|
name: function_name,
|
|
source: naga::valid::FunctionError::InvalidArgumentPointerSpace {
|
|
index: 0,
|
|
name: argument_name,
|
|
space: naga::AddressSpace::Storage { .. },
|
|
},
|
|
..
|
|
})
|
|
if function_name == "unacceptable_ptr_space" && argument_name == "arg"
|
|
}
|
|
check_validation! {
|
|
"fn unacceptable_ptr_space(arg: ptr<uniform, f32>) { }":
|
|
Err(naga::valid::ValidationError::Function {
|
|
name: function_name,
|
|
source: naga::valid::FunctionError::InvalidArgumentPointerSpace {
|
|
index: 0,
|
|
name: argument_name,
|
|
space: naga::AddressSpace::Uniform,
|
|
},
|
|
..
|
|
})
|
|
if function_name == "unacceptable_ptr_space" && argument_name == "arg"
|
|
}
|
|
check_validation! {
|
|
"fn unacceptable_ptr_space(arg: ptr<workgroup, f32>) { }":
|
|
Err(naga::valid::ValidationError::Function {
|
|
name: function_name,
|
|
source: naga::valid::FunctionError::InvalidArgumentPointerSpace {
|
|
index: 0,
|
|
name: argument_name,
|
|
space: naga::AddressSpace::WorkGroup,
|
|
},
|
|
..
|
|
})
|
|
if function_name == "unacceptable_ptr_space" && argument_name == "arg"
|
|
}
|
|
|
|
check_validation! {
|
|
"
|
|
struct AFloat {
|
|
said_float: f32
|
|
};
|
|
@group(0) @binding(0)
|
|
var<storage> float: AFloat;
|
|
|
|
fn return_pointer() -> ptr<storage, f32> {
|
|
return &float.said_float;
|
|
}
|
|
":
|
|
Err(naga::valid::ValidationError::Function {
|
|
name: function_name,
|
|
source: naga::valid::FunctionError::NonConstructibleReturnType,
|
|
..
|
|
})
|
|
if function_name == "return_pointer"
|
|
}
|
|
|
|
check_validation! {
|
|
"
|
|
@group(0) @binding(0)
|
|
var<storage> atom: atomic<u32>;
|
|
|
|
fn return_atomic() -> atomic<u32> {
|
|
return atom;
|
|
}
|
|
":
|
|
Err(naga::valid::ValidationError::Function {
|
|
name: function_name,
|
|
source: naga::valid::FunctionError::NonConstructibleReturnType,
|
|
..
|
|
})
|
|
if function_name == "return_atomic"
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn pointer_type_equivalence() {
|
|
check_validation! {
|
|
r#"
|
|
fn f(pv: ptr<function, vec2<f32>>, pf: ptr<function, f32>) { }
|
|
|
|
fn g() {
|
|
var m: mat2x2<f32>;
|
|
let pv: ptr<function, vec2<f32>> = &m.x;
|
|
let pf: ptr<function, f32> = &m.x.x;
|
|
|
|
f(pv, pf);
|
|
}
|
|
"#:
|
|
Ok(_)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn missing_bindings() {
|
|
check_validation! {
|
|
"
|
|
@fragment
|
|
fn fragment(_input: vec4<f32>) -> @location(0) vec4<f32> {
|
|
return _input;
|
|
}
|
|
":
|
|
Err(naga::valid::ValidationError::EntryPoint {
|
|
stage: naga::ShaderStage::Fragment,
|
|
source: naga::valid::EntryPointError::Argument(
|
|
0,
|
|
naga::valid::VaryingError::MissingBinding,
|
|
),
|
|
..
|
|
})
|
|
}
|
|
|
|
check_validation! {
|
|
"
|
|
@fragment
|
|
fn fragment(@location(0) _input: vec4<f32>, more_input: f32) -> @location(0) vec4<f32> {
|
|
return _input + more_input;
|
|
}
|
|
":
|
|
Err(naga::valid::ValidationError::EntryPoint {
|
|
stage: naga::ShaderStage::Fragment,
|
|
source: naga::valid::EntryPointError::Argument(
|
|
1,
|
|
naga::valid::VaryingError::MissingBinding,
|
|
),
|
|
..
|
|
})
|
|
}
|
|
|
|
check_validation! {
|
|
"
|
|
@fragment
|
|
fn fragment(@location(0) _input: vec4<f32>) -> vec4<f32> {
|
|
return _input;
|
|
}
|
|
":
|
|
Err(naga::valid::ValidationError::EntryPoint {
|
|
stage: naga::ShaderStage::Fragment,
|
|
source: naga::valid::EntryPointError::Result(
|
|
naga::valid::VaryingError::MissingBinding,
|
|
),
|
|
..
|
|
})
|
|
}
|
|
|
|
check_validation! {
|
|
"
|
|
struct FragmentIn {
|
|
@location(0) pos: vec4<f32>,
|
|
uv: vec2<f32>
|
|
}
|
|
|
|
@fragment
|
|
fn fragment(_input: FragmentIn) -> @location(0) vec4<f32> {
|
|
return _input.pos;
|
|
}
|
|
":
|
|
Err(naga::valid::ValidationError::EntryPoint {
|
|
stage: naga::ShaderStage::Fragment,
|
|
source: naga::valid::EntryPointError::Argument(
|
|
0,
|
|
naga::valid::VaryingError::MemberMissingBinding(1),
|
|
),
|
|
..
|
|
})
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn missing_bindings2() {
|
|
check_validation! {
|
|
"
|
|
@vertex
|
|
fn vertex() {}
|
|
":
|
|
Err(naga::valid::ValidationError::EntryPoint {
|
|
stage: naga::ShaderStage::Vertex,
|
|
source: naga::valid::EntryPointError::MissingVertexOutputPosition,
|
|
..
|
|
})
|
|
}
|
|
|
|
check_validation! {
|
|
"
|
|
struct VertexOut {
|
|
@location(0) a: vec4<f32>,
|
|
}
|
|
|
|
@vertex
|
|
fn vertex() -> VertexOut {
|
|
return VertexOut(vec4<f32>());
|
|
}
|
|
":
|
|
Err(naga::valid::ValidationError::EntryPoint {
|
|
stage: naga::ShaderStage::Vertex,
|
|
source: naga::valid::EntryPointError::MissingVertexOutputPosition,
|
|
..
|
|
})
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_access() {
|
|
check_validation! {
|
|
r#"
|
|
fn main() -> f32 {
|
|
let a = array<f32, 3>(0., 1., 2.);
|
|
return a[3];
|
|
}
|
|
"#:
|
|
Err(naga::valid::ValidationError::Function {
|
|
source: naga::valid::FunctionError::Expression {
|
|
source: naga::valid::ExpressionError::IndexOutOfBounds(_, _),
|
|
..
|
|
},
|
|
..
|
|
})
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn valid_access() {
|
|
check_validation! {
|
|
"
|
|
fn vector_by_value(v: vec4<i32>, i: i32) -> i32 {
|
|
return v[i];
|
|
}
|
|
",
|
|
"
|
|
fn matrix_dynamic(m: mat4x4<f32>, i: i32, j: i32) -> f32 {
|
|
var temp: mat4x4<f32> = m;
|
|
// Dynamically indexing the column vector applies
|
|
// `Access` to a `ValuePointer`.
|
|
return temp[i][j];
|
|
}
|
|
",
|
|
"
|
|
fn main() {
|
|
var v: vec4<f32> = vec4<f32>(1.0, 1.0, 1.0, 1.0);
|
|
let pv = &v;
|
|
let a = (*pv)[3];
|
|
}
|
|
":
|
|
Ok(_)
|
|
}
|
|
|
|
check_validation! {
|
|
"
|
|
fn matrix_by_value(m: mat4x4<f32>, i: i32) -> vec4<f32> {
|
|
return m[i];
|
|
}
|
|
":
|
|
Ok(_)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_local_vars() {
|
|
check_validation! {
|
|
"
|
|
struct Unsized { data: array<f32> }
|
|
fn local_ptr_dynamic_array(okay: ptr<storage, Unsized>) {
|
|
var not_okay: ptr<storage, array<f32>> = &(*okay).data;
|
|
}
|
|
":
|
|
Err(naga::valid::ValidationError::Function {
|
|
source: naga::valid::FunctionError::LocalVariable {
|
|
name: local_var_name,
|
|
source: naga::valid::LocalVariableError::InvalidType(_),
|
|
..
|
|
},
|
|
..
|
|
})
|
|
if local_var_name == "not_okay"
|
|
}
|
|
|
|
check_validation! {
|
|
"
|
|
fn f() {
|
|
var x: atomic<u32>;
|
|
}
|
|
":
|
|
Err(naga::valid::ValidationError::Function {
|
|
source: naga::valid::FunctionError::LocalVariable {
|
|
name: local_var_name,
|
|
source: naga::valid::LocalVariableError::InvalidType(_),
|
|
..
|
|
},
|
|
..
|
|
})
|
|
if local_var_name == "x"
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn dead_code() {
|
|
check_validation! {
|
|
"
|
|
fn dead_code_after_if(condition: bool) -> i32 {
|
|
if (condition) {
|
|
return 1;
|
|
} else {
|
|
return 2;
|
|
}
|
|
return 3;
|
|
}
|
|
":
|
|
Ok(_)
|
|
}
|
|
check_validation! {
|
|
"
|
|
fn dead_code_after_block() -> i32 {
|
|
{
|
|
return 1;
|
|
}
|
|
return 2;
|
|
}
|
|
":
|
|
Err(naga::valid::ValidationError::Function {
|
|
source: 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! {
|
|
"
|
|
struct Unsized {
|
|
arr: array<f32>
|
|
}
|
|
|
|
struct Outer {
|
|
legit: i32,
|
|
_unsized: Unsized
|
|
}
|
|
|
|
@group(0) @binding(0) var<storage> outer: Outer;
|
|
|
|
fn fetch(i: i32) -> f32 {
|
|
return outer._unsized.arr[i];
|
|
}
|
|
":
|
|
Err(naga::valid::ValidationError::Type {
|
|
name: struct_name,
|
|
source: naga::valid::TypeError::InvalidDynamicArray(member_name, _),
|
|
..
|
|
})
|
|
if struct_name == "Outer" && member_name == "_unsized"
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn select() {
|
|
check_validation! {
|
|
"
|
|
fn select_pointers(which: bool) -> i32 {
|
|
var x: i32 = 1;
|
|
var y: i32 = 2;
|
|
let p = select(&x, &y, which);
|
|
return *p;
|
|
}
|
|
",
|
|
"
|
|
fn select_arrays(which: bool) -> i32 {
|
|
var x: array<i32, 4>;
|
|
var y: array<i32, 4>;
|
|
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,
|
|
source: naga::valid::FunctionError::Expression {
|
|
source: naga::valid::ExpressionError::InvalidSelectTypes,
|
|
..
|
|
},
|
|
..
|
|
},
|
|
)
|
|
if name.starts_with("select_")
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn missing_default_case() {
|
|
check_validation! {
|
|
"
|
|
fn test_missing_default_case() {
|
|
switch(0) {
|
|
case 0: {}
|
|
}
|
|
}
|
|
":
|
|
Err(
|
|
naga::valid::ValidationError::Function {
|
|
source: naga::valid::FunctionError::MissingDefaultCase,
|
|
..
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn wrong_access_mode() {
|
|
// The assignments to `global.i` should be forbidden, because they are in
|
|
// variables whose access mode is `read`, not `read_write`.
|
|
check_validation! {
|
|
"
|
|
struct Globals {
|
|
i: i32
|
|
}
|
|
|
|
@group(0) @binding(0)
|
|
var<storage> globals: Globals;
|
|
|
|
fn store(v: i32) {
|
|
globals.i = v;
|
|
}
|
|
",
|
|
"
|
|
struct Globals {
|
|
i: i32
|
|
}
|
|
|
|
@group(0) @binding(0)
|
|
var<uniform> globals: Globals;
|
|
|
|
fn store(v: i32) {
|
|
globals.i = v;
|
|
}
|
|
":
|
|
Err(
|
|
naga::valid::ValidationError::Function {
|
|
name,
|
|
source: naga::valid::FunctionError::InvalidStorePointer(_),
|
|
..
|
|
},
|
|
)
|
|
if name == "store"
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn io_shareable_types() {
|
|
for numeric in "i32 u32 f32".split_whitespace() {
|
|
let types = format!("{numeric} vec2<{numeric}> vec3<{numeric}> vec4<{numeric}>");
|
|
for ty in types.split_whitespace() {
|
|
check_one_validation! {
|
|
&format!("@vertex
|
|
fn f(@location(0) arg: {ty}) -> @builtin(position) vec4<f32>
|
|
{{ return vec4<f32>(0.0); }}"),
|
|
Ok(_module)
|
|
}
|
|
}
|
|
}
|
|
|
|
for ty in "bool
|
|
vec2<bool> vec3<bool> vec4<bool>
|
|
array<f32,4>
|
|
mat2x2<f32>
|
|
ptr<function,f32>"
|
|
.split_whitespace()
|
|
{
|
|
check_one_validation! {
|
|
&format!("@vertex
|
|
fn f(@location(0) arg: {ty}) -> @builtin(position) vec4<f32>
|
|
{{ return vec4<f32>(0.0); }}"),
|
|
Err(
|
|
naga::valid::ValidationError::EntryPoint {
|
|
stage: naga::ShaderStage::Vertex,
|
|
name,
|
|
source: naga::valid::EntryPointError::Argument(
|
|
0,
|
|
naga::valid::VaryingError::NotIOShareableType(
|
|
_,
|
|
),
|
|
),
|
|
},
|
|
)
|
|
if name == "f"
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn host_shareable_types() {
|
|
// Host-shareable, constructible types.
|
|
let types = "i32 u32 f32
|
|
vec2<i32> vec3<u32> vec4<f32>
|
|
mat4x4<f32>
|
|
array<mat4x4<f32>,4>
|
|
AStruct";
|
|
for ty in types.split_whitespace() {
|
|
check_one_validation! {
|
|
&format!("struct AStruct {{ member: array<mat4x4<f32>, 8> }};
|
|
@group(0) @binding(0) var<uniform> ubuf: {ty};
|
|
@group(0) @binding(1) var<storage> sbuf: {ty};"),
|
|
Ok(_module)
|
|
}
|
|
}
|
|
|
|
// Host-shareable but not constructible types.
|
|
let types = "atomic<i32> atomic<u32>
|
|
array<atomic<u32>,4>
|
|
array<u32>
|
|
AStruct";
|
|
for ty in types.split_whitespace() {
|
|
check_one_validation! {
|
|
&format!("struct AStruct {{ member: array<atomic<u32>, 8> }};
|
|
@group(0) @binding(1) var<storage> sbuf: {ty};"),
|
|
Ok(_module)
|
|
}
|
|
}
|
|
|
|
// Types that are neither host-shareable nor constructible.
|
|
for ty in "bool ptr<storage,i32>".split_whitespace() {
|
|
check_one_validation! {
|
|
&format!("@group(0) @binding(0) var<storage> sbuf: {ty};"),
|
|
Err(
|
|
naga::valid::ValidationError::GlobalVariable {
|
|
name,
|
|
handle: _,
|
|
source: naga::valid::GlobalVariableError::MissingTypeFlags { .. },
|
|
},
|
|
)
|
|
if name == "sbuf"
|
|
}
|
|
|
|
check_one_validation! {
|
|
&format!("@group(0) @binding(0) var<uniform> ubuf: {ty};"),
|
|
Err(naga::valid::ValidationError::GlobalVariable {
|
|
name,
|
|
handle: _,
|
|
source: naga::valid::GlobalVariableError::MissingTypeFlags { .. },
|
|
},
|
|
)
|
|
if name == "ubuf"
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn var_init() {
|
|
check_validation! {
|
|
"
|
|
var<workgroup> initialized: u32 = 0u;
|
|
":
|
|
Err(
|
|
naga::valid::ValidationError::GlobalVariable {
|
|
source: naga::valid::GlobalVariableError::InitializerNotAllowed(naga::AddressSpace::WorkGroup),
|
|
..
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn misplaced_break_if() {
|
|
check(
|
|
"
|
|
fn test_misplaced_break_if() {
|
|
loop {
|
|
break if true;
|
|
}
|
|
}
|
|
",
|
|
r###"error: A break if is only allowed in a continuing block
|
|
┌─ wgsl:4:17
|
|
│
|
|
4 │ break if true;
|
|
│ ^^^^^^^^ not in a continuing block
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn break_if_bad_condition() {
|
|
check_validation! {
|
|
"
|
|
fn test_break_if_bad_condition() {
|
|
loop {
|
|
continuing {
|
|
break if 1;
|
|
}
|
|
}
|
|
}
|
|
":
|
|
Err(
|
|
naga::valid::ValidationError::Function {
|
|
source: naga::valid::FunctionError::InvalidIfType(_),
|
|
..
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn swizzle_assignment() {
|
|
check(
|
|
"
|
|
fn f() {
|
|
var v = vec2(0);
|
|
v.xy = vec2(1);
|
|
}
|
|
",
|
|
r###"error: invalid left-hand side of assignment
|
|
┌─ wgsl:4:13
|
|
│
|
|
4 │ v.xy = vec2(1);
|
|
│ ^^^^ cannot assign to this expression
|
|
│
|
|
= note: WGSL does not support assignments to swizzles
|
|
= note: consider assigning each component individually
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn binary_statement() {
|
|
check(
|
|
"
|
|
fn f() {
|
|
3 + 5;
|
|
}
|
|
",
|
|
r###"error: expected assignment or increment/decrement, found ';'
|
|
┌─ wgsl:3:18
|
|
│
|
|
3 │ 3 + 5;
|
|
│ ^ expected assignment or increment/decrement
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn assign_to_expr() {
|
|
check(
|
|
"
|
|
fn f() {
|
|
3 + 5 = 10;
|
|
}
|
|
",
|
|
r###"error: invalid left-hand side of assignment
|
|
┌─ wgsl:3:13
|
|
│
|
|
3 │ 3 + 5 = 10;
|
|
│ ^^^^^ cannot assign to this expression
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn assign_to_let() {
|
|
check(
|
|
"
|
|
fn f() {
|
|
let a = 10;
|
|
a = 20;
|
|
}
|
|
",
|
|
r###"error: invalid left-hand side of assignment
|
|
┌─ wgsl:3:17
|
|
│
|
|
3 │ let a = 10;
|
|
│ ^ this is an immutable binding
|
|
4 │ a = 20;
|
|
│ ^ cannot assign to this expression
|
|
│
|
|
= note: consider declaring 'a' with `var` instead of `let`
|
|
|
|
"###,
|
|
);
|
|
|
|
check(
|
|
"
|
|
fn f() {
|
|
let a = array(1, 2);
|
|
a[0] = 1;
|
|
}
|
|
",
|
|
r###"error: invalid left-hand side of assignment
|
|
┌─ wgsl:3:17
|
|
│
|
|
3 │ let a = array(1, 2);
|
|
│ ^ this is an immutable binding
|
|
4 │ a[0] = 1;
|
|
│ ^^^^ cannot assign to this expression
|
|
│
|
|
= note: consider declaring 'a' with `var` instead of `let`
|
|
|
|
"###,
|
|
);
|
|
|
|
check(
|
|
"
|
|
struct S { a: i32 }
|
|
|
|
fn f() {
|
|
let a = S(10);
|
|
a.a = 20;
|
|
}
|
|
",
|
|
r###"error: invalid left-hand side of assignment
|
|
┌─ wgsl:5:17
|
|
│
|
|
5 │ let a = S(10);
|
|
│ ^ this is an immutable binding
|
|
6 │ a.a = 20;
|
|
│ ^^^ cannot assign to this expression
|
|
│
|
|
= note: consider declaring 'a' with `var` instead of `let`
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn recursive_function() {
|
|
check(
|
|
"
|
|
fn f() {
|
|
f();
|
|
}
|
|
",
|
|
r###"error: declaration of `f` is recursive
|
|
┌─ wgsl:2:12
|
|
│
|
|
2 │ fn f() {
|
|
│ ^
|
|
3 │ f();
|
|
│ ^ uses itself here
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn cyclic_function() {
|
|
check(
|
|
"
|
|
fn f() {
|
|
g();
|
|
}
|
|
fn g() {
|
|
f();
|
|
}
|
|
",
|
|
r###"error: declaration of `f` is cyclic
|
|
┌─ wgsl:2:12
|
|
│
|
|
2 │ fn f() {
|
|
│ ^
|
|
3 │ g();
|
|
│ ^ uses `g`
|
|
4 │ }
|
|
5 │ fn g() {
|
|
│ ^
|
|
6 │ f();
|
|
│ ^ ending the cycle
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn switch_signed_unsigned_mismatch() {
|
|
check(
|
|
"
|
|
fn x(y: u32) {
|
|
switch y {
|
|
case 1: {}
|
|
}
|
|
}
|
|
",
|
|
r###"error: invalid switch value
|
|
┌─ wgsl:4:16
|
|
│
|
|
4 │ case 1: {}
|
|
│ ^ expected unsigned integer
|
|
│
|
|
= note: suffix the integer with a `u`: '1u'
|
|
|
|
"###,
|
|
);
|
|
|
|
check(
|
|
"
|
|
fn x(y: i32) {
|
|
switch y {
|
|
case 1u: {}
|
|
}
|
|
}
|
|
",
|
|
r###"error: invalid switch value
|
|
┌─ wgsl:4:16
|
|
│
|
|
4 │ case 1u: {}
|
|
│ ^^ expected signed integer
|
|
│
|
|
= note: remove the `u` suffix: '1'
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn function_returns_void() {
|
|
check(
|
|
"
|
|
fn x() {
|
|
let a = vec2<f32>(1.0, 2.0);
|
|
}
|
|
|
|
fn b() {
|
|
let a = x();
|
|
}
|
|
",
|
|
r###"error: function does not return any value
|
|
┌─ wgsl:7:18
|
|
│
|
|
7 │ let a = x();
|
|
│ ^
|
|
│
|
|
= note: perhaps you meant to call the function in a separate statement?
|
|
|
|
"###,
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn function_param_redefinition_as_param() {
|
|
check(
|
|
"
|
|
fn x(a: f32, a: vec2<f32>) {}
|
|
",
|
|
r###"error: redefinition of `a`
|
|
┌─ wgsl:2:14
|
|
│
|
|
2 │ fn x(a: f32, a: vec2<f32>) {}
|
|
│ ^ ^ redefinition of `a`
|
|
│ │
|
|
│ previous definition of `a`
|
|
|
|
"###,
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn function_param_redefinition_as_local() {
|
|
check(
|
|
"
|
|
fn x(a: f32) {
|
|
let a = 0.0;
|
|
}
|
|
",
|
|
r###"error: redefinition of `a`
|
|
┌─ wgsl:2:14
|
|
│
|
|
2 │ fn x(a: f32) {
|
|
│ ^ previous definition of `a`
|
|
3 │ let a = 0.0;
|
|
│ ^ redefinition of `a`
|
|
|
|
"###,
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn constructor_type_error_span() {
|
|
check(
|
|
"
|
|
fn unfortunate() {
|
|
var i: i32;
|
|
var a: array<f32, 1> = array<f32, 1>(i);
|
|
}
|
|
",
|
|
r###"error: automatic conversions cannot convert `i32` to `f32`
|
|
┌─ wgsl:4:36
|
|
│
|
|
4 │ var a: array<f32, 1> = array<f32, 1>(i);
|
|
│ ^^^^^^^^^^^^^ a value of type f32 is required here
|
|
|
|
"###,
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn global_initialization_type_mismatch() {
|
|
check(
|
|
"
|
|
var<private> a: vec2<f32> = vec2<i32>(1i, 2i);
|
|
",
|
|
r###"error: the type of `a` is expected to be `vec2<f32>`, but got `vec2<i32>`
|
|
┌─ wgsl:2:22
|
|
│
|
|
2 │ var<private> a: vec2<f32> = vec2<i32>(1i, 2i);
|
|
│ ^ definition of `a`
|
|
|
|
"###,
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn binding_array_local() {
|
|
check_validation! {
|
|
"fn f() { var x: binding_array<sampler, 4>; }":
|
|
Err(_)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn binding_array_private() {
|
|
check_validation! {
|
|
"var<private> x: binding_array<sampler, 4>;":
|
|
Err(_)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn binding_array_non_struct() {
|
|
check_validation! {
|
|
"var<storage> x: binding_array<i32, 4>;":
|
|
Err(naga::valid::ValidationError::Type {
|
|
source: naga::valid::TypeError::BindingArrayBaseTypeNotStruct(_),
|
|
..
|
|
})
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn compaction_preserves_spans() {
|
|
let source = r#"
|
|
fn f() {
|
|
var a: i32 = -(-(-(-42i)));
|
|
var x: i32;
|
|
x = 42u;
|
|
}
|
|
"#; // ^^^ correct error span: 95..98
|
|
let mut module = naga::front::wgsl::parse_str(source).expect("source ought to parse");
|
|
naga::compact::compact(&mut module);
|
|
let err = naga::valid::Validator::new(
|
|
naga::valid::ValidationFlags::all(),
|
|
naga::valid::Capabilities::default(),
|
|
)
|
|
.validate(&module)
|
|
.expect_err("source ought to fail validation");
|
|
|
|
// Ideally this would all just be a `matches!` with a big pattern,
|
|
// but the `Span` API is full of opaque structs.
|
|
let mut spans = err.spans();
|
|
|
|
// The first span is the whole function.
|
|
let _ = spans.next().expect("error should have at least one span");
|
|
|
|
// The second span is the assignment destination.
|
|
let dest_span = spans
|
|
.next()
|
|
.expect("error should have at least two spans")
|
|
.0;
|
|
if !matches!(
|
|
dest_span.to_range(),
|
|
Some(std::ops::Range { start: 95, end: 98 })
|
|
) {
|
|
panic!("Error message has wrong span:\n\n{err:#?}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn limit_braced_statement_nesting() {
|
|
let too_many_braces = "fn f() {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{";
|
|
|
|
let expected_diagnostic = r###"error: brace nesting limit reached
|
|
┌─ wgsl:1:72
|
|
│
|
|
1 │ fn f() {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{
|
|
│ ^ limit reached at this brace
|
|
│
|
|
= note: nesting limit is currently set to 64
|
|
|
|
"###;
|
|
|
|
// In debug builds, we might actually overflow the stack before exercising this error case,
|
|
// depending on the platform and the `RUST_MIN_STACK` env. var. Use a thread with a custom
|
|
// stack size that works on all platforms.
|
|
std::thread::Builder::new()
|
|
.stack_size(1024 * 1024 * 2 /* MB */)
|
|
.spawn(|| check(too_many_braces, expected_diagnostic))
|
|
.unwrap()
|
|
.join()
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn too_many_unclosed_loops() {
|
|
let too_many_braces = "fn f() {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
loop {
|
|
";
|
|
|
|
let expected_diagnostic = r###"error: brace nesting limit reached
|
|
┌─ wgsl:65:13
|
|
│
|
|
65 │ loop {
|
|
│ ^ limit reached at this brace
|
|
│
|
|
= note: nesting limit is currently set to 64
|
|
|
|
"###;
|
|
|
|
// In debug builds, we might actually overflow the stack before exercising this error case,
|
|
// depending on the platform and the `RUST_MIN_STACK` env. var. Use a thread with a custom
|
|
// stack size that works on all platforms.
|
|
std::thread::Builder::new()
|
|
.stack_size(1024 * 1024 * 2 /* MB */)
|
|
.spawn(|| check(too_many_braces, expected_diagnostic))
|
|
.unwrap()
|
|
.join()
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn local_const_wrong_type() {
|
|
check(
|
|
"
|
|
fn f() {
|
|
const c: i32 = 5u;
|
|
}
|
|
",
|
|
r###"error: the type of `c` is expected to be `i32`, but got `u32`
|
|
┌─ wgsl:3:19
|
|
│
|
|
3 │ const c: i32 = 5u;
|
|
│ ^ definition of `c`
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn local_const_from_let() {
|
|
check(
|
|
"
|
|
fn f() {
|
|
let a = 5;
|
|
const c = a;
|
|
}
|
|
",
|
|
r###"error: this operation is not supported in a const context
|
|
┌─ wgsl:4:23
|
|
│
|
|
4 │ const c = a;
|
|
│ ^ operation not supported here
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn local_const_from_var() {
|
|
check(
|
|
"
|
|
fn f() {
|
|
var a = 5;
|
|
const c = a;
|
|
}
|
|
",
|
|
r###"error: this operation is not supported in a const context
|
|
┌─ wgsl:4:23
|
|
│
|
|
4 │ const c = a;
|
|
│ ^ operation not supported here
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn local_const_from_override() {
|
|
check(
|
|
"
|
|
override o: i32;
|
|
fn f() {
|
|
const c = o;
|
|
}
|
|
",
|
|
r###"error: Unexpected override-expression
|
|
┌─ wgsl:4:23
|
|
│
|
|
4 │ const c = o;
|
|
│ ^ see msg
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn local_const_from_global_var() {
|
|
check(
|
|
"
|
|
var v: i32;
|
|
fn f() {
|
|
const c = v;
|
|
}
|
|
",
|
|
r###"error: Unexpected runtime-expression
|
|
┌─ wgsl:4:23
|
|
│
|
|
4 │ const c = v;
|
|
│ ^ see msg
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn only_one_swizzle_type() {
|
|
check(
|
|
"
|
|
const ok1 = vec2(0.0, 0.0).xy;
|
|
const ok2 = vec2(0.0, 0.0).rg;
|
|
const err = vec2(0.0, 0.0).xg;
|
|
",
|
|
r###"error: invalid field accessor `xg`
|
|
┌─ wgsl:4:36
|
|
│
|
|
4 │ const err = vec2(0.0, 0.0).xg;
|
|
│ ^^ invalid accessor
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn const_assert_must_be_const() {
|
|
check(
|
|
"
|
|
fn foo() {
|
|
let a = 5;
|
|
const_assert a != 0;
|
|
}
|
|
",
|
|
r###"error: this operation is not supported in a const context
|
|
┌─ wgsl:4:26
|
|
│
|
|
4 │ const_assert a != 0;
|
|
│ ^ operation not supported here
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn const_assert_must_be_bool() {
|
|
check(
|
|
"
|
|
const_assert(5); // 5 is not bool
|
|
",
|
|
r###"error: must be a const-expression that resolves to a bool
|
|
┌─ wgsl:2:26
|
|
│
|
|
2 │ const_assert(5); // 5 is not bool
|
|
│ ^ must resolve to bool
|
|
|
|
"###,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn const_assert_failed() {
|
|
check(
|
|
"
|
|
const_assert(false);
|
|
",
|
|
r###"error: const_assert failure
|
|
┌─ wgsl:2:26
|
|
│
|
|
2 │ const_assert(false);
|
|
│ ^^^^^ evaluates to false
|
|
|
|
"###,
|
|
);
|
|
}
|