agentleFS
LuisaGroupLuisaGroup/LuisaCompute/.agents/skills/xir_passes/SKILL.md

xir-passes

Skill1.1k starsChanged 36 days agoApache-2.0View on GitHub

Implement, compose, test, and debug LuisaCompute XIR transformation and analysis passes. Use when modifying files under src/xir/passes or include/luisa/xir/passes, changing CFG or SSA invariants, authoring a PassPipeline, or diagnosing pass-order, reachability, PHI, dominance, aliasing, or memory-effect bugs.

Trending

Quiet since July.thin dataabout the whole repository

Dec 2025: 0 mentionsJan 2026: 0 mentionsFeb 2026: 0 mentionsMar 2026: 0 mentionsApr 2026: 0 mentionsMay 2026: 0 mentionsJun 2026: 0 mentionsJul 2026: 1 mentionAug 2026: 0 mentionsSep 2026: 0 mentionsOct 2026: 0 mentionsNov 2026: 0 mentions
Dec 2025Jun 2026Nov 2026
Mentions a month of the LuisaGroup/LuisaCompute repository that our weekly crawl found: only 5 in all, so this is a hint, not a trend. There is no adoption data for this file yet.

The file

Show its rules and text

We split this skill into the rules it gives an agent

37 rules. About 0 are common, 17 are distinctive, and 20 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈17

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 20

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use the common layout without assuming a fixed line number or alphabetical registration:

    line 21
  2. Very specific:

    test registration: src/tests/CMakeLists.txt and src/tests/xmake.lua (both enumerate every test target)

    line 30
  3. Distinctive:

    Use these exact APIs:

    line 48
  4. Very specific:

    Do not use LLVM casting helpers such as cast_or_null.

    line 66
  5. Very specific:

    Run lower_ray_query_to_loop to convert ray-query loops into ordinary structured loop/if constructs, and check its rejection result.

    line 81
  6. Very specific:

    Run destructure_cfg to lower IfInst, SwitchInst, LoopInst, SimpleLoopInst, BreakInst, and ContinueInst.

    line 85
  7. Very specific:

    Do not claim that destructure_cfg lowers RayQueryLoopInst or removes every specialized terminator: RAY_QUERY_LOOP terminators are counted …

    line 90
  8. Very specific:

    Use contains_structured_control_flow from src/xir/passes/helpers.h before a plain-CFG-only mutation.

    line 95
  9. Distinctive:

    Follow the inliner's actual contract:

    line 100
  10. Very specific:

    Treat a nonzero rejected_malformed_call_count as a hard error in backend normalization.

    line 108
  11. Distinctive:

    When changing this schedule, add a shape test that checks the call disappears, the inliner does not report a structured skip, eligible …

    line 124
  12. Distinctive:

    After basic optimization, the Metal4 AIR consumer conditionally normalizes autodiff scopes with:

    line 129
  13. Distinctive:

    Prefer collect-then-rewrite when a transform removes or replaces nodes during traversal.

    line 172
  14. Distinctive:

    Create or append the replacement.

    line 181
  15. Distinctive:

    Do not remove blocks with a naive reachable-set recipe on structured CFG.

    line 184
  16. Distinctive:

    Create all destination blocks before cloning branch targets.

    line 192
  17. Distinctive:

    Create PHIs before resolving cyclic incoming values.

    line 193
  18. Distinctive:

    Ensure each PHI incoming block remains a real predecessor.

    line 198
  19. Very specific:

    Run mem2reg only after CFG and alloca placement are valid.

    line 200
  20. Very specific:

    Use reg2mem before a transform that cannot preserve live PHIs, when that transform's documented precondition requires it.

    line 201
  21. Very specific:

    Use get_memory_info() from src/xir/passes/helpers.h.

    line 210
  22. Distinctive:

    Check all of the following independently:

    line 213
  23. Very specific:

    Treat CLOCK as a non-deterministic read, not a pure instruction.

    line 223
  24. Very specific:

    Treat these ResourceQueryOp values as stateful ray-query constructors, not ordinary pure queries:

    line 225
  25. Very specific:

    Keep the classification centralized in helpers.cpp, and make optimizers consult it instead of whitelisting all ResourceQueryInst values.

    line 236
  26. Very specific:

    Use PassPipeline::add adapters that return whether the module changed:

    line 241
  27. Very specific:

    Use add_fixed_point only when every child is safe to repeat and every change predicate is correct.

    line 253
  28. Very specific:

    Do not infer implementation maturity from registration.

    line 259
  29. Distinctive:

    Keep producer and preflight assumptions paired.

    line 287
  30. Distinctive:

    Treat raster stage functions as ABI roots:

    line 299
  31. Very specific:

    Keep destructure_cfg -> inline_all immediately adjacent for each separately translated vertex and fragment module, then run the same SSA …

    line 305
  32. Distinctive:

    When a pass change affects this boundary, add both:

    line 311
  33. Very specific:

    Use Boost.UT for XIR unit tests.

    line 320
  34. Distinctive:

    all reachable blocks terminated.

    line 325
  35. Distinctive:

    no stale uses after removal.

    line 327
  36. Very specific:

    Use CMake/Ninja when the Metal4 integration tests are in scope (they are registered only in src/tests/CMakeLists.txt):

    line 347
  37. Very specific:

    Use the Metal4 backend for an end-to-end AIR test.

    line 356

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

XIR Pass Development

Read the exact contract first

Open the pass's public header, implementation, nearest related pass, and tests before editing. Treat the header as authoritative: pass entry points are not uniform. Depending on the pass, an API may be module-only, accept Function * or FunctionDefinition *, omit PassReport, or return a specialized result. There is no pass base class and no analysis manager: each pass is a free function named <name>_pass_run_on_module / <name>_pass_run_on_function declared in its own header and returning a specialized *Info counter struct, while PassReport and PassPipeline in include/luisa/xir/passes/pass_pipeline.h carry the shared report and composition surface.

Use the common layout without assuming a fixed line number or alphabetical registration:

  • public header: include/luisa/xir/passes/<pass>.h
  • implementation: src/xir/passes/<pass>.cpp
  • library registration: src/xir/CMakeLists.txt (CMake lists each pass source explicitly; xmake globs src/xir/passes/*.cpp from src/runtime/xmake.lua, so only the CMake list needs a manual entry)
  • focused tests: src/tests/unit/xir/
  • test registration: src/tests/CMakeLists.txt and src/tests/xmake.lua (both enumerate every test target)

Copy the closest correct pass pattern. Aggregate per-function counters rather than overwriting the result on each function.

Use core APIs safely

Resolve a definition before traversing blocks:

if (auto *definition = function->definition()) {
    definition->traverse_basic_blocks([](xir::BasicBlock *block) noexcept {
        // inspect reachable blocks
    });
}

Use these exact APIs:

NeedAPI
all module functionsmodule->function_list()
all blocks owned by a functionfunction->basic_blocks()
reachable blocks from the bodydefinition->traverse_basic_blocks(...)
function entrydefinition->body_block()
return operandreturn_inst->return_value()
conditional targetstrue_block(), false_block()
retarget a conditionalset_true_target(...), set_false_target(...)
branch targettarget_block(), set_target_block(...)
detach a noderemove_self()

Guard BasicBlock::terminator() and traverse_successors() with is_terminated(). terminator() asserts on an unterminated block; it does not return null as a malformed-IR probe.

Do not use LLVM casting helpers such as cast_or_null. Check isa<T>() and then use static_cast<T *>.

Distinguish structured and plain CFG

Structured terminators own or reference regions and merge blocks. Plain CFG uses BranchInst, ConditionalBranchInst, and the raw multi-way IndexedBranchInst edges. SwitchInst also carries merge information and contains_structured_control_flow classifies it as structured; destructuring converts it to IndexedBranchInst. Inspect DerivedInstructionTag for the complete current instruction inventory rather than copying an exhaustive list into a pass.

Apply the relevant lowering order:

  1. Run lower_ray_query_to_loop to convert ray-query loops into ordinary structured loop/if constructs, and check its rejection result. The lower_ray_query_loop_to_loop_* entry points are a deprecated compatibility alias for these names.
  2. Run destructure_cfg to lower IfInst, SwitchInst, LoopInst, SimpleLoopInst, BreakInst, and ContinueInst; a structured SwitchInst becomes the raw-CFG IndexedBranchInst. It also spills early returns.

Do not claim that destructure_cfg lowers RayQueryLoopInst or removes every specialized terminator: RAY_QUERY_LOOP terminators are counted as errors, and unterminated owned blocks are reported through leaked_block_count while the function is rejected unchanged before any mutation.

Use contains_structured_control_flow from src/xir/passes/helpers.h before a plain-CFG-only mutation.

Keep inlining after destructuring when it is multi-block

Follow the inliner's actual contract:

  • Inline a single-block callee into a structured caller without splitting CFG.
  • Reject a multi-block call while either caller or callee contains structured control flow.
  • Destructure caller and callee, then run inline_all immediately. Do not insert cleanup, SSA, or another CFG pass between those two steps. This enables eligible multi-block call sites.
  • Treat a nonzero rejected_malformed_call_count as a hard error in backend normalization; a zero inlined-call count by itself is not a failure.
  • In ordinary lowering, run mem2reg right after inline_all to remove inliner-created argument and return spill slots before SSA optimization; the Metal4 lowering pipeline inserts hoist-allocas-after-inline between them, and the CUDA and fallback backends run mem2reg in a separate pre-CFG pipeline. In pre-autodiff normalization, allow autodiff scopes in the caller, then use cleanup and reg2mem before restructuring instead.
  • Remember that destructure_cfg deliberately preserves ray-query constructs and other specialized structured operations; reference-argument and other unsupported uses may also leave allocas after mem2reg.
  • Preserve opaque custom references. create_value_argument rejects custom types and promote_ref_arg intentionally excludes them; a backend-owned handle may have a concrete ABI only after backend lowering.
  • Expect recursive callables and preserved structured cases to remain.

When changing this schedule, add a shape test that checks the call disappears, the inliner does not report a structured skip, eligible generated temporaries are promoted, every reachable block remains terminated, and successor PHI incoming blocks still match real predecessors after block splitting.

After basic optimization, the Metal4 AIR consumer conditionally normalizes autodiff scopes with:

optional outline-ray-query-pipelines (when ray-query pipelines are enabled)
lower_ray_query_to_loop (checked)
-> destructure_cfg (checked)
-> inline_all (immediately adjacent; allow_autodiff_scope_in_caller)
-> post-inline cleanup (one fixed-point iteration)
-> simplify_cfg
-> reg2mem
-> restructure_cfg (checked)
-> verify(no PHIs, unique merge blocks)
-> autodiff
-> reg2mem
-> verify(no PHIs, unique merge blocks)

It then lowers every module with:

optional outline-ray-query-pipelines (when ray-query pipelines are enabled)
lower_ray_query_to_loop (checked)
-> destructure_cfg (checked)
-> inline_all (immediately adjacent)
-> hoist-allocas-after-inline
-> mem2reg
-> SSA optimization
-> unused_callable_removal
-> simplify_cfg
-> verify(require_reachable_blocks = true)

The AIR entry point repeats the reachable-block verification before XIR-to-LLVM translation. Treat destructure_cfg -> inline_all as the common adjacency and inline_all -> mem2reg, possibly with an intervening alloca hoist, as the ordinary-phase adjacency. Read src/backends/metal4/metal_xir_pipeline.cpp before changing either schedule; read src/xir/passes/pass_pipeline.cpp for the current contents of factory pipelines rather than copying their expansion here.

Mutate lists deliberately

Prefer collect-then-rewrite when a transform removes or replaces nodes during traversal. Mutate in place only when iterator advancement and node lifetime are explicitly controlled by an established local pattern.

For a replacement:

  1. Capture every operand, parent, and target needed after removal.
  2. Detach the old instruction.
  3. Set the builder insertion point.
  4. Create or append the replacement.
  5. Repair use-def edges, PHIs, and metadata.

Do not remove blocks with a naive reachable-set recipe on structured CFG. Merge blocks and owned regions require structurally aware traversal. On plain CFG, clean incoming PHI entries and detach contained instructions before deleting a block (src/xir/passes/simplify_cfg.cpp). Never remove body_block().

Preserve SSA and dominance invariants

  • Create all destination blocks before cloning branch targets.
  • Create PHIs before resolving cyclic incoming values; attach incoming pairs after all blocks and values are mapped.
  • Recompute dominance and loop analyses after CFG mutation (compute_dom_tree, discover_natural_loops; the latter requires a plain CFG).
  • Ensure each PHI incoming block remains a real predecessor; the verifier reports "PHI incoming blocks do not match CFG predecessors."
  • Run mem2reg only after CFG and alloca placement are valid.
  • Use reg2mem before a transform that cannot preserve live PHIs, when that transform's documented precondition requires it.

Remember that traverse_basic_blocks visits blocks reachable from body_block(), while basic_blocks() includes disconnected owned blocks. Choose intentionally.

Model memory effects conservatively

Use get_memory_info() from src/xir/passes/helpers.h; do not maintain a second hard-coded purity whitelist in a new pass.

Check all of the following independently:

  • memory scope and read/write effects;
  • volatility and synchronization;
  • aliasing with intervening accesses (alias_analysis_query);
  • dominance and availability;
  • speculation safety for the exact opcode.

Purity alone does not make an instruction safe to hoist or speculate. Integer division, remainder, and shifts are examples that require additional checks. Treat CLOCK as a non-deterministic read, not a pure instruction.

Treat these ResourceQueryOp values as stateful ray-query constructors, not ordinary pure queries:

  • RAY_TRACING_QUERY_ALL
  • RAY_TRACING_QUERY_ANY
  • RAY_TRACING_QUERY_ALL_MOTION_BLUR
  • RAY_TRACING_QUERY_ANY_MOTION_BLUR

Each creates fresh mutable traversal state. get_memory_info() classifies it as GLOBAL/READ, non-volatile: it remains removable when unused, but is not safe to value-number, common with Early CSE/GVN, speculate, or hoist with LICM. Keep the classification centralized in helpers.cpp, and make optimizers consult it instead of whitelisting all ResourceQueryInst values.

Build pipelines with accurate change reporting

Use PassPipeline::add adapters that return whether the module changed:

xir::PassPipeline pipeline;
pipeline.add("inline-all", [](xir::Module *module,
                              xir::PassReport &report) {
    auto info = xir::inline_all_pass_run_on_module(module, &report);
    return info.inlined_call_count != 0u;
});
auto stats = pipeline.run(module);

Use add_fixed_point only when every child is safe to repeat and every change predicate is correct. A false negative terminates the group early; a false positive wastes iterations or masks non-convergence. For a named one-shot group, use add_sequence instead: unlike a fixed point capped at one iteration, child changes there do not consume a convergence budget.

Do not infer implementation maturity from registration. Read the header and source. Region outlining is unimplemented: outline_pass_run_on_module leaves the IR unchanged and reports "outlining is not implemented. IR was left unchanged." (include/luisa/xir/passes/outline.h, src/xir/passes/outline.cpp). The loop transforms that do rewrite (loop_fusion, loop_rotation, loop_vectorization) are unstructured-CFG-only: structured functions are rejected without mutation, and they stay out of the factory pipelines in src/xir/passes/pass_pipeline.cpp.

Respect backend preflight normal forms

A pass can preserve valid XIR while making a backend reject it. For the Metal4 AIR path, inspect luisa_compute_metal_codegen_llvm_supported alongside any change that affects types, special registers, calls, atomics, or resource-use shape. Every emitter entry calls it before LLVM construction and fails closed on unsupported XIR: JIT compute including autodiff kernels (src/backends/metal4/metal_device.cpp), compute and per-stage raster codegen (codegen_entry, codegen_raster_entry in src/backends/metal4/metal_air_pipeline.cpp, which also serve the compile-only raster archive path in src/backends/metal4/metal_raster_ext.cpp), the shared MetalCodegenLLVMImpl::generate, and the tile path (src/backends/metal4/tile/metal_tile.cpp). Metal4 has no MSL or legacy-IR shader fallback. Compute and raster AOT loading consume validated archives and bypass XIR preflight.

This preflight and its pass schedule live in the independent metal4 backend; the original metal backend remains the source-MSL compatibility path.

Keep producer and preflight assumptions paired. In particular, texture uses must reach preflight as direct resource operations after normalization. A compute AIR module requires exactly one kernel and rejects raster special registers. A raster AIR module instead requires exactly one RasterStageFunction whose stage() matches the configured vertex or fragment program; object ID is valid in both roles, while barycentrics, front-facing, and discard require the fragment role and base-instance requires the vertex role. External declarations may remain only when native_include supplies ABI-compatible LLVM IR/bitcode definitions at link time. Ensure every operand and result type is checked; constants can otherwise carry an unsupported type past an instruction-only scan.

Treat raster stage functions as ABI roots:

  • dead_arg_elim must not remove their arguments, even when a stage body does not read a slot.
  • unused_callable_removal must seed reachability from both kernels and raster stages.
  • Keep destructure_cfg -> inline_all immediately adjacent for each separately translated vertex and fragment module, then run the same SSA cleanup and reachable-block verification used by compute AIR.
  • Preserve RasterStageFunction::stage() through interchange text/bitcode and debug text; use raster_vertex and raster_fragment interchange kinds.

When a pass change affects this boundary, add both:

  1. a XIR shape test for the required normal form; and
  2. a Metal4 runtime test for JIT compute, or the dedicated raster test. The AIR-only backend fails closed, and the dedicated raster test exercises AIR end to end.

Test behavior and shape

Use Boost.UT for XIR unit tests. Wire every test block to body_block() when it must be reached by traversal. Test both the information counters and the IR invariants that matter:

  • expected instruction count or absence;
  • all reachable blocks terminated;
  • branch targets and PHI predecessors valid;
  • no stale uses after removal;
  • idempotence when promised;
  • rejection leaves the module unchanged;
  • pass reports match returned counters.

Build and run focused tests with xmake; the unit XIR pass tests need no device argument:

xmake build test_xir_passes
xmake run test_xir_passes

There is a second, older test location: src/xir/tests/test_<name>.cpp (dce, mem2reg, debug_printer, ray_query, aggregate_field_bitmasks) is built as xir_test_<name> by luisa_compute_xir_add_test() only when -DLUISA_COMPUTE_ENABLE_XIR_TESTS is passed (src/xir/CMakeLists.txt:177-189). That variable is never declared with option(), so it does not appear in the generated help, and those targets are plain add_executable — not CTest-registered. Prefer src/tests/unit/xir/ for new pass tests.

Use CMake/Ninja when the Metal4 integration tests are in scope (they are registered only in src/tests/CMakeLists.txt):

cmake --build cmake-build-metal4-air --target test_xir_passes -j 8
ctest --test-dir cmake-build-metal4-air -R '^test_xir_passes$' --output-on-failure
ctest --test-dir cmake-build-metal4-air -L unit_xir --output-on-failure

Use the Metal4 backend for an end-to-end AIR test. The dedicated test_metal_xir_air CTest passes metal4 as its own backend argument, so no selection environment is needed. Use test_metal_xir_air_raster for vertex/fragment stage identity, raster-only operations, and render readback; launch other binaries with backend metal4.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.