LEVIATHAN v962456e · 962456eee1

Declarations

const declarations

Fix a local, field, global, parameter or loop binding after its initializer, with the exact rules for each kind of slot.

since 0.1.0-alpha.1linuxwindowswasm

Description

const makes a slot read-only after its initialization. It applies to local variables, fields, namespace-level variables, parameters and loop bindings:

const int maxRedirs = 50;                    // local: fixed at declaration
const var limit = 100;                       // composes with inference
const Array<string> args = std::sysArgs();   // namespace global: fixed at startup

class Session {
    public const string id = "s-1";          // field: a named compile-time constant
    const int SSL = 0x0800;
}

void handle(const Options o) { ... }         // parameter
for (const string a in args) { ... }         // per-iteration binding

const is not part of a type. It never appears where a type is expected and does not change assignability, overload selection or generics. It is also not transitive: const MyClass m = MyClass(); fixes the variable m, but the object it refers to can still change, so m.field = 5; is allowed. For deeply immutable data use a struct, a pure array or map, or a get-only accessor.

const bindings, fields, parameters and loops

const int maxRedirs = 50;
const var limit = 100;
const Array<string> names = ["a", "b"];
class Session {
    public const string id = "s-1";
    const int SSL = 0x0800;
}
class Opts { int level = 1; }
void handle(const Opts o) {
    o.level = 5;
    console.writeln(o.level);
}
console.writeln(maxRedirs);
console.writeln(limit);
console.writeln(Session().id);
console.writeln(Session().SSL);
const Opts shared = Opts();
shared.level = 3;
console.writeln(shared.level);
handle(shared);
for (const string n in names) {
    console.writeln(n);
}
int base = 7;
const int computed = base * 2;
console.writeln(computed);
50
100
s-1
2048
3
5
a
b
14

The objects behind shared and o are still mutable; only the variables are fixed.

Rules

Each slot has a window in which it may be written; after the window closes it is read-only.

  • Locals. The window is the declaration's initializer. const int x; without an initializer is a compile error, because the default value would be frozen forever. The initializer can be any expression.
  • Fields. A const field is a named compile-time constant. It must have an initializer, and the initializer must be a compile-time constant: a literal, None, an array of constants, another const value, or an arithmetic or bitwise operator over constants. No constructor may assign a const field. A field whose value is known only at construction time is readonly (lang.readonly).
  • Namespace and top-level variables. The window is the initializer, which can be any expression.
  • Parameters. The value bound by the call; assigning to a const parameter in the body is an error.
  • for (const T x in ...). Each iteration has its own fresh binding, which cannot be assigned.
  • Compound assignment (x += 1) is a write, like =. a[i] = v on a pure array is a write to the array variable and is rejected for a const array.
  • Calling a mutating method on a const struct variable is a compile error.
  • A set accessor cannot be declared over a const field, and a field cannot be both const and readonly.
  • If two bases declare a field with the same name and type, one const and one not, the class that inherits both is a compile error; resolve it with distinct or by restating the field in the derived class.
  • A const: label inside a class body declares a run of fields const, in the way public: and private: set access. The two are independent: public: after const: does not end the const run, and a member written var opts out of it.

Initialization order of globals

Every namespace-level and top-level variable is first created with its type's default value, before any initializer or statement runs, so an initializer always sees a valid variable. Then:

  1. Namespace-level initializers run, before the program body starts.
  2. Top-level statements run in source order, and a top-level variable's explicit initializer runs at its own position among them.

A namespace initializer therefore sees a top-level variable at its default value, not at the value its own initializer will assign later.

Top-level statements

The top level of a file is the program body and accepts the usual statements: expressions, declarations, if, while, for, do-while, labeled loops, match, try/catch and throw. It does not accept break, continue or return, since there is no enclosing loop or function, nor using, which declares a resource binding for a block.

Examples

Namespace initializers run first and see top-level variables at their defaults:

Initialization order

int seedValue = 5;
namespace Reg {
    int fromTop = seedValue;
    int fixed = 3;
    int fromNs = fixed * 2;
}
console.writeln("start");
console.writeln(Reg::fromTop);
console.writeln(Reg::fromNs);
int later = seedValue + 1;
console.writeln(later);
start
0
6
6

Reg::fromTop is 0, not 5: the namespace initializer ran before the top-level statement that sets seedValue.

A const: section:

A const: section with a var opt-out

class Config {
const:
    string host = "localhost";
    int port = 8080;
public:
    string name = "svc";
    var int hits = 0;
    void hit() { hits = hits + 1; }
}
Config c = Config();
c.hit();
c.hit();
console.writeln(c.host);
console.writeln(c.port);
console.writeln(c.name);
console.writeln(c.hits);
localhost
8080
svc
2

name is const too, because public: does not end the const: run; only hits, marked var, can change.

Writes that are rejected at compile time:

Writing a const

const int x = 1;
x = 2;                                  // error: cannot assign to const 'x'
const int y;                            // error: needs an initializer
class C {
    const int n = 1;
    new C() { n = 2; }                  // error: cannot assign to const field 'n'
    const readonly int m = 1;           // error: a field cannot be both const and readonly
}
int f() => 3;
class D { const int k = f(); }          // error: needs a compile-time-constant initializer
not run — a compile error on purpose

A const field may be computed from other const fields:

A const field computed from other constants

class Flags {
    const int A = 1;
    const int B = 2;
    const int BOTH = A | B;
}
console.writeln(Flags().BOTH);   // 3
not run — blocked by an open compiler bug

See also

  • readonly fields — Fix an instance field once, either in its initializer or in every constructor, and keep it read-only afterwards.
  • Fields — Declare the data a class or struct holds, with optional initializers, and how a field without an initializer gets its default value.
  • Value structs — Declare value types that are copied on every assignment, pass and return, with derived equality and explicit mutating methods.
  • Namespaces — Group declarations under a name, reopen a namespace to add to it, nest namespaces, and reach members with ::.