Skip to content

Commit 22cb771

Browse files
committed
Code review fixes
* Moved the design description from a comment to a separate document `docs/SpeculativeFieldNonNullability.md` * Extracted intermediate expressions in `nullPointerExceptionCheck` * Removed obsolete temporary comment in `Value.resolve` * Replaced package import to class imports to conform the coding style * Replaced negated call to `isApplicationClass` with `isLibraryClass`
1 parent d5b45d8 commit 22cb771

File tree

3 files changed

+62
-35
lines changed

3 files changed

+62
-35
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Speculative field non-nullability assumptions
2+
3+
## The problem
4+
5+
When a field is used as a base for a dot call (i.e., a method call or a field access),
6+
the symbolic engine creates a branch corresponding to the potential `NullPointerException`
7+
that can occur if the value of the field is `null`. For this path, the engine generates
8+
the hard constraint `addr(field) == null`.
9+
10+
If the field is marked as `@NotNull`, a hard constraint `addr(field) != null` is generated
11+
for it. If both constraints have been generated simultaneously, the `NPE` branch is discarded
12+
as the constraint set is unsatisfiable.
13+
14+
If a field does not have `@NotNull` annotation, the `NPE` branch will be kept. This behavior
15+
is desirable, as it increases the coverage, but it has a downside. It is possible that
16+
most of generated branches would be `NPE` branches, while useful paths could be lost due to timeout.
17+
18+
Beyond that, in many cases the `null` value of a field can't be generated using the public API
19+
of the class. This is particularly true for final fields, especially in system classes.
20+
Automatically generated tests assign `null` values to fields in questions using reflection,
21+
but these tests may be uninformative as the corresponding `NPE` branches would never occur
22+
in the real code that limits itself to the public API.
23+
24+
## The solution
25+
26+
To discard irrelevant `NPE` branches, we can speculatively mark fields we as non-nullable even they
27+
do not have an explicit `@NotNull` annotation. In particular, we can use this approach to final
28+
fields of system classes, as they are usually correctly initialized and are not equal `null`.
29+
30+
At the same time, we can't always add the "not null" hard constraint for the field: it would break
31+
some special cases like `Optional<T>` class, which uses the `null` value of its final field
32+
as a marker of an empty value.
33+
34+
The engine checks for NPE and creates an NPE branch every time the address is used
35+
as a base of a dot call (i.e., a method call or a field access);
36+
see `UtBotSymbolicEngine.nullPointerExceptionCheck`). The problem is what at that moment, we would have
37+
no way to check whether the address corresponds to a final field, as the corresponding node
38+
of the global graph would refer to a local variable. The only place where we have the complete
39+
information about the field is this method.
40+
41+
We use the following approach. If the field is final and belongs to a system class,
42+
we mark it as a speculatively non-nullable in the memory
43+
(see `org.utbot.engine.Memory.speculativelyNotNullAddresses`). During the NPE check
44+
we will add the `!isSpeculativelyNotNull(addr(field))` constraint
45+
to the `NPE` branch together with the usual `addr(field) == null` constraint.
46+
47+
For final fields, these two conditions can't be satisfied at the same time, as we speculatively
48+
mark final fields as non-nullable. As a result, the NPE branch would be discarded. If a field
49+
is not final, the condition is satisfiable, so the NPE branch would stay alive.
50+
51+
We limit this approach to the system classes only, because it is hard to speculatively assume
52+
something about non-nullability of final fields in the user code.
53+
54+
The same approach can be extended for other cases where we want to speculatively consider some
55+
fields as non-nullable to prevent `NPE` branch generation.

utbot-framework/src/main/kotlin/org/utbot/engine/Memory.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ data class Memory( // TODO: split purely symbolic memory and information about s
141141

142142
/**
143143
* Storage for addresses that we speculatively consider non-nullable (e.g., final fields of system classes).
144-
* See [org.utbot.engine.UtBotSymbolicEngine.createFieldOrMock] for details.
144+
* See [org.utbot.engine.UtBotSymbolicEngine.createFieldOrMock] for usage,
145+
* and [docs/SpeculativeFieldNonNullability.md] for details.
145146
*/
146147
private val speculativelyNotNullAddresses: UtArrayExpressionBase = UtConstArrayExpression(
147148
UtFalse,

utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1938,10 +1938,6 @@ class UtBotSymbolicEngine(
19381938
is JInstanceFieldRef -> {
19391939
val instance = (base.resolve() as ObjectValue)
19401940
recordInstanceFieldRead(instance.addr, field)
1941-
1942-
// We know that [base] is not null as we are accessing its field (dot access).
1943-
// At the same time, we don't want to check for NPE if [base] is a final field
1944-
// (or if it is a non-nullable field).
19451941
nullPointerExceptionCheck(instance.addr)
19461942

19471943
val objectType = if (instance.concrete?.value is BaseOverriddenWrapper) {
@@ -2153,34 +2149,8 @@ class UtBotSymbolicEngine(
21532149
queuedSymbolicStateUpdates += mkNot(mkEq(createdField.addr, nullObjectAddr)).asHardConstraint()
21542150
}
21552151

2156-
// We suppose that accessing final fields in system classes can't produce NullPointerException
2157-
// because they are properly initialized in corresponding constructors. It is therefore
2158-
// desirable to avoid the generation of redundant test cases for NPE branches.
2159-
//
2160-
// At the same time, we can't always add the "not null" hard constraint for the field: it would break
2161-
// some special cases like `Optional<T>` class, which uses the null value of its final field
2162-
// as a marker of an empty value.
2163-
//
2164-
// The engine checks for NPE and creates an NPE branch every time the address is used
2165-
// as a base of a dot call (i.e., a method call or a field access);
2166-
// see [UtBotSymbolicEngine.nullPointerExceptionCheck]). The problem is what at that moment, we would have
2167-
// no way to check whether the address corresponds to a final field, as the corresponding node
2168-
// of the global graph would refer to a local variable. The only place where we have the complete
2169-
// information about the field is this method.
2170-
//
2171-
// We use the following approach. If the field is final and belongs to a system class,
2172-
// we mark it as a speculatively non-nullable in the memory. During the NPE check
2173-
// we will add two constraints to the NPE branch: "address has not been speculatively marked
2174-
// as non-nullable", and "address is null".
2175-
//
2176-
// For final fields, this condition can't be satisfied, as we speculatively mark final fields
2177-
// as non-nullable here. As a result, the NPE branch would be discarded. If a field is not final,
2178-
// the condition is satisfiable, so the NPE branch would stay alive.
2179-
//
2180-
// We limit this approach to the system classes only, because it is hard to speculatively assume
2181-
// something about non-nullability of final fields in the user code.
2182-
2183-
if (field.isFinal && !field.declaringClass.isApplicationClass && !checkNpeForFinalFields) {
2152+
// See docs/SpeculativeFieldNonNullability.md for details
2153+
if (field.isFinal && field.declaringClass.isLibraryClass && !checkNpeForFinalFields) {
21842154
markAsSpeculativelyNotNull(createdField.addr)
21852155
}
21862156
}
@@ -3274,10 +3244,11 @@ class UtBotSymbolicEngine(
32743244
private fun nullPointerExceptionCheck(addr: UtAddrExpression) {
32753245
val canBeNull = addrEq(addr, nullObjectAddr)
32763246
val canNotBeNull = mkNot(canBeNull)
3277-
val notFinalAndNull = mkAnd(mkEq(memory.isSpeculativelyNotNull(addr), mkFalse()), canBeNull)
3247+
val notMarked = mkEq(memory.isSpeculativelyNotNull(addr), mkFalse())
3248+
val notMarkedAndNull = mkAnd(notMarked, canBeNull)
32783249

32793250
if (environment.method.checkForNPE(environment.state.executionStack.size)) {
3280-
implicitlyThrowException(NullPointerException(), setOf(notFinalAndNull))
3251+
implicitlyThrowException(NullPointerException(), setOf(notMarkedAndNull))
32813252
}
32823253

32833254
queuedSymbolicStateUpdates += canNotBeNull.asHardConstraint()

0 commit comments

Comments
 (0)