`CallOpInterface` Semantics + Verification around Value Forwarding

The CallOpInterface has an interface method to distinguish between operands that are passed to the callee (“forwarded operands”) and other operands:

    InterfaceMethod<[{
        Returns the operands within this call that are used as arguments to the
        callee.
      }],
      "::mlir::Operation::operand_range", "getArgOperands"
    >,

First observation: There is no corresponding interface method for results.

Now on to verification. The CallOpInterface does not verify that:

  1. The number of forwarded operands matches the number of arguments of the callee.
  2. The types of the forwarded operands match the argument types of the callee.

For (2) there is precedent in other interfaces (e.g. BranchOpInterface::areTypesCompatible). Existing transformations for CallOpInterface either bail on a type mismatch or build a conversion (e.g. DialectInlinerInterface::materializeCallConversion).

Case (1) seems a bit fishy to me. The absence of this verifier check suggests that the number of caller operands and callee arguments may not necessarily match. Then why do we have getArgOperands in the first place? Even worse: Even if the count matches, can we safely assume that operand i is passed as argument i? (Probably yes, but it’s not documented anywhere.) Maybe the semantics of the concrete call op are such that the callee receives the forwarded values in inverse order. In such a case, the inliner implementation would be incorrect.

(Note: Concrete ops like func.func perform more verification, but many transformations and analyses operate on the interface.)

What I would suggest:

  1. Add a getProducedResults (or getForwardedResults) interface method to CallOpInterface, so that the interface can distinguish between forwarded and produced results. (This will make result handling symmetric to operand handling.)
  2. Verify that the number of forwarded operands (as per getArgOperands) matches the number arguments of the callee.
  3. Verify that the number of forwarded results (as per getForwardedResults) matches the number of results of the callee.
  4. Document that the forwarded operands are in a 1:1 relationship with the callee arguments. I.e. i-th forwarded operand corresponds to i-th argument. (However, the type can be different.) Same for results.
  5. Optional: Add a new interface method CallOpInterface::areTypesCompatible with a default implementation of return true.

Any thoughts?

We also have precedent with RegionBranchOpInterface, it would be great to align all these to have similar restrictions.

One weird edge case is variadic functions, which we don’t model natively. A call to a variadic function may have more arguments than the function signature, but they are not assigned to an entry block value in the function. LLVM dialect will have to support this somehow and we need to double-check that it fits the interface model. Not necessarily relaxing the interface, we may also tighten the modeling by having some sort of “argument pack” concept in the LLVM dialect.

The handling of variadic operands was also inconsistent across CallOpInterface and CallableOpInterface: the former included them, the latter did not.

I decided to exclude variadic operands from the “forwarded operands” in CallOpInterface and classify them as “consumed operands”. The LLVM dialect has special operations (llvm.intr.vastart) to access variadic operands. They are not forwarded like regular operands using block arguments.

PR here: [mlir][Interfaces] `CallOpInterface`: Model forwarded result + improve verification by matthias-springer · Pull Request #214724 · llvm/llvm-project · GitHub