Ownership · Lesson 11.10

Initialization

Source
Declare a var without a value, and see the compiler insist that every path assigns it before a read.
You'll need: Else if, Constructor

A var may be declared before its value is known, and given one later. Until then it holds nothing, and the compiler tracks it: every path that reaches a read must have assigned it first. A read that some path can reach without an assignment is rejected at compile time — so a Rux program never sees leftover garbage from an unassigned variable.

This is the last lesson of the part, and it closes the circle on ownership. A value's life ends with its destructor; this lesson is about how it begins, and why the compiler must always know whether a variable holds a value at all.

Declare now, assign later

Sometimes the value depends on a decision that has not been made yet. grade is declared with a type and no value, and each branch of an else if chain assigns it:

func Grade(score: int32) -> char8[..] {
    var grade: char8[..];
    if score >= 90 {
        grade = "excellent";
    } else if score >= 50 {
        grade = "pass";
    } else {
        grade = "retry";
    }
    return grade;
}

The compiler follows every path from the declaration to the return, and on each one grade has been assigned. The read is accepted.

flowchart LR
    d["var grade: char8[..]<br/>holds nothing"] --> q{"score?"}
    q -- ">= 90" --> a1["grade = excellent"]
    q -- ">= 50" --> a2["grade = pass"]
    q -- "else" --> a3["grade = retry"]
    a1 --> r["return grade<br/>assigned on every path"]
    a2 --> r
    a3 --> r
    q -. "without the final else" .-> x["error: value 'grade' may be<br/>uninitialized on some<br/>control-flow paths"]

Delete the final else, and a score below 50 reaches the return with nothing assigned. The compiler refuses the program and points at where that path starts.

Reading too early

The simplest case is a read straight after the declaration:

var total: int32;
PrintLine("{}", total);
error: variable 'total' is used before it is initialized
  note: 'total' was declared without an initializer at 47:5
  help: assign a value to 'total' before this use

Many languages would print whatever happened to be in that memory, or a silent zero. Rux makes you say what the value is.

A let needs its value now

A let has no such freedom. It can never be assigned later, so it needs its value where it is declared: let total: int32; on its own is rejected with error: immutable variable requires an initializer.

One field at a time

A plain struct of numbers may be filled in one field at a time, and it counts as assigned once every field has been written. The compiler tracks the fields individually: after var p: Point; p.x = 1;, reading p.x is fine, but reading p.y is refused, with a note that only p.x has been written.

A type that needs destroying is different. When a struct holds a Guest — the type with a destructor from the Destructor lesson — a guest written into one field of an empty struct would belong to no whole value, and nothing would ever destroy it. So such a variable must get its value whole:

var table: Table;
table.guest = Guest { name: "Gus" };
error: cannot write field 'guest' of 'table', which holds no value
  help: initialize 'table' whole, as in 'table = Table { ... }'

It is the same rule as in Partial move, seen from the other end: a value with a destructor is alive whole, or not at all.

A constructor runs for you

There is one exception to "holds nothing". When the type has a constructor that takes no arguments, var value: T; calls it, and the variable starts out initialized:

extend Tally {
    func Tally() -> Tally {
        PrintLine("    Tally() starts the count at zero");
        return Tally { count: 0 };
    }
}
var tally: Tally;
tally.count += 1;

tally.count += 1 reads the count before changing it, and that read is fine — the constructor has already run, as the output shows.

DeclarationStarts as
var grade: char8[..];nothing — assign before reading
var tally: Tally; with Tally()whatever Tally() returns
let total: int32;an error — a let needs its value

The program

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

Src/Main.rux
// A `var` may be declared before its value is known, and given one later. Until then it holds
// nothing, and the compiler tracks it: every path that reaches a read must have assigned it
// first. A read that some path can reach without an assignment is rejected at compile time, so
// a Rux program never sees leftover garbage from an unassigned variable.
//
// A `let` has no such freedom. It cannot be assigned later, so it needs its value where it is
// declared.
//
// One exception to "holds nothing": when the type has a constructor taking no arguments,
// `var value: T;` calls it, and the variable starts out initialized.
import Io::PrintLine;

// Every branch assigns `grade`, so the read in `return` is accepted. Delete the final `else` and
// one path reaches the read with nothing assigned:
//
// error: value 'grade' may be uninitialized on some control-flow paths
//   note: one unavailable path for 'grade' originates at 20:5
//   help: initialize or preserve 'grade' on every path before this use
func Grade(score: int32) -> char8[..] {
    var grade: char8[..];
    if score >= 90 {
        grade = "excellent";
    } else if score >= 50 {
        grade = "pass";
    } else {
        grade = "retry";
    }
    return grade;
}

struct Tally {
    count: int32;
}

extend Tally {
    func Tally() -> Tally {
        PrintLine("    Tally() starts the count at zero");
        return Tally { count: 0 };
    }
}

func Main() -> int {
    PrintLine("grades: {}, {}, {}", Grade(95), Grade(72), Grade(31));

    // Declared without a value and read straight away:
    //
    //     var total: int32;
    //     PrintLine("{}", total);
    //
    // error: variable 'total' is used before it is initialized
    //   note: 'total' was declared without an initializer at 47:5
    //   help: assign a value to 'total' before this use

    // When the type needs destroying, a variable without a value must also get its value whole.
    // Here `Table` is a struct holding a `Guest`, the type with a destructor from the Destructor
    // lesson. A guest written into one field would belong to no whole table, and nothing would
    // ever destroy it:
    //
    //     var table: Table;
    //     table.guest = Guest { name: "Gus" };
    //
    // error: cannot write field 'guest' of 'table', which holds no value
    //   help: initialize 'table' whole, as in 'table = Table { ... }'

    // `Tally` has a constructor without arguments, so this declaration runs it.
    PrintLine("tally:");
    var tally: Tally;
    tally.count += 1;
    PrintLine("    count {}", tally.count);
    return 0;
}

Run it

cd Examples/Ownership/Initialization
rux run
grades: excellent, pass, retry
tally:
    Tally() starts the count at zero
    count 1

Common mistakes

A path that never assigns.
Without the final else in Grade, return grade; fails with error: value 'grade' may be uninitialized on some control-flow paths. Every branch that can reach the read must assign — add the missing else, or give the variable a sensible starting value.
Reading before the first assignment.
var total: int32; PrintLine("{}", total); fails with error: variable 'total' is used before it is initialized. There is no hidden zero. If zero is what you mean, write var total: int32 = 0;.
A let without a value.
let total: int32; fails with error: immutable variable requires an initializer. Declare it with var if it must be assigned later, or give it its value on the spot.
Filling a destroyable struct one field at a time.
var table: Table; table.guest = Guest { name: "Gus" }; fails with error: cannot write field 'guest' of 'table', which holds no value. Assign the whole struct: table = Table { guest: Guest { name: "Gus" } };.

Try it yourself

  1. Delete the final else in Grade and read the error and its note.
  2. Declare struct Point { x: int32; y: int32; }, then var p: Point; p.x = 1;, and print p.y. Then assign p.y too and print both.
  3. Remove the Tally() constructor. Which lines in Main are now rejected, and what would you write instead?

Learn more