-
Notifications
You must be signed in to change notification settings - Fork 6
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
Ghost constructors #11
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting idea! I'm a bit worried that this is an unintuitive interpretation of the ghost
keyword in this context though. I found it surprising to read that a ghost constructor can in fact be compiled and referenced at runtime, but you are just not allowed to "do anything" with the value.
I'd love to see a motivating example where just using /definiteAssignment:3
results in really sub-optimal code to model this manually.
been verified), and a use of the destructor `u.value` is compiled as | ||
just `u`. | ||
|
||
This lets a program allocate an array with element type `Unknown<T>` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It sounds like this paragraph is the key motivation for this feature, so consider pulling it up to the beginning of the document. :)
Do we have any other use cases for ghost constructors in mind? It would be worth spelling out alternative ways of ensuring storage is always initialized. I recently discovered the undocumented new T[n] (i => ...)
syntax for initializing arrays, which presumably could be used to explicitly assign an initial value when T
is not auto-initializable. You would then have to manually spell out the pre-conditions that prevent such values from being read and used in any way, of course.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is the main motivating example. Here's another plausible use:
You may develop a model that uses several different variants of a datatype, say Red
, Green
, Yellow
, and Blue
. But in your running program, you only ever want to store Green
or Blue
values. To make sure of this, you can mark Green
and Yellow
as ghost constructors.
A ghost-variant datatype is considered _not_ an equality-supporting | ||
type. That is, the type does not obtain the type characteristic | ||
`(==)`, and thus values of the type can be compared for equality only | ||
in ghost contexts. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this an attempt to prevent ghost constructors from being used in any expression? I worry that just the fact that you can successfully read such a value at runtime already allows control flow that shouldn't be, and that this construction will be unsound in a subtle way. It sounds like the following program would be valid and halt at runtime, but I would expect the myArray[0]
reference to be rejected statically, if I am trying to soundly avoid ever reading uninitialized storage:
datatype D = ghost G(x: int) | D0(y: int) | D1(z: int)
method Main() {
var myArray := new D[5];
var shouldNotReferenceThisAtRuntime := myArray[0];
expect false;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rules are intended to ensure soundness. You can read and copy these "uninitialized" values at run time, but there's no way to, at run time, determine whether or not you're holding a value that has been initialized.
The assignment shouldNotReferenceThisAtRuntime := myArray[0];
in your example is legal and causes no problem at run time. If the compiler decides to initialize myArray[0]
with a G
value, then myArray[0]
is essentially some rubbish that you should do anything with. But it's fine and safe to copy this rubbish into another variable.
(Btw, not that it changes the example, but just to point out that the compiler is also free to initialize myArray[0]
with a D0
or D1
value. Still, there's no way to determine at run time if that's what the compiler did.)
This PR introduces ghost constructors for datatypes, as per RFC dafny-lang/rfcs#11. For motivation of this feature, see the RFC or the example `Stack` class in `Test/comp/Uninitialized.dfy`. ## Feature description * Any (and all) constructor of a datatype can be declared as ghost. This is done by preceding the constructor name by the keyword `ghost`. (Note, if the constructor has attributes, those are placed before the name of the constructor, which is consistent with the attribute placement in other declarations. For a ghost constructor, this means the attribute is placed between the `ghost` keyword and the identifier that names the constructor.) * Compiled code may not explicitly name a ghost constructor. Moreover, compiled code is not allowed to depend on the need to check if a datatype value is of a ghost variant. Consequently, the verifier (and in some cases, the resolver) will enforce that any discriminator (e.g., `Cons?`) or destructor (e.g., `tail`) is used only when the datatype value is known not to be of a ghost variant. In other words, a precondition for these operations (in compiled contexts) is that the datatype value is not of a ghost variant. * Even though a program cannot (in compiled contexts) explicitly create ghost variants, Dafny might assign ghost variants as part of auto-initialization. These "assignments" are performed by... doing nothing! (Or nearly nothing.) In other words, since the verifier makes sure that ghost variants are never looked at, the compiler feels free to leave such variables as uninitialized. * Uninitialized variables usually means asking for trouble. Ghost constructors provides uninitialized storage and verifies that that program never looks at uninitialized variables. Still, there are two areas where uninitialized storage may affect execution (these two points were not mentioned in the RFC): - If storage is really uninitialized, then a run-time garbage collector may not be able to trace pointers accurately. - Dafny does not restrict `print`ing of uninitialized values. If a program uses `print` with an uninitialized values, there is in general no way to tell what will happen. The compilers that Dafny supports are managed languages (with the exception of C++, but C++ does not use a garbage collector; furthermore, the C++ compiler currently supports only a subset of Dafny, and that subset does not include ghost constructors). Since the target languages are managed and type safe, there is no way for the Dafny compilers to really leave storage uninitialized. Instead, values will typically have a `null`-equivalent value. This means that garbage collection will work and `print`ing will not crash the program. As for printing, `print` should be considered a debugging facility or a facility to make it easy to write student programs. * The RFC mentions a compiler optimization where a datatype with exactly one non-ghost constructor with exactly one non-ghost parameter can compiled as just that one parameter. This PR does not implement that optimization. A separate PR could implement this optimization along with the similar optimization for tuples with ghost components. Ghost constructors would benefit from the optimization, but do not depend on it. Closes dafny-lang/rfcs#11 <small>By submitting this pull request, I confirm that my contribution is made under the terms of the [MIT license](https://github.com/dafny-lang/dafny/blob/master/LICENSE.txt).</small> Co-authored-by: Fabio Madge <[email protected]>
This RFC proposes that each constructor of a datatype can be declared to be
ghost
.Declaring a constructor to be
ghost
limits usage of that constructor to ghost contexts and auto-initialization.It also restricts discriminators and destructors in compiled contexts, so that it is never necessary at run time to check if a value is of a ghost variant.