# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 Andrew Hendel. Part of mojolearn, https://doi.org/10.5281/zenodo.22068632
"""`Array`: the NumPy-free container every public estimator returns.
DEVIATION 2301 -- the class `python/mojolearn/NUMPY_FREE_CONTRACT.md`
specifies. It is a typed, shaped, C- or F-contiguous block of memory that
OWNS its bytes (an `array.array`) or, through `from_buffer`, borrows them
from another buffer-protocol object it keeps alive. Its address never moves
for the whole lifetime of the object: the backing `array.array` is never
resized, so `buffer_info()[0]` taken at construction is the address for
good, which is what the Mojo side's borrow-an-address contract needs.
WHAT THIS IS NOT. No arithmetic operators, no broadcasting, no fancy
indexing. `min`, `max`, `sum` and `argmax` are HOST reductions in Python
floats, in element order, and are NOT part of any identity claim; the
kernels never use them. They exist so a caller without NumPy can look at a
result.
ZERO-COPY INTO NUMPY. `__array_interface__` (version 3) hands NumPy the
address, so `numpy.asarray(a)` is a view, never a copy, whenever a caller
has NumPy. `__buffer__` does the same for `memoryview(a)` on Python 3.12+;
on 3.10 and 3.11 a pure-Python class cannot export the buffer protocol at
all, so `memoryview(a)` raises TypeError there and the library's own code
never relies on it: `_buffer.view` recognizes `Array` and reads its backing
store directly on every supported Python (recorded in DEVIATION 2305).
FLOAT16 IS STORED, NOT COMPUTED. `'<f2'` is backed by an `array.array('H')`
and converted with `struct` on the way to Python floats; nothing here does
arithmetic in half precision.
"""
from __future__ import annotations
import array
import struct
import sys
from itertools import chain
# NumPy before 2.4 treats a null interface pointer as a scalar conversion,
# even for an empty shape. Keep a stable, non-null address for empty exports.
_EMPTY_ARRAY_STORAGE = array.array("B", [0] * 8)
# typestr -> array.array typecode of the backing store
_CODE = {
"<f4": "f", "<f8": "d", "<i4": "i", "<i8": "q",
"<u4": "I", "<u1": "B", "<f2": "H",
# lane/identical-lowbit-inference (2026-09-17): bf16 bits travel as
# uint16 and int8 codes as int8 (gemm/IDENTICAL_LOWBIT_CONTRACT.md).
"<u2": "H", "<i1": "b",
}
_ITEMSIZE = {"<f4": 4, "<f8": 8, "<i4": 4, "<i8": 8, "<u4": 4, "<u1": 1, "<f2": 2,
"<u2": 2, "<i1": 1}
_FLOAT = {"<f4", "<f8", "<f2"}
_INT = {"<i4", "<i8", "<u4", "<u1", "<u2", "<i1"}
SUPPORTED_DTYPES = tuple(_CODE)
# The store typecodes must have the sizes the typestrs promise. `'i'` is 4
# and `'q'` is 8 on every platform this library builds for; checked once at
# import rather than assumed.
for _ts, _c in _CODE.items():
if array.array(_c).itemsize != _ITEMSIZE[_ts]:
raise ImportError(
f"mojolearn: array.array typecode {_c!r} is "
f"{array.array(_c).itemsize} bytes on this platform, expected "
f"{_ITEMSIZE[_ts]} for {_ts!r}"
)
# lane/python-hotpath (2026-09-17, DEVIATIONS 3100-3102). dtype codes of the
# core helpers in `bindings/hotpath_helpers.mojo`; float16, bf16 bits and the
# 8/16-bit integers have no code and keep the Python routines. lane
# apple-fast-py2mojo-core (2026-10-03): every non-empty block of a coded
# dtype takes the helper, whatever its size (the `_NATIVE_MIN` cut that kept
# blocks under 256 elements in Python is gone from `_helper`; `_block_store`
# keeps it for its memcpy), and the integer sum is the helper's too
# (`_REDUCE_ISUM`, an exact 128-bit accumulator). The Python routines stay
# as the helpers' definitions: `_native_optional` answers None only for a
# binary that lacks the helper, the state the differential test's reference
# arm (`tests/test_hotpath_native.py`) plants.
_NATIVE_CODE = {"<f4": 0, "<f8": 1, "<i4": 2, "<i8": 3, "<u4": 4, "<u1": 5,
# lane py-runtime round 2: bf16 bits, int8 and float16 too
"<u2": 6, "<i1": 7, "<f2": 8}
#: equal_elements' exact int64-against-float64 code (lane py-runtime round 3)
_EQ_I64_F64 = 9
def _reference():
"""True only inside the differential test's reference arm
(`tests/test_hotpath_native.py::_python_arm` sets `_buffer._REFERENCE`):
the Python definitions of the core helpers then answer. A production
binary that lacks a helper is a build to redo, never a Python pass over
the elements (lane py-runtime round 2)."""
from . import _buffer
return _buffer._REFERENCE
def _rebuild(key):
return RuntimeError(f"mojolearn: this binary has no `{key}` helper (an older build); rebuild it")
_NATIVE_MIN = 256
_REDUCE_MIN, _REDUCE_MAX, _REDUCE_SUM, _REDUCE_ARGMAX, _REDUCE_INTEGRAL, _REDUCE_ISUM = range(6)
_SCALAR_TYPES = frozenset((float, int, bool))
_ROW_TYPES = frozenset((list, tuple))
_NOT = bytes((1, 0)) + bytes(range(2, 256))
def _helper(key, size):
"""The core helper `key` for a non-empty block, or None for an empty
block (whose Python routine answers or refuses without a loop) or a
binary that lacks the helper."""
if size <= 0:
return None
from . import _buffer
return _buffer._native_optional(key)
_INT_BOUNDS = {"<i4": (-(1 << 31), (1 << 31) - 1), "<i8": (-(1 << 63), (1 << 63) - 1),
"<u4": (0, (1 << 32) - 1), "<u1": (0, 255), "<u2": (0, 65535), "<i1": (-128, 127)}
def _exact_in(value, dtype):
"""`value` (a Python int, float or bool) as an element of `dtype` that
compares equal to it EXACTLY under Python's `==`, or None when no
element of `dtype` can equal it."""
if isinstance(value, bool):
value = int(value)
if dtype in _INT_BOUNDS:
if isinstance(value, float):
if value != value or value in (float("inf"), float("-inf")) or value != int(value):
return None
value = int(value)
lo, hi = _INT_BOUNDS[dtype]
return value if lo <= value <= hi else None
# float dtypes: a float element x equals value iff float(x) == value
if isinstance(value, int):
try:
f = float(value)
except OverflowError:
return None
if f != value:
return None # an int no double holds: no float element equals it
value = f
if value != value:
return None # NaN equals nothing
if dtype in ("<f4", "<f2"):
f = "<f" if dtype == "<f4" else "<e"
try:
g = struct.unpack(f, struct.pack(f, value))[0]
except OverflowError:
return None
if g != value:
return None
return g
return value
def normalize_dtype(dtype):
"""A typestr in `SUPPORTED_DTYPES` from a typestr, a NumPy dtype or
scalar type (when the caller has NumPy; nothing is imported here) or a
Python `float` / `int` / `bool` type. Unknown -> ValueError."""
if isinstance(dtype, str):
ts = _ALIAS.get(dtype, dtype)
if ts in _CODE:
return ts
raise ValueError(
f"mojolearn: unsupported dtype {dtype!r}; supported: "
f"{', '.join(SUPPORTED_DTYPES)}"
)
if dtype is float:
return "<f8"
if dtype is int:
return "<i8"
if dtype is bool:
return "<u1"
# numpy.dtype instances carry `.str`; numpy scalar types carry
# `__name__` ('float32'). Neither needs numpy imported to be read.
s = getattr(dtype, "str", None)
if isinstance(s, str):
return normalize_dtype(s)
n = getattr(dtype, "__name__", None)
if isinstance(n, str) and n in _ALIAS:
return _ALIAS[n]
raise ValueError(f"mojolearn: unsupported dtype {dtype!r}")
_ALIAS = {
"float32": "<f4", "float64": "<f8", "int32": "<i4", "int64": "<i8",
"uint32": "<u4", "uint8": "<u1", "float16": "<f2",
"uint16": "<u2", "int8": "<i1", "|i1": "<i1", "=u2": "<u2", "u2": "<u2", "i1": "<i1",
"|u1": "<u1", "=f4": "<f4", "=f8": "<f8", "=i4": "<i4", "=i8": "<i8",
"=u4": "<u4", "=f2": "<f2", "f4": "<f4", "f8": "<f8", "i4": "<i4",
"i8": "<i8", "u4": "<u4", "u1": "<u1", "f2": "<f2",
"float": "<f8", "int": "<i8", "bool": "<u1", "bool_": "<u1",
}
def _c_strides(shape, itemsize):
strides = []
acc = itemsize
for n in reversed(shape): # glue: one stride per array axis
strides.append(acc)
acc *= n
return tuple(reversed(strides))
def _f_strides(shape, itemsize):
strides = []
acc = itemsize
for n in shape: # glue: one stride per array axis
strides.append(acc)
acc *= n
return tuple(strides)
def _prod(shape):
n = 1
for s in shape: # glue: product of shape dimensions
n *= s
return n
def _check_shape(shape):
if isinstance(shape, int):
shape = (shape,)
shape = tuple(int(s) for s in shape) # glue: validates the shape argument
for s in shape: # glue: validates the shape argument
if s < 0:
raise ValueError(f"mojolearn: negative dimension in shape {shape}")
return shape
def _new_store(code, size):
"""A zero-filled `array.array` of `size` elements, allocated once."""
if size == 0:
return array.array(code)
return array.array(code, [0]) * size
def _to_py_values(mv, dtype):
"""Python scalars for a flat store memoryview."""
if dtype == "<f2":
n = len(mv)
return list(struct.unpack("<%de" % n, mv.tobytes())) if n else []
return mv.tolist()
def _strided_copy(src_addr, dst_addr, itemsize, shape, src_strides, dst_strides,
src_base=0, dst_base=0):
"""Element (i_0..i_k) of `shape`, in C order, moves from
src[src_base + sum i_j * src_strides[j]] to dst[dst_base + sum i_j *
dst_strides[j]] (strides and bases in elements, bits unchanged), in Mojo
(`strided_copy_bytes`, `bindings/array_helpers.mojo`; lane pyglue-sweep:
the per-row and per-column Python loops it replaces are gone). The GPU
base binding runs it on the device (lane cpu2-l1-input,
`core/input_device.mojo`); the host loop is the host column."""
from . import _buffer
dims = array.array("q", (*shape, *src_strides, *dst_strides, src_base, dst_base))
_buffer._native("strided_copy_bytes")(src_addr, dst_addr, dims.buffer_info()[0],
len(shape), itemsize)
def _reorder(a, src_order, out):
"""Copy the elements of `a` (laid out in `src_order`) into the store
`out` laid out in the other order: one Mojo strided copy."""
shape = a.shape
if len(shape) <= 1 or a.size == 0:
memoryview(out)[:] = a._mv
return
cs = _c_strides(shape, 1)
fs = _f_strides(shape, 1)
src, dst = (cs, fs) if src_order == "C" else (fs, cs)
_strided_copy(a._addr, out.buffer_info()[0], a.itemsize, shape, src, dst)
def _restore_array(raw, shape, dtype, order, readonly):
"""Pickle restores values and layout, never a borrowed pointer or GPU handle."""
if readonly:
flat = Array.from_buffer(memoryview(raw).cast(_CODE[dtype]))
return Array._view_of(flat, shape, order)
store = array.array(_CODE[dtype])
store.frombytes(raw)
return Array._owned(store, shape, dtype, order)
class _ArrayInterface:
"""Carries only an Array's `__array_interface__`, so `Array.__array__`
can hand NumPy a zero-copy view without recursing into itself. NumPy
keeps this object (and through it the Array) alive as the view's base."""
__slots__ = ("_owner", "__array_interface__")
def __init__(self, owner):
self._owner = owner
self.__array_interface__ = owner.__array_interface__
# A typed, shaped, contiguous block of memory. See the module docstring.
#
# `Array(shape, dtype)` allocates zeros; `empty`, `zeros`, `full`,
# `frombytes`, `from_list` and `from_buffer` are the usual constructors.
[docs]
class Array:
"""The array type mojolearn returns: typed, shaped, contiguous memory.
mojolearn does not require NumPy, so estimators return ``Array`` objects.
An ``Array`` is a C- or F-contiguous block that either owns its memory or,
through :meth:`from_buffer`, borrows it from another buffer it keeps alive.
Its memory address never changes during its lifetime.
With NumPy installed, ``numpy.asarray(a)`` is a zero-copy view (through
``__array_interface__``). On Python 3.12 and later ``memoryview(a)`` is
zero-copy too.
``Array`` is a container, not a NumPy replacement: there are no arithmetic
operators, no broadcasting and no fancy indexing. Indexing with ints and
slices is supported (a full integer index returns a Python scalar, anything
else a C-order copy), as are :meth:`tolist`, :meth:`reshape`,
:meth:`astype`, ``==`` and the reductions :meth:`min`, :meth:`max`,
:meth:`sum` and :meth:`argmax`, so you can inspect results without NumPy.
``'<f2'`` (float16) values are stored, not computed with.
Parameters
----------
shape : int or tuple of int
Shape of the new, zero-filled array.
dtype : str, default "<f4"
One of ``"<f4"``, ``"<f8"``, ``"<f2"``, ``"<i4"``, ``"<i8"``, ``"<i1"``,
``"<u4"``, ``"<u2"``, ``"<u1"``.
Attributes
----------
shape : tuple of int
dtype : str
ndim, size, nbytes, itemsize : int
strides : tuple of int
order : {"C", "F"}
flags : dict
``C_CONTIGUOUS``, ``F_CONTIGUOUS`` and ``WRITEABLE``.
Examples
--------
>>> from mojolearn import Array
>>> a = Array.from_list([[1.0, 2.0], [3.0, 4.0]], "<f4")
>>> a.shape, a.sum()
((2, 2), 10.0)
>>> import numpy as np
>>> np.asarray(a).dtype
dtype('float32')
"""
__slots__ = (
"_store", "_base", "_pin", "_mv", "_addr", "_readonly",
"shape", "dtype", "ndim", "size", "nbytes", "itemsize", "strides",
"order",
)
[docs]
def __init__(self, shape, dtype="<f4"):
dtype = normalize_dtype(dtype)
shape = _check_shape(shape)
store = _new_store(_CODE[dtype], _prod(shape))
self._init_owned(store, shape, dtype, "C")
# ------------------------------------------------------------ construction
def _init_owned(self, store, shape, dtype, order):
self._store = store
self._base = None
self._pin = None
self._mv = memoryview(store)
if self._mv.format != _CODE[dtype]:
self._mv = self._mv.cast("B").cast(_CODE[dtype])
self._addr = store.buffer_info()[0]
self._readonly = False
self._set_meta(shape, dtype, order)
return self
def _set_meta(self, shape, dtype, order):
self.shape = shape
self.dtype = dtype
self.ndim = len(shape)
self.size = _prod(shape)
self.itemsize = _ITEMSIZE[dtype]
self.nbytes = self.size * self.itemsize
self.order = order
if order == "C":
self.strides = _c_strides(shape, self.itemsize)
else:
self.strides = _f_strides(shape, self.itemsize)
if len(self._mv) != self.size:
raise ValueError(
f"mojolearn: buffer holds {len(self._mv)} elements, shape "
f"{shape} needs {self.size}"
)
@classmethod
def _owned(cls, store, shape, dtype, order="C"):
self = cls.__new__(cls)
return self._init_owned(store, shape, dtype, order)
@classmethod
def _view_of(cls, other, shape, order="C"):
"""Another shape over the SAME memory (no copy, no ownership
transfer): `other` stays alive through `_base`."""
self = cls.__new__(cls)
self._store = other._store
self._base = other
self._pin = other._pin
self._mv = other._mv
self._addr = other._addr
self._readonly = other._readonly
self._set_meta(shape, other.dtype, order)
return self
[docs]
@classmethod
def from_buffer(cls, obj):
"""A zero-copy view of any contiguous buffer-protocol object.
The result keeps ``obj`` alive but does not own it; writes through the
result reach ``obj``.
Parameters
----------
obj : buffer-protocol object
For example a NumPy array, ``array.array``, ``bytearray`` or
``memoryview``.
Returns
-------
Array
Raises
------
ValueError, TypeError
For a non-contiguous buffer or an unsupported format (no copy is made).
"""
from . import _buffer # lazy: _buffer imports this module
if isinstance(obj, Array):
return cls._view_of(obj, obj.shape, obj.order)
with _buffer.view(obj, name="object") as b:
dtype = _buffer.typestr_of(b)
if dtype not in _CODE:
raise TypeError(
f"mojolearn: buffer format {b.format!r} (itemsize "
f"{b.itemsize}) is not a supported Array dtype"
)
if b.c_contiguous:
order = "C"
elif b.f_contiguous:
order = "F"
else:
raise ValueError(
"mojolearn: from_buffer needs a contiguous buffer; "
f"got shape {b.shape} strides {b.strides}"
)
self = cls.__new__(cls)
self._store = obj
self._base = None
# a memoryview of the exporter pins its buffer for our lifetime
self._pin = memoryview(obj)
self._addr = b.addr
self._readonly = b.readonly
self._mv = _buffer.memory_at(
b.addr, b.nbytes, writable=not b.readonly
).cast(_CODE[dtype])
self._set_meta(b.shape, dtype, order)
return self
[docs]
@classmethod
def from_list(cls, nested, dtype="<f8"):
"""A new C-order Array from a (nested) list of Python numbers.
The shape is inferred and must be rectangular. Floats converted to
``"<f4"`` round to nearest even. Floats are not converted to integer dtypes
(an error is raised instead of truncating), and integers too large for
the dtype raise an error rather than wrap.
Parameters
----------
nested : list
Nested lists of numbers.
dtype : str, default "<f8"
Element type.
Returns
-------
Array
"""
dtype = normalize_dtype(dtype)
shape, flat = _flatten(nested)
return cls._from_flat(flat, shape, dtype)
@classmethod
def _from_flat(cls, flat, shape, dtype):
code = _CODE[dtype]
if dtype == "<f2":
store = array.array("H")
if len(flat):
store.frombytes(struct.pack("<%de" % len(flat), *flat))
else:
try:
store = array.array(code, flat)
except (TypeError, OverflowError) as e:
raise TypeError(
f"mojolearn: cannot build a {dtype} Array from these "
f"values: {e}"
) from None
return cls._owned(store, shape, dtype, "C")
# ------------------------------------------------------------- interface
def _both_orders(self):
"""True when C and F layouts coincide: at most one dimension is
larger than 1 (NumPy's rule), so either label is free."""
return sum(1 for s in self.shape if s != 1) <= 1 # glue: counts non-unit shape dimensions
def _has_order(self, order):
return self.order == order or self._both_orders()
@property
def flags(self):
"""``{"C_CONTIGUOUS", "F_CONTIGUOUS", "WRITEABLE"}`` as booleans."""
both = self._both_orders()
return {
"C_CONTIGUOUS": self.order == "C" or both,
"F_CONTIGUOUS": self.order == "F" or both,
"WRITEABLE": not self._readonly,
}
@property
def __array_interface__(self):
# Version 3. `strides` None means C-contiguous; an F-order block
# reports its real strides so NumPy views it without copying.
return {
"shape": self.shape,
"typestr": self.dtype,
"data": (self._addr if self.size or self._addr else
_EMPTY_ARRAY_STORAGE.buffer_info()[0], bool(self._readonly)),
"strides": None if self.order == "C" else self.strides,
"version": 3,
}
def __array__(self, dtype=None, copy=None):
"""`numpy.asarray(a)` through the `__array__` protocol.
NumPy itself reads `__array_interface__` above and never gets here;
this exists for the libraries that test `hasattr(x, "__array__")`
before converting. scikit-learn's `type_of_target` (reached by
`accuracy_score`, `f1_score`, `confusion_matrix`, ...) refused a
classifier's `predict` output as "Expected array-like" without it
(Sep 22 pip smoke). Only NumPy calls this method, so NumPy is
already loaded: it is read from `sys.modules`, never imported, and
the module keeps its no-NumPy import contract.
"""
np = sys.modules.get("numpy")
if np is None:
raise TypeError("mojolearn: Array.__array__ needs NumPy loaded")
out = np.asarray(_ArrayInterface(self))
if dtype is not None and out.dtype != np.dtype(dtype):
if copy is False:
raise ValueError("mojolearn: a dtype change needs a copy")
return out.astype(dtype)
return out.copy() if copy else out
def __buffer__(self, flags):
"""Python 3.12+ buffer protocol: a memoryview of the backing store
with this Array's shape.
An F-order block of rank 2+ and a `'<f2'` block RAISE BufferError:
a memoryview built from Python cannot carry column-major strides or
the `'e'` format, and handing out the flat store instead would be
wrong in a way that matters. NumPy consults the buffer protocol
BEFORE `__array_interface__` (measured on numpy 2.4 / CPython
3.14), so a flat view here would make `numpy.asarray` return a 1-D
array for a matrix; on the BufferError it falls through to
`__array_interface__`, which describes both cases exactly and is
still zero-copy. `tobytes()` and `_flat()` reach the raw store.
"""
if self.dtype == "<f2":
raise BufferError(
"mojolearn: a float16 Array has no memoryview format; use "
"numpy.asarray (zero-copy) or tolist()"
)
if self.ndim == 0:
return self._mv
if self.order != "C" and self.ndim > 1:
raise BufferError(
"mojolearn: an F-order Array cannot be exported as a "
"memoryview; use numpy.asarray (zero-copy), tobytes() or "
"reshape() for a C-order copy"
)
if self.size == 0:
# CPython refuses `cast(fmt, shape)` with a zero in the shape.
# A 1-D empty Array exports as a typed empty view (so `bytes()`
# and `memoryview()` work); an empty matrix raises BufferError
# so `numpy.asarray` falls through to `__array_interface__`,
# which carries the (0, k) shape exactly (found 2026-09-10 by
# the DEVIATION 2489 gate on an all-zero affinity matrix).
if self.ndim == 1:
return self._mv.cast("B").cast(_CODE[self.dtype])
raise BufferError(
"mojolearn: an empty matrix has no memoryview shape; use "
"numpy.asarray (zero-copy), tobytes() or reshape(-1)"
)
return self._mv.cast("B").cast(_CODE[self.dtype], self.shape)
def _flat(self):
"""A 1-D Array over the storage IN STORAGE ORDER, no copy. For an
F-order matrix that is the column-major flat, `flat[f * rows + r]
== a[r, f]`; the shape used by the gbdt quantizer."""
return Array._view_of(self, (self.size,), "C")
# ------------------------------------------------------------- basic ops
def __reduce__(self):
return (_restore_array, (self.tobytes(), self.shape, self.dtype,
self.order, self._readonly))
[docs]
def tobytes(self):
"""The raw bytes, in storage order (row-major for a C-order Array, column-major for an F-order one)."""
return self._mv.tobytes()
def _values(self):
return _to_py_values(self._mv, self.dtype)
def _as_c(self):
"""Self if C-order, else a C-order copy."""
if self.order == "C":
return self
if self._both_orders():
return Array._view_of(self, self.shape, "C")
out = _new_store(_CODE[self.dtype], self.size)
_reorder(self, "F", out)
return Array._owned(out, self.shape, self.dtype, "C")
def _as_order(self, order):
"""Self when already laid out as `order`, a relabeled view when
both layouts coincide, else a copy in `order`."""
if order == self.order:
return self
if self._both_orders():
return Array._view_of(self, self.shape, order)
if order == "C":
return self._as_c()
out = _new_store(_CODE[self.dtype], self.size)
_reorder(self, "C", out)
return Array._owned(out, self.shape, self.dtype, "F")
[docs]
def tolist(self):
"""The values as nested Python lists (a scalar for a 0-d Array)."""
a = self._as_c()
if self.ndim == 0:
return a._values()[0]
return _nest(a)
[docs]
def copy(self):
"""A new Array with the same dtype, shape, order and values."""
store = array.array(_CODE[self.dtype])
store.frombytes(self._mv.cast("B")) # one memcpy
return Array._owned(store, self.shape, self.dtype, self.order)
[docs]
def astype(self, dtype):
"""A converted copy (always a copy, even for the same dtype).
Float to ``"<f4"`` rounds to nearest even; float to ``"<f8"`` and integers
up to 2**53 to float are exact. Float to integer truncates toward zero, like
NumPy. The memory order is kept.
Parameters
----------
dtype : str
Target element type.
Returns
-------
Array
"""
dtype = normalize_dtype(dtype)
if dtype == self.dtype:
return self.copy()
src = self.dtype
code = _CODE[dtype]
if self.size == 0:
return Array._owned(_new_store(code, 0), self.shape, dtype, self.order)
fast = self._native_astype(dtype)
if fast is not None:
return fast
if _reference():
return self._astype_reference(dtype)
if _helper("cast_elements", self.size) is None:
raise _rebuild("cast_elements")
# the helper refused an element (a NaN, an infinity or a value
# outside the target): the Python definition raises its own words
# (it never returns here: every element it accepts, the helper did)
return self._astype_reference(dtype)
def _astype_reference(self, dtype):
"""`astype`'s Python definition (the differential test's reference)."""
src = self.dtype
code = _CODE[dtype]
if src == "<f2" or dtype == "<f2":
values = self._values()
if dtype in _INT:
values = [int(v) for v in values] # cpu-route: the refusal path and the test reference arm only
out = Array._from_flat(values, self.shape, dtype)
elif dtype in _FLOAT:
# C-level loop: array.array's item setter converts each element
# exactly as `(float)` / `(double)` would.
out = Array._owned(
array.array(code, self._mv), self.shape, dtype, "C"
)
elif src in _INT:
try:
store = array.array(code, self._mv)
except OverflowError as e:
raise OverflowError(
f"mojolearn: value does not fit {dtype}: {e}"
) from None
out = Array._owned(store, self.shape, dtype, "C")
else:
values = [int(v) for v in self._mv] # cpu-route: the refusal path and the test reference arm only
out = Array._from_flat(values, self.shape, dtype)
out.order = self.order
out._set_meta(self.shape, dtype, self.order)
return out
def _native_astype(self, dtype):
"""DEVIATION 3100: `astype` through the core helper `cast_elements`,
or None. The routine below it is `array.array(code, <memoryview>)`,
a C loop that builds one Python object per element (22 to 31 ns);
the helper performs the same single conversion per element with no
object. When the helper meets an element the routine below REFUSES
(an integer outside the target, a NaN or infinity headed for an
integer dtype) it reports so and the routine below runs and raises
its own words."""
sc = _NATIVE_CODE.get(self.dtype)
dc = _NATIVE_CODE.get(dtype)
if sc is None or dc is None:
return None
fn = _helper("cast_elements", self.size)
if fn is None:
return None
from . import _buffer
store = _buffer._output_store(_CODE[dtype], self.size)
if int(fn(self._addr, sc, store.buffer_info()[0], dc, self.size)):
return None
return Array._owned(store, self.shape, dtype, self.order)
[docs]
def reshape(self, shape):
"""A C-order view with a new shape (one dimension may be ``-1``).
An F-order Array is copied to C order first, as NumPy does.
Parameters
----------
shape : int or tuple of int
Returns
-------
Array
"""
if isinstance(shape, int):
shape = (shape,)
shape = [int(s) for s in shape] # glue: validates the shape argument
if shape.count(-1) > 1:
raise ValueError("mojolearn: reshape allows one -1")
if -1 in shape:
known = _prod(s for s in shape if s != -1) # glue: product of the known dimensions
if known == 0 or self.size % known:
raise ValueError(
f"mojolearn: cannot reshape size {self.size} into {tuple(shape)}"
)
shape[shape.index(-1)] = self.size // known
shape = tuple(shape)
if _prod(shape) != self.size:
raise ValueError(
f"mojolearn: cannot reshape size {self.size} into {shape}"
)
return Array._view_of(self._as_c(), shape, "C")
[docs]
def ravel(self):
"""A 1-D view, same as ``reshape(-1)``."""
return self.reshape((-1,))
def __len__(self):
if self.ndim == 0:
raise TypeError("len() of a 0-d Array")
return self.shape[0]
def __iter__(self):
if self.ndim == 0:
raise TypeError("iteration over a 0-d Array")
if self.ndim == 1:
return iter(self._values())
return _RowIter(self._as_c())
def _row_view(self, i, shape, step):
"""Row `i` of a C-order Array as a zero-copy view (`shape` = the
trailing dimensions, `step` = their element count): the same
memory, kept alive through `_base`, as NumPy's iteration gives."""
v = Array.__new__(Array)
v._store = self._store
v._base = self
v._pin = self._pin
off = i * step
v._mv = self._mv[off:off + step]
v._addr = self._addr + off * self.itemsize
v._readonly = self._readonly
v._set_meta(shape, self.dtype, "C")
return v
def __repr__(self):
return f"Array(shape={self.shape}, dtype={self.dtype!r})"
def __bool__(self):
if self.size != 1:
raise ValueError(
"mojolearn: the truth value of an Array with more than one "
"element is ambiguous"
)
return bool(self._values()[0])
# ------------------------------------------------------------- indexing
def __getitem__(self, key):
"""Int / slice / tuple of them. Full integer indexing returns a
Python scalar; anything else a COPY as a C-order Array."""
a = self._as_c()
if not isinstance(key, tuple):
key = (key,)
if len(key) > a.ndim:
raise IndexError(
f"mojolearn: too many indices ({len(key)}) for shape {a.shape}"
)
dims = [] # (start, step, count) or int
for axis, k in enumerate(key): # glue: one index entry per array axis
n = a.shape[axis]
if isinstance(k, slice):
start, stop, step = k.indices(n)
count = len(range(start, stop, step))
dims.append((start, step, count))
elif isinstance(k, int) or hasattr(k, "__index__"):
k = k.__index__()
if k < -n or k >= n:
raise IndexError(
f"mojolearn: index {k} out of range for axis {axis} "
f"of size {n}"
)
dims.append(k % n if n else k)
else:
raise TypeError(
"mojolearn: Array indices are ints, slices or tuples of "
f"them, not {type(k).__name__}"
)
for axis in range(len(key), a.ndim): # glue: one entry per remaining array axis
dims.append((0, 1, a.shape[axis]))
cs = _c_strides(a.shape, 1)
# The selection as one strided copy (shape, strides, base offset in
# elements), done in Mojo; a contiguous block is one memcpy.
base_off = 0
sel_shape = []
sel_strides = []
for axis, d in enumerate(dims): # glue: one entry per array axis
if isinstance(d, tuple):
base_off += d[0] * cs[axis]
sel_shape.append(d[2])
sel_strides.append(d[1] * cs[axis])
else:
base_off += d * cs[axis]
out_shape = tuple(sel_shape)
if not out_shape:
return _to_py_values(a._mv[base_off:base_off + 1], a.dtype)[0]
block = _contiguous_block(dims, a.shape, cs)
store = _block_store(a, block[0], block[1]) if block is not None else None
if store is None:
total = _prod(out_shape)
store = _new_store(_CODE[a.dtype], total)
if total:
_strided_copy(a._addr, store.buffer_info()[0], a.itemsize, out_shape,
sel_strides, _c_strides(out_shape, 1), base_off, 0)
if a.dtype == "<f4":
# The contiguous block above (`_buffer._same_dtype_store`)
# quiets a float32 signaling NaN, as the item setter does;
# a strided selection gets the same in-place
# `cast_elements` float32 -> float32 so a copy's bits do
# not depend on the selection's layout.
from . import _buffer
fn = _buffer._native_optional("cast_elements")
if fn is not None:
at = store.buffer_info()[0]
fn(at, 0, at, 0, total) # in place: slot i reads and writes slot i only
return Array._owned(store, out_shape, a.dtype, "C")
def __eq__(self, other):
"""Elementwise equality against an Array of the same shape or a
scalar, as a `'<u1'` Array of 0/1."""
if isinstance(other, Array) and other.shape == self.shape:
fast = self._native_eq(other)
if fast is not None:
return fast
elif isinstance(other, (int, float, bool)):
if self.size == 0:
return Array._owned(array.array("B"), self.shape, "<u1", "C")
fast = self._native_eq_scalar(other)
if fast is not None:
return fast
if not _reference():
raise _rebuild("equal_elements")
if isinstance(other, Array) and other.shape == self.shape and not _reference():
if self.size == 0:
return Array._owned(array.array("B"), self.shape, "<u1", "C")
if other.dtype == self.dtype:
raise _rebuild("equal_elements")
# two dtypes (lane py-runtime round 2): both widened EXACTLY to one
# dtype by the cast helper, then one native comparison. int64
# against a float is the one pair no exact widening covers.
a_int, b_int = self.dtype in _INT, other.dtype in _INT
wide = "<i8" if (a_int and b_int) else ("<f8" if "<i8" not in (self.dtype, other.dtype) else None)
if wide is not None:
return self.astype(wide).__eq__(other.astype(wide))
# int64 against a float (lane py-runtime round 3): the float side
# widened exactly to float64, then the helper's exact int == float
fn = _helper("equal_elements", self.size)
if fn is None:
raise _rebuild("equal_elements")
ints, flts = (self, other) if self.dtype in _INT else (other, self)
ia = ints.astype("<i8")._as_c() if ints.dtype != "<i8" else ints._as_c()
fa = flts.astype("<f8")._as_c() if flts.dtype != "<f8" else flts._as_c()
store = array.array("B", bytes(self.size))
fn(ia._addr, fa._addr, _EQ_I64_F64, self.size, store.buffer_info()[0])
return Array._owned(store, self.shape, "<u1", "C")
mine = self._as_c()._values()
if isinstance(other, Array):
if other.shape != self.shape:
raise ValueError(
f"mojolearn: shapes {self.shape} and {other.shape} differ"
)
theirs = other._as_c()._values()
bits = [1 if x == y else 0 for x, y in zip(mine, theirs)] # cpu-route: the differential test reference arm only
elif isinstance(other, (int, float, bool)):
bits = [1 if x == other else 0 for x in mine] # cpu-route: the differential test's reference arm only
else:
return NotImplemented
return Array._owned(array.array("B", bits), self.shape, "<u1", "C")
def _native_eq(self, other):
"""DEVIATION 3102: elementwise equality of two same-shape Arrays of
ONE dtype through the core helper `equal_elements`, or None. IEEE
equality is Python's float equality; two dtypes (an int against a
float compares exactly in Python) keep the Python routine."""
code = _NATIVE_CODE.get(self.dtype)
if code is None or other.dtype != self.dtype:
return None
fn = _helper("equal_elements", self.size)
if fn is None:
return None
a = self._as_c()
b = other._as_c()
store = array.array("B", bytes(self.size))
fn(a._addr, b._addr, code, self.size, store.buffer_info()[0])
return Array._owned(store, self.shape, "<u1", "C")
def _native_eq_scalar(self, other):
"""lane apple-fast-py2mojo-core: equality against a Python scalar
through `equal_elements`, the scalar repeated in C as this dtype, or
None. Python compares exactly (an int against a float included), so
the scalar is first taken to this dtype EXACTLY; a scalar no element
can equal (NaN, a non-integer against an integer dtype, a value out
of the dtype's range or not representable in float32) answers all
zeros without a pass."""
code = _NATIVE_CODE.get(self.dtype)
if code is None:
return None
fn = _helper("equal_elements", self.size)
if fn is None:
return None
v = _exact_in(other, self.dtype)
store = array.array("B", bytes(self.size))
if v is not None:
a = self._as_c()
fill = array.array(_CODE[self.dtype], [v]) * self.size
fn(a._addr, fill.buffer_info()[0], code, self.size, store.buffer_info()[0])
return Array._owned(store, self.shape, "<u1", "C")
def __ne__(self, other):
eq = self.__eq__(other)
if eq is NotImplemented:
return eq
# `bytes.translate` flips 0 and 1 in C (was a comprehension per element)
return Array._owned(
array.array("B", eq._mv.tobytes().translate(_NOT)), self.shape, "<u1", "C"
)
__hash__ = None
# ------------------------------------------------------------ reductions
# HOST reductions in Python scalars, sequential in storage order. They
# are NOT part of any identity claim and no kernel path uses them.
def _reduce_values(self, what):
if self.size == 0:
raise ValueError(f"mojolearn: {what} of an empty Array")
return self._values()
def _native_reduce(self, what):
"""DEVIATION 3101: `what` through the core helper `reduce_stat`, or
None. Sequential in storage order with Python's own comparison, so a
NaN or a signed zero answers as `min(list)` / `max(list)` does."""
code = _NATIVE_CODE.get(self.dtype)
if code is None:
return None
fn = _helper("reduce_stat", self.size)
if fn is None:
return None
return fn(self._addr, code, self.size, what)
def _no_reduce(self, what):
"""An empty Array refuses `what`; else the helper is missing: the
reference arm answers in Python, a production binary is rebuilt."""
if self.size == 0:
raise ValueError(f"mojolearn: {what} of an empty Array")
if not _reference():
raise _rebuild("reduce_stat")
[docs]
def min(self):
"""Smallest element, in storage order; ValueError for an empty Array."""
fast = self._native_reduce(_REDUCE_MIN)
if fast is not None:
return fast
self._no_reduce("min")
return min(self._reduce_values("min")) # cpu-route: the differential test's reference arm only
[docs]
def max(self):
"""Largest element, in storage order; ValueError for an empty Array."""
fast = self._native_reduce(_REDUCE_MAX)
if fast is not None:
return fast
self._no_reduce("max")
return max(self._reduce_values("max")) # cpu-route: the differential test's reference arm only
[docs]
def sum(self):
"""Sum of all elements: an exact ``int`` for integer dtypes, a float summed in storage order for float dtypes."""
if self.size == 0:
return 0 if self.dtype in _INT else 0.0
if self.dtype in ("<f4", "<f8", "<f2"):
fast = self._native_reduce(_REDUCE_SUM)
if fast is not None:
return fast
elif self.dtype in _NATIVE_CODE:
# lane apple-fast-py2mojo-core: the exact integer sum in the
# helper, (hi, lo >> 32, lo & 0xffffffff) of a 128-bit total
parts = self._native_reduce(_REDUCE_ISUM)
if parts is not None:
hi, mid, low = (int(v) for v in parts) # glue: three words of a 128-bit sum
return (hi << 64) + (mid << 32) + low
if not _reference():
raise _rebuild("reduce_stat")
values = self._values()
if self.dtype in _INT:
return sum(values) # cpu-route: the differential test's reference arm only
acc = 0.0
for v in values: # cpu-route: the differential test's reference arm only
acc += v
return acc
[docs]
def argmax(self):
"""Flat (C-order) index of the first maximum."""
fast = self._as_c()._native_reduce(_REDUCE_ARGMAX)
if fast is not None:
return fast
self._no_reduce("argmax")
values = self._as_c()._reduce_values("argmax")
best = 0
best_v = values[0]
for i in range(1, len(values)): # cpu-route: the differential test's reference arm only
v = values[i]
if v > best_v:
best = i
best_v = v
return best
class _RowIter:
"""Iteration over the rows of a C-order Array of rank >= 2: each step
hands out a zero-copy view of the next row (no element is read)."""
__slots__ = ("_a", "_i", "_n", "_shape", "_step")
def __init__(self, a):
self._a = a
self._i = 0
self._n = a.shape[0]
self._shape = a.shape[1:]
self._step = _prod(self._shape)
def __iter__(self):
return self
def __next__(self):
i = self._i
if i >= self._n:
raise StopIteration
self._i = i + 1
return self._a._row_view(i, self._shape, self._step)
def __length_hint__(self):
return self._n - self._i
def _contiguous_block(dims, shape, cs):
"""`(offset, length)` in elements when the selection `dims` of a C-order
block is ONE contiguous run whose C-order walk is the run itself:
leading integer indices, then at most one step-1 slice, then only whole
axes. None otherwise (a stride, a slice after a partial slice)."""
offset = 0
axis = 0
n = len(dims)
while axis < n and not isinstance(dims[axis], tuple):
offset += dims[axis] * cs[axis]
axis += 1
if axis == n:
return None
start, step, count = dims[axis]
if step != 1:
return None
for later in range(axis + 1, n): # glue: one check per trailing array axis
d = dims[later]
if not isinstance(d, tuple) or d != (0, 1, shape[later]):
return None
return offset + start * cs[axis], count * cs[axis]
def _block_store(a, offset, length):
"""DEVIATION 3105: an owned store holding `length` elements of `a` from
`offset`, equal byte for byte to the per-run `array.array(code,
<memoryview slice>)` copies it replaces, or None when that needs the
helper and the helper is absent. One memcpy instead of one Python object
per element (0.2 ns against 22); see `_buffer._same_dtype_store` for the
one float32 subtlety."""
if length < _NATIVE_MIN:
return None # a short run: the per-element copy is as fast as the detour
from . import _buffer
raw = a._mv[offset:offset + length].cast("B")
return _buffer._same_dtype_store(raw, a.dtype)
def _flatten_fast(nested):
"""DEVIATION 3106: `_flatten` for the two shapes every estimator input
takes, a flat list of Python scalars and a list of equal-length rows of
them, with the leaf test and the row walk done by C builtins instead of
one recursive call per leaf (1.5 s per 10,000,000 leaves). None for
anything else (a ragged block, a NumPy scalar, a nested Array, rank 3),
which `_flatten` then walks and, where it must, refuses in its words."""
if type(nested) not in _ROW_TYPES or not nested:
return None
kinds = set(map(type, nested))
if kinds <= _SCALAR_TYPES:
return (len(nested),), list(nested)
if not kinds <= _ROW_TYPES:
return None
widths = set(map(len, nested))
if len(widths) != 1:
return None
flat = list(chain.from_iterable(nested))
if not set(map(type, flat)) <= _SCALAR_TYPES:
return None
return (len(nested), widths.pop()), flat
def _flatten(nested):
"""`(shape, flat_list)` for a rectangular nested sequence; a scalar
gives shape ()."""
if isinstance(nested, Array):
return nested.shape, nested._as_c()._values()
fast = _flatten_fast(nested)
if fast is not None:
return fast
if isinstance(nested, (str, bytes)) or not hasattr(nested, "__len__"):
return (), [nested]
seq = list(nested)
if not seq:
return (0,), []
first_shape, flat = _flatten(seq[0])
for item in seq[1:]: # cpu-route: flattens a ragged user Python list, the explicit input step
shape, vals = _flatten(item)
if shape != first_shape:
raise ValueError(
"mojolearn: nested sequence is not rectangular "
f"({shape} vs {first_shape})"
)
flat.extend(vals)
return (len(seq),) + first_shape, flat
def _nest(a):
"""Nested Python lists of a C-order Array, built by `memoryview.tolist`
(CPython walks the rows in C; a float16 block goes through its exact
float64 values first)."""
if a.size == 0:
return _empty_nest(a.shape)
if a.dtype == "<f2":
flat = array.array("d", a._values())
return memoryview(flat).cast("B").cast("d", a.shape).tolist()
return a._mv.cast("B").cast(_CODE[a.dtype], a.shape).tolist()
def _empty_nest(shape):
"""The nested lists of an empty block: no elements, only the list
structure up to the first zero dimension."""
if not shape or shape[0] == 0:
return []
return [_empty_nest(shape[1:]) for _ in range(shape[0])] # glue: list structure of an empty block, no elements