[spirv-reader][ir] Support `OpKill`

Translate an `OpKill` SPIR-V instruction into a `discard; ret` IR
instructions. The semantics of `OpKill` don't currently exist in WGSL,
the `discard` instruction is more like `OpDemoteToHelperInvocation`. The
`discard; ret` is similar, although not completely the same, but it's as
close as we can currently get.

Bug: 391487196
Change-Id: I8abd5526b866e74f90a3c5cba06cf2001837ab28
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/225094
Commit-Queue: dan sinclair <dsinclair@chromium.org>
Reviewed-by: James Price <jrprice@google.com>
diff --git a/src/tint/lang/spirv/reader/parser/misc_test.cc b/src/tint/lang/spirv/reader/parser/misc_test.cc
index d814c5e..548223b 100644
--- a/src/tint/lang/spirv/reader/parser/misc_test.cc
+++ b/src/tint/lang/spirv/reader/parser/misc_test.cc
@@ -378,5 +378,137 @@
 )");
 }
 
+TEST_F(SpirvParserTest, OpKill_TopLevel) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main"
+               OpExecutionMode %main OriginUpperLeft
+       %void = OpTypeVoid
+    %ep_type = OpTypeFunction %void
+       %main = OpFunction %void None %ep_type
+         %10 = OpLabel
+               OpKill
+               OpFunctionEnd)",
+              R"(
+%main = @fragment func():void {
+  $B1: {
+    discard
+    ret
+  }
+}
+)");
+}
+
+// TODO(dsinclair): Requires OpSelectionMerge and OpBranchConditional
+TEST_F(SpirvParserTest, DISABLED_OpKill_InsideIf) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main"
+               OpExecutionMode %main OriginUpperLeft
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+    %ep_type = OpTypeFunction %void
+       %true = OpConstantTrue %bool
+       %main = OpFunction %void None %ep_type
+         %10 = OpLabel
+               OpSelectionMerge %99 None
+               OpBranchConditional %true %20 %99
+         %20 = OpLabel
+               OpKill
+         %99 = OpLabel
+               OpKill
+               OpFunctionEnd
+  )",
+              R"(
+%main = @fragment func():void {
+  $B1: {
+    %2:void = if true [t: $B2] {
+      discard
+      ret
+    }
+    discard
+    ret
+  }
+}
+)");
+}
+
+// TODO(dsinclair): Requires OpBranch
+TEST_F(SpirvParserTest, DISABLED_OpKill_InsideLoop) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main"
+               OpExecutionMode %main OriginUpperLeft
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+    %ep_type = OpTypeFunction %void
+       %true = OpConstantTrue %bool
+       %main = OpFunction %void None %ep_type
+         %10 = OpLabel
+               OpBranch %20
+         %20 = OpLabel
+               OpLoopMerge %99 %80 None
+               OpBranchConditional %true %30 %30
+         %30 = OpLabel
+               OpKill
+         %80 = OpLabel
+               OpBranch %20
+         %99 = OpLabel
+               OpKill
+               OpFunctionEnd
+  )",
+              R"(
+%main = @fragment func():void {
+  $B1: {
+    %2:void = loop %true []
+      discard
+      ret
+    }
+    discard
+    ret
+  }
+}
+)");
+}
+
+TEST_F(SpirvParserTest, OpKill_InNonVoidFunction) {
+    EXPECT_IR(R"(
+               OpCapability Shader
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %main "main"
+               OpExecutionMode %main OriginUpperLeft
+       %void = OpTypeVoid
+       %bool = OpTypeBool
+    %ep_type = OpTypeFunction %void
+     %boolfn = OpTypeFunction %bool
+        %200 = OpFunction %bool None %boolfn
+        %210 = OpLabel
+               OpKill
+               OpFunctionEnd
+       %main = OpFunction %void None %ep_type
+         %10 = OpLabel
+         %11 = OpFunctionCall %bool %200
+               OpReturn
+               OpFunctionEnd
+  )",
+              R"(
+%1 = func():bool {
+  $B1: {
+    discard
+    ret false
+  }
+}
+%main = @fragment func():void {
+  $B2: {
+    %3:bool = call %1
+    ret
+  }
+}
+)");
+}
+
 }  // namespace
 }  // namespace tint::spirv::reader
diff --git a/src/tint/lang/spirv/reader/parser/parser.cc b/src/tint/lang/spirv/reader/parser/parser.cc
index 86edda6..b73f9e6 100644
--- a/src/tint/lang/spirv/reader/parser/parser.cc
+++ b/src/tint/lang/spirv/reader/parser/parser.cc
@@ -113,8 +113,7 @@
         }
 
         EmitFunctions();
-
-        EmitEntryPoints();
+        EmitEntryPointAttributes();
 
         // TODO(crbug.com/tint/1907): Handle annotation instructions.
         // TODO(crbug.com/tint/1907): Handle names.
@@ -490,7 +489,7 @@
     }
 
     /// Emit entry point attributes.
-    void EmitEntryPoints() {
+    void EmitEntryPointAttributes() {
         // Handle OpEntryPoint declarations.
         for (auto& entry_point : spirv_context_->module()->entry_points()) {
             auto model = entry_point.GetSingleWordInOperand(0);
@@ -617,6 +616,9 @@
                 case spv::Op::OpUnreachable:
                     EmitWithoutResult(b_.Unreachable());
                     break;
+                case spv::Op::OpKill:
+                    EmitKill(inst);
+                    break;
                 default:
                     TINT_UNIMPLEMENTED()
                         << "unhandled SPIR-V instruction: " << static_cast<uint32_t>(inst.opcode());
@@ -624,6 +626,27 @@
         }
     }
 
+    /// @param inst the SPIR-V instruction
+    /// Note: This isn't technically correct, but there is no `kill` equivalent in WGSL. The closets
+    /// we have is `discard` which maps to `OpDemoteToHelperInvocation` in SPIR-V.
+    void EmitKill([[maybe_unused]] const spvtools::opt::Instruction& inst) {
+        EmitWithoutResult(b_.Discard());
+
+        // An `OpKill` is a terminator in SPIR-V. `discard` is not a terminator in WGSL. After the
+        // `discard` we inject a `return` for the current function. This is similar in spirit to
+        // what `OpKill` does although not totally correct (i.e. we don't early return from calling
+        // functions, just the function where `OpKill` was emitted. There are also limited places in
+        // which `OpKill` can be used. So, we don't have to worry about it in a `continuing` block
+        // because the continuing must end with a branching terminator which `OpKill` does not
+        // branch.
+        if (current_function_->ReturnType()->Is<core::type::Void>()) {
+            EmitWithoutResult(b_.Return(current_function_));
+        } else {
+            EmitWithoutResult(
+                b_.Return(current_function_, b_.Zero(current_function_->ReturnType())));
+        }
+    }
+
     /// @param inst the SPIR-V instruction for OpCopyObject
     void EmitCopyObject(const spvtools::opt::Instruction& inst) {
         // Make the result Id a pointer to the original copied value.