C representation of complex types?

For testing purposes I’d like to call MLIR functions that take as input and produce as output variables of type complex<f32>, and print these results. I discovered that these values are lowered directly to %xmm registers, meaning that you can’t see them from the C side as simple struct { float re; float im; }. So the question would be, how do I see C variables from C? If it’s not possible, does that mean that any scope where complex variables appear must be dealt with in native MLIR? Can’t I even have opaque references to complex objects, even if they are only manipulated by MLIR-based code?
Dumitru

MLIR doesn’t normally go to the assembly levels, so I cannot understand where does %xmm come from. Our lowering to LLVM models values of complex type precisely as you describe, converting

func @foo(%arg0: complex<f32>) -> complex<f32> {
  return %arg0: complex<f32>
}

into

define { float, float } @foo({ float, float } %0) {
  ret { float, float } %0
}

this is exactly the same signature as clang produces for _Complex float foo(_Complex float x) { return x; } and should be ABI-compatible at this level. LLVM is then free to run SRoA and register allocation depending on the target architecture.

There were some ABI issues for the Complex type in FIR.

See the discussion in the comments starting from the link below.

And the target specific handling in X86_64 and AArch64 in the links below.

CC: @schweitz

If the semantics of complex should be that of {float,float}, then there’s a bug. Consider the following mlir function:

func @complex_swap(%i:complex<f32>)->(complex<f32>) {
  %re = std.re %i : complex<f32>
  %im = std.im %i : complex<f32>
  %o = std.create_complex %im, %re : complex<f32>
  return %o : complex<f32>
}

If you try to interface if with the following C code, the results are incorrect (the output values are in my case 0 instead of what they should have been, so the ABI is incorrectly implemented):

typedef struct {
  float re;
  float im;
} complex ;
extern complex complex_swap(complex) ;
1 Like

This is quite interesting. We can consider moving the lowering alternative into core and having a configuration option for this, but this would require a separate discussion.

Can you show us the LLVM IR generated in both cases?

I can do better. Here are the 3 files needed to compile (MLIR source, C source, list of commands for compilation). The second printed value is incorrect (<0,0> instead of <3,4>).

func @complex_swap(%i:complex<f32>)->(complex<f32>) {
  %re = std.re %i : complex<f32>
  %im = std.im %i : complex<f32>
  %o = std.create_complex %im, %re : complex<f32>
  return %o : complex<f32>
}
#include <stdio.h>
#include <assert.h>

typedef struct {
  float re ;
  float im ;
} complex ;

complex complex_swap(complex);

void print_complex(complex c) {
  printf("<%f,%f>\n",c.re,c.im) ;
}
int main() {
  complex x ;
  x.re = 4 ;
  x.im = 3 ;
  complex y = complex_swap(x) ;
  print_complex(x) ;
  print_complex(y) ;
  return 0 ;
}
mlir-opt --convert-std-to-llvm complex.mlir -o=complex.llvm.mlir
mlir-translate --mlir-to-llvmir complex.llvm.mlir -o=complex.bc
llc complex.bc -o=complex.s
gcc -c complex.s
gcc -c try.c
gcc try.o complex.o -o try

I am not sure it is safe to rely on an equivalent to a struct passed by value: the ABI for this is very platform specific I believe.

From what I see at the moment MLIR is emitting define { float, float } @complex_swap({ float, float } %0) { while clang emits (on my Linux X86_64): declare dso_local <2 x float> @_Z12complex_swap7complex(<2 x float>) (Compiler Explorer)

Note that on RISC-V clang is using a i64: declarei64 @_Z12complex_swap7complex(i64) (Compiler Explorer).

And ARM64 (Compiler Explorer):

%struct.complex = type { float, float }
declare %struct.complex @_Z12complex_swap7complex([2 x float])

Yep, the reason why I asked for LLVM IR is because I suspected something along these lines. MLIR does emit the LLVM IR it claims it does. Generally, I would claim that MLIR’s contract is with LLVM IR, rather than any particular C ABI. This is a long-standing issue that I am interested in resolving without bringing a dependency on clang into MLIR.

Mehdi is correct. The LLVM-IR for the complex data type is highly specific to the target. To support compilation where it is possible that build != host != target (this is the normal case for LLVM), one really has to have a target specified to know the calling conventions, etc.

Ok, so the answer to my initial question is that it is generally not possible to see MLIR variables of complex type from C (not in a platform-independent way).

Thanks!

We can consider “unpacking” them into two components at the function boundary, the same way we do with memref descriptors. But that won’t help with calling C functions accepting a _Complex without additional glue code.

Thanks. This is what I also did, using an MLIR function. The single problem is that a priori I can’t even see opaque references to such objects from C (which precludes having a top-level written in C).

Something along the lines of

func @forward_call_to_mlir(%re: f32, %im: f32) {
  %0 = create_complex %re, %im : complex<f32>
  call @f(%0) : (complex<f32>) -> ()
  return
}

func @forward_call_to_c(%complex: complex<f32>) {
  %0 = re %complex : complex<f32>
  %1 = im %complex : complex<f32>
  call @f(%0, %1) : (f32, f32) -> ()
  return
}

with the C interfacing being void f(float, float) should work both ways. These are annoying to write by hand because each function needs to be wrapped this way, hence the idea of generating these wrappers.

Yes, but it’s more complicated, because you would need a full wrapper (and the associated conversions complex<->(float,float) for every function you call from C and uses complex on the input or on the output. Again, the sensible solution is to write the top-level in MLIR.

That’s why I repeatedly proposed this: