Write lit tests with FileCheck for LLVM 22 IR transforms, pass output, and codegen. Covers RUN lines, CHECK patterns, common directives, negative checks, and running tests with llvm-lit.
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/lit-filecheck/SKILL.mdUse this skill when the user wants to add tests for an LLVM pass, IR transform, codegen output, or compiler frontend.
# Verify tools are on PATH
llvm-lit --version # the lit test runner
FileCheck --version # the pattern matcher
opt --version # for IR transform tests
llc --version # for codegen testsFor out-of-tree projects, set LLVM_TOOLS_BINARY_DIR in cmake and use:
${LLVM_TOOLS_BINARY_DIR}/llvm-lit
${LLVM_TOOLS_BINARY_DIR}/FileCheckLit tests are plain text files (.ll for IR, .c/.cpp for Clang tests):
; RUN: opt -passes=<pass-name> -S %s | FileCheck %s
;
; A short comment explaining what this test covers.
define i32 @test_basic(i32 %x, i32 %y) {
entry:
; CHECK-LABEL: @test_basic
; CHECK: %sum = add i32 %x, %y
%sum = add i32 %x, %y
ret i32 %sum
; CHECK: ret i32 %sum
}%s — expands to the test file path-S — print IR as text (not bitcode)FileCheck %s — reads CHECK patterns from the same file; RUN: <command> | FileCheck %s
; RUN: <command> | FileCheck %s --check-prefix=OPT
; RUN: <command> 2>&1 | FileCheck %s ; capture stderr too
; RUN: not <command> 2>&1 | FileCheck %s ; expect non-zero exitMultiple RUN lines run sequentially; each must pass:
; RUN: opt -passes=instcombine -S %s | FileCheck %s --check-prefix=IC
; RUN: opt -passes=simplifycfg -S %s | FileCheck %s --check-prefix=SCFG; CHECK: <pattern> — match pattern anywhere on the next matching line
; CHECK-NEXT: <pattern> — match on the very next line after previous CHECK
; CHECK-SAME: <pattern> — match on the same line as previous CHECK
; CHECK-NOT: <pattern> — assert pattern does NOT appear until next CHECK
; CHECK-EMPTY: — assert next line is empty
; CHECK-LABEL: <pattern> — like CHECK but resets the search position (use for function names); CHECK: %[[REG:[a-z0-9.]+]] = add i32
; CHECK: ret i32 %[[REG]]%[[REG:[a-z0-9.]+]] captures the register name into REG for later use.
Numeric variables:
; CHECK: define i32 @foo(i32 %x)
; CHECK: [[#LINE:]]:
; CHECK: line [[#LINE+1]]; CHECK-NOT: call void @expensive_function
; CHECK-NOT: unreachableCHECK-NOT asserts the pattern never appears between the previous and next CHECK.
; RUN: opt -passes=instcombine -S %s | FileCheck %s
; RUN: opt -passes=instcombine -disable-output %s ; smoke test: must not crash
; Test: x + 0 → x
define i32 @add_zero(i32 %x) {
; CHECK-LABEL: @add_zero(
; CHECK-NEXT: ret i32 %x
%r = add i32 %x, 0
ret i32 %r
}
; Test: x * 1 → x
define i32 @mul_one(i32 %x) {
; CHECK-LABEL: @mul_one(
; CHECK-NEXT: ret i32 %x
%r = mul i32 %x, 1
ret i32 %r
}
; Negative test: should NOT eliminate a volatile load
define i32 @volatile_load(ptr %p) {
; CHECK-LABEL: @volatile_load(
; CHECK: load volatile i32
; CHECK-NOT: ret i32 0
%v = load volatile i32, ptr %p
ret i32 %v
}; RUN: opt -load-pass-plugin=%llvmshlibdir/MyPass%pluginext \
; RUN: -passes=my-pass -S %s 2>&1 | FileCheck %s
; CHECK: MyPass: processing function foo
define void @foo() {
ret void
}For in-tree passes, just use -passes=my-pass without --load-pass-plugin.
; RUN: llc -mtriple=x86_64-unknown-linux-gnu -O2 < %s | FileCheck %s
define i32 @add(i32 %a, i32 %b) {
entry:
; CHECK-LABEL: add:
; CHECK: leal (%rdi,%rsi), %eax
; CHECK: retq
%r = add i32 %a, %b
ret i32 %r
}Test for a specific CPU feature:
; RUN: llc -mtriple=x86_64 -mattr=+avx2 < %s | FileCheck %s --check-prefix=AVX2
; AVX2: vmovdqu; For a custom compiler binary:
; RUN: my-compiler %s -emit-llvm -S -o - | FileCheck %s
; RUN: my-compiler %s -o %t && %t | FileCheck %s --check-prefix=OUTPUT
; OUTPUT: Hello, World!In the root of your test directory, create lit.cfg.py:
import lit.formats
config.name = "MyProject"
config.test_format = lit.formats.ShTest(True)
config.test_source_root = os.path.dirname(__file__)
config.suffixes = [".ll", ".c"]
# Substitutions for tool paths
config.substitutions.append(("%opt", "/path/to/opt"))
config.substitutions.append(("%filecheck","/path/to/FileCheck"))
config.substitutions.append(("%llvmshlibdir", "/path/to/llvm/lib"))
config.substitutions.append(("%pluginext",
".dylib" if sys.platform == "darwin" else ".so"))# Run all tests in a directory
llvm-lit test/ -v
# Run a single test file
llvm-lit test/Transforms/my-pass.ll -v
# Run with parallelism
llvm-lit test/ -j8
# Filter by name
llvm-lit test/ --filter "instcombine"
# Show output of failing tests
llvm-lit test/ --show-all# Print the exact FileCheck invocation
llvm-lit test/my-test.ll -v --show-all
# Run the RUN line manually (replace %s with the file path)
opt -passes=my-pass -S test/my-test.ll | FileCheck test/my-test.ll
# Check what the pass actually emits:
opt -passes=my-pass -S test/my-test.ll
# Use FileCheck --dump-input to show which lines matched/failed:
opt -passes=my-pass -S test/my-test.ll | \
FileCheck test/my-test.ll --dump-input=fail; Match any register name
; CHECK: %{{[a-z0-9.]+}} = add
; Match optional whitespace
; CHECK: ret{{[ \t]+}}i32
; Match a function definition regardless of attributes
; CHECK-LABEL: define {{.*}}@my_func(
; Assert a block is completely eliminated (no label in output)
; CHECK-NOT: dead_block:
; Test that a call is inlined (no call instruction remains)
; CHECK-NOT: call {{.*}}@small_funcCHECK: without CHECK-LABEL: for function boundaries — without labels, patterns from one function can match in another.%0, %1 — they change easily; use named values or %[[VAR:...]] captures instead.RUN line with -disable-output to catch crashes separately from correctness.CHECK-LABEL: at each function so FileCheck resets its search position.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.