8.7. Arrays
Arrays are fixed-size collections, where each element of the array has the same
type. An array element may be of any storable type: a primitive type (boolean,
integer, real, character), or an
aggregate type
such as a struct, tuple, vector, string, or another
array (which yields a higher-rank array; see Matrices).
8.7.1. Sizing
Arrays are initialization-time sized. The extents of each rank are determined once at initialization, and from that point on are fixed for the entire lifetime of the variable. Concretely:
A declaration such as
integer[n] v;evaluatesnonce, at initialization. Later changes tonhave no effect on the length ofv.A declaration such as
integer[*] v = <expr>;takes its length from the value of<expr>at initialization. The*declares the extent of a dimension ‘inferred’ from the RHS expression.No subsequent operation can change the length of an array variable. Assignment, concatenation, and casting all produce array values; storing such a value into an array variable never resizes that variable. If the value’s length does not match, it is padded with the element type’s zero value when the value is too short, or the compiler must emit a
SizeError(see Errors) at compile time or run time when it is too long, as described below.
If you need a collection whose length changes as the program runs, use a vector, which is runtime-sized. See Arrays Versus Vectors.
8.7.2. Declaration
An array is declared like any other variable, with the extent of the array ranks enclosed using square brackets immediately after the element type.
If possible, initialization expressions may go through an implicit cast. For
instance, when declaring a real array that is initialized with an integer value
the integer is implicitly cast to a real value, and then used as a scalar
initialization of the array. Be careful about type inference! If the type of
the array is being inferred from the right hand side, the previous example
would create an integer array instead of a real array.
Explicit Size Declarations
When an array is declared it may be explicitly given a size. Every array, whether explicitly or implicitly sized, has a size that is fixed on the first execution of its declaration at run time and is immutable thereafter; a collection whose size can grow requires a vector.
[<qualifier>] <type>[<int-expr>]([<int-expr>])* <identifier>; [<qualifier>] <type>[<int-expr>]([<int-expr>])* <identifier> = <type-expr>; [<qualifier>] <type>[<int-expr>]([<int-expr>])* <identifier> = <type-array>;
The extents of the array are given by the integer expression between the square brackets.
If the array is given a scalar value (
type-expr) of the same element type then the scalar value is duplicated for every single element of the array.An array may also be initialized with another array. Initialization occurs element-wise, with the RHS element type’s initialization semantics applying from left to right. If the LHS array is initialized using a RHS array that is too small then the LHS array is padded with the element type’s zero value. However, if the LHS array is initialized with a RHS array that is too large then the compiler must emit a
SizeError(see Errors) at compile time or run time.Inferred Size Declarations
If an array is assigned an initial value when it is declared, then its size may be inferred. There is no need to repeat the size in the declaration because the size of the array on the right-hand side is known.
<type>[*] <identifier> = <type-array>;
Inferred Type and Size
It is also possible to declare an array with an implied type and length using the var or const keyword. This type of declaration can only be used when the variable is initialized in the declaration, otherwise the compiler will not be able to infer the type or the size of the array.
integer[*] v = [1, 2, 3]; var w = v + 1;
In this example the compiler can infer both the size and the type of
wfromv. As with any array, this inferred size is fixed once, at initialization, and never changes afterwards in contrast to a vector, whose size may change at runtime.
8.7.3. Construction
An array value in Gazprea may be constructed using the following notation:
[expr1, expr2, ..., exprN]
Each expK is an expression with a compatible type. In the simplest
cases each expression is of the same type, but it is possible to mix the
types as long as all of the types can be implicitly cast to a common type. For
instance it is possible to mix integers and real numbers.
real[*] v = [1, 3.3, 5 * 3.4];
It is also possible to construct a single-element array using this method of construction.
real[*] v = [7];
Gazprea DOES support empty arrays.
real[*] v = []; /* Should create an empty array */
Because the length of an array is fixed at initialization, such an array has a length of zero permanently.
Note that the empty array literal [] carries no element type of its own, so
the element type must come from context (the declared type, as in
real[*] v = [] above). A declaration that elides the type and asks the
compiler to infer it from an empty literal (ex. var v = [];) is
ill-formed, because the element type cannot be deduced; the compiler
must emit a TypeError (see Errors). The same holds anywhere a
bare [] appears without a type to fix its element type (see also
Array to Array and Domain Expressions).
8.7.4. Arrays Versus Vectors
Gazprea has two collection types that share the same element-wise operations but differ in their policies for length modification:
Array ( |
Vector ( |
|
|---|---|---|
When is the length set? |
Once, at initialization |
Variable, no fixed length |
Can it change afterwards? |
No |
Yes |
Written in the type? |
Yes ( |
No |
Growth mechanism |
None |
|
Too-short value stored into it |
Padded with the element type’s zero value |
The vector takes the value’s length |
Too-long value stored into it |
|
The vector takes the value’s length |
The two types interoperate, but only through values: a vector used in an array context yields an array value of the vector’s current length, and an array value stored into a vector sets that vector’s length. Neither direction ever makes an array variable resizable. See Vectors for the details of that interoperation.
One possible future extension merges arrays and vectors into a single type, however this is left to future editions of gazprea.
8.7.5. Operations
Array Operations and functions
length
The number of elements in an array is given by the built-in function
length; see Length for its full definition.Concatenation
Two arrays with the same element type may be concatenated into a single array using the concatenation operator,
||. For instance:[1, 2, 3] || [4, 5] // produces [1, 2, 3, 4, 5] [1, 2] || [] || [3, 4] // produces [1, 2, 3, 4]
Concatenation is also allowed between arrays of different element types, as long as one element type can be implicitly cast to the other. For instance:
integer[3] v = [1, 2, 3]; real[3] u = [4.0, 5.0, 6.0]; real[6] j = v || u;
would be permitted, and the integer array
vwould be implicitly cast to a real array before the concatenation.Concatenation may also be used with scalar values. Every scalar operand is implicitly promoted to a single-element array of its type before the operation, so
||never requires a composite operand and even two scalars may be concatenated directly:1 || [2, 3, 4] // produces [1, 2, 3, 4] [1, 2, 3] || 4 // produces [1, 2, 3, 4] 1 || 2 || 3 // produces [1, 2, 3]
Concatenation is right-associative, and its receiver, defined as the rightmost operand, fixes the kind and element type of the result. When the receiver is a vector (a
vector<T>or astring) the whole concatenation is a vector of that element type; when it is an array, the result is an array of that element type. When the receiver is a scalar it is promoted as above, so the result is a plain array of that scalar’s type.A result of the reciever type rule is that an
integerreceiver (having an integer-typed expression as the right-most expression in the concatenation) yields anintegerarray and, importantly, acharacterreceiver yields acharacterarray, not astring. A concatenation therefore prints as text only when its rightmost operand is already astring:"x = " || format(x)is astring(its receiverformat(x)is a string) and renders as text when sent to a stream, whereas"x = " || 'y'is acharacterarray. The operands must still share a common element type through implicit casts, and a vector result can be stored into an array through the usual vector/array interoperability.Concatenation generalizes to arrays of any rank. Viewing a rank-
karray as the sequence of its rank-(k-1)outer slices – its rows, for a matrix –a || bjoins those two sequences along the outermost axis: both operands must have the same rankk(after the scalar-to-rank-1 promotion above) and identical extents in every axis but the first, and the result has rankkwith its first extent the sum of the two concatenating array extents.[[1, 2], [3, 4]] || [[5, 6]] // produces [[1, 2], [3, 4], [5, 6]] // [2][2] [1][2] [3][2]
A mismatch in those trailing extents is a
SizeError(see Errors); operands whose ranks differ – other than a scalar promoted to rank 1 – are aTypeError, so to append a single rowrto a matrixMyou writeM || [r]rather thanM || r. See Matrices for the rank-general rule.Remember that arrays have a fixed length, which means you cannot grow an array by concatenating elements to the end:
var integer[*] growme = [0]; // length is now 1 var integer i = 1; loop while (i < 10) { growme = growme || i; // illegal: SizeError i = i + 1; }Dot Product
Two rank-1 arrays with the same size and a numeric element type (types with the
+and*operators) may be used in a dot product operation using the**operator. The two operands must have the same size; if they do not, the compiler must emit aSizeError(see Errors) at compile time or run time. The dot product is the rank-1 case of a single rule:**is defined for numeric arrays of any rank as the linear-algebra contraction of the last dimension of the left operand with the first dimension of the right operand, so the rank-2 case is matrix multiplication. See Matrices for the general definition and itsSizeError. For instance:integer[3] v = [1, 2, 3]; integer[3] u = [4, 5, 6]; /* v[1] * u[1] + v[2] * u[2] + v[3] * u[3] */ /* 1 * 4 + 2 * 5 + 3 * 6 = 32 */ integer dot = v ** u; /* Perform a dot product */
A scalar operand broadcasts to the other operand’s length here, just as it does for element-wise array operations: because a rank-1 array has a single dimension, the broadcast shape is unambiguous. Thus a scalar may be dotted with a rank-1 array, ex.
[1, 2, 3] ** 4is the dot product[1, 2, 3] ** [4, 4, 4], i.e.1*4 + 2*4 + 3*4 == 24.Range
The
..operator creates an integer array holding the specified range of integer values. This operator must have an expression resulting in an integer on both sides of it. The range is inclusive of both bounds: the left bound and the right bound are both included, soi..jholds the integersi, i+1, ..., jand has lengthmax(0, j - i + 1). This is the same convention used when a range is written inside an index position to form a slice.For example:
1..10 -> std_output; (10-8)..(9+2) -> std_output;
prints the following:
[1 2 3 4 5 6 7 8 9 10] [2 3 4 5 6 7 8 9 10 11]
The number of integers in a range may not be known at compile time when the integer expressions use variables. In another example, assuming at run time that
iis computed as -4:i..5 -> std_output;
prints the following:
[-4 -3 -2 -1 0 1 2 3 4 5]
Therefore, it is valid to have bounds that will produce an empty array: because both bounds are inclusive,
i..jis empty whenj < i(for example5..2or6..5). A single-point range such as5..5is not empty, rather it is the one-element array[5]and a reversed range such as10..1is empty rather than descending.Indexing
An array may be indexed in order to retrieve the values stored in the array. An array may be indexed using an integer, in which case the index yields a single element, or using range syntax written directly at the index position, in which case the index yields a slice (see Array Slices). An array value is not a legal index:
v[w]is illegal wheneverwevaluates to an array value.Attempting to index an array with any array-typed variable or expression outside of the range syntax described later for slices is ill-formed, and the compiler must emit a
TypeError(see Errors).A range written directly inside an index position is not an array-valued index; it forms a slice. Gazprea is 1-indexed, so the first element of an array is at index 1 (as opposed to index 0 in languages like C). For instance:
integer[3] v = [4, 5, 6]; integer x = v[2]; /* x == 5 */ integer y = [4,5,6][3]; /* y == 6 */
Like Python, Gazprea allows negative indices, which are interpreted as starting from the back of the array instead of the front:
integer[3] v = [4, 5, 6]; integer x = v[-2]; /* x == 5 */ integer y = [4,5,6][-1]; /* y == 6 */
A negative index
-krefers to elementn + 1 - k, so-1is the last element and-nthe first. An index is in bounds when it lies in1..nor in-n..-1;0, or any index greater thannin either direction, is out of bounds, and the compiler must emit anIndexError(see Errors) at compile time or run time.Slices
A slice is a contiguous subset of array elements. Slice bounds and shorthand forms are specified in Array Slices.
Operations of the Element Type
Unary operations that are valid for the element type of an array may be applied to the array in order to produce an array whose result is the equivalent to applying that unary operation to each element of the array. For instance:
boolean[*] v = [true, false, true, true]; boolean[*] nv = not v;
nvwould have a value of[not true, not false, not true, not true] = [false, true, false, false].Similarly, every binary operation that is valid for the element type of an array may also be applied to two arrays. When applied to two arrays of the same size, the result of the binary operation is an array formed by the element-wise application of the binary operation to the array operands. The sole exceptions are the equality operators
==and!=, which collapse to a singlebooleanrather than abooleanarray (see below); the ordering comparisons<,>,<=,>=are not exceptions, ratherthey apply element-wise and yield abooleanarray.[1, 2, 3, 4] + [2, 2, 2, 2] // results in [3, 4, 5, 6]
The compiler must emit a
SizeError(see Errors) when a binary operation is performed between two arrays of different sizes, at compile time or run time.When one of the operands of a binary operation is an array and the other operand is a scalar, the scalar value must first be implicitly cast to an array of the same size as the array operand and with the value of each element equal to the scalar value. For example:
[1, 2, 3, 4] + 2 // results in [3, 4, 5, 6]
Additionally the element types of arrays may be implicitly cast, for instance in this case the integer array must be implicitly cast to a real array in order to perform the operation:
[1, 2, 3, 4] + 2.3 // results in [3.3, 4.3, 5.3, 6.3]
Note that this behaviour generalizes to arbitrary-rank arrays. A scalar under any of the binary operators excepting the
**dot-product/matrix multiplication operator can be implicitly broadcast to an array of equivalent extent to perform element-wise binary or logical operations with another array.The equality operation is the exception to the behavior of the binary operations. Instead of producing a boolean array, an equality operation checks whether or not all of the elements of two arrays are equal, and return a single boolean value reflecting the result of this comparison.
[1, 2, 3] == [1, 2, 3]
yields
true[1, 1, 3] == [1, 2, 3]
yields
falseThe
!=operation also produces a boolean instead of a boolean array. The result is the logical negation of the result of the==operator.Only
==and!=collapse to a single boolean in this way. The ordering comparisons<,>,<=, and>=follow the ordinary element-wise rule: applied between two arrays of the same size they produce abooleanarray (a bitmask) of that size, whose elementkis the comparison of the two operands’ elementk. As with any element-wise binary operation, a size mismatch is aSizeError(see Errors) and a scalar operand is first broadcast to the array’s size. For example:[1, 5, 3] < [2, 2, 2] // results in [true, false, false] [1, 2, 3] <= 2 // results in [true, true, false]
Operator precedence and associativity are specified once, for all types, in the table of operator precedence.
8.7.6. Array Slices
An array slice is a contiguous subset of elements, described by a range.
Both bounds are inclusive:
a[i..j] selects the elements from i through j, including both,
identical to a range expression. A slice
always selects a contiguous run of elements.
The following forms are accepted inside an index position, where n is
the length of the array being sliced and elements are 1-indexed. A negative
right bound -i counts i positions back from the end and is likewise
inclusive, resolving to position n + 1 - i, so ..-1 selects everything
through the final element and ..-2 stops at the second-to-last:
Form |
Elements selected |
|---|---|
|
all elements, |
|
|
|
|
|
|
|
|
A slice is an ordinary array value except when a slice is the target on the left of an assignment, where it writes through to the array it slices:
In value position (an rvalue) (as an initializer, on the right of an assignment, as an argument, in a larger expression, or anywhere an array value is expected) a slice behaves equivalently to a fresh, independent array holding the selected elements, exactly as an array literal would. It is in no sense a view: binding it to a variable creates a new array, and the copy and the original never observe each other’s later writes. Because the elements behave as though copied, the source array need not be mutable and the copy’s own mutability is decided by the declaration that receives it:
integer[3] a = [1, 2, 3]; // a is const (the default) var b = a[1..2]; // b is a fresh var integer[2] == [1, 2] (a copy) b[1] = 9; // b == [9, 2]; a is unchanged, still [1, 2, 3]
(
a[1..2]selects indices 1 and 2.)As the target on the left of an assignment (an lvalue) a slice writes through to its backing array. Slicing on the left works exactly like indexing a whole array on the left, such as
a[2] = 5, except that it names a contiguous run of elements rather than a single one. Any writes to the slice are then reflected in the base array. This requires that the array is mutable: the backing array must be declaredvar, and a slice of aconstarray can never be an assignment target. The assigned value is fitted to the slice’s length exactly as for a whole-array assignment: a shorter value is padded with the element type’s zero value and a longer value is aSizeError(see Sizing):var integer[3] a = [1, 2, 3]; a[1..2] = [4, 5]; // writes through: a == [4, 5, 3] a -> std_output; // [4 5 3]
These two rules apply unchanged to arrays of any rank; the higher-rank case is described below and in Matrices.
Slicing shorthand forms are shown below. A slice is itself an array value, so it may be indexed or sliced again – each subscript re-indexes the value the previous one produced (see Matrices):
integer[*] a = [0, 2, 4, 6, 8, 10];
integer[3] x = a[2..4]; /* x == [2, 4, 6]: indices 2, 3, 4 */
integer y = a[2..4][1]; /* y == 2: index into the slice's result */
integer[*] u = a[..4]; /* u == [0, 2, 4, 6] */
integer[*] v = a[4..]; /* v == [6, 8, 10] */
integer[*] w = a[..-2]; /* w == [0, 2, 4, 6, 8]: through the second-to-last */
// A slice of the whole array behaves as the array itself, so slicing may be
// repeated before a final index:
integer z1 = a[4]; /* z1 == 6 */
integer z2 = a[1..6][1..6][1..6][4]; /* z2 == 6 */
The right bound of a slice may be negative: -j counts from the end,
resolving to n + 1 - j (so ..-1 denotes the last element itself, exactly
as a single index -1 does), and this applies equally in the two-sided form
a[i..-j]. The left bound may not be negative. If given a negative
left bound, the compiler must emit
an IndexError (see Errors). After resolving any negative right
bound, both i and j in a[i..j] must lie between 1 and n
inclusive. If the compiler observes an index outside of that range, the compiler
must emit an IndexError, at
compile time or run time, exactly as for a single-element index.
A slice whose (in-bounds) left bound is greater than its right bound, such as
a[4..2], is not an error: like a range
value whose right bound lies below
its left (see Operations), it simply selects no elements and yields
an empty array of a’s element type.
Slicing generalizes to arrays of any rank. Indexing is composite: in a
subscript chain a[s1][s2]...[sk]
each subscript is applied, left to right, to
the value the previous subscripts produced. The [] operator is
left-associative, so the chain groups as (((a[s1])[s2])...)[sk] (see
Matrices). Each subscript indexes the outermost remaining axis of
that value: a single integer selects one element along it and drops that axis,
while a range selects a contiguous run along it and keeps it. An array is thus
peeled from the outside in, exactly as in C, and applying k integer
subscripts to a rank-k array reaches a single element.
// a rank-3 array whose 27 elements are 1, 2, 3, ..., 27 in order
var integer[3][3][3] a = [
[
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
],
[
[10, 11, 12],
[13, 14, 15],
[16, 17, 18]
],
[
[19, 20, 21],
[22, 23, 24],
[25, 26, 27]
]
];
var b = a[1]; // the first plane: an integer[3][3] == [[1,2,3],[4,5,6],[7,8,9]]
var c = a[1][1..2]; // its first two rows: an integer[2][3] == [[1,2,3],[4,5,6]]
var d = a[1][1..2][2]; // the second of those rows: an integer[3] == [4,5,6]
integer e = a[1][2][3]; // the third element of the second row of the first plane == 6
Because each subscript re-indexes the value the previous one produced, indexing
directly into a slice needs no parentheses. In the above example
a[1][1..2][2] slices the
first plane to two rows and then selects the second of them. Once a subscript
chain has reached a single element, applying a further subscript is a
TypeError (see Errors) since there is no axis left to index.
A slice may also be handed to a function or procedure. Because a slice in argument position is an ordinary array value the callee receives a copy of the selected elements and cannot reach the caller’s array through it. Two consequences follow:
A slice may be passed to any
constparameter, and the callee gets a copy; the caller’s array is left untouched. Every function parameter isconst(functions are pure), as may be a procedure parameter, so a slice is always a legalconstargument.A slice may not be passed to a
varparameter. Avarparameter is call by reference and so requires a mutable lvalue; a slice, like an array literal or any other expression, is an rvalue in argument position, and only aconstparameter accepts one. Passing a slice to avarparameter is ill-formed, and the compiler must emit aTypeError(see Errors), similar to passing an array literal in the same position. To have a procedure write into part of an array, pass the whole array to avarparameter and slice on the left of an assignment inside the callee, or assign the call’s result into a slice at the call site.
procedure sum_arrays(const integer[*] in1, const integer[*] in2, var integer[*] out) {
/* sum the two inputs and fill the output with the result */
}
procedure main() returns integer {
integer[6] a = [0, 2, 4, 6, 8, 10]; /* a and b are const */
integer[6] b = [0, 3, 6, 9, 12, 15];
var integer[6] c; /* c must be var */
/* whole arrays bind directly: a and b to the const inputs, c to var out */
call sum_arrays(a, b, c);
c -> std_output; /* [0 5 10 15 20 25] */
/* a[1..3] and b[1..3] are slices in value position, so they are copied
into the const parameters, exactly as array literals would be */
var integer[3] d;
call sum_arrays(a[1..3], b[1..3], d);
d -> std_output; /* [0 5 10] */
/* a slice may NOT bind to the var parameter out: like a literal, a
slice is an rvalue in argument position, so this is a TypeError */
// call sum_arrays(a[1..3], b[1..3], c[4..6]); // TypeError
/* to write through into part of c, put the slice on the LEFT of an
assignment -- the one place a slice reaches its backing array */
c[4..6] = d; /* writes through: c == [0, 5, 10, 0, 5, 10] */
c[3..4] = [415, 429]; /* writes through: c == [0, 5, 415, 429, 5, 10] */
c -> std_output; /* [0 5 415 429 5 10] */
return 0;
}
Here c[4..6] and c[3..4] are slices on the left of an assignment
so each write passes through to
c itself. The slices a[1..3] and b[1..3] are arguments in value
position, so they are copied into the const parameters and leave a and
b untouched, exactly as array literals would be; a slice such as c[4..6]
could not have been passed to the var parameter out at all.
8.7.7. Type Casting and Implicit Casts
To see the types that an array may be cast and/or implicitly cast to, see the sections on Type Casting and Implicit Casts respectively.