[llvm] [llubi] Add initial support for llubi (PR #180022)
Yingwei Zheng via llvm-commits
llvm-commits at lists.llvm.org
Sat Feb 7 02:50:34 PST 2026
https://github.com/dtcxzyw updated https://github.com/llvm/llvm-project/pull/180022
>From aed4605d9760d1ea49b2f7bb22c9fd2e9562c7cf Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Fri, 6 Feb 2026 03:22:45 +0800
Subject: [PATCH 01/15] [llubi] Add initial support for llubi
---
llvm/docs/CommandGuide/index.rst | 1 +
llvm/docs/CommandGuide/llubi.rst | 79 ++++++++
llvm/docs/TestingGuide.rst | 3 +
llvm/test/tools/llubi/main.ll | 12 ++
llvm/test/tools/llubi/poison.ll | 12 ++
llvm/tools/llubi/CMakeLists.txt | 17 ++
llvm/tools/llubi/Context.cpp | 123 ++++++++++++
llvm/tools/llubi/Context.h | 165 ++++++++++++++++
llvm/tools/llubi/Interpreter.cpp | 177 +++++++++++++++++
llvm/tools/llubi/Value.cpp | 228 ++++++++++++++++++++++
llvm/tools/llubi/Value.h | 139 ++++++++++++++
llvm/tools/llubi/llubi.cpp | 251 +++++++++++++++++++++++++
llvm/utils/UpdateTestChecks/common.py | 1 +
llvm/utils/update_llubi_test_checks.py | 137 ++++++++++++++
14 files changed, 1345 insertions(+)
create mode 100644 llvm/docs/CommandGuide/llubi.rst
create mode 100644 llvm/test/tools/llubi/main.ll
create mode 100644 llvm/test/tools/llubi/poison.ll
create mode 100644 llvm/tools/llubi/CMakeLists.txt
create mode 100644 llvm/tools/llubi/Context.cpp
create mode 100644 llvm/tools/llubi/Context.h
create mode 100644 llvm/tools/llubi/Interpreter.cpp
create mode 100644 llvm/tools/llubi/Value.cpp
create mode 100644 llvm/tools/llubi/Value.h
create mode 100644 llvm/tools/llubi/llubi.cpp
create mode 100755 llvm/utils/update_llubi_test_checks.py
diff --git a/llvm/docs/CommandGuide/index.rst b/llvm/docs/CommandGuide/index.rst
index 8f080ded81c69..c6427d1245b9a 100644
--- a/llvm/docs/CommandGuide/index.rst
+++ b/llvm/docs/CommandGuide/index.rst
@@ -17,6 +17,7 @@ Basic Commands
dsymutil
llc
lli
+ llubi
llvm-as
llvm-cgdata
llvm-config
diff --git a/llvm/docs/CommandGuide/llubi.rst b/llvm/docs/CommandGuide/llubi.rst
new file mode 100644
index 0000000000000..0b66b45c44cd8
--- /dev/null
+++ b/llvm/docs/CommandGuide/llubi.rst
@@ -0,0 +1,79 @@
+llubi - LLVM UB-aware Interpreter
+=================================================
+
+.. program:: llubi
+
+SYNOPSIS
+--------
+
+:program:`llubi` [*options*] [*filename*] [*program args*]
+
+DESCRIPTION
+-----------
+
+:program:`llubi` directly executes programs in LLVM bitcode format and track values in LLVM IR semantics.
+Unlike :program:`lli`, :program:`llubi` is designed to be aware of undefined behaviors during execution.
+It detects immediate undefined behaviors such as integer division by zero, and respects poison generating flags
+like `nsw` and `nuw`. As it captures most of the guardable undefined behaviors, it is highly suitable for
+constructing an interesting-ness test for miscompilation bugs.
+
+If `filename` is not specified, then :program:`llubi` reads the LLVM bitcode for the
+program from standard input.
+
+The optional *args* specified on the command line are passed to the program as
+arguments.
+
+GENERAL OPTIONS
+---------------
+
+.. option:: -fake-argv0=executable
+
+ Override the ``argv[0]`` value passed into the executing program.
+
+.. option:: -entry-function=function
+
+ Specify the name of the function to execute as the program's entry point.
+ By default, :program:`llubi` uses the function named ``main``.
+
+.. option:: -help
+
+ Print a summary of command line options.
+
+.. option:: -verbose
+
+ Print results for each instruction executed.
+
+.. option:: -version
+
+ Print out the version of :program:`llubi` and exit without doing anything else.
+
+INTERPRETER OPTIONS
+--------------
+
+.. option:: -max-mem=N
+
+ Limit the amount of memory (in bytes) that can be allocated by the program, including
+ stack, heap, and global variables. If the limit is exceeded, execution will be terminated.
+ By default, there is no limit (N = 0).
+
+.. option:: -max-stack-depth=N
+
+ Limit the maximum stack depth to N. If the limit is exceeded, execution will be terminated.
+ The default limit is 256. Set N to 0 to disable the limit.
+
+.. option:: -max-steps=N
+
+ Limit the number of instructions executed to N. If the limit is reached, execution will
+ be terminated. By default, there is no limit (N = 0).
+
+.. option:: -vscale=N
+
+ Set the value of `llvm.vscale` to N. The default value is 4.
+
+EXIT STATUS
+-----------
+
+If :program:`llubi` fails to load the program, or an error occurs during execution (e.g, an immediate undefined
+behavior is triggered), it will exit with an exit code of 1.
+If the return type of entry function is not an integer type, it will return 0.
+Otherwise, it will return the exit code of the program.
diff --git a/llvm/docs/TestingGuide.rst b/llvm/docs/TestingGuide.rst
index 6ab33383e929b..f2c953a6221fc 100644
--- a/llvm/docs/TestingGuide.rst
+++ b/llvm/docs/TestingGuide.rst
@@ -328,6 +328,9 @@ assertions:
update_test_checks.py
opt
+ update_llubi_test_checks.py
+ llubi
+
Precommit workflow for tests
----------------------------
diff --git a/llvm/test/tools/llubi/main.ll b/llvm/test/tools/llubi/main.ll
new file mode 100644
index 0000000000000..1f5e8c77ff9fa
--- /dev/null
+++ b/llvm/test/tools/llubi/main.ll
@@ -0,0 +1,12 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: llubi --verbose < %s 2>&1 | FileCheck %s
+
+define i32 @main(i32 %argc, ptr %argv) {
+ ret i32 0
+}
+
+; CHECK: Entering function: main
+; CHECK: i32 %argc = i32 1
+; CHECK: ptr %argv = ptr 16
+; CHECK: ret i32 0
+; CHECK: Exiting function: main
diff --git a/llvm/test/tools/llubi/poison.ll b/llvm/test/tools/llubi/poison.ll
new file mode 100644
index 0000000000000..ed88126b557cd
--- /dev/null
+++ b/llvm/test/tools/llubi/poison.ll
@@ -0,0 +1,12 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: not llubi --verbose < %s 2>&1 | FileCheck %s
+
+define i32 @main(i32 %argc, ptr %argv) {
+ ret i32 poison
+}
+; CHECK: Entering function: main
+; CHECK: i32 %argc = i32 1
+; CHECK: ptr %argv = ptr 16
+; CHECK: ret i32 poison
+; CHECK: Exiting function: main
+; CHECK: llubi: error: Execution of function 'main' resulted in poison return value.
diff --git a/llvm/tools/llubi/CMakeLists.txt b/llvm/tools/llubi/CMakeLists.txt
new file mode 100644
index 0000000000000..e308ab9445a21
--- /dev/null
+++ b/llvm/tools/llubi/CMakeLists.txt
@@ -0,0 +1,17 @@
+set(LLVM_LINK_COMPONENTS
+ Analysis
+ Core
+ IRReader
+ IRPrinter
+ Support
+ )
+
+add_llvm_tool(llubi
+ llubi.cpp
+ Context.cpp
+ Interpreter.cpp
+ Value.cpp
+
+ DEPENDS
+ intrinsics_gen
+ )
diff --git a/llvm/tools/llubi/Context.cpp b/llvm/tools/llubi/Context.cpp
new file mode 100644
index 0000000000000..5a9428b84cb26
--- /dev/null
+++ b/llvm/tools/llubi/Context.cpp
@@ -0,0 +1,123 @@
+//===- Context.cpp - State Tracking for llubi -----------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file tracks the global states (e.g., memory) of the interpreter.
+//
+//===----------------------------------------------------------------------===//
+
+#include "Context.h"
+#include "llvm/Support/MathExtras.h"
+
+namespace llvm::ubi {
+
+Context::Context(Module &M)
+ : Ctx(M.getContext()), M(M), DL(M.getDataLayout()),
+ TLIImpl(M.getTargetTriple()) {}
+
+Context::~Context() = default;
+
+AnyValue Context::getConstantValueImpl(Constant *C) {
+ if (isa<PoisonValue>(C))
+ return AnyValue::getPoisonValue(*this, C->getType());
+
+ // TODO: Handle ConstantInt vector.
+ if (auto *CI = dyn_cast<ConstantInt>(C))
+ return CI->getValue();
+
+ llvm_unreachable("Unrecognized constant");
+}
+
+const AnyValue &Context::getConstantValue(Constant *C) {
+ auto It = ConstCache.find(C);
+ if (It != ConstCache.end())
+ return It->second;
+
+ return ConstCache.emplace(C, getConstantValueImpl(C)).first->second;
+}
+
+MemoryObject::~MemoryObject() = default;
+MemoryObject::MemoryObject(uint64_t Addr, uint64_t Size, StringRef Name,
+ unsigned AS, MemInitKind InitKind)
+ : Address(Addr), Size(Size), Name(Name), AS(AS),
+ IsAlive(InitKind != MemInitKind::Poisoned) {
+ switch (InitKind) {
+ case MemInitKind::Zeroed:
+ Bytes.resize(Size, Byte{0, ByteKind::Concrete});
+ break;
+ case MemInitKind::Uninitialized:
+ Bytes.resize(Size, Byte{0, ByteKind::Undef});
+ break;
+ case MemInitKind::Poisoned:
+ Bytes.resize(Size, Byte{0, ByteKind::Poison});
+ break;
+ default:
+ llvm_unreachable("Unknown MemInitKind");
+ }
+}
+
+IntrusiveRefCntPtr<MemoryObject> Context::allocate(uint64_t Size,
+ uint64_t Align,
+ StringRef Name, unsigned AS,
+ MemInitKind InitKind) {
+ if (MaxMem != 0 && SaturatingAdd(UsedMem, Size) >= MaxMem)
+ return nullptr;
+ uint64_t AlignedAddr = alignTo(AllocationBase, Align);
+ auto MemObj =
+ makeIntrusiveRefCnt<MemoryObject>(AlignedAddr, Size, Name, AS, InitKind);
+ MemoryObjects[AlignedAddr] = MemObj;
+ AllocationBase = AlignedAddr + Size;
+ UsedMem += Size;
+ return MemObj;
+}
+bool Context::free(uint64_t Address) {
+ auto It = MemoryObjects.find(Address);
+ if (It == MemoryObjects.end())
+ return false;
+ It->second->markAsFreed();
+ UsedMem -= It->second->getSize();
+ MemoryObjects.erase(It);
+ return true;
+}
+
+void MemoryObject::markAsFreed() {
+ IsAlive = false;
+ IsFreed = true;
+ Size = 0;
+ Bytes.clear();
+}
+
+void MemoryObject::writeRawBytes(uint64_t Offset, const void *Data,
+ uint64_t Length) {
+ assert(SaturatingAdd(Offset, Length) <= Size && "Write out of bounds");
+ const uint8_t *ByteData = static_cast<const uint8_t *>(Data);
+ for (uint64_t I = 0; I < Length; ++I)
+ Bytes[Offset + I].Set(ByteData[I]);
+}
+
+void MemoryObject::writeInteger(uint64_t Offset, const APInt &Int,
+ const DataLayout &DL) {
+ uint64_t BitWidth = Int.getBitWidth();
+ uint64_t IntSize = divideCeil(BitWidth, 8);
+ assert(SaturatingAdd(Offset, IntSize) <= Size && "Write out of bounds");
+ for (uint64_t I = 0; I < IntSize; ++I) {
+ uint64_t ByteIndex = DL.isLittleEndian() ? I : (IntSize - 1 - I);
+ uint64_t Bits = std::min(BitWidth - ByteIndex * 8, uint64_t(8));
+ Bytes[Offset + I].Set(Int.extractBitsAsZExtValue(Bits, ByteIndex * 8));
+ }
+}
+void MemoryObject::writeFloat(uint64_t Offset, const APFloat &Float,
+ const DataLayout &DL) {
+ writeInteger(Offset, Float.bitcastToAPInt(), DL);
+}
+void MemoryObject::writePointer(uint64_t Offset, const Pointer &Ptr,
+ const DataLayout &DL) {
+ writeInteger(Offset, Ptr.address(), DL);
+ // TODO: provenance
+}
+
+} // namespace llvm::ubi
diff --git a/llvm/tools/llubi/Context.h b/llvm/tools/llubi/Context.h
new file mode 100644
index 0000000000000..19ae740811fd6
--- /dev/null
+++ b/llvm/tools/llubi/Context.h
@@ -0,0 +1,165 @@
+//===--- Context.h - State Tracking for llubi -------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_TOOLS_LLUBI_CONTEXT_H
+#define LLVM_TOOLS_LLUBI_CONTEXT_H
+
+#include "Value.h"
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/Analysis/TargetLibraryInfo.h"
+#include "llvm/IR/Module.h"
+#include <map>
+
+namespace llvm::ubi {
+
+class Frame;
+
+enum class MemInitKind {
+ Zeroed,
+ Uninitialized,
+ Poisoned,
+};
+
+class MemoryObject : public RefCountedBase<MemoryObject> {
+ uint64_t Address;
+ uint64_t Size;
+ SmallVector<Byte, 8> Bytes;
+ StringRef Name;
+ unsigned AS;
+
+ bool IsAlive = false;
+ bool IsConstant = false;
+ bool IsFreed = false;
+
+public:
+ MemoryObject(uint64_t Addr, uint64_t Size, StringRef Name, unsigned AS,
+ MemInitKind InitKind);
+ MemoryObject(const MemoryObject &) = delete;
+ MemoryObject(MemoryObject &&) = delete;
+ MemoryObject &operator=(const MemoryObject &) = delete;
+ MemoryObject &operator=(MemoryObject &&) = delete;
+ ~MemoryObject();
+
+ uint64_t getAddress() const { return Address; }
+ uint64_t getSize() const { return Size; }
+ StringRef getName() const { return Name; }
+ unsigned getAddressSpace() const { return AS; }
+ bool isAlive() const { return IsAlive; }
+ bool isFreed() const { return IsFreed; }
+ bool isConstant() const { return IsConstant; }
+ void setIsConstant(bool C) { IsConstant = C; }
+
+ Byte &operator[](uint64_t Offset) {
+ assert(Offset < Size && "Offset out of bounds");
+ return Bytes[Offset];
+ }
+ void writeRawBytes(uint64_t Offset, const void *Data, uint64_t Length);
+ void writeInteger(uint64_t Offset, const APInt &Int, const DataLayout &DL);
+ void writeFloat(uint64_t Offset, const APFloat &Float, const DataLayout &DL);
+ void writePointer(uint64_t Offset, const Pointer &Ptr, const DataLayout &DL);
+
+ void markAsFreed();
+};
+
+/// An interface for handling events and managing outputs during interpretation.
+/// If the handler returns false from any of the methods, the interpreter will
+/// stop execution immediately.
+class EventHandler {
+public:
+ virtual ~EventHandler() = default;
+
+ virtual bool onInstructionExecuted(Instruction &I, const AnyValue &Result) {
+ return true;
+ }
+ virtual void onUnrecognizedInstruction(Instruction &I) {}
+ virtual void onImmediateUB(StringRef Msg) {}
+ virtual bool onBBJump(Instruction &I, BasicBlock &To) { return true; }
+ virtual bool onFunctionEntry(Function &F, ArrayRef<AnyValue> Args,
+ CallBase *CallSite) {
+ return true;
+ }
+ virtual bool onFunctionExit(Function &F, const AnyValue &RetVal) {
+ return true;
+ }
+ virtual bool onPrint(StringRef Msg) {
+ outs() << Msg;
+ return true;
+ }
+};
+
+class Context {
+ // Module
+ LLVMContext &Ctx;
+ Module &M;
+ const DataLayout &DL;
+ const TargetLibraryInfoImpl TLIImpl;
+
+ // Configuration
+ uint64_t MaxMem = 0;
+ uint32_t VScale = 4;
+ uint32_t MaxSteps = 0;
+ uint32_t MaxStackDepth = 256;
+
+ // Memory
+ uint64_t UsedMem = 0;
+ // The addresses of memory objects are monotonically increasing.
+ // For now we don't model the behavior of address reuse, which is common
+ // with stack coloring.
+ uint64_t AllocationBase = 8;
+ std::map<uint64_t, IntrusiveRefCntPtr<MemoryObject>> MemoryObjects;
+
+ // Constants
+ // Use std::map to avoid iterator/reference invalidation.
+ std::map<Constant *, AnyValue> ConstCache;
+ AnyValue getConstantValueImpl(Constant *C);
+
+ // TODO: errno and fpenv
+
+public:
+ explicit Context(Module &M);
+ Context(const Context &) = delete;
+ Context(Context &&) = delete;
+ Context &operator=(const Context &) = delete;
+ Context &operator=(Context &&) = delete;
+ ~Context();
+
+ void setMemoryLimit(uint64_t Max) { MaxMem = Max; }
+ void setVScale(uint32_t VS) { VScale = VS; }
+ void setMaxSteps(uint32_t MS) { MaxSteps = MS; }
+ void setMaxStackDepth(uint32_t Depth) { MaxStackDepth = Depth; }
+ uint64_t getMemoryLimit() const { return MaxMem; }
+ uint32_t getVScale() const { return VScale; }
+ uint32_t getMaxSteps() const { return MaxSteps; }
+ uint32_t getMaxStackDepth() const { return MaxStackDepth; }
+
+ LLVMContext &getContext() const { return Ctx; }
+ const DataLayout &getDataLayout() const { return DL; }
+ const TargetLibraryInfoImpl &getTLIImpl() const { return TLIImpl; }
+ uint32_t getEVL(ElementCount EC) const {
+ if (EC.isScalable())
+ return VScale * EC.getKnownMinValue();
+ return EC.getFixedValue();
+ }
+
+ const AnyValue &getConstantValue(Constant *C);
+ IntrusiveRefCntPtr<MemoryObject> allocate(uint64_t Size, uint64_t Align,
+ StringRef Name, unsigned AS,
+ MemInitKind InitKind);
+ bool free(uint64_t Address);
+
+ /// Execute the function \p F with arguments \p Args, and store the return
+ /// value in \p RetVal if the function is not void.
+ /// Returns true if the function executed successfully. False indicates an
+ /// error occurred during execution.
+ bool runFunction(Function &F, ArrayRef<AnyValue> Args, AnyValue &RetVal,
+ EventHandler &Handler);
+};
+
+} // namespace llvm::ubi
+
+#endif
diff --git a/llvm/tools/llubi/Interpreter.cpp b/llvm/tools/llubi/Interpreter.cpp
new file mode 100644
index 0000000000000..f9dff534933a0
--- /dev/null
+++ b/llvm/tools/llubi/Interpreter.cpp
@@ -0,0 +1,177 @@
+//===- Interpreter.cpp - Interpreter Loop for llubi -----------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file implements the evaluation loop for each kind of instruction.
+//
+//===----------------------------------------------------------------------===//
+
+#include "Context.h"
+#include "Value.h"
+#include "llvm/IR/InstVisitor.h"
+#include "llvm/Support/Allocator.h"
+
+namespace llvm::ubi {
+
+enum class FrameState {
+ Entry,
+ Running,
+ Pending,
+ Exit,
+};
+
+struct Frame {
+ Function &Func;
+ Frame *LastFrame;
+ CallBase *CallSite;
+ ArrayRef<AnyValue> Args;
+ AnyValue &RetVal;
+
+ TargetLibraryInfo TLI;
+ BasicBlock *BB;
+ BasicBlock::iterator PC;
+ FrameState State = FrameState::Entry;
+ SmallVector<MemoryObject *> Allocas;
+ DenseMap<Instruction *, AnyValue> ValueMap;
+
+ // Reserved for in-flight subroutines.
+ SmallVector<AnyValue> CalleeArgs;
+ AnyValue CalleeRetVal;
+
+ Frame(Function &F, CallBase *CallSite, Frame *LastFrame,
+ ArrayRef<AnyValue> Args, AnyValue &RetVal,
+ const TargetLibraryInfoImpl &TLIImpl)
+ : Func(F), LastFrame(LastFrame), CallSite(CallSite), Args(Args),
+ RetVal(RetVal), TLI(TLIImpl, &F) {
+ BB = &Func.getEntryBlock();
+ PC = BB->begin();
+ }
+};
+
+/// Instruction executor using the visitor pattern.
+/// visit* methods return true on success, false on error.
+/// Unlike the Context class that manages the global state,
+/// InstExecutor only maintains the state for call frames.
+class InstExecutor : public InstVisitor<InstExecutor, bool> {
+ Context &Ctx;
+ EventHandler &Handler;
+ std::list<Frame> CallStack;
+ // Used to indicate whether the interpreter should continue execution.
+ bool Status;
+ Frame *CurrentFrame = nullptr;
+ AnyValue None;
+
+ void reportImmediateUB(StringRef Msg) {
+ // Check if we have already reported an immediate UB.
+ if (!Status) {
+ return;
+ }
+ Status = false;
+ // TODO: Provide stack trace information.
+ Handler.onImmediateUB(Msg);
+ }
+
+ const AnyValue &getValue(Value *V) {
+ if (auto *C = dyn_cast<Constant>(V))
+ return Ctx.getConstantValue(C);
+ return CurrentFrame->ValueMap.at(cast<Instruction>(V));
+ }
+
+public:
+ InstExecutor(Context &C, EventHandler &H, Function &F,
+ ArrayRef<AnyValue> Args, AnyValue &RetVal)
+ : Ctx(C), Handler(H), Status(true) {
+ CallStack.emplace_back(F, /*CallSite=*/nullptr, /*LastFrame=*/nullptr, Args,
+ RetVal, Ctx.getTLIImpl());
+ }
+ bool visitReturnInst(ReturnInst &RI) {
+ if (auto *RV = RI.getReturnValue())
+ CurrentFrame->RetVal = getValue(RV);
+ CurrentFrame->State = FrameState::Exit;
+ return Handler.onInstructionExecuted(RI, None);
+ }
+ bool visitInstruction(Instruction &I) {
+ Handler.onUnrecognizedInstruction(I);
+ return false;
+ }
+
+ bool runMainLoop() {
+ uint32_t MaxSteps = Ctx.getMaxSteps();
+ uint32_t Steps = 0;
+ while (Status && !CallStack.empty()) {
+ Frame &Top = CallStack.back();
+ CurrentFrame = &Top;
+ if (Top.State == FrameState::Entry) {
+ Handler.onFunctionEntry(Top.Func, Top.Args, Top.CallSite);
+ // TODO:Handle arg attributes
+ } else {
+ assert(Top.State == FrameState::Pending &&
+ "Expected to return from a callee.");
+ }
+
+ Top.State = FrameState::Running;
+ // Interpreter loop inside a function
+ while (Status) {
+ assert(Top.State == FrameState::Running &&
+ "Expected to be in running state.");
+ if (MaxSteps != 0 && Steps >= MaxSteps) {
+ reportImmediateUB("Exceeded maximum number of execution steps.");
+ break;
+ }
+ ++Steps;
+
+ Instruction &I = *Top.PC;
+ if (!visit(&I)) {
+ Status = false;
+ break;
+ }
+ if (!Status)
+ break;
+
+ if (Top.State != FrameState::Pending && !I.isTerminator()) {
+ if (I.getType()->isVoidTy())
+ Handler.onInstructionExecuted(I, None);
+ else
+ Handler.onInstructionExecuted(I, Top.ValueMap.at(&I));
+ }
+
+ if (Top.State != FrameState::Running) {
+ // A function call or return has occurred.
+ break;
+ } else if (!I.isTerminator()) {
+ ++Top.PC;
+ }
+ }
+
+ if (!Status)
+ break;
+
+ if (Top.State == FrameState::Exit) {
+ assert((Top.Func.getReturnType()->isVoidTy() || !Top.RetVal.isNone()) &&
+ "Expected return value to be set on function exit.");
+ // TODO:Handle retval attributes
+ Handler.onFunctionExit(Top.Func, Top.RetVal);
+ CallStack.pop_back();
+ } else {
+ assert(Top.State == FrameState::Pending &&
+ "Expected to enter a callee.");
+ }
+ }
+ return Status;
+ }
+};
+
+/// This function implements the main interpreter loop.
+/// It handles function calls in a non-recursive manner to avoid stack
+/// overflows.
+bool Context::runFunction(Function &F, ArrayRef<AnyValue> Args,
+ AnyValue &RetVal, EventHandler &Handler) {
+ InstExecutor Executor(*this, Handler, F, Args, RetVal);
+ return Executor.runMainLoop();
+}
+
+} // namespace llvm::ubi
diff --git a/llvm/tools/llubi/Value.cpp b/llvm/tools/llubi/Value.cpp
new file mode 100644
index 0000000000000..8d05e086851ce
--- /dev/null
+++ b/llvm/tools/llubi/Value.cpp
@@ -0,0 +1,228 @@
+//===- Value.cpp - Value Representation for llubi -------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file implements utility functions for the value representation.
+//
+//===----------------------------------------------------------------------===//
+
+#include "Value.h"
+#include "Context.h"
+
+namespace llvm::ubi {
+
+void Pointer::print(raw_ostream &OS) const {
+ // TODO: print information about the underlying memory object
+ OS << "ptr " << Address;
+}
+
+AnyValue Pointer::null(unsigned BitWidth) {
+ return AnyValue(Pointer(nullptr, APInt::getZero(BitWidth), 0, 0));
+}
+
+MemoryObject *Pointer::getMemoryObject() const {
+ if (!Obj)
+ return nullptr;
+ if (Obj->isFreed())
+ Obj.reset();
+ return Obj.get();
+}
+
+void AnyValue::print(raw_ostream &OS) const {
+ switch (Kind) {
+ case StorageKind::Integer:
+ if (IntVal.getBitWidth() == 1)
+ OS << (IntVal.getBoolValue() ? "T" : "F");
+ OS << "i" << IntVal.getBitWidth() << ' ' << IntVal;
+ break;
+ case StorageKind::Float:
+ OS << FloatVal;
+ break;
+ case StorageKind::Pointer:
+ PtrVal.print(OS);
+ break;
+ case StorageKind::Poison:
+ OS << "poison";
+ break;
+ case StorageKind::None:
+ OS << "none";
+ break;
+ case StorageKind::Aggregate:
+ OS << "{ ";
+ for (size_t I = 0, E = AggVal.size(); I != E; ++I) {
+ if (I != 0)
+ OS << ", ";
+ AggVal[I].print(OS);
+ }
+ OS << " }";
+ break;
+ }
+}
+
+void AnyValue::destroy() {
+ switch (Kind) {
+ case StorageKind::Integer:
+ IntVal.~APInt();
+ break;
+ case StorageKind::Float:
+ FloatVal.~APFloat();
+ break;
+ case StorageKind::Pointer:
+ PtrVal.~Pointer();
+ break;
+ case StorageKind::Poison:
+ case StorageKind::None:
+ break;
+ case StorageKind::Aggregate:
+ AggVal.~vector();
+ break;
+ }
+}
+
+AnyValue::AnyValue(const AnyValue &Other) : Kind(Other.Kind) {
+ switch (Other.Kind) {
+ case StorageKind::Integer:
+ new (&IntVal) APInt(Other.IntVal);
+ break;
+ case StorageKind::Float:
+ new (&FloatVal) APFloat(Other.FloatVal);
+ break;
+ case StorageKind::Pointer:
+ new (&PtrVal) Pointer(Other.PtrVal);
+ break;
+ case StorageKind::Poison:
+ case StorageKind::None:
+ break;
+ case StorageKind::Aggregate:
+ new (&AggVal) std::vector<AnyValue>(Other.AggVal);
+ break;
+ }
+}
+AnyValue::AnyValue(AnyValue &&Other) : Kind(Other.Kind) {
+ switch (Other.Kind) {
+ case StorageKind::Integer:
+ new (&IntVal) APInt(std::move(Other.IntVal));
+ break;
+ case StorageKind::Float:
+ new (&FloatVal) APFloat(std::move(Other.FloatVal));
+ break;
+ case StorageKind::Pointer:
+ new (&PtrVal) Pointer(std::move(Other.PtrVal));
+ break;
+ case StorageKind::Poison:
+ case StorageKind::None:
+ break;
+ case StorageKind::Aggregate:
+ new (&AggVal) std::vector<AnyValue>(std::move(Other.AggVal));
+ break;
+ }
+}
+
+AnyValue &AnyValue::operator=(const AnyValue &Other) {
+ if (&Other == this)
+ return *this;
+
+ destroy();
+ Kind = Other.Kind;
+ switch (Other.Kind) {
+ case StorageKind::Integer:
+ new (&IntVal) APInt(Other.IntVal);
+ break;
+ case StorageKind::Float:
+ new (&FloatVal) APFloat(Other.FloatVal);
+ break;
+ case StorageKind::Pointer:
+ new (&PtrVal) Pointer(Other.PtrVal);
+ break;
+ case StorageKind::Poison:
+ case StorageKind::None:
+ break;
+ case StorageKind::Aggregate:
+ new (&AggVal) std::vector<AnyValue>(Other.AggVal);
+ break;
+ }
+
+ return *this;
+}
+AnyValue &AnyValue::operator=(AnyValue &&Other) {
+ if (&Other == this)
+ return *this;
+ destroy();
+ Kind = Other.Kind;
+ switch (Other.Kind) {
+ case StorageKind::Integer:
+ new (&IntVal) APInt(std::move(Other.IntVal));
+ break;
+ case StorageKind::Float:
+ new (&FloatVal) APFloat(std::move(Other.FloatVal));
+ break;
+ case StorageKind::Pointer:
+ new (&PtrVal) Pointer(std::move(Other.PtrVal));
+ break;
+ case StorageKind::Poison:
+ case StorageKind::None:
+ break;
+ case StorageKind::Aggregate:
+ new (&AggVal) std::vector<AnyValue>(std::move(Other.AggVal));
+ break;
+ }
+
+ return *this;
+}
+
+AnyValue AnyValue::getPoisonValue(Context &Ctx, Type *Ty) {
+ if (Ty->isFloatingPointTy() || Ty->isIntegerTy() || Ty->isPointerTy())
+ return AnyValue::poison();
+ if (auto *VecTy = dyn_cast<VectorType>(Ty)) {
+ uint32_t NumElements = Ctx.getEVL(VecTy->getElementCount());
+ return AnyValue(std::vector<AnyValue>(NumElements, AnyValue::poison()));
+ }
+ if (auto *ArrTy = dyn_cast<ArrayType>(Ty)) {
+ uint64_t NumElements = ArrTy->getNumElements();
+ return AnyValue(std::vector<AnyValue>(
+ NumElements, getPoisonValue(Ctx, ArrTy->getElementType())));
+ }
+ if (auto *StructTy = dyn_cast<StructType>(Ty)) {
+ std::vector<AnyValue> Elements;
+ Elements.reserve(StructTy->getNumElements());
+ for (uint32_t I = 0, E = StructTy->getNumElements(); I != E; ++I) {
+ Elements.push_back(getPoisonValue(Ctx, StructTy->getElementType(I)));
+ }
+ return AnyValue(std::move(Elements));
+ }
+ llvm_unreachable("Unsupported type");
+}
+AnyValue AnyValue::getNullValue(Context &Ctx, Type *Ty) {
+ if (Ty->isIntegerTy())
+ return AnyValue(APInt::getZero(Ty->getIntegerBitWidth()));
+ if (Ty->isFloatingPointTy())
+ return AnyValue(APFloat::getZero(Ty->getFltSemantics()));
+ if (Ty->isPointerTy())
+ return Pointer::null(
+ Ctx.getDataLayout().getPointerSizeInBits(Ty->getPointerAddressSpace()));
+ if (auto *VecTy = dyn_cast<VectorType>(Ty)) {
+ uint32_t NumElements = Ctx.getEVL(VecTy->getElementCount());
+ return AnyValue(std::vector<AnyValue>(
+ NumElements, getNullValue(Ctx, VecTy->getElementType())));
+ }
+ if (auto *ArrTy = dyn_cast<ArrayType>(Ty)) {
+ uint64_t NumElements = ArrTy->getNumElements();
+ return AnyValue(std::vector<AnyValue>(
+ NumElements, getNullValue(Ctx, ArrTy->getElementType())));
+ }
+ if (auto *StructTy = dyn_cast<StructType>(Ty)) {
+ std::vector<AnyValue> Elements;
+ Elements.reserve(StructTy->getNumElements());
+ for (uint32_t I = 0, E = StructTy->getNumElements(); I != E; ++I) {
+ Elements.push_back(getNullValue(Ctx, StructTy->getElementType(I)));
+ }
+ return AnyValue(std::move(Elements));
+ }
+ llvm_unreachable("Unsupported type");
+}
+
+} // namespace llvm::ubi
diff --git a/llvm/tools/llubi/Value.h b/llvm/tools/llubi/Value.h
new file mode 100644
index 0000000000000..706d79437c65e
--- /dev/null
+++ b/llvm/tools/llubi/Value.h
@@ -0,0 +1,139 @@
+//===--- Value.h - Value Representation for llubi ---------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_TOOLS_LLUBI_VALUE_H
+#define LLVM_TOOLS_LLUBI_VALUE_H
+
+#include "llvm/ADT/APFloat.h"
+#include "llvm/ADT/APInt.h"
+#include "llvm/ADT/IntrusiveRefCntPtr.h"
+#include "llvm/IR/Type.h"
+#include "llvm/Support/raw_ostream.h"
+
+namespace llvm::ubi {
+
+class MemoryObject;
+class Context;
+class AnyValue;
+
+enum class ByteKind : uint8_t {
+ Concrete,
+ Undef,
+ Poison,
+};
+
+struct Byte {
+ uint8_t Value;
+ ByteKind Kind : 2;
+ // TODO: provenance
+
+ void Set(uint8_t V) {
+ Value = V;
+ Kind = ByteKind::Concrete;
+ }
+};
+
+// TODO: Byte
+enum class StorageKind {
+ Integer,
+ Float,
+ Pointer,
+ Poison,
+ None, // Placeholder for void type
+ Aggregate, // Struct, Array or Vector
+};
+
+class Pointer {
+ mutable IntrusiveRefCntPtr<MemoryObject> Obj;
+ APInt Address;
+ uint64_t Offset;
+ uint64_t Bound;
+
+public:
+ explicit Pointer(IntrusiveRefCntPtr<MemoryObject> Obj, const APInt &Address,
+ uint64_t Offset, uint64_t Bound)
+ : Obj(std::move(Obj)), Address(Address), Offset(Offset), Bound(Bound) {}
+ static AnyValue null(unsigned BitWidth);
+ void print(raw_ostream &OS) const;
+ const APInt &address() const { return Address; }
+ MemoryObject *getMemoryObject() const;
+};
+
+class [[nodiscard]] AnyValue {
+ StorageKind Kind;
+ union {
+ APInt IntVal;
+ APFloat FloatVal;
+ Pointer PtrVal;
+ std::vector<AnyValue> AggVal;
+ };
+
+ struct PoisonTag {};
+ void destroy();
+
+public:
+ AnyValue() : Kind(StorageKind::None) {}
+ explicit AnyValue(PoisonTag) : Kind(StorageKind::Poison) {}
+ AnyValue(APInt Val) : Kind(StorageKind::Integer), IntVal(std::move(Val)) {}
+ AnyValue(APFloat Val) : Kind(StorageKind::Float), FloatVal(std::move(Val)) {}
+ AnyValue(Pointer Val) : Kind(StorageKind::Pointer), PtrVal(std::move(Val)) {}
+ AnyValue(std::vector<AnyValue> Val)
+ : Kind(StorageKind::Aggregate), AggVal(std::move(Val)) {}
+ AnyValue(const AnyValue &Other);
+ AnyValue(AnyValue &&Other);
+ AnyValue &operator=(const AnyValue &);
+ AnyValue &operator=(AnyValue &&);
+ ~AnyValue() { destroy(); }
+
+ void print(raw_ostream &OS) const;
+
+ static AnyValue poison() { return AnyValue(PoisonTag{}); }
+ static AnyValue getPoisonValue(Context &Ctx, Type *Ty);
+ static AnyValue getNullValue(Context &Ctx, Type *Ty);
+
+ bool isNone() const { return Kind == StorageKind::None; }
+ bool isPoison() const { return Kind == StorageKind::Poison; }
+
+ const APInt &asInteger() const {
+ assert(Kind == StorageKind::Integer && "Expect an integer value");
+ return IntVal;
+ }
+
+ const APFloat &asFloat() const {
+ assert(Kind == StorageKind::Float && "Expect a float value");
+ return FloatVal;
+ }
+
+ const Pointer &asPointer() const {
+ assert(Kind == StorageKind::Pointer && "Expect a pointer value");
+ return PtrVal;
+ }
+
+ const std::vector<AnyValue> &asAggregate() const {
+ assert(Kind == StorageKind::Aggregate &&
+ "Expect an aggregate/vector value");
+ return AggVal;
+ }
+
+ // Helper function for C++ 17 structured bindings.
+ template <size_t I> const AnyValue &get() const {
+ assert(Kind == StorageKind::Aggregate &&
+ "Expect an aggregate/vector value");
+ assert(I < AggVal.size() && "Index out of bounds");
+ return AggVal[I];
+ }
+};
+
+inline raw_ostream &operator<<(raw_ostream &OS, const AnyValue &V) {
+ V.print(OS);
+ return OS;
+}
+
+} // namespace llvm::ubi
+
+#endif
diff --git a/llvm/tools/llubi/llubi.cpp b/llvm/tools/llubi/llubi.cpp
new file mode 100644
index 0000000000000..4c3701cb25a3d
--- /dev/null
+++ b/llvm/tools/llubi/llubi.cpp
@@ -0,0 +1,251 @@
+//===------------- llubi.cpp - LLVM UB-aware Interpreter --------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This utility provides an UB-aware interpreter for programs in LLVM bitcode.
+// It is not built on top of the existing ExecutionEngine interface, but instead
+// implements its own value representation, state tracking and interpreter loop.
+//
+//===----------------------------------------------------------------------===//
+
+#include "Context.h"
+#include "llvm/Config/llvm-config.h"
+#include "llvm/IR/LLVMContext.h"
+#include "llvm/IR/Module.h"
+#include "llvm/IR/Type.h"
+#include "llvm/IRReader/IRReader.h"
+#include "llvm/Support/CommandLine.h"
+#include "llvm/Support/Format.h"
+#include "llvm/Support/InitLLVM.h"
+#include "llvm/Support/MathExtras.h"
+#include "llvm/Support/SourceMgr.h"
+#include "llvm/Support/WithColor.h"
+#include "llvm/Support/raw_ostream.h"
+
+using namespace llvm;
+
+static cl::opt<std::string> InputFile(cl::desc("<input bitcode>"),
+ cl::Positional, cl::init("-"));
+
+static cl::list<std::string> InputArgv(cl::ConsumeAfter,
+ cl::desc("<program arguments>..."));
+
+static cl::opt<std::string>
+ EntryFunc("entry-function",
+ cl::desc("Specify the entry function (default = 'main') "
+ "of the executable"),
+ cl::value_desc("function"), cl::init("main"));
+
+static cl::opt<std::string>
+ FakeArgv0("fake-argv0",
+ cl::desc("Override the 'argv[0]' value passed into the executing"
+ " program"),
+ cl::value_desc("executable"));
+
+static cl::opt<bool>
+ Verbose("verbose", cl::desc("Print results for each instruction executed."),
+ cl::init(false));
+
+cl::OptionCategory InterpreterCategory("Interpreter Options");
+
+static cl::opt<unsigned> MaxMem(
+ "max-mem",
+ cl::desc("Max amount of memory (in bytes) that can be allocated by the"
+ " program, including stack, heap, and global variables."
+ " Set to 0 to disable the limit."),
+ cl::value_desc("N"), cl::init(0), cl::cat(InterpreterCategory));
+
+static cl::opt<unsigned>
+ MaxSteps("max-steps",
+ cl::desc("Max number of instructions executed."
+ " Set to 0 to disable the limit."),
+ cl::value_desc("N"), cl::init(0), cl::cat(InterpreterCategory));
+
+static cl::opt<unsigned> MaxStackDepth(
+ "max-stack-depth",
+ cl::desc("Max stack depth (default = 256). Set to 0 to disable the limit."),
+ cl::value_desc("N"), cl::init(256), cl::cat(InterpreterCategory));
+
+static cl::opt<unsigned>
+ VScale("vscale", cl::desc("The value of llvm.vscale (default = 4)"),
+ cl::value_desc("N"), cl::init(4), cl::cat(InterpreterCategory));
+
+class VerboseEventHandler : public ubi::EventHandler {
+public:
+ bool onInstructionExecuted(Instruction &I,
+ const ubi::AnyValue &Result) override {
+ if (Result.isNone()) {
+ errs() << I << '\n';
+ } else {
+ errs() << I << " => " << Result << '\n';
+ }
+
+ return true;
+ }
+
+ void onImmediateUB(StringRef Msg) override {
+ errs() << "Immediate UB detected: " << Msg << '\n';
+ }
+
+ bool onBBJump(Instruction &I, BasicBlock &To) override {
+ errs() << I << " jump to ";
+ To.printAsOperand(errs(), /*PrintType=*/false);
+ return true;
+ }
+
+ bool onFunctionEntry(Function &F, ArrayRef<ubi::AnyValue> Args,
+ CallBase *CallSite) override {
+ errs() << "Entering function: " << F.getName() << '\n';
+ size_t ArgSize = F.arg_size();
+ for (auto &&[Idx, Arg] : enumerate(Args)) {
+ if (Idx >= ArgSize)
+ errs() << " vaarg[" << (Idx - ArgSize) << "] = " << Arg << '\n';
+ else
+ errs() << " " << *F.getArg(Idx) << " = " << Arg << '\n';
+ }
+ return true;
+ }
+
+ bool onFunctionExit(Function &F, const ubi::AnyValue &RetVal) override {
+ errs() << "Exiting function: " << F.getName() << '\n';
+ return true;
+ }
+
+ void onUnrecognizedInstruction(Instruction &I) override {
+ errs() << "Unrecognized instruction: " << I << '\n';
+ }
+};
+
+int main(int argc, char **argv) {
+ InitLLVM X(argc, argv);
+
+ cl::ParseCommandLineOptions(argc, argv, "llvm ub-aware interpreter\n");
+
+ if (EntryFunc.empty()) {
+ WithColor::error(errs(), argv[0])
+ << "--entry-function name cannot be empty\n";
+ return 1;
+ }
+
+ LLVMContext Context;
+
+ // Load the bitcode...
+ SMDiagnostic Err;
+ std::unique_ptr<Module> Owner = parseIRFile(InputFile, Err, Context);
+ Module *Mod = Owner.get();
+ if (!Mod) {
+ Err.print(argv[0], errs());
+ return 1;
+ }
+
+ // If the user specifically requested an argv[0] to pass into the program,
+ // do it now.
+ if (!FakeArgv0.empty()) {
+ InputFile = static_cast<std::string>(FakeArgv0);
+ } else {
+ // Otherwise, if there is a .bc suffix on the executable strip it off, it
+ // might confuse the program.
+ if (StringRef(InputFile).ends_with(".bc"))
+ InputFile.erase(InputFile.length() - 3);
+ }
+
+ // Add the module's name to the start of the vector of arguments to main().
+ InputArgv.insert(InputArgv.begin(), InputFile);
+
+ // Initialize the execution context and set parameters.
+ ubi::Context Ctx(*Mod);
+ Ctx.setMemoryLimit(MaxMem);
+ Ctx.setVScale(VScale);
+ Ctx.setMaxSteps(MaxSteps);
+ Ctx.setMaxStackDepth(MaxStackDepth);
+
+ // Call the main function from M as if its signature were:
+ // int main (int argc, char **argv)
+ // using the contents of Args to determine argc & argv
+ Function *EntryFn = Mod->getFunction(EntryFunc);
+ if (!EntryFn) {
+ WithColor::error(errs(), argv[0])
+ << '\'' << EntryFunc << "\' function not found in module.\n";
+ return 1;
+ }
+ TargetLibraryInfo TLI(Ctx.getTLIImpl());
+ Type *IntTy = IntegerType::get(Ctx.getContext(), TLI.getIntSize());
+ auto *MainFuncTy = FunctionType::get(
+ IntTy, {IntTy, PointerType::getUnqual(Ctx.getContext())}, false);
+ SmallVector<ubi::AnyValue> Args;
+ if (EntryFn->getFunctionType() == MainFuncTy) {
+ Args.push_back(
+ Ctx.getConstantValue(ConstantInt::get(IntTy, InputArgv.size())));
+
+ uint64_t Size = 0;
+ for (const auto &Arg : InputArgv)
+ Size += Arg.length() + 1;
+ auto ArgvStrMem = Ctx.allocate(Size, 8, "argv_str",
+ /*AS=*/0, ubi::MemInitKind::Zeroed);
+ if (!ArgvStrMem) {
+ WithColor::error(errs(), argv[0])
+ << "Failed to allocate memory for argv strings.\n";
+ return 1;
+ }
+ uint64_t Offset = 0;
+ uint32_t PtrBitWidth = Ctx.getDataLayout().getPointerSizeInBits(0);
+ uint64_t PtrsSize = PtrBitWidth * (InputArgv.size() + 1);
+ auto ArgvPtrsMem = Ctx.allocate(PtrsSize, 8, "argv",
+ /*AS=*/0, ubi::MemInitKind::Zeroed);
+ if (!ArgvPtrsMem) {
+ WithColor::error(errs(), argv[0])
+ << "Failed to allocate memory for argv pointers.\n";
+ return 1;
+ }
+ for (const auto &[Idx, Arg] : enumerate(InputArgv)) {
+ ubi::Pointer ArgPtr(ArgvStrMem,
+ APInt(PtrBitWidth, ArgvStrMem->getAddress() + Offset),
+ /*Offset=*/Offset, /*Bound=*/Size);
+ ArgvStrMem->writeRawBytes(Offset, Arg.c_str(), Arg.length());
+ ArgvPtrsMem->writePointer(Idx * PtrBitWidth, ArgPtr, Ctx.getDataLayout());
+ Offset += Arg.length() + 1;
+ }
+ Args.push_back(ubi::Pointer(ArgvPtrsMem,
+ APInt(PtrBitWidth, ArgvPtrsMem->getAddress()),
+ /*Offset=*/0, /*Bound=*/PtrsSize));
+ } else {
+ // If the signature does not match (e.g., llvm-reduce change the signature
+ // of main), it will pass null values for all arguments.
+ WithColor::warning(errs(), argv[0])
+ << "The signature of function '" << EntryFunc
+ << "' does not match 'int main(int, char**)', passing null values for "
+ "all arguments.\n";
+ Args.reserve(EntryFn->arg_size());
+ for (Argument &Arg : EntryFn->args())
+ Args.push_back(ubi::AnyValue::getNullValue(Ctx, Arg.getType()));
+ }
+
+ ubi::EventHandler NoopHandler;
+ VerboseEventHandler VerboseHandler;
+ ubi::AnyValue RetVal;
+ if (!Ctx.runFunction(*EntryFn, Args, RetVal,
+ Verbose ? VerboseHandler : NoopHandler)) {
+ WithColor::error(errs(), argv[0])
+ << "Execution of function '" << EntryFunc << "' failed.\n";
+ return 1;
+ }
+
+ // If the function returns an integer, return that as the exit code.
+ if (EntryFn->getReturnType()->isIntegerTy()) {
+ assert(!RetVal.isNone() && "Expected a return value from entry function");
+ if (RetVal.isPoison()) {
+ WithColor::error(errs(), argv[0])
+ << "Execution of function '" << EntryFunc
+ << "' resulted in poison return value.\n";
+ return 1;
+ }
+ APInt Result = RetVal.asInteger();
+ return (int)Result.extractBitsAsZExtValue(
+ std::min(Result.getBitWidth(), 8U), 0);
+ }
+ return 0;
+}
diff --git a/llvm/utils/UpdateTestChecks/common.py b/llvm/utils/UpdateTestChecks/common.py
index cd15994326f98..22519f2645c6e 100644
--- a/llvm/utils/UpdateTestChecks/common.py
+++ b/llvm/utils/UpdateTestChecks/common.py
@@ -2712,6 +2712,7 @@ def get_autogennote_suffix(parser, args):
"tool_binary",
"opt_binary",
"llc_binary",
+ "llubi_binary",
"clang",
"opt",
"llvm_bin",
diff --git a/llvm/utils/update_llubi_test_checks.py b/llvm/utils/update_llubi_test_checks.py
new file mode 100755
index 0000000000000..9a7a2bdd3ef16
--- /dev/null
+++ b/llvm/utils/update_llubi_test_checks.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+
+"""A test case update script.
+
+This script is a utility to update LLVM 'llubi' based test cases with new
+FileCheck patterns.
+"""
+
+from __future__ import print_function
+
+from sys import stderr
+from traceback import print_exc
+import argparse
+import os # Used to advertise this file's name ("autogenerated_note").
+import subprocess
+import sys
+
+from UpdateTestChecks import common
+
+# llubi is the only llubi-like in the LLVM tree but downstream forks can add
+# additional ones here if they have them.
+LLUBI_LIKE_TOOLS = [
+ "llubi",
+]
+
+# Invoke the tool that is being tested.
+def invoke_tool(exe, cmd_args, ir, check_rc):
+ with open(ir) as ir_file:
+ substitutions = common.getSubstitutions(ir)
+ stdout = subprocess.run(
+ exe + " " + common.applySubstitutions(cmd_args, substitutions),
+ shell=True,
+ stdin=ir_file,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ check=check_rc
+ ).stdout.decode()
+ # Fix line endings to unix CR style.
+ return stdout.replace("\r\n", "\n")
+
+def update_test(ti: common.TestInfo):
+ if len(ti.run_lines) == 0:
+ common.warn("No RUN lines found in test: " + ti.path)
+ return
+ if len(ti.run_lines) > 1:
+ common.warn("Multiple RUN lines found in test: " + ti.path)
+ common.warn("Only the first RUN line will be processed.")
+
+ l = ti.run_lines[0]
+ if "|" not in l:
+ common.warn("Skipping unparsable RUN line: " + l)
+ return
+
+ commands = [cmd.strip() for cmd in l.split("|")]
+ assert len(commands) == 2
+ llubi_cmd = commands[-2]
+ filecheck_cmd = commands[-1]
+ args = llubi_cmd.split(" ")
+ llubi_tool = args[0]
+ check_rc = True
+ if len(args) > 1 and args[0] == "not":
+ llubi_tool = args[1]
+ check_rc = False
+
+ common.verify_filecheck_prefixes(filecheck_cmd)
+
+ llubi_like_tools = LLUBI_LIKE_TOOLS[:]
+ if ti.args.tool:
+ llubi_like_tools.append(ti.args.tool)
+ if llubi_tool not in llubi_like_tools:
+ common.warn("Skipping non-llubi RUN line: " + l)
+ return
+
+ if not filecheck_cmd.startswith("FileCheck "):
+ common.warn("Skipping non-FileChecked RUN line: " + l)
+ return
+
+ llubi_args = llubi_cmd[llubi_cmd.index(llubi_tool) + len(llubi_tool) :].strip()
+ llubi_args = llubi_args.replace("< %s", "").replace("%s", "").strip()
+ prefixes = common.get_check_prefixes(filecheck_cmd)
+
+ common.debug("Extracted LLC cmd:", llubi_tool, llubi_args)
+ common.debug("Extracted FileCheck prefixes:", str(prefixes))
+ prefix_set = set([prefix for prefix in prefixes])
+
+ raw_tool_output = invoke_tool(
+ ti.args.llubi_binary or llubi_tool,
+ llubi_args,
+ ti.path,
+ check_rc=check_rc,
+ )
+ if ti.args.llubi_binary:
+ raw_tool_output = raw_tool_output.replace(ti.args.llubi_binary, llubi_tool)
+
+ output_lines = []
+ common.dump_input_lines(output_lines, ti, prefix_set, ";")
+ output_lines.extend(
+ ["; CHECK: " + line for line in raw_tool_output.splitlines()]
+ )
+
+ common.debug("Writing %d lines to %s..." % (len(output_lines), ti.path))
+ with open(ti.path, "wb") as f:
+ f.writelines(["{}\n".format(l).encode("utf-8") for l in output_lines])
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ "--llubi-binary",
+ default=None,
+ help='The "llubi" binary to use to generate the test case',
+ )
+ parser.add_argument(
+ "--tool",
+ default=None,
+ help="Treat the given tool name as an llubi-like tool for which check lines should be generated",
+ )
+ parser.add_argument("tests", nargs="+")
+ initial_args = common.parse_commandline_args(parser)
+
+ script_name = os.path.basename(__file__)
+
+ returncode = 0
+ for ti in common.itertests(
+ initial_args.tests, parser, script_name="utils/" + script_name
+ ):
+ try:
+ update_test(ti)
+ except Exception as e:
+ stderr.write(f"Error: Failed to update test {ti.path}\n")
+ print_exc()
+ returncode = 1
+ return returncode
+
+
+if __name__ == "__main__":
+ sys.exit(main())
>From 6f9d40639197959f0bc77c19b0fd37edde459eae Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Fri, 6 Feb 2026 04:20:12 +0800
Subject: [PATCH 02/15] [llubi] Fix format
---
llvm/utils/update_llubi_test_checks.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/llvm/utils/update_llubi_test_checks.py b/llvm/utils/update_llubi_test_checks.py
index 9a7a2bdd3ef16..c8434438edb12 100755
--- a/llvm/utils/update_llubi_test_checks.py
+++ b/llvm/utils/update_llubi_test_checks.py
@@ -23,6 +23,7 @@
"llubi",
]
+
# Invoke the tool that is being tested.
def invoke_tool(exe, cmd_args, ir, check_rc):
with open(ir) as ir_file:
@@ -33,11 +34,12 @@ def invoke_tool(exe, cmd_args, ir, check_rc):
stdin=ir_file,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
- check=check_rc
+ check=check_rc,
).stdout.decode()
# Fix line endings to unix CR style.
return stdout.replace("\r\n", "\n")
+
def update_test(ti: common.TestInfo):
if len(ti.run_lines) == 0:
common.warn("No RUN lines found in test: " + ti.path)
@@ -94,9 +96,7 @@ def update_test(ti: common.TestInfo):
output_lines = []
common.dump_input_lines(output_lines, ti, prefix_set, ";")
- output_lines.extend(
- ["; CHECK: " + line for line in raw_tool_output.splitlines()]
- )
+ output_lines.extend(["; CHECK: " + line for line in raw_tool_output.splitlines()])
common.debug("Writing %d lines to %s..." % (len(output_lines), ti.path))
with open(ti.path, "wb") as f:
>From 66f46694f82bf0a8be170e2364cbb4587edfbbd4 Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Fri, 6 Feb 2026 04:21:41 +0800
Subject: [PATCH 03/15] [llubi] Drop braces
---
llvm/tools/llubi/Value.cpp | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/llvm/tools/llubi/Value.cpp b/llvm/tools/llubi/Value.cpp
index 8d05e086851ce..cc1fa3022e961 100644
--- a/llvm/tools/llubi/Value.cpp
+++ b/llvm/tools/llubi/Value.cpp
@@ -189,9 +189,8 @@ AnyValue AnyValue::getPoisonValue(Context &Ctx, Type *Ty) {
if (auto *StructTy = dyn_cast<StructType>(Ty)) {
std::vector<AnyValue> Elements;
Elements.reserve(StructTy->getNumElements());
- for (uint32_t I = 0, E = StructTy->getNumElements(); I != E; ++I) {
+ for (uint32_t I = 0, E = StructTy->getNumElements(); I != E; ++I)
Elements.push_back(getPoisonValue(Ctx, StructTy->getElementType(I)));
- }
return AnyValue(std::move(Elements));
}
llvm_unreachable("Unsupported type");
@@ -217,9 +216,8 @@ AnyValue AnyValue::getNullValue(Context &Ctx, Type *Ty) {
if (auto *StructTy = dyn_cast<StructType>(Ty)) {
std::vector<AnyValue> Elements;
Elements.reserve(StructTy->getNumElements());
- for (uint32_t I = 0, E = StructTy->getNumElements(); I != E; ++I) {
+ for (uint32_t I = 0, E = StructTy->getNumElements(); I != E; ++I)
Elements.push_back(getNullValue(Ctx, StructTy->getElementType(I)));
- }
return AnyValue(std::move(Elements));
}
llvm_unreachable("Unsupported type");
>From 3e702f8d7a45e184d7fd9f7bb00adc5cf600cad7 Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Fri, 6 Feb 2026 04:23:49 +0800
Subject: [PATCH 04/15] [llubi] Drop braces. NFC.
---
llvm/tools/llubi/Interpreter.cpp | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/llvm/tools/llubi/Interpreter.cpp b/llvm/tools/llubi/Interpreter.cpp
index f9dff534933a0..bfd5f588aa9a4 100644
--- a/llvm/tools/llubi/Interpreter.cpp
+++ b/llvm/tools/llubi/Interpreter.cpp
@@ -67,9 +67,8 @@ class InstExecutor : public InstVisitor<InstExecutor, bool> {
void reportImmediateUB(StringRef Msg) {
// Check if we have already reported an immediate UB.
- if (!Status) {
+ if (!Status)
return;
- }
Status = false;
// TODO: Provide stack trace information.
Handler.onImmediateUB(Msg);
>From 565b168b3572bbb6c3c9d2b2c159a4516f0cf5fd Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Fri, 6 Feb 2026 04:31:46 +0800
Subject: [PATCH 05/15] [llubi] Fix docs format. NFC.
---
llvm/docs/CommandGuide/llubi.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/llvm/docs/CommandGuide/llubi.rst b/llvm/docs/CommandGuide/llubi.rst
index 0b66b45c44cd8..7c870c5a116de 100644
--- a/llvm/docs/CommandGuide/llubi.rst
+++ b/llvm/docs/CommandGuide/llubi.rst
@@ -1,5 +1,5 @@
llubi - LLVM UB-aware Interpreter
-=================================================
+=================================
.. program:: llubi
@@ -48,7 +48,7 @@ GENERAL OPTIONS
Print out the version of :program:`llubi` and exit without doing anything else.
INTERPRETER OPTIONS
---------------
+-------------------
.. option:: -max-mem=N
>From 52bceef00513af4115a79d2370fe2f8e127591a8 Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Fri, 6 Feb 2026 04:41:33 +0800
Subject: [PATCH 06/15] [llubi] Move llubi specific source/header files into
./lib
---
llvm/tools/llubi/CMakeLists.txt | 8 ++++----
llvm/tools/llubi/lib/CMakeLists.txt | 12 ++++++++++++
llvm/tools/llubi/{ => lib}/Context.cpp | 0
llvm/tools/llubi/{ => lib}/Context.h | 0
llvm/tools/llubi/{ => lib}/Interpreter.cpp | 0
llvm/tools/llubi/{ => lib}/Value.cpp | 0
llvm/tools/llubi/{ => lib}/Value.h | 0
llvm/tools/llubi/llubi.cpp | 2 +-
8 files changed, 17 insertions(+), 5 deletions(-)
create mode 100644 llvm/tools/llubi/lib/CMakeLists.txt
rename llvm/tools/llubi/{ => lib}/Context.cpp (100%)
rename llvm/tools/llubi/{ => lib}/Context.h (100%)
rename llvm/tools/llubi/{ => lib}/Interpreter.cpp (100%)
rename llvm/tools/llubi/{ => lib}/Value.cpp (100%)
rename llvm/tools/llubi/{ => lib}/Value.h (100%)
diff --git a/llvm/tools/llubi/CMakeLists.txt b/llvm/tools/llubi/CMakeLists.txt
index e308ab9445a21..46d06f6e5dfc2 100644
--- a/llvm/tools/llubi/CMakeLists.txt
+++ b/llvm/tools/llubi/CMakeLists.txt
@@ -1,17 +1,17 @@
set(LLVM_LINK_COMPONENTS
Analysis
Core
- IRReader
IRPrinter
+ IRReader
Support
)
add_llvm_tool(llubi
llubi.cpp
- Context.cpp
- Interpreter.cpp
- Value.cpp
DEPENDS
intrinsics_gen
)
+
+add_subdirectory(lib)
+target_link_libraries(llubi PRIVATE LLVMUBAwareInterpreter)
diff --git a/llvm/tools/llubi/lib/CMakeLists.txt b/llvm/tools/llubi/lib/CMakeLists.txt
new file mode 100644
index 0000000000000..d3b54d0bd45b5
--- /dev/null
+++ b/llvm/tools/llubi/lib/CMakeLists.txt
@@ -0,0 +1,12 @@
+set(LLVM_LINK_COMPONENTS
+ Analysis
+ Core
+ Support
+ )
+
+add_llvm_library(LLVMUBAwareInterpreter
+ STATIC
+ Context.cpp
+ Interpreter.cpp
+ Value.cpp
+ )
diff --git a/llvm/tools/llubi/Context.cpp b/llvm/tools/llubi/lib/Context.cpp
similarity index 100%
rename from llvm/tools/llubi/Context.cpp
rename to llvm/tools/llubi/lib/Context.cpp
diff --git a/llvm/tools/llubi/Context.h b/llvm/tools/llubi/lib/Context.h
similarity index 100%
rename from llvm/tools/llubi/Context.h
rename to llvm/tools/llubi/lib/Context.h
diff --git a/llvm/tools/llubi/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
similarity index 100%
rename from llvm/tools/llubi/Interpreter.cpp
rename to llvm/tools/llubi/lib/Interpreter.cpp
diff --git a/llvm/tools/llubi/Value.cpp b/llvm/tools/llubi/lib/Value.cpp
similarity index 100%
rename from llvm/tools/llubi/Value.cpp
rename to llvm/tools/llubi/lib/Value.cpp
diff --git a/llvm/tools/llubi/Value.h b/llvm/tools/llubi/lib/Value.h
similarity index 100%
rename from llvm/tools/llubi/Value.h
rename to llvm/tools/llubi/lib/Value.h
diff --git a/llvm/tools/llubi/llubi.cpp b/llvm/tools/llubi/llubi.cpp
index 4c3701cb25a3d..be16e1cbfe77d 100644
--- a/llvm/tools/llubi/llubi.cpp
+++ b/llvm/tools/llubi/llubi.cpp
@@ -12,7 +12,7 @@
//
//===----------------------------------------------------------------------===//
-#include "Context.h"
+#include "lib/Context.h"
#include "llvm/Config/llvm-config.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
>From 77ec12030f776be2c7b4c469e188d85b67ac24a0 Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Fri, 6 Feb 2026 04:50:50 +0800
Subject: [PATCH 07/15] [llubi] Drop UTC scripts
---
llvm/test/tools/llubi/main.ll | 1 -
llvm/test/tools/llubi/poison.ll | 1 -
llvm/utils/UpdateTestChecks/common.py | 1 -
llvm/utils/update_llubi_test_checks.py | 137 -------------------------
4 files changed, 140 deletions(-)
delete mode 100755 llvm/utils/update_llubi_test_checks.py
diff --git a/llvm/test/tools/llubi/main.ll b/llvm/test/tools/llubi/main.ll
index 1f5e8c77ff9fa..b9004c6d4c8e6 100644
--- a/llvm/test/tools/llubi/main.ll
+++ b/llvm/test/tools/llubi/main.ll
@@ -1,4 +1,3 @@
-; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
; RUN: llubi --verbose < %s 2>&1 | FileCheck %s
define i32 @main(i32 %argc, ptr %argv) {
diff --git a/llvm/test/tools/llubi/poison.ll b/llvm/test/tools/llubi/poison.ll
index ed88126b557cd..b5f6962f1de15 100644
--- a/llvm/test/tools/llubi/poison.ll
+++ b/llvm/test/tools/llubi/poison.ll
@@ -1,4 +1,3 @@
-; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
; RUN: not llubi --verbose < %s 2>&1 | FileCheck %s
define i32 @main(i32 %argc, ptr %argv) {
diff --git a/llvm/utils/UpdateTestChecks/common.py b/llvm/utils/UpdateTestChecks/common.py
index 22519f2645c6e..cd15994326f98 100644
--- a/llvm/utils/UpdateTestChecks/common.py
+++ b/llvm/utils/UpdateTestChecks/common.py
@@ -2712,7 +2712,6 @@ def get_autogennote_suffix(parser, args):
"tool_binary",
"opt_binary",
"llc_binary",
- "llubi_binary",
"clang",
"opt",
"llvm_bin",
diff --git a/llvm/utils/update_llubi_test_checks.py b/llvm/utils/update_llubi_test_checks.py
deleted file mode 100755
index c8434438edb12..0000000000000
--- a/llvm/utils/update_llubi_test_checks.py
+++ /dev/null
@@ -1,137 +0,0 @@
-#!/usr/bin/env python3
-
-"""A test case update script.
-
-This script is a utility to update LLVM 'llubi' based test cases with new
-FileCheck patterns.
-"""
-
-from __future__ import print_function
-
-from sys import stderr
-from traceback import print_exc
-import argparse
-import os # Used to advertise this file's name ("autogenerated_note").
-import subprocess
-import sys
-
-from UpdateTestChecks import common
-
-# llubi is the only llubi-like in the LLVM tree but downstream forks can add
-# additional ones here if they have them.
-LLUBI_LIKE_TOOLS = [
- "llubi",
-]
-
-
-# Invoke the tool that is being tested.
-def invoke_tool(exe, cmd_args, ir, check_rc):
- with open(ir) as ir_file:
- substitutions = common.getSubstitutions(ir)
- stdout = subprocess.run(
- exe + " " + common.applySubstitutions(cmd_args, substitutions),
- shell=True,
- stdin=ir_file,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- check=check_rc,
- ).stdout.decode()
- # Fix line endings to unix CR style.
- return stdout.replace("\r\n", "\n")
-
-
-def update_test(ti: common.TestInfo):
- if len(ti.run_lines) == 0:
- common.warn("No RUN lines found in test: " + ti.path)
- return
- if len(ti.run_lines) > 1:
- common.warn("Multiple RUN lines found in test: " + ti.path)
- common.warn("Only the first RUN line will be processed.")
-
- l = ti.run_lines[0]
- if "|" not in l:
- common.warn("Skipping unparsable RUN line: " + l)
- return
-
- commands = [cmd.strip() for cmd in l.split("|")]
- assert len(commands) == 2
- llubi_cmd = commands[-2]
- filecheck_cmd = commands[-1]
- args = llubi_cmd.split(" ")
- llubi_tool = args[0]
- check_rc = True
- if len(args) > 1 and args[0] == "not":
- llubi_tool = args[1]
- check_rc = False
-
- common.verify_filecheck_prefixes(filecheck_cmd)
-
- llubi_like_tools = LLUBI_LIKE_TOOLS[:]
- if ti.args.tool:
- llubi_like_tools.append(ti.args.tool)
- if llubi_tool not in llubi_like_tools:
- common.warn("Skipping non-llubi RUN line: " + l)
- return
-
- if not filecheck_cmd.startswith("FileCheck "):
- common.warn("Skipping non-FileChecked RUN line: " + l)
- return
-
- llubi_args = llubi_cmd[llubi_cmd.index(llubi_tool) + len(llubi_tool) :].strip()
- llubi_args = llubi_args.replace("< %s", "").replace("%s", "").strip()
- prefixes = common.get_check_prefixes(filecheck_cmd)
-
- common.debug("Extracted LLC cmd:", llubi_tool, llubi_args)
- common.debug("Extracted FileCheck prefixes:", str(prefixes))
- prefix_set = set([prefix for prefix in prefixes])
-
- raw_tool_output = invoke_tool(
- ti.args.llubi_binary or llubi_tool,
- llubi_args,
- ti.path,
- check_rc=check_rc,
- )
- if ti.args.llubi_binary:
- raw_tool_output = raw_tool_output.replace(ti.args.llubi_binary, llubi_tool)
-
- output_lines = []
- common.dump_input_lines(output_lines, ti, prefix_set, ";")
- output_lines.extend(["; CHECK: " + line for line in raw_tool_output.splitlines()])
-
- common.debug("Writing %d lines to %s..." % (len(output_lines), ti.path))
- with open(ti.path, "wb") as f:
- f.writelines(["{}\n".format(l).encode("utf-8") for l in output_lines])
-
-
-def main():
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument(
- "--llubi-binary",
- default=None,
- help='The "llubi" binary to use to generate the test case',
- )
- parser.add_argument(
- "--tool",
- default=None,
- help="Treat the given tool name as an llubi-like tool for which check lines should be generated",
- )
- parser.add_argument("tests", nargs="+")
- initial_args = common.parse_commandline_args(parser)
-
- script_name = os.path.basename(__file__)
-
- returncode = 0
- for ti in common.itertests(
- initial_args.tests, parser, script_name="utils/" + script_name
- ):
- try:
- update_test(ti)
- except Exception as e:
- stderr.write(f"Error: Failed to update test {ti.path}\n")
- print_exc()
- returncode = 1
- return returncode
-
-
-if __name__ == "__main__":
- sys.exit(main())
>From 4d6aeb592fe53b37d6d1f4ded5ae55e6a4c894ee Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Fri, 6 Feb 2026 05:26:36 +0800
Subject: [PATCH 08/15] [llubi] Fix tool discovery
---
llvm/test/lit.cfg.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/llvm/test/lit.cfg.py b/llvm/test/lit.cfg.py
index 8463e667d9f71..79b78ffeb2dab 100644
--- a/llvm/test/lit.cfg.py
+++ b/llvm/test/lit.cfg.py
@@ -235,6 +235,7 @@ def get_asan_rtlib():
"dsymutil",
"lli",
"lli-child-target",
+ "llubi",
"llvm-ar",
"llvm-as",
"llvm-addr2line",
>From e01160c56435906d17a5dbc9792de16fee44df80 Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Fri, 6 Feb 2026 05:28:52 +0800
Subject: [PATCH 09/15] [llubi] Print addresses in hexadecimal
---
llvm/test/tools/llubi/main.ll | 2 +-
llvm/test/tools/llubi/poison.ll | 2 +-
llvm/tools/llubi/lib/Value.cpp | 5 ++++-
3 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/llvm/test/tools/llubi/main.ll b/llvm/test/tools/llubi/main.ll
index b9004c6d4c8e6..21a02ea001d9e 100644
--- a/llvm/test/tools/llubi/main.ll
+++ b/llvm/test/tools/llubi/main.ll
@@ -6,6 +6,6 @@ define i32 @main(i32 %argc, ptr %argv) {
; CHECK: Entering function: main
; CHECK: i32 %argc = i32 1
-; CHECK: ptr %argv = ptr 16
+; CHECK: ptr %argv = ptr 0x10
; CHECK: ret i32 0
; CHECK: Exiting function: main
diff --git a/llvm/test/tools/llubi/poison.ll b/llvm/test/tools/llubi/poison.ll
index b5f6962f1de15..2fa6f1955f9ec 100644
--- a/llvm/test/tools/llubi/poison.ll
+++ b/llvm/test/tools/llubi/poison.ll
@@ -5,7 +5,7 @@ define i32 @main(i32 %argc, ptr %argv) {
}
; CHECK: Entering function: main
; CHECK: i32 %argc = i32 1
-; CHECK: ptr %argv = ptr 16
+; CHECK: ptr %argv = ptr 0x10
; CHECK: ret i32 poison
; CHECK: Exiting function: main
; CHECK: llubi: error: Execution of function 'main' resulted in poison return value.
diff --git a/llvm/tools/llubi/lib/Value.cpp b/llvm/tools/llubi/lib/Value.cpp
index cc1fa3022e961..2749bfea3a040 100644
--- a/llvm/tools/llubi/lib/Value.cpp
+++ b/llvm/tools/llubi/lib/Value.cpp
@@ -12,12 +12,15 @@
#include "Value.h"
#include "Context.h"
+#include "llvm/ADT/SmallString.h"
namespace llvm::ubi {
void Pointer::print(raw_ostream &OS) const {
// TODO: print information about the underlying memory object
- OS << "ptr " << Address;
+ SmallString<32> AddrStr;
+ Address.toStringUnsigned(AddrStr, 16);
+ OS << "ptr 0x" << AddrStr;
}
AnyValue Pointer::null(unsigned BitWidth) {
>From 509e48794c01d667befd4fbe960fa96f8901cf3c Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Fri, 6 Feb 2026 06:42:25 +0800
Subject: [PATCH 10/15] [llubi] Address review comments.
---
llvm/docs/CommandGuide/llubi.rst | 2 +-
llvm/test/CMakeLists.txt | 1 +
llvm/test/tools/llubi/main.ll | 2 +-
llvm/test/tools/llubi/poison.ll | 2 +-
llvm/tools/llubi/lib/Context.cpp | 19 +++++++++++----
llvm/tools/llubi/lib/Context.h | 19 +++++++++++----
llvm/tools/llubi/lib/Interpreter.cpp | 28 ++++++++++++++++------
llvm/tools/llubi/lib/Value.cpp | 4 ++--
llvm/tools/llubi/lib/Value.h | 21 ++++++++++++----
llvm/tools/llubi/llubi.cpp | 36 +++++++++++-----------------
10 files changed, 87 insertions(+), 47 deletions(-)
diff --git a/llvm/docs/CommandGuide/llubi.rst b/llvm/docs/CommandGuide/llubi.rst
index 7c870c5a116de..f652af83d810a 100644
--- a/llvm/docs/CommandGuide/llubi.rst
+++ b/llvm/docs/CommandGuide/llubi.rst
@@ -11,7 +11,7 @@ SYNOPSIS
DESCRIPTION
-----------
-:program:`llubi` directly executes programs in LLVM bitcode format and track values in LLVM IR semantics.
+:program:`llubi` directly executes programs in LLVM bitcode format and tracks values in LLVM IR semantics.
Unlike :program:`lli`, :program:`llubi` is designed to be aware of undefined behaviors during execution.
It detects immediate undefined behaviors such as integer division by zero, and respects poison generating flags
like `nsw` and `nuw`. As it captures most of the guardable undefined behaviors, it is highly suitable for
diff --git a/llvm/test/CMakeLists.txt b/llvm/test/CMakeLists.txt
index 77fbbe28ca56d..388ce613ad1d0 100644
--- a/llvm/test/CMakeLists.txt
+++ b/llvm/test/CMakeLists.txt
@@ -76,6 +76,7 @@ set(LLVM_TEST_DEPENDS
llc
lli
lli-child-target
+ llubi
llvm-addr2line
llvm-ar
llvm-as
diff --git a/llvm/test/tools/llubi/main.ll b/llvm/test/tools/llubi/main.ll
index 21a02ea001d9e..8359c7d5e82f0 100644
--- a/llvm/test/tools/llubi/main.ll
+++ b/llvm/test/tools/llubi/main.ll
@@ -6,6 +6,6 @@ define i32 @main(i32 %argc, ptr %argv) {
; CHECK: Entering function: main
; CHECK: i32 %argc = i32 1
-; CHECK: ptr %argv = ptr 0x10
+; CHECK: ptr %argv = ptr 0x8
; CHECK: ret i32 0
; CHECK: Exiting function: main
diff --git a/llvm/test/tools/llubi/poison.ll b/llvm/test/tools/llubi/poison.ll
index 2fa6f1955f9ec..11ccd6be5f463 100644
--- a/llvm/test/tools/llubi/poison.ll
+++ b/llvm/test/tools/llubi/poison.ll
@@ -5,7 +5,7 @@ define i32 @main(i32 %argc, ptr %argv) {
}
; CHECK: Entering function: main
; CHECK: i32 %argc = i32 1
-; CHECK: ptr %argv = ptr 0x10
+; CHECK: ptr %argv = ptr 0x8
; CHECK: ret i32 poison
; CHECK: Exiting function: main
; CHECK: llubi: error: Execution of function 'main' resulted in poison return value.
diff --git a/llvm/tools/llubi/lib/Context.cpp b/llvm/tools/llubi/lib/Context.cpp
index 5a9428b84cb26..d203f9b126cd7 100644
--- a/llvm/tools/llubi/lib/Context.cpp
+++ b/llvm/tools/llubi/lib/Context.cpp
@@ -44,7 +44,8 @@ MemoryObject::~MemoryObject() = default;
MemoryObject::MemoryObject(uint64_t Addr, uint64_t Size, StringRef Name,
unsigned AS, MemInitKind InitKind)
: Address(Addr), Size(Size), Name(Name), AS(AS),
- IsAlive(InitKind != MemInitKind::Poisoned) {
+ State(InitKind != MemInitKind::Poisoned ? MemoryObjectState::Alive
+ : MemoryObjectState::Dead) {
switch (InitKind) {
case MemInitKind::Zeroed:
Bytes.resize(Size, Byte{0, ByteKind::Concrete});
@@ -74,6 +75,7 @@ IntrusiveRefCntPtr<MemoryObject> Context::allocate(uint64_t Size,
UsedMem += Size;
return MemObj;
}
+
bool Context::free(uint64_t Address) {
auto It = MemoryObjects.find(Address);
if (It == MemoryObjects.end())
@@ -84,9 +86,16 @@ bool Context::free(uint64_t Address) {
return true;
}
+Pointer Context::deriveFromMemoryObject(IntrusiveRefCntPtr<MemoryObject> Obj) {
+ assert(Obj && "Cannot determine the address space of a null memory object");
+ return Pointer(
+ Obj,
+ APInt(DL.getPointerSizeInBits(Obj->getAddressSpace()), Obj->getAddress()),
+ /*Offset=*/0);
+}
+
void MemoryObject::markAsFreed() {
- IsAlive = false;
- IsFreed = true;
+ State = MemoryObjectState::Freed;
Size = 0;
Bytes.clear();
}
@@ -96,7 +105,7 @@ void MemoryObject::writeRawBytes(uint64_t Offset, const void *Data,
assert(SaturatingAdd(Offset, Length) <= Size && "Write out of bounds");
const uint8_t *ByteData = static_cast<const uint8_t *>(Data);
for (uint64_t I = 0; I < Length; ++I)
- Bytes[Offset + I].Set(ByteData[I]);
+ Bytes[Offset + I].set(ByteData[I]);
}
void MemoryObject::writeInteger(uint64_t Offset, const APInt &Int,
@@ -107,7 +116,7 @@ void MemoryObject::writeInteger(uint64_t Offset, const APInt &Int,
for (uint64_t I = 0; I < IntSize; ++I) {
uint64_t ByteIndex = DL.isLittleEndian() ? I : (IntSize - 1 - I);
uint64_t Bits = std::min(BitWidth - ByteIndex * 8, uint64_t(8));
- Bytes[Offset + I].Set(Int.extractBitsAsZExtValue(Bits, ByteIndex * 8));
+ Bytes[Offset + I].set(Int.extractBitsAsZExtValue(Bits, ByteIndex * 8));
}
}
void MemoryObject::writeFloat(uint64_t Offset, const APFloat &Float,
diff --git a/llvm/tools/llubi/lib/Context.h b/llvm/tools/llubi/lib/Context.h
index 19ae740811fd6..c9c8f524e6d09 100644
--- a/llvm/tools/llubi/lib/Context.h
+++ b/llvm/tools/llubi/lib/Context.h
@@ -25,6 +25,16 @@ enum class MemInitKind {
Poisoned,
};
+enum class MemoryObjectState {
+ // This memory object is accessible.
+ Alive,
+ // This memory object is out of lifetime. It is OK to perform
+ // operations that do not access its content, e.g., getelementptr.
+ // Otherwise, an immediate UB occurs.
+ Dead,
+ Freed,
+};
+
class MemoryObject : public RefCountedBase<MemoryObject> {
uint64_t Address;
uint64_t Size;
@@ -32,9 +42,8 @@ class MemoryObject : public RefCountedBase<MemoryObject> {
StringRef Name;
unsigned AS;
- bool IsAlive = false;
+ MemoryObjectState State;
bool IsConstant = false;
- bool IsFreed = false;
public:
MemoryObject(uint64_t Addr, uint64_t Size, StringRef Name, unsigned AS,
@@ -49,8 +58,7 @@ class MemoryObject : public RefCountedBase<MemoryObject> {
uint64_t getSize() const { return Size; }
StringRef getName() const { return Name; }
unsigned getAddressSpace() const { return AS; }
- bool isAlive() const { return IsAlive; }
- bool isFreed() const { return IsFreed; }
+ MemoryObjectState getState() const { return State; }
bool isConstant() const { return IsConstant; }
void setIsConstant(bool C) { IsConstant = C; }
@@ -151,6 +159,9 @@ class Context {
StringRef Name, unsigned AS,
MemInitKind InitKind);
bool free(uint64_t Address);
+ // Derive a pointer from a memory object with offset 0.
+ // Please use Pointer's interface for further manipulations.
+ Pointer deriveFromMemoryObject(IntrusiveRefCntPtr<MemoryObject> Obj);
/// Execute the function \p F with arguments \p Args, and store the return
/// value in \p RetVal if the function is not void.
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index bfd5f588aa9a4..1fc4eb6447e40 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -18,9 +18,20 @@
namespace llvm::ubi {
enum class FrameState {
+ // It is about to enter the function.
+ // Valid transition:
+ // -> Running
Entry,
+ // It is executing instructions inside the function.
+ // Valid transitions:
+ // -> Pending (on call)
+ // -> Exit (on return)
Running,
+ // It is about to enter a callee or handle return value from the callee.
+ // Valid transitions:
+ // -> Running (after returning from callee)
Pending,
+ // It is about to return the control to the caller.
Exit,
};
@@ -98,6 +109,9 @@ class InstExecutor : public InstVisitor<InstExecutor, bool> {
return false;
}
+ /// This function implements the main interpreter loop.
+ /// It handles function calls in a non-recursive manner to avoid stack
+ /// overflows.
bool runMainLoop() {
uint32_t MaxSteps = Ctx.getMaxSteps();
uint32_t Steps = 0;
@@ -138,12 +152,15 @@ class InstExecutor : public InstVisitor<InstExecutor, bool> {
Handler.onInstructionExecuted(I, Top.ValueMap.at(&I));
}
- if (Top.State != FrameState::Running) {
- // A function call or return has occurred.
+ // A function call or return has occurred.
+ // We need to exit the inner loop and switch to a different frame.
+ if (Top.State != FrameState::Running)
break;
- } else if (!I.isTerminator()) {
+
+ // Otherwise, move to the next instruction if it is not a terminator.
+ // For terminators, the PC is updated in the visit* method.
+ if (!I.isTerminator())
++Top.PC;
- }
}
if (!Status)
@@ -164,9 +181,6 @@ class InstExecutor : public InstVisitor<InstExecutor, bool> {
}
};
-/// This function implements the main interpreter loop.
-/// It handles function calls in a non-recursive manner to avoid stack
-/// overflows.
bool Context::runFunction(Function &F, ArrayRef<AnyValue> Args,
AnyValue &RetVal, EventHandler &Handler) {
InstExecutor Executor(*this, Handler, F, Args, RetVal);
diff --git a/llvm/tools/llubi/lib/Value.cpp b/llvm/tools/llubi/lib/Value.cpp
index 2749bfea3a040..77c801e673234 100644
--- a/llvm/tools/llubi/lib/Value.cpp
+++ b/llvm/tools/llubi/lib/Value.cpp
@@ -24,13 +24,13 @@ void Pointer::print(raw_ostream &OS) const {
}
AnyValue Pointer::null(unsigned BitWidth) {
- return AnyValue(Pointer(nullptr, APInt::getZero(BitWidth), 0, 0));
+ return AnyValue(Pointer(nullptr, APInt::getZero(BitWidth), 0));
}
MemoryObject *Pointer::getMemoryObject() const {
if (!Obj)
return nullptr;
- if (Obj->isFreed())
+ if (Obj->getState() == MemoryObjectState::Freed)
Obj.reset();
return Obj.get();
}
diff --git a/llvm/tools/llubi/lib/Value.h b/llvm/tools/llubi/lib/Value.h
index 706d79437c65e..8a478a3581ced 100644
--- a/llvm/tools/llubi/lib/Value.h
+++ b/llvm/tools/llubi/lib/Value.h
@@ -22,8 +22,14 @@ class Context;
class AnyValue;
enum class ByteKind : uint8_t {
+ // A concrete byte with a known value.
Concrete,
+ // A uninitialized byte. Each load from an uninitialized byte yields
+ // a nondeterministic value.
Undef,
+ // A poisoned byte. It occurs when the program stores a poison value to
+ // memory,
+ // or when a memory object is dead.
Poison,
};
@@ -32,7 +38,7 @@ struct Byte {
ByteKind Kind : 2;
// TODO: provenance
- void Set(uint8_t V) {
+ void set(uint8_t V) {
Value = V;
Kind = ByteKind::Concrete;
}
@@ -49,21 +55,28 @@ enum class StorageKind {
};
class Pointer {
+ // The underlying memory object. It can be null for invalid or dangling
+ // pointers.
mutable IntrusiveRefCntPtr<MemoryObject> Obj;
+ // The address of the pointer. The bit width is determined by
+ // DataLayout::getPointerSizeInBits.
APInt Address;
+ // The offset within the memory object.
uint64_t Offset;
- uint64_t Bound;
+ // TODO: modeling inrange(Start, End) attribute
public:
explicit Pointer(IntrusiveRefCntPtr<MemoryObject> Obj, const APInt &Address,
- uint64_t Offset, uint64_t Bound)
- : Obj(std::move(Obj)), Address(Address), Offset(Offset), Bound(Bound) {}
+ uint64_t Offset)
+ : Obj(std::move(Obj)), Address(Address), Offset(Offset) {}
static AnyValue null(unsigned BitWidth);
void print(raw_ostream &OS) const;
const APInt &address() const { return Address; }
MemoryObject *getMemoryObject() const;
};
+// Value representation for actual values of LLVM values.
+// We don't model undef values here (except for byte types).
class [[nodiscard]] AnyValue {
StorageKind Kind;
union {
diff --git a/llvm/tools/llubi/llubi.cpp b/llvm/tools/llubi/llubi.cpp
index be16e1cbfe77d..2e5b1b5696e07 100644
--- a/llvm/tools/llubi/llubi.cpp
+++ b/llvm/tools/llubi/llubi.cpp
@@ -181,19 +181,8 @@ int main(int argc, char **argv) {
Args.push_back(
Ctx.getConstantValue(ConstantInt::get(IntTy, InputArgv.size())));
- uint64_t Size = 0;
- for (const auto &Arg : InputArgv)
- Size += Arg.length() + 1;
- auto ArgvStrMem = Ctx.allocate(Size, 8, "argv_str",
- /*AS=*/0, ubi::MemInitKind::Zeroed);
- if (!ArgvStrMem) {
- WithColor::error(errs(), argv[0])
- << "Failed to allocate memory for argv strings.\n";
- return 1;
- }
- uint64_t Offset = 0;
- uint32_t PtrBitWidth = Ctx.getDataLayout().getPointerSizeInBits(0);
- uint64_t PtrsSize = PtrBitWidth * (InputArgv.size() + 1);
+ uint32_t PtrSize = Ctx.getDataLayout().getPointerSize();
+ uint64_t PtrsSize = PtrSize * (InputArgv.size() + 1);
auto ArgvPtrsMem = Ctx.allocate(PtrsSize, 8, "argv",
/*AS=*/0, ubi::MemInitKind::Zeroed);
if (!ArgvPtrsMem) {
@@ -202,16 +191,19 @@ int main(int argc, char **argv) {
return 1;
}
for (const auto &[Idx, Arg] : enumerate(InputArgv)) {
- ubi::Pointer ArgPtr(ArgvStrMem,
- APInt(PtrBitWidth, ArgvStrMem->getAddress() + Offset),
- /*Offset=*/Offset, /*Bound=*/Size);
- ArgvStrMem->writeRawBytes(Offset, Arg.c_str(), Arg.length());
- ArgvPtrsMem->writePointer(Idx * PtrBitWidth, ArgPtr, Ctx.getDataLayout());
- Offset += Arg.length() + 1;
+ uint64_t Size = Arg.length() + 1;
+ auto ArgvStrMem = Ctx.allocate(Size, 8, "argv_str",
+ /*AS=*/0, ubi::MemInitKind::Zeroed);
+ if (!ArgvStrMem) {
+ WithColor::error(errs(), argv[0])
+ << "Failed to allocate memory for argv strings.\n";
+ return 1;
+ }
+ ubi::Pointer ArgPtr = Ctx.deriveFromMemoryObject(ArgvStrMem);
+ ArgvStrMem->writeRawBytes(0, Arg.c_str(), Arg.length());
+ ArgvPtrsMem->writePointer(Idx * PtrSize, ArgPtr, Ctx.getDataLayout());
}
- Args.push_back(ubi::Pointer(ArgvPtrsMem,
- APInt(PtrBitWidth, ArgvPtrsMem->getAddress()),
- /*Offset=*/0, /*Bound=*/PtrsSize));
+ Args.push_back(Ctx.deriveFromMemoryObject(ArgvPtrsMem));
} else {
// If the signature does not match (e.g., llvm-reduce change the signature
// of main), it will pass null values for all arguments.
>From ec9ffda3351386d8749625fd9834629f977b3166 Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Fri, 6 Feb 2026 21:46:32 +0800
Subject: [PATCH 11/15] [llubi] Address review comments.
---
llvm/docs/TestingGuide.rst | 3 ---
llvm/test/tools/llubi/poison.ll | 2 +-
llvm/tools/llubi/lib/Context.cpp | 2 +-
llvm/tools/llubi/lib/Context.h | 8 ++++++++
llvm/tools/llubi/lib/Interpreter.cpp | 2 +-
llvm/tools/llubi/lib/Value.cpp | 4 +++-
llvm/tools/llubi/llubi.cpp | 24 ++++++++++--------------
7 files changed, 24 insertions(+), 21 deletions(-)
diff --git a/llvm/docs/TestingGuide.rst b/llvm/docs/TestingGuide.rst
index f2c953a6221fc..6ab33383e929b 100644
--- a/llvm/docs/TestingGuide.rst
+++ b/llvm/docs/TestingGuide.rst
@@ -328,9 +328,6 @@ assertions:
update_test_checks.py
opt
- update_llubi_test_checks.py
- llubi
-
Precommit workflow for tests
----------------------------
diff --git a/llvm/test/tools/llubi/poison.ll b/llvm/test/tools/llubi/poison.ll
index 11ccd6be5f463..0f6cbeaca3b6b 100644
--- a/llvm/test/tools/llubi/poison.ll
+++ b/llvm/test/tools/llubi/poison.ll
@@ -8,4 +8,4 @@ define i32 @main(i32 %argc, ptr %argv) {
; CHECK: ptr %argv = ptr 0x8
; CHECK: ret i32 poison
; CHECK: Exiting function: main
-; CHECK: llubi: error: Execution of function 'main' resulted in poison return value.
+; CHECK: error: Execution of function 'main' resulted in poison return value.
diff --git a/llvm/tools/llubi/lib/Context.cpp b/llvm/tools/llubi/lib/Context.cpp
index d203f9b126cd7..efe85c918f7cf 100644
--- a/llvm/tools/llubi/lib/Context.cpp
+++ b/llvm/tools/llubi/lib/Context.cpp
@@ -80,8 +80,8 @@ bool Context::free(uint64_t Address) {
auto It = MemoryObjects.find(Address);
if (It == MemoryObjects.end())
return false;
- It->second->markAsFreed();
UsedMem -= It->second->getSize();
+ It->second->markAsFreed();
MemoryObjects.erase(It);
return true;
}
diff --git a/llvm/tools/llubi/lib/Context.h b/llvm/tools/llubi/lib/Context.h
index c9c8f524e6d09..7f08c401fc857 100644
--- a/llvm/tools/llubi/lib/Context.h
+++ b/llvm/tools/llubi/lib/Context.h
@@ -27,11 +27,19 @@ enum class MemInitKind {
enum class MemoryObjectState {
// This memory object is accessible.
+ // Valid transitions:
+ // -> Dead (after the end of lifetime of an alloca)
+ // -> Freed (after free is called on a heap object)
Alive,
// This memory object is out of lifetime. It is OK to perform
// operations that do not access its content, e.g., getelementptr.
// Otherwise, an immediate UB occurs.
+ // Valid transition:
+ // -> Alive (after the start of lifetime of an alloca)
Dead,
+ // This heap memory object has been freed. Any access to it
+ // causes immediate UB. Like dead objects, it is still possible to
+ // perform operations that do not access its content.
Freed,
};
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index 1fc4eb6447e40..5b5b96a571c70 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -120,7 +120,7 @@ class InstExecutor : public InstVisitor<InstExecutor, bool> {
CurrentFrame = &Top;
if (Top.State == FrameState::Entry) {
Handler.onFunctionEntry(Top.Func, Top.Args, Top.CallSite);
- // TODO:Handle arg attributes
+ // TODO: Handle arg attributes
} else {
assert(Top.State == FrameState::Pending &&
"Expected to return from a callee.");
diff --git a/llvm/tools/llubi/lib/Value.cpp b/llvm/tools/llubi/lib/Value.cpp
index 77c801e673234..e9386f6c444fb 100644
--- a/llvm/tools/llubi/lib/Value.cpp
+++ b/llvm/tools/llubi/lib/Value.cpp
@@ -38,8 +38,10 @@ MemoryObject *Pointer::getMemoryObject() const {
void AnyValue::print(raw_ostream &OS) const {
switch (Kind) {
case StorageKind::Integer:
- if (IntVal.getBitWidth() == 1)
+ if (IntVal.getBitWidth() == 1) {
OS << (IntVal.getBoolValue() ? "T" : "F");
+ break;
+ }
OS << "i" << IntVal.getBitWidth() << ' ' << IntVal;
break;
case StorageKind::Float:
diff --git a/llvm/tools/llubi/llubi.cpp b/llvm/tools/llubi/llubi.cpp
index 2e5b1b5696e07..e3d438dd86d6b 100644
--- a/llvm/tools/llubi/llubi.cpp
+++ b/llvm/tools/llubi/llubi.cpp
@@ -126,8 +126,7 @@ int main(int argc, char **argv) {
cl::ParseCommandLineOptions(argc, argv, "llvm ub-aware interpreter\n");
if (EntryFunc.empty()) {
- WithColor::error(errs(), argv[0])
- << "--entry-function name cannot be empty\n";
+ WithColor::error() << "--entry-function name cannot be empty\n";
return 1;
}
@@ -168,8 +167,8 @@ int main(int argc, char **argv) {
// using the contents of Args to determine argc & argv
Function *EntryFn = Mod->getFunction(EntryFunc);
if (!EntryFn) {
- WithColor::error(errs(), argv[0])
- << '\'' << EntryFunc << "\' function not found in module.\n";
+ WithColor::error() << '\'' << EntryFunc
+ << "\' function not found in module.\n";
return 1;
}
TargetLibraryInfo TLI(Ctx.getTLIImpl());
@@ -186,8 +185,7 @@ int main(int argc, char **argv) {
auto ArgvPtrsMem = Ctx.allocate(PtrsSize, 8, "argv",
/*AS=*/0, ubi::MemInitKind::Zeroed);
if (!ArgvPtrsMem) {
- WithColor::error(errs(), argv[0])
- << "Failed to allocate memory for argv pointers.\n";
+ WithColor::error() << "Failed to allocate memory for argv pointers.\n";
return 1;
}
for (const auto &[Idx, Arg] : enumerate(InputArgv)) {
@@ -195,8 +193,7 @@ int main(int argc, char **argv) {
auto ArgvStrMem = Ctx.allocate(Size, 8, "argv_str",
/*AS=*/0, ubi::MemInitKind::Zeroed);
if (!ArgvStrMem) {
- WithColor::error(errs(), argv[0])
- << "Failed to allocate memory for argv strings.\n";
+ WithColor::error() << "Failed to allocate memory for argv strings.\n";
return 1;
}
ubi::Pointer ArgPtr = Ctx.deriveFromMemoryObject(ArgvStrMem);
@@ -207,7 +204,7 @@ int main(int argc, char **argv) {
} else {
// If the signature does not match (e.g., llvm-reduce change the signature
// of main), it will pass null values for all arguments.
- WithColor::warning(errs(), argv[0])
+ WithColor::warning()
<< "The signature of function '" << EntryFunc
<< "' does not match 'int main(int, char**)', passing null values for "
"all arguments.\n";
@@ -221,8 +218,8 @@ int main(int argc, char **argv) {
ubi::AnyValue RetVal;
if (!Ctx.runFunction(*EntryFn, Args, RetVal,
Verbose ? VerboseHandler : NoopHandler)) {
- WithColor::error(errs(), argv[0])
- << "Execution of function '" << EntryFunc << "' failed.\n";
+ WithColor::error() << "Execution of function '" << EntryFunc
+ << "' failed.\n";
return 1;
}
@@ -230,9 +227,8 @@ int main(int argc, char **argv) {
if (EntryFn->getReturnType()->isIntegerTy()) {
assert(!RetVal.isNone() && "Expected a return value from entry function");
if (RetVal.isPoison()) {
- WithColor::error(errs(), argv[0])
- << "Execution of function '" << EntryFunc
- << "' resulted in poison return value.\n";
+ WithColor::error() << "Execution of function '" << EntryFunc
+ << "' resulted in poison return value.\n";
return 1;
}
APInt Result = RetVal.asInteger();
>From bc06026929eec2a560a7d8075ddc8009c486e7c4 Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Fri, 6 Feb 2026 22:08:51 +0800
Subject: [PATCH 12/15] [llubi] Don't reset size of freed object
---
llvm/tools/llubi/lib/Context.cpp | 1 -
llvm/tools/llubi/lib/Value.cpp | 8 --------
llvm/tools/llubi/lib/Value.h | 4 ++--
3 files changed, 2 insertions(+), 11 deletions(-)
diff --git a/llvm/tools/llubi/lib/Context.cpp b/llvm/tools/llubi/lib/Context.cpp
index efe85c918f7cf..e047dca4fd83b 100644
--- a/llvm/tools/llubi/lib/Context.cpp
+++ b/llvm/tools/llubi/lib/Context.cpp
@@ -96,7 +96,6 @@ Pointer Context::deriveFromMemoryObject(IntrusiveRefCntPtr<MemoryObject> Obj) {
void MemoryObject::markAsFreed() {
State = MemoryObjectState::Freed;
- Size = 0;
Bytes.clear();
}
diff --git a/llvm/tools/llubi/lib/Value.cpp b/llvm/tools/llubi/lib/Value.cpp
index e9386f6c444fb..dab1791acf200 100644
--- a/llvm/tools/llubi/lib/Value.cpp
+++ b/llvm/tools/llubi/lib/Value.cpp
@@ -27,14 +27,6 @@ AnyValue Pointer::null(unsigned BitWidth) {
return AnyValue(Pointer(nullptr, APInt::getZero(BitWidth), 0));
}
-MemoryObject *Pointer::getMemoryObject() const {
- if (!Obj)
- return nullptr;
- if (Obj->getState() == MemoryObjectState::Freed)
- Obj.reset();
- return Obj.get();
-}
-
void AnyValue::print(raw_ostream &OS) const {
switch (Kind) {
case StorageKind::Integer:
diff --git a/llvm/tools/llubi/lib/Value.h b/llvm/tools/llubi/lib/Value.h
index 8a478a3581ced..0828941538798 100644
--- a/llvm/tools/llubi/lib/Value.h
+++ b/llvm/tools/llubi/lib/Value.h
@@ -57,7 +57,7 @@ enum class StorageKind {
class Pointer {
// The underlying memory object. It can be null for invalid or dangling
// pointers.
- mutable IntrusiveRefCntPtr<MemoryObject> Obj;
+ IntrusiveRefCntPtr<MemoryObject> Obj;
// The address of the pointer. The bit width is determined by
// DataLayout::getPointerSizeInBits.
APInt Address;
@@ -72,7 +72,7 @@ class Pointer {
static AnyValue null(unsigned BitWidth);
void print(raw_ostream &OS) const;
const APInt &address() const { return Address; }
- MemoryObject *getMemoryObject() const;
+ MemoryObject *getMemoryObject() const { return Obj.get(); }
};
// Value representation for actual values of LLVM values.
>From 3a2115971d9a8d89356bed809918e7119eb3f1a0 Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Fri, 6 Feb 2026 22:54:08 +0800
Subject: [PATCH 13/15] [llubi] Address review comments
---
llvm/test/tools/llubi/main.ll | 2 +-
llvm/test/tools/llubi/poison.ll | 2 +-
llvm/tools/llubi/lib/Context.cpp | 2 --
llvm/tools/llubi/lib/Context.h | 2 --
llvm/tools/llubi/lib/Interpreter.cpp | 14 +++++++++++---
llvm/tools/llubi/lib/Value.cpp | 11 +++++++++--
6 files changed, 22 insertions(+), 11 deletions(-)
diff --git a/llvm/test/tools/llubi/main.ll b/llvm/test/tools/llubi/main.ll
index 8359c7d5e82f0..c10824621018e 100644
--- a/llvm/test/tools/llubi/main.ll
+++ b/llvm/test/tools/llubi/main.ll
@@ -6,6 +6,6 @@ define i32 @main(i32 %argc, ptr %argv) {
; CHECK: Entering function: main
; CHECK: i32 %argc = i32 1
-; CHECK: ptr %argv = ptr 0x8
+; CHECK: ptr %argv = ptr 0x8 [argv]
; CHECK: ret i32 0
; CHECK: Exiting function: main
diff --git a/llvm/test/tools/llubi/poison.ll b/llvm/test/tools/llubi/poison.ll
index 0f6cbeaca3b6b..cf3b69d1aeb77 100644
--- a/llvm/test/tools/llubi/poison.ll
+++ b/llvm/test/tools/llubi/poison.ll
@@ -5,7 +5,7 @@ define i32 @main(i32 %argc, ptr %argv) {
}
; CHECK: Entering function: main
; CHECK: i32 %argc = i32 1
-; CHECK: ptr %argv = ptr 0x8
+; CHECK: ptr %argv = ptr 0x8 [argv]
; CHECK: ret i32 poison
; CHECK: Exiting function: main
; CHECK: error: Execution of function 'main' resulted in poison return value.
diff --git a/llvm/tools/llubi/lib/Context.cpp b/llvm/tools/llubi/lib/Context.cpp
index e047dca4fd83b..6b5362204cfde 100644
--- a/llvm/tools/llubi/lib/Context.cpp
+++ b/llvm/tools/llubi/lib/Context.cpp
@@ -56,8 +56,6 @@ MemoryObject::MemoryObject(uint64_t Addr, uint64_t Size, StringRef Name,
case MemInitKind::Poisoned:
Bytes.resize(Size, Byte{0, ByteKind::Poison});
break;
- default:
- llvm_unreachable("Unknown MemInitKind");
}
}
diff --git a/llvm/tools/llubi/lib/Context.h b/llvm/tools/llubi/lib/Context.h
index 7f08c401fc857..61dc614f25838 100644
--- a/llvm/tools/llubi/lib/Context.h
+++ b/llvm/tools/llubi/lib/Context.h
@@ -17,8 +17,6 @@
namespace llvm::ubi {
-class Frame;
-
enum class MemInitKind {
Zeroed,
Uninitialized,
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index 5b5b96a571c70..fddab7e0e1636 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -46,8 +46,11 @@ struct Frame {
BasicBlock *BB;
BasicBlock::iterator PC;
FrameState State = FrameState::Entry;
- SmallVector<MemoryObject *> Allocas;
- DenseMap<Instruction *, AnyValue> ValueMap;
+ // Stack objects allocated in this frame. They will be automatically freed
+ // when the function returns.
+ SmallVector<IntrusiveRefCntPtr<MemoryObject>> Allocas;
+ // Values of arguments and executed instructions in this function.
+ DenseMap<Value *, AnyValue> ValueMap;
// Reserved for in-flight subroutines.
SmallVector<AnyValue> CalleeArgs;
@@ -58,8 +61,13 @@ struct Frame {
const TargetLibraryInfoImpl &TLIImpl)
: Func(F), LastFrame(LastFrame), CallSite(CallSite), Args(Args),
RetVal(RetVal), TLI(TLIImpl, &F) {
+ assert((Args.size() == F.arg_size() ||
+ (F.isVarArg() && Args.size() >= F.arg_size())) &&
+ "Expected enough arguments to call the function.");
BB = &Func.getEntryBlock();
PC = BB->begin();
+ for (Argument &Arg : F.args())
+ ValueMap[&Arg] = Args[Arg.getArgNo()];
}
};
@@ -88,7 +96,7 @@ class InstExecutor : public InstVisitor<InstExecutor, bool> {
const AnyValue &getValue(Value *V) {
if (auto *C = dyn_cast<Constant>(V))
return Ctx.getConstantValue(C);
- return CurrentFrame->ValueMap.at(cast<Instruction>(V));
+ return CurrentFrame->ValueMap.at(V);
}
public:
diff --git a/llvm/tools/llubi/lib/Value.cpp b/llvm/tools/llubi/lib/Value.cpp
index dab1791acf200..57cd94ef0f7bb 100644
--- a/llvm/tools/llubi/lib/Value.cpp
+++ b/llvm/tools/llubi/lib/Value.cpp
@@ -17,10 +17,17 @@
namespace llvm::ubi {
void Pointer::print(raw_ostream &OS) const {
- // TODO: print information about the underlying memory object
SmallString<32> AddrStr;
Address.toStringUnsigned(AddrStr, 16);
- OS << "ptr 0x" << AddrStr;
+ OS << "ptr 0x" << AddrStr << " [";
+ if (Obj) {
+ OS << Obj->getName();
+ if (Offset)
+ OS << " + " << Offset;
+ } else {
+ OS << "dangling";
+ }
+ OS << "]";
}
AnyValue Pointer::null(unsigned BitWidth) {
>From 3d43b7c062e91573893429215719135a8f14dff3 Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Fri, 6 Feb 2026 23:00:35 +0800
Subject: [PATCH 14/15] [llubi] Add more comments.
---
llvm/tools/llubi/lib/Context.h | 3 +++
llvm/tools/llubi/lib/Interpreter.cpp | 4 ++++
2 files changed, 7 insertions(+)
diff --git a/llvm/tools/llubi/lib/Context.h b/llvm/tools/llubi/lib/Context.h
index 61dc614f25838..a0153752b4404 100644
--- a/llvm/tools/llubi/lib/Context.h
+++ b/llvm/tools/llubi/lib/Context.h
@@ -106,6 +106,9 @@ class EventHandler {
}
};
+/// The global context for the interpreter.
+/// It tracks global state such as heap memory objects and floating point
+/// environment.
class Context {
// Module
LLVMContext &Ctx;
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index fddab7e0e1636..aaad8fb15262e 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -35,6 +35,10 @@ enum class FrameState {
Exit,
};
+/// Context for a function call.
+/// This struct maintains the state during the execution of a function,
+/// including the control flow, values of executed instructions, and stack
+/// objects.
struct Frame {
Function &Func;
Frame *LastFrame;
>From d1e9326d8d7251d2d1460f08d27e376f8359651f Mon Sep 17 00:00:00 2001
From: Yingwei Zheng <dtcxzyw2333 at gmail.com>
Date: Sat, 7 Feb 2026 18:38:36 +0800
Subject: [PATCH 15/15] [llubi] Allow `int main()`
---
llvm/test/tools/llubi/main2.ll | 9 +++++++++
llvm/tools/llubi/llubi.cpp | 2 +-
2 files changed, 10 insertions(+), 1 deletion(-)
create mode 100644 llvm/test/tools/llubi/main2.ll
diff --git a/llvm/test/tools/llubi/main2.ll b/llvm/test/tools/llubi/main2.ll
new file mode 100644
index 0000000000000..58c5744bb0909
--- /dev/null
+++ b/llvm/test/tools/llubi/main2.ll
@@ -0,0 +1,9 @@
+; RUN: llubi --verbose < %s 2>&1 | FileCheck %s
+
+define i32 @main() {
+ ret i32 0
+}
+
+; CHECK: Entering function: main
+; CHECK: ret i32 0
+; CHECK: Exiting function: main
diff --git a/llvm/tools/llubi/llubi.cpp b/llvm/tools/llubi/llubi.cpp
index e3d438dd86d6b..67ab01eca89fe 100644
--- a/llvm/tools/llubi/llubi.cpp
+++ b/llvm/tools/llubi/llubi.cpp
@@ -201,7 +201,7 @@ int main(int argc, char **argv) {
ArgvPtrsMem->writePointer(Idx * PtrSize, ArgPtr, Ctx.getDataLayout());
}
Args.push_back(Ctx.deriveFromMemoryObject(ArgvPtrsMem));
- } else {
+ } else if (!EntryFn->arg_empty()) {
// If the signature does not match (e.g., llvm-reduce change the signature
// of main), it will pass null values for all arguments.
WithColor::warning()
More information about the llvm-commits
mailing list