# 'omp' Dialect

The `omp` dialect is for representing directives, clauses and other definitions
of the [OpenMP programming model](https://www.openmp.org). This directive-based
programming model, defined for the C, C++ and Fortran programming languages,
provides abstractions to simplify the development of parallel and accelerated
programs. All versions of the OpenMP specification can be found
[here](https://www.openmp.org/specifications/).

Operations in this MLIR dialect generally correspond to a single OpenMP
directive, taking arguments that represent their supported clauses, though this
is not always the case. For a detailed information of operations, types and
other definitions in this dialect, refer to the automatically-generated
[ODS Documentation](ODS.md).

[TOC]

## Operation Naming Conventions

This section aims to standardize how dialect operation names are chosen, to
ensure a level of consistency. There are two categories of names: tablegen names
and assembly names. The former also corresponds to the C++ class that is
generated for the operation, whereas the latter is used to represent it in MLIR
text form.

Tablegen names are CamelCase, with the first letter capitalized and an "Op"
suffix, whereas assembly names are snake_case, with all lowercase letters and
words separated by underscores.

If the operation corresponds to a directive, clause or other kind of definition
in the OpenMP specification, it must use the same name split into words in the
same way. For example, the `target data` directive would become `TargetDataOp` /
`omp.target_data`, whereas `taskloop` would become `TaskloopWrapperOp` /
`omp.taskloop.wrapper`.

Operations intended to carry extra information for another particular operation
or clause must be named after that other operation or clause, followed by the
name of the additional information. The assembly name must use a period to
separate both parts. For example, the operation used to define some extra
mapping information is named `MapInfoOp` / `omp.map.info`. The same rules are
followed if multiple operations are created for different variants of the same
directive, e.g. `atomic` becomes `Atomic{Read,Write,Update,Capture}Op` /
`omp.atomic.{read,write,update,capture}`.

## Clause-Based Operation Definition

One main feature of the OpenMP specification is that, even though the set of
clauses that could be applied to a given directive is independent from other
directives, these clauses can generally apply to multiple directives. Since
clauses usually define which arguments the corresponding MLIR operation takes,
it is possible (and preferred) to define OpenMP dialect operations based on the
list of clauses taken by the corresponding directive. This makes it simpler to
keep their representation consistent across operations and minimizes redundancy
in the dialect.

To achieve this, the base `OpenMP_Clause` tablegen class has been created. It is
intended to be used to create clause definitions that can be then attached to
multiple `OpenMP_Op` definitions, resulting in the latter inheriting by default
all properties defined by clauses attached, similarly to the trait mechanism.
This mechanism is implemented in
[OpenMPOpBase.td](https://github.com/llvm/llvm-project/blob/main/mlir/include/mlir/Dialect/OpenMP/OpenMPOpBase.td).

### Adding a Clause

OpenMP clause definitions are located in
[OpenMPClauses.td](https://github.com/llvm/llvm-project/blob/main/mlir/include/mlir/Dialect/OpenMP/OpenMPClauses.td).
For each clause, an `OpenMP_Clause` subclass and a definition based on it must
be created. The subclass must take a `bit` template argument for each of the
properties it can populate on associated `OpenMP_Op`s. These must be forwarded
to the base class. The definition must be an instantiation of the base class
where all these template arguments are set to `false`. The definition's name
must be `OpenMP_<Name>Clause`, whereas its base class' must be
`OpenMP_<Name>ClauseSkip`. Following this pattern makes it possible to
optionally skip the inheritance of some properties when defining operations:
[more info](#overriding-clause-inherited-properties).

Clauses can define the following properties:
  - `list<Traits> traits`: To be used when having a certain clause always
implies some op trait, like the `map` clause and the `MapClauseOwningInterface`.
  - `dag(ins) arguments`: Mandatory property holding values and attributes
used to represent the clause. Argument names use snake_case and should contain
the clause name to avoid name clashes between clauses. Variadic arguments
(non-attributes) must contain the "_vars" suffix.
  - `string {req,opt}AssemblyFormat`: Optional formatting strings to produce
custom human-friendly printers and parsers for arguments associated with the
clause. It will be combined with assembly formats for other clauses as explained
[below](#adding-an-operation).
  - `string description`: Optional description text to describe the clause and
its representation.
  - `string extraClassDeclaration`: Optional C++ declarations to be added to
operation classes including the clause.

For example:

```tablegen
class OpenMP_ExampleClauseSkip<
    bit traits = false, bit arguments = false, bit assemblyFormat = false,
    bit description = false, bit extraClassDeclaration = false
  > : OpenMP_Clause<traits, arguments, assemblyFormat, description,
                    extraClassDeclaration> {
  let arguments = (ins
    Optional<AnyType>:$example_var
  );

  let optAssemblyFormat = [{
    `example` `(` $example_var `:` type($example_var) `)`
  }];

  let description = [{
    The `example_var` argument defines the variable to which the EXAMPLE clause
    applies.
  }];
}

def OpenMP_ExampleClause : OpenMP_ExampleClauseSkip<>;
```

### Adding an Operation

Operations in the OpenMP dialect, located in
[OpenMPOps.td](https://github.com/llvm/llvm-project/blob/main/mlir/include/mlir/Dialect/OpenMP/OpenMPOps.td),
can be defined like any other regular operation by just specifying a `mnemonic`
and optional list of `traits` when inheriting from `OpenMP_Op`, and then
defining the expected `description`, `arguments`, etc. properties inside of its
body. However, in most cases, basing the operation definition on its list of
accepted clauses is significantly simpler because some of the properties can
just be inherited from these clauses.

In general, the way to achieve this is to specify, in addition to the `mnemonic`
and optional list of `traits`, a list of `clauses` where all the applicable
`OpenMP_<Name>Clause` definitions are added. Then, the only properties that
would have to be defined in the operation's body are the `summary` and
`description`. For the latter, only the operation itself would have to be
defined, and the description for its clause-inherited arguments is appended
through the inherited `clausesDescription` property. By convention, the list of
clauses for an operation must be specified in alphabetical order.

If the operation is intended to have a single region, this is better achieved by
setting the `singleRegion=true` template argument of `OpenMP_Op` rather manually
populating the `regions` property of the operation, because that way the default
`assemblyFormat` is also updated correspondingly.

For example:

```tablegen
def ExampleOp : OpenMP_Op<"example", traits = [
    AttrSizedOperandSegments, ...
  ], clauses = [
    OpenMP_AlignedClause, OpenMP_IfClause, OpenMP_LinearClause, ...
  ], singleRegion = true> {
  let summary = "example construct";
  let description = [{
    The example construct represents...
  }] # clausesDescription;
}
```

This is possible because the `arguments`, `assemblyFormat` and
`extraClassDeclaration` properties of the operation are by default
populated by concatenating the corresponding properties of the clauses on the
list. In the case of the `assemblyFormat`, this involves combining the
`reqAssemblyFormat` and the `optAssemblyFormat` properties. The
`reqAssemblyFormat` of all clauses is concatenated first and separated using
spaces, whereas the `optAssemblyFormat` is wrapped in an `oilist()` and
interleaved with "|" instead of spaces. The resulting `assemblyFormat` contains
the required assembly format strings, followed by the optional assembly format
strings, optionally the `$region` and the `attr-dict`.

### Overriding Clause-Inherited Properties

Although the clause-based definition of operations can greatly reduce work, it's
also somewhat restrictive, since there may be some situations where only part of
the operation definition can be automated in that manner. For a fine-grained
control over properties inherited from each clause two features are available:

  - Inhibition of properties. By using `OpenMP_<Name>ClauseSkip` tablegen
classes, the list of properties copied from the clause to the operation can be
selected. For example, `OpenMP_IfClauseSkip<assemblyFormat = true>` would result
in every property defined for the `OpenMP_IfClause` except for the
`assemblyFormat` being used to initially populate the properties of the
operation.
  - Augmentation of properties. There are times when there is a need to add to
a clause-populated operation property. Instead of overriding the property in the
definition of the operation and having to manually replicate what would
otherwise be automatically populated before adding to it, some internal
properties are defined to hold this default value: `clausesArgs`,
`clausesAssemblyFormat`, `clauses{Req,Opt}AssemblyFormat` and
`clausesExtraClassDeclaration`.

In the following example, assuming both the `OpenMP_InReductionClause` and the
`OpenMP_ReductionClause` define a `getReductionVars` extra class declaration,
we skip the conflicting `extraClassDeclaration`s inherited by both clauses and
provide another implementation, without having to also re-define other
declarations inherited from the `OpenMP_AllocateClause`:

```tablegen
def ExampleOp : OpenMP_Op<"example", traits = [
    AttrSizedOperandSegments, ...
  ], clauses = [
    OpenMP_AllocateClause,
    OpenMP_InReductionClauseSkip<extraClassDeclaration = true>,
    OpenMP_ReductionClauseSkip<extraClassDeclaration = true>
  ], singleRegion = true> {
  let summary = "example construct";
  let description = [{
    This operation represents...
  }] # clausesDescription;

  // Override the clause-populated extraClassDeclaration and add the default
  // back via appending clausesExtraClassDeclaration to it. This has the effect
  // of adding one declaration. Since this property is skipped for the
  // InReduction and Reduction clauses, clausesExtraClassDeclaration won't
  // incorporate the definition of this property for these clauses.
  let extraClassDeclaration = [{
    SmallVector<Value> getReductionVars() {
      // Concatenate inReductionVars and reductionVars and return the result...
    }
  }] # clausesExtraClassDeclaration;
}
```

These features are intended for complex edge cases, but an effort should be made
to avoid having to use them, since they may introduce inconsistencies and
complexity to the dialect.

### Tablegen Verification Pass

As a result of the implicit way in which fundamental properties of MLIR
operations are populated following this approach, and the ability to override
them, forgetting to append clause-inherited values might result in hard to debug
tablegen errors.

For this reason, the `-verify-openmp-ops` tablegen pseudo-backend was created.
It runs before any other tablegen backends are triggered for the
[OpenMPOps.td](https://github.com/llvm/llvm-project/blob/main/mlir/include/mlir/Dialect/OpenMP/OpenMPOps.td)
file and warns any time a property defined for a clause is not found in the
corresponding operation, except if it is explicitly skipped as described
[above](#overriding-clause-inherited-properties). This way, in case of a later
tablegen failure while processing OpenMP dialect operations, earlier messages
triggered by that pass can point to a likely solution.

### Operand Structures

One consequence of basing the representation of operations on the set of values
and attributes defined for each clause applicable to the corresponding OpenMP
directive is that operation argument lists tend to be long. This has the effect
of making C++ operation builders difficult to work with and easy to mistakenly
pass arguments in the wrong order, which may sometimes introduce hard to detect
problems.

A solution provided to this issue are operand structures. The main idea behind
them is that there is one defined for each clause, holding a set of fields that
contain the data needed to initialize each of the arguments associated with that
clause. Clause operand structures are aggregated into operation operand
structures via class inheritance. Then, a custom builder is defined for each
operation taking the corresponding operand structure as a parameter. Since each
argument is a named member of the structure, it becomes much simpler to set up
the desired arguments to create a new operation.

Ad-hoc operand structures available for use within the ODS definition of custom
operation builders might be defined in
[OpenMPClauseOperands.h](https://github.com/llvm/llvm-project/blob/main/mlir/include/mlir/Dialect/OpenMP/OpenMPClauseOperands.h).
However, this is generally not needed for clause-based operation definitions.
The `-gen-openmp-clause-ops` tablegen backend, triggered when building the 'omp'
dialect, will automatically produce structures in the following way:

- It will create a `<Name>ClauseOps` structure for each `OpenMP_Clause`
definition with one field per argument.
- The name of each field will match the tablegen name of the corresponding
argument, except for replacing snake case with camel case.
- The type of the field will be obtained from the corresponding tablegen
argument's type:
  - Values are represented with `mlir::Value`, except for `Variadic`, which
  makes it an `llvm::SmallVector<mlir::Value>`.
  - `OptionalAttr` is represented by the translation of its `baseAttr`.
  - `TypedArrayAttrBase`-based attribute types are represented by wrapping the
  translation of their `elementAttr` in an `llvm::SmallVector`. The only
  exception for this case is if the `elementAttr` is a "scalar" (i.e. non
  array-like) attribute type, in which case the more generic `mlir::Attribute`
  will be used in place of its `storageType`.
  - For `ElementsAttrBase`-based attribute types a best effort is attempted to
  obtain an element type (`llvm::APInt`, `llvm::APFloat` or
  `DenseArrayAttrBase`'s `returnType`) to be wrapped in an `llvm::SmallVector`.
  If it cannot be obtained, which will happen with non-builtin direct subclasses
  of `ElementsAttrBase`, a warning will be emitted and the `storageType` (i.e.
  specific `mlir::Attribute` subclass) will be used instead.
  - Other attribute types will be represented with their `storageType`.
- It will create `<Name>Operands` structure for each operation, which is an
empty structure subclassing all operand structures defined for the corresponding
`OpenMP_Op`'s clauses.

### Entry Block Argument-Defining Clauses

In their MLIR representation, certain OpenMP clauses introduce a mapping between
values defined outside the operation they are applied to and entry block
arguments for the region of that MLIR operation. This enables, for example, the
introduction of private copies of the same underlying variable defined outside
the MLIR operation the clause is attached to. Currently, clauses with this
property can be classified into three main categories:
  - Map-like clauses: `host_eval` (compiler internal, not defined by the OpenMP
  specification: [see more](#host-evaluated-clauses-in-target-regions)), `map`,
  `use_device_addr` and `use_device_ptr`.
  - Reduction-like clauses: `in_reduction`, `reduction` and `task_reduction`.
  - Privatization clauses: `private`.

All three kinds of entry block argument-defining clauses use a similar custom
assembly format representation, only differing based on the different pieces of
information attached to each kind. Below, one example of each is shown:

```mlir
omp.target map_entries(%x -> %x.m, %y -> %y.m : !llvm.ptr, !llvm.ptr) {
  // Use %x.m, %y.m in place of %x and %y...
}

omp.wsloop reduction(@add.i32 %x -> %x.r, byref @add.f32 %y -> %y.r : !llvm.ptr, !llvm.ptr) {
  // Use %x.r, %y.r in place of %x and %y...
}

omp.parallel private(@x.privatizer %x -> %x.p, @y.privatizer %y -> %y.p : !llvm.ptr, !llvm.ptr) {
  // Use %x.p, %y.p in place of %x and %y...
}
```

As a consequence of parsing and printing the operation's first region entry
block argument names together with the custom assembly format of these clauses,
entry block arguments (i.e. the `^bb0(...):` line) must not be explicitly
defined for these operations. Additionally, it is not possible to implement this
feature while allowing each clause to be independently parsed and printed,
because they need to be printed/parsed together with the corresponding
operation's first region. They must have a well-defined ordering in which
multiple of these clauses are specified for a given operation, as well.

The parsing/printing of these clauses together with the region provides the
ability to define entry block arguments directly after the `->`. Forcing a
specific ordering between these clauses makes the block argument ordering
well-defined, which is the property used to easily match each clause with the
entry block arguments defined by it.

Custom printers and parsers for operation regions based on the entry block
argument-defining clauses they take are implemented based on the
`{parse,print}BlockArgRegion` functions, which take care of the sorting and
formatting of each kind of clause, minimizing code duplication resulting from
this approach. One example of the custom assembly format of an operation taking
the `private` and `reduction` clauses is the following:

```tablegen
let assemblyFormat = clausesAssemblyFormat # [{
  custom<PrivateReductionRegion>($region, $private_vars, type($private_vars),
      $private_syms, $reduction_vars, type($reduction_vars), $reduction_byref,
      $reduction_syms) attr-dict
}];
```

The `BlockArgOpenMPOpInterface` has been introduced to simplify the addition and
handling of these kinds of clauses. Adding it to an operation directly, or
indirectly through a clause, results in the addition of overridable
`get<ClauseName>Vars()` and `num<ClauseName>BlockArgs()` public functions for
all entry block argument-generating clauses. By default, the reported number of
block arguments defined by a clause will correspond to the number of operands
taken by the operation for that clause. This list of operands will be empty by
default, and will automatically be overriden by getters of the corresponding
`Variadic<...> $<clause_name>_vars` argument of the same clause's definition.

In addition to these methods added to the actual operations, the
`BlockArgOpenMPOpInterface` itself defines a set of methods based on the
previous ones and on the convention that entry block arguments for multiple
clauses are sorted alphabetically by clause name. These are listed below, and
they represent the main way in which clause-defined block arguments should be
accessed:
  - `get<ClauseName>BlockArgsStart()`: Returns the index within the list of
  entry block arguments where the first element defined by the given clause
  should be located.
  - `get<ClauseName>BlockArgs()`: Returns the list of entry block arguments
  defined by the given clause.
  - `numClauseBlockArgs()`: Returns the total number of entry block arguments
  defined by all clauses.
  - `getBlockArgsPairs()`: Returns a list of pairs where the first element is
  the outside value, or operand, and the second element is the corresponding
  entry block argument.

## Loop-Associated Directives

Loop-associated OpenMP constructs are represented in the dialect as loop wrapper
operations. These implement the `LoopWrapperInterface`, which enforces a series
of restrictions upon the operation:
  - It has the `NoTerminator` and `SingleBlock` traits;
  - It contains a single region; and
  - Its only block contains exactly one operation, which must be another loop
wrapper or `omp.loop_nest` operation.

This approach splits the representation for a loop nest and the loop-associated
constructs that specify how its iterations are executed, possibly across various
SIMD lanes (`omp.simd`), threads (`omp.wsloop`), teams of threads
(`omp.distribute`) or tasks (`omp.taskloop.wrapper`). The ability to directly nest
multiple loop wrappers to impact the execution of a single loop nest is used to
represent composite constructs in a modular way.

The `omp.loop_nest` operation represents a collapsed rectangular loop nest that
must always be wrapped by at least one loop wrapper, which defines how it is
intended to be executed. It serves as a simpler and more restrictive
representation of OpenMP loops while a more general approach to support
non-rectangular loop nests, loop transformations and non-perfectly nested loops
based on a new `omp.canonical_loop` definition is developed.

The following example shows how a `parallel {do,for}` construct would be
represented:
```mlir
omp.parallel ... {
  ...
  omp.wsloop ... {
    omp.loop_nest (%i) : index = (%lb) to (%ub) step (%step) {
      %a = load %a[%i] : memref<?xf32>
      %b = load %b[%i] : memref<?xf32>
      %sum = arith.addf %a, %b : f32
      store %sum, %c[%i] : memref<?xf32>
      omp.yield
    }
  }
  ...
  omp.terminator
}
```

### Loop Transformations

In addition to the worksharing loop-associated constructs described above, the
OpenMP specification also defines a set of loop transformation constructs. They
replace the associated loop(s) before worksharing constructs are executed on the
generated loop(s). Some examples of such constructs are `tile` and `unroll`.

A general approach for representing these types of OpenMP constructs has not yet
been implemented, but it is closely linked to the `omp.canonical_loop` work.
Nevertheless, loop transformation that the `collapse` clause for loop-associated
worksharing constructs defines can be represented by introducing multiple
bounds, step and induction variables to the `omp.loop_nest` operation.

## Compound Construct Representation

The OpenMP specification defines certain shortcuts that allow specifying
multiple constructs in a single directive, which are referred to as compound
constructs (e.g. `parallel do` contains the `parallel` and `do` constructs).
These can be further classified into [combined](#combined-constructs) and
[composite](#composite-constructs) constructs. This section describes how they
are represented in the dialect.

When clauses are specified for compound constructs, the OpenMP specification
defines a set of rules to decide to which leaf constructs they apply, as well as
potentially introducing some other implicit clauses. These rules must be taken
into account by those creating the MLIR representation, since it is a per-leaf
representation that expects these rules to have already been followed.

### Combined Constructs

Combined constructs are semantically equivalent to specifying one construct
immediately nested inside another. This property is used to simplify the dialect
by representing them through the operations associated to each leaf construct.
For example, `target teams` would be represented as follows:

```mlir
omp.target ... {
  ...
  omp.teams ... {
    ...
    omp.terminator
  }
  ...
  omp.terminator
}
```

### Composite Constructs

Composite constructs are similar to combined constructs in that they specify the
effect of one construct being applied immediately after another. However, they
group together constructs that cannot be directly nested into each other.
Specifically, they group together multiple loop-associated constructs that apply
to the same collapsed loop nest.

As of version 5.2 of the OpenMP specification, the list of composite constructs
is the following:
  - `{do,for} simd`;
  - `distribute simd`;
  - `distribute parallel {do,for}`;
  - `distribute parallel {do,for} simd`; and
  - `taskloop simd`.

Even though the list of composite constructs is relatively short and it would
also be possible to create dialect operations for each, it was decided to
allow attaching multiple loop wrappers to a single loop instead. This minimizes
redundancy in the dialect and maximizes its modularity, since there is a single
operation for each leaf construct regardless of whether it can be part of a
composite construct. On the other hand, this means the `omp.loop_nest` operation
will have to be interpreted differently depending on how many and which loop
wrappers are attached to it.

To simplify the detection of operations taking part in the representation of a
composite construct, the `ComposableOpInterface` was introduced. Its purpose is
to handle the `omp.composite` discardable dialect attribute that can optionally
be attached to these operations. Operation verifiers will ensure its presence is
consistent with the context the operation appears in, so that it is valid when
the attribute is present if and only if it represents a leaf of a composite
construct.

For example, the `distribute simd` composite construct is represented as
follows:

```mlir
omp.distribute ... {
  omp.simd ... {
    omp.loop_nest (%i) : index = (%lb) to (%ub) step (%step) {
      ...
      omp.yield
    }
  } {omp.composite}
} {omp.composite}
```

One exception to this is the representation of the
`distribute parallel {do,for}` composite construct. The presence of a
block-associated `parallel` leaf construct would introduce many problems if it
was allowed to work as a loop wrapper. In this case, the "hoisted `omp.parallel`
representation" is used instead. This consists in making `omp.parallel` the
parent operation, with a nested `omp.loop_nest` wrapped by `omp.distribute` and
`omp.wsloop` (and `omp.simd`, in the `distribute parallel {do,for} simd` case).

This approach works because `parallel` is a parallelism-generating construct,
whereas `distribute` is a worksharing construct impacting the higher level
`teams` construct, making the ordering between these constructs not cause
semantic mismatches. This property is also exploited by LLVM's SPMD-mode.

```mlir
omp.parallel ... {
  ...
  omp.distribute ... {
    omp.wsloop ... {
      omp.loop_nest (%i) : index = (%lb) to (%ub) step (%step) {
        ...
        omp.yield
      }
    } {omp.composite}
  } {omp.composite}
  ...
  omp.terminator
} {omp.composite}
```

## Host-Evaluated Clauses in Target Regions

The `omp.target` operation, which represents the OpenMP `target` construct, is
marked with the `IsolatedFromAbove` trait. This means that, inside of its
region, no MLIR values defined outside of the op itself can be used. This is
consistent with the OpenMP specification of the `target` construct, which
mandates that all host device values used inside of the `target` region must
either be privatized (data-sharing) or mapped (data-mapping).

Normally, clauses applied to a construct are evaluated before entering that
construct. Further, in some cases, the OpenMP specification stipulates that
clauses be evaluated _on the host device_ on entry to a parent `target`
construct. In particular, the `num_teams` and `thread_limit` clauses of the
`teams` construct must be evaluated on the host device if it's nested inside or
combined with a `target` construct.

Additionally, the runtime library targeted by the MLIR to LLVM IR translation of
the OpenMP dialect supports the optimized launch of SPMD kernels (i.e.
`target teams distribute parallel {do,for}` in OpenMP), which requires
specifying in advance what the total trip count of the loop is. Consequently, it
is also beneficial to evaluate the trip count on the host device prior to the
kernel launch.

These host-evaluated values in MLIR would need to be placed outside of the
`omp.target` region and also attached to the corresponding nested operations,
which is not possible because of the `IsolatedFromAbove` trait. The solution
implemented to address this problem has been to introduce the `host_eval`
argument to the `omp.target` operation. It works similarly to a `map` clause,
but its only intended use is to forward host-evaluated values to their
corresponding operation inside of the region. Any uses outside of the previously
described result in a verifier error.

```mlir
// Initialize %0, %1, %2, %3...
omp.target host_eval(%0 -> %nt, %1 -> %lb, %2 -> %ub, %3 -> %step : i32, i32, i32, i32) {
  omp.teams num_teams(to %nt : i32) {
    omp.parallel {
      omp.distribute {
        omp.wsloop {
          omp.loop_nest (%iv) : i32 = (%lb) to (%ub) step (%step) {
            // ...
            omp.yield
          }
          omp.terminator
        } {omp.composite}
        omp.terminator
      } {omp.composite}
      omp.terminator
    } {omp.composite}
    omp.terminator
  }
  omp.terminator
}
```
