[llvm] e40e0bc - [llubi] Add support for llvm.speculative.load. (#226839)
via llvm-commits
llvm-commits at lists.llvm.org
Mon Sep 28 10:54:22 PDT 2026
Author: Florian Hahn
Date: 2026-09-28T18:54:16+01:00
New Revision: e40e0bc36b14d296df5798c52792f662c715b445
URL: https://github.com/llvm/llvm-project/commit/e40e0bc36b14d296df5798c52792f662c715b445
DIFF: https://github.com/llvm/llvm-project/commit/e40e0bc36b14d296df5798c52792f662c715b445.diff
LOG: [llubi] Add support for llvm.speculative.load. (#226839)
Implement the direct form of llvm.speculative.load, where the number of
accessible bytes N is passed as an i64. Only the N accessible bytes are
read from memory and they must be in bounds of the underlying object;
all other bytes are poison. With from_end, the accessible bytes are the
last N bytes of the loaded value. It is UB if N is poison or exceeds the
size of the loaded type.
Support for the oracle form will be added as follow-up.
PR: https://github.com/llvm/llvm-project/pull/226839
Added:
llvm/test/tools/llubi/intr_speculative_load.ll
llvm/test/tools/llubi/intr_speculative_load_ub.ll
Modified:
llvm/tools/llubi/lib/Interpreter.cpp
Removed:
################################################################################
diff --git a/llvm/test/tools/llubi/intr_speculative_load.ll b/llvm/test/tools/llubi/intr_speculative_load.ll
new file mode 100644
index 0000000000000..8e927f5b49038
--- /dev/null
+++ b/llvm/test/tools/llubi/intr_speculative_load.ll
@@ -0,0 +1,56 @@
+; RUN: llubi --verbose --entry-function=first_bytes < %s 2>&1 | FileCheck %s --check-prefix=FIRST
+; RUN: llubi --verbose --entry-function=last_bytes < %s 2>&1 | FileCheck %s --check-prefix=LAST
+; RUN: llubi --verbose --entry-function=past_end < %s 2>&1 | FileCheck %s --check-prefix=PAST-END
+; RUN: not llubi --verbose --entry-function=oracle_load < %s 2>&1 | FileCheck %s --check-prefix=ORACLE
+
+ at a = global [6 x i32] [i32 0, i32 1, i32 2, i32 3, i32 4, i32 5]
+
+; Returns the number of bytes from %p to %end, clamped to 16.
+define i64 @oracle(ptr %p, ptr %end) memory(none) nounwind nosync willreturn {
+ %p.int = ptrtoaddr ptr %p to i64
+ %end.int = ptrtoaddr ptr %end to i64
+ %
diff = sub i64 %end.int, %p.int
+ %n = call i64 @llvm.umin.i64(i64 %
diff , i64 16)
+ ret i64 %n
+}
+
+define void @first_bytes() {
+; FIRST: Entering function: first_bytes
+; FIRST-NEXT: %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr @a, i1 false, i64 8) => { i32 0, i32 1, poison, poison }
+; FIRST-NEXT: ret void
+; FIRST-NEXT: Exiting function: first_bytes
+ %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr @a, i1 false, i64 8)
+ ret void
+}
+
+define void @last_bytes() {
+; LAST: Entering function: last_bytes
+; LAST-NEXT: %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr @a, i1 true, i64 8) => { poison, poison, i32 2, i32 3 }
+; LAST-NEXT: ret void
+; LAST-NEXT: Exiting function: last_bytes
+ %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr @a, i1 true, i64 8)
+ ret void
+}
+
+define void @past_end() {
+; PAST-END: Entering function: past_end
+; PAST-END-NEXT: %p = getelementptr i32, ptr @a, i64 4 => ptr 0x20 [@a + 16]
+; PAST-END-NEXT: %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr %p, i1 false, i64 8) => { i32 4, i32 5, poison, poison }
+; PAST-END-NEXT: ret void
+; PAST-END-NEXT: Exiting function: past_end
+ %p = getelementptr i32, ptr @a, i64 4
+ %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr %p, i1 false, i64 8)
+ ret void
+}
+
+define void @oracle_load() {
+; ORACLE: Entering function: oracle_load
+; ORACLE-NEXT: %p = getelementptr i32, ptr @a, i64 4 => ptr 0x20 [@a + 16]
+; ORACLE-NEXT: %end = getelementptr i32, ptr @a, i64 6 => ptr 0x28 [@a + 24]
+; ORACLE-NEXT: Unrecognized instruction: %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr %p, i1 false, ptr @oracle, ptr %p, ptr %end)
+; ORACLE-NEXT: error: Execution of function 'oracle_load' failed.
+ %p = getelementptr i32, ptr @a, i64 4
+ %end = getelementptr i32, ptr @a, i64 6
+ %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr %p, i1 false, ptr @oracle, ptr %p, ptr %end)
+ ret void
+}
diff --git a/llvm/test/tools/llubi/intr_speculative_load_ub.ll b/llvm/test/tools/llubi/intr_speculative_load_ub.ll
new file mode 100644
index 0000000000000..e08e1e410183d
--- /dev/null
+++ b/llvm/test/tools/llubi/intr_speculative_load_ub.ll
@@ -0,0 +1,86 @@
+; RUN: not llubi --verbose --entry-function=out_of_bounds < %s 2>&1 | FileCheck %s --check-prefix=OOB
+; RUN: not llubi --verbose --entry-function=from_end_out_of_bounds < %s 2>&1 | FileCheck %s --check-prefix=FROM-END-OOB
+; RUN: not llubi --verbose --entry-function=exceeds_size < %s 2>&1 | FileCheck %s --check-prefix=EXCEEDS-SIZE
+; RUN: not llubi --verbose --entry-function=poison_num_bytes < %s 2>&1 | FileCheck %s --check-prefix=POISON-N
+; RUN: not llubi --verbose --entry-function=poison_pointer < %s 2>&1 | FileCheck %s --check-prefix=POISON-PTR
+; RUN: not llubi --verbose --entry-function=oracle_out_of_bounds < %s 2>&1 | FileCheck %s --check-prefix=ORACLE-OOB
+; RUN: not llubi --verbose --entry-function=oracle_declaration < %s 2>&1 | FileCheck %s --check-prefix=ORACLE-DECL
+
+ at a = global [2 x i32] [i32 0, i32 1]
+
+; Returns one element more than the number of bytes from %p to %end.
+define i64 @oracle_off_by_one(ptr %p, ptr %end) memory(none) nounwind nosync willreturn {
+ %p.int = ptrtoaddr ptr %p to i64
+ %end.int = ptrtoaddr ptr %end to i64
+ %
diff = sub i64 %end.int, %p.int
+ %n = add i64 %
diff , 4
+ ret i64 %n
+}
+
+declare i64 @oracle_decl(i64) memory(none) nounwind nosync willreturn
+
+define void @out_of_bounds() {
+; OOB: Entering function: out_of_bounds
+; OOB-NEXT: Stacktrace:
+; OOB-NEXT: #0 %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr @a, i1 false, i64 12) at @out_of_bounds <stdin>:{{[0-9]+}}
+; OOB-NEXT: Immediate UB detected: Memory access is out of bounds. Accessed size: 12, Address: 0xc, Object base: 0xc, Object size: 8.
+; OOB-NEXT: error: Execution of function 'out_of_bounds' failed.
+ %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr @a, i1 false, i64 12)
+ ret void
+}
+
+define void @from_end_out_of_bounds() {
+; FROM-END-OOB: Entering function: from_end_out_of_bounds
+; FROM-END-OOB-NEXT: Stacktrace:
+; FROM-END-OOB-NEXT: #0 %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr getelementptr (i8, ptr @a, i64 -8), i1 true, i64 12) at @from_end_out_of_bounds <stdin>:{{[0-9]+}}
+; FROM-END-OOB-NEXT: Immediate UB detected: Memory access is out of bounds. Accessed size: 12, Address: 0x8, Object base: 0xc, Object size: 8.
+; FROM-END-OOB-NEXT: error: Execution of function 'from_end_out_of_bounds' failed.
+ %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr getelementptr (i8, ptr @a, i64 -8), i1 true, i64 12)
+ ret void
+}
+
+define void @exceeds_size() {
+; EXCEEDS-SIZE: Entering function: exceeds_size
+; EXCEEDS-SIZE-NEXT: Stacktrace:
+; EXCEEDS-SIZE-NEXT: #0 %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr @a, i1 false, i64 17) at @exceeds_size <stdin>:{{[0-9]+}}
+; EXCEEDS-SIZE-NEXT: Immediate UB detected: llvm.speculative.load number of accessible bytes 17 exceeds the loaded size 16.
+; EXCEEDS-SIZE-NEXT: error: Execution of function 'exceeds_size' failed.
+ %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr @a, i1 false, i64 17)
+ ret void
+}
+
+define void @poison_num_bytes() {
+; POISON-N: Entering function: poison_num_bytes
+; POISON-N-NEXT: Stacktrace:
+; POISON-N-NEXT: #0 %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr @a, i1 false, i64 poison) at @poison_num_bytes <stdin>:{{[0-9]+}}
+; POISON-N-NEXT: Immediate UB detected: llvm.speculative.load with poison number of accessible bytes.
+; POISON-N-NEXT: error: Execution of function 'poison_num_bytes' failed.
+ %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr @a, i1 false, i64 poison)
+ ret void
+}
+
+define void @poison_pointer() {
+; POISON-PTR: Entering function: poison_pointer
+; POISON-PTR-NEXT: Stacktrace:
+; POISON-PTR-NEXT: #0 %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr poison, i1 false, i64 0) at @poison_pointer <stdin>:{{[0-9]+}}
+; POISON-PTR-NEXT: Immediate UB detected: llvm.speculative.load with poison pointer.
+; POISON-PTR-NEXT: error: Execution of function 'poison_pointer' failed.
+ %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr poison, i1 false, i64 0)
+ ret void
+}
+
+define void @oracle_out_of_bounds() {
+; ORACLE-OOB: Entering function: oracle_out_of_bounds
+; ORACLE-OOB-NEXT: Unrecognized instruction: %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr @a, i1 false, ptr @oracle_off_by_one, ptr @a, ptr getelementptr (i8, ptr @a, i64 8))
+; ORACLE-OOB-NEXT: error: Execution of function 'oracle_out_of_bounds' failed.
+ %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr @a, i1 false, ptr @oracle_off_by_one, ptr @a, ptr getelementptr (i8, ptr @a, i64 8))
+ ret void
+}
+
+define void @oracle_declaration() {
+; ORACLE-DECL: Entering function: oracle_declaration
+; ORACLE-DECL-NEXT: Unrecognized instruction: %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr @a, i1 false, ptr @oracle_decl, i64 4)
+; ORACLE-DECL-NEXT: error: Execution of function 'oracle_declaration' failed.
+ %r = call <4 x i32> (ptr, i1, ...) @llvm.speculative.load.v4i32.p0(ptr @a, i1 false, ptr @oracle_decl, i64 4)
+ ret void
+}
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index 0c7e74f52f689..3e4b250f4c85c 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -919,6 +919,46 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
return AnyValue();
}
+ AnyValue callSpeculativeLoadIntrinsic(CallBase &CB, const AnyValue &Ptr,
+ const AnyValue &NumBytes) {
+ Type *RetTy = CB.getType();
+ if (Ptr.isPoison()) {
+ reportImmediateUB() << "llvm.speculative.load with poison pointer.";
+ return AnyValue();
+ }
+ if (NumBytes.isPoison()) {
+ reportImmediateUB()
+ << "llvm.speculative.load with poison number of accessible bytes.";
+ return AnyValue();
+ }
+
+ const uint64_t Size = Ctx.getEffectiveTypeStoreSize(RetTy);
+ const APInt &NumBytesInt = NumBytes.asInteger();
+ if (NumBytesInt.ugt(Size)) {
+ reportImmediateUB() << "llvm.speculative.load number of accessible bytes "
+ << NumBytesInt.getZExtValue()
+ << " exceeds the loaded size " << Size << ".";
+ return AnyValue();
+ }
+
+ // Only the accessible bytes are read from memory and must be in bounds of
+ // the underlying object. All other bytes are poison.
+ const uint64_t N = NumBytesInt.getZExtValue();
+ SmallVector<Byte> Bytes(Size, Byte::poison());
+ if (N != 0) {
+ const bool FromEnd = cast<ConstantInt>(CB.getArgOperand(1))->isOne();
+ const uint64_t Start = FromEnd ? Size - N : 0;
+ const Pointer &PtrVal = Ptr.asPointer();
+ auto [MO, Offset] =
+ verifyMemAccess(PtrVal.getWithNewAddr(PtrVal.address() + Start), N,
+ Align(1), /*IsStore=*/false);
+ if (!MO)
+ return AnyValue();
+ copy(MO->getBytes().slice(Offset, N), Bytes.begin() + Start);
+ }
+ return Ctx.fromBytes(Bytes, RetTy);
+ }
+
public:
InstExecutor(Context &C, EventHandler &H, Function &F,
ArrayRef<AnyValue> Args, AnyValue &RetVal)
@@ -1743,6 +1783,14 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
case Intrinsic::memset:
case Intrinsic::memset_inline:
return callMemSetIntrinsic(CB, Args);
+ case Intrinsic::speculative_load:
+ // TODO: Support the oracle form.
+ if (isa<Function>(CB.getArgOperand(2))) {
+ Handler.onUnrecognizedInstruction(CB);
+ setFailed();
+ return AnyValue();
+ }
+ return callSpeculativeLoadIntrinsic(CB, Args[0], Args[2]);
case Intrinsic::experimental_noalias_scope_decl:
// FIXME: Not implemented yet. Currently it acts as a noop.
return AnyValue();
More information about the llvm-commits
mailing list