7  Value

Values are the dataflow edges of MLIR. An operation produces results, blocks introduce block arguments, and both are Value objects in the in-memory IR. Every use of a value records a directed dependency from a definition to an operation that consumes it. Most of the compiler’s job is to inspect, preserve, or safely rewrite this graph.

%x = arith.constant 4 : i32
%y = arith.constant 5 : i32
%sum = arith.addi %x, %y : i32
%twice = arith.muli %sum, %y : i32

%x, %y, %sum, and %twice are values. Their textual names make the program readable, but they are not variables in the source-language sense. MLIR does not mutate %sum; a later computation creates a different value. This is the static single-assignment (SSA) discipline.

7.1 Two Kinds Of Definition

Every ordinary MLIR value is one of two things:

  • an operation result, defined by a particular operation; or
  • a block argument, defined at the start of a block.
func.func @scale(%input: tensor<?xf32>, %factor: f32) -> tensor<?xf32> {
  %result = arith.mulf %factor, %factor : f32
  return %input : tensor<?xf32>
}

%input and %factor are entry-block arguments for the function body. %result is an operation result. Function parameters, loop induction variables, loop-carried values, and values passed through a branch are all block arguments, even though their source-level roles differ.

This distinction matters in C++ and in transformations. An operation result has a defining operation; a block argument instead has an owning block and an argument number. Code that assumes value.getDefiningOp() always succeeds is wrong for function parameters and CFG-carried values.

7.2 SSA Is A Scope And Dominance Rule

SSA does not merely mean “one name, one assignment.” A use must be in scope and dominated by its definition. In a single block, a result must be defined before its use. In a control-flow graph, the definition must lie on every path that reaches the use, unless the value is passed explicitly as a block argument.

^bb0:
  %one = arith.constant 1 : i32
  cf.br ^bb1(%one : i32)
^bb1(%incoming: i32):
  %two = arith.constant 2 : i32
  %sum = arith.addi %incoming, %two : i32

The branch operand %one is not a hidden assignment to %incoming. Rather, the edge into ^bb1 supplies the value for that block argument. This design generalizes SSA phi nodes without a separate phi operation. A block argument is the merge point; each predecessor provides one argument for it.

Regions introduce lexical scope as well. A nested operation can normally use a value from an enclosing region, but a value defined inside a loop body cannot escape the body unless the enclosing operation exposes it as a result. This prevents accidental dependencies on temporary loop-local values.

7.3 Uses Are First-Class Information

MLIR tracks uses of each value. If %sum is used by five operations, the IR can enumerate those five use sites directly. This enables central compiler operations:

  • dead-code elimination asks whether a result has any uses and whether its defining operation is effect-free;
  • replacement redirects every use to a new value;
  • liveness and dominance analyses follow definition-use relationships;
  • pattern rewrites check whether an operation has one use or many before changing its shape.

Consider replacing a redundant add:

%zero = arith.constant 0 : i32
%same = arith.addi %x, %zero : i32
%out = arith.muli %same, %y : i32

If integer semantics and any relevant flags make the replacement valid, a canonicalization pattern replaces all uses of %same with %x, then erases the add when it has no uses. It does not rename source text. In C++, this is typically an operation on use lists through replaceAllUsesWith or a PatternRewriter equivalent.

7.4 Types Belong To Values

A value always has an MLIR Type. The type is not a comment and cannot be changed in place. A rewrite that needs a different type must introduce a new value of that type and adapt its users, often through a materialization or cast operation.

%i = arith.constant 7 : i32
%f = arith.sitofp %i : i32 to f32

%i and %f have different types and different definitions. The conversion operation makes the semantic change explicit. This is especially important for tensor-to-buffer lowering, index-width lowering, and dialect conversion: the same conceptual quantity may require a different representation and new operations to bridge it.

7.5 Values Are Not Always Runtime Scalars

The word “value” can sound like an integer in a register, but MLIR values can represent tensors, references to memory, asynchronous tokens, handles used by the Transform dialect, blocks of GPU work, symbolic objects, or target-specific machine-level entities. The type and dialect define what a value means.

For example, a tensor<?xf32> value is a value-like aggregate: operations produce a new tensor rather than mutate an existing one. A memref<?xf32> value denotes a reference to mutable storage. Passing either through the same SSA machinery does not erase the semantic difference. Whether an operation can be reordered depends on its effects and the representation’s contract, not on the fact that both are printed with % names.

7.6 Loop-Carried And Region-Carried Values

Structured control flow uses block arguments and terminators to make changing state explicit. Here a loop carries an accumulator across iterations:

%sum = scf.for %i = %c0 to %n step %c1 iter_args(%acc = %initial) -> (i32) {
  %next = arith.addi %acc, %i : i32
  scf.yield %next : i32
}

%acc is a block argument of the loop body. On the first iteration it receives %initial; later it receives the preceding iteration’s %next. The yield supplies the next edge value, and the loop’s result %sum is the final carried value. This is SSA’s explicit dataflow form of a familiar mutable accumulator.

The order and types of iter_args, block arguments, scf.yield operands, and operation results must agree. The verifier checks this. When a rewrite adds or removes a carried value, it must update all four connected pieces, not just the loop header.

7.7 Common Errors In Passes

Do not store a raw Value past the lifetime of its defining IR unless the pass owns the rewrite ordering and knows it remains valid. Do not erase an operation while one of its results still has uses. Do not replace a value with one that does not dominate all of the old uses. Do not assume a Value has a defining operation, and do not confuse a null/empty Value sentinel in C++ with a valid IR value.

When a rewrite changes a value’s users, use PatternRewriter APIs inside a rewrite pattern. They keep replacement, insertion points, and listener notifications coherent. Direct mutation is appropriate only when you fully understand the pass framework’s requirements.