Variables
Variables in Zena are declared using let or var. let creates an immutable
binding, while var creates a mutable variable.
let and var ​
let creates an immutable binding. Once initialized, the binding cannot be
reassigned:
let x = 42;
x = 43; // Compile error: Cannot reassign immutable variable 'x'
var creates a mutable variable that can be reassigned with the assignment
operator = or compound assignment operators:
var count = 0;
count = 10;
Local and module variables ​
Zena distinguishes between local variables declared inside functions or blocks and module-level variables declared at the top level of a file.
Local variables ​
Variables declared inside function bodies, blocks ({ ... }), if expressions,
and loops are local variables:
let calculate = (a: i32, b: i32): i32 => {
let sum = a + b;
return sum * 2;
};
Local variables are compiled to Wasm locals, unless there are captured by a closure, in which case they're compiled to a closure context object that lives on the heap.
Module-level variables ​
Variables declared at the top level of a .zena file are module-level
variables:
let maxRetries = 3;
var currentSessionId = 0;
export let defaultTimeout = 5000;
export var activeConnections = 0;
function getMaxRetries(): i32 {
return maxRetries;
}
Module-level variables are compiled to WebAssembly globals and initialized in
declaration order during module startup. Because module-level variables reside
in Wasm globals, top-level function declarations can reference them directly
without creating closures or capturing environments.
Top-level variables can be exported using the export keyword so other modules
can import them.
Potential change
Module-level initializers currently execute during module startup, though future versions of Zena may restrict them to WebAssembly-compatible constant global initializers. See elematic/zena#125.
Scoping and shadowing ​
Variables in Zena are lexically scoped to the block in which they are declared. A variable declared in an inner block can shadow a variable of the same name in an outer block:
let limit = 100;
if (limit > 50) {
let limit = 10; // Shadows the outer 'limit' within this block
console.log(limit); // 10
}
console.log(limit); // 100
Declaring two variables with the same name in the same lexical scope is a compile-time error.
Type annotations and inference ​
Variables can include an explicit type annotation after the identifier:
let count: i32 = 42;
var rate: f64 = 3.5;
let name: String = 'Zena';
When an annotation is omitted, the compiler infers the variable's type from its initializing expression:
let count = 42; // Inferred as i32
let rate = 3.5; // Inferred as f64
let message = 'hi'; // Inferred as String
let isReady = true; // Inferred as true (literal type)
Literal types and type widening ​
Numeric and string literals default to their base types (i32, f64, String)
unless given an explicit literal type annotation (such as let x: 42 = 42;) or
contextual typing from a union.
Boolean literals (true, false) are literal types by default:
- Immutable bindings (
let) preserve boolean literal types:zenalet isEnabled = true; // Type: true - Mutable bindings (
var) widen literal types to their base types (boolean) so the variable can be reassigned:zenavar isEnabled = true; // Type: boolean (widened from true) isEnabled = false; // OK
If a mutable variable requires a specific literal or union type, supply an explicit type annotation:
var mode: 'read' | 'write' = 'read';
mode = 'write'; // OK
mode = 'exec'; // Compile error: Type '"exec"' is not assignable to '"read" | "write"'
Destructuring ​
Variable declarations support destructuring patterns to unpack values from records, tuples, arrays, and classes directly into bindings.
Record destructuring ​
Record fields are extracted by matching field names inside {}:
let user = { id: 1, name: 'Alice', role: 'admin' };
let { id, name } = user;
To bind a field to a different variable name, use the as keyword:
let { id as userId, name as userName } = user;
Tuple destructuring ​
Tuple elements are extracted by position using parentheses ():
let point = (10.0, 20.0);
let (x, y) = point;
// Elements can be skipped with commas:
let triple = (1, 2, 3);
let (first, , third) = triple;
Tuple destructuring is commonly used with multi-value returns from standard library functions:
let scores = {'Alice' => 95, 'Bob' => 87};
let (found, score) = scores.get('Alice');
Array destructuring ​
Arrays (ImmutableArray, FixedArray, GrowableArray, and the Array
interface) are unpacked using bracket syntax []:
let numbers = [10, 20, 30, 40];
let [first, second] = numbers;
let [a, , c] = numbers; // Skipping elements
// Rest patterns bind remaining elements as a FixedArray:
let [head, ...tail] = numbers;
Class destructuring ​
Class instances can be destructured using field patterns:
class Point {
x: f64;
y: f64;
new(this.x, this.y);
}
let p = new Point(1.0, 2.0);
let { x, y } = p;
let { x as px, y as py } = p;
Nested patterns and mutability ​
Destructuring patterns can be nested to unpack complex data structures in a single declaration:
let config = {
server: { host: 'localhost', port: 8080 },
retries: 3,
};
let { server: { host, port }, retries } = config;
Destructuring declarations can also use var to create mutable bindings for each
unpacked variable:
var (currentX, currentY) = (0.0, 0.0);
currentX += 5.0;
Next ​
- Types — primitive types, references, unions, and aliases
- Functions — arrow functions, parameters, and closures
- Control Flow —
if,match, loops, and expression orientation