Memory · Lesson 15.1

Pointer

Source
Take an address with @, then read through a *T and write through a *var T.

Every value lives somewhere in memory, and that place has an address: a number that says where it is. A pointer is a value that holds such an address. With one, a part of the program can reach a value that lives somewhere else, read it and — when it is allowed to — change it.

You have met a close relative already: the reference, &T and &var T, from Reference. A pointer does the same job with more freedom and fewer checks. The whole of this part is built on that freedom, so this first lesson takes it slowly.

Taking an address and reaching through it

Two operators do all the work. @score takes the address of score, and *pointer reaches the value at an address:

var score: int = 10;

// Two pointers to one variable. Neither is a copy of `score`; both lead back to it.
let reader: *int = @score;
let writer: *var int = @score;

*writer = 25;

Neither pointer holds a copy of 10. Both hold the address of score, so after *writer = 25 the variable itself says 25, and reading *reader says 25 too.

flowchart LR
    reader["reader: *int"] -- "reads" --> score[("score: int<br/>25")]
    writer["writer: *var int"] -- "reads and writes" --> score
OperatorRead it asGives
@value"the address of value"a pointer
*pointer"the value at pointer"the value itself, to read or to assign

*T reads, *var T writes

Pointers split the same way bindings do. The var after the * describes the value pointed at — the pointee — not the pointer:

TypeThrough it you mayLike a binding made with
*intread *plet
*var intread and write *pvar

The split follows the original binding, too. The address of a let binding is always a read-only *int, so a pointer can never be used to change something that was declared unchangeable:

let limit: int = 100;
let limitPointer: *int = @limit;

Asking for a *var int there is an error; the exact message is under Common mistakes below.

Fields through a pointer

To reach a field through a pointer, use a plain .. There is no need to write * first:

var point = Point { x: 1, y: 2 };
let cursor: *var Point = @point;
cursor.x = 7;

The write lands in point itself, which then prints as (7, 2).

null, the pointer to nowhere

Unlike a reference, a pointer can be stored, compared and set to null, which means "no address at all":

var target: *int = null;
PrintLine("target is null   {}", target == null);
target = @score;
if target != null {
    PrintLine("target now reads {}", *target);
}

Comparing with == and != is always safe. Reaching through null with * is not, and the compiler will not stop you: the program simply crashes when it gets there. Check before you reach.

Two pointers are equal when they hold the same address. That is why the last line prints true — target and reader both lead to score.

Reference &TPointer *T
Checked by the compileryes — it is always validno
Can be nullnoyes
Can be stored in a field or returnednoyes
Reached withthe name alone*p, or p.field

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// A pointer is an address: a plain value that says where another value lives. `@score` takes
// the address of `score`, and `*pointer` reaches the value at that address, to read it or to
// write it.
//
// Pointers split the same way bindings do. A `*int` may only read what it points at; a
// `*var int` may also write. And the address of a `let` binding is always a read-only `*int`,
// so a pointer can never be used to change something declared unchangeable.
//
// A reference (`&T`, from the Types part) also reaches another value, but the compiler checks
// that it stays valid. A pointer is not checked: it can be stored, compared, and set to `null`,
// which means "no address at all". The rest of this part is built on that freedom, and on the
// care it asks for.
import Io::PrintLine;

struct Point {
    x: int;
    y: int;
}

func Main() -> int {
    var score: int = 10;

    // Two pointers to one variable. Neither is a copy of `score`; both lead back to it.
    let reader: *int = @score;
    let writer: *var int = @score;

    *writer = 25;
    PrintLine("score is now     {}", score);
    PrintLine("read via pointer {}", *reader);

    // The address of a `let` binding is read-only. Asking for `*var int` here is an error:
    // "'@limit' yields a read-only '*T'; declare 'limit' with 'var' for a '*var T'".
    let limit: int = 100;
    let limitPointer: *int = @limit;
    PrintLine("limit is         {}", *limitPointer);

    // A field is reached through a pointer with a plain `.`, without writing `*` first.
    var point = Point { x: 1, y: 2 };
    let cursor: *var Point = @point;
    cursor.x = 7;
    PrintLine("point is         ({}, {})", point.x, point.y);

    // `null` is the pointer that points nowhere. Comparing with `==` is safe; reaching through
    // it with `*` is not, and the compiler will not stop you. Check before you reach.
    var target: *int = null;
    PrintLine("target is null   {}", target == null);
    target = @score;
    if target != null {
        PrintLine("target now reads {}", *target);
    }

    // Two pointers are equal when they hold the same address.
    PrintLine("same address     {}", target == reader);
    return 0;
}

Run it

cd Examples/Memory/Pointer
rux run
score is now     25
read via pointer 25
limit is         100
point is         (7, 2)
target is null   true
target now reads 25
same address     true

Common mistakes

A writable pointer to a let binding.
let limit: int = 100; followed by let w: *var int = @limit; fails with error: cannot assign '*int' to '*var int': '@limit' yields a read-only '*T'; declare 'limit' with 'var' for a '*var T'. Do what the message says, or settle for a *int if you only need to read.
Writing through a read-only pointer.
*reader = 5; with reader: *int fails with error: cannot modify data through read-only pointer '*int'. The pointee type decides what you may do, whatever the variable it points at was declared with.
Forgetting the @.
let reader: *int = score; hands over the value 10, not the place it lives, and fails with error: cannot assign 'int' to '*int'. A pointer is made from an address: @score.
Reaching through null.
*target on a null pointer compiles without a word and crashes the program when that line runs. Compare with null first wherever a pointer might not have been set.

Try it yourself

  1. Add a second variable, var bonus: int = 5;, point target at it, and check that target == reader now prints false.
  2. Write func Swap(a: *var int, b: *var int) that exchanges two values, and call it as Swap(@first, @second).
  3. A pointer is a value, so it has an address too. Make var p: *var int = @score; and let pp: **var int = @p;, then write **pp = 42 and print score.

Learn more