[RFC][ClangIR] Making CIR Pipeline Boundaries First-Class Driver Artifacts

CIR has more than one meaningful serialization boundary. In particular, there is a high-level/pre-target-lowering CIR form before target and ABI lowering (the ABI-free form), and a lowered/post-ABI CIR form closer to CIR-to-LLVM (the ABI-aware form).

Today, the driver exposes only one CIR output mode through -emit-cir, and that output is produced after the current CIR-to-CIR lowering sequence. This RFC argues that the pre-lowering CIR boundary should be made first-class and driver-visible because it is useful for testing, debugging, reduction, and CIR-level transformations.

The purpose of this RFC is to identify the missing boundary. The exact spelling can be discussed separately.

Background

CIR is Clang’s MLIR-based intermediate representation, intended to sit between the Clang AST and LLVM IR: lower than the AST, still higher-level and more source-adjacent than LLVM IR. The current pipeline is:

CIRGen(AST) → early CIR cleanup/canonicalization → target/ABI lowering → CIR-to-LLVM.

  • CIRGen.
  • pre-lowering CIR cleanup/canonicalization (target-independent where possible, language-aware)
    • cir-canonicalize
    • cir-simplify
    • cir-idiom-recognizer
  • target and ABI lowering (target specific, language aware)
    • cir-target-lowering
    • cir-cxxabi-lowering
    • cir-callconv-lowering
    • cir-lowering-prepare

Problem Statement

The CIR pipeline already has a meaningful conceptual boundary before target/ABI lowering: after CIRGen and early CIR cleanup/canonicalization, but before cir-target-lowering, cir-cxxabi-lowering, and future calling-convention lowering.

This boundary matters because pre-lowering and post-lowering CIR serve different purposes. Pre-lowering CIR is more source-adjacent: it preserves high-level function signatures, aggregate parameters, return types, and source/CIR dialect structure more directly. This makes it useful for CIRGen testing, high-level analyses, transformations, and multi-TU combine/link-style experiments or tools. In that sense, it maps more closely to ClangIR’s stated intent as “a high-level representation in Clang that reflects aspects of the C/C++ languages and their extensions.”

Post-lowering CIR is also valuable because target and ABI decisions have been materialized in the representation. The problem is therefore not that the current -emit-cir artifact is useless, but that the driver exposes only one CIR artifact boundary. Workflows that need the high-level boundary must currently rely on debug-style pass instrumentation (e.g. -mllvm -print-after=cir-canonicalize), which requires knowing internal pass names, breaks when the pipeline is reordered, and does not compose with normal driver output (-c -o file.cir). This is a debug/testing mechanism, not a first-class compiler output mode.

Relationship to -emit-llvm

The analogy with -emit-llvm is useful, but should not be understood as an argument about target independence. The LLVM IR produced by Clang is already tied to a target context: IRGen has recorded target facts such as the triple, datalayout, address spaces, calling conventions, and ABI-influenced layout decisions.

The important property of -emit-llvm is that it names a well-understood serialized IR boundary before the next major lowering/codegen stage, and that boundary is useful for testing, debugging, reduction, pass development, and tooling. The analogous question for CIR is therefore not “which CIR is target-independent” but “which CIR boundaries are worth naming as first-class driver outputs”.

Proposal

Expose the pre-lowering CIR boundary as a first-class driver artifact, in addition to the existing post-lowering -emit-cir output.

Emit points are defined by conditions, not pass positions. Each CIR emit point is specified by the conditions that hold at that point, rather than by the pass after which emission occurs. Anchoring to “after pass X” re-binds the artifact to a pass position, so restructuring the pipeline silently changes the guarantees at that spelling; anchoring to conditions keeps the contract stable as the pipeline evolves and makes explicit what is actually important about the point. The proposal names two emittable forms on this basis.

ABI-free form (pre-lowering). The IR state after CIRGen and target-agnostic CIR cleanup/canonicalization (cir-canonicalize, cir-simplify, cir-idiom-recognizer), and before any pass that materializes target or ABI decisions into the IR (cir-target-lowering, cir-cxxabi-lowering, cir-lowering-prepare, and cir-callconv-lowering). The defining condition is that no target or ABI decision has been materialized — function signatures, aggregate parameters and return types, and high-level dialect structure remain source-adjacent. Self-containment with respect to the AST (below) is also among the conditions for this point.

ABI-aware form (post-lowering). The IR state after target and ABI lowering have been materialized, before CIR-to-LLVM. This is the form the existing -emit-cir already produces; the proposal names it rather than changing it (see Relation to current -emit-cir). Its defining condition is the converse of the ABI-free form’s: target and ABI facts that were previously implicit are now explicit in the IR, which suits post-ABI analyses and tooling that reason about materialized layout and calling conventions.

Relation to current -emit-cir. Today -emit-cir emits CIR after the full CIR-to-CIR lowering sequence, including target/ABI lowering. That artifact remains useful and this proposal does not change it. The proposal adds a second, earlier artifact rather than redefining the existing one.

Stability. Neither boundary is proposed as a stable IR contract for out-of-tree consumers. Both are compiler outputs in the same sense -emit-llvm is: useful and well-defined at a given Clang version, but free to evolve.

Self-containment. For the ABI-free form to be useful as a first-class artifact and digestible by other tools, it must be self-contained with respect to the Clang frontend state needed by downstream CIR passes. Passes after the boundary should not require a live ASTContext; any AST-derived facts they need should be represented in the serialized CIR module. Where this is not yet true, exposing the boundary surfaces those dependencies as bugs to be fixed; the intended direction is that the artifact is independently consumable by CIR tools and by the downstream CIR pipeline.

Self-containment is a gate, not a form-distinguishing condition. The two forms above are distinguished by an axis (ABI-awareness) along which both states are independently useful artifacts. Self-containment with respect to the AST differs in kind: it is not an axis yielding a distinct useful artifact, because the AST-dependent state is never something one would emit — any external consumer of the serialized module has no live ASTContext. AST-freedom is therefore the gate an internal form must pass through to be emittable at all, applied equally to every emit point, rather than a property that tells one form from another. (Dumping AST-dependent CIR for debugging remains the province of -print-after-style instrumentation, which is not a driver artifact.)

Sequencing. The two forms reach self-containment on different timelines, and this proposal does not assume they arrive together. For the ABI-aware form, only CIR-to-LLVM must be AST-free (downstream of where AST-derived facts are consumed), so it may become round-trippable as cc1 input. For the ABI-free form, the entire target/ABI lowering sequence downstream of it must be AST-free, which requires more effort. Both forms can be named and exposed as outputs now; round-trip is expected to land for the ABI-aware form earlier than for the ABI-free form. The RFC names the destination for both and blocks neither on the other.

Spelling. The proposed surface is -emit-cir=, which extends naturally as further emit points are named. Form tokens name the condition that defines the point — the state the module is in — rather than its position in the pipeline; for example -emit-cir=abi-free and -emit-cir=abi-aware for the two forms above. These spellings are illustrative; exact tokens are deferred to discussion. The naming question is independent of whether the boundaries themselves are worth exposing, which is the subject of this RFC.

Many thanks to @davidriverg for doing the research on this.

6 Likes

Thanks for working on improving the way we go about this.

I disagree with this specific approach though. I think we should separate concerns: -emit-cir should have one job — emit CIR to disk. It’s the pipeline stopping point flags that should control when we emit, not the output action itself.

Clang and LLVM already have established precedent for this pattern:

  • -disable-llvm-passes: “use together with -emit-llvm to get pristine LLVM IR from the frontend by not running any LLVM passes at all” (cc1-only)
  • -mllvm -stop-before=<pass> / -stop-after=<pass>: stop the LLVM backend at a specific pass boundary
  • DXC’s -emit-pristine-llvm: explicitly documented as an alias for -S + -emit-llvm + -disable-llvm-passes, a convenience composition of orthogonal flags

The -disable-llvm-passes + -emit-llvm combination is the direct analog: it doesn’t change what -emit-llvm means, it controls where the pipeline stops. The output action stays orthogonal to the stopping point. We already have -clangir-disable-passes following this same pattern.

For CIR, I’d propose extending this with flags that define well-known pipeline stopping points, composed with -emit-cir:

-fclangir-stop-before-lowering -emit-cir    # ABI-free form (pre-lowering)
-fclangir-stop-after-lowering  -emit-cir    # ABI-aware form (post-lowering, today's default)

(Exact flag names TBD — the point is the pattern, not the spelling.)

This keeps -emit-cir as a simple action (one meaning, no modal behavior, matching how -emit-llvm works today), is extensible (new pipeline boundaries are new flags, not new values on -emit-cir=), and aligns with existing Clang/LLVM precedent.

Whether these flags should be -cc1-only or driver-visible depends on who the consumers are. For testing, cc1-only is fine (matching -disable-llvm-passes). If external tooling or split compilation workflows need them, driver visibility makes sense, but that can be decided per-flag without baking the decision into -emit-cir itself.

We should also codify that every CIR pass must belong to at least one of these pipeline groupings — so when someone adds a new pass, they’re forced to decide where it fits, and the boundaries stay well-defined.

The same pattern can extend to cir-opt: flags that represent whole pass groups with similar names, giving tool-level control that mirrors the driver-level semantics.

Another alternative worth considering: a -mcir passthrough (analogous to -mllvm) that exposes CIR pass pipeline options like -mcir -stop-before=<pass> / -stop-after=<pass>, mirroring the LLVM backend’s existing mechanism.

4 Likes

Thanks Bruno. I think you’re right on the orthogonality, and I’m happy to drop -emit-cir=<form>. Keeping -emit-cir as a single-meaning action and controlling where the pipeline stops with a separate composable flag is cleaner and matches the existing precedent. The spelling was never the point of the RFC; the boundaries themselves are, and in your reply you point the two “forms” it names.

One thing I’d want to preserve: the stop point should name a condition the IR is in, not a pass. I’d be wary of -stop-before=<pass> specifically, anchoring to a pass name reintroduces the fragility (move the pass, and the guarantee silently changes) that the condition framing was meant to avoid. The boundary is the combination of a stable user-facing property and wherever the passes that establish it happen to sit, not the pass itself.

That also makes me lean toward a fixed flag with a property value over a new flag per boundary, e.g. -stop-before=<property> rather than -stop-before-lowering / -stop-after-lowering. Adding a future emission point becomes a new value rather than a new flag, as long as the values are named conditions and not pass names. Same effect as your last point about every pass belonging to a grouping. Curious which way you’d lean on that within the composable-flags model.

2 Likes

The spelling was never the point of the RFC; the boundaries themselves are, and in your reply you point the two “forms” it names.

Yep, I got that. I also explicit mentioned the specific spelling is not what I’m suggesting either.

One thing I’d want to preserve: the stop point should name a condition the IR is in, not a pass. I’d be wary of -stop-before=<pass> specifically, anchoring to a pass name reintroduces the fragility

+1

That also makes me lean toward a fixed flag with a property value over a new flag per boundary, e.g. -stop-before=<property> rather than -stop-before-lowering / -stop-after-lowering

Property value sounds good to me (assuming we have more than one to begin with).

3 Likes

I think this RFC ends up being a bit of a ‘devil in the details’. I like the idea of having more stopping points, there are some very important/useful spots we can do this. I think the ‘details’ here are going to be to figure out WHICH of these we want to preserve. The important balance of course is that maintaining these separations as points where the IR is known-‘valid’ long-term is pretty important, and a bit of a long term commitment.

1 Like

We discussed this at the clang area team meeting, but we concluded this needs further discussion.


I’m not sure what “first-class” really means in this context. -flto is a flag I’d consider “first-class”: it emits bitcode in a specific output format, and it’s intended as input for a specific tool (the linker).

IR dumps that aren’t strongly connected to a specific workflow are always going to be a developer aid, not something we ship to users. We don’t expect users to understand what the different stages mean; we expect users to request a specific kind of output.

That isn’t to say we shouldn’t make the flags easier to understand, but I don’t really think we can design user-facing flags independent of whatever tool we expect to consume the output.


I’m not sure “ABI-free form” is really a good description of pre-lowering CIR. Specific kinds of analysis passes might find it convenient to use unlowered IR, but it’s not really independent of the platform ABI in any meaningful sense. We bake in various decisions at each level from the preprocessor on. It might be possible to take pre-lowering CIR and compile it for a narrowly defined group of targets (like a CPU offloading to a GPU), but you’d need to carefully define that group.

4 Likes

Ping – does the community want to continue this discussion? The open questions above need to be discussed and answered. The Clang Area Team is considering rejecting this due to lack of community engagement/interest.

1 Like

I strongly support the intent of this RFC, and I am confident that we can agree on a way to achieve the goals. I’m imagining that this would be an option that the driver uses rather than something exposed to the user. The point, if I understand it correctly, is that there are various forms that other tools would want to consume as input, and those tools are likely to construct their own MLIR pass pipeline which may or may not include some of the default CIR pipeline and may or may not eventually feed back into the LLVM optimizer pipeline.

The existing CIR pipeline construction is not entirely mature and is mostly focused on a single use case (eventual lowering to LLVM IR).

I’d love to see a PR implementing a proposed solution.

2 Likes

Thanks for the pings, and yes. I’d like to continue and the discussion already narrowed the design a lot, so let me restate the request with a Draft-PR-223093.

Briefly this is what I prototyped:

  1. Today clang -cc1 only produces CIR (-emit-cir). It has no way to consume it. It always starts from C/C++ source, builds an AST, and runs CIRGen. The PR adds capability to do round trip.
  2. I removed the reliance on the ASTContext on the target specific passes. I have not followed what @bcardosolopes @andykaylor and @erichkeane suggest in this PR because I just re-used what I had locally in my system to build up the prototype. For clarity, the suggestions are still valid, the implementation is not there.
  3. Provided 1+2 work, you can add a cc1 flag -clangir-stop-before-lowering (I am sure we can find a better flag name) that emits a .cir before performing target specific transformations and lowering.

With 1+2+3 one can envision there exists a workflow of commands. In the PR I use cir-opt as an example just for simplicity. I would imagine though that tools will use the clang-driver to orchestrate the proper invocation of tools that start from C/C++.

This RFC has basically 2 requests.

  1. The first request is to allow CIR to round trip through CC1 (see prototype bullet 1).
  2. The second request is to introduce an emission point before we apply target specific transformations.

@efriedma-quic

I’m not sure what “first-class” really means in this context

I used that term vaguely and I judged poorly to not delete the AI text in the first place. In any case, I think “first class” should be able to do a proper round-trip through cc1: clang emit → transform → cc1 resume → working object. The prototype above does that + a little more (arguably you don’t need clangir-stop-before-lowering) to get the roundtrip. But this RFC would like to provide that capability and the prototype does so.

I’m not sure “ABI-free form” is really a good description of pre-lowering CIR.

Agreed, I did not intend to claim target independence or ABI independence. My intent was to provide a emission point for which target/ABI decisions made by the CIR passes haven’t been materialized yet; the artifact carries the same triple as the emitting invocation and is only guaranteed to resume for that target.

@efriedma-quic

IR dumps that aren’t strongly connected to a specific workflow are always going to be a developer aid, not something we ship to users.

The tools that I have in mind are:

  • This GSOC-Project. I am happy to give details, but it is not the point of this RFC.
  • Later on, some cir-link tool that takes multiple CIR files and merges them. This one is not well thought out, and at this stage is more of a concept rather than an actual direction.

Regardless of whether the GSoC project or cir-link are going to succeed, the fact is that today cc1 can only emit CIR, never consume it. Concretely: allowing to round-trip enables researcher/explorer to emit CIR, run a pass, and get a working object with the same target setup as a plain clang invocation + some script.

@erichkeane

I think this RFC ends up being a bit of a ‘devil in the details’. I like the idea of having more stopping points, there are some very important/useful spots we can do this. I think the ‘details’ here are going to be to figure out WHICH of these we want to preserve. The important balance of course is that maintaining these separations as points where the IR is known-‘valid’ long-term is pretty important, and a bit of a long-term commitment.

Agreed that the hard part is choosing which points to preserve, and I don’t think we need to enumerate all of them now. This RFC asks for exactly one: a stopping point before target lowering, with the goal that the resulting CIR can be fed back to cc1 and lowering resumed from there.

On the long-term commitment: I’m deliberately not asking for one yet. As Bruno pointed out in #219048, making that point resumable means serializing AST-derived facts into CIR. Today that surface is small and each new fact has been tractable, but if the set keeps growing we will hit a wall. I don’t think we can make the call on where that wall is until there are real consumers of the artifact. My interpretation of the direction that @bcardosolopes points to (the prototype does not fully implement it) avoids committing either way: the ASTContext stays alive on the normal compilation path, and the resume path simply works with the facts that have been serialized. The tools that end up using this boundary should decide how much of the AST we need to carry across it and I guess will also shape the final points.

@andykaylor

I strongly support the intent of this RFC, and I am confident that we can agree on a way to achieve the goals.

Thank you!

I’m imagining that this would be an option that the driver uses rather than something exposed to the user.

Yes!

1 Like

Regarding the:

  1. The second request is to introduce an emission point before we apply target specific transformations.

@davidriverg shared some of his results on the GSOC project here RFC: Enable ClangIR Build By Default- - #96 by davidriverg

We believe working on the pre-lowered CIR will make such analysis more general on different targets and likely in the future will allow us to enter the GPU dialect in an earlier CIR stage.

I think I’m following where you’re going with the clarifications. This would be developer-oriented for now, but could potentially be integrated into some user-facing feature in the future.

Actually making resuming from IR work reliably is hard to do correctly; you have to make sure all the relevant state is actually serialized, including the IR itself, supporting data structures, and some command-line flags. There’s nothing unsolvable there, but you may want to plan for the time it will take to make this work.

I don’t think we want to make a commitment about what forms of lowering happen at which step right now. But I’ll leave that analysis to the experts.

Over the last few weeks I have been working on moving some of that implicit state into CIR itself (see #224692, #224744, #224757, #225224, #225236, #225565, #225953, and the pending #225971).

With those changes, we are getting close to a point where we can resume the pipeline as it exists today. I still need to check how much coverage we are getting from the tests, and I am sure there are pieces of state that I have not accounted for yet.

I also do not think we need to define what a stable CIR format means today; that is probably too early. I see this more as a capability that we can bring up gradually. It is hard to predict up front how useful it will be or how many consumers will rely on it. But I do think making the capability available now is useful for the ecosystem, because it enables experimentation and gives us real consumers to learn from.

If it proves useful and real consumers start using the resume path, I expect the required boundaries and serialized state to become clearer over time. If maintaining the boundary becomes a continuous burden without enough use cases to justify it, that would also tell us something about whether this is a direction worth pursuing.

Is there a preferred way in Clang to explicitly mark this kind of capability as experimental, so that making the round trip available does not imply a stability or compatibility guarantee for the CIR artifact?

As a non-CAT member (that is, as a CIR person), I agree with all of this. CIR is already experimental, so having the ‘stopping points’ be also experimental as we mess with it/mature the product seems perfectly acceptable to me.

IMO, we should just accept this right now, and let the experimentation happen, and re-visit what points are the ‘actual reliable’ stopping points in the future.

CAN we as contributors just ‘accept’ this? @bcardosolopes : You and I were the only ‘mildly against’ here, do you object to just accepting this?

@efriedma-quic / @AaronBallman / @cor3ntin / @shafik : IF Bruno is ok with it too, can we out-of-band-accept this as CAT?

1 Like

The Clang Area Team can make out-of-band decisions, so there’s nothing stopping us from doing that.

So long as it’s understood that experimental also means “might be removed entirely someday”, I am personally okay with the RFC.

2 Likes

I accept this :slight_smile:

1 Like

@erichkeane I think there is enough support to accept but given that there are a bunch of ideas floating around, I’d prefer we only modify the -cc1 -emit-cir flag if we can. Especially if we might remove the whole thing if it does not pan out or someone comes up with a better design.

1 Like

I’d prefer we only modify the -cc1 -emit-cir flag if we can.

sounds reasonable -emit-cir=keyword should be ok. We can decide the keywords discussion when I get to the respective PR. Thank you.