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.func
  • emitc.func
  • spirv.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.func for GPU kernel functions.
  • Use llvm.func after committing to LLVM-level ABI and LLVM dialect types.
  • Use emitc.func when lowering to C/C++ emission.
  • Use spirv.func when 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.return is rewritten into the appropriate branch or value replacement.
  • no_inline on 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_llvm
  • transform.func.cast_and_call
  • transform.func.replace_func_signature
  • transform.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.func to llvm.func
  • func.call to llvm.call
  • func.call_indirect to LLVM-compatible indirect call form
  • func.constant to LLVM address/function value materialization
  • func.return to llvm.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.
  • index bitwidth can be derived from data layout or overridden with index-bitwidth.
  • use-bare-ptr-memref-call-conv can use bare pointers for memref arguments.
  • func.varargs is interpreted for variadic LLVM signatures.
  • Explicit llvm.* attributes on func.func can 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.func to emitc.func
  • func.call to EmitC-compatible calls
  • func.return to emitc.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.func to spirv.func
  • func.call to spirv.FunctionCall
  • func.return to spirv.Return or spirv.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-llvm
  • convert-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.func is isolated from above. Body operations cannot implicitly capture outside SSA values.
  • A bodyless func.func is an external declaration.
  • func.call references a symbol, not an SSA function value.
  • Use func.constant plus func.call_indirect for function-typed SSA values.
  • func.return operands must exactly match the enclosing function result types.
  • Only dialect attribute names are allowed in function, argument, and result attribute dictionaries.
  • no_inline on 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.td
  • mlir/include/mlir/Dialect/Func/IR/FuncOps.h
  • mlir/lib/Dialect/Func/IR/FuncOps.cpp
  • mlir/include/mlir/Dialect/Func/Transforms/Passes.td
  • mlir/lib/Dialect/Func/Transforms/
  • mlir/include/mlir/Dialect/Func/Extensions/InlinerExtension.h
  • mlir/lib/Dialect/Func/Extensions/InlinerExtension.cpp
  • mlir/include/mlir/Dialect/Func/TransformOps/FuncTransformOps.td
  • mlir/lib/Dialect/Func/TransformOps/FuncTransformOps.cpp
  • mlir/include/mlir/Conversion/FuncToLLVM/
  • mlir/lib/Conversion/FuncToLLVM/FuncToLLVM.cpp
  • mlir/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.call
  • func.call_indirect
  • func.constant
  • func.func
  • func.return

Func-related passes and transforms covered:

  • inline
  • duplicate-function-elimination
  • symbol-dce
  • symbol-privatize
  • buffer-results-to-out-params
  • scalarize-single-element-tensor-return
  • set-llvm-module-datalayout
  • transform.apply_conversion_patterns.func.func_to_llvm
  • transform.func.cast_and_call
  • transform.func.replace_func_signature
  • transform.func.deduplicate_func_args

Conversion paths covered:

  • convert-func-to-llvm
  • convert-func-to-emitc
  • convert-func-to-spirv
  • convert-to-llvm
  • convert-to-emitc