40 func Dialect
40.1 Beginner Summary
The func dialect provides ordinary function structure in MLIR.
It defines:
func.func: a named function or external declaration.func.call: a direct call to a named function symbol.func.constant: a function symbol as an SSA value.func.call_indirect: an indirect call through a function-typed value.func.return: a function return terminator.
For beginners, func is the dialect that makes MLIR modules look like programs with functions.
Most dialects describe computation inside a function. The func dialect describes the function boundary: names, arguments, results, calls, returns, visibility, and attributes on function arguments/results.
40.2 Why This Dialect Exists
MLIR is multi-level. Some IR is high-level tensor code. Some IR is low-level LLVM-like code. Many pipelines still need a common way to say:
this is a callable function
this function has these inputs and results
this call invokes that symbol
this return leaves the function
The func dialect provides that common callable layer.
It avoids committing too early to a target ABI. A func.func can hold tensor, memref, vector, index, dialect-specific, or target-independent types while the compiler is still transforming the program.
Later, the function boundary can be lowered to target-specific forms such as:
llvm.funcemitc.funcspirv.func
This separation matters because function signatures are one of the hardest places in a compiler pipeline. The signature is where values cross from one callable body to another, where ABI choices appear, and where type conversion must update both definitions and call sites consistently.
40.3 When It Matters
The func dialect matters in almost every MLIR pipeline.
It appears when IR needs:
- Named functions.
- External function declarations.
- Direct calls.
- Indirect calls.
- Function return values.
- Symbol visibility.
- Function attributes.
- Argument and result attributes.
- Inlining.
- Function signature conversion.
- Call graph updates.
- Lowering to LLVM, SPIR-V, or EmitC.
A typical flow looks like this:
module
-> func.func definitions and declarations
-> dialect-specific operations inside function bodies
-> inlining, symbol cleanup, function-boundary transforms
-> convert-func-to-llvm / convert-func-to-emitc / convert-func-to-spirv
-> target-specific function representation
The func dialect is not about one particular computation domain. It is about program structure.
40.4 When To Use It
Use func when your IR needs normal callable functions.
Use it for:
- Top-level functions in a module.
- Helper functions generated by lowering passes.
- External declarations for runtime calls.
- Direct calls by symbol.
- Indirect calls through function values.
- Function arguments and results before final ABI lowering.
- Function-level attributes such as target metadata, no-inline markers, or dialect-specific annotations.
Do not use func when a more specific callable abstraction is required.
Examples:
- Use
gpu.funcfor GPU kernel functions. - Use
llvm.funcafter committing to LLVM-level ABI and LLVM dialect types. - Use
emitc.funcwhen lowering to C/C++ emission. - Use
spirv.funcwhen targeting SPIR-V function representation.
The func dialect is the portable middle layer.
40.5 Core Concepts
40.5.1 Functions Are Symbols
A func.func is a symbol operation. It has a symbol name:
func.func @add(%lhs: f32, %rhs: f32) -> f32 {
...
}
A direct call refers to that symbol:
%0 = func.call @add(%x, %y) : (f32, f32) -> f32
This is different from an SSA value. The function definition is not passed directly as a block argument. Calls use symbol references.
40.5.2 Functions Are Isolated From Above
func.func is IsolatedFromAbove.
That means operations inside a function cannot implicitly capture SSA values defined outside the function. Values must enter through function arguments or through symbolic references such as attributes.
This property is important for:
- Parallel compiler passes.
- Function-level analyses.
- Cloning and outlining.
- Symbol-based call graph reasoning.
40.5.3 External Declarations
A function without a body is an external declaration:
func.func private @external_scale(f32) -> f32
Declarations are useful for runtime calls or functions that will be supplied by another module or library.
40.5.4 Function Type
The type of a func.func is a built-in function type:
(input types) -> (result types)
For example:
func.func @pair(%x: i32, %y: f32) -> (i32, f32) {
func.return %x, %y : i32, f32
}
The operands of every func.return must match the function result types.
The operands and result types of every func.call must match the callee’s function type.
40.5.5 Direct Versus Indirect Calls
Direct call:
%0 = func.call @add(%x, %y) : (f32, f32) -> f32
Indirect call:
%fn = func.constant @add : (f32, f32) -> f32
%0 = func.call_indirect %fn(%x, %y) : (f32, f32) -> f32
Use direct calls when the callee is statically known. Use indirect calls when the callee is represented as a value of function type.
40.5.6 Attributes On Functions, Arguments, And Results
func.func can carry:
- Function attributes.
- Argument attributes.
- Result attributes.
Example:
func.func private @with_attrs(
%x: i32 {test.readonly}
) -> (i32 {test.result}) attributes {test.marker} {
func.return %x : i32
}
Only dialect attribute names may be used in function, argument, and result attribute dictionaries. This prevents generic, unowned attribute names from becoming ambiguous at important ABI boundaries.
40.5.7 Visibility
Functions can have symbol visibility such as:
- public/default visibility
private- nested visibility forms supported by MLIR symbols
Private functions are internal to the current symbol table. This matters for inlining, symbol DCE, duplicate elimination, and target lowering.
40.5.8 No-Inline
Both func.func and func.call can carry a no_inline marker.
The Func inliner extension checks this marker. A call is not legal to inline when either the call operation or the callable function says no-inline.
40.6 Operations
40.6.1 func.func
func.func defines a named function or external declaration.
func.func @identity(%x: i32) -> i32 {
func.return %x : i32
}
Important properties:
- It is a symbol.
- It implements
FunctionOpInterface. - It has one SSACFG region when defined.
- It can be external when it has no body.
- It is isolated from above.
- It creates an automatic allocation scope.
- It is an affine scope.
- It can carry argument and result attributes.
40.6.2 func.call
func.call directly calls a function symbol in the same symbol scope.
%0 = func.call @identity(%x) : (i32) -> i32
The callee is stored as a symbol reference attribute. The call’s operand and result types must match the referenced function type.
The custom syntax often prints as:
%0 = call @identity(%x) : (i32) -> i32
The operation is still func.call.
40.6.3 func.constant
func.constant creates an SSA value that refers to a function symbol.
%fn = func.constant @identity : (i32) -> i32
This is needed because MLIR does not use ordinary SSA values to directly capture function objects. A function symbol can be materialized as a value and then passed to func.call_indirect.
40.6.4 func.call_indirect
func.call_indirect calls a value of function type.
%fn = func.constant @add : (f32, f32) -> f32
%0 = func.call_indirect %fn(%x, %y) : (f32, f32) -> f32
The callee value must have a function type. The operand and result types are checked against that function type.
40.6.5 func.return
func.return terminates a func.func body.
func.return %value : i32
It has no results. Its operands must match the enclosing function’s result types.
When a function returns nothing, the custom syntax can be:
func.return
or printed as:
return
40.7 Transformations
40.7.1 Inlining
The generic inline pass can inline func.call operations when the Func inliner extension is registered.
For Func:
- Function bodies are legal to inline.
- Operations inside function bodies are legal to inline.
func.returnis rewritten into the appropriate branch or value replacement.no_inlineon the call or callee prevents inlining.
Inlining is one of the most important transformations involving the func dialect because it changes call graph structure and exposes optimization opportunities inside callers.
40.7.2 Duplicate Function Elimination
The Func dialect defines:
duplicate-function-elimination
This pass deduplicates functions that are equivalent except for symbol name. It keeps one representative, erases duplicate definitions, and updates call sites.
Use it after transformations that generate many helper functions or specialize functions in ways that may create identical bodies.
40.7.3 Symbol Cleanup
Generic symbol passes often matter around func.func:
symbol-dce: removes dead symbols.symbol-privatize: marks symbols private, with an option to exclude names.
These are not Func-only passes, but they are especially useful because functions are common symbol definitions.
40.7.4 Function Boundary Utilities
Several broader MLIR passes work at function boundaries and commonly affect func.func signatures:
buffer-results-to-out-params: converts memref-typed function results to out-parameters.scalarize-single-element-tensor-return: scalarizes private functions that return single-element tensors.- Bufferization passes can convert tensor function boundaries to memref function boundaries.
These transformations are not all owned by the Func dialect, but beginners will encounter them when function signatures change during lowering.
40.7.5 Transform Dialect Controls
Func also has Transform dialect operations:
transform.apply_conversion_patterns.func.func_to_llvmtransform.func.cast_and_calltransform.func.replace_func_signaturetransform.func.deduplicate_func_args
These are Transform dialect operations that target Func operations or Func conversion patterns.
Use them when a transform script needs to collect Func-to-LLVM conversion patterns, replace value uses with a call, reorder function signatures, or deduplicate function arguments.
40.8 Conversions And Lowering Paths
40.8.1 Func To LLVM
The main low-level conversion is:
convert-func-to-llvm
It lowers:
func.functollvm.funcfunc.calltollvm.callfunc.call_indirectto LLVM-compatible indirect call formfunc.constantto LLVM address/function value materializationfunc.returntollvm.return
Important behavior:
- Function argument types are converted using the LLVM type converter.
- Multiple function results are packed into an LLVM struct.
- Calls and returns are updated to match converted signatures.
indexbitwidth can be derived from data layout or overridden withindex-bitwidth.use-bare-ptr-memref-call-convcan use bare pointers for memref arguments.func.varargsis interpreted for variadic LLVM signatures.- Explicit
llvm.*attributes onfunc.funccan lower to LLVM function properties.
The related pass:
set-llvm-module-datalayout
attaches an LLVM data layout string to the module for use by LLVM conversion.
40.8.2 Func To EmitC
convert-func-to-emitc
lowers Func operations to EmitC operations:
func.functoemitc.funcfunc.callto EmitC-compatible callsfunc.returntoemitc.return
The pass has a lower-to-cpp option. Multi-result functions may be represented with generated EmitC struct-like classes.
40.8.3 Func To SPIR-V
convert-func-to-spirv
lowers supported Func operations to SPIR-V:
func.functospirv.funcfunc.calltospirv.FunctionCallfunc.returntospirv.Returnorspirv.ReturnValue
SPIR-V has stricter function rules than generic Func. For example, multiple return values are not generally converted in the simple pattern path.
The pass has options for emulating narrower scalar types and unsupported float types.
40.8.4 Conversion Interfaces
Func also registers conversion pattern interfaces used by broader conversion pipelines:
convert-to-llvmconvert-to-emitc
This means a pipeline may call a broad conversion pass and still pick up Func conversion patterns through the dialect interface.
40.9 Example IR
40.9.1 Direct Function Call
func.func @add_direct(%lhs: f32, %rhs: f32) -> f32 {
%sum = arith.addf %lhs, %rhs : f32
func.return %sum : f32
}
func.func @caller(%x: f32, %y: f32) -> f32 {
%0 = func.call @add_direct(%x, %y) : (f32, f32) -> f32
func.return %0 : f32
}
@add_direct is a function symbol. func.call references it directly.
40.9.2 External Declaration
func.func private @external_scale(f32) -> f32
func.func @use_external(%x: f32) -> f32 {
%0 = func.call @external_scale(%x) : (f32) -> f32
func.return %0 : f32
}
@external_scale has no body, so it is a declaration. Later lowering can map it to a target-level function declaration.
40.9.3 Indirect Call
func.func @add_indirect(%lhs: f32, %rhs: f32) -> f32 {
%sum = arith.addf %lhs, %rhs : f32
func.return %sum : f32
}
func.func @indirect(%x: f32, %y: f32) -> f32 {
%fn = func.constant @add_indirect : (f32, f32) -> f32
%0 = func.call_indirect %fn(%x, %y) : (f32, f32) -> f32
func.return %0 : f32
}
func.constant materializes a function symbol as a value. func.call_indirect calls through that value.
40.9.4 Argument And Result Attributes
func.func private @with_attrs(
%x: i32 {test.readonly}
) -> (i32 {test.result}) attributes {test.marker} {
func.return %x : i32
}
Function boundary attributes are important because later ABI lowering may need to preserve, translate, or reject them.
40.10 Mental Model
The func dialect is the program skeleton.
Other dialects usually fill in the body:
func.func @kernel(%input: tensor<16xf32>) -> tensor<16xf32> {
%0 = linalg.generic ...
func.return %0 : tensor<16xf32>
}
The func dialect says:
what is callable
what it is named
what it takes
what it returns
who calls it
how control exits it
It does not say much about the computation itself. That is the job of the dialects inside the body.
Good beginner rule:
Use func for function structure.
Use other dialects for the work done inside the function.
Lower func when the ABI target is known.
40.11 Gotchas
func.funcis isolated from above. Body operations cannot implicitly capture outside SSA values.- A bodyless
func.funcis an external declaration. func.callreferences a symbol, not an SSA function value.- Use
func.constantplusfunc.call_indirectfor function-typed SSA values. func.returnoperands must exactly match the enclosing function result types.- Only dialect attribute names are allowed in function, argument, and result attribute dictionaries.
no_inlineon a call or function prevents Func inlining.- Signature conversion must update both function definitions and all call sites.
- Func-to-LLVM conversion has input invariants: no tensors, one-dimensional vectors, and reachable blocks.
- Multiple Func results may lower differently depending on the target. LLVM packs multiple results into a struct; EmitC may generate a struct-like return type; SPIR-V support is more restrictive.
40.12 Source Map
Primary definitions:
mlir/include/mlir/Dialect/Func/IR/FuncOps.tdmlir/include/mlir/Dialect/Func/IR/FuncOps.hmlir/lib/Dialect/Func/IR/FuncOps.cppmlir/include/mlir/Dialect/Func/Transforms/Passes.tdmlir/lib/Dialect/Func/Transforms/mlir/include/mlir/Dialect/Func/Extensions/InlinerExtension.hmlir/lib/Dialect/Func/Extensions/InlinerExtension.cppmlir/include/mlir/Dialect/Func/TransformOps/FuncTransformOps.tdmlir/lib/Dialect/Func/TransformOps/FuncTransformOps.cppmlir/include/mlir/Conversion/FuncToLLVM/mlir/lib/Conversion/FuncToLLVM/FuncToLLVM.cppmlir/include/mlir/Conversion/FuncToEmitC/mlir/lib/Conversion/FuncToEmitC/mlir/include/mlir/Conversion/FuncToSPIRV/mlir/lib/Conversion/FuncToSPIRV/
All Func dialect operations covered in this chapter:
func.callfunc.call_indirectfunc.constantfunc.funcfunc.return
Func-related passes and transforms covered:
inlineduplicate-function-eliminationsymbol-dcesymbol-privatizebuffer-results-to-out-paramsscalarize-single-element-tensor-returnset-llvm-module-datalayouttransform.apply_conversion_patterns.func.func_to_llvmtransform.func.cast_and_calltransform.func.replace_func_signaturetransform.func.deduplicate_func_args
Conversion paths covered:
convert-func-to-llvmconvert-func-to-emitcconvert-func-to-spirvconvert-to-llvmconvert-to-emitc