llvm/mlir/include/mlir/Dialect/SPIRV/IR/SPIRVMemoryOps.td

//===-- SPIRVMemoryOps.td - MLIR SPIR-V Memory 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 memory ops for the SPIR-V dialect. It corresponds
// to "3.32.8. Memory Instructions" of the SPIR-V spec.
//
//===----------------------------------------------------------------------===//

#ifndef MLIR_DIALECT_SPIRV_IR_MEMORY_OPS
#define MLIR_DIALECT_SPIRV_IR_MEMORY_OPS

include "mlir/Dialect/SPIRV/IR/SPIRVBase.td"

// -----

def SPIRV_AccessChainOp : SPIRV_Op<"AccessChain", [Pure]> {
  let summary = "Create a pointer into a composite object.";

  let description = [{
    Result Type must be an OpTypePointer. Its Type operand must be the type
    reached by walking the Base’s type hierarchy down to the last provided
    index in Indexes, and its Storage Class operand must be the same as the
    Storage Class of Base.

    Base must be a pointer, pointing to the base of a composite object.

    Indexes walk the type hierarchy to the desired depth, potentially down
    to scalar granularity. The first index in Indexes will select the top-
    level member/element/component/element of the base composite. All
    composite constituents use zero-based numbering, as described by their
    OpType… instruction. The second index will apply similarly to that
    result, and so on. Once any non-composite type is reached, there must be
    no remaining (unused) indexes.

     Each index in Indexes

    - must be a scalar integer type,

    - is treated as a signed count, and

    - must be an OpConstant when indexing into a structure.

    <!-- End of AutoGen section -->
    ```
    access-chain-op ::= ssa-id `=` `spirv.AccessChain` ssa-use
                        `[` ssa-use (',' ssa-use)* `]`
                        `:` pointer-type
    ```

    #### Example:

    ```mlir
    %0 = "spirv.Constant"() { value = 1: i32} : () -> i32
    %1 = spirv.Variable : !spirv.ptr<!spirv.struct<f32, !spirv.array<4xf32>>, Function>
    %2 = spirv.AccessChain %1[%0] : !spirv.ptr<!spirv.struct<f32, !spirv.array<4xf32>>, Function>
    %3 = spirv.Load "Function" %2 ["Volatile"] : !spirv.array<4xf32>
    ```
  }];

  let arguments = (ins
    SPIRV_AnyPtr:$base_ptr,
    Variadic<SPIRV_Integer>:$indices
  );

  let results = (outs
    SPIRV_AnyPtr:$component_ptr
  );

  let builders = [OpBuilder<(ins "Value":$basePtr, "ValueRange":$indices)>];

  let hasCanonicalizer = 1;
}

// -----

def SPIRV_CopyMemoryOp : SPIRV_Op<"CopyMemory", []> {
  let summary = [{
    Copy from the memory pointed to by Source to the memory pointed to by
    Target. Both operands must be non-void pointers and having the same <id>
    Type operand in their OpTypePointer type declaration.  Matching Storage
    Class is not required.  The amount of memory copied is the size of the
    type pointed to. The copied type must have a fixed size; i.e., it must
    not be, nor include, any OpTypeRuntimeArray types.
  }];

  let description = [{
    If present, any Memory Operands must begin with a memory operand
    literal. If not present, it is the same as specifying the memory operand
    None. Before version 1.4, at most one memory operands mask can be
    provided. Starting with version 1.4 two masks can be provided, as
    described in Memory Operands. If no masks or only one mask is present,
    it applies to both Source and Target. If two masks are present, the
    first applies to Target and cannot include MakePointerVisible, and the
    second applies to Source and cannot include MakePointerAvailable.

    <!-- End of AutoGen section -->

    ```
    copy-memory-op ::= `spirv.CopyMemory ` storage-class ssa-use
                       storage-class ssa-use
                       (`[` memory-access `]` (`, [` memory-access `]`)?)?
                       ` : ` spirv-element-type
    ```

    #### Example:

    ```mlir
    %0 = spirv.Variable : !spirv.ptr<f32, Function>
    %1 = spirv.Variable : !spirv.ptr<f32, Function>
    spirv.CopyMemory "Function" %0, "Function" %1 : f32
    ```
  }];

  let arguments = (ins
    SPIRV_AnyPtr:$target,
    SPIRV_AnyPtr:$source,
    OptionalAttr<SPIRV_MemoryAccessAttr>:$memory_access,
    OptionalAttr<I32Attr>:$alignment,
    OptionalAttr<SPIRV_MemoryAccessAttr>:$source_memory_access,
    OptionalAttr<I32Attr>:$source_alignment
  );

  let results = (outs);

  let autogenSerialization = 0;
}

// -----

def SPIRV_InBoundsPtrAccessChainOp : SPIRV_Op<"InBoundsPtrAccessChain", [Pure]> {
  let summary = [{
    Has the same semantics as OpPtrAccessChain, with the addition that the
    resulting pointer is known to point within the base object.
  }];

  let description = [{


    <!-- End of AutoGen section -->

    ```
    access-chain-op ::= ssa-id `=` `spirv.InBoundsPtrAccessChain` ssa-use
                        `[` ssa-use (',' ssa-use)* `]`
                        `:` pointer-type
    ```

    #### Example:

    ```mlir
    func @inbounds_ptr_access_chain(%arg0: !spirv.ptr<f32, CrossWorkgroup>, %arg1 : i64) -> () {
      %0 = spirv.InBoundsPtrAccessChain %arg0[%arg1] : !spirv.ptr<f32, CrossWorkgroup>, i64
      ...
    }
    ```
  }];

  let availability = [
    MinVersion<SPIRV_V_1_0>,
    MaxVersion<SPIRV_V_1_6>,
    Extension<[]>,
    Capability<[SPIRV_C_Addresses]>
  ];

  let arguments = (ins
    SPIRV_AnyPtr:$base_ptr,
    SPIRV_Integer:$element,
    Variadic<SPIRV_Integer>:$indices
  );

  let results = (outs
    SPIRV_AnyPtr:$result
  );

  let builders = [OpBuilder<(ins "Value":$basePtr, "Value":$element, "ValueRange":$indices)>];
}

// -----

def SPIRV_LoadOp : SPIRV_Op<"Load", []> {
  let summary = "Load through a pointer.";

  let description = [{
    Result Type is the type of the loaded object. It must be a type with
    fixed size; i.e., it cannot be, nor include, any OpTypeRuntimeArray
    types.

    Pointer is the pointer to load through.  Its type must be an
    OpTypePointer whose Type operand is the same as Result Type.

    If present, any Memory Operands must begin with a memory operand
    literal. If not present, it is the same as specifying the memory operand
    None.

    <!-- End of AutoGen section -->

    ```
    memory-access ::= `"None"` | `"Volatile"` | `"Aligned", ` integer-literal
                    | `"NonTemporal"`

    load-op ::= ssa-id ` = spirv.Load ` storage-class ssa-use
                (`[` memory-access `]`)? ` : ` spirv-element-type
    ```

    #### Example:

    ```mlir
    %0 = spirv.Variable : !spirv.ptr<f32, Function>
    %1 = spirv.Load "Function" %0 : f32
    %2 = spirv.Load "Function" %0 ["Volatile"] : f32
    %3 = spirv.Load "Function" %0 ["Aligned", 4] : f32
    ```
  }];

  let arguments = (ins
    SPIRV_AnyPtr:$ptr,
    OptionalAttr<SPIRV_MemoryAccessAttr>:$memory_access,
    OptionalAttr<I32Attr>:$alignment
  );

  let results = (outs
    SPIRV_Type:$value
  );

  let builders = [
    OpBuilder<(ins "Value":$basePtr,
      CArg<"MemoryAccessAttr", "{}">:$memory_access,
      CArg<"IntegerAttr", "{}">:$alignment)>
  ];
}

// -----

def SPIRV_PtrAccessChainOp : SPIRV_Op<"PtrAccessChain", [Pure]> {
  let summary = [{
    Has the same semantics as OpAccessChain, with the addition of the
    Element operand.
  }];

  let description = [{
    Element is used to do an initial dereference of Base: Base is treated as
    the address of an element in an array, and a new element address is
    computed from Base and Element to become the OpAccessChain Base to
    dereference as per OpAccessChain. This computed Base has the same type
    as the originating Base.

    To compute the new element address, Element is treated as a signed count
    of elements E, relative to the original Base element B, and the address
    of element B + E is computed using enough precision to avoid overflow
    and underflow. For objects in the Uniform, StorageBuffer, or
    PushConstant storage classes, the element's address or location is
    calculated using a stride, which will be the Base-type's Array Stride if
    the Base type is decorated with ArrayStride. For all other objects, the
    implementation calculates the element's address or location.

    With one exception, undefined behavior results when B + E is not an
    element in the same array (same innermost array, if array types are
    nested) as B. The exception being when B + E = L, where L is the length
    of the array: the address computation for element L is done with the
    same stride as any other B + E computation that stays within the array.

    Note: If Base is typed to be a pointer to an array and the desired
    operation is to select an element of that array, OpAccessChain should be
    directly used, as its first Index selects the array element.

    <!-- End of AutoGen section -->

    ```
    [access-chain-op ::= ssa-id `=` `spirv.PtrAccessChain` ssa-use
                        `[` ssa-use (',' ssa-use)* `]`
                        `:` pointer-type
    ```

    #### Example:

    ```mlir
    func @ptr_access_chain(%arg0: !spirv.ptr<f32, CrossWorkgroup>, %arg1 : i64) -> () {
      %0 = spirv.PtrAccessChain %arg0[%arg1] : !spirv.ptr<f32, CrossWorkgroup>, i64
      ...
    }
    ```
  }];

  let availability = [
    MinVersion<SPIRV_V_1_0>,
    MaxVersion<SPIRV_V_1_6>,
    Extension<[]>,
    Capability<[
      SPIRV_C_Addresses, SPIRV_C_PhysicalStorageBufferAddresses,
      SPIRV_C_VariablePointers, SPIRV_C_VariablePointersStorageBuffer]>
  ];

  let arguments = (ins
    SPIRV_AnyPtr:$base_ptr,
    SPIRV_Integer:$element,
    Variadic<SPIRV_Integer>:$indices
  );

  let results = (outs
    SPIRV_AnyPtr:$result
  );

  let builders = [OpBuilder<(ins "Value":$basePtr, "Value":$element, "ValueRange":$indices)>];
}

// -----

def SPIRV_StoreOp : SPIRV_Op<"Store", []> {
  let summary = "Store through a pointer.";

  let description = [{
    Pointer is the pointer to store through.  Its type must be an
    OpTypePointer whose Type operand is the same as the type of Object.

    Object is the object to store.

    If present, any Memory Operands must begin with a memory operand
    literal. If not present, it is the same as specifying the memory operand
    None.

    <!-- End of AutoGen section -->

    ```
    store-op ::= `spirv.Store ` storage-class ssa-use `, ` ssa-use `, `
                  (`[` memory-access `]`)? `:` spirv-element-type
    ```

    #### Example:

    ```mlir
    %0 = spirv.Variable : !spirv.ptr<f32, Function>
    %1 = spirv.FMul ... : f32
    spirv.Store "Function" %0, %1 : f32
    spirv.Store "Function" %0, %1 ["Volatile"] : f32
    spirv.Store "Function" %0, %1 ["Aligned", 4] : f32
    ```
  }];

  let arguments = (ins
    SPIRV_AnyPtr:$ptr,
    SPIRV_Type:$value,
    OptionalAttr<SPIRV_MemoryAccessAttr>:$memory_access,
    OptionalAttr<I32Attr>:$alignment
  );

  let results = (outs);

  let builders = [
    OpBuilder<(ins "Value":$ptr, "Value":$value,
      CArg<"ArrayRef<NamedAttribute>", "{}">:$namedAttrs),
    [{
      $_state.addOperands(ptr);
      $_state.addOperands(value);
      $_state.addAttributes(namedAttrs);
    }]>
  ];
}

// -----

def SPIRV_VariableOp : SPIRV_Op<"Variable", []> {
  let summary = [{
    Allocate an object in memory, resulting in a pointer to it, which can be
    used with OpLoad and OpStore.
  }];

  let description = [{
    Result Type must be an OpTypePointer. Its Type operand is the type of
    object in memory.

    Storage Class is the Storage Class of the memory holding the object.
    Since the op is used to model function-level variables, the storage class
    must be the `Function` Storage Class.

    Initializer is optional. If Initializer is present, it will be the
    initial value of the variable's memory content. Initializer must be an
    <id> from a constant instruction or a global (module scope) OpVariable
    instruction. Initializer must have the same type as the type pointed to
    by Result Type.

    From `SPV_KHR_physical_storage_buffer`:
    If an OpVariable's pointee type is a pointer (or array of pointers) in
    PhysicalStorageBuffer storage class, then the variable must be decorated
    with exactly one of AliasedPointer or RestrictPointer.

    <!-- End of AutoGen section -->

    ```
    variable-op ::= ssa-id `=` `spirv.Variable` (`init(` ssa-use `)`)?
                    attribute-dict? `:` spirv-pointer-type
    ```

    where `init` specifies initializer.

    #### Example:

    ```mlir
    %0 = spirv.Constant ...

    %1 = spirv.Variable : !spirv.ptr<f32, Function>
    %2 = spirv.Variable init(%0): !spirv.ptr<f32, Function>

    %3 = spirv.Variable {aliased_pointer} :
      !spirv.ptr<!spirv.ptr<f32, PhysicalStorageBuffer>, Function>
    ```
  }];

  let arguments = (ins
    SPIRV_StorageClassAttr:$storage_class,
    Optional<AnyType>:$initializer
  );

  let results = (outs
    SPIRV_AnyPtr:$pointer
  );

  let extraClassDeclaration = [{
    ::mlir::spirv::PointerType getPointerType() {
      return ::llvm::cast<::mlir::spirv::PointerType>(getType());
    }
    ::mlir::Type getPointeeType() {
      return getPointerType().getPointeeType();
    }
  }];
}

#endif // MLIR_DIALECT_SPIRV_IR_MEMORY_OPS