Vectors and matrices carried their shape in the type: matrix[2, 3] of float64 refuses a mismatched product before the program exists. That is the right home for shapes you know while writing. A data file’s row count, a network message’s width, a model loaded at run time — those shapes arrive with the data, and for them the same convention that gives you array of T beside array[0..9] of T gives you the extent-dynamic tensors:

var
    a : matrix of float64;    { rank two in the type, extents in the value }
    v : vector of float64;    { rank one in the type, extent in the value }

The rank stays in the type — a matrix of is always rank two, and the compiler still refuses m[i] where two indices are owed. Only the extents move into the value, where Resize writes them and Rows, Columns, and Length read them back.

The example is examples/DynamicTensors in the tutorial repository.

Shaping and reading

Resize(address a, 2, 3);          { a matrix shapes by rows and columns }
Resize(address v, 3);             { a vector by its one extent }
WriteLn("a is %lld x %lld, %lld elements", Rows(a), Columns(a), Length(a));

Resize is the sequence family’s own verb with the tensor’s rank as its arity — one axis for a vector, two for a matrix, anything else refused at compile time. A freshly resized tensor reads zero everywhere, exactly as a resized array does.

The same operators, and the guard that replaces the compile-time refusal

Everything the static tensors carry runs on the dynamic ones: + and - elementwise, scalar scaling from either side, * as the linear-algebra product, Dot, Transpose, negation. What changes is where the shape facts come from — and what happens when they disagree. A static mismatch was refused at compile time; a dynamic mismatch can only be judged where the values exist, so every operation carries an always-on shape guard: agreeing extents cost one compare, and a disagreement stops the program at the operation with tensor_shape_mismatch and the source position — never a silent walk past the shorter backing.

a * b is 2 x 2
  22 28
  49 64

Static and dynamic mix under one law

The two worlds meet in one expression without ceremony, governed by a single rule: a result dimension the algebra derives from a static operand stays static, and the result type is static exactly when all of its dimensions are. So a static and a dynamic matrix add into the static shape — the guard proves the dynamic value against it — and the one mixed product with a fully static answer keeps it:

cw := w + a;         { matrix[2, 3] + (matrix of float64) answers matrix[2, 3] }
sr := w * v;         { matrix[2, 3] * (vector of float64) answers vector[2]    }

Nothing you knew at compile time is forgotten because a run-time shape stood beside it. And the crossing back is one assignment: a static tensor value assigns into a dynamic variable of its class and element — the deep copy adopts the shape — so a mixed operation’s static answer lands in a matrix of destination without ceremony.

Broadcasting is spelled

numpy’s implicit stretch is its most famous source of silent bugs, so Mica refuses it — and gives the two real shapes their own verbs instead. The expansion is a value you can print, and its natural count is the other operand’s own axis:

c := a + RowSpread(v, Rows(a));          { v laid across every row    }
c := a + ColumnSpread(y, Columns(a));    { y laid down every column   }

RowSpread(v, n) answers an n-by-Length(v) dynamic matrix whose every row is v; ColumnSpread is its transposed twin. Both compose with either matrix family through the mixing law above.

Sum, whole or along an axis

Sum(t) folds any tensor — static or dynamic, any rank — to its element scalar. Sum(t, axis) removes exactly that axis from the shape: a matrix summed along axis 0 answers its column sums, along axis 1 its row sums. The axis is a compile-time constant, because the result’s shape depends on it.

Sum(a) = 21
Sum(a, 0) = column sums: 5 7 9
Sum(a, 1) = row sums: 6 15

Every fold walks the same documented reduction tree the math unit’s Sum walks, so the word answers one bit pattern wherever it appears — the same bit-exact promise Mica’s tensor operators keep, on every optimization tier and both architectures. (See deterministic reductions for the tree itself.)

The rank-general word

tensor[2, 2, 4] of float64 extends the pair upward: the class follows the written shape, so tensor[3] of T is vector[3] of T and tensor[2, 3] of T is matrix[2, 3] of T — one shape is one type whichever word declared it. Rank three and above carry the elementwise operators, scaling, negation, full-rank indexing, and Sum; the products stay the rank-2 world’s own. The bound-less tensor of T is refused — an extent-dynamic tensor keeps its rank in the type, and the compiler’s sentence names the spellings that do.

Where the memory lives

A dynamic tensor is the dynamic array’s descriptor with a tensor class — the same value-model deep copy on assignment, the same behavior in both memory classes. Expression temporaries are reclaimed by the end of their statement, and an assignment whose right side is one tensor operation writes the destination’s backing directly, so a loop re-filling one result reuses one allocation. The memory classes chapter’s rules apply unchanged.

Next: the linalg unit puts systems of equations on top of these matrices — born on the failure channel.