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

[mlir][emitc] Add a declare_func operation #80297

Merged
merged 2 commits into from
Feb 6, 2024
Merged

Conversation

marbre
Copy link
Member

@marbre marbre commented Feb 1, 2024

This adds the emitc.declare_func operation that allows to emit the declaration of an emitc.func at a specific location.

@llvmbot
Copy link
Member

llvmbot commented Feb 1, 2024

@llvm/pr-subscribers-mlir

@llvm/pr-subscribers-mlir-emitc

Author: Marius Brehler (marbre)

Changes

This adds the emitc.declare_func operation that allows to emit the declaration of an emitc.func at a specific location.


Full diff: https://github.com/llvm/llvm-project/pull/80297.diff

6 Files Affected:

  • (modified) mlir/include/mlir/Dialect/EmitC/IR/EmitC.td (+42)
  • (modified) mlir/lib/Dialect/EmitC/IR/EmitC.cpp (+18)
  • (modified) mlir/lib/Target/Cpp/TranslateToCpp.cpp (+39-6)
  • (modified) mlir/test/Dialect/EmitC/invalid_ops.mlir (+10)
  • (modified) mlir/test/Dialect/EmitC/ops.mlir (+2)
  • (added) mlir/test/Target/Cpp/declare_func.mlir (+16)
diff --git a/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td b/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
index 5c8c3c9ce7bb3..fe004e441e9f9 100644
--- a/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
+++ b/mlir/include/mlir/Dialect/EmitC/IR/EmitC.td
@@ -460,6 +460,48 @@ def EmitC_CallOp : EmitC_Op<"call",
   }];
 }
 
+def EmitC_DeclareFuncOp : EmitC_Op<"declare_func", [
+  DeclareOpInterfaceMethods<SymbolUserOpInterface>
+]> {
+  let summary = "An operation to declare a function";
+  let description = [{
+    The `declare_func` operation allows to insert a function declaration for an
+    `emitc.func` at a specific position. The operation only requires the `callee`
+    of the `emitc.func` to be specified as an attribute.
+
+    Example:
+
+    ```mlir
+    emitc.declare_func @bar
+    emitc.func @foo(%arg0: i32) -> i32 {
+      %0 = emitc.call @bar(%arg0) : (i32) -> (i32)
+      emitc.return %0 : i32
+    }
+
+    emitc.func @bar(%arg0: i32) -> i32 {
+      emitc.return %arg0 : i32
+    }
+    ```
+
+    ```c++
+    // Code emitted for the operations above.
+    int32_t bar(int32_t v1);
+    int32_t foo(int32_t v1) {
+      int32_t v2 = bar(v1);
+      return v2;
+    }
+
+    int32_t bar(int32_t v1) {
+      return v1;
+    }
+    ```
+  }];
+  let arguments = (ins FlatSymbolRefAttr:$callee);
+  let assemblyFormat = [{
+    $callee attr-dict
+  }];
+}
+
 def EmitC_FuncOp : EmitC_Op<"func", [
   AutomaticAllocationScope,
   FunctionOpInterface, IsolatedFromAbove
diff --git a/mlir/lib/Dialect/EmitC/IR/EmitC.cpp b/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
index df489c6d90fb1..67bcb7079672e 100644
--- a/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
+++ b/mlir/lib/Dialect/EmitC/IR/EmitC.cpp
@@ -393,6 +393,24 @@ FunctionType CallOp::getCalleeType() {
   return FunctionType::get(getContext(), getOperandTypes(), getResultTypes());
 }
 
+//===----------------------------------------------------------------------===//
+// DeclareFuncOp
+//===----------------------------------------------------------------------===//
+
+LogicalResult
+DeclareFuncOp::verifySymbolUses(SymbolTableCollection &symbolTable) {
+  // Check that the callee attribute was specified.
+  auto fnAttr = (*this)->getAttrOfType<FlatSymbolRefAttr>("callee");
+  if (!fnAttr)
+    return emitOpError("requires a 'callee' symbol reference attribute");
+  FuncOp fn = symbolTable.lookupNearestSymbolFrom<FuncOp>(*this, fnAttr);
+  if (!fn)
+    return emitOpError() << "'" << fnAttr.getValue()
+                         << "' does not reference a valid function";
+
+  return success();
+}
+
 //===----------------------------------------------------------------------===//
 // FuncOp
 //===----------------------------------------------------------------------===//
diff --git a/mlir/lib/Target/Cpp/TranslateToCpp.cpp b/mlir/lib/Target/Cpp/TranslateToCpp.cpp
index c0c6105409f8d..28aef02e72566 100644
--- a/mlir/lib/Target/Cpp/TranslateToCpp.cpp
+++ b/mlir/lib/Target/Cpp/TranslateToCpp.cpp
@@ -14,6 +14,7 @@
 #include "mlir/IR/BuiltinTypes.h"
 #include "mlir/IR/Dialect.h"
 #include "mlir/IR/Operation.h"
+#include "mlir/IR/SymbolTable.h"
 #include "mlir/Support/IndentedOstream.h"
 #include "mlir/Support/LLVM.h"
 #include "mlir/Target/Cpp/CppEmitter.h"
@@ -847,8 +848,9 @@ static LogicalResult printFunctionBody(CppEmitter &emitter,
       // needs to be printed after the closing brace.
       // When generating code for an emitc.for and emitc.verbatim op, printing a
       // trailing semicolon is handled within the printOperation function.
-      bool trailingSemicolon = !isa<cf::CondBranchOp, emitc::ForOp, emitc::IfOp,
-                                    emitc::LiteralOp, emitc::VerbatimOp>(op);
+      bool trailingSemicolon =
+          !isa<cf::CondBranchOp, emitc::DeclareFuncOp, emitc::ForOp,
+               emitc::IfOp, emitc::LiteralOp, emitc::VerbatimOp>(op);
 
       if (failed(emitter.emitOperation(
               op, /*trailingSemicolon=*/trailingSemicolon)))
@@ -923,6 +925,37 @@ static LogicalResult printOperation(CppEmitter &emitter,
   return success();
 }
 
+static LogicalResult printOperation(CppEmitter &emitter,
+                                    DeclareFuncOp declareFuncOp) {
+  CppEmitter::Scope scope(emitter);
+  raw_indented_ostream &os = emitter.ostream();
+
+  auto functionOp = SymbolTable::lookupNearestSymbolFrom<emitc::FuncOp>(
+      declareFuncOp, declareFuncOp.getCalleeAttr());
+
+  if (!functionOp)
+    return failure();
+
+  if (functionOp.getSpecifiers()) {
+    for (Attribute specifier : functionOp.getSpecifiersAttr()) {
+      os << cast<StringAttr>(specifier).str() << " ";
+    }
+  }
+
+  if (failed(emitter.emitTypes(functionOp.getLoc(),
+                               functionOp.getFunctionType().getResults())))
+    return failure();
+  os << " " << functionOp.getName();
+
+  os << "(";
+  Operation *operation = functionOp.getOperation();
+  if (failed(printFunctionArgs(emitter, operation, functionOp.getArguments())))
+    return failure();
+  os << ");";
+
+  return success();
+}
+
 CppEmitter::CppEmitter(raw_ostream &os, bool declareVariablesAtTop)
     : os(os), declareVariablesAtTop(declareVariablesAtTop) {
   valueInScopeCount.push(0);
@@ -1236,10 +1269,10 @@ LogicalResult CppEmitter::emitOperation(Operation &op, bool trailingSemicolon) {
           // EmitC ops.
           .Case<emitc::AddOp, emitc::ApplyOp, emitc::AssignOp, emitc::CallOp,
                 emitc::CallOpaqueOp, emitc::CastOp, emitc::CmpOp,
-                emitc::ConstantOp, emitc::DivOp, emitc::ExpressionOp,
-                emitc::ForOp, emitc::FuncOp, emitc::IfOp, emitc::IncludeOp,
-                emitc::MulOp, emitc::RemOp, emitc::ReturnOp, emitc::SubOp,
-                emitc::VariableOp, emitc::VerbatimOp>(
+                emitc::ConstantOp, emitc::DeclareFuncOp, emitc::DivOp,
+                emitc::ExpressionOp, emitc::ForOp, emitc::FuncOp, emitc::IfOp,
+                emitc::IncludeOp, emitc::MulOp, emitc::RemOp, emitc::ReturnOp,
+                emitc::SubOp, emitc::VariableOp, emitc::VerbatimOp>(
               [&](auto op) { return printOperation(*this, op); })
           // Func ops.
           .Case<func::CallOp, func::ConstantOp, func::FuncOp, func::ReturnOp>(
diff --git a/mlir/test/Dialect/EmitC/invalid_ops.mlir b/mlir/test/Dialect/EmitC/invalid_ops.mlir
index 707f9a5b23b0b..b95fafa379ff7 100644
--- a/mlir/test/Dialect/EmitC/invalid_ops.mlir
+++ b/mlir/test/Dialect/EmitC/invalid_ops.mlir
@@ -326,3 +326,13 @@ emitc.func @func_variadic(...)
 
 // expected-error@+1 {{'emitc.func' op does not support empty function bodies}}
 emitc.func private @empty()
+
+// -----
+
+// expected-error@+1 {{'emitc.declare_func' op 'bar' does not reference a valid function}}
+emitc.declare_func @bar
+
+// -----
+
+// expected-error@+1 {{'emitc.declare_func' op requires attribute 'callee'}}
+"emitc.declare_func"()  : () -> ()
diff --git a/mlir/test/Dialect/EmitC/ops.mlir b/mlir/test/Dialect/EmitC/ops.mlir
index b41333faa4d4e..81ad5a2b46334 100644
--- a/mlir/test/Dialect/EmitC/ops.mlir
+++ b/mlir/test/Dialect/EmitC/ops.mlir
@@ -15,6 +15,8 @@ func.func @f(%arg0: i32, %f: !emitc.opaque<"int32_t">) {
   return
 }
 
+emitc.declare_func @func
+
 emitc.func @func(%arg0 : i32) {
   emitc.call_opaque "foo"(%arg0) : (i32) -> ()
   emitc.return
diff --git a/mlir/test/Target/Cpp/declare_func.mlir b/mlir/test/Target/Cpp/declare_func.mlir
new file mode 100644
index 0000000000000..72c087a3388e2
--- /dev/null
+++ b/mlir/test/Target/Cpp/declare_func.mlir
@@ -0,0 +1,16 @@
+// RUN: mlir-translate -mlir-to-cpp %s | FileCheck %s
+
+// CHECK: int32_t bar(int32_t [[V1:[^ ]*]]);
+emitc.declare_func @bar
+// CHECK: int32_t bar(int32_t [[V1:[^ ]*]]) {
+emitc.func @bar(%arg0: i32) -> i32 {
+    emitc.return %arg0 : i32
+}
+
+
+// CHECK: static inline int32_t foo(int32_t [[V1:[^ ]*]]);
+emitc.declare_func @foo
+// CHECK: static inline int32_t foo(int32_t [[V1:[^ ]*]]) {
+emitc.func @foo(%arg0: i32) -> i32 attributes {specifiers = ["static","inline"]} {
+    emitc.return %arg0 : i32
+}

@marbre
Copy link
Member Author

marbre commented Feb 1, 2024

@AlexDenisov if you don't mind I would like to ask you for a code review.

Copy link

@simon-camp simon-camp left a comment

Choose a reason for hiding this comment

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

I just took a quick look and will review in more detail tomorrow.

mlir/include/mlir/Dialect/EmitC/IR/EmitC.td Outdated Show resolved Hide resolved
Copy link
Member

@AlexDenisov AlexDenisov left a comment

Choose a reason for hiding this comment

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

That's a great and much needed feature. I used some terrible workarounds to achieve the same, and ended up using VerbatimOp.

I'd add a test case for when the prototype is emitted inside the function body as well.

It looks like this feature would be incomplete without support of external functions (mlir::func::FuncOp which are only declaring the functions).
I'd expect this case to naturally map to this operation? Otherwise the use case is rather limited.

mlir/lib/Dialect/EmitC/IR/EmitC.cpp Outdated Show resolved Hide resolved
@marbre
Copy link
Member Author

marbre commented Feb 1, 2024

That's a great and much needed feature. I used some terrible workarounds to achieve the same, and ended up using VerbatimOp.

I'd add a test case for when the prototype is emitted inside the function body as well.

Well @simon-camp and me already started discussion the value of a function declaration within a function body. While this is legal in C, it is not as soon as it is a static function. Therefore, I see three option:

  • Don't allow function declarations in function bodies at all.
  • Allow function declarations in function bodies, but disallow declarations for static functions.
  • Allow function declarations anyway and check if the output results in legal C in a validation pass (a validation pass is something I am currently working on).

It looks like this feature would be incomplete without support of external functions (mlir::func::FuncOp which are only declaring the functions). I'd expect this case to naturally map to this operation? Otherwise the use case is rather limited.

To make sure I understand what you're proposing: A func.func with empty body should be converted to a emitc.declare_func with an extern specifier?

@AlexDenisov
Copy link
Member

Allow function declarations in function bodies, but disallow declarations for static functions.
Allow function declarations anyway and check if the output results in legal C in a validation pass (a validation pass is something I am currently working on).

These two sound better than disallowing them completely.

To make sure I understand what you're proposing: A func.func with empty body should be converted to a emitc.declare_func with an extern specifier?

Yes, exactly. Although, I'm not sure about the extern specifier: it would help to have a mechanism to specify extern "C" without having a conflict.

@simon-camp
Copy link

I agree with the need to declare external functions. We can't rewrite an external func.func to a emitc.declare_func though. The former provides a symbol while the latter refrences an existing symbol. We can convert it to an emitc.func with empty body and adjust the emitter to print it correctly as a declaration. Then extern "C" would simply go into the specifier attribute.

So the emitc.declare_func is an operation that can be automatically inserted by a legalization pass to make the compiler happy for cases like mutually recursive functions.

int isEven(unsigned int n) {
   return n == 0 ? 1 : isOdd(n-1);
}

int isOdd(unsigned int n) {
  return n == 1 = 1 : isEven(n-1);
}

@marbre
Copy link
Member Author

marbre commented Feb 2, 2024

I agree with the need to declare external functions. We can't rewrite an external func.func to a emitc.declare_func though. The former provides a symbol while the latter refrences an existing symbol. We can convert it to an emitc.func with empty body and adjust the emitter to print it correctly as a declaration. Then extern "C" would simply go into the specifier attribute.

I'll prototype something and see if we have other options. If those don't work out and if we follow this approach, we could (more or less) land the current implementation of emitc.declare_func as is. Any objections @simon-camp @AlexDenisov?

@simon-camp
Copy link

None other than resolving the two open inline discussions. 👍

@marbre marbre force-pushed the emitc.declare_func branch from 213f721 to 589be75 Compare February 4, 2024 09:23
@marbre
Copy link
Member Author

marbre commented Feb 4, 2024

None other than resolving the two open inline discussions. 👍

For now I added a verifier that checks if the parent is a ModuleOp. However, I am not sure if this is too restrictive. E.g. what about function declarations within IREE's VM modules @simon-camp? Is this still something we want or need to do? If though, I would rather drop to model this restriction as part of the IR and would rather go for a validator.

auto funcOp =
SymbolTable::lookupNearestSymbolFrom<emitc::FuncOp>((*this), fnAttr);

if (!isa<ModuleOp>((*this)->getParentOp()) && funcOp.isPrivate())

Choose a reason for hiding this comment

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

We shouldn't be using isPrivate here. The symbol visibility is not neccessarily in sync with the specifiers.

Copy link
Member Author

Choose a reason for hiding this comment

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

If it's not, something other is broken but I agree that can be the case. That's another point why a validation pass might be more favorable here. Beside that, going through all specifiers args in the array adds more a much larger overhead to the verifier.

@simon-camp
Copy link

For now I added a verifier that checks if the parent is a ModuleOp. However, I am not sure if this is too restrictive. E.g. what about function declarations within IREE's VM modules @simon-camp? Is this still something we want or need to do? If though, I would rather drop to model this restriction as part of the IR and would rather go for a validator.

I would leave this out for now and see how it works out. Seems easier to add restrictions later.

This adds the `emitc.declare_func` operation that allows to emit the
declaration of an `emitc.func` at a specific location.
@marbre marbre force-pushed the emitc.declare_func branch from 589be75 to 0864be4 Compare February 5, 2024 16:04
@marbre
Copy link
Member Author

marbre commented Feb 5, 2024

For now I added a verifier that checks if the parent is a ModuleOp. However, I am not sure if this is too restrictive. E.g. what about function declarations within IREE's VM modules @simon-camp? Is this still something we want or need to do? If though, I would rather drop to model this restriction as part of the IR and would rather go for a validator.

I would leave this out for now and see how it works out. Seems easier to add restrictions later.

I dropped the change and will add it to our todo list for the validation pass.

@marbre marbre requested a review from simon-camp February 5, 2024 16:05
@marbre marbre merged commit 03881dc into llvm:main Feb 6, 2024
5 checks passed
@marbre marbre deleted the emitc.declare_func branch February 6, 2024 07:49
mgehre-amd pushed a commit to Xilinx/llvm-project that referenced this pull request Mar 11, 2024
This adds the `emitc.declare_func` operation that allows to emit the
declaration of an `emitc.func` at a specific location.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants