Part 1: Ownership and Assets

Our new programming language is object-oriented. As usual, it includes 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:


class Money {
}

class Wallet {
    owned Money 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 owned Money. Information about ownership is NOT available at runtime; it is only available during compilation.

Assets

One benefit of tracking ownership is preventing accidental loss of valuable objects, such as money. To do so, we can declare classes with the keyword asset. Let's do this for Money:


asset class 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:


void test() {
    Money m = ...; // 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 method. For example:

owned Money test() {
    Money m = ...; // 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.

Method return types

When methods return objects, the return types can be annotated. For example:


owned Money withdraw() {
    // body not shown
}

If the return type is unannotated, then unowned is assumed.

Method parameters

When a reference is passed to a method as an argument, the method's declaration specifies initial and final ownership with >>. For example:


void spend(owned Money >> unowned Money m) { // m is owned initially but must be unowned at the end.
	// implementation not shown
};

void testSpend(owned Money >> Unowned Money m) {
	spend(m);
	// m is now of type unowned Money due to specification on spend() declaration.
}

If a method expects an argument that is unowned, this means that the method cannot take ownership. As a result, it is safe to pass an owned reference as an argument to a method that expects an unowned argument. After the method returns, the caller still holds ownership.

If a method parameter lacks >>, then ownership will not change. For example, void test(owned Money m) is equivalent to void test(owned Money >> owned Money m). void foo(unowned Money m) can accept a Money reference with any ownership and the caller maintains whatever ownership it had initially when it called that method.

If a method parameter lacks an annotation, then unowned is assumed.

Assignment

Assignment (=) transfers ownership when the variables on both sides are owning references. That is, the variable on the left becomes the new owner, and the variable on the right loses ownership. For example:


owned Money m = ...;
owned Money newMoney = m;
// Now m is of type unowned Money, not owned Money. 

unowned Money n = newMoney; // Ownership is still with newMoney because n is unowned

Invoking methods

The compiler checks each invocation to make sure it is permitted according to the ownership of the method arguments. For example:


void spend(owned Money >> unowned Money m) {
	// implementation not shown
};
void print(unowned Money m) {
    // implementation not shown
}

void test() {
	owned Money m = ...
	print(m);
	spend(m);
	spend(m); // COMPILE ERROR: spend() requires owned input, but m is unowned here
}

Local variables

Local variables are declared without any ownership specified; instead, you can optionally write compiler ownership checks [] to specify their ownership. The compiler will output an error if the ownership does not match the check. For example:

owned Money withdraw() {
    // body not shown
}

void test() {
    Money m = withdraw(); [m owned];
    spend(m); [m unowned];
    [m owned]; // COMPILE ERROR: m is unowned here

}

Getting rid of ownership

If ownership is no longer desired, disown can be used to relinquish ownership. For example:


class Money {
    int amount;
    
    void merge(owned Money >> unowned Money mergeFrom) {
        amount += mergeFrom.amount;
        disown mergeFrom; // We absorbed the value of mergeFrom, so the owner doesn't own it anymore.
    }
}

Method receivers (this)

Sometimes the ownership of this needs to change in a method. That fact can be specified by adding this as the first argument in the method declaration. For example:


class Money {
    void discard(owned Money >> unowned Money this) {
        disown this;
    }
}

class Wallet {
    void throwAwayMoney(owned Money >> unowned Money money) {
        money.discard(); // 'this' argument is implicit; do not include it in method call.
    }
}

Fields

Field declarations MUST include ownership specifications. These mean that at the END of each method, the state of the object the field references MUST match the annotation on the field. For example:



class Wallet {
	owned Money 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.
	owned Money exchange(owned Money >> unowned Money otherMoney) {
        Money temporaryMoney = money; // OK temporarily...
        money = otherMoney; // as long as ownership of money is restored by the end of the method.
        return temporaryMoney;
	}
}