[hlsl] Start `struct` type emission.

This CL adds the start of struct type emission to the HLSL IR backend.

Bug: 42251045
Change-Id: I3b6be8a1de2ab67cc2ef065ca47e715c36863f49
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/193980
Commit-Queue: dan sinclair <dsinclair@chromium.org>
Reviewed-by: James Price <jrprice@google.com>
diff --git a/src/tint/lang/hlsl/writer/constant_test.cc b/src/tint/lang/hlsl/writer/constant_test.cc
index 9755198..b0c7646 100644
--- a/src/tint/lang/hlsl/writer/constant_test.cc
+++ b/src/tint/lang/hlsl/writer/constant_test.cc
@@ -38,7 +38,8 @@
     f->Block()->Append(b.Return(f, false));
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(bool a() {
+    EXPECT_EQ(output_.hlsl, R"(
+bool a() {
   return false;
 }
 
@@ -54,7 +55,8 @@
     f->Block()->Append(b.Return(f, true));
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(bool a() {
+    EXPECT_EQ(output_.hlsl, R"(
+bool a() {
   return true;
 }
 
@@ -70,7 +72,8 @@
     f->Block()->Append(b.Return(f, -12345_i));
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(int a() {
+    EXPECT_EQ(output_.hlsl, R"(
+int a() {
   return -12345;
 }
 
@@ -86,7 +89,8 @@
     f->Block()->Append(b.Return(f, 56779_u));
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(uint a() {
+    EXPECT_EQ(output_.hlsl, R"(
+uint a() {
   return 56779u;
 }
 
@@ -103,7 +107,8 @@
     f->Block()->Append(b.Return(f, f32((1 << 30) - 4)));
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(float a() {
+    EXPECT_EQ(output_.hlsl, R"(
+float a() {
   return 1073741824.0f;
 }
 
@@ -120,7 +125,8 @@
     f->Block()->Append(b.Return(f, f16((1 << 15) - 8)));
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(float16_t a() {
+    EXPECT_EQ(output_.hlsl, R"(
+float16_t a() {
   return float16_t(32752.0h);
 }
 
@@ -136,7 +142,8 @@
     b.Append(f->Block(), [&] { b.Return(f, b.Composite(ty.vec3<f32>(), 1_f, 2_f, 3_f)); });
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(float3 a() {
+    EXPECT_EQ(output_.hlsl, R"(
+float3 a() {
   return float3(1.0f, 2.0f, 3.0f);
 }
 
@@ -152,7 +159,8 @@
     b.Append(f->Block(), [&] { b.Return(f, b.Composite(ty.vec3<f16>(), 1_h, 2_h, 3_h)); });
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(vector<float16_t, 3> a() {
+    EXPECT_EQ(output_.hlsl, R"(
+vector<float16_t, 3> a() {
   return vector<float16_t, 3>(float16_t(1.0h), float16_t(2.0h), float16_t(3.0h));
 }
 
@@ -168,7 +176,8 @@
     b.Append(f->Block(), [&] { b.Return(f, b.Zero<vec3<f32>>()); });
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(float3 a() {
+    EXPECT_EQ(output_.hlsl, R"(
+float3 a() {
   return (0.0f).xxx;
 }
 
@@ -184,7 +193,8 @@
     b.Append(f->Block(), [&] { b.Return(f, b.Zero<vec3<f16>>()); });
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(vector<float16_t, 3> a() {
+    EXPECT_EQ(output_.hlsl, R"(
+vector<float16_t, 3> a() {
   return (float16_t(0.0h)).xxx;
 }
 
@@ -200,7 +210,8 @@
     b.Append(f->Block(), [&] { b.Return(f, b.Splat(ty.vec3<f32>(), 2_f)); });
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(float3 a() {
+    EXPECT_EQ(output_.hlsl, R"(
+float3 a() {
   return (2.0f).xxx;
 }
 
@@ -216,7 +227,8 @@
     b.Append(f->Block(), [&] { b.Return(f, b.Splat(ty.vec3<f16>(), 2_h)); });
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(vector<float16_t, 3> a() {
+    EXPECT_EQ(output_.hlsl, R"(
+vector<float16_t, 3> a() {
   return (float16_t(2.0h)).xxx;
 }
 
@@ -274,7 +286,8 @@
     b.Append(f->Block(), [&] { b.Return(f, b.Splat(ty.vec3<bool>(), true)); });
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(bool3 a() {
+    EXPECT_EQ(output_.hlsl, R"(
+bool3 a() {
   return (true).xxx;
 }
 
@@ -311,7 +324,8 @@
     b.Append(f->Block(), [&] { b.Return(f, b.Splat(ty.vec3<i32>(), 2_i)); });
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(int3 a() {
+    EXPECT_EQ(output_.hlsl, R"(
+int3 a() {
   return (2).xxx;
 }
 
@@ -327,7 +341,8 @@
     b.Append(f->Block(), [&] { b.Return(f, b.Splat(ty.vec3<u32>(), 2_u)); });
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(uint3 a() {
+    EXPECT_EQ(output_.hlsl, R"(
+uint3 a() {
   return (2u).xxx;
 }
 
@@ -346,7 +361,8 @@
     });
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(float2x3 a() {
+    EXPECT_EQ(output_.hlsl, R"(
+float2x3 a() {
   return float2x3(float3(1.0f, 2.0f, 3.0f), float3(3.0f, 4.0f, 5.0f));
 }
 
@@ -365,7 +381,8 @@
     });
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(matrix<float16_t, 2, 3> a() {
+    EXPECT_EQ(output_.hlsl, R"(
+matrix<float16_t, 2, 3> a() {
   return matrix<float16_t, 2, 3>(vector<float16_t, 3>(float16_t(1.0h), float16_t(2.0h), float16_t(3.0h)), vector<float16_t, 3>(float16_t(3.0h), float16_t(4.0h), float16_t(5.0h)));
 }
 
@@ -391,7 +408,8 @@
     });
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(float4x4 a() {
+    EXPECT_EQ(output_.hlsl, R"(
+float4x4 a() {
   return float4x4(float4(2.0f, 3.0f, 4.0f, 8.0f), (0.0f).xxxx, (7.0f).xxxx, float4(42.0f, 21.0f, 6.0f, -5.0f));
 }
 
@@ -417,7 +435,8 @@
     });
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(matrix<float16_t, 4, 4> a() {
+    EXPECT_EQ(output_.hlsl, R"(
+matrix<float16_t, 4, 4> a() {
   return matrix<float16_t, 4, 4>(vector<float16_t, 4>(float16_t(2.0h), float16_t(3.0h), float16_t(4.0h), float16_t(8.0h)), (float16_t(0.0h)).xxxx, (float16_t(7.0h)).xxxx, vector<float16_t, 4>(float16_t(42.0h), float16_t(21.0h), float16_t(6.0h), float16_t(-5.0h)));
 }
 
@@ -433,7 +452,8 @@
     b.Append(f->Block(), [&] { b.Return(f, b.Zero<mat2x3<f32>>()); });
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(float2x3 a() {
+    EXPECT_EQ(output_.hlsl, R"(
+float2x3 a() {
   return float2x3((0.0f).xxx, (0.0f).xxx);
 }
 
@@ -449,7 +469,8 @@
     b.Append(f->Block(), [&] { b.Return(f, b.Zero<mat2x3<f16>>()); });
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(matrix<float16_t, 2, 3> a() {
+    EXPECT_EQ(output_.hlsl, R"(
+matrix<float16_t, 2, 3> a() {
   return matrix<float16_t, 2, 3>((float16_t(0.0h)).xxx, (float16_t(0.0h)).xxx);
 }
 
@@ -540,7 +561,8 @@
     b.Append(f->Block(), [&] { b.Return(f, b.Zero<array<vec3<f32>, 3>>()); });
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(float3[3] a() {
+    EXPECT_EQ(output_.hlsl, R"(
+float3[3] a() {
   return (float3[3])0;
 }
 
@@ -586,8 +608,7 @@
 )");
 }
 
-// TODO(dsinclair): Needs `struct` constant emission
-TEST_F(HlslWriterTest, DISABLED_ConstantTypeStructEmpty) {
+TEST_F(HlslWriterTest, ConstantTypeStructEmpty) {
     Vector members{
         ty.Get<core::type::StructMember>(b.ir.symbols.New("a"), ty.i32(), 0u, 0u, 4u, 4u,
                                          core::type::StructMemberAttributes{}),
@@ -602,11 +623,11 @@
     b.Append(f->Block(), [&] { b.Return(f, b.Zero(strct)); });
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(struct S
+    EXPECT_EQ(output_.hlsl, R"(struct S {
   int a;
   float b;
   int3 c;
-}
+};
 
 S a() {
   return (S)0;
@@ -619,7 +640,7 @@
 )");
 }
 
-// TODO(dsinclair): Needs `struct` constant emission
+// TODO(dsinclair): Needs `construct` emission
 TEST_F(HlslWriterTest, DISABLED_ConstantTypeStructStatic) {
     Vector members{
         ty.Get<core::type::StructMember>(b.ir.symbols.New("a"), ty.i32(), 0u, 0u, 4u, 4u,
diff --git a/src/tint/lang/hlsl/writer/function_test.cc b/src/tint/lang/hlsl/writer/function_test.cc
index 929c3d9..40af080 100644
--- a/src/tint/lang/hlsl/writer/function_test.cc
+++ b/src/tint/lang/hlsl/writer/function_test.cc
@@ -47,7 +47,8 @@
     func->Block()->Append(b.Return(func));
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"(void foo() {
+    EXPECT_EQ(output_.hlsl, R"(
+void foo() {
 }
 
 [numthreads(1, 1, 1)]
@@ -80,7 +81,8 @@
     func->Block()->Append(b.Return(func));
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"([numthreads(1, 1, 1)]
+    EXPECT_EQ(output_.hlsl, R"(
+[numthreads(1, 1, 1)]
 void main() {
 }
 
@@ -728,7 +730,8 @@
     func->Block()->Append(b.Return(func));
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"([numthreads(1, 1, 1)]
+    EXPECT_EQ(output_.hlsl, R"(
+[numthreads(1, 1, 1)]
 void main() {
 }
 
@@ -743,7 +746,8 @@
     func->Block()->Append(b.Return(func));
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"([numthreads(2, 4, 6)]
+    EXPECT_EQ(output_.hlsl, R"(
+[numthreads(2, 4, 6)]
 void main() {
 }
 
diff --git a/src/tint/lang/hlsl/writer/printer/printer.cc b/src/tint/lang/hlsl/writer/printer/printer.cc
index 6ea7666..813937f 100644
--- a/src/tint/lang/hlsl/writer/printer/printer.cc
+++ b/src/tint/lang/hlsl/writer/printer/printer.cc
@@ -30,15 +30,20 @@
 #include <cmath>
 #include <cstddef>
 #include <cstdint>
+#include <string>
 #include <unordered_map>
+#include <unordered_set>
 #include <utility>
 #include <vector>
 
 #include "src/tint/lang/core/access.h"
 #include "src/tint/lang/core/address_space.h"
+#include "src/tint/lang/core/builtin_value.h"
 #include "src/tint/lang/core/constant/splat.h"
 #include "src/tint/lang/core/constant/value.h"
 #include "src/tint/lang/core/fluent_types.h"
+#include "src/tint/lang/core/interpolation_sampling.h"
+#include "src/tint/lang/core/interpolation_type.h"
 #include "src/tint/lang/core/ir/access.h"
 #include "src/tint/lang/core/ir/bitcast.h"
 #include "src/tint/lang/core/ir/block.h"
@@ -93,6 +98,7 @@
 #include "src/tint/utils/rtti/switch.h"
 #include "src/tint/utils/strconv/float_to_string.h"
 #include "src/tint/utils/text/string.h"
+#include "src/tint/utils/text/string_stream.h"
 
 using namespace tint::core::fluent_types;  // NOLINT
 
@@ -120,7 +126,10 @@
             EmitFunction(func);
         }
 
-        result_.hlsl = main_buffer_.String();
+        StringStream ss;
+        ss << preamble_buffer_.String() << "\n" << main_buffer_.String();
+        result_.hlsl = ss.str();
+
         return std::move(result_);
     }
 
@@ -130,10 +139,15 @@
 
     core::ir::Module& ir_;
 
+    /// The buffer holding preamble text
+    TextBuffer preamble_buffer_;
+
     /// A hashmap of value to name
     Hashmap<const core::ir::Value*, std::string, 32> names_;
     /// Map of builtin structure to unique generated name
     std::unordered_map<const core::type::Struct*, std::string> builtin_struct_names_;
+    /// Set of structs which have been emitted already
+    std::unordered_set<const core::type::Struct*> emitted_structs_;
 
     /// The current function being emitted
     const core::ir::Function* current_function_ = nullptr;
@@ -208,20 +222,15 @@
     }
 
     void EmitVar(const core::ir::Var* var) {
-        auto out = Line();
-
-        // TODO(dsinclair): This isn't right, as some types contain their names
-        EmitType(out, var->Result(0)->Type());
-        out << " ";
-        out << NameOf(var->Result(0));
-
-        out << " = ";
-
         auto* ptr = var->Result(0)->Type()->As<core::type::Pointer>();
         TINT_ASSERT(ptr);
 
         auto space = ptr->AddressSpace();
 
+        auto out = Line();
+        EmitTypeAndName(out, var->Result(0)->Type(), space, ptr->Access(), NameOf(var->Result(0)));
+        out << " = ";
+
         if (var->Initializer()) {
             EmitValue(out, var->Initializer());
         } else if (space == core::AddressSpace::kPrivate ||
@@ -242,12 +251,10 @@
     void EmitLet(const core::ir::Let* l) {
         auto out = Line();
 
-        // TODO(dsinclair): This isn't right, as some types contain their names.
         // TODO(dsinclair): Investigate using `const` here as well, the AST printer doesn't emit
         //                  const with a let, but we should be able to.
-        EmitType(out, l->Result(0)->Type());
-        out << " ";
-        out << NameOf(l->Result(0));
+        EmitTypeAndName(out, l->Result(0)->Type(), core::AddressSpace::kUndefined,
+                        core::Access::kUndefined, NameOf(l->Result(0)));
         out << " = ";
         EmitValue(out, l->Value());
         out << ";";
@@ -497,7 +504,8 @@
             [&](const core::type::U32*) { out << c->ValueAs<AInt>() << "u"; },
             [&](const core::type::Array* a) { EmitConstantArray(out, c, a); },
             [&](const core::type::Vector* v) { EmitConstantVector(out, c, v); },
-            [&](const core::type::Matrix* m) { EmitConstantMatrix(out, c, m); },  //
+            [&](const core::type::Matrix* m) { EmitConstantMatrix(out, c, m); },
+            [&](const core::type::Struct* s) { EmitConstantStruct(out, c, s); },  //
             TINT_ICE_ON_NO_MATCH);
     }
 
@@ -573,6 +581,17 @@
         }
     }
 
+    void EmitConstantStruct(StringStream& out,
+                            const core::constant::Value* c,
+                            const core::type::Struct* s) {
+        EmitStructType(&preamble_buffer_, s);
+
+        if (c->AllZero()) {
+            out << "(" << StructName(s) << ")0";
+            return;
+        }
+    }
+
     void EmitType(StringStream& out,
                   const core::type::Type* ty,
                   core::AddressSpace address_space = core::AddressSpace::kUndefined,
@@ -627,6 +646,19 @@
             TINT_ICE_ON_NO_MATCH);
     }
 
+    void EmitTypeAndName(StringStream& out,
+                         const core::type::Type* type,
+                         core::AddressSpace address_space,
+                         core::Access access,
+                         const std::string& name) {
+        bool name_printed = false;
+        EmitType(out, type, address_space, access, name, &name_printed);
+
+        if (!name.empty() && !name_printed) {
+            out << " " << name;
+        }
+    }
+
     void EmitArrayType(StringStream& out,
                        const core::type::Array* ary,
                        core::AddressSpace address_space,
@@ -768,6 +800,140 @@
         out << "State";
     }
 
+    void EmitStructType(TextBuffer* b, const core::type::Struct* str) {
+        auto it = emitted_structs_.emplace(str);
+        if (!it.second) {
+            return;
+        }
+
+        Line(b) << "struct " << StructName(str) << " {";
+        {
+            const ScopedIndent si(b);
+            for (auto* mem : str->Members()) {
+                auto mem_name = mem->Name().Name();
+                auto* ty = mem->Type();
+                auto out = Line(b);
+                std::string pre, post;
+
+                auto& attributes = mem->Attributes();
+
+                if (auto location = attributes.location) {
+                    auto& pipeline_stage_uses = str->PipelineStageUses();
+                    if (TINT_UNLIKELY(pipeline_stage_uses.Count() != 1)) {
+                        TINT_ICE() << "invalid entry point IO struct uses";
+                    }
+                    if (pipeline_stage_uses.Contains(
+                            core::type::PipelineStageUsage::kVertexInput)) {
+                        post += " : TEXCOORD" + std::to_string(location.value());
+                    } else if (pipeline_stage_uses.Contains(
+                                   core::type::PipelineStageUsage::kVertexOutput)) {
+                        post += " : TEXCOORD" + std::to_string(location.value());
+                    } else if (pipeline_stage_uses.Contains(
+                                   core::type::PipelineStageUsage::kFragmentInput)) {
+                        post += " : TEXCOORD" + std::to_string(location.value());
+                    } else if (TINT_LIKELY(pipeline_stage_uses.Contains(
+                                   core::type::PipelineStageUsage::kFragmentOutput))) {
+                        if (auto blend_src = attributes.blend_src) {
+                            post += " : SV_Target" +
+                                    std::to_string(location.value() + blend_src.value());
+                        } else {
+                            post += " : SV_Target" + std::to_string(location.value());
+                        }
+
+                    } else {
+                        TINT_ICE() << "invalid use of location attribute";
+                    }
+                }
+                if (auto builtin = attributes.builtin) {
+                    auto name = builtin_to_attribute(builtin.value());
+                    TINT_ASSERT(!name.empty());
+
+                    post += " : " + name;
+                }
+                if (auto interpolation = attributes.interpolation) {
+                    auto mod =
+                        interpolation_to_modifiers(interpolation->type, interpolation->sampling);
+                    TINT_ASSERT(!mod.empty());
+
+                    pre += mod;
+                }
+                if (attributes.invariant) {
+                    // Note: `precise` is not exactly the same as `invariant`, but is
+                    // stricter and therefore provides the necessary guarantees.
+                    // See discussion here: https://github.com/gpuweb/gpuweb/issues/893
+                    pre += "precise ";
+                }
+
+                out << pre;
+                EmitTypeAndName(out, ty, core::AddressSpace::kUndefined, core::Access::kReadWrite,
+                                mem_name);
+                out << post << ";";
+            }
+        }
+
+        Line(b) << "};";
+    }
+
+    std::string builtin_to_attribute(core::BuiltinValue builtin) const {
+        switch (builtin) {
+            case core::BuiltinValue::kPosition:
+                return "SV_Position";
+            case core::BuiltinValue::kVertexIndex:
+                return "SV_VertexID";
+            case core::BuiltinValue::kInstanceIndex:
+                return "SV_InstanceID";
+            case core::BuiltinValue::kFrontFacing:
+                return "SV_IsFrontFace";
+            case core::BuiltinValue::kFragDepth:
+                return "SV_Depth";
+            case core::BuiltinValue::kLocalInvocationId:
+                return "SV_GroupThreadID";
+            case core::BuiltinValue::kLocalInvocationIndex:
+                return "SV_GroupIndex";
+            case core::BuiltinValue::kGlobalInvocationId:
+                return "SV_DispatchThreadID";
+            case core::BuiltinValue::kWorkgroupId:
+                return "SV_GroupID";
+            case core::BuiltinValue::kSampleIndex:
+                return "SV_SampleIndex";
+            case core::BuiltinValue::kSampleMask:
+                return "SV_Coverage";
+            default:
+                break;
+        }
+        return "";
+    }
+
+    std::string interpolation_to_modifiers(core::InterpolationType type,
+                                           core::InterpolationSampling sampling) const {
+        std::string modifiers;
+        switch (type) {
+            case core::InterpolationType::kPerspective:
+                modifiers += "linear ";
+                break;
+            case core::InterpolationType::kLinear:
+                modifiers += "noperspective ";
+                break;
+            case core::InterpolationType::kFlat:
+                modifiers += "nointerpolation ";
+                break;
+            case core::InterpolationType::kUndefined:
+                break;
+        }
+        switch (sampling) {
+            case core::InterpolationSampling::kCentroid:
+                modifiers += "centroid ";
+                break;
+            case core::InterpolationSampling::kSample:
+                modifiers += "sample ";
+                break;
+            case core::InterpolationSampling::kCenter:
+            case core::InterpolationSampling::kUndefined:
+                break;
+        }
+        return modifiers;
+    }
+
     /// @returns the name of the given value, creating a new unique name if the value is unnamed in
     /// the module.
     std::string NameOf(const core::ir::Value* value) {
diff --git a/src/tint/lang/hlsl/writer/var_let_test.cc b/src/tint/lang/hlsl/writer/var_let_test.cc
index a0374f5..8677eb1 100644
--- a/src/tint/lang/hlsl/writer/var_let_test.cc
+++ b/src/tint/lang/hlsl/writer/var_let_test.cc
@@ -45,7 +45,8 @@
     });
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"([numthreads(1, 1, 1)]
+    EXPECT_EQ(output_.hlsl, R"(
+[numthreads(1, 1, 1)]
 void main() {
   uint a = 1u;
 }
@@ -62,7 +63,8 @@
     });
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"([numthreads(1, 1, 1)]
+    EXPECT_EQ(output_.hlsl, R"(
+[numthreads(1, 1, 1)]
 void main() {
   float a = 0.0f;
 }
@@ -79,7 +81,8 @@
     });
 
     ASSERT_TRUE(Generate()) << err_ << output_.hlsl;
-    EXPECT_EQ(output_.hlsl, R"([numthreads(1, 1, 1)]
+    EXPECT_EQ(output_.hlsl, R"(
+[numthreads(1, 1, 1)]
 void main() {
   float a = 2.0f;
 }