[mlir][EmitC] Support pointer-based memrefs in load/store lowering (#186828)
## Problem
In the MemRef → EmitC conversion, `memref.load` and `memref.store`
assume that the converted memref operand is an `emitc.array`, as defined
by the type conversion in `populateMemRefToEmitCTypeConversion`.
However, `memref.alloc` is lowered to a `malloc` call returning
`emitc.ptr`. When such values are used by `memref.load` or
`memref.store`, the conversion framework inserts a bridging
`builtin.unrealized_conversion_cast` from `emitc.ptr` to `emitc.array`.
These casts have no EmitC representation and therefore remain in the IR
after conversion, preventing valid C/C++ emission.
## Solution
Extend the `memref.load` and `memref.store` conversions to handle
pointer-backed buffers.
If the memref operand is defined by an `UnrealizedConversionCastOp`
whose input is an `emitc.ptr`, the cast is stripped and the underlying
pointer operand is used directly. Since pointer subscripting in EmitC is
one-dimensional, the multi-dimensional memref indices are converted to a
row-major linear index (matching the default memref layout) using the
original `MemRefType` shape before emitting `emitc.subscript`.
The existing array-based lowering path remains unchanged.
This patch intentionally does ***not*** modify the MemRef → EmitC type
conversion rule (`memref → emitc.array`). Instead, the mismatch
introduced by `memref.alloc` returning a pointer is handled locally in
the `LoadOp` and `StoreOp` conversions.
## Example 1: Single-dimensional store
### Input
```mlir
func.func @alloc_store(%arg0: i32, %i: index) {
%alloc = memref.alloc() : memref<999xi32>
memref.store %arg0, %alloc[%i] : memref<999xi32>
return
}
```
### Current lowering
```mlir
// AllocOp conversion unchanged -> excluded for brevity
%5 = builtin.unrealized_conversion_cast %4 : !emitc.ptr<i32> to !emitc.array<999xi32>
%6 = subscript %5[%arg1]
assign %arg0 to %6 : <i32>
```
The `unrealized_conversion_cast` remains in the IR.
### Lowering after this patch
```mlir
%5 = subscript %4[%arg1] : (!emitc.ptr<i32>, !emitc.size_t) -> !emitc.lvalue<i32>
assign %arg0 : i32 to %5 : <i32>
```
The cast is eliminated and pointer subscripting is used directly.
## Example 2: Multi-dimensional store
### Input
```mlir
func.func @memref_alloc_store(%v : f32, %i : index, %j : index) {
%alloc = memref.alloc() : memref<4x8xf32>
memref.store %v, %alloc[%i, %j] : memref<4x8xf32>
return
}
```
### Current lowering
```mlir
// AllocOp conversion unchanged -> excluded for brevity
%5 = builtin.unrealized_conversion_cast %4 : !emitc.ptr<f32> to !emitc.array<4x8xf32>
%6 = subscript %5[%arg1, %arg2] : (!emitc.array<4x8xf32>, !emitc.size_t, !emitc.size_t) -> !emitc.lvalue<f32>
assign %arg0 : f32 to %6 : <f32>
```
### Lowering after this patch
```mlir
%5 = "emitc.constant"() <{value = 8 : index}> : () -> !emitc.size_t
%6 = mul %arg1, %5 : (!emitc.size_t, !emitc.size_t) -> !emitc.size_t
%7 = add %6, %arg2 : (!emitc.size_t, !emitc.size_t) -> !emitc.size_t
%8 = subscript %4[%7] : (!emitc.ptr<f32>, !emitc.size_t) -> !emitc.lvalue<f32>
assign %arg0 : f32 to %8 : <f32>
```
The multi-dimensional indices are converted into a linear row-major
index before pointer subscripting.
Assisted-by: ChatGPT (refine implementation + tests). I reviewed all
code and tests before submission.