size-classes 0.1.0

Const-built mimalloc-style size-class tables with an O(1) size->class lookup and an alignment-aware classifier. no_std, zero deps, #![forbid(unsafe_code)].
Documentation
# Changelog

All notable changes to this project will be documented in this file.

This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 0.1.0 - 2026-08-30

First release. Everything below is new in this version; nothing has shipped
before it.

### Added

- **`build_table(params) -> [usize; N]`** — a `const fn` that builds a
  mimalloc-style size-class table: a geometric progression
  (`round_up(ceil(prev * num / den), min_block)`, minimum step `min_block`, starting
  at `min_block`) sorted-merged with an explicit `extras` list. `extras`
  preconditions — every entry a multiple of `min_block` AND `>= min_block`,
  the list strictly increasing — plus `min_block` being a power of two,
  `geo_count > 0`, a non-zero growth denominator, `N == geo_count +
  extras.len()`, and the FINAL MERGED table being strictly increasing (an
  `extras` entry that DUPLICATES a geometric value is rejected at this
  function's own chokepoint, not left for a caller of `build_size2class` to
  discover downstream; an extra landing strictly between two geometric
  values is valid and expected) are **machine-checked**: a
  violation panics identically in `const` evaluation (a compile error at
  the consumer's table definition) and at runtime, never a silently
  accepted bad table — every precondition is a named assert, not a bare
  division/index panic.
- **`build_size2class(table, min_block) -> [u8; L]`** — derives the O(1)
  `size → class` lookup from a table at compile time using the
  monotone-pointer technique (`O(buckets + classes)` const-eval), with a
  compile-time pin that every class INDEX fits a `u8` (up to 256 classes,
  indices `0..=255`), and a machine-checked global-monotonicity/disjointness
  pass over the supplied `table` — this stays in place as defense-in-depth
  for a table a caller assembles by hand rather than through `build_table`.
- **`size2class_len(max_class, min_block)`** — the `const fn` a consumer uses
  to pin the lookup length `L` (`max_class / min_block + 1`) as a `const`
  expression; asserts `min_block` is a power of two like its siblings, and
  that the `+ 1` itself does not overflow `usize`.
- **`Params<'a>`** — the scheme's parameter set (`min_block`, `growth` as
  `(num, den)``(5, 4)` is the classic mimalloc 1.25× spacing — `geo_count`,
  `extras`, `huge_threshold`), all plain data so the whole scheme is usable in
  `const` context. `#[non_exhaustive]` **with** a `const fn new` constructor:
  future field additions are semver-MINOR, and the const constructor keeps
  the type constructible in `const` context where struct literals no longer
  compile.
- **`SizeClasses<N, L>`** — the built scheme, generic over table length `N`
  and lookup length `L`, both pure functions of `Params`:
  - `const fn build(params)` — construct the whole scheme at compile time;
    intended placement is a `static` (a `const` this size re-materializes at
    every use site -- see the `SizeClasses` doc);
  - accessors `table()`, `size2class()`, `min_block()`, `min_block_shift()`,
    `small_align_max()`, `small_max()`, `huge_threshold()`, `count()`,
    `block_size(idx)`, `is_huge(size)`;
  - NB: `min_block()` and `small_align_max()` return the SAME value today —
    both are `1 << min_block_shift()` — but that equality is a convenience of
    the current design, not a stability promise: the two accessors stay
    separate so a future `Params` field could decouple them (giving
    `small_align_max` its own policy), so a consumer caching one result must
    not assume it stays in sync with the other across releases.
  - **`class_for(size, align) -> Option<usize>`** — resolve a request to the
    smallest class whose block is `>= max(size, align)` **and** a multiple of
    `align` (`None` routes to the caller's large path). O(1) fast path for
    `align <= min_block` (every class size is a multiple of `min_block`, so
    the stride trivially satisfies divisibility -- see below for the
    separate base-address requirement); for larger
    power-of-two alignments, a provably
    equivalent **jump** slow path rounds the block up to the next multiple of
    `align` and re-seeds through the lookup, skipping whole runs of
    non-divisible classes instead of stepping one class at a time (see
    `class_for`'s own rustdoc for why this matters -- a request whose
    `align` exceeds what a hand-rolled classifier happens to handle is a
    real bug class this crate exists to remove).
    `align` must be a power of two (the `Layout` contract), enforced by a
    `debug_assert!` — with `debug_assertions` off, a non-zero
    non-power-of-two `align` is unspecified: it can return a wrong
    `Some`/`None`. The `(size, align) == (0, 0)` corner does not panic, but
    only with `overflow-checks` ALSO off (never memory unsafety either way,
    regardless of profile). `try_class_for` below closes this for callers
    that don't already know `align` is valid. The divisibility check is a
    STRIDE property,
    not an address guarantee: it preserves whatever alignment the caller's
    carve base already has, it does not create it -- `base % align == 0` for
    every served `align` is the caller's own documented precondition, which
    this crate (pure size arithmetic, no addresses) cannot check.
  - **`try_class_for(size, align) -> Result<Option<usize>, InvalidAlign>`**    the checked twin of `class_for`: validates `align` instead of assuming
    it (`Err(InvalidAlign(align))` for a non-power-of-two `align`, including
    `0`, before any arithmetic runs), then delegates. Never panics, for any
    `(size, align)` pair — the substantive reason to prefer it over
    `class_for` whenever `align` is not already known-valid by construction
    (e.g. taken directly from a `core::alloc::Layout`). Mirrors
    `Layout::from_size_align` (checked) next to
    `Layout::from_size_align_unchecked` (trusted) in `core::alloc`; being a
    separate function, it adds zero cost to `class_for`'s own hot path --
    `try_class_for` itself does strictly more work than `class_for` (an
    added power-of-two check before delegating). `InvalidAlign` implements
    `Display` and `core::error::Error`, and is a plain `pub` tuple struct
    (not `#[non_exhaustive]`) — a deliberate pre-0.1.0 decision, since it
    has exactly one reason to exist and no foreseeable second field.
- `is_huge` compares against the caller-supplied `huge_threshold` policy
  parameter — the crate has no notion of an OS segment size; the consumer
  decides where "large" ends and "huge" begins for its own segment policy.
- The whole crate is `no_std`, zero-dependency, and `#![forbid(unsafe_code)]`;
  the geometric-advance step is computed in `u128` and range-checked, so an
  overflowing scheme is a loud error rather than a silently wrapped
  (wrong-but-valid-looking) table, and a scheme whose next class fits is not
  rejected merely because an intermediate product does not.
- `SizeClasses` is `Clone` but deliberately **not** `Copy`: an instance
  embeds both tables (~16 KiB for a realistic scheme), so `Copy` would make
  a full-object duplicate look as cheap as a move; omitting it keeps
  explicit duplication available while forcing the call site to say so
  (`.clone()`), and no method needs ownership. Settled before the first
  release, since removing `Copy` afterwards would be a breaking change.
- `SizeClasses` also implements `Debug`, hand-written rather than derived:
  a derive would print both raw tables on any accidental `{:?}`/`dbg!`,
  so the impl instead prints a short summary (`N`, `L`, `min_block`,
  `small_max`, `huge_threshold`) and marks the rest `..` via
  `finish_non_exhaustive`.

### MSRV

- Rust 1.88.