Our new programming language is object-oriented. It includes contracts, which are like classes, which can have fields. In addition, of the many variables or fields that reference objects, one of them can own the object. For example:
contract Money {
}
contract Wallet {
Money@Owned m; // @Owned indicates that m owns the object it refers to
}
The compiler tracks ownership of each variable every time the variable is used. This information is part of the type of the variable. For example, the type of m
is Money@Owned
. Information about ownership is NOT available at runtime; it is only available during compilation.
Owned
.Shared
.Unowned
.Some owning references are to things that should not be accidentally lost. To prevent accidental loss, we can declare contracts with the keyword asset
. Let's do this for Money
:
asset contract Money {
}
Now, if we accidentally lose track of an owning reference to a Money
object (by letting it go out of scope without transferring ownership to somewhere else, such as a field), the compiler will give an error:
transaction test() {
Money m = ...; [m@Owned]; // OK, m is Owned here
// ERROR: cannot drop reference to owned asset m
}
We can fix this by (for example) returning m, assigning it to an owning field, or passing it as an argument to an appropriate transaction. For example:
transaction test() returns Money@Owned {
Money m = ...; [m@Owned]; // OK, m is Owned here
return m; // gives ownership of m to the caller of test()
}
Note that non-owning references to Money
are not restricted; the compiler gives no errors when they go out of scope.
When transactions return objects, the types must be annotated. For example:
transaction withdraw() returns Money@Owned {
// body not shown
}
When a reference is passed to a transaction as an argument, the transaction's declaration specifies initial and final ownership with >>
. For example:
transaction spend(Money@Owned >> Unowned m) { // m is Owned initially but must be Unowned at the end.
// implementation not shown
};
transaction testSpend(Money@Owned >> Unowned m) {
spend(m);
// m is now of type Money@Unowned due to specification on spend() declaration.
}
If a transaction expects an argument that is Unowned
, this means that the transaction cannot take ownership. As a result, it is safe to pass an Owned
reference as an argument to a transaction that expects an Unowned
argument. After the transaction returns, the caller still holds ownership.
If a transaction parameter lacks >>, then ownership will not change. For example, transaction test(Money@Owned m)
is equivalent to transaction test(Money@Owned >> Owned m)
. transaction foo(Money@Unowned m)
can accept a Money
reference with any ownership and the caller maintains whatever ownership it had initially when it called that transaction.
Ownership can be documented and checked by writing the ownership in brackets. For example, [m@Owned]
indicates that m
is an owning reference at that particular point in the code. The compiler will generate an error if this might not be the case. For example, with spend
as above:
transaction testSpend(Money@Owned >> Unowned m) {
spend(m); [m@Unowned]; // OK
[m@Owned]; // COMPILE ERROR: m is Unowned here
}
Ownership checks in []
never change ownership; they only document and check what is known.
Assignment (=
) transfers ownership. That is, the variable on the left becomes the new owner, and the variable on the right loses ownership. For example:
Money m = ...; [m@Owned];
Money newMoney = m; [m@Unowned];
// Now m is of type Money@Unowned, not Money@Owned.
The compiler checks each invocation to make sure it is permitted according to the ownership of the transaction arguments. For example:
transaction spend(Money@Owned >> Unowned m) {
// implementation not shown
};
transaction print(Money@Unowned m) {
// implementation not shown
}
transaction test() {
Money m = ... // assume m is now owned.
print(m); [m@Owned];
spend(m); [m@Unowned];
spend(m); // COMPILE ERROR: spend() requires Owned input, but m is Unowned here
}
[]
to specify their ownership. The compiler will output an error if the ownership does not match the check. For example:
transaction withdraw() returns Money@Owned {
// body not shown
}
transaction test() {
Money m = withdraw(); [m@Owned];
spend(m); [m@Unowned];
[m@Owned]; // COMPILE ERROR: m is Unowned here
If ownership is no longer desired, disown
can be used to relinquish ownership. For example:
contract Money {
int amount;
transaction merge(Money@Owned >> Unowned mergeFrom) {
amount += mergeFrom.amount;
disown mergeFrom; // We absorbed the value of mergeFrom, so the owner doesn't own it anymore.
}
}
this
)Sometimes the ownership of this
needs to change in a transaction. That fact can be specified by adding this
as the first argument in the transaction declaration. For example:
contract Money {
transaction discard(Money@Owned >> Unowned this) {
disown this;
}
}
contract Wallet {
transaction throwAwayMoney(Money @ Owned >> Unowned money) {
money.discard(); // 'this' argument is implicit; do not include it in transaction call.
}
}
Field declarations MUST include ownership specifications. These mean that at the END of each transaction, the state of the object the field references MUST match the annotation on the field. For example:
contract Wallet {
Money@Owned money; // Note that this annotation is on a field declaration!
// exchange removes (and returns) the current contents of the Wallet, replacing it with the input Money.
transaction exchange(Money@Owned >> Unowned otherMoney) returns Money@Owned {
Money temporaryMoney = money; [money@Unowned]; // OK temporarily...
money = otherMoney; [money@Owned]; // as long as money@Owned is restored by the end of the transaction.
return temporaryMoney;
}
}