Skip to content

Java: collections assume default behavior of equals(), hashCode() #6491

Description

@fcseidl

Dafny version

4.11.0

Code to produce this issue

This is a language-interface issue, so reproduction requires a Java file and a Dafny file. In BoxPkg/Box.java:

public class Box {

    public BigInteger v;

    /** Factory: each call allocates a fresh, distinct object. */
    public static Box Make(BigInteger v) {
        Box b = new Box();
        b.v = v;
        return b;
    }

    /** Mutate the wrapped (hash-relevant) value. */
    public void setVal(BigInteger v) {
        this.v = v;
    }

    @Override
    public boolean equals(Object other) {
        return other instanceof Box && this.v.equals(((Box) other).v);
    }

    @Override
    public int hashCode() {
        return v.intValue();
    }
}

And in BoxUnsound.dfy:

module {:compile false} {:extern "BoxPkg"} BoxPkg {
    class {:extern "Box"} Box {
        static method {:extern "Make"} {:axiom} Make(v: int) returns (b: Box)
            ensures fresh(b)

        method {:extern "setVal"} SetVal(v: int)
            modifies this
    }
}

module Repro {
    import opened BoxPkg

    // BUG 1: structural equals() makes the compiled set identify two physically
    // distinct objects, contradicting the verified cardinality.
    method DemoCollision() {
        var a := Box.Make(1);
        var b := Box.Make(1);       // distinct object, equal value

        assert a != b;              // verified: two distinct fresh references
        var s := {a, b};
        assert |s| == 2;            // verified from a != b, false at runtime

        print "[collision] verified |s| == 2, but runtime |s| = ", |s|, "\n";
    }

    // BUG 2: hashCode() tracks the mutable value, so mutating it moves the object
    // to a different bucket and the compiled set can no longer find a member the
    // model still says is present.
    method DemoDiscontinuity() {
        var p := Box.Make(1);
        var s := { p };

        p.SetVal(2);                // change the (hash-relevant) value

        assert p in s;              // verified: the set is keyed by reference
        print "[discontinuity] verified (p in s) == true, but runtime = ", p in s, "\n";
    }

    method Main() {
        DemoCollision();
        DemoDiscontinuity();
    }
}

Command to run and resulting output

dafny run BoxUnsound.dfy --target:java --input BoxPkg/Box.java

What happened?

I get this output:


Dafny program verifier finished with 3 verified, 0 errors
[collision] verified |s| == 2, but runtime |s| = 1
[discontinuity] verified (p in s) == true, but runtime = false

It shows that in each demo method in the Dafny source, the verifier proves an expression which is false at runtime.
The extern Java class Box, unbeknownst to Dafny, overrides equals() and hashCode(), rendering Dafny's model of set<Box> unsound.

There should be a way to create a sound model of Box in Dafny without changing the Java source. The default behaviors of equals() and hashCode() are available even for Java classes which override them through == and System.identityHashCode(). If the Java implementation of set<Box> used the latter two, rather than their overridden alternatives, the unsoundness would disappear.

EDIT: admittedly, assuming every generic is a reference type would cause unexpected behavior from, e.g., set<int>, so this is a complicated issue.

What type of operating system are you experiencing the problem on?

Mac

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind: bugCrashes, unsoundness, incorrect output, etc. If possible, add a `part:` label

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions