Arrays and tuples
In TVM, a tuple is a dynamic container that stores from 0 to 255 elements in a single stack slot. Tolk provides several types built on top of TVM tuples:
array<T>— a dynamically sized array of elements of typeT.[T1, T2, ...]— a shaped tuple with a fixed number of elements of known types.tuple— an alias forarray<unknown>, a legacy name for an untyped dynamic container.
In many general-purpose languages, syntax [A, B, C, ...] is used for array or list literals, while syntax (A, B, C, ...) is used for tuples. However, in TON, there are no traditional arrays or lists. The [A, B, C, ...] syntax creates arrays or shaped tuples, while (A, B, C, ...) is used by tensors: a distinct type that represents ordered collections of values that occupy multiple TVM stack entries.
For example, array<int> is a single stack entry containing multiple integers. In contrast, a tensor (int, int, int) signifies three separate integers that occupy individual stack entries or are serialized sequentially.
Arrays
array<T> is a dynamically sized container that holds elements of type T:
array<int>
array<int?>
array<Point>
array<int | slice>
array<array<bool>>Creating arrays with [...]
Use [...] to create an array:
// array<int>
var numbers = [1, 2, 3];
// array<unknown>
var empty = [];
// array<int?>
var optionals = [1, null, 3];
// array<array<int>>
var matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];To force a certain type T over unknown for empty arrays, specify it manually:
var nums: array<int> = [];
// or
var nums = array<int> [];The syntax array<int> [...] is similar to the syntax of object creation with Point {...}, where a type hint may be omitted if clear from context.
If types within [...] are incompatible, a compilation error is reported:
// invalid:
var arr = [1, "aba"];
// error: type of `[...]` is `array<int | string>`;
// probably, it's not what you expected
// valid:
var arr: array<unknown> = [1, "aba"];
// or
var arr: array<int | string> = [1, "aba"];Array methods
Arrays have several commonly-named methods. An IDE suggests them after a dot:
var nums = [] as array<int>;
nums.push(10);
nums.push(20);
nums.push(30);
nums.get(1); // 20
nums.first(); // 10
nums.pop(); // 30
nums.size(); // 2 (pop removed the last element)
[1, 2, 3].last(); // 3Additional methods such as array.set and others are available in the standard library.
T can be any type
An array supports any element type, including structures and unions:
struct Point {
x: int
y: int
}
fun getArr(): array<Point> {
return [
{ x: 10, y: 20 },
{ x: 50, y: 60 },
];
}The compiler automatically packs complex items into sub-tuples. At the TVM level:
var arr = getArr();
// stack: [ [ 10 20 ] [ 50 60 ] ]
arr.get(0);
// stack: 10 20 (automatically un-tupled)
arr.get(0).y;
// stack: 20The limit of 255 elements does not change regardless of T.
Internally backed by TVM tuples
An array can contain from 0 to 255 elements. It occupies one stack slot regardless of its inner size. Accessing the first 16 elements consumes less gas than greater indices, because for i >= 16 an additional instruction is required.
Arrays are assignable to each other
An array<T1> can be assigned to array<T2> when T1 is assignable to T2. The compiler handles all runtime transitions if required:
fun demo(in: array<int>) {
// ok, no runtime transitions needed
var a1: array<int?> = in;
// ok, but with runtime transitions
var a2: array<int | slice> = in;
}As a result, any array<T> can be assigned to array<unknown> directly.
The unknown type
unknown represents one TVM primitive with contents unknown at compile time. Any type T can be cast to unknown and back using the as operator. If T is a primitive itself, this is a type-only cast. Otherwise, the object is packed into a sub-tuple and stored as a single slot:
var u1 = 5 as unknown;
// stack: 5
u1 as int;
// stack: 5
var u2 = (10, 20) as unknown;
// stack: [ 10 20 ] (a TVM tuple)
u2 as (int, int);
// stack: 10 20 (two integers)Storing an element inside array<T> is effectively converting T to unknown followed by writing that slot into a TVM tuple.
The tuple type
tuple is an alias for array<unknown>:
// declared in stdlib
type tuple = array<unknown>Because of that, tuple has all the array methods: push, size, and others. Such arrays are opaque, so the push method accepts values of any type:
var t = []; // array<unknown>
t.push(1);
t.push(null);
t.push(Point { x: 10, y: 20 });
// stack: [ 1 null [ 10 20 ] ]Getter methods like t.get() and t.first() return unknown. To perform meaningful operations, cast the result to a known type:
var one = t.first(); // unknown
one + 123; // error, can not apply operator `+`
// cast at reading
var one = t.first() as int;
one + 123; // ok, 124Shaped tuples
Besides array<T> with a dynamic size, Tolk has fixed-size containers with a known shape, called shaped tuples: [T1, T2, ...].
var intAndStr: [int, string] = [1, "aba"];
// or
var intAndStr = [1, "aba"] as [int, string];Shaped tuples differ from arrays:
- They do not have methods like
push,get, orpop. - They support indexed access:
value.0,value.1, etc. - Each element can have a different type.
var pair: [int32, Point] = [1, { x: 10, y: 20 }];
// stack: [ 1 [ 10 20 ] ]
var point = pair.1;
// stack: 10 20A shaped tuple may be destructured into multiple variables:
fun sumFirstTwo(t: [int, int, builder]) {
val [first, second, _] = t;
return first + second;
}The [...] constructor
The literal [...] is a universal constructor that creates different types depending on context. By default, it creates an array. If the target type is known, it creates that type:
// no hint — infer `array<int>`
var arr = [1, 2, 3];
// explicit array types
var tuple: array<unknown> = [1, 2, 3];
var optionals: array<int?> = [1, 2, 3];
// shaped tuple
var shape: [int, int, int?] = [1, 2, 3];
// lisp list
var list: lisp_list<int> = [1, 2, 3];
// empty map
var m: map<int32, address> = [];The behavior is identical to creating an object with { ... }: a type hint may exist on the variable, or the Type [...] syntax may be used:
// analogy between {...} and [...]
var p: Point = { ... };
var a: array<int> = [ ... ];
// the same — but on the right
var p = Point { ... };
var a = array<int> [ ... ];This also works in function parameters, struct fields, and assignments.
Lisp-style lists
lisp_list<T> is a set of nested two-element TVM tuples. For instance, [1, [2, [3, null]]] represents the list [1, 2, 3].
Unlike array<T>, a lisp list can store more than 255 elements, because TVM tuples are not limited in depth. This is typically the only reason to use them: when output from a get method may grow unpredictably.
To use lists, import the @stdlib/lisp-lists file:
import "@stdlib/lisp-lists"Lisp lists allow accessing only the front element (head). There is no array.get(i) or cheap array.size:
var list = lisp_list<int> [];
list.prependHead(1);
list.prependHead(2);
list.prependHead(3);
// list is now "3 2 1"
// stack: [3 [2 [1 null]]]
var front = list.popHead(); // 3
// list is now "2 1"Similarly, T can be any type: complex types like Point are represented as a single slot with an intermediate conversion to unknown.
Several helper methods are available in the standard library, such as array.calculateSize and array.calculateConcatenation. These have O(N) complexity: the longer the list, the higher the gas consumption.
val depth = list.calculateSize();If there is a clear upper bound on the number of elements, prefer arrays or maps. Otherwise, consider contract sharding.
Conversion between arrays and composites
For composite types, generic built-in methods T.toTuple() and T.fromTuple() convert composites to and from tuples. The length of the resulting tuple equals the number of stack slots occupied by the converted type:
struct Point3d {
x: int
y: int
z: int
}
fun demo(p: Point3d) {
val t = p.toTuple(); // a tuple with 3 elements
t.get(2) as int; // z
p = Point3d.fromTuple(t); // back to a struct
}Stack layout and serialization
array<T>andtupleoccupy a single stack slot: a TVMTUPLE.- Shaped tuples
[T1, T2, ...]also occupy a single TVMTUPLEslot. unknownoccupies a single stack slot.
Arrays can be serialized to cells when T is serializable. The binary format uses snake references: a uint8 length followed by chained cell references containing the elements.
Raw tuples are not serializable to cells, because unknown is unserializable. But they can be returned from get methods, since contract getters operate directly on the stack.
Last updated on