TableGenTutorial

Blogging site

TableGenTutorial

Getting started with TableGen via a tutorial. For this tutorial to work, you will need to clone the LLVM project.

X86 “mydouble” Tutorial Feature

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).


1. Add the IR intrinsic

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:

2. Create a TableGen file

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)>;

3. Generate a toggle include for TableGen

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"

4. Expand the pseudos in C++

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

5. Wire up CMake

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})

6. Add a LLVM Integrated Test (Lit) feature flag

Edit llvm/test/lit.site.cfg.py.in:

if @LLVM_ENABLE_X86_MYDOUBLE_ENABLED@:
    config.available_features.add('x86-mydouble')

7. Create a regression test

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

8. Build or Re-build with the toggle

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')

9. Test

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.

Notes

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.

Advanced Problem statement

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:

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.

1. Fleshing out the concepts

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.

2. Define the intrinsic

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)

3. Post-ISel expansion practice

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)>;

4. Wire up CMAKE

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.

5. Expand the pseudos

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

6. Add a LLVM Integrated Test (Lit) feature flag

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.

7. Class A shim in LLVM IR

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

8. Build or Re-build with the toggle

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

9. Test

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%)