[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