ClangIR Code Duplication Roadmap — Clang 22.1.8 documentation

# Clang 22.1.8 documentation

## ClangIR Code Duplication Roadmap

## Introduction

This document describes the general approach to code duplication in the ClangIR code generation implementation. It acknowledges specific problems with the current implementation, discusses strategies for mitigating the risk inherent in the current approach, and describes a general long-term plan for addressing the issue.

## Background

The ClangIR code generation is very closely modeled after Clang’s LLVM IR code generation, and we intend for the CIR produced to eventually be semantically equivalent to the LLVM IR produced when not going through ClangIR. However, we acknowledge that as the ClangIR implementation is under development, there will be differences in semantics, both because we have not yet implemented all features of the classic codegen and because the CIR dialect is still evolving and does not yet have a way to represent all of the necessary semantics.

We have chosen to model the ClangIR code generation directly after the classic codegen, to the point of following identical code structure, using similar names and often duplicating the logic because this seemed to be the most certain path to producing equivalent results. Having such nearly identical code allows for direct comparison between the CIR codegen and the LLVM IR codegen to find what is missing or incorrect in the CIR implementation.

However, we recognize that this is not a sustainable permanent solution. As bugs are fixed and new features are added to the classic codegen, the process of keeping the analogous CIR code up to date will be a purely manual process.

Long term, we need a more sustainable approach.

## Current Strategy

Practical considerations require that we make steady progress towards a working implementation of ClangIR. This necessity is directly opposed to the goal of minimizing code duplication.

For this reason, we have decided to accept a large amount of code duplication in the short term, even with the explicit understanding that this is producing a significant amount of technical debt as the project progresses.

As the CIR implementation is developed, we often note small pieces of code that could be shared with the classic codegen if they were moved to a different part of the source, such as a shared utility class in some directory available to both codegen implementations or by moving the function into a related AST class. It is left to the discretion of the developer and reviewers to decide whether such refactoring should be done during the CIR development, or if it is sufficient to leave a comment in the code indicating this as an opportunity for future improvement. Because much of the current code is likely to change when the long term code sharing strategy is complete, we will lean towards only implementing refactorings that make sense independent of the code sharing problem.

We have discussed various ways that major classes such as CGCXXABI/CIRGenCXXABI could be refactored to allow parts of there implementation to be shared today through inheritence and templated base classes. However, this may prove to be wasted effort when the permanent solution is developed. Also, deferring this kind of intertwined implementation prevents introducing cross-dependencies that would make it more difficult to remove one IR code generation implementation without degrading the quality of the other. Therefore, we have decided that it is better to accept significant amounts of code duplication now, and defer this type of refactoring until it is clear what the permanent solution will be.

## Mitigation Through Testing

The most important tactic that we are using to mitigate the risk of CIR diverging from classic codegen is to incorporate two sets of LLVM IR checks in the CIR codegen LIT tests. One set checks the LLVM IR that is produced by first generating CIR and then lowering that to LLVM IR. Another set checks the LLVM IR that is produced directly by the classic codegen.

At the time that tests are created, we compare the LLVM IR output from these two paths to verify (manually) that any meaningful differences between them are the result of known missing features in the current CIR implementation. Whenever possible, differences are corrected in the same PR that the test is being added, updating the CIR implementation as it is being developed.

However, these tests serve a second purpose. They also serve as sentinels to alert us to changes in the classic codegen behavior that will need to be accounted for in the CIR implementation. While we appreciate any help from developers contributing to classic codegen, our current expectation is that it will be the responsibility of the ClangIR contributors to update the CIR implementation when these tests fail.

As the CIR implementation gets closer to the goal of IR that is semantically equivalent to the LLVM IR produced by the classic codegen, we would like to enhance the CIR tests to perform some automatic verification of the equivalence of the generated LLVM IR, perhaps using a combination of tools such as opt -pass-normalize and Alive2.

Eventually, we would like to be able to run all existing classic codegen tests using the CIR path as well.

## Other Considerations

The close modeling of CIR after classic codegen has also meant that the CIR dialect often represents language details at a much lower level than it ideally should.

In the interest of having a complete working implementation of ClangIR as soon as is practical, we have chosen to take the approach of following the classic codegen implementation closely in the initial implementation and only raising the representation in the CIR dialect to a higher level when there is a clear and immediate benefit to doing so.

Over time, we expect to progressively raise the CIR representation to a higher level and remove low level details, including ABI-specific handling from the dialect. (See the “Long Term Vision” section below for more details.) Having a working implementation in place makes it easier to verify that the high-level representation and subsequent lowering are correct.

## Mixing With Other Dialects

Mixing of dialects is a central design feature of MLIR. The CIR dialect is currently more self-contained than most dialects, but even now we generate the ACC (OpenACCC) dialect in combination with CIR, and when support for OpenMP and CUDA are added, similar mixing will occur.

We also expect CIR to be at least partially lowered to other dialects during the optimization phase to enable features such as data dependence analysis, even if we will eventually be lowering it to LLVM IR.

Therefore, any plan for generating LLVM IR from CIR must be integrated with the general MLIR lowering design, which typically involves lowering to the LLVM dialect, which is then transformed to LLVM IR.

## Other Consumers of CIR and MLIR

We must also consider that we will not always be lowering CIR to LLVM IR. CIR, usually mixed with other dialects, will also be directed to offload targets and other code generators through interfaces that are opaque to Clang, such as SPIR-V and MLIR core dialects. We must still produce semantically correct CIR for these consumers.

## Long Term Vision

As the CIR implementation matures, we will eliminate target-specific handling from the high-level CIR generated by Clang. The high-level CIR will then be progressively lowered to a form that is closer to LLVM IR, including a pass that inserts ABI-specific handling, potentially representing the target-specific details in another dialect. More complex transformations, such as library-aware idiom recognition or advanced loop representations—may occur later in the compilation pipeline through additional passes, which can be controlled by specific compiler flags.

As we raise CIR to this higher level implementation, there will naturally be less code duplication, and less need to have the same logic repeated in the CIR generation.

We will continue to use that same basic design and structure for CIR code generation, with classes like CIRGenModule and CIRGenFunction that serve the same purpose as their counterparts in classic codegen, but the handling there will be more closely tied to core semantics and therefore less likely to require frequent changes to stay in sync with classic codegen.

As the handling of low-level details is moved to later lowering phases, we will need to move away from the current tight coupling of the CIR and classic codegen implementations. As this happens, we will look for ways that this handling can be moved to new classes that are specifically designed to be shared among clients that are targeting different IR substrates. That is, rather than trying to overlay reuse onto the existing implementations, we will replace relevant parts of the existing implementation, piece by piece, as appropriate, with new implementations that perform the same function but with a more general design.

## Example: C Calling Convention Handling

C calling convention handling is an example of a general purpose redesign that is already underway. This was started independently of CIR, but it will be directly useful for lowering from high-level call representation in CIR to a representation that includes the target- and calling convention-specific details of function signatures, parameter type coercion, and so on.

The current CIR implementation duplicates most of the classic codegen handling for function call handling, but it omits several pieces that handle type coercion. This leads to an implementation that has all of the complexity of the class codegen without actually achieving the goals of that complexity. It will be a significant improvement to the CIR implementation to simplify the function call handling in such a way that it generates a high-level representation of the call, while preserving all information that will be needed to lower the call to an ABI-compliant representation in a later phase of compilation.

This provides a clear example where trying to refactor the classic codegen in some way to be reused by CIR would have been counterproductive. The classic codegen implementation was tightly coupled with Clang’s LLVM IR generation. The implementation is being completely redesigned to allow general reuse, not just by CIR, but also by other front ends.

The CIR calling convention lowering will make use of the general purpose C calling convention library that is being created, but it should create an MLIR transform pass on top of that library that is general enough to be used by other dialects, such as FIR, that also need the same calling convention handling.

## Significant Areas For Improvement

The following list enumerates some of the areas where significant restructuring of the code is needed to enable better code sharing between CIR and classic codegen. Each of these areas is relatively self-contained in the codegen implementation, making the path to a shared implementation relatively clear.

- Constant expression evaluation

- Complex multiplication and division expansion

- Builtin function handling

- Exception Handling and C++ Cleanups

- Inline assembly handling

- C++ ABI Handling

  - VTable generation

  - Virtual function calls

  - Constructor and destructor arguments

  - Dynamic casts

  - Base class address calculation

  - Type descriptors

  - Array new and delete

## Pervasive Low-Level Issues

This section lists some of the features where a non-trivial amount of code is shared between CIR and classic codegen, but the handling of the feature is distributed across the codegen implementation, making it more difficult to design an abstraction that can easily be shared.

- Global variable and function linkage

- Alignment management

- Debug information

- TBAA handling

- Sanitizer integration

- Lifetime markers