zax.io

A guide to the Zax programming language

View project on GitHub

Zax Programming Language

Strong and Weak Pointers

Pointers qualified as strong will automatically track the lifetime of an allocated type instance by performing reference counting so when the last common reference to a type’s instance is discarded, the instance becomes deallocated.

The strong pointers are a form of smart pointer logic as a tool to ensure the lifetime of an type’s instance is destroyed when the last referencing pointer is discarded. A weak reference can be used to prevent circular dependencies by detecting when a type’s instance kept is alive by a strong pointer or when a type instance was already destroyed. The usage of weak references is a common technique to prevent memory leakage issue as strong pointers are not automatically garbage collected.

Thread safety for the reference counting mechanism for strong and weak pointers is guaranteed. When a strong pointer is assigned to new variables in a concurrency conflicts, the reference counting mechanism does not require thread barriers to ensure a reference count is kept accurate. The lifetime is maintained using atomic operations and concurrency of a lifetime is kept accurate across threads. However, this does not imply accessing contents inside a strong pointer has any concurrency protection or safety guarantees. If two threads modify contents inside a type’s instance pointed to by a strong pointer at the same time, then those contents can have concurrency issues as well as undefined behaviors.

strong versus handle pointers

Pointers qualified as strong operate in the same manner as handle pointers with a few key differences. Whereas strong pointers have a weak counterpart, handle pointers have a hint pointer counterpart. Pointers qualified as strong have thread safety properties related to reference counting mechanism of a type’s instance whereas handle pointers do not.

Due to additional overhead related to thread safety guarantees for strong pointers, handle pointers are more efficient at the cost of thread concurrency safety.

Allocation of strong pointers

Pointers qualified as strong are allocated in similar manners to other pointers, such as own, discard, handle, and collect pointers. The difference is that strong pointers can be co-owned by more than one variable. When the last variable holding a strong pointer is discarded (or reset to point to nothing) the allocated type instance is destructed and deallocated.

print final : ()(...) = {
    // ...
}

assert final : ()(check : Boolean) = {
    // ...
}

MyType :: type {
    myValue1 : Integer
    myValue2 : String
}

printIfValidPointer final : ()(pointerToValue : MyType *) = {
    if pointerToValue
        print("true")
    else
        print("false")
}

func final : (result : String)() = {

    scope {
        // the @ operator allocates `value1` dynamically with the context's
        // allocator and a `strong` pointer is maintained
        value1 : MyType * strong @

        // `value2` is is a `strong` pointer but the value is initialized to
        // point to nothing
        value2 : MyType * strong

        // both `value2` and `value1` have a `strong` pointer to the same
        // `MyType` instance
        value2 = value1

        printIfValidPointer(value1) // will print "true"
        printIfValidPointer(value2) // will print "true"

        assert(value1 == value2)

        // `value2` and `value1` pointers both fall out of scope but only a
        // single `MyType` instance is destructed and deallocated
    }

    return "I'll be back."
}

strong pointer value replacement

Pointers qualified as strong can only point to a single instance of a type. If a pointer is reset to point to a new instance of a type then the original ownership claim is released and if the value was the last owner of a type’s instance then the type instance is discarded and the memory is deallocated.

print final : ()(...) = {
    // ...
}

assert final : ()(check : Boolean) = {
    // ...
}

MyType :: type {
    myValue1 : Integer
    myValue2 : String
}

printIfValidPointer final : ()(pointerToValue : MyType *) = {
    if pointerToValue
        print("true")
    else
        print("false")
}

func final : (result : String)() = {

    scope {
        // the @ operator allocates `value1` dynamically with the context's
        // allocator and a `strong` pointer is maintained
        value1 : MyType * strong @

        // the @ operator allocates `value2` dynamically with the context's
        // allocator and a `strong` pointer is maintained to another `MyType`
        // instance
        value2 : MyType * strong @

        printIfValidPointer(value1) // will print "true"
        printIfValidPointer(value2) // will print "true"

        // `value1` and `value2` point to different `MyType` instances
        assert(value1 != value2)

        // `value2`'s value is replaced, thus `MyType` instance being `own`
        // in `value2` is destructed and deallocated and then both
        // `value1` and `value2` point to the same `MyType` instance
        value2 = value1

        printIfValidPointer(value1) // will print "true"
        printIfValidPointer(value2) // will print "true"

        // `value1` and `value2` point to the same `MyType` instances
        assert(value1 == value2)

        // `value2` and `value1` pointers both fall out of scope but only
        // the single `MyType` instance originally allocated in `value1` is
        // destructed and deallocated
    }

    return "I'll be back."
}

strong pointers lifetime

The lifetime of strong pointers is entirely dependent on all strong pointers pointing to the same instance of a type being discarded (or reset to point to nothing). Only when the final strong pointer is discarded or reset will a shared instance of a type be released.

print final : ()(...) = {
    // ...
}

assert final : ()(check : Boolean) = {
    // ...
}

MyType :: type {
    myValue1 : Integer
    myValue2 : String
}

printIfValidPointer final : ()(pointerToValue : MyType *) = {
    if pointerToValue
        print("true")
    else
        print("false")
}

func final : (
    // `stayingAlive` is a `strong` pointer defaulted to point to nothing
    stayingAlive : MyType * strong
)() = {

    scope {
        // the @ operator allocates `value1` dynamically with the context's
        // allocator and a `strong` pointer is maintained
        value1 : MyType * strong @

        // the @ operator allocates `value2` dynamically with the context's
        // allocator and a `strong` pointer is maintained to another `MyType`
        // instance
        value2 : MyType * strong @

        // `stayingAlive` will point to `value2` thus they will both point to
        // the same `MyType` instance
        stayingAlive = value2

        printIfValidPointer(value1)         // will print "true"
        printIfValidPointer(value2)         // will print "true"
        printIfValidPointer(stayingAlive)   // will print "true"

        // `value1` and `value2` point to different `MyType` instances
        assert(value1 != value2)

        // `value2` and `stayingAlive` point to the same `MyType` instance
        assert(stayingAlive == value2)

        // `value2`'s value is replaced, thus `MyType` instance being `own`
        // in `value2` but the original type's instance in `value2` is not
        // destructed or deallocated as `stayingAlive` will keep the
        // type originally allocated in `value2` alive.
        value2 = value1

        printIfValidPointer(value1)         // will print "true"
        printIfValidPointer(value2)         // will print "true"
        printIfValidPointer(stayingAlive)   // will print "true"

        // `value1` and `value2` point to the same `MyType` instances
        assert(value1 == value2)

        // `stayingAlive` and `value2` point to different `MyType` instances
        assert(stayingAlive != value2)

        // `value2` and `value1` pointers both fall out of scope but only
        // the single `MyType` instance originally allocated in `value1` is
        // destructed and deallocated
    }

    // `stayingAlive` is returned to the caller and thus will not fall out
    // fall out of scope and the type originally allocated in `value2` is
    // kept alive beyond the lifetime of this function call
    return
}

func2 final : ()() = {
    // the `strong` pointer returned by `func` is now being kept alive by
    // `liveAndLetDie`
    liveAndLetDie := func()

    printIfValidPointer(liveAndLetDie)  // will print "true"

    // reset `liveAndLetDie` which causes the lifetime originally allocated in
    // `value2` of `func` function to be destructed and deallocated
    liveAndLetDie = #:

    printIfValidPointer(liveAndLetDie)  // will print "false"
}

Breaking circular strong pointer lifetime

Care must be used when dealing with strong pointer lifetimes to ensure a circular dependency is not created where a circular chain is never broken. If a strong pointer points to another strong pointer that directly or indirectly points back to the original type’s instance then the lifetime of the strong pointers in the chain will never become destructed nor deallocated.

The example below creates a circular strong pointer chain:

print final : ()(...) = {
    // ...
}

assert final : ()(check : Boolean) = {
    // ...
}

MyType :: type {
    head : MyType * strong
    next : MyType * strong

    myValue1 : Integer
    myValue2 : String

    +++ final : ()() = {
        print("This will be displayed three times in this example.")
    }

    --- final : ()() = {
        print("BAD NEWS! This will never be displayed in this example!!!")
    }
}

printIfValidPointer final : ()(pointerToValue : MyType *) = {
    if pointerToValue
        print("true")
    else
        print("false")
}

func final : (result : String)() = {

    scope {
        // the @ operator allocates `value1` dynamically with the context's
        // allocator and a `strong` pointer is maintained
        value1 : MyType * strong @

        // the @ operator allocates `value2` dynamically with the context's
        // allocator and a `strong` pointer is maintained to another `MyType` 
        // instance
        value2 : MyType * strong @

        // the @ operator allocates `value3` dynamically with the
        // context's allocator and a `strong` pointer is maintained to
        // another `MyType` instance
        value3 : MyType * strong @

        // setup pointers to the head of the linked list
        value1.head = value1
        value2.head = value1
        value3.head = value1

        // setup points to the linked list chain
        value1.next = value2
        value2.next = value3

        printIfValidPointer(value1) // will print "true"
        printIfValidPointer(value2) // will print "true"
        printIfValidPointer(value3) // will print "true"

        // reset all three `strong` pointers to point to nothing
        value1 = #:
        value2 = #:
        value3 = #:

        // validate the pointers are indeed pointing to nothing
        assert(!value1)
        assert(!value2)
        assert(!value3)

        // despite `value1`, `value2`, and `value3` being reset and falling
        // out of scope, the original constructed `value1`, `value2`, and
        // `value3` type instances will never be destroyed because they are all
        // keeping their lifetimes alive by pointing to each other in a
        // circular pointer loop

        // `value1` points to itself and `value2`' instance
        // `value2` points to `value1`'s instance and `value3`'s instance
        // `value3` points to `value1`'s instance

        // all of the following circular `strong` pointer prevent
        // `MyType` destruction/deallocation
        // 1 points to 1 (itself)
        // 1 points to 2 points to 1 again
        // 1 points to 2 points to 3 points to 1 again
        // 2 points to 3 which points to 1 which points to 2 again
        // 3 points to 1 which points to 2 which points to 3 again
    }

    return "I'll be back."
}

The example below creates a circular strong pointer chain and manually fixes the issue:

print final : ()(...) = {
    // ...
}

assert final : ()(check : Boolean) = {
    // ...
}

MyType :: type {
    head : MyType * strong
    next : MyType * strong

    myValue1 : Integer
    myValue2 : String

    +++ final : ()() = {
        print("This will be displayed three times in this example.")
    }

    --- final : ()() = {
        print("GOOD NEWS! This will be called three times in this example.")
    }
}

printIfValidPointer final : ()(pointerToValue : MyType *) = {
    if pointerToValue
        print("true")
    else
        print("false")
}

func final : (result : String)() = {

    scope {
        // the @ operator allocates `value1` dynamically with the context's
        // allocator and a `strong` pointer is maintained
        value1 : MyType * strong @

        // the @ operator allocates `value2` dynamically with the context's
        // allocator and a `strong` pointer is maintained to another `MyType`
        // instance
        value2 : MyType * strong @

        // the @ operator allocates `value3` dynamically with the context's
        // allocator and a `strong` pointer is maintained to another `MyType`
        // instance
        value3 : MyType * strong @

        // setup pointers to the head of the linked list
        value1.head = value1
        value2.head = value1
        value3.head = value1

        // setup points to the linked list chain
        value1.next = value2
        value2.next = value3

        printIfValidPointer(value1) // will print "true"
        printIfValidPointer(value2) // will print "true"
        printIfValidPointer(value3) // will print "true"

        // break apart any way the pointer relationships could be circular
        value1.head = #
        value2.head = #
        value3.head = #

        // reset all three `strong` pointers to point to nothing
        value1 = #  // `value1`'s instance will be destructed/deallocated (which
                    // removes the strong pointer to `value2`)
        value2 = #  // `value2`'s instance will be destructed/deallocated (which
                    // removes the strong pointer to `value3`)
        value3 = #  // `value3`'s instance will be destructed/deallocated

        // validate the pointers are indeed pointing to nothing
        assert(!value1)
        assert(!value2)
        assert(!value3)
    }

    return "I'll be back."
}

Using weak pointers to break a chain

Pointers qualified as weak will only contain a valid pointer to a type’s instance so long as the original allocated type is not destructed/deallocated. In other words, weak points will not extend the lifetime of a strong pointer beyond the last strong pointer keeping a type’s instance alive.

print final : ()(...) = {
    // ...
}

assert final : ()(check : Boolean) = {
    // ...
}

MyType :: type {
    head : MyType * weak    // head will not be the lifetime of whatever it
                            // points to alive
    next : MyType * strong  // next will keep whatever it points to alive

    myValue1 : Integer
    myValue2 : String

    +++ final : ()() = {
        print("This will be displayed three times in this example.")
    }

    --- final : ()() = {
        print("GOOD NEWS! This will be called three times in this example.")
    }
}

printIfValidPointer final : ()(pointerToValue : MyType *) = {
    if pointerToValue
        print("true")
    else
        print("false")
}

func final : (result : String)() = {

    scope {
        // the @ operator allocates `value1` dynamically with the context's
        // allocator and a `strong` pointer is maintained
        value1 : MyType * strong @

        // the @ operator allocates `value2` dynamically with the context's
        // allocator and a `strong` pointer is maintained to another `MyType`
        // instance
        value2 : MyType * strong @

        // the @ operator allocates `value3` dynamically with the context's
        // allocator and a `strong` pointer is maintained to another `MyType`
        // instance
        value3 : MyType * strong @

        // setup pointers to the head of the linked list
        value1.head = value1
        value2.head = value1
        value3.head = value1

        // setup points to the linked list chain
        value1.next = value2
        value2.next = value3

        printIfValidPointer(value1) // will print "true"
        printIfValidPointer(value2) // will print "true"
        printIfValidPointer(value3) // will print "true"

        // obtain a `strong` pointer to the head type
        headPointer := value3.head as strong

        printIfValidPointer(headPointer) // will print "true"

        // reset all three `strong` pointers to point to nothing
        value1 = #:
        value2 = #:
        value3 = #:

        // validate the pointers are indeed pointing to nothing
        assert(!value1)
        assert(!value2)
        assert(!value3)

        // validate the original three instances are still alive
        printIfValidPointer(headPointer)            // will print "true"
        printIfValidPointer(headPointer.next)       // will print "true"
        printIfValidPointer(headPointer.next.next)  // will print "true"

        // take the `head` pointer of the `next` pointed type
        copyOfWeakHead : MyType * weak = headPointer.next.head

        // the `MyType` instance pointed to by the `weak` pointer is still
        // alive thus converting the `weak` point to a `strong` pointer will
        // obtain a `strong` pointer reference to the original type's instance
        printIfValidPointer(copyOfWeakHead as strong)   // will print "true"

        // the `strong` pointer both point to the same head type's instance
        assert((copyOfWeakHead as strong) == headPointer)

        // reset the head pointer to point to nothing (which is the only
        // pointer keeping all three `MyType` instances alive thus all three
        // `MyType` instances are destructed/deallocated)
        headPointer = #:

        // the `MyType` instance originally pointed to by the copy of the
        // `weak` pointer to the head type's instance is now
        // destructed/deallocated thus a `strong` pointer obtained from the
        // `weak` pointer now points to nothing
        printIfValidPointer(copyOfWeakHead as strong)   // will print "false"

        // all instance of `MyTpe` are already destructed thus nothing further
        // occurs when all the values fall out of scope
    }

    return "I'll be back."
}

Transferring own pointers to strong pointers

Pointers qualified as own can be transferred to pointers qualified as strong. Once a transfer is completed, the original own pointer will point to nothing as the strong pointer will track the lifetime of the instance. Likewise, pointers qualified as strong can be transferred to pointers qualified as own on the condition that no other pointers qualified as strong points to the same type’s instance otherwise the resulting own pointer will point to nothing.

Caution: care must be taken when transferring an allocated own pointer to a strong pointer. Pointers qualified as own are allocated using the standard allocator which is typically set to the sequential allocator by default. If an own pointer gets transferred later into a strong pointer, replacing the standard allocator with the parallel allocator may be desired.

print final : ()(...) = {
    // ...
}

assert final : ()(check : Boolean) = {
    // ...
}

MyType :: type {
    myValue1 : Integer
    myValue2 : String
}

printIfValidPointer final : ()(pointerToValue : MyType *) = {
    if pointerToValue
        print("true")
    else
        print("false")
}

func final : (result : String)() = {

    scope {
        // the @ operator allocates `value1` dynamically with the context's
        // allocator and a `strong` pointer is maintained
        value1 : MyType * own @

        // `value2` is is a `strong` pointer but the value is initialized to
        // point to nothing
        value2 : MyType * strong

        // `value1` used to `own` the instance but the instance ownership is
        // transferred from a `value1` to `value2` which now keeps the
        // `MyType` instance alive
        value2 = value1

        printIfValidPointer(value1) // will print "false"
        printIfValidPointer(value2) // will print "true"

        assert(value1 != value2)

        // transferring a `strong` pointer to an `own` pointer can be
        // done so long as no other `strong` pointers exist to the same
        // instance (otherwise `value1` would point to nothing)
        value1 = value2

        // `value2`'s `strong` pointer was automatically reset when ownership
        // was exclusively taken over by `value1`
        assert(!value2)

        printIfValidPointer(value1) // will print "true"
        printIfValidPointer(value2) // will print "false"

        assert(value1 != value2)


        // `value3` is is a `strong` pointer but the value is initialized to
        // point to nothing
        value3 : MyType * strong

        // once again transfer ownership from `value1` to `value2`
        value2 = value1

        printIfValidPointer(value1) // will print "false"
        printIfValidPointer(value2) // will print "true"
        printIfValidPointer(value3) // will print "false"

        // `value3` is a `strong` point but ownership in `value1` was already
        // lost thus `value3` will point to nothing
        value3 = value1

        printIfValidPointer(value1) // will print "false"
        printIfValidPointer(value2) // will print "true"
        printIfValidPointer(value3) // will print "false"

        // `value2` and `value3` now point to the same allocated instance
        // originally allocated in `value1`
        value3 = value2

        printIfValidPointer(value1) // will print "false"
        printIfValidPointer(value2) // will print "true"
        printIfValidPointer(value3) // will print "true"

        assert(value1 != value2)
        assert(value2 == value3)

        // `value1` cannot retake ownership from `value2` as another pointer
        // to the same instance of `value2` exists thus `value1` cannot take
        // exclusive ownership and `value1` will point to nothing
        value1 = value2

        printIfValidPointer(value1) // will print "false"
        printIfValidPointer(value2) // will print "true"
        printIfValidPointer(value3) // will print "true"
    }

    return "I'll be back."
}

Transferring strong pointers to handle pointers

Pointers qualified as strong cannot be directly transferred to a pointer qualified as handle. The only method by which this transfer can happen is if a strong pointer is first transferred to an own pointer and then transferred to a handle pointer. The vice versa limitation is also true. Pointers qualified as handle cannot be directly transferred to a pointer qualified as strong. The only method by which this transfer can happen is if a handle pointer is first transferred to an own pointer and then transferred to a strong pointer.

Transferring to an own pointer have limitations. Only if a pointer qualified as strong or handle is the exclusive reference to the instance of a type can the transfer to an own pointer occur.

Casting contained variables into strong pointers

Converting from a container strong pointer to a contained strong pointer

The lifetime of operators can be used to cast a raw pointer to a variable which has a lifetime tied to an original strong or handle pointer.

A strong pointer to a type’s instance may contain other types within the instance that share a common lifetime. While the lifetime of these contained types are the same as the container type, only a strong pointer to the container type may exist (despite both types being considered as a single instance). The lifetime of operator is especially useful to create a strong pointer of a contained type from a strong pointer to a container’s type.

Example as follows:

MyType :: type {
    value1 : Integer
    value2 : String
}

myType : MyType * strong @

// create a pointer to `value` and link the lifetime of `myType` to the pointer
value : Integer * strong = myType.value1 lifetime of myType

// resetting the `myType`'s `strong` pointer will not impact the real lifetime
// of the instance connected to `myType` (as the `value` `strong` pointer will
// keep it's container instance alive)
myType = #

// set the `MyType::value1` to `5` (which is still valid as the
// lifetime of of the original `strong` pointer is kept alive)
value. = 5

Converting from a container strong pointer to a contained strong pointer

While any pointer to any type can be linked to a strong or handle pointer using an unsafe lifetime of operator, a lifetime of operator can link a pointer to a contained type back to the original strong or handle pointer safely by verifying a pointer refers to a memory addresses within the bounds of the allocated strong or handle pointer.

An example of a runtime lifetime of being applied onto a strong pointer:

A :: type managed {
    foo : Integer
}

B :: type {
    bar : Integer
    a : A
}

C :: type {
    weight : Double
}

doSomething final : ()(a : A * strong) = {
    // probe `a` to see if it is indeed within a `B` type and if so then
    // return a pointer to a B type and create a `strong` pointer from `a`
    b := (a outer of B *) lifetime of a
}

function final : ()() = {
    value : B * strong @
    value.a.foo = 1
    value.bar = 2

    // link a `strong` pointer to `a` from `value`
    a : A * strong = value.a lifetime of value

    doSomething(a)
}

lifetime of versus unsafe lifetime of

The exclusive difference between these operators is safety. An unsafe lifetime of operator will force a conversion of any raw pointer to link to handle or strong pointer even for unrelated pointers. A lifetime of operator will validate a raw pointer actually points inside the address boundaries of the handle or strong pointer. If a pointer to memory is not located in the correct allocation boundary then lifetime of will return a pointer to nothing. Thus a programmer can choose their tradeoff between safety and speed.

Converting using unsafe lifetime of

Any pointer to any type can be linked to a handle or strong pointer using an unsafe lifetime of operator. The memory is not checked to see if the memory address being casted resides within the memory space of the original handle or strong pointer. The programmer must be careful to use this feature carefully since this function will forcefully adopt the lifetime of a handle or strong pointer.

An example of a runtime unsafe lifetime of being applied onto a handle pointer:

A :: type managed {
    foo : Integer
}

B :: type {
    bar : Integer
    a : A
}

C :: type {
    weight : Double
}

doSomething final : (result : Unknown * strong)(a : A * strong, unknown : Unknown *) = {
    // forcefully convert from the lifetime of one pointer to another type
    // without conducting any safety checks to ensure the casted pointer lives
    // within the memory range of the original allocation
    b := unknown unsafe lifetime of a
    assert(b)

    // ...
    return b
}

function final : ()() = {
    value : B * handle @
    value.a.foo = 1
    value.bar = 2

    // link a `strong` pointer to `a` from `value`
    a : A * strong = value.a lifetime of value

    doSomething(a, value)
}

strong and weak overhead and control blocks

A strong and weak pointer contains a pointer to an instance of a type and a pointer to the control block. When a type is allocated for storage in a strong pointer, a control block is typically reserved with the allocation of a type’s instance.

Since weak pointers also need control blocks, weak pointers can keep a memory control block alive and possibly a type’s reserved memory too if a control block and type are allocated within the same memory block. To clean up allocated memory, reset any weak pointers to point to nothing after all strong pointers are gone.

An example strong / weak pointer content and control block:

/*
StrongPointerControlBlock$(Type) :: type {
    strongCount : Atomic$(Integer)
    weakCount : Atomic$(Integer)
    destructor : ()() *
    deallocateType : Unknown *
    deallocateControl : Unknown *
    type : :: union {
        type : $Type
    }
}

StrongPointerContent$(Type) :: type {
    instance : $Type *
    control : StrongPointerControlBlock$($Type) *
}
*/