[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