Rework PointerNullState APIs

These now return Formulas instead of BoolValues, which drops the requirement
to be atomic bools, and opens the door to true/false becoming Formula primitives
rather than Atoms in the framework (and formula simplification).

The connection between nonnull annotations and the is_null property is now
expressed as a global invariant, so it works properly when is_null is neither
exactly true/false nor unconstrained - i.e. when it's restricted by a symbolic
nullability variable in inference.

PiperOrigin-RevId: 567282695
Change-Id: I2e8ac1c173c8bb1526a357d3eafc08935ec8a2da
diff --git a/nullability/BUILD b/nullability/BUILD
index 0ee6158..7b06d42 100644
--- a/nullability/BUILD
+++ b/nullability/BUILD
@@ -100,6 +100,7 @@
         "//nullability/test:__pkg__",
     ],
     deps = [
+        ":type_nullability",
         "@llvm-project//clang:analysis",
         "@llvm-project//clang:ast",
         "@llvm-project//clang:basic",
@@ -112,6 +113,7 @@
     srcs = ["pointer_nullability_test.cc"],
     deps = [
         ":pointer_nullability",
+        ":type_nullability",
         "@llvm-project//clang:analysis",
         "@llvm-project//clang:ast",
         "@llvm-project//clang:basic",
diff --git a/nullability/inference/collect_evidence.cc b/nullability/inference/collect_evidence.cc
index 3505d0c..330bf33 100644
--- a/nullability/inference/collect_evidence.cc
+++ b/nullability/inference/collect_evidence.cc
@@ -126,8 +126,7 @@
       getPointerValueFromExpr(Target, Env);
   if (!DereferencedValue) return;
   auto &A = Env.getDataflowAnalysisContext().arena();
-  auto &NotIsNull =
-      A.makeNot(getPointerNullState(*DereferencedValue).second.formula());
+  auto &NotIsNull = A.makeNot(getPointerNullState(*DereferencedValue).IsNull);
 
   // If the flow conditions already imply the dereferenced value is not null,
   // then we don't have any new evidence of a necessary annotation.
diff --git a/nullability/pointer_nullability.cc b/nullability/pointer_nullability.cc
index 2d86787..495b72e 100644
--- a/nullability/pointer_nullability.cc
+++ b/nullability/pointer_nullability.cc
@@ -4,9 +4,12 @@
 
 #include "nullability/pointer_nullability.h"
 
-#include <utility>
+#include <cassert>
+#include <optional>
 
+#include "nullability/type_nullability.h"
 #include "clang/AST/Type.h"
+#include "clang/Analysis/FlowSensitive/DataflowAnalysisContext.h"
 #include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
 #include "clang/Analysis/FlowSensitive/Formula.h"
 #include "clang/Analysis/FlowSensitive/Value.h"
@@ -18,7 +21,9 @@
 
 using dataflow::AtomicBoolValue;
 using dataflow::BoolValue;
+using dataflow::DataflowAnalysisContext;
 using dataflow::Environment;
+using dataflow::Formula;
 using dataflow::PointerValue;
 using dataflow::Value;
 
@@ -41,41 +46,55 @@
          PointerVal.getProperty(kNull) != nullptr;
 }
 
-std::pair<AtomicBoolValue &, AtomicBoolValue &> getPointerNullState(
-    const PointerValue &PointerVal) {
+PointerNullState getPointerNullState(const PointerValue &PointerVal) {
   auto &FromNullable =
       *cast<AtomicBoolValue>(PointerVal.getProperty(kFromNullable));
   auto &Null = *cast<AtomicBoolValue>(PointerVal.getProperty(kNull));
-  return {FromNullable, Null};
+  return {FromNullable.formula(), Null.formula()};
 }
 
-void initPointerBoolProperty(PointerValue &PointerVal, llvm::StringRef Name,
-                             BoolValue *BoolVal, Environment &Env) {
-  if (PointerVal.getProperty(Name) != nullptr) return;
-  // The property must always be a non-null boolean atom.
-  if (!isa_and_nonnull<AtomicBoolValue>(BoolVal)) {
-    auto &Atom = Env.makeAtomicBoolValue();
-    if (BoolVal)
-      Env.addToFlowCondition(
-          Env.arena().makeEquals(Atom.formula(), BoolVal->formula()));
-    BoolVal = &Atom;
+static bool tryCreatePointerNullState(PointerValue &PointerVal,
+                                      dataflow::Arena &A,
+                                      const Formula *FromNullable = nullptr,
+                                      const Formula *IsNull = nullptr) {
+  // TODO: for now we assume that we have both nullability properties, or none.
+  // We'll need to relax this when properties can be independently widened away.
+  if (hasPointerNullState(PointerVal)) return false;
+  if (!FromNullable) FromNullable = &A.makeAtomRef(A.makeAtom());
+  if (!IsNull) IsNull = &A.makeAtomRef(A.makeAtom());
+  PointerVal.setProperty(kFromNullable, A.makeBoolValue(*FromNullable));
+  PointerVal.setProperty(kNull, A.makeBoolValue(*IsNull));
+  return true;
+}
+
+void initPointerNullState(PointerValue &PointerVal,
+                          DataflowAnalysisContext &Ctx,
+                          std::optional<PointerTypeNullability> Source) {
+  auto &A = Ctx.arena();
+  if (tryCreatePointerNullState(PointerVal, A,
+                                Source ? &Source->isNullable(A) : nullptr)) {
+    // The symbolic/nonnull check is not needed for correctness, but it avoids
+    // adding meaningless (false => !null) invariant clauses.
+    // TODO: remove this once such clauses are recognized and dropped.
+    if (Source &&
+        (Source->isSymbolic() || Source == NullabilityKind::NonNull)) {
+      const Formula &IsNull = getPointerNullState(PointerVal).IsNull;
+      Ctx.addInvariant(A.makeImplies(Source->isNonnull(A), A.makeNot(IsNull)));
+    }
   }
-  PointerVal.setProperty(Name, BoolVal ? *BoolVal : Env.makeAtomicBoolValue());
 }
 
-void initPointerNullState(PointerValue &PointerVal, Environment &Env,
-                          BoolValue *FromNullableConstraint,
-                          BoolValue *NullConstraint) {
-  initPointerBoolProperty(PointerVal, kFromNullable, FromNullableConstraint,
-                          Env);
-  initPointerBoolProperty(PointerVal, kNull, NullConstraint, Env);
+void initNullPointer(PointerValue &PointerVal, DataflowAnalysisContext &Ctx) {
+  tryCreatePointerNullState(PointerVal, Ctx.arena(),
+                            /*FromNullable=*/&Ctx.arena().makeLiteral(true),
+                            /*IsNull=*/&Ctx.arena().makeLiteral(true));
 }
 
 bool isNullable(const PointerValue &PointerVal, const Environment &Env,
                 const dataflow::Formula *AdditionalConstraints) {
   auto &A = Env.getDataflowAnalysisContext().arena();
   auto [FromNullable, Null] = getPointerNullState(PointerVal);
-  auto *ForseeablyNull = &A.makeAnd(FromNullable.formula(), Null.formula());
+  auto *ForseeablyNull = &A.makeAnd(FromNullable, Null);
   if (AdditionalConstraints)
     ForseeablyNull = &A.makeAnd(*AdditionalConstraints, *ForseeablyNull);
   return !Env.flowConditionImplies(A.makeNot(*ForseeablyNull));
@@ -85,7 +104,7 @@
                                const dataflow::Environment &Env,
                                const dataflow::Formula *AdditionalConstraints) {
   auto &A = Env.getDataflowAnalysisContext().arena();
-  auto *Null = &getPointerNullState(PointerVal).second.formula();
+  auto *Null = &getPointerNullState(PointerVal).IsNull;
   if (AdditionalConstraints) Null = &A.makeAnd(*AdditionalConstraints, *Null);
   if (Env.flowConditionImplies(A.makeNot(*Null)))
     return NullabilityKind::NonNull;
diff --git a/nullability/pointer_nullability.h b/nullability/pointer_nullability.h
index 7dfef80..8e156bb 100644
--- a/nullability/pointer_nullability.h
+++ b/nullability/pointer_nullability.h
@@ -13,12 +13,15 @@
 //    (e.g. a nullptr literal, or a reference to a Nullable-annotated variable)
 //    If this is false, dereferencing may be safe: we don't know the contract.
 
-#include <utility>
+#include <optional>
 
+#include "nullability/type_nullability.h"
 #include "clang/AST/ASTContext.h"
 #include "clang/AST/ASTDumper.h"
 #include "clang/AST/Expr.h"
+#include "clang/Analysis/FlowSensitive/DataflowAnalysisContext.h"
 #include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
+#include "clang/Analysis/FlowSensitive/Formula.h"
 #include "clang/Analysis/FlowSensitive/Value.h"
 #include "clang/Basic/Specifiers.h"
 
@@ -39,70 +42,47 @@
 // PointerNullabilityAnalysis, not by the dataflow framework.
 bool hasPointerNullState(const dataflow::PointerValue &PointerVal);
 
+/// The properties representing nullness information for a pointer.
+///
+/// We attach these properties to every PointerValue taken by an expression.
+struct PointerNullState {
+  /// Did the pointer come from a known-nullable source?
+  const dataflow::Formula &FromNullable;
+  /// Is the pointer's value null?
+  const dataflow::Formula &IsNull;
+  // These are independent: sources with unknown nullability can yield nullptr!
+};
+
 /// Returns the properties representing the nullness information of a pointer.
-///
-/// The first indicates if the pointer's value is from a known-nullable source.
-/// The second indicates if the pointer's value is null.
-///
-/// These are independent: sources with unknown nullability can yield nullptr.
-std::pair<dataflow::AtomicBoolValue &, dataflow::AtomicBoolValue &>
-getPointerNullState(const dataflow::PointerValue &PointerVal);
+PointerNullState getPointerNullState(const dataflow::PointerValue &PointerVal);
 
-/// Sets the nullness properties on `PointerVal` if not already initialised.
+/// Creates the nullness properties on `PointerVal` if not already initialised.
 ///
-/// The boolean properties may be constrained by `FromNullableConstraint`
-/// and `NullConstraint`. Otherwise, the properties are set to freshly
-/// created atomic booleans.
-void initPointerNullState(dataflow::PointerValue &PointerVal,
-                          dataflow::Environment &Env,
-                          dataflow::BoolValue *FromNullableConstraint = nullptr,
-                          dataflow::BoolValue *NullConstraint = nullptr);
+/// We call this when the framework produces a PointerValue for an expression.
+/// This ensures that the variable has usable "from nullable" and "is null"
+/// boolean variables, and that they are constrained based on the *original*
+/// source of the PointerValue.
+///
+/// For example:
+///    Unknown<int> *x = makeNullable();
+///                      ~~~~~~~~~~~~~~ <-- initPointerNullState(Nullable)
+///    *x;
+///     ~ <-- initPointerNullState(Unknown) - no effect, already initialized
+///
+/// The constraints are added to the context as a non-flow-sensitive invariant,
+/// so the source nullability may not depend on flow-sensitive information.
+///
+/// (We assume that the framework will not provide the same pointer from
+/// different initial sources, so the `Source` nullability is the same
+/// regardless of block evaluation order).
+void initPointerNullState(
+    dataflow::PointerValue &PointerVal, dataflow::DataflowAnalysisContext &Ctx,
+    std::optional<PointerTypeNullability> Source = std::nullopt);
 
-/// Sets the nullness properties on `PointerVal` representing a nullptr if not
-/// already initialised.
-///
-/// `Known` is constrained to true, `Null` is constrained to true.
-inline void initNullPointer(dataflow::PointerValue &PointerVal,
-                            dataflow::Environment &Env) {
-  initPointerNullState(
-      PointerVal, Env,
-      /*FromNullableConstraint=*/&Env.getBoolLiteralValue(true),
-      /*NullConstraint=*/&Env.getBoolLiteralValue(true));
-}
-
-/// Sets the nullness properties on `PointerVal` representing a pointer that is
-/// not null if not already initialised.
-///
-/// `Known` is constrained to true, `Null` is constrained to false.
-inline void initNotNullPointer(dataflow::PointerValue &PointerVal,
-                               dataflow::Environment &Env) {
-  initPointerNullState(
-      PointerVal, Env,
-      /*FromNullableConstraint=*/&Env.getBoolLiteralValue(false),
-      /*NullConstraint=*/&Env.getBoolLiteralValue(false));
-}
-
-/// Sets the nullness properties on `PointerVal` representing a pointer that is
-/// nullable if not already initialised.
-///
-/// `Known` is constrained to true, `Null` is unconstrained.
-inline void initNullablePointer(dataflow::PointerValue &PointerVal,
-                                dataflow::Environment &Env) {
-  initPointerNullState(
-      PointerVal, Env,
-      /*FromNullableConstraint=*/&Env.getBoolLiteralValue(true));
-}
-
-/// Sets the nullness properties on `PointerVal` representing a pointer with
-/// unknown nullability if not already initialised.
-///
-/// `Known` is constrained to false, `Null` is unconstrained.
-inline void initUnknownPointer(dataflow::PointerValue &PointerVal,
-                               dataflow::Environment &Env) {
-  initPointerNullState(
-      PointerVal, Env,
-      /*FromNullableConstraint=*/&Env.getBoolLiteralValue(false));
-}
+/// Variant of initPointerNullState, where the pointer is guaranteed null.
+/// (This is flow-insensitive, but PointerTypeNullability can't represent it).
+void initNullPointer(dataflow::PointerValue &PointerVal,
+                     dataflow::DataflowAnalysisContext &Ctx);
 
 /// Returns true if there is evidence that `PointerVal` may hold a nullptr.
 bool isNullable(const dataflow::PointerValue &PointerVal,
diff --git a/nullability/pointer_nullability_analysis.cc b/nullability/pointer_nullability_analysis.cc
index 25fc9ea..2624727 100644
--- a/nullability/pointer_nullability_analysis.cc
+++ b/nullability/pointer_nullability_analysis.cc
@@ -40,6 +40,7 @@
 using dataflow::BoolValue;
 using dataflow::CFGMatchSwitchBuilder;
 using dataflow::Environment;
+using dataflow::Formula;
 using dataflow::PointerValue;
 using dataflow::StorageLocation;
 using dataflow::TransferState;
@@ -264,40 +265,15 @@
 void initPointerFromTypeNullability(
     PointerValue &PointerVal, const Expr *E,
     TransferState<PointerNullabilityLattice> &State) {
-  if (auto Nullability = getPointerTypeNullability(E, State.Lattice);
-      Nullability.isSymbolic()) {
-    auto &Arena = State.Env.getDataflowAnalysisContext().arena();
-    auto &Nonnull = Nullability.isNonnull(Arena);
-    auto &Nullable = Nullability.isNullable(Arena);
-    // from_nullable = nullable
-    initPointerNullState(PointerVal, State.Env, &Arena.makeBoolValue(Nullable));
-    // nonnull => !is_null
-    auto [FromNullable, IsNull] = getPointerNullState(PointerVal);
-    State.Env.addToFlowCondition(
-        Arena.makeImplies(Nonnull, Arena.makeNot(IsNull.formula())));
-  } else {
-    // TODO: The above code should also handle concrete nullability correctly.
-    //       But right now, the formulas it creates are overcomplicated.
-    //       Eliminate this case once we simplify true/false in formulas, and
-    //       make addToFlowCondition(true) a no-op.
-    switch (Nullability.concrete()) {
-      case NullabilityKind::NonNull:
-        initNotNullPointer(PointerVal, State.Env);
-        break;
-      case NullabilityKind::Nullable:
-        initNullablePointer(PointerVal, State.Env);
-        break;
-      default:
-        initUnknownPointer(PointerVal, State.Env);
-    }
-  }
+  initPointerNullState(PointerVal, State.Env.getDataflowAnalysisContext(),
+                       getPointerTypeNullability(E, State.Lattice));
 }
 
 void transferFlowSensitiveNullPointer(
     const Expr *NullPointer, const MatchFinder::MatchResult &,
     TransferState<PointerNullabilityLattice> &State) {
   if (auto *PointerVal = getPointerValueFromExpr(NullPointer, State.Env)) {
-    initNullPointer(*PointerVal, State.Env);
+    initNullPointer(*PointerVal, State.Env.getDataflowAnalysisContext());
   }
 }
 
@@ -305,7 +281,8 @@
     const Expr *NotNullPointer, const MatchFinder::MatchResult &,
     TransferState<PointerNullabilityLattice> &State) {
   if (auto *PointerVal = getPointerValueFromExpr(NotNullPointer, State.Env)) {
-    initNotNullPointer(*PointerVal, State.Env);
+    initPointerNullState(*PointerVal, State.Env.getDataflowAnalysisContext(),
+                         NullabilityKind::NonNull);
   }
 }
 
@@ -330,26 +307,24 @@
   if (!LHS || !RHS) return;
   if (!hasPointerNullState(*LHS) || !hasPointerNullState(*RHS)) return;
 
-  BoolValue &LHSNullVal = getPointerNullState(*LHS).second;
-  BoolValue &RHSNullVal = getPointerNullState(*RHS).second;
-  auto &LHSNull = LHSNullVal.formula();
-  auto &RHSNull = RHSNullVal.formula();
+  auto &LHSNull = getPointerNullState(*LHS).IsNull;
+  auto &RHSNull = getPointerNullState(*RHS).IsNull;
 
   // Special case: Are we comparing against `nullptr`?
   // We can avoid modifying the flow condition in this case and simply propagate
   // the nullability of the other operand (potentially with a negation).
   if (&LHSNull == &A.makeLiteral(true)) {
     if (BinaryOp->getOpcode() == BO_EQ)
-      State.Env.setValue(*BinaryOp, RHSNullVal);
+      State.Env.setValue(*BinaryOp, A.makeBoolValue(RHSNull));
     else
-      State.Env.setValue(*BinaryOp, State.Env.makeNot(RHSNullVal));
+      State.Env.setValue(*BinaryOp, A.makeBoolValue(A.makeNot(RHSNull)));
     return;
   }
   if (&RHSNull == &A.makeLiteral(true)) {
     if (BinaryOp->getOpcode() == BO_EQ)
-      State.Env.setValue(*BinaryOp, LHSNullVal);
+      State.Env.setValue(*BinaryOp, A.makeBoolValue(LHSNull));
     else
-      State.Env.setValue(*BinaryOp, State.Env.makeNot(LHSNullVal));
+      State.Env.setValue(*BinaryOp, A.makeBoolValue(A.makeNot(LHSNull)));
     return;
   }
 
@@ -380,12 +355,13 @@
 void transferFlowSensitiveNullCheckImplicitCastPtrToBool(
     const Expr *CastExpr, const MatchFinder::MatchResult &,
     TransferState<PointerNullabilityLattice> &State) {
+  auto &A = State.Env.arena();
   auto *PointerVal =
       getPointerValueFromExpr(CastExpr->IgnoreImplicit(), State.Env);
   if (!PointerVal) return;
 
-  auto [FromNullable, PointerNull] = getPointerNullState(*PointerVal);
-  State.Env.setValue(*CastExpr, State.Env.makeNot(PointerNull));
+  auto Nullability = getPointerNullState(*PointerVal);
+  State.Env.setValue(*CastExpr, A.makeBoolValue(A.makeNot(Nullability.IsNull)));
 }
 
 void initializeOutputParameter(const Expr *Arg, dataflow::Environment &Env,
@@ -426,7 +402,8 @@
 
   auto *InnerPointer =
       cast<PointerValue>(Env.createValue(ParamTy->getPointeeType()));
-  initUnknownPointer(*InnerPointer, Env);
+  initPointerNullState(*InnerPointer, Env.getDataflowAnalysisContext(),
+                       NullabilityKind::Unspecified);
 
   Env.setValue(*Loc, *InnerPointer);
 }
@@ -857,9 +834,11 @@
   FlowSensitiveTransferer(Elt, getASTContext(), State);
 }
 
-static BoolValue &mergeBoolValues(BoolValue &Bool1, const Environment &Env1,
-                                  BoolValue &Bool2, const Environment &Env2,
-                                  Environment &MergedEnv) {
+static const Formula &mergeFormulas(const Formula &Bool1,
+                                    const Environment &Env1,
+                                    const Formula &Bool2,
+                                    const Environment &Env2,
+                                    Environment &MergedEnv) {
   if (&Bool1 == &Bool2) {
     return Bool1;
   }
@@ -872,11 +851,10 @@
   // path taken - this simplifies the flow condition tracked in `MergedEnv`.
   // Otherwise, information about which path was taken is used to associate
   // `MergedBool` with `Bool1` and `Bool2`.
-  if (Env1.flowConditionImplies(Bool1.formula()) &&
-      Env2.flowConditionImplies(Bool2.formula())) {
+  if (Env1.flowConditionImplies(Bool1) && Env2.flowConditionImplies(Bool2)) {
     MergedEnv.addToFlowCondition(MergedBool);
-  } else if (Env1.flowConditionImplies(A.makeNot(Bool1.formula())) &&
-             Env2.flowConditionImplies(A.makeNot(Bool2.formula()))) {
+  } else if (Env1.flowConditionImplies(A.makeNot(Bool1)) &&
+             Env2.flowConditionImplies(A.makeNot(Bool2))) {
     MergedEnv.addToFlowCondition(A.makeNot(MergedBool));
   } else {
     // TODO(b/233582219): Flow conditions are not necessarily mutually
@@ -884,13 +862,11 @@
     // this section when the patch is commited.
     auto FC1 = Env1.getFlowConditionToken();
     auto FC2 = Env2.getFlowConditionToken();
-    MergedEnv.addToFlowCondition(
-        A.makeOr(A.makeAnd(A.makeAtomRef(FC1),
-                           A.makeEquals(MergedBool, Bool1.formula())),
-                 A.makeAnd(A.makeAtomRef(FC2),
-                           A.makeEquals(MergedBool, Bool2.formula()))));
+    MergedEnv.addToFlowCondition(A.makeOr(
+        A.makeAnd(A.makeAtomRef(FC1), A.makeEquals(MergedBool, Bool1)),
+        A.makeAnd(A.makeAtomRef(FC2), A.makeEquals(MergedBool, Bool2))));
   }
-  return A.makeBoolValue(MergedBool);
+  return MergedBool;
 }
 
 bool PointerNullabilityAnalysis::merge(QualType Type, const Value &Val1,
@@ -908,15 +884,22 @@
     return false;
   }
 
-  auto [FromNullable1, Null1] = getPointerNullState(cast<PointerValue>(Val1));
-  auto [FromNullable2, Null2] = getPointerNullState(cast<PointerValue>(Val2));
+  auto Nullability1 = getPointerNullState(cast<PointerValue>(Val1));
+  auto Nullability2 = getPointerNullState(cast<PointerValue>(Val2));
 
   auto &FromNullable =
-      mergeBoolValues(FromNullable1, Env1, FromNullable2, Env2, MergedEnv);
-  auto &Null = mergeBoolValues(Null1, Env1, Null2, Env2, MergedEnv);
+      mergeFormulas(Nullability1.FromNullable, Env1, Nullability2.FromNullable,
+                    Env2, MergedEnv);
+  auto &Null = mergeFormulas(Nullability1.IsNull, Env1, Nullability2.IsNull,
+                             Env2, MergedEnv);
 
-  initPointerNullState(cast<PointerValue>(MergedVal), MergedEnv, &FromNullable,
-                       &Null);
+  initPointerNullState(cast<PointerValue>(MergedVal),
+                       MergedEnv.getDataflowAnalysisContext());
+  auto MergedNullability = getPointerNullState(cast<PointerValue>(MergedVal));
+  auto &A = MergedEnv.arena();
+  MergedEnv.addToFlowCondition(
+      A.makeEquals(MergedNullability.FromNullable, FromNullable));
+  MergedEnv.addToFlowCondition(A.makeEquals(MergedNullability.IsNull, Null));
 
   return true;
 }
diff --git a/nullability/pointer_nullability_analysis_test.cc b/nullability/pointer_nullability_analysis_test.cc
index ef84a92..61eb15d 100644
--- a/nullability/pointer_nullability_analysis_test.cc
+++ b/nullability/pointer_nullability_analysis_test.cc
@@ -71,21 +71,20 @@
   auto *Ret =
       dyn_cast_or_null<dataflow::PointerValue>(ExitState.Env.getReturnValue());
   ASSERT_NE(Ret, nullptr);
-  auto [RetFromNullable, RetNull] = getPointerNullState(*Ret);
+  auto State = getPointerNullState(*Ret);
 
   // The param nullability hasn't been fixed.
   EXPECT_EQ(std::nullopt, evaluate(PN.isNonnull(A), ExitState.Env));
   EXPECT_EQ(std::nullopt, evaluate(PN.isNullable(A), ExitState.Env));
   // Nor has the the nullability of the returned pointer.
-  EXPECT_EQ(std::nullopt, evaluate(RetFromNullable.formula(), ExitState.Env));
-  EXPECT_EQ(std::nullopt, evaluate(RetNull.formula(), ExitState.Env));
+  EXPECT_EQ(std::nullopt, evaluate(State.FromNullable, ExitState.Env));
+  EXPECT_EQ(std::nullopt, evaluate(State.IsNull, ExitState.Env));
   // However, the two are linked as expected.
-  EXPECT_EQ(true, evaluate(A.makeImplies(PN.isNonnull(A),
-                                         A.makeNot(RetNull.formula())),
-                           ExitState.Env));
   EXPECT_EQ(true,
-            evaluate(A.makeEquals(PN.isNullable(A), RetFromNullable.formula()),
+            evaluate(A.makeImplies(PN.isNonnull(A), A.makeNot(State.IsNull)),
                      ExitState.Env));
+  EXPECT_EQ(true, evaluate(A.makeEquals(PN.isNullable(A), State.FromNullable),
+                           ExitState.Env));
 }
 
 }  // namespace
diff --git a/nullability/pointer_nullability_test.cc b/nullability/pointer_nullability_test.cc
index f410358..67e47de 100644
--- a/nullability/pointer_nullability_test.cc
+++ b/nullability/pointer_nullability_test.cc
@@ -6,6 +6,7 @@
 
 #include <memory>
 
+#include "nullability/type_nullability.h"
 #include "clang/AST/Type.h"
 #include "clang/Analysis/FlowSensitive/DataflowAnalysisContext.h"
 #include "clang/Analysis/FlowSensitive/Formula.h"
@@ -19,12 +20,17 @@
 
 class NullabilityPropertiesTest : public ::testing::Test {
  protected:
-  dataflow::PointerValue &makePointer(const dataflow::Formula &FromNullable,
-                                      const dataflow::Formula &Null) {
+  dataflow::PointerValue &makePointer(PointerTypeNullability N) {
     auto &P = Env.create<dataflow::PointerValue>(
         DACtx.createStorageLocation(QualType()));
-    initPointerNullState(P, Env, &Env.arena().makeBoolValue(FromNullable),
-                         &Env.arena().makeBoolValue(Null));
+    initPointerNullState(P, DACtx, N);
+    return P;
+  }
+
+  dataflow::PointerValue &makeNullPointer() {
+    auto &P = Env.create<dataflow::PointerValue>(
+        DACtx.createStorageLocation(QualType()));
+    initNullPointer(P, DACtx);
     return P;
   }
 
@@ -34,43 +40,46 @@
 };
 
 TEST_F(NullabilityPropertiesTest, Test) {
-  auto &True = DACtx.arena().makeLiteral(true);
-  auto &False = DACtx.arena().makeLiteral(false);
-  auto &False2 = DACtx.arena().makeNot(True);
+  auto &A = DACtx.arena();
 
-  EXPECT_TRUE(
-      isNullable(makePointer(/*FromNullable=*/True, /*Null=*/True), Env));
-  EXPECT_FALSE(
-      isNullable(makePointer(/*FromNullable=*/True, /*Null=*/False), Env));
-  EXPECT_FALSE(
-      isNullable(makePointer(/*FromNullable=*/False, /*Null=*/True), Env));
-  EXPECT_FALSE(
-      isNullable(makePointer(/*FromNullable=*/False, /*Null=*/False), Env));
+  EXPECT_TRUE(isNullable(makeNullPointer(), Env));
 
-  EXPECT_FALSE(
-      isNullable(makePointer(/*FromNullable=*/True, /*Null=*/False2), Env));
-  EXPECT_FALSE(
-      isNullable(makePointer(/*FromNullable=*/False2, /*Null=*/True), Env));
+  auto &NullableButNotNull = makePointer(NullabilityKind::Nullable);
+  EXPECT_TRUE(isNullable(NullableButNotNull, Env));
+  Env.addToFlowCondition(
+      A.makeNot(getPointerNullState(NullableButNotNull).IsNull));
+  EXPECT_FALSE(isNullable(NullableButNotNull, Env));
+
+  auto &NullableAndNull = makePointer(NullabilityKind::Nullable);
+  Env.addToFlowCondition(getPointerNullState(NullableAndNull).IsNull);
+  EXPECT_TRUE(isNullable(NullableAndNull, Env));
+
+  auto &NonnullAndNotNull = makePointer(NullabilityKind::NonNull);
+  EXPECT_FALSE(isNullable(NonnullAndNotNull, Env));
+  Env.addToFlowCondition(
+      A.makeNot(getPointerNullState(NonnullAndNotNull).IsNull));
+  EXPECT_FALSE(isNullable(NonnullAndNotNull, Env));
+
+  // This is a little surprising: if a pointer comes from a non-null source but
+  // is dynamically discovered to be definitely null, we still don't consider
+  // it nullable.
+  auto &NonnullAndNull = makePointer(NullabilityKind::NonNull);
+  Env.addToFlowCondition(getPointerNullState(NonnullAndNotNull).IsNull);
+  EXPECT_FALSE(isNullable(NonnullAndNull, Env));
 }
 
 TEST_F(NullabilityPropertiesTest, IsNullableAdditionalConstraints) {
-  auto &FromNullable = Env.makeAtomicBoolValue().formula();
-  auto &Null = Env.makeAtomicBoolValue().formula();
-  EXPECT_TRUE(isNullable(makePointer(FromNullable, Null), Env));
-
-  auto *NotNull = &DACtx.arena().makeNot(Null);
-  EXPECT_FALSE(isNullable(makePointer(FromNullable, Null), Env, NotNull));
+  auto &P = makePointer(NullabilityKind::Nullable);
+  EXPECT_TRUE(isNullable(P, Env));
+  auto *NotNull = &DACtx.arena().makeNot(getPointerNullState(P).IsNull);
+  EXPECT_FALSE(isNullable(P, Env, NotNull));
 }
 
 TEST_F(NullabilityPropertiesTest, GetNullabilityAdditionalConstraints) {
-  auto &FromNullable = Env.makeAtomicBoolValue().formula();
-  auto &Null = Env.makeAtomicBoolValue().formula();
-  EXPECT_EQ(getNullability(makePointer(FromNullable, Null), Env),
-            NullabilityKind::Nullable);
-
-  auto *NotNull = &DACtx.arena().makeNot(Null);
-  EXPECT_EQ(getNullability(makePointer(FromNullable, Null), Env, NotNull),
-            NullabilityKind::NonNull);
+  auto &P = makePointer(NullabilityKind::Nullable);
+  EXPECT_EQ(getNullability(P, Env), NullabilityKind::Nullable);
+  auto *NotNull = &DACtx.arena().makeNot(getPointerNullState(P).IsNull);
+  EXPECT_EQ(getNullability(P, Env, NotNull), NullabilityKind::NonNull);
 }
 
 }  // namespace