NDArray
Typed contiguous shapes, indexing, shared reshape, broadcasting, matrix multiplication, and time and space complexity.
NDArray<T> is a built in, fixed size multidimensional data structure for homogeneous scalar values. It supports row major indexing, reshape, axis permutations, numerical broadcasting, and rank two matrix multiplication. No import is required.
Construction
The required constructor arguments are shape: List and fill: T. T must be Int64, Float64, Bool, or String. Known argument types are checked before execution; Any payloads are validated when used. There is no implicit numeric promotion. Float64 values and results must be finite. Nullable, nested, and user object element types are unsupported.
Each NDArray is contiguous and row major. The last axis varies fastest. Shape is runtime metadata; different shapes with the same dtype use the same static type. Rank is limited to 1 through 8, each dimension must be an Int64 from 0 through 1,000,000, and the total nonempty element count cannot exceed 1,000,000. List() is not a scalar shape. All dimensions are explicit: constructor and reshape reject negative values, including −1 inference.
let matrix = NDArray<Int64>(shape: List(2, 3), fill: 0)
matrix.set(indices: List(0, 0), value: 1)
matrix.set(List(0, 1), 2)
matrix.set(List(0, 2), 3)
matrix.set(List(1, 0), 4)
matrix.set(List(1, 1), 5)
matrix.set(List(1, 2), 6)
print(matrix.ndim())
print(matrix.shape())
print(matrix.strides())
print(matrix.get(List(1, 2)))
print(matrix.to_list())Output is 2, List(2, 3), List(3, 1), 6, and List(1, 2, 3, 4, 5, 6). Strides are measured in elements, not bytes: offset = row × 3 + column. get and set require exactly one nonnegative in range Int64 coordinate per axis. Bool, Float64, null, or String coordinates are not integers. Negative indices do not count from the end.
Shape and coordinate Lists are copied/validated descriptions. Changing an input shape List after construction cannot change the NDArray shape. shape() and strides() return fresh Lists. Empty axes are valid, for example shape List(2, 0, 3) has zero elements. Every stride of an empty array is canonically zero; no valid coordinate can address it. This avoids overflowing meaningless stride products in empty high rank shapes.
Storage and ownership
Int64 and Float64 occupy contiguous eight byte native slots; Bool occupies one byte slots. String uses contiguous native descriptors and separately owned text payloads. Elements do not carry individual Any tags.
Assignment shares descriptor identity. Equality compares identity, not contents. A let binding prevents rebinding, not element mutation. reshape(shape: List) returns a new descriptor sharing the same fixed size buffer, preserving dtype, total element count, and row major order. Shape never changes on an existing descriptor.
let matrix = NDArray<Int64>(shape: List(2, 3), fill: 0)
let alternate = matrix.reshape(shape: List(3, 2))
alternate.set(List(0, 0), 99)
print(matrix.get(List(0, 0)))
let independent = matrix.copy()
independent.set(List(0, 0), -1)
print(matrix.get(List(0, 0)))Both output lines are 99. Shared buffer mutations are visible through every descriptor; copy() creates independent storage. No length changing methods are available. flatten() copies values into an independent compact Array, and to_list() returns an independent flat List. Neither operation is a view.
A fixed Array can share its buffer through reshape. DynamicArray reshape creates a detached fixed copy of live elements, excluding unused capacity; later growth cannot invalidate the shaped result. Those construction paths are documented in their own references. General strided views and writable broadcast views are unavailable.
Transpose and axis permutations
transpose() reverses the axis order. For a matrix it swaps rows and columns. For shape [2, 3, 4] it produces [4, 3, 2]. permute(axes: List) takes a complete permutation of zero based axes; [1, 2, 0] changes [2, 3, 4] into [3, 4, 2]. Duplicated, missing, or out of range axes fail.
let matrix = NDArray<Int64>(List(2, 3), 0)
matrix.set(List(0, 1), 7)
let transposed = matrix.transpose()
print(transposed.shape())
print(transposed.get(List(1, 0)))
transposed.set(List(1, 0), 8)
print(matrix.get(List(0, 1)))
let cube = NDArray<Int64>(List(2, 3, 4), 1)
print(cube.permute(List(1, 2, 0)).shape())Output is List(3, 2), 7, 7, and List(3, 4, 2). Both operations make detached contiguous copies, including an identity permutation. This keeps indexing, mutation, and future reshape predictable.
Broadcasting rules
Numerical add, sub, mul, and Float64 only div accept another NDArray with the same dtype. Shapes may differ. Compare axes from right to left: dimensions must be equal, or one must be 1. Missing leading axes act as 1. A singleton axis supplies the same input value to every compatible output coordinate.
let matrix = NDArray<Float64>(shape: List(2, 3), fill: 2.0)
let bias = NDArray<Float64>(shape: List(3), fill: 0.0)
bias.set(List(0), 10.0)
bias.set(List(1), 20.0)
bias.set(List(2), 30.0)
let shifted = matrix.add(bias)
print(shifted.shape())
print(shifted.to_list())
let scalar = NDArray<Float64>(shape: List(1), fill: 5.0)
print(matrix.add(scalar).to_list())
print(matrix.scale(factor: 0.5).to_list())Output begins List(2, 3) and List(12.0, 22.0, 32.0, 12.0, 22.0, 32.0). Singleton NDArray inputs provide scalar broadcasting; scale also accepts a typed scalar directly. An operand from another typed array family must first be explicitly reshaped into NDArray. Int64/Float64 operands cannot be mixed; call to_float() explicitly when conversion is intended.
| Left shape | Right shape | Result |
|---|---|---|
[2, 3] | [3] | [2, 3] |
[2, 1] | [1, 3] | [2, 3] |
[2, 1, 4] | [3, 1] | [2, 3, 4] |
[2, 3] | [2] | Error: trailing dimensions 3 and 2 conflict |
[0, 3] | [1, 3] | [0, 3], an empty result |
[0, 3] | [2, 3] | Error: dimensions 0 and 2 conflict |
Broadcasting allocates the output only. It does not expand or copy either input into a temporary repeated array. The runtime validates result shape and allocation budget before starting arithmetic. A compatible shape can still produce a larger output than either input, so the result must fit the element and shared heap limits. Empty outputs do no element arithmetic; a zero divisor in an input that contributes no output element is not evaluated.
Matrix products and reductions
mul is elementwise multiplication with broadcasting. matmul is a separate rank two matrix product: [m, k] times [k, p] produces [m, p]. It requires the same numerical dtype and matching inner dimensions. It does not broadcast batch axes or accept scalar/vector special cases. Work is capped at 50,000,000 multiply adds. Output initialization is also accounted for, including products with k = 0.
let left = NDArray<Float64>(shape: List(2, 3), fill: 2.0)
let right = NDArray<Float64>(shape: List(3, 2), fill: 1.0)
let product = left.matmul(right)
print(product.shape())
print(product.to_list())
print(product.sum())
print(product.mean())
let detached_tensor = product.to_tensor()
print(type_of(detached_tensor))The shape is [2, 2], all four values are 6.0, the sum is 24.0, and the mean is 6.0. Float64 to_tensor() requires rank two and positive dimensions, and makes a detached row major Tensor copy. The inferred result needs no import; explicit Tensor annotations and operations require the appropriate numerical module. NDArray does not record gradients.
Numeric reductions operate over all elements, not an axis. sum of an empty array returns typed zero. Mean, variance, standard deviation, extrema, and their indices require nonempty input. argmin/argmax return the first tied flat row major index, not coordinate Lists. Variance and standard deviation use population ddof = 0. Checked Int64 intermediates reject overflow; Float64 results must remain finite. Integer means use exact accumulation before Float64 conversion; floating reductions use scaled and compensated computation, subject to representable range and roundoff. to_float is an explicit detached conversion preserving shape; large integer values may round in Float64.
Methods and complexity
Let r be rank (at most 8), n the input element count, N the broadcast output element count, and B the String bytes copied or released. Result storage below is newly allocated storage; shared buffers are not counted again. Matrix dimensions are m × k and k × p. Empty cases include their metadata work.
| Method | Result | Time | Additional space |
|---|---|---|---|
add(other), sub(other), mul(other), Float64 div(other) | Independent NDArray<T> | O(N + r) | O(N) result + O(r) traversal metadata |
argmin(), argmax() | First flat Int64 index | O(n) | O(1) |
copy() | Independent NDArray<T> | O(n + r + B) | O(n + r + B) result |
fill(value: T) | Unit | O(n); String O(n + B) | O(1); String O(n + B) replacement buffer |
flatten() | Independent compact Array<T> | O(n + B) | O(n + B) result |
get(indices: List) | T | O(r), plus returned String bytes | O(r) validated coordinates; String result bytes |
matmul(other: NDArray<T>) | Independent rank two NDArray<T> | O(m · k · p + m · p) | O(m · p) result |
mean(), variance(), std() | Float64 | O(n) | O(1) |
NDArray<T>(shape: List, fill: T) | NDArray<T> | O(n + r), plus copied String bytes | O(n + r) result, plus String bytes |
is_contiguous(), is_empty(), len(), ndim() | Int64 or Bool | O(1) | O(1) |
reshape(shape: List) | Shared NDArray<T> descriptor | O(r) | O(r) metadata |
scale(factor: T) | Independent NDArray<T> | O(n + r) | O(n + r) result |
set(indices: List, value: T) | Unit | O(r), plus String copy/comparison/release work | O(r), plus replacement String bytes |
shape(), strides() | Fresh List | O(r) | O(r) result |
sum(), min(), max() | T | O(n) | O(1) |
to_float() | Independent NDArray<Float64> | O(n + r) | O(n + r) result |
to_list() | Independent flat List | O(n + B) | O(n + B) result |
Float64 to_tensor() | Detached Tensor | O(n) | O(n) result |
transpose(), permute(axes: List) | Independent contiguous NDArray<T> | O(n + r + B) | O(n + r + B) result and traversal metadata |
Coordinate get/set costs O(rank) because every coordinate is validated and multiplied by its stride. Rank is bounded, but the table reports that work explicitly. Copying, retrieving, or replacing a String adds its payload bytes; String comparisons may inspect neighboring common prefixes to maintain the shared flat buffer's ordering metadata. All metadata methods copy at most eight dimensions.
Broadcast and permutation loops use incremental stride offsets, removing output axes of length one before traversal. Carries across the remaining axes amortize over the output, so traversal takes O(N + r) rather than unraveling every output index across every axis. Broadcasting therefore needs no O(N) expanded input storage. Shapes are still checked with overflow safe product validation before allocation. Matrix product loops access native typed values and retain source order for checked accumulation.
Bounds and errors
A shared reshape is a different object descriptor using the same native fixed buffer. Any element mutation through one descriptor is visible through every descriptor of that buffer. Copies, transposes, permutations, dynamic reshapes, and arithmetic results are independent. Fixed Array sorted search metadata is maintained even when a shared NDArray changes the buffer. shape, strides, dtype, and logical length never change on an existing NDArray.
Typed array buffers share a 64 MiB logical buffer and String payload budget within one heap. Each physical buffer is charged once regardless of how many reshape descriptors refer to it; it remains charged until its final heap descriptor is collected. Metadata/view objects still count toward the shared 16,384 object limit. Unreachable view/source descriptors are collected after successful session cells; keeping a view alive keeps its buffer alive even if the original descriptor is gone. These are logical safety budgets, excluding allocator overhead, descriptor vectors, process RSS, and transient buffers used for atomic String operations.
Invalid shapes, coordinate types/bounds, permutations, mismatched reshape totals, incompatible broadcasts, matrix shape/work limit failures, quota exhaustion, checked integer overflow, and nonfinite numerical results report source located R3001. Unsupported dtypes and methods are checked before execution. Actual host allocation failure uses the CLI's global E9003 handler. Mutation validation/allocation failures do not partially change an existing buffer; arithmetic failures do not publish a partial result. Persistent runtime errors still clear the complete Session.
Development inspection
Development previews display dtype, shape, element strides, logical length, and a stable storage identifier. Matching storage identifiers explain shared reshape data; matching object identifiers explain aliases to the same descriptor. Up to 16 elements are shown in flat row major order, with omissions reported. NDArray has no sorting or search methods. Use a fresh report directory:
aner examples/ndarray_basic.aner --dev artifacts/multidimensional-inspectionCurrent scope
Implemented operations cover contiguous rank 1 to 8 storage, checked shape/indexing, shared reshape, detached copies and axis permutations, numerical broadcasting, all element reductions, rank two matrix products, and detached Float64 Tensor conversion.
Rank zero shapes, inferred dimensions, arbitrary strided slicing/views, writable broadcast views, axis specific reductions or sorting, masks/advanced indexing, batched matrix products, additional dtypes, implicit promotion, accelerator execution, and whole program compilation are unavailable. Native typed loops execute within the checked interpreter.
Examples and related references
Run multidimensional indexing and reshape, numerical broadcasting and matrix products, the VS Code multidimensional notebook, or the Jupyter multidimensional notebook:
aner check examples/ndarray_basic.aner
aner examples/ndarray_basic.aner
aner examples/ndarray_numeric.aner- Array: fixed one dimensional storage and shared reshape.
- DynamicArray: capacity managed storage and detached reshape.
- Tensor: numerical computation and differentiation.
- Development mode: bounded reports and completed cell inspection.