CtrlK
BlogDocsLog inGet started
Tessl Logo

add-gc-statepoints

Add garbage-collection support to an LLVM 22 IR frontend. Covers the shadow-stack (gcroot) model for simple GCs and the statepoint/gc.relocate model for moving/relocating collectors, including the RewriteStatepointsForGC pass and StackMap section.

67

Quality

80%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

Fix and improve this skill with Tessl

tessl review fix ./tiles/tessl-llvm/skills/add-gc-statepoints/SKILL.md
SKILL.md
Quality
Evals
Security

Skill: Add GC Support to an LLVM Frontend (LLVM 22)

Use this skill when your language runtime needs a garbage collector and you must tell LLVM where GC-safe points are and which pointers are live.


Step 0 — Choose a model

ModelUse when
gcroot / shadow stackSimple stop-the-world GC, prototype, or interpreter. Easy to implement.
StatepointsMoving/relocating collector (objects can be copied). Production use.

For new language runtimes, prefer statepoints — gcroot is legacy and does not support moving objects.


Model A: gcroot (shadow stack)

Step A1 — Set the GC strategy on functions

// Mark each function that manages GC pointers
F->setGC("shadow-stack");

Step A2 — Mark GC root allocas

Every heap-pointer local variable needs a root alloca in the entry block:

#include "llvm/IR/Intrinsics.h"

// Alloca for the GC root (always in entry block)
llvm::AllocaInst *Root = B.CreateAlloca(B.getPtrTy(), nullptr, "obj.root");
// Initialize to null before first use — GC may scan it before you store
B.CreateStore(llvm::ConstantPointerNull::get(B.getPtrTy()), Root);

// Declare llvm.gcroot
llvm::Function *GCRootFn = llvm::Intrinsic::getOrInsertDeclaration(
    M, llvm::Intrinsic::gcroot, {});

// Mark the alloca as a GC root (must call immediately after alloca)
B.CreateCall(GCRootFn,
    {Root, llvm::ConstantPointerNull::get(B.getPtrTy())});

Step A3 — Use the root across GC points

// Store the object pointer into the root before any call that may GC
B.CreateStore(ObjPtr, Root);

// ... calls that may trigger GC ...

// Reload after the GC point — the shadow-stack GC does NOT move objects,
// but reading from Root keeps the pattern consistent.
llvm::Value *LiveObj = B.CreateLoad(B.getPtrTy(), Root, "obj");

Step A4 — Link the shadow-stack runtime

You must supply a shadow-stack runtime (or link against one). Minimal C structure:

struct FrameMap  { int NumRoots; int NumMeta; const void *Meta[]; };
struct StackEntry { struct StackEntry *Next; const struct FrameMap *Map; void *Roots[]; };
extern struct StackEntry *llvm_gc_root_chain; // thread-local in production

Model B: Statepoints (relocating GC)

Step B1 — Set the GC strategy on functions

F->setGC("statepoint-example"); // built-in test strategy
// Or register a custom strategy — see Step B5

Step B2 — Use RewriteStatepointsForGC pass (recommended)

Let the pass automatically insert gc.statepoint + gc.relocate sequences around all calls:

#include "llvm/Transforms/Scalar/RewriteStatepointsForGC.h"

// Add to your function pass pipeline before codegen:
FPM.addPass(llvm::RewriteStatepointsForGCPass());

The pass:

  1. Finds all calls in functions with a GC strategy.
  2. Computes live GC pointer sets at each call.
  3. Rewrites calls to gc.statepoint + gc.relocate sequences.
  4. Emits StackMap metadata.

After this pass, every GC pointer use after a call site is replaced with a gc.relocate result.

Step B3 — Manual statepoint emission (advanced / understanding only)

If you emit statepoints manually instead of using the pass:

; Statepoint wraps a call to @foo(i32 %x) with GC pointer %obj1 live:
%sp = call token @llvm.experimental.gc.statepoint.p0(
    i64 0,      ; call ID
    i32 0,      ; patch bytes
    ptr @foo,   ; callee
    i32 1,      ; # call args
    i32 0,      ; flags
    i32 %x,     ; call arg 0
    i32 0,      ; # transition args
    i32 0,      ; # deopt args
    ptr %obj1   ; GC live value (will appear in stack map)
)
%result = call i32  @llvm.experimental.gc.result.i32(token %sp)
%obj1.r = call ptr  @llvm.experimental.gc.relocate(token %sp, i32 7, i32 7)
; After the statepoint, always use %obj1.r — NOT %obj1

The C++ API equivalent:

#include "llvm/IR/Statepoint.h"

// Use IRBuilder helpers or construct via StatepointFlags / makeStatepointCall
// In practice, always prefer RewriteStatepointsForGCPass

Step B4 — Read the StackMap section

After compiling, the object file contains a __llvm_stackmaps section:

# Inspect the stack map
llvm-readobj --llvm-stackmap output.o

Your GC runtime reads this section at startup to discover the locations of all live pointers at each GC-safe point.

Step B5 — Custom GC strategy (optional)

Register a custom strategy for code generation hooks:

#include "llvm/CodeGen/GCStrategy.h"

class MyGCStrategy : public llvm::GCStrategy {
public:
  MyGCStrategy() {
    UseStatepoints = true;
    UsesMetadata   = false;
    NeededSafePoints = 0;
  }
};

static llvm::GCRegistry::Add<MyGCStrategy>
    X("my-gc", "My language GC strategy");

Functions with gc "my-gc" will use it during codegen.


Verifying the output

# Compile the IR
llc -filetype=obj output.ll -o output.o

# Check the stack map section exists
llvm-readobj --llvm-stackmap output.o

# Disassemble to verify relocate sequences are present
llvm-dis output.bc

Common mistakes

  • Never use a pointer variable after a statepoint — always use the gc.relocate result. The original pointer is invalid if the GC moved the object.
  • Never apply RewriteStatepointsForGC without first calling F->setGC(...) — the pass silently skips functions without a GC strategy.
  • Never mix gcroot and statepoints in the same module — they are incompatible.
  • Always initialize root allocas to null before the first gcroot call.
  • Always reload GC roots after any call that may trigger collection (gcroot model).
  • Always use gc.relocate for every live GC pointer across a statepoint — not just the ones you think are "interesting" to the optimizer.
Repository
hbarve1/tessl-llvm
Last updated
First committed

Is this your skill?

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.