Integer Policy
| Status | Applies to | Owner |
|---|---|---|
| Active development contract | main branch as of 2026-07-10 Rust, PyO3, and JAX integer boundaries | Native runtime maintainers |
Native Rust code uses different integer types for different contracts. The goal
is not one project-wide integer type. The goal is to keep usize in Rust
memory indexing code, fixed-width integers at persistence and Python
boundaries, and checked conversions where values cross those boundaries.
Policy
- In-memory Rust indexing, chunk lengths, matrix dimensions, queue sizes, and
slice offsets use
usize. - Persistent schemas do not use
usize. - Python, JSON, TOML, manifest, Arrow, and Parquet numeric fields use fixed-width integers.
- Host configuration capacities are unsigned fixed-width values. Use
NonZeroU32when zero is invalid; convert to the consumer's integer domain with an explicit bound check. - JAX indices, loop counters, count reductions, and output count arrays use
signed
i32. Enabling JAX x64 changes floating-point capability; it does not change the index policy. - Genomic positions use
i64unless a file format requires a narrower or wider type. - File byte offsets use
u64or parser-local checkedusizeafter validating the mapped buffer bounds. - Large stored index arrays may use
u32only after validating the maximum index and benchmarking the memory-bandwidth benefit. - Raw pointer addresses are not public interchange values. Public buffer
boundaries use owned typed containers or borrowed slices. Internal
unsafe/SIMD pointers derive from slices and never round-trip through
usize. - Narrowing or sign-changing conversions use
TryFromor a named boundary helper. - Unchecked
ascasts are not used for integer-to-integer conversion or pointer round trips. Audited float conversions may retain them where the approximation is part of the numerical algorithm.
The Rust crates declare:
#[cfg(not(target_pointer_width = "64"))]
compile_error!("g requires a 64-bit target.");
This makes the supported target assumption explicit. It does not make usize
a stable storage or interchange type.
Integer width follows storage and execution cost, not the smallest value seen
in ordinary runs. A scalar thread count gains no meaningful memory or cache
benefit from u8, while it would require widening for Rayon, OS, and slice
APIs and would impose an arbitrary limit. Narrow integers are appropriate for
large arrays or stable schemas only when the range is part of the contract and
the footprint reduction is material.
Boundary Conversion
Checked conversions live at the boundary that owns the source value. A local helper is appropriate when one boundary performs the same validated conversion more than once. Do not create forwarding modules or project-wide conversion frameworks for a single call site.
Correctness paths use checked_add, checked_sub, and checked_mul; saturation
is reserved for explicitly lossy telemetry counters. Native run preparation
proves configured u32 loop/capacity values, sample counts, trait counts,
chunk sizes, flattened trait-by-variant lanes, candidate capacities, and
padded Firth batches fit i32 before calling
Python. Python static shape integers originate from those checked values, and
every JAX index-producing operation has an explicit int32 result. NumPy
arrays crossing PyO3 have explicit fixed-width element types. Fixed-capacity
mask compaction constructs int32 indices directly instead of producing a
default int64 nonzero result and narrowing it afterward.
Float-to-integer timestamp conversion is isolated to telemetry formatting and falls back when Chrono rejects the resulting timestamp.
Typed Buffer Boundaries
BgenReadSession::decode_variant_major_batch returns a
DecodedGenotypeBatch whose OwnedGenotypeBuffer owns either a Vec<f32> or
PooledPacked8Buffer backed by a Vec<u8>. The decoder reserves or reuses the
final capacity, initializes the vector's spare capacity, and sets its length
only after every requested output position has been written successfully. No
address or value-count wrapper crosses from g-genotype to g-engine.
Review Checklist
- New memory indices and lengths are
usize. - New output, manifest, JSON, and Python fields are fixed-width integers.
- New JAX indices and integer reductions explicitly request
int32even when JAX x64 is enabled. - New JAX shapes and derived products are bounded natively before device work.
- New conversions that can overflow or change sign are checked.
- Raw pointer-sized values do not appear in public genotype, engine, or binding APIs; internal pointer use remains slice-derived and locally audited.
- Count vectors stay in the documented output schema type.
- Any retained unchecked cast has a local reason and is not a casual boundary conversion.
Enforcement
The Rust workspace enables these Clippy cast lints:
cast_possible_truncation = "deny"
cast_possible_wrap = "deny"
cast_sign_loss = "deny"
Local exceptions use a narrowly scoped allow with an audited reason. The
check_rust_architecture development check also scans crates/ and
src/binding/ for integer as casts outside tests.rs files. Retained casts
must either be replaced with checked conversion or listed in
tooling/debug/integer_cast_allowlist.txt with an audited reason.
The current architecture-check allowlist is limited to timestamp formatting,
where finite floating-point seconds are intentionally split into whole seconds
and nanoseconds for chrono.
Clippy and cast scanning cannot identify a newly serialized or PyO3-exposed
usize. The boundary-type and JAX-capacity checklist remains a required part
of review; production boundary types are also made concrete in _core.pyi.