74 emitc Dialect
74.1 Beginner Summary
The emitc dialect represents C and C++ code inside MLIR. It is used near the end of a lowering pipeline when the goal is to print readable C/C++ instead of lowering to LLVM IR, SPIR-V, or another binary-oriented target.
For a beginner, the important idea is that EmitC is source-oriented. It has operations for C-like arithmetic, function calls, variables, assignments, arrays, pointers, includes, globals, control flow, classes, fields, and raw verbatim text. The final step is usually the mlir-to-cpp translation, which prints EmitC IR as C/C++ code.
Use EmitC when your compiler should produce C or C++ as an output artifact. Do not use it as a replacement for arith, scf, memref, or func early in a compiler pipeline.
74.2 Why This Dialect Exists
Many compiler pipelines lower to LLVM or a hardware dialect. Some pipelines instead need a portable C or C++ source file. This can be useful for:
- Generating code for platforms where a C/C++ compiler is the final backend.
- Integrating with build systems or embedded toolchains.
- Emitting readable reference code.
- Lowering MLIR programs into C APIs, C++ classes, or C-like kernels.
- Preserving source-level constructs that would be awkward after LLVM lowering.
The emitc dialect exists because C/C++ has concepts that generic SSA dialects do not model directly: lvalues, declarations, includes, arrays as source types, pointer syntax, class fields, raw code fragments, and operator precedence.
74.3 When It Matters
EmitC matters when the pipeline target is source code rather than object code. It is especially relevant after the program has already been simplified into structured loops, scalar arithmetic, memref-like access, and function calls.
Typical users include:
- Code generators that want to emit C99 or C++11-compatible code.
- MLIR examples and integrations that need a human-readable output target.
- Embedded or accelerator flows that use vendor C/C++ compilers downstream.
- Frontends that want to lower a restricted MLIR program to portable source.
- Tests that check how MLIR constructs print as C/C++.
74.4 When To Use It
Use emitc when:
- You are deliberately targeting C or C++ source emission.
- Your IR is already close to C-like structured control flow.
- You need explicit arrays, pointers, lvalues, assignments, and source-level calls.
- You need to inject target-specific C/C++ names with opaque calls or verbatim text.
- You want to use
mlir-translate -mlir-to-cpp.
Avoid using EmitC when:
- You still need high-level tensor, vector, affine, or sparse optimizations.
- You need LLVM-level ABI control, instruction selection, or binary codegen.
- You want aggressive target-specific optimization inside MLIR.
- The code depends on C/C++ semantics that the EmitC emitter does not model.
74.5 Core Concepts
74.5.1 Source-Oriented IR
EmitC is closer to C/C++ source than to machine IR. Operations such as emitc.add, emitc.mul, emitc.cmp, and emitc.call_opaque are intended to print as source expressions. Statement-like operations such as emitc.assign, emitc.if, emitc.for, emitc.switch, and emitc.return print as C/C++ statements.
74.5.2 Lvalues
C and C++ distinguish values from locations that can be assigned to. EmitC models assignable locations with !emitc.lvalue<T>.
Common producers of lvalues include:
emitc.variableemitc.get_globalemitc.subscriptemitc.memberemitc.member_of_ptremitc.dereference
Use emitc.load to read an lvalue as an SSA value, and emitc.assign to write an SSA value to an lvalue.
74.5.3 EmitC Types
Important EmitC-specific types include:
!emitc.array<...>: source-level C/C++ array type.!emitc.ptr<T>: pointer toT.!emitc.lvalue<T>: assignable location containingT.!emitc.opaque<"...">: a C/C++ type printed exactly as text.!emitc.size_t: unsignedsize_t.!emitc.ssize_t: signed size type corresponding tossize_t.!emitc.ptrdiff_t: pointer difference type.
The dialect also accepts many builtin integer, index, and floating-point types when they have a clear C/C++ spelling.
74.5.4 Opaque Escape Hatches
EmitC deliberately has escape hatches:
#emitc.opaque<"...">represents an opaque attribute printed as text.!emitc.opaque<"...">represents an opaque type printed as text.emitc.call_opaquecalls a named external function or intrinsic-like spelling.emitc.literalcreates an expression from literal text.emitc.verbatimemits raw C/C++ text, optionally substituting operands.
These are useful, but they shift correctness to the producer. The verifier cannot fully understand arbitrary C++ text.
74.5.5 Expressions
The emitc.expression op groups expression operations so the C++ emitter can inline them with correct precedence and avoid unnecessary temporary variables. The form-expressions pass can wrap C-operator operations into expression regions and fold single-use expressions into users.
74.5.6 C++ Emitter
The final printer is registered as mlir-to-cpp. It expects the translated operation, and almost all operations in that region, to be EmitC-compatible. It has useful options:
-declare-variables-at-top: declare variables at the beginning of functions.-file-id=<id>: emit only matchingemitc.fileregions.
74.6 Operations
EmitC operations are best understood by source-code role.
74.6.1 Modules, Files, Includes, And Raw Text
emitc.filegroups code for a named output file id. The C++ translation can filter by-file-id.emitc.includeemits an include directive, either quoted or angled.emitc.verbatimemits raw source text and can substitute operands into placeholders.
74.6.2 Functions And Calls
emitc.funcdefines an EmitC function with an SSACFG region.emitc.returnreturns from anemitc.func.emitc.declare_funcemits a function declaration for a referenced function symbol.emitc.callcalls anemitc.funcsymbol.emitc.call_opaqueemits a call to a named C/C++ function or expression that is not modeled as an MLIR symbol.emitc.member_call_opaqueemits an opaque member call, such as a C++ method call on a receiver.
74.6.3 Values, Variables, Loads, And Assignments
emitc.constantcreates a typed constant from a typed attribute or an opaque attribute.emitc.literalcreates a source expression from literal text.emitc.variabledeclares a mutable local variable and returns an lvalue or array value.emitc.loadreads an!emitc.lvalue<T>into a value of typeT.emitc.assignwrites a value into an lvalue.emitc.yieldterminates regions in EmitC control-flow and expression operations.
74.6.4 Arithmetic, Logical, And Comparison Operators
emitc.addemits+.emitc.subemits-.emitc.mulemits*.emitc.divemits/.emitc.rememits%.emitc.unary_plusemits unary+.emitc.unary_minusemits unary-.emitc.bitwise_andemits bitwise&.emitc.bitwise_oremits bitwise|.emitc.bitwise_xoremits bitwise^.emitc.bitwise_notemits bitwise~.emitc.bitwise_left_shiftemits<<.emitc.bitwise_right_shiftemits>>.emitc.logical_andemits logical&&.emitc.logical_oremits logical||.emitc.logical_notemits logical!.emitc.cmpemits comparison operators such as==,!=,<,<=,>,>=, and C++ three-way comparison.emitc.conditionalemits a ternary conditional expression.emitc.castemits a cast between EmitC-supported types.
74.6.5 Pointers, Arrays, Members, And Globals
emitc.address_ofemits address-of on an lvalue.emitc.dereferenceemits pointer dereference and returns an lvalue.emitc.subscriptemits array, pointer, or opaque subscription and returns an lvalue.emitc.memberemits member access with..emitc.member_of_ptremits member access with->.emitc.globaldefines a global variable.emitc.get_globalobtains access to a global variable.
74.6.6 Structured Control Flow
emitc.ifemits an if or if-else statement.emitc.foremits a structured for loop.emitc.doemits a do-while loop.emitc.switchemits a switch statement with case regions and an optional default.
74.6.7 Classes And Fields
emitc.classdefines a C++ class, struct, or union-like aggregate.emitc.fielddefines a field inside anemitc.class.emitc.get_fieldobtains access to a field inside a class instance or class wrapper context.
74.6.8 Expression Grouping
emitc.expressiongroups expression-producing EmitC operations into one C/C++ expression. It can be markednoinlinewhen a producer needs a separate temporary instead of inline expression emission.
74.7 Transformations
EmitC-specific transformations include:
form-expressions: wraps EmitC C-operator operations inemitc.expressionand folds single-use expressions into users where possible.wrap-emitc-func-in-class: transformsemitc.funcoperations intoemitc.classoperations. Function arguments become fields, and the function body becomes a member method. Thefunc-nameoption controls the generated member function name, defaulting tooperator().
The broader conversion passes that produce EmitC are:
convert-to-emitc: generic conversion to EmitC through dialect interfaces. Options includefilter-dialectsandlower-to-cpp.convert-arith-to-emitc: converts supportedarithoperations to EmitC operators and constants.convert-func-to-emitc: convertsfunc.func, calls, and returns to EmitC functions and calls. It has alower-to-cppoption.convert-math-to-emitc: converts supportedmathoperations toemitc.call_opaquecalls targeting libc/libm-style functions. Thelanguage-targetoption can selectc99orcpp11.convert-memref-to-emitc: converts supportedmemrefoperations to EmitC arrays, pointers, subscripts, loads, stores, allocations, and copies. It has alower-to-cppoption.convert-scf-to-emitc: converts structuredscfcontrol flow to EmitC structured statements while preserving structured control flow.
74.8 Conversions/Lowering Paths
A common EmitC pipeline is:
- Lower high-level tensor, linalg, vector, or domain-specific IR to scalar, structured, and memory-like IR.
- Convert structured control flow with
convert-scf-to-emitc. - Convert scalar arithmetic with
convert-arith-to-emitc. - Convert math calls with
convert-math-to-emitcif needed. - Convert memory operations with
convert-memref-to-emitc. - Convert functions with
convert-func-to-emitc. - Optionally run
form-expressionsto make the output more source-like. - Optionally run
wrap-emitc-func-in-classfor C++ class wrappers. - Print with
mlir-translate -mlir-to-cpp.
The generic convert-to-emitc pass can use dialect conversion interfaces to delegate conversion pattern population to dialects that know how to lower themselves to EmitC.
EmitC does not usually lower onward to LLVM. It is itself a target-facing representation. The “lowering” after EmitC is source translation to C/C++ text.
74.9 Example IR
This example emits a simple C-like function with arithmetic operators.
emitc.include <"math.h">
emitc.func @saxpy(%x: f32, %y: f32, %a: f32) -> f32 {
%prod = emitc.mul %a, %x : (f32, f32) -> f32
%sum = emitc.add %prod, %y : (f32, f32) -> f32
emitc.return %sum : f32
}
This example shows source-level array access, lvalues, and assignment.
emitc.func @store(%buffer: !emitc.array<4xf32>, %i: !emitc.size_t, %value: f32) {
%slot = emitc.subscript %buffer[%i] : (!emitc.array<4xf32>, !emitc.size_t) -> !emitc.lvalue<f32>
emitc.assign %value : f32 to %slot : !emitc.lvalue<f32>
emitc.return
}
This example shows a C++ class wrapper shape.
emitc.class final @Model {
emitc.field @weights : !emitc.array<4xf32>
emitc.func @execute() {
%w = emitc.get_field @weights : !emitc.array<4xf32>
emitc.return
}
}
This example shows file grouping and a forward declaration.
emitc.file "header" {
emitc.include <"stdint.h">
emitc.declare_func @run
emitc.func @run() {
emitc.return
}
}
74.10 Mental Model
Think of EmitC as “C/C++ before printing.” It is still MLIR, so it has SSA values, verifiers, regions, symbols, and passes. But the meaning of each operation is tied to the source text it will emit.
The central question is not “what machine instruction does this become?” The question is “what C/C++ source fragment does this represent?”
That explains many design choices:
!emitc.lvalue<T>exists because assignment and address-of need locations.emitc.expressionexists because source code has nested expressions and precedence.emitc.opaqueexists because C/C++ ecosystems often need names and types MLIR does not model.emitc.verbatimexists because no source dialect can model every target header, macro, pragma, or vendor API.emitc.fileexists because source generation may produce more than one output file.
74.11 Gotchas
- EmitC is a target dialect. Running high-level optimizations after converting to EmitC is usually too late.
- Opaque types, opaque attributes,
emitc.literal, andemitc.verbatimare powerful but weakly checked. Use them for target integration, not as a way to avoid modeling important semantics. emitc.call_opaqueis not a symbol call. It prints a named call-like expression and assumes the generated source environment provides it.emitc.declare_funcmust reference a validemitc.funcsymbol.!emitc.arraymodels source arrays, which have different behavior than memrefs or LLVM pointers.!emitc.lvalue<T>is not an SSA value of typeT; useemitc.loadto read it.emitc.assignwrites to an lvalue and returns no SSA result.emitc.expressionaffects printing. It is not a general-purpose region container.- The
mlir-to-cpptranslation expects almost all relevant IR to be EmitC-compatible. Leftover high-level dialect operations usually cause translation failures. emitc.verbatimcan easily generate invalid C/C++ if the format string and operands do not match the intended output.
74.12 Source Map
Primary source files:
mlir/include/mlir/Dialect/EmitC/IR/EmitCBase.tdmlir/include/mlir/Dialect/EmitC/IR/EmitC.tdmlir/include/mlir/Dialect/EmitC/IR/EmitCTypes.tdmlir/include/mlir/Dialect/EmitC/IR/EmitCAttributes.tdmlir/include/mlir/Dialect/EmitC/IR/EmitCInterfaces.tdmlir/include/mlir/Dialect/EmitC/Transforms/Passes.tdmlir/lib/Dialect/EmitC/IR/EmitC.cppmlir/lib/Dialect/EmitC/Transforms/FormExpressions.cppmlir/lib/Dialect/EmitC/Transforms/WrapFuncInClass.cppmlir/include/mlir/Conversion/Passes.tdmlir/lib/Conversion/ConvertToEmitC/mlir/lib/Conversion/ArithToEmitC/mlir/lib/Conversion/FuncToEmitC/mlir/lib/Conversion/MathToEmitC/mlir/lib/Conversion/MemRefToEmitC/mlir/lib/Conversion/SCFToEmitC/mlir/include/mlir/Target/Cpp/CppEmitter.hmlir/lib/Target/Cpp/TranslateToCpp.cppmlir/lib/Target/Cpp/TranslateRegistration.cpp
Useful tests:
mlir/test/Dialect/EmitC/ops.mlirmlir/test/Dialect/EmitC/types.mlirmlir/test/Dialect/EmitC/attrs.mlirmlir/test/Dialect/EmitC/form-expressions.mlirmlir/test/Dialect/EmitC/wrap-func-in-class.mlirmlir/test/Conversion/MemRefToEmitC/mlir/test/Conversion/ConvertToEmitC/mlir/test/Target/Cpp/