Arrays
An array is an ordered, length-bearing sequence of values of a uniform element type. Every array in Cloth is a slice — a two-word value carrying a pointer to the data and an i64 length. The same shape covers fixed-size globals, locals constructed from literals, heap allocations via new T[n], and slices returned from functions.
Type syntax
The array type is written with trailing brackets on the element type:
int[] // array of i32
string[] // array of strings
Foo[] // array of Foo referencesArray types nest: int[][] is an array of arrays of int.
Construction
Array literal
The bracket form [a, b, c] produces a fresh array. The element type is inferred from the first typed element and the result has type T[] where T is that element type:
let nums = [10, 20, 30]; // i8[] (smallest-fit per element)
let names = ["Mark", "Mary"]; // string[]The backing buffer is heap-allocated and zero-initialized by calloc(n, sizeof(T)). The slice value (a { ptr, i64 } pair) holds the data pointer and the length.
Literals nest. An array literal whose elements are themselves array literals constructs a nested array of the appropriate depth:
string[][] arrays = [["a", "b"], ["c", "d"]];
p(arrays[0][1]); // "b"
p(arrays[1][0]); // "c"Each inner literal allocates its own row (one calloc per row, same shape as new T[a][b]).
Empty literal with declared type
When the declared type is array-shaped, an empty [] literal adopts that element type:
string[] empty = []; // length 0, element type string
let len = empty::LENGTH; // 0Without a declared type, let x = []; is rejected — the element type cannot be inferred from no elements.
Element widening from a declared type
When the declared type is T[] and the initializer is an array literal, each element is widened to T if the source value lossless-promotes to it. The literal does not need to be already typed as T[]:
i32[] wide = [100, 200, 300]; // i8 literals widen to i32
f64[] flt = [1, 2, 3]; // int literals widen to f64This applies only when the destination type is statically known (variable declarations, parameters, returns). A bare let nums = [1, 2, 3] still picks the smallest-fit element type for each literal.
Heap allocation via new T[n]
For runtime-sized arrays, use the new syntax with a bracket expression giving the length:
let dynSize = 5;
let zeros = new i32[dynSize]; // i32[] of length 5, every slot is 0
let buffer = new string[100]; // string[] of length 100, slots are nullThe size expression may be any integer expression that lossless-promotes to i64. Allocation is via calloc, which zero-fills every slot — primitives get 0, references get null.
Multi-dimensional allocation: new T[a][b]
Trailing [size] brackets after the first allocate a nested array. new T[a][b] produces a T[][] of length a where each row is a freshly-allocated T[] of length b:
let grid = new i32[3][4];
p(grid::LENGTH); // 3 — outer rows
p(grid[0]::LENGTH); // 4 — inner cols, each row independently allocated
p(grid[0][0]); // 0 — every slot zero-initializedThe nesting is right-extensible: new T[a][b][c] is a T[][][]. Every level uses calloc, so all slots zero-initialize at every depth. Each row is its own heap allocation — there is no rectangular-block optimization; rows can be freed or reassigned independently.
Indexing
arr[i] reads the i-th element. The index expression must be an integer; it sign-extends to i64 before the address calculation. The result type is the array’s element type:
let nums = [10, 20, 30];
let first = nums[0]; // 10 (i8)
let last = nums[nums::LENGTH - 1];Every index access is bounds-checked at runtime. If i < 0 or i >= arr::LENGTH, the program prints a diagnostic and aborts:
cloth: array index 5 out of bounds [0, 3)The check is three instructions in the hot path (two icmps plus a branch); modern CPUs branch-predict the in-bounds case effectively. For-in iteration is bounds-safe by construction — the loop’s i < length condition can never produce an out-of-range index — so the bounds check only adds cost to explicit arr[i] reads.
Slot assignment
arr[i] = v writes the value v into the i-th slot. Compound forms (+=, -=, *=, …) also work and load-modify-store through the same slot. The same bounds check fires on every write:
let buf = new i32[3];
buf[0] = 10;
buf[1] = 20;
buf[2] = 30;
buf[2] += 5; // 35The right-hand side is coerced to the array’s element type using the usual lossless-promotion rules — a i8 literal stored into an i32[] slot widens automatically.
Overwriting class-owned slots: when an array of class references holds an owned value and you overwrite the slot, the prior reference is not auto-destructed. Free it explicitly first:
let things = new Thing[3];
things[0] = new Thing();
delete things[0]; // free the old occupant
things[0] = new Thing(); // then reassignSub-slicing: arr[lo..hi]
A range index arr[lo..hi] produces a new slice covering elements lo (inclusive) through hi (exclusive). The result aliases the same backing buffer — no copy is made:
let big = [10, 20, 30, 40, 50];
let mid = big[1..4];
p(mid::LENGTH); // 3
p(mid[0]); // 20
p(mid[2]); // 40The bounds are checked once at the slice point: 0 <= lo <= hi <= arr::LENGTH. Any violation aborts with the same diagnostic shape as ordinary indexing.
Ownership: sub-slices do not own their backing buffer. If the parent is freed while a sub-slice is still in use the sub-slice dangles. Until borrow tracking lands you must ensure the parent outlives every sub-slice carved from it. Do not delete a sub-slice — release the parent instead.
Length: arr::LENGTH
Every array exposes its length through the ::LENGTH meta accessor, returning i64:
let nums = [10, 20, 30];
p(nums::LENGTH); // 3
p(new i32[7]::LENGTH); // 7
p(People.values()::LENGTH); // works on any array-typed expression::LENGTH works on any expression whose type is T[] — locals, returns from function calls, results of new T[n], etc.
Iteration: for (T x in arr)
The for-in loop iterates every element in declaration order. The loop binding has the element’s type:
let nums = [10, 20, 30];
for (i8 n in nums) {
println(n);
}
for (People person in People.values()) {
println(person.name());
}break exits the loop; continue skips to the next iteration. The binding is a fresh copy of each element — for class-typed elements this is a pointer copy (the underlying object is shared with the array).
Ownership and delete
Array literals and new T[n] allocations are owned values — Cloth’s ownership tracker treats them like any other heap allocation. Owned values must be released, either by transferring ownership (passing to a T! parameter) or by explicit delete:
let buf = new i32[100];
// ... use buf ...
delete buf; // frees the data bufferFor primitive-element arrays (int[], f64[], bool[], etc.), delete simply releases the data buffer.
For class-element arrays (Foo[] where Foo is a class), delete walks every non-null element, calls that element’s destructor + frees it, then releases the data buffer. This prevents the obvious leak when an owning array drops without cleaning up its references.
let things = new Thing[3];
things[0] = new Thing();
things[1] = new Thing();
// things[2] stays null — skipped by the cleanup walk
delete things; // dtors run for slots 0 and 1, buffer freedSlices returned from auto-generated enum values() are also owned — delete on them frees the pointer buffer; the variant globals themselves are static constants and stay live.
Argument iteration
The Main entry’s string[] args is delivered as a normal Cloth slice: the C-level (argc, argv) pair is wrapped into a { ptr, i64 } value before the constructor runs. Use it like any other array:
public Main(string[] args) {
for (string a in args) {
println(a);
}
}Runtime layout
At the LLVM level, T[] lowers to a struct value { ptr data, i64 length }. The data buffer is laid out as a contiguous [n x T] allocation. Indexing computes the element address through a typed getelementptr; the length lives in the slice value alongside the pointer.
This shape has a few useful consequences:
- Passing an array to a function moves two words (data + length); no separate length parameter is needed.
arr::LENGTHis anextractvaluefrom the slice — no memory load.- Local arrays sit in a stack
allocaof size 16 bytes; the data they point to is on the heap.
Limitations
A few things are still deliberately out:
- No element-type widening between array values. A value of type
i8[]cannot be assigned to ani32[]variable directly — widening happens only at literal-construction time (against a declared destination type), not as a value-level conversion between two already-built arrays. Converting between element types still requires reallocating and copying. - No built-in slice helpers (
arr.concat(other),arr.map(f),arr.filter(p), …). These belong in the standard library once arrays stabilize, not the core language. - No lifetime tracking for sub-slices. Sub-slices alias their parent’s backing buffer and the compiler does not yet stop you from freeing the parent first. A future borrow-tracker pass will close this gap.