Pointer types

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-pointerPin-pointer@composition
&weakBy assignmentWith ? or : operator (or && or ||)Through lock ptr
with ? or : and then @copy operator
PinWith &operatorBy assignmentWith @copy operator
@compositionWith &operatorAutomatically 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
classDiagram x_var *-- MyClass
x.w := &x;    // @composition_ptr -> &weak_ptr conversion
classDiagram class MyClass{ weak(MyClass) w } x_var *-- MyClass MyClass <-- MyClass
a = x;        // @composition_ptr -> lock_ptr auto-conversion by accessing a @value
classDiagram class MyClass{ weak(MyClass) w } x_var *-- MyClass MyClass <-- MyClass a_temp_var o-- MyClass
y = @x;  // @composition_ptr -> lock_ptr auto-conversion by accessing a @value
         // followed by lock_ptr -> @composition_ptr conversion with @copy operator
classDiagram class MyClass{ weak(MyClass) w } class MyClass_copy{ weak(MyClass) w } x_var *-- MyClass MyClass <-- MyClass a_temp_var o-- MyClass y_var *-- MyClass_copy MyClass_copy <-- MyClass_copy
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
classDiagram class MyClass{ weak(MyClass) w } class MyClass_copy2{ weak(MyClass) w } class MyClass_copy{ weak(MyClass) w } x_var *-- MyClass_copy2 MyClass_copy2 <-- MyClass_copy2 a_temp_var o-- MyClass MyClass<--MyClass y_var *-- MyClass_copy MyClass_copy <-- MyClass_copy
w = x.w;      // &weak -> &weak conversion by assignment
classDiagram class MyClass{ weak(MyClass) w } class MyClass_copy2{ weak(MyClass) w } class MyClass_copy{ weak(MyClass) w } x_var *-- MyClass_copy2 MyClass_copy2 <-- MyClass_copy2 a_temp_var o-- MyClass MyClass<--MyClass y_var *-- MyClass_copy MyClass_copy <-- MyClass_copy w_var --> MyClass_copy2
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
};
classDiagram class MyClass{ weak(MyClass) w } class MyClass_instance1{ weak(MyClass) w } class MyClass_copy{ weak(MyClass) w } x_var *-- MyClass_instance1 MyClass_copy<-- MyClass_instance1 w_var --> nothing a_temp_var o-- MyClass MyClass<--MyClass y_var *-- MyClass_copy MyClass_copy <-- MyClass_copy

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.

3 Comments

  1. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *