Dafny compilation of trait and class
This document describes the compilation of trait
and class
declarations.
Specifically, the document addresses
trait
andclass
declarations with type parameters andextends
clauses- instance members of those declarations
- compilation into the target languages C#, Java, JavaScript, and Go
The document does not include descriptions of
static
members:extern
declarations- how to translate a method with multiple out-parameters into languages that support only one (Java and JavaScript)
Member declarations
For the purpose of compilation, there are eight kinds of member declarations:
- Mutable fields:
var a: X
- Immutable fields:
const c: X
- Immutable fields with a right-hand side (RHS):
const d: X := E
- Body-less functions:
function method F<U>(x: X): Y
- Functions with an implementation:
function method G<U>(x: X): Y { E }
- Body-less methods:
method M<U>(x: X) returns (y: Y)
- Methods with an implementation:
method N<U>(x: X) returns (y: Y) { S }
- Constructors:
constructor Ctor(x: X) { S }
Here and throughout, X
and Y
denote any types, E
denotes
an expression, and S
denotes a statement.
Preliminaries
Target languages
When speaking in general terms about the target language, this document uses
words like “interface”, “class”, and “static”, but meaning the target-language
equivalent of those. Such an “interface” is like a trait
in Dafny, but with
no fields and no implementations.
Type descriptors
The compilation of (non-reference) Dafny types to target types is many-to-one. For example, a subset type
type Odd = x: int | x % 2 == 1
compiles to the same thing as the base type int
does. For this reason,
Dafny adds run-time type descriptors, which lets these types be distinguished
at run time. The primary purpose of this is to be able to find a “default”
value of a type parameter at run time. Not all type parameters need a corresponding
type descriptor, but the sections below show them everywhere. The name
RTD
is used to denote the type of the type descriptors.
Outline
The rest of this document presents, in order, the compilation of:
- members for simple traits (no parent traits)
- members for simple classes (no parent traits)
- members inherited from parent traits
- constructor bodies (in classes)
Compilation of traits
Consider a trait declared as
trait Trait<V> {
var a: X
const c: X
const d: X := E
function method F<U>(x: X): Y
function method G<U>(x: X): Y { E }
method M<U>(x: X) returns (y: Y)
method N<U>(x: X) returns (y: Y) { S }
}
Note that a trait does not have any constructors.
A trait gets compiled into one interface and one “companion” class. Using a Dafny-like syntax, the target of the compilation is:
interface Trait<V> {
function method a(): X
method set_a(value: X)
function method c(): X
function method d(): X
function method F<U>(rtdU: RTD, x: X): Y
function method G<U>(rtdU: RTD, x: X): Y
method M<U>(rtdU: RTD, x: X) returns (y: Y)
method N<U>(rtdU: RTD, x: X) returns (y: Y)
}
class Companion_Trait<V> {
static function method d(rtdV: RTD, _this: Trait<V>) { E }
static function method G<U>(rtdV: RTD, rtdU: RTD, _this: Trait<V>, x: X): Y { E }
static method N<U>(rtdV: RTD, rtdU: RTD, _this: Trait<V>, x: X) returns (y: Y) { S }
}
There is no subtype relation between Trait
and Companion_Trait
.
The companion class is used only as a “home” for the static methods.
For any member with an implementation (d
, G
, and N
), there is both a declaration
in the interface and a corresponding definition in the companion class. The implementation
(RHS or body) of the member is compiled into the latter.
The functions and methods in the interface take a type descriptor for U
, but not for
V
. This is because type descriptors of the receiver object are supplied at the time
the object is created. In contrast, the implementations in the companion class
do take a type descriptor for V
, because any type descriptors of the receiver will
be stored in the receiver in terms of the type parameters of the receiver’s class.
Note: Evidently, type parameters of the trait (or class) are not in scope in
the RHS a const
declaration. That is probably a bug. If this changes, then
method d
in the companion class also needs to take type-descriptor parameters.
Note: Evidently, a const
without a RHS in a trait is not allowed to be overridden (to
be given a RHS). That restriction was probably necessary under the previous encoding, but
it isn’t anymore. It would be good to remove this restriction.
C#
C# supports properties. Such a property is a pair of methods: a getter and a(n optional) setter. Dafny compiles a mutable trait field into a property with a getter and setter, and compiles an immutable field into a property with just a getter.
The immutable field d
with a RHS still gets translated into a static function in the companion
class, since it is a static function with an parameter called _this
.
The C# target code uses some features of reflection instead of using explicit type-descriptor parameters.
interface Trait<V> {
property a: X { get; set; }
property c: X { get; }
property d: X { get; }
function method F<U>(x: X): Y
function method G<U>(x: X): Y
method M<U>(x: X) returns (y: Y)
method N<U>(x: X) returns (y: Y)
}
class Companion_Trait<V> {
static function method d(_this: Trait<V>) { E }
static function method G<U>(_this: Trait<V>, x: X): Y { E }
static method N<U>(_this: Trait<V>, x: X) returns (y: Y) { S }
}
Java
A static method in Java cannot use the type parameters of the enclosing class. Therefore, the companion class for Java instead adds these type parameter to the method.
interface Trait<V> {
function method a(): X
method set_a(value: X)
function method c(): X
function method d(): X
function method F<U>(rtdU: RTD, x: X): Y
function method G<U>(rtdU: RTD, x: X): Y
method M<U>(rtdU: RTD, x: X) returns (y: Y)
method N<U>(rtdU: RTD, x: X) returns (y: Y)
}
class Companion_Trait {
static function method d<V>(rtdV: RTD, _this: Trait<V>) { E }
static function method G<V, U>(rtdV: RTD, rtdU: RTD, _this: Trait<V>, x: X): Y { E }
static method N<V, U>(rtdV: RTD, rtdU: RTD, _this: Trait<V>, x: X) returns (y: Y) { S }
}
JavaScript
Being a dynamicly typed language, objects in JavaScript are built by creating members during object construction. This flexibility means that body-less definitions don’t generate any code.
A trait gives rise to just one main type, which is a JavaScript class. It serves the purpose of the companion class.
Finally, since JavaScript is dynamicly typed, the language does not require (or support) type parameters. However, compilation still generates type descriptors.
The result is thus organized as follows:
class Trait {
static function method d(rtdV, _this) { E }
static function method G(rtdV, rtdU, _this, x) { E }
static method N(rtdV, rtdU, _this, x) { S }
}
The members without implementations (a
, c
, F
, and M
) are during object construction,
as described in a later section.
Go
Go has no type parameters, so those are replaced by the empty interface type.
interface Trait {
function method a(): X
method set_a(value: X)
function method c(): X
function method d(): X
function method F(rtdU: RTD, x: X): Y
function method G(rtdU: RTD, x: X): Y
method M(rtdU: RTD, x: X) returns (y: Y)
method N(rtdU: RTD, x: X) returns (y: Y)
}
class Companion_Trait {
static function method d(_this: Trait) { E }
static function method G(rtdV: RTD, rtdU: RTD, _this: Trait, x: X): Y { E }
static method N(rtdV: RTD, rtdU: RTD, _this: Trait, x: X) returns (y: Y) { S }
}
Compilation of class members
Consider a class declared as
class Class<V> {
var a: X
const c: X
const d: X := E
function method G<U>(x: X): Y { E }
method N<U>(x: X) returns (y: Y) { S }
}
Constructors of the class are considered in a later section. Note that all functions and methods of a class to be compiled have bodies.
A class gets compiled into one target class. Using a Dafny-like syntax, the target of the compilation is:
class Class<V> {
var _rtdV: RTD
var a: X
var _c: X
function method c(): X { _c }
function method d(): X { E }
function method G<U>(rtdU: RTD, x: X): Y { E }
method N<U>(rtdU: RTD, x: X) returns (y: Y) { S }
}
The type descriptor for V
is passed into the constructor (not shown here, but see
a later section) and stored in the field _rtdV
. The functions and methods in the class
take a type descriptor for U
and access _rtdV
via the receiver object.
The value of the immutable field c
is computed in the constructor (not shown here)
and therefore needs to be stored. For this purpose, the target class uses a backing
field _c
, which is returned by the function c()
.
Design alternative: Immutable field d
does not need a backing store, since its value
is an expression that can easily be compiled to be evaluated each time the field is
accessed. An alternative would be to nevertheless introduce a backing store for it.
That would cost additional space in each object, but would avoid re-evaluating the
RHS E
and would make the treatment of c
and d
more uniform. A possible advantage
of the current design is that it gives a way in a Dafny program to select between
the backing-stored constant and a re-evaluate constant (an argument that doesn’t
apply to immutable fields inherited from traits).
Design alternative: Instead of using a backing store for immutable fields, the target class could just declare these as fields. This would be more straightforward, though it would requires storage in every object and wouldn’t offer the possibility of letting a Dafny program select between backing store and re-evaluation.
Note: Evidently, type parameters of the trait (or class) are not in scope in
the RHS a const
declaration. That is probably a bug. If this changes, then
method d
above also needs to take type-descriptor parameters. The type descriptors
must be initialized before an other initializing expression needs them.
C#
The compilation to C# does not use type descriptors, so the _rtdV
field is not
present and neither are the type-descriptor parameters.
The functions for retrieving c
and d
are declared as getter properties.
class Class<V> {
var a: X
var _c: X
property c: X { get { _c } }
property d: X { get { E } }
function method G<U>(x: X): Y { E }
method N<U>(x: X) returns (y: Y) { S }
}
Java
class Class<V> {
var _rtdV: RTD
var a: X
var _c: X
function method c(): X { _c }
function method d(): X { E }
function method G<U>(rtdU: RTD, x: X): Y { E }
method N<U>(rtdU: RTD, x: X) returns (y: Y) { S }
}
JavaScript
The compilation to JavaScript uses getters for the immutable fields.
The _rtdV
, a
, and _c
fields are declared by virtue of being assigned in the
constructor. In the following, they are nevertheless shown as explicit field
declarations:
class Class<V> {
var _rtdV
var a
var _c
property c { get { _c } }
property d { get { E } }
function method G(rtdU, x) { E }
method N(rtdU, x) { S }
}
Go
Go doesn’t have type parameters, but the compiler nevertheless generates type descriptors.
class Class {
var _rtdV: RTD
var a: X
var _c: X
function method c(): X { _c }
function method d(): X { E }
function method G(rtdU: RTD, x: X): Y { E }
method N(rtdU: RTD, x: X) returns (y: Y) { S }
}
Inherited members
Here is a trait Parent
and two types that extend it, a trait Trait
and a class Class
.
Other than both extending Parent
, types Trait
and Class
are unrelated.
The extending types inherit all members of Parent
and override F
and M
to give
them implementations.
trait Parent<V> {
var a: X
const c: X
const d := c
function method F<U>(x: X): X
function method G<U>(x: X): X { E }
method M<U>(x: X) returns (y: X)
method N<U>(x: X) returns (y: X) { S }
}
trait Trait<V> extends Parent<W> {
function method F<U>(x: X): Y { E }
method M<U>(x: X) returns (y: Y) { S }
}
class Class<V> extends Parent<W> {
function method F<U>(x: X): Y { E }
method M<U>(x: X) returns (y: Y) { S }
}
The compilation of Trait
is as follows:
interface Trait<V> extends Parent<W> {
}
class Companion_Trait<V> {
static function method F<U>(rtdV: RTD, rtdU: RTD, _this: Trait<V>, x: X): Y { E }
static method N<U>(rtdV: RTD, rtdU: RTD, _this: Trait<V>, x: X) returns (y: Y) { S }
}
The extending trait simply indicates its relationship to Parent
. The overriding
member implementations are placed in the companion class, where the type of their receiver
is Trait<V>
and where the type-descriptor parameters correspond to the type parameters of
Trait
(not Parent
) and the member itself.
The compilation of Class
is as follows:
class Class<V> extends Trait<W> {
var _rtdV: RTD
var _a: X
function method a(): X { _a }
method set_a(value: X) { _a := value; }
var _c: X
function method c(): X { _c }
function method d(): X {
Companion_Parent<W>.d(W(_rtdV), this)
}
function method F<U>(rtdU: RTD, x: X): Y { E }
function method G<U>(rtdU: RTD, x: X): Y {
Companion_Parent<W>.G(W(_rtdV), rtdU, this, x)
}
method M<U>(x: X) returns (y: X) { S }
method N<U>(rtdU: RTD, x: X) returns (y: Y) {
y := Companion_Parent<W>.N(W(_rtdV), rtdU, this, x);
}
}
As shown in a section above, the class adds a field to hold the type descriptor for V
.
The extending class provides a backing store for the inherited mutable field and immutable field without a RHS. Using these, it then implements the target-language inherited getters and setters.
It implements the inherited getter for d
, whose implementation calls the implementation
given in the companion class for Parent
. The notation W(_rtdV)
stands for the type
descriptor for W
that uses _rtdV
as the type descriptor for V
.
The overridden members F
and M
are straightforwardly declared in the class.
The implementation of the inherited members G
and N
are given as calls to the
respective implementations in the companion class for Parent
.
C#
The compilation to C# uses property getters and setters for a
and c
.
Java
Note: Evidently, the compiler emits a setter for c
. It would be nice if it can be removed.
JavaScript
As described above, but with no type parameters.
Go
When a class or trait extends another trait Parent
in Dafny, it instantiates the
type parameters of Parent
. Consequently, any members inherited from Parent
have
different type signatures than in Parent
. This works the same way in C# and Java,
so the Dafny type signatures of overriding members get the new types. Since type
signatures are never declared in JavaScript, so this is a moot point there. In Go,
however, the lack of type parameters means that any inherited members retain their
original signatures (except for the receiver parameter).
To let the overriding body use the Dafny types, compilation to Go adds new local variables corresponding to the formal parameters of the member. The local variables have the instantiated types. Those corresponding to in-parameters are initialized by a downcast of the given in-parameters. Dually, an upcast is performed at return points.
In the following, X
and Y
refer to the types used in Parent
, whereas WX
and WY
refer to those types with Parent
’s type parameter V
replaced by
the type W
. The Dafny-like as
notation shows upcasts and downcasts.
class Class extends Trait {
var _rtdV: RTD
var _a: WX
function method a(): X { _a as X }
method set_a(value: X) { _a := value as WX; }
var _c: WX
function method c(): X { _c as X }
function method d(): X {
Companion_Parent.d(W(_rtdV), this) as X
}
function method F(rtdU: RTD, x: X): Y {
var x: WX := x;
E as Y
}
function method G(rtdU: RTD, x: X): Y {
Companion_Parent.G(W(_rtdV), rtdU, this, x as WX) as WY
}
method M(x: X) returns (y: Y) {
{
var x: WX := x;
var y: WY;
S
return y as Y;
}
}
method N(rtdU: RTD, x: X) returns (y: Y) {
var y: WY := Companion_Parent.N(W(_rtdV), rtdU, this, x as WY);
return y as Y;
}
}
There is no need to say extends Trait
in Go, because trait membership is not nominal.
Nevertheless, the compiler generates some code that will cause the Go compiler to
verify Class extends Trait
at compile time.
Note: Previously, traits were represented using a mix of a “struct” and an “interface” in Go.
In that design, the “struct” portion was embedded into the class, which allowed fields
to be inherited using Go’s embedding. On the downside, this required each Dafny
object to be represented as a collection of Go objects, with mutual pointers between them
This required the compiler to insert upcasts, which became difficult to maintain in the
implementation. Moreover, the design was problematic for checking equality and became
impractical in the presence of type variance. For example, consider using a seq<Class>
as
a seq<Parent>
. One possibility is to copy the entire sequence, performing an upcast for
each element. This would have a large cost at run time. A more tantalizing possibility is
to always use the “main” pointer to an object when storing it in the sequence. Then, the
conversion from a seq<Class>
to a seq<Parent>
is a no-op. However, consider some
other seq<Parent>
whose element include instances from a variety of classes that extend
Parent
. Go obtain an element of that sequence as a Parent
would require casting the
“main” object, whose type is unknown, to its Parent
component. It would be possible to
do this if the “interface” definition of the object had a function to return the Parent
portion. So, rather than keeping this web of pointers among the various portions of the
object, this design was abandoned in favor of what is done for the other target languages,
where instead of a “struct” portion of a trait, the trait “interface” is used for everything
and include getters/setters for fields.
Constructors
A class constructor is responsible for initializing all fields of the object. This includes the fields declared in the class as well as the set of fields inherited from ancestor traits. Note that even if a trait is an ancestor in more than one way, there is only one copy of its fields. (Dafny restricts programs so that every occurrences of the same ancestor trait is given the same type parameters.)
Target languages provide various constructor mechanisms of their own. Dafny compilation uses those only to the extent required by the target language. Each Dafny constructor is compiled into a target-language method that performs the bulk of the work.
Consider the following declarations of a class and its ancestor traits:
trait A<V> {
var a: X
const c: X
}
trait B<V> extends A<W> {
}
trait C<V> extends A<W> {
}
class Class<V> extends B<W>, C<W> {
var k: X
constructor Init(x: X) {
a := x;
c := x;
}
}
The class as follows, where the target-language constructor is indicated with the
keyword constructor
and the bulk of the work is done in a method:
class Class<V> extends A<W>, B<W>, C<W> {
var _rtdV: RTD
var _a: X
function method a(): X { _a }
method set_a(value: X) { _a := value; }
var _c: X
function method c(): X { _c }
var k: X
constructor (rtdV: RTD) {
_rtdV := rtdV;
_a := 0;
_c := 0;
k := 0;
}
method Init(x: X) {
_a := x;
_c := x;
k := x;
}
}
The target constructor assigns some initial values to _a
and _c
(to cover the
case where these are not assigned explicitly in the Dafny constructor). The RHS 0
in these assignments is meant to be suggestive of an appropriate default value of
the appropriate type.
Note: It would be better to move the initial assignments of the fields into the initialization method, because then the program could be analyzed to determine whether or not those assignment are needed at all.
C#
The compilation follows the general case, except for three things. First, type descriptors are not used for C#. Second, the setters and getters make use of C# properties. Third, the initial assignments to the fields are done as part of the field declarations.
class Class<V> extends A<W>, B<W>, C<W> {
var _a: X := 0
property a: X {
get { _a }
set(value: X) { _a := value; }
}
var _c: X := 0
property c: X {
get { _c }
}
var k: X := 0
constructor () { }
method Init(x: X) {
_a := x;
_c := x;
k := x;
}
}
Java
class Class<V> extends A<W>, B<W>, C<W> {
var _rtdV: RTD
var _a: X
function method a(): X { _a }
method set_a(value: X) { _a := value; }
var _c: X
function method c(): X { _c }
method set_c(value: X) { _c := value; }
var k: X
constructor (rtdV: RTD) {
_rtdV := rtdV;
_a := 0;
_c := 0;
k := 0;
}
method Init(x: X) {
_a := x;
_c := x;
k := x;
}
}
Note: Evidently, the Java target always uses setters when assigning to fields.
JavaScript
Compilation to JavaScript follows the general form, except that there are no type
parameters and no need for extends
clauses.
Go
The compilation follows the general form, but without type parameters and no need
for extends
clauses.