[RFC][Tablegen] Explicit Type Constraints for Overloaded LLVM Intrinsics

LLVM’s current model for overloaded intrinsics use broad type categories such as LLVMAnyType and LLVMMatchType. These categories accept almost any type, which makes intrinsics flexible, but opens up a few practical limitations:

  1. Late & Per-Intrinsic Basis Error Handling: Type validation occurs only during lowering on a per-intrinsic basis and cannot express target-specific type restrictions. As a result, LLVM may accept syntactically valid IR that later fails during lowering, causing late and hard-to-recognize errors (mostly in the backends).
  2. Uninformative Diagnostics: When a type mismatch does occur, the emitted diagnostic is frequently generic, offering little guidance on what the valid type set actually is.

This RFC proposes a fine-grained type constraint mechanism for overloaded LLVM intrinsics, by allowing them to explicitly declare permitted types, enabling IR-level verification of these constraints. To support this, a constrained type class is added:

AnyTypeOf<[list of LLVMType]>: Restricts types to a specified subset.

Features

  1. Backward Compatibility: Existing overloaded intrinsics workflow remains unchanged.
  2. No IR or .ll File Impact: Introducing constrained type class does not alter existing LLVM IR. Intrinsics that currently rely on LLVMAnyType continue to parse and behave as before, and the AnyTypeOf constraint affects only verification, not the structure or semantics of the emitted IR.
  3. Error Detection & Handling: Type violations are caught during IR verification as part of LLVM’s existing Verifier pipeline with precise error messages.
  4. Precise Type Control in TableGen: Overloaded intrinsic definitions can now specify exact allowed type subsets directly in TableGen. This provides a clear, declarative way to express type restrictions for overloaded intrinsics.

Examples

def foo.bar.example1 : Intrinsic<[llvm_i32_ty],
[AnyTypeOf<[llvm_i16_ty, llvm_i32_ty]>],
[IntrNoMem]>;

def foo.bar.example2 : Intrinsic<[AnyTypeOf<[llvm_float_ty, llvm_double_ty]>],
[LLVMMatchType<0>],
[IntrNoMem]>;

def foo.bar.example3 : Intrinsic<[llvm_i16_ty],
[AnyTypeOf<[llvm_ptr_ty, llvm_shared_ptr_ty]>, llvm_i32_ty],
[IntrNoMem]>;

def foo.bar.example4 : Intrinsic<[llvm_i16_ty],
[llvm_float_ty, AnyTypeOf<[llvm_i16_ty, llvm_v4i32_ty, llvm_v4f32_ty]>, AnyTypeOf<[llvm_i32_ty, llvm_float_ty]>],
[IntrNoMem]>;

The following error is emitted when an invalid type is provided:

“Intrinsic declaration ‘foo.bar.example1’ violates type constraint: Parameter 0 type ‘i8’ not in allowed types”

A prototype implementation has been developed including:

  • TableGen support for specifying type constraints
  • IR-level verification logic integrated into the LLVM Verifier
  • Detailed diagnostic reporting for constraint violations

For those interested in the implementation details, a draft PR is available here: [LLVM-Tablegen] Explicit Type Constraints for Overloaded LLVM Intrinsics by DharuniRAcharya · Pull Request #172442 · llvm/llvm-project.

3 Likes

This would be very nice. I would also like if particular type signatures (and the intrinsics themselves) depended on a subtarget feature

2 Likes

Adding @jurahul @Artem-B also for thoughts.

Type-constrained intrinsics would be very useful.

Proposal as is is fine with me, but after looking at the examples, I think that we may want or need to make the constraint system more flexible. It can be done later and should not block the work on implementation of the current proposal.

Proposed solution gives us better control over the cartesian product of types valid for the intrinsic. However, we still miss the ability to handle “holes” in the parameter space of possible intrinsic overloads. We can carve parameter space into contiguous sub-regions, but I think ultimately we may need to be able to specify a list of predicates parametrized by the types of the specific intrinsic specialization. E.g.

def foo.bar.example4 : Intrinsic<[llvm_i16_ty],
[llvm_float_ty, 
 AnyTypeOf<[llvm_i16_ty, llvm_v4i32_ty, llvm_v4f32_ty]>, 
 AnyTypeOf<[llvm_i32_ty, llvm_float_ty]>],
[IntrNoMem],
// f32 foo.bar.example4.v4i32.f32 is not supported.
[Except<llvm_float_ty, llvm_v4i32_ty, llvm_float_ty>]
>;

The syntax above is a strawman to illustrate what I want. I’m not sure what exactly it’s going to look like in the end. I think the closest functionality we have now is LLVMMatchType<0> , only instead of deriving the type, we want something we can use to produce the argument type to be used in specializing the predicate.

In addition to that we may want to have a set of helpers handling common use cases. E.g. the fictional Except predicate above which would presumably return false if intrinsic types match specified types. Or we could make a more flexible variant which would take a list of argument types (produced by the LLVMArgType<N>) and a list of lists of types to match, enumerating the single combinations or contiguous sub-blocks of parameter space to exclude.

Thanks @Artem-B for your thoughts!
I will think through your suggestions (Except predicate etc.) and update here.

+1 for supporting type constraints that are more restricted than what is currently supported. As @Artem-B suggested, we can look into restricting the combinations of overloaded types (for intrinsics with > 1 overloaded type) as a next step, either with list of allowed combinations or excluded combinations (or maybe having a utility that can build such lists).

One thought I have w.r.t the future direction here is whether we should consider borrowing a page from MLIR and extending intrinsics to have an optional verifier function that can do all sorts of complex verification that may be too complex to describe declaratively in the .td file. As an example, if there is some interaction between overload types and allowed values of ImmArg flags supplied, it may be too complex to add support for expressing such constraints declaratively in TableGen. Delegating such checks to C++ code may be the right tradeoff for such cases. If we are considering this, then the set of allowed overload combinations != full cartesian product can be considered as a complex constraint that can be delegated to the C++ verification code instead of adding additional TableGen infra.

If we do implement predicate-based constraints on supported type combinations, then we’d get proposed verifier functions available for free, as we can already implement arbitrarily complex predicates as calls to C++ functions.

I guess your point is that even today we can extend LLVM IR verification with any intrinsic specific checks like these one and we don’t necessarily need any TableGen support for associating verification functions with intrinsics (unlike MLIR which has an open operation set). In which case, yeah, I agree that the infra to specify the subset of allowed combinations would help to avoid extending the IR verifier with such checks for each new intrinsis.

While we’re considering improvements for overloaded intrinsics, there’s another aspect we may want to address here.

One of the reasons we need this flexibility for types is the explosion of instruction variants in PTX instruction set we want to generate in NVPTX back-end.

However, type combinations are not the only factors that drive that. Those instructions tend to have other parameters that we end up enumerating and encoding in the intrinsic name.

While the current proposal helps with intrinsics overloaded by types, we still often need to generate nearly identical copies that differ in, let’s call it “feature”. E.g. [NVPTX] Add support for barrier.cta.red.* instructions by AlexMaclean · Pull Request #172541 · llvm/llvm-project · GitHub which adds a handful of instruction variants that differ by the operation kind, but not the types.

Right now we’re instantiating them via nested for loops. It works, but it’s an ever present pattern that begs for a more convenient way to handle it.

What if we could piggy-back on intrinsic overloading mechanisms to handle creation of families of intrinsics based on cartesian-product not only types but also user-provided “features”.

E.g I want something like this:

intr_reduce_{op},{typeA}
where:
  {op} = ["add", "mul"],
  {typeA} ["float", "half"],

I do not have a good idea whether it’s feasible (maybe) or useful(probably) in practice. Just food for thought.

1 Like

We do have that ability for the tablegen constructs where we can use predicates. If/when we can apply predicates to the overloaded intrinsic types, then we’ll be able to use existing mechanisms for making our predicates arbitrarily complex, if necessary.

I like the idea. I believe, that it also would be nice to add support something like the following

def int_foo : Intrinsic<[AnyVectorOf<[llvm_i32_ty, llvm_i16_ty]>], // scalar or any vector of i32 or i16
[MatchVectorLength<0, llvm_float_ty>>, // vector of float, same length as result
 MatchVectorLength<0, AnyTypeOf<[llvm_float_ty, llvm_double_ty]>> // vector of float or double, same length as result
]>;