Wado

Statements and Expressions

A function body is a sequence of statements. This chapter covers what a statement is, how variables and globals are declared and scoped, the operators and their precedence, and ranges. Branches and loops are in Control Flow, and literal values in Literals.

Statements

An expression may stand as a statement. It is evaluated and its value discarded. A function returns a value only through return (Return Values).

Semicolons

; separates statements; it does not terminate them. A block's last statement may drop it, whatever kind of statement it is. Dropping it does not make the statement an expression, so a value-returning function still needs return.

fn f() -> i32 {
    let x = 1;
    return x + 1   // no `;` needed on the last statement
}

test {
    assert f() == 2;
}

A newline never separates. There is no automatic semicolon insertion, so two statements always need a ; between them:

let x = 1 let y = 2    // error: expected `;`

Consecutive semicolons enclose empty statements, which mean nothing. wado format removes them.

A block's value is its last expression whether or not a ; follows it. Unlike in Rust, a trailing ; does not turn it into (). Write () to mean ():

let a = if c { 1 } else { 2 };
let b = if c { 1; } else { 2; };       // a trailing `;` keeps the value
let u = if c { g(); () } else { () };  // write `()` to mean `()`
assert a == 1 && b == 1 && u == ();

Only if, match, with … do and labeled blocks produce a block value. A brace in value position is a struct literal: let x = { 1 }; is an error, and let p = { x: 1, y: 2 }; is an implicit struct literal.

loop is a statement, not an expression. A loop that computes a value goes inside a labeled block, which break LABEL: expr leaves with the result.

Variable Mutability

mut governs every write reaching the binding's storage, not just reassignment. A &mut self method, a mutable borrow, and a store to a field or element all require it.

let xs: List<i32> = [1, 2, 3];
xs.push(4);      // Error: `push` takes `&mut self`
let xs: List<i32> = [1, 2, 3];
xs[0] = 9;       // Error: the store roots at an immutable binding

A write through a &mut T is what that reference grants, so the binding holding one needs no mut of its own.

Variable Scoping

Variables are scoped to their enclosing block. Variables declared inside control flow bodies (if, while, for, loop) are not accessible outside.

for let mut i = 0; i < 10; i = i + 1 {
    let x = i * 2;
}
// i and x are not in scope here

if true {
    let y = 42;
}
assert y == 42;  // Error: y is not in scope here

Shadowing in an inner block creates a new binding:

let x = 1;
if true {
    let x = x + 1;  // New binding, initialized from outer x
    assert x == 2;
}
assert x == 1;      // outer x unchanged

Same-scope shadowing is allowed when the new value is derived from the old one:

let x = 1;
let x = x + 1;  // OK: RHS references x
let x = transform(x);  // OK: RHS references x
assert x == 20;

Same-scope redeclaration without referencing the old value is not allowed:

let x = 1;
let x = 2;  // Error: cannot redeclare 'x' in the same scope
let x = 1;
let x = |x: i32| x + 1;  // Error: the x inside is the closure parameter, not the outer variable

A function's parameters share the body's scope, so a let taking a parameter's name redeclares it, and so does a parameter named twice. One pattern binding a name twice is a redeclaration too. Only a name that binds counts: a bare name that a case or a constant pattern answers binds nothing, so writing let None = a else { … }; twice in one scope redeclares nothing.

The shadowed_name Lint

A binder that takes a name already reaching a known symbol is legal and warns. Every binder counts: a let, a parameter, a closure parameter, a type parameter, a pattern binding, a local item. So does every kind of symbol, in any namespace: a function, a global, a type, a trait, a case, an outer binding. The exemption is the derivation above, since the language already sanctions it.

fn draw(Point: i32) { }        // warns: `Point` shadows the struct of the same name
fn keep<i32>(v: i32) { }       // warns: `i32` shadows the builtin type of the same name

test {
    let println = 1;           // warns: `println` shadows the function of the same name
    assert println == 1;
}

Mark the binder #[allow(shadowed_name)] where the shadowing is deliberate, or the module #![allow(shadowed_name)]:

fn twice(#[allow(shadowed_name)] String: i32) -> i32 { return String * 2; }

test {
    assert twice(21) == 42;
}

As for redeclaration, only a name that binds counts. Patterns says which reading a bare name takes. In a let or for binding a name reaching a global binds, so the lint reports it.

The derivation is read off the binder's own source, not off the let keyword, and holds at any scope. let x = x + 1 under an if, if let Some(x) = x and while let Some(x) = x are all exempt; if let Some(x) = y warns. A match arm is not exempt. Its scrutinee stays in scope across every arm, so match x { Some(x) => … } gives one name two meanings side by side.

A binder shadows only what is in scope where it is written. A name binds after the expression it binds from, and reaches only what the construct carries it to. So an if let binding is not in scope in the else, and the alternatives of an or-pattern bind one set of names rather than shadowing each other.

Local Item Definitions

struct and type (newtype) may be declared inside a function or method body, scoped to the block that declares them:

fn area(width: i32, height: i32) -> i32 {
    struct Size {
        width: i32,
        height: i32,
    }
    let s = Size { width, height };
    return s.width * s.height;
}

test {
    assert area(3, 4) == 12;
}

A local item is in scope for the whole of its block: unlike let, a use may precede the declaration statement, and one local item may name another declared later in the same block. Once the block closes the name is gone, so a nested if/while/for body cannot export an item to the rest of the function. Within its block a local item shadows a same-named module-level one, and two unrelated blocks may declare the same name without collision.

A local item is always private to its enclosing function: it never reaches another function, even one in the same module. A pub, internal or export prefix on one is an error, whether or not attributes precede it.

Local structs support their own generic parameters:

fn wrap<T>(value: T) -> T {
    struct Box<T> {
        value: T,
    }
    let b = Box { value };
    return b.value;
}

test {
    assert wrap("boxed") == "boxed";
}

So do local newtypes: type N<T> = List<T>;.

A function body may also declare enum, variant and flags items, and impl/trait blocks that give a local type methods.

No other item is local. fn, use, interface, global, world, test and resource are module-level only, and one inside a function body is a parse error.

Rationale: WEP: Local Item Definitions.

Global Variables

Global variables are module-level state. Unlike local variables (let), they live as long as the module.

// Immutable global
global PI: f64 = 3.14159;

// Mutable global
global mut counter: i32 = 0;

// With visibility
pub global VERSION: i32 = 1;

test {
    counter += 1;
    assert PI > 3.0 && counter == 1 && VERSION == 1;
}

A global may have any type. Its initializer declares no with clause and has nowhere to declare one. So calling a function that declares an effect is an error, as is dispatching an operation, except for the effects a #[benign(E, ...)] on the global lists.

An initializer runs outside every handler, those installed where the global is first read included. Unless it installs a handler of its own, an operation it performs runs as it would with no handler installed: the host answers a host-backed interface (Handlers). The handler it installs has a body that holds the effect it handles as a function body's would. Installing it demands what the handler performs (Installing a Handler), so only a handler that performs nothing can be installed here.

Whether an initializer runs, and when, is unspecified, except that it runs at most once and has run before the global is read. An initializer that traps may trap the program at start, at the first read, or never, if nothing reads the global. A #[benign] global is no exception.

Mutability

Globals follow Variable Mutability: without mut a global keeps what its initializer gave it for the whole program.

global CONSTANT: i32 = 42;
global TABLE: List<i32> = [1, 2, 3];
global mut variable: i32 = 0;

fn example() {
    variable = 10;    // OK: mutable global
}

test {
    example();
    assert variable == 10 && CONSTANT == 42 && TABLE.len() == 3;
}

Not yet implemented: the effect system will track access to globals, so example will have to declare it. Until then, a function reads and writes a global without declaring an effect.

Assigning an immutable global, or calling a &mut self method on one, is an error:

fn example() {
    CONSTANT = 10;    // Error: cannot assign to immutable global
}
fn example() {
    TABLE.push(4);    // Error: `push` takes `&mut self`
}

Initialization Order

Initializers run in dependency order, so one may read another global whatever the declaration order. This holds across modules, and whether the initializer names the global or reaches it through a call. A cycle among them is an error. The order between two initializers that do not depend on each other is unspecified.

Operators

Precedence

From the tightest binding to the loosest:

Operators Kind Associativity
., ::, (), [], ? Postfix Left
-, ~, *, &, &mut Prefix unary Right
as Type Type cast Left
*, /, % Multiplicative Left
+, - Additive Left
<<, >> Bitwise shift Left
& Bitwise AND Left
^ Bitwise XOR Left
\| Bitwise OR Left
matches { pattern } Pattern test Left
! Logical NOT Right
==, !=, <, <=, >, >= Comparison Chained
&& Logical AND Left
\|\| Logical OR Left
..<, ..= Range None: a..<b..<c is an error
=, +=, -=, *=, /=, %=, and the rest Assignment Right

The bitwise operators bind tighter than comparison, so flags & MASK == EXPECTED is (flags & MASK) == EXPECTED. A postfix operator binds tighter than a prefix one, so -x? is -(x?) and *p.x is *(p.x). The postfix ? is error propagation.

matches and !

matches binds looser than the arithmetic and bitwise operators. Logical ! binds looser than matches and tighter than comparison, unlike the other prefix operators. So:

Prohibited Operators

Wado has no ++/--: write x += 1 and x -= 1. Neither is a token, so each reads as two operators. --x is -(-x) and a--b is a - (-b). There is no prefix +, so x++ and a++b are parse errors.

It has no ** power operator: call f64::pow(x, y) or f32::pow(x, y).

Overflow and Division by Zero

An arithmetic operator behaves as the Wasm instruction for it does. A width Wasm has no instruction for (i8, i16, u8, u16, i128, u128) behaves as that instruction would at its width. Arithmetic that behaves otherwise, such as checked or saturating arithmetic, is a method rather than an operator.

So integer +, - and * wrap in two's complement at every width, signed and unsigned alike, as does unary - on a signed integer, and their overflow never traps. An integer / or % whose divisor is zero traps. A signed MIN / -1 traps, as div_s does, because the quotient is one past MAX. A signed MIN % -1 is 0, as rem_s gives. A float operation follows IEEE 754: dividing by zero gives an infinity, or NaN for 0.0 / 0.0.

test "integer arithmetic wraps" {
    let max: i8 = 127;
    assert max + 1 == -128;
    let zero: u8 = 0;
    assert zero - 1 == 255;
    assert i32::MAX * 2 == -2;
    assert -i64::MIN == i64::MIN;
}

#[expect_trap]
test "integer division by zero traps" {
    let divisor = 0;
    let _ = 10 / divisor;
}

#[expect_trap]
test "signed MIN / -1 traps" {
    let min: i8 = -128;
    let _ = min / -1;
}

test "signed MIN % -1 is 0" {
    let min: i8 = -128;
    assert min % -1 == 0;
}

Type Cast (as)

The as operator converts between primitive types. It also reinterprets a value across a newtype boundary, between any two types sharing an ultimate base. It converts a flags value to and from u32, and coerces a collection literal to its target type (see Collection Literal Coercion). A named struct literal or a range literal takes its type arguments from the target, or from the target's base where that is a newtype, as it would from an annotation: Pair { a: 1, b: 2 } as Wide, where type Wide = Pair<u64>, builds a Pair<u64>. A diverging operand (!) casts to any type. References and function types follow Casts.

Between primitives, as follows Rust's as. A cast between two numbers is a numeric cast, and bool and char have rules of their own (bool Casts, char Casts). An enum or a variant casts to no number and from none. ReflectEnum::<T>::discriminant(&v) reads a case's tag, and ReflectEnum::<T>::from_discriminant(tag) finds the case with a tag (see Static Reflection).

By its precedence, -x as u32 is (-x) as u32, and a / b as f64 is a / (b as f64).

let i = 42;
let f = i as f64;           // i32 to f64
let truncated = 3.14 as i32; // f64 to i32 (truncates to 3)
let clamped = 300.0 as u8;   // saturates to 255
assert f == 42.0 && truncated == 3 && clamped == 255;

// Chained casts
let x = 10 as f64 as i32 as f64;
assert x == 10.0;

// Cast in expressions
let result = (a as f64) + b;
assert result == 1.5;

Numeric Casts

A numeric cast converts between two integer or float types. The integer types include i128 and u128, and the float types include f16 and bf16. The result is stated in these terms, which follow the Rust Reference:

Each pair of types gets one of them:

Source → target Result
integer → integer of equal width transmute
integer → narrower integer truncate
unsigned integer → wider integer zero-extend
signed integer → wider integer sign-extend
float → integer round toward zero, then saturate; NaN becomes 0
integer → float round to nearest
float → float round to nearest; exact where the target holds the value

A float-to-float cast keeps a NaN a NaN. No numeric cast traps.

let n: i32 = -1;
assert n as u32 == u32::MAX;           // transmute
assert 300 as u16 as u8 == 44;         // truncate
assert (200 as u8) as i32 == 200;      // zero-extend
assert n as i64 == -1;                 // sign-extend
assert -3.9 as i32 == -3;              // round toward zero
assert 1.0e10 as i32 == i32::MAX;      // saturate
assert f64::NAN as u8 == 0;
assert 16_777_217 as f32 == 16_777_216.0;   // round to nearest
let big: f64 = 1.0e300;
assert big as f32 == f32::INFINITY;    // round to nearest
assert 0.1 as f16 as f32 == 0.0999755859375;

A cast types a literal operand as an annotation of its target would, wherever the literal can take that type: an integer literal takes any numeric type, and a float literal any float type. The literal must then lie in the target's range (see Compile-time range checking). Rust gives an integer literal no type from a float target and types it i32, so 3_000_000_000 as f64 is an error there. Wado types it by every numeric target alike, since Rust's exception for float targets is the inconsistent rule. A float literal cast to an integer type stays a float and converts as a float value does. To reinterpret a bit pattern, cast a value that already has the unsigned type:

assert (0xFF as u8) as i8 == -1;
let a = 0xFF as i8;   // compile error: literal out of range for `i8`: 0xFF
let b = 300 as u8;    // compile error: literal out of range for `u8`: 300
let c = -1 as u32;    // compile error: literal out of range for `u32`: -1

bool Casts

A bool casts to any integer type: false is 0 and true is 1. It casts to no float, and nothing casts to bool:

assert true as i32 == 1 && false as u8 == 0;
let x: i32 = 1;
let a = x as bool;     // compile error
let b = 1.0 as bool;   // compile error
let c = true as f64;   // compile error

char Casts

A char casts to any integer type, which yields its Unicode scalar value. A type too narrow for the value truncates it:

let c = 'A';
let code = c as i32;
let ucode = c as u32;
let byte = c as u8;     // truncated to low byte
assert code == 65 && ucode == 65 && byte == 65;

u8 as char is allowed, because every u8 value is a Unicode scalar value. An integer literal cast to char is typed u8:

let b: u8 = 65;
assert b as char == 'A';
assert 97 as char == 'a';

Every other integer-to-char cast is an error, because the source type holds values that are not scalar values: a surrogate (0xD800..=0xDFFF), a value past 0x10FFFF, or a negative number:

let x: i32 = 65;
let c = x as char;  // compile error

let y: i8 = 65;
let d = y as char;  // compile error (i8 can be negative)

The checked conversions take their place. They answer None for a value that is not a scalar value. core:prelude has the full char API.

let from_u32 = char::from_u32(65 as u32);
let from_i32 = char::from_i32(65);
assert from_u32 == Option::Some('A') && from_i32 == Option::Some('A');

A char casts to no type but an integer:

let c = 'A';
let f = c as f64;     // compile error: char can only be cast to integer types
let s = c as String;  // compile error: the two types share no representation

Casts in Generic Code

A cast with a type parameter or a projection on either side is judged by the bounds alone, as Rust's is, since every type that settles it later must take the same cast. A ReflectNewtype<Base = B> bound makes the cast a newtype step to or from B, and a cast it does not make one is an error:

fn to_base<N: ReflectNewtype<Base = B>, B>(n: N) -> B {
    return n as B;
}

test {
    let m: Meters = 2.5;
    assert to_base(m) == 2.5;
}
fn to_f32<T>(x: T) -> f32 {
    return x as f32;
}

Parentheses for Grouping

Parentheses () can be used to override operator precedence:

let a = 2 + 3 * 4;      // multiplication first
let b = (2 + 3) * 4;    // addition first due to parentheses
assert a == 14 && b == 20;

let c = 3 | 4 & 6;      // & has higher precedence than |
let d = (3 | 4) & 6;    // | first due to parentheses
assert c == 7 && d == 6;

Comparison Chaining

Comparisons chain as they do in mathematics. The syntax is Python's, but the evaluation is not:

let [a, b, c, x] = [1, 2, 3, 50];
assert a < b < c;               // (a < b) & (b < c)
assert c >= b >= a;             // (c >= b) & (b >= a)
assert a + 1 == b == c - 1;     // (a + 1 == b) & (b == c - 1)
assert 0 <= x <= 100;           // a range check

A chain uses operators from one group only: ascending (<, <=), descending (>, >=), or equality (==). != never chains. Any other chain is a parse error:

a < b > c       // Error: mixed directions
a == b < c      // Error: mixing == and inequality
a != b != c     // Error: != chaining not allowed

A chain evaluates every operand exactly once, left to right, and then tests them. It does not short-circuit, so a later operand runs even where an earlier comparison already decided the answer. Write && where an operand must not run on that path.

Rationale: WEP: Operator Precedence.

The self_comparison Lint

The self_comparison lint warns about a comparison whose two operands are the same expression, where evaluating that expression twice cannot answer differently. It performs no effect and writes nothing: it assigns nothing, takes no &mut, and calls only declared functions, and methods that do not take &mut self. No call in it is handed a value reaching a &mut: one the value is or holds at any depth, or one a function, resource or signal it holds may capture. Such a comparison has one answer on every type, because the laws of Eq and Ord make both reflexive: x == x, x <= x and x >= x are true, and x != x, x < x and x > x are false. In a chain, each adjacent pair is one comparison. A #line is the line it is written on, so two on different lines are not the same expression.

On a float the warning adds that a NaN is tested with is_nan(), since x != x is the NaN test other languages teach.

Mark the item #[allow(self_comparison)], or the module #![allow(self_comparison)], where the comparison is deliberate.

Rationale: WEP: One Order per Type.

Ranges

A range operator builds a range value from two bounds:

let a: RangeExclusive<i32> = 0..<10;     // 0 up to, but not including, 10
let b: RangeInclusive<i32> = 1..=10;     // 1 through 10
let c: RangeInclusive<char> = 'a'..='z';
let d: RangeExclusive<f64> = 0.0..<1.0;
assert !a.contains(&10) && b.contains(&10) && c.contains(&'q') && d.contains(&0.5);

RangeExclusive<T> and RangeInclusive<T> are prelude structs with public start and end fields. Every range has both bounds: there is no a..<, ..<b or bare .. range.

Both bounds must have the same type after literal coercion, and that type is T. T must implement Ord, so a range over any other type is an error.

An expected range of the same kind over a numeric T types both bounds as that T would type them alone. A let annotation supplies it, and so do a parameter type and a generic parameter the call's other arguments settle. So let r: RangeExclusive<u64> = 0..<0x1_0000_0000; types both bounds as u64, and so does 0..<(1 << 40). A range names its own type, as a named struct literal does, so an expected newtype over a range supplies nothing; a cast to one does (see Type Cast).

A range whose bounds are both literals must not run backwards. An integer, float or char literal counts as one, negated or cast too:

let a = 10..<5;           // Error: reversed range
let b = (10 as u8)..=5;   // Error: reversed range
let c = 5..<5;            // OK: empty
assert c.is_empty();

Bounds known only at run time are not checked. A reversed range is then empty.

A range written as a pattern is a range pattern, with rules of its own. A range of i32 used as an index gives a Slice<T> (The Sequence Family).

Range Methods

Range Iteration

Where T implements the prelude trait Step, as every integer type and char does, a range is itself an iterator over T. It serves for-of and every Iterator method:

let mut seen: List<i32> = [];
for let i of 0..<3 { seen.push(i); }
assert seen == [0, 1, 2];
let sum = (1..=100).fold(0, |acc, x| acc + x);
assert sum == 5050;
let steps = (0..<10).step_by(3).collect();
assert steps == [0, 3, 6, 9];

Iteration never steps past T's maximum. 0 as u8..=255 yields all 256 values and stops. A float range does not iterate, but contains still works on it.

Rationale: WEP: Range Object.