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) *
}
*/