Fallible op builders and return type inference

Operations that implement the InferTypeOpInterface gain the nice inferResultTypes method and along with it builder methods that don’t require the result types to be passed in. Upon inference failure, the builder simply asserts/aborts. This is nice in general, but things like parsers consuming user input would sometimes like to gracefully error out if inference fails.

The CIRCT project encounters this for example when parsing FIRRTL files: failure to infer a type is often a user error which the parser would like to report and then gracefully exit. To use the inferResultTypes mechanism, the parser in a sense has to build up +/- the entire operation state to pass into the method, to catch the error before the builder can abort:

// Extracted from the code that parses "x.y" into a SubfieldOp
ParseResult parsePostFixFieldId(Value &indexee, ...) {
  auto fieldName = parseFieldName();
  auto fieldNameAttr = builder.getStringAttr(fieldName);
  auto attrs = builder.getDictionaryAttr({
    std::make_pair(builder.getIdentifier("fieldName"), fieldNameAttr)
  });
  SmallVector<Type, 1> resultTypes;
  if (failed(SubfieldOp::inferReturnTypes(
      ctx, loc, {indexee}, attrs, {}, resultTypes)))
    return failure();

  auto op = builder.create<SubfieldOp>(...);
  ...
}

For more complex operations, the redundancy with the builder function itself gets more pronounced. For reference, the builder which infers the result type looks as follows:

void SubfieldOp::build(OpBuilder &odsBuilder, OperationState &odsState,
    Value input, StringAttr fieldName) {
  odsState.addOperands(input);
  odsState.addAttribute("fieldName", fieldName);

  SmallVector<Type, 2> inferredReturnTypes;
  if (succeeded(SubfieldOp::inferReturnTypes(odsBuilder.getContext(),
                odsState.location, odsState.operands,
                odsState.attributes.getDictionary(odsState.getContext()),
                /*regions=*/{}, inferredReturnTypes)))
    odsState.addTypes(inferredReturnTypes);
  else
    llvm::report_fatal_error("Failed to infer result type(s).");
}

I was wondering if it would make sense to add a fallible builder variant for the type-inferred case, which user-facing code like a parser could leverage to benignly error out. Something like the following:

LogicalResult SubfieldOp::tryBuild(OpBuilder &odsBuilder,
    OperationState &odsState, Value input, StringAttr fieldName) {
  odsState.addOperands(input);
  odsState.addAttribute("fieldName", fieldName);

  SmallVector<Type, 2> inferredReturnTypes;
  if (succeeded(SubfieldOp::inferReturnTypes(odsBuilder.getContext(),
                odsState.location, odsState.operands,
                odsState.attributes.getDictionary(odsState.getContext()),
                /*regions=*/{}, inferredReturnTypes))) {
    odsState.addTypes(inferredReturnTypes);
    return success();
  }
  return failure();
}
void SubfieldOp::build(OpBuilder &odsBuilder, OperationState &odsState,
    Value input, StringAttr fieldName) {
  if (failed(tryBuild(odsBuilder, odsState, input, fieldName)))
    llvm::report_fatal_error("Failed to infer result type(s).");
}

The op builder could then gain a fallible variant of create:

class OpBuilder {
  template <typename OpTy, typename... Args>
  OpTy tryCreate(Location location, Args &&... args) {
    ...
    if (failed(OpTy::tryBuild(*this, state, std::forward<Args>(args)...)))
      return {};
    auto *op = createOperation(state);
    ...
  }
};

Parser code could then try to build an op, and upon type inferrence failure just error out gracefully:

ParseResult parsePostFixFieldId(Value &indexee, ...) {
  auto fieldName = parseFieldName();
  auto op = builder.tryCreate<SubfieldOp>(loc, indexee, fieldName);
  if (!op)
    return failure();
  ...
}

Would something like this be of value to MLIR in general?