LLVM IR does not turn every invalid arithmetic or pointer condition into immediate undefined behavior. Many instructions produce a poison value instead. That value can flow through later instructions, preserving the optimizer’s ability to rely on promises such as “this addition does not overflow” without forcing undefined behavior at the exact instruction where the promise is violated.
This is part of LLVM’s semantics, not an optimizer implementation detail. A frontend that emits nsw, nuw, inbounds, noundef, or related constraints is stating facts that later passes may trust.
Poison is deferred undefined behavior
Consider a signed addition:
define i32 @next(i32 %x) {
entry:
%y = add nsw i32 %x, 1
ret i32 %y
}The nsw flag says the signed addition does not wrap. If %x is 2147483647, the mathematical result cannot be represented by i32. LLVM does not require the add itself to trigger immediate undefined behavior. Its result becomes poison.
This separation supports speculative transformations. An optimizer may move or evaluate an instruction on a path where its result is never consumed. Poison allows the invalid result to exist while postponing the stronger consequence until a later operation requires a usable value.
The same pattern appears beyond add nsw. No-wrap flags and several pointer or metadata constraints can make an instruction produce poison when their declared condition is violated.
Poison propagates through ordinary dependent operations
Most ordinary instructions receiving poison produce poison in turn:
%a = add nsw i32 %x, 1
%b = mul i32 %a, 4
%c = icmp sgt i32 %b, 100If %a is poison, %b is poison, then %c is poison. Even an operation that looks as if it erases the input does not necessarily erase poison:
%a = add nsw i32 %x, 1
%z = and i32 %a, 0For a concrete integer, %z is zero. If %a is poison, %z remains poison. Replacing it unconditionally with zero would discard the semantic consequence of the violated nsw contract.
The instruction that eventually makes execution undefined can therefore be far from the instruction that first created poison.
Sensitive uses turn poison into immediate undefined behavior
LLVM defines several operand positions where poison cannot remain deferred. A branch condition is one of them:
define i32 @classify(i32 %x) {
entry:
%y = add nsw i32 %x, 1
%positive = icmp sgt i32 %y, 0
br i1 %positive, label %yes, label %no
yes:
ret i32 1
no:
ret i32 0
}If %y is poison, %positive is poison. Using that value as the condition of br produces undefined behavior.
Pointer dereferences form another boundary:
%p = getelementptr inbounds i32, ptr %base, i64 %index
%v = load i32, ptr %pAn inbounds GEP carries constraints on the pointer calculation. Violating those constraints can produce poison. Computing the pointer itself is not a memory access, but using a poison pointer as the pointer operand of load produces undefined behavior.
The visible failure site can therefore be a branch, load, store, call, division, or another sensitive operation even though the original contract violation happened earlier.
select is deliberately path-sensitive
Poison does not taint every instruction identically. select is a notable exception:
%r = select i1 %cond, i32 %a, i32 %bIf %cond is well defined, poison in the unselected value does not make %r poison. The selected value determines the result. Poison in the condition itself still poisons the select.
That distinction matters when optimizations replace control flow with select or move computations around it. Correctness depends on poison semantics, not only on the concrete values observed in ordinary executions.
freeze turns deferred uncertainty into one stable value
LLVM provides freeze when a transformation needs a well-defined value even if the input may be undef or poison:
%raw = add nsw i32 %x, 1
%safe = freeze i32 %raw
%cmp = icmp sgt i32 %safe, 0
br i1 %cmp, label %yes, label %noIf %raw is already a normal value, freeze returns it unchanged. If %raw is poison or undef, freeze produces an arbitrary value of the same type. The important rule is that every use of that particular freeze result observes the same chosen value.
freeze is not a blanket fix for an incorrect frontend contract. Adding it everywhere would weaken useful facts and can block valid optimization. Its role is narrower: a transformation can explicitly stop deferred undefined behavior from propagating into a use that requires a stable value.
undef and poison are not interchangeable
LLVM also has undef, but its semantics are different. An undef value can provide an arbitrary bit pattern, and different uses can observe different values. Current LLVM documentation deprecates general use of undef and reserves it mainly for cases that still require it, notably uninitialized memory.
Poison is stronger. It records a violated semantic constraint and propagates through most dependent instructions. That stronger property permits optimizations that undef would not justify.
freeze makes the distinction visible:
%u = freeze i32 undef
%a = add i32 %u, %uBoth uses of %u see the same value because the frozen result is stable. Without freeze, transitive uses involving undef are not generally required to agree.
Frontend flags are semantic contracts
Many poison bugs originate before optimization. A frontend emits a flag because a condition is usually true rather than because the source-language semantics guarantee it.
%sum = add nsw i32 %a, %bThis is not merely a faster spelling of integer addition. It asserts that signed overflow does not occur. If the source language defines wrapping behavior, or the frontend has not established the no-overflow condition, adding nsw changes the IR semantics.
The same discipline applies to inbounds on getelementptr. Without inbounds, GEP can compute an out-of-object address without dereferencing it. Adding inbounds introduces stronger constraints, and violating them can yield poison.
Optimization passes are entitled to trust these properties. A later transformation that looks surprising may be correct under the declared IR contract even when the frontend annotation was wrong.
Poison therefore separates the broken invariant from the eventual failure site. That separation gives LLVM room to optimize aggressively, but it also makes IR generation exacting: flags and attributes are contracts, poison carries their violations forward, and freeze is the explicit boundary for turning deferred uncertainty into one stable value.