Blogging site
Getting started with TableGen via a tutorial. For this tutorial to work, you will need to clone the LLVM project.
This tutorial shows how to plumb a target-prefixed LLVM IR intrinsic through
TableGen into the X86 backend, expanding a pseudo to a real instruction.
It is controlled by a CMake option -DLLVM_ENABLE_X86_MYDOUBLE
.
The intrinsic llvm.x86.mydouble.*
doubles its input integer value (x + x
).
The following files are affected: • Intrinsic: include/llvm/IR/IntrinsicsX86.td (int_x86_mydouble) • TableGen map: lib/Target/X86/X86MyDouble.td • CMake toggle: lib/Target/X86/X86MyDoubleToggle.td.in → X86MyDoubleToggle.td lib/Target/X86/X86MyDoubleConfig.h.in → X86MyDoubleConfig.h • Expansion: lib/Target/X86/X86ExpandPseudo.cpp (ExpandMI cases) • Tests: test/CodeGen/X86/mydouble.ll (REQUIRES: x86-mydouble)
llvm/
├─ include/llvm/IR/IntrinsicsX86.td # +1 intrinsic (edit)
├─ lib/Target/X86/
│ ├─ X86.td # +1 include (edit)
│ ├─ X86MyDouble.td # NEW (your td)
│ ├─ X86ExpandPseudo.cpp # +2 switch cases (edit)
│ ├─ X86MyDoubleToggle.td.in # NEW (CMake-configured include)
│ ├─ X86MyDoubleConfig.h.in # NEW (C++ macro for guards)
│ └─ CMakeLists.txt # + configure_file & include dir (edit)
└─ test
└─ lit.site.cfg.py.in # NEW (lit test)
└─ test/CodeGen/X86/
└─ mydouble.ll # NEW (lit test)
CAUTION: Works with current LLVM layout (18-20+); might need adjustment for LLVM versions before 18 (e.g., < 17).
Edit include/llvm/IR/IntrinsicsX86.td
and add:
let TargetPrefix = "x86" in {
def int_x86_mydouble :
Intrinsic<[llvm_anyint_ty], [LLVMMatchType<0>], [IntrNoMem]>;
}
This generates overloads such as:
declare i32 @llvm.x86.mydouble.i32(i32)
declare i64 @llvm.x86.mydouble.i64(i64)
Add lib/Target/X86/X86MyDouble.td
:
def MYDOUBLE32r : PseudoI<(outs GR32:$dst), (ins GR32:$src), []> {
let Constraints = "$dst = $src";
let hasSideEffects = 0;
let SchedRW = [WriteALU];
}
def MYDOUBLE64r : PseudoI<(outs GR64:$dst), (ins GR64:$src), []> {
let Constraints = "$dst = $src";
let hasSideEffects = 0;
let SchedRW = [WriteALU];
}
def : Pat<(i32 (int_x86_mydouble GR32:$s)), (MYDOUBLE32r GR32:$s)>;
def : Pat<(i64 (int_x86_mydouble GR64:$s)), (MYDOUBLE64r GR64:$s)>;
Create lib/Target/X86/X86MyDoubleToggle.td.in
:
@MYDOUBLE_TO_INCLUDE@
CMake will replace @MYDOUBLE_TO_INCLUDE@
with either include
"X86MyDouble.td"
or an empty string. Edit lib/Target/X86/X86.td
and add:
include "X86MyDoubleToggle.td"
Create the file lib/Target/X86/X86MyDoubleConfig.h.in
with the following contents:
/* generated by CMake */
#cmakedefine01 LLVM_ENABLE_X86_MYDOUBLE
Create lib/Target/X86/X86ExpandPseudo.cpp
, with the following contents:
#include "X86MyDoubleConfig.h"
#if LLVM_ENABLE_X86_MYDOUBLE
case X86::MYDOUBLE32r: {
Register R = MI.getOperand(0).getReg();
MachineBasicBlock &MBB = *MI.getParent();
BuildMI(MBB, MBBI, DL, (*TII).get(X86::ADD32rr), R).addReg(R).addReg(R);
MBBI = MBB.erase(MI);
return true;
}
case X86::MYDOUBLE64r: {
Register R = MI.getOperand(0).getReg();
MachineBasicBlock &MBB = *MI.getParent();
BuildMI(MBB, MBBI, DL, (*TII).get(X86::ADD64rr), R).addReg(R).addReg(R);
MBBI = MBB.erase(MI);
return true;
}
#endif
Edit lib/Target/X86/CMakeLists.txt
with the following contents:
option(LLVM_ENABLE_X86_MYDOUBLE "Enable x86 mydouble tutorial feature" ON)
if(LLVM_ENABLE_X86_MYDOUBLE)
set(MYDOUBLE_TO_INCLUDE "include \"X86MyDouble.td\"\n")
set(LLVM_ENABLE_X86_MYDOUBLE_ENABLED 1)
else()
set(MYDOUBLE_TO_INCLUDE "")
set(LLVM_ENABLE_X86_MYDOUBLE_ENABLED 0)
endif()
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/X86MyDoubleToggle.td.in
${CMAKE_CURRENT_BINARY_DIR}/X86MyDoubleToggle.td
@ONLY
)
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/X86MyDoubleConfig.h.in
${CMAKE_CURRENT_BINARY_DIR}/X86MyDoubleConfig.h
@ONLY
)
include_directories(${CMAKE_CURRENT_BINARY_DIR})
Edit llvm/test/lit.site.cfg.py.in
:
if @LLVM_ENABLE_X86_MYDOUBLE_ENABLED@:
config.available_features.add('x86-mydouble')
Add test/CodeGen/X86/2026-september-7-mydouble.ll
with the following contents:
The name of the file is based on the naming convention of existing files found
in the directory.
; REQUIRES: x86-registered-target, x86-mydouble
; RUN: llc -mtriple=x86_64-apple-darwin %s -o - | FileCheck %s
declare i32 @llvm.x86.mydouble.i32(i32)
declare i64 @llvm.x86.mydouble.i64(i64)
define i32 @twice32(i32 %a) {
%x = call i32 @llvm.x86.mydouble.i32(i32 %a)
ret i32 %x
}
define i64 @twice64(i64 %a) {
%x = call i64 @llvm.x86.mydouble.i64(i64 %a)
ret i64 %x
}
; CHECK-LABEL: _twice32:
; CHECK: addl %edi, %edi
; CHECK-NEXT: retq
; CHECK-LABEL: _twice64:
; CHECK: addq %rdi, %rdi
; CHECK-NEXT: retq
At this time, it is time to build the LLVM compiler which triggers the
compilation and build of llc
, llvm-tblgen
, FileCheck
etc.
Here’s how to configure the build on the command line:
cmake -S llvm -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DLLVM_TARGETS_TO_BUILD="X86;AArch64" \
-DLLVM_ENABLED_PROJECTS="llvm" \
-DLLVM_BUILD_UTILS=ON \
-DLLVM_INCLUDE_TESTS=ON \
-DLLVM_ENABLE_X86_MYDOUBLE=ON \
-DLLVM_ENABLE_X86_MYDOUBLE_ENABLED=1
ninja -C build llc llvm-tblgen
Once the compilation is done and you are satisfied with the test, you might wish to disable the compile and you can do it like this:
cmake -S llvm -B build -G Ninja \
...
-DLLVM_ENABLE_X86_MYDOUBLE_ENABLED=0 \
-DLLVM_ENABLE_X86_MYDOUBLE=OFF
ninja -C build llc llvm-tblgen
Now, the test file 2025-september-7-mydouble.ll
test is skipped since the cod
snippet in the file lit/...py
becomes:
if @LLVM_ENABLE_X86_MYDOUBLE_ENABLED@:
config.available_features.add('x86-mydouble')
# becomes the following
if 0:
config.available_features.add('x86-mydouble')
Rebuild the llc
and llvm-tblgen
components. The testing is going to be
manual, though we can use LLVM’s Machine IR Test (MIR) using the LLVM Integrated
Tester (lit).
./build/bin/llc -mtriple=x86_64-apple-darwin \
-print-before=asm-printer ../TableGenTutorial/mydouble.ll -o /dev/null \
| grep MYDOUBLE || echo "no pseudos ✅"
Generate the Darwin flavored x86 ASM on Apple Silicon:
./build/bin/llc -mtriple=x86_64-apple-darwin ../TableGenTutorial/mydouble.ll -o -
Run the test by using the recently built artefacts like llvm-lit
etc
./build/bin/llvm-lit -v llvm/test/CodeGen/X86/2025-september-7-mydouble.ll
You should expect the assembly with addl
or addq
instead of pseudos.
If you don’t need pseudos, you can simplify X86MyDouble.td
to map directly to
actual and real instructions:
def : Pat<(i32 (int_x86_mydouble GR32:$s)), (ADD32rr GR32:$s, GR32:$s)>;
def : Pat<(i64 (int_x86_mydouble GR64:$s)), (ADD64rr GR64:$s, GR64:$s)>;
In this case, the functions ADD32rr
and ADD64rr
are examples of the direct
functions.
At this time, I’m going to add the above notion with a tiny “class-like” value
A that holds on i32
field x
, and an instance method-style add so that
you can write the moral equivalent of a1 + a2
. I’ll show you how to make
modifications to the above.
I’m going to show you how to do the following:
TableGen
patterns that select to a real x86 ADDrr
(or if you want the
full pattern, via a pseudo + expansion)class A
as a single-field struct
and implements A_add(a1, a2)
,lit
test.In LLVM IR, there are no concepts of classes; instead i’m going to use a struct
{ i32 }
to represent A
. Frontends would normally lower a1 + a2
into a call
to A_add
which then extracts the fields of x
, adds them (via the intrinsic)
and reconstructs A
.
Desired high-level semantics
struct A { i32 x; }
A A::add(A u) { this->x += u.x; return *this; }
// usage: a3 = a1 + a2 <==> a3 = A_add(a1, a2)
I shall encode the “ALU add on the x field using a new intrinsic
llvm.x86.myadd.*(a, b) -> a
’s type.
Add the following content into the the file include/llvm/IR/IntrinsicsX86.td
:
let TargetPrefix = "x86" in {
// Previous definitions ...
def int_x86_myadd :
Intrinsic<[llvm_anyint_ty], // result type
[LLVMMatchType<0>, // a : same as result
LLVMMatchType<1>], // b : same as result
[IntrNoMem]>;
}
This gives IR declarations like:
declare i32 @llvm.x86.myadd.i32(i32, i32)
declare i64 @llvm.x86.myadd.i64(i64, i64)
As before, let’s create the pseudos (for some reason it does not work, you can replace the pseudo function(s) with the actual ones.)
def MYADD32rr : PseudoI<(outs GR32: $dst), (ins GR32: $a, GR32: $b), []> {
let Constraints = "$dst = $a"; // two-addr: result in `a`
let hasSideEffects = 0;
let SchedRW = [WriteALU];
}
def MYADD64rr : PseudoI<(outs GR64: $dst), (ins GR64: $a, GR64: $b), []> {
let Constraints = "$dst = $a"; // two-addr: result in `a`
let hasSideEffects = 0;
let SchedRW = [WriteALU];
}
def : Pat<(i32 (int_x86_myadd GR32:$a, GR32:$b)), (MYADD32rr GR32:$a, GR32:$b)>;
def : Pat<(i64 (int_x86_myadd GR64:$a, GR64:$b)), (MYADD64rr GR64:$a, GR64:$b)>;
As before, let’s wire up the CMake file (i.e., lib/Target/X86/CMakeLists.txt
)
with the following contents:
option(LLVM_ENABLE_X86_MYADD "Enable x86 myadd tutorial feature" ON)
if(LLVM_ENABLE_X86_MYADD)
set(MYADD_TO_INCLUDE "include \"X86MyAdd.td\"\n")
set(LLVM_ENABLE_X86_MYADD_ENABLED 1)
else()
set(MYADD_TO_INCLUDE "")
set(LLVM_ENABLE_X86_MYADD_ENABLED 0)
endif()
# To support `myadd`
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/X86MyAddToggle.td.in
${CMAKE_CURRENT_BINARY_DIR}/X86MyAddToggle.td
@ONLY
)
# To support `myadd`
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/X86MyAddConfig.h.in
${CMAKE_CURRENT_BINARY_DIR}/X86MyAddConfig.h
@ONLY
)
Referring to the previous section, the content of lib/Target/X86/X86MyAddToggle.td.in
would be:
@MYADD_TO_INCLUDE@
Similarly, the content of lib/Target/X86/X86MyAddConfig.h.in
would be:
/* generated by CMake */
#cmakedefine01 LLVM_ENABLE_X86_MYADD
Finally, ensure that you add the appropriate toggle files into
lib/Target/X86/X86.td
, something like the following:
//===----------------------------------------------------------------------===//
// Instruction Descriptions
//===----------------------------------------------------------------------===//
include "X86Schedule.td"
include "X86InstrInfo.td"
include "X86SchedPredicates.td"
def X86InstrInfo : InstrInfo;
// @date added on 15 september 2025
// @author Raymond TAY
include "X86MyDoubleToggle.td"
include "X86MyAddToggle.td"
Depending on whether the feature is enabled, the appropriate source file will be included.
Now, we are ready to expand pseudos in lib/Target/X86/X86ExpandPseudo.cpp
and look for the function expandMI(...)
:
#if LLVM_ENABLE_X86_MYADD
case X86::MYADD32rr: {
Register A = MI.getOperand(0).getReg(); // tied to input 0 (a)
Register B = MI.getOperand(2).getReg(); // input 1 (b)
MachineBasicBlock &MBB = *MI.getParent();
BuildMI(MBB, MBBI, DL, (*TII).get(X86::ADD32rr), A).addReg(A).addReg(B);
MBBI = MBB.erase(MI);
return true;
}
case X86::MYADD64rr: {
Register A = MI.getOperand(0).getReg();
Register B = MI.getOperand(2).getReg();
MachineBasicBlock &MBB = *MI.getParent();
BuildMI(MBB, MI, DL, (*TII).get(X86::ADD64rr), A).addReg(A).addReg(B);
MBBI = MBB.erase(MI);
return true;
}
#endif
Edit llvm/test/lit.site.cfg.py.in
:
if @LLVM_ENABLE_X86_MYADD_ENABLED@:
config.available_features.add('x86-myadd')
If you recall, you should see the x86-mydouble
somewhere before.
Add a small IR file to model your class and operator in the following file
test/CodeGen/X86/myclass_add.ll
:
; REQUIRES: x86-registered-target, x86-myadd
; RUN: llc -mtriple=x86_64-apple-darwin %s -o - | FileCheck %s
; If you gated the feature with a lit flag, add: ; REQUIRES: x86-mydouble
%struct.A = type { i32 }
declare i32 @llvm.x86.myadd.i32(i32, i32)
; A A_add(A a1, A a2) { a1.x += a2.x; return a1; }
define %struct.A @A_add(%struct.A %a1, %struct.A %a2) {
entry:
%a1x = extractvalue %struct.A %a1, 0
%a2x = extractvalue %struct.A %a2, 0
%sum = call i32 @llvm.x86.myadd.i32(i32 %a1x, i32 %a2x)
%ret = insertvalue %struct.A undef, i32 %sum, 0
ret %struct.A %ret
}
; For convenience, also expose a plain add on i32 'x' fields:
define i32 @A_add_x(%struct.A %a1, %struct.A %a2) {
entry:
%a1x = extractvalue %struct.A %a1, 0
%a2x = extractvalue %struct.A %a2, 0
%sum = call i32 @llvm.x86.myadd.i32(i32 %a1x, i32 %a2x)
ret i32 %sum
}
; CHECK-LABEL: _A_add:
; CHECK: movl , %eax
; CHECK: addl , %eax
; CHECK: ret
; CHECK-LABEL: _A_add_x:
; CHECK: movl , %eax
; CHECK: addl , %eax
; CHECK: ret
At this time, it is time to build the LLVM compiler which triggers the
compilation and build of llc
, llvm-tblgen
, FileCheck
etc.
Here’s how to configure the build on the command line:
cmake -S llvm -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DLLVM_TARGETS_TO_BUILD="X86;AArch64" \
-DLLVM_ENABLED_PROJECTS="llvm" \
-DLLVM_BUILD_UTILS=ON \
-DLLVM_INCLUDE_TESTS=ON \
-DLLVM_ENABLE_X86_MYDOUBLE=ON \
-DLLVM_ENABLE_X86_MYDOUBLE_ENABLED=1 \
-DLLVM_ENABLE_X86_MYADD=ON \
-DLLVM_ENABLE_X86_MYADD_ENABLED=1
ninja -C build llc llvm-tblgen
Once the compilation is done and you are satisfied with the test, you might wish to disable the compile and you can do it like this:
cmake -S llvm -B build -G Ninja \
...
-DLLVM_ENABLE_X86_MYADD_ENABLED=0 \
-DLLVM_ENABLE_X86_MYADD=OFF
ninja -C build llc llvm-tblgen
Rebuild the llc
and llvm-tblgen
components. The testing is going to be
manual, though we can use LLVM’s Machine IR Test (MIR) using the LLVM Integrated
Tester (lit).
./build/bin/llc -mtriple=x86_64-apple-darwin \
-print-before=asm-printer ../TableGenTutorial/myclass_add.ll -o /dev/null \
| grep MYADD || echo "no pseudos ✅"
Generate the Darwin flavored x86 ASM on Apple Silicon:
./build/bin/llc -mtriple=x86_64-apple-darwin ../TableGenTutorial/myclass_add.ll -o -
Run the test by using the recently built artefacts like llvm-lit
etc
./build/bin/llvm-lit -v llvm/test/CodeGen/X86/2025-september-15-myadd.ll
-- Testing: 1 tests, 1 workers --
PASS: LLVM :: CodeGen/X86/2025-september-15-myadd.ll (1 of 1)
Testing Time: 0.15s
Total Discovered Tests: 1
Passed: 1 (100.00%)