fir.assertStatus: draft design, for discussion. Context: llvm/llvm-project#223287. Source references are to llvm-project at fe1de64cb3c2 (2026-10-02). Line numbers drift, so each reference also names the function or pattern.
Abbreviations used in the tables:
| Short | Path |
|---|---|
SHI | flang/lib/Optimizer/HLFIR/Transforms/SimplifyHLFIRIntrinsics.cpp |
OB | flang/lib/Optimizer/HLFIR/Transforms/OptimizedBufferization.cpp |
IHA | flang/lib/Optimizer/HLFIR/Transforms/InlineHLFIRAssign.cpp |
SAA | flang/lib/Optimizer/HLFIR/Transforms/SeparateAllocatableAssign.cpp |
C2F | flang/lib/Optimizer/HLFIR/Transforms/ConvertToFIR.cpp |
SI | flang/lib/Optimizer/Transforms/SimplifyIntrinsics.cpp (FIR level) |
IC | flang/lib/Optimizer/Builder/IntrinsicCall.cpp |
X2H | flang/lib/Lower/ConvertExprToHLFIR.cpp |
RT/ | flang-rt/lib/runtime/ |
Terminology:
flang/lib/Lower.fir.assert operation, either in lowering or in an HLFIR or FIR pass.FIRToLLVMLowering in flang/lib/Optimizer/CodeGen/CodeGen.cpp), where fir.assert becomes a branch and a runtime call.Flang relies on the Fortran runtime for almost all dynamic checks: shape conformance, allocation status, argument consistency of intrinsics. Every time an HLFIR or FIR pass replaces a runtime call with inline code, the check silently disappears. Three consequences:
-O1 and above, so the same non-conforming program stops with a clear message at -O0 and does something else at -O2.fir::factory::deduceOptimalExtents). A size mismatch becomes an out-of-bounds read or write instead of an error.The few inline checks that exist today (MOD/MODULO P==0, NEAREST S==0, STORAGE_SIZE, the realloc-with-scalar-RHS case) use fir.if plus fir.call @_FortranAReportFatalUserError. That pattern:
mlir/docs/Rationale/SideEffectsAndSpeculation.md);noreturn), so LLVM cannot use the checked condition afterwards.Goals:
fatal Fortran runtime error(<file>:<line>): <message>, and with message text as close as possible to the runtime’s.-O0 and -O2, so that tests disabled because of that difference (for example in the llvm-test-suite gfortran tests) can be re-enabled.-fcheck=bounds, pointer, do, bits).Non-goals:
fir.assert operation%g:N = fir.assert %ok, <kind>, "<message>" [values(%v0, ... : T0, ...)] [guard(%x0, ... : U0, ...)]
%ok (i1) is the condition that must hold.<kind> is a check category (section 4). Options act on categories.<message> is the error text, a printf format.values(%v0, ... : T0, ...) are optional i8, i16, i32, i64, or index SSA values (T0, ... are their types) printed as signed integers into the message, one per %lld directive. Boolean diagnostics must be explicitly extended by the producer to select their signed or unsigned interpretation. Values are only read on the failure path. For example, the two extents in "DOT_PRODUCT: SIZE(VECTOR_A) is %lld but SIZE(VECTOR_B) is %lld".guard(%x0, ... : U0, ...) are the optional guarded values, of any type. The op has one result per guarded value, with the same type (%g#i has type Ui).%ok is true, each result equals the corresponding guarded value and nothing else happens. Otherwise the program terminates through the Fortran runtime with the formatted message.fir.assert. Asserts without a guard only order themselves with respect to I/O and calls (section 3.2).nsw or nuw operation, or a read of an absent OPTIONAL argument outside fir.if. Such operations come after the assert and use its guarded results.What to guard: the operand of the protected operation, such as a divisor, a DO step, a subscript, or a box that is dereferenced. For conformance checks, it is the array being accessed (a variable, or an !hlfir.expr), at the point where it is combined with another array:
Conformance checks do not guard shapes or extents. HLFIR can re-derive them without going through a guarded value: from static types (hlfir.shape_of folds to a constant shape), from the preference for constant extents (fir::factory::deduceOptimalExtents), or from the other side of an assignment. Leaving them unguarded also keeps constant loop bounds visible to MLIR loop optimizations. Array bases are never compile-time constants, so guarding them hides no constant. It does hide their origin (global, local, dummy) from the analyses that find it through the defining operation, which must look through the guard (section 3.2). Section 7 explains how the guards follow an expression when it is inlined.
MemoryEffects<[MemWrite<FortranRuntimeResource>]>, where FortranRuntimeResource is a non-addressable resource (like the existing fir::DebuggingResource).
flang/lib/Optimizer/Analysis/AliasAnalysis.cpp, getModRef), and MLIR CSE ignores writes to resources disjoint from a load's resource. So asserts do not block load CSE, store-to-load forwarding, or FIR LICM of loads. This also means SimplifyHLFIRIntrinsics can emit asserts in its first run (allowNewSideEffects=false, SHI:3321-3333): the concern there is new effects that stop CSE from merging loads, which this op does not have.Not speculatable.
Not transparent to other operations' folds and patterns. The assert's own canonicalization removes it when %ok is a constant true (section 3.4). But no other rewrite may look through an assert to its guarded operand, because that drops the dependence. For example, in %a = fir.box_addr %b; %a_ok = fir.assert ... guard(%a); %c = fir.embox %a_ok, a fold that rewrites an fir.embox of a fir.box_addr into the original box must not replace %c with %b. In practice:
ViewLikeOpInterface;fir.declare or fir.convert to find a defining op;%ok is unknown.Analyses, on the other hand, must look through the assert, since they do not rewrite uses. This covers alias analysis, and the helpers that find the origin, Fortran attributes, contiguity, or optionality of a variable through its defining operation (for example the matching done by OptimizedBufferization). Otherwise guarding both sides of every array assignment (A1) would pessimize them, for example by adding overlap tests and temporaries, so this is a prerequisite of the conformance checks (section 8). Shape and length queries may also look through conformance and argument guards, but not through pointer guards, which protect the validity of the guarded value itself.
def fir_AssertOp : fir_Op<"assert", [AttrSizedOperandSegments,
MemoryEffects<[MemWrite<FortranRuntimeResource>]>]> {
let arguments = (ins I1:$condition,
fir_RuntimeCheckKindAttr:$kind,
StrAttr:$message,
Variadic<AnySignlessIntegerOrIndex>:$values,
Variadic<AnyType>:$guarded);
let results = (outs Variadic<AnyType>:$results); // types == guarded types
let hasVerifier = 1;
let hasCanonicalizer = 1;
}
Constant true: erase the assert and replace its results with the guarded operands. This replaces the hand-written fir::getIntIfConstant early-outs of today's checks (IC:6929-6931). It must be a canonicalization pattern rather than only a fold: for an assert with no guard, an empty fold result means an in-place update, not an erasure.
Constant false: keep it, with no diagnostic. A false condition is common in code that is unreachable but hard to prove unreachable at compile time (for example, after inlining or specialization on constant arguments), so a warning would be noisy.
A later pass could erase the code strictly dominated by a constant-false assert. This is not proposed for the first version, and it must not run in any pipeline that may drop asserts (for example, device code with checks disabled). There, the assert would disappear together with the pruned code, and a program that should have stopped would silently produce wrong results.
Dominating assert with the same condition and kind: remove the later one, mapping its results to the earlier one's results. This is valid regardless of what lies between them and regardless of the message, because the earlier check fails first. Two restrictions:
Upstream CSE cannot do this: it skips ops with write effects (mlir/lib/Transforms/Utils/CSE.cpp, simplifyOperation). So this needs a small dominance-based pass (section 6). It is not required to start with: once the runtime call is declared noreturn, LLVM already removes many redundant checks (GVN, jump threading). The MLIR pass is for cleaning up checks early, before the MLIR loop analyses and transformations that would otherwise have to step around them.
fir.assert is converted in FIRToLLVMLowering, where the IR is already a control-flow graph:
// fir.assert %ok, conformance, "..." values(%na, %nb : index, index) guard(%b : !fir.box<...>) llvm.cond_br %ok weights([2000, 1]), ^cont, ^fail ^fail: llvm.call @_FortranAReportFatalUserErrorValues(%msg, %file, %line, %na64, %nb64, ...) llvm.unreachable ^cont: // uses of the result are replaced with %b
_FortranAReportFatalUserError(msg, source, line) (flang/include/flang/Runtime/stop.h) is [[noreturn]] and device-callable (RT_API_ATTRS). It takes no values. Assertions with up to four values use ReportFatalUserErrorValues(msg, source, line, v0..v3) with fixed long long arguments, padding unused arguments with zeros. Assertions with more values use ReportFatalUserErrorValueArray(msg, source, line, count, values), where count is a size_t and values points to an array of long long. Code generation packs that array on the failure path; the assertion's diagnostic operands remain SSA values until then. Both entries have fixed signatures to keep the ABI simple on devices. Messages use only %lld and %% (for a literal percent sign). The array entry prints the diagnostic in pieces through the existing runtime termination machinery, without allocating a formatted message. Concurrent failures may interleave their output.noreturn, so LLVM can use the checked condition on the continuation path.FusedLoc and CallSiteLoc to the innermost FileLineColLoc: today fir::factory::locationToFilename returns null for them, so inlined checks lose file and line.gpu.module for device code), and the conversion must also exist in any device code generation pipeline. If a device runtime lacks the entry point, fall back to printf plus llvm.trap. The function is not in the OpenMP offload API group today, unlike Abort.Converting asserts earlier, before CFGConversion (to fir.if plus call), is also possible, but fir.if regions cannot end with fir.unreachable, and asserts would stop being visible to the passes after that point.
fir.if plus a runtime call (today's pattern). Not retained, for the reasons in section 1: there is no data dependence on the protected operation, the call's unknown memory effects block alias-based optimizations, and LLVM is not told that the call never returns.
cf.assert. Not retained. It writes the default resource, which is how it models program termination, so it blocks store-to-load forwarding and load CSE across it. It still does not order non-speculatable operations without memory effects, such as arith.remsi, after it. Its conversion to LLVM prints with puts and calls abort, rather than reporting through the Fortran runtime. It also has no guarded results.
A result-less fir.assert (the current revision of #223287). The PR defines fir.assert as a non-speculatable op with a MemWrite<FortranRuntimeResource> effect, converts it to cf.assert, and teaches Flang LICM not to hoist non-speculatable operations across a preceding assert in the same block. This design keeps that effect model (section 3.2), which gives the right DCE, alias-analysis, and forwarding behavior. It is not retained as is, because the ordering of the protected operations is not expressed in the IR:
fir.load or an arith.remsi above an assert whose effects do not conflict with it. The LICM change protects one pass. Every other pass that moves or recreates operations would need the same fix, and future passes would have to remember it. Examples are sinking, scheduling, and rewrite patterns that build new operations at a different insertion point.fir.assert. Code generation then calls the Fortran runtime entry directly, so messages match the runtime's format (section 3.5).The design in this document is therefore the PR's op, with guarded results added and code generation calling the runtime directly.
A region-based assert, like shape.assuming. The protected operations go inside a region attached to the assert, and the values used afterwards are yielded out:
%r = fir.assert %ok, conformance, "..." -> (f32) {
// inlined DOT_PRODUCT loop nest
fir.result %sum : f32
}
Ordering is structural, so it is as safe as the guarded-result form. It is not retained because the region gets in the way of the optimizations the checks must not block:
OptimizedBufferization pattern that matches an hlfir.elemental and its hlfir.assign, store-to-load forwarding, and LICM of loads out of the region.The guarded-result form gives the same guarantee at the granularity of a value: guarding the arrays a loop accesses protects every access in the loop, without moving the loop into a region.
| Kind | Meaning | Default | Option |
|---|---|---|---|
status | Allocation status required by the standard (ALLOCATE/DEALLOCATE, assignment to an unallocated or disassociated left-hand side) and allocation failure | On, all optimization levels | Option to disable, for diagnosing regressions |
conformance | Shape or size agreement that the runtime checks when not inlined | On, all optimization levels (same behavior as -O0) | -fno-check=conformance (name to be decided) |
argument | Intrinsic argument values the runtime checks (DIM range, RESHAPE SHAPE values) | On | same mechanism |
bounds | Subscript, section, substring, and character-dummy-length checks | Off | -fcheck=bounds |
pointer | Dereference of a disassociated POINTER, an unallocated ALLOCATABLE, or an absent OPTIONAL | Off | -fcheck=pointer |
do | DO loop with zero step | Off | -fcheck=do |
bits | Bit-intrinsic position and shift ranges | Off | -fcheck=bits |
divide | Integer MOD/MODULO with P==0 | Off | existing -fcheck-integer-mod-zero-divisor |
Gating:
-fcheck-integer-mod-zero-divisor does today (fir.check_integer_mod_zero_divisor). A single fir.runtime_checks attribute listing the enabled kinds would scale better.acc routine, lowering emits a single func.func that later becomes a host and a device variant, and some asserts may have to be dropped or simplified in the device variant. A small pass drops asserts by kind, replacing their results with the guarded operands. It runs only in such pipelines, after the variants are split, not in the normal pipeline.-fcheck= currently exists only as a gfortran pass-through in clang/include/clang/Options/FlangOptions.td. It would become a Flang option with gfortran-compatible sub-options.Each table gives the condition in IR terms, the runtime message to reuse, the insertion point, and what to guard. ext(x,d) is the extent of dimension d (0-based) from hlfir::genExtentsVector or fir.box_dims. null(p) is a null test of a fir.box_addr result. Messages are adapted from the runtime's rather than quoted: %s is substituted at compile time, values use %lld, and some texts are new (for example the per-dimension A1 message).
status, conformance, argument)These checks exist in the runtime today and disappear at -O1 and above.
| ID | Operation | Condition (%ok) | Runtime message | Inserted in | Guard |
|---|---|---|---|---|---|
| A1 | Array assignment, no realloc, whatever the right-hand side (variable, hlfir.elemental, or hlfir.eval_in_mem such as an inlined MATMUL) | for each d: ext(lhs,d) == ext(rhs,d) | Assign: mismatching element counts in array assignment (to %lld, from %lld) (RT/assign.cpp:453-461; AssignSimple at :921-928) | Lowering, before hlfir.assign (flang/lib/Lower/Bridge.cpp, genDataAssignment, :5884) | Both sides |
| A2 | Assignment to a POINTER or non-realloc ALLOCATABLE left-hand side | !null(lhs) || size(lhs) == 0 | Assign: left-hand side variable is neither allocated nor allocatable (RT/assign.cpp:371-374) | IHA InlineHLFIRAssignConversion; OB ElementalAssignBufferization, EvaluateIntoMemoryAssignBufferization | Left-hand-side box |
| A5 | Scalar broadcast to an unallocated allocatable (realloc) or disassociated pointer | !null(lhs) | Assign: mismatched ranks (%lld != %lld) in assignment to unallocated allocatable (RT/assign.cpp:361-370), or the existing inline text array left hand side must be allocated when the right hand side is a scalar (MutableBox.cpp:947-954) | OB BroadcastAssignBufferization (:514-612); it fires even with realloc | Left-hand-side box |
| A6 | Inline (re)allocation for assignment | !null(newStorage) after fir.allocmem | Memory allocation failed / AssignSimple: allocation failed (stat=%lld) (RT/assign.cpp:130, :1050) | SAA (runs at every optimization level) and IHA InlineAllocatableExprAssignConversion, through fir::factory::genReallocIfNeeded / allocateAndInitNewStorage; C2F scalar allocatable path (:123-150), which creates the fir.allocmem directly | New storage address |
Not lost: InlineCopyInConversion replaces ShallowCopyDirect, whose checks are internal, because the temporary is built from the variable's own shape.
Notes on A1:
hlfir.assign (InlineHLFIRAssign, the OptimizedBufferization patterns, BufferizeHLFIR, the runtime-call path). Those passes only see the guarded sides. OptimizedBufferization inlines the right-hand side's elemental body through the guard (section 7), and tryUsingAssignLhsDirectly writes through the guarded left-hand side.-O0, the runtime then checks the assignment again, which is harmless.temporary_lhs, the ones produced by SeparateAllocatableAssign) conform by construction and are not checked.hlfir.region_assign. The runtime does not check them either, so their check is new (B7).2x3 = 3x2 assignment passes the runtime and gives a wrong result in element order. The inline loop nest would access out of bounds, so the assert must compare per-dimension extents. That is stricter than the runtime, and it is exactly F2023 10.2.1.2 conformance. The message then also names the dimension (see the A1 example in section 5.4).| ID | Operation | Condition | Runtime message | Inserted in | Guard |
|---|---|---|---|---|---|
| A7 | DOT_PRODUCT | ext(a,0) == ext(b,0) | DOT_PRODUCT: SIZE(VECTOR_A) is %lld but SIZE(VECTOR_B) is %lld (RT/dot-product.h:63-67) | SHI DotProductConversion::genProductExtent, before deduceOptimalExtents; SI DOT_PRODUCT at the call site (the generated body always loops over the first vector) | Both vectors (VECTOR_B is enough for SI) |
| A8 | MATMUL | ext(a, rank(a)-1) == ext(b,0) | MATMUL: unacceptable operand shapes (%lldx%lld, %lldx%lld) and the vector forms (RT/matmul.h:272-293) | SHI MatmulConversion::genResultShape, before deduceOptimalExtents | Both matrices |
| A9 | MATMUL_TRANSPOSE | ext(a,0) == ext(b,0) | MATMUL-TRANSPOSE: unacceptable operand shapes (%lldx%lld, %lldx%lld) (RT/matmul-transpose.h:201-209) | same | Both matrices |
| A10 | Array MASK of SUM, PRODUCT, MAXVAL, MINVAL, MAXLOC, MINLOC (a scalar MASK conforms with anything) | !present(mask) || for each d: ext(mask,d) == ext(array,d), with the extents read inside fir.if present | Incompatible array arguments to %s: dimension %lld of ARRAY has extent %lld but MASK has extent %lld (RT/tools.cpp:97-104) | SHI ReductionAsElementalConverter::convert (:1142-1266); SI simplifyMinMaxlocReduction | MASK |
| A11 | Non-constant DIM on a rank-1 argument of ALL, ANY, COUNT, MAXLOC, MINLOC | dim == 1 | %s: bad DIM=%lld for ARRAY with rank 1 (RT/reduction.cpp:256, RT/tools.cpp:345) | SHI AllAnyAsElementalConverter, CountAsElementalConverter, MinMaxlocAsElementalConverter; SI simplifyLogicalDim1Reduction, simplifyMinMaxlocReduction | None (DIM is not used for addressing) |
Not lost:
getConstDim).argument).| ID | Operation | Condition | Runtime message | Inserted in | Guard |
|---|---|---|---|---|---|
| A12 | CSHIFT and EOSHIFT with an array SHIFT | for each k, with j the matching ARRAY dimension (skipping DIM): ext(shift,k) == ext(array,j) | %s: on dimension %lld, SHIFT= has extent %lld but ARRAY= has extent %lld (RT/transformational.cpp:48-53) | SHI ArrayShiftConversion, after the ARRAY extents are computed, before hlfir.elemental or hlfir.eval_in_mem | SHIFT |
| A13 | EOSHIFT with an array BOUNDARY | !present(boundary) || same per-dimension test | EOSHIFT: BOUNDARY= has extent %lld on dimension %lld but must conform with extent %lld of ARRAY= (RT/transformational.cpp:630-643) | same | BOUNDARY |
| A14 | EOSHIFT on CHARACTER with BOUNDARY | len(boundary) == len(array) | EOSHIFT: BOUNDARY= has element byte length %lld, but ARRAY= has length %lld (RT/transformational.cpp:625-629, :700-704) | same; the inline path pads or truncates instead of failing | None |
| A15 | RESHAPE: SHAPE values | for each i: shape(i) >= 0 | RESHAPE: bad value for SHAPE(%lld)=%lld (RT/transformational.cpp:818-821) | SHI ReshapeAsElementalConversion, after the SHAPE loads, before fir.shape (:3130-3137) | SHAPE extents fed to fir.shape |
| A16 | RESHAPE: enough elements | product(shape) <= size(array) || (present(pad) && size(pad) > 0) | RESHAPE: not enough elements, need %lld but only have %lld (RT/transformational.cpp:831-836) | same | SOURCE and PAD, and the dynamic extents used as divisors |
A16 also prevents an unsigned division by zero in the inline index computation when ARRAY or PAD is empty (SHI:3262-3266). The divisions do not access memory, so guarding SOURCE does not order them: the extents they divide by are guarded too. Constant extents need no guard, because MLIR already treats a division by a non-zero constant as speculatable.
Not lost: TRANSPOSE (rank only, static), character comparison (hlfir.cmpchar), and INDEX. Their runtime entry points have no user-facing checks.
| ID | Kind | Operation | Condition | Message | Inserted in | Guard |
|---|---|---|---|---|---|---|
| B1 | status | ALLOCATE of an allocated allocatable (#223287) | null(box) | the object '%s' is already allocated (runtime stat text: Base address is not null) | flang/lib/Lower/Allocatable.cpp, AllocateStmtHelper, before genInlinedAllocation (:562-574) | None needed (the program terminates before the leak is observable) |
| B2 | status | DEALLOCATE of an unallocated allocatable | !null(box) | the object '%s' is not allocated (runtime: Base address is null) | same file, inline branch of genDeallocate, before genFreemem (:1015-1020) | None needed (free(NULL) is harmless) |
| B3 | divide | Integer MOD/MODULO, P==0 | p != 0 | MOD with P==0 / MODULO with P==0 | IC genIntegerZeroDivisorCheck: replace fir.if; also add it to the UNSIGNED path (genMod, genModulo), which has none | p (the arith.remsi operand) |
| B4 | migrate | NEAREST S==0, IEEE RADIX/=2, STORAGE_SIZE of an unallocated polymorphic, realloc with scalar RHS | existing conditions | existing messages | IC genNearest, checkRadix, genStorageSize; MutableBox.cpp genReallocIfNeeded | Operand of the protected op |
| B5 | bits | BTEST, IBCLR/IBSET, IBITS, ISHFTC, MASKL/MASKR, MVBITS (out of range gives poison); ISHFT and SHIFTL/SHIFTR/SHIFTA/DSHIFTL/DSHIFTR (clamped, so silently accepted) | 0 <= pos < bit_size, pos + len <= bit_size, and similar, computed on the arguments' original kind (before any conversion) and without overflow | gfortran -fcheck=bits style | IC per intrinsic | Shift or position operand |
| B6 | conformance | Elemental operations and elemental procedure references on arrays | for each d: ext(x,d) == ext(s,d) for every array operand x, where s is the operand providing the iteration space | New, no runtime equivalent | X2H binary and unary elemental operations (:1838-1886), elemental calls in flang/lib/Lower/ConvertCall.cpp (:2440-2472); both carry a TODO: merge shape | Operands other than s |
| B7 | conformance | WHERE mask and assignments, FORALL assignments, assignments to vector-subscripted left-hand sides (hlfir.region_assign) | mask, left-hand side, and right-hand side extents agree (F2023 10.2.1.2, 10.2.3.1) | New | LowerHLFIROrderedAssignments, which builds the loops | Arrays accessed in the loops, including vector subscripts |
B1 and B2 are required by the standard and should be unconditional. The runtime path already checks them (RT/allocatable.cpp:142-143, :198-199); -mllvm -use-alloc-runtime works around the problem today.
B6 and B7 are new behavior at every optimization level: the runtime is never involved, and Flang checks neither elemental expressions nor these assignments today. gfortran only checks conformance under -fcheck=bounds, and ifx under -check shape. Whether they belong to conformance or to a separate opt-in kind is open (section 9). The choice does not affect the other checks, which protect their own accesses. An elemental-call argument with dynamic OPTIONAL handling (ConvertCall.cpp:2452-2471) may be absent, so its condition must include presence.
Also note: the CSHIFT shift normalization calls genModulo with the extent hidden behind a select, so with -fcheck-integer-mod-zero-divisor every CSHIFT gets a dead MODULO with P==0 check (SHI normalizeShiftValue). Constant folding of the assert, or emitting arith.remsi directly there, fixes it.
bounds, pointer, do)inb(i, lb, ext) is the single unsigned test (i - lb) <u ext. It is equivalent to lb <= i <= lb + ext - 1 and false when ext == 0.
| ID | Kind | Check | Condition | Inserted in | Guard | Notes |
|---|---|---|---|---|---|---|
| C1 | bounds | Array element subscript | for each d: inb(i_d, lb_d, ext_d); for the last dimension of an assumed-size array, only i >= lb | X2H HlfirDesignatorBuilder::visit(ArrayRef), scalar-subscript branch, after genSubscript (:737-743) | Subscript value | Skip constant subscripts with constant bounds (checked by semantics). Keep the legacy dimension(1) dummy idiom working. |
| C2 | bounds | Section triplet lo:hi:st | st != 0; then n == 0 || (inb(lo) && inb(last)), last = lo + (n-1)*st, with the extent n computed from the guarded st | same function, triplet branch, before the extent is computed with genExtentFromTriplet (:713-736) | st first, then lo, hi | Check last, not hi (a(1:11:3) is valid for ub=10). Zero-size sections are exempt. |
| C3 | bounds | Vector subscript elements | inb(v(k), lb, ext) | X2H createVectorSubscriptElementAddrOp, after the element load (:995-1003) | Loaded subscript | O(n). A MINVAL/MAXVAL pre-check is cheaper but loses the index in the message. |
| C4 | bounds | Substring s(lo:hi) | hi < lo || (lo >= 1 && hi <= len) | X2H gen(Substring), before the length is replaced (:549-555) | lo, hi | Must be done before ConvertToFIR, which drops hi. |
| C5 | bounds | Character dummy length | As required by F2023 15.5.2.5: passedLen >= declaredLen for scalars of default or C character kind, equality for assumed-shape and assumed-rank dummies and other kinds, no check for sequence-associated arrays | flang/lib/Lower/ConvertVariable.cpp, dummy mapping (non-box :3129-3142, box :2898-2939) | Dummy base address | Skip bare-reference dummies (no length) and absent OPTIONAL. |
| C6 | pointer | POINTER dereference | !null(box_addr(load p)) | Context-aware call sites, not inside hlfir::derefPointersAndAllocatables: X2H visit(SymbolRef) (:648-653), the pointer-component paths (:808-809, :889), and flang/lib/Lower/ConvertCall.cpp for a non-optional, non-pointer dummy (:1497-1498) | Loaded box or base address | Several callers of derefPointersAndAllocatables dereference a possibly null box on purpose (a disassociated actual passed to an OPTIONAL dummy counts as absent, F2023 15.5.2.13 p1). |
| C7 | pointer | ALLOCATABLE dereference | same as C6 | same sites | same | Exempt realloc assignment, ALLOCATED, MOVE_ALLOC, and actuals passed to ALLOCATABLE or OPTIONAL dummies. A zero-size allocation has a non-null address, so “allocated” means “non-null”. |
| C8 | pointer | Use of an absent OPTIONAL dummy | fir.is_present %x | Part references (X2H visit(SymbolRef)), value uses, actuals passed to non-optional dummies (ConvertCall.cpp, genIsPresentIfArgMaybeAbsent) | Declare result at the use site | Guard per use, not once at entry (uses under if (present(x)) are legal). Exempt PRESENT and forwarding to OPTIONAL dummies. |
| C9 | do | Zero step in DO, DO CONCURRENT, FORALL, and array-constructor and I/O implied-DOs | step != 0 | Where lowering computes the step, e.g. flang/lib/Lower/Bridge.cpp for DO (:3094-3099) | Step value | A zero step currently divides by zero in the trip-count computation (SIGFPE at every optimization level). Section triplets are covered by C2. |
Out of scope for now: the explicit-shape dummy size versus the actual (callee only has an address), and checks of undefined POINTER status.
A7, DOT_PRODUCT (SHI DotProductConversion):
%da:3 = fir.box_dims %a, %c0 : (!fir.box<!fir.array<?xf32>>, index) -> (index, index, index)
%db:3 = fir.box_dims %b, %c0 : (!fir.box<!fir.array<?xf32>>, index) -> (index, index, index)
%eq = arith.cmpi eq, %da#1, %db#1 : index
%a_ok, %b_ok = fir.assert %eq, conformance,
"DOT_PRODUCT: SIZE(VECTOR_A) is %lld but SIZE(VECTOR_B) is %lld"
values(%da#1, %db#1 : index, index)
guard(%a, %b : !fir.box<!fir.array<?xf32>>, !fir.box<!fir.array<?xf32>>)
// The loop bound is not guarded, and stays a constant when an extent is one.
%r = fir.do_loop %i = %c1 to %da#1 step %c1 iter_args(%acc = %zero) -> (f32) {
%ea = hlfir.designate %a_ok (%i) : (!fir.box<!fir.array<?xf32>>, index) -> !fir.ref<f32>
%eb = hlfir.designate %b_ok (%i) : (!fir.box<!fir.array<?xf32>>, index) -> !fir.ref<f32>
...
}
A1, assignment of an elemental expression (x = y + 1.0, lowering, then OB ElementalAssignBufferization):
%e = hlfir.elemental %shy unordered : (!fir.shape<1>) -> !hlfir.expr<?xf32> {
^bb0(%i: index):
%yi = hlfir.designate %y#0 (%i) : (!fir.box<!fir.array<?xf32>>, index) -> !fir.ref<f32>
...
}
%dx:3 = fir.box_dims %x#0, %c0 : (!fir.box<!fir.array<?xf32>>, index) -> (index, index, index)
%eq = arith.cmpi eq, %dx#1, %ny : index
%x_ok, %e_ok = fir.assert %eq, conformance,
"Assign: mismatching extents on dimension %lld in array assignment (to %lld, from %lld)"
values(%c1, %dx#1, %ny : index, index, index)
guard(%x#0, %e : !fir.box<!fir.array<?xf32>>, !hlfir.expr<?xf32>)
hlfir.assign %e_ok to %x_ok : !hlfir.expr<?xf32>, !fir.box<!fir.array<?xf32>>
// After OptimizedBufferization, the guard on %e has moved to the array that
// its body accesses (section 7), and the loop is free of asserts.
%x_ok, %y_ok = fir.assert %eq, conformance, "..." values(...)
guard(%x#0, %y#0 : !fir.box<!fir.array<?xf32>>, !fir.box<!fir.array<?xf32>>)
fir.do_loop %i = %c1 to %ny step %c1 unordered {
%yi = hlfir.designate %y_ok (%i) : (!fir.box<!fir.array<?xf32>>, index) -> !fir.ref<f32>
...
%xi = hlfir.designate %x_ok (%i) : (!fir.box<!fir.array<?xf32>>, index) -> !fir.ref<f32>
hlfir.assign %v to %xi : f32, !fir.ref<f32>
}
When the elemental shape comes from x itself (x = x + 1.0), the two extents are the same SSA value and the assert folds away.
A10, SUM with an OPTIONAL MASK (SHI ReductionAsElementalConverter):
%present = fir.is_present %mask : (!fir.box<!fir.array<?x!fir.logical<4>>>) -> i1
%m:2 = fir.if %present -> (i1, index) {
%dm:3 = fir.box_dims %mask, %c0 : (...) -> (index, index, index)
%eq = arith.cmpi eq, %dm#1, %na : index
fir.result %eq, %dm#1 : i1, index
} else {
fir.result %true, %na : i1, index
}
%mask_ok = fir.assert %m#0, conformance,
"Incompatible array arguments to SUM: dimension %lld of ARRAY has extent %lld but MASK has extent %lld"
values(%c1, %na, %m#1 : index, index, index)
guard(%mask : !fir.box<!fir.array<?x!fir.logical<4>>>)
// reduction loop over 1..%na, MASK read through %mask_ok at the same index
A15/A16, RESHAPE without PAD (SHI ReshapeAsElementalConversion):
%s1 = fir.load %p1 : !fir.ref<i32>
%nn = arith.cmpi sge, %s1, %c0_i32 : i32
%s1i = fir.convert %s1 : (i32) -> index
%da:3 = fir.box_dims %a, %c0 : (!fir.box<!fir.array<?xf32>>, index) -> (index, index, index)
%enough = arith.cmpi ule, %s1i, %da#1 : index
%ok = arith.andi %nn, %enough : i1
%ext, %a_ok = fir.assert %ok, argument, "RESHAPE: bad SHAPE= or not enough elements in SOURCE="
guard(%s1i, %a : index, !fir.box<!fir.array<?xf32>>)
%shape = fir.shape %ext : (index) -> !fir.shape<1>
%r = hlfir.elemental %shape unordered : (!fir.shape<1>) -> !hlfir.expr<?xf32> { ... reads %a_ok ... }
The SHAPE values are guarded because they size the result, which may be allocated as a temporary. This is not an iteration-space guard: SOURCE is guarded separately, so its accesses stay protected wherever the elemental is inlined.
Two separate asserts give the two exact runtime messages. One combined assert gives a smaller IR.
B1, ALLOCATE status (Allocatable.cpp):
%bx = fir.load %a#0 : !fir.ref<!fir.box<!fir.heap<!fir.array<?xi32>>>>
%addr = fir.box_addr %bx : (!fir.box<!fir.heap<!fir.array<?xi32>>>) -> !fir.heap<!fir.array<?xi32>>
%allocated = fir.is_present %addr : (!fir.heap<!fir.array<?xi32>>) -> i1 // non-null test
%free = arith.xori %allocated, %true : i1
fir.assert %free, status, "the object 'array' is already allocated"
%mem = fir.allocmem !fir.array<?xi32>, %n {uniq_name = "_QFEarray.alloc"}
C1, array element a(i) (X2H HlfirDesignatorBuilder):
%d:3 = fir.box_dims %a#0, %c0 : (!fir.box<!fir.array<?xf32>>, index) -> (index, index, index)
%off = arith.subi %i, %d#0 : index
%ok = arith.cmpi ult, %off, %d#1 : index
%i_ok = fir.assert %ok, bounds, "subscript %lld is out of bounds for dimension 1 of 'a' (%lld:%lld)"
values(%i, %d#0, %ub : index, index, index) guard(%i : index)
%e = hlfir.designate %a#0 (%i_ok) : (!fir.box<!fir.array<?xf32>>, index) -> !fir.ref<f32>
C6, POINTER dereference p(i):
%bx = fir.load %p#0 : !fir.ref<!fir.box<!fir.ptr<!fir.array<?xf32>>>> %adr = fir.box_addr %bx : (!fir.box<!fir.ptr<!fir.array<?xf32>>>) -> !fir.ptr<!fir.array<?xf32>> %ok = fir.is_present %adr : (!fir.ptr<!fir.array<?xf32>>) -> i1 %bx_ok = fir.assert %ok, pointer, "POINTER 'p' is not associated" guard(%bx : !fir.box<!fir.ptr<!fir.array<?xf32>>>) %e = hlfir.designate %bx_ok (%i) : (!fir.box<!fir.ptr<!fir.array<?xf32>>>, index) -> !fir.ref<f32>
Pipeline (flang/lib/Optimizer/Passes/Pipelines.cpp), with the fir.assert steps added:
| Stage | Pass (existing, unless marked new) | Optimization level | fir.assert role |
|---|---|---|---|
| Lowering | flang/lib/Lower (assignments, elemental expressions, designators, ALLOCATE, DEALLOCATE, DO, intrinsics in IC) | all | Emit A1, B1-B6, and C1-C9 |
| HLFIR | SimplifyHLFIRIntrinsics (twice) | O1+ | Emit A7-A16 |
| HLFIR | InlineElementals, SeparateAllocatableAssign | all | Emit A6 (SeparateAllocatableAssign); move guards from inlined expressions to the arrays they access (section 7) |
| HLFIR | CSE, canonicalize | O1+ | Fold constant asserts; merges the conditions |
| HLFIR | OptimizedBufferization, InlineHLFIRAssign (twice), InlineHLFIRCopy (O3) | O1+ (InlineHLFIRAssign also at O0 for device code, scalar right-hand sides only) | Emit A2, A5, A6; move guards as InlineElementals does |
| HLFIR | LowerHLFIROrderedAssignments | all | Emit B7; move guards as InlineElementals does |
| HLFIR | BufferizeHLFIR | all | Move guards from expressions to their buffers |
| HLFIR | ConvertHLFIRtoFIR | all | Emit A6 (scalar allocatable assignment) |
| FIR | CSE, canonicalize | all | Fold constant asserts |
| FIR | new, optional: redundant-assert elimination | O1+ | Dominance-based merging (section 3.4) |
| FIR | SimplifyIntrinsics | O1+ | Emit A7, A10, A11 at the call site |
| FIR | flang LICM, extended | O1+ | Hoist invariant asserts (section 7) |
| FIR | new, optional: redundant-assert elimination (second run) | O1+ | Merge asserts that hoisting made comparable |
| FIR | SimplifyFIROperations, CFGConversion | all | Nothing |
| Device | new: drop asserts by kind, only in pipelines that split one function into host and device variants (for example OpenACC acc routine) | all | Drop the kinds disabled for the device variant (section 4) |
| Code generation | FIRToLLVMLowering | all | Convert to a branch plus a noreturn call (section 3.5) |
At -O0, the passes marked “all” above still emit asserts or move guards, and CSE and canonicalization still run, but hoisting and redundancy elimination do not. The code generation pattern must work on unoptimized IR.
No interference with memory optimizations. The assert writes only the non-addressable runtime resource (section 3.2). Passes that inspect memory effects themselves, instead of asking alias analysis, must also skip effects on non-addressable resources (Resource::isAddressable), as FIR alias analysis does. For example, InlineElementals (isConflictingWrite) and ScheduleOrderedAssignments currently treat any write without a value as a conflict, so an assert between an hlfir.elemental and its hlfir.apply would block inlining.
Inlining a guarded expression. InlineElementals, OptimizedBufferization, and LowerHLFIROrderedAssignments replace hlfir.apply %e, %j, or an hlfir.assign of %e, with the body of the elemental %e evaluated at the consumer‘s index. LowerHLFIROrderedAssignments does the same for the hlfir.elemental_addr of a vector-subscripted designator. When %e is guarded by an assert, the guard must survive. The body’s accesses run at a foreign index, so they are exactly what the assert protects. These passes go through hlfir::inlineElementalOp, which applies one rule:
hlfir.apply. Then map them to the guarded results when cloning the body. The arrays are defined before the elemental, so they dominate the assert and nothing has to move.A guard on a nested expression is handled by the same rule when that expression is inlined. BufferizeHLFIR also uses inlineElementalOp, but to evaluate an elemental at its own definition, so there the guard simply moves to the buffer, as for hlfir.no_reassoc. A pass that does not apply the rule must not inline through the assert. That loses an optimization but does not produce wrong code. The asserts stay before the loops, and the loop bounds remain the unguarded extents. InlineElementals must also find the elemental through the assert (getTwoUses already looks through hlfir.declare and fir.convert).
Hoisting. Conformance asserts are already outside the loops. Asserts inside loops come from per-element checks (section 5.3), from checks on operands that vary per element, and from the fallback above. FIR LICM hoists only pure ops and loads, so asserts stay in the loop, and so do the operations depending on their guarded results. A dedicated rule is needed. Moving an assert earlier is legal only if (a) it executes whenever the new position does: the loop runs at least once (or the hoisted condition accounts for an empty loop), and the assert runs on every iteration, which a conditional CYCLE or EXIT can break; and (b) no operation it crosses conflicts with its effect (calls, I/O, STOP, other asserts). Its users can then be hoisted too. LLVM could also do this after code generation, but doing it in Flang leaves simpler loops for the MLIR loop optimizations.
Range checks for subscripts (C1). For an affine subscript in a fir.do_loop, check the range of values it takes before the loop instead of each value. Two variants:
fir::LoopVersioning. Recommended for -O2 -fcheck=bounds.Vectorization. After code generation, an assert inside a loop is an early exit to a noreturn call, which the LLVM loop vectorizer does not handle. The A checks, and the B checks on loop-invariant operands, are placed before the loop nests, so they cost nothing per element. B checks on operands that vary per element (for example MOD with an array P) and the C checks can end up in hot loops.
Never check compiler-generated accesses. Element accesses in loops created by BufferizeHLFIR and the inlining passes are in bounds by construction once the A checks hold. That is why bounds checks belong in HlfirDesignatorBuilder and not in a pass over all hlfir.designate, or in the code generation of fir.array_coor.
Device code. Per-element checks in kernels cost registers and block unrolling and vectorized loads. For -fcheck=bounds in offloaded loops, prefer range checks before the kernel launch, and keep in-kernel checks for non-affine subscripts.
fir.assert with FortranRuntimeResource, verifier, canonicalization, code generation, the runtime entry with values, and a noreturn declaration. Make the passes that inspect effects skip non-addressable ones, make the analyses look through guards (section 3.2), and implement the rule for inlining guarded expressions in hlfir::inlineElementalOp, with the BufferizeHLFIR pattern for asserts on expressions (section 7).-fcheck= plumbing, then C9, C1, C2, C4, C5, then C6-C8, then C3 and B5.conformance, argument) is disabled by an option, should its checks be emitted as assumptions (llvm.intr.assume at code generation) instead of not at all? An assumption gives more optimization, but a false condition becomes undefined behavior. This seems acceptable when the standard only makes the program non-conforming, which is the case for these kinds. It does not apply to status: there the standard requires error termination, and disabling the kind is only meant for diagnosing regressions, so its checks are simply not emitted. Input from users on the expected behavior would help. Opt-in kinds that are not enabled are never emitted, as assertions or as assumptions.status, conformance, and argument. Should the checks that are new at every optimization level (B6, B7) be in conformance (on by default) or in a separate opt-in kind?fir.allocmem, or give fir.allocmem a “fail on null” attribute handled in code generation?RT/character.cpp, CharacterCompare: yChars uses shift<char> instead of shift<CHAR>, so KIND=2 and KIND=4 array comparisons read past the end of the second operand. This descriptor-based entry point is not called by Flang lowering, which compares characters element by element with the scalar entry points.RT/tools.cpp, CheckIntegerKind: the format arguments are passed in the wrong order.RT/reduction.cpp: the rank-1 DIM check of ALL, ANY, COUNT and PARITY accepts DIM=0.ReportFatalUserError is not in the OpenMP offload API group, unlike Abort.