// The storage-backend machine: everything that reads or changes which
// backend a column sits on, in four faces —
//
// - converge `try_column_to_numeric` the total content → `Numeric` move
// that every canonicalising path lands through
// - preserve `preserve_backend` re-converge a freshly rebuilt
// column to its source's backend instead of deriving one from content
// - read `validity_bools` a column's validity as a dense
// `Array[Bool]`, skipping the synthetic bitmap a `Numeric` column has
// none of
// - assert `Series::storage_kind` /
// `Series::is_canonical` the invariant readers `frame`'s
// tests assert through (declared exceptions: no production caller
// outside this package)
//
// The `pub` entries are `#doc(hidden)` + `#internal(engine)` engine seams,
// with pinned signatures and consumers. The
// differential parity harness guards exactly this
// machine: the same cells must behave identically whichever backend they
// sit on.
///|
/// Best-effort, **total** move of one column onto the `Numeric` fast-path
/// backend: an all-valid Int / Float `Builtin` column is rebuilt on
/// `NumericColumn`; every other column — already `Numeric`, carrying nulls,
/// or non-numeric — is returned unchanged. This is the whole of how a column
/// reaches that backend after construction: the canonicalisation in the row
/// gathers (`gather_series` / `Series::gather` / `rebuild_options`), in
/// `preserve_backend`, and in the expression engine's computed-column
/// convergence. Total by design, so a column that cannot move is simply kept
/// rather than failing (and rather than leaving a dead error arm,
/// `feedback_lib_no_abort`). The null-free Int /
/// Float arms rebuild through the `NumericColumn` constructors, which copy
/// their input, ingestion being immutable — one extra `O(n)`
/// array copy per converged column. (The buffer-free move the column package
/// used to offer alongside it is gone: nothing outside `series` converged a
/// column, so the entry point went with it.)
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn Series::try_column_to_numeric(self : Series) -> Series {
match self.storage.kind() {
@column.StorageKind::Numeric => self
@column.StorageKind::Builtin =>
match self.storage.data() {
// The `Numeric` backend is dtype-less: only a *numeric* dtype (the
// registry's `numeric` flag) with no nulls may converge onto it, so
// an all-valid `Date` column — also I64-typed, also null-free —
// keeps its `Builtin` buffer instead of losing its identity on the
// fast path.
@column.ColumnData::I64(a) if self.dtype().is_numeric() &&
self.storage.null_count() == 0 =>
Series::new(
self.name,
@column.ColumnStorage::Numeric(@column.NumericColumn::from_ints(a)),
)
@column.ColumnData::F64(a) if self.storage.null_count() == 0 =>
Series::new(
self.name,
@column.ColumnStorage::Numeric(
@column.NumericColumn::from_floats(a),
),
)
// Everything else stays `Builtin` as-is: a null-free Int / Float
// column never reaches this arm (its guard passed), so the arm
// carries Date (numericity false), any null-carrying Int / Float,
// and Bool / String.
@column.ColumnData::I64(_)
| @column.ColumnData::F64(_)
| @column.ColumnData::Bool(_)
| @column.ColumnData::Utf8(_) => self
}
}
}
///|
/// Re-converge a freshly rebuilt (`Builtin`-backed) column to `source`'s
/// backend: a `Numeric` source's all-valid rebuild moves back onto the
/// `Numeric` fast path, a `Builtin` source's stays `Builtin`. This is the
/// *preserve* class — the transforms that must hold their source's backend
/// rather than derive one
/// from content, `fill_null` being the one that needs it (its filled column is
/// built straight on `Builtin`). A caller whose input already canonicalised —
/// the join assembly, over a `rebuild_options` result — gets a no-op
/// passthrough; the slices preserve the backend by sharing the validity bitmap
/// and never reach here.
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn Series::preserve_backend(self : Series, rebuilt : Series) -> Series {
match self.storage.kind() {
@column.StorageKind::Numeric => rebuilt.try_column_to_numeric()
@column.StorageKind::Builtin => rebuilt
}
}
///|
/// Materialise a column's validity as a dense `Array[Bool]` (`true = valid`)
/// for a bounded scan. The `Numeric` arm builds the all-`true` array
/// directly, skipping the synthetic all-valid `Bitmap` that
/// `ColumnStorage::validity()` would allocate for a backend that has none —
/// a `Numeric` column is present in every slot by construction. Equivalent
/// to `storage.validity().to_bools()` but without the intermediate bitmap on
/// the fast path. The row-level scanners (`sort` and `drop_nulls`; `group_by`
/// and `join` build keys from `to_scalars` instead) and the elementwise
/// expression kernels (the frame evaluator and the kernel layer,
/// whose operands are frequently `Numeric`) read the mask by plain index, so
/// they share this instead of each repacking a throwaway bitmap for a
/// `Numeric` column. Takes the `Series`, not its storage, so a caller reads a
/// column's validity without naming the storage layer.
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn Series::validity_bools(self : Series) -> Array[Bool] {
let storage = self.storage
match storage.kind() {
@column.StorageKind::Numeric => Array::make(storage.len(), true)
@column.StorageKind::Builtin => storage.validity().to_bools()
}
}
///|
/// Which storage backend currently backs this column: `Numeric` for the
/// all-valid unboxed fast path (built by `from_ints` / `from_floats`, and
/// preserved through structural transforms), `Builtin` otherwise.
///
/// No production code outside this package calls it — the packages above
/// choose a backend through `try_column_to_numeric` / `preserve_backend`
/// rather than by asking. It stays `pub` for the *assertions*: "an operator's
/// output backend is a function of its content" is engine-wide behaviour, and
/// the operators live in `frame`, whose tests are a separate package and can
/// only observe the backend through a `pub` reader. That is a declared
/// exception in the seam guard's allowlist, not a precedent — the documented
/// contract for an `#internal(engine)` seam.
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn Series::storage_kind(self : Series) -> @column.StorageKind {
self.storage.kind()
}
///|
/// Whether this column sits on its **canonical** storage backend — the fixed
/// point of `try_column_to_numeric`, the representation every
/// content-determined transform (`gather` / `drop_nulls` / `filter`
/// and the expression engine's computed columns) lands its result on.
///
/// A column is canonical unless it is a `Builtin` column that is both
/// all-valid *and* numeric (`Int` / `Float`): that lone shape can move onto
/// the unboxed `Numeric` fast path losslessly, so a content-determined
/// transform never leaves a result there. Every other shape is already
/// canonical — a `Numeric` column (all-valid Int / Float by construction), a
/// `Builtin` column carrying a null (a `NumericColumn` has no validity bitmap
/// to record it), or a `Builtin` `Bool` / `String` column (the fast path is
/// numeric-only).
///
/// This is the representation invariant the query optimizer relies on for
/// `collect ≡ eager` — a derived column's backend is a function of its
/// *content*, not of how it was produced — surfaced as a predicate so tests
/// can assert it directly instead of pinning a specific `storage_kind`. It
/// mirrors `try_column_to_numeric` arm for arm: `is_canonical` is `true`
/// exactly when that total move is a backend no-op. The `ColumnData` match is
/// wildcard-free, so a future dtype must decide here whether it converges onto
/// `Numeric`. A backend-preserving transform (`slice` / `head` / `fill_null`)
/// can still return a non-canonical column when its source was one — the
/// invariant governs the content-determined path, not every column.
///
/// Like every invariant predicate in this repository it has no production
/// caller: what it is *for* is being asserted, once per transform that claims
/// to produce a canonical column. That is why it stays `pub` while the
/// backend-forcing helpers around it did not, and it is listed as such in the
/// seam guard's allowlist.
#doc(hidden)
#internal(engine, "MoonFrame execution engine API")
pub fn Series::is_canonical(self : Series) -> Bool {
match self.storage.kind() {
@column.StorageKind::Numeric => true
@column.StorageKind::Builtin =>
if self.storage.null_count() > 0 {
// A null forces the bitmap-backed `Builtin` backend — already canonical.
true
} else {
match self.storage.data() {
// An I64 buffer is non-canonical only when its dtype is numeric:
// a null-free Date column also rides I64, but the registry's
// numeric flag keeps `try_column_to_numeric` off it, so `Builtin`
// is its canonical backend — exactly the arm-for-arm mirror the
// docstring above promises.
@column.ColumnData::I64(_) if !self.dtype().is_numeric() => true
@column.ColumnData::I64(_) | @column.ColumnData::F64(_) => false
@column.ColumnData::Bool(_) | @column.ColumnData::Utf8(_) => true
}
}
}
}