New TFL-to-tensor pass
@jpienaar I did my best to address your comments. Let me know if there's anything else you'd like me to take care of. Hi @rafaelubalmw Can you please resolve conflicts? Thank you!
 This PR is stale because it has been open for 14 days with no activity. It will be closed if no further activity occurs. Thank you. Hi @rafaelubalmw Can you please resolve conflicts? Thank you!
This pull request introduces a new pass called `tensor-legalize-tfl` aimed at lowering the TFL dialect into *standard* MLIR dialects (`arith`, `builtin`, `func`, `index`, `linalg`, `math`, `memref`, or `tensor`). The pass includes a first conversion pattern aimed at lowering the `tfl.reshape` operation.


## 1. Organization

The current support to convert the TFL dialect into lower-level MLIR dialects is concentrated in pass `tosa-legalize-tfl`, which uses TOSA as the target dialect. A limitation of this pass is posed by the fact that some TFL ops are representable in TOSA only for some combinations of their input operands, while other 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. After several discussions on the topic, it has been concluded that introducing a separate pass independent from TOSA is the most appropriate course of action.

This pull request introduces pass `tensor-legalize-tfl`. While the name of this pass suggests that the `tensor` dialect is the conversion target, the pass may generate operation in any MLIR standard dialect other than `tosa`. These are some reasons that justify the choice of the `tensor` prefix in the pass name:

- It is likely that the emitted code for a given conversion pattern uses an operation in the `tensor` dialect as its centerpiece. This is the case of the first lowering pattern introduced in this pull request, where op `tfl.reshape` is lowered directly into `tensor.reshape` in some straightforward corner cases, although the general case involves the introduction of significantly more complex logic in other dialects.

- The proposed implementation aims at a parallel structure between the existing TFL-to-TOSA and the new TFL-to-tensor pass, regarding pass names (`tosa-legalize-tfl` vs `tensor-legalize-tfl`), directory hierarchy (directories `tosa` vs `tensor` under `tensorflow/compiler/mlir`), and file structure (files `legalize_tfl.cc`, `legalize_utils.cc`, `passes.td`, `passes.h`, unit tests, etc.).


## 2. Design principles

The following principles have governed implementation decisions in the present pull request, and are also intended to determine future decisions in new conversion patterns added to the new `tensor-legalize-tfl` pass:

- Pass `tensor-legalize-tfl` is intended as a complete pass to fully lower the TFL dialect in such a way that the resulting IR can be picked up by the MLIR repository for further lowering.

- Every new conversion pattern introduced in `tensor-legalize-tfl` is intended to support every possible combination of input arguments for a given TFL operation, as described in the corresponding section of the TFL dialect specification. In some cases, this will introduce redundant lowering routes for the same TFL op (TFL -> TOSA -> standard dialects vs. TFL -> standard dialects). However, an independent design of the TFL-to-tensor pass provides valuable flexibility that frees us from having to reason about the semantic limitations of TOSA.

- In spite of the independent design of `tosa-legalize-tfl` and `tensor-legalize-tfl`, development of new conversion patterns in `tensor-legalize-tfl` should focus on operations that are currently not supported (or that are only partially supported) in `tosa-legalize-tfl`. Such roadmap will speed up the ability for a user to connect Tensorflow with standard MLIR dialects for arbitrary sets of TFL ops by running passes TFL-to-TOSA and TFL-to-tensor sequentially.


## 3. Lowering `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).

The examples below illustrate the conversion strategy for various combinations of input arguments. The converted code has been formatted with descriptive SSA value names and detailed comments to break down intermediate steps.

### Constant shape

When the shape tensor is constant, operation `tfl.reshape` has a direct conversion to `tensor.reshape`.

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<*xf32> {
  %cst = arith.constant dense<[1, 819]> : tensor<2xi32>
  %reshape = tensor.reshape %arg0(%cst) : (tensor<13x21x3xf32>, tensor<2xi32>) -> tensor<*xf32>
  return %reshape : tensor<*xf32>
}
```

### Constant shape with wildcard

The `tfl.reshape` operation allows for the shape tensor to include at most one wildcard value set to -1. If such value is present, it must be substituted with the intended dimension size of the shape tensor. If the input tensor has a static shape and the shape tensor is a constant, the wildcard substitution may happen at compile time. In the example below, size 819 is deduced for the second dimension of the shape tensor after conversion.

TFL code

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

### Variable shape

When the target shape is not a constant, new logic must be generated to determine whether the shape tensor has a wildcard value of -1 present in one of its components. When present, this wildcard must be substituted with the appropriate value at runtime. The substituting value is computed as the total size of the input tensor divided by each value

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> {

  // 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 = arith.constant dense<-1> : tensor<i32>
  %c1 = arith.constant 1 : index
  %c0 = arith.constant 0 : index
  %1 = arith.constant dense<1> : 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>
}
```


