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 64Static 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 15Every 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.