$ mlir-opt 02_bufferization.mlir --one-shot-bufferize="bufferize-function-boundaries function-boundary-type-conversion=identity-layout-map" | grep -E 'func.func|memref.copy|memref.alloc'
func.func @inplace(%arg0: memref<8xf32>) -> memref<8xf32> {
func.func @needs_copy(%arg0: memref<8xf32>) -> (memref<8xf32>, memref<8xf32>) {
%alloc = memref.alloc() alignment = 64 : memref<8xf32>
memref.copy %arg0, %alloc : memref<8xf32> to memref<8xf32>
func.func @fresh_dest(%arg0: memref<8xf32>) -> (memref<8xf32>, memref<8xf32>) {
%alloc = memref.alloc() alignment = 64 : memref<8xf32>(A) @inplace: no alloc, no copy. The input's buffer is updated directly. (B) @needs_copy: alloc + COPY, because %t is returned after being used as a destination; writing in place would change the second result. (C) @fresh_dest: alloc, no copy. `tensor.empty` says "I don't care about the contents", so the fresh buffer needs no initialisation. Same semantics as (B), one full memory pass cheaper.