blob: 21687ced7204c1f0c50d801c9c614b6f79d275af [file] [log] [blame]
// Copyright 2020 The Tint Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "gmock/gmock.h"
#include "src/reader/spirv/function.h"
#include "src/reader/spirv/parser_impl_test_helper.h"
#include "src/reader/spirv/spirv_tools_helpers_test.h"
namespace tint {
namespace reader {
namespace spirv {
namespace {
using ::testing::Eq;
using ::testing::HasSubstr;
using SpvParserMemoryTest = SpvParserTest;
std::string Preamble() {
return R"(
OpCapability Shader
OpMemoryModel Logical Simple
OpEntryPoint Fragment %100 "main"
OpExecutionMode %100 OriginUpperLeft
)";
}
TEST_F(SpvParserMemoryTest, EmitStatement_StoreBoolConst) {
auto p = parser(test::Assemble(Preamble() + R"(
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%ty = OpTypeBool
%true = OpConstantTrue %ty
%false = OpConstantFalse %ty
%null = OpConstantNull %ty
%ptr_ty = OpTypePointer Function %ty
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%1 = OpVariable %ptr_ty Function
OpStore %1 %true
OpStore %1 %false
OpStore %1 %null
OpReturn
OpFunctionEnd
)"));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody()) << p->error();
auto ast_body = fe.ast_body();
EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(x_1 = true;
x_1 = false;
x_1 = false;
)"));
}
TEST_F(SpvParserMemoryTest, EmitStatement_StoreUintConst) {
auto p = parser(test::Assemble(Preamble() + R"(
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%ty = OpTypeInt 32 0
%val = OpConstant %ty 42
%null = OpConstantNull %ty
%ptr_ty = OpTypePointer Function %ty
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%1 = OpVariable %ptr_ty Function
OpStore %1 %val
OpStore %1 %null
OpReturn
OpFunctionEnd
)"));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody());
auto ast_body = fe.ast_body();
EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(x_1 = 42u;
x_1 = 0u;
)"));
}
TEST_F(SpvParserMemoryTest, EmitStatement_StoreIntConst) {
auto p = parser(test::Assemble(Preamble() + R"(
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%ty = OpTypeInt 32 1
%val = OpConstant %ty 42
%null = OpConstantNull %ty
%ptr_ty = OpTypePointer Function %ty
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%1 = OpVariable %ptr_ty Function
OpStore %1 %val
OpStore %1 %null
OpReturn
OpFunctionEnd
)"));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody());
auto ast_body = fe.ast_body();
EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(x_1 = 42;
x_1 = 0;
)"));
}
TEST_F(SpvParserMemoryTest, EmitStatement_StoreFloatConst) {
auto p = parser(test::Assemble(Preamble() + R"(
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%ty = OpTypeFloat 32
%val = OpConstant %ty 42
%null = OpConstantNull %ty
%ptr_ty = OpTypePointer Function %ty
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%1 = OpVariable %ptr_ty Function
OpStore %1 %val
OpStore %1 %null
OpReturn
OpFunctionEnd
)"));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody());
auto ast_body = fe.ast_body();
EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr(R"(x_1 = 42.0;
x_1 = 0.0;
)"));
}
TEST_F(SpvParserMemoryTest, EmitStatement_LoadBool) {
auto p = parser(test::Assemble(Preamble() + R"(
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%ty = OpTypeBool
%true = OpConstantTrue %ty
%false = OpConstantFalse %ty
%null = OpConstantNull %ty
%ptr_ty = OpTypePointer Function %ty
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%1 = OpVariable %ptr_ty Function %true
%2 = OpLoad %ty %1
OpReturn
OpFunctionEnd
)"));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody()) << p->error();
auto ast_body = fe.ast_body();
EXPECT_THAT(test::ToString(p->program(), ast_body),
HasSubstr("let x_2 : bool = x_1;"));
}
TEST_F(SpvParserMemoryTest, EmitStatement_LoadScalar) {
auto p = parser(test::Assemble(Preamble() + R"(
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%ty = OpTypeInt 32 0
%ty_42 = OpConstant %ty 42
%ptr_ty = OpTypePointer Function %ty
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%1 = OpVariable %ptr_ty Function %ty_42
%2 = OpLoad %ty %1
%3 = OpLoad %ty %1
OpReturn
OpFunctionEnd
)"));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody()) << p->error();
auto ast_body = fe.ast_body();
EXPECT_THAT(test::ToString(p->program(), ast_body),
HasSubstr(R"(let x_2 : u32 = x_1;
let x_3 : u32 = x_1;
)"));
}
TEST_F(SpvParserMemoryTest, EmitStatement_UseLoadedScalarTwice) {
auto p = parser(test::Assemble(Preamble() + R"(
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%ty = OpTypeInt 32 0
%ty_42 = OpConstant %ty 42
%ptr_ty = OpTypePointer Function %ty
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%1 = OpVariable %ptr_ty Function %ty_42
%2 = OpLoad %ty %1
OpStore %1 %2
OpStore %1 %2
OpReturn
OpFunctionEnd
)"));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody()) << p->error();
auto ast_body = fe.ast_body();
EXPECT_THAT(test::ToString(p->program(), ast_body),
HasSubstr(R"(let x_2 : u32 = x_1;
x_1 = x_2;
x_1 = x_2;
)"));
}
TEST_F(SpvParserMemoryTest, EmitStatement_StoreToModuleScopeVar) {
auto p = parser(test::Assemble(Preamble() + R"(
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%ty = OpTypeInt 32 0
%val = OpConstant %ty 42
%ptr_ty = OpTypePointer Private %ty
%1 = OpVariable %ptr_ty Private
%100 = OpFunction %void None %voidfn
%entry = OpLabel
OpStore %1 %val
OpReturn
OpFunctionEnd
)"));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody());
auto ast_body = fe.ast_body();
EXPECT_THAT(test::ToString(p->program(), ast_body), HasSubstr("x_1 = 42u;"));
}
TEST_F(SpvParserMemoryTest,
EmitStatement_CopyMemory_Scalar_Function_To_Private) {
auto p = parser(test::Assemble(Preamble() + R"(
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%ty = OpTypeInt 32 0
%val = OpConstant %ty 42
%ptr_fn_ty = OpTypePointer Function %ty
%ptr_priv_ty = OpTypePointer Private %ty
%2 = OpVariable %ptr_priv_ty Private
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%1 = OpVariable %ptr_fn_ty Function
OpCopyMemory %2 %1
OpReturn
OpFunctionEnd
)"));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody());
auto ast_body = fe.ast_body();
const auto got = test::ToString(p->program(), ast_body);
const auto* expected = "x_2 = x_1;";
EXPECT_THAT(got, HasSubstr(expected));
}
TEST_F(SpvParserMemoryTest, EmitStatement_AccessChain_NoOperands) {
auto err = test::AssembleFailure(Preamble() + R"(
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%ty = OpTypeInt 32 0
%val = OpConstant %ty 42
%ptr_ty = OpTypePointer Private %ty
%1 = OpVariable %ptr_ty Private
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%2 = OpAccessChain %ptr_ty ; Needs a base operand
OpStore %1 %val
OpReturn
)");
EXPECT_THAT(err,
Eq("16:5: Expected operand, found next instruction instead."));
}
TEST_F(SpvParserMemoryTest, EmitStatement_AccessChain_BaseIsNotPointer) {
auto p = parser(test::Assemble(Preamble() + R"(
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%10 = OpTypeInt 32 0
%val = OpConstant %10 42
%ptr_ty = OpTypePointer Private %10
%20 = OpVariable %10 Private ; bad pointer type
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%1 = OpAccessChain %ptr_ty %20
OpStore %1 %val
OpReturn
)"));
EXPECT_FALSE(p->BuildAndParseInternalModuleExceptFunctions());
EXPECT_THAT(p->error(), Eq("variable with ID 20 has non-pointer type 10"));
}
TEST_F(SpvParserMemoryTest, EmitStatement_AccessChain_VectorSwizzle) {
const std::string assembly = Preamble() + R"(
OpName %1 "myvar"
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%uint = OpTypeInt 32 0
%store_ty = OpTypeVector %uint 4
%uint_2 = OpConstant %uint 2
%uint_42 = OpConstant %uint 42
%elem_ty = OpTypePointer Private %uint
%var_ty = OpTypePointer Private %store_ty
%1 = OpVariable %var_ty Private
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%2 = OpAccessChain %elem_ty %1 %uint_2
OpStore %2 %uint_42
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions())
<< assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody());
auto ast_body = fe.ast_body();
EXPECT_THAT(test::ToString(p->program(), ast_body),
HasSubstr("myvar.z = 42u;"));
}
TEST_F(SpvParserMemoryTest, EmitStatement_AccessChain_VectorConstOutOfBounds) {
const std::string assembly = Preamble() + R"(
OpName %1 "myvar"
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%uint = OpTypeInt 32 0
%store_ty = OpTypeVector %uint 4
%42 = OpConstant %uint 42
%uint_99 = OpConstant %uint 99
%elem_ty = OpTypePointer Private %uint
%var_ty = OpTypePointer Private %store_ty
%1 = OpVariable %var_ty Private
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%2 = OpAccessChain %elem_ty %1 %42
OpStore %2 %uint_99
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions())
<< assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_FALSE(fe.EmitBody());
EXPECT_THAT(p->error(), Eq("Access chain %2 index %42 value 42 is out of "
"bounds for vector of 4 elements"));
}
TEST_F(SpvParserMemoryTest, EmitStatement_AccessChain_VectorNonConstIndex) {
const std::string assembly = Preamble() + R"(
OpName %1 "myvar"
OpName %13 "a_dynamic_index"
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%uint = OpTypeInt 32 0
%store_ty = OpTypeVector %uint 4
%uint_2 = OpConstant %uint 2
%uint_42 = OpConstant %uint 42
%elem_ty = OpTypePointer Private %uint
%var_ty = OpTypePointer Private %store_ty
%1 = OpVariable %var_ty Private
%10 = OpVariable %var_ty Private
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%11 = OpLoad %store_ty %10
%12 = OpCompositeExtract %uint %11 2
%13 = OpCopyObject %uint %12
%2 = OpAccessChain %elem_ty %1 %13
OpStore %2 %uint_42
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions())
<< assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody());
auto ast_body = fe.ast_body();
EXPECT_THAT(test::ToString(p->program(), ast_body),
HasSubstr("myvar[a_dynamic_index] = 42u;"));
}
TEST_F(SpvParserMemoryTest,
EmitStatement_AccessChain_VectorComponent_MultiUse) {
// WGSL does not support pointer-to-vector-component, so test that we sink
// these pointers into the point of use.
const std::string assembly = Preamble() + R"(
OpName %1 "myvar"
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%uint = OpTypeInt 32 0
%store_ty = OpTypeVector %uint 4
%uint_2 = OpConstant %uint 2
%uint_42 = OpConstant %uint 42
%elem_ty = OpTypePointer Private %uint
%var_ty = OpTypePointer Private %store_ty
%1 = OpVariable %var_ty Private
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%ptr = OpAccessChain %elem_ty %1 %uint_2
%load = OpLoad %uint %ptr
%result = OpIAdd %uint %load %uint_2
OpStore %ptr %result
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions())
<< assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody()) << p->error();
auto ast_body = fe.ast_body();
auto wgsl = test::ToString(p->program(), ast_body);
EXPECT_THAT(wgsl, Not(HasSubstr("&")));
EXPECT_THAT(wgsl, HasSubstr(" = myvar.z;"));
EXPECT_THAT(wgsl, HasSubstr("myvar.z = "));
}
TEST_F(SpvParserMemoryTest,
EmitStatement_AccessChain_VectorComponent_MultiUse_NonConstIndex) {
// WGSL does not support pointer-to-vector-component, so test that we sink
// these pointers into the point of use.
const std::string assembly = Preamble() + R"(
OpName %1 "myvar"
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%uint = OpTypeInt 32 0
%store_ty = OpTypeVector %uint 4
%uint_2 = OpConstant %uint 2
%uint_42 = OpConstant %uint 42
%elem_ty = OpTypePointer Private %uint
%var_ty = OpTypePointer Private %store_ty
%1 = OpVariable %var_ty Private
%2 = OpVariable %elem_ty Private
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%idx = OpLoad %uint %2
%ptr = OpAccessChain %elem_ty %1 %idx
%load = OpLoad %uint %ptr
%result = OpIAdd %uint %load %uint_2
OpStore %ptr %result
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions())
<< assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody()) << p->error();
auto ast_body = fe.ast_body();
auto wgsl = test::ToString(p->program(), ast_body);
EXPECT_THAT(wgsl, Not(HasSubstr("&")));
EXPECT_THAT(wgsl, HasSubstr(" = myvar[x_12];"));
EXPECT_THAT(wgsl, HasSubstr("myvar[x_12] = "));
}
TEST_F(SpvParserMemoryTest,
EmitStatement_AccessChain_VectorComponent_SinkThroughChain) {
// Test that we can sink a pointer-to-vector-component through a chain of
// instructions that propagate it.
const std::string assembly = Preamble() + R"(
OpName %1 "myvar"
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%uint = OpTypeInt 32 0
%store_ty = OpTypeVector %uint 4
%uint_2 = OpConstant %uint 2
%uint_42 = OpConstant %uint 42
%elem_ty = OpTypePointer Private %uint
%var_ty = OpTypePointer Private %store_ty
%1 = OpVariable %var_ty Private
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%ptr = OpAccessChain %elem_ty %1 %uint_2
%ptr2 = OpCopyObject %elem_ty %ptr
%ptr3 = OpInBoundsAccessChain %elem_ty %ptr2
%ptr4 = OpAccessChain %elem_ty %ptr3
%load = OpLoad %uint %ptr3
%result = OpIAdd %uint %load %uint_2
OpStore %ptr4 %result
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions())
<< assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody()) << p->error();
auto ast_body = fe.ast_body();
auto wgsl = test::ToString(p->program(), ast_body);
EXPECT_THAT(wgsl, Not(HasSubstr("&")));
EXPECT_THAT(wgsl, HasSubstr(" = myvar.z;"));
EXPECT_THAT(wgsl, HasSubstr("myvar.z = "));
}
TEST_F(SpvParserMemoryTest, EmitStatement_AccessChain_Matrix) {
const std::string assembly = Preamble() + R"(
OpName %1 "myvar"
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%float = OpTypeFloat 32
%v4float = OpTypeVector %float 4
%m3v4float = OpTypeMatrix %v4float 3
%elem_ty = OpTypePointer Private %v4float
%var_ty = OpTypePointer Private %m3v4float
%uint = OpTypeInt 32 0
%uint_2 = OpConstant %uint 2
%float_42 = OpConstant %float 42
%v4float_42 = OpConstantComposite %v4float %float_42 %float_42 %float_42 %float_42
%1 = OpVariable %var_ty Private
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%2 = OpAccessChain %elem_ty %1 %uint_2
OpStore %2 %v4float_42
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions())
<< assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody());
auto ast_body = fe.ast_body();
EXPECT_THAT(test::ToString(p->program(), ast_body),
HasSubstr("myvar[2u] = vec4<f32>(42.0, 42.0, 42.0, 42.0);"));
}
TEST_F(SpvParserMemoryTest, EmitStatement_AccessChain_Array) {
const std::string assembly = Preamble() + R"(
OpName %1 "myvar"
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%float = OpTypeFloat 32
%v4float = OpTypeVector %float 4
%m3v4float = OpTypeMatrix %v4float 3
%elem_ty = OpTypePointer Private %v4float
%var_ty = OpTypePointer Private %m3v4float
%uint = OpTypeInt 32 0
%uint_2 = OpConstant %uint 2
%float_42 = OpConstant %float 42
%v4float_42 = OpConstantComposite %v4float %float_42 %float_42 %float_42 %float_42
%1 = OpVariable %var_ty Private
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%2 = OpAccessChain %elem_ty %1 %uint_2
OpStore %2 %v4float_42
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions())
<< assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody());
auto ast_body = fe.ast_body();
EXPECT_THAT(test::ToString(p->program(), ast_body),
HasSubstr("myvar[2u] = vec4<f32>(42.0, 42.0, 42.0, 42.0);"));
}
TEST_F(SpvParserMemoryTest, EmitStatement_AccessChain_Struct) {
const std::string assembly = Preamble() + R"(
OpName %1 "myvar"
OpMemberName %strct 1 "age"
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%float = OpTypeFloat 32
%float_42 = OpConstant %float 42
%strct = OpTypeStruct %float %float
%elem_ty = OpTypePointer Private %float
%var_ty = OpTypePointer Private %strct
%uint = OpTypeInt 32 0
%uint_1 = OpConstant %uint 1
%1 = OpVariable %var_ty Private
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%2 = OpAccessChain %elem_ty %1 %uint_1
OpStore %2 %float_42
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions())
<< assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody());
auto ast_body = fe.ast_body();
EXPECT_THAT(test::ToString(p->program(), ast_body),
HasSubstr("myvar.age = 42.0;"));
}
TEST_F(SpvParserMemoryTest,
EmitStatement_AccessChain_Struct_DifferOnlyMemberName) {
// The spirv-opt internal representation will map both structs to the
// same canonicalized type, because it doesn't care about member names.
// But we care about member names when producing a member-access expression.
// crbug.com/tint/213
const std::string assembly = Preamble() + R"(
OpName %1 "myvar"
OpName %10 "myvar2"
OpMemberName %strct 1 "age"
OpMemberName %strct2 1 "ancientness"
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%float = OpTypeFloat 32
%float_42 = OpConstant %float 42
%float_420 = OpConstant %float 420
%strct = OpTypeStruct %float %float
%strct2 = OpTypeStruct %float %float
%elem_ty = OpTypePointer Private %float
%var_ty = OpTypePointer Private %strct
%var2_ty = OpTypePointer Private %strct2
%uint = OpTypeInt 32 0
%uint_1 = OpConstant %uint 1
%1 = OpVariable %var_ty Private
%10 = OpVariable %var2_ty Private
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%2 = OpAccessChain %elem_ty %1 %uint_1
OpStore %2 %float_42
%20 = OpAccessChain %elem_ty %10 %uint_1
OpStore %20 %float_420
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions())
<< assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody());
auto ast_body = fe.ast_body();
EXPECT_THAT(test::ToString(p->program(), ast_body),
HasSubstr(R"(myvar.age = 42.0;
myvar2.ancientness = 420.0;
)"));
}
TEST_F(SpvParserMemoryTest, EmitStatement_AccessChain_StructNonConstIndex) {
const std::string assembly = Preamble() + R"(
OpName %1 "myvar"
OpMemberName %55 1 "age"
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%float = OpTypeFloat 32
%float_42 = OpConstant %float 42
%55 = OpTypeStruct %float %float
%elem_ty = OpTypePointer Private %float
%var_ty = OpTypePointer Private %55
%uint = OpTypeInt 32 0
%uint_1 = OpConstant %uint 1
%uint_ptr = OpTypePointer Private %uint
%uintvar = OpVariable %uint_ptr Private
%1 = OpVariable %var_ty Private
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%10 = OpLoad %uint %uintvar
%2 = OpAccessChain %elem_ty %1 %10
OpStore %2 %float_42
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions())
<< assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_FALSE(fe.EmitBody());
EXPECT_THAT(p->error(), Eq("Access chain %2 index %10 is a non-constant "
"index into a structure %55"));
}
TEST_F(SpvParserMemoryTest, EmitStatement_AccessChain_StructConstOutOfBounds) {
const std::string assembly = Preamble() + R"(
OpName %1 "myvar"
OpMemberName %55 1 "age"
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%float = OpTypeFloat 32
%float_42 = OpConstant %float 42
%55 = OpTypeStruct %float %float
%elem_ty = OpTypePointer Private %float
%var_ty = OpTypePointer Private %55
%uint = OpTypeInt 32 0
%uint_99 = OpConstant %uint 99
%1 = OpVariable %var_ty Private
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%2 = OpAccessChain %elem_ty %1 %uint_99
OpStore %2 %float_42
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions())
<< assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_FALSE(fe.EmitBody());
EXPECT_THAT(p->error(), Eq("Access chain %2 index value 99 is out of bounds "
"for structure %55 having 2 members"));
}
TEST_F(SpvParserMemoryTest, EmitStatement_AccessChain_Struct_RuntimeArray) {
const std::string assembly = Preamble() + R"(
OpName %1 "myvar"
OpMemberName %strct 1 "age"
OpDecorate %1 DescriptorSet 0
OpDecorate %1 Binding 0
OpDecorate %strct BufferBlock
OpMemberDecorate %strct 0 Offset 0
OpMemberDecorate %strct 1 Offset 4
OpDecorate %rtarr ArrayStride 4
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%float = OpTypeFloat 32
%float_42 = OpConstant %float 42
%rtarr = OpTypeRuntimeArray %float
%strct = OpTypeStruct %float %rtarr
%elem_ty = OpTypePointer Uniform %float
%var_ty = OpTypePointer Uniform %strct
%uint = OpTypeInt 32 0
%uint_1 = OpConstant %uint 1
%uint_2 = OpConstant %uint 2
%1 = OpVariable %var_ty Uniform
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%2 = OpAccessChain %elem_ty %1 %uint_1 %uint_2
OpStore %2 %float_42
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions())
<< assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody());
auto ast_body = fe.ast_body();
EXPECT_THAT(test::ToString(p->program(), ast_body),
HasSubstr("myvar.age[2u] = 42.0;"));
}
TEST_F(SpvParserMemoryTest, EmitStatement_AccessChain_Compound_Matrix_Vector) {
const std::string assembly = Preamble() + R"(
OpName %1 "myvar"
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%float = OpTypeFloat 32
%v4float = OpTypeVector %float 4
%m3v4float = OpTypeMatrix %v4float 3
%elem_ty = OpTypePointer Private %float
%var_ty = OpTypePointer Private %m3v4float
%uint = OpTypeInt 32 0
%uint_2 = OpConstant %uint 2
%uint_3 = OpConstant %uint 3
%float_42 = OpConstant %float 42
%1 = OpVariable %var_ty Private
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%2 = OpAccessChain %elem_ty %1 %uint_2 %uint_3
OpStore %2 %float_42
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions())
<< assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody());
auto ast_body = fe.ast_body();
EXPECT_THAT(test::ToString(p->program(), ast_body),
HasSubstr("myvar[2u].w = 42.0;"));
}
TEST_F(SpvParserMemoryTest, EmitStatement_AccessChain_InvalidPointeeType) {
const std::string assembly = Preamble() + R"(
OpName %1 "myvar"
%55 = OpTypeVoid
%voidfn = OpTypeFunction %55
%float = OpTypeFloat 32
%60 = OpTypePointer Private %55
%var_ty = OpTypePointer Private %60
%uint = OpTypeInt 32 0
%uint_2 = OpConstant %uint 2
%1 = OpVariable %var_ty Private
%100 = OpFunction %55 None %voidfn
%entry = OpLabel
%2 = OpAccessChain %60 %1 %uint_2
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions())
<< assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_FALSE(fe.EmitBody());
EXPECT_THAT(p->error(),
HasSubstr("Access chain with unknown or invalid pointee type "
"%60: %60 = OpTypePointer Private %55"));
}
TEST_F(SpvParserMemoryTest, EmitStatement_AccessChain_DereferenceBase) {
// The base operand to OpAccessChain may have to be dereferenced first.
// crbug.com/tint/737
const std::string assembly = Preamble() + R"(
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%uint = OpTypeInt 32 0
%v2uint = OpTypeVector %uint 2
%elem_ty = OpTypePointer Private %uint
%vec_ty = OpTypePointer Private %v2uint
%ptrfn = OpTypeFunction %void %vec_ty
%uint_0 = OpConstant %uint 0
; The shortest way to make a pointer example is as a function parameter.
%200 = OpFunction %void None %ptrfn
%1 = OpFunctionParameter %vec_ty
%entry = OpLabel
%2 = OpAccessChain %elem_ty %1 %uint_0
%3 = OpLoad %uint %2
OpReturn
OpFunctionEnd
%100 = OpFunction %void None %voidfn
%main_entry = OpLabel
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModule());
const auto got = test::ToString(p->program());
const std::string expected = R"(fn x_200(x_1 : ptr<private, vec2<u32>>) {
let x_3 : u32 = (*(x_1)).x;
return;
}
fn main_1() {
return;
}
[[stage(fragment)]]
fn main() {
main_1();
}
)";
EXPECT_EQ(got, expected) << got;
}
TEST_F(SpvParserMemoryTest,
EmitStatement_AccessChain_InferFunctionStorageClass) {
// An access chain can have no indices. When the base is a Function variable,
// the reference type has no explicit storage class in the AST representation.
// But the pointer type for the let declaration must have an explicit
// 'function' storage class. From crbug.com/tint/807
const std::string assembly = R"(
OpCapability Shader
OpMemoryModel Logical Simple
OpEntryPoint Fragment %main "main"
OpExecutionMode %main OriginUpperLeft
%uint = OpTypeInt 32 0
%ptr_ty = OpTypePointer Function %uint
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%main = OpFunction %void None %voidfn
%entry = OpLabel
%1 = OpVariable %ptr_ty Function
%2 = OpAccessChain %ptr_ty %1
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModule()) << assembly;
const auto got = test::ToString(p->program());
const std::string expected = R"(fn main_1() {
var x_1 : u32;
let x_2 : ptr<function, u32> = &(x_1);
return;
}
[[stage(fragment)]]
fn main() {
main_1();
}
)";
EXPECT_EQ(got, expected) << got;
}
std::string OldStorageBufferPreamble() {
return Preamble() + R"(
OpName %myvar "myvar"
OpDecorate %myvar DescriptorSet 0
OpDecorate %myvar Binding 0
OpDecorate %struct BufferBlock
OpMemberDecorate %struct 0 Offset 0
OpMemberDecorate %struct 1 Offset 4
OpDecorate %arr ArrayStride 4
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%uint = OpTypeInt 32 0
%uint_0 = OpConstant %uint 0
%uint_1 = OpConstant %uint 1
%arr = OpTypeRuntimeArray %uint
%struct = OpTypeStruct %uint %arr
%ptr_struct = OpTypePointer Uniform %struct
%ptr_uint = OpTypePointer Uniform %uint
%myvar = OpVariable %ptr_struct Uniform
)";
}
TEST_F(SpvParserMemoryTest, RemapStorageBuffer_TypesAndVarDeclarations) {
// Enusure we get the right module-scope declaration. This tests translation
// of the structure type, arrays of the structure, pointers to them, and
// OpVariable of these.
const auto assembly = OldStorageBufferPreamble() + R"(
; The preamble declared %100 to be an entry point, so supply it.
%100 = OpFunction %void None %voidfn
%entry = OpLabel
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions())
<< assembly << p->error();
const auto module_str = test::ToString(p->program());
EXPECT_THAT(module_str, HasSubstr(R"(type RTArr = [[stride(4)]] array<u32>;
[[block]]
struct S {
field0 : u32;
field1 : RTArr;
};
[[group(0), binding(0)]] var<storage, read_write> myvar : S;
)"));
}
TEST_F(SpvParserMemoryTest, RemapStorageBuffer_ThroughAccessChain_NonCascaded) {
const auto assembly = OldStorageBufferPreamble() + R"(
%100 = OpFunction %void None %voidfn
%entry = OpLabel
; the scalar element
%1 = OpAccessChain %ptr_uint %myvar %uint_0
OpStore %1 %uint_0
; element in the runtime array
%2 = OpAccessChain %ptr_uint %myvar %uint_1 %uint_1
OpStore %2 %uint_0
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModule()) << assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody()) << p->error();
auto ast_body = fe.ast_body();
const auto got = test::ToString(p->program(), ast_body);
EXPECT_THAT(got, HasSubstr(R"(myvar.field0 = 0u;
myvar.field1[1u] = 0u;
)"));
}
TEST_F(SpvParserMemoryTest,
RemapStorageBuffer_ThroughAccessChain_NonCascaded_InBoundsAccessChain) {
// Like the previous test, but using OpInBoundsAccessChain.
const auto assembly = OldStorageBufferPreamble() + R"(
%100 = OpFunction %void None %voidfn
%entry = OpLabel
; the scalar element
%1 = OpInBoundsAccessChain %ptr_uint %myvar %uint_0
OpStore %1 %uint_0
; element in the runtime array
%2 = OpInBoundsAccessChain %ptr_uint %myvar %uint_1 %uint_1
OpStore %2 %uint_0
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModule()) << assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody()) << p->error();
auto ast_body = fe.ast_body();
const auto got = test::ToString(p->program(), ast_body);
EXPECT_THAT(got, HasSubstr(R"(myvar.field0 = 0u;
myvar.field1[1u] = 0u;
)")) << got
<< p->error();
}
TEST_F(SpvParserMemoryTest, RemapStorageBuffer_ThroughAccessChain_Cascaded) {
const auto assembly = OldStorageBufferPreamble() + R"(
%ptr_rtarr = OpTypePointer Uniform %arr
%100 = OpFunction %void None %voidfn
%entry = OpLabel
; get the runtime array
%1 = OpAccessChain %ptr_rtarr %myvar %uint_1
; now an element in it
%2 = OpAccessChain %ptr_uint %1 %uint_1
OpStore %2 %uint_0
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModule()) << assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody()) << p->error();
auto ast_body = fe.ast_body();
EXPECT_THAT(test::ToString(p->program(), ast_body),
HasSubstr("myvar.field1[1u] = 0u;"))
<< p->error();
}
TEST_F(SpvParserMemoryTest,
RemapStorageBuffer_ThroughCopyObject_WithoutHoisting) {
// Generates a const declaration directly.
// We have to do a bunch of storage class tracking for locally
// defined values in order to get the right pointer-to-storage-buffer
// value type for the const declration.
const auto assembly = OldStorageBufferPreamble() + R"(
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%1 = OpAccessChain %ptr_uint %myvar %uint_1 %uint_1
%2 = OpCopyObject %ptr_uint %1
OpStore %2 %uint_0
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModule()) << assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody()) << p->error();
auto ast_body = fe.ast_body();
EXPECT_THAT(test::ToString(p->program(), ast_body),
HasSubstr(R"(let x_2 : ptr<storage, u32> = &(myvar.field1[1u]);
*(x_2) = 0u;
)")) << p->error();
p->SkipDumpingPending(
"crbug.com/tint/1041 track access mode in spirv-reader parser type");
}
TEST_F(SpvParserMemoryTest, RemapStorageBuffer_ThroughCopyObject_WithHoisting) {
// TODO(dneto): Hoisting non-storable values (pointers) is not yet supported.
// It's debatable whether this test should run at all.
// crbug.com/tint/98
// Like the previous test, but the declaration for the copy-object
// has its declaration hoisted.
const auto assembly = OldStorageBufferPreamble() + R"(
%bool = OpTypeBool
%cond = OpConstantTrue %bool
%100 = OpFunction %void None %voidfn
%entry = OpLabel
OpSelectionMerge %99 None
OpBranchConditional %cond %20 %30
%20 = OpLabel
%1 = OpAccessChain %ptr_uint %myvar %uint_1 %uint_1
; this definintion dominates the use in %99
%2 = OpCopyObject %ptr_uint %1
OpBranch %99
%30 = OpLabel
OpReturn
%99 = OpLabel
OpStore %2 %uint_0
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModule()) << assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody()) << p->error();
auto ast_body = fe.ast_body();
EXPECT_EQ(test::ToString(p->program(), ast_body),
R"(var x_2 : ptr<storage, u32>;
if (true) {
x_2 = &(myvar.field1[1u]);
} else {
return;
}
x_2 = 0u;
return;
)") << p->error();
p->SkipDumpingPending("crbug.com/tint/98");
}
TEST_F(SpvParserMemoryTest, DISABLED_RemapStorageBuffer_ThroughFunctionCall) {
// WGSL does not support pointer-to-storage-buffer as function parameter
}
TEST_F(SpvParserMemoryTest,
DISABLED_RemapStorageBuffer_ThroughFunctionParameter) {
// WGSL does not support pointer-to-storage-buffer as function parameter
}
std::string RuntimeArrayPreamble() {
return R"(
OpCapability Shader
OpMemoryModel Logical Simple
OpEntryPoint Fragment %100 "main"
OpExecutionMode %100 OriginUpperLeft
OpName %myvar "myvar"
OpMemberName %struct 0 "first"
OpMemberName %struct 1 "rtarr"
OpDecorate %struct Block
OpMemberDecorate %struct 0 Offset 0
OpMemberDecorate %struct 1 Offset 4
OpDecorate %arr ArrayStride 4
OpDecorate %myvar DescriptorSet 0
OpDecorate %myvar Binding 0
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
%uint = OpTypeInt 32 0
%uint_0 = OpConstant %uint 0
%uint_1 = OpConstant %uint 1
%arr = OpTypeRuntimeArray %uint
%struct = OpTypeStruct %uint %arr
%ptr_struct = OpTypePointer StorageBuffer %struct
%ptr_uint = OpTypePointer StorageBuffer %uint
%myvar = OpVariable %ptr_struct StorageBuffer
)";
}
TEST_F(SpvParserMemoryTest, ArrayLength_FromVar) {
const auto assembly = RuntimeArrayPreamble() + R"(
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%1 = OpArrayLength %uint %myvar 1
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModule()) << assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody()) << p->error();
auto ast_body = fe.ast_body();
const auto body_str = test::ToString(p->program(), ast_body);
EXPECT_THAT(body_str,
HasSubstr("let x_1 : u32 = arrayLength(&(myvar.rtarr));"))
<< body_str;
}
TEST_F(SpvParserMemoryTest, ArrayLength_FromCopyObject) {
const auto assembly = RuntimeArrayPreamble() + R"(
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%2 = OpCopyObject %ptr_struct %myvar
%1 = OpArrayLength %uint %2 1
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModule()) << assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody()) << p->error();
auto ast_body = fe.ast_body();
const auto body_str = test::ToString(p->program(), ast_body);
EXPECT_THAT(body_str, HasSubstr(R"(let x_2 : ptr<storage, S> = &(myvar);
let x_1 : u32 = arrayLength(&((*(x_2)).rtarr));
)")) << body_str;
p->SkipDumpingPending(
"crbug.com/tint/1041 track access mode in spirv-reader parser type");
}
TEST_F(SpvParserMemoryTest, ArrayLength_FromAccessChain) {
const auto assembly = RuntimeArrayPreamble() + R"(
%100 = OpFunction %void None %voidfn
%entry = OpLabel
%2 = OpAccessChain %ptr_struct %myvar ; no indices
%1 = OpArrayLength %uint %2 1
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
ASSERT_TRUE(p->BuildAndParseInternalModule()) << assembly << p->error();
auto fe = p->function_emitter(100);
EXPECT_TRUE(fe.EmitBody()) << p->error();
auto ast_body = fe.ast_body();
const auto body_str = test::ToString(p->program(), ast_body);
EXPECT_THAT(body_str,
HasSubstr("let x_1 : u32 = arrayLength(&(myvar.rtarr));"))
<< body_str;
}
std::string InvalidPointerPreamble() {
return R"(
OpCapability Shader
OpMemoryModel Logical Simple
OpEntryPoint Fragment %main "main"
OpExecutionMode %main OriginUpperLeft
%uint = OpTypeInt 32 0
%ptr_ty = OpTypePointer Function %uint
%void = OpTypeVoid
%voidfn = OpTypeFunction %void
)";
}
TEST_F(SpvParserMemoryTest, InvalidPointer_Undef_ModuleScope_IsError) {
const std::string assembly = InvalidPointerPreamble() + R"(
%ptr = OpUndef %ptr_ty
%main = OpFunction %void None %voidfn
%entry = OpLabel
%1 = OpCopyObject %ptr_ty %ptr
%2 = OpAccessChain %ptr_ty %ptr
%3 = OpInBoundsAccessChain %ptr_ty %ptr
; now show the invalid pointer propagates
%10 = OpCopyObject %ptr_ty %1
%20 = OpAccessChain %ptr_ty %2
%30 = OpInBoundsAccessChain %ptr_ty %3
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
EXPECT_FALSE(p->BuildAndParseInternalModule()) << assembly;
EXPECT_EQ(p->error(), "undef pointer is not valid: %9 = OpUndef %6");
}
TEST_F(SpvParserMemoryTest, InvalidPointer_Undef_FunctionScope_IsError) {
const std::string assembly = InvalidPointerPreamble() + R"(
%main = OpFunction %void None %voidfn
%entry = OpLabel
%ptr = OpUndef %ptr_ty
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
EXPECT_FALSE(p->BuildAndParseInternalModule()) << assembly;
EXPECT_EQ(p->error(), "undef pointer is not valid: %7 = OpUndef %3");
}
TEST_F(SpvParserMemoryTest, InvalidPointer_ConstantNull_IsError) {
// OpConstantNull on logical pointer requires variable-pointers, which
// is not (yet) supported by WGSL features.
const std::string assembly = InvalidPointerPreamble() + R"(
%ptr = OpConstantNull %ptr_ty
%main = OpFunction %void None %voidfn
%entry = OpLabel
%1 = OpCopyObject %ptr_ty %ptr
%2 = OpAccessChain %ptr_ty %ptr
%3 = OpInBoundsAccessChain %ptr_ty %ptr
; now show the invalid pointer propagates
%10 = OpCopyObject %ptr_ty %1
%20 = OpAccessChain %ptr_ty %2
%30 = OpInBoundsAccessChain %ptr_ty %3
OpReturn
OpFunctionEnd
)";
auto p = parser(test::Assemble(assembly));
EXPECT_FALSE(p->BuildAndParseInternalModule());
EXPECT_EQ(p->error(), "null pointer is not valid: %9 = OpConstantNull %6");
}
} // namespace
} // namespace spirv
} // namespace reader
} // namespace tint