Jagged Arrays — Design

Issue: #1246, split out of #1186.

Why

Rectangular arrays exist (Int[,]), but rows of different lengths could not be declared, built or passed. Code boxed each row (IntArrayRef[], Vector<IntArrayRef>), which costs an object per row and an unwrap on every access. Ragged data is everyday work in research code: token sequences of different lengths, adjacency lists, triangles, batches.

rows := Int[]->New[3];          # three rows, each Nil until set
rows[0] := [1, 2, 3];
rows[1] := Int->New[5];
rows[2] := [x, y];
total := rows[0][1] + rows[2][0];

function : Sum(rows : Int[][]) ~ Int { ... }
triangle := [[1], [1, 1], [1, 2, 1]];   # ragged, so Int[][]

Decisions

Syntax

FormMeaning
T[][]a one-dimensional array whose elements are T[]
T[][][]an array of T[][], and so on
T[]->New[n]allocate n rows, each Nil
rows[i]a row, a T[]
rows[i][j]an element
[[1, 2, 3], [4]]a ragged literal is a T[][]. A rectangular one stays T[,], as today.
[a, b] with a, b : T[]a T[][] (array literals with expressions, #1245)

Each level is one-dimensional in this phase. Int[,][] (rows that are themselves rectangular) is not supported: no task in the batch needs it, and it would tie the syntax to a choice about which bracket is outer.

Representation

At run time a T[][] is an object array whose elements are T[] arrays. The VM, both JIT backends and the collector already handle this:

So deeper nesting is covered too, and no VM change is needed.

In the compiler, a jagged type is a one-dimensional array of a row type, a class name that only the compiler uses: Int[][] is $Int[] with dimension 1, and Int[][][] is $Int[][] with dimension 1. Everything that already works for object arrays then works unchanged: allocation, ->Size(), each, parameters, fields, return types, signature encoding (o.$Int[]*) and memory types. The row type turns back into the real array type in the few places an element is read or written:

Assignment through two subscripts

rows[i][j] := v is rewritten by the parser to #ary := rows[i]; #ary[j] := v. This is the hidden-local form already used for an indexed call result as a target (ParseCallElementOperation). The row is read before v is evaluated.

Chained indexing

rows[i][j] is a variable with more than one index group. The groups are emitted innermost first, then the variable. Each element load then takes the next group's index from below the array it just loaded:

<j>  <i>  LOAD rows  LOAD_INT_ARY_ELM 1  LOAD_INT_ARY_ELM 1

A store is the same with the value first: <v> <j> <i> LOAD rows LOAD_INT_ARY_ELM 1 STOR_INT_ARY_ELM 1. This means index expressions are evaluated innermost group first (j before i). That only shows when two index expressions have side effects, and it is documented. Keeping written order would need a temporary per group.

Out of scope

Tests

jagged_arrays.obs covers declaration, allocation, ragged literals, chained reads and writes, rows of objects (String[][]), parameters and return values, each/->Size(), three levels, and a loop that allocates enough rows to collect (JIT, GC, three modes). It also checks that rows survive collection under --gc-threshold. The bad_jagged_*.obs files cover a wrong element type and a wrong number of index groups.