Exposing LLVM ops to the MLIR type system

Thanks for your replies.

It seems that for ops that are meant to remain low-level such as the llvm.inline.asm, just using the LLVM form with casts is fine so let’s isolate that class of ops that do not need special MLIR behavior (i.e. OpInterface or special verification (e.g. on types).

There remain at least 2 classes of ops I can see:

  1. ops that need to be understood by other parts of the codegen stack (e.g. vscale)
  2. ops that benefit from better semantics at the MLIR-level than just MLIR type or attribute -> LLVMType

Generally, I am skeptical that we can define and use the op “at a distance” via only defining the LLVM instruction operating on LLVM types + extra casts. It seems to invite immediate issues with at least OpInterfaces: what do you attach it on, does it need to “traverse the type system”?

One thing I am wondering is whether we can have a single ODS op in the LLVM dialect that can support both MLIR-y ODS and LLVM-y ODS. To make it concrete, does it seem reasonable to turn these 2 ops:

def LLVM_vector_scale :
  LLVMArmSVE_NonSVEIntrUnaryOverloadedOp<"vscale">;

def VScaleOp : SVE_Op<"vscale", [NoSideEffect, VectorParametricTileOpInterface]> {
  ...
  let arguments = (ins);
  let results = (outs Index:$res);
  let assemblyFormat =
}

into a single op that would have both the MLIR and LLVM specification by literally merging the 2 together.
This would only be for types/attributes that convert trivially (I would put index in that box: given a target the index is determined and the conversion can do the right thing) so that conversion from MLIR form to LLVM form can be autogenerated. Such a mechanism could already carry ops that are almost 1-1 and that we don’t want to leak into other dialects. The price/generalization effort does not seem too steep.

I’ll assume the above is reasonable, now how about this case: can we just merge these 2 ops into a single op?

// pasted for context
//class LLVMNeon_IntrBinaryOverloadedOp<string mnemonic, list<OpTrait> traits = []> :
//  LLVM_IntrOpBase</*Dialect dialect=*/LLVMNeon_Dialect,
//                  /*string opName=*/mnemonic,
//                  /*string enumName=*/"aarch64_neon_" # !subst(".", "_", mnemonic),
//                  /*list<int> overloadedResults=*/[0],
//                  /*list<int> overloadedOperands=*/[], // defined by result overload
//                  /*list<OpTrait> traits=*/traits,
//                  /*int numResults=*/1>;
//*/

def LLVM_aarch64_neon_smull :
  LLVMNeon_IntrBinaryOverloadedOp<"smull">, Arguments<(ins LLVM_Type, LLVM_Type)>;

def SMullOp : Neon_Op<"smull", [NoSideEffect,
  AllTypesMatch<["a", "b"]>,
  TypesMatchWith<
    "res has same vector shape and element bitwidth scaled by 2 as a",
    "a", "res", "$_self.cast<VectorType>().scaleElementBitwidth(2)">]> {
  let summary = "smull roundscale op";
  let description = [{...}];
  // Supports either:
  //   (vector<8xi8>, vector<8xi8>) -> (vector<8xi16>)
  //   (vector<4xi16>, vector<4xi16>) -> (vector<4xi32>)
  //   (vector<2xi32>, vector<2xi32>) -> (vector<2xi64>)
  let arguments = (ins VectorOfLengthAndType<[8, 4, 2], [I8, I16, I32]>:$a,
                       VectorOfLengthAndType<[8, 4, 2], [I8, I16, I32]>:$b);
  let results = (outs VectorOfLengthAndType<[8, 4, 2], [I16, I32, I64]>:$res);
  let assemblyFormat =
    "$a `,` $b attr-dict `:` type($a) `to` type($res)";
}

If the answer is no and we really need 2 ops then we might as well also have them in 2 dialects.

If it seems reasonable to have a single op with all these attributes then the problem of auto-generating kicks in: the LLVM_aarch64_neon_smull can be pretty easily generated, but not the SMullOp part.
We can think of an extra level of Tablegen indirection so that we can specify these things better but it seems we would still have to specify quite a few things manually. At which point is it still worth it vs not ?

To sum up, in the 3 examples above, I’d lean towards:

  1. inline_asm, just use casts
  2. vscale-like 1-1 ops with need for OpInterfaces, extend the tablegen infra
  3. more advanced use cases: don’t see a clear way to achieve it and I’m really not willing trade the nice MLIR type error messages for runtime assertion errors :stuck_out_tongue: