Lowering tfl.reshape + TFL-to-TOSA/standard pattern organization
This pull request includes the following features:

1. Introduction of alternative TFL-to-TOSA and TFL-to-standard conversion patterns, each activated based on the ability of a given TFL op to be represented in the TOSA dialect.
2. TFL-to-TOSA lowering for operation `tfl.reshape`.
3. TFL-to-standard lowering for operation `tfl.reshape`.
4. Handling `complex` data type in `tfl.reshape`.

Here, the term *standard dialect* is used to refer to MLIR dialect `arith`, `builtin`, `func`, `index`, `linalg`, `math`, `memref`, or `tensor`.

## 1. Extending the current lowering scheme

The conversion patterns lowering TFL ops are implemented in file `tensorflow/compiler/mlir/tosa/transforms/legalize_tfl.cc`. In their current state, these patterns convert TFL ops into groups of TOSA ops on a best-effort basis. However, some TFL ops are representable in TOSA only for some combinations of their input operands. And some TFL ops are not representable in TOSA at all. To successfully lower such ops, one must rely on a richer variety of lower-level ops from MLIR standard dialects.

This merge request intends to set a precedent in the way such TFL ops will be handled, based on the following mechanism:

- Two independent conversion patterns are in charge of the TFL-to-TOSA and TFL-to-standard conversions (e.g., `ConvertTFLReshapeOpToTosa` and `ConvertTFLReshapeOpToStandard`). Both are implemented in `legalize_tfl.cc`, though given the current size of this file, it might be wise to split these in the near future.

- The TFL-to-TOSA pattern is assigned high priority with a *pattern benefit* value of 2. This pattern attempts to convert the TFL op by targeting the TOSA dialect exclusively. If the current combination of arguments makes the TFL op not representable in TOSA, conversion fails, and the transform driver follows its course by attempting to apply other compatible patterns.

- The TFL-to-standard pattern is assigned a low priority with a *pattern benefit* value of 1. It is activated when the TFL-to-TOSA counterpart fails. This pattern is primarily intended to cover TFL ops or argument combinations that are not representable in TOSA. However, an implementation may choose to disregard whatever TOSA can or cannot do, and simply focus on translating any possible TFL op configuration into standard dialects. This has the benefit of not duplicating the control logic used in the TFL-to-TOSA pattern, at the expense of possibly covering cases already handled by it. Additionally, new features incorporated into the TOSA dialect over time only affect the TFL-to-TOSA pattern, which will capture new TFL corner cases. When the TFL-to-standard pattern is designed with this principle in mind, its implementation will conveniently remain unaffected by TFL dialect extensions.

- Two different macros may now be used to declare TFL lowering patterns: `DECL_CONVERT_OP` and `DECL_CONVERT_OP_TOSA_STANDARD`. The former declares a stand-alone conversion pattern for those TFL ops that are fully representable in TOSA, named `ConvertTFL<OPNAME>Op`. The latter declares a pair of conversion patterns targeting TOSA and standard dialects, named `ConvertTFL<OPNAME>OpToTosa` and `ConvertTFL<OPNAME>OpToStandard`, respectively.



## 2. TFL-to-TOSA lowering of `tfl.reshape`

The `tfl.reshape` op takes a shape argument in the form of an SSA value. The closest match in the TOSA dialect is the `tosa.reshape` op. However, this op takes the target tensor shape as an attribute, and therefore, a direct conversion is only possible when the shape is constant.

TFL code

```
func.func @test_reshape(%arg0: tensor<13x21x3xf32>) -> tensor<*xf32> {
  %cst = arith.constant dense<[1, 819]> : tensor<2xi32>
  %0 = "tfl.reshape"(%arg0, %cst) : (tensor<13x21x3xf32>, tensor<2xi32>) -> tensor<*xf32>
  func.return %0 : tensor<*xf32>
}
```

converts to

```
func.func @test_reshape(%arg0: tensor<13x21x3xf32>) -> tensor<1x819xf32> {
  %0 = "tosa.reshape"(%arg0) <{new_shape = array<i64: 1, 819>}> : (tensor<13x21x3xf32>) -> tensor<1x819xf32>
  return %0 : tensor<1x819xf32>
}
```

## 3. TFL-to-standard lowering of `tfl.reshape`

This lowering is intended to support any combination of valid operands for `tfl.reshape`, according to its somewhat ambiguous specification available [here](https://www.tensorflow.org/mlir/tfl_ops#tflreshape_tflreshapeop) and the less ambiguous and (presumably) intended counterpart in the TF dialect (`tf.Reshape`) available [here](https://www.tensorflow.org/mlir/tf_ops#tfreshape_tfreshapeop).


### Ranked tensors

The example below illustrates the conversion strategy for the case in which both input and shape tensors are ranked. The converted code has been formatted with descriptive SSA value names and detailed comments to lay out the intermediate steps in the generated algorithm based on MLIR standard dialects.

TFL code

```
func.func @test_reshape_variable(%arg0: tensor<?xf32>, %arg1: tensor<2xi32>) -> tensor<?x?xf32> {
  %0 = "tfl.reshape"(%arg0, %arg1) : (tensor<?xf32>, tensor<2xi32>) -> tensor<?x?xf32>
  return %0 : tensor<?x?xf32>
}
```

converts to

```
func.func @test_reshape_variable(%arg0: tensor<?xf32>, %arg1: tensor<2xi32>) -> tensor<?x?xf32> {
func.func @test_reshape_variable(%arg0: tensor<?xf32>, %arg1: tensor<2xi32>) -> tensor<?x?xf32> {

  // Constants
  %const_0 = arith.constant 0 : index
  %const_0_i32 = arith.constant 0 : i32
  %const_1 = arith.constant 1 : index
  %const_1_tensor = arith.constant dense<1> : tensor<i32>}
  %const_minus_1_splat = arith.constant dense<-1> tensor<2xi32>

  // Calculate product of all element in the shape
  %shape_product_tensor = linalg.reduce
    ins(%arg1 : tensor<2xi32>)
    outs(%const_1_tensor : tensor<i32>)
    dimensions = [0]
    (%in: i32, %init: i32) {
      %temp_product = arith.muli %in, %init : i32
      linalg.yield %temp_product : i32
    }
  %shape_product = tensor.extract %shape_product_tensor[] : tensor<i32>

  // Check if the shape product is negative. Since there can be at most one
  // shape component set to -1, a negative product indicates that such shape
  // wildcard was present.
  shape_is_negative = arith.cmpi slt, %shape_product, %const_0_i32 : i32
  shape_no_wildcard = scf.if shape_is_negative -> (tensor<2xi32>) {

    // Calculate size of input tensor
    input_size = scf.for %i = %const_0 to %const_1 step %const_1 iter_args(%acc_size = %const_1) -> (index) {
      %dim = tensor.dim %arg0, %i : tensor<?xf32>
      %temp_size = arith.muli %acc_size, %dim : index
      scf.yield %temp_size : index
    }
    input_size_i32 = arith.index_cast input_size : index to i32

    // Calculate the missing shape item by diving input tensor size by all other
    // shape components. Then broadcast it into a tensor with as many elements as
    // the shape tensor.
    shape_product_abs = math.absi %shape_product : i32
    shape_wildcard_value = arith.divsi input_size_i32, shape_product_abs : i32
    shape_wildcard_splat = tensor.splat shape_wildcard_value : tensor<2xi32>

    // Create a mask for the shape tensor. This mask contains a 1 only at that
    // position where wildcard -1 is located in the shape tensor.
    shape_wildcard_mask = arith.cmpi eq, %arg1, %const_minus_1_splat : tensor<2xi32>

    // Use the mask to replace the wildcard with the computed size in the shape
    // tensor.
    %resolved_shape = arith.select shape_wildcard_mask, shape_wildcard_splat, %arg1 : tensor<2xi1>, tensor<2xi32>
    scf.yield %resolved_shape : tensor<2xi32>

  } else {

    // Shape has no wildcard and need not be modified.
    scf.yield %arg1 : tensor<2xi32>
  }

  // Now that we have a shape with no wildcard, it is safe to use
  // 'tensor.reshape'.
  %result = tensor.reshape %arg0(shape_no_wildcard) : (tensor<?xf32>, tensor<2xi32>) -> tensor<?x?xf32>
  return %result : tensor<?x?xf32>
}
```

### Unranked tensors

Additional considerations are involved in lowering `tfl.reshape` when either the input or the shape tensor is unranked, as illustrated in the following example. The converted code includes comments prefixed with `NEW` to highlight the differences over the ranked case.

TFL code

```
func.func @test_reshape_unranked(%arg0: tensor<*xf32>, %arg1: tensor<*xi32>) -> tensor<*xf32> {
  %0 = "tfl.reshape"(%arg0, %arg1) : (tensor<*xf32>, tensor<*xi32>) -> tensor<*xf32>
  return %0 : tensor<*xf32>
}
```

converts to

```
func.func @test_reshape_unranked(%arg0: tensor<*xf32>, %arg1: tensor<*xi32>) -> tensor<*xf32> {
  %0 = "tosa.const"() <{value = dense<-1> : tensor<i32>}> : () -> tensor<i32>
  %c1 = arith.constant 1 : index
  %c0 = arith.constant 0 : index
  %1 = "tosa.const"() <{value = dense<1> : tensor<i32>}> : () -> tensor<i32>
  %c0_i32 = arith.constant 0 : i32

  // NEW: Cast shape tensor into a 1D ranked tensor.
  %cast = tensor.cast %arg1 : tensor<*xi32> to tensor<?xi32>

  %reduced = linalg.reduce ins(%cast : tensor<?xi32>) outs(%1 : tensor<i32>) dimensions = [0]
    (%in: i32, %init: i32) {
      %4 = arith.muli %in, %init : i32
      linalg.yield %4 : i32
    }
  %extracted = tensor.extract %reduced[] : tensor<i32>
  %2 = arith.cmpi slt, %extracted, %c0_i32 : i32
  %3 = scf.if %2 -> (tensor<?xi32>) {

    // NEW: Query the input tensor rank in order to calculate its total size.
    %rank = tensor.rank %arg0 : tensor<*xf32>
    %4 = scf.for %arg2 = %c0 to %rank step %c1 iter_args(%arg3 = %c1) -> (index) {
      %dim_1 = tensor.dim %arg0, %arg2 : tensor<*xf32>
      %12 = arith.muli %arg3, %dim_1 : index
      scf.yield %12 : index
    }
    %5 = arith.index_cast %4 : index to i32
    %6 = math.absi %extracted : i32
    %dim = tensor.dim %cast, %c0 : tensor<?xi32>
    %7 = arith.divsi %5, %6 : i32
    %from_elements = tensor.from_elements %7 : tensor<i32>

    // NEW: When the size of the shape tensor is not known at compile time,
    // tensor splats are computed dynamically with a 'linalg.broadcast' op,
    // instead of 'tensor.splat'.
    %8 = tensor.empty(%dim) : tensor<?xi32>
    %broadcasted = linalg.broadcast ins(%from_elements : tensor<i32>) outs(%8 : tensor<?xi32>) dimensions = [0]
    %9 = tensor.empty(%dim) : tensor<?xi32>
    %broadcasted_0 = linalg.broadcast ins(%0 : tensor<i32>) outs(%9 : tensor<?xi32>) dimensions = [0]

    %10 = arith.cmpi eq, %cast, %broadcasted_0 : tensor<?xi32>
    %11 = arith.select %10, %broadcasted, %cast : tensor<?xi1>, tensor<?xi32>
    scf.yield %11 : tensor<?xi32>
  } else {
    scf.yield %cast : tensor<?xi32>
  }
  %reshape = tensor.reshape %arg0(%3) : (tensor<*xf32>, tensor<?xi32>) -> tensor<*xf32>
  return %reshape : tensor<*xf32>
}
```




## 4. Handling `complex` type in `tfl.reshape`

### Problem

A pass named `tosa-lower-complex-types` is currently in charge of lowering any occurrence of the `complex<float>` data type into a pair of `float` values (e.g., `tensor<3xcomplex<float>>` becomes `tensor<3x2xfloat>`). This pass runs after the TFL-to-TOSA dialect conversion within a pass pipeline named `tfl-to-tosa-pipeline`.

The `tosa-lower-complex-types` processes occurrences of type `complex` indiscriminately, with no regard to the operation in which such type occurs. This is fine in most cases, but poses a problem specifically for the `tfl.reshape` op. If its input and output tensors are of type `complex`, flattening them into tensors of `float` affects their rank, but should also affect the target shape for the operation to remain consistent. For example, operation

```
%shape = arith.constant dense<2, 3> : tensor<2xi32>
%result = "tfl.reshape"(%input, %shape) : (tensor<6xcomplex<f32>>, tensor<2xi32>) -> tensor<2x3xcomplex<f32>>
```

cannot simply be translated to


```
%shape = arith.constant dense<2, 3> : tensor<2xi32>
%result = "tfl.reshape"(%input, %shape) : (tensor<6x2xf32>, tensor<2xi32>) -> tensor<2x3x2xf32>
```

In the resulting code above, the size of the target shape (2) does not match the rank of the output tensor (3). The target shape must be extended with an additional component set to 2 in order to reflect the additional dimension added by the `complex`-to-`float` flattening process. The resulting code should then be

```
%shape = arith.constant dense<2, 3, 2> : tensor<3xi32>
%result = "tfl.reshape"(%input, %shape) : (tensor<6x2xf32>, tensor<3xi32>) -> tensor<2x3x2xf32>
```

### Solution

When an input tensor of type `complex` is detected, the generated code is extended as follows:

- The input tensor of type `complex<float>` is converted into a tensor of type `float` with an additional dimension of size 2 by means of a `builtin.unrealized_conversion_cast` op. The remaining lowering process deals with the modified input tensor.

- The shape tensor is extended with an additional element set to 2. The remaining lowering process deals with the new extended shape tensor.

- The final tensor resulting from the emitted `tensor.reshape` op is converted back to type `complex<float>` through another `builtin.unrealized_conversion_cast` op. Both occurrences of `builtin.unrealized_conversion_cast` will be automatically folded away further down in the conversion pipeline once the `tosa-lower-complex-types` pass runs.

The converted code in the example below has been annotated with `NEW` to highlight the key locations affected by the presence of complex tensors.

TFL code

```
func.func @test_reshape_complex(%arg0: tensor<?x1x257xcomplex<f32>>) -> tensor<?x257xcomplex<f32>> {
  %cst = "tfl.pseudo_const"() {value = dense<[-1, 257]> : tensor<2xi32>} : () -> tensor<2xi32>
  %1 = "tfl.reshape"(%arg0, %cst) : (tensor<?x1x257xcomplex<f32>>, tensor<2xi32>) -> tensor<?x257xcomplex<f32>>
  func.return %1 : tensor<?x257xcomplex<f32>>
}
```

converts to

```
func.func @test_reshape_complex(%arg0: tensor<?x1x257xcomplex<f32>>) -> tensor<?x257xcomplex<f32>> {
  %0 = "tosa.const"() <{value = dense<-1> : tensor<3xi32>}> : () -> tensor<3xi32>
  %c4 = arith.constant 4 : index
  %c1 = arith.constant 1 : index
  %c0 = arith.constant 0 : index
  %2 = "tosa.const"() <{value = dense<1> : tensor<i32>}> : () -> tensor<i32>
  %c0_i32 = arith.constant 0 : i32
  %3 = "tosa.const"() <{value = dense<[-1, 257]> : tensor<2xi32>}> : () -> tensor<2xi32>

  // NEW: Flatten complex input tensor
  %4 = builtin.unrealized_conversion_cast %arg0 : tensor<?x1x257xcomplex<f32>> to tensor<?x1x257x2xf32>

  // NEW: Extend shape tensor with additional component set to 2
  %const_2 = "tosa.const"() <{value = dense<2> : tensor<1xi32>}> : () -> tensor<1xi32>
  %5 = "tosa.concat"(%3, %const_2) <{axis = 0 : i64}> : (tensor<2xi32>, tensor<1xi32>) -> tensor<3xi32>

  %reduced = linalg.reduce ins(%5 : tensor<3xi32>) outs(%2 : tensor<i32>) dimensions = [0]
    (%in: i32, %init: i32) {
      %9 = arith.muli %in, %init : i32
      linalg.yield %9 : i32
    }
  %extracted = tensor.extract %reduced[] : tensor<i32>
  %6 = arith.cmpi slt, %extracted, %c0_i32 : i32
  %7 = scf.if %6 -> (tensor<3xi32>) {
    %9 = scf.for %arg1 = %c0 to %c4 step %c1 iter_args(%arg2 = %c1) -> (index) {
      %dim = tensor.dim %4, %arg1 : tensor<?x1x257x2xf32>
      %15 = arith.muli %arg2, %dim : index
      scf.yield %15 : index
    }
    %10 = arith.index_cast %9 : index to i32
    %11 = math.absi %extracted : i32
    %12 = arith.divsi %10, %11 : i32
    %splat = tensor.splat %12 : tensor<3xi32>
    %13 = arith.cmpi eq, %5, %0 : tensor<3xi32>
    %14 = arith.select %13, %splat, %5 : tensor<3xi1>, tensor<3xi32>
    scf.yield %14 : tensor<3xi32>
  } else {
    scf.yield %5 : tensor<3xi32>
  }
  %reshape = tensor.reshape %4(%7) : (tensor<?x1x257x2xf32>, tensor<3xi32>) -> tensor<?x257x2xf32>

  // NEW: Convert resulting tensor back to complex type
  %8 = builtin.unrealized_conversion_cast %reshape : tensor<?x257x2xf32> to tensor<?x257xcomplex<f32>>

  return %8 : tensor<?x257xcomplex<f32>>
}
```


### Limitation

The presented strategy relies on compile-time knowledge of the input tensor rank for the introduction of an additional dimension in its data type. This restriction limits its application to ranked input tensors. The current TFL-to-core conversion pattern does not support unranked input tensors of type `complex`.

