Add a new LLVM IR intrinsic in LLVM 22. Covers TableGen definition, header regeneration, IRBuilder call-site usage, verifier rules, and lit testing.
67
80%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./tiles/tessl-llvm/skills/add-intrinsic/SKILL.mdUse this skill when the user wants to add a new first-class LLVM IR intrinsic — a built-in function with a well-known name (llvm.*) that the optimizer and backends can reason about specially.
If the user only needs to call an existing intrinsic from a frontend, skip to Step 4.
| Category | TableGen file | Example |
|---|---|---|
| Generic (target-independent) | include/llvm/IR/Intrinsics.td | llvm.memcpy, llvm.expect |
| Target-specific | include/llvm/IR/Intrinsics<Target>.td | llvm.x86.*, llvm.aarch64.* |
| In-tree experimental | include/llvm/IR/IntrinsicsExperimental.td | Prefix: llvm.experimental.* |
For a new language or target, use target-specific (IntrinsicsMyTarget.td) or prefix with llvm.experimental. during development.
Open the appropriate .td file and add:
// In include/llvm/IR/Intrinsics.td (or Intrinsics<Target>.td)
def int_my_intrinsic : DefaultAttrsIntrinsic<
[llvm_i32_ty], // Return types (list; use [] for void)
[llvm_i32_ty, llvm_i32_ty], // Parameter types
[IntrNoMem, IntrWillReturn] // Attributes / properties
>;The function name is derived from the def name:
int_my_intrinsic → llvm.my.intrinsicint_x86_my_op → llvm.x86.my.op| Property | Meaning |
|---|---|
IntrNoMem | Does not access memory |
IntrReadMem | Only reads memory |
IntrWriteMem | Only writes memory |
IntrArgMemOnly | Only accesses memory via pointer args |
IntrWillReturn | Always returns (no infinite loops/throws) |
IntrSpeculatable | Safe to speculate (no side effects) |
IntrHasSideEffects | Has side effects; cannot be removed |
Commutative | First two args commute |
ImmArg<ArgIndex<N>> | Arg N must be an immediate constant |
ReadOnly<ArgIndex<N>> | Pointer arg N is read-only |
llvm_i1_ty llvm_i8_ty llvm_i16_ty llvm_i32_ty llvm_i64_ty
llvm_half_ty llvm_float_ty llvm_double_ty
llvm_ptr_ty // opaque pointer (LLVM 22 default)
llvm_anyint_ty llvm_anyfloat_ty // overloaded
llvm_anyvector_ty
llvm_void_tyFor an overloaded intrinsic (type varies per call):
def int_my_abs : DefaultAttrsIntrinsic<
[llvm_anyint_ty],
[LLVMMatchType<0>], // param type must match return type
[IntrNoMem, IntrWillReturn]
>;LLVM generates IntrinsicEnums.inc and IntrinsicImpl.inc from the .td files.
During development (rebuild LLVM):
cmake --build build --target intrinsics_gen
# or just rebuild:
cmake --build build -j$(nproc)The generated files end up in:
build/include/llvm/IR/IntrinsicEnums.inc
build/include/llvm/IR/IntrinsicImpl.incYou do not hand-edit these files — they are always regenerated.
After regeneration, the new intrinsic is accessible via:
llvm::Intrinsic::my_intrinsic // enum valueFor a target-specific intrinsic to generate real machine code:
Add a GINodeEquiv entry in the target's .td file and a GICombineRule or
GISelCombinerPass entry, or handle it in <Target>LegalizerInfo.cpp:
// In <Target>LegalizerInfo.cpp, legalizeIntrinsic():
case Intrinsic::my_intrinsic: {
MIRBuilder.buildInstr(MyTarget::MY_OPCODE)
.addDef(MI.getOperand(0).getReg())
.addUse(MI.getOperand(2).getReg());
MI.eraseFromParent();
return true;
}Override LowerINTRINSIC_WO_CHAIN or LowerINTRINSIC_W_CHAIN in
<Target>ISelLowering.cpp:
SDValue <Target>TargetLowering::LowerINTRINSIC_WO_CHAIN(
SDValue Op, SelectionDAG &DAG) const {
unsigned IntNo = Op.getConstantOperandVal(0);
switch (IntNo) {
case Intrinsic::my_intrinsic:
return DAG.getNode(MyTargetISD::MY_NODE, SDLoc(Op), Op.getValueType(),
Op.getOperand(1), Op.getOperand(2));
}
return SDValue();
}#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/IntrinsicInst.h"
#include "llvm/IR/Intrinsics.h"
// Inside a function body:
IRBuilder<> Builder(InsertBB, InsertBB->end());
// Option A: IRBuilder::CreateIntrinsic (LLVM 22 preferred API)
Value *Result = Builder.CreateIntrinsic(
Intrinsic::my_intrinsic,
{Builder.getInt32Ty()}, // type arguments for overloaded intrinsics; {} if not overloaded
{ArgA, ArgB} // call arguments
);
// Option B: Manual — getOrInsertFunction then CreateCall
Function *F = Intrinsic::getOrInsertDeclaration(M, Intrinsic::my_intrinsic,
{Builder.getInt32Ty()});
Value *Result2 = Builder.CreateCall(F, {ArgA, ArgB});In LLVM 22,
Intrinsic::getDeclaration()is removed — useIntrinsic::getOrInsertDeclarationinstead.
If the intrinsic has non-trivial constraints (e.g., arg 2 must be a constant,
pointer alignment requirements), add a check in lib/IR/Verifier.cpp:
case Intrinsic::my_intrinsic: {
Assert(isa<ConstantInt>(Call.getArgOperand(1)),
"llvm.my.intrinsic: second argument must be a constant integer", &Call);
break;
}Path: test/CodeGen/<Target>/my-intrinsic.ll (for codegen tests)
or test/Transforms/<Category>/my-intrinsic.ll (for IR-level tests)
; RUN: opt -passes=verify -S %s | FileCheck %s
; RUN: opt -passes=instcombine -S %s | FileCheck %s --check-prefix=OPT
declare i32 @llvm.my.intrinsic(i32, i32)
define i32 @test(i32 %a, i32 %b) {
; CHECK-LABEL: @test
%r = call i32 @llvm.my.intrinsic(i32 %a, i32 %b)
; CHECK: call i32 @llvm.my.intrinsic
ret i32 %r
}
; Test constant folding (if applicable)
define i32 @test_const() {
; OPT-LABEL: @test_const
%r = call i32 @llvm.my.intrinsic(i32 3, i32 4)
; OPT: ret i32 7
ret i32 %r
}Run:
llvm-lit test/CodeGen/<Target>/my-intrinsic.ll -vIf the intrinsic has constant-fold / simplification rules, add them in
lib/Transforms/InstCombine/InstCombineCalls.cpp:
case Intrinsic::my_intrinsic: {
// Fold llvm.my.intrinsic(c1, c2) to constant when both args are constants.
if (auto *C1 = dyn_cast<ConstantInt>(II->getArgOperand(0)))
if (auto *C2 = dyn_cast<ConstantInt>(II->getArgOperand(1)))
return IC.replaceInstUsesWith(
*II, ConstantInt::get(II->getType(),
C1->getValue() + C2->getValue()));
break;
}Intrinsic::getDeclaration() — it is removed in LLVM 22; use getOrInsertDeclaration() or getDeclarationIfExists().intrinsics_gen after editing .td files.i8*) in intrinsic signatures — LLVM 22 uses opaque pointers (ptr) everywhere.declare in your .td and also hand-write a declare in IR — the verifier will reject duplicate declarations with mismatched types.IntrNoMem, IntrReadMem, etc.) — wrong attributes block valid optimizations or allow incorrect ones.-passes=verify before testing with optimization passes.a251bde
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.