Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CIR][CIRGen] Add complex type and its CIRGen support #513

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

Lancern
Copy link
Collaborator

@Lancern Lancern commented Mar 17, 2024

This PR adds !cir.complex type to model the _Complex type in C. It also contains support for its CIRGen.

In detail, this patch adds the following CIR types, ops, and attributes:

  • The !cir.complex type is added to model the _Complex type in C. This type is parameterized with the type of the components of the complex number, which must be either an integer type or a floating-point type.
  • The #cir.complex attribute is added to represent a literal value of _Complex type. It is a struct-like attribute that provides the real and imaginary part of the literal _Complex value.
  • The #cir.imag attribute is added to represent a purely imaginary number.
  • The cir.complex.create op is added to create a complex value from its real and imaginary parts.
  • The cir.complex.real and cir.complex.imag op is added to extract the real and imaginary part of a value of !cir.complex type, respectively.
  • The cir.complex.get_real and cir.complex.get_imag op is added to derive a pointer to the real and imaginary part of a value of !cir.complex type, respectively.

CIRGen support for some of the fundamental complex number operations is also included. Note the implementation diverges from the original clang CodeGen, where expressions of complex types are handled differently from scalars and aggregates. Instead, this patch treats expressions of complex types as scalars, as such expressions can be simply lowered to a CIR value of !cir.complex type.

This PR addresses #445 .

Copy link
Member

@bcardosolopes bcardosolopes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is nice, thanks for working on it!

Comments inline but overall I'm just a bit confused by the fact that LLVM skeleton isn't being much followed here (and some comments on CIRGen decisions).

let cppNamespace = "::mlir::cir";
}

def ComplexExtractOp : CIR_Op<"complex.extract", [Pure]> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we split this into two operations: complex.real and complex.imag

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

@@ -200,8 +200,6 @@ static void buildDeclInit(CIRGenFunction &CGF, const VarDecl *D,
case TEK_Scalar:
CGF.buildScalarInit(Init, CGF.getLoc(D->getLocation()), lv, false);
return;
case TEK_Complex:
llvm_unreachable("complext evaluation NYI");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at traditional LLVM codegen:

  case TEK_Complex:
    CGF.EmitComplexExprIntoLValue(Init, lv, /*isInit*/ true);
    return;

Any specific reason why you are not following the skeleton?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this isn't calling buildComplexExprIntoLValue?

clang/lib/CIR/CodeGen/CIRGenClass.cpp Outdated Show resolved Hide resolved
@@ -171,6 +171,25 @@ class ScalarExprEmitter : public StmtVisitor<ScalarExprEmitter, mlir::Value> {
CGF.getLoc(E->getExprLoc()), Ty,
Builder.getAttr<mlir::cir::FPAttr>(Ty, E->getValue()));
}
mlir::Value VisitImaginaryLiteral(const ImaginaryLiteral *E) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also create a CIRGenExprComplex.cpp and place relevant methods there. Looks like you should create a ComplexExprEmitter visitor?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

mlir::TypedAttr real;
mlir::TypedAttr imag;

if (elementTy.isa<mlir::cir::IntType>()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps move getZeroInitAttr to CIRBaseBuilder and call it here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

llvm::function_ref<mlir::InFlightDiagnostic()> emitError,
mlir::Type elementTy) {
if (!elementTy.isa<mlir::cir::IntType, mlir::cir::CIRFPTypeInterface>()) {
emitError() << "element type of !cir.complex must be either a "
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wish there was a way to enforce this in tablegen like there is for operations.

clang/test/CIR/CodeGen/complex.c Outdated Show resolved Hide resolved
// RUN: FileCheck --input-file=%t.cir %s

double _Complex float_complex_basic(void) {
double _Complex x = 2.0 + 3.0i;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused with the approach here, seems a bit less ideal than what LLVM generates: https://godbolt.org/z/3Go78MMh1, looking at the CIR output:

    %2 = cir.const(#cir.fp<2.000000e+00> : !cir.double) : !cir.double loc(#loc5)
    %3 = cir.const(#cir.complex<#cir.fp<0.000000e+00> : !cir.double, #cir.fp<3.000000e+00> : !cir.double> : !cir.complex<!cir.double>) : !cir.complex<!cir.double> loc(#loc4)
    %4 = cir.cast(float_to_complex, %2 : !cir.double), !cir.complex<!cir.double> loc(#loc5)
    %5 = cir.binop(add, %4, %3) : !cir.complex<!cir.double> loc(#loc81)

Why doesn't %3 already builds the complex? Seems off that CIRGen first creates %2, followed by bitcast + addition.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question still remains, sounds like we could do a better initialization job here. It's also fine if you plan to do this as incremental improvement, but I'm mostly looking to hear more about the plan here.

Example:

```mlir
!fcmp = !cir.complex<!cir.float>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!fcmp might sounds like "float comparison", perhaps use !complex to make the docs more readable.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

@Lancern
Copy link
Collaborator Author

Lancern commented Mar 18, 2024

@bcardosolopes Hi Bruno, thanks for your review! Let me explain in more details about why I'm not following the skeleton here.

The original clang CodeGen divide expressions into three categories according to their types: 1) scalar, 2) complex, and 3) aggregate. CodeGen handles these three categories of expressions differently:

  • CodeGen of a scalar expression yields a llvm::Value * that represent the prvalue result of the expression. Note that scalar expressions are always prvalue.
  • CodeGen of a complex expression yields a std::pair<llvm::Value *, llvm::Value *> that represent the prvalue result of the expression. The two llvm::Value * represent the real part and the imaginary part of the result complex value, respectively. Note that complex expressions are also always prvalue.
  • CodeGen of an aggregate expression does not yield anything. Instead, a destination address must be given to the CodeGen and the aggregate produced by the expression will be emitted into that location.

So the CodeGen of scalar expressions and complex expressions are actually very similar. I believe the reason why CodeGen distinguishes them is that CodeGen of scalar expressions yields llvm::Value * while CodeGen of complex expressions yields std::pair<llvm::Value *, llvm::Value *>.

OK, that's enough of background story. In CIR, what makes a difference is that instead of using something like std::pair<mlir::Value, mlir::Value> to represent a complex value, we now have the !cir.complex type which allows us to use a single mlir::Value to represent a complex value. In other words, in CIR, if we keep following the skeleton:

  • CIRGen of a scalar expression yields a mlir::Value that represents the prvalue result.
  • CIRGen of a complex expression also yields a mlir::Value that represents the prvalue result.
  • CIRGen of an aggregate expression keeps what it is.

If we follow the skeleton, the code in CIRGenExprScalar.cpp and CIRGenExprComplex.cpp (which hasn't been invented) would be very very similar to each other, resulting in unnecessary redundancy. Thus I decided to treat expressions of complex types as scalar expressions in CIRGen. This eases the implementation greatly and it still follow the intuition.

I hope this explanation is clear enough for you! If you are still confused please let me know.

@bcardosolopes
Copy link
Member

bcardosolopes commented Mar 19, 2024

Thanks for the detailed explanation (very clarifying) and for thinking about code duplication!

If we follow the skeleton, the code in CIRGenExprScalar.cpp and CIRGenExprComplex.cpp (which hasn't been invented) would be very very similar to each other, resulting in unnecessary redundancy. Thus I decided to treat expressions of complex types as scalar expressions in CIRGen. This eases the implementation greatly and it still follow the intuition.

clang/lib/CodeGen/CGExprComplex.cpp is quite big. Are you confident that current missing complex codegen will just apply with all scalar stuff? I have the feeling that as soon as something differs, we'll need to change scalar emitters to take complex types into account, and because we're assuming scalar handles it all, we won't have asserts to catch what's not currently implemented - so might end up with bad codegen which will take time to investigate and debug.

I believe duplication will lead us to converge faster and have less issues. How about you add a FIXME(cir): unlike traditional CodeGen, this shares common code with ScalarExprEmitter to every method that is the same between ComplexExprEmitter and ScalarExprEmitter, so that when we complete complex support, we can decide if there's a refactoring approach we could take?

@lanza lanza force-pushed the main branch 2 times, most recently from ed3955a to 8b7417c Compare March 23, 2024 05:07
@Lancern
Copy link
Collaborator Author

Lancern commented Mar 23, 2024

How about you add a FIXME(cir): unlike traditional CodeGen, this shares common code with ScalarExprEmitter to every method that is the same between ComplexExprEmitter and ScalarExprEmitter, so that when we complete complex support, we can decide if there's a refactoring approach we could take?

OK I believe this is also acceptable and it allows us to evaluate this divergence more deeply before we can make further decisions. I'll move the complex CIRGen code to a new CIRGenComplex.cpp file and keep the upstream code structure for now.

@Lancern
Copy link
Collaborator Author

Lancern commented Mar 23, 2024

Rebased onto the latest main, but not yet started to work on the reviews.

@Lancern Lancern marked this pull request as draft March 24, 2024 15:11
@Lancern Lancern marked this pull request as ready for review March 29, 2024 14:30
@bcardosolopes
Copy link
Member

@Lancern sorry, I was out on vacation. Let me know when you address all reviews and update the PR.

@Lancern
Copy link
Collaborator Author

Lancern commented Apr 6, 2024

@bcardosolopes Hi Bruno, I have updated the implementation so that it now follows the upstream skeleton.

Copy link
Member

@bcardosolopes bcardosolopes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice. This is in the right direction, thanks the updates. More comments inline!

```
}];

let results = (outs CIR_AnyType:$result);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the constraint be CIR_AnyIntOrFloat?

```
}];

let results = (outs CIR_AnyType:$result);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the constraint be CIR_AnyIntOrFloat?


%0 = cir.const(#cir.int<1> : !u32i) : !u32i
%1 = cir.const(#cir.int<2> : !u32i) : !u32i
%2 = cir.complex.create(%0 : !u32i, %1) : !complex
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This example is probably best if done in term of building a complex number out of non-constant pieces, because for this one specific we could be using the complex attribute to initialize a constant complex.

}];

let results = (outs CIR_ComplexType:$result);
let arguments = (ins CIR_AnyType:$real, CIR_AnyType:$imag);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the operands be constrained in terms of CIR_AnyIntOrFloat?

@@ -200,8 +200,6 @@ static void buildDeclInit(CIRGenFunction &CGF, const VarDecl *D,
case TEK_Scalar:
CGF.buildScalarInit(Init, CGF.getLoc(D->getLocation()), lv, false);
return;
case TEK_Complex:
llvm_unreachable("complext evaluation NYI");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this isn't calling buildComplexExprIntoLValue?

}

mlir::Value ComplexExprEmitter::VisitInitListExpr(InitListExpr *E) {
if (E->getNumInits() == 2) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for introducing the skeleton and for all the added testcases. However, this new file is quite big and introduces lots of CIRGen that I don't see tested (example, this one). Few options here:

  • Add more testcases and exercise each of the visitors.
  • Delete non-tested visitors and add llvm_unreacheable to them. Later on, do incremental patches to add more visitors and their correspondent tests.

@@ -684,6 +684,15 @@ void CIRGenFunction::buildScalarInit(const Expr *init, mlir::Location loc,
return;
}

void CIRGenFunction::buildComplexInit(const Expr *init, mlir::Location loc,
LValue lvalue) {
// TODO: this is where a lot of ObjC lifetime stuff would be done.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs an assert+unimplemented.

number value of the specified complex type.

The `real` parameter gives the real part of the complex number, and the
`imag` parameter gives the imaginary part of the complex number.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an example of it's use with cir.const.

def ComplexCreateOp : CIR_Op<"complex.create", [Pure, SameTypeOperands]> {
let summary = "Create a new complex value from its real and imaginary parts";
let description = [{
The `cir.complex.create` operation takes two operands of the same type and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any use of this operation in the tests, please make sure it shows up.

// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir-enable -emit-cir %s -o %t.cir
// RUN: FileCheck --input-file=%t.cir %s
// RUN: %clang_cc1 -x c++ -triple x86_64-unknown-linux-gnu -fclangir-enable -emit-cir %s -o %t.cir
// RUN: FileCheck --input-file=%t.cir %s
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be nice to have a test/CIR/IR/complex.cir test for printing and parsing as well.

@bcardosolopes
Copy link
Member

Let me know once this is ready for review again!

@bcardosolopes
Copy link
Member

@lanza rebased over the weekend, sorry for the churn. Can you rebase this PR again?

@Lancern
Copy link
Collaborator Author

Lancern commented May 12, 2024

Rebased onto the latest main.

This patch adds an initial support for the C complex type, i.e. `_Complex`. It
introduces the following new types, attributes, and operations:

- `!cir.complex`, which represents the C complex number type;
- `#cir.imag`, which represents an imaginary number literal in C;
- `cir.complex.create`, which creates a complex number from its real and
  imaginary parts;
- `cir.complex.get_real`, which derives a pointer to the real part of a complex
  number given a pointer to the complex number;
- `cir.complex.get_imag`, which derives a pointer to the imaginary part of a
  complex number given a pointer to the complex number.

CIRGen for some basic complex number operations is also included in this patch.
@Lancern Lancern marked this pull request as ready for review May 14, 2024 14:39
@Lancern
Copy link
Collaborator Author

Lancern commented May 14, 2024

@bcardosolopes Hi Bruno, I believe this PR is now ready for another round of review.

I decide to keep this PR simpler (but the size of the PR is still quite big). In the latest version I made the following adjustments:

  • I replaced the #cir.complex attribute with the #cir.imag attribute. The former represents a whole complex number, while the latter represents a complex number that is purely imaginary (i.e. its real part is 0), which can be written in C as a number literal followed by the i suffix (e.g. 3.0i will become #cir.imag<#cir.fp<3.0>>).
  • I replaced the cir.complex.real and cir.complex.imag with cir.complex.get_real and cir.complex.get_imag, respectively. The main difference here is the get_* version yields pointers to the corresponding complex number component just like what cir.get_member is doing. This change is to support the __real__ and __imag__ operator which yield lvalues.
  • I removed a lot of code in CGExprComplex.cpp and left most visitors as unimplemented for now. Only a few fundamental complex number operations are supported in this PR so code review can be easier, including:
    • List initialization of complex numbers that initializes both of the real part and the imaginary part;
    • Imaginary literals;
    • Load and store of complex number values;
    • __real__ and __imag__ operator.

All supported operations are tested in clang/test/CIR/CodeGen/complex.c . Support for more complex number operations can be added in future PRs.

Copy link
Member

@bcardosolopes bcardosolopes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decide to keep this PR simpler (but the size of the PR is still quite big).

Thanks, I appreciate that.

I replaced the #cir.complex attribute with the #cir.imag attribute. The former represents a whole complex number, while the latter represents a complex number that is purely imaginary (i.e. its real part is 0), which can be written in C as a number literal followed by the i suffix (e.g. 3.0i will become #cir.imag<#cir.fp<3.0>>).

After looking the PR, I'm not sure we need #cir.imag, it should just be an int or fp attribute directly.

//      CHECK:   %[[#REAL:]] = cir.const #cir.fp<1.000000e+00> : !cir.double
// CHECK-NEXT:   %[[#IMAG:]] = cir.const #cir.fp<2.000000e+00> : !cir.double
// CHECK-NEXT:   %{{.+}} = cir.complex.create %[[#REAL]], %[[#IMAG]] : !cir.double -> !cir.complex<!cir.double>

This composes better with what cir.complex.create ends up doing (building imaginary out of regulaer fp/int directly).

I replaced the cir.complex.real and cir.complex.imag with cir.complex.get_real and cir.complex.get_imag, respectively. The main difference here is the get_* version yields pointers to the corresponding complex number component just like what cir.get_member is doing. This change is to support the real and imag operator which yield lvalues.

I'll address this in another comment.

I removed a lot of code in CGExprComplex.cpp and left most visitors as unimplemented for now. Only a few fundamental complex number operations are supported in this PR so code review can be easier, including

Awesome!

All supported operations are tested in clang/test/CIR/CodeGen/complex.c . Support for more complex number operations can be added in future PRs.

Can you please add LLVM IR support and test it altogether in complex.c?

#ifdef __cplusplus__
#define VOID
#else
#define VOID void
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: please remove the VOID thing and just use empty paren.

@@ -0,0 +1,130 @@
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir -o - %s | FileCheck %s --check-prefixes=C,CHECK
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please write files to disk, it makes it convenient to run locally and look at the output

// CHECK: }

void get_real(VOID) {
double *r1 = &__real__ c;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add another example where instead of c being global, its another local complex variable.


void imag_literal(VOID) {
c = 3.0i;
ci = 3i;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here in imag_literal()

    %0 = cir.const #cir.imag<#cir.fp<3.000000e+00> : !cir.double> : !cir.complex<!cir.double>
    %1 = cir.get_global @c : !cir.ptr<!cir.complex<!cir.double>>
    cir.store %0, %1 : !cir.complex<!cir.double>, !cir.ptr<!cir.complex<!cir.double>>

How do you plan to distinguish if the cir.store is storing to real or imag part when lowering this? Relying on the imag attribute sounds brittle. Another reason to add LLVM lowering as part of the work, it'd have popped up this kind of question / design issue. This should be using the same mechanism as elsewhere:

    %0 = cir.const #cir.imag<#cir.fp<3.000000e+00> : !cir.double> : !cir.complex<!cir.double>
    %1 = cir.get_global @c : !cir.ptr<!cir.complex<!cir.double>>
    %3 = cir.complex.get_img %1 : !cir.ptr<!cir.complex<!cir.double>> -> !cir.ptr<!cir.double>
    cir.store %0, %3 : !cir.complex<!cir.double>, !cir.ptr<!cir.complex<!cir.double>>

Please rename cir.complex.get_img to cir.complex.imag_ptr, cir.complex.get_real to cir.complex.real_ptr.


void list_init(VOID) {
double _Complex c1 = {1.0, 2.0};
int _Complex c2 = {1, 2};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add another testcase that the list initialization happens with two params passed to the function.

// CPP: cir.func @_Z9list_initv()
// CHECK: %[[#REAL:]] = cir.const #cir.fp<1.000000e+00> : !cir.double
// CHECK-NEXT: %[[#IMAG:]] = cir.const #cir.fp<2.000000e+00> : !cir.double
// CHECK-NEXT: %{{.+}} = cir.complex.create %[[#REAL]], %[[#IMAG]] : !cir.double -> !cir.complex<!cir.double>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  %0 = cir.const #cir.fp<1.000000e+00> : !cir.double
  %1 = cir.const #cir.fp<2.000000e+00> : !cir.double
  %2 = cir.complex.create %0, %1 : !cir.complex<!cir.double>
  cir.store %2, ... : !cir.ptr<!cir.complex<!cir.double>>

I prefer keeping the #cir.complex to be used directly with cir.const, it composes well with the rest of CIR consts. By applying that change, cir.complex.create becomes only useful for "merging" two non-constants fp/ints in order to build a !cir.complex. But this brings the question if this is really useful, two possible approaches:

  1. We remove it in favor of "get_real/get_imag + store to each", which reuses existing ops. A single store would still be used for when cir.const creates the complex type, which also brings some inconsistency.
  2. We keep it. Useful to "join" two non-constants into a complex type so that we only have one final store instead of two, theoretically easier to track on analysis. The downside is having an extra op to build and later lower.

Perhaps we should do (2) because it's easier to translate to MLIR's complex dialect?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants