12 Module
builtin.module is MLIR’s usual top-level container. It is itself an operation with one region, but it establishes the symbol scope and compilation-unit boundary that most tools and pipelines use.
module {
func.func @add_one(%x: i32) -> i32 {
%one = arith.constant 1 : i32
%result = arith.addi %x, %one : i32
return %result : i32
}
}
A module is not simply “the file.” It is an isolated IR container. It can be parsed, cloned, transformed, and serialized because operations inside it cannot silently capture arbitrary SSA values from outside it.
12.1 Isolation Creates A Compilation Boundary
The module operation is IsolatedFromAbove. Its nested operations must refer to outside entities through explicit mechanisms: symbols, attributes, imports, or allowed operation interfaces. A function body may capture a value from an enclosing structured operation, but an operation inside a module cannot reach through the module boundary to use an unrelated SSA value.
This restriction gives MLIR important engineering properties. Symbol lookup has an unambiguous scope. A module can be transformed in parallel with another module. Cloning it does not leave hidden SSA edges pointing to its old parent. Separate compilation and device-code packaging become representable IR operations rather than informal tool conventions.
12.2 The Module Body Is Usually A Symbol Table
The module body is one block whose operations are commonly symbols: functions, globals, aliases, dialect declarations, or target-specific objects.
module {
func.func private @helper(%x: i32) -> i32
func.func @entry(%x: i32) -> i32 {
%result = func.call @helper(%x) : (i32) -> i32
return %result : i32
}
}
@helper is a symbol, not an SSA value. A direct call stores a symbol-reference attribute, and resolution finds the definition in the relevant enclosing symbol table. That indirection supports forward declarations, recursion, linking, and references that survive writing the IR to disk.
Symbol scope is lexical. Nested symbol tables create nested scopes, and two symbols with the same spelling may be distinct if they are owned by different modules or nested containers. Rename and lookup through SymbolTable APIs, never with string replacement.
12.3 Module-Level Configuration
A module may carry target, data-layout, debug, or dialect-specific attributes. They are part of the compilation context, but no dialect interprets them merely because they exist. A pass that converts index to a fixed-width integer, for example, must use an explicit target or data-layout policy and preserve it for later passes.
This is why module attributes belong in correctness discussions. Losing a target attribute while cloning or merging modules can make later lowering choose a different ABI or pointer width. Copying every attribute blindly can also be wrong if an attribute names a resource that is no longer present. The pass must know which module-level facts it owns.
12.4 Modules In Pipelines
Pipelines commonly run a top-level module pass and nest function passes inside it. Module-level work includes symbol resolution, interprocedural analysis, linking, global optimization, and target setup. Function passes handle local rewrites. The boundary is useful but not absolute: a module pass may traverse functions, and a function pass may query module context. What matters is that each pass declares its scope, dialect dependencies, and analysis invalidation.