Reussir Language Reference

MLIR Transform

MLIR Transform

Reussir exposes a deliberate path from source code into its MLIR compilation pipeline. A program can identify a kernel, then use MLIR’s Transform dialect to describe how the lowered operations should be matched and rewritten. This is useful when an algorithm author knows a profitable schedule—such as a specific tiling or unrolling strategy—that a general-purpose optimizer cannot safely choose for every workload.

Payload and schedule

Transform dialect separates two kinds of IR:

  • The payload is the program being compiled: functions, loops, array views, arithmetic, reference-count operations, and eventually LLVM operations.
  • The schedule contains transform operations. It searches the payload for handles to selected operations and applies rewrites through those handles.

The schedule does not run at program runtime. RRC interprets it while compiling the payload, then erases the schedule before translating the result to LLVM IR.

An inline transform

Start with a predictable loop nest. A rank-two array::splatarray::splat lowers to nested loops that store one value at every index:

import core::intrinsic::array;
#[transform_anchor]
fn tiled_seed() -> [i64; 4, 16] {
array::splat<[i64; 4, 16]>(7)
}
import core::intrinsic::array;
#[transform_anchor]
fn tiled_seed() -> [i64; 4, 16] {
array::splat<[i64; 4, 16]>(7)
}

#[transform_anchor]#[transform_anchor] places reussir.transform_anchorreussir.transform_anchor and no_inlineno_inline on the lowered func.funcfunc.func. The first attribute gives schedules a stable selector; the second keeps the selected function from disappearing into a caller before the kernel point. The attribute takes no arguments, may occur only once, and requires a function with a body.

For a generic function, every emitted ground instance is marked independently. The attribute does not instantiate a generic by itself: the program must still call or export each specialization it wants to schedule.

Add a top-level transformtransform item:

transform [{
%stores = transform.structured.match ops{["memref.store"]} in %target
: (!transform.any_op) -> !transform.op<"memref.store">
%outer = transform.get_parent_op %stores {
op_name = "scf.for", nth_parent = 2 : i64, deduplicate
} : (!transform.op<"memref.store">) -> !transform.op<"scf.for">
transform.loop.unroll_and_jam %outer { factor = 2 }
: !transform.op<"scf.for">
transform.yield
}];
transform [{
%stores = transform.structured.match ops{["memref.store"]} in %target
: (!transform.any_op) -> !transform.op<"memref.store">
%outer = transform.get_parent_op %stores {
op_name = "scf.for", nth_parent = 2 : i64, deduplicate
} : (!transform.op<"memref.store">) -> !transform.op<"scf.for">
transform.loop.unroll_and_jam %outer { factor = 2 }
: !transform.op<"scf.for">
transform.yield
}];

The compiler wraps this body in a named Transform dialect sequence. %target%target is an injected, read-only handle containing every emitted func.funcfunc.func marked by #[transform_anchor]#[transform_anchor]. This convenience name exists only inside an inline transform [{ ... }];transform [{ ... }]; body. A file-based script receives the whole module and must match reussir.transform_anchorreussir.transform_anchor itself.

The schedule then proceeds in three steps:

  1. transform.structured.matchtransform.structured.match finds memref.storememref.store operations inside the anchored function.
  2. transform.get_parent_optransform.get_parent_op walks from each store to its second enclosing scf.forscf.for, deduplicating repeated handles to the same loop.
  3. transform.loop.unroll_and_jamtransform.loop.unroll_and_jam unrolls that outer loop by two and combines the unrolled bodies.

Inline schedules run at Reussir’s kernel point. At this point ownership and high-level Reussir operations have been lowered, PolyFFI has been compiled, and array kernels are ordinary memrefmemref, scfscf, and aritharith operations. LLVM lowering has not started, so structured-loop transforms still have useful information to work with. Inline schedules run before a file-based kernelkernel schedule and before invariant-group analysis.

You may add more than one transform [{ ... }];transform [{ ... }]; item. Each receives the same anchored target set and runs in source order. Every body must be valid MLIR Transform dialect syntax and must end as a valid named-sequence body, normally with transform.yieldtransform.yield.

Inspect before writing a schedule

The transform sees lowered MLIR, not the source expression tree. First inspect the relevant compilation stages:

$ rrc kernel.rr --emit mlir -o kernel.mlir
$ rrc kernel.rr --emit mlir-llvm -O none -o lowered.mlir
$ rrc kernel.rr --emit mlir -o kernel.mlir
$ rrc kernel.rr --emit mlir-llvm -O none -o lowered.mlir

The first command shows Reussir’s initial MLIR. The second runs the lowering pipeline and stops in the LLVM dialect. When developing a schedule, use a fixed optimization level so earlier inlining or canonicalization does not silently change the structure you intend to match.

--emit mlir--emit mlir stops before the lowering pipeline: it embeds and validates an inline schedule but does not execute it, and file-based --transform-script--transform-script options are not applied at that stage. Use --emit mlir-llvm--emit mlir-llvm, an object, or a linked output to execute schedules. In a successful mlir-llvmmlir-llvm dump, the inline schedule has already run and has been erased.

An invalid inline operation is reported at its Reussir source location:

transform [{
transform.not_a_real_operation %target
transform.yield
}];
transform [{
transform.not_a_real_operation %target
transform.yield
}];
Error: invalid inline transform: custom op 'transform.not_a_real_operation' is unknown
Error: invalid inline transform: custom op 'transform.not_a_real_operation' is unknown

Matching the wrong operation, requesting an illegal rewrite, or invalidating a handle can also fail the build. Prefer narrow matching rooted at explicit anchors and let failures propagate while developing a schedule.

Loading a schedule from a file

Larger or reusable schedules can live in ordinary MLIR files. RRC recognizes two pipeline anchors:

Anchor Required entry point Payload state
entryentry @__reussir_anchor_entry@__reussir_anchor_entry Initial Reussir-dialect MLIR, before token instantiation or any Reussir transformation pass.
kernelkernel @__reussir_anchor_kernel@__reussir_anchor_kernel After Reussir lowering and PolyFFI compilation, before the descent to LLVM. This is the default.

An entry script operates on the high-level operations shown by --emit mlir--emit mlir. For example, this schedule finds every initial RC allocation and attaches a marker before ownership lowering begins:

module attributes {transform.with_named_sequence} {
transform.named_sequence @__reussir_anchor_entry(
%module: !transform.any_op {transform.readonly}) {
%creates = transform.structured.match
ops{["reussir.rc.create"]} in %module
: (!transform.any_op)
-> !transform.op<"reussir.rc.create">
transform.annotate %creates "tutorial.entry_seen"
: !transform.op<"reussir.rc.create">
transform.yield
}
}
module attributes {transform.with_named_sequence} {
transform.named_sequence @__reussir_anchor_entry(
%module: !transform.any_op {transform.readonly}) {
%creates = transform.structured.match
ops{["reussir.rc.create"]} in %module
: (!transform.any_op)
-> !transform.op<"reussir.rc.create">
transform.annotate %creates "tutorial.entry_seen"
: !transform.op<"reussir.rc.create">
transform.yield
}
}

Save that form to a file and select it with --transform-script FILE@entry--transform-script FILE@entry. An entry transform must leave IR that the ordinary Reussir passes can still consume.

Here is a kernel schedule that unrolls loops inside source functions marked with #[transform_anchor]#[transform_anchor]:

module attributes {transform.with_named_sequence} {
transform.named_sequence @__reussir_anchor_kernel(
%module: !transform.any_op {transform.readonly}) {
%functions = transform.structured.match ops{["func.func"]}
attributes{reussir.transform_anchor} in %module
: (!transform.any_op) -> !transform.any_op
%loops = transform.structured.match ops{["scf.for"]} in %functions
: (!transform.any_op) -> !transform.op<"scf.for">
transform.loop.unroll %loops { factor = 4 }
: !transform.op<"scf.for">
transform.yield
}
}
module attributes {transform.with_named_sequence} {
transform.named_sequence @__reussir_anchor_kernel(
%module: !transform.any_op {transform.readonly}) {
%functions = transform.structured.match ops{["func.func"]}
attributes{reussir.transform_anchor} in %module
: (!transform.any_op) -> !transform.any_op
%loops = transform.structured.match ops{["scf.for"]} in %functions
: (!transform.any_op) -> !transform.op<"scf.for">
transform.loop.unroll %loops { factor = 4 }
: !transform.op<"scf.for">
transform.yield
}
}

Save it as unroll.transform.mlirunroll.transform.mlir, then compile:

$ rrc kernel.rr -O none \
--transform-script unroll.transform.mlir@kernel \
-o kernel.o
$ rrc kernel.rr -O none \
--transform-script unroll.transform.mlir@kernel \
-o kernel.o

The @kernel@kernel suffix is optional because kernelkernel is the default. Use @entry@entry for a script with the entry anchor’s required named sequence. The path/anchor split uses the final @@; if the file name itself contains @@, append an explicit @kernel@kernel or @entry@entry.

RRC accepts repeated --transform-script--transform-script options and preloads all selected files before either anchor runs. Each anchor has one fixed entry-point symbol, however, so two files for the same anchor collide. In practice, provide at most one entry file and one kernel file. Combine several same-anchor transforms in that file’s entry sequence, or call helper named sequences from it.

File-script diagnostics

The anchor suffix and named sequence are checked independently. A misspelled CLI anchor is rejected before lowering:

unknown anchor `bogus` in `--transform-script schedule.mlir@bogus`: expected `entry` or `kernel`
unknown anchor `bogus` in `--transform-script schedule.mlir@bogus`: expected `entry` or `kernel`

A valid MLIR library with the wrong entry-point name reaches the interpreter but fails rather than becoming a silent no-op:

could not find a nested named sequence with name: __reussir_anchor_entry
could not find a nested named sequence with name: __reussir_anchor_entry

A missing file is likewise reported by the preload pass. Transform failures propagate and stop compilation; they never fall back to the unscheduled program.

Why expose the compiler this way?

A fixed optimization pipeline has to serve every program. Transform dialect adds an opt-in layer for domain-specific scheduling while retaining MLIR’s verification, diagnostics, and composable operation handles. It also gives compiler researchers a short path from a Reussir workload to experiments with new MLIR transformations, without first adding a permanent surface-language intrinsic or forking the whole compiler pipeline.

That power comes with responsibility. A schedule can make code slower, enlarge it dramatically, or become invalid after a lowering change. Keep the unscheduled implementation correct, constrain transforms to explicit anchors, benchmark representative inputs, and regard each schedule as part of the build configuration for its pinned compiler version.