NativeArray
NativeArray — flat numeric arrays for high-performance numeric loops.
Unlike Array (which uses a 32-way persistent trie), NativeArray is backed by a flat OCaml array that maps directly to sequential memory. This makes it cache-friendly for sequential scan patterns and enables tight inner loops for sum, map, and fold operations.
Compiled (--compile) vectorization, in practice:
- sum_int / sum_float: both auto-vectorize under clang -O2 (NEON on
arm64, SSE4.2+ on x86-64) — sum_float needed a scoped
`#pragma clang fp reassociate(on)` in the runtime C loop, since
strict IEEE 754 float semantics otherwise block the reduction from
vectorizing at all.
- map_int with a non-capturing or single-capture lambda: the compiler
inlines the callback directly into the loop instead of dispatching
through a closure pointer, which lets clang's own inliner and
vectorizer see through it — arithmetic-heavy bodies (`fn x -> x*x+1`,
`fn x -> x + captured`) compile to real SIMD instructions. A closure
reused elsewhere, or one with more than one captured variable, still
works correctly but falls back to the slower closure-dispatch path.
- map_float: correct and (for capturing/non-capturing alike) gets the
same closure-inlining treatment as map_int. Float crossing a
closure-call boundary is normally heap-boxed (march_alloc_float),
which blocks vectorization — but when the callback's signature is
concretely all-Float (no generic type variable), the compiler clones
the inlined callback under natural double params/return with zero
boxing, and the loop DOES vectorize (real NEON/SSE, confirmed via
-emit-llvm). A callback whose signature is still generic at this
point falls back to the boxed (correct, non-vectorized) path. See
specs/optimizations.md P10 "Float-boxing Stage 4, Option B" and
specs/plans/2026-07-13-float-boxing-design.md.
- fold_int / fold_float / fold_f32 / fold_i32 / fold_u8 all have compiled
implementations: the accumulator crosses the closure boundary in the
erased/boxed representation (see march_typed_array_fold's RC
discipline, which native_*_arr_fold mirrors), so no per-width
vectorization applies — this is a correctness-first scalar loop.
- map2_int / map2_float (two-array zip-with, e.g. for column-column
arithmetic) get the identical closure-inlining/vectorization treatment
map_int/map_float do above — same eligibility bar, same unboxed clone
for a concrete-Float callback. Verified ~47x faster than the pre-fix
boxed closure-call path on a 5M-element benchmark; see
docs/simd-benchmarks.md's "Fix history: map2".Sharing and sending (Part C of specs/plans/2026-09-25-send-data-race-freedom-plan.md): A native array is a VALUE with copy-on-write semantics. set_ and sort_ "return a new array"; the compiled runtime writes in place only when the caller held the sole reference (march_rc_is_unique, an acquire load of the reference count) and copies otherwise, and the interpreter always copies. No program can tell the two apart, so an array may be sent in an actor message, captured by a Task or Parallel closure, or stored in a record like any other value; each holder sees only its own writes. Cost model:
- a write to an array nobody else holds is in place: O(1) for set,
O(n log n) for sort, no allocation;
- the first write to a SHARED array copies it, O(n), one allocation;
later writes to the copy are in place again;
- sending, capturing in a closure or storing in a record shares the
array. A loop `let arr = NativeArray.set_int(arr, i, v)` that threads
the array through never shares it and never copies.
Audited 2026-10-06 (runtime/march_runtime.c): set and sort at all five
widths including the sort fast paths, and the SIMD vector store, are
gated on sole ownership; map/map2 (C and the inlined fast path),
filter_mask, the width conversions and from_list allocate fresh. Nothing
writes an array it does not solely own.Five opaque element types are supported: NativeIntArr — wraps int array (i64) NativeFloatArr — wraps float array (f64) NativeF32Arr — narrow float array (f32); stores round to nearest-even NativeI32Arr — narrow int array (i32); stores truncate mod 2^32 NativeU8Arr — narrow int array (u8); stores truncate mod 2^8 Narrow-width stores never trap; loads widen exactly (u8 zero-extends, i32 sign-extends). See docs/simd-vectorization.md's "Narrow element widths" section for the full boundary rule and benchmarks.
Usage: let arr = NativeArray.make_float(1000, 0.0) let arr2 = NativeArray.map_float(arr, fn x -> x *. 2.0) let s = NativeArray.sum_float(arr2)
Functions
Widen a NativeF32Arr to NativeFloatArr (exact). O(n).
Examples
march> NativeArray.to_list_float(NativeArray.f32_to_float_arr(NativeArray.from_list_f32([0.5, 2.25])))
[0.5, 2.25]Narrow a NativeFloatArr to a NativeF32Arr, rounding each element to binary32 (nearest-even). O(n).
Examples
march> NativeArray.to_list_f32(NativeArray.float_to_f32_arr(NativeArray.from_list_float([1.5, -0.25])))
[1.5, -0.25]
march> NativeArray.get_f32(NativeArray.float_to_f32_arr(NativeArray.from_list_float([0.1])), 0) == 0.1
falseLeft fold over all elements (each widened to Float). Argument order: collection first, init second, callback last (uncurried-collection convention). Callback f(acc, elem) = new_acc. O(n).
Examples
march> NativeArray.fold_f32(NativeArray.from_list_f32([0.5, 0.25]), 1.5, fn (acc, x) -> acc +. x)
2.25Left fold over all elements. Argument order: collection first, init second, callback last (uncurried-collection convention). Callback f(acc, elem) = new_acc. O(n).
Examples
march> NativeArray.fold_float(NativeArray.from_list_float([1.5, 2.25]), 0.5, fn (acc, x) -> acc +. x)
4.25Left fold over all elements (each sign-extended to Int). Argument order: collection first, init second, callback last (uncurried-collection convention). Callback f(acc, elem) = new_acc. O(n).
Examples
march> NativeArray.fold_i32(NativeArray.from_list_i32([-1, 2]), 10, fn (acc, x) -> acc - x)
9Left fold over all elements. Argument order: collection first, init second, callback last (uncurried-collection convention). Callback f(acc, elem) = new_acc. O(n).
Examples
march> NativeArray.fold_int(NativeArray.from_list_int([1, 2, 3]), 0, fn (acc, x) -> acc * 10 + x)
123
march> NativeArray.fold_int(NativeArray.from_list_int([]), 5, fn (acc, x) -> acc + x)
5Left fold over all elements (each zero-extended to Int). Argument order: collection first, init second, callback last (uncurried-collection convention). Callback f(acc, elem) = new_acc. O(n).
Examples
march> NativeArray.fold_u8(NativeArray.from_list_u8([1, 2, 3]), 0, fn (acc, x) -> acc * 10 + x)
123Build a NativeF32Arr from a List(Float), rounding each element to binary32. O(n).
Examples
march> NativeArray.length_f32(NativeArray.from_list_f32([0.5, 1.5, 2.5]))
3
march> NativeArray.get_f32(NativeArray.from_list_f32([0.1]), 0) == 0.1
falseBuild a NativeFloatArr from a List(Float), preserving order. O(n).
Examples
march> NativeArray.length_float(NativeArray.from_list_float([0.5, 1.5]))
2Build a NativeI32Arr from a List(Int), wrapping each element to i32. O(n).
Examples
march> NativeArray.to_list_i32(NativeArray.from_list_i32([-5, 2147483647]))
[-5, 2147483647]Build a NativeIntArr from a List(Int), preserving order. O(n).
Examples
march> NativeArray.get_int(NativeArray.from_list_int([5, 6, 7]), 0)
5Build a NativeU8Arr from a List(Int), wrapping each element mod 256. O(n).
Examples
march> NativeArray.to_list_u8(NativeArray.from_list_u8([256, -1, 300]))
[0, 255, 44]Return the element at index i (0-based), widened (exactly) to Float. O(1). Panics if i is negative or >= length_f32(arr).
Examples
march> NativeArray.get_f32(NativeArray.from_list_f32([0.5, 0.75]), 1)
0.75
march> NativeArray.get_f32(NativeArray.from_list_f32([16777217.0]), 0) == 16777216.0
trueReturn the element at index i (0-based). O(1). Panics if i is negative or >= length_float(arr).
Examples
march> NativeArray.get_float(NativeArray.from_list_float([9.5, 8.5]), 1)
8.5Return the element at index i (0-based), sign-extended to Int. O(1). Panics if i is negative or >= length_i32(arr).
Examples
march> NativeArray.get_i32(NativeArray.from_list_i32([9, -8]), 1)
-8Return the element at index i (0-based). O(1). Panics if i is negative or >= length_int(arr).
Examples
march> NativeArray.get_int(NativeArray.from_list_int([10, 20, 30]), 2)
30Return the element at index i (0-based), zero-extended to Int (0..255). O(1). Panics if i is negative or >= length_u8(arr).
Examples
march> NativeArray.get_u8(NativeArray.from_list_u8([9, 255]), 1)
255Convert a NativeI32Arr to NativeF32Arr, rounding each element to binary32 (nearest-even; exact for magnitudes up to 2^24). O(n).
Examples
march> NativeArray.to_list_f32(NativeArray.i32_to_f32_arr(NativeArray.from_list_i32([3, -4]))) == [3.0, -4.0]
trueWiden a NativeI32Arr to NativeIntArr (exact sign-extend). O(n).
Examples
march> NativeArray.to_list_int(NativeArray.i32_to_int_arr(NativeArray.from_list_i32([-7, 8])))
[-7, 8]Narrow a NativeIntArr to NativeI32Arr, wrapping each element mod 2^32 two's-complement. O(n).
Examples
march> NativeArray.to_list_i32(NativeArray.int_to_i32_arr(NativeArray.from_list_int([1, -1])))
[1, -1]Narrow a NativeIntArr to NativeU8Arr, wrapping each element mod 256. O(n).
Examples
march> NativeArray.to_list_u8(NativeArray.int_to_u8_arr(NativeArray.from_list_int([255, 256, -1])))
[255, 0, 255]Return the number of elements in arr. O(1).
Examples
march> NativeArray.length_f32(NativeArray.make_f32(4, 1.5))
4Return the number of elements in arr. O(1).
Examples
march> NativeArray.length_float(NativeArray.make_float(4, 1.5))
4Return the number of elements in arr. O(1).
Examples
march> NativeArray.length_i32(NativeArray.make_i32(4, 1))
4Return the number of elements in arr. O(1).
Examples
march> NativeArray.length_int(NativeArray.from_list_int([4, 5, 6]))
3Return the number of elements in arr. O(1).
Examples
march> NativeArray.length_u8(NativeArray.make_u8(4, 1))
4Create a new f32 array of n elements, each initialised to fill rounded to binary32. O(n). n must be non-negative (a refinement precondition).
Examples
march> NativeArray.to_list_f32(NativeArray.make_f32(2, 0.25))
[0.25, 0.25]
march> NativeArray.get_f32(NativeArray.make_f32(1, 0.1), 0) == 0.1
falseCreate a new float array of n elements, each initialised to fill. O(n). n must be non-negative (a refinement precondition; a negative size is a runtime error).
Examples
march> NativeArray.to_list_float(NativeArray.make_float(2, 0.5))
[0.5, 0.5]Create a new i32 array of n elements, each initialised to fill wrapped to i32. O(n). n must be non-negative (a refinement precondition).
Examples
march> NativeArray.to_list_i32(NativeArray.make_i32(2, -3))
[-3, -3]Create a new integer array of n elements, each initialised to fill. O(n). n must be non-negative (a refinement precondition; a negative size is a runtime error).
Examples
march> NativeArray.to_list_int(NativeArray.make_int(3, 7))
[7, 7, 7]
march> NativeArray.length_int(NativeArray.make_int(0, 7))
0Create a new u8 array of n elements, each initialised to fill wrapped mod 256. O(n). n must be non-negative (a refinement precondition).
Examples
march> NativeArray.to_list_u8(NativeArray.make_u8(2, 7))
[7, 7]
march> NativeArray.to_list_u8(NativeArray.make_u8(2, 257))
[1, 1]Apply f elementwise to two same-length arrays, returning a new NativeF32Arr. f(a_elem, b_elem) = out_elem, rounded to binary32. Panics if the arrays differ in length. O(n).
Examples
march> NativeArray.to_list_f32(NativeArray.map2_f32(NativeArray.from_list_f32([1.5, 2.5]), NativeArray.from_list_f32([0.5, 0.5]), fn (a, b) -> a *. b))
[0.75, 1.25]Apply f elementwise to two same-length arrays, returning a new NativeFloatArr. f(a_elem, b_elem) = out_elem. Panics if the arrays differ in length. O(n).
Examples
march> NativeArray.to_list_float(NativeArray.map2_float(NativeArray.from_list_float([1.5, 2.5]), NativeArray.from_list_float([0.25, 0.25]), fn (a, b) -> a +. b))
[1.75, 2.75]Apply f elementwise to two same-length arrays, returning a new NativeI32Arr. f(a_elem, b_elem) = out_elem, wrapped to i32. Panics if the arrays differ in length. O(n).
Examples
march> NativeArray.to_list_i32(NativeArray.map2_i32(NativeArray.from_list_i32([1, 2]), NativeArray.from_list_i32([3, -4]), fn (a, b) -> a * b))
[3, -8]Apply f elementwise to two same-length arrays, returning a new NativeIntArr. f(a_elem, b_elem) = out_elem. Panics if the arrays differ in length. O(n).
Examples
march> NativeArray.to_list_int(NativeArray.map2_int(NativeArray.from_list_int([1, 2]), NativeArray.from_list_int([10, 20]), fn (a, b) -> a + b))
[11, 22]Apply f elementwise to two same-length arrays, returning a new NativeU8Arr. f(a_elem, b_elem) = out_elem, wrapped mod 256. Panics if the arrays differ in length. O(n).
Examples
march> NativeArray.to_list_u8(NativeArray.map2_u8(NativeArray.from_list_u8([200, 1]), NativeArray.from_list_u8([100, 2]), fn (a, b) -> a + b))
[44, 3]Apply f to every element (widened Float in, rounded to binary32 out), returning a new NativeF32Arr. O(n).
Examples
march> NativeArray.to_list_f32(NativeArray.map_f32(NativeArray.from_list_f32([1.5, 2.25]), fn x -> x +. 1.0))
[2.5, 3.25]Apply f to every element, returning a new NativeFloatArr. O(n).
Examples
march> NativeArray.to_list_float(NativeArray.map_float(NativeArray.from_list_float([1.5, 2.5]), fn x -> x *. 0.5))
[0.75, 1.25]Apply f to every element (sign-extended Int in, wrapped to i32 out), returning a new NativeI32Arr. O(n).
Examples
march> NativeArray.to_list_i32(NativeArray.map_i32(NativeArray.from_list_i32([1, -2]), fn x -> x * 3))
[3, -6]Apply f to every element, returning a new NativeIntArr. O(n).
Examples
march> NativeArray.to_list_int(NativeArray.map_int(NativeArray.from_list_int([1, 2, 3]), fn x -> x * x))
[1, 4, 9]Apply f to every element (zero-extended Int in, wrapped mod 256 out), returning a new NativeU8Arr. O(n).
Examples
march> NativeArray.to_list_u8(NativeArray.map_u8(NativeArray.from_list_u8([100, 200]), fn x -> x * 2))
[200, 144]Return an array with element i set to v rounded to binary32 (nearest-even); the value conversion never traps. Same value semantics and cost as set_int. Panics if i is negative or >= length_f32(arr).
Examples
march> NativeArray.to_list_f32(NativeArray.set_f32(NativeArray.make_f32(2, 0.25), 0, 0.5))
[0.5, 0.25]Return an array with element i replaced by v. Same value semantics and cost as set_int: in place when solely owned (compiled), otherwise an O(n) copy. Panics if i is negative or >= length_float(arr).
Examples
march> NativeArray.to_list_float(NativeArray.set_float(NativeArray.make_float(2, 0.5), 1, 2.5))
[0.5, 2.5]Return an array with element i set to v wrapped mod 2^32 two's-complement; the value conversion never traps. Same value semantics and cost as set_int. Panics if i is negative or >= length_i32(arr).
Examples
march> NativeArray.to_list_i32(NativeArray.set_i32(NativeArray.make_i32(2, 0), 0, -9))
[-9, 0]Return an array with element i replaced by v. Value semantics: the original is unchanged as far as any other holder can tell. O(1) in place when the compiled program holds the only reference, otherwise an O(n) copy (the interpreter always copies). Panics if i is negative or >= length_int(arr).
Examples
march> NativeArray.to_list_int(NativeArray.set_int(NativeArray.from_list_int([1, 2, 3]), 0, 9))
[9, 2, 3]
march> let orig = NativeArray.from_list_int([1, 2, 3])
march> let changed = NativeArray.set_int(orig, 0, 100)
march> (NativeArray.to_list_int(orig), NativeArray.to_list_int(changed))
([1, 2, 3], [100, 2, 3])Return an array with element i set to v wrapped mod 256; the value conversion never traps. Same value semantics and cost as set_int. Panics if i is negative or >= length_u8(arr).
Examples
march> NativeArray.to_list_u8(NativeArray.set_u8(NativeArray.make_u8(2, 0), 1, 511))
[0, 255]Sort ascending by IEEE 754 totalOrder on binary32. Same algorithm, ownership and NaN / -0.0 placement as sort_float, on 32-bit keys: the elements are not widened to f64 to sort. Unstable, in place when the array is uniquely owned, so a threaded let a = NativeArray.sort_f32(a) allocates nothing.
-NaN < -Inf < ... < -0.0 < +0.0 < ... < +Inf < +NaN, so -0.0 sorts strictly before +0.0 although -0.0 == 0.0 is true.
march> NativeArray.get_f32(NativeArray.sort_f32(NativeArray.from_list_f32([2.5, -1.5, 0.5])), 0)
-1.5Sort ascending by IEEE 754 totalOrder. Same algorithm and ownership as sort_int: unstable, in place when the array is uniquely owned, so a threaded let a = NativeArray.sort_float(a) allocates nothing.
totalOrder is a total order on every bit pattern, NaN included: -NaN < -Inf < ... < -0.0 < +0.0 < ... < +Inf < +NaN. Two consequences that differ from the rest of the language:
-0.0sorts strictly before+0.0, although-0.0 == 0.0is true
and `compare(-0.0, 0.0)` is `0`.- A NaN goes to the end, or to the front if its sign bit is set.
<
is false for every NaN comparison and `compare` does not order NaN
consistently, so a sort built on either has no defined NaN placement.
march> NativeArray.get_float(NativeArray.sort_float(NativeArray.from_list_float([2.5, -1.5, 0.5])), 0)
-1.5Sort ascending (signed 32-bit order). The same ipnsort-style algorithm and ownership as sort_int, on 4-byte elements: unstable, in place when the array is uniquely owned, O(n) on sorted or reversed input, O(n log n) worst case.
march> NativeArray.to_list_i32(NativeArray.sort_i32(NativeArray.from_list_i32([3, -1, 2])))
[-1, 2, 3]Sort ascending. Unstable and in place when the array is uniquely owned, so a threaded let a = NativeArray.sort_int(a) allocates nothing.
ipnsort-style: a full-run scan makes already-sorted and reversed input O(n), an equal-partition makes low-cardinality input near-linear, and a heapsort fallback bounds the worst case at O(n log n). Measured 5-30x faster than libc qsort at 5M elements; see specs/progress/2026-09-25-native-array-sort-narrow-widths.md.
Unstable is unobservable here: i64 elements carry no payload to reorder.
march> NativeArray.to_list_int(NativeArray.sort_int(NativeArray.from_list_int([3, 1, 2])))
[1, 2, 3]Sort ascending. A counting sort over the 256 possible values: O(n + 256), no comparisons. Same ownership as sort_int: in place when the array is uniquely owned, so a threaded let a = NativeArray.sort_u8(a) allocates nothing.
march> NativeArray.to_list_u8(NativeArray.sort_u8(NativeArray.from_list_u8([200, 7, 255, 0])))
[0, 7, 200, 255]Sum all elements as a double (Float) accumulation over the already-rounded binary32 elements. O(n) tight loop.
Examples
march> NativeArray.sum_f32(NativeArray.from_list_f32([0.5, 0.25, 1.5]))
2.25Sum all elements (0.0 for an empty array). O(n) tight loop. Compiled code may reassociate the additions to vectorize, so results on inputs that are not exactly representable can differ in the last bits from a left-to-right sum.
Examples
march> NativeArray.sum_float(NativeArray.from_list_float([0.5, 1.5, 2.25]))
4.25Sum all elements, accumulated in Int (i64), so the sum does not wrap at 32 bits. O(n) tight loop.
Examples
march> NativeArray.sum_i32(NativeArray.from_list_i32([-5, 7, 10]))
12Sum all elements (0 for an empty array). O(n) tight loop.
Examples
march> NativeArray.sum_int(NativeArray.from_list_int([1, 2, 3, 4]))
10
march> NativeArray.sum_int(NativeArray.from_list_int([]))
0Sum all elements, accumulated in Int (i64), so the sum does not wrap at 255. O(n) tight loop.
Examples
march> NativeArray.sum_u8(NativeArray.from_list_u8([255, 255]))
510Widen every element to Float, returning a new NativeFloatArr. O(n).
Examples
march> NativeArray.to_list_float(NativeArray.to_float_arr(NativeArray.from_list_int([3, -2]))) == [3.0, -2.0]
trueConvert a NativeF32Arr to a List(Float) (exact widen), in index order. O(n).
Examples
march> NativeArray.to_list_f32(NativeArray.from_list_f32([0.5, -0.25]))
[0.5, -0.25]Convert a NativeFloatArr to a List(Float), in index order. O(n).
Examples
march> NativeArray.to_list_float(NativeArray.from_list_float([0.5, -1.5]))
[0.5, -1.5]Convert a NativeI32Arr to a List(Int) (exact widen), in index order. O(n).
Examples
march> NativeArray.to_list_i32(NativeArray.from_list_i32([3, -1]))
[3, -1]Convert a NativeIntArr to a List(Int), in index order. O(n).
Examples
march> NativeArray.to_list_int(NativeArray.from_list_int([5, 6, 7]))
[5, 6, 7]Convert a NativeU8Arr to a List(Int) (exact zero-extend), in index order. O(n).
Examples
march> NativeArray.to_list_u8(NativeArray.from_list_u8([0, 128, 255]))
[0, 128, 255]Convert a NativeU8Arr to NativeF32Arr (every u8 value is exact in binary32). O(n).
Examples
march> NativeArray.to_list_f32(NativeArray.u8_to_f32_arr(NativeArray.from_list_u8([0, 255]))) == [0.0, 255.0]
trueWiden a NativeU8Arr to NativeIntArr (exact zero-extend). O(n).
Examples
march> NativeArray.to_list_int(NativeArray.u8_to_int_arr(NativeArray.from_list_u8([255, 7])))
[255, 7]