test: Add E2E test coverage for shader IO

This provides much more complete coverage than what we have in the
unit tests. We now test:
- All builtins, for all stages, both struct and non-struct
- Multiple location attributes for vertex and fragment stages, both
  struct and non-struct
- Mixing builtins and location attributes, whilst mixing struct and
  non-struct
- A few "interesting" cases of IO structs being shared between
  different functions, stages, and with an SSBO variable

There are 7 skipped tests for MSL due to two different MSL bugs which
will be fixed in upcoming patches.

Change-Id: I8b802591762c8ff018e01bf37838551e353162b1
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/53120
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
Auto-Submit: James Price <jrprice@google.com>
diff --git a/test/shader_io/compute_input_builtins.wgsl b/test/shader_io/compute_input_builtins.wgsl
new file mode 100644
index 0000000..b3d6672
--- /dev/null
+++ b/test/shader_io/compute_input_builtins.wgsl
@@ -0,0 +1,14 @@
+[[stage(compute)]]
+fn main(
+  [[builtin(local_invocation_id)]] local_invocation_id : vec3<u32>,
+  [[builtin(local_invocation_index)]] local_invocation_index : u32,
+  [[builtin(global_invocation_id)]] global_invocation_id : vec3<u32>,
+  [[builtin(workgroup_id)]] workgroup_id : vec3<u32>,
+  // TODO(crbug.com/tint/752): [[builtin(num_workgroups)]] num_workgroups : vec3<u32>;
+) {
+  let foo : u32 =
+    local_invocation_id.x +
+    local_invocation_index + 
+    global_invocation_id.x + 
+    workgroup_id.x;
+}
diff --git a/test/shader_io/compute_input_builtins.wgsl.expected.hlsl b/test/shader_io/compute_input_builtins.wgsl.expected.hlsl
new file mode 100644
index 0000000..476144a
--- /dev/null
+++ b/test/shader_io/compute_input_builtins.wgsl.expected.hlsl
@@ -0,0 +1,17 @@
+struct tint_symbol_1 {
+  uint3 local_invocation_id : SV_GroupThreadID;
+  uint local_invocation_index : SV_GroupIndex;
+  uint3 global_invocation_id : SV_DispatchThreadID;
+  uint3 workgroup_id : SV_GroupID;
+};
+
+[numthreads(1, 1, 1)]
+void main(tint_symbol_1 tint_symbol) {
+  const uint3 local_invocation_id = tint_symbol.local_invocation_id;
+  const uint local_invocation_index = tint_symbol.local_invocation_index;
+  const uint3 global_invocation_id = tint_symbol.global_invocation_id;
+  const uint3 workgroup_id = tint_symbol.workgroup_id;
+  const uint foo = (((local_invocation_id.x + local_invocation_index) + global_invocation_id.x) + workgroup_id.x);
+  return;
+}
+
diff --git a/test/shader_io/compute_input_builtins.wgsl.expected.msl b/test/shader_io/compute_input_builtins.wgsl.expected.msl
new file mode 100644
index 0000000..71d62fb
--- /dev/null
+++ b/test/shader_io/compute_input_builtins.wgsl.expected.msl
@@ -0,0 +1 @@
+SKIP: crbug.com/tint/817 attribute only applies to parameters
diff --git a/test/shader_io/compute_input_builtins.wgsl.expected.spvasm b/test/shader_io/compute_input_builtins.wgsl.expected.spvasm
new file mode 100644
index 0000000..b47c180
--- /dev/null
+++ b/test/shader_io/compute_input_builtins.wgsl.expected.spvasm
@@ -0,0 +1,43 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 24
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main" %tint_symbol %tint_symbol_1 %tint_symbol_2 %tint_symbol_3
+               OpExecutionMode %main LocalSize 1 1 1
+               OpName %tint_symbol "tint_symbol"
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %tint_symbol_2 "tint_symbol_2"
+               OpName %tint_symbol_3 "tint_symbol_3"
+               OpName %main "main"
+               OpDecorate %tint_symbol BuiltIn LocalInvocationId
+               OpDecorate %tint_symbol_1 BuiltIn LocalInvocationIndex
+               OpDecorate %tint_symbol_2 BuiltIn GlobalInvocationId
+               OpDecorate %tint_symbol_3 BuiltIn WorkgroupId
+       %uint = OpTypeInt 32 0
+     %v3uint = OpTypeVector %uint 3
+%_ptr_Input_v3uint = OpTypePointer Input %v3uint
+%tint_symbol = OpVariable %_ptr_Input_v3uint Input
+%_ptr_Input_uint = OpTypePointer Input %uint
+%tint_symbol_1 = OpVariable %_ptr_Input_uint Input
+%tint_symbol_2 = OpVariable %_ptr_Input_v3uint Input
+%tint_symbol_3 = OpVariable %_ptr_Input_v3uint Input
+       %void = OpTypeVoid
+          %9 = OpTypeFunction %void
+     %uint_0 = OpConstant %uint 0
+       %main = OpFunction %void None %9
+         %12 = OpLabel
+         %14 = OpAccessChain %_ptr_Input_uint %tint_symbol %uint_0
+         %15 = OpLoad %uint %14
+         %16 = OpLoad %uint %tint_symbol_1
+         %17 = OpIAdd %uint %15 %16
+         %18 = OpAccessChain %_ptr_Input_uint %tint_symbol_2 %uint_0
+         %19 = OpLoad %uint %18
+         %20 = OpIAdd %uint %17 %19
+         %21 = OpAccessChain %_ptr_Input_uint %tint_symbol_3 %uint_0
+         %22 = OpLoad %uint %21
+         %23 = OpIAdd %uint %20 %22
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/compute_input_builtins.wgsl.expected.wgsl b/test/shader_io/compute_input_builtins.wgsl.expected.wgsl
new file mode 100644
index 0000000..95ee85f
--- /dev/null
+++ b/test/shader_io/compute_input_builtins.wgsl.expected.wgsl
@@ -0,0 +1,4 @@
+[[stage(compute)]]
+fn main([[builtin(local_invocation_id)]] local_invocation_id : vec3<u32>, [[builtin(local_invocation_index)]] local_invocation_index : u32, [[builtin(global_invocation_id)]] global_invocation_id : vec3<u32>, [[builtin(workgroup_id)]] workgroup_id : vec3<u32>) {
+  let foo : u32 = (((local_invocation_id.x + local_invocation_index) + global_invocation_id.x) + workgroup_id.x);
+}
diff --git a/test/shader_io/compute_input_builtins_struct.wgsl b/test/shader_io/compute_input_builtins_struct.wgsl
new file mode 100644
index 0000000..17f15b2
--- /dev/null
+++ b/test/shader_io/compute_input_builtins_struct.wgsl
@@ -0,0 +1,16 @@
+struct ComputeInputs {
+  [[builtin(local_invocation_id)]] local_invocation_id : vec3<u32>;
+  [[builtin(local_invocation_index)]] local_invocation_index : u32;
+  [[builtin(global_invocation_id)]] global_invocation_id : vec3<u32>;
+  [[builtin(workgroup_id)]] workgroup_id : vec3<u32>;
+  // TODO(crbug.com/tint/752): [[builtin(num_workgroups)]] num_workgroups : vec3<u32>;
+};
+
+[[stage(compute)]]
+fn main(inputs : ComputeInputs) {
+  let foo : u32 =
+    inputs.local_invocation_id.x +
+    inputs.local_invocation_index + 
+    inputs.global_invocation_id.x + 
+    inputs.workgroup_id.x;
+}
diff --git a/test/shader_io/compute_input_builtins_struct.wgsl.expected.hlsl b/test/shader_io/compute_input_builtins_struct.wgsl.expected.hlsl
new file mode 100644
index 0000000..a33806b
--- /dev/null
+++ b/test/shader_io/compute_input_builtins_struct.wgsl.expected.hlsl
@@ -0,0 +1,20 @@
+struct ComputeInputs {
+  uint3 local_invocation_id;
+  uint local_invocation_index;
+  uint3 global_invocation_id;
+  uint3 workgroup_id;
+};
+struct tint_symbol_1 {
+  uint3 local_invocation_id : SV_GroupThreadID;
+  uint local_invocation_index : SV_GroupIndex;
+  uint3 global_invocation_id : SV_DispatchThreadID;
+  uint3 workgroup_id : SV_GroupID;
+};
+
+[numthreads(1, 1, 1)]
+void main(tint_symbol_1 tint_symbol) {
+  const ComputeInputs inputs = {tint_symbol.local_invocation_id, tint_symbol.local_invocation_index, tint_symbol.global_invocation_id, tint_symbol.workgroup_id};
+  const uint foo = (((inputs.local_invocation_id.x + inputs.local_invocation_index) + inputs.global_invocation_id.x) + inputs.workgroup_id.x);
+  return;
+}
+
diff --git a/test/shader_io/compute_input_builtins_struct.wgsl.expected.msl b/test/shader_io/compute_input_builtins_struct.wgsl.expected.msl
new file mode 100644
index 0000000..71d62fb
--- /dev/null
+++ b/test/shader_io/compute_input_builtins_struct.wgsl.expected.msl
@@ -0,0 +1 @@
+SKIP: crbug.com/tint/817 attribute only applies to parameters
diff --git a/test/shader_io/compute_input_builtins_struct.wgsl.expected.spvasm b/test/shader_io/compute_input_builtins_struct.wgsl.expected.spvasm
new file mode 100644
index 0000000..f54703d
--- /dev/null
+++ b/test/shader_io/compute_input_builtins_struct.wgsl.expected.spvasm
@@ -0,0 +1,57 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 29
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main" %tint_symbol %tint_symbol_1 %tint_symbol_2 %tint_symbol_3
+               OpExecutionMode %main LocalSize 1 1 1
+               OpName %tint_symbol "tint_symbol"
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %tint_symbol_2 "tint_symbol_2"
+               OpName %tint_symbol_3 "tint_symbol_3"
+               OpName %main "main"
+               OpName %ComputeInputs "ComputeInputs"
+               OpMemberName %ComputeInputs 0 "local_invocation_id"
+               OpMemberName %ComputeInputs 1 "local_invocation_index"
+               OpMemberName %ComputeInputs 2 "global_invocation_id"
+               OpMemberName %ComputeInputs 3 "workgroup_id"
+               OpDecorate %tint_symbol BuiltIn LocalInvocationId
+               OpDecorate %tint_symbol_1 BuiltIn LocalInvocationIndex
+               OpDecorate %tint_symbol_2 BuiltIn GlobalInvocationId
+               OpDecorate %tint_symbol_3 BuiltIn WorkgroupId
+               OpMemberDecorate %ComputeInputs 0 Offset 0
+               OpMemberDecorate %ComputeInputs 1 Offset 12
+               OpMemberDecorate %ComputeInputs 2 Offset 16
+               OpMemberDecorate %ComputeInputs 3 Offset 32
+       %uint = OpTypeInt 32 0
+     %v3uint = OpTypeVector %uint 3
+%_ptr_Input_v3uint = OpTypePointer Input %v3uint
+%tint_symbol = OpVariable %_ptr_Input_v3uint Input
+%_ptr_Input_uint = OpTypePointer Input %uint
+%tint_symbol_1 = OpVariable %_ptr_Input_uint Input
+%tint_symbol_2 = OpVariable %_ptr_Input_v3uint Input
+%tint_symbol_3 = OpVariable %_ptr_Input_v3uint Input
+       %void = OpTypeVoid
+          %9 = OpTypeFunction %void
+%ComputeInputs = OpTypeStruct %v3uint %uint %v3uint %v3uint
+       %main = OpFunction %void None %9
+         %12 = OpLabel
+         %14 = OpLoad %v3uint %tint_symbol
+         %15 = OpLoad %uint %tint_symbol_1
+         %16 = OpLoad %v3uint %tint_symbol_2
+         %17 = OpLoad %v3uint %tint_symbol_3
+         %18 = OpCompositeConstruct %ComputeInputs %14 %15 %16 %17
+         %19 = OpCompositeExtract %v3uint %18 0
+         %20 = OpCompositeExtract %uint %19 0
+         %21 = OpCompositeExtract %uint %18 1
+         %22 = OpIAdd %uint %20 %21
+         %23 = OpCompositeExtract %v3uint %18 2
+         %24 = OpCompositeExtract %uint %23 0
+         %25 = OpIAdd %uint %22 %24
+         %26 = OpCompositeExtract %v3uint %18 3
+         %27 = OpCompositeExtract %uint %26 0
+         %28 = OpIAdd %uint %25 %27
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/compute_input_builtins_struct.wgsl.expected.wgsl b/test/shader_io/compute_input_builtins_struct.wgsl.expected.wgsl
new file mode 100644
index 0000000..c7f51b9
--- /dev/null
+++ b/test/shader_io/compute_input_builtins_struct.wgsl.expected.wgsl
@@ -0,0 +1,15 @@
+struct ComputeInputs {
+  [[builtin(local_invocation_id)]]
+  local_invocation_id : vec3<u32>;
+  [[builtin(local_invocation_index)]]
+  local_invocation_index : u32;
+  [[builtin(global_invocation_id)]]
+  global_invocation_id : vec3<u32>;
+  [[builtin(workgroup_id)]]
+  workgroup_id : vec3<u32>;
+};
+
+[[stage(compute)]]
+fn main(inputs : ComputeInputs) {
+  let foo : u32 = (((inputs.local_invocation_id.x + inputs.local_invocation_index) + inputs.global_invocation_id.x) + inputs.workgroup_id.x);
+}
diff --git a/test/shader_io/compute_input_mixed.wgsl b/test/shader_io/compute_input_mixed.wgsl
new file mode 100644
index 0000000..e9ccaaa
--- /dev/null
+++ b/test/shader_io/compute_input_mixed.wgsl
@@ -0,0 +1,20 @@
+struct ComputeInputs0 {
+  [[builtin(local_invocation_id)]] local_invocation_id : vec3<u32>;
+};
+struct ComputeInputs1 {
+  [[builtin(workgroup_id)]] workgroup_id : vec3<u32>;
+};
+
+[[stage(compute)]]
+fn main(
+  inputs0 : ComputeInputs0,
+  [[builtin(local_invocation_index)]] local_invocation_index : u32,
+  [[builtin(global_invocation_id)]] global_invocation_id : vec3<u32>,
+  inputs1 : ComputeInputs1,
+) {
+  let foo : u32 =
+    inputs0.local_invocation_id.x +
+    local_invocation_index + 
+    global_invocation_id.x + 
+    inputs1.workgroup_id.x;
+}
diff --git a/test/shader_io/compute_input_mixed.wgsl.expected.hlsl b/test/shader_io/compute_input_mixed.wgsl.expected.hlsl
new file mode 100644
index 0000000..f9a8369
--- /dev/null
+++ b/test/shader_io/compute_input_mixed.wgsl.expected.hlsl
@@ -0,0 +1,23 @@
+struct ComputeInputs0 {
+  uint3 local_invocation_id;
+};
+struct ComputeInputs1 {
+  uint3 workgroup_id;
+};
+struct tint_symbol_1 {
+  uint3 local_invocation_id : SV_GroupThreadID;
+  uint local_invocation_index : SV_GroupIndex;
+  uint3 global_invocation_id : SV_DispatchThreadID;
+  uint3 workgroup_id : SV_GroupID;
+};
+
+[numthreads(1, 1, 1)]
+void main(tint_symbol_1 tint_symbol) {
+  const ComputeInputs0 inputs0 = {tint_symbol.local_invocation_id};
+  const uint local_invocation_index = tint_symbol.local_invocation_index;
+  const uint3 global_invocation_id = tint_symbol.global_invocation_id;
+  const ComputeInputs1 inputs1 = {tint_symbol.workgroup_id};
+  const uint foo = (((inputs0.local_invocation_id.x + local_invocation_index) + global_invocation_id.x) + inputs1.workgroup_id.x);
+  return;
+}
+
diff --git a/test/shader_io/compute_input_mixed.wgsl.expected.msl b/test/shader_io/compute_input_mixed.wgsl.expected.msl
new file mode 100644
index 0000000..71d62fb
--- /dev/null
+++ b/test/shader_io/compute_input_mixed.wgsl.expected.msl
@@ -0,0 +1 @@
+SKIP: crbug.com/tint/817 attribute only applies to parameters
diff --git a/test/shader_io/compute_input_mixed.wgsl.expected.spvasm b/test/shader_io/compute_input_mixed.wgsl.expected.spvasm
new file mode 100644
index 0000000..145635e
--- /dev/null
+++ b/test/shader_io/compute_input_mixed.wgsl.expected.spvasm
@@ -0,0 +1,55 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 30
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %main "main" %tint_symbol %tint_symbol_4 %tint_symbol_2 %tint_symbol_3
+               OpExecutionMode %main LocalSize 1 1 1
+               OpName %tint_symbol "tint_symbol"
+               OpName %tint_symbol_2 "tint_symbol_2"
+               OpName %tint_symbol_3 "tint_symbol_3"
+               OpName %tint_symbol_4 "tint_symbol_4"
+               OpName %main "main"
+               OpName %ComputeInputs0 "ComputeInputs0"
+               OpMemberName %ComputeInputs0 0 "local_invocation_id"
+               OpName %ComputeInputs1 "ComputeInputs1"
+               OpMemberName %ComputeInputs1 0 "workgroup_id"
+               OpDecorate %tint_symbol BuiltIn LocalInvocationId
+               OpDecorate %tint_symbol_2 BuiltIn LocalInvocationIndex
+               OpDecorate %tint_symbol_3 BuiltIn GlobalInvocationId
+               OpDecorate %tint_symbol_4 BuiltIn WorkgroupId
+               OpMemberDecorate %ComputeInputs0 0 Offset 0
+               OpMemberDecorate %ComputeInputs1 0 Offset 0
+       %uint = OpTypeInt 32 0
+     %v3uint = OpTypeVector %uint 3
+%_ptr_Input_v3uint = OpTypePointer Input %v3uint
+%tint_symbol = OpVariable %_ptr_Input_v3uint Input
+%_ptr_Input_uint = OpTypePointer Input %uint
+%tint_symbol_2 = OpVariable %_ptr_Input_uint Input
+%tint_symbol_3 = OpVariable %_ptr_Input_v3uint Input
+%tint_symbol_4 = OpVariable %_ptr_Input_v3uint Input
+       %void = OpTypeVoid
+          %9 = OpTypeFunction %void
+%ComputeInputs0 = OpTypeStruct %v3uint
+%ComputeInputs1 = OpTypeStruct %v3uint
+     %uint_0 = OpConstant %uint 0
+       %main = OpFunction %void None %9
+         %12 = OpLabel
+         %14 = OpLoad %v3uint %tint_symbol
+         %15 = OpCompositeConstruct %ComputeInputs0 %14
+         %17 = OpLoad %v3uint %tint_symbol_4
+         %18 = OpCompositeConstruct %ComputeInputs1 %17
+         %19 = OpCompositeExtract %v3uint %15 0
+         %20 = OpCompositeExtract %uint %19 0
+         %21 = OpLoad %uint %tint_symbol_2
+         %22 = OpIAdd %uint %20 %21
+         %24 = OpAccessChain %_ptr_Input_uint %tint_symbol_3 %uint_0
+         %25 = OpLoad %uint %24
+         %26 = OpIAdd %uint %22 %25
+         %27 = OpCompositeExtract %v3uint %18 0
+         %28 = OpCompositeExtract %uint %27 0
+         %29 = OpIAdd %uint %26 %28
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/compute_input_mixed.wgsl.expected.wgsl b/test/shader_io/compute_input_mixed.wgsl.expected.wgsl
new file mode 100644
index 0000000..2233b53
--- /dev/null
+++ b/test/shader_io/compute_input_mixed.wgsl.expected.wgsl
@@ -0,0 +1,14 @@
+struct ComputeInputs0 {
+  [[builtin(local_invocation_id)]]
+  local_invocation_id : vec3<u32>;
+};
+
+struct ComputeInputs1 {
+  [[builtin(workgroup_id)]]
+  workgroup_id : vec3<u32>;
+};
+
+[[stage(compute)]]
+fn main(inputs0 : ComputeInputs0, [[builtin(local_invocation_index)]] local_invocation_index : u32, [[builtin(global_invocation_id)]] global_invocation_id : vec3<u32>, inputs1 : ComputeInputs1) {
+  let foo : u32 = (((inputs0.local_invocation_id.x + local_invocation_index) + global_invocation_id.x) + inputs1.workgroup_id.x);
+}
diff --git a/test/shader_io/fragment_input_builtins.wgsl b/test/shader_io/fragment_input_builtins.wgsl
new file mode 100644
index 0000000..0395bfd
--- /dev/null
+++ b/test/shader_io/fragment_input_builtins.wgsl
@@ -0,0 +1,12 @@
+[[stage(fragment)]]
+fn main(
+  [[builtin(position)]] position : vec4<f32>,
+  [[builtin(front_facing)]] front_facing : bool,
+  [[builtin(sample_index)]] sample_index : u32,
+  [[builtin(sample_mask)]] sample_mask : u32,
+) {
+  if (front_facing) {
+    let foo : vec4<f32> = position;
+    let bar : u32 = sample_index + sample_mask;
+  }
+}
diff --git a/test/shader_io/fragment_input_builtins.wgsl.expected.hlsl b/test/shader_io/fragment_input_builtins.wgsl.expected.hlsl
new file mode 100644
index 0000000..461ac3f
--- /dev/null
+++ b/test/shader_io/fragment_input_builtins.wgsl.expected.hlsl
@@ -0,0 +1,19 @@
+struct tint_symbol_1 {
+  float4 position : SV_Position;
+  bool front_facing : SV_IsFrontFace;
+  uint sample_index : SV_SampleIndex;
+  uint sample_mask : SV_Coverage;
+};
+
+void main(tint_symbol_1 tint_symbol) {
+  const float4 position = tint_symbol.position;
+  const bool front_facing = tint_symbol.front_facing;
+  const uint sample_index = tint_symbol.sample_index;
+  const uint sample_mask = tint_symbol.sample_mask;
+  if (front_facing) {
+    const float4 foo = position;
+    const uint bar = (sample_index + sample_mask);
+  }
+  return;
+}
+
diff --git a/test/shader_io/fragment_input_builtins.wgsl.expected.msl b/test/shader_io/fragment_input_builtins.wgsl.expected.msl
new file mode 100644
index 0000000..3e46c6f
--- /dev/null
+++ b/test/shader_io/fragment_input_builtins.wgsl.expected.msl
@@ -0,0 +1,22 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct tint_symbol_2 {
+  float4 position [[position]];
+  bool front_facing [[front_facing]];
+  uint sample_index [[sample_id]];
+  uint sample_mask [[sample_mask]];
+};
+
+fragment void tint_symbol(tint_symbol_2 tint_symbol_1 [[stage_in]]) {
+  float4 const position = tint_symbol_1.position;
+  bool const front_facing = tint_symbol_1.front_facing;
+  uint const sample_index = tint_symbol_1.sample_index;
+  uint const sample_mask = tint_symbol_1.sample_mask;
+  if (front_facing) {
+    float4 const foo = position;
+    uint const bar = (sample_index + sample_mask);
+  }
+  return;
+}
+
diff --git a/test/shader_io/fragment_input_builtins.wgsl.expected.spvasm b/test/shader_io/fragment_input_builtins.wgsl.expected.spvasm
new file mode 100644
index 0000000..b6e26bd
--- /dev/null
+++ b/test/shader_io/fragment_input_builtins.wgsl.expected.spvasm
@@ -0,0 +1,53 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 29
+; Schema: 0
+               OpCapability Shader
+               OpCapability SampleRateShading
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main" %tint_symbol_1 %tint_symbol %tint_symbol_2 %tint_symbol_3
+               OpExecutionMode %main OriginUpperLeft
+               OpName %tint_symbol "tint_symbol"
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %tint_symbol_2 "tint_symbol_2"
+               OpName %tint_symbol_3 "tint_symbol_3"
+               OpName %main "main"
+               OpDecorate %tint_symbol BuiltIn FragCoord
+               OpDecorate %tint_symbol_1 BuiltIn FrontFacing
+               OpDecorate %tint_symbol_2 BuiltIn SampleId
+               OpDecorate %_arr_uint_uint_1 ArrayStride 4
+               OpDecorate %tint_symbol_3 BuiltIn SampleMask
+      %float = OpTypeFloat 32
+    %v4float = OpTypeVector %float 4
+%_ptr_Input_v4float = OpTypePointer Input %v4float
+%tint_symbol = OpVariable %_ptr_Input_v4float Input
+       %bool = OpTypeBool
+%_ptr_Input_bool = OpTypePointer Input %bool
+%tint_symbol_1 = OpVariable %_ptr_Input_bool Input
+       %uint = OpTypeInt 32 0
+%_ptr_Input_uint = OpTypePointer Input %uint
+%tint_symbol_2 = OpVariable %_ptr_Input_uint Input
+     %uint_1 = OpConstant %uint 1
+%_arr_uint_uint_1 = OpTypeArray %uint %uint_1
+%_ptr_Input__arr_uint_uint_1 = OpTypePointer Input %_arr_uint_uint_1
+%tint_symbol_3 = OpVariable %_ptr_Input__arr_uint_uint_1 Input
+       %void = OpTypeVoid
+         %15 = OpTypeFunction %void
+        %int = OpTypeInt 32 1
+      %int_0 = OpConstant %int 0
+       %main = OpFunction %void None %15
+         %18 = OpLabel
+         %19 = OpLoad %bool %tint_symbol_1
+               OpSelectionMerge %20 None
+               OpBranchConditional %19 %21 %20
+         %21 = OpLabel
+         %22 = OpLoad %v4float %tint_symbol
+         %23 = OpLoad %uint %tint_symbol_2
+         %26 = OpAccessChain %_ptr_Input_uint %tint_symbol_3 %int_0
+         %27 = OpLoad %uint %26
+         %28 = OpIAdd %uint %23 %27
+               OpBranch %20
+         %20 = OpLabel
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/fragment_input_builtins.wgsl.expected.wgsl b/test/shader_io/fragment_input_builtins.wgsl.expected.wgsl
new file mode 100644
index 0000000..cfad6db
--- /dev/null
+++ b/test/shader_io/fragment_input_builtins.wgsl.expected.wgsl
@@ -0,0 +1,7 @@
+[[stage(fragment)]]
+fn main([[builtin(position)]] position : vec4<f32>, [[builtin(front_facing)]] front_facing : bool, [[builtin(sample_index)]] sample_index : u32, [[builtin(sample_mask)]] sample_mask : u32) {
+  if (front_facing) {
+    let foo : vec4<f32> = position;
+    let bar : u32 = (sample_index + sample_mask);
+  }
+}
diff --git a/test/shader_io/fragment_input_builtins_struct.wgsl b/test/shader_io/fragment_input_builtins_struct.wgsl
new file mode 100644
index 0000000..af9e0d3
--- /dev/null
+++ b/test/shader_io/fragment_input_builtins_struct.wgsl
@@ -0,0 +1,14 @@
+struct FragmentInputs {
+  [[builtin(position)]] position : vec4<f32>;
+  [[builtin(front_facing)]] front_facing : bool;
+  [[builtin(sample_index)]] sample_index : u32;
+  [[builtin(sample_mask)]] sample_mask : u32;
+};
+
+[[stage(fragment)]]
+fn main(inputs : FragmentInputs) {
+  if (inputs.front_facing) {
+    let foo : vec4<f32> = inputs.position;
+    let bar : u32 = inputs.sample_index + inputs.sample_mask;
+  }
+}
diff --git a/test/shader_io/fragment_input_builtins_struct.wgsl.expected.hlsl b/test/shader_io/fragment_input_builtins_struct.wgsl.expected.hlsl
new file mode 100644
index 0000000..21e09a5
--- /dev/null
+++ b/test/shader_io/fragment_input_builtins_struct.wgsl.expected.hlsl
@@ -0,0 +1,22 @@
+struct FragmentInputs {
+  float4 position;
+  bool front_facing;
+  uint sample_index;
+  uint sample_mask;
+};
+struct tint_symbol_1 {
+  float4 position : SV_Position;
+  bool front_facing : SV_IsFrontFace;
+  uint sample_index : SV_SampleIndex;
+  uint sample_mask : SV_Coverage;
+};
+
+void main(tint_symbol_1 tint_symbol) {
+  const FragmentInputs inputs = {tint_symbol.position, tint_symbol.front_facing, tint_symbol.sample_index, tint_symbol.sample_mask};
+  if (inputs.front_facing) {
+    const float4 foo = inputs.position;
+    const uint bar = (inputs.sample_index + inputs.sample_mask);
+  }
+  return;
+}
+
diff --git a/test/shader_io/fragment_input_builtins_struct.wgsl.expected.msl b/test/shader_io/fragment_input_builtins_struct.wgsl.expected.msl
new file mode 100644
index 0000000..5b5ed67
--- /dev/null
+++ b/test/shader_io/fragment_input_builtins_struct.wgsl.expected.msl
@@ -0,0 +1,25 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct FragmentInputs {
+  float4 position;
+  bool front_facing;
+  uint sample_index;
+  uint sample_mask;
+};
+struct tint_symbol_2 {
+  float4 position [[position]];
+  bool front_facing [[front_facing]];
+  uint sample_index [[sample_id]];
+  uint sample_mask [[sample_mask]];
+};
+
+fragment void tint_symbol(tint_symbol_2 tint_symbol_1 [[stage_in]]) {
+  FragmentInputs const inputs = {tint_symbol_1.position, tint_symbol_1.front_facing, tint_symbol_1.sample_index, tint_symbol_1.sample_mask};
+  if (inputs.front_facing) {
+    float4 const foo = inputs.position;
+    uint const bar = (inputs.sample_index + inputs.sample_mask);
+  }
+  return;
+}
+
diff --git a/test/shader_io/fragment_input_builtins_struct.wgsl.expected.spvasm b/test/shader_io/fragment_input_builtins_struct.wgsl.expected.spvasm
new file mode 100644
index 0000000..16ace31
--- /dev/null
+++ b/test/shader_io/fragment_input_builtins_struct.wgsl.expected.spvasm
@@ -0,0 +1,68 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 35
+; Schema: 0
+               OpCapability Shader
+               OpCapability SampleRateShading
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main" %tint_symbol %tint_symbol_1 %tint_symbol_2 %tint_symbol_3
+               OpExecutionMode %main OriginUpperLeft
+               OpName %tint_symbol "tint_symbol"
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %tint_symbol_2 "tint_symbol_2"
+               OpName %tint_symbol_3 "tint_symbol_3"
+               OpName %main "main"
+               OpName %FragmentInputs "FragmentInputs"
+               OpMemberName %FragmentInputs 0 "position"
+               OpMemberName %FragmentInputs 1 "front_facing"
+               OpMemberName %FragmentInputs 2 "sample_index"
+               OpMemberName %FragmentInputs 3 "sample_mask"
+               OpDecorate %tint_symbol BuiltIn FragCoord
+               OpDecorate %tint_symbol_1 BuiltIn FrontFacing
+               OpDecorate %tint_symbol_2 BuiltIn SampleId
+               OpDecorate %_arr_uint_uint_1 ArrayStride 4
+               OpDecorate %tint_symbol_3 BuiltIn SampleMask
+               OpMemberDecorate %FragmentInputs 0 Offset 0
+               OpMemberDecorate %FragmentInputs 1 Offset 16
+               OpMemberDecorate %FragmentInputs 2 Offset 20
+               OpMemberDecorate %FragmentInputs 3 Offset 24
+      %float = OpTypeFloat 32
+    %v4float = OpTypeVector %float 4
+%_ptr_Input_v4float = OpTypePointer Input %v4float
+%tint_symbol = OpVariable %_ptr_Input_v4float Input
+       %bool = OpTypeBool
+%_ptr_Input_bool = OpTypePointer Input %bool
+%tint_symbol_1 = OpVariable %_ptr_Input_bool Input
+       %uint = OpTypeInt 32 0
+%_ptr_Input_uint = OpTypePointer Input %uint
+%tint_symbol_2 = OpVariable %_ptr_Input_uint Input
+     %uint_1 = OpConstant %uint 1
+%_arr_uint_uint_1 = OpTypeArray %uint %uint_1
+%_ptr_Input__arr_uint_uint_1 = OpTypePointer Input %_arr_uint_uint_1
+%tint_symbol_3 = OpVariable %_ptr_Input__arr_uint_uint_1 Input
+       %void = OpTypeVoid
+         %15 = OpTypeFunction %void
+%FragmentInputs = OpTypeStruct %v4float %bool %uint %uint
+        %int = OpTypeInt 32 1
+      %int_0 = OpConstant %int 0
+       %main = OpFunction %void None %15
+         %18 = OpLabel
+         %20 = OpLoad %v4float %tint_symbol
+         %21 = OpLoad %bool %tint_symbol_1
+         %22 = OpLoad %uint %tint_symbol_2
+         %25 = OpAccessChain %_ptr_Input_uint %tint_symbol_3 %int_0
+         %26 = OpLoad %uint %25
+         %27 = OpCompositeConstruct %FragmentInputs %20 %21 %22 %26
+         %28 = OpCompositeExtract %bool %27 1
+               OpSelectionMerge %29 None
+               OpBranchConditional %28 %30 %29
+         %30 = OpLabel
+         %31 = OpCompositeExtract %v4float %27 0
+         %32 = OpCompositeExtract %uint %27 2
+         %33 = OpCompositeExtract %uint %27 3
+         %34 = OpIAdd %uint %32 %33
+               OpBranch %29
+         %29 = OpLabel
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/fragment_input_builtins_struct.wgsl.expected.wgsl b/test/shader_io/fragment_input_builtins_struct.wgsl.expected.wgsl
new file mode 100644
index 0000000..0cf1aeb
--- /dev/null
+++ b/test/shader_io/fragment_input_builtins_struct.wgsl.expected.wgsl
@@ -0,0 +1,18 @@
+struct FragmentInputs {
+  [[builtin(position)]]
+  position : vec4<f32>;
+  [[builtin(front_facing)]]
+  front_facing : bool;
+  [[builtin(sample_index)]]
+  sample_index : u32;
+  [[builtin(sample_mask)]]
+  sample_mask : u32;
+};
+
+[[stage(fragment)]]
+fn main(inputs : FragmentInputs) {
+  if (inputs.front_facing) {
+    let foo : vec4<f32> = inputs.position;
+    let bar : u32 = (inputs.sample_index + inputs.sample_mask);
+  }
+}
diff --git a/test/shader_io/fragment_input_locations.wgsl b/test/shader_io/fragment_input_locations.wgsl
new file mode 100644
index 0000000..09aa769
--- /dev/null
+++ b/test/shader_io/fragment_input_locations.wgsl
@@ -0,0 +1,12 @@
+[[stage(fragment)]]
+fn main(
+  [[location(0)]] loc0 : i32,
+  [[location(1)]] loc1 : u32,
+  [[location(2)]] loc2 : f32,
+  [[location(3)]] loc3 : vec4<f32>,
+) {
+  let i : i32 = loc0;
+  let u : u32 = loc1;
+  let f : f32 = loc2;
+  let v : vec4<f32> = loc3;
+}
diff --git a/test/shader_io/fragment_input_locations.wgsl.expected.hlsl b/test/shader_io/fragment_input_locations.wgsl.expected.hlsl
new file mode 100644
index 0000000..6f08c68
--- /dev/null
+++ b/test/shader_io/fragment_input_locations.wgsl.expected.hlsl
@@ -0,0 +1,19 @@
+struct tint_symbol_1 {
+  int loc0 : TEXCOORD0;
+  uint loc1 : TEXCOORD1;
+  float loc2 : TEXCOORD2;
+  float4 loc3 : TEXCOORD3;
+};
+
+void main(tint_symbol_1 tint_symbol) {
+  const int loc0 = tint_symbol.loc0;
+  const uint loc1 = tint_symbol.loc1;
+  const float loc2 = tint_symbol.loc2;
+  const float4 loc3 = tint_symbol.loc3;
+  const int i = loc0;
+  const uint u = loc1;
+  const float f = loc2;
+  const float4 v = loc3;
+  return;
+}
+
diff --git a/test/shader_io/fragment_input_locations.wgsl.expected.msl b/test/shader_io/fragment_input_locations.wgsl.expected.msl
new file mode 100644
index 0000000..c8014ad
--- /dev/null
+++ b/test/shader_io/fragment_input_locations.wgsl.expected.msl
@@ -0,0 +1,22 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct tint_symbol_2 {
+  int loc0 [[user(locn0)]];
+  uint loc1 [[user(locn1)]];
+  float loc2 [[user(locn2)]];
+  float4 loc3 [[user(locn3)]];
+};
+
+fragment void tint_symbol(tint_symbol_2 tint_symbol_1 [[stage_in]]) {
+  int const loc0 = tint_symbol_1.loc0;
+  uint const loc1 = tint_symbol_1.loc1;
+  float const loc2 = tint_symbol_1.loc2;
+  float4 const loc3 = tint_symbol_1.loc3;
+  int const i = loc0;
+  uint const u = loc1;
+  float const f = loc2;
+  float4 const v = loc3;
+  return;
+}
+
diff --git a/test/shader_io/fragment_input_locations.wgsl.expected.spvasm b/test/shader_io/fragment_input_locations.wgsl.expected.spvasm
new file mode 100644
index 0000000..98e3d29
--- /dev/null
+++ b/test/shader_io/fragment_input_locations.wgsl.expected.spvasm
@@ -0,0 +1,40 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 21
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main" %tint_symbol %tint_symbol_1 %tint_symbol_2 %tint_symbol_3
+               OpExecutionMode %main OriginUpperLeft
+               OpName %tint_symbol "tint_symbol"
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %tint_symbol_2 "tint_symbol_2"
+               OpName %tint_symbol_3 "tint_symbol_3"
+               OpName %main "main"
+               OpDecorate %tint_symbol Location 0
+               OpDecorate %tint_symbol_1 Location 1
+               OpDecorate %tint_symbol_2 Location 2
+               OpDecorate %tint_symbol_3 Location 3
+        %int = OpTypeInt 32 1
+%_ptr_Input_int = OpTypePointer Input %int
+%tint_symbol = OpVariable %_ptr_Input_int Input
+       %uint = OpTypeInt 32 0
+%_ptr_Input_uint = OpTypePointer Input %uint
+%tint_symbol_1 = OpVariable %_ptr_Input_uint Input
+      %float = OpTypeFloat 32
+%_ptr_Input_float = OpTypePointer Input %float
+%tint_symbol_2 = OpVariable %_ptr_Input_float Input
+    %v4float = OpTypeVector %float 4
+%_ptr_Input_v4float = OpTypePointer Input %v4float
+%tint_symbol_3 = OpVariable %_ptr_Input_v4float Input
+       %void = OpTypeVoid
+         %13 = OpTypeFunction %void
+       %main = OpFunction %void None %13
+         %16 = OpLabel
+         %17 = OpLoad %int %tint_symbol
+         %18 = OpLoad %uint %tint_symbol_1
+         %19 = OpLoad %float %tint_symbol_2
+         %20 = OpLoad %v4float %tint_symbol_3
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/fragment_input_locations.wgsl.expected.wgsl b/test/shader_io/fragment_input_locations.wgsl.expected.wgsl
new file mode 100644
index 0000000..2ab1394
--- /dev/null
+++ b/test/shader_io/fragment_input_locations.wgsl.expected.wgsl
@@ -0,0 +1,7 @@
+[[stage(fragment)]]
+fn main([[location(0)]] loc0 : i32, [[location(1)]] loc1 : u32, [[location(2)]] loc2 : f32, [[location(3)]] loc3 : vec4<f32>) {
+  let i : i32 = loc0;
+  let u : u32 = loc1;
+  let f : f32 = loc2;
+  let v : vec4<f32> = loc3;
+}
diff --git a/test/shader_io/fragment_input_locations_struct.wgsl b/test/shader_io/fragment_input_locations_struct.wgsl
new file mode 100644
index 0000000..710ded1
--- /dev/null
+++ b/test/shader_io/fragment_input_locations_struct.wgsl
@@ -0,0 +1,14 @@
+struct FragmentInputs {
+  [[location(0)]] loc0 : i32;
+  [[location(1)]] loc1 : u32;
+  [[location(2)]] loc2 : f32;
+  [[location(3)]] loc3 : vec4<f32>;
+};
+
+[[stage(fragment)]]
+fn main(inputs : FragmentInputs) {
+  let i : i32 = inputs.loc0;
+  let u : u32 = inputs.loc1;
+  let f : f32 = inputs.loc2;
+  let v : vec4<f32> = inputs.loc3;
+}
diff --git a/test/shader_io/fragment_input_locations_struct.wgsl.expected.hlsl b/test/shader_io/fragment_input_locations_struct.wgsl.expected.hlsl
new file mode 100644
index 0000000..8174496
--- /dev/null
+++ b/test/shader_io/fragment_input_locations_struct.wgsl.expected.hlsl
@@ -0,0 +1,22 @@
+struct FragmentInputs {
+  int loc0;
+  uint loc1;
+  float loc2;
+  float4 loc3;
+};
+struct tint_symbol_1 {
+  int loc0 : TEXCOORD0;
+  uint loc1 : TEXCOORD1;
+  float loc2 : TEXCOORD2;
+  float4 loc3 : TEXCOORD3;
+};
+
+void main(tint_symbol_1 tint_symbol) {
+  const FragmentInputs inputs = {tint_symbol.loc0, tint_symbol.loc1, tint_symbol.loc2, tint_symbol.loc3};
+  const int i = inputs.loc0;
+  const uint u = inputs.loc1;
+  const float f = inputs.loc2;
+  const float4 v = inputs.loc3;
+  return;
+}
+
diff --git a/test/shader_io/fragment_input_locations_struct.wgsl.expected.msl b/test/shader_io/fragment_input_locations_struct.wgsl.expected.msl
new file mode 100644
index 0000000..3250f3e
--- /dev/null
+++ b/test/shader_io/fragment_input_locations_struct.wgsl.expected.msl
@@ -0,0 +1,25 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct FragmentInputs {
+  int loc0;
+  uint loc1;
+  float loc2;
+  float4 loc3;
+};
+struct tint_symbol_2 {
+  int loc0 [[user(locn0)]];
+  uint loc1 [[user(locn1)]];
+  float loc2 [[user(locn2)]];
+  float4 loc3 [[user(locn3)]];
+};
+
+fragment void tint_symbol(tint_symbol_2 tint_symbol_1 [[stage_in]]) {
+  FragmentInputs const inputs = {tint_symbol_1.loc0, tint_symbol_1.loc1, tint_symbol_1.loc2, tint_symbol_1.loc3};
+  int const i = inputs.loc0;
+  uint const u = inputs.loc1;
+  float const f = inputs.loc2;
+  float4 const v = inputs.loc3;
+  return;
+}
+
diff --git a/test/shader_io/fragment_input_locations_struct.wgsl.expected.spvasm b/test/shader_io/fragment_input_locations_struct.wgsl.expected.spvasm
new file mode 100644
index 0000000..8088af5
--- /dev/null
+++ b/test/shader_io/fragment_input_locations_struct.wgsl.expected.spvasm
@@ -0,0 +1,55 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 27
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main" %tint_symbol %tint_symbol_1 %tint_symbol_2 %tint_symbol_3
+               OpExecutionMode %main OriginUpperLeft
+               OpName %tint_symbol "tint_symbol"
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %tint_symbol_2 "tint_symbol_2"
+               OpName %tint_symbol_3 "tint_symbol_3"
+               OpName %main "main"
+               OpName %FragmentInputs "FragmentInputs"
+               OpMemberName %FragmentInputs 0 "loc0"
+               OpMemberName %FragmentInputs 1 "loc1"
+               OpMemberName %FragmentInputs 2 "loc2"
+               OpMemberName %FragmentInputs 3 "loc3"
+               OpDecorate %tint_symbol Location 0
+               OpDecorate %tint_symbol_1 Location 1
+               OpDecorate %tint_symbol_2 Location 2
+               OpDecorate %tint_symbol_3 Location 3
+               OpMemberDecorate %FragmentInputs 0 Offset 0
+               OpMemberDecorate %FragmentInputs 1 Offset 4
+               OpMemberDecorate %FragmentInputs 2 Offset 8
+               OpMemberDecorate %FragmentInputs 3 Offset 16
+        %int = OpTypeInt 32 1
+%_ptr_Input_int = OpTypePointer Input %int
+%tint_symbol = OpVariable %_ptr_Input_int Input
+       %uint = OpTypeInt 32 0
+%_ptr_Input_uint = OpTypePointer Input %uint
+%tint_symbol_1 = OpVariable %_ptr_Input_uint Input
+      %float = OpTypeFloat 32
+%_ptr_Input_float = OpTypePointer Input %float
+%tint_symbol_2 = OpVariable %_ptr_Input_float Input
+    %v4float = OpTypeVector %float 4
+%_ptr_Input_v4float = OpTypePointer Input %v4float
+%tint_symbol_3 = OpVariable %_ptr_Input_v4float Input
+       %void = OpTypeVoid
+         %13 = OpTypeFunction %void
+%FragmentInputs = OpTypeStruct %int %uint %float %v4float
+       %main = OpFunction %void None %13
+         %16 = OpLabel
+         %18 = OpLoad %int %tint_symbol
+         %19 = OpLoad %uint %tint_symbol_1
+         %20 = OpLoad %float %tint_symbol_2
+         %21 = OpLoad %v4float %tint_symbol_3
+         %22 = OpCompositeConstruct %FragmentInputs %18 %19 %20 %21
+         %23 = OpCompositeExtract %int %22 0
+         %24 = OpCompositeExtract %uint %22 1
+         %25 = OpCompositeExtract %float %22 2
+         %26 = OpCompositeExtract %v4float %22 3
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/fragment_input_locations_struct.wgsl.expected.wgsl b/test/shader_io/fragment_input_locations_struct.wgsl.expected.wgsl
new file mode 100644
index 0000000..f5f3a8b
--- /dev/null
+++ b/test/shader_io/fragment_input_locations_struct.wgsl.expected.wgsl
@@ -0,0 +1,18 @@
+struct FragmentInputs {
+  [[location(0)]]
+  loc0 : i32;
+  [[location(1)]]
+  loc1 : u32;
+  [[location(2)]]
+  loc2 : f32;
+  [[location(3)]]
+  loc3 : vec4<f32>;
+};
+
+[[stage(fragment)]]
+fn main(inputs : FragmentInputs) {
+  let i : i32 = inputs.loc0;
+  let u : u32 = inputs.loc1;
+  let f : f32 = inputs.loc2;
+  let v : vec4<f32> = inputs.loc3;
+}
diff --git a/test/shader_io/fragment_input_mixed.wgsl b/test/shader_io/fragment_input_mixed.wgsl
new file mode 100644
index 0000000..d6864ee
--- /dev/null
+++ b/test/shader_io/fragment_input_mixed.wgsl
@@ -0,0 +1,27 @@
+struct FragmentInputs0 {
+  [[builtin(position)]] position : vec4<f32>;
+  [[location(0)]] loc0 : i32;
+};
+struct FragmentInputs1 {
+  [[location(3)]] loc3 : vec4<f32>;
+  [[builtin(sample_mask)]] sample_mask : u32;
+};
+
+[[stage(fragment)]]
+fn main(
+  inputs0 : FragmentInputs0,
+  [[builtin(front_facing)]] front_facing : bool,
+  [[location(1)]] loc1 : u32,
+  [[builtin(sample_index)]] sample_index : u32,
+  inputs1 : FragmentInputs1,
+  [[location(2)]] loc2 : f32,
+) {
+  if (front_facing) {
+    let foo : vec4<f32> = inputs0.position;
+    let bar : u32 = sample_index + inputs1.sample_mask;
+    let i : i32 = inputs0.loc0;
+    let u : u32 = loc1;
+    let f : f32 = loc2;
+    let v : vec4<f32> = inputs1.loc3;
+  }
+}
diff --git a/test/shader_io/fragment_input_mixed.wgsl.expected.hlsl b/test/shader_io/fragment_input_mixed.wgsl.expected.hlsl
new file mode 100644
index 0000000..d83e6cc
--- /dev/null
+++ b/test/shader_io/fragment_input_mixed.wgsl.expected.hlsl
@@ -0,0 +1,37 @@
+struct FragmentInputs0 {
+  float4 position;
+  int loc0;
+};
+struct FragmentInputs1 {
+  float4 loc3;
+  uint sample_mask;
+};
+struct tint_symbol_1 {
+  int loc0 : TEXCOORD0;
+  uint loc1 : TEXCOORD1;
+  float loc2 : TEXCOORD2;
+  float4 loc3 : TEXCOORD3;
+  float4 position : SV_Position;
+  bool front_facing : SV_IsFrontFace;
+  uint sample_index : SV_SampleIndex;
+  uint sample_mask : SV_Coverage;
+};
+
+void main(tint_symbol_1 tint_symbol) {
+  const FragmentInputs0 inputs0 = {tint_symbol.position, tint_symbol.loc0};
+  const bool front_facing = tint_symbol.front_facing;
+  const uint loc1 = tint_symbol.loc1;
+  const uint sample_index = tint_symbol.sample_index;
+  const FragmentInputs1 inputs1 = {tint_symbol.loc3, tint_symbol.sample_mask};
+  const float loc2 = tint_symbol.loc2;
+  if (front_facing) {
+    const float4 foo = inputs0.position;
+    const uint bar = (sample_index + inputs1.sample_mask);
+    const int i = inputs0.loc0;
+    const uint u = loc1;
+    const float f = loc2;
+    const float4 v = inputs1.loc3;
+  }
+  return;
+}
+
diff --git a/test/shader_io/fragment_input_mixed.wgsl.expected.msl b/test/shader_io/fragment_input_mixed.wgsl.expected.msl
new file mode 100644
index 0000000..a115991
--- /dev/null
+++ b/test/shader_io/fragment_input_mixed.wgsl.expected.msl
@@ -0,0 +1,40 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct FragmentInputs0 {
+  float4 position;
+  int loc0;
+};
+struct FragmentInputs1 {
+  float4 loc3;
+  uint sample_mask;
+};
+struct tint_symbol_2 {
+  int loc0 [[user(locn0)]];
+  uint loc1 [[user(locn1)]];
+  float loc2 [[user(locn2)]];
+  float4 loc3 [[user(locn3)]];
+  float4 position [[position]];
+  bool front_facing [[front_facing]];
+  uint sample_index [[sample_id]];
+  uint sample_mask [[sample_mask]];
+};
+
+fragment void tint_symbol(tint_symbol_2 tint_symbol_1 [[stage_in]]) {
+  FragmentInputs0 const inputs0 = {tint_symbol_1.position, tint_symbol_1.loc0};
+  bool const front_facing = tint_symbol_1.front_facing;
+  uint const loc1 = tint_symbol_1.loc1;
+  uint const sample_index = tint_symbol_1.sample_index;
+  FragmentInputs1 const inputs1 = {tint_symbol_1.loc3, tint_symbol_1.sample_mask};
+  float const loc2 = tint_symbol_1.loc2;
+  if (front_facing) {
+    float4 const foo = inputs0.position;
+    uint const bar = (sample_index + inputs1.sample_mask);
+    int const i = inputs0.loc0;
+    uint const u = loc1;
+    float const f = loc2;
+    float4 const v = inputs1.loc3;
+  }
+  return;
+}
+
diff --git a/test/shader_io/fragment_input_mixed.wgsl.expected.spvasm b/test/shader_io/fragment_input_mixed.wgsl.expected.spvasm
new file mode 100644
index 0000000..df5c0a7
--- /dev/null
+++ b/test/shader_io/fragment_input_mixed.wgsl.expected.spvasm
@@ -0,0 +1,89 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 47
+; Schema: 0
+               OpCapability Shader
+               OpCapability SampleRateShading
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main" %tint_symbol %tint_symbol_1 %tint_symbol_6 %tint_symbol_7 %tint_symbol_3 %tint_symbol_5 %tint_symbol_4 %tint_symbol_9
+               OpExecutionMode %main OriginUpperLeft
+               OpName %tint_symbol "tint_symbol"
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %tint_symbol_3 "tint_symbol_3"
+               OpName %tint_symbol_4 "tint_symbol_4"
+               OpName %tint_symbol_5 "tint_symbol_5"
+               OpName %tint_symbol_6 "tint_symbol_6"
+               OpName %tint_symbol_7 "tint_symbol_7"
+               OpName %tint_symbol_9 "tint_symbol_9"
+               OpName %main "main"
+               OpName %FragmentInputs0 "FragmentInputs0"
+               OpMemberName %FragmentInputs0 0 "position"
+               OpMemberName %FragmentInputs0 1 "loc0"
+               OpName %FragmentInputs1 "FragmentInputs1"
+               OpMemberName %FragmentInputs1 0 "loc3"
+               OpMemberName %FragmentInputs1 1 "sample_mask"
+               OpDecorate %tint_symbol BuiltIn FragCoord
+               OpDecorate %tint_symbol_1 Location 0
+               OpDecorate %tint_symbol_3 BuiltIn FrontFacing
+               OpDecorate %tint_symbol_4 Location 1
+               OpDecorate %tint_symbol_5 BuiltIn SampleId
+               OpDecorate %tint_symbol_6 Location 3
+               OpDecorate %_arr_uint_uint_1 ArrayStride 4
+               OpDecorate %tint_symbol_7 BuiltIn SampleMask
+               OpDecorate %tint_symbol_9 Location 2
+               OpMemberDecorate %FragmentInputs0 0 Offset 0
+               OpMemberDecorate %FragmentInputs0 1 Offset 16
+               OpMemberDecorate %FragmentInputs1 0 Offset 0
+               OpMemberDecorate %FragmentInputs1 1 Offset 16
+      %float = OpTypeFloat 32
+    %v4float = OpTypeVector %float 4
+%_ptr_Input_v4float = OpTypePointer Input %v4float
+%tint_symbol = OpVariable %_ptr_Input_v4float Input
+        %int = OpTypeInt 32 1
+%_ptr_Input_int = OpTypePointer Input %int
+%tint_symbol_1 = OpVariable %_ptr_Input_int Input
+       %bool = OpTypeBool
+%_ptr_Input_bool = OpTypePointer Input %bool
+%tint_symbol_3 = OpVariable %_ptr_Input_bool Input
+       %uint = OpTypeInt 32 0
+%_ptr_Input_uint = OpTypePointer Input %uint
+%tint_symbol_4 = OpVariable %_ptr_Input_uint Input
+%tint_symbol_5 = OpVariable %_ptr_Input_uint Input
+%tint_symbol_6 = OpVariable %_ptr_Input_v4float Input
+     %uint_1 = OpConstant %uint 1
+%_arr_uint_uint_1 = OpTypeArray %uint %uint_1
+%_ptr_Input__arr_uint_uint_1 = OpTypePointer Input %_arr_uint_uint_1
+%tint_symbol_7 = OpVariable %_ptr_Input__arr_uint_uint_1 Input
+%_ptr_Input_float = OpTypePointer Input %float
+%tint_symbol_9 = OpVariable %_ptr_Input_float Input
+       %void = OpTypeVoid
+         %22 = OpTypeFunction %void
+%FragmentInputs0 = OpTypeStruct %v4float %int
+%FragmentInputs1 = OpTypeStruct %v4float %uint
+      %int_0 = OpConstant %int 0
+       %main = OpFunction %void None %22
+         %25 = OpLabel
+         %27 = OpLoad %v4float %tint_symbol
+         %28 = OpLoad %int %tint_symbol_1
+         %29 = OpCompositeConstruct %FragmentInputs0 %27 %28
+         %31 = OpLoad %v4float %tint_symbol_6
+         %33 = OpAccessChain %_ptr_Input_uint %tint_symbol_7 %int_0
+         %34 = OpLoad %uint %33
+         %35 = OpCompositeConstruct %FragmentInputs1 %31 %34
+         %36 = OpLoad %bool %tint_symbol_3
+               OpSelectionMerge %37 None
+               OpBranchConditional %36 %38 %37
+         %38 = OpLabel
+         %39 = OpCompositeExtract %v4float %29 0
+         %40 = OpLoad %uint %tint_symbol_5
+         %41 = OpCompositeExtract %uint %35 1
+         %42 = OpIAdd %uint %40 %41
+         %43 = OpCompositeExtract %int %29 1
+         %44 = OpLoad %uint %tint_symbol_4
+         %45 = OpLoad %float %tint_symbol_9
+         %46 = OpCompositeExtract %v4float %35 0
+               OpBranch %37
+         %37 = OpLabel
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/fragment_input_mixed.wgsl.expected.wgsl b/test/shader_io/fragment_input_mixed.wgsl.expected.wgsl
new file mode 100644
index 0000000..afc2348
--- /dev/null
+++ b/test/shader_io/fragment_input_mixed.wgsl.expected.wgsl
@@ -0,0 +1,25 @@
+struct FragmentInputs0 {
+  [[builtin(position)]]
+  position : vec4<f32>;
+  [[location(0)]]
+  loc0 : i32;
+};
+
+struct FragmentInputs1 {
+  [[location(3)]]
+  loc3 : vec4<f32>;
+  [[builtin(sample_mask)]]
+  sample_mask : u32;
+};
+
+[[stage(fragment)]]
+fn main(inputs0 : FragmentInputs0, [[builtin(front_facing)]] front_facing : bool, [[location(1)]] loc1 : u32, [[builtin(sample_index)]] sample_index : u32, inputs1 : FragmentInputs1, [[location(2)]] loc2 : f32) {
+  if (front_facing) {
+    let foo : vec4<f32> = inputs0.position;
+    let bar : u32 = (sample_index + inputs1.sample_mask);
+    let i : i32 = inputs0.loc0;
+    let u : u32 = loc1;
+    let f : f32 = loc2;
+    let v : vec4<f32> = inputs1.loc3;
+  }
+}
diff --git a/test/shader_io/fragment_output_builtins.wgsl b/test/shader_io/fragment_output_builtins.wgsl
new file mode 100644
index 0000000..462cb77
--- /dev/null
+++ b/test/shader_io/fragment_output_builtins.wgsl
@@ -0,0 +1,9 @@
+[[stage(fragment)]]
+fn main1() -> [[builtin(frag_depth)]] f32 {
+  return 1.0;
+}
+
+[[stage(fragment)]]
+fn main2() -> [[builtin(sample_mask)]] u32 {
+  return 1u;
+}
diff --git a/test/shader_io/fragment_output_builtins.wgsl.expected.hlsl b/test/shader_io/fragment_output_builtins.wgsl.expected.hlsl
new file mode 100644
index 0000000..30309e5
--- /dev/null
+++ b/test/shader_io/fragment_output_builtins.wgsl.expected.hlsl
@@ -0,0 +1,17 @@
+struct tint_symbol {
+  float value : SV_Depth;
+};
+struct tint_symbol_1 {
+  uint value : SV_Coverage;
+};
+
+tint_symbol main1() {
+  const tint_symbol tint_symbol_2 = {1.0f};
+  return tint_symbol_2;
+}
+
+tint_symbol_1 main2() {
+  const tint_symbol_1 tint_symbol_3 = {1u};
+  return tint_symbol_3;
+}
+
diff --git a/test/shader_io/fragment_output_builtins.wgsl.expected.msl b/test/shader_io/fragment_output_builtins.wgsl.expected.msl
new file mode 100644
index 0000000..d56fd42
--- /dev/null
+++ b/test/shader_io/fragment_output_builtins.wgsl.expected.msl
@@ -0,0 +1,20 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct tint_symbol {
+  float value [[depth(any)]];
+};
+struct tint_symbol_1 {
+  uint value [[sample_mask]];
+};
+
+fragment tint_symbol main1() {
+  tint_symbol const tint_symbol_2 = {1.0f};
+  return tint_symbol_2;
+}
+
+fragment tint_symbol_1 main2() {
+  tint_symbol_1 const tint_symbol_3 = {1u};
+  return tint_symbol_3;
+}
+
diff --git a/test/shader_io/fragment_output_builtins.wgsl.expected.spvasm b/test/shader_io/fragment_output_builtins.wgsl.expected.spvasm
new file mode 100644
index 0000000..cf60151
--- /dev/null
+++ b/test/shader_io/fragment_output_builtins.wgsl.expected.spvasm
@@ -0,0 +1,64 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 32
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main1 "main1" %tint_symbol_1
+               OpEntryPoint Fragment %main2 "main2" %tint_symbol_4
+               OpExecutionMode %main1 OriginUpperLeft
+               OpExecutionMode %main1 DepthReplacing
+               OpExecutionMode %main2 OriginUpperLeft
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %tint_symbol_4 "tint_symbol_4"
+               OpName %tint_symbol_2 "tint_symbol_2"
+               OpName %tint_symbol "tint_symbol"
+               OpName %main1 "main1"
+               OpName %tint_symbol_5 "tint_symbol_5"
+               OpName %tint_symbol_3 "tint_symbol_3"
+               OpName %main2 "main2"
+               OpDecorate %tint_symbol_1 BuiltIn FragDepth
+               OpDecorate %_arr_uint_uint_1 ArrayStride 4
+               OpDecorate %tint_symbol_4 BuiltIn SampleMask
+      %float = OpTypeFloat 32
+%_ptr_Output_float = OpTypePointer Output %float
+          %4 = OpConstantNull %float
+%tint_symbol_1 = OpVariable %_ptr_Output_float Output %4
+       %uint = OpTypeInt 32 0
+     %uint_1 = OpConstant %uint 1
+%_arr_uint_uint_1 = OpTypeArray %uint %uint_1
+%_ptr_Output__arr_uint_uint_1 = OpTypePointer Output %_arr_uint_uint_1
+         %10 = OpConstantNull %_arr_uint_uint_1
+%tint_symbol_4 = OpVariable %_ptr_Output__arr_uint_uint_1 Output %10
+       %void = OpTypeVoid
+         %11 = OpTypeFunction %void %float
+         %16 = OpTypeFunction %void
+    %float_1 = OpConstant %float 1
+         %21 = OpTypeFunction %void %uint
+        %int = OpTypeInt 32 1
+      %int_0 = OpConstant %int 0
+%_ptr_Output_uint = OpTypePointer Output %uint
+%tint_symbol_2 = OpFunction %void None %11
+%tint_symbol = OpFunctionParameter %float
+         %15 = OpLabel
+               OpStore %tint_symbol_1 %tint_symbol
+               OpReturn
+               OpFunctionEnd
+      %main1 = OpFunction %void None %16
+         %18 = OpLabel
+         %19 = OpFunctionCall %void %tint_symbol_2 %float_1
+               OpReturn
+               OpFunctionEnd
+%tint_symbol_5 = OpFunction %void None %21
+%tint_symbol_3 = OpFunctionParameter %uint
+         %24 = OpLabel
+         %28 = OpAccessChain %_ptr_Output_uint %tint_symbol_4 %int_0
+               OpStore %28 %tint_symbol_3
+               OpReturn
+               OpFunctionEnd
+      %main2 = OpFunction %void None %16
+         %30 = OpLabel
+         %31 = OpFunctionCall %void %tint_symbol_5 %uint_1
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/fragment_output_builtins.wgsl.expected.wgsl b/test/shader_io/fragment_output_builtins.wgsl.expected.wgsl
new file mode 100644
index 0000000..462cb77
--- /dev/null
+++ b/test/shader_io/fragment_output_builtins.wgsl.expected.wgsl
@@ -0,0 +1,9 @@
+[[stage(fragment)]]
+fn main1() -> [[builtin(frag_depth)]] f32 {
+  return 1.0;
+}
+
+[[stage(fragment)]]
+fn main2() -> [[builtin(sample_mask)]] u32 {
+  return 1u;
+}
diff --git a/test/shader_io/fragment_output_builtins_struct.wgsl b/test/shader_io/fragment_output_builtins_struct.wgsl
new file mode 100644
index 0000000..d7b37f8
--- /dev/null
+++ b/test/shader_io/fragment_output_builtins_struct.wgsl
@@ -0,0 +1,9 @@
+struct FragmentOutputs {
+  [[builtin(frag_depth)]] frag_depth : f32;
+  [[builtin(sample_mask)]] sample_mask : u32;
+};
+
+[[stage(fragment)]]
+fn main() -> FragmentOutputs {
+  return FragmentOutputs(1.0, 1u);
+}
diff --git a/test/shader_io/fragment_output_builtins_struct.wgsl.expected.hlsl b/test/shader_io/fragment_output_builtins_struct.wgsl.expected.hlsl
new file mode 100644
index 0000000..d266413
--- /dev/null
+++ b/test/shader_io/fragment_output_builtins_struct.wgsl.expected.hlsl
@@ -0,0 +1,15 @@
+struct FragmentOutputs {
+  float frag_depth;
+  uint sample_mask;
+};
+struct tint_symbol {
+  float frag_depth : SV_Depth;
+  uint sample_mask : SV_Coverage;
+};
+
+tint_symbol main() {
+  const FragmentOutputs tint_symbol_1 = {1.0f, 1u};
+  const tint_symbol tint_symbol_2 = {tint_symbol_1.frag_depth, tint_symbol_1.sample_mask};
+  return tint_symbol_2;
+}
+
diff --git a/test/shader_io/fragment_output_builtins_struct.wgsl.expected.msl b/test/shader_io/fragment_output_builtins_struct.wgsl.expected.msl
new file mode 100644
index 0000000..343945f
--- /dev/null
+++ b/test/shader_io/fragment_output_builtins_struct.wgsl.expected.msl
@@ -0,0 +1,18 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct FragmentOutputs {
+  float frag_depth;
+  uint sample_mask;
+};
+struct tint_symbol_1 {
+  float frag_depth [[depth(any)]];
+  uint sample_mask [[sample_mask]];
+};
+
+fragment tint_symbol_1 tint_symbol() {
+  FragmentOutputs const tint_symbol_2 = {1.0f, 1u};
+  tint_symbol_1 const tint_symbol_3 = {tint_symbol_2.frag_depth, tint_symbol_2.sample_mask};
+  return tint_symbol_3;
+}
+
diff --git a/test/shader_io/fragment_output_builtins_struct.wgsl.expected.spvasm b/test/shader_io/fragment_output_builtins_struct.wgsl.expected.spvasm
new file mode 100644
index 0000000..45f416d
--- /dev/null
+++ b/test/shader_io/fragment_output_builtins_struct.wgsl.expected.spvasm
@@ -0,0 +1,57 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 29
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main" %tint_symbol_1 %tint_symbol_2
+               OpExecutionMode %main OriginUpperLeft
+               OpExecutionMode %main DepthReplacing
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %tint_symbol_2 "tint_symbol_2"
+               OpName %FragmentOutputs "FragmentOutputs"
+               OpMemberName %FragmentOutputs 0 "frag_depth"
+               OpMemberName %FragmentOutputs 1 "sample_mask"
+               OpName %tint_symbol_3 "tint_symbol_3"
+               OpName %tint_symbol "tint_symbol"
+               OpName %main "main"
+               OpDecorate %tint_symbol_1 BuiltIn FragDepth
+               OpDecorate %_arr_uint_uint_1 ArrayStride 4
+               OpDecorate %tint_symbol_2 BuiltIn SampleMask
+               OpMemberDecorate %FragmentOutputs 0 Offset 0
+               OpMemberDecorate %FragmentOutputs 1 Offset 4
+      %float = OpTypeFloat 32
+%_ptr_Output_float = OpTypePointer Output %float
+          %4 = OpConstantNull %float
+%tint_symbol_1 = OpVariable %_ptr_Output_float Output %4
+       %uint = OpTypeInt 32 0
+     %uint_1 = OpConstant %uint 1
+%_arr_uint_uint_1 = OpTypeArray %uint %uint_1
+%_ptr_Output__arr_uint_uint_1 = OpTypePointer Output %_arr_uint_uint_1
+         %10 = OpConstantNull %_arr_uint_uint_1
+%tint_symbol_2 = OpVariable %_ptr_Output__arr_uint_uint_1 Output %10
+       %void = OpTypeVoid
+%FragmentOutputs = OpTypeStruct %float %uint
+         %11 = OpTypeFunction %void %FragmentOutputs
+        %int = OpTypeInt 32 1
+      %int_0 = OpConstant %int 0
+%_ptr_Output_uint = OpTypePointer Output %uint
+         %23 = OpTypeFunction %void
+    %float_1 = OpConstant %float 1
+         %28 = OpConstantComposite %FragmentOutputs %float_1 %uint_1
+%tint_symbol_3 = OpFunction %void None %11
+%tint_symbol = OpFunctionParameter %FragmentOutputs
+         %16 = OpLabel
+         %17 = OpCompositeExtract %float %tint_symbol 0
+               OpStore %tint_symbol_1 %17
+         %21 = OpAccessChain %_ptr_Output_uint %tint_symbol_2 %int_0
+         %22 = OpCompositeExtract %uint %tint_symbol 1
+               OpStore %21 %22
+               OpReturn
+               OpFunctionEnd
+       %main = OpFunction %void None %23
+         %25 = OpLabel
+         %26 = OpFunctionCall %void %tint_symbol_3 %28
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/fragment_output_builtins_struct.wgsl.expected.wgsl b/test/shader_io/fragment_output_builtins_struct.wgsl.expected.wgsl
new file mode 100644
index 0000000..6617a26
--- /dev/null
+++ b/test/shader_io/fragment_output_builtins_struct.wgsl.expected.wgsl
@@ -0,0 +1,11 @@
+struct FragmentOutputs {
+  [[builtin(frag_depth)]]
+  frag_depth : f32;
+  [[builtin(sample_mask)]]
+  sample_mask : u32;
+};
+
+[[stage(fragment)]]
+fn main() -> FragmentOutputs {
+  return FragmentOutputs(1.0, 1u);
+}
diff --git a/test/shader_io/fragment_output_locations.wgsl b/test/shader_io/fragment_output_locations.wgsl
new file mode 100644
index 0000000..b5858e8
--- /dev/null
+++ b/test/shader_io/fragment_output_locations.wgsl
@@ -0,0 +1,19 @@
+[[stage(fragment)]]
+fn main0() -> [[location(0)]] i32 {
+  return 1;
+}
+
+[[stage(fragment)]]
+fn main1() -> [[location(1)]] u32 {
+  return 1u;
+}
+
+[[stage(fragment)]]
+fn main2() -> [[location(2)]] f32 {
+  return 1.0;
+}
+
+[[stage(fragment)]]
+fn main3() -> [[location(3)]] vec4<f32> {
+  return vec4<f32>(1.0, 2.0, 3.0, 4.0);
+}
diff --git a/test/shader_io/fragment_output_locations.wgsl.expected.hlsl b/test/shader_io/fragment_output_locations.wgsl.expected.hlsl
new file mode 100644
index 0000000..69f77e0
--- /dev/null
+++ b/test/shader_io/fragment_output_locations.wgsl.expected.hlsl
@@ -0,0 +1,33 @@
+struct tint_symbol {
+  int value : SV_Target0;
+};
+struct tint_symbol_1 {
+  uint value : SV_Target1;
+};
+struct tint_symbol_2 {
+  float value : SV_Target2;
+};
+struct tint_symbol_3 {
+  float4 value : SV_Target3;
+};
+
+tint_symbol main0() {
+  const tint_symbol tint_symbol_4 = {1};
+  return tint_symbol_4;
+}
+
+tint_symbol_1 main1() {
+  const tint_symbol_1 tint_symbol_5 = {1u};
+  return tint_symbol_5;
+}
+
+tint_symbol_2 main2() {
+  const tint_symbol_2 tint_symbol_6 = {1.0f};
+  return tint_symbol_6;
+}
+
+tint_symbol_3 main3() {
+  const tint_symbol_3 tint_symbol_7 = {float4(1.0f, 2.0f, 3.0f, 4.0f)};
+  return tint_symbol_7;
+}
+
diff --git a/test/shader_io/fragment_output_locations.wgsl.expected.msl b/test/shader_io/fragment_output_locations.wgsl.expected.msl
new file mode 100644
index 0000000..58191b0
--- /dev/null
+++ b/test/shader_io/fragment_output_locations.wgsl.expected.msl
@@ -0,0 +1,36 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct tint_symbol {
+  int value [[color(0)]];
+};
+struct tint_symbol_1 {
+  uint value [[color(1)]];
+};
+struct tint_symbol_2 {
+  float value [[color(2)]];
+};
+struct tint_symbol_3 {
+  float4 value [[color(3)]];
+};
+
+fragment tint_symbol main0() {
+  tint_symbol const tint_symbol_4 = {1};
+  return tint_symbol_4;
+}
+
+fragment tint_symbol_1 main1() {
+  tint_symbol_1 const tint_symbol_5 = {1u};
+  return tint_symbol_5;
+}
+
+fragment tint_symbol_2 main2() {
+  tint_symbol_2 const tint_symbol_6 = {1.0f};
+  return tint_symbol_6;
+}
+
+fragment tint_symbol_3 main3() {
+  tint_symbol_3 const tint_symbol_7 = {float4(1.0f, 2.0f, 3.0f, 4.0f)};
+  return tint_symbol_7;
+}
+
diff --git a/test/shader_io/fragment_output_locations.wgsl.expected.spvasm b/test/shader_io/fragment_output_locations.wgsl.expected.spvasm
new file mode 100644
index 0000000..ef873b5
--- /dev/null
+++ b/test/shader_io/fragment_output_locations.wgsl.expected.spvasm
@@ -0,0 +1,108 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 54
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main0 "main0" %tint_symbol_1
+               OpEntryPoint Fragment %main1 "main1" %tint_symbol_4
+               OpEntryPoint Fragment %main2 "main2" %tint_symbol_7
+               OpEntryPoint Fragment %main3 "main3" %tint_symbol_10
+               OpExecutionMode %main0 OriginUpperLeft
+               OpExecutionMode %main1 OriginUpperLeft
+               OpExecutionMode %main2 OriginUpperLeft
+               OpExecutionMode %main3 OriginUpperLeft
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %tint_symbol_4 "tint_symbol_4"
+               OpName %tint_symbol_7 "tint_symbol_7"
+               OpName %tint_symbol_10 "tint_symbol_10"
+               OpName %tint_symbol_2 "tint_symbol_2"
+               OpName %tint_symbol "tint_symbol"
+               OpName %main0 "main0"
+               OpName %tint_symbol_5 "tint_symbol_5"
+               OpName %tint_symbol_3 "tint_symbol_3"
+               OpName %main1 "main1"
+               OpName %tint_symbol_8 "tint_symbol_8"
+               OpName %tint_symbol_6 "tint_symbol_6"
+               OpName %main2 "main2"
+               OpName %tint_symbol_11 "tint_symbol_11"
+               OpName %tint_symbol_9 "tint_symbol_9"
+               OpName %main3 "main3"
+               OpDecorate %tint_symbol_1 Location 0
+               OpDecorate %tint_symbol_4 Location 1
+               OpDecorate %tint_symbol_7 Location 2
+               OpDecorate %tint_symbol_10 Location 3
+        %int = OpTypeInt 32 1
+%_ptr_Output_int = OpTypePointer Output %int
+          %4 = OpConstantNull %int
+%tint_symbol_1 = OpVariable %_ptr_Output_int Output %4
+       %uint = OpTypeInt 32 0
+%_ptr_Output_uint = OpTypePointer Output %uint
+          %8 = OpConstantNull %uint
+%tint_symbol_4 = OpVariable %_ptr_Output_uint Output %8
+      %float = OpTypeFloat 32
+%_ptr_Output_float = OpTypePointer Output %float
+         %12 = OpConstantNull %float
+%tint_symbol_7 = OpVariable %_ptr_Output_float Output %12
+    %v4float = OpTypeVector %float 4
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+         %16 = OpConstantNull %v4float
+%tint_symbol_10 = OpVariable %_ptr_Output_v4float Output %16
+       %void = OpTypeVoid
+         %17 = OpTypeFunction %void %int
+         %22 = OpTypeFunction %void
+      %int_1 = OpConstant %int 1
+         %27 = OpTypeFunction %void %uint
+     %uint_1 = OpConstant %uint 1
+         %35 = OpTypeFunction %void %float
+    %float_1 = OpConstant %float 1
+         %43 = OpTypeFunction %void %v4float
+    %float_2 = OpConstant %float 2
+    %float_3 = OpConstant %float 3
+    %float_4 = OpConstant %float 4
+         %53 = OpConstantComposite %v4float %float_1 %float_2 %float_3 %float_4
+%tint_symbol_2 = OpFunction %void None %17
+%tint_symbol = OpFunctionParameter %int
+         %21 = OpLabel
+               OpStore %tint_symbol_1 %tint_symbol
+               OpReturn
+               OpFunctionEnd
+      %main0 = OpFunction %void None %22
+         %24 = OpLabel
+         %25 = OpFunctionCall %void %tint_symbol_2 %int_1
+               OpReturn
+               OpFunctionEnd
+%tint_symbol_5 = OpFunction %void None %27
+%tint_symbol_3 = OpFunctionParameter %uint
+         %30 = OpLabel
+               OpStore %tint_symbol_4 %tint_symbol_3
+               OpReturn
+               OpFunctionEnd
+      %main1 = OpFunction %void None %22
+         %32 = OpLabel
+         %33 = OpFunctionCall %void %tint_symbol_5 %uint_1
+               OpReturn
+               OpFunctionEnd
+%tint_symbol_8 = OpFunction %void None %35
+%tint_symbol_6 = OpFunctionParameter %float
+         %38 = OpLabel
+               OpStore %tint_symbol_7 %tint_symbol_6
+               OpReturn
+               OpFunctionEnd
+      %main2 = OpFunction %void None %22
+         %40 = OpLabel
+         %41 = OpFunctionCall %void %tint_symbol_8 %float_1
+               OpReturn
+               OpFunctionEnd
+%tint_symbol_11 = OpFunction %void None %43
+%tint_symbol_9 = OpFunctionParameter %v4float
+         %46 = OpLabel
+               OpStore %tint_symbol_10 %tint_symbol_9
+               OpReturn
+               OpFunctionEnd
+      %main3 = OpFunction %void None %22
+         %48 = OpLabel
+         %49 = OpFunctionCall %void %tint_symbol_11 %53
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/fragment_output_locations.wgsl.expected.wgsl b/test/shader_io/fragment_output_locations.wgsl.expected.wgsl
new file mode 100644
index 0000000..b5858e8
--- /dev/null
+++ b/test/shader_io/fragment_output_locations.wgsl.expected.wgsl
@@ -0,0 +1,19 @@
+[[stage(fragment)]]
+fn main0() -> [[location(0)]] i32 {
+  return 1;
+}
+
+[[stage(fragment)]]
+fn main1() -> [[location(1)]] u32 {
+  return 1u;
+}
+
+[[stage(fragment)]]
+fn main2() -> [[location(2)]] f32 {
+  return 1.0;
+}
+
+[[stage(fragment)]]
+fn main3() -> [[location(3)]] vec4<f32> {
+  return vec4<f32>(1.0, 2.0, 3.0, 4.0);
+}
diff --git a/test/shader_io/fragment_output_locations_struct.wgsl b/test/shader_io/fragment_output_locations_struct.wgsl
new file mode 100644
index 0000000..c196843
--- /dev/null
+++ b/test/shader_io/fragment_output_locations_struct.wgsl
@@ -0,0 +1,11 @@
+struct FragmentOutputs {
+  [[location(0)]] loc0 : i32;
+  [[location(1)]] loc1 : u32;
+  [[location(2)]] loc2 : f32;
+  [[location(3)]] loc3 : vec4<f32>;
+};
+
+[[stage(fragment)]]
+fn main() -> FragmentOutputs {
+  return FragmentOutputs(1, 1u, 1.0, vec4<f32>(1.0, 2.0, 3.0, 4.0));
+}
diff --git a/test/shader_io/fragment_output_locations_struct.wgsl.expected.hlsl b/test/shader_io/fragment_output_locations_struct.wgsl.expected.hlsl
new file mode 100644
index 0000000..b399bc2
--- /dev/null
+++ b/test/shader_io/fragment_output_locations_struct.wgsl.expected.hlsl
@@ -0,0 +1,19 @@
+struct FragmentOutputs {
+  int loc0;
+  uint loc1;
+  float loc2;
+  float4 loc3;
+};
+struct tint_symbol {
+  int loc0 : SV_Target0;
+  uint loc1 : SV_Target1;
+  float loc2 : SV_Target2;
+  float4 loc3 : SV_Target3;
+};
+
+tint_symbol main() {
+  const FragmentOutputs tint_symbol_1 = {1, 1u, 1.0f, float4(1.0f, 2.0f, 3.0f, 4.0f)};
+  const tint_symbol tint_symbol_2 = {tint_symbol_1.loc0, tint_symbol_1.loc1, tint_symbol_1.loc2, tint_symbol_1.loc3};
+  return tint_symbol_2;
+}
+
diff --git a/test/shader_io/fragment_output_locations_struct.wgsl.expected.msl b/test/shader_io/fragment_output_locations_struct.wgsl.expected.msl
new file mode 100644
index 0000000..4e1b359
--- /dev/null
+++ b/test/shader_io/fragment_output_locations_struct.wgsl.expected.msl
@@ -0,0 +1,22 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct FragmentOutputs {
+  int loc0;
+  uint loc1;
+  float loc2;
+  float4 loc3;
+};
+struct tint_symbol_1 {
+  int loc0 [[color(0)]];
+  uint loc1 [[color(1)]];
+  float loc2 [[color(2)]];
+  float4 loc3 [[color(3)]];
+};
+
+fragment tint_symbol_1 tint_symbol() {
+  FragmentOutputs const tint_symbol_2 = {1, 1u, 1.0f, float4(1.0f, 2.0f, 3.0f, 4.0f)};
+  tint_symbol_1 const tint_symbol_3 = {tint_symbol_2.loc0, tint_symbol_2.loc1, tint_symbol_2.loc2, tint_symbol_2.loc3};
+  return tint_symbol_3;
+}
+
diff --git a/test/shader_io/fragment_output_locations_struct.wgsl.expected.spvasm b/test/shader_io/fragment_output_locations_struct.wgsl.expected.spvasm
new file mode 100644
index 0000000..9ad60b7
--- /dev/null
+++ b/test/shader_io/fragment_output_locations_struct.wgsl.expected.spvasm
@@ -0,0 +1,75 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 39
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main" %tint_symbol_1 %tint_symbol_2 %tint_symbol_3 %tint_symbol_4
+               OpExecutionMode %main OriginUpperLeft
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %tint_symbol_2 "tint_symbol_2"
+               OpName %tint_symbol_3 "tint_symbol_3"
+               OpName %tint_symbol_4 "tint_symbol_4"
+               OpName %FragmentOutputs "FragmentOutputs"
+               OpMemberName %FragmentOutputs 0 "loc0"
+               OpMemberName %FragmentOutputs 1 "loc1"
+               OpMemberName %FragmentOutputs 2 "loc2"
+               OpMemberName %FragmentOutputs 3 "loc3"
+               OpName %tint_symbol_5 "tint_symbol_5"
+               OpName %tint_symbol "tint_symbol"
+               OpName %main "main"
+               OpDecorate %tint_symbol_1 Location 0
+               OpDecorate %tint_symbol_2 Location 1
+               OpDecorate %tint_symbol_3 Location 2
+               OpDecorate %tint_symbol_4 Location 3
+               OpMemberDecorate %FragmentOutputs 0 Offset 0
+               OpMemberDecorate %FragmentOutputs 1 Offset 4
+               OpMemberDecorate %FragmentOutputs 2 Offset 8
+               OpMemberDecorate %FragmentOutputs 3 Offset 16
+        %int = OpTypeInt 32 1
+%_ptr_Output_int = OpTypePointer Output %int
+          %4 = OpConstantNull %int
+%tint_symbol_1 = OpVariable %_ptr_Output_int Output %4
+       %uint = OpTypeInt 32 0
+%_ptr_Output_uint = OpTypePointer Output %uint
+          %8 = OpConstantNull %uint
+%tint_symbol_2 = OpVariable %_ptr_Output_uint Output %8
+      %float = OpTypeFloat 32
+%_ptr_Output_float = OpTypePointer Output %float
+         %12 = OpConstantNull %float
+%tint_symbol_3 = OpVariable %_ptr_Output_float Output %12
+    %v4float = OpTypeVector %float 4
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+         %16 = OpConstantNull %v4float
+%tint_symbol_4 = OpVariable %_ptr_Output_v4float Output %16
+       %void = OpTypeVoid
+%FragmentOutputs = OpTypeStruct %int %uint %float %v4float
+         %17 = OpTypeFunction %void %FragmentOutputs
+         %27 = OpTypeFunction %void
+      %int_1 = OpConstant %int 1
+     %uint_1 = OpConstant %uint 1
+    %float_1 = OpConstant %float 1
+    %float_2 = OpConstant %float 2
+    %float_3 = OpConstant %float 3
+    %float_4 = OpConstant %float 4
+         %37 = OpConstantComposite %v4float %float_1 %float_2 %float_3 %float_4
+         %38 = OpConstantComposite %FragmentOutputs %int_1 %uint_1 %float_1 %37
+%tint_symbol_5 = OpFunction %void None %17
+%tint_symbol = OpFunctionParameter %FragmentOutputs
+         %22 = OpLabel
+         %23 = OpCompositeExtract %int %tint_symbol 0
+               OpStore %tint_symbol_1 %23
+         %24 = OpCompositeExtract %uint %tint_symbol 1
+               OpStore %tint_symbol_2 %24
+         %25 = OpCompositeExtract %float %tint_symbol 2
+               OpStore %tint_symbol_3 %25
+         %26 = OpCompositeExtract %v4float %tint_symbol 3
+               OpStore %tint_symbol_4 %26
+               OpReturn
+               OpFunctionEnd
+       %main = OpFunction %void None %27
+         %29 = OpLabel
+         %30 = OpFunctionCall %void %tint_symbol_5 %38
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/fragment_output_locations_struct.wgsl.expected.wgsl b/test/shader_io/fragment_output_locations_struct.wgsl.expected.wgsl
new file mode 100644
index 0000000..879805c
--- /dev/null
+++ b/test/shader_io/fragment_output_locations_struct.wgsl.expected.wgsl
@@ -0,0 +1,15 @@
+struct FragmentOutputs {
+  [[location(0)]]
+  loc0 : i32;
+  [[location(1)]]
+  loc1 : u32;
+  [[location(2)]]
+  loc2 : f32;
+  [[location(3)]]
+  loc3 : vec4<f32>;
+};
+
+[[stage(fragment)]]
+fn main() -> FragmentOutputs {
+  return FragmentOutputs(1, 1u, 1.0, vec4<f32>(1.0, 2.0, 3.0, 4.0));
+}
diff --git a/test/shader_io/fragment_output_mixed.wgsl b/test/shader_io/fragment_output_mixed.wgsl
new file mode 100644
index 0000000..29e5108
--- /dev/null
+++ b/test/shader_io/fragment_output_mixed.wgsl
@@ -0,0 +1,13 @@
+struct FragmentOutputs {
+  [[location(0)]] loc0 : i32;
+  [[builtin(frag_depth)]] frag_depth : f32;
+  [[location(1)]] loc1 : u32;
+  [[location(2)]] loc2 : f32;
+  [[builtin(sample_mask)]] sample_mask : u32;
+  [[location(3)]] loc3 : vec4<f32>;
+};
+
+[[stage(fragment)]]
+fn main() -> FragmentOutputs {
+  return FragmentOutputs(1, 2.0, 1u, 1.0, 2u, vec4<f32>(1.0, 2.0, 3.0, 4.0));
+}
diff --git a/test/shader_io/fragment_output_mixed.wgsl.expected.hlsl b/test/shader_io/fragment_output_mixed.wgsl.expected.hlsl
new file mode 100644
index 0000000..73f00c5
--- /dev/null
+++ b/test/shader_io/fragment_output_mixed.wgsl.expected.hlsl
@@ -0,0 +1,23 @@
+struct FragmentOutputs {
+  int loc0;
+  float frag_depth;
+  uint loc1;
+  float loc2;
+  uint sample_mask;
+  float4 loc3;
+};
+struct tint_symbol {
+  int loc0 : SV_Target0;
+  uint loc1 : SV_Target1;
+  float loc2 : SV_Target2;
+  float4 loc3 : SV_Target3;
+  float frag_depth : SV_Depth;
+  uint sample_mask : SV_Coverage;
+};
+
+tint_symbol main() {
+  const FragmentOutputs tint_symbol_1 = {1, 2.0f, 1u, 1.0f, 2u, float4(1.0f, 2.0f, 3.0f, 4.0f)};
+  const tint_symbol tint_symbol_2 = {tint_symbol_1.loc0, tint_symbol_1.loc1, tint_symbol_1.loc2, tint_symbol_1.loc3, tint_symbol_1.frag_depth, tint_symbol_1.sample_mask};
+  return tint_symbol_2;
+}
+
diff --git a/test/shader_io/fragment_output_mixed.wgsl.expected.msl b/test/shader_io/fragment_output_mixed.wgsl.expected.msl
new file mode 100644
index 0000000..a6e3359
--- /dev/null
+++ b/test/shader_io/fragment_output_mixed.wgsl.expected.msl
@@ -0,0 +1,26 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct FragmentOutputs {
+  int loc0;
+  float frag_depth;
+  uint loc1;
+  float loc2;
+  uint sample_mask;
+  float4 loc3;
+};
+struct tint_symbol_1 {
+  int loc0 [[color(0)]];
+  uint loc1 [[color(1)]];
+  float loc2 [[color(2)]];
+  float4 loc3 [[color(3)]];
+  float frag_depth [[depth(any)]];
+  uint sample_mask [[sample_mask]];
+};
+
+fragment tint_symbol_1 tint_symbol() {
+  FragmentOutputs const tint_symbol_2 = {1, 2.0f, 1u, 1.0f, 2u, float4(1.0f, 2.0f, 3.0f, 4.0f)};
+  tint_symbol_1 const tint_symbol_3 = {tint_symbol_2.loc0, tint_symbol_2.loc1, tint_symbol_2.loc2, tint_symbol_2.loc3, tint_symbol_2.frag_depth, tint_symbol_2.sample_mask};
+  return tint_symbol_3;
+}
+
diff --git a/test/shader_io/fragment_output_mixed.wgsl.expected.spvasm b/test/shader_io/fragment_output_mixed.wgsl.expected.spvasm
new file mode 100644
index 0000000..164fdf0
--- /dev/null
+++ b/test/shader_io/fragment_output_mixed.wgsl.expected.spvasm
@@ -0,0 +1,97 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 49
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main" %tint_symbol_1 %tint_symbol_2 %tint_symbol_3 %tint_symbol_4 %tint_symbol_5 %tint_symbol_6
+               OpExecutionMode %main OriginUpperLeft
+               OpExecutionMode %main DepthReplacing
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %tint_symbol_2 "tint_symbol_2"
+               OpName %tint_symbol_3 "tint_symbol_3"
+               OpName %tint_symbol_4 "tint_symbol_4"
+               OpName %tint_symbol_5 "tint_symbol_5"
+               OpName %tint_symbol_6 "tint_symbol_6"
+               OpName %FragmentOutputs "FragmentOutputs"
+               OpMemberName %FragmentOutputs 0 "loc0"
+               OpMemberName %FragmentOutputs 1 "frag_depth"
+               OpMemberName %FragmentOutputs 2 "loc1"
+               OpMemberName %FragmentOutputs 3 "loc2"
+               OpMemberName %FragmentOutputs 4 "sample_mask"
+               OpMemberName %FragmentOutputs 5 "loc3"
+               OpName %tint_symbol_7 "tint_symbol_7"
+               OpName %tint_symbol "tint_symbol"
+               OpName %main "main"
+               OpDecorate %tint_symbol_1 Location 0
+               OpDecorate %tint_symbol_2 BuiltIn FragDepth
+               OpDecorate %tint_symbol_3 Location 1
+               OpDecorate %tint_symbol_4 Location 2
+               OpDecorate %_arr_uint_uint_1 ArrayStride 4
+               OpDecorate %tint_symbol_5 BuiltIn SampleMask
+               OpDecorate %tint_symbol_6 Location 3
+               OpMemberDecorate %FragmentOutputs 0 Offset 0
+               OpMemberDecorate %FragmentOutputs 1 Offset 4
+               OpMemberDecorate %FragmentOutputs 2 Offset 8
+               OpMemberDecorate %FragmentOutputs 3 Offset 12
+               OpMemberDecorate %FragmentOutputs 4 Offset 16
+               OpMemberDecorate %FragmentOutputs 5 Offset 32
+        %int = OpTypeInt 32 1
+%_ptr_Output_int = OpTypePointer Output %int
+          %4 = OpConstantNull %int
+%tint_symbol_1 = OpVariable %_ptr_Output_int Output %4
+      %float = OpTypeFloat 32
+%_ptr_Output_float = OpTypePointer Output %float
+          %8 = OpConstantNull %float
+%tint_symbol_2 = OpVariable %_ptr_Output_float Output %8
+       %uint = OpTypeInt 32 0
+%_ptr_Output_uint = OpTypePointer Output %uint
+         %12 = OpConstantNull %uint
+%tint_symbol_3 = OpVariable %_ptr_Output_uint Output %12
+%tint_symbol_4 = OpVariable %_ptr_Output_float Output %8
+     %uint_1 = OpConstant %uint 1
+%_arr_uint_uint_1 = OpTypeArray %uint %uint_1
+%_ptr_Output__arr_uint_uint_1 = OpTypePointer Output %_arr_uint_uint_1
+         %18 = OpConstantNull %_arr_uint_uint_1
+%tint_symbol_5 = OpVariable %_ptr_Output__arr_uint_uint_1 Output %18
+    %v4float = OpTypeVector %float 4
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+         %22 = OpConstantNull %v4float
+%tint_symbol_6 = OpVariable %_ptr_Output_v4float Output %22
+       %void = OpTypeVoid
+%FragmentOutputs = OpTypeStruct %int %float %uint %float %uint %v4float
+         %23 = OpTypeFunction %void %FragmentOutputs
+      %int_0 = OpConstant %int 0
+         %37 = OpTypeFunction %void
+      %int_1 = OpConstant %int 1
+    %float_2 = OpConstant %float 2
+    %float_1 = OpConstant %float 1
+     %uint_2 = OpConstant %uint 2
+    %float_3 = OpConstant %float 3
+    %float_4 = OpConstant %float 4
+         %47 = OpConstantComposite %v4float %float_1 %float_2 %float_3 %float_4
+         %48 = OpConstantComposite %FragmentOutputs %int_1 %float_2 %uint_1 %float_1 %uint_2 %47
+%tint_symbol_7 = OpFunction %void None %23
+%tint_symbol = OpFunctionParameter %FragmentOutputs
+         %28 = OpLabel
+         %29 = OpCompositeExtract %int %tint_symbol 0
+               OpStore %tint_symbol_1 %29
+         %30 = OpCompositeExtract %float %tint_symbol 1
+               OpStore %tint_symbol_2 %30
+         %31 = OpCompositeExtract %uint %tint_symbol 2
+               OpStore %tint_symbol_3 %31
+         %32 = OpCompositeExtract %float %tint_symbol 3
+               OpStore %tint_symbol_4 %32
+         %34 = OpAccessChain %_ptr_Output_uint %tint_symbol_5 %int_0
+         %35 = OpCompositeExtract %uint %tint_symbol 4
+               OpStore %34 %35
+         %36 = OpCompositeExtract %v4float %tint_symbol 5
+               OpStore %tint_symbol_6 %36
+               OpReturn
+               OpFunctionEnd
+       %main = OpFunction %void None %37
+         %39 = OpLabel
+         %40 = OpFunctionCall %void %tint_symbol_7 %48
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/fragment_output_mixed.wgsl.expected.wgsl b/test/shader_io/fragment_output_mixed.wgsl.expected.wgsl
new file mode 100644
index 0000000..3fb401c
--- /dev/null
+++ b/test/shader_io/fragment_output_mixed.wgsl.expected.wgsl
@@ -0,0 +1,19 @@
+struct FragmentOutputs {
+  [[location(0)]]
+  loc0 : i32;
+  [[builtin(frag_depth)]]
+  frag_depth : f32;
+  [[location(1)]]
+  loc1 : u32;
+  [[location(2)]]
+  loc2 : f32;
+  [[builtin(sample_mask)]]
+  sample_mask : u32;
+  [[location(3)]]
+  loc3 : vec4<f32>;
+};
+
+[[stage(fragment)]]
+fn main() -> FragmentOutputs {
+  return FragmentOutputs(1, 2.0, 1u, 1.0, 2u, vec4<f32>(1.0, 2.0, 3.0, 4.0));
+}
diff --git a/test/shader_io/shared_struct_different_stages.wgsl b/test/shader_io/shared_struct_different_stages.wgsl
new file mode 100644
index 0000000..802ce43
--- /dev/null
+++ b/test/shader_io/shared_struct_different_stages.wgsl
@@ -0,0 +1,16 @@
+struct Interface {
+  [[location(1)]] col1 : f32;
+  [[location(2)]] col2 : f32;
+  [[builtin(position)]] pos : vec4<f32>;
+};
+
+[[stage(vertex)]]
+fn vert_main() -> Interface {
+  return Interface(0.4, 0.6, vec4<f32>());
+}
+
+[[stage(fragment)]]
+fn frag_main(colors : Interface) {
+  let r : f32 = colors.col1;
+  let g : f32 = colors.col2;
+}
diff --git a/test/shader_io/shared_struct_different_stages.wgsl.expected.hlsl b/test/shader_io/shared_struct_different_stages.wgsl.expected.hlsl
new file mode 100644
index 0000000..f15cb86
--- /dev/null
+++ b/test/shader_io/shared_struct_different_stages.wgsl.expected.hlsl
@@ -0,0 +1,29 @@
+struct Interface {
+  float col1;
+  float col2;
+  float4 pos;
+};
+struct tint_symbol {
+  float col1 : TEXCOORD1;
+  float col2 : TEXCOORD2;
+  float4 pos : SV_Position;
+};
+struct tint_symbol_3 {
+  float col1 : TEXCOORD1;
+  float col2 : TEXCOORD2;
+  float4 pos : SV_Position;
+};
+
+tint_symbol vert_main() {
+  const Interface tint_symbol_1 = {0.400000006f, 0.600000024f, float4(0.0f, 0.0f, 0.0f, 0.0f)};
+  const tint_symbol tint_symbol_4 = {tint_symbol_1.col1, tint_symbol_1.col2, tint_symbol_1.pos};
+  return tint_symbol_4;
+}
+
+void frag_main(tint_symbol_3 tint_symbol_2) {
+  const Interface colors = {tint_symbol_2.col1, tint_symbol_2.col2, tint_symbol_2.pos};
+  const float r = colors.col1;
+  const float g = colors.col2;
+  return;
+}
+
diff --git a/test/shader_io/shared_struct_different_stages.wgsl.expected.msl b/test/shader_io/shared_struct_different_stages.wgsl.expected.msl
new file mode 100644
index 0000000..f303dc0
--- /dev/null
+++ b/test/shader_io/shared_struct_different_stages.wgsl.expected.msl
@@ -0,0 +1,32 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct Interface {
+  float col1;
+  float col2;
+  float4 pos;
+};
+struct tint_symbol {
+  float col1 [[user(locn1)]];
+  float col2 [[user(locn2)]];
+  float4 pos [[position]];
+};
+struct tint_symbol_3 {
+  float col1 [[user(locn1)]];
+  float col2 [[user(locn2)]];
+  float4 pos [[position]];
+};
+
+vertex tint_symbol vert_main() {
+  Interface const tint_symbol_1 = {0.400000006f, 0.600000024f, float4()};
+  tint_symbol const tint_symbol_4 = {tint_symbol_1.col1, tint_symbol_1.col2, tint_symbol_1.pos};
+  return tint_symbol_4;
+}
+
+fragment void frag_main(tint_symbol_3 tint_symbol_2 [[stage_in]]) {
+  Interface const colors = {tint_symbol_2.col1, tint_symbol_2.col2, tint_symbol_2.pos};
+  float const r = colors.col1;
+  float const g = colors.col2;
+  return;
+}
+
diff --git a/test/shader_io/shared_struct_different_stages.wgsl.expected.spvasm b/test/shader_io/shared_struct_different_stages.wgsl.expected.spvasm
new file mode 100644
index 0000000..9c505c1
--- /dev/null
+++ b/test/shader_io/shared_struct_different_stages.wgsl.expected.spvasm
@@ -0,0 +1,85 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 41
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Vertex %vert_main "vert_main" %tint_pointsize %tint_symbol_1 %tint_symbol_2 %tint_symbol_3
+               OpEntryPoint Fragment %frag_main "frag_main" %tint_symbol_5 %tint_symbol_6 %tint_symbol_7
+               OpExecutionMode %frag_main OriginUpperLeft
+               OpName %tint_pointsize "tint_pointsize"
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %tint_symbol_2 "tint_symbol_2"
+               OpName %tint_symbol_3 "tint_symbol_3"
+               OpName %tint_symbol_5 "tint_symbol_5"
+               OpName %tint_symbol_6 "tint_symbol_6"
+               OpName %tint_symbol_7 "tint_symbol_7"
+               OpName %Interface "Interface"
+               OpMemberName %Interface 0 "col1"
+               OpMemberName %Interface 1 "col2"
+               OpMemberName %Interface 2 "pos"
+               OpName %tint_symbol_4 "tint_symbol_4"
+               OpName %tint_symbol "tint_symbol"
+               OpName %vert_main "vert_main"
+               OpName %frag_main "frag_main"
+               OpDecorate %tint_pointsize BuiltIn PointSize
+               OpDecorate %tint_symbol_1 Location 1
+               OpDecorate %tint_symbol_2 Location 2
+               OpDecorate %tint_symbol_3 BuiltIn Position
+               OpDecorate %tint_symbol_5 Location 1
+               OpDecorate %tint_symbol_6 Location 2
+               OpDecorate %tint_symbol_7 BuiltIn FragCoord
+               OpMemberDecorate %Interface 0 Offset 0
+               OpMemberDecorate %Interface 1 Offset 4
+               OpMemberDecorate %Interface 2 Offset 16
+      %float = OpTypeFloat 32
+%_ptr_Output_float = OpTypePointer Output %float
+          %4 = OpConstantNull %float
+%tint_pointsize = OpVariable %_ptr_Output_float Output %4
+%tint_symbol_1 = OpVariable %_ptr_Output_float Output %4
+%tint_symbol_2 = OpVariable %_ptr_Output_float Output %4
+    %v4float = OpTypeVector %float 4
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+         %10 = OpConstantNull %v4float
+%tint_symbol_3 = OpVariable %_ptr_Output_v4float Output %10
+%_ptr_Input_float = OpTypePointer Input %float
+%tint_symbol_5 = OpVariable %_ptr_Input_float Input
+%tint_symbol_6 = OpVariable %_ptr_Input_float Input
+%_ptr_Input_v4float = OpTypePointer Input %v4float
+%tint_symbol_7 = OpVariable %_ptr_Input_v4float Input
+       %void = OpTypeVoid
+  %Interface = OpTypeStruct %float %float %v4float
+         %16 = OpTypeFunction %void %Interface
+         %25 = OpTypeFunction %void
+    %float_1 = OpConstant %float 1
+%float_0_400000006 = OpConstant %float 0.400000006
+%float_0_600000024 = OpConstant %float 0.600000024
+         %32 = OpConstantComposite %Interface %float_0_400000006 %float_0_600000024 %10
+%tint_symbol_4 = OpFunction %void None %16
+%tint_symbol = OpFunctionParameter %Interface
+         %21 = OpLabel
+         %22 = OpCompositeExtract %float %tint_symbol 0
+               OpStore %tint_symbol_1 %22
+         %23 = OpCompositeExtract %float %tint_symbol 1
+               OpStore %tint_symbol_2 %23
+         %24 = OpCompositeExtract %v4float %tint_symbol 2
+               OpStore %tint_symbol_3 %24
+               OpReturn
+               OpFunctionEnd
+  %vert_main = OpFunction %void None %25
+         %27 = OpLabel
+               OpStore %tint_pointsize %float_1
+         %29 = OpFunctionCall %void %tint_symbol_4 %32
+               OpReturn
+               OpFunctionEnd
+  %frag_main = OpFunction %void None %25
+         %34 = OpLabel
+         %35 = OpLoad %float %tint_symbol_5
+         %36 = OpLoad %float %tint_symbol_6
+         %37 = OpLoad %v4float %tint_symbol_7
+         %38 = OpCompositeConstruct %Interface %35 %36 %37
+         %39 = OpCompositeExtract %float %38 0
+         %40 = OpCompositeExtract %float %38 1
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/shared_struct_different_stages.wgsl.expected.wgsl b/test/shader_io/shared_struct_different_stages.wgsl.expected.wgsl
new file mode 100644
index 0000000..67a91d6
--- /dev/null
+++ b/test/shader_io/shared_struct_different_stages.wgsl.expected.wgsl
@@ -0,0 +1,19 @@
+struct Interface {
+  [[location(1)]]
+  col1 : f32;
+  [[location(2)]]
+  col2 : f32;
+  [[builtin(position)]]
+  pos : vec4<f32>;
+};
+
+[[stage(vertex)]]
+fn vert_main() -> Interface {
+  return Interface(0.400000006, 0.600000024, vec4<f32>());
+}
+
+[[stage(fragment)]]
+fn frag_main(colors : Interface) {
+  let r : f32 = colors.col1;
+  let g : f32 = colors.col2;
+}
diff --git a/test/shader_io/shared_struct_helper_function.wgsl b/test/shader_io/shared_struct_helper_function.wgsl
new file mode 100644
index 0000000..6527653
--- /dev/null
+++ b/test/shader_io/shared_struct_helper_function.wgsl
@@ -0,0 +1,16 @@
+struct VertexOutput {
+  [[builtin(position)]] pos : vec4<f32>;
+  [[location(0)]] loc0 : i32;
+};
+
+fn foo(x : f32) -> VertexOutput {
+  return VertexOutput(vec4<f32>(x, x, x, 1.0), 42);
+}
+
+fn vert_main1() -> VertexOutput {
+  return foo(0.5);
+}
+
+fn vert_main2() -> VertexOutput {
+  return foo(0.25);
+}
diff --git a/test/shader_io/shared_struct_helper_function.wgsl.expected.hlsl b/test/shader_io/shared_struct_helper_function.wgsl.expected.hlsl
new file mode 100644
index 0000000..46dca7e
--- /dev/null
+++ b/test/shader_io/shared_struct_helper_function.wgsl.expected.hlsl
@@ -0,0 +1,23 @@
+struct VertexOutput {
+  float4 pos;
+  int loc0;
+};
+
+VertexOutput foo(float x) {
+  const VertexOutput tint_symbol = {float4(x, x, x, 1.0f), 42};
+  return tint_symbol;
+}
+
+VertexOutput vert_main1() {
+  return foo(0.5f);
+}
+
+VertexOutput vert_main2() {
+  return foo(0.25f);
+}
+
+[numthreads(1, 1, 1)]
+void unused_entry_point() {
+  return;
+}
+
diff --git a/test/shader_io/shared_struct_helper_function.wgsl.expected.msl b/test/shader_io/shared_struct_helper_function.wgsl.expected.msl
new file mode 100644
index 0000000..f6c8f25
--- /dev/null
+++ b/test/shader_io/shared_struct_helper_function.wgsl.expected.msl
@@ -0,0 +1,21 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct VertexOutput {
+  float4 pos;
+  int loc0;
+};
+
+VertexOutput foo(float x) {
+  VertexOutput const tint_symbol = {float4(x, x, x, 1.0f), 42};
+  return tint_symbol;
+}
+
+VertexOutput vert_main1() {
+  return foo(0.5f);
+}
+
+VertexOutput vert_main2() {
+  return foo(0.25f);
+}
+
diff --git a/test/shader_io/shared_struct_helper_function.wgsl.expected.spvasm b/test/shader_io/shared_struct_helper_function.wgsl.expected.spvasm
new file mode 100644
index 0000000..ee3e97b
--- /dev/null
+++ b/test/shader_io/shared_struct_helper_function.wgsl.expected.spvasm
@@ -0,0 +1,52 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 26
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint GLCompute %unused_entry_point "unused_entry_point"
+               OpExecutionMode %unused_entry_point LocalSize 1 1 1
+               OpName %unused_entry_point "unused_entry_point"
+               OpName %VertexOutput "VertexOutput"
+               OpMemberName %VertexOutput 0 "pos"
+               OpMemberName %VertexOutput 1 "loc0"
+               OpName %foo "foo"
+               OpName %x "x"
+               OpName %vert_main1 "vert_main1"
+               OpName %vert_main2 "vert_main2"
+               OpMemberDecorate %VertexOutput 0 Offset 0
+               OpMemberDecorate %VertexOutput 1 Offset 16
+       %void = OpTypeVoid
+          %1 = OpTypeFunction %void
+      %float = OpTypeFloat 32
+    %v4float = OpTypeVector %float 4
+        %int = OpTypeInt 32 1
+%VertexOutput = OpTypeStruct %v4float %int
+          %5 = OpTypeFunction %VertexOutput %float
+    %float_1 = OpConstant %float 1
+     %int_42 = OpConstant %int 42
+         %17 = OpTypeFunction %VertexOutput
+  %float_0_5 = OpConstant %float 0.5
+ %float_0_25 = OpConstant %float 0.25
+%unused_entry_point = OpFunction %void None %1
+          %4 = OpLabel
+               OpReturn
+               OpFunctionEnd
+        %foo = OpFunction %VertexOutput None %5
+          %x = OpFunctionParameter %float
+         %12 = OpLabel
+         %14 = OpCompositeConstruct %v4float %x %x %x %float_1
+         %16 = OpCompositeConstruct %VertexOutput %14 %int_42
+               OpReturnValue %16
+               OpFunctionEnd
+ %vert_main1 = OpFunction %VertexOutput None %17
+         %19 = OpLabel
+         %20 = OpFunctionCall %VertexOutput %foo %float_0_5
+               OpReturnValue %20
+               OpFunctionEnd
+ %vert_main2 = OpFunction %VertexOutput None %17
+         %23 = OpLabel
+         %24 = OpFunctionCall %VertexOutput %foo %float_0_25
+               OpReturnValue %24
+               OpFunctionEnd
diff --git a/test/shader_io/shared_struct_helper_function.wgsl.expected.wgsl b/test/shader_io/shared_struct_helper_function.wgsl.expected.wgsl
new file mode 100644
index 0000000..e7a0978
--- /dev/null
+++ b/test/shader_io/shared_struct_helper_function.wgsl.expected.wgsl
@@ -0,0 +1,18 @@
+struct VertexOutput {
+  [[builtin(position)]]
+  pos : vec4<f32>;
+  [[location(0)]]
+  loc0 : i32;
+};
+
+fn foo(x : f32) -> VertexOutput {
+  return VertexOutput(vec4<f32>(x, x, x, 1.0), 42);
+}
+
+fn vert_main1() -> VertexOutput {
+  return foo(0.5);
+}
+
+fn vert_main2() -> VertexOutput {
+  return foo(0.25);
+}
diff --git a/test/shader_io/shared_struct_storage_buffer.wgsl b/test/shader_io/shared_struct_storage_buffer.wgsl
new file mode 100644
index 0000000..56f983c
--- /dev/null
+++ b/test/shader_io/shared_struct_storage_buffer.wgsl
@@ -0,0 +1,17 @@
+[[block]]
+struct S {
+  [[align(64)]] [[location(0)]] f : f32;
+  [[size(32)]] [[location(1)]] u : u32;
+  [[align(128)]] [[builtin(position)]] v : vec4<f32>;
+};
+
+[[group(0), binding(0)]]
+var<storage> output : [[access(write)]] S;
+
+[[stage(fragment)]]
+fn frag_main(input : S) {
+  let f : f32 = input.f;
+  let u : u32 = input.u;
+  let v : vec4<f32> = input.v;
+  output = input;
+}
diff --git a/test/shader_io/shared_struct_storage_buffer.wgsl.expected.hlsl b/test/shader_io/shared_struct_storage_buffer.wgsl.expected.hlsl
new file mode 100644
index 0000000..69591b8
--- /dev/null
+++ b/test/shader_io/shared_struct_storage_buffer.wgsl.expected.hlsl
@@ -0,0 +1,28 @@
+struct S {
+  float f;
+  uint u;
+  float4 v;
+};
+struct tint_symbol_1 {
+  float f : TEXCOORD0;
+  uint u : TEXCOORD1;
+  float4 v : SV_Position;
+};
+
+RWByteAddressBuffer output : register(u0, space0);
+
+void tint_symbol_5(RWByteAddressBuffer buffer, uint offset, S value) {
+  buffer.Store((offset + 0u), asuint(value.f));
+  buffer.Store((offset + 4u), asuint(value.u));
+  buffer.Store4((offset + 128u), asuint(value.v));
+}
+
+void frag_main(tint_symbol_1 tint_symbol) {
+  const S input = {tint_symbol.f, tint_symbol.u, tint_symbol.v};
+  const float f = input.f;
+  const uint u = input.u;
+  const float4 v = input.v;
+  tint_symbol_5(output, 0u, input);
+  return;
+}
+
diff --git a/test/shader_io/shared_struct_storage_buffer.wgsl.expected.msl b/test/shader_io/shared_struct_storage_buffer.wgsl.expected.msl
new file mode 100644
index 0000000..8831e0a
--- /dev/null
+++ b/test/shader_io/shared_struct_storage_buffer.wgsl.expected.msl
@@ -0,0 +1 @@
+SKIP: crbug.com/tint/853 type constructors broken with explicit layout structs
diff --git a/test/shader_io/shared_struct_storage_buffer.wgsl.expected.spvasm b/test/shader_io/shared_struct_storage_buffer.wgsl.expected.spvasm
new file mode 100644
index 0000000..527ffad
--- /dev/null
+++ b/test/shader_io/shared_struct_storage_buffer.wgsl.expected.spvasm
@@ -0,0 +1,54 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 24
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %frag_main "frag_main" %tint_symbol %tint_symbol_1 %tint_symbol_2
+               OpExecutionMode %frag_main OriginUpperLeft
+               OpName %S "S"
+               OpMemberName %S 0 "f"
+               OpMemberName %S 1 "u"
+               OpMemberName %S 2 "v"
+               OpName %output "output"
+               OpName %tint_symbol "tint_symbol"
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %tint_symbol_2 "tint_symbol_2"
+               OpName %frag_main "frag_main"
+               OpDecorate %S Block
+               OpMemberDecorate %S 0 Offset 0
+               OpMemberDecorate %S 1 Offset 4
+               OpMemberDecorate %S 2 Offset 128
+               OpDecorate %output NonReadable
+               OpDecorate %output DescriptorSet 0
+               OpDecorate %output Binding 0
+               OpDecorate %tint_symbol Location 0
+               OpDecorate %tint_symbol_1 Location 1
+               OpDecorate %tint_symbol_2 BuiltIn FragCoord
+      %float = OpTypeFloat 32
+       %uint = OpTypeInt 32 0
+    %v4float = OpTypeVector %float 4
+          %S = OpTypeStruct %float %uint %v4float
+%_ptr_StorageBuffer_S = OpTypePointer StorageBuffer %S
+     %output = OpVariable %_ptr_StorageBuffer_S StorageBuffer
+%_ptr_Input_float = OpTypePointer Input %float
+%tint_symbol = OpVariable %_ptr_Input_float Input
+%_ptr_Input_uint = OpTypePointer Input %uint
+%tint_symbol_1 = OpVariable %_ptr_Input_uint Input
+%_ptr_Input_v4float = OpTypePointer Input %v4float
+%tint_symbol_2 = OpVariable %_ptr_Input_v4float Input
+       %void = OpTypeVoid
+         %13 = OpTypeFunction %void
+  %frag_main = OpFunction %void None %13
+         %16 = OpLabel
+         %17 = OpLoad %float %tint_symbol
+         %18 = OpLoad %uint %tint_symbol_1
+         %19 = OpLoad %v4float %tint_symbol_2
+         %20 = OpCompositeConstruct %S %17 %18 %19
+         %21 = OpCompositeExtract %float %20 0
+         %22 = OpCompositeExtract %uint %20 1
+         %23 = OpCompositeExtract %v4float %20 2
+               OpStore %output %20
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/shared_struct_storage_buffer.wgsl.expected.wgsl b/test/shader_io/shared_struct_storage_buffer.wgsl.expected.wgsl
new file mode 100644
index 0000000..db8682f
--- /dev/null
+++ b/test/shader_io/shared_struct_storage_buffer.wgsl.expected.wgsl
@@ -0,0 +1,19 @@
+[[block]]
+struct S {
+  [[align(64), location(0)]]
+  f : f32;
+  [[size(32), location(1)]]
+  u : u32;
+  [[align(128), builtin(position)]]
+  v : vec4<f32>;
+};
+
+[[group(0), binding(0)]] var<storage> output : [[access(write)]] S;
+
+[[stage(fragment)]]
+fn frag_main(input : S) {
+  let f : f32 = input.f;
+  let u : u32 = input.u;
+  let v : vec4<f32> = input.v;
+  output = input;
+}
diff --git a/test/shader_io/vertex_input_builtins.wgsl b/test/shader_io/vertex_input_builtins.wgsl
new file mode 100644
index 0000000..6d207b5
--- /dev/null
+++ b/test/shader_io/vertex_input_builtins.wgsl
@@ -0,0 +1,8 @@
+[[stage(vertex)]]
+fn main(
+  [[builtin(vertex_index)]] vertex_index : u32,
+  [[builtin(instance_index)]] instance_index : u32,
+) -> [[builtin(position)]] vec4<f32> {
+  let foo : u32 = vertex_index + instance_index;
+  return vec4<f32>();
+}
diff --git a/test/shader_io/vertex_input_builtins.wgsl.expected.hlsl b/test/shader_io/vertex_input_builtins.wgsl.expected.hlsl
new file mode 100644
index 0000000..5a0e122
--- /dev/null
+++ b/test/shader_io/vertex_input_builtins.wgsl.expected.hlsl
@@ -0,0 +1,16 @@
+struct tint_symbol_1 {
+  uint vertex_index : SV_VertexID;
+  uint instance_index : SV_InstanceID;
+};
+struct tint_symbol_2 {
+  float4 value : SV_Position;
+};
+
+tint_symbol_2 main(tint_symbol_1 tint_symbol) {
+  const uint vertex_index = tint_symbol.vertex_index;
+  const uint instance_index = tint_symbol.instance_index;
+  const uint foo = (vertex_index + instance_index);
+  const tint_symbol_2 tint_symbol_3 = {float4(0.0f, 0.0f, 0.0f, 0.0f)};
+  return tint_symbol_3;
+}
+
diff --git a/test/shader_io/vertex_input_builtins.wgsl.expected.msl b/test/shader_io/vertex_input_builtins.wgsl.expected.msl
new file mode 100644
index 0000000..71d62fb
--- /dev/null
+++ b/test/shader_io/vertex_input_builtins.wgsl.expected.msl
@@ -0,0 +1 @@
+SKIP: crbug.com/tint/817 attribute only applies to parameters
diff --git a/test/shader_io/vertex_input_builtins.wgsl.expected.spvasm b/test/shader_io/vertex_input_builtins.wgsl.expected.spvasm
new file mode 100644
index 0000000..198293c
--- /dev/null
+++ b/test/shader_io/vertex_input_builtins.wgsl.expected.spvasm
@@ -0,0 +1,50 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 26
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Vertex %main "main" %tint_pointsize %tint_symbol %tint_symbol_1 %tint_symbol_3
+               OpName %tint_pointsize "tint_pointsize"
+               OpName %tint_symbol "tint_symbol"
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %tint_symbol_3 "tint_symbol_3"
+               OpName %tint_symbol_4 "tint_symbol_4"
+               OpName %tint_symbol_2 "tint_symbol_2"
+               OpName %main "main"
+               OpDecorate %tint_pointsize BuiltIn PointSize
+               OpDecorate %tint_symbol BuiltIn VertexIndex
+               OpDecorate %tint_symbol_1 BuiltIn InstanceIndex
+               OpDecorate %tint_symbol_3 BuiltIn Position
+      %float = OpTypeFloat 32
+%_ptr_Output_float = OpTypePointer Output %float
+          %4 = OpConstantNull %float
+%tint_pointsize = OpVariable %_ptr_Output_float Output %4
+       %uint = OpTypeInt 32 0
+%_ptr_Input_uint = OpTypePointer Input %uint
+%tint_symbol = OpVariable %_ptr_Input_uint Input
+%tint_symbol_1 = OpVariable %_ptr_Input_uint Input
+    %v4float = OpTypeVector %float 4
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+         %12 = OpConstantNull %v4float
+%tint_symbol_3 = OpVariable %_ptr_Output_v4float Output %12
+       %void = OpTypeVoid
+         %13 = OpTypeFunction %void %v4float
+         %18 = OpTypeFunction %void
+    %float_1 = OpConstant %float 1
+%tint_symbol_4 = OpFunction %void None %13
+%tint_symbol_2 = OpFunctionParameter %v4float
+         %17 = OpLabel
+               OpStore %tint_symbol_3 %tint_symbol_2
+               OpReturn
+               OpFunctionEnd
+       %main = OpFunction %void None %18
+         %20 = OpLabel
+               OpStore %tint_pointsize %float_1
+         %22 = OpLoad %uint %tint_symbol
+         %23 = OpLoad %uint %tint_symbol_1
+         %24 = OpIAdd %uint %22 %23
+         %25 = OpFunctionCall %void %tint_symbol_4 %12
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/vertex_input_builtins.wgsl.expected.wgsl b/test/shader_io/vertex_input_builtins.wgsl.expected.wgsl
new file mode 100644
index 0000000..ef2f3ac
--- /dev/null
+++ b/test/shader_io/vertex_input_builtins.wgsl.expected.wgsl
@@ -0,0 +1,5 @@
+[[stage(vertex)]]
+fn main([[builtin(vertex_index)]] vertex_index : u32, [[builtin(instance_index)]] instance_index : u32) -> [[builtin(position)]] vec4<f32> {
+  let foo : u32 = (vertex_index + instance_index);
+  return vec4<f32>();
+}
diff --git a/test/shader_io/vertex_input_builtins_struct.wgsl b/test/shader_io/vertex_input_builtins_struct.wgsl
new file mode 100644
index 0000000..d81bcdc
--- /dev/null
+++ b/test/shader_io/vertex_input_builtins_struct.wgsl
@@ -0,0 +1,10 @@
+struct VertexInputs {
+  [[builtin(vertex_index)]] vertex_index : u32;
+  [[builtin(instance_index)]] instance_index : u32;
+};
+
+[[stage(vertex)]]
+fn main(inputs : VertexInputs) -> [[builtin(position)]] vec4<f32> {
+  let foo : u32 = inputs.vertex_index + inputs.instance_index;
+  return vec4<f32>();
+}
diff --git a/test/shader_io/vertex_input_builtins_struct.wgsl.expected.hlsl b/test/shader_io/vertex_input_builtins_struct.wgsl.expected.hlsl
new file mode 100644
index 0000000..632e212
--- /dev/null
+++ b/test/shader_io/vertex_input_builtins_struct.wgsl.expected.hlsl
@@ -0,0 +1,19 @@
+struct VertexInputs {
+  uint vertex_index;
+  uint instance_index;
+};
+struct tint_symbol_1 {
+  uint vertex_index : SV_VertexID;
+  uint instance_index : SV_InstanceID;
+};
+struct tint_symbol_2 {
+  float4 value : SV_Position;
+};
+
+tint_symbol_2 main(tint_symbol_1 tint_symbol) {
+  const VertexInputs inputs = {tint_symbol.vertex_index, tint_symbol.instance_index};
+  const uint foo = (inputs.vertex_index + inputs.instance_index);
+  const tint_symbol_2 tint_symbol_3 = {float4(0.0f, 0.0f, 0.0f, 0.0f)};
+  return tint_symbol_3;
+}
+
diff --git a/test/shader_io/vertex_input_builtins_struct.wgsl.expected.msl b/test/shader_io/vertex_input_builtins_struct.wgsl.expected.msl
new file mode 100644
index 0000000..71d62fb
--- /dev/null
+++ b/test/shader_io/vertex_input_builtins_struct.wgsl.expected.msl
@@ -0,0 +1 @@
+SKIP: crbug.com/tint/817 attribute only applies to parameters
diff --git a/test/shader_io/vertex_input_builtins_struct.wgsl.expected.spvasm b/test/shader_io/vertex_input_builtins_struct.wgsl.expected.spvasm
new file mode 100644
index 0000000..b2af529
--- /dev/null
+++ b/test/shader_io/vertex_input_builtins_struct.wgsl.expected.spvasm
@@ -0,0 +1,59 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 30
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Vertex %main "main" %tint_pointsize %tint_symbol %tint_symbol_1 %tint_symbol_4
+               OpName %tint_pointsize "tint_pointsize"
+               OpName %tint_symbol "tint_symbol"
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %tint_symbol_4 "tint_symbol_4"
+               OpName %tint_symbol_5 "tint_symbol_5"
+               OpName %tint_symbol_3 "tint_symbol_3"
+               OpName %main "main"
+               OpName %VertexInputs "VertexInputs"
+               OpMemberName %VertexInputs 0 "vertex_index"
+               OpMemberName %VertexInputs 1 "instance_index"
+               OpDecorate %tint_pointsize BuiltIn PointSize
+               OpDecorate %tint_symbol BuiltIn VertexIndex
+               OpDecorate %tint_symbol_1 BuiltIn InstanceIndex
+               OpDecorate %tint_symbol_4 BuiltIn Position
+               OpMemberDecorate %VertexInputs 0 Offset 0
+               OpMemberDecorate %VertexInputs 1 Offset 4
+      %float = OpTypeFloat 32
+%_ptr_Output_float = OpTypePointer Output %float
+          %4 = OpConstantNull %float
+%tint_pointsize = OpVariable %_ptr_Output_float Output %4
+       %uint = OpTypeInt 32 0
+%_ptr_Input_uint = OpTypePointer Input %uint
+%tint_symbol = OpVariable %_ptr_Input_uint Input
+%tint_symbol_1 = OpVariable %_ptr_Input_uint Input
+    %v4float = OpTypeVector %float 4
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+         %12 = OpConstantNull %v4float
+%tint_symbol_4 = OpVariable %_ptr_Output_v4float Output %12
+       %void = OpTypeVoid
+         %13 = OpTypeFunction %void %v4float
+         %18 = OpTypeFunction %void
+    %float_1 = OpConstant %float 1
+%VertexInputs = OpTypeStruct %uint %uint
+%tint_symbol_5 = OpFunction %void None %13
+%tint_symbol_3 = OpFunctionParameter %v4float
+         %17 = OpLabel
+               OpStore %tint_symbol_4 %tint_symbol_3
+               OpReturn
+               OpFunctionEnd
+       %main = OpFunction %void None %18
+         %20 = OpLabel
+               OpStore %tint_pointsize %float_1
+         %23 = OpLoad %uint %tint_symbol
+         %24 = OpLoad %uint %tint_symbol_1
+         %25 = OpCompositeConstruct %VertexInputs %23 %24
+         %26 = OpCompositeExtract %uint %25 0
+         %27 = OpCompositeExtract %uint %25 1
+         %28 = OpIAdd %uint %26 %27
+         %29 = OpFunctionCall %void %tint_symbol_5 %12
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/vertex_input_builtins_struct.wgsl.expected.wgsl b/test/shader_io/vertex_input_builtins_struct.wgsl.expected.wgsl
new file mode 100644
index 0000000..f63100d
--- /dev/null
+++ b/test/shader_io/vertex_input_builtins_struct.wgsl.expected.wgsl
@@ -0,0 +1,12 @@
+struct VertexInputs {
+  [[builtin(vertex_index)]]
+  vertex_index : u32;
+  [[builtin(instance_index)]]
+  instance_index : u32;
+};
+
+[[stage(vertex)]]
+fn main(inputs : VertexInputs) -> [[builtin(position)]] vec4<f32> {
+  let foo : u32 = (inputs.vertex_index + inputs.instance_index);
+  return vec4<f32>();
+}
diff --git a/test/shader_io/vertex_input_locations.wgsl b/test/shader_io/vertex_input_locations.wgsl
new file mode 100644
index 0000000..13fad04
--- /dev/null
+++ b/test/shader_io/vertex_input_locations.wgsl
@@ -0,0 +1,13 @@
+[[stage(vertex)]]
+fn main(
+  [[location(0)]] loc0 : i32,
+  [[location(1)]] loc1 : u32,
+  [[location(2)]] loc2 : f32,
+  [[location(3)]] loc3 : vec4<f32>,
+) -> [[builtin(position)]] vec4<f32> {
+  let i : i32 = loc0;
+  let u : u32 = loc1;
+  let f : f32 = loc2;
+  let v : vec4<f32> = loc3;
+  return vec4<f32>();
+}
diff --git a/test/shader_io/vertex_input_locations.wgsl.expected.hlsl b/test/shader_io/vertex_input_locations.wgsl.expected.hlsl
new file mode 100644
index 0000000..3bb4613
--- /dev/null
+++ b/test/shader_io/vertex_input_locations.wgsl.expected.hlsl
@@ -0,0 +1,23 @@
+struct tint_symbol_1 {
+  int loc0 : TEXCOORD0;
+  uint loc1 : TEXCOORD1;
+  float loc2 : TEXCOORD2;
+  float4 loc3 : TEXCOORD3;
+};
+struct tint_symbol_2 {
+  float4 value : SV_Position;
+};
+
+tint_symbol_2 main(tint_symbol_1 tint_symbol) {
+  const int loc0 = tint_symbol.loc0;
+  const uint loc1 = tint_symbol.loc1;
+  const float loc2 = tint_symbol.loc2;
+  const float4 loc3 = tint_symbol.loc3;
+  const int i = loc0;
+  const uint u = loc1;
+  const float f = loc2;
+  const float4 v = loc3;
+  const tint_symbol_2 tint_symbol_3 = {float4(0.0f, 0.0f, 0.0f, 0.0f)};
+  return tint_symbol_3;
+}
+
diff --git a/test/shader_io/vertex_input_locations.wgsl.expected.msl b/test/shader_io/vertex_input_locations.wgsl.expected.msl
new file mode 100644
index 0000000..a4c8606
--- /dev/null
+++ b/test/shader_io/vertex_input_locations.wgsl.expected.msl
@@ -0,0 +1,26 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct tint_symbol_2 {
+  int loc0 [[attribute(0)]];
+  uint loc1 [[attribute(1)]];
+  float loc2 [[attribute(2)]];
+  float4 loc3 [[attribute(3)]];
+};
+struct tint_symbol_3 {
+  float4 value [[position]];
+};
+
+vertex tint_symbol_3 tint_symbol(tint_symbol_2 tint_symbol_1 [[stage_in]]) {
+  int const loc0 = tint_symbol_1.loc0;
+  uint const loc1 = tint_symbol_1.loc1;
+  float const loc2 = tint_symbol_1.loc2;
+  float4 const loc3 = tint_symbol_1.loc3;
+  int const i = loc0;
+  uint const u = loc1;
+  float const f = loc2;
+  float4 const v = loc3;
+  tint_symbol_3 const tint_symbol_4 = {float4()};
+  return tint_symbol_4;
+}
+
diff --git a/test/shader_io/vertex_input_locations.wgsl.expected.spvasm b/test/shader_io/vertex_input_locations.wgsl.expected.spvasm
new file mode 100644
index 0000000..9a0c93b
--- /dev/null
+++ b/test/shader_io/vertex_input_locations.wgsl.expected.spvasm
@@ -0,0 +1,61 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 33
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Vertex %main "main" %tint_pointsize %tint_symbol %tint_symbol_1 %tint_symbol_2 %tint_symbol_3 %tint_symbol_5
+               OpName %tint_pointsize "tint_pointsize"
+               OpName %tint_symbol "tint_symbol"
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %tint_symbol_2 "tint_symbol_2"
+               OpName %tint_symbol_3 "tint_symbol_3"
+               OpName %tint_symbol_5 "tint_symbol_5"
+               OpName %tint_symbol_6 "tint_symbol_6"
+               OpName %tint_symbol_4 "tint_symbol_4"
+               OpName %main "main"
+               OpDecorate %tint_pointsize BuiltIn PointSize
+               OpDecorate %tint_symbol Location 0
+               OpDecorate %tint_symbol_1 Location 1
+               OpDecorate %tint_symbol_2 Location 2
+               OpDecorate %tint_symbol_3 Location 3
+               OpDecorate %tint_symbol_5 BuiltIn Position
+      %float = OpTypeFloat 32
+%_ptr_Output_float = OpTypePointer Output %float
+          %4 = OpConstantNull %float
+%tint_pointsize = OpVariable %_ptr_Output_float Output %4
+        %int = OpTypeInt 32 1
+%_ptr_Input_int = OpTypePointer Input %int
+%tint_symbol = OpVariable %_ptr_Input_int Input
+       %uint = OpTypeInt 32 0
+%_ptr_Input_uint = OpTypePointer Input %uint
+%tint_symbol_1 = OpVariable %_ptr_Input_uint Input
+%_ptr_Input_float = OpTypePointer Input %float
+%tint_symbol_2 = OpVariable %_ptr_Input_float Input
+    %v4float = OpTypeVector %float 4
+%_ptr_Input_v4float = OpTypePointer Input %v4float
+%tint_symbol_3 = OpVariable %_ptr_Input_v4float Input
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+         %18 = OpConstantNull %v4float
+%tint_symbol_5 = OpVariable %_ptr_Output_v4float Output %18
+       %void = OpTypeVoid
+         %19 = OpTypeFunction %void %v4float
+         %24 = OpTypeFunction %void
+    %float_1 = OpConstant %float 1
+%tint_symbol_6 = OpFunction %void None %19
+%tint_symbol_4 = OpFunctionParameter %v4float
+         %23 = OpLabel
+               OpStore %tint_symbol_5 %tint_symbol_4
+               OpReturn
+               OpFunctionEnd
+       %main = OpFunction %void None %24
+         %26 = OpLabel
+               OpStore %tint_pointsize %float_1
+         %28 = OpLoad %int %tint_symbol
+         %29 = OpLoad %uint %tint_symbol_1
+         %30 = OpLoad %float %tint_symbol_2
+         %31 = OpLoad %v4float %tint_symbol_3
+         %32 = OpFunctionCall %void %tint_symbol_6 %18
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/vertex_input_locations.wgsl.expected.wgsl b/test/shader_io/vertex_input_locations.wgsl.expected.wgsl
new file mode 100644
index 0000000..3730fec
--- /dev/null
+++ b/test/shader_io/vertex_input_locations.wgsl.expected.wgsl
@@ -0,0 +1,8 @@
+[[stage(vertex)]]
+fn main([[location(0)]] loc0 : i32, [[location(1)]] loc1 : u32, [[location(2)]] loc2 : f32, [[location(3)]] loc3 : vec4<f32>) -> [[builtin(position)]] vec4<f32> {
+  let i : i32 = loc0;
+  let u : u32 = loc1;
+  let f : f32 = loc2;
+  let v : vec4<f32> = loc3;
+  return vec4<f32>();
+}
diff --git a/test/shader_io/vertex_input_locations_struct.wgsl b/test/shader_io/vertex_input_locations_struct.wgsl
new file mode 100644
index 0000000..441ffdc
--- /dev/null
+++ b/test/shader_io/vertex_input_locations_struct.wgsl
@@ -0,0 +1,15 @@
+struct VertexInputs {
+  [[location(0)]] loc0 : i32;
+  [[location(1)]] loc1 : u32;
+  [[location(2)]] loc2 : f32;
+  [[location(3)]] loc3 : vec4<f32>;
+};
+
+[[stage(vertex)]]
+fn main(inputs : VertexInputs) -> [[builtin(position)]] vec4<f32> {
+  let i : i32 = inputs.loc0;
+  let u : u32 = inputs.loc1;
+  let f : f32 = inputs.loc2;
+  let v : vec4<f32> = inputs.loc3;
+  return vec4<f32>();
+}
diff --git a/test/shader_io/vertex_input_locations_struct.wgsl.expected.hlsl b/test/shader_io/vertex_input_locations_struct.wgsl.expected.hlsl
new file mode 100644
index 0000000..f93f2fe
--- /dev/null
+++ b/test/shader_io/vertex_input_locations_struct.wgsl.expected.hlsl
@@ -0,0 +1,26 @@
+struct VertexInputs {
+  int loc0;
+  uint loc1;
+  float loc2;
+  float4 loc3;
+};
+struct tint_symbol_1 {
+  int loc0 : TEXCOORD0;
+  uint loc1 : TEXCOORD1;
+  float loc2 : TEXCOORD2;
+  float4 loc3 : TEXCOORD3;
+};
+struct tint_symbol_2 {
+  float4 value : SV_Position;
+};
+
+tint_symbol_2 main(tint_symbol_1 tint_symbol) {
+  const VertexInputs inputs = {tint_symbol.loc0, tint_symbol.loc1, tint_symbol.loc2, tint_symbol.loc3};
+  const int i = inputs.loc0;
+  const uint u = inputs.loc1;
+  const float f = inputs.loc2;
+  const float4 v = inputs.loc3;
+  const tint_symbol_2 tint_symbol_3 = {float4(0.0f, 0.0f, 0.0f, 0.0f)};
+  return tint_symbol_3;
+}
+
diff --git a/test/shader_io/vertex_input_locations_struct.wgsl.expected.msl b/test/shader_io/vertex_input_locations_struct.wgsl.expected.msl
new file mode 100644
index 0000000..6cce9d7
--- /dev/null
+++ b/test/shader_io/vertex_input_locations_struct.wgsl.expected.msl
@@ -0,0 +1,29 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct VertexInputs {
+  int loc0;
+  uint loc1;
+  float loc2;
+  float4 loc3;
+};
+struct tint_symbol_2 {
+  int loc0 [[attribute(0)]];
+  uint loc1 [[attribute(1)]];
+  float loc2 [[attribute(2)]];
+  float4 loc3 [[attribute(3)]];
+};
+struct tint_symbol_3 {
+  float4 value [[position]];
+};
+
+vertex tint_symbol_3 tint_symbol(tint_symbol_2 tint_symbol_1 [[stage_in]]) {
+  VertexInputs const inputs = {tint_symbol_1.loc0, tint_symbol_1.loc1, tint_symbol_1.loc2, tint_symbol_1.loc3};
+  int const i = inputs.loc0;
+  uint const u = inputs.loc1;
+  float const f = inputs.loc2;
+  float4 const v = inputs.loc3;
+  tint_symbol_3 const tint_symbol_4 = {float4()};
+  return tint_symbol_4;
+}
+
diff --git a/test/shader_io/vertex_input_locations_struct.wgsl.expected.spvasm b/test/shader_io/vertex_input_locations_struct.wgsl.expected.spvasm
new file mode 100644
index 0000000..3766af8
--- /dev/null
+++ b/test/shader_io/vertex_input_locations_struct.wgsl.expected.spvasm
@@ -0,0 +1,76 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 39
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Vertex %main "main" %tint_pointsize %tint_symbol %tint_symbol_1 %tint_symbol_2 %tint_symbol_3 %tint_symbol_6
+               OpName %tint_pointsize "tint_pointsize"
+               OpName %tint_symbol "tint_symbol"
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %tint_symbol_2 "tint_symbol_2"
+               OpName %tint_symbol_3 "tint_symbol_3"
+               OpName %tint_symbol_6 "tint_symbol_6"
+               OpName %tint_symbol_7 "tint_symbol_7"
+               OpName %tint_symbol_5 "tint_symbol_5"
+               OpName %main "main"
+               OpName %VertexInputs "VertexInputs"
+               OpMemberName %VertexInputs 0 "loc0"
+               OpMemberName %VertexInputs 1 "loc1"
+               OpMemberName %VertexInputs 2 "loc2"
+               OpMemberName %VertexInputs 3 "loc3"
+               OpDecorate %tint_pointsize BuiltIn PointSize
+               OpDecorate %tint_symbol Location 0
+               OpDecorate %tint_symbol_1 Location 1
+               OpDecorate %tint_symbol_2 Location 2
+               OpDecorate %tint_symbol_3 Location 3
+               OpDecorate %tint_symbol_6 BuiltIn Position
+               OpMemberDecorate %VertexInputs 0 Offset 0
+               OpMemberDecorate %VertexInputs 1 Offset 4
+               OpMemberDecorate %VertexInputs 2 Offset 8
+               OpMemberDecorate %VertexInputs 3 Offset 16
+      %float = OpTypeFloat 32
+%_ptr_Output_float = OpTypePointer Output %float
+          %4 = OpConstantNull %float
+%tint_pointsize = OpVariable %_ptr_Output_float Output %4
+        %int = OpTypeInt 32 1
+%_ptr_Input_int = OpTypePointer Input %int
+%tint_symbol = OpVariable %_ptr_Input_int Input
+       %uint = OpTypeInt 32 0
+%_ptr_Input_uint = OpTypePointer Input %uint
+%tint_symbol_1 = OpVariable %_ptr_Input_uint Input
+%_ptr_Input_float = OpTypePointer Input %float
+%tint_symbol_2 = OpVariable %_ptr_Input_float Input
+    %v4float = OpTypeVector %float 4
+%_ptr_Input_v4float = OpTypePointer Input %v4float
+%tint_symbol_3 = OpVariable %_ptr_Input_v4float Input
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+         %18 = OpConstantNull %v4float
+%tint_symbol_6 = OpVariable %_ptr_Output_v4float Output %18
+       %void = OpTypeVoid
+         %19 = OpTypeFunction %void %v4float
+         %24 = OpTypeFunction %void
+    %float_1 = OpConstant %float 1
+%VertexInputs = OpTypeStruct %int %uint %float %v4float
+%tint_symbol_7 = OpFunction %void None %19
+%tint_symbol_5 = OpFunctionParameter %v4float
+         %23 = OpLabel
+               OpStore %tint_symbol_6 %tint_symbol_5
+               OpReturn
+               OpFunctionEnd
+       %main = OpFunction %void None %24
+         %26 = OpLabel
+               OpStore %tint_pointsize %float_1
+         %29 = OpLoad %int %tint_symbol
+         %30 = OpLoad %uint %tint_symbol_1
+         %31 = OpLoad %float %tint_symbol_2
+         %32 = OpLoad %v4float %tint_symbol_3
+         %33 = OpCompositeConstruct %VertexInputs %29 %30 %31 %32
+         %34 = OpCompositeExtract %int %33 0
+         %35 = OpCompositeExtract %uint %33 1
+         %36 = OpCompositeExtract %float %33 2
+         %37 = OpCompositeExtract %v4float %33 3
+         %38 = OpFunctionCall %void %tint_symbol_7 %18
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/vertex_input_locations_struct.wgsl.expected.wgsl b/test/shader_io/vertex_input_locations_struct.wgsl.expected.wgsl
new file mode 100644
index 0000000..28d40aa
--- /dev/null
+++ b/test/shader_io/vertex_input_locations_struct.wgsl.expected.wgsl
@@ -0,0 +1,19 @@
+struct VertexInputs {
+  [[location(0)]]
+  loc0 : i32;
+  [[location(1)]]
+  loc1 : u32;
+  [[location(2)]]
+  loc2 : f32;
+  [[location(3)]]
+  loc3 : vec4<f32>;
+};
+
+[[stage(vertex)]]
+fn main(inputs : VertexInputs) -> [[builtin(position)]] vec4<f32> {
+  let i : i32 = inputs.loc0;
+  let u : u32 = inputs.loc1;
+  let f : f32 = inputs.loc2;
+  let v : vec4<f32> = inputs.loc3;
+  return vec4<f32>();
+}
diff --git a/test/shader_io/vertex_input_mixed.wgsl b/test/shader_io/vertex_input_mixed.wgsl
new file mode 100644
index 0000000..7c5eafe
--- /dev/null
+++ b/test/shader_io/vertex_input_mixed.wgsl
@@ -0,0 +1,23 @@
+struct VertexInputs0 {
+  [[builtin(vertex_index)]] vertex_index : u32;
+  [[location(0)]] loc0 : i32;
+};
+struct VertexInputs1 {
+  [[location(2)]] loc2 : f32;
+  [[location(3)]] loc3 : vec4<f32>;
+};
+
+[[stage(vertex)]]
+fn main(
+  inputs0 : VertexInputs0,
+  [[location(1)]] loc1 : u32,
+  [[builtin(instance_index)]] instance_index : u32,
+  inputs1 : VertexInputs1,
+) -> [[builtin(position)]] vec4<f32> {
+  let foo : u32 = inputs0.vertex_index + instance_index;
+  let i : i32 = inputs0.loc0;
+  let u : u32 = loc1;
+  let f : f32 = inputs1.loc2;
+  let v : vec4<f32> = inputs1.loc3;
+  return vec4<f32>();
+}
diff --git a/test/shader_io/vertex_input_mixed.wgsl.expected.hlsl b/test/shader_io/vertex_input_mixed.wgsl.expected.hlsl
new file mode 100644
index 0000000..aa630cf
--- /dev/null
+++ b/test/shader_io/vertex_input_mixed.wgsl.expected.hlsl
@@ -0,0 +1,34 @@
+struct VertexInputs0 {
+  uint vertex_index;
+  int loc0;
+};
+struct VertexInputs1 {
+  float loc2;
+  float4 loc3;
+};
+struct tint_symbol_1 {
+  int loc0 : TEXCOORD0;
+  uint loc1 : TEXCOORD1;
+  float loc2 : TEXCOORD2;
+  float4 loc3 : TEXCOORD3;
+  uint vertex_index : SV_VertexID;
+  uint instance_index : SV_InstanceID;
+};
+struct tint_symbol_2 {
+  float4 value : SV_Position;
+};
+
+tint_symbol_2 main(tint_symbol_1 tint_symbol) {
+  const VertexInputs0 inputs0 = {tint_symbol.vertex_index, tint_symbol.loc0};
+  const uint loc1 = tint_symbol.loc1;
+  const uint instance_index = tint_symbol.instance_index;
+  const VertexInputs1 inputs1 = {tint_symbol.loc2, tint_symbol.loc3};
+  const uint foo = (inputs0.vertex_index + instance_index);
+  const int i = inputs0.loc0;
+  const uint u = loc1;
+  const float f = inputs1.loc2;
+  const float4 v = inputs1.loc3;
+  const tint_symbol_2 tint_symbol_3 = {float4(0.0f, 0.0f, 0.0f, 0.0f)};
+  return tint_symbol_3;
+}
+
diff --git a/test/shader_io/vertex_input_mixed.wgsl.expected.msl b/test/shader_io/vertex_input_mixed.wgsl.expected.msl
new file mode 100644
index 0000000..71d62fb
--- /dev/null
+++ b/test/shader_io/vertex_input_mixed.wgsl.expected.msl
@@ -0,0 +1 @@
+SKIP: crbug.com/tint/817 attribute only applies to parameters
diff --git a/test/shader_io/vertex_input_mixed.wgsl.expected.spvasm b/test/shader_io/vertex_input_mixed.wgsl.expected.spvasm
new file mode 100644
index 0000000..f930879
--- /dev/null
+++ b/test/shader_io/vertex_input_mixed.wgsl.expected.spvasm
@@ -0,0 +1,88 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 46
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Vertex %main "main" %tint_pointsize %tint_symbol %tint_symbol_1 %tint_symbol_5 %tint_symbol_6 %tint_symbol_4 %tint_symbol_3 %tint_symbol_9
+               OpName %tint_pointsize "tint_pointsize"
+               OpName %tint_symbol "tint_symbol"
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %tint_symbol_3 "tint_symbol_3"
+               OpName %tint_symbol_4 "tint_symbol_4"
+               OpName %tint_symbol_5 "tint_symbol_5"
+               OpName %tint_symbol_6 "tint_symbol_6"
+               OpName %tint_symbol_9 "tint_symbol_9"
+               OpName %tint_symbol_10 "tint_symbol_10"
+               OpName %tint_symbol_8 "tint_symbol_8"
+               OpName %main "main"
+               OpName %VertexInputs0 "VertexInputs0"
+               OpMemberName %VertexInputs0 0 "vertex_index"
+               OpMemberName %VertexInputs0 1 "loc0"
+               OpName %VertexInputs1 "VertexInputs1"
+               OpMemberName %VertexInputs1 0 "loc2"
+               OpMemberName %VertexInputs1 1 "loc3"
+               OpDecorate %tint_pointsize BuiltIn PointSize
+               OpDecorate %tint_symbol BuiltIn VertexIndex
+               OpDecorate %tint_symbol_1 Location 0
+               OpDecorate %tint_symbol_3 Location 1
+               OpDecorate %tint_symbol_4 BuiltIn InstanceIndex
+               OpDecorate %tint_symbol_5 Location 2
+               OpDecorate %tint_symbol_6 Location 3
+               OpDecorate %tint_symbol_9 BuiltIn Position
+               OpMemberDecorate %VertexInputs0 0 Offset 0
+               OpMemberDecorate %VertexInputs0 1 Offset 4
+               OpMemberDecorate %VertexInputs1 0 Offset 0
+               OpMemberDecorate %VertexInputs1 1 Offset 16
+      %float = OpTypeFloat 32
+%_ptr_Output_float = OpTypePointer Output %float
+          %4 = OpConstantNull %float
+%tint_pointsize = OpVariable %_ptr_Output_float Output %4
+       %uint = OpTypeInt 32 0
+%_ptr_Input_uint = OpTypePointer Input %uint
+%tint_symbol = OpVariable %_ptr_Input_uint Input
+        %int = OpTypeInt 32 1
+%_ptr_Input_int = OpTypePointer Input %int
+%tint_symbol_1 = OpVariable %_ptr_Input_int Input
+%tint_symbol_3 = OpVariable %_ptr_Input_uint Input
+%tint_symbol_4 = OpVariable %_ptr_Input_uint Input
+%_ptr_Input_float = OpTypePointer Input %float
+%tint_symbol_5 = OpVariable %_ptr_Input_float Input
+    %v4float = OpTypeVector %float 4
+%_ptr_Input_v4float = OpTypePointer Input %v4float
+%tint_symbol_6 = OpVariable %_ptr_Input_v4float Input
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+         %20 = OpConstantNull %v4float
+%tint_symbol_9 = OpVariable %_ptr_Output_v4float Output %20
+       %void = OpTypeVoid
+         %21 = OpTypeFunction %void %v4float
+         %26 = OpTypeFunction %void
+    %float_1 = OpConstant %float 1
+%VertexInputs0 = OpTypeStruct %uint %int
+%VertexInputs1 = OpTypeStruct %float %v4float
+%tint_symbol_10 = OpFunction %void None %21
+%tint_symbol_8 = OpFunctionParameter %v4float
+         %25 = OpLabel
+               OpStore %tint_symbol_9 %tint_symbol_8
+               OpReturn
+               OpFunctionEnd
+       %main = OpFunction %void None %26
+         %28 = OpLabel
+               OpStore %tint_pointsize %float_1
+         %31 = OpLoad %uint %tint_symbol
+         %32 = OpLoad %int %tint_symbol_1
+         %33 = OpCompositeConstruct %VertexInputs0 %31 %32
+         %35 = OpLoad %float %tint_symbol_5
+         %36 = OpLoad %v4float %tint_symbol_6
+         %37 = OpCompositeConstruct %VertexInputs1 %35 %36
+         %38 = OpCompositeExtract %uint %33 0
+         %39 = OpLoad %uint %tint_symbol_4
+         %40 = OpIAdd %uint %38 %39
+         %41 = OpCompositeExtract %int %33 1
+         %42 = OpLoad %uint %tint_symbol_3
+         %43 = OpCompositeExtract %float %37 0
+         %44 = OpCompositeExtract %v4float %37 1
+         %45 = OpFunctionCall %void %tint_symbol_10 %20
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/vertex_input_mixed.wgsl.expected.wgsl b/test/shader_io/vertex_input_mixed.wgsl.expected.wgsl
new file mode 100644
index 0000000..0cd61f6
--- /dev/null
+++ b/test/shader_io/vertex_input_mixed.wgsl.expected.wgsl
@@ -0,0 +1,23 @@
+struct VertexInputs0 {
+  [[builtin(vertex_index)]]
+  vertex_index : u32;
+  [[location(0)]]
+  loc0 : i32;
+};
+
+struct VertexInputs1 {
+  [[location(2)]]
+  loc2 : f32;
+  [[location(3)]]
+  loc3 : vec4<f32>;
+};
+
+[[stage(vertex)]]
+fn main(inputs0 : VertexInputs0, [[location(1)]] loc1 : u32, [[builtin(instance_index)]] instance_index : u32, inputs1 : VertexInputs1) -> [[builtin(position)]] vec4<f32> {
+  let foo : u32 = (inputs0.vertex_index + instance_index);
+  let i : i32 = inputs0.loc0;
+  let u : u32 = loc1;
+  let f : f32 = inputs1.loc2;
+  let v : vec4<f32> = inputs1.loc3;
+  return vec4<f32>();
+}
diff --git a/test/shader_io/vertex_output_builtins.wgsl b/test/shader_io/vertex_output_builtins.wgsl
new file mode 100644
index 0000000..46776de
--- /dev/null
+++ b/test/shader_io/vertex_output_builtins.wgsl
@@ -0,0 +1,4 @@
+[[stage(vertex)]]
+fn main() -> [[builtin(position)]] vec4<f32> {
+  return vec4<f32>(1.0, 2.0, 3.0, 4.0);
+}
diff --git a/test/shader_io/vertex_output_builtins.wgsl.expected.hlsl b/test/shader_io/vertex_output_builtins.wgsl.expected.hlsl
new file mode 100644
index 0000000..1c95996
--- /dev/null
+++ b/test/shader_io/vertex_output_builtins.wgsl.expected.hlsl
@@ -0,0 +1,9 @@
+struct tint_symbol {
+  float4 value : SV_Position;
+};
+
+tint_symbol main() {
+  const tint_symbol tint_symbol_1 = {float4(1.0f, 2.0f, 3.0f, 4.0f)};
+  return tint_symbol_1;
+}
+
diff --git a/test/shader_io/vertex_output_builtins.wgsl.expected.msl b/test/shader_io/vertex_output_builtins.wgsl.expected.msl
new file mode 100644
index 0000000..6cb014c
--- /dev/null
+++ b/test/shader_io/vertex_output_builtins.wgsl.expected.msl
@@ -0,0 +1,12 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct tint_symbol_1 {
+  float4 value [[position]];
+};
+
+vertex tint_symbol_1 tint_symbol() {
+  tint_symbol_1 const tint_symbol_2 = {float4(1.0f, 2.0f, 3.0f, 4.0f)};
+  return tint_symbol_2;
+}
+
diff --git a/test/shader_io/vertex_output_builtins.wgsl.expected.spvasm b/test/shader_io/vertex_output_builtins.wgsl.expected.spvasm
new file mode 100644
index 0000000..c92e684
--- /dev/null
+++ b/test/shader_io/vertex_output_builtins.wgsl.expected.spvasm
@@ -0,0 +1,43 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 23
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Vertex %main "main" %tint_pointsize %tint_symbol_1
+               OpName %tint_pointsize "tint_pointsize"
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %tint_symbol_2 "tint_symbol_2"
+               OpName %tint_symbol "tint_symbol"
+               OpName %main "main"
+               OpDecorate %tint_pointsize BuiltIn PointSize
+               OpDecorate %tint_symbol_1 BuiltIn Position
+      %float = OpTypeFloat 32
+%_ptr_Output_float = OpTypePointer Output %float
+          %4 = OpConstantNull %float
+%tint_pointsize = OpVariable %_ptr_Output_float Output %4
+    %v4float = OpTypeVector %float 4
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+          %8 = OpConstantNull %v4float
+%tint_symbol_1 = OpVariable %_ptr_Output_v4float Output %8
+       %void = OpTypeVoid
+          %9 = OpTypeFunction %void %v4float
+         %14 = OpTypeFunction %void
+    %float_1 = OpConstant %float 1
+    %float_2 = OpConstant %float 2
+    %float_3 = OpConstant %float 3
+    %float_4 = OpConstant %float 4
+         %22 = OpConstantComposite %v4float %float_1 %float_2 %float_3 %float_4
+%tint_symbol_2 = OpFunction %void None %9
+%tint_symbol = OpFunctionParameter %v4float
+         %13 = OpLabel
+               OpStore %tint_symbol_1 %tint_symbol
+               OpReturn
+               OpFunctionEnd
+       %main = OpFunction %void None %14
+         %16 = OpLabel
+               OpStore %tint_pointsize %float_1
+         %18 = OpFunctionCall %void %tint_symbol_2 %22
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/vertex_output_builtins.wgsl.expected.wgsl b/test/shader_io/vertex_output_builtins.wgsl.expected.wgsl
new file mode 100644
index 0000000..46776de
--- /dev/null
+++ b/test/shader_io/vertex_output_builtins.wgsl.expected.wgsl
@@ -0,0 +1,4 @@
+[[stage(vertex)]]
+fn main() -> [[builtin(position)]] vec4<f32> {
+  return vec4<f32>(1.0, 2.0, 3.0, 4.0);
+}
diff --git a/test/shader_io/vertex_output_builtins_struct.wgsl b/test/shader_io/vertex_output_builtins_struct.wgsl
new file mode 100644
index 0000000..9e09d38
--- /dev/null
+++ b/test/shader_io/vertex_output_builtins_struct.wgsl
@@ -0,0 +1,8 @@
+struct VertexOutputs {
+  [[builtin(position)]] position : vec4<f32>;
+};
+
+[[stage(vertex)]]
+fn main() -> VertexOutputs {
+  return VertexOutputs(vec4<f32>(1.0, 2.0, 3.0, 4.0));
+}
diff --git a/test/shader_io/vertex_output_builtins_struct.wgsl.expected.hlsl b/test/shader_io/vertex_output_builtins_struct.wgsl.expected.hlsl
new file mode 100644
index 0000000..8cb1a5f
--- /dev/null
+++ b/test/shader_io/vertex_output_builtins_struct.wgsl.expected.hlsl
@@ -0,0 +1,13 @@
+struct VertexOutputs {
+  float4 position;
+};
+struct tint_symbol {
+  float4 position : SV_Position;
+};
+
+tint_symbol main() {
+  const VertexOutputs tint_symbol_1 = {float4(1.0f, 2.0f, 3.0f, 4.0f)};
+  const tint_symbol tint_symbol_2 = {tint_symbol_1.position};
+  return tint_symbol_2;
+}
+
diff --git a/test/shader_io/vertex_output_builtins_struct.wgsl.expected.msl b/test/shader_io/vertex_output_builtins_struct.wgsl.expected.msl
new file mode 100644
index 0000000..86fbbe5
--- /dev/null
+++ b/test/shader_io/vertex_output_builtins_struct.wgsl.expected.msl
@@ -0,0 +1,16 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct VertexOutputs {
+  float4 position;
+};
+struct tint_symbol_1 {
+  float4 position [[position]];
+};
+
+vertex tint_symbol_1 tint_symbol() {
+  VertexOutputs const tint_symbol_2 = {float4(1.0f, 2.0f, 3.0f, 4.0f)};
+  tint_symbol_1 const tint_symbol_3 = {tint_symbol_2.position};
+  return tint_symbol_3;
+}
+
diff --git a/test/shader_io/vertex_output_builtins_struct.wgsl.expected.spvasm b/test/shader_io/vertex_output_builtins_struct.wgsl.expected.spvasm
new file mode 100644
index 0000000..ae99bdc
--- /dev/null
+++ b/test/shader_io/vertex_output_builtins_struct.wgsl.expected.spvasm
@@ -0,0 +1,49 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 26
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Vertex %main "main" %tint_pointsize %tint_symbol_1
+               OpName %tint_pointsize "tint_pointsize"
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %VertexOutputs "VertexOutputs"
+               OpMemberName %VertexOutputs 0 "position"
+               OpName %tint_symbol_2 "tint_symbol_2"
+               OpName %tint_symbol "tint_symbol"
+               OpName %main "main"
+               OpDecorate %tint_pointsize BuiltIn PointSize
+               OpDecorate %tint_symbol_1 BuiltIn Position
+               OpMemberDecorate %VertexOutputs 0 Offset 0
+      %float = OpTypeFloat 32
+%_ptr_Output_float = OpTypePointer Output %float
+          %4 = OpConstantNull %float
+%tint_pointsize = OpVariable %_ptr_Output_float Output %4
+    %v4float = OpTypeVector %float 4
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+          %8 = OpConstantNull %v4float
+%tint_symbol_1 = OpVariable %_ptr_Output_v4float Output %8
+       %void = OpTypeVoid
+%VertexOutputs = OpTypeStruct %v4float
+          %9 = OpTypeFunction %void %VertexOutputs
+         %16 = OpTypeFunction %void
+    %float_1 = OpConstant %float 1
+    %float_2 = OpConstant %float 2
+    %float_3 = OpConstant %float 3
+    %float_4 = OpConstant %float 4
+         %24 = OpConstantComposite %v4float %float_1 %float_2 %float_3 %float_4
+         %25 = OpConstantComposite %VertexOutputs %24
+%tint_symbol_2 = OpFunction %void None %9
+%tint_symbol = OpFunctionParameter %VertexOutputs
+         %14 = OpLabel
+         %15 = OpCompositeExtract %v4float %tint_symbol 0
+               OpStore %tint_symbol_1 %15
+               OpReturn
+               OpFunctionEnd
+       %main = OpFunction %void None %16
+         %18 = OpLabel
+               OpStore %tint_pointsize %float_1
+         %20 = OpFunctionCall %void %tint_symbol_2 %25
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/vertex_output_builtins_struct.wgsl.expected.wgsl b/test/shader_io/vertex_output_builtins_struct.wgsl.expected.wgsl
new file mode 100644
index 0000000..77d3b51
--- /dev/null
+++ b/test/shader_io/vertex_output_builtins_struct.wgsl.expected.wgsl
@@ -0,0 +1,9 @@
+struct VertexOutputs {
+  [[builtin(position)]]
+  position : vec4<f32>;
+};
+
+[[stage(vertex)]]
+fn main() -> VertexOutputs {
+  return VertexOutputs(vec4<f32>(1.0, 2.0, 3.0, 4.0));
+}
diff --git a/test/shader_io/vertex_output_locations_struct.wgsl b/test/shader_io/vertex_output_locations_struct.wgsl
new file mode 100644
index 0000000..a2303ed
--- /dev/null
+++ b/test/shader_io/vertex_output_locations_struct.wgsl
@@ -0,0 +1,12 @@
+struct VertexOutputs {
+  [[location(0)]] loc0 : i32;
+  [[location(1)]] loc1 : u32;
+  [[location(2)]] loc2 : f32;
+  [[location(3)]] loc3 : vec4<f32>;
+  [[builtin(position)]] position : vec4<f32>;
+};
+
+[[stage(vertex)]]
+fn main() -> VertexOutputs {
+  return VertexOutputs(1, 1u, 1.0, vec4<f32>(1.0, 2.0, 3.0, 4.0), vec4<f32>());
+}
diff --git a/test/shader_io/vertex_output_locations_struct.wgsl.expected.hlsl b/test/shader_io/vertex_output_locations_struct.wgsl.expected.hlsl
new file mode 100644
index 0000000..32ba657
--- /dev/null
+++ b/test/shader_io/vertex_output_locations_struct.wgsl.expected.hlsl
@@ -0,0 +1,21 @@
+struct VertexOutputs {
+  int loc0;
+  uint loc1;
+  float loc2;
+  float4 loc3;
+  float4 position;
+};
+struct tint_symbol {
+  int loc0 : TEXCOORD0;
+  uint loc1 : TEXCOORD1;
+  float loc2 : TEXCOORD2;
+  float4 loc3 : TEXCOORD3;
+  float4 position : SV_Position;
+};
+
+tint_symbol main() {
+  const VertexOutputs tint_symbol_1 = {1, 1u, 1.0f, float4(1.0f, 2.0f, 3.0f, 4.0f), float4(0.0f, 0.0f, 0.0f, 0.0f)};
+  const tint_symbol tint_symbol_2 = {tint_symbol_1.loc0, tint_symbol_1.loc1, tint_symbol_1.loc2, tint_symbol_1.loc3, tint_symbol_1.position};
+  return tint_symbol_2;
+}
+
diff --git a/test/shader_io/vertex_output_locations_struct.wgsl.expected.msl b/test/shader_io/vertex_output_locations_struct.wgsl.expected.msl
new file mode 100644
index 0000000..3bd0de3
--- /dev/null
+++ b/test/shader_io/vertex_output_locations_struct.wgsl.expected.msl
@@ -0,0 +1,24 @@
+#include <metal_stdlib>
+
+using namespace metal;
+struct VertexOutputs {
+  int loc0;
+  uint loc1;
+  float loc2;
+  float4 loc3;
+  float4 position;
+};
+struct tint_symbol_1 {
+  int loc0 [[user(locn0)]];
+  uint loc1 [[user(locn1)]];
+  float loc2 [[user(locn2)]];
+  float4 loc3 [[user(locn3)]];
+  float4 position [[position]];
+};
+
+vertex tint_symbol_1 tint_symbol() {
+  VertexOutputs const tint_symbol_2 = {1, 1u, 1.0f, float4(1.0f, 2.0f, 3.0f, 4.0f), float4()};
+  tint_symbol_1 const tint_symbol_3 = {tint_symbol_2.loc0, tint_symbol_2.loc1, tint_symbol_2.loc2, tint_symbol_2.loc3, tint_symbol_2.position};
+  return tint_symbol_3;
+}
+
diff --git a/test/shader_io/vertex_output_locations_struct.wgsl.expected.spvasm b/test/shader_io/vertex_output_locations_struct.wgsl.expected.spvasm
new file mode 100644
index 0000000..dffd402
--- /dev/null
+++ b/test/shader_io/vertex_output_locations_struct.wgsl.expected.spvasm
@@ -0,0 +1,85 @@
+; SPIR-V
+; Version: 1.3
+; Generator: Google Tint Compiler; 0
+; Bound: 42
+; Schema: 0
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Vertex %main "main" %tint_pointsize %tint_symbol_1 %tint_symbol_2 %tint_symbol_3 %tint_symbol_4 %tint_symbol_5
+               OpName %tint_pointsize "tint_pointsize"
+               OpName %tint_symbol_1 "tint_symbol_1"
+               OpName %tint_symbol_2 "tint_symbol_2"
+               OpName %tint_symbol_3 "tint_symbol_3"
+               OpName %tint_symbol_4 "tint_symbol_4"
+               OpName %tint_symbol_5 "tint_symbol_5"
+               OpName %VertexOutputs "VertexOutputs"
+               OpMemberName %VertexOutputs 0 "loc0"
+               OpMemberName %VertexOutputs 1 "loc1"
+               OpMemberName %VertexOutputs 2 "loc2"
+               OpMemberName %VertexOutputs 3 "loc3"
+               OpMemberName %VertexOutputs 4 "position"
+               OpName %tint_symbol_6 "tint_symbol_6"
+               OpName %tint_symbol "tint_symbol"
+               OpName %main "main"
+               OpDecorate %tint_pointsize BuiltIn PointSize
+               OpDecorate %tint_symbol_1 Location 0
+               OpDecorate %tint_symbol_2 Location 1
+               OpDecorate %tint_symbol_3 Location 2
+               OpDecorate %tint_symbol_4 Location 3
+               OpDecorate %tint_symbol_5 BuiltIn Position
+               OpMemberDecorate %VertexOutputs 0 Offset 0
+               OpMemberDecorate %VertexOutputs 1 Offset 4
+               OpMemberDecorate %VertexOutputs 2 Offset 8
+               OpMemberDecorate %VertexOutputs 3 Offset 16
+               OpMemberDecorate %VertexOutputs 4 Offset 32
+      %float = OpTypeFloat 32
+%_ptr_Output_float = OpTypePointer Output %float
+          %4 = OpConstantNull %float
+%tint_pointsize = OpVariable %_ptr_Output_float Output %4
+        %int = OpTypeInt 32 1
+%_ptr_Output_int = OpTypePointer Output %int
+          %8 = OpConstantNull %int
+%tint_symbol_1 = OpVariable %_ptr_Output_int Output %8
+       %uint = OpTypeInt 32 0
+%_ptr_Output_uint = OpTypePointer Output %uint
+         %12 = OpConstantNull %uint
+%tint_symbol_2 = OpVariable %_ptr_Output_uint Output %12
+%tint_symbol_3 = OpVariable %_ptr_Output_float Output %4
+    %v4float = OpTypeVector %float 4
+%_ptr_Output_v4float = OpTypePointer Output %v4float
+         %17 = OpConstantNull %v4float
+%tint_symbol_4 = OpVariable %_ptr_Output_v4float Output %17
+%tint_symbol_5 = OpVariable %_ptr_Output_v4float Output %17
+       %void = OpTypeVoid
+%VertexOutputs = OpTypeStruct %int %uint %float %v4float %v4float
+         %19 = OpTypeFunction %void %VertexOutputs
+         %30 = OpTypeFunction %void
+    %float_1 = OpConstant %float 1
+      %int_1 = OpConstant %int 1
+     %uint_1 = OpConstant %uint 1
+    %float_2 = OpConstant %float 2
+    %float_3 = OpConstant %float 3
+    %float_4 = OpConstant %float 4
+         %40 = OpConstantComposite %v4float %float_1 %float_2 %float_3 %float_4
+         %41 = OpConstantComposite %VertexOutputs %int_1 %uint_1 %float_1 %40 %17
+%tint_symbol_6 = OpFunction %void None %19
+%tint_symbol = OpFunctionParameter %VertexOutputs
+         %24 = OpLabel
+         %25 = OpCompositeExtract %int %tint_symbol 0
+               OpStore %tint_symbol_1 %25
+         %26 = OpCompositeExtract %uint %tint_symbol 1
+               OpStore %tint_symbol_2 %26
+         %27 = OpCompositeExtract %float %tint_symbol 2
+               OpStore %tint_symbol_3 %27
+         %28 = OpCompositeExtract %v4float %tint_symbol 3
+               OpStore %tint_symbol_4 %28
+         %29 = OpCompositeExtract %v4float %tint_symbol 4
+               OpStore %tint_symbol_5 %29
+               OpReturn
+               OpFunctionEnd
+       %main = OpFunction %void None %30
+         %32 = OpLabel
+               OpStore %tint_pointsize %float_1
+         %34 = OpFunctionCall %void %tint_symbol_6 %41
+               OpReturn
+               OpFunctionEnd
diff --git a/test/shader_io/vertex_output_locations_struct.wgsl.expected.wgsl b/test/shader_io/vertex_output_locations_struct.wgsl.expected.wgsl
new file mode 100644
index 0000000..8716f56
--- /dev/null
+++ b/test/shader_io/vertex_output_locations_struct.wgsl.expected.wgsl
@@ -0,0 +1,17 @@
+struct VertexOutputs {
+  [[location(0)]]
+  loc0 : i32;
+  [[location(1)]]
+  loc1 : u32;
+  [[location(2)]]
+  loc2 : f32;
+  [[location(3)]]
+  loc3 : vec4<f32>;
+  [[builtin(position)]]
+  position : vec4<f32>;
+};
+
+[[stage(vertex)]]
+fn main() -> VertexOutputs {
+  return VertexOutputs(1, 1u, 1.0, vec4<f32>(1.0, 2.0, 3.0, 4.0), vec4<f32>());
+}