Memory


RAM is divided into memory units we call Byte which equal 8 bits, this is the smallest unit that could be allocated to store variables. Every byte in memory is numbered with a serial number starting from 0, we call this number the memory address.
So if the memory capacity is 1 KB (in reality it is much bigger, like 4 GB, 8 GB, or even 16 GB) which means the memory consists of 1024 byte because 1KB equal 1024 byte, hence we will have 1024 memory addresses available starting from 0 (the first memory unit), and ending with 1023 (the last byte).
It should be noted that computer architecture is what determine the number of memory addresses that we can reach, you must heard "This processor or that computer is 32bit or 64bit", this number is related to the processor architecture. 32bit architectures could address 2 to the power of 32, which is 4294967296 addresses and that equal to 4 GB memory. And these 4294967296 addresses could be stored in 32bit (4 byte) which is the size of a variable of type pointer in this architecture (32 bit).

Pointers


Alusus allows dealing with memory directly using pointers, which in turn allows for creating programs with high performance and low memory usage. Pointers can be used to manipulate memory, and can also be used to avoid copying memory blocks when calling functions, by passing pointers to those blocks instead of passing copies of those blocks. Passing pointers to functions is known as "pass by reference". Pointers are considered a vital subject when talking about data structures because we use them to link data units (or what is known as "Nodes"). We also use pointers to create dynamic variables, which allows the programmer to create variables at run time by allocating their memory dynamically, or create arrays with dynamic sizes.

What is a pointer?

A pointer is a variable used to point to a location in memory and using it we could read and manipulate the contents of that memory location. In other words, it is a variable with value equal to the location of another variable in memory.

Let's first agree that every variable created in the program has a location in memory and hence a memory address. We can know that location by writing the variable name followed by `~ptr`, as shown in the next example:

import "Srl/Console.alusus";
use Srl.Console;
def x: int;
x = 51;
print("value of x: %d\n", x); // value of x: 51
print("address of x: %p\n", x~ptr); // address of x: 0x55a3b28493f0

As you should know, the variable address is changed every time we run the program. Each time a new address is reserved to variables in the code.

To define a variable of type `ptr` in Alusus, we use the keyword `def` followed by the name of the variable then `:` followed by the type name (here `ptr`) and finally the data type enclosed in square brackets.

def pointer_name: ptr[Data_type];

The next example shows us how to create a variable of type `ptr`:

import "Srl/Console.alusus";
use Srl.Console;
def p: ptr[Int];
print("Pointer Address is : %p\n", p); // Pointer Address is : (nil)
// note that pointer value is nil which means nothing, so you defined a variable
// with type `ptr` but it does not point to any other variable yet.
def x: Int;
x = 5;
p = x~ptr; // now `p` is pointing to `x` which means its value equal to the memory address of `x`.
print("x Address is : %p\n", x~ptr); // x Address is : 0x55cdf422e950
print("Pointer Address is : %p\n", p); // Pointer Address is : 0x55cdf422e950
// to get the content that the pointer points to we use `~cnt` operator.
print("Pointer Content is : %d\n", p~cnt); // Pointer Content is: 5
// this is what we called "dereferencing", which means getting the value of the variable the pointer points to
// and to change this value we could do the following:
p~cnt = 9; // change the value using the pointer.
print("x Value is : %d\n", x); // x Value is : 9

Addition and subtraction operations can be applied to pointers, and in that case the increment size will be a multiple of the size of the type the pointer points to. For example, if we add 1 to a pointer to integer then the increment will be the size of `Int` which is 4 bytes.

References


References are considered a simpler way to deal with pointers. The user needs to define the variable as a reference and set its pointer to point to another variable, and then it can be used the same way regular variables are used, which means we can access its content without `~cnt` operator. We can define a reference as follows:

def ref_name: ref[Data_Type]

Where:
ref_name: is variable's name
Data_Type: the data type of the other variable it will be a reference to.

Example:

import "Srl/Console.alusus";
use Srl.Console;
def x: int = 9;
def re: ref[int]; // define a reference
print("value of x: %d\n", x);
print("x Address is : %p\n", x~ptr);
re~ptr = x~ptr; // now `re` is a reference to `x`
x = 51;
print("value of re: %d\n", re); // print reference value
print("re Address is : %p\n", re~ptr); // print its address
/*
value of x: 9
x Address is : 0x563b63224bf0
value of re: 51
re Address is : 0x563b63224bf0
*/

Note that any modification on the variable or the reference will change the other too since they have the same memory address.

You can also use `~no_deref` operator as a second method to change the reference address, which means instead of writing the following in the previous example:

re~ptr = x~ptr;

we can write:

re~no_deref = x;

The `~no_deref` operator tells the compiler that you want to change the value of the reference not the value of the object it points to. This operator is useful in some cases like building templates we want to work as expected in case of references (for example, `Array` class needs this operator to allow the array to assign the references values instead of objects values).
The benefit of this operator is that it does not give an error if you use it on a variable that is not a reference, which makes it useful in templates (template will work as expected whether the specified type is a reference or not).

You can also define a reference to a reference, as a modification to the previous example we will add another reference to the reference `re` as follows:

import "Srl/Console.alusus";
use Srl.Console;
def x: int = 9;
def re: ref[int];
def rere: ref[ref[int]]; // define a reference to a reference to an int.
re~ptr = x~ptr; // now `re` is a reference to `x`.
rere~ptr~ptr = re~ptr~ptr; // in the same way we let `rere` be a reference to `re`.
// if we print the values of all variables we will notice they all have the same value
print("%d\n",x) // 9
print("%d\n",re) // 9
print("%d\n",rere) // 9
// if we change the value of the new reference all other values will change
rere=100
print("%d\n",x) // 100
print("%d\n",re) // 100
print("%d\n",rere) // 100

We notice the following in the previous example:

  1. Operations applied on the reference are always applied on the content regardless of reference depth (we can notice that when we assign value 100 to `rere`).
  2. In reference to a reference case (`rere` variable), using `~ptr` operator will give you a pointer to the object not to the reference, whereas `~ptr~ptr` will give a pointer to the inner reference.

It is also possible to use `~ptr` operator to make the reference points to a dynamic allocation of the memory (we will see that at the end of this lesson).

Using references will be very useful in some cases as we will see later in functions and classes lessons. Regarding functions for example, when we want to define a function to do some procedural operations on some data (we will talk about functions in later lesson) and we want to apply this changes on the original data (not on a copy). See the next example:

import "Srl/Console.alusus"
use Srl.Console;
def x: int = 3;
// now we will define a function the increase the variable value by 1
function inc (a: ref[int]) {
  a += 1;
}
inc(x) // now we call the function to increase x by 1
print("%d", x); // 4

Note the followings:

  1. When the reference is an argument to the function (as the above example) then all you need to do is pass to the function a variable with the same type as the reference data type.
  2. If we don't use the reference as an argument to a function as in the previous example, then the changes will be applied on a copy from the variable (the value will change in the function scope only). Note in the following example how the changes can be seen only the function scope, whereas the variable outside the function is not affected.
import "Srl/Console.alusus"
use Srl.Console;
def x: int = 3;
function inc (a: int) {
  a += 1;
  print("%d\n", a); // 4
}
inc(x);
print("%d", x); // 3

When should we use pointers and when should we use references?

We recommend that you use references unless you have a reason to use pointers. If you don't know that you need to use a pointer then this might be a sign that you don't need it and you can use a reference instead.
For more information about references and pointers you can visit the Language Reference

Smart References


It is recommended to use smart references instead of plain references to avoid problems related to memory leaks. Smart references automatically manages the life cycle of the allocated object. It's recommended to limit the use of plain references to simple cases like passing argument by reference to functions that do not hold copies of the passed referneces (as is the case with using references to allow a function to return additional values to the caller).

Smart references are povided by the Standard Runtime Library, and can be imported like this:

import "Srl/refs.alusus";

SrdRef

The shared reference class allows multiple references to share the same object and it handles releasing the object when it is no longer needed. It keeps a counter of the number of shared references that points to the same object. Each time a reference is terminated the counter decreases by 1 and when it reaches zero the object is terminated and its memory is released automatically.

To define a shared reference that points to an object of a specified type:

def ref_name: Srl.SrdRef[Data_Type];

Where:
ref_name: the name of the shared reference.
Data_Type: the name of data's type we want to point to by the reference.

To allocate memory for the object that the reference will point to and initialize it (construct it) we use `construct` function.

ref_name.construct();

To release the shared reference we use `release` function.

ref_name.release();

Here the shared reference is released but the object is not terminated unless this is the last reference pointing to the object. Note that when execution goes out of the scope in which a local SrdRef is defined, that ref will automatically be released without needing to call `release` manually.

To access the object that the reference points to we use the property `obj`.

ref_name.obj

To get the number of shared references in the ownership of the object we `refCounter.count`.

ref_name.refCounter.count

You can view other methods defined in this class here.

Example:
Now we will define a shared reference that points to an object with `int` type and we will create the object using `construct` function as follows:

def x: SrdRef[int];
x.construct();

Now to assign a value to the object that the shared reference points to (which is `x`) we use the property `obj` as follows:

x.obj = 9;

To create multiple shared references that share the same object, we need to create another shared reference an assign to it the first one:

def y: SrdRef[MyType] = x; // now both references points to the same object.

Now to print the value of the object that the references `x` and `y` point to:

print(x.obj); // 9
print(y.obj); // 9

To get the number of shared references:

print(x.refCounter.count); // 2

To release the first shared reference we write:

x.release();

So this cause the release of the first shared reference and decrease the counter by 1, which means its current value is 1. This means that there exists a reference that points to the object, so the object referred to will not released.
Now if we release the second shared reference:

y.release();

This cause the release of the second shared reference and decreasing the counter by 1, which means its value is zero so there no other references to the object and we no longer need it so it will be terminated and its memory will be released.

Note: usually we won't need to call `release` function manually since it is called automatically when terminating a shared reference (like when when the workflow exit the scope that the shared reference is define in it) or when assigning a new object to the shared reference.

The full code:

import "Srl/Console.alusus";
import "Srl/refs.alusus";
use Srl;
def x: SrdRef[int];
x.construct();
x.obj = 9;
def y: SrdRef[int] = x;
Console.print(x.obj); // 9
Console.print(y.obj); // 9
Console.print(x.refCounter.count); // 2
x.release();
y.release();

Note: later when dealing with classes and dealing with its members we won't need to write `obj` to access the member `x` from the object because you could write `myRef.x` instead of `myRef.obj.x`.

import "Srl/Console.alusus";
import "Srl/refs.alusus";
use Srl;
class A {
  def val: int;
}
def a: SrdRef[A]; // define a shared reference for this class
a.construct();
a.val = 200; // assign a value to the object
Console.print("%d", a.val); // 200
a.release();


WkRef

Weak Refs are another type of smart references. A `WkRef` is created as a copy of `SrdRef` that provide the ability to access an object owned by a one or more `SrdRef` references but it does not contribute to "reference counting". Creating and terminating a WkRef for an object does not effect the reference count of that object. In case all `SrdRef` of an object are released then the object is terminated even if there are `WkRef` instances pointing to it This type of references is useful to avoid closed reference cycles or what is called "cyclic dependency" which leads to a memory leak.
For example, let's think about the scenario where we have two classes `A` and `B`, and both of them has a reference to the other. So it is always the case that `A` points to `B` and `B` points to `A` so the reference counter will never reach zero and they will never terminated. This the reason for using `WkRef` since it is a reference that does not count in the counter, which means the the ownership is not sharedm but each of them can reach the other. This will prevent "cyclic dependency". For example in the previous example we could `SrdRef` from `A` to `B` and use `Wkref` from `B` to `A`, and in this case as soon as the external references to `A` is terminated it will terminate and `B` will also be terminated. You can find more information about this class from here.


UnqRef

Unique references are references that mange the lifecycle of objects allocated dynamically. In contrast to `SrdRef`, each object is owned by one unique reference and it does not use a reference counter. When a unique reference is terminated the object it points to will be released immediately.

Example:

import "Srl/Console.alusus";
import "Srl/refs.alusus";
import "Srl/String.alusus";
use Srl;
def str: UnqRef[String];
str.construct();
str.obj = "Hi..";
Console.print(str.obj); // Hi..
str.release(); // the object will be released.

Dynamic Memory Allocation


Dynamic memory allocation allows allocating or changing the size of a data structure at run time. Alusus standard runtime library (SRL) provides a `Memory` module that includes functions for manually allocating, resizing, and freeing memory blocks. Visit the SRT reference for details about this module.

`Memory` module contains `alloc` function that has following signature:

@expname[malloc] func alloc (size: ArchInt): ptr;

This function (as its name shows) could be used to allocate one block of memory dynamically with the size we want, and as shown in its definition it returns a `ptr` but without specifying its data type, so you should cast it to a pointer of the data type you want.
In the following example, we want to allocate a block of memory enough for storing two Int values. We use `alloc` function then we cast it to the required data type, which is `ptr[Int]` as shown in the next example:

import "Srl/Memory.alusus";
use Srl;
def p: ref[int];
// allocate memory with the required size and then apply casting.
p~ptr = Memory.alloc(int~size * 2)~cast[ptr[int]];