-
Notifications
You must be signed in to change notification settings - Fork 256
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Constant fields depending on non-initialized fields should not be usable in first-phase construction #2727
Comments
Good catch! You can do a bit worse, too, by creating cycles: class C {
const a := b
const b: int
constructor () {
b := a + 1;
new;
assert false;
}
} |
This PR fixes #2727 by adding an appropriate error message if constants are initialized in the wrong order.
I would like it if constant fields with RHS assignments can be translated to constant fields without a RHS that are assigned in the constructor, to reduce the amount of semantic concepts that exist in the language. However, I think the current 'lazy' behavior of constant fields cannot be mimicked by assigning them in the constructor. I think if we would add the concept of a lazy variable, as it exists in Scala (ref), then we can implement the desired equivalence, using a transformation that moves all assignment to the constructor, which is trivial since the location where they're assigned does not matter due to the semantics of lazy variables. |
Here is a variation of @cpitclaudel 's program, but getting to class C {
const a := B()
const b: int
function B(): int { b }
constructor (x: int) {
b := a + 1;
new;
assert false; // ouch! provable
}
} and yet another version that throws in a trait Tr {
const a := B()
const b: int
function B(): int
}
class C extends Tr {
function B(): int { b }
constructor (x: int) {
b := a + 1;
new;
assert false; // ouch! provable
}
} |
Great catch ! dafny/Test/git-issues/git-issue-2727.dfy Lines 144 to 154 in 3ac8137
Here is the error it will generate:
Specifically, I took the most permissive route: I forbid the access to constants before the "new;" if their right-hand-side transitively contains a call to a non-static function, because non-static functions could otherwise access uninitialized constants |
Here is how I would now approach this soundness issue. I want a solution that follows the general idea of being able to express anything Dafny needs to prove in Dafny (thanks @robin-aws for the wording)
Let's start with Fact B. Fact A and C: tell us that passing "this" to any regular function or method, before the Fact D then confirm we cannot call instance methods and functions with "this" available unless we have a way for them to NOT assume that all fields are assigned. This could be done in the future with an attribute So now, we have merely two choices:
|
It turns out, at least for the verifier of local variables, a subset type is assumed only after the first initialization. So for the verifier, x has type "Int" all the time, it's just that the subset constraint is assumed after the assignment. However, I was able to coin an example that does verify for the latest Dafny type Empty = x: int | false witness *
class TestEmpty {
const x: Empty
var y: int
constructor() ensures false {
x := 2;
new;
var y := x;
}
} This is alarming. |
Inspecting the Boogie file reveals something curious: implementation {:verboseName "TestEmpty._ctor (correctness)"} Impl$$_module.TestEmpty.__ctor() returns (this: ref, $_reverifyPost: bool)
{
var $_Frame: <beta>[ref,Field beta]bool;
var this.x: int;
var this.y: int;
var defass#this.x: bool;
var defass#y#0: bool;
var y#0: int where defass#y#0 ==> Lit(false);
// AddMethodImpl: _ctor, Impl$$_module.TestEmpty.__ctor
$_Frame := (lambda<alpha> $o: ref, $f: Field alpha ::
$o != null && read($Heap, $o, alloc) ==> false);
$_reverifyPost := false;
// ----- divided block before new; ----- C:\Users\mimayere\Documents\Dafny tests\classes-empty.dfy(6,32)
// ----- assignment statement ----- C:\Users\mimayere\Documents\Dafny tests\classes-empty.dfy(7,7)
assume true;
assume true;
assert $Is(LitInt(2), Tclass._module.Empty()); // Should not be provable
this.x := LitInt(2);
defass#this.x := true;
// ----- new; ----- C:\Users\mimayere\Documents\Dafny tests\classes-empty.dfy(6,32)
assert defass#this.x;
assume !read($Heap, this, alloc);
assume _module.TestEmpty.x(this) == this.x;
assume read($Heap, this, _module.TestEmpty.y) == this.y;
$Heap := update($Heap, this, alloc, true);
assume $IsGoodHeap($Heap);
assume $IsHeapAnchor($Heap);
// ----- divided block after new; ----- C:\Users\mimayere\Documents\Dafny tests\classes-empty.dfy(6,32)
// ----- assignment statement ----- C:\Users\mimayere\Documents\Dafny tests\classes-empty.dfy(9,11)
assume true;
assume true;
y#0 := _module.TestEmpty.x(this);
} It can prove Iin this context with the last assignment, I try to add assert $Is(LitInt(2), Tclass._module.Empty()); // Error, cannot prove
assert false; assert $Is(LitInt(2), Tclass._module.Empty()); // OK, can prove
//assert false; but there is an axiom stating that the two are equivalent. Asserting false at the end always works though. I shortened the issue tom: var y#0: int;
assert $Is(LitInt(2), Tclass._module.Empty());
y#0 := _module.TestEmpty.x(this); which still verifies, despite Now, here is one axiom that is required for this to pass: axiom 1 < $FunctionContextHeight
==> (forall $o: ref ::
{ _module.TestEmpty.x($o) }
$o != null && dtype($o) == Tclass._module.TestEmpty?()
==> $Is(_module.TestEmpty.x($o), Tclass._module.Empty())); As you can see, this axiom matches the RHS of the assignment of y, so it fires. Replacing |
In short, the fact that there is a constant of a type an empty type is actually picked up before the new; thanks to the encoding, and the solver can assume false and that's it. |
I minimized the example. SMT is more clever than I thought: No need to assign a variable at the end, just need to "ensures false" type Empty = x: int | false witness *
class TestEmpty {
const x: Empty
constructor() ensures false {
x := 2;
new;
}
} |
Dafny restricts the use of field in the first phase of an object constructor. It currently allows a field to be used if the field is a
const
with a given RHS. This is sometimes unsound, as the following program demonstrates.The allowance of reading, in the first phase of a constructor, a
const
with a RHS should apply only if the RHS can be determined not to depend on anyconst
field without a RHS.The text was updated successfully, but these errors were encountered: