[llvm] [llubi] Initial support for floating-point numbers (PR #188453)

Zhige Chen via llvm-commits llvm-commits at lists.llvm.org
Thu Apr 2 04:06:17 PDT 2026


https://github.com/nofe1248 updated https://github.com/llvm/llvm-project/pull/188453

>From 0b1c62180b78d09449f14f5a8f575be5b02147c1 Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhige_chen at outlook.com>
Date: Wed, 25 Mar 2026 14:46:09 +0800
Subject: [PATCH 01/20] [llubi] Initial support for floating-point numbers

---
 llvm/test/tools/llubi/fp_arith.ll    |  33 +++
 llvm/test/tools/llubi/fp_cast.ll     |  29 +++
 llvm/test/tools/llubi/fp_cmp.ll      |  25 +++
 llvm/test/tools/llubi/fp_denorm.ll   |  15 ++
 llvm/test/tools/llubi/fp_fastmath.ll |  18 ++
 llvm/tools/llubi/lib/Interpreter.cpp | 296 ++++++++++++++++++++++++++-
 6 files changed, 415 insertions(+), 1 deletion(-)
 create mode 100644 llvm/test/tools/llubi/fp_arith.ll
 create mode 100644 llvm/test/tools/llubi/fp_cast.ll
 create mode 100644 llvm/test/tools/llubi/fp_cmp.ll
 create mode 100644 llvm/test/tools/llubi/fp_denorm.ll
 create mode 100644 llvm/test/tools/llubi/fp_fastmath.ll

diff --git a/llvm/test/tools/llubi/fp_arith.ll b/llvm/test/tools/llubi/fp_arith.ll
new file mode 100644
index 0000000000000..2fd09ac53b1a2
--- /dev/null
+++ b/llvm/test/tools/llubi/fp_arith.ll
@@ -0,0 +1,33 @@
+; 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 void @main() {
+  %add = fadd float 1.5, 2.5
+  %sub = fsub double 5.0, 1.0
+  %mul = fmul float 2.0, 3.0
+  %div = fdiv double 1.0, 2.0
+  %neg = fneg float -1.5
+
+  %vadd = fadd <2 x float> <float 1.0, float 2.0>, <float 3.0, float 4.0>
+
+  %p1 = fadd float 1.0, poison
+  %p2 = fneg float poison
+
+  %inf = fdiv float 1.0, 0.0
+  %nan = fdiv float 0.0, 0.0
+
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %add = fadd float 1.500000e+00, 2.500000e+00 => float 4.000000e+00
+; CHECK-NEXT:   %sub = fsub double 5.000000e+00, 1.000000e+00 => double 4.000000e+00
+; CHECK-NEXT:   %mul = fmul float 2.000000e+00, 3.000000e+00 => float 6.000000e+00
+; CHECK-NEXT:   %div = fdiv double 1.000000e+00, 2.000000e+00 => double 5.000000e-01
+; CHECK-NEXT:   %neg = fneg float -1.500000e+00 => float 1.500000e+00
+; CHECK-NEXT:   %vadd = fadd <2 x float> <float 1.000000e+00, float 2.000000e+00>, <float 3.000000e+00, float 4.000000e+00> => { float 4.000000e+00, float 6.000000e+00 }
+; CHECK-NEXT:   %p1 = fadd float 1.000000e+00, poison => poison
+; CHECK-NEXT:   %p2 = fneg float poison => poison
+; CHECK-NEXT:   %inf = fdiv float 1.000000e+00, 0.000000e+00 => float +Inf
+; CHECK-NEXT:   %nan = fdiv float 0.000000e+00, 0.000000e+00 => float NaN
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/fp_cast.ll b/llvm/test/tools/llubi/fp_cast.ll
new file mode 100644
index 0000000000000..bab205f1c5891
--- /dev/null
+++ b/llvm/test/tools/llubi/fp_cast.ll
@@ -0,0 +1,29 @@
+; 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 void @main() {
+  %ext = fpext float 2.0 to double
+  %trunc = fptrunc double 3.5 to float
+
+  %si2fp = sitofp i32 -2 to float
+  %ui2fp = uitofp i32 255 to float
+
+  %fp2si = fptosi double -4.75 to i32
+  %fp2ui = fptoui double 4.75 to i32
+
+  %oob_ui = fptoui float -1.0 to i32
+  %nan_si = fptosi float 0x7FF8000000000000 to i32
+
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %ext = fpext float 2.000000e+00 to double => double 2.000000e+00
+; CHECK-NEXT:   %trunc = fptrunc double 3.500000e+00 to float => float 3.500000e+00
+; CHECK-NEXT:   %si2fp = sitofp i32 -2 to float => float -2.000000e+00
+; CHECK-NEXT:   %ui2fp = uitofp i32 255 to float => float 2.550000e+02
+; CHECK-NEXT:   %fp2si = fptosi double -4.750000e+00 to i32 => i32 -4
+; CHECK-NEXT:   %fp2ui = fptoui double 4.750000e+00 to i32 => i32 4
+; CHECK-NEXT:   %oob_ui = fptoui float -1.000000e+00 to i32 => poison
+; CHECK-NEXT:   %nan_si = fptosi float 0x7FF8000000000000 to i32 => poison
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/fp_cmp.ll b/llvm/test/tools/llubi/fp_cmp.ll
new file mode 100644
index 0000000000000..489a91f256e91
--- /dev/null
+++ b/llvm/test/tools/llubi/fp_cmp.ll
@@ -0,0 +1,25 @@
+; 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 void @main() {
+  %eq = fcmp oeq float 1.0, 1.0
+  %ne = fcmp one float 1.0, 2.0
+  %gt = fcmp ogt float 2.0, 1.0
+
+  %nan = fdiv float 0.0, 0.0
+  %ord_nan = fcmp oeq float %nan, 1.0
+  %uno_nan = fcmp uno float %nan, 1.0
+  %une_nan = fcmp une float %nan, 1.0
+
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %eq = fcmp oeq float 1.000000e+00, 1.000000e+00 => T
+; CHECK-NEXT:   %ne = fcmp one float 1.000000e+00, 2.000000e+00 => T
+; CHECK-NEXT:   %gt = fcmp ogt float 2.000000e+00, 1.000000e+00 => T
+; CHECK-NEXT:   %nan = fdiv float 0.000000e+00, 0.000000e+00 => float NaN
+; CHECK-NEXT:   %ord_nan = fcmp oeq float %nan, 1.000000e+00 => F
+; CHECK-NEXT:   %uno_nan = fcmp uno float %nan, 1.000000e+00 => T
+; CHECK-NEXT:   %une_nan = fcmp une float %nan, 1.000000e+00 => T
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/fp_denorm.ll b/llvm/test/tools/llubi/fp_denorm.ll
new file mode 100644
index 0000000000000..982c172baf6c7
--- /dev/null
+++ b/llvm/test/tools/llubi/fp_denorm.ll
@@ -0,0 +1,15 @@
+; 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 void @main() denormal_fpenv(positivezero|positivezero) {
+  %input_flush = fadd float 1.434929627468612680625899e-42, 0.0
+
+  %output_flush = fmul float 5.421010862427522170037264e-20, 5.421010862427522170037264e-20
+
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %input_flush = fadd float 0x3740000000000000, 0.000000e+00 => float 0.000000e+00
+; CHECK-NEXT:   %output_flush = fmul float 0x3BF0000000000000, 0x3BF0000000000000 => float 0.000000e+00
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/fp_fastmath.ll b/llvm/test/tools/llubi/fp_fastmath.ll
new file mode 100644
index 0000000000000..475f40d2b0457
--- /dev/null
+++ b/llvm/test/tools/llubi/fp_fastmath.ll
@@ -0,0 +1,18 @@
+; 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 void @main() {
+  %bad_nan = fdiv nnan float 0.0, 0.0
+
+  %bad_inf = fdiv ninf float 1.0, 0.0
+
+  %safe = fadd nnan ninf float 1.0, 2.0
+
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %bad_nan = fdiv nnan float 0.000000e+00, 0.000000e+00 => poison
+; CHECK-NEXT:   %bad_inf = fdiv ninf float 1.000000e+00, 0.000000e+00 => poison
+; CHECK-NEXT:   %safe = fadd nnan ninf float 1.000000e+00, 2.000000e+00 => float 3.000000e+00
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index 393391d52c89c..009f4aedeec2b 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -118,6 +118,29 @@ static AnyValue mulNoWrap(const APInt &LHS, const APInt &RHS, bool HasNSW,
   return Res;
 }
 
+static APFloat handleDenormal(APFloat Val,
+                              DenormalMode::DenormalModeKind Mode) {
+  if (!Val.isDenormal())
+    return Val;
+  if (Mode == DenormalMode::PositiveZero)
+    return APFloat::getZero(Val.getSemantics(), false);
+  if (Mode == DenormalMode::PreserveSign)
+    return APFloat::getZero(Val.getSemantics(), Val.isNegative());
+  // Default case for IEEE, Dynamic, and Invalid
+  return Val;
+}
+
+static std::optional<APFloat> validateFMF(APFloat Val, FastMathFlags FMF) {
+  // The returned std::nullopt indicates that a poison value is produced.
+  if (FMF.noNaNs() && Val.isNaN())
+    return std::nullopt;
+  if (FMF.noInfs() && Val.isInfinity())
+    return std::nullopt;
+  // Since nsz (no signed zeros) is an optimization hint, so it doesn't requires
+  // runtime modification to maintain semantic correctness.
+  return Val;
+}
+
 /// Instruction executor using the visitor pattern.
 /// Unlike the Context class that manages the global state,
 /// InstExecutor only maintains the state for call frames.
@@ -130,6 +153,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
   bool Status;
   Frame *CurrentFrame = nullptr;
   AnyValue None;
+  APFloat::roundingMode CurrentRoundingMode;
 
   void reportImmediateUB(StringRef Msg) {
     // Check if we have already reported an immediate UB.
@@ -187,6 +211,33 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
     });
   }
 
+  void visitFPUnOp(Instruction &I,
+                   function_ref<AnyValue(const APFloat &)> ScalarFn) {
+    FastMathFlags FMF = cast<FPMathOperator>(I).getFastMathFlags();
+    DenormalMode DenormMode = CurrentFrame->Func.getDenormalMode(
+        I.getType()->getScalarType()->getFltSemantics());
+
+    visitUnOp(I, [&](const AnyValue &Operand) -> AnyValue {
+      if (Operand.isPoison())
+        return AnyValue::poison();
+
+      // Flush input denormals
+      APFloat FInput = handleDenormal(Operand.asFloat(), DenormMode.Input);
+
+      APFloat Result = ScalarFn(FInput).asFloat();
+
+      // Flush output denormals
+      APFloat FResult = handleDenormal(Result, DenormMode.Output);
+
+      if (auto ValidateRes = validateFMF(FResult, FMF)) {
+        return *ValidateRes;
+      }
+
+      // A poison value is produced when FMFs are violated
+      return AnyValue::poison();
+    });
+  }
+
   AnyValue computeBinOp(
       Type *Ty, const AnyValue &LHS, const AnyValue &RHS,
       function_ref<AnyValue(const AnyValue &, const AnyValue &)> ScalarFn) {
@@ -219,6 +270,42 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
     });
   }
 
+  void visitFPBinOp(
+      Instruction &I,
+      function_ref<AnyValue(const APFloat &, const APFloat &)> ScalarFn) {
+    FastMathFlags FMF = cast<FPMathOperator>(I).getFastMathFlags();
+    DenormalMode DenormMode = CurrentFrame->Func.getDenormalMode(
+        I.getType()->getScalarType()->getFltSemantics());
+
+    visitBinOp(I, [&](const AnyValue &LHS, const AnyValue &RHS) -> AnyValue {
+      if (LHS.isPoison() || RHS.isPoison())
+        return AnyValue::poison();
+
+      if (auto ValidateRes = validateFMF(LHS.asFloat(), FMF); !ValidateRes) {
+        return AnyValue::poison();
+      }
+      if (auto ValidateRes = validateFMF(RHS.asFloat(), FMF); !ValidateRes) {
+        return AnyValue::poison();
+      }
+
+      // Flush input denormals
+      APFloat FLHS = handleDenormal(LHS.asFloat(), DenormMode.Input);
+      APFloat FRHS = handleDenormal(RHS.asFloat(), DenormMode.Input);
+
+      APFloat Result = ScalarFn(FLHS, FRHS).asFloat();
+
+      // Flush output denormals
+      APFloat FResult = handleDenormal(Result, DenormMode.Output);
+
+      if (auto ValidateRes = validateFMF(FResult, FMF)) {
+        return *ValidateRes;
+      }
+
+      // A poison value is produced when FMFs are violated
+      return AnyValue::poison();
+    });
+  }
+
   void jumpTo(Instruction &Terminator, BasicBlock *DestBB) {
     if (!Handler.onBBJump(Terminator, *DestBB)) {
       Status = false;
@@ -418,7 +505,8 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
 public:
   InstExecutor(Context &C, EventHandler &H, Function &F,
                ArrayRef<AnyValue> Args, AnyValue &RetVal)
-      : Ctx(C), DL(Ctx.getDataLayout()), Handler(H), Status(true) {
+      : Ctx(C), DL(Ctx.getDataLayout()), Handler(H), Status(true),
+        CurrentRoundingMode(APFloat::rmNearestTiesToEven) {
     CallStack.emplace_back(F, /*CallSite=*/nullptr, /*LastFrame=*/nullptr, Args,
                            RetVal, Ctx.getTLIImpl());
   }
@@ -767,6 +855,50 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
     });
   }
 
+  void visitFAdd(BinaryOperator &I) {
+    visitFPBinOp(I, [this](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
+      APFloat Res = LHS;
+      Res.add(RHS, CurrentRoundingMode);
+      return Res;
+    });
+  }
+
+  void visitFSub(BinaryOperator &I) {
+    visitFPBinOp(I, [this](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
+      APFloat Res = LHS;
+      Res.subtract(RHS, CurrentRoundingMode);
+      return Res;
+    });
+  }
+
+  void visitFMul(BinaryOperator &I) {
+    visitFPBinOp(I, [this](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
+      APFloat Res = LHS;
+      Res.multiply(RHS, CurrentRoundingMode);
+      return Res;
+    });
+  }
+
+  void visitFDiv(BinaryOperator &I) {
+    visitFPBinOp(I, [this](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
+      APFloat Res = LHS;
+      Res.divide(RHS, CurrentRoundingMode);
+      return Res;
+    });
+  }
+
+  void visitFRem(BinaryOperator &I) {
+    visitFPBinOp(I, [](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
+      APFloat Res = LHS;
+      Res.mod(RHS);
+      return Res;
+    });
+  }
+
+  void visitFNeg(UnaryOperator &I) {
+    visitFPUnOp(I, [](const APFloat &Operand) -> AnyValue { return -Operand; });
+  }
+
   void visitTruncInst(TruncInst &Trunc) {
     visitIntUnOp(Trunc, [&](const APInt &Operand) -> AnyValue {
       unsigned DestBW = Trunc.getType()->getScalarSizeInBits();
@@ -794,6 +926,77 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
     });
   }
 
+  void visitFPExtInst(FPExtInst &FPExt) { visitFPConvInst(FPExt); }
+
+  void visitFPTruncInst(FPTruncInst &FPTrunc) { visitFPConvInst(FPTrunc); }
+
+  void visitFPConvInst(Instruction &I) {
+    const fltSemantics &DstSem =
+        I.getType()->getScalarType()->getFltSemantics();
+
+    visitUnOp(I, [&](const AnyValue &Operand) -> AnyValue {
+      if (Operand.isPoison())
+        return AnyValue::poison();
+
+      APFloat Res = Operand.asFloat();
+      bool LosesInfo;
+      Res.convert(DstSem, CurrentRoundingMode, &LosesInfo);
+      return AnyValue(Res);
+    });
+  }
+
+  void visitFPToSIInst(FPToSIInst &FPToSI) {
+    visitFPToIntInst(FPToSI, /*IsUnsigned=*/false);
+  }
+
+  void visitFPToUIInst(FPToUIInst &FPToUI) {
+    visitFPToIntInst(FPToUI, /*IsUnsigned=*/true);
+  }
+
+  void visitFPToIntInst(Instruction &I, bool IsUnsigned) {
+    // Note: We DO NOT use CurrentRoundingMode here.
+    // Language specs require truncation towards zero for FP-to-Int conversions.
+    visitUnOp(I, [&](const AnyValue &Operand) -> AnyValue {
+      if (Operand.isPoison())
+        return AnyValue::poison();
+
+      APSInt Res(I.getType()->getScalarSizeInBits(), /*isUnsigned=*/IsUnsigned);
+      bool IsExact;
+      APFloat::opStatus Status = Operand.asFloat().convertToInteger(
+          Res, APFloat::rmTowardZero, &IsExact);
+
+      if (Status == APFloat::opInvalidOp)
+        return AnyValue::poison();
+
+      return AnyValue(Res);
+    });
+  }
+
+  void visitSIToFPInst(SIToFPInst &SIToFP) {
+    visitIntToFPInst(SIToFP, /*IsSigned=*/true);
+  }
+
+  void visitUIToFPInst(UIToFPInst &UIToFP) {
+    visitIntToFPInst(UIToFP, /*IsSigned=*/false);
+  }
+
+  void visitIntToFPInst(Instruction &I, bool IsSigned) {
+    const fltSemantics &DstSem =
+        I.getType()->getScalarType()->getFltSemantics();
+
+    visitUnOp(I, [&](const AnyValue &Operand) -> AnyValue {
+      if (Operand.isPoison())
+        return AnyValue::poison();
+
+      APFloat Res(DstSem);
+
+      Res.convertFromAPInt(Operand.asInteger(), /*IsSigned=*/IsSigned,
+                           CurrentRoundingMode);
+
+      return AnyValue(Res);
+    });
+  }
+
   void visitAnd(BinaryOperator &I) {
     visitIntBinOp(I, [](const APInt &LHS, const APInt &RHS) -> AnyValue {
       return LHS & RHS;
@@ -860,6 +1063,97 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
     });
   }
 
+  void visitFCmpInst(FCmpInst &I) {
+    DenormalMode DenormMode = CurrentFrame->Func.getDenormalMode(
+        I.getOperand(0)->getType()->getScalarType()->getFltSemantics());
+    FastMathFlags FMF = I.getFastMathFlags();
+
+    visitBinOp(I, [&](const AnyValue &LHS, const AnyValue &RHS) -> AnyValue {
+      if (LHS.isPoison() || RHS.isPoison()) {
+        return AnyValue::poison();
+      }
+
+      if (auto ValidateRes = validateFMF(LHS.asFloat(), FMF); !ValidateRes) {
+        return AnyValue::poison();
+      }
+      if (auto ValidateRes = validateFMF(RHS.asFloat(), FMF); !ValidateRes) {
+        return AnyValue::poison();
+      }
+
+      APFloat FLHS = handleDenormal(LHS.asFloat(), DenormMode.Input);
+      APFloat FRHS = handleDenormal(RHS.asFloat(), DenormMode.Input);
+
+      APFloat::cmpResult CmpResult = FLHS.compare(FRHS);
+
+      bool Result = false;
+
+      switch (I.getPredicate()) {
+      case FCmpInst::FCMP_FALSE:
+        Result = false;
+        break;
+      case FCmpInst::FCMP_OEQ:
+        Result = (CmpResult == APFloat::cmpEqual);
+        break;
+      case FCmpInst::FCMP_OGT:
+        Result = (CmpResult == APFloat::cmpGreaterThan);
+        break;
+      case FCmpInst::FCMP_OGE:
+        Result = (CmpResult == APFloat::cmpEqual ||
+                  CmpResult == APFloat::cmpGreaterThan);
+        break;
+      case FCmpInst::FCMP_OLT:
+        Result = (CmpResult == APFloat::cmpLessThan);
+        break;
+      case FCmpInst::FCMP_OLE:
+        Result = (CmpResult == APFloat::cmpEqual ||
+                  CmpResult == APFloat::cmpLessThan);
+        break;
+      case FCmpInst::FCMP_ONE:
+        Result = (CmpResult == APFloat::cmpGreaterThan ||
+                  CmpResult == APFloat::cmpLessThan);
+        break;
+      case FCmpInst::FCMP_ORD:
+        Result = (CmpResult != APFloat::cmpUnordered);
+        break;
+      case FCmpInst::FCMP_UNO:
+        Result = (CmpResult == APFloat::cmpUnordered);
+        break;
+      case FCmpInst::FCMP_UEQ:
+        Result = (CmpResult == APFloat::cmpEqual ||
+                  CmpResult == APFloat::cmpUnordered);
+        break;
+      case FCmpInst::FCMP_UGT:
+        Result = (CmpResult == APFloat::cmpGreaterThan ||
+                  CmpResult == APFloat::cmpUnordered);
+        break;
+      case FCmpInst::FCMP_UGE:
+        Result = (CmpResult == APFloat::cmpEqual ||
+                  CmpResult == APFloat::cmpGreaterThan ||
+                  CmpResult == APFloat::cmpUnordered);
+        break;
+      case FCmpInst::FCMP_ULT:
+        Result = (CmpResult == APFloat::cmpLessThan ||
+                  CmpResult == APFloat::cmpUnordered);
+        break;
+      case FCmpInst::FCMP_ULE:
+        Result = (CmpResult == APFloatBase::cmpEqual ||
+                  CmpResult == APFloat::cmpLessThan ||
+                  CmpResult == APFloat::cmpUnordered);
+        break;
+      case FCmpInst::FCMP_UNE:
+        Result = (CmpResult != APFloatBase::cmpEqual);
+        break;
+      case FCmpInst::FCMP_TRUE:
+        Result = true;
+        break;
+      default:
+        llvm_unreachable("Invalid FCmp predicate");
+      }
+
+      return AnyValue::boolean(Result);
+    });
+  }
+
   void visitSelect(SelectInst &SI) {
     // TODO: handle fast-math flags.
     if (SI.getCondition()->getType()->isIntegerTy(1)) {

>From de1a32af17e08b4c24bc69372272eabde531a31c Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhige_chen at outlook.com>
Date: Wed, 25 Mar 2026 16:36:27 +0800
Subject: [PATCH 02/20] [llubi] Handle fast-math flags in phi nodes and select

---
 llvm/test/tools/llubi/fp_phi_select.ll |  33 +++++++
 llvm/tools/llubi/lib/Interpreter.cpp   | 124 ++++++++++++++-----------
 2 files changed, 104 insertions(+), 53 deletions(-)
 create mode 100644 llvm/test/tools/llubi/fp_phi_select.ll

diff --git a/llvm/test/tools/llubi/fp_phi_select.ll b/llvm/test/tools/llubi/fp_phi_select.ll
new file mode 100644
index 0000000000000..9c0685a4d1cbc
--- /dev/null
+++ b/llvm/test/tools/llubi/fp_phi_select.ll
@@ -0,0 +1,33 @@
+; 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 void @main() {
+entry:
+  %nan = fdiv float 0.0, 0.0
+  %inf = fdiv float 1.0, 0.0
+
+  %safe_sel = select i1 true, float %nan, float 1.0
+  %bad_sel_nan = select nnan i1 true, float %nan, float 1.0
+  %bad_sel_inf = select ninf i1 false, float 1.0, float %inf
+
+  br label %phi.test
+
+phi.test:
+  %safe_phi = phi float [ %nan, %entry ]
+  %bad_phi_nan = phi nnan float [ %nan, %entry ]
+  %bad_phi_inf = phi ninf float [ %inf, %entry ]
+
+  ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %nan = fdiv float 0.000000e+00, 0.000000e+00 => float NaN
+; CHECK-NEXT:   %inf = fdiv float 1.000000e+00, 0.000000e+00 => float +Inf
+; CHECK-NEXT:   %safe_sel = select i1 true, float %nan, float 1.000000e+00 => float NaN
+; CHECK-NEXT:   %bad_sel_nan = select nnan i1 true, float %nan, float 1.000000e+00 => poison
+; CHECK-NEXT:   %bad_sel_inf = select ninf i1 false, float 1.000000e+00, float %inf => poison
+; CHECK-NEXT:   br label %phi.test jump to %phi.test
+; CHECK-NEXT:   %safe_phi = phi float [ %nan, %entry ] => float NaN
+; CHECK-NEXT:   %bad_phi_nan = phi nnan float [ %nan, %entry ] => poison
+; CHECK-NEXT:   %bad_phi_inf = phi ninf float [ %inf, %entry ] => poison
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index 009f4aedeec2b..e187cd348384f 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -130,12 +130,24 @@ static APFloat handleDenormal(APFloat Val,
   return Val;
 }
 
-static std::optional<APFloat> validateFMF(APFloat Val, FastMathFlags FMF) {
-  // The returned std::nullopt indicates that a poison value is produced.
-  if (FMF.noNaNs() && Val.isNaN())
-    return std::nullopt;
-  if (FMF.noInfs() && Val.isInfinity())
-    return std::nullopt;
+static AnyValue handleFMFFlags(AnyValue Val, FastMathFlags FMF) {
+  if (Val.isPoison() || Val.isNone())
+    return AnyValue::poison();
+
+  if (Val.isAggregate()) {
+    std::vector<AnyValue> ResVec;
+    ResVec.reserve(Val.asAggregate().size());
+    for (const auto &A : Val.asAggregate()) {
+      ResVec.push_back(handleFMFFlags(A, FMF));
+    }
+    return AnyValue(ResVec);
+  }
+
+  const APFloat &APVal = Val.asFloat();
+  if (FMF.noNaNs() && APVal.isNaN())
+    return AnyValue::poison();
+  if (FMF.noInfs() && APVal.isInfinity())
+    return AnyValue::poison();
   // Since nsz (no signed zeros) is an optimization hint, so it doesn't requires
   // runtime modification to maintain semantic correctness.
   return Val;
@@ -229,12 +241,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
       // Flush output denormals
       APFloat FResult = handleDenormal(Result, DenormMode.Output);
 
-      if (auto ValidateRes = validateFMF(FResult, FMF)) {
-        return *ValidateRes;
-      }
-
-      // A poison value is produced when FMFs are violated
-      return AnyValue::poison();
+      return handleFMFFlags(FResult, FMF);
     });
   }
 
@@ -281,11 +288,11 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
       if (LHS.isPoison() || RHS.isPoison())
         return AnyValue::poison();
 
-      if (auto ValidateRes = validateFMF(LHS.asFloat(), FMF); !ValidateRes) {
-        return AnyValue::poison();
+      if (auto ValidateRes = handleFMFFlags(LHS, FMF); ValidateRes.isPoison()) {
+        return ValidateRes;
       }
-      if (auto ValidateRes = validateFMF(RHS.asFloat(), FMF); !ValidateRes) {
-        return AnyValue::poison();
+      if (auto ValidateRes = handleFMFFlags(RHS, FMF); ValidateRes.isPoison()) {
+        return ValidateRes;
       }
 
       // Flush input denormals
@@ -297,12 +304,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
       // Flush output denormals
       APFloat FResult = handleDenormal(Result, DenormMode.Output);
 
-      if (auto ValidateRes = validateFMF(FResult, FMF)) {
-        return *ValidateRes;
-      }
-
-      // A poison value is produced when FMFs are violated
-      return AnyValue::poison();
+      return handleFMFFlags(FResult, FMF);
     });
   }
 
@@ -321,9 +323,16 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
     SmallVector<std::pair<PHINode *, AnyValue>> IncomingValues;
     PHINode *PHI = nullptr;
     while ((PHI = dyn_cast<PHINode>(CurrentFrame->PC))) {
-      Value *Incoming = PHI->getIncomingValueForBlock(From);
-      // TODO: handle fast-math flags.
-      IncomingValues.emplace_back(PHI, getValue(Incoming));
+      AnyValue IncomingVal = getValue(PHI->getIncomingValueForBlock(From));
+
+      // Fast-math flags validation
+      if (isa<FPMathOperator>(PHI)) {
+        FastMathFlags FMF = PHI->getFastMathFlags();
+        if (FMF.any())
+          IncomingVal = handleFMFFlags(std::move(IncomingVal), FMF);
+      }
+
+      IncomingValues.emplace_back(PHI, IncomingVal);
       ++CurrentFrame->PC;
     }
     for (auto &[K, V] : IncomingValues)
@@ -1073,11 +1082,11 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
         return AnyValue::poison();
       }
 
-      if (auto ValidateRes = validateFMF(LHS.asFloat(), FMF); !ValidateRes) {
-        return AnyValue::poison();
+      if (auto ValidateRes = handleFMFFlags(LHS, FMF); ValidateRes.isPoison()) {
+        return ValidateRes;
       }
-      if (auto ValidateRes = validateFMF(RHS.asFloat(), FMF); !ValidateRes) {
-        return AnyValue::poison();
+      if (auto ValidateRes = handleFMFFlags(RHS, FMF); ValidateRes.isPoison()) {
+        return ValidateRes;
       }
 
       APFloat FLHS = handleDenormal(LHS.asFloat(), DenormMode.Input);
@@ -1155,40 +1164,49 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
   }
 
   void visitSelect(SelectInst &SI) {
-    // TODO: handle fast-math flags.
+    AnyValue Res;
+
     if (SI.getCondition()->getType()->isIntegerTy(1)) {
       switch (getValue(SI.getCondition()).asBoolean()) {
       case BooleanKind::True:
-        setResult(SI, getValue(SI.getTrueValue()));
-        return;
-      case BooleanKind::False:
-        setResult(SI, getValue(SI.getFalseValue()));
-        return;
-      case BooleanKind::Poison:
-        setResult(SI, AnyValue::getPoisonValue(Ctx, SI.getType()));
-        return;
-      }
-    }
-
-    auto &Cond = getValue(SI.getCondition()).asAggregate();
-    auto &TV = getValue(SI.getTrueValue()).asAggregate();
-    auto &FV = getValue(SI.getFalseValue()).asAggregate();
-    std::vector<AnyValue> Res;
-    size_t Len = Cond.size();
-    Res.reserve(Len);
-    for (uint32_t I = 0; I != Len; ++I) {
-      switch (Cond[I].asBoolean()) {
-      case BooleanKind::True:
-        Res.push_back(TV[I]);
+        Res = getValue(SI.getTrueValue());
         break;
       case BooleanKind::False:
-        Res.push_back(FV[I]);
+        Res = getValue(SI.getFalseValue());
         break;
       case BooleanKind::Poison:
-        Res.push_back(AnyValue::poison());
+        Res = AnyValue::getPoisonValue(Ctx, SI.getType());
         break;
       }
+    } else {
+      auto &Cond = getValue(SI.getCondition()).asAggregate();
+      auto &TV = getValue(SI.getTrueValue()).asAggregate();
+      auto &FV = getValue(SI.getFalseValue()).asAggregate();
+      std::vector<AnyValue> ResVec;
+      size_t Len = Cond.size();
+      ResVec.reserve(Len);
+      for (uint32_t I = 0; I != Len; ++I) {
+        switch (Cond[I].asBoolean()) {
+        case BooleanKind::True:
+          ResVec.push_back(TV[I]);
+          break;
+        case BooleanKind::False:
+          ResVec.push_back(FV[I]);
+          break;
+        case BooleanKind::Poison:
+          ResVec.push_back(AnyValue::poison());
+          break;
+        }
+      }
+      Res = AnyValue(std::move(ResVec));
     }
+
+    // Handle fast-math flags
+    if (auto *FPMO = dyn_cast<FPMathOperator>(&SI)) {
+      if (FastMathFlags FMF = FPMO->getFastMathFlags(); FMF.any())
+        Res = handleFMFFlags(std::move(Res), FMF);
+    }
+
     setResult(SI, std::move(Res));
   }
 

>From 5c505b3d1b86d4a9b18d734ad1b36aa7aeef2b4f Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhigec_cpp at outlook.com>
Date: Fri, 27 Mar 2026 13:37:11 +0800
Subject: [PATCH 03/20] [llubi] Minor fixes to floating-point numbers

---
 llvm/tools/llubi/lib/Context.cpp     |   5 +
 llvm/tools/llubi/lib/Context.h       |   2 +
 llvm/tools/llubi/lib/Interpreter.cpp | 145 +++++++--------------------
 3 files changed, 46 insertions(+), 106 deletions(-)

diff --git a/llvm/tools/llubi/lib/Context.cpp b/llvm/tools/llubi/lib/Context.cpp
index 968ac4f561558..ca7105153a34c 100644
--- a/llvm/tools/llubi/lib/Context.cpp
+++ b/llvm/tools/llubi/lib/Context.cpp
@@ -499,6 +499,11 @@ uint64_t Context::getEffectiveTypeStoreSize(Type *Ty) {
   return getEffectiveTypeSize(DL.getTypeStoreSize(Ty));
 }
 
+bool Context::getRandomBool() {
+  // We use the lowest bit of the raw bits from RNG as the result:
+  return static_cast<bool>(Rng() & 1);
+}
+
 void MemoryObject::markAsFreed() {
   State = MemoryObjectState::Freed;
   Bytes.clear();
diff --git a/llvm/tools/llubi/lib/Context.h b/llvm/tools/llubi/lib/Context.h
index d1960b270d9bd..fcd6a3ad4485a 100644
--- a/llvm/tools/llubi/lib/Context.h
+++ b/llvm/tools/llubi/lib/Context.h
@@ -262,6 +262,8 @@ class Context {
   /// error occurred during execution.
   bool runFunction(Function &F, ArrayRef<AnyValue> Args, AnyValue &RetVal,
                    EventHandler &Handler);
+
+  bool getRandomBool();
 };
 
 } // namespace llvm::ubi
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index e187cd348384f..e743193630957 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -127,29 +127,8 @@ static APFloat handleDenormal(APFloat Val,
   if (Mode == DenormalMode::PreserveSign)
     return APFloat::getZero(Val.getSemantics(), Val.isNegative());
   // Default case for IEEE, Dynamic, and Invalid
-  return Val;
-}
-
-static AnyValue handleFMFFlags(AnyValue Val, FastMathFlags FMF) {
-  if (Val.isPoison() || Val.isNone())
-    return AnyValue::poison();
-
-  if (Val.isAggregate()) {
-    std::vector<AnyValue> ResVec;
-    ResVec.reserve(Val.asAggregate().size());
-    for (const auto &A : Val.asAggregate()) {
-      ResVec.push_back(handleFMFFlags(A, FMF));
-    }
-    return AnyValue(ResVec);
-  }
-
-  const APFloat &APVal = Val.asFloat();
-  if (FMF.noNaNs() && APVal.isNaN())
-    return AnyValue::poison();
-  if (FMF.noInfs() && APVal.isInfinity())
-    return AnyValue::poison();
-  // Since nsz (no signed zeros) is an optimization hint, so it doesn't requires
-  // runtime modification to maintain semantic correctness.
+  // Currently we treat Dynamic the same as IEEE, since we don't support
+  // changing the mode at this point.
   return Val;
 }
 
@@ -196,6 +175,31 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
     CurrentFrame->ValueMap.insert_or_assign(&I, std::move(V));
   }
 
+  AnyValue handleFMFFlags(AnyValue Val, FastMathFlags FMF) {
+    if (Val.isPoison())
+      return AnyValue::poison();
+
+    if (Val.isAggregate()) {
+      std::vector<AnyValue> ResVec;
+      ResVec.reserve(Val.asAggregate().size());
+      for (const auto &A : Val.asAggregate())
+        ResVec.push_back(handleFMFFlags(A, FMF));
+      return AnyValue(ResVec);
+    }
+
+    const APFloat &APVal = Val.asFloat();
+    if (FMF.noNaNs() && APVal.isNaN())
+      return AnyValue::poison();
+    if (FMF.noInfs() && APVal.isInfinity())
+      return AnyValue::poison();
+    // Non-deterministically flip the sign of the input.
+    if (FMF.noSignedZeros() && APVal.isZero()) {
+      return AnyValue(
+          APFloat::getZero(APVal.getSemantics(), Ctx.getRandomBool()));
+    }
+    return Val;
+  }
+
   AnyValue computeUnOp(Type *Ty, const AnyValue &Operand,
                        function_ref<AnyValue(const AnyValue &)> ScalarFn) {
     if (Ty->isVectorTy()) {
@@ -233,15 +237,13 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
       if (Operand.isPoison())
         return AnyValue::poison();
 
-      // Flush input denormals
-      APFloat FInput = handleDenormal(Operand.asFloat(), DenormMode.Input);
-
-      APFloat Result = ScalarFn(FInput).asFloat();
+      // We don't flush denormals here since the only floating-point unary
+      // operation is fneg. And fneg is specified as a bitwise operation which
+      // only flips the sign bit of the input.
 
-      // Flush output denormals
-      APFloat FResult = handleDenormal(Result, DenormMode.Output);
+      APFloat Result = ScalarFn(Operand.asFloat()).asFloat();
 
-      return handleFMFFlags(FResult, FMF);
+      return handleFMFFlags(Result, FMF);
     });
   }
 
@@ -288,12 +290,10 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
       if (LHS.isPoison() || RHS.isPoison())
         return AnyValue::poison();
 
-      if (auto ValidateRes = handleFMFFlags(LHS, FMF); ValidateRes.isPoison()) {
+      if (auto ValidateRes = handleFMFFlags(LHS, FMF); ValidateRes.isPoison())
         return ValidateRes;
-      }
-      if (auto ValidateRes = handleFMFFlags(RHS, FMF); ValidateRes.isPoison()) {
+      if (auto ValidateRes = handleFMFFlags(RHS, FMF); ValidateRes.isPoison())
         return ValidateRes;
-      }
 
       // Flush input denormals
       APFloat FLHS = handleDenormal(LHS.asFloat(), DenormMode.Input);
@@ -867,7 +867,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
   void visitFAdd(BinaryOperator &I) {
     visitFPBinOp(I, [this](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
       APFloat Res = LHS;
-      Res.add(RHS, CurrentRoundingMode);
+      Res.add(RHS, APFloat::rmNearestTiesToEven);
       return Res;
     });
   }
@@ -875,7 +875,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
   void visitFSub(BinaryOperator &I) {
     visitFPBinOp(I, [this](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
       APFloat Res = LHS;
-      Res.subtract(RHS, CurrentRoundingMode);
+      Res.subtract(RHS, APFloat::rmNearestTiesToEven);
       return Res;
     });
   }
@@ -883,7 +883,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
   void visitFMul(BinaryOperator &I) {
     visitFPBinOp(I, [this](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
       APFloat Res = LHS;
-      Res.multiply(RHS, CurrentRoundingMode);
+      Res.multiply(RHS, APFloat::rmNearestTiesToEven);
       return Res;
     });
   }
@@ -891,7 +891,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
   void visitFDiv(BinaryOperator &I) {
     visitFPBinOp(I, [this](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
       APFloat Res = LHS;
-      Res.divide(RHS, CurrentRoundingMode);
+      Res.divide(RHS, APFloat::rmNearestTiesToEven);
       return Res;
     });
   }
@@ -1092,74 +1092,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
       APFloat FLHS = handleDenormal(LHS.asFloat(), DenormMode.Input);
       APFloat FRHS = handleDenormal(RHS.asFloat(), DenormMode.Input);
 
-      APFloat::cmpResult CmpResult = FLHS.compare(FRHS);
-
-      bool Result = false;
-
-      switch (I.getPredicate()) {
-      case FCmpInst::FCMP_FALSE:
-        Result = false;
-        break;
-      case FCmpInst::FCMP_OEQ:
-        Result = (CmpResult == APFloat::cmpEqual);
-        break;
-      case FCmpInst::FCMP_OGT:
-        Result = (CmpResult == APFloat::cmpGreaterThan);
-        break;
-      case FCmpInst::FCMP_OGE:
-        Result = (CmpResult == APFloat::cmpEqual ||
-                  CmpResult == APFloat::cmpGreaterThan);
-        break;
-      case FCmpInst::FCMP_OLT:
-        Result = (CmpResult == APFloat::cmpLessThan);
-        break;
-      case FCmpInst::FCMP_OLE:
-        Result = (CmpResult == APFloat::cmpEqual ||
-                  CmpResult == APFloat::cmpLessThan);
-        break;
-      case FCmpInst::FCMP_ONE:
-        Result = (CmpResult == APFloat::cmpGreaterThan ||
-                  CmpResult == APFloat::cmpLessThan);
-        break;
-      case FCmpInst::FCMP_ORD:
-        Result = (CmpResult != APFloat::cmpUnordered);
-        break;
-      case FCmpInst::FCMP_UNO:
-        Result = (CmpResult == APFloat::cmpUnordered);
-        break;
-      case FCmpInst::FCMP_UEQ:
-        Result = (CmpResult == APFloat::cmpEqual ||
-                  CmpResult == APFloat::cmpUnordered);
-        break;
-      case FCmpInst::FCMP_UGT:
-        Result = (CmpResult == APFloat::cmpGreaterThan ||
-                  CmpResult == APFloat::cmpUnordered);
-        break;
-      case FCmpInst::FCMP_UGE:
-        Result = (CmpResult == APFloat::cmpEqual ||
-                  CmpResult == APFloat::cmpGreaterThan ||
-                  CmpResult == APFloat::cmpUnordered);
-        break;
-      case FCmpInst::FCMP_ULT:
-        Result = (CmpResult == APFloat::cmpLessThan ||
-                  CmpResult == APFloat::cmpUnordered);
-        break;
-      case FCmpInst::FCMP_ULE:
-        Result = (CmpResult == APFloatBase::cmpEqual ||
-                  CmpResult == APFloat::cmpLessThan ||
-                  CmpResult == APFloat::cmpUnordered);
-        break;
-      case FCmpInst::FCMP_UNE:
-        Result = (CmpResult != APFloatBase::cmpEqual);
-        break;
-      case FCmpInst::FCMP_TRUE:
-        Result = true;
-        break;
-      default:
-        llvm_unreachable("Invalid FCmp predicate");
-      }
-
-      return AnyValue::boolean(Result);
+      return AnyValue::boolean(FCmpInst::compare(FLHS, FRHS, I.getPredicate()));
     });
   }
 

>From 1d1d54e6754f5980a515e4e0304dfe69f4e91408 Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhigec_cpp at outlook.com>
Date: Fri, 27 Mar 2026 14:17:14 +0800
Subject: [PATCH 04/20] [llubi] NaN payload propagation for floating-point
 operations

---
 llvm/tools/llubi/lib/Context.h       |  1 +
 llvm/tools/llubi/lib/Interpreter.cpp | 63 +++++++++++++++++++++++++---
 2 files changed, 58 insertions(+), 6 deletions(-)

diff --git a/llvm/tools/llubi/lib/Context.h b/llvm/tools/llubi/lib/Context.h
index fcd6a3ad4485a..68deef1ef5951 100644
--- a/llvm/tools/llubi/lib/Context.h
+++ b/llvm/tools/llubi/lib/Context.h
@@ -202,6 +202,7 @@ class Context {
   uint32_t getMaxStackDepth() const { return MaxStackDepth; }
   void setUndefValueBehavior(UndefValueBehavior UB) { UndefBehavior = UB; }
   void reseed(uint32_t Seed) { Rng.seed(Seed); }
+  std::mt19937_64 &getRng() { return Rng; }
 
   LLVMContext &getContext() const { return Ctx; }
   const DataLayout &getDataLayout() const { return DL; }
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index e743193630957..2b885b45df022 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -299,12 +299,63 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
       APFloat FLHS = handleDenormal(LHS.asFloat(), DenormMode.Input);
       APFloat FRHS = handleDenormal(RHS.asFloat(), DenormMode.Input);
 
-      APFloat Result = ScalarFn(FLHS, FRHS).asFloat();
-
-      // Flush output denormals
-      APFloat FResult = handleDenormal(Result, DenormMode.Output);
-
-      return handleFMFFlags(FResult, FMF);
+      APFloat RawResult = ScalarFn(FLHS, FRHS).asFloat();
+
+      // Flush output denormals and handle fast-math flags.
+      AnyValue FResult =
+          handleFMFFlags(handleDenormal(RawResult, DenormMode.Output), FMF);
+
+      if (FResult.isPoison())
+        return FResult;
+
+      APFloat Result = FResult.asFloat();
+      // NaN payload propagation
+      if (Result.isNaN()) {
+        // We non-deterministically choose from the four cases as specified in
+        // the language reference. The sign is also non-deterministic
+        const int8_t Choice = static_cast<int8_t>(Ctx.getRng()() % 4);
+        const bool Sign = Ctx.getRandomBool();
+        if (Choice == 0) {
+          // Preferred NaN: the quiet bit is set and the payload is all-zero
+          return APFloat::getQNaN(Result.getSemantics(), Sign);
+        }
+        if (Choice == 1) {
+          // Quieting NaN propagation: the quiet bit is set and the payload is
+          // copied from any input operand that is a NaN. We implement this by
+          // directly set the quiet bit of the input NaN, and
+          // non-deterministically flip its sign bit
+          auto QuietNaN = [&](APFloat &Input) {
+            APFloat Quieted = Input.makeQuiet();
+            if (Sign)
+              Quieted.changeSign();
+            return Quieted;
+          };
+          if (FLHS.isNaN())
+            return QuietNaN(FLHS);
+          if (FRHS.isNaN())
+            return QuietNaN(FRHS);
+        }
+        if (Choice == 2) {
+          // Unchanged NaN propagation: the quiet bit and payload are copied
+          // from any input operand that is a NaN
+          auto FlipSign = [&](APFloat &Input) {
+            if (Sign)
+              Input.changeSign();
+            return Input;
+          };
+          if (FLHS.isNaN())
+            return FlipSign(FLHS);
+          if (FRHS.isNaN())
+            return FlipSign(FRHS);
+        }
+        if (Choice == 3) {
+          // Target-specific NaN: the quiet bit is set and the payload is picked
+          // from a target-specific set of "extra" possible NaN payloads. We
+          // approximate this by filling the payload with random values.
+          auto Payload = APInt(64, Ctx.getRng()());
+          return APFloat::getQNaN(Result.getSemantics(), Sign, &Payload);
+        }
+      }
     });
   }
 

>From 2878ac0a40f723db299d6acab232b805b9738af9 Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhigec_cpp at outlook.com>
Date: Fri, 27 Mar 2026 14:19:28 +0800
Subject: [PATCH 05/20] [llubi] Small fix to visitBinOp

---
 llvm/tools/llubi/lib/Interpreter.cpp | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index 2b885b45df022..11c38afa02a59 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -334,6 +334,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
             return QuietNaN(FLHS);
           if (FRHS.isNaN())
             return QuietNaN(FRHS);
+          return QuietNaN(Result);
         }
         if (Choice == 2) {
           // Unchanged NaN propagation: the quiet bit and payload are copied
@@ -347,6 +348,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
             return FlipSign(FLHS);
           if (FRHS.isNaN())
             return FlipSign(FRHS);
+          return FlipSign(Result);
         }
         if (Choice == 3) {
           // Target-specific NaN: the quiet bit is set and the payload is picked

>From c55d92cb731d9d60c794ff85abda443425af8d72 Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhigec_cpp at outlook.com>
Date: Fri, 27 Mar 2026 14:21:28 +0800
Subject: [PATCH 06/20] [llubi] Small fix to visitBinOp

---
 llvm/tools/llubi/lib/Interpreter.cpp | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index 11c38afa02a59..f9de9c8ca463e 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -358,6 +358,8 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
           return APFloat::getQNaN(Result.getSemantics(), Sign, &Payload);
         }
       }
+
+      return Result;
     });
   }
 

>From b967a7bb8c6f201e4f14f7c3b6f16c6e2fe7ef70 Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhigec_cpp at outlook.com>
Date: Fri, 27 Mar 2026 14:51:18 +0800
Subject: [PATCH 07/20] [llubi] Add new CLI option to control the behavior of
 NaN propagation

---
 llvm/tools/llubi/lib/Context.cpp     | 10 +++++++++-
 llvm/tools/llubi/lib/Context.h       | 22 +++++++++++++++++++++-
 llvm/tools/llubi/lib/Interpreter.cpp | 28 ++++++++++++++--------------
 llvm/tools/llubi/llubi.cpp           | 21 +++++++++++++++++++++
 4 files changed, 65 insertions(+), 16 deletions(-)

diff --git a/llvm/tools/llubi/lib/Context.cpp b/llvm/tools/llubi/lib/Context.cpp
index ca7105153a34c..2f2788f7b50fb 100644
--- a/llvm/tools/llubi/lib/Context.cpp
+++ b/llvm/tools/llubi/lib/Context.cpp
@@ -501,7 +501,15 @@ uint64_t Context::getEffectiveTypeStoreSize(Type *Ty) {
 
 bool Context::getRandomBool() {
   // We use the lowest bit of the raw bits from RNG as the result:
-  return static_cast<bool>(Rng() & 1);
+  if (UndefBehavior == UndefValueBehavior::NonDeterministic)
+    return static_cast<bool>(Rng() & 1);
+  return false;
+}
+
+uint64_t Context::getRandomUInt64() {
+  if (UndefBehavior == UndefValueBehavior::NonDeterministic)
+    return Rng();
+  return 0;
 }
 
 void MemoryObject::markAsFreed() {
diff --git a/llvm/tools/llubi/lib/Context.h b/llvm/tools/llubi/lib/Context.h
index 68deef1ef5951..28a4583dace1d 100644
--- a/llvm/tools/llubi/lib/Context.h
+++ b/llvm/tools/llubi/lib/Context.h
@@ -47,6 +47,19 @@ enum class UndefValueBehavior {
   Zero,             // All uses of the undef value yield zero.
 };
 
+enum class NaNPropagationBehavior {
+  NonDeterministic, // Non-deterministically choose from the following 4 cases
+  PreferredNaN,     // The quiet bit is set and the payload is all-zero
+  QuietingNaN,  // The quiet bit is set and the payload is copied from any input
+                // operand that is a NaN
+  UnchangedNaN, // The quiet bit and payload are copied from any input operand
+                // that is a NaN
+  TargetSpecificNaN // The quiet bit is set and the payload is picked from a
+                    // target-specific set of “extra” possible NaN payloads,
+                    // currently we implement it by filling payload with random
+                    // values
+};
+
 class MemoryObject : public RefCountedBase<MemoryObject> {
   uint64_t Address;
   uint64_t Size;
@@ -149,6 +162,7 @@ class Context {
   uint32_t MaxSteps = 0;
   uint32_t MaxStackDepth = 256;
   UndefValueBehavior UndefBehavior = UndefValueBehavior::NonDeterministic;
+  NaNPropagationBehavior NaNBehavior = NaNPropagationBehavior::NonDeterministic;
 
   std::mt19937_64 Rng;
 
@@ -200,9 +214,14 @@ class Context {
   uint32_t getVScale() const { return VScale; }
   uint32_t getMaxSteps() const { return MaxSteps; }
   uint32_t getMaxStackDepth() const { return MaxStackDepth; }
+  NaNPropagationBehavior getNaNPropagationBehavior() const {
+    return NaNBehavior;
+  }
   void setUndefValueBehavior(UndefValueBehavior UB) { UndefBehavior = UB; }
+  void setNaNPropagationBehavior(NaNPropagationBehavior NaNBehav) {
+    NaNBehavior = NaNBehav;
+  }
   void reseed(uint32_t Seed) { Rng.seed(Seed); }
-  std::mt19937_64 &getRng() { return Rng; }
 
   LLVMContext &getContext() const { return Ctx; }
   const DataLayout &getDataLayout() const { return DL; }
@@ -265,6 +284,7 @@ class Context {
                    EventHandler &Handler);
 
   bool getRandomBool();
+  uint64_t getRandomUInt64();
 };
 
 } // namespace llvm::ubi
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index f9de9c8ca463e..6433e41eb1c53 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -230,8 +230,6 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
   void visitFPUnOp(Instruction &I,
                    function_ref<AnyValue(const APFloat &)> ScalarFn) {
     FastMathFlags FMF = cast<FPMathOperator>(I).getFastMathFlags();
-    DenormalMode DenormMode = CurrentFrame->Func.getDenormalMode(
-        I.getType()->getScalarType()->getFltSemantics());
 
     visitUnOp(I, [&](const AnyValue &Operand) -> AnyValue {
       if (Operand.isPoison())
@@ -311,15 +309,17 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
       APFloat Result = FResult.asFloat();
       // NaN payload propagation
       if (Result.isNaN()) {
-        // We non-deterministically choose from the four cases as specified in
-        // the language reference. The sign is also non-deterministic
-        const int8_t Choice = static_cast<int8_t>(Ctx.getRng()() % 4);
+        NaNPropagationBehavior Choice = Ctx.getNaNPropagationBehavior();
+        if (Choice == NaNPropagationBehavior::NonDeterministic) {
+          uint64_t NonDetChoice = Ctx.getRandomUInt64() % 4 + 1;
+          Choice = static_cast<NaNPropagationBehavior>(NonDetChoice);
+        }
         const bool Sign = Ctx.getRandomBool();
-        if (Choice == 0) {
+        if (Choice == NaNPropagationBehavior::PreferredNaN) {
           // Preferred NaN: the quiet bit is set and the payload is all-zero
           return APFloat::getQNaN(Result.getSemantics(), Sign);
         }
-        if (Choice == 1) {
+        if (Choice == NaNPropagationBehavior::QuietingNaN) {
           // Quieting NaN propagation: the quiet bit is set and the payload is
           // copied from any input operand that is a NaN. We implement this by
           // directly set the quiet bit of the input NaN, and
@@ -336,7 +336,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
             return QuietNaN(FRHS);
           return QuietNaN(Result);
         }
-        if (Choice == 2) {
+        if (Choice == NaNPropagationBehavior::UnchangedNaN) {
           // Unchanged NaN propagation: the quiet bit and payload are copied
           // from any input operand that is a NaN
           auto FlipSign = [&](APFloat &Input) {
@@ -350,11 +350,11 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
             return FlipSign(FRHS);
           return FlipSign(Result);
         }
-        if (Choice == 3) {
+        if (Choice == NaNPropagationBehavior::TargetSpecificNaN) {
           // Target-specific NaN: the quiet bit is set and the payload is picked
           // from a target-specific set of "extra" possible NaN payloads. We
           // approximate this by filling the payload with random values.
-          auto Payload = APInt(64, Ctx.getRng()());
+          auto Payload = APInt(64, Ctx.getRandomUInt64());
           return APFloat::getQNaN(Result.getSemantics(), Sign, &Payload);
         }
       }
@@ -920,7 +920,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
   }
 
   void visitFAdd(BinaryOperator &I) {
-    visitFPBinOp(I, [this](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
+    visitFPBinOp(I, [](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
       APFloat Res = LHS;
       Res.add(RHS, APFloat::rmNearestTiesToEven);
       return Res;
@@ -928,7 +928,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
   }
 
   void visitFSub(BinaryOperator &I) {
-    visitFPBinOp(I, [this](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
+    visitFPBinOp(I, [](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
       APFloat Res = LHS;
       Res.subtract(RHS, APFloat::rmNearestTiesToEven);
       return Res;
@@ -936,7 +936,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
   }
 
   void visitFMul(BinaryOperator &I) {
-    visitFPBinOp(I, [this](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
+    visitFPBinOp(I, [](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
       APFloat Res = LHS;
       Res.multiply(RHS, APFloat::rmNearestTiesToEven);
       return Res;
@@ -944,7 +944,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
   }
 
   void visitFDiv(BinaryOperator &I) {
-    visitFPBinOp(I, [this](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
+    visitFPBinOp(I, [](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
       APFloat Res = LHS;
       Res.divide(RHS, APFloat::rmNearestTiesToEven);
       return Res;
diff --git a/llvm/tools/llubi/llubi.cpp b/llvm/tools/llubi/llubi.cpp
index de76a7e64c27b..4200c9246e8d8 100644
--- a/llvm/tools/llubi/llubi.cpp
+++ b/llvm/tools/llubi/llubi.cpp
@@ -87,6 +87,26 @@ cl::opt<ubi::UndefValueBehavior> UndefBehavior(
                clEnumVal(ubi::UndefValueBehavior::Zero,
                          "All uses of an uninitialized byte yield zero.")));
 
+cl::opt<ubi::NaNPropagationBehavior> NaNPropagationBehavior(
+    "", cl::desc("Choose NaN propagation behavior:"),
+    cl::values(
+        clEnumVal(ubi::NaNPropagationBehavior::NonDeterministic,
+                  "Non-deterministically choose from 4 cases as specified by "
+                  "language reference."),
+        clEnumVal(ubi::NaNPropagationBehavior::PreferredNaN,
+                  "The quiet bit is set and the payload is all-zero."),
+        clEnumVal(
+            ubi::NaNPropagationBehavior::QuietingNaN,
+            "The quiet bit is set and the payload is copied from any input"
+            "operand that is a NaN."),
+        clEnumVal(ubi::NaNPropagationBehavior::UnchangedNaN,
+                  "The quiet bit and payload are copied from any input operand"
+                  "that is a NaN"),
+        clEnumVal(ubi::NaNPropagationBehavior::TargetSpecificNaN,
+                  "The quiet bit is set and the payload is picked from a"
+                  "target-specific set of “extra” possible NaN payloads."
+                  "Implemented by filling payload with random values")));
+
 class VerboseEventHandler : public ubi::EventHandler {
 public:
   bool onInstructionExecuted(Instruction &I,
@@ -178,6 +198,7 @@ int main(int argc, char **argv) {
   Ctx.setMaxSteps(MaxSteps);
   Ctx.setMaxStackDepth(MaxStackDepth);
   Ctx.setUndefValueBehavior(UndefBehavior);
+  Ctx.setNaNPropagationBehavior(NaNPropagationBehavior);
   Ctx.reseed(Seed);
 
   if (!Ctx.initGlobalValues()) {

>From ddceccdfc39000bfc069868d2b19ea155549c87d Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhigec_cpp at outlook.com>
Date: Fri, 27 Mar 2026 15:15:45 +0800
Subject: [PATCH 08/20] [llubi] Update test cases

---
 llvm/test/tools/llubi/fp_arith.ll      | 2 +-
 llvm/test/tools/llubi/fp_cmp.ll        | 2 +-
 llvm/test/tools/llubi/fp_phi_select.ll | 6 +++---
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/llvm/test/tools/llubi/fp_arith.ll b/llvm/test/tools/llubi/fp_arith.ll
index 2fd09ac53b1a2..69b091844ce8d 100644
--- a/llvm/test/tools/llubi/fp_arith.ll
+++ b/llvm/test/tools/llubi/fp_arith.ll
@@ -28,6 +28,6 @@ define void @main() {
 ; CHECK-NEXT:   %p1 = fadd float 1.000000e+00, poison => poison
 ; CHECK-NEXT:   %p2 = fneg float poison => poison
 ; CHECK-NEXT:   %inf = fdiv float 1.000000e+00, 0.000000e+00 => float +Inf
-; CHECK-NEXT:   %nan = fdiv float 0.000000e+00, 0.000000e+00 => float NaN
+; CHECK-NEXT:   %nan = fdiv float 0.000000e+00, 0.000000e+00 => float 0xFFC00000
 ; CHECK-NEXT:   ret void
 ; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/fp_cmp.ll b/llvm/test/tools/llubi/fp_cmp.ll
index 489a91f256e91..da6d7d550479b 100644
--- a/llvm/test/tools/llubi/fp_cmp.ll
+++ b/llvm/test/tools/llubi/fp_cmp.ll
@@ -17,7 +17,7 @@ define void @main() {
 ; CHECK-NEXT:   %eq = fcmp oeq float 1.000000e+00, 1.000000e+00 => T
 ; CHECK-NEXT:   %ne = fcmp one float 1.000000e+00, 2.000000e+00 => T
 ; CHECK-NEXT:   %gt = fcmp ogt float 2.000000e+00, 1.000000e+00 => T
-; CHECK-NEXT:   %nan = fdiv float 0.000000e+00, 0.000000e+00 => float NaN
+; CHECK-NEXT:   %nan = fdiv float 0.000000e+00, 0.000000e+00 => float 0xFFC00000
 ; CHECK-NEXT:   %ord_nan = fcmp oeq float %nan, 1.000000e+00 => F
 ; CHECK-NEXT:   %uno_nan = fcmp uno float %nan, 1.000000e+00 => T
 ; CHECK-NEXT:   %une_nan = fcmp une float %nan, 1.000000e+00 => T
diff --git a/llvm/test/tools/llubi/fp_phi_select.ll b/llvm/test/tools/llubi/fp_phi_select.ll
index 9c0685a4d1cbc..d3492e84ea880 100644
--- a/llvm/test/tools/llubi/fp_phi_select.ll
+++ b/llvm/test/tools/llubi/fp_phi_select.ll
@@ -20,13 +20,13 @@ phi.test:
   ret void
 }
 ; CHECK: Entering function: main
-; CHECK-NEXT:   %nan = fdiv float 0.000000e+00, 0.000000e+00 => float NaN
+; CHECK-NEXT:   %nan = fdiv float 0.000000e+00, 0.000000e+00 => float 0xFFC00000
 ; CHECK-NEXT:   %inf = fdiv float 1.000000e+00, 0.000000e+00 => float +Inf
-; CHECK-NEXT:   %safe_sel = select i1 true, float %nan, float 1.000000e+00 => float NaN
+; CHECK-NEXT:   %safe_sel = select i1 true, float %nan, float 1.000000e+00 => float 0xFFC00000
 ; CHECK-NEXT:   %bad_sel_nan = select nnan i1 true, float %nan, float 1.000000e+00 => poison
 ; CHECK-NEXT:   %bad_sel_inf = select ninf i1 false, float 1.000000e+00, float %inf => poison
 ; CHECK-NEXT:   br label %phi.test jump to %phi.test
-; CHECK-NEXT:   %safe_phi = phi float [ %nan, %entry ] => float NaN
+; CHECK-NEXT:   %safe_phi = phi float [ %nan, %entry ] => float 0xFFC00000
 ; CHECK-NEXT:   %bad_phi_nan = phi nnan float [ %nan, %entry ] => poison
 ; CHECK-NEXT:   %bad_phi_inf = phi ninf float [ %inf, %entry ] => poison
 ; CHECK-NEXT:   ret void

>From 2f8b3057d0dcfc1e32e2fe5b116e0825eeb7f5eb Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhigec_cpp at outlook.com>
Date: Fri, 27 Mar 2026 15:21:04 +0800
Subject: [PATCH 09/20] [llubi] Non-deterministically flushing the output FP
 numbers

---
 llvm/tools/llubi/lib/Interpreter.cpp | 38 ++++++++++++++++------------
 1 file changed, 22 insertions(+), 16 deletions(-)

diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index 6433e41eb1c53..c3568f687174f 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -118,20 +118,6 @@ static AnyValue mulNoWrap(const APInt &LHS, const APInt &RHS, bool HasNSW,
   return Res;
 }
 
-static APFloat handleDenormal(APFloat Val,
-                              DenormalMode::DenormalModeKind Mode) {
-  if (!Val.isDenormal())
-    return Val;
-  if (Mode == DenormalMode::PositiveZero)
-    return APFloat::getZero(Val.getSemantics(), false);
-  if (Mode == DenormalMode::PreserveSign)
-    return APFloat::getZero(Val.getSemantics(), Val.isNegative());
-  // Default case for IEEE, Dynamic, and Invalid
-  // Currently we treat Dynamic the same as IEEE, since we don't support
-  // changing the mode at this point.
-  return Val;
-}
-
 /// Instruction executor using the visitor pattern.
 /// Unlike the Context class that manages the global state,
 /// InstExecutor only maintains the state for call frames.
@@ -175,6 +161,26 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
     CurrentFrame->ValueMap.insert_or_assign(&I, std::move(V));
   }
 
+  APFloat handleDenormal(APFloat Val, DenormalMode::DenormalModeKind Mode,
+                         bool NonDet = false) {
+    if (!Val.isDenormal())
+      return Val;
+    if (NonDet) {
+      // Non-deterministically choose between flushing or preserving the
+      // denormal value.
+      if (Ctx.getRandomBool())
+        return Val;
+    }
+    if (Mode == DenormalMode::PositiveZero)
+      return APFloat::getZero(Val.getSemantics(), false);
+    if (Mode == DenormalMode::PreserveSign)
+      return APFloat::getZero(Val.getSemantics(), Val.isNegative());
+    // Default case for IEEE, Dynamic, and Invalid
+    // Currently we treat Dynamic the same as IEEE, since we don't support
+    // changing the mode at this point.
+    return Val;
+  }
+
   AnyValue handleFMFFlags(AnyValue Val, FastMathFlags FMF) {
     if (Val.isPoison())
       return AnyValue::poison();
@@ -300,8 +306,8 @@ class InstExecutor : public InstVisitor<InstExecutor, void> {
       APFloat RawResult = ScalarFn(FLHS, FRHS).asFloat();
 
       // Flush output denormals and handle fast-math flags.
-      AnyValue FResult =
-          handleFMFFlags(handleDenormal(RawResult, DenormMode.Output), FMF);
+      AnyValue FResult = handleFMFFlags(
+          handleDenormal(RawResult, DenormMode.Output, true), FMF);
 
       if (FResult.isPoison())
         return FResult;

>From e5cd154a2e678bd2d80553a44f59c185e84f28f3 Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhige_chen at outlook.com>
Date: Thu, 2 Apr 2026 14:06:27 +0800
Subject: [PATCH 10/20] [llubi] Some FP-related fixes

---
 llvm/tools/llubi/lib/Interpreter.cpp | 49 ++++++++++++++++++----------
 1 file changed, 31 insertions(+), 18 deletions(-)

diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index ee711a59fc025..48ebd53d2c504 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -155,7 +155,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   }
 
   void visitFPUnOp(Instruction &I,
-                   function_ref<AnyValue(const APFloat &)> ScalarFn) {
+                   function_ref<APFloat(const APFloat &)> ScalarFn) {
     FastMathFlags FMF = cast<FPMathOperator>(I).getFastMathFlags();
 
     visitUnOp(I, [&](const AnyValue &Operand) -> AnyValue {
@@ -166,7 +166,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
       // operation is fneg. And fneg is specified as a bitwise operation which
       // only flips the sign bit of the input.
 
-      APFloat Result = ScalarFn(Operand.asFloat()).asFloat();
+      APFloat Result = ScalarFn(Operand.asFloat());
 
       return handleFMFFlags(Result, FMF);
     });
@@ -206,10 +206,9 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
 
   void visitFPBinOp(
       Instruction &I,
-      function_ref<AnyValue(const APFloat &, const APFloat &)> ScalarFn) {
+      function_ref<APFloat(const APFloat &, const APFloat &)> ScalarFn) {
     FastMathFlags FMF = cast<FPMathOperator>(I).getFastMathFlags();
-    DenormalMode DenormMode = CurrentFrame->Func.getDenormalMode(
-        I.getType()->getScalarType()->getFltSemantics());
+    DenormalMode DenormMode = getCurrentDenormalMode(I);
 
     visitBinOp(I, [&](const AnyValue &LHS, const AnyValue &RHS) -> AnyValue {
       if (LHS.isPoison() || RHS.isPoison())
@@ -224,7 +223,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
       APFloat FLHS = handleDenormal(LHS.asFloat(), DenormMode.Input);
       APFloat FRHS = handleDenormal(RHS.asFloat(), DenormMode.Input);
 
-      APFloat RawResult = ScalarFn(FLHS, FRHS).asFloat();
+      APFloat RawResult = ScalarFn(FLHS, FRHS);
 
       // Flush output denormals and handle fast-math flags.
       AnyValue FResult = handleFMFFlags(
@@ -281,7 +280,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
           // Target-specific NaN: the quiet bit is set and the payload is picked
           // from a target-specific set of "extra" possible NaN payloads. We
           // approximate this by filling the payload with random values.
-          auto Payload = APInt(64, Ctx.getRandomUInt64());
+          APInt Payload(64, Ctx.getRandomUInt64());
           return APFloat::getQNaN(Result.getSemantics(), Sign, &Payload);
         }
       }
@@ -411,6 +410,11 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     return IdxInt.sext(IndexBitWidth);
   }
 
+  DenormalMode getCurrentDenormalMode(Instruction &I) {
+    return CurrentFrame->Func.getDenormalMode(
+        I.getType()->getScalarType()->getFltSemantics());
+  }
+
 public:
   InstExecutor(Context &C, EventHandler &H, Function &F,
                ArrayRef<AnyValue> Args, AnyValue &RetVal)
@@ -765,7 +769,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   }
 
   void visitFAdd(BinaryOperator &I) {
-    visitFPBinOp(I, [](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
+    visitFPBinOp(I, [](const APFloat &LHS, const APFloat &RHS) -> APFloat {
       APFloat Res = LHS;
       Res.add(RHS, APFloat::rmNearestTiesToEven);
       return Res;
@@ -773,7 +777,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   }
 
   void visitFSub(BinaryOperator &I) {
-    visitFPBinOp(I, [](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
+    visitFPBinOp(I, [](const APFloat &LHS, const APFloat &RHS) -> APFloat {
       APFloat Res = LHS;
       Res.subtract(RHS, APFloat::rmNearestTiesToEven);
       return Res;
@@ -781,7 +785,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   }
 
   void visitFMul(BinaryOperator &I) {
-    visitFPBinOp(I, [](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
+    visitFPBinOp(I, [](const APFloat &LHS, const APFloat &RHS) -> APFloat {
       APFloat Res = LHS;
       Res.multiply(RHS, APFloat::rmNearestTiesToEven);
       return Res;
@@ -789,7 +793,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   }
 
   void visitFDiv(BinaryOperator &I) {
-    visitFPBinOp(I, [](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
+    visitFPBinOp(I, [](const APFloat &LHS, const APFloat &RHS) -> APFloat {
       APFloat Res = LHS;
       Res.divide(RHS, APFloat::rmNearestTiesToEven);
       return Res;
@@ -797,7 +801,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   }
 
   void visitFRem(BinaryOperator &I) {
-    visitFPBinOp(I, [](const APFloat &LHS, const APFloat &RHS) -> AnyValue {
+    visitFPBinOp(I, [](const APFloat &LHS, const APFloat &RHS) -> APFloat {
       APFloat Res = LHS;
       Res.mod(RHS);
       return Res;
@@ -805,7 +809,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   }
 
   void visitFNeg(UnaryOperator &I) {
-    visitFPUnOp(I, [](const APFloat &Operand) -> AnyValue { return -Operand; });
+    visitFPUnOp(I, [](const APFloat &Operand) -> APFloat { return -Operand; });
   }
 
   void visitTruncInst(TruncInst &Trunc) {
@@ -847,10 +851,20 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
       if (Operand.isPoison())
         return AnyValue::poison();
 
-      APFloat Res = Operand.asFloat();
+      FastMathFlags FMF = cast<FPMathOperator>(I).getFastMathFlags();
+      APFloat FOperand = Operand.asFloat();
+      if (auto ValidateRes = handleFMFFlags(FOperand, FMF);
+          ValidateRes.isPoison())
+        return ValidateRes;
+
       bool LosesInfo;
-      Res.convert(DstSem, CurrentRoundingMode, &LosesInfo);
-      return AnyValue(Res);
+      FOperand.convert(DstSem, CurrentRoundingMode, &LosesInfo);
+
+      if (auto ValidateRes = handleFMFFlags(FOperand, FMF);
+          ValidateRes.isPoison())
+        return ValidateRes;
+
+      return AnyValue(FOperand);
     });
   }
 
@@ -973,8 +987,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   }
 
   void visitFCmpInst(FCmpInst &I) {
-    DenormalMode DenormMode = CurrentFrame->Func.getDenormalMode(
-        I.getOperand(0)->getType()->getScalarType()->getFltSemantics());
+    DenormalMode DenormMode = getCurrentDenormalMode(I);
     FastMathFlags FMF = I.getFastMathFlags();
 
     visitBinOp(I, [&](const AnyValue &LHS, const AnyValue &RHS) -> AnyValue {

>From 6f70afe607d965c906d49b825b34152e1d08cc91 Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhige_chen at outlook.com>
Date: Thu, 2 Apr 2026 14:13:45 +0800
Subject: [PATCH 11/20] [llubi] Small format fix

---
 llvm/tools/llubi/lib/Interpreter.cpp | 9 +++------
 1 file changed, 3 insertions(+), 6 deletions(-)

diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index 48ebd53d2c504..20981dad6c143 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -991,16 +991,13 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     FastMathFlags FMF = I.getFastMathFlags();
 
     visitBinOp(I, [&](const AnyValue &LHS, const AnyValue &RHS) -> AnyValue {
-      if (LHS.isPoison() || RHS.isPoison()) {
+      if (LHS.isPoison() || RHS.isPoison())
         return AnyValue::poison();
-      }
 
-      if (auto ValidateRes = handleFMFFlags(LHS, FMF); ValidateRes.isPoison()) {
+      if (auto ValidateRes = handleFMFFlags(LHS, FMF); ValidateRes.isPoison())
         return ValidateRes;
-      }
-      if (auto ValidateRes = handleFMFFlags(RHS, FMF); ValidateRes.isPoison()) {
+      if (auto ValidateRes = handleFMFFlags(RHS, FMF); ValidateRes.isPoison())
         return ValidateRes;
-      }
 
       APFloat FLHS = handleDenormal(LHS.asFloat(), DenormMode.Input);
       APFloat FRHS = handleDenormal(RHS.asFloat(), DenormMode.Input);

>From 1256652434d5b31c2ea04054d5dd70dfd2d8f3ee Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhige_chen at outlook.com>
Date: Thu, 2 Apr 2026 14:42:18 +0800
Subject: [PATCH 12/20] [llubi] Small FP-related fixes

---
 llvm/tools/llubi/lib/Interpreter.cpp | 60 +++++++++++++++++++++++++---
 llvm/tools/llubi/llubi.cpp           |  3 +-
 2 files changed, 57 insertions(+), 6 deletions(-)

diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index 20981dad6c143..5d62d8cd69ec5 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -68,7 +68,8 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   const DataLayout &DL;
   std::list<Frame> CallStack;
   AnyValue None;
-  APFloat::roundingMode CurrentRoundingMode;
+  RoundingMode CurrentRoundingMode;
+  fp::ExceptionBehavior CurrentExceptionBehavior;
 
   const AnyValue &getValue(Value *V) {
     if (auto *C = dyn_cast<Constant>(V))
@@ -121,8 +122,8 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
       return AnyValue::poison();
     // Non-deterministically flip the sign of the input.
     if (FMF.noSignedZeros() && APVal.isZero()) {
-      return AnyValue(
-          APFloat::getZero(APVal.getSemantics(), Ctx.getRandomBool()));
+      return AnyValue(APFloat::getZero(
+          APVal.getSemantics(), APVal.isNegative() ^ Ctx.getRandomBool()));
     }
     return Val;
   }
@@ -419,7 +420,8 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   InstExecutor(Context &C, EventHandler &H, Function &F,
                ArrayRef<AnyValue> Args, AnyValue &RetVal)
       : ExecutorBase(C, H), DL(Ctx.getDataLayout()),
-        CurrentRoundingMode(APFloat::rmNearestTiesToEven) {
+        CurrentRoundingMode(APFloat::rmNearestTiesToEven),
+        CurrentExceptionBehavior(fp::ebIgnore) {
     CallStack.emplace_back(F, /*CallSite=*/nullptr, /*LastFrame=*/nullptr, Args,
                            RetVal, Ctx.getTLIImpl());
   }
@@ -769,6 +771,12 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   }
 
   void visitFAdd(BinaryOperator &I) {
+    if (!isDefaultFPEnvironment(CurrentExceptionBehavior,
+                                CurrentRoundingMode)) {
+      Status = false;
+      reportImmediateUB("Non-constrained floating-point operation assumes "
+                        "default floating-point environment");
+    }
     visitFPBinOp(I, [](const APFloat &LHS, const APFloat &RHS) -> APFloat {
       APFloat Res = LHS;
       Res.add(RHS, APFloat::rmNearestTiesToEven);
@@ -777,6 +785,12 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   }
 
   void visitFSub(BinaryOperator &I) {
+    if (!isDefaultFPEnvironment(CurrentExceptionBehavior,
+                                CurrentRoundingMode)) {
+      Status = false;
+      reportImmediateUB("Non-constrained floating-point operation assumes "
+                        "default floating-point environment");
+    }
     visitFPBinOp(I, [](const APFloat &LHS, const APFloat &RHS) -> APFloat {
       APFloat Res = LHS;
       Res.subtract(RHS, APFloat::rmNearestTiesToEven);
@@ -785,6 +799,12 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   }
 
   void visitFMul(BinaryOperator &I) {
+    if (!isDefaultFPEnvironment(CurrentExceptionBehavior,
+                                CurrentRoundingMode)) {
+      Status = false;
+      reportImmediateUB("Non-constrained floating-point operation assumes "
+                        "default floating-point environment");
+    }
     visitFPBinOp(I, [](const APFloat &LHS, const APFloat &RHS) -> APFloat {
       APFloat Res = LHS;
       Res.multiply(RHS, APFloat::rmNearestTiesToEven);
@@ -793,6 +813,12 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   }
 
   void visitFDiv(BinaryOperator &I) {
+    if (!isDefaultFPEnvironment(CurrentExceptionBehavior,
+                                CurrentRoundingMode)) {
+      Status = false;
+      reportImmediateUB("Non-constrained floating-point operation assumes "
+                        "default floating-point environment");
+    }
     visitFPBinOp(I, [](const APFloat &LHS, const APFloat &RHS) -> APFloat {
       APFloat Res = LHS;
       Res.divide(RHS, APFloat::rmNearestTiesToEven);
@@ -801,6 +827,12 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   }
 
   void visitFRem(BinaryOperator &I) {
+    if (!isDefaultFPEnvironment(CurrentExceptionBehavior,
+                                CurrentRoundingMode)) {
+      Status = false;
+      reportImmediateUB("Non-constrained floating-point operation assumes "
+                        "default floating-point environment");
+    }
     visitFPBinOp(I, [](const APFloat &LHS, const APFloat &RHS) -> APFloat {
       APFloat Res = LHS;
       Res.mod(RHS);
@@ -809,6 +841,12 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   }
 
   void visitFNeg(UnaryOperator &I) {
+    if (!isDefaultFPEnvironment(CurrentExceptionBehavior,
+                                CurrentRoundingMode)) {
+      Status = false;
+      reportImmediateUB("Non-constrained floating-point operation assumes "
+                        "default floating-point environment");
+    }
     visitFPUnOp(I, [](const APFloat &Operand) -> APFloat { return -Operand; });
   }
 
@@ -844,6 +882,13 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
   void visitFPTruncInst(FPTruncInst &FPTrunc) { visitFPConvInst(FPTrunc); }
 
   void visitFPConvInst(Instruction &I) {
+    if (!isDefaultFPEnvironment(CurrentExceptionBehavior,
+                                CurrentRoundingMode)) {
+      Status = false;
+      reportImmediateUB("Non-constrained floating-point operation assumes "
+                        "default floating-point environment");
+    }
+
     const fltSemantics &DstSem =
         I.getType()->getScalarType()->getFltSemantics();
 
@@ -852,7 +897,10 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
         return AnyValue::poison();
 
       FastMathFlags FMF = cast<FPMathOperator>(I).getFastMathFlags();
-      APFloat FOperand = Operand.asFloat();
+      DenormalMode DenormMode = getCurrentDenormalMode(I);
+
+      APFloat FOperand = handleDenormal(Operand.asFloat(), DenormMode.Input);
+
       if (auto ValidateRes = handleFMFFlags(FOperand, FMF);
           ValidateRes.isPoison())
         return ValidateRes;
@@ -864,6 +912,8 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
           ValidateRes.isPoison())
         return ValidateRes;
 
+      FOperand = handleDenormal(std::move(FOperand), DenormMode.Output, true);
+
       return AnyValue(FOperand);
     });
   }
diff --git a/llvm/tools/llubi/llubi.cpp b/llvm/tools/llubi/llubi.cpp
index 4200c9246e8d8..825d8e17149e6 100644
--- a/llvm/tools/llubi/llubi.cpp
+++ b/llvm/tools/llubi/llubi.cpp
@@ -105,7 +105,8 @@ cl::opt<ubi::NaNPropagationBehavior> NaNPropagationBehavior(
         clEnumVal(ubi::NaNPropagationBehavior::TargetSpecificNaN,
                   "The quiet bit is set and the payload is picked from a"
                   "target-specific set of “extra” possible NaN payloads."
-                  "Implemented by filling payload with random values")));
+                  "Implemented by filling payload with random values")),
+    cl::init(ubi::NaNPropagationBehavior::NonDeterministic));
 
 class VerboseEventHandler : public ubi::EventHandler {
 public:

>From 322880654b6d5dff023f2f3bdbaa18da876d0119 Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhige_chen at outlook.com>
Date: Thu, 2 Apr 2026 15:01:54 +0800
Subject: [PATCH 13/20] [llubi] Refactor out NaN propagation logic & add NaN
 propagation to fpext/fptrunc

---
 llvm/tools/llubi/lib/Interpreter.cpp | 176 +++++++++++++++++++--------
 1 file changed, 122 insertions(+), 54 deletions(-)

diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index 5d62d8cd69ec5..787fa1fc88849 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -128,6 +128,122 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
     return Val;
   }
 
+  enum class NaNPayloadCastMode {
+    None,
+    FPExt,
+    FPTrunc,
+  };
+
+  static unsigned getNaNPayloadBitWidth(const fltSemantics &Sem) {
+    unsigned Precision = APFloat::semanticsPrecision(Sem);
+    return Precision > 2 ? Precision - 2 : 0;
+  }
+
+  NaNPropagationBehavior resolveNaNPropagationBehavior() {
+    NaNPropagationBehavior Choice = Ctx.getNaNPropagationBehavior();
+    if (Choice == NaNPropagationBehavior::NonDeterministic) {
+      uint64_t NonDetChoice = Ctx.getRandomUInt64() % 4 + 1;
+      Choice = static_cast<NaNPropagationBehavior>(NonDetChoice);
+    }
+    return Choice;
+  }
+
+  const APFloat &pickNaNSource(ArrayRef<const APFloat *> Inputs,
+                               const APFloat &Fallback) {
+    for (const APFloat *Input : Inputs) {
+      if (Input && Input->isNaN())
+        return *Input;
+    }
+    return Fallback;
+  }
+
+  APInt getNaNPayload(const APFloat &NaNVal) {
+    unsigned PayloadBits = getNaNPayloadBitWidth(NaNVal.getSemantics());
+    if (!PayloadBits)
+      return APInt(1, 0);
+    return NaNVal.bitcastToAPInt().extractBits(PayloadBits, 0);
+  }
+
+  APInt castNaNPayload(const APInt &SrcPayload, unsigned SrcPayloadBits,
+                       unsigned DstPayloadBits, NaNPayloadCastMode CastMode) {
+    if (!DstPayloadBits)
+      return APInt(1, 0);
+
+    APInt CanonicalSrc = SrcPayload.zextOrTrunc(SrcPayloadBits);
+    if (CastMode == NaNPayloadCastMode::FPExt &&
+        SrcPayloadBits < DstPayloadBits)
+      return CanonicalSrc.zext(DstPayloadBits)
+          .shl(DstPayloadBits - SrcPayloadBits);
+    if (CastMode == NaNPayloadCastMode::FPTrunc &&
+        SrcPayloadBits > DstPayloadBits)
+      return CanonicalSrc.lshr(SrcPayloadBits - DstPayloadBits)
+          .trunc(DstPayloadBits);
+    return CanonicalSrc.zextOrTrunc(DstPayloadBits);
+  }
+
+  APFloat buildNaN(const fltSemantics &Sem, bool IsNegative, bool IsSignaling,
+                   const APInt &Payload) {
+    unsigned SignificandBits = APFloat::semanticsPrecision(Sem) - 1;
+    APInt Fill = APInt::getZero(SignificandBits);
+    unsigned PayloadBits = getNaNPayloadBitWidth(Sem);
+    if (PayloadBits)
+      Fill.insertBits(Payload.zextOrTrunc(PayloadBits), 0);
+
+    if (IsSignaling)
+      return APFloat::getSNaN(Sem, IsNegative, &Fill);
+    return APFloat::getQNaN(Sem, IsNegative, &Fill);
+  }
+
+  APFloat propagateInputNaN(const APFloat &InputNaN, const fltSemantics &DstSem,
+                            bool QuietingMode, bool FlipSign,
+                            NaNPayloadCastMode CastMode) {
+    unsigned SrcPayloadBits = getNaNPayloadBitWidth(InputNaN.getSemantics());
+    unsigned DstPayloadBits = getNaNPayloadBitWidth(DstSem);
+    APInt MappedPayload = castNaNPayload(
+        getNaNPayload(InputNaN), SrcPayloadBits, DstPayloadBits, CastMode);
+
+    bool IsSignaling = InputNaN.isSignaling();
+    bool OutputIsSignaling = !QuietingMode && IsSignaling;
+    if (OutputIsSignaling &&
+        (DstPayloadBits == 0 ||
+         (CastMode == NaNPayloadCastMode::FPTrunc && MappedPayload.isZero())))
+      OutputIsSignaling = false;
+
+    bool IsNegative = InputNaN.isNegative();
+    if (FlipSign)
+      IsNegative = !IsNegative;
+    return buildNaN(DstSem, IsNegative, OutputIsSignaling, MappedPayload);
+  }
+
+  APFloat
+  applyNaNPropagation(const APFloat &Result, ArrayRef<const APFloat *> Inputs,
+                      NaNPayloadCastMode CastMode = NaNPayloadCastMode::None) {
+    if (!Result.isNaN())
+      return Result;
+
+    NaNPropagationBehavior Choice = resolveNaNPropagationBehavior();
+    const bool SignChoice = Ctx.getRandomBool();
+    switch (Choice) {
+    case NaNPropagationBehavior::PreferredNaN:
+      return APFloat::getQNaN(Result.getSemantics(), SignChoice);
+    case NaNPropagationBehavior::QuietingNaN:
+      return propagateInputNaN(pickNaNSource(Inputs, Result),
+                               Result.getSemantics(), /*QuietingMode=*/true,
+                               SignChoice, CastMode);
+    case NaNPropagationBehavior::UnchangedNaN:
+      return propagateInputNaN(pickNaNSource(Inputs, Result),
+                               Result.getSemantics(), /*QuietingMode=*/false,
+                               SignChoice, CastMode);
+    case NaNPropagationBehavior::TargetSpecificNaN: {
+      APInt Payload(64, Ctx.getRandomUInt64());
+      return APFloat::getQNaN(Result.getSemantics(), SignChoice, &Payload);
+    }
+    case NaNPropagationBehavior::NonDeterministic:
+      llvm_unreachable("NonDeterministic should be resolved earlier.");
+    }
+    llvm_unreachable("Unhandled NaN propagation behavior.");
+  }
+
   AnyValue computeUnOp(Type *Ty, const AnyValue &Operand,
                        function_ref<AnyValue(const AnyValue &)> ScalarFn) {
     if (Ty->isVectorTy()) {
@@ -234,59 +350,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
         return FResult;
 
       APFloat Result = FResult.asFloat();
-      // NaN payload propagation
-      if (Result.isNaN()) {
-        NaNPropagationBehavior Choice = Ctx.getNaNPropagationBehavior();
-        if (Choice == NaNPropagationBehavior::NonDeterministic) {
-          uint64_t NonDetChoice = Ctx.getRandomUInt64() % 4 + 1;
-          Choice = static_cast<NaNPropagationBehavior>(NonDetChoice);
-        }
-        const bool Sign = Ctx.getRandomBool();
-        if (Choice == NaNPropagationBehavior::PreferredNaN) {
-          // Preferred NaN: the quiet bit is set and the payload is all-zero
-          return APFloat::getQNaN(Result.getSemantics(), Sign);
-        }
-        if (Choice == NaNPropagationBehavior::QuietingNaN) {
-          // Quieting NaN propagation: the quiet bit is set and the payload is
-          // copied from any input operand that is a NaN. We implement this by
-          // directly set the quiet bit of the input NaN, and
-          // non-deterministically flip its sign bit
-          auto QuietNaN = [&](APFloat &Input) {
-            APFloat Quieted = Input.makeQuiet();
-            if (Sign)
-              Quieted.changeSign();
-            return Quieted;
-          };
-          if (FLHS.isNaN())
-            return QuietNaN(FLHS);
-          if (FRHS.isNaN())
-            return QuietNaN(FRHS);
-          return QuietNaN(Result);
-        }
-        if (Choice == NaNPropagationBehavior::UnchangedNaN) {
-          // Unchanged NaN propagation: the quiet bit and payload are copied
-          // from any input operand that is a NaN
-          auto FlipSign = [&](APFloat &Input) {
-            if (Sign)
-              Input.changeSign();
-            return Input;
-          };
-          if (FLHS.isNaN())
-            return FlipSign(FLHS);
-          if (FRHS.isNaN())
-            return FlipSign(FRHS);
-          return FlipSign(Result);
-        }
-        if (Choice == NaNPropagationBehavior::TargetSpecificNaN) {
-          // Target-specific NaN: the quiet bit is set and the payload is picked
-          // from a target-specific set of "extra" possible NaN payloads. We
-          // approximate this by filling the payload with random values.
-          APInt Payload(64, Ctx.getRandomUInt64());
-          return APFloat::getQNaN(Result.getSemantics(), Sign, &Payload);
-        }
-      }
-
-      return Result;
+      return applyNaNPropagation(Result, {&FLHS, &FRHS});
     });
   }
 
@@ -900,6 +964,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
       DenormalMode DenormMode = getCurrentDenormalMode(I);
 
       APFloat FOperand = handleDenormal(Operand.asFloat(), DenormMode.Input);
+      APFloat SourceNaN = FOperand;
 
       if (auto ValidateRes = handleFMFFlags(FOperand, FMF);
           ValidateRes.isPoison())
@@ -914,7 +979,10 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
 
       FOperand = handleDenormal(std::move(FOperand), DenormMode.Output, true);
 
-      return AnyValue(FOperand);
+      NaNPayloadCastMode CastMode = isa<FPExtInst>(I)
+                                        ? NaNPayloadCastMode::FPExt
+                                        : NaNPayloadCastMode::FPTrunc;
+      return AnyValue(applyNaNPropagation(FOperand, {&SourceNaN}, CastMode));
     });
   }
 

>From 25f4828c32de9bbb95c04e9721db8302b419d3b4 Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhige_chen at outlook.com>
Date: Thu, 2 Apr 2026 15:39:17 +0800
Subject: [PATCH 14/20] [llubi] Add basic arithmetic tests for
 half/bfloat/double/x86_fp80/fp128/ppc_fp128

---
 llvm/test/tools/llubi/fp_arith.ll           | 33 ---------------------
 llvm/test/tools/llubi/fp_arith_bfloat.ll    | 20 +++++++++++++
 llvm/test/tools/llubi/fp_arith_double.ll    | 20 +++++++++++++
 llvm/test/tools/llubi/fp_arith_float.ll     | 20 +++++++++++++
 llvm/test/tools/llubi/fp_arith_fp128.ll     | 20 +++++++++++++
 llvm/test/tools/llubi/fp_arith_fp80.ll      | 20 +++++++++++++
 llvm/test/tools/llubi/fp_arith_half.ll      | 20 +++++++++++++
 llvm/test/tools/llubi/fp_arith_ppc_fp128.ll | 20 +++++++++++++
 8 files changed, 140 insertions(+), 33 deletions(-)
 delete mode 100644 llvm/test/tools/llubi/fp_arith.ll
 create mode 100644 llvm/test/tools/llubi/fp_arith_bfloat.ll
 create mode 100644 llvm/test/tools/llubi/fp_arith_double.ll
 create mode 100644 llvm/test/tools/llubi/fp_arith_float.ll
 create mode 100644 llvm/test/tools/llubi/fp_arith_fp128.ll
 create mode 100644 llvm/test/tools/llubi/fp_arith_fp80.ll
 create mode 100644 llvm/test/tools/llubi/fp_arith_half.ll
 create mode 100644 llvm/test/tools/llubi/fp_arith_ppc_fp128.ll

diff --git a/llvm/test/tools/llubi/fp_arith.ll b/llvm/test/tools/llubi/fp_arith.ll
deleted file mode 100644
index 69b091844ce8d..0000000000000
--- a/llvm/test/tools/llubi/fp_arith.ll
+++ /dev/null
@@ -1,33 +0,0 @@
-; 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 void @main() {
-  %add = fadd float 1.5, 2.5
-  %sub = fsub double 5.0, 1.0
-  %mul = fmul float 2.0, 3.0
-  %div = fdiv double 1.0, 2.0
-  %neg = fneg float -1.5
-
-  %vadd = fadd <2 x float> <float 1.0, float 2.0>, <float 3.0, float 4.0>
-
-  %p1 = fadd float 1.0, poison
-  %p2 = fneg float poison
-
-  %inf = fdiv float 1.0, 0.0
-  %nan = fdiv float 0.0, 0.0
-
-  ret void
-}
-; CHECK: Entering function: main
-; CHECK-NEXT:   %add = fadd float 1.500000e+00, 2.500000e+00 => float 4.000000e+00
-; CHECK-NEXT:   %sub = fsub double 5.000000e+00, 1.000000e+00 => double 4.000000e+00
-; CHECK-NEXT:   %mul = fmul float 2.000000e+00, 3.000000e+00 => float 6.000000e+00
-; CHECK-NEXT:   %div = fdiv double 1.000000e+00, 2.000000e+00 => double 5.000000e-01
-; CHECK-NEXT:   %neg = fneg float -1.500000e+00 => float 1.500000e+00
-; CHECK-NEXT:   %vadd = fadd <2 x float> <float 1.000000e+00, float 2.000000e+00>, <float 3.000000e+00, float 4.000000e+00> => { float 4.000000e+00, float 6.000000e+00 }
-; CHECK-NEXT:   %p1 = fadd float 1.000000e+00, poison => poison
-; CHECK-NEXT:   %p2 = fneg float poison => poison
-; CHECK-NEXT:   %inf = fdiv float 1.000000e+00, 0.000000e+00 => float +Inf
-; CHECK-NEXT:   %nan = fdiv float 0.000000e+00, 0.000000e+00 => float 0xFFC00000
-; CHECK-NEXT:   ret void
-; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/fp_arith_bfloat.ll b/llvm/test/tools/llubi/fp_arith_bfloat.ll
new file mode 100644
index 0000000000000..c546a65ccf21e
--- /dev/null
+++ b/llvm/test/tools/llubi/fp_arith_bfloat.ll
@@ -0,0 +1,20 @@
+; 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 void @main() {
+  %add = fadd bfloat 1.5, 2.5
+  %sub = fsub bfloat 5.0, 1.0
+  %mul = fmul bfloat 2.0, 3.0
+  %div = fdiv bfloat 1.0, 2.0
+  %neg = fneg bfloat -1.5
+
+  %vadd = fadd <2 x bfloat> <bfloat 1.0, bfloat 2.0>, <bfloat 3.0, bfloat 4.0>
+
+  %p1 = fadd bfloat 1.0, poison
+  %p2 = fneg bfloat poison
+
+  %inf = fdiv bfloat 1.0, 0.0
+  %nan = fdiv bfloat 0.0, 0.0
+
+  ret void
+}
diff --git a/llvm/test/tools/llubi/fp_arith_double.ll b/llvm/test/tools/llubi/fp_arith_double.ll
new file mode 100644
index 0000000000000..7201058392b6e
--- /dev/null
+++ b/llvm/test/tools/llubi/fp_arith_double.ll
@@ -0,0 +1,20 @@
+; 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 void @main() {
+  %add = fadd double 1.5, 2.5
+  %sub = fsub double 5.0, 1.0
+  %mul = fmul double 2.0, 3.0
+  %div = fdiv double 1.0, 2.0
+  %neg = fneg double -1.5
+
+  %vadd = fadd <2 x double> <double 1.0, double 2.0>, <double 3.0, double 4.0>
+
+  %p1 = fadd double 1.0, poison
+  %p2 = fneg double poison
+
+  %inf = fdiv double 1.0, 0.0
+  %nan = fdiv double 0.0, 0.0
+
+  ret void
+}
diff --git a/llvm/test/tools/llubi/fp_arith_float.ll b/llvm/test/tools/llubi/fp_arith_float.ll
new file mode 100644
index 0000000000000..3dff7003c1f70
--- /dev/null
+++ b/llvm/test/tools/llubi/fp_arith_float.ll
@@ -0,0 +1,20 @@
+; 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 void @main() {
+  %add = fadd float 1.5, 2.5
+  %sub = fsub float 5.0, 1.0
+  %mul = fmul float 2.0, 3.0
+  %div = fdiv float 1.0, 2.0
+  %neg = fneg float -1.5
+
+  %vadd = fadd <2 x float> <float 1.0, float 2.0>, <float 3.0, float 4.0>
+
+  %p1 = fadd float 1.0, poison
+  %p2 = fneg float poison
+
+  %inf = fdiv float 1.0, 0.0
+  %nan = fdiv float 0.0, 0.0
+
+  ret void
+}
diff --git a/llvm/test/tools/llubi/fp_arith_fp128.ll b/llvm/test/tools/llubi/fp_arith_fp128.ll
new file mode 100644
index 0000000000000..1fb3b5b060ed6
--- /dev/null
+++ b/llvm/test/tools/llubi/fp_arith_fp128.ll
@@ -0,0 +1,20 @@
+; 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 void @main() {
+  %add = fadd fp128 0xL00000000000000003FFF800000000000, 0xL00000000000000004000400000000000
+  %sub = fsub fp128 0xL00000000000000004001400000000000, 0xL00000000000000003FFF000000000000
+  %mul = fmul fp128 0xL00000000000000004000000000000000, 0xL00000000000000004000800000000000
+  %div = fdiv fp128 0xL00000000000000003FFF000000000000, 0xL00000000000000004000000000000000
+  %neg = fneg fp128 0xL0000000000000000BFFF800000000000
+
+  %vadd = fadd <2 x fp128> <fp128 0xL00000000000000003FFF000000000000, fp128 0xL00000000000000004000000000000000>, <fp128 0xL00000000000000004000800000000000, fp128 0xL00000000000000004001000000000000>
+
+  %p1 = fadd fp128 0xL00000000000000003FFF000000000000, poison
+  %p2 = fneg fp128 poison
+
+  %inf = fdiv fp128 0xL00000000000000003FFF000000000000, 0xL00000000000000000000000000000000
+  %nan = fdiv fp128 0xL00000000000000000000000000000000, 0xL00000000000000000000000000000000
+
+  ret void
+}
diff --git a/llvm/test/tools/llubi/fp_arith_fp80.ll b/llvm/test/tools/llubi/fp_arith_fp80.ll
new file mode 100644
index 0000000000000..78c58fdae0d54
--- /dev/null
+++ b/llvm/test/tools/llubi/fp_arith_fp80.ll
@@ -0,0 +1,20 @@
+; 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 void @main() {
+  %add = fadd x86_fp80 0xK3FFFC000000000000000, 0xK4000A000000000000000
+  %sub = fsub x86_fp80 0xK4001A000000000000000, 0xK3FFF8000000000000000
+  %mul = fmul x86_fp80 0xK40008000000000000000, 0xK4000C000000000000000
+  %div = fdiv x86_fp80 0xK3FFF8000000000000000, 0xK40008000000000000000
+  %neg = fneg x86_fp80 0xKBFFFC000000000000000
+
+  %vadd = fadd <2 x x86_fp80> <x86_fp80 0xK3FFF8000000000000000, x86_fp80 0xK40008000000000000000>, <x86_fp80 0xK4000C000000000000000, x86_fp80 0xK40018000000000000000>
+
+  %p1 = fadd x86_fp80 0xK3FFF8000000000000000, poison
+  %p2 = fneg x86_fp80 poison
+
+  %inf = fdiv x86_fp80 0xK3FFF8000000000000000, 0xK00000000000000000000
+  %nan = fdiv x86_fp80 0xK00000000000000000000, 0xK00000000000000000000
+
+  ret void
+}
diff --git a/llvm/test/tools/llubi/fp_arith_half.ll b/llvm/test/tools/llubi/fp_arith_half.ll
new file mode 100644
index 0000000000000..49e1dbc57498c
--- /dev/null
+++ b/llvm/test/tools/llubi/fp_arith_half.ll
@@ -0,0 +1,20 @@
+; 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 void @main() {
+  %add = fadd half 1.5, 2.5
+  %sub = fsub half 5.0, 1.0
+  %mul = fmul half 2.0, 3.0
+  %div = fdiv half 1.0, 2.0
+  %neg = fneg half -1.5
+
+  %vadd = fadd <2 x half> <half 1.0, half 2.0>, <half 3.0, half 4.0>
+
+  %p1 = fadd half 1.0, poison
+  %p2 = fneg half poison
+
+  %inf = fdiv half 1.0, 0.0
+  %nan = fdiv half 0.0, 0.0
+
+  ret void
+}
diff --git a/llvm/test/tools/llubi/fp_arith_ppc_fp128.ll b/llvm/test/tools/llubi/fp_arith_ppc_fp128.ll
new file mode 100644
index 0000000000000..d2a8a5c06dd4a
--- /dev/null
+++ b/llvm/test/tools/llubi/fp_arith_ppc_fp128.ll
@@ -0,0 +1,20 @@
+; 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 void @main() {
+  %add = fadd ppc_fp128 0xM3FF80000000000000000000000000000, 0xM40040000000000000000000000000000
+  %sub = fsub ppc_fp128 0xM40140000000000000000000000000000, 0xM3FF00000000000000000000000000000
+  %mul = fmul ppc_fp128 0xM40000000000000000000000000000000, 0xM40080000000000000000000000000000
+  %div = fdiv ppc_fp128 0xM3FF00000000000000000000000000000, 0xM40000000000000000000000000000000
+  %neg = fneg ppc_fp128 0xMBFF80000000000000000000000000000
+
+  %vadd = fadd <2 x ppc_fp128> <ppc_fp128 0xM3FF00000000000000000000000000000, ppc_fp128 0xM40000000000000000000000000000000>, <ppc_fp128 0xM40080000000000000000000000000000, ppc_fp128 0xM40100000000000000000000000000000>
+
+  %p1 = fadd ppc_fp128 0xM3FF00000000000000000000000000000, poison
+  %p2 = fneg ppc_fp128 poison
+
+  %inf = fdiv ppc_fp128 0xM3FF00000000000000000000000000000, 0xM00000000000000000000000000000000
+  %nan = fdiv ppc_fp128 0xM00000000000000000000000000000000, 0xM00000000000000000000000000000000
+
+  ret void
+}

>From 5777b35ea2b054dcce956fe1e8e96e17914c3cdc Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhige_chen at outlook.com>
Date: Thu, 2 Apr 2026 15:40:16 +0800
Subject: [PATCH 15/20] [llubi] Update tests

---
 llvm/test/tools/llubi/fp_arith_bfloat.ll    | 13 +++++++++++++
 llvm/test/tools/llubi/fp_arith_double.ll    | 13 +++++++++++++
 llvm/test/tools/llubi/fp_arith_float.ll     | 13 +++++++++++++
 llvm/test/tools/llubi/fp_arith_fp128.ll     | 13 +++++++++++++
 llvm/test/tools/llubi/fp_arith_fp80.ll      | 13 +++++++++++++
 llvm/test/tools/llubi/fp_arith_half.ll      | 13 +++++++++++++
 llvm/test/tools/llubi/fp_arith_ppc_fp128.ll | 13 +++++++++++++
 7 files changed, 91 insertions(+)

diff --git a/llvm/test/tools/llubi/fp_arith_bfloat.ll b/llvm/test/tools/llubi/fp_arith_bfloat.ll
index c546a65ccf21e..e386261261f94 100644
--- a/llvm/test/tools/llubi/fp_arith_bfloat.ll
+++ b/llvm/test/tools/llubi/fp_arith_bfloat.ll
@@ -18,3 +18,16 @@ define void @main() {
 
   ret void
 }
+; CHECK: Entering function: main
+; CHECK-NEXT:   %add = fadd bfloat 0xR3FC0, 0xR4020 => bfloat 4.000000e+00
+; CHECK-NEXT:   %sub = fsub bfloat 0xR40A0, 0xR3F80 => bfloat 4.000000e+00
+; CHECK-NEXT:   %mul = fmul bfloat 0xR4000, 0xR4040 => bfloat 6.000000e+00
+; CHECK-NEXT:   %div = fdiv bfloat 0xR3F80, 0xR4000 => bfloat 5.000000e-01
+; CHECK-NEXT:   %neg = fneg bfloat 0xRBFC0 => bfloat 1.500000e+00
+; CHECK-NEXT:   %vadd = fadd <2 x bfloat> <bfloat 0xR3F80, bfloat 0xR4000>, <bfloat 0xR4040, bfloat 0xR4080> => { bfloat 4.000000e+00, bfloat 6.000000e+00 }
+; CHECK-NEXT:   %p1 = fadd bfloat 0xR3F80, poison => poison
+; CHECK-NEXT:   %p2 = fneg bfloat poison => poison
+; CHECK-NEXT:   %inf = fdiv bfloat 0xR3F80, 0xR0000 => bfloat +Inf
+; CHECK-NEXT:   %nan = fdiv bfloat 0xR0000, 0xR0000 => bfloat 0xFFC0
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/fp_arith_double.ll b/llvm/test/tools/llubi/fp_arith_double.ll
index 7201058392b6e..755af05cbbf9d 100644
--- a/llvm/test/tools/llubi/fp_arith_double.ll
+++ b/llvm/test/tools/llubi/fp_arith_double.ll
@@ -18,3 +18,16 @@ define void @main() {
 
   ret void
 }
+; CHECK: Entering function: main
+; CHECK-NEXT:   %add = fadd double 1.500000e+00, 2.500000e+00 => double 4.000000e+00
+; CHECK-NEXT:   %sub = fsub double 5.000000e+00, 1.000000e+00 => double 4.000000e+00
+; CHECK-NEXT:   %mul = fmul double 2.000000e+00, 3.000000e+00 => double 6.000000e+00
+; CHECK-NEXT:   %div = fdiv double 1.000000e+00, 2.000000e+00 => double 5.000000e-01
+; CHECK-NEXT:   %neg = fneg double -1.500000e+00 => double 1.500000e+00
+; CHECK-NEXT:   %vadd = fadd <2 x double> <double 1.000000e+00, double 2.000000e+00>, <double 3.000000e+00, double 4.000000e+00> => { double 4.000000e+00, double 6.000000e+00 }
+; CHECK-NEXT:   %p1 = fadd double 1.000000e+00, poison => poison
+; CHECK-NEXT:   %p2 = fneg double poison => poison
+; CHECK-NEXT:   %inf = fdiv double 1.000000e+00, 0.000000e+00 => double +Inf
+; CHECK-NEXT:   %nan = fdiv double 0.000000e+00, 0.000000e+00 => double 0xFFF8000000000000
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/fp_arith_float.ll b/llvm/test/tools/llubi/fp_arith_float.ll
index 3dff7003c1f70..28ee59f3d7fe2 100644
--- a/llvm/test/tools/llubi/fp_arith_float.ll
+++ b/llvm/test/tools/llubi/fp_arith_float.ll
@@ -18,3 +18,16 @@ define void @main() {
 
   ret void
 }
+; CHECK: Entering function: main
+; CHECK-NEXT:   %add = fadd float 1.500000e+00, 2.500000e+00 => float 4.000000e+00
+; CHECK-NEXT:   %sub = fsub float 5.000000e+00, 1.000000e+00 => float 4.000000e+00
+; CHECK-NEXT:   %mul = fmul float 2.000000e+00, 3.000000e+00 => float 6.000000e+00
+; CHECK-NEXT:   %div = fdiv float 1.000000e+00, 2.000000e+00 => float 5.000000e-01
+; CHECK-NEXT:   %neg = fneg float -1.500000e+00 => float 1.500000e+00
+; CHECK-NEXT:   %vadd = fadd <2 x float> <float 1.000000e+00, float 2.000000e+00>, <float 3.000000e+00, float 4.000000e+00> => { float 4.000000e+00, float 6.000000e+00 }
+; CHECK-NEXT:   %p1 = fadd float 1.000000e+00, poison => poison
+; CHECK-NEXT:   %p2 = fneg float poison => poison
+; CHECK-NEXT:   %inf = fdiv float 1.000000e+00, 0.000000e+00 => float +Inf
+; CHECK-NEXT:   %nan = fdiv float 0.000000e+00, 0.000000e+00 => float 0xFFC00000
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/fp_arith_fp128.ll b/llvm/test/tools/llubi/fp_arith_fp128.ll
index 1fb3b5b060ed6..83a2e7295d879 100644
--- a/llvm/test/tools/llubi/fp_arith_fp128.ll
+++ b/llvm/test/tools/llubi/fp_arith_fp128.ll
@@ -18,3 +18,16 @@ define void @main() {
 
   ret void
 }
+; CHECK: Entering function: main
+; CHECK-NEXT:   %add = fadd fp128 0xL00000000000000003FFF800000000000, 0xL00000000000000004000400000000000 => fp128 4.000000e+00
+; CHECK-NEXT:   %sub = fsub fp128 0xL00000000000000004001400000000000, 0xL00000000000000003FFF000000000000 => fp128 4.000000e+00
+; CHECK-NEXT:   %mul = fmul fp128 0xL00000000000000004000000000000000, 0xL00000000000000004000800000000000 => fp128 6.000000e+00
+; CHECK-NEXT:   %div = fdiv fp128 0xL00000000000000003FFF000000000000, 0xL00000000000000004000000000000000 => fp128 5.000000e-01
+; CHECK-NEXT:   %neg = fneg fp128 0xL0000000000000000BFFF800000000000 => fp128 1.500000e+00
+; CHECK-NEXT:   %vadd = fadd <2 x fp128> <fp128 0xL00000000000000003FFF000000000000, fp128 0xL00000000000000004000000000000000>, <fp128 0xL00000000000000004000800000000000, fp128 0xL00000000000000004001000000000000> => { fp128 4.000000e+00, fp128 6.000000e+00 }
+; CHECK-NEXT:   %p1 = fadd fp128 0xL00000000000000003FFF000000000000, poison => poison
+; CHECK-NEXT:   %p2 = fneg fp128 poison => poison
+; CHECK-NEXT:   %inf = fdiv fp128 0xL00000000000000003FFF000000000000, 0xL00000000000000000000000000000000 => fp128 +Inf
+; CHECK-NEXT:   %nan = fdiv fp128 0xL00000000000000000000000000000000, 0xL00000000000000000000000000000000 => fp128 0xFFFF8000000000000000000000000000
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/fp_arith_fp80.ll b/llvm/test/tools/llubi/fp_arith_fp80.ll
index 78c58fdae0d54..759e162773cd4 100644
--- a/llvm/test/tools/llubi/fp_arith_fp80.ll
+++ b/llvm/test/tools/llubi/fp_arith_fp80.ll
@@ -18,3 +18,16 @@ define void @main() {
 
   ret void
 }
+; CHECK: Entering function: main
+; CHECK-NEXT:   %add = fadd x86_fp80 0xK3FFFC000000000000000, 0xK4000A000000000000000 => x86_fp80 4.000000e+00
+; CHECK-NEXT:   %sub = fsub x86_fp80 0xK4001A000000000000000, 0xK3FFF8000000000000000 => x86_fp80 4.000000e+00
+; CHECK-NEXT:   %mul = fmul x86_fp80 0xK40008000000000000000, 0xK4000C000000000000000 => x86_fp80 6.000000e+00
+; CHECK-NEXT:   %div = fdiv x86_fp80 0xK3FFF8000000000000000, 0xK40008000000000000000 => x86_fp80 5.000000e-01
+; CHECK-NEXT:   %neg = fneg x86_fp80 0xKBFFFC000000000000000 => x86_fp80 1.500000e+00
+; CHECK-NEXT:   %vadd = fadd <2 x x86_fp80> <x86_fp80 0xK3FFF8000000000000000, x86_fp80 0xK40008000000000000000>, <x86_fp80 0xK4000C000000000000000, x86_fp80 0xK40018000000000000000> => { x86_fp80 4.000000e+00, x86_fp80 6.000000e+00 }
+; CHECK-NEXT:   %p1 = fadd x86_fp80 0xK3FFF8000000000000000, poison => poison
+; CHECK-NEXT:   %p2 = fneg x86_fp80 poison => poison
+; CHECK-NEXT:   %inf = fdiv x86_fp80 0xK3FFF8000000000000000, 0xK00000000000000000000 => x86_fp80 +Inf
+; CHECK-NEXT:   %nan = fdiv x86_fp80 0xK00000000000000000000, 0xK00000000000000000000 => x86_fp80 0xFFFFC000000000000000
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/fp_arith_half.ll b/llvm/test/tools/llubi/fp_arith_half.ll
index 49e1dbc57498c..093ec2b1e92da 100644
--- a/llvm/test/tools/llubi/fp_arith_half.ll
+++ b/llvm/test/tools/llubi/fp_arith_half.ll
@@ -18,3 +18,16 @@ define void @main() {
 
   ret void
 }
+; CHECK: Entering function: main
+; CHECK-NEXT:   %add = fadd half 0xH3E00, 0xH4100 => half 4.000000e+00
+; CHECK-NEXT:   %sub = fsub half 0xH4500, 0xH3C00 => half 4.000000e+00
+; CHECK-NEXT:   %mul = fmul half 0xH4000, 0xH4200 => half 6.000000e+00
+; CHECK-NEXT:   %div = fdiv half 0xH3C00, 0xH4000 => half 5.000000e-01
+; CHECK-NEXT:   %neg = fneg half 0xHBE00 => half 1.500000e+00
+; CHECK-NEXT:   %vadd = fadd <2 x half> <half 0xH3C00, half 0xH4000>, <half 0xH4200, half 0xH4400> => { half 4.000000e+00, half 6.000000e+00 }
+; CHECK-NEXT:   %p1 = fadd half 0xH3C00, poison => poison
+; CHECK-NEXT:   %p2 = fneg half poison => poison
+; CHECK-NEXT:   %inf = fdiv half 0xH3C00, 0xH0000 => half +Inf
+; CHECK-NEXT:   %nan = fdiv half 0xH0000, 0xH0000 => half 0xFE00
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/fp_arith_ppc_fp128.ll b/llvm/test/tools/llubi/fp_arith_ppc_fp128.ll
index d2a8a5c06dd4a..14dabc9bc1753 100644
--- a/llvm/test/tools/llubi/fp_arith_ppc_fp128.ll
+++ b/llvm/test/tools/llubi/fp_arith_ppc_fp128.ll
@@ -18,3 +18,16 @@ define void @main() {
 
   ret void
 }
+; CHECK: Entering function: main
+; CHECK-NEXT:   %add = fadd ppc_fp128 0xM3FF80000000000000000000000000000, 0xM40040000000000000000000000000000 => ppc_fp128 4.000000e+00
+; CHECK-NEXT:   %sub = fsub ppc_fp128 0xM40140000000000000000000000000000, 0xM3FF00000000000000000000000000000 => ppc_fp128 0x80000000000000004010000000000000
+; CHECK-NEXT:   %mul = fmul ppc_fp128 0xM40000000000000000000000000000000, 0xM40080000000000000000000000000000 => ppc_fp128 6.000000e+00
+; CHECK-NEXT:   %div = fdiv ppc_fp128 0xM3FF00000000000000000000000000000, 0xM40000000000000000000000000000000 => ppc_fp128 5.000000e-01
+; CHECK-NEXT:   %neg = fneg ppc_fp128 0xMBFF80000000000000000000000000000 => ppc_fp128 0x80000000000000003FF8000000000000
+; CHECK-NEXT:   %vadd = fadd <2 x ppc_fp128> <ppc_fp128 0xM3FF00000000000000000000000000000, ppc_fp128 0xM40000000000000000000000000000000>, <ppc_fp128 0xM40080000000000000000000000000000, ppc_fp128 0xM40100000000000000000000000000000> => { ppc_fp128 4.000000e+00, ppc_fp128 6.000000e+00 }
+; CHECK-NEXT:   %p1 = fadd ppc_fp128 0xM3FF00000000000000000000000000000, poison => poison
+; CHECK-NEXT:   %p2 = fneg ppc_fp128 poison => poison
+; CHECK-NEXT:   %inf = fdiv ppc_fp128 0xM3FF00000000000000000000000000000, 0xM00000000000000000000000000000000 => ppc_fp128 +Inf
+; CHECK-NEXT:   %nan = fdiv ppc_fp128 0xM00000000000000000000000000000000, 0xM00000000000000000000000000000000 => ppc_fp128 0x0000000000000000FFF8000000000000
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main

>From 7d20161ca50dd952b244c3f5c72dfa8bd9bbf10d Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhige_chen at outlook.com>
Date: Thu, 2 Apr 2026 15:55:56 +0800
Subject: [PATCH 16/20] [llubi] Add nneg handling to uitofp & related test
 update

---
 llvm/test/tools/llubi/fp_cast.ll     | 10 ++++++++++
 llvm/tools/llubi/lib/Interpreter.cpp |  5 +++++
 2 files changed, 15 insertions(+)

diff --git a/llvm/test/tools/llubi/fp_cast.ll b/llvm/test/tools/llubi/fp_cast.ll
index bab205f1c5891..00e1578d72395 100644
--- a/llvm/test/tools/llubi/fp_cast.ll
+++ b/llvm/test/tools/llubi/fp_cast.ll
@@ -8,12 +8,18 @@ define void @main() {
   %si2fp = sitofp i32 -2 to float
   %ui2fp = uitofp i32 255 to float
 
+  %ui2fp_nneg_pos = uitofp nneg i32 255 to float
+  %ui2fp_nneg_neg = uitofp nneg i32 -255 to float
+
   %fp2si = fptosi double -4.75 to i32
   %fp2ui = fptoui double 4.75 to i32
 
   %oob_ui = fptoui float -1.0 to i32
   %nan_si = fptosi float 0x7FF8000000000000 to i32
 
+  %oob_si_half = sitofp i32 -100000 to half
+  %oob_ui_half = uitofp i32 100000 to half
+
   ret void
 }
 ; CHECK: Entering function: main
@@ -21,9 +27,13 @@ define void @main() {
 ; CHECK-NEXT:   %trunc = fptrunc double 3.500000e+00 to float => float 3.500000e+00
 ; CHECK-NEXT:   %si2fp = sitofp i32 -2 to float => float -2.000000e+00
 ; CHECK-NEXT:   %ui2fp = uitofp i32 255 to float => float 2.550000e+02
+; CHECK-NEXT:   %ui2fp_nneg_pos = uitofp nneg i32 255 to float => float 2.550000e+02
+; CHECK-NEXT:   %ui2fp_nneg_neg = uitofp nneg i32 -255 to float => poison
 ; CHECK-NEXT:   %fp2si = fptosi double -4.750000e+00 to i32 => i32 -4
 ; CHECK-NEXT:   %fp2ui = fptoui double 4.750000e+00 to i32 => i32 4
 ; CHECK-NEXT:   %oob_ui = fptoui float -1.000000e+00 to i32 => poison
 ; CHECK-NEXT:   %nan_si = fptosi float 0x7FF8000000000000 to i32 => poison
+; CHECK-NEXT:   %oob_si_half = sitofp i32 -100000 to half => half -Inf
+; CHECK-NEXT:   %oob_ui_half = uitofp i32 100000 to half => half +Inf
 ; CHECK-NEXT:   ret void
 ; CHECK-NEXT: Exiting function: main
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index 787fa1fc88849..a0230f48c3ef6 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -1029,6 +1029,11 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
       if (Operand.isPoison())
         return AnyValue::poison();
 
+      APInt IOperand = Operand.asInteger();
+
+      if (isa<UIToFPInst>(I) && I.hasNonNeg() && IOperand.isNegative())
+        return AnyValue::poison();
+
       APFloat Res(DstSem);
 
       Res.convertFromAPInt(Operand.asInteger(), /*IsSigned=*/IsSigned,

>From 966f4f282c31f431b3a4b6c8f9e43b5317738ebe Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhige_chen at outlook.com>
Date: Thu, 2 Apr 2026 16:05:40 +0800
Subject: [PATCH 17/20] [llubi] Test update for the nsz flag

---
 llvm/test/tools/llubi/fp_fastmath.ll | 5 +++++
 llvm/tools/llubi/lib/Interpreter.cpp | 4 +---
 2 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/llvm/test/tools/llubi/fp_fastmath.ll b/llvm/test/tools/llubi/fp_fastmath.ll
index 475f40d2b0457..ba1cf3fca9c42 100644
--- a/llvm/test/tools/llubi/fp_fastmath.ll
+++ b/llvm/test/tools/llubi/fp_fastmath.ll
@@ -8,11 +8,16 @@ define void @main() {
 
   %safe = fadd nnan ninf float 1.0, 2.0
 
+  %nsz_1 = fneg nsz float -0.0
+  %nsz_2 = fneg nsz float -0.0
+
   ret void
 }
 ; CHECK: Entering function: main
 ; CHECK-NEXT:   %bad_nan = fdiv nnan float 0.000000e+00, 0.000000e+00 => poison
 ; CHECK-NEXT:   %bad_inf = fdiv ninf float 1.000000e+00, 0.000000e+00 => poison
 ; CHECK-NEXT:   %safe = fadd nnan ninf float 1.000000e+00, 2.000000e+00 => float 3.000000e+00
+; CHECK-NEXT:   %nsz_1 = fneg nsz float -0.000000e+00 => float 0.000000e+00
+; CHECK-NEXT:   %nsz_2 = fneg nsz float -0.000000e+00 => float -0.000000e+00
 ; CHECK-NEXT:   ret void
 ; CHECK-NEXT: Exiting function: main
diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index a0230f48c3ef6..d872d73598fdc 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -120,11 +120,9 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
       return AnyValue::poison();
     if (FMF.noInfs() && APVal.isInfinity())
       return AnyValue::poison();
-    // Non-deterministically flip the sign of the input.
-    if (FMF.noSignedZeros() && APVal.isZero()) {
+    if (FMF.noSignedZeros() && APVal.isZero())
       return AnyValue(APFloat::getZero(
           APVal.getSemantics(), APVal.isNegative() ^ Ctx.getRandomBool()));
-    }
     return Val;
   }
 

>From 0b0b2a7e23973d4792049cc6471b6449895eccd0 Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhige_chen at outlook.com>
Date: Thu, 2 Apr 2026 16:52:06 +0800
Subject: [PATCH 18/20] [llubi] New tests for NaN propagation

---
 .../test/tools/llubi/fp_nan_platform_specific.ll | 16 ++++++++++++++++
 llvm/test/tools/llubi/fp_nan_preferred.ll        | 16 ++++++++++++++++
 llvm/test/tools/llubi/fp_nan_quieting.ll         | 16 ++++++++++++++++
 llvm/test/tools/llubi/fp_nan_unchanged.ll        | 16 ++++++++++++++++
 4 files changed, 64 insertions(+)
 create mode 100644 llvm/test/tools/llubi/fp_nan_platform_specific.ll
 create mode 100644 llvm/test/tools/llubi/fp_nan_preferred.ll
 create mode 100644 llvm/test/tools/llubi/fp_nan_quieting.ll
 create mode 100644 llvm/test/tools/llubi/fp_nan_unchanged.ll

diff --git a/llvm/test/tools/llubi/fp_nan_platform_specific.ll b/llvm/test/tools/llubi/fp_nan_platform_specific.ll
new file mode 100644
index 0000000000000..74665faf1a224
--- /dev/null
+++ b/llvm/test/tools/llubi/fp_nan_platform_specific.ll
@@ -0,0 +1,16 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: llubi --ubi::NaNPropagationBehavior::TargetSpecificNaN --verbose < %s 2>&1 | FileCheck %s\
+
+define void @main() {
+    %qnan_zero_payload = fadd double 1.0, 0x7FF8000000000000
+    %qnan_nonzero_payload = fadd double 1.0, 0x7FF8000000000001
+    %snan = fadd double 1.0, 0x7FF0000000000001
+
+    ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %qnan_zero_payload = fadd double 1.000000e+00, 0x7FF8000000000000 => double 0x7FFD3A7C3E40F98B
+; CHECK-NEXT:   %qnan_nonzero_payload = fadd double 1.000000e+00, 0x7FF8000000000001 => double 0xFFFD6903CEE3FCEE
+; CHECK-NEXT:   %snan = fadd double 1.000000e+00, 0x7FF0000000000001 => double 0x7FFA062AFD5FE9EE
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/fp_nan_preferred.ll b/llvm/test/tools/llubi/fp_nan_preferred.ll
new file mode 100644
index 0000000000000..f85784868e796
--- /dev/null
+++ b/llvm/test/tools/llubi/fp_nan_preferred.ll
@@ -0,0 +1,16 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: llubi --ubi::NaNPropagationBehavior::PreferredNaN --verbose < %s 2>&1 | FileCheck %s
+
+define void @main() {
+    %qnan_zero_payload = fadd double 1.0, 0x7FF8000000000000
+    %qnan_nonzero_payload = fadd double 1.0, 0x7FF8000000000001
+    %snan = fadd double 1.0, 0x7FF0000000000001
+
+    ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %qnan_zero_payload = fadd double 1.000000e+00, 0x7FF8000000000000 => double NaN
+; CHECK-NEXT:   %qnan_nonzero_payload = fadd double 1.000000e+00, 0x7FF8000000000001 => double 0xFFF8000000000000
+; CHECK-NEXT:   %snan = fadd double 1.000000e+00, 0x7FF0000000000001 => double 0xFFF8000000000000
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/fp_nan_quieting.ll b/llvm/test/tools/llubi/fp_nan_quieting.ll
new file mode 100644
index 0000000000000..cf6db52e57a38
--- /dev/null
+++ b/llvm/test/tools/llubi/fp_nan_quieting.ll
@@ -0,0 +1,16 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: llubi --ubi::NaNPropagationBehavior::QuietingNaN --verbose < %s 2>&1 | FileCheck %s
+
+define void @main() {
+    %qnan_zero_payload = fadd double 1.0, 0x7FF8000000000000
+    %qnan_nonzero_payload = fadd double 1.0, 0x7FF8000000000001
+    %snan = fadd double 1.0, 0x7FF0000000000001
+
+    ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %qnan_zero_payload = fadd double 1.000000e+00, 0x7FF8000000000000 => double NaN
+; CHECK-NEXT:   %qnan_nonzero_payload = fadd double 1.000000e+00, 0x7FF8000000000001 => double 0xFFF8000000000001
+; CHECK-NEXT:   %snan = fadd double 1.000000e+00, 0x7FF0000000000001 => double 0xFFF8000000000001
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main
diff --git a/llvm/test/tools/llubi/fp_nan_unchanged.ll b/llvm/test/tools/llubi/fp_nan_unchanged.ll
new file mode 100644
index 0000000000000..63edb6859ce65
--- /dev/null
+++ b/llvm/test/tools/llubi/fp_nan_unchanged.ll
@@ -0,0 +1,16 @@
+; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
+; RUN: llubi --ubi::NaNPropagationBehavior::UnchangedNaN --verbose < %s 2>&1 | FileCheck %s
+
+define void @main() {
+    %qnan_zero_payload = fadd double 1.0, 0x7FF8000000000000
+    %qnan_nonzero_payload = fadd double 1.0, 0x7FF8000000000001
+    %snan = fadd double 1.0, 0x7FF0000000000001
+
+    ret void
+}
+; CHECK: Entering function: main
+; CHECK-NEXT:   %qnan_zero_payload = fadd double 1.000000e+00, 0x7FF8000000000000 => double NaN
+; CHECK-NEXT:   %qnan_nonzero_payload = fadd double 1.000000e+00, 0x7FF8000000000001 => double 0xFFF8000000000001
+; CHECK-NEXT:   %snan = fadd double 1.000000e+00, 0x7FF0000000000001 => double 0xFFF0000000000001
+; CHECK-NEXT:   ret void
+; CHECK-NEXT: Exiting function: main

>From 8c2a6690581c61aedab5dadc52d267d789b9e1af Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhige_chen at outlook.com>
Date: Thu, 2 Apr 2026 16:58:15 +0800
Subject: [PATCH 19/20] [llubi] Fix getCurrentDenormalMode()

---
 llvm/tools/llubi/lib/Interpreter.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/llvm/tools/llubi/lib/Interpreter.cpp b/llvm/tools/llubi/lib/Interpreter.cpp
index d872d73598fdc..e54b1271b5bef 100644
--- a/llvm/tools/llubi/lib/Interpreter.cpp
+++ b/llvm/tools/llubi/lib/Interpreter.cpp
@@ -475,7 +475,7 @@ class InstExecutor : public InstVisitor<InstExecutor, void>,
 
   DenormalMode getCurrentDenormalMode(Instruction &I) {
     return CurrentFrame->Func.getDenormalMode(
-        I.getType()->getScalarType()->getFltSemantics());
+        I.getOperand(0)->getType()->getScalarType()->getFltSemantics());
   }
 
 public:

>From 1785cf3170e7c14906e2b56e60ca360066e0d22f Mon Sep 17 00:00:00 2001
From: Zhige Chen <zhige_chen at outlook.com>
Date: Thu, 2 Apr 2026 19:05:52 +0800
Subject: [PATCH 20/20] [llubi] Fix incorrect RUN command in test

---
 llvm/test/tools/llubi/fp_nan_platform_specific.ll | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/llvm/test/tools/llubi/fp_nan_platform_specific.ll b/llvm/test/tools/llubi/fp_nan_platform_specific.ll
index 74665faf1a224..dd59663b8e76d 100644
--- a/llvm/test/tools/llubi/fp_nan_platform_specific.ll
+++ b/llvm/test/tools/llubi/fp_nan_platform_specific.ll
@@ -1,5 +1,5 @@
 ; NOTE: Assertions have been autogenerated by utils/update_llubi_test_checks.py UTC_ARGS: --version 6
-; RUN: llubi --ubi::NaNPropagationBehavior::TargetSpecificNaN --verbose < %s 2>&1 | FileCheck %s\
+; RUN: llubi --ubi::NaNPropagationBehavior::TargetSpecificNaN --verbose < %s 2>&1 | FileCheck %s
 
 define void @main() {
     %qnan_zero_payload = fadd double 1.0, 0x7FF8000000000000



More information about the llvm-commits mailing list