//===-- SPIRVControlFlowOps.td - SPIR-V Control Flow Ops ---*- tablegen -*-===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// // // This file contains control flow ops for the SPIR-V dialect. It corresponds // to "3.32.17. Control-Flow Instructions" of the SPIR-V specification. // //===----------------------------------------------------------------------===// #ifndef MLIR_DIALECT_SPIRV_IR_CONTROLFLOW_OPS #define MLIR_DIALECT_SPIRV_IR_CONTROLFLOW_OPS include "mlir/Dialect/SPIRV/IR/SPIRVBase.td" include "mlir/IR/SymbolInterfaces.td" include "mlir/Interfaces/CallInterfaces.td" include "mlir/Interfaces/ControlFlowInterfaces.td" include "mlir/Interfaces/SideEffectInterfaces.td" // ----- def SPIRV_BranchOp : SPIRV_Op<"Branch", [ DeclareOpInterfaceMethods, InFunctionScope, Pure, Terminator]> { let summary = "Unconditional branch to target block."; let description = [{ This instruction must be the last instruction in a block. #### Example: ```mlir spirv.Branch ^target spirv.Branch ^target(%0, %1: i32, f32) ``` }]; let arguments = (ins Variadic:$targetOperands); let results = (outs); let successors = (successor AnySuccessor:$target); let hasVerifier = 0; let builders = [ OpBuilder<(ins "Block *":$successor, CArg<"ValueRange", "{}">:$arguments), [{ $_state.addSuccessors(successor); $_state.addOperands(arguments); }]> ]; let skipDefaultBuilders = 1; let extraClassDeclaration = [{ /// Returns the block arguments. operand_range getBlockArguments() { return getTargetOperands(); } }]; let autogenSerialization = 0; let assemblyFormat = [{ $target (`(` $targetOperands^ `:` type($targetOperands) `)`)? attr-dict }]; } // ----- def SPIRV_BranchConditionalOp : SPIRV_Op<"BranchConditional", [ AttrSizedOperandSegments, DeclareOpInterfaceMethods, InFunctionScope, Pure, Terminator]> { let summary = [{ If Condition is true, branch to true block, otherwise branch to false block. }]; let description = [{ Condition must be a Boolean type scalar. Branch weights are unsigned 32-bit integer literals. There must be either no Branch Weights or exactly two branch weights. If present, the first is the weight for branching to True Label, and the second is the weight for branching to False Label. The implied probability that a branch is taken is its weight divided by the sum of the two Branch weights. At least one weight must be non-zero. A weight of zero does not imply a branch is dead or permit its removal; branch weights are only hints. The two weights must not overflow a 32-bit unsigned integer when added together. This instruction must be the last instruction in a block. ``` branch-conditional-op ::= `spirv.BranchConditional` ssa-use (`[` integer-literal, integer-literal `]`)? `,` successor `,` successor successor ::= bb-id branch-use-list? branch-use-list ::= `(` ssa-use-list `:` type-list-no-parens `)` ``` #### Example: ```mlir spirv.BranchConditional %condition, ^true_branch, ^false_branch spirv.BranchConditional %condition, ^true_branch(%0: i32), ^false_branch(%1: i32) ``` }]; let arguments = (ins SPIRV_Bool:$condition, Variadic:$trueTargetOperands, Variadic:$falseTargetOperands, OptionalAttr:$branch_weights ); let results = (outs); let successors = (successor AnySuccessor:$trueTarget, AnySuccessor:$falseTarget); let builders = [ OpBuilder<(ins "Value":$condition, "Block *":$trueBlock, "ValueRange":$trueArguments, "Block *":$falseBlock, "ValueRange":$falseArguments, CArg<"std::optional>", "{}">:$weights), [{ ArrayAttr weightsAttr; if (weights) { weightsAttr = $_builder.getI32ArrayAttr({static_cast(weights->first), static_cast(weights->second)}); } build($_builder, $_state, condition, trueArguments, falseArguments, weightsAttr, trueBlock, falseBlock); }]> ]; let autogenSerialization = 0; let extraClassDeclaration = [{ /// Branch indices into the successor list. enum { kTrueIndex = 0, kFalseIndex = 1 }; /// Returns the target block for the true branch. Block *getTrueBlock() { return getOperation()->getSuccessor(kTrueIndex); } /// Returns the target block for the false branch. Block *getFalseBlock() { return getOperation()->getSuccessor(kFalseIndex); } /// Returns the number of arguments to the true target block. unsigned getNumTrueBlockArguments() { return getTrueTargetOperands().size(); } /// Returns the number of arguments to the false target block. unsigned getNumFalseBlockArguments() { return getFalseTargetOperands().size(); } // Iterator and range support for true target block arguments. operand_range getTrueBlockArguments() { return getTrueTargetOperands(); } // Iterator and range support for false target block arguments. operand_range getFalseBlockArguments() { return getFalseTargetOperands(); } private: /// Gets the index of the first true block argument in the operand list. unsigned getTrueBlockArgumentIndex() { return 1; // Omit the first argument, which is the condition. } /// Gets the index of the first false block argument in the operand list. unsigned getFalseBlockArgumentIndex() { return getTrueBlockArgumentIndex() + getNumTrueBlockArguments(); } }]; } // ----- def SPIRV_FunctionCallOp : SPIRV_Op<"FunctionCall", [ InFunctionScope, DeclareOpInterfaceMethods, DeclareOpInterfaceMethods]> { let summary = "Call a function."; let description = [{ Result Type is the type of the return value of the function. It must be the same as the Return Type operand of the Function Type operand of the Function operand. Function is an OpFunction instruction. This could be a forward reference. Argument N is the object to copy to parameter N of Function. Note: A forward call is possible because there is no missing type information: Result Type must match the Return Type of the function, and the calling argument types must match the formal parameter types. #### Example: ```mlir spirv.FunctionCall @f_void(%arg0) : (i32) -> () %0 = spirv.FunctionCall @f_iadd(%arg0, %arg1) : (i32, i32) -> i32 ``` }]; let arguments = (ins FlatSymbolRefAttr:$callee, Variadic:$arguments, OptionalAttr:$arg_attrs, OptionalAttr:$res_attrs ); let results = (outs Optional:$return_value ); let builders = [ OpBuilder<(ins "Type":$returnType, "FlatSymbolRefAttr":$callee, "ValueRange":$arguments), [{ build($_builder, $_state, returnType, callee, arguments, /*arg_attrs=*/nullptr, /*res_attrs=*/nullptr); }]> ]; let autogenSerialization = 0; let assemblyFormat = [{ $callee `(` $arguments `)` attr-dict `:` functional-type($arguments, results) }]; } // ----- def SPIRV_SwitchOp : SPIRV_Op<"Switch", [AttrSizedOperandSegments, InFunctionScope, DeclareOpInterfaceMethods, Pure, Terminator]> { let summary = [{ Multi-way branch to one of the operand label ``. }]; let description = [{ Selector must have a type of OpTypeInt. Selector is compared for equality to the Target literals. Default must be the `` of a label. If Selector does not equal any of the Target literals, control flow branches to the Default label ``. Target must be alternating scalar integer literals and the `` of a label. If Selector equals a literal, control flow branches to the following label ``. It is invalid for any two literal to be equal to each other. If Selector does not equal any literal, control flow branches to the Default label ``. Each literal is interpreted with the type of Selector: The bit width of Selector’s type is the width of each literal’s type. If this width is not a multiple of 32-bits and the OpTypeInt Signedness is set to 1, the literal values are interpreted as being sign extended. If Selector is an OpUndef, behavior is undefined. This instruction must be the last instruction in a block. #### Example: ```mlir spirv.Switch %selector : si32, [ default: ^bb1(%a : i32), 0: ^bb1(%b : i32), 1: ^bb3(%c : i32) ] ``` }]; let arguments = (ins SPIRV_Integer:$selector, Variadic:$defaultOperands, VariadicOfVariadic:$targetOperands, OptionalAttr:$literals, DenseI32ArrayAttr:$case_operand_segments ); let results = (outs); let successors = (successor AnySuccessor:$defaultTarget, VariadicSuccessor:$targets); let builders = [ OpBuilder<(ins "Value":$selector, "Block *":$defaultTarget, "ValueRange":$defaultOperands, CArg<"ArrayRef", "{}">:$literals, CArg<"BlockRange", "{}">:$targets, CArg<"ArrayRef", "{}">:$targetOperands)>, OpBuilder<(ins "Value":$selector, "Block *":$defaultTarget, "ValueRange":$defaultOperands, CArg<"ArrayRef", "{}">:$literals, CArg<"BlockRange", "{}">:$targets, CArg<"ArrayRef", "{}">:$targetOperands)>, OpBuilder<(ins "Value":$selector, "Block *":$defaultTarget, "ValueRange":$defaultOperands, CArg<"DenseIntElementsAttr", "{}">:$literals, CArg<"BlockRange", "{}">:$targets, CArg<"ArrayRef", "{}">:$targetOperands)> ]; let assemblyFormat = [{ $selector `:` type($selector) `,` `[` `\n` custom(ref(type($selector)),$defaultTarget, $defaultOperands, type($defaultOperands), $literals, $targets, $targetOperands, type($targetOperands)) `]` attr-dict }]; let extraClassDeclaration = [{ /// Return the operands for the target block at the given index. OperandRange getTargetOperands(unsigned index) { return getTargetOperands()[index]; } /// Return a mutable range of operands for the target block at the /// given index. MutableOperandRange getTargetOperandsMutable(unsigned index) { return getTargetOperandsMutable()[index]; } }]; let autogenSerialization = 0; let hasVerifier = 1; } // ----- def SPIRV_KillOp : SPIRV_Op<"Kill", [Terminator]> { let summary = [{ Deprecated (use OpTerminateInvocation or OpDemoteToHelperInvocation). }]; let description = [{ Fragment-shader discard. Ceases all further processing in any invocation that executes it: Only instructions these invocations executed before OpKill have observable side effects. If this instruction is executed in non-uniform control flow, all subsequent control flow is non-uniform (for invocations that continue to execute). This instruction must be the last instruction in a block. This instruction is only valid in the Fragment Execution Model. #### Example: ```mlir spirv.Kill ``` }]; let availability = [ MinVersion, MaxVersion, Extension<[]>, Capability<[SPIRV_C_Shader]> ]; let arguments = (ins); let results = (outs); let assemblyFormat = "attr-dict"; let hasVerifier = 0; } // ----- def SPIRV_LoopOp : SPIRV_Op<"mlir.loop", [InFunctionScope]> { let summary = "Define a structured loop."; let description = [{ SPIR-V can explicitly declare structured control-flow constructs using merge instructions. These explicitly declare a header block before the control flow diverges and a merge block where control flow subsequently converges. These blocks delimit constructs that must nest, and can only be entered and exited in structured ways. See "2.11. Structured Control Flow" of the SPIR-V spec for more details. Instead of having a `spirv.LoopMerge` op to directly model loop merge instruction for indicating the merge and continue target, we use regions to delimit the boundary of the loop: the merge target is the next op following the `spirv.mlir.loop` op and the continue target is the block that has a back-edge pointing to the entry block inside the `spirv.mlir.loop`'s region. This way it's easier to discover all blocks belonging to a construct and it plays nicer with the MLIR system. The `spirv.mlir.loop` region should contain at least four blocks: one entry block, one loop header block, one loop continue block, one loop merge block. The entry block should be the first block and it should jump to the loop header block, which is the second block. The loop merge block should be the last block. The merge block should only contain a `spirv.mlir.merge` op. The continue block should be the second to last block and it should have a branch to the loop header block. The loop continue block should be the only block, except the entry block, branching to the header block. Values defined inside the loop regions cannot be directly used outside of them; however, the loop region can yield values. These values are yielded using a `spirv.mlir.merge` op and returned as a result of the loop op. }]; let arguments = (ins SPIRV_LoopControlAttr:$loop_control ); let results = (outs Variadic:$results); let regions = (region AnyRegion:$body); let builders = [ OpBuilder<(ins)>, OpBuilder<(ins "spirv::LoopControl":$loopControl), [{ build($_builder, $_state, TypeRange(), loopControl); }]> ]; let extraClassDeclaration = [{ // Returns the entry block. Block *getEntryBlock(); // Returns the loop header block. Block *getHeaderBlock(); // Returns the loop continue block. Block *getContinueBlock(); // Returns the loop merge block. Block *getMergeBlock(); // Adds an empty entry block and loop merge block containing one // spirv.mlir.merge op. void addEntryAndMergeBlock(OpBuilder &builder); }]; let hasOpcode = 0; let autogenSerialization = 0; let hasVerifier = 0; let hasRegionVerifier = 1; } // ----- def SPIRV_MergeOp : SPIRV_Op<"mlir.merge", [ Pure, Terminator, ParentOneOf<["SelectionOp", "LoopOp"]>, ReturnLike]> { let summary = "A special terminator for merging a structured selection/loop."; let description = [{ We use `spirv.mlir.selection`/`spirv.mlir.loop` for modelling structured selection/loop. This op is a terminator used inside their regions to mean jumping to the merge point, which is the next op following the `spirv.mlir.selection` or `spirv.mlir.loop` op. This op does not have a corresponding instruction in the SPIR-V binary format; it's solely for structural purpose. The instruction is also used to yield values from inside the selection/loop region to the outside, as values that were sunk into the region cannot otherwise escape it. }]; let arguments = (ins Variadic:$operands); let results = (outs); let assemblyFormat = "attr-dict ($operands^ `:` type($operands))?"; let builders = [ OpBuilder<(ins), [{ build($_builder, $_state, ValueRange()); }]> ]; let hasOpcode = 0; let autogenSerialization = 0; let hasVerifier = 0; } // ----- def SPIRV_ReturnOp : SPIRV_Op<"Return", [InFunctionScope, Pure, ReturnLike, Terminator]> { let summary = "Return with no value from a function with void return type."; let description = [{ This instruction must be the last instruction in a block. #### Example: ```mlir spirv.Return ``` }]; let arguments = (ins); let results = (outs); let assemblyFormat = "attr-dict"; } // ----- def SPIRV_UnreachableOp : SPIRV_Op<"Unreachable", [InFunctionScope, Terminator]> { let summary = "Behavior is undefined if this instruction is executed."; let description = [{ This instruction must be the last instruction in a block. }]; let arguments = (ins); let results = (outs); let assemblyFormat = "attr-dict"; } // ----- def SPIRV_ReturnValueOp : SPIRV_Op<"ReturnValue", [InFunctionScope, Pure, ReturnLike, Terminator]> { let summary = "Return a value from a function."; let description = [{ Value is the value returned, by copy, and must match the Return Type operand of the OpTypeFunction type of the OpFunction body this return instruction is in. This instruction must be the last instruction in a block. #### Example: ```mlir spirv.ReturnValue %0 : f32 ``` }]; let arguments = (ins SPIRV_Type:$value ); let results = (outs); let assemblyFormat = "$value attr-dict `:` type($value)"; } def SPIRV_SelectionOp : SPIRV_Op<"mlir.selection", [InFunctionScope]> { let summary = "Define a structured selection."; let description = [{ SPIR-V can explicitly declare structured control-flow constructs using merge instructions. These explicitly declare a header block before the control flow diverges and a merge block where control flow subsequently converges. These blocks delimit constructs that must nest, and can only be entered and exited in structured ways. See "2.11. Structured Control Flow" of the SPIR-V spec for more details. Instead of having a `spirv.SelectionMerge` op to directly model selection merge instruction for indicating the merge target, we use regions to delimit the boundary of the selection: the merge target is the next op following the `spirv.mlir.selection` op. This way it's easier to discover all blocks belonging to the selection and it plays nicer with the MLIR system. The `spirv.mlir.selection` region should contain at least two blocks: one selection header block, and one selection merge. The selection header block should be the first block. The selection merge block should be the last block. The merge block should only contain a `spirv.mlir.merge` op. Values defined inside the selection regions cannot be directly used outside of them; however, the selection region can yield values. These values are yielded using a `spirv.mlir.merge` op and returned as a result of the selection op. }]; let arguments = (ins SPIRV_SelectionControlAttr:$selection_control ); let results = (outs Variadic:$results); let regions = (region AnyRegion:$body); let extraClassDeclaration = [{ /// Returns the selection header block. Block *getHeaderBlock(); /// Returns the selection merge block. Block *getMergeBlock(); /// Adds a selection merge block containing one spirv.mlir.merge op. void addMergeBlock(OpBuilder &builder); /// Creates a spirv.mlir.selection op for `if () then { }` /// with `builder`. `builder`'s insertion point will remain at after the /// newly inserted spirv.mlir.selection op afterwards. static SelectionOp createIfThen( Location loc, Value condition, function_ref thenBody, OpBuilder &builder); }]; let builders = [ OpBuilder<(ins "spirv::SelectionControl":$selectionControl), [{ build($_builder, $_state, TypeRange(), selectionControl); }]> ]; let hasOpcode = 0; let autogenSerialization = 0; let hasCanonicalizer = 1; let hasVerifier = 0; let hasRegionVerifier = 1; } #endif // MLIR_DIALECT_SPIRV_IR_CONTROLFLOW_OPS