AdeptLanguage / Adept

The Adept Programming Language
GNU General Public License v3.0
119 stars 9 forks source link

Checking non-owing reference validity #65

Open vtereshkov opened 2 years ago

vtereshkov commented 2 years ago

The language documentation mentions

...a method named commit(), which maintains the validity of the original owner while transferring ownership.

I understand this as that the old reference will remain valid while the new owner is alive. In other words, this old reference is now a weak reference. But how can I check that it is actually valid when I'm going to use it? How can I ensure that the new owner has not left its scope?

IsaacShelton commented 2 years ago

As of Adept v2.6, weak ownership references do not know whether underlying data is still alive. It's up to the programmer to only use weak references if they are valid.

Weak ownership references (like weak raw pointers) should really only be used when it's obvious that they will remain valid until they (or their parent) run out of scope.

When ownership is murky, heap allocating or cloning are the two of the simplest solutions. (but also now shared pointers which I later describe in this response)

With heap allocating (raw pointers example):

import basics

func main {
    original *String = new String    
    defer {
        original.__defer__()
        delete original
    }

    *original = "This is a string that I own".clone()

    if true {
        string *String = original
        print("String: " + *string)
    }

    // We can know that 'original' still exists since it was heap allocated and
    // it hasn't been explicitly destroyed yet
    print("Original: " + *original)
}

With types that are cloneable (e.g. String, <*$T> List) for example:

import basics
import random

func main {
    randomize()

    original String = "This is a string that I own".clone()
    copied String

    if true {
        string String = normalizedRandom() < 0.5 ? original.commit() : original.clone()
        print("String: " + string)
        copied = string.clone()
    }

    // We don't know if 'original' still exists or not, so it must be assumed to be invalid
    // However, 'copied' is still guaranteed to be valid, so we can use that
    print("Copied: " + copied)
}

Also, you can now wrap objects in shared pointers to get the same effect to what you describe. Using an implementation of shared pointers like https://github.com/IsaacShelton/AdeptSharedPtr for example,

import basics
import "Shared.adept"

func main {
    name <String> WeakPtr

    if true {
        shared_pointer <String> SharedPtr = SharedPtr(new String)
        *shared_pointer.get() = "Hello" + " " + "World"

        name = shared_pointer.weak()

        printf("name is %S, while data is still alive\n", *name.get())
        printf("name pointer is %p, while data is still alive\n", name.get())
    }

    printf("name pointer is %p, when data is no longer alive\n", name.get())
}

or a more complicated example

import basics
import "Shared.adept"

record PhoneBook (people <String> List)
record Office (phonebook <PhoneBook> SharedPtr)

func main {
    weak_pointer <PhoneBook> WeakPtr

    if true {
        office Office

        if true {
            phonebook <PhoneBook> SharedPtr = makeExamplePhoneBook()
            office = Office(phonebook)
        }

        weak_pointer = office.phonebook.weak()
    }

    printf("%p is null, since the phonebook has no strong references remaining\n", weak_pointer.get())
}

func makeExamplePhoneBook() <PhoneBook> SharedPtr {
    phonebook <PhoneBook> SharedPtr = SharedPtr(new PhoneBook)
    phonebook.get().people.add("Person1")
    phonebook.get().people.add("Person2")
    phonebook.get().people.add("Person3")
    return phonebook.clone()
}
vtereshkov commented 2 years ago

So I suppose that using commit() is very dangerous, donate() being safer. But even with donate(), I get neither a compile-time error, nor a run-time panic when trying to use a donated string. Just a "<DONATED>" value (which may accidentally coincide with a well-formed string with the same value).

IsaacShelton commented 2 years ago

Yeah, the compiler doesn't guaranteed safety for them.

Using previously donated strings should probably be a runtime error like you describe.

You can however check the ownership status of a string though if you want to determine if it donated its contents.

import basics

func main {
    // Start with two strings that have ownership
    string1 String = "Hello".clone()
    string2 String = "World".clone()

    // Capture raw given value returned from 'donate' for one of them
    given POD String = string1.donate()
    defer given.__defer__()

    // Capture taken value returned from 'donate' for the other one of them
    taken String = string2.donate()

    printf("'string1' should be marked as donated (is donor): %B\n", string1.ownership == StringOwnership::DONATED)
    printf("'string2' should be marked as donated (is donor): %B\n", string2.ownership == StringOwnership::DONATED)
    printf("'given' should not be marked as donated (is given) %B\n", given.ownership == StringOwnership::DONATED)
    printf("'taken' should not be marked as donated (is owned) %B\n", taken.ownership == StringOwnership::DONATED)
}
'string1' should be marked as donated (is donor): true
'string2' should be marked as donated (is donor): true
'given' should not be marked as donated (is given) false
'taken' should not be marked as donated (is owned) false

Also StringOwnership::DONATED should really be renamed to StringOwnership::DONOR to avoid confusion, although this is a breaking change. Opt-out runtime errors for usage of donated strings is another good feature for the next release.

--- Update: ---

In the latest build of the standard library for Adept 2.7...

Runtime errors now exist when a donor string is used after donating. (opt-out by default)

And StringOwnership::DONATED is now renamed to StringOwnership::DONOR.

vtereshkov commented 2 years ago

Perhaps any attempt to commit() or donate() something that is not owned should be a run-time error as well.

IsaacShelton commented 2 years ago

Yes donate() should probably raise a runtime error if the subject string doesn't have ownership.

With commit(), sometimes you only want to transfer ownership if it's possessed. So instead of breaking it, a stricter version named give() which requires ownership, might be good to introduce.

(these changes have now been implemented in the latest version of the standard library for 2.7)

.commit()

.commit() will transfer ownership (if able to) and will retain a reference.

(using .donate() or .give() is recommended instead in most cases)

pragma ignore_unused
import basics

func main {
    source String = "This string has ownership".clone()
    reference String = "This string does not"

    // 'source' gives ownership to 'ok', but retains a reference
    ok String = source.commit()

    // 'reference' will give a reference to 'still_ok', since it doesn't have ownership
    still_ok String = reference.commit()
}

.donate()

.donate() will transfer ownership (or raise a runtime error if unable to). Invalidates the subject after use.

pragma ignore_unused
import basics

func main {
    source String = "This string has ownership".clone()
    reference String = "This string does not"

    // 'source' gives ownership to 'ok', and does not retain a reference
    ok String = source.donate()

    // 'reference' cannot give ownership to 'failure', since it doesn't have ownership
    // (this will raise a runtime error)
    failure String = reference.donate()
}

.give()

.give() will transfer ownership (or raise a runtime error if unable to). Will retain a reference.

pragma ignore_unused
import basics

func main {
    source String = "This string has ownership".clone()
    reference String = "This string does not"

    // 'source' gives ownership to 'ok', but retains a reference
    ok String = source.give()

    // 'reference' cannot give ownership to 'failure', since it doesn't have ownership
    // (this line will raise a runtime error)
    failure String = reference.give()
}