Cone Language Reference

Global state: constants, globals and initialization

On this page

A module holds state as well as names: its constants and its global variables, one of each for the whole program, the way a struct's value holds its fields. State that lasts as long as the program, and that every function and every thread may reach, needs more care than a local variable: it has to be set up before anything reads it, released when the program ends, and kept safe from threads that share it. This page covers constants, global and static variables, globals with one copy per thread, and the initializer and finalizer every module may have. Afterwards you can give a module the state it needs, and know when it is ready.

mod visits;

import stdio use *;

// A named constant: fixed when the program is compiled
const daysInWeek = 7;

// A global, declared with its type and given its value at start-up
let days i32;

// The module's initializer runs before main
fn @initpure init() {
  days = 52 * daysInWeek;
}

// The module's finalizer runs after main returns
fn final() {
  print <- "closing the visitor book\n";
}

fn visit() i32 {
  // A static variable: one copy, kept from call to call
  static var count = 0;
  count += 1;
  count;
}

fn main() {
  visit();
  visit();
  print <- visit(), " visits in ", days, " days\n";
}

It prints:

3 visits in 364 days
closing the visitor book

Try this: move the line in final into init, and see it print before anything main does.

A module's own state#

Because there is one of each global and every thread may reach it, a global is more constrained than a local. Because a module may need to set its state up when the program starts, and release it when the program ends, every module may have an initializer and a finalizer, run in the order the modules depend on each other, so a module's state is ready before any module that depends on it starts. And because a global is to a module what a field is to a type, its members may be folded in as names of the module (Folding through a global).

Named constants#

A const gives a value fixed when the program is compiled a name:

const pi = 3.141592;

Wherever the program names pi, the compiler puts in the constant itself. A constant's value may be an expression computed from constants alone: number literals, true and false, and other named constants, declared before it or after, combined by the arithmetic, bitwise and shift operators (+ - * / % | & ^ ~ << >>, and a prefix -), the comparisons, not, and and or, a number's conversion T.from(x), a reinterpretation with as, and parentheses. The constant is the literal the expression computes:

const Red = 0x1u32;
const Green = 0x2u32;
const All = Red | Green | Blue;    // 0x7u32
const Blue = 0x4u32;
const Half = 1.0f32 / 2.0f32;      // 0.5f32

Each operator gives the value it gives when the program runs, on the same type: an f32 is computed as an f32, integer division truncates toward zero, and << drops the bits shifted out. What has no value when the program runs is an error: a result its type cannot hold (255u8 + 1u8), a division or remainder by zero, and a float converted to an integer that cannot hold it. So is a shift by the type's width or more, or by a negative amount, although the running program gives one a value (Shift operators). So is a constant whose value names itself, directly or through other constants. A function call, a variable or a method like sqrt is not a constant, so neither is an expression using one. The same expressions may be written wherever a constant is required: a global variable's or a static's value, a parameter's or a field's default, and the elements of an array, struct or tuple literal in any of these.

A variant of an enum that holds no value is a constant: Kind.Os for enum Kind {Os, Cancelled, TimedOut;}, and None for an Option, whose other variant does hold one. It is its tag, and the rest of the enum's value, where it is wider than the tag, is zero. It may be a named constant's value, a global's, a parameter's or a field's default, or an element of an array, struct or tuple literal in any of these. A variant that holds a value is not, nor is a variant of an enum that has fields in common, whose value is no part of the tag.

enum Kind {Os, Cancelled, TimedOut;}

const Fallback Kind = Kind.TimedOut;
struct Failure {
	pub code u32;
	pub kind Kind = Kind.Os;       // left out of new Failure(5u32), it is Os
	pub next Option[i32] = None;   // and none; Some(5) is not a constant
}

A constant may name a compound literal too, such as an array, a struct or a tuple. Each use of it is that literal, so it may be used however the literal could be:

const primes Array[u32, 4] = [2u32, 3u32, 5u32, 7u32];

primes[n]          // an element, by a constant or run-time index
&primes            // a reference to the array, as &[2u32, 3u32, 5u32, 7u32] is

A constant's elements and fields may be read, and a constant array or string, which is kept in read-only memory as its literal is, may be borrowed: as a reference or a slice, never mut, living as long as the program. No other constant may be borrowed, nor may a field of one or of one of its elements, since each use of it is a fresh copy with no place in memory, as its literal would be; that includes calling a method that takes a borrowed self on one. (Borrowed references explains borrowing.)

Global variables#

A constant cannot change and has no place in memory. A global variable has one, for as long as the program runs. Global variables across all modules, and types' statics, are managed by the global region: their memory is allocated all at once when the program is loaded, they come into being when the program starts and disappear only when it stops, and the lifetime that names that scope is 'static (Lifetimes). Two constraints apply to every one:

  • Its type must be Shareable. Every thread may name a global at once, and every program is taken to have threads, so a plain var global is refused. A global may be let (given its value where it is declared, or by the module's initializer), let holding a lock or an atomic value (the shared mutable form, Lock permissions), @threadlocal (one per thread, never shared), or @workgroup. A value known only after the program has started (a device made, a choice read from the command line) belongs to the object it describes, and is handed down as state rather than kept in a global.
    Not yet checked: a plain var global is still accepted today. The check at the declaration is being built.
  • It must be given a valid value. Usually the initial value is known when the program is compiled: a literal, a named constant, or a constant expression. Otherwise the global is declared with its type and no value, and the module's initializer assigns it, as days is above.

A borrow of a global may be returned, since the global outlives every caller. A borrow into a var global has one rule more, about calls that may reshape a collection through the global while the borrow is held: A borrow into a var global.

A global may also fold its type's members in as names of the module, with the same use clause a struct's field carries, so a function of the module writes area() where it would have written config.area(). There is exactly one global, at an address known when the program is compiled, which makes this the cheapest fold there is; Folding through a global has the whole clause.

Static variables#

Static variables in a function#

A variable declared inside a function is ordinarily created afresh on every call and released when the call ends. One declared static is one copy instead, shared by every call of the function, initialized once before the program starts running, and kept across calls, like count in visit above:

fn tick() i32 {
  static var calls = 0;
  calls = calls + 1;   // 1 on the first call, 2 on the second, ...
  calls;
}

The same word means the same thing at every scale. Inside a type, a static variable is one copy shared by every value of that type, rather than a field held by each value. At the top of a module, it is the module's one copy, which is what a global variable is already. A generic type or module has one copy per instance: each instance, such as Box[i32], is a type or module in its own right with its own static, since the static's type may name the generic parameter. Because a static's initial value is written into its storage before anything runs, it must be a literal, as a global variable's must. static applies to a variable and to nothing else: a function has no copy per call to share, and neither does a type or a statement.

Static variables in a struct#

A field is held by every value of the struct, one copy each. A variable declared static inside the struct is one copy shared by all of them. It is not a field: no value carries it and no initializer sets it. From outside the struct it is reached through the type's name, in the same way as the struct's static functions; inside the struct's own methods and functions it is reached by its bare name.

struct Tally {
  pub n i32;
  pub static var made = 0;

  pub fn make(n i32) Tally {
    made = made + 1;       // shared by every Tally
    new Tally(n);
  }
}

let a = Tally.make(1);
let b = Tally.make(2);
let count = Tally.made;   // 2

Like a global variable, a static must be initialized with a constant value (a literal, a named constant or a constant expression), and it obeys pub as any other member of the struct does. A generic struct's static belongs to each instance separately: Crate[i32] and Crate[f32] are two types, and each has its own copy.

Thread-local global variables#

A global variable written with @threadlocal after its let or var is one copy per thread: every thread that runs has its own, and a thread's code reads and writes only its own. Since no other thread can see it, it may be var:

var @threadlocal depth i64 = 0i64;

fn enter() {
  depth = depth + 1i64;   // this thread's depth, whatever the others' are
}

Each thread's copy starts from the initial value written on the declaration, which is why a thread-local global is constrained more than other globals:

  • It must be written with an initial value, a literal as every global's is. It cannot be left for the module's initializer to assign, since that runs on one thread and would give a value to that thread's copy alone.
  • It must be var. A copy per thread of a value no thread can change would be the same value in every copy.
  • Its type may not need finalizing: not a struct with a finalizer, nor anything holding one, nor an owning reference whose region has work to do when it is dropped. Nothing finalizes a thread's copy yet when its thread ends.
  • Only a global declared in a module may be thread-local: not a local, a function's or a type's static, a parameter, a field, or a module trait's global.

A thread-local global is declared extern like any other, with @threadlocal kept and no value, and a library's include file declares its thread-local globals that way. Every declaration of it must say @threadlocal, because a compiled object reaches a thread's copy by a different route than it reaches ordinary storage. The compiler checks this only among the declarations of one compile. The language itself starts no thread; a thread the thread package starts, or one started through the operating system directly, sees its own copy as described.

A global written with @workgroup after its let or var is a GPU workgroup's: one copy for each workgroup of a compute dispatch, which its invocations share, and an ordinary global on the CPU (Workgroup memory). It is written as a thread-local is, but with no initial value; a global is one or the other, never both.

The module's initializer#

Sometimes a global's initial value is not known, or is too complex to write out, when the program is compiled. Then the global is declared with its type and no value, and the module's initializer, a function called init, gives it one when the program or library is loaded:

struct Handler {
  pub port i32;
}

let handler Handler;

fn @initpure init() {
  handler = new Handler(80);
}

The global is declared with its type, since there is no value to infer it from. Inside init it starts out holding nothing, exactly as a local declared without a value does: init may assign it once, even when it is let, and may not read it before it does. Every other function sees it holding the value init gave it.

The initializer is declared initpure, a less constrained version of pure. A function marked initpure may not read any uninitialized global of its module, and may call only functions marked pure or initpure. That marks it as safe to call while initialization runs, because it relies on no state that is not ready yet: no uninitialized global of its module, and the modules it depends on are initialized first. It does not forbid input and output. Set-up an initializer needs, such as loading a library, opening a device or reading the environment, is done through functions marked initpure, kept to what the module's own state needs.

To make initialization safe and complete, the compiler:

  • verifies that every global declared without a value is assigned one somewhere in its module's init;
  • verifies that pure and initpure functions keep their constraints;
  • runs the initializers of the modules a module depends on before its own.

The first and last are implemented. The second is not: a function marked @initpure is accepted, and nothing yet checks what it calls, since pure functions are not implemented.

The module's finalizer#

The initializer is written fn @initpure init(), taking no parameters and returning nothing. A module may also declare a finalizer, a function called final, written fn final(), to release its own state when the program is shutting down, as visits does at the top of this page.

Each module declares only its own initializer and finalizer, for its own state; it cannot set up another module's. A module may declare either, both or neither. At module scope the names init and final mean these two, and naming anything else by them is an error.

Where a module's globals hold values whose type has a finalizer, or owning references, the compiler gives the module a finalizer that runs the module's own final, if it declares one, and then finalizes each such global, releasing an owner as a local's end would, in the order they are declared: the way a type's final runs before its fields are finalized. Such a module may not declare a function named drop, which is the name the compiler gives it.

When every module's initializer and finalizer run#

A program does not start them itself. The compiler stitches every module's initializer together in the order the modules depend on each other: each module's runs after the initializers of every module it depends on, and the program's own module's runs last. Every module's finalizer runs in exactly the reverse order, the program's own first. A module with neither is left out.

Then the compiler builds the program's entry, the C main the system calls, round the program's own main: the entry runs every module's initializer, calls main (with the command line's argc and argv if it takes them), runs every module's finalizer, and exits with the value main returned (0 if it returns nothing). So everything has been initialized when main begins, and a program that opened files in an initializer has them closed by a finalizer when main returns. The program's main is a function of the module that is the program, taking nothing, or an i32 and a **u8, and returning an integer or nothing:

fn main() i32 {
  run();
  0;
}

The functions initAll() and finalAll(), which every module can use, run the same two stitched sequences, and each runs its modules once: a program that calls them itself, as programs did before the entry was built, finds nothing left to run at initAll(), and runs the finalizers at its first finalAll() (so it may finalize early) and not again when main returns.

A program ends without running any finalizer when it panics, or calls the C library's exit or abort: the entry's finalizers run only when main returns. A library compiled on its own has no entry of its own, and keeps its initializer and finalizer for the program that imports it, whose include file for the library declares them with extern, and declares the globals its finalizer finalizes. A C program that calls such a library is on its own for setting it up. A main that takes any other parameters, a function with a C name that is the program's own C entry, and a WebAssembly or GPU module get no entry from the compiler.

A C-named module is reached by its C names, not by an import, so a program need not know it exists: the program may link it, as every program links the runtime, and declare extern only the functions it calls. Its initializer is therefore run when the object holding it is loaded, as a C static constructor is, before the program's main and ahead of every other module's initializer. So a C-named module's initializer may not depend on a module with an initializer of its own, unless that module is C-named too. Its initializer and finalizer are named after the module as any module's are, cla.init and not the C name init, so two C-named modules may each declare them.

Not followed yet: where two C-named modules with initializers are compiled separately, the order their initializers run is the linker's, not the order the modules depend on each other (within one compile it is that order). A C-named module's finalizer runs only in a program that imports the module.

You can now give a module state, and know that it is ready before any code that depends on it runs. The pages so far divided a program into parts; the next three are about making parts work together without being welded together, starting with reaching a part's names as your own.