Skip to content

Bindings ​

talor
const MAX: i64 = 1024;      // compile-time constant, module level
let x = 1;                  // a local
let y: f64 = 2.0;
let n = 0;                  // the same binding: a local that is written is mutable
n = n + 1;
n += 1;

Mutability is inferred, and let is the one binding keyword ​

Whether a name can be reassigned, and whether the value it holds can be mutated in place (p.x = 1, xs.push(v), x.bump()), follows from what the body does with it: a local that is written is mutable, and one that is not is not. let declares a local, and there is no second spelling to write the intent down, because an annotation the compiler can compute is not asked for.

talor
let x = 1;
x = 2;                      // accepted: `x` is written, so `x` is mutable

let xs: Array<i64> = [];
xs.push(1);                 // a method that edits its receiver needs a place, and a binding is one

What mutability is not:

  • A const is not a local and is never assignable.
  • A place reached through a view is read-only, whatever the binding says.
  • A parameter follows its mode: a parameter the clause names in neither edits nor takes is read-only, a parameter the clause says edits (edits self on a method) is writable, and a plain parameter that the body writes is an edits, written or inferred from the body.
  • A value that was moved or taken cannot be written through the old name.
  • A place reached through a shared is read-only, because two tasks could write it at once. The place a lock lends, a Locked<T>, is writable for the length of one call: see concurrency.

A binding that nothing writes is accepted: the mutability follows from the body, and an unused local is the unused-declaration analysis' concern.

const: computed when the program is compiled ​

const lives at module level and its value is computed at compile time, so what every use reads is a literal however the initializer was written.

talor
const fn buckets(bits: i64): i64 {
    let base = 1 << bits;
    if base > 8 { base } else { 8 }
}

const BUCKETS: i64 = buckets(12);        // 4096, computed here
const MASK: i64 = BUCKETS - 1;           // a constant may mention another
const READY: bool = BUCKETS > 100;

fn main(): i32 {
    let n = const { buckets(4) * 2 };    // computed where a program needs one
    n as i32
}

The rules:

  • A const fn is a function the compiler can run at compile time. It is an ordinary function too: calling it at run time changes nothing.
  • What the evaluator holds is what a value can be at compile time and nothing that allocates: integers, bools, chars, and string literals. Arithmetic, comparison, the logical operators, as, if, let, return, a block's tail value, another constant by name, and a call to a const fn.
  • const { expr } computes a value where a program needs one.
  • A const fn whose body holds anything else - a loop, an index, an assignment to a local, a match, a literal that allocates, a method call, a call to a function that is not a const fn, a closure, a template - is refused at its declaration, whether or not anything calls it.
  • Evaluation at compile time has a bounded room, so a const fn that recurses without end is an error when the program is compiled rather than a compiler that hangs.
  • A constant that overflows, divides by zero, or shifts past the width of its type is an error at compile time: compile time has no panic to fall back on.
  • There are no const generics: Array<T> is dynamic, so nothing in the language has a size to parameterize.

Conventions ​

Not enforced, but the whole tree follows them: snake_case for functions, variables and modules; PascalCase for types, traits and the cases of a variant; SCREAMING_SNAKE for constants.

Talor v0.1.0 - Released under the MIT OR Apache-2.0 license.