Browse the handbook
Language

Typed fixed and dynamic arrays

Choose an explicit dtype, inspect capacity growth, sort and search efficiently, and use checked native numerical loops with complete time and space complexity.

Array<T> and DynamicArray<T> are built in Aner data structures. They need no import. Both require one explicit element type: Int64, Float64, Bool, or String. The compiler checks every stored value and method argument against that type. Array without <T>, Array<Any>, nullable elements, nested arrays, and user object elements are unsupported.

Use the compatible native interpreter and VS Code extension 0.1.26 or newer. Updating only the extension does not update the interpreter. Run fixed and dynamic arrays and numerical arrays:

sh
aner check examples/arrays_basic.aner
aner examples/arrays_basic.aner
aner examples/arrays_numeric.aner

Why arrays belong in the core language

A mixed value List is useful for records, heterogeneous data, and teaching general collections. A typed array provides a different guarantee: one element representation, predictable indexed access, and native loops over contiguous storage. This avoids storing an individually tagged Any value for every element. Fixed arrays teach bounded storage; dynamic arrays teach capacity, reallocation, amortized cost, and the difference between logical length and allocated space. Aner includes both because those contracts matter in academic exercises and numerical programs.

Python's array module similarly distinguishes typed compact storage from general lists. A NumPy ndarray adds homogeneous multidimensional shape and stride semantics. Aner starts with one dimensional typed arrays and connects numerical arrays to its existing Tensor. It does not claim NumPy compatibility.

StructureContentsLengthMain purpose
ListMixed values and referencesGrowableGeneral collections
Array<T>One required scalar dtypeFixed at creationPredictable indexed storage
DynamicArray<T>One required scalar dtypeGrowable within an explicit capacity policyTyped sequences
TensorFloat64 numerical values, rows × columnsFixed shapeNumerical operators and neural differentiation

Int64 elements occupy contiguous 64 bit signed slots; Float64 elements occupy contiguous doubles; Bool uses one byte per element rather than proxy bit references. String stores contiguous native String descriptors; text payloads are separately owned. String arrays do not form a packed character matrix. Native buffers contain no per element Value tags or heap object references.

Fixed length

aner
let scores = Array<Int64>(size: 4, fill: 0)
scores.set(index: 0, value: 30)
scores.set(1, 10)
scores.set(2, 20)
print(scores.len())
print(scores.capacity())
print(scores.get(0))
scores.sort()
print(scores.to_list())
print(scores.find(20))

The length and capacity are both 4. Sorting produces List(0, 10, 20, 30) and finding 20 returns index 2. Size zero is valid. Every element is initialized by the required fill; there is no uninitialized storage exposed to Aner code. Fixed length permits set, fill, sort, and reverse, but not append, insert, remove_at, pop, resize, clear, or reserve. Those methods are rejected during checking.

Dynamic length and capacity

aner
let readings = DynamicArray<Float64>(capacity: 2)
print(readings.len())
readings.append(3.0)
readings.append(1.0)
readings.append(2.0)
print(readings.len())
print(readings.capacity())
readings.sort()
print(readings.find(2.0))
readings.resize(size: 5, fill: 0.0)
readings.reserve(capacity: 16)

The initial length is zero and capacity is 2. The third append grows capacity to 4. resize changes logical length, initializes new elements with the required fill value, and preserves the earlier prefix. Shrinking discards elements. reserve ensures at least the requested capacity without changing length; a nonnegative request below the current capacity is an operation with no effect.

Initial capacity is required and may be zero. When a growth operation needs more room, the new capacity is the larger of twice the old capacity and the requested length. Zero grows to one first. Growth is capped at 1,000,000 elements; requests beyond that fail instead of silently exceeding the bound. An explicit reserve allocates the requested minimum directly. Removing elements and clear retain capacity; there is no automatic shrinking or shrink_to_fit in this version. Pre reserving a known final size avoids intermediate reallocations.

let makes the binding immutable, while the array's elements remain mutable. Assigning an array to another binding shares its identity and storage. Call copy() to obtain independent storage, preserving the original capacity. slice(start, stop) makes an independent array of the same family and dtype, with stop excluded; it is a copy, not a strided view. Array equality compares identity, not deep contents.

aner
let values = DynamicArray<Int64>(capacity: 2)
values.append(9)
let alias = values
let independent = values.copy()
alias.set(0, 3)
print(values.get(0))
print(independent.get(0))

The output is 3, then 9. Arrays can be stored inside mixed Lists, Dictionary values, Any, and user class fields, including fields such as Array<T> in a user generic class when T resolves to a supported scalar dtype. cast<Array<Int64>>(value) checks the family and dtype without converting or copying.

Strict element types

aner
let numbers = Array<Float64>(size: 3, fill: 0.0)
numbers.set(0, Float64(2))
let text = DynamicArray<String>(capacity: 2)
text.append("hello")

An Int64 cannot be inserted into a Float64 array without an explicit conversion. String and Int64 cannot be mixed; Bool is a separate dtype. to_float() converts an Int64 or Float64 numerical array into a separate Float64 array of the same family. Large Int64 values may round during this explicit conversion. to_list() copies the elements into a mixed value List, preserving their scalar types.

Float64 elements must be finite. NaN, infinity, and nonfinite arithmetic results report a runtime error. This gives sorting and searching a consistent order and matches the numerical safety contract of Aner Tensor. The policy is stricter than NumPy, which can represent nonfinite floats.

Sorting and searching

sort() changes the array into ascending order. Int64 and Float64 use numerical ordering, Bool orders false before true, and String uses case sensitive lexicographic ordering of encoded bytes. String comparison does not apply locale rules, natural number ordering, or Unicode normalization. Equal floating positive and negative zero compare equally.

Arrays use in place heapsort: O(n log n) worst case time and O(1) auxiliary storage. The sort is not stable. Use List sorting when stable sorting of mixed Int64/Float64 contents is needed. Array sorting has no custom comparator or descending parameter in this version.

find(value) returns the first matching zero based index, or −1 when absent. contains(value) uses the same search. The runtime maintains the exact number of adjacent inversions as mutations happen, so is_sorted() is O(1). If that count is zero, find uses lower bound binary search in O(log n); otherwise it uses a linear scan in O(n). No binary search tree is allocated. Mutations that break ascending order immediately cause linear search, and mutations that restore order immediately permit binary search. Duplicates still return the first matching index. Empty arrays are sorted.

aner
let items = DynamicArray<Int64>(capacity: 4)
items.append(30)
items.append(10)
items.append(20)
print(items.is_sorted())
print(items.find(20))
items.sort()
print(items.is_sorted())
print(items.find(20))
items.set(0, 99)
print(items.is_sorted())
print(items.find(20))

The output is false, 2, true, 1, false, 1. The final find still works after indexed mutation invalidates sorted order. NumPy searchsorted also uses binary search on sorted inputs, but returns insertion positions; Aner find returns a matching position or −1.

Method signatures and complexity

Let n be logical length, C allocated capacity, m the copied slice length, and C′ the new capacity. Space below means additional auxiliary or result storage for the operation; persistent storage is O(C) scalar slots. Empty arrays take constant work. String costs additionally include bytes copied, destroyed, or compared, as described after the tables.

Method on either familyResultTimeAdditional space
Array<T>(size: Int64, fill: T)Array<T>O(n)O(n) result
DynamicArray<T>(capacity: Int64)DynamicArray<T>O(C) initializationO(C) result
len(), capacity()Int64O(1)O(1)
is_empty(), is_sorted()BoolO(1)O(1)
get(index: Int64)TO(1)O(1)
set(index: Int64, value: T)UnitO(1)O(1)
fill(value: T)UnitO(n)O(1)
copy()Same family and dtype, same capacityO(C)O(C) result
slice(start: Int64, stop: Int64)Same family and dtypeO(m)O(m) result
sort()UnitO(n log n), worst caseO(1)
find(value: T)Int64O(log n) sorted; O(n) otherwiseO(1)
contains(value: T)BoolO(log n) sorted; O(n) otherwiseO(1)
reverse()UnitO(n)O(1)
to_list()ListO(n)O(n) result
DynamicArray only methodResultTimeAdditional space
append(value: T)UnitAmortized O(1); O(C′) when growingO(1); O(C′) during growth
insert(index: Int64, value: T)UnitO(n − index); O(C′) when growingO(1); O(C′) during growth
remove_at(index: Int64)Removed TO(n − index)O(1)
pop()Removed TO(1)O(1)
resize(size: Int64, fill: T)UnitO(abs(new size − n)); O(C′) when growingO(1); O(C′) during growth
clear()UnitO(n)O(1)
reserve(capacity: Int64)UnitO(1) operation with no effect; O(C′) when allocatingO(C′) during allocation

Reserved buffers are initialized, so explicitly creating or reserving a large C has a real O(C) cost even while length is zero. Doubling bounds the total amount copied and initialized across successive scalar appends by O(n + initial C), yielding amortized O(1) append after construction. One append can still allocate and copy the buffer; it is not a real time constant latency operation. Growth temporarily holds the old and replacement buffers. Allocation limits constrain the committed logical heap, not process RSS or transient allocator overhead.

The scalar operation rows above apply to Int64, Float64, and Bool. String fill builds a replacement buffer for failure atomicity: O(C + copied/destroyed text bytes) time and O(C + new text bytes) temporary space. String resize within capacity stages newly added text before mutation, using O(new size − n + added text bytes) temporary space. String reallocation copies live payloads as well as descriptors.

String get returns an owned String copy; its time and result space include the returned text bytes. String set, append, fill, copy, slices, and conversions likewise include copied payload bytes. Removing or clearing String elements includes destroyed payload bytes. String searching performs O(log n) or O(n) comparisons; each comparison may inspect a common prefix. If compared Strings have at most k bytes, sorting has O(n log n · (1 + k)) worst case time and O(1) auxiliary descriptors. Total persistent String storage is O(C + B), where B is the stored text bytes. These qualifications prevent scalar O(1) bounds from implying constant time copying of arbitrary text.

Numerical operations

Int64 and Float64 arrays provide native vector loops with checked arithmetic. Array operands require the same family, dtype, and length. These methods return independent arrays where applicable; they do not change their inputs. Scalar loops operate on typed buffers directly.

aner
let x = Array<Float64>(size: 3, fill: 0.0)
x.set(0, 1.0)
x.set(1, 2.0)
x.set(2, 3.0)
let y = x.scale(factor: 2.0)
print(x.sum())
print(x.mean())
print(x.dot(y))
print(x.cumsum().to_list())
print(x.variance())
print(x.std())
let matrix = x.to_tensor(rows: 1, cols: 3)

to_tensor is available only on Float64 arrays, makes a detached row major copy, and requires positive rows/cols whose product equals n and fits the existing Tensor limits. Int64 arrays can use to_float().to_tensor(rows, cols). Creating and inferring a binding for the bridge result needs no import; Tensor operations and explicit : Tensor annotations still use import aner.tensor. The copied Tensor participates in the existing differentiation API independently of the source array.

Numeric methodResultTimeAdditional space
sum()TO(n)O(1)
mean(), variance(), std()Float64O(n)O(1)
min(), max()TO(n)O(1)
argmin(), argmax()Int64, first tied indexO(n)O(1)
dot(other: SameArrayType)TO(n)O(1)
add(other), sub(other), mul(other)Same array typeO(n)O(n) result
div(other): Float64 onlySame array typeO(n)O(n) result
scale(factor: T), cumsum()Same array typeO(n)O(n) result
to_float()Same family, Float64O(n)O(n) result
to_tensor(rows: Int64, cols: Int64): Float64 onlyTensorO(n)O(n) result

sum and dot of empty arrays return typed zero; cumsum and elementwise operations can return empty arrays. Mean, variance, standard deviation, extrema, and their indices require at least one element. Variance and standard deviation use the population definition (ddof = 0). They do not provide sample statistics or a ddof argument yet.

Int64 arithmetic checks each intermediate addition, subtraction, and multiplication; an overflowing intermediate fails even if a later term could cancel it. Int64 arrays do not provide integer division: convert explicitly to Float64 to use div. Float division by zero and nonfinite results fail. Mean and population statistics use scaled floating computation to reduce unnecessary overflow; floating results remain subject to roundoff and representable range. A finite standard deviation may be representable even when its variance is not. Arrays do not track gradients; use the Tensor bridge for differentiable computation.

Bounds, errors, and inspection

Indices are zero based. get, set, and remove_at require 0 <= index < n; insert permits index == n. Negative indices do not count from the end. Slices require 0 <= start <= stop <= n. Empty pop, invalid size/capacity, out of bounds indices, nonfinite Float64 values, integer overflow, shape mismatch, or array/heap quota exhaustion report R3001 with a source location. Static dtype and unsupported method errors are detected before execution. A host allocation failure uses the CLI's global E9003 insufficient memory handler. A failed native mutation does not partially change an existing array. The usual persistent Session rule still clears session state after a runtime error.

Each buffer is limited to 1,000,000 elements. All live arrays in one heap share a 64 MiB logical allocation budget that accounts for capacity slots, String descriptors, and owned String payload bytes. These limits are separate from List/Dictionary/Set slots and the Tensor budget. Garbage collection releases unreachable array buffers after successful persistent session cells; standalone scripts release their heap at termination. Limits are safety bounds, not a claim about total operating system memory.

Development mode and notebook variable inspection show dtype, length, capacity, current sorted status, and up to 16 elements per array. Observations are bounded copies and do not keep a live buffer attached. This makes growth and changes visible in academic exercises without inspecting unused capacity. Run the array notebook, or write a fresh report directory:

sh
aner examples/arrays_basic.aner --dev artifacts/array-inspection

The Jupyter array notebook uses %aner dev for completed cell inspection with the optional Aner kernel.

The report directory must not already exist. Production runs keep the same array checks and algorithms while omitting observation work.

NumPy inspired scope and deferred work

CapabilityCurrent decision
Explicit homogeneous dtype and contiguous numeric storageImplemented in both array families
Indexed access, independent slice/copy, sorting and searchImplemented
Elementwise arithmetic, scalar scaling, dot, cumulative sumImplemented for Int64/Float64
Reductions and population statisticsImplemented
Float64 Tensor interoperabilityImplemented as a detached copy
Arbitrary rank ndarray, strides, shared views, reshapingDeferred; existing Tensor remains the rank two numerical type
Broadcasting, masks, advanced indexing, general ufunc dispatchDeferred; requires a coherent shape and result allocation design
Float32, Int32, unsigned, complex, nullable or object dtypesDeferred; four explicit scalar dtypes keep the initial contract clear
Random generators, arange/linspace and general linear algebraDeferred; use explicit initialization and existing Tensor/ML APIs
Native compilation of array programsDeferred with the broader compiler architecture; these are optimized native buffers called by the current checked interpreter

The next numerical expansion should extend one shared typed storage/shape system rather than introduce incompatible competing arrays and Tensors. Views would need a clear lifetime policy when a dynamic buffer reallocates; broadcasting would need checked shapes and predictable memory costs. They are useful future capabilities, but a partial imitation would weaken Aner's simple type and memory guarantees. The initial implementation provides the scalar and one dimensional foundations now and records those boundaries explicitly.

Aner handbook · Guides and API reference