diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/common/RefIdentity.java b/symbolic-executor/src/main/java/de/uzl/its/swat/common/RefIdentity.java new file mode 100644 index 00000000..d51af9a0 --- /dev/null +++ b/symbolic-executor/src/main/java/de/uzl/its/swat/common/RefIdentity.java @@ -0,0 +1,123 @@ +package de.uzl.its.swat.common; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.util.HashSet; + +/** + * Bookkeeping that lets {@link UtilInstrumented#refEquals} reproduce Java's reference identity + * although the NoCache transformer de-interns string literals and the boxing caches. + * + *

Every object that SWAT creates in place of a shared instance (a string literal, a {@code + * valueOf} result, an {@code intern()} result) is registered here. Two such objects are one + * instance in the original program exactly when their values are equal (and, for boxes, the value + * is in the cache range). Objects that are not registered keep their own identity. + * + *

This class is not instrumented and its methods are ignored invocations: their results only + * depend on where an object was created, which the control flow already determines, so the + * symbolic state receives them as concrete values. + */ +@SuppressWarnings("unused") +public final class RefIdentity { + + private static final ReferenceQueue queue = new ReferenceQueue<>(); + private static final HashSet deInterned = new HashSet<>(); + + private RefIdentity() {} + + /** Registers {@code copy} as standing for the shared instance of its value. */ + public static void markDeInterned(Object copy) { + if (copy == null) return; + synchronized (deInterned) { + expunge(); + deInterned.add(new Key(copy, queue)); + } + } + + public static boolean isDeInterned(Object o) { + if (o == null) return false; + synchronized (deInterned) { + return deInterned.contains(new Key(o, null)); + } + } + + /** + * Whether {@code s} is, in the original program, the canonical (interned) instance of its value: + * a de-interned copy, or a pooled string that reached the program without passing the NoCache + * transformer (e.g. a literal returned by the JDK). The pool lookup interns a fresh copy rather + * than {@code s}, so it never turns {@code s} itself into the pooled instance. + */ + public static boolean isCanonicalString(String s) { + return isDeInterned(s) || isPooledOnly(s); + } + + /** Whether {@code s} is an unregistered string that is the pooled instance of its value. */ + public static boolean isPooledOnly(String s) { + return !isDeInterned(s) && new String(s).intern() == s; + } + + /** + * Whether {@code o} is, in the original program, a cached box: a de-interned {@code valueOf} + * copy (cached if its value is in the cache range, which the caller checks), or the cached + * instance itself. + */ + public static boolean isCanonicalBox(Object o) { + if (isDeInterned(o)) return true; + if (o instanceof Integer i) return i == Integer.valueOf(i.intValue()); + if (o instanceof Long l) return l == Long.valueOf(l.longValue()); + if (o instanceof Short s) return s == Short.valueOf(s.shortValue()); + if (o instanceof Byte b) return b == Byte.valueOf(b.byteValue()); + if (o instanceof Character c) return c == Character.valueOf(c.charValue()); + if (o instanceof Boolean b) return b == Boolean.valueOf(b.booleanValue()); + return false; + } + + /** + * {@code s.intern() == s} with the JVM's own side effect: an unregistered string whose value is + * not pooled yet becomes the pooled instance. A registered copy never does, its value is pooled. + */ + public static boolean internsToItself(String s) { + return !isDeInterned(s) && s.intern() == s; + } + + /** Plain reference identity, kept out of the symbolic state's view of {@code IF_ACMP}. */ + public static boolean same(Object a, Object b) { + return a == b; + } + + /** + * Marks a reference comparison whose outcome depends on the values of {@code a} and {@code b} + * in a way SWAT does not encode. The symbolic state records a reference semantic change when an + * argument is symbolic. + */ + public static void valueDependentIdentity(Object a, Object b) {} + + private static void expunge() { + Object k; + while ((k = queue.poll()) != null) { + deInterned.remove(k); + } + } + + private static final class Key extends WeakReference { + private final int hash; + + Key(Object referent, ReferenceQueue q) { + super(referent, q); + this.hash = System.identityHashCode(referent); + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof Key k)) return false; + Object referent = get(); + return referent != null && referent == k.get(); + } + } +} diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/common/Util.java b/symbolic-executor/src/main/java/de/uzl/its/swat/common/Util.java index 2247fb86..98cf9a46 100644 --- a/symbolic-executor/src/main/java/de/uzl/its/swat/common/Util.java +++ b/symbolic-executor/src/main/java/de/uzl/its/swat/common/Util.java @@ -42,10 +42,6 @@ public class Util { private static final HashMap shouldInstrumentCache = new HashMap<>(); - // Maintain a set of class names that have been de-interned - private static final Set deInternedClasses = new HashSet<>(Arrays.asList( - String.class.getName(), Boolean.class.getName(), Byte.class.getName(), Short.class.getName(), - Character.class.getName(), Integer.class.getName(), Long.class.getName())); /** * Just a method for validating the engine! @@ -419,19 +415,4 @@ public static boolean isSymbolicMethod(String cname, String name) { return Pattern.matches(symbolicMethodPattern, name); } - static boolean shouldUseValueEquality(Object a, Object b) { - // If either is null, Objects.equals handles it correctly anyway - if (a == null || b == null) { - return true; - } - - // Check if either object's class has been de-interned - return isDeInternedClass(a.getClass()) || isDeInternedClass(b.getClass()); - } - - private static boolean isDeInternedClass(Class clazz) { - // Check the exact class and optionally superclasses if inheritance matters - return deInternedClasses.contains(clazz.getName()); - } - } diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/common/UtilInstrumented.java b/symbolic-executor/src/main/java/de/uzl/its/swat/common/UtilInstrumented.java index e288bec7..8282618f 100644 --- a/symbolic-executor/src/main/java/de/uzl/its/swat/common/UtilInstrumented.java +++ b/symbolic-executor/src/main/java/de/uzl/its/swat/common/UtilInstrumented.java @@ -5,7 +5,6 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.Objects; @SuppressWarnings("unused") public class UtilInstrumented { @@ -24,14 +23,69 @@ public static void liftClass(Object param, String paramCnameDot, String methodNa } } /** - * Conditionally uses Objects.equals() for de-interned classes, - * otherwise uses reference equality. + * Replaces {@code IF_ACMPEQ}/{@code IF_ACMPNE}: Java reference identity, computed on objects of + * which SWAT de-interned the literals and boxing caches (see {@link RefIdentity}). + * + *

Two canonical instances (literals, interned strings, cached boxes) are identical exactly + * when their values are equal, and that comparison goes through the modelled {@code equals} + * and unboxing calls so the symbolic state sees its dependence on the values. Every other + * comparison is plain identity and concrete. The comparisons themselves must not be + * {@code IF_ACMP}s here: the symbolic state encodes those on strings as value equality. */ @SuppressWarnings("unused") public static boolean refEquals(Object a, Object b) { - if (Util.shouldUseValueEquality(a, b)) { - return Objects.equals(a, b); + if (a instanceof String sa && b instanceof String sb) { + if (RefIdentity.isCanonicalString(sa) && RefIdentity.isCanonicalString(sb)) { + if (RefIdentity.isPooledOnly(sa) || RefIdentity.isPooledOnly(sb)) { + // Whether a string from uninstrumented code is pooled may depend on its value. + RefIdentity.valueDependentIdentity(sa, sb); + } + return sa.equals(sb); + } + return RefIdentity.same(a, b); } - return a == b; + if (RefIdentity.same(a, b)) { + return true; + } + if (RefIdentity.isCanonicalBox(a) && RefIdentity.isCanonicalBox(b)) { + // Distinct cached boxes: the same instance iff the values are equal and in the cache range. + if (a instanceof Integer x && b instanceof Integer y) { + int v = x.intValue(); + return v == y.intValue() && v >= -128 && v <= 127; + } + if (a instanceof Long x && b instanceof Long y) { + long v = x.longValue(); + return v == y.longValue() && v >= -128 && v <= 127; + } + if (a instanceof Short x && b instanceof Short y) { + short v = x.shortValue(); + return v == y.shortValue() && v >= -128 && v <= 127; + } + if (a instanceof Character x && b instanceof Character y) { + char v = x.charValue(); + return v == y.charValue() && v <= 127; + } + if (a instanceof Byte x && b instanceof Byte y) { + return x.byteValue() == y.byteValue(); + } + if (a instanceof Boolean x && b instanceof Boolean y) { + return x.booleanValue() == y.booleanValue(); + } + } + return RefIdentity.same(a, b); + } + + /** + * Replaces {@code String.intern()}: a de-interned copy standing for the pooled instance, or + * {@code s} itself when {@code s} is (or, by this call, becomes) the pooled instance. + */ + @SuppressWarnings("unused") + public static String intern(String s) { + if (RefIdentity.internsToItself(s)) { + return s; + } + String copy = new String(s); + RefIdentity.markDeInterned(copy); + return copy; } } diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/instrument/nocache/NoCacheMethodAdapter.java b/symbolic-executor/src/main/java/de/uzl/its/swat/instrument/nocache/NoCacheMethodAdapter.java index aba06b80..b301652e 100644 --- a/symbolic-executor/src/main/java/de/uzl/its/swat/instrument/nocache/NoCacheMethodAdapter.java +++ b/symbolic-executor/src/main/java/de/uzl/its/swat/instrument/nocache/NoCacheMethodAdapter.java @@ -23,6 +23,7 @@ public void visitLdcInsn(Object value) { mv.visitLdcInsn(value); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/String", "", "(Ljava/lang/String;)V", false); + markDeInterned(); //NoCacheTransformer.getPrintBox() // .addMsg("Replacing LDC with new String"); } else { @@ -33,14 +34,16 @@ public void visitLdcInsn(Object value) { // Intercept method calls to disable interning and caching. @Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { - // Skip String.intern() calls. + // Replace String.intern() with a de-interned copy that stands for the pooled instance. if (opcode == Opcodes.INVOKEVIRTUAL && owner.equals("java/lang/String") && name.equals("intern") && descriptor.equals("()Ljava/lang/String;") && Config.instance().isUseStringInterning()) { + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "de/uzl/its/swat/common/UtilInstrumented", "intern", + "(Ljava/lang/String;)Ljava/lang/String;", false); NoCacheTransformer.getPrintBox() - .addMsg("Removing String.intern() call"); + .addMsg("Replacing String.intern() with UtilInstrumented.intern"); return; } // Replace Integer.valueOf(int) with new Integer(int) @@ -54,6 +57,7 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri mv.visitInsn(Opcodes.DUP); mv.visitVarInsn(Opcodes.ILOAD, localVarIndex); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Integer", "", "(I)V", false); + markDeInterned(); NoCacheTransformer.getPrintBox() .addMsg("Replacing Integer.valueOf with new Integer"); return; @@ -69,6 +73,7 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri mv.visitInsn(Opcodes.DUP); mv.visitVarInsn(Opcodes.LLOAD, localVarIndex); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Long", "", "(J)V", false); + markDeInterned(); NoCacheTransformer.getPrintBox() .addMsg("Replacing Long.valueOf with new Long"); return; @@ -84,6 +89,7 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri mv.visitInsn(Opcodes.DUP); mv.visitVarInsn(Opcodes.ILOAD, localVarIndex); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Short", "", "(S)V", false); + markDeInterned(); NoCacheTransformer.getPrintBox() .addMsg("Replacing Short.valueOf with new Short"); return; @@ -99,6 +105,7 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri mv.visitInsn(Opcodes.DUP); mv.visitVarInsn(Opcodes.ILOAD, localVarIndex); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Byte", "", "(B)V", false); + markDeInterned(); NoCacheTransformer.getPrintBox() .addMsg("Replacing Byte.valueOf with new Byte"); return; @@ -114,6 +121,7 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri mv.visitInsn(Opcodes.DUP); mv.visitVarInsn(Opcodes.ILOAD, localVarIndex); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Character", "", "(C)V", false); + markDeInterned(); NoCacheTransformer.getPrintBox() .addMsg("Replacing Character.valueOf with new Character"); return; @@ -129,6 +137,7 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri mv.visitInsn(Opcodes.DUP); mv.visitVarInsn(Opcodes.ILOAD, localVarIndex); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Boolean", "", "(Z)V", false); + markDeInterned(); NoCacheTransformer.getPrintBox() .addMsg("Replacing Boolean.valueOf with new Boolean"); return; @@ -137,6 +146,16 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri mv.visitMethodInsn(opcode, owner, name, descriptor, isInterface); } + /** + * Registers the copy on top of the stack as standing for a shared instance, so that + * {@code UtilInstrumented.refEquals} can reproduce the identity of the original program. + */ + private void markDeInterned() { + mv.visitInsn(Opcodes.DUP); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "de/uzl/its/swat/common/RefIdentity", "markDeInterned", + "(Ljava/lang/Object;)V", false); + } + /* // Instrument object reference comparisons (IF_ACMPEQ and IF_ACMPNE) to print a warning. @Override diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/InvocationHandler.java b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/InvocationHandler.java index d89f7cc2..e7d8208e 100644 --- a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/InvocationHandler.java +++ b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/InvocationHandler.java @@ -28,6 +28,7 @@ public class InvocationHandler { "java/io/PrintStream/println", "de/uzl/its/swat/instrument/Intrinsics", "de/uzl/its/swat/common/UtilInstrumented", + "de/uzl/its/swat/common/RefIdentity", "de/uzl/its/swat/witness/Witness", "de/uzl/its/swat/instrument/svcomp/Verifier", "java/io/PrintStream", @@ -67,8 +68,12 @@ private static boolean hasSymbolicElement(Value v) throws NotImplementedEx Type[] types = Type.getArgumentTypes(desc); logger.trace("Invoking method {} in class {} with arguments {}", name, owner, arguments); - // We do that first to include the pointer in instance invocation cases - boolean containsSymbolicArgument = arguments.stream().anyMatch(v -> { + boolean ignored = IGNORED_INVOCATIONS.contains(owner + "/" + name) + || IGNORED_INVOCATIONS.contains(owner); + + // We do that first to include the pointer in instance invocation cases. Ignored + // invocations are never recorded, so their arguments need no (possibly deep) scan. + boolean containsSymbolicArgument = !ignored && arguments.stream().anyMatch(v -> { try { return v.isSymbolic() || hasSymbolicElement(v); } catch (NotImplementedException e) { @@ -78,7 +83,7 @@ private static boolean hasSymbolicElement(Value v) throws NotImplementedEx }); if (isInstance) { - containsSymbolicArgument = containsSymbolicArgument || instance.isSymbolic(); + containsSymbolicArgument = containsSymbolicArgument || (!ignored && instance.isSymbolic()); retValue = instance.invokeMethod(name, types, arguments.toArray(new Value[0])); } else { retValue = @@ -91,9 +96,7 @@ private static boolean hasSymbolicElement(Value v) throws NotImplementedEx } // When the method is not implemented and its not on the ignore list, we record it - if (retValue instanceof PlaceHolder && - !(IGNORED_INVOCATIONS.contains(owner + "/" + name) - || IGNORED_INVOCATIONS.contains(owner))) { + if (retValue instanceof PlaceHolder && !ignored) { if(isInstance) arguments.add(0, instance); diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/RefIdentityInvocation.java b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/RefIdentityInvocation.java new file mode 100644 index 00000000..750ea751 --- /dev/null +++ b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/RefIdentityInvocation.java @@ -0,0 +1,27 @@ +package de.uzl.its.swat.symbolic.invoke; + +import de.uzl.its.swat.common.exceptions.NotImplementedException; +import de.uzl.its.swat.symbolic.trace.SymbolicTraceHandler; +import de.uzl.its.swat.symbolic.value.PlaceHolder; +import de.uzl.its.swat.symbolic.value.Value; + +/** + * Symbolic side of {@link de.uzl.its.swat.common.RefIdentity}. Its results are concrete facts about + * where objects were created; only {@code valueDependentIdentity} affects the symbolic state. + */ +public final class RefIdentityInvocation { + + public static Value invokeStaticMethod( + String name, Value[] args, SymbolicTraceHandler symbolicTraceHandler) throws NotImplementedException { + if (name.equals("valueDependentIdentity")) { + for (Value arg : args) { + if (arg.isSymbolic()) { + // A reference comparison decided by symbolic values that SWAT does not encode. + symbolicTraceHandler.recordReferenceSemanticChange(); + break; + } + } + } + return PlaceHolder.instance; + } +} diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/StaticInvocation.java b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/StaticInvocation.java index 21b2849f..d1a9878d 100644 --- a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/StaticInvocation.java +++ b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/StaticInvocation.java @@ -53,6 +53,8 @@ public final class StaticInvocation { name, args, desc, symbolicStateHandler); case "java/lang/Double" -> DoubleInvocation.invokeStaticMethod( name, args, desc, symbolicStateHandler); + case "de/uzl/its/swat/common/RefIdentity" -> RefIdentityInvocation.invokeStaticMethod( + name, args, symbolicStateHandler); case "java/util/Objects" -> ObjectsInvocation.invokeStaticMethod( name, args, desc, symbolicStateHandler); default -> PlaceHolder.instance; diff --git a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/java/util/ObjectsInvocation.java b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/java/util/ObjectsInvocation.java index 2f6cf86a..68d77efa 100644 --- a/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/java/util/ObjectsInvocation.java +++ b/symbolic-executor/src/main/java/de/uzl/its/swat/symbolic/invoke/java/util/ObjectsInvocation.java @@ -7,15 +7,10 @@ import de.uzl.its.swat.symbolic.trace.SymbolicTraceHandler; import de.uzl.its.swat.symbolic.value.PlaceHolder; import de.uzl.its.swat.symbolic.value.Value; -import de.uzl.its.swat.symbolic.value.primitive.numeric.integral.CharValue; +import de.uzl.its.swat.symbolic.value.primitive.numeric.integral.BooleanValue; import de.uzl.its.swat.symbolic.value.reference.ObjectValue; -import de.uzl.its.swat.symbolic.value.reference.lang.CharacterObjectValue; -import de.uzl.its.swat.symbolic.value.reference.lang.StringValue; -import de.uzl.its.swat.thread.ThreadHandler; import org.objectweb.asm.Type; -import static java.lang.Thread.currentThread; - public class ObjectsInvocation { public static Value invokeStaticMethod( @@ -35,21 +30,9 @@ public class ObjectsInvocation { ObjectValue a = args[0].asObjectValue(); ObjectValue b = args[1].asObjectValue(); - // Check for user-de-interned strings with different addresses. - // This detects when refEquals (which calls Objects.equals) compares strings - // that were explicitly created with new String() in user code. - // In such cases, reference equality semantics may differ from what the user intended. - if (a instanceof StringValue s1 && b instanceof StringValue s2) { - if ((s1.isUserDeInterned() || s2.isUserDeInterned()) - && s1.getAddress() != s2.getAddress()) { - // Record that reference equality semantics may have changed - ThreadHandler.getSymbolicTraceHandler(currentThread().getId()) - .recordReferenceSemanticChange(); - } - } - if(a.getAddress() == ObjectValue.ADDRESS_NULL || b.getAddress() == ObjectValue.ADDRESS_NULL) { - return PlaceHolder.instance; + // A null operand: equal iff both are null, independent of any symbolic content. + return new BooleanValue(a.context, a.getAddress() == b.getAddress()); } return a.invokeMethod("equals", null, new Value[]{b} ); } diff --git a/symbolic-executor/src/test/groovy/de/uzl/its/swat/common/RefEqualsSpec.groovy b/symbolic-executor/src/test/groovy/de/uzl/its/swat/common/RefEqualsSpec.groovy new file mode 100644 index 00000000..331432a4 --- /dev/null +++ b/symbolic-executor/src/test/groovy/de/uzl/its/swat/common/RefEqualsSpec.groovy @@ -0,0 +1,110 @@ +package de.uzl.its.swat.common + +import spock.lang.Specification + +/** + * Concrete semantics of the IF_ACMP replacement: on objects de-interned the way the NoCache + * transformer does it, refEquals must give the answer the original program's {@code ==} gives. + */ +class RefEqualsSpec extends Specification { + + /** What the NoCache transformer emits for a string literal. */ + private static String literal(String s) { + String copy = new String(s) + RefIdentity.markDeInterned(copy) + return copy + } + + /** What the NoCache transformer emits for Integer.valueOf. */ + private static Integer boxed(int v) { + Integer copy = new Integer(v) + RefIdentity.markDeInterned(copy) + return copy + } + + def "two de-interned copies of one literal are the same instance"() { + expect: + UtilInstrumented.refEquals(literal("test"), literal("test")) + !UtilInstrumented.refEquals(literal("test"), literal("other")) + } + + def "a string built with new String is never a literal"() { + given: + String s1 = new String(literal("test")) + + expect: + !UtilInstrumented.refEquals(s1, literal("test")) + UtilInstrumented.refEquals(s1, s1) + } + + def "new String of an input differs from the input"() { + given: + String input = new String("fixed".toCharArray()) + String s1 = new String(input) + + expect: + !UtilInstrumented.refEquals(s1, input) + UtilInstrumented.refEquals(input, input) + } + + def "a pooled string from uninstrumented code equals a literal of its value"() { + expect: + UtilInstrumented.refEquals(Boolean.toString(true), literal("true")) + !UtilInstrumented.refEquals(Boolean.toString(false), literal("true")) + } + + def "intern returns the instance a literal of the same value stands for"() { + given: + String built = new StringBuilder("te").append("st").toString() + + expect: + UtilInstrumented.refEquals(UtilInstrumented.intern(built), literal("test")) + !UtilInstrumented.refEquals(built, literal("test")) + } + + def "boxes share an instance only inside the cache range"() { + expect: + UtilInstrumented.refEquals(boxed(v), boxed(v)) == shared + + where: + v || shared + 0 || true + 127 || true + -128 || true + 128 || false + -129 || false + 1000 || false + } + + def "a box is identical to itself outside the cache range"() { + given: + Integer b = boxed(1000) + + expect: + UtilInstrumented.refEquals(b, b) + } + + def "boxes created with new are distinct"() { + expect: + !UtilInstrumented.refEquals(new Integer(5), new Integer(5)) + !UtilInstrumented.refEquals(new Integer(5), boxed(5)) + UtilInstrumented.refEquals(Integer.valueOf(5), boxed(5)) + } + + def "boxes of different types or with null"() { + expect: + !UtilInstrumented.refEquals(boxed(5), Long.valueOf(5)) + !UtilInstrumented.refEquals(boxed(5), null) + UtilInstrumented.refEquals(null, null) + !UtilInstrumented.refEquals(literal("a"), null) + } + + def "other objects compare by identity"() { + given: + Object o = new Object() + + expect: + UtilInstrumented.refEquals(o, o) + !UtilInstrumented.refEquals(o, new Object()) + } +} diff --git a/symbolic-explorer/driver/SVCompDriver.py b/symbolic-explorer/driver/SVCompDriver.py index a6c70f11..ccc4cd07 100644 --- a/symbolic-explorer/driver/SVCompDriver.py +++ b/symbolic-explorer/driver/SVCompDriver.py @@ -464,8 +464,8 @@ def run(self): logger.warning(f'[SVCOMP] Found uncaught exceptions during symbolic execution') verdict = Verdict.UNKNOWN - if (verdict == Verdict.VIOLATION) and Database.instance().get_tree(ENDPOINT_ID).reference_semantic_change: - logger.warning(f'[SVCOMP] Found reference semantic change (user-de-interned strings compared via Objects.equals) - downgrading VIOLATION to UNKNOWN') + if (verdict in (Verdict.SAFE, Verdict.VIOLATION)) and Database.instance().get_tree(ENDPOINT_ID).reference_semantic_change: + logger.warning(f'[SVCOMP] Found reference semantic change (reference comparison decided by symbolic values SWAT does not encode) - downgrading {verdict.name} to UNKNOWN') verdict = Verdict.UNKNOWN if verdict == Verdict.NO_SYMBOLIC_VARS: