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

Zhige Chen via llvm-commits llvm-commits at lists.llvm.org
Fri Mar 27 00:16:03 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 1/8] [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 2/8] [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 3/8] [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 4/8] [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 5/8] [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 6/8] [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 7/8] [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 8/8] [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



More information about the llvm-commits mailing list