Skip to content

Commit d5aecf0

Browse files
authored
[clang][nullability] Don't discard expression state before end of full-expression. (#82611)
In #72985, I made a change to discard expression state (`ExprToLoc` and `ExprToVal`) at the beginning of each basic block. I did so with the claim that "we never need to access entries from these maps outside of the current basic block", noting that there are exceptions to this claim when control flow happens inside a full-expression (the operands of `&&`, `||`, and the conditional operator live in different basic blocks than the operator itself) but that we already have a mechanism for retrieving the values of these operands from the environment for the block they are computed in. It turns out, however, that the operands of these operators aren't the only expressions whose values can be accessed from a different basic block; when control flow happens within a full-expression, that control flow can be "interposed" between an expression and its parent. Here is an example: ```cxx void f(int*, int); bool cond(); void target() { int i = 0; f(&i, cond() ? 1 : 0); } ``` ([godbolt](https://godbolt.org/z/hrbj1Mj3o)) In the CFG[^1] , note how the expression for `&i` is computed in block B4, but the parent of this expression (the `CallExpr`) is located in block B1. The the argument expression `&i` and the `CallExpr` are essentially "torn apart" into different basic blocks by the conditional operator in the second argument. In other words, the edge between the `CallExpr` and its argument `&i` straddles the boundary between two blocks. I used to think that this scenario -- where an edge between an expression and one of its children straddles a block boundary -- could only happen between the expression that triggers the control flow (`&&`, `||`, or the conditional operator) and its children, but the example above shows that other expressions can be affected as well; the control flow is still triggered by `&&`, `||` or the conditional operator, but the expressions affected lie outside these operators. Discarding expression state too soon is harmful. For example, an analysis that checks the arguments of the `CallExpr` above would not be able to retrieve a value for the `&i` argument. This patch therefore ensures that we don't discard expression state before the end of a full-expression. In other cases -- when the evaluation of a full-expression is complete -- we still want to discard expression state for the reasons explained in #72985 (avoid performing joins on boolean values that are no longer needed, which unnecessarily extends the flow condition; improve debuggability by removing clutter from the expression state). The impact on performance from this change is about a 1% slowdown in the Crubit nullability check benchmarks: ``` name old cpu/op new cpu/op delta BM_PointerAnalysisCopyPointer 71.9µs ± 1% 71.9µs ± 2% ~ (p=0.987 n=15+20) BM_PointerAnalysisIntLoop 190µs ± 1% 192µs ± 2% +1.06% (p=0.000 n=14+16) BM_PointerAnalysisPointerLoop 325µs ± 5% 324µs ± 4% ~ (p=0.496 n=18+20) BM_PointerAnalysisBranch 193µs ± 0% 192µs ± 4% ~ (p=0.488 n=14+18) BM_PointerAnalysisLoopAndBranch 521µs ± 1% 525µs ± 3% +0.94% (p=0.017 n=18+19) BM_PointerAnalysisTwoLoops 337µs ± 1% 341µs ± 3% +1.19% (p=0.004 n=17+19) BM_PointerAnalysisJoinFilePath 1.62ms ± 2% 1.64ms ± 3% +0.92% (p=0.021 n=20+20) BM_PointerAnalysisCallInLoop 1.14ms ± 1% 1.15ms ± 4% ~ (p=0.135 n=16+18) ``` [^1]: ``` [B5 (ENTRY)] Succs (1): B4 [B1] 1: [B4.9] ? [B2.1] : [B3.1] 2: [B4.4]([B4.6], [B1.1]) Preds (2): B2 B3 Succs (1): B0 [B2] 1: 1 Preds (1): B4 Succs (1): B1 [B3] 1: 0 Preds (1): B4 Succs (1): B1 [B4] 1: 0 2: int i = 0; 3: f 4: [B4.3] (ImplicitCastExpr, FunctionToPointerDecay, void (*)(int *, int)) 5: i 6: &[B4.5] 7: cond 8: [B4.7] (ImplicitCastExpr, FunctionToPointerDecay, _Bool (*)(void)) 9: [B4.8]() T: [B4.9] ? ... : ... Preds (1): B5 Succs (2): B2 B3 [B0 (EXIT)] Preds (1): B1 ```
1 parent a11ab13 commit d5aecf0

File tree

7 files changed

+189
-52
lines changed

7 files changed

+189
-52
lines changed

clang/include/clang/Analysis/FlowSensitive/ControlFlowContext.h

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,36 @@ class ControlFlowContext {
5858
return BlockReachable[B.getBlockID()];
5959
}
6060

61+
/// Returns whether `B` contains an expression that is consumed in a
62+
/// different block than `B` (i.e. the parent of the expression is in a
63+
/// different block).
64+
/// This happens if there is control flow within a full-expression (triggered
65+
/// by `&&`, `||`, or the conditional operator). Note that the operands of
66+
/// these operators are not the only expressions that can be consumed in a
67+
/// different block. For example, in the function call
68+
/// `f(&i, cond() ? 1 : 0)`, `&i` is in a different block than the `CallExpr`.
69+
bool containsExprConsumedInDifferentBlock(const CFGBlock &B) const {
70+
return ContainsExprConsumedInDifferentBlock.contains(&B);
71+
}
72+
6173
private:
62-
ControlFlowContext(const Decl &D, std::unique_ptr<CFG> Cfg,
63-
llvm::DenseMap<const Stmt *, const CFGBlock *> StmtToBlock,
64-
llvm::BitVector BlockReachable)
74+
ControlFlowContext(
75+
const Decl &D, std::unique_ptr<CFG> Cfg,
76+
llvm::DenseMap<const Stmt *, const CFGBlock *> StmtToBlock,
77+
llvm::BitVector BlockReachable,
78+
llvm::DenseSet<const CFGBlock *> ContainsExprConsumedInDifferentBlock)
6579
: ContainingDecl(D), Cfg(std::move(Cfg)),
6680
StmtToBlock(std::move(StmtToBlock)),
67-
BlockReachable(std::move(BlockReachable)) {}
81+
BlockReachable(std::move(BlockReachable)),
82+
ContainsExprConsumedInDifferentBlock(
83+
std::move(ContainsExprConsumedInDifferentBlock)) {}
6884

6985
/// The `Decl` containing the statement used to construct the CFG.
7086
const Decl &ContainingDecl;
7187
std::unique_ptr<CFG> Cfg;
7288
llvm::DenseMap<const Stmt *, const CFGBlock *> StmtToBlock;
7389
llvm::BitVector BlockReachable;
90+
llvm::DenseSet<const CFGBlock *> ContainsExprConsumedInDifferentBlock;
7491
};
7592

7693
} // namespace dataflow

clang/include/clang/Analysis/FlowSensitive/DataflowEnvironment.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,14 @@ class Environment {
210210
bool equivalentTo(const Environment &Other,
211211
Environment::ValueModel &Model) const;
212212

213+
/// How to treat expression state (`ExprToLoc` and `ExprToVal`) in a join.
214+
/// If the join happens within a full expression, expression state should be
215+
/// kept; otherwise, we can discard it.
216+
enum ExprJoinBehavior {
217+
DiscardExprState,
218+
KeepExprState,
219+
};
220+
213221
/// Joins two environments by taking the intersection of storage locations and
214222
/// values that are stored in them. Distinct values that are assigned to the
215223
/// same storage locations in `EnvA` and `EnvB` are merged using `Model`.
@@ -218,7 +226,8 @@ class Environment {
218226
///
219227
/// `EnvA` and `EnvB` must use the same `DataflowAnalysisContext`.
220228
static Environment join(const Environment &EnvA, const Environment &EnvB,
221-
Environment::ValueModel &Model);
229+
Environment::ValueModel &Model,
230+
ExprJoinBehavior ExprBehavior);
222231

223232
/// Widens the environment point-wise, using `PrevEnv` as needed to inform the
224233
/// approximation.

clang/lib/Analysis/FlowSensitive/ControlFlowContext.cpp

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,38 @@ static llvm::BitVector findReachableBlocks(const CFG &Cfg) {
9494
return BlockReachable;
9595
}
9696

97+
static llvm::DenseSet<const CFGBlock *>
98+
buildContainsExprConsumedInDifferentBlock(
99+
const CFG &Cfg,
100+
const llvm::DenseMap<const Stmt *, const CFGBlock *> &StmtToBlock) {
101+
llvm::DenseSet<const CFGBlock *> Result;
102+
103+
auto CheckChildExprs = [&Result, &StmtToBlock](const Stmt *S,
104+
const CFGBlock *Block) {
105+
for (const Stmt *Child : S->children()) {
106+
if (!isa<Expr>(Child))
107+
continue;
108+
const CFGBlock *ChildBlock = StmtToBlock.lookup(Child);
109+
if (ChildBlock != Block)
110+
Result.insert(ChildBlock);
111+
}
112+
};
113+
114+
for (const CFGBlock *Block : Cfg) {
115+
if (Block == nullptr)
116+
continue;
117+
118+
for (const CFGElement &Element : *Block)
119+
if (auto S = Element.getAs<CFGStmt>())
120+
CheckChildExprs(S->getStmt(), Block);
121+
122+
if (const Stmt *TerminatorCond = Block->getTerminatorCondition())
123+
CheckChildExprs(TerminatorCond, Block);
124+
}
125+
126+
return Result;
127+
}
128+
97129
llvm::Expected<ControlFlowContext>
98130
ControlFlowContext::build(const FunctionDecl &Func) {
99131
if (!Func.doesThisDeclarationHaveABody())
@@ -140,8 +172,12 @@ ControlFlowContext::build(const Decl &D, Stmt &S, ASTContext &C) {
140172

141173
llvm::BitVector BlockReachable = findReachableBlocks(*Cfg);
142174

175+
llvm::DenseSet<const CFGBlock *> ContainsExprConsumedInDifferentBlock =
176+
buildContainsExprConsumedInDifferentBlock(*Cfg, StmtToBlock);
177+
143178
return ControlFlowContext(D, std::move(Cfg), std::move(StmtToBlock),
144-
std::move(BlockReachable));
179+
std::move(BlockReachable),
180+
std::move(ContainsExprConsumedInDifferentBlock));
145181
}
146182

147183
} // namespace dataflow

clang/lib/Analysis/FlowSensitive/DataflowEnvironment.cpp

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,24 @@ static llvm::DenseMap<const ValueDecl *, StorageLocation *> intersectDeclToLoc(
4848
return Result;
4949
}
5050

51+
// Performs a join on either `ExprToLoc` or `ExprToVal`.
52+
// The maps must be consistent in the sense that any entries for the same
53+
// expression must map to the same location / value. This is the case if we are
54+
// performing a join for control flow within a full-expression (which is the
55+
// only case when this function should be used).
56+
template <typename MapT> MapT joinExprMaps(const MapT &Map1, const MapT &Map2) {
57+
MapT Result = Map1;
58+
59+
for (const auto &Entry : Map2) {
60+
[[maybe_unused]] auto [It, Inserted] = Result.insert(Entry);
61+
// If there was an existing entry, its value should be the same as for the
62+
// entry we were trying to insert.
63+
assert(It->second == Entry.second);
64+
}
65+
66+
return Result;
67+
}
68+
5169
// Whether to consider equivalent two values with an unknown relation.
5270
//
5371
// FIXME: this function is a hack enabling unsoundness to support
@@ -627,7 +645,8 @@ LatticeJoinEffect Environment::widen(const Environment &PrevEnv,
627645
}
628646

629647
Environment Environment::join(const Environment &EnvA, const Environment &EnvB,
630-
Environment::ValueModel &Model) {
648+
Environment::ValueModel &Model,
649+
ExprJoinBehavior ExprBehavior) {
631650
assert(EnvA.DACtx == EnvB.DACtx);
632651
assert(EnvA.ThisPointeeLoc == EnvB.ThisPointeeLoc);
633652
assert(EnvA.CallStack == EnvB.CallStack);
@@ -675,9 +694,10 @@ Environment Environment::join(const Environment &EnvA, const Environment &EnvB,
675694
JoinedEnv.LocToVal =
676695
joinLocToVal(EnvA.LocToVal, EnvB.LocToVal, EnvA, EnvB, JoinedEnv, Model);
677696

678-
// We intentionally leave `JoinedEnv.ExprToLoc` and `JoinedEnv.ExprToVal`
679-
// empty, as we never need to access entries in these maps outside of the
680-
// basic block that sets them.
697+
if (ExprBehavior == KeepExprState) {
698+
JoinedEnv.ExprToVal = joinExprMaps(EnvA.ExprToVal, EnvB.ExprToVal);
699+
JoinedEnv.ExprToLoc = joinExprMaps(EnvA.ExprToLoc, EnvB.ExprToLoc);
700+
}
681701

682702
return JoinedEnv;
683703
}

clang/lib/Analysis/FlowSensitive/TypeErasedDataflowAnalysis.cpp

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -221,18 +221,21 @@ class PrettyStackTraceCFGElement : public llvm::PrettyStackTraceEntry {
221221
// Avoids unneccesary copies of the environment.
222222
class JoinedStateBuilder {
223223
AnalysisContext &AC;
224+
Environment::ExprJoinBehavior JoinBehavior;
224225
std::vector<const TypeErasedDataflowAnalysisState *> All;
225226
std::deque<TypeErasedDataflowAnalysisState> Owned;
226227

227228
TypeErasedDataflowAnalysisState
228229
join(const TypeErasedDataflowAnalysisState &L,
229230
const TypeErasedDataflowAnalysisState &R) {
230231
return {AC.Analysis.joinTypeErased(L.Lattice, R.Lattice),
231-
Environment::join(L.Env, R.Env, AC.Analysis)};
232+
Environment::join(L.Env, R.Env, AC.Analysis, JoinBehavior)};
232233
}
233234

234235
public:
235-
JoinedStateBuilder(AnalysisContext &AC) : AC(AC) {}
236+
JoinedStateBuilder(AnalysisContext &AC,
237+
Environment::ExprJoinBehavior JoinBehavior)
238+
: AC(AC), JoinBehavior(JoinBehavior) {}
236239

237240
void addOwned(TypeErasedDataflowAnalysisState State) {
238241
Owned.push_back(std::move(State));
@@ -248,12 +251,12 @@ class JoinedStateBuilder {
248251
// initialize the state of each basic block differently.
249252
return {AC.Analysis.typeErasedInitialElement(), AC.InitEnv.fork()};
250253
if (All.size() == 1)
251-
// Join the environment with itself so that we discard the entries from
252-
// `ExprToLoc` and `ExprToVal`.
254+
// Join the environment with itself so that we discard expression state if
255+
// desired.
253256
// FIXME: We could consider writing special-case code for this that only
254257
// does the discarding, but it's not clear if this is worth it.
255-
return {All[0]->Lattice,
256-
Environment::join(All[0]->Env, All[0]->Env, AC.Analysis)};
258+
return {All[0]->Lattice, Environment::join(All[0]->Env, All[0]->Env,
259+
AC.Analysis, JoinBehavior)};
257260

258261
auto Result = join(*All[0], *All[1]);
259262
for (unsigned I = 2; I < All.size(); ++I)
@@ -307,7 +310,22 @@ computeBlockInputState(const CFGBlock &Block, AnalysisContext &AC) {
307310
}
308311
}
309312

310-
JoinedStateBuilder Builder(AC);
313+
// If any of the predecessor blocks contains an expression consumed in a
314+
// different block, we need to keep expression state.
315+
// Note that in this case, we keep expression state for all predecessors,
316+
// rather than only those predecessors that actually contain an expression
317+
// consumed in a different block. While this is potentially suboptimal, it's
318+
// actually likely, if we have control flow within a full expression, that
319+
// all predecessors have expression state consumed in a different block.
320+
Environment::ExprJoinBehavior JoinBehavior = Environment::DiscardExprState;
321+
for (const CFGBlock *Pred : Preds) {
322+
if (Pred && AC.CFCtx.containsExprConsumedInDifferentBlock(*Pred)) {
323+
JoinBehavior = Environment::KeepExprState;
324+
break;
325+
}
326+
}
327+
328+
JoinedStateBuilder Builder(AC, JoinBehavior);
311329
for (const CFGBlock *Pred : Preds) {
312330
// Skip if the `Block` is unreachable or control flow cannot get past it.
313331
if (!Pred || Pred->hasNoReturnElement())

clang/unittests/Analysis/FlowSensitive/DataflowEnvironmentTest.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ TEST_F(EnvironmentTest, JoinRecords) {
190190
Env2.setValue(Loc, Val2);
191191

192192
Environment::ValueModel Model;
193-
Environment EnvJoined = Environment::join(Env1, Env2, Model);
193+
Environment EnvJoined =
194+
Environment::join(Env1, Env2, Model, Environment::DiscardExprState);
194195
auto *JoinedVal = cast<RecordValue>(EnvJoined.getValue(Loc));
195196
EXPECT_NE(JoinedVal, &Val1);
196197
EXPECT_NE(JoinedVal, &Val2);

clang/unittests/Analysis/FlowSensitive/TypeErasedDataflowAnalysisTest.cpp

Lines changed: 70 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -244,62 +244,98 @@ TEST_F(DiscardExprStateTest, WhileStatement) {
244244
EXPECT_NE(NotEqOpState.Env.getValue(NotEqOp), nullptr);
245245

246246
// In the block that calls `foo(p)`, the value for `p != nullptr` is discarded
247-
// because it is not consumed by this block.
247+
// because it is not consumed outside the block it is in.
248248
const auto &CallFooState = blockStateForStmt(BlockStates, CallFoo);
249249
EXPECT_EQ(CallFooState.Env.getValue(NotEqOp), nullptr);
250250
}
251251

252252
TEST_F(DiscardExprStateTest, BooleanOperator) {
253253
std::string Code = R"(
254-
bool target(bool b1, bool b2) {
255-
return b1 && b2;
254+
void f();
255+
void target(bool b1, bool b2) {
256+
if (b1 && b2)
257+
f();
256258
}
257259
)";
258260
auto BlockStates = llvm::cantFail(runAnalysis<NoopAnalysis>(
259261
Code, [](ASTContext &C) { return NoopAnalysis(C); }));
260262

261263
const auto &AndOp =
262264
matchNode<BinaryOperator>(binaryOperator(hasOperatorName("&&")));
263-
const auto &Return = matchNode<ReturnStmt>(returnStmt());
265+
const auto &CallF =
266+
matchNode<CallExpr>(callExpr(callee(functionDecl(hasName("f")))));
264267

265268
// In the block that evaluates the LHS of the `&&` operator, the LHS is
266269
// associated with a value, while the right-hand side is not (unsurprisingly,
267270
// as it hasn't been evaluated yet).
268271
const auto &LHSState = blockStateForStmt(BlockStates, *AndOp.getLHS());
269272
auto *LHSValue = cast<BoolValue>(LHSState.Env.getValue(*AndOp.getLHS()));
270-
ASSERT_NE(LHSValue, nullptr);
273+
EXPECT_NE(LHSValue, nullptr);
271274
EXPECT_EQ(LHSState.Env.getValue(*AndOp.getRHS()), nullptr);
272275

273-
// In the block that evaluates the RHS, the RHS is associated with a
274-
// value. The value for the LHS has been discarded as it is not consumed by
275-
// this block.
276+
// In the block that evaluates the RHS, both the LHS and RHS are associated
277+
// with values, as they are both subexpressions of the `&&` operator, which
278+
// is evaluated in a later block.
276279
const auto &RHSState = blockStateForStmt(BlockStates, *AndOp.getRHS());
277-
EXPECT_EQ(RHSState.Env.getValue(*AndOp.getLHS()), nullptr);
278-
auto *RHSValue = cast<BoolValue>(RHSState.Env.getValue(*AndOp.getRHS()));
279-
ASSERT_NE(RHSValue, nullptr);
280-
281-
// In the block that evaluates the return statement, the expression `b1 && b2`
282-
// is associated with a value (and check that it's the right one).
283-
// The expressions `b1` and `b2` are _not_ associated with a value in this
284-
// block, even though they are consumed by the block, because:
285-
// * This block has two prececessor blocks (the one that evaluates `b1` and
286-
// the one that evaluates `b2`).
287-
// * `b1` is only associated with a value in the block that evaluates `b1` but
288-
// not the block that evalutes `b2`, so the join operation discards the
289-
// value for `b1`.
290-
// * `b2` is only associated with a value in the block that evaluates `b2` but
291-
// not the block that evaluates `b1`, the the join operation discards the
292-
// value for `b2`.
293-
// Nevertheless, the analysis generates the correct formula for `b1 && b2`
294-
// because the transfer function for the `&&` operator retrieves the values
295-
// for its operands from the environments for the blocks that compute the
296-
// operands, rather than from the environment for the block that contains the
297-
// `&&`.
298-
const auto &ReturnState = blockStateForStmt(BlockStates, Return);
299-
EXPECT_EQ(ReturnState.Env.getValue(*AndOp.getLHS()), nullptr);
300-
EXPECT_EQ(ReturnState.Env.getValue(*AndOp.getRHS()), nullptr);
301-
EXPECT_EQ(ReturnState.Env.getValue(AndOp),
302-
&ReturnState.Env.makeAnd(*LHSValue, *RHSValue));
280+
EXPECT_EQ(RHSState.Env.getValue(*AndOp.getLHS()), LHSValue);
281+
auto *RHSValue = RHSState.Env.get<BoolValue>(*AndOp.getRHS());
282+
EXPECT_NE(RHSValue, nullptr);
283+
284+
// In the block that evaluates `b1 && b2`, the `&&` as well as its operands
285+
// are associated with values.
286+
const auto &AndOpState = blockStateForStmt(BlockStates, AndOp);
287+
EXPECT_EQ(AndOpState.Env.getValue(*AndOp.getLHS()), LHSValue);
288+
EXPECT_EQ(AndOpState.Env.getValue(*AndOp.getRHS()), RHSValue);
289+
EXPECT_EQ(AndOpState.Env.getValue(AndOp),
290+
&AndOpState.Env.makeAnd(*LHSValue, *RHSValue));
291+
292+
// In the block that calls `f()`, none of `b1`, `b2`, or `b1 && b2` should be
293+
// associated with values.
294+
const auto &CallFState = blockStateForStmt(BlockStates, CallF);
295+
EXPECT_EQ(CallFState.Env.getValue(*AndOp.getLHS()), nullptr);
296+
EXPECT_EQ(CallFState.Env.getValue(*AndOp.getRHS()), nullptr);
297+
EXPECT_EQ(CallFState.Env.getValue(AndOp), nullptr);
298+
}
299+
300+
TEST_F(DiscardExprStateTest, ConditionalOperator) {
301+
std::string Code = R"(
302+
void f(int*, int);
303+
void g();
304+
bool cond();
305+
306+
void target() {
307+
int i = 0;
308+
if (cond())
309+
f(&i, cond() ? 1 : 0);
310+
g();
311+
}
312+
)";
313+
auto BlockStates = llvm::cantFail(runAnalysis<NoopAnalysis>(
314+
Code, [](ASTContext &C) { return NoopAnalysis(C); }));
315+
316+
const auto &AddrOfI =
317+
matchNode<UnaryOperator>(unaryOperator(hasOperatorName("&")));
318+
const auto &CallF =
319+
matchNode<CallExpr>(callExpr(callee(functionDecl(hasName("f")))));
320+
const auto &CallG =
321+
matchNode<CallExpr>(callExpr(callee(functionDecl(hasName("g")))));
322+
323+
// In the block that evaluates `&i`, it should obviously have a value.
324+
const auto &AddrOfIState = blockStateForStmt(BlockStates, AddrOfI);
325+
auto *AddrOfIVal = AddrOfIState.Env.get<PointerValue>(AddrOfI);
326+
EXPECT_NE(AddrOfIVal, nullptr);
327+
328+
// Because of the conditional operator, the `f(...)` call is evaluated in a
329+
// different block than `&i`, but `&i` still needs to have a value here
330+
// because it's a subexpression of the call.
331+
const auto &CallFState = blockStateForStmt(BlockStates, CallF);
332+
EXPECT_NE(&CallFState, &AddrOfIState);
333+
EXPECT_EQ(CallFState.Env.get<PointerValue>(AddrOfI), AddrOfIVal);
334+
335+
// In the block that calls `g()`, `&i` should no longer be associated with a
336+
// value.
337+
const auto &CallGState = blockStateForStmt(BlockStates, CallG);
338+
EXPECT_EQ(CallGState.Env.get<PointerValue>(AddrOfI), nullptr);
303339
}
304340

305341
struct NonConvergingLattice {

0 commit comments

Comments
 (0)