Background
This RFC proposes that all .enable metadata
nodes must use a single-operand format (the name string
only, no boolean operand). As a consequence, this RFC also proposes
new .disable nodes for a few.
As LangRef stands today, no .enable node enforces this. The following nodes
are present in LangRef with an i1 boolean (two
operands):
llvm.loop.vectorize.enablellvm.loop.vectorize.predicate.enablellvm.loop.vectorize.scalable.enablellvm.loop.distribute.enable
While the following are without bool (single
operand) as per LangRef:
llvm.loop.unroll.enablellvm.loop.unroll.disablellvm.loop.unroll_and_jam.enablellvm.loop.unroll_and_jam.disablellvm.loop.unroll.runtime.disablellvm.loop.licm_versioning.disablellvm.licm.disable
As one can notice, there is .enable and .disable version for
llvm.loop.unroll but enablement/disablement is done via second argument
for llvm.loop.vectorize.enable. This is inconsistent and causes
unnecessary confusion.
Moreover, the shared reader function
getOptionalBoolLoopAttribute silently accepts both forms
for every .enable node. This means nothing prevents
!{!"llvm.loop.unroll.enable", i1 1} from appearing in IR,
and indeed such forms exist in the wild.
This RFC proposes enforcing that every .enable node takes exactly one operand
(the name string), eliminating the boolean operand entirely.
The below PR tried to add checks considering the current
state of LangRef, but during review we felt that it is
better to fix the inconsistencies than point fixes.
Motivation/Why?
1. No .enable node enforces a single-operand format
All .enable metadata nodes are read through the same
helper, getOptionalBoolLoopAttribute, which silently accepts
both the single-operand form and the two-operand boolean form:
This means every .enable node — including those that are
conventionally emitted as single-operand — will happily
accept a boolean. For example, the following are all legal
today with no verifier rejection:
!{!"llvm.loop.unroll.enable"} ; typical form
!{!"llvm.loop.unroll.enable", i1 1} ; also accepted
!{!"llvm.loop.vectorize.enable", i1 1} ; typical form
!{!"llvm.loop.vectorize.enable", i1 0} ; "enable = false"
Some nodes (e.g., llvm.loop.vectorize.enable) are
conventionally emitted with a boolean by Clang and in-tree
code, while others (e.g., llvm.loop.unroll.enable) are
typically emitted without one. But this is merely convention,
not enforcement — nothing prevents either form from appearing
for any node.
2. Confusing double-negative semantics
The boolean form allows constructs like:
!0 = !{!"llvm.loop.distribute.enable", i1 0}
This reads as “enable distribution = false”, which is a confusing
double negative. Is it the same as “disable distribution”? Is it
the same as the attribute being absent? The existing helper
getOptionalBoolLoopAttribute treats i1 0 as “attribute set to
false” while absence means “unspecified”, creating a three-valued
logic that is error-prone and hard to reason about.
3. Malformed IR in the wild
Tests and frontends have produced metadata with incorrect types
(e.g., i32 instead of i1) or incorrect operand counts. Without
strict verifier enforcement, these go undetected and can lead to
crashes in passes (see
#156685
where opt -passes=loop-distribute crashes on malformed metadata).
A single canonical form is easier to verify and less prone to
encoding errors.
4. IR should have one canonical form
As noted in the review discussion:
“This is not a place for user flexibility, this is an IR. There
should be one true form that every consumer can rely on.”
– @arsenm
Current State
The following table summarizes all loop metadata nodes with
enable/disable semantics today. The “Boolean operand in
LangRef?” column shows what the upstream LangRef
documents as the canonical format.
| # | Metadata | Boolean operand in LangRef? |
|---|---|---|
| 1 | llvm.loop.unroll.enable |
No |
| 2 | llvm.loop.unroll.disable |
No |
| 3 | llvm.loop.unroll.runtime.disable |
No |
| 4 | llvm.loop.unroll_and_jam.enable |
No |
| 5 | llvm.loop.unroll_and_jam.disable |
No |
| 6 | llvm.loop.vectorize.enable |
Yes |
| 7 | llvm.loop.vectorize.predicate.enable |
Yes |
| 8 | llvm.loop.vectorize.scalable.enable |
Yes |
| 9 | llvm.loop.distribute.enable |
Yes |
| 10 | llvm.loop.licm_versioning.disable |
No |
| 11 | llvm.licm.disable |
No |
Rows 6–9 are documented in LangRef with an i1 boolean
operand (e.g., !{!"llvm.loop.vectorize.enable", i1 1}).
Rows 1–5 and 10–11 are documented as single-operand
(e.g., !{!"llvm.loop.unroll.enable"}). But because all
nodes are read through getOptionalBoolLoopAttribute, every
node accepts a boolean at runtime with no verifier
rejection.
Additionally, the following enable/disable nodes exist in
the implementation but are not documented in LangRef:
llvm.loop.pipeline.disable— takesi1booleanllvm.loop.unswitch.partial.disable— single-operandllvm.loop.unswitch.injection.disable— single-operand
These are out of scope for this RFC but should be documented
in LangRef as a separate cleanup.
Proposed Changes
All .enable nodes become single-operand
Every .enable loop metadata node to take exactly
one operand — the name string. No boolean operand is
permitted.
This applies to all .enable nodes, not just the ones that
conventionally carry a boolean today. Nodes like
llvm.loop.unroll.enable that are typically emitted as
single-operand will now be required to be single-operand.
For nodes that previously used i1 0 on an .enable
node to mean “disable”, an explicit .disable counterpart
is introduced.
Before (accepted today for any .enable node):
!{!"llvm.loop.unroll.enable"} ; ok
!{!"llvm.loop.unroll.enable", i1 1} ; also accepted
!{!"llvm.loop.vectorize.enable", i1 1} ; enable
!{!"llvm.loop.vectorize.enable", i1 0} ; disable
After (proposed — single-operand enable/disable pairs):
!{!"llvm.loop.unroll.enable"} ; enable unrolling
!{!"llvm.loop.vectorize.enable"} ; enable vectorization
!{!"llvm.loop.vectorize.disable"} ; disable vectorization
Affected .enable nodes
The following table lists every .enable node and the
change. For nodes that were conventionally emitted without a
boolean, the change is enforcement only (rejecting the
boolean form). For nodes conventionally emitted with a
boolean, the boolean is dropped and a new .disable
counterpart is introduced.
| Node | Change |
|---|---|
llvm.loop.unroll.enable |
Enforce single-operand (reject i1) |
llvm.loop.unroll_and_jam.enable |
Enforce single-operand (reject i1) |
llvm.loop.vectorize.enable |
Drop boolean; Introduce vectorize.disable |
llvm.loop.vectorize.predicate.enable |
Drop boolean; Introduce predicate.disable |
llvm.loop.vectorize.scalable.enable |
Drop boolean; Introduce scalable.disable |
llvm.loop.distribute.enable |
Drop boolean; Introduce distribute.disable |
New .disable nodes
The following new single-operand .disable nodes are
introduced to replace the ..enable, i1 0 pattern:
| New node | Replaces |
|---|---|
llvm.loop.vectorize.disable |
llvm.loop.vectorize.enable, i1 0 |
llvm.loop.vectorize.predicate.disable |
llvm.loop.vectorize.predicate.enable, i1 0 |
llvm.loop.vectorize.scalable.disable |
llvm.loop.vectorize.scalable.enable, i1 0 |
llvm.loop.distribute.disable |
llvm.loop.distribute.enable, i1 0 |
Semantics: three-valued logic preserved
The current three-valued interpretation remains valid:
| State | Meaning |
|---|---|
.enable present |
User explicitly requests the transformation |
.disable present |
User explicitly suppresses the transformation |
| Neither present | Unspecified; pass uses its own heuristics |
This maps cleanly onto the existing TransformationMode
enum:
TM_ForcedByUser↔.enablepresentTM_SuppressedByUser↔.disablepresentTM_Unspecified↔ neither present
Implementation Plan
I plan to do the changes in four phases, one per node
that currently carries a boolean operand (rows 6–9 in the
table above). Each phase is an independent patch series
that can be reviewed and landed separately, limiting blast
radius.
Phase 1 — llvm.loop.distribute.enable
- Introduce
llvm.loop.distribute.disable
(single-operand). - Update readers/callers in the loop-distribute pass to
recognise the new.disablenode. - Update Clang to emit the single-operand
.enableand
the new.disablenode. - Update LangRef.
- Migrate all in-tree tests.
- Add a verifier check that rejects the boolean form.
Phase 2 — llvm.loop.vectorize.enable
- Introduce
llvm.loop.vectorize.disable(single-operand). - Update
getOptionalBoolLoopAttributeand all callers in
the vectorizer to recognise the new.disablenode. - Update Clang to emit
llvm.loop.vectorize.enableas
single-operand andllvm.loop.vectorize.disableinstead
of..enable, i1 0. - Update LangRef to document the new format and deprecate
the boolean form. - Migrate all in-tree tests.
- Add a verifier check that rejects the boolean form for
llvm.loop.vectorize.enable.
Phase 3 — llvm.loop.vectorize.predicate.enable
- Introduce
llvm.loop.vectorize.predicate.disable
(single-operand). - Update readers/callers in the vectorizer to recognise
the new.disablenode. - Update Clang to emit the single-operand
.enableand
the new.disablenode. - Update LangRef.
- Migrate all in-tree tests.
- Add a verifier check that rejects the boolean form.
Phase 4 — llvm.loop.vectorize.scalable.enable
- Introduce
llvm.loop.vectorize.scalable.disable
(single-operand). - Update readers/callers in the vectorizer to recognise
the new.disablenode. - Update Clang to emit the single-operand
.enableand
the new.disablenode. - Update LangRef.
- Migrate all in-tree tests.
- Add a verifier check that rejects the boolean form.
Final cleanup
After all four phases land:
- Remove the
case 2:(boolean) handling from
getOptionalBoolLoopAttributeso that any residual
two-operand.enablemetadata is caught at parse time. - Add a blanket verifier rule: every
.enablenode must
have exactly one operand.
Proof-of-Concept:
A working POC for Phase 1 (llvm.loop.distribute.enable) is available at the PR
This branch demonstrates the end-to-end change pattern that
each phase will follow. In summary it:
-
Introduces
llvm.loop.distribute.disableas a
single-operand node, replacing the old
!{!"llvm.loop.distribute.enable", i1 0}pattern. -
Updates Clang codegen (
CGLoopInfo.cpp) to emit
llvm.loop.distribute.enablewithout a boolean operand
and to emitllvm.loop.distribute.disableinstead of
..enable, i1 0. -
Updates LLVM passes and analysis —
LoopDistribute.cppandLoopAccessAnalysis.cppnow
query for.enableand.disableas separate
single-operand nodes viagetBooleanLoopAttribute,
replacing the oldfindStringMetadataForLoop+
ConstantIntextraction. -
Updates
LoopUtils.cppso that
hasDistributeTransformationchecks for the new
.disablenode and returnsTM_SuppressedByUser. -
Updates
LoopConstrainer.cppto emit
llvm.loop.distribute.disableinstead of
..enable, i1 0when disabling all loop opts on
pre/post loops created by IRCE. -
Updates LangRef to document the single-operand
.enable/.disablepair forllvm.loop.distribute. -
Updates MLIR loop-annotation import/export to
round-trip the new format correctly. -
Migrates all affected in-tree tests (34 files)
across Clang, LLVM, and MLIR.
(The PR is currently marked as Draft but once I see consensus,
I will open it for review.)
This branch serves as the template for Phases 2–4; each
will follow the same pattern for its respective node.
Alternatives Considered
Fused boolean form: keep only .enable with i1
An alternative is to go the opposite direction: make
every .enable node take a mandatory i1 boolean and
remove all existing .disable nodes. Under this scheme,
enablement and disablement would both be expressed through
a single node:
!{!"llvm.loop.vectorize.enable", i1 1} ; enable
!{!"llvm.loop.vectorize.enable", i1 0} ; disable
!{!"llvm.loop.unroll.enable", i1 1} ; enable
!{!"llvm.loop.unroll.enable", i1 0} ; disable
I am open to listen opinions of this approach but I am not
keen on it due to the following reasons:
-
Double-negative confusion.
!{!"llvm.loop.distribute.enable", i1 0}reads as
“enable = false”, which is a double negative. In
contrast,!{!"llvm.loop.distribute.disable"}states
the intent directly and unambiguously. -
Existing convention already uses
.disablenodes.
Upstream already hasllvm.loop.unroll.disable,
llvm.loop.unroll_and_jam.disable,
llvm.loop.unroll.runtime.disable,
llvm.loop.licm_versioning.disable, and
llvm.licm.disable. Moving away from.disablenodes
would require changing all of these, breaking more
existing IR and tests for no readability gain. -
Less self-documenting IR. When reading a metadata
dump,!{!"llvm.loop.vectorize.disable"}communicates
intent at a glance, while
!{!"llvm.loop.vectorize.enable", i1 0}requires the
reader to parse the boolean and mentally negate the
name.
The single-operand .enable/.disable pair proposed in
this RFC is more consistent with existing upstream
conventions and produces clearer, more readable IR.
Looking for opinions, suggestions and comments.
References
- PR #182252: [Verifier] Add checks for loop metadata nodes
[Verifier] Add checks for loop metadata nodes by madhur13490 · Pull Request #182252 · llvm/llvm-project · GitHub - Issue #156685: opt -passes=loop-distribute crashes
opt -passes=loop-distribute crashes · Issue #156685 · llvm/llvm-project · GitHub - LLVM LangRef: Loop Metadata
LLVM Language Reference Manual — LLVM 23.0.0git documentation
Assisted by: Claude Opus 4.6