In contrast with C++ or Rust Argentum pointers always point to class instance or interface instance. They cannot point to local variables or object fields. This is much like references in Java/C#.
In contrast with Java and other GC-based languages Argentum provides multiple different pointer types. These pointer types have three purposes:
- They let document your intensions on each particular parameter/field/result improving readability.
- They let compiler automate multiple operations on objects.
- They allow to check at compile time for the program actions that may produce wrong data structures.
Owning (composition) @pointer
Composition pointers are unique owners of mutable objects. They are so valuable, that they have a dedicated post: here.
They have syntax "@ClassName"
They are produced by two types of expressions:
- new object instance creation:
ClassName
- copy of the existing object with unary prefix operator "@".
a = Array(Object); // Create a new instance of Array, so `a` has type @Array(Object).
a.append(MyClass) // Create a new instance of MyClass and pass it to the `append` method.
x = MyClass; // Create a new instance of MyClass.
a.append(@x) // Append to `a` a copy of `x`.
In line 4 we cannot pass just "x
" to the append
method, because "append
" places its parameter in the array and if it would be allowed, the array and the x variable would be owners of the same object. This would break the composition rules of single ownership and make all sorts of problems immanent to languages that don't support composition (like Java). For example, when user alters one element of the array, this modification would be suddenly reflected in another expected-to-be-separate object. That's why this append
parameter should be @pointer i.e. totally separate instance of "MyClass" not owned elsewhere.
Composition @pointers are backbone of all data structures in computer world. They define trees of ownership in HTML/XML/JSON DOMs, they define scene graphs in UI and game engines, they define AST in compilers and structures of office documents. It's the main pointer type in argentum object fields:
class MyClass {
items = Array(Item); // @Array type
style = Style; // @Style type
rawBytes = Blob; // @Blob type
}
fn toBytes(i int) @Blob { // Function returning a newly created Blob
Blob resize(8).putInt(0, i) // Creates and initializes a Blob instance
}
When composition pointer is located in a function result, it has one meaning - it points to a newly created (or copied) object that can be directly assigned to a @field (or processed and returned further by the call stack).
BTW despite example in the above section it is not allowed to pass @pointers as parameters. The reasoning behind this will be discussed in a separate post. The legit alternative is to pass lambdas returning @pointers. An this is how theappend
method is declared, it accepts a lambda that returns @pointers. And Argentum automatically generates lambda out of our expressions.
Example:
class MyClass {
name = String;
setData1(a @Blob) { // Error, @pointers cannot be parameters
name := a;
}
setData2(a()@Blob) { // Ok, lambda returning @Blob is legit
rawBytes := a(); // Now call this lambda and store its @Blob result
}
}
Bottom line: the @poiter
syntax is a direct sign of a factory function or setter method. Something that deals with object creation.
Non-owning (associative) &pointer
In Argentum, most of the pointers guarantee that the pointed-to object remains in existence as long as the pointer points to it.
There is one exception: the weak-pointer
. It is declared as &ClassName
.
fn myFunction(wp &MyObject) ....
Also the expression &ClassName creates a disconnected weak pointer to the given class/interface. It is useful to declare fields:
class MyClass {
peer = &MyClass; // weak pointer to MyClass, initially points nowhere
}
There is also a unary prefix operator "&
" that creates these pointers from other pointer types:
x = MyClass; // x is an @owning pointer, that holds a newly created instance of MyClass.
w = &x; // w is a &weak pointer, that references the instance upheld by `x`.
w1 = &MyClass; // w1 is a &weak pointer to MyClass not bound to any object.
w1 := w; // Now both w and w1 point to the same instance.
x := MyClass; // Now x points to a new instance of MyClass,
// the previous MyClass gets deleted,
// w and w1 became not bound to objects.
w1 := &x; // Make w1 point to a new object stored in x
Weak pointer fields are useful when it's needed to store a cross-relations between objects from different parts of object hierarchy without assumption of ownership. They can:
- connect nodes in a graph
- store relations between tables in database
- connect a page element to its style in a web page or office document
- make the smart connector of a diagram know the objects that define the coordinates of its endpoints
- make formulas of spreadsheets reference other cells (in internal representation)
- store references from the variable usage to variable definition in compiler AST
- and may other applications.
Every time we are adding attributes like "id" or "idref" when serialize our data into JSON or XML, every time some arbitrary cross references involved, every time a UML diagram contains an arrow without diamonds, it is the sign of "plain association", and Argentum &pointers
are the "association" pointers.
These association &links exist as long as two objects are alive - the one that points and the one that is pointed-to.
Application can easily check if a &weak pointer is alive and take some temporary lock pointer out of it:
w1 ? _.someMethod(); // check if w1 is bound, and if it is, call its method
When used in a function parameter or return value the &pointer
tells that the function is ok with loosing this object, is willing to check it on each usage or is going to assign it to some &field
.
This is the only one inherently nullable type of pointers, because it can drop connection by itself on target destruction.
Temporary pin pointer
Temporary pin pointers exist only in parameters/locals/temporary values/return values i. e. in stacks.
They cannot reside in object fields. They are used in almost all cases when we pass data between functions and operators.
They are created:
- When a weak &pointer is checked/locked with "?" operator
- When we read value of any owning @composition object field or array item
It's like temporary @composition pointer: as long as this pointer exists, the object it points-to is alive.
It's syntax is - just a name of a class or interface "MyClass
".
In the above example "w1 ? _.someMethod()
" the "_" variable is a temporary lock pointer, that protects object from being deleted as long as someMethod
is working.
Another example:
class MyObject {
rawData = Blob;
}
fn getBytes(o MyObject) Blob { // o is a pin-pointer
o.rawData // function returns a temp lock pointer to a Blob field
}
a = MyObject; // a is an owning pointer
n = getName(a); // When passing a to a getName function, a lock pointer is created
// Now n is a lock pointer to Blob
a := MyObject; // Previous MyObject from variable a was destroyed,
// but its rawData field is still alive because it is retained by variable n
Pin-pointers is a work force of the Argentum language. They are used every time we pass object to or from functions, every time we store object in a local variable or when we need a temporary value.
Internally pin-pointers are implemented as automated ref counters. But compiler uses some minor optimizations that reduces the amount of retain/release operations to the bare minimum. And also, these counters will never use atomic operations, even in multithreaded environments (It will be discussed in a separate post). In short: mutable objects always belong to one thread, they get passed across threads through queues that perform the cross-thread transfer only when object is not locked by temp pointers.
Conversion between pointers to mutable objects
From\To | &weak-pointer | Pin-pointer | @composition |
&weak | By assignment | With ? or : operator (or && or || ) | Through lock ptr with ? or : and then @copy operator |
Pin | With &operator | By assignment | With @copy operator |
@composition | With &operator | Automatically by reading value of @field or @variable | With @copy operator or by returning @local from function or {} block |
Examples:
class MyClass {
w = &MyClass; // w is an empty &weak pointer to MyClass
}
x = MyClass; // x is a composition ptr @MyClass
x.w := &x; // @composition -> &weak conversion
a = x; // @composition -> lock-pointer auto-conversion by accessing @value
y = @x; // @composition -> @composition conversion with @copy operator
x.w ? x:= @_; // if x.w is not empty, make a copy of it and assign to x
// &weak(x.w) -> lock_ptr(_) conversion with ?operator
// lock_ptr(_) -> @composition conversion with @copy operator
w = x.w; // &weak -> &weak conversion by assignment
x := {
temp = MyClass; // temp of type @MyClass
temp.w := &y; // When accessing `temp` it reads as lock_ptr to MyClass
temp // When returned from {block} temp gives it the type of @MyClass
};
Examples with pictures
class MyClass {
w = &MyClass; // w is an empty &weak pointer to MyClass
}
x = MyClass; // x is a composition ptr @MyClass
x.w := &x; // @composition_ptr -> &weak_ptr conversion
a = x; // @composition_ptr -> lock_ptr auto-conversion by accessing a @value
y = @x; // @composition_ptr -> lock_ptr auto-conversion by accessing a @value
// followed by lock_ptr -> @composition_ptr conversion with @copy operator
x.w ? x:= @_; // if x.w is not empty, make a copy of it and assign to x
// &weak(x.w) -> lock_ptr(_) conversion with ?operator
// lock_ptr(_) -> @composition conversion with @copy operator
w = x.w; // &weak -> &weak conversion by assignment
x := {
temp = MyClass; // temp of type @MyClass
temp.w := &y; // When accessing `temp` it reads as lock_ptr to MyClass
temp // When returned from {block} temp gives it the type of @MyClass
};
Conclusion
- Argentum has two pointer types allowed in object fields: @composition pointer and &weak pointer. They are designed to hold mutable data structures of arbitrary complexity.
- Pin-pointers allow safe access and easy processing of these data structures.
- Argentum provides multiple built-in operations: copy, lock, release, pass to another thread. These operations are implemented automatically. They performed on object hierarchies in safe manner, preserving composition and association invariants and ensuring memory safety.
- These operations work with no time-space overheads.
- Argentum pointer type declarations are easy to remember: they match operations used to produce these pointers: & - create weak, @ - create unique instance by copy.
Tutorial 2: Loop in Graph provides a practical example on how these pointers types work together.
Probably I don’t understand clearly what the weak pointers are. I’ve played a bit with your example:
using sys { log }
class MyClass{
print() { log(“test”); }
}
x = MyClass;
w = &x;
w1 = &MyClass;
w1 := w;
x := MyClass;
w1 := &x;
//x.print(); // OK
w1 ? _.print(); // OK
//w1.print(); // error
//w1.&print(); // crash
Why does “w1 ? _.print(); // OK” works, while “w1.print();” does not? As it looks to me, the only difference if existence check.
> w1.&print(); // crash
Thanks for finding a bug in compiler, I’ll submit it to the GitHub tracker on your behalf. Compiler must report a syntax error here, not be crashing 😁👍
Why w1.print() doesn’t work.
w1 is a weak pointer, it can be not pointing anywhere from the beginning, its target can be deleted as a result of disposal of all object hierarchy it belongs, or its parent object stops referencing it with its owning pointer field, or it’s removed from the collection that owned it. In general if you hold a weak pointer to anything, you must be ready that this pointer can become unbound (null) any second.
If w1 is null, what the construction w1.print() should do? Crashing as in Java or UB as in C++ are not good for safety and resilience, that’s why Argentum mandates checking.