Mehdi Akiki
Rust Failure Atlas / Upgrades and compatibility

RFA-257 · Case file with fixtures · Case 229 of 694 · Runtime evidence

Why checked_ilog2 Returns None for Zero

Integer logarithm is defined only for positive input. checked_ilog2 reports zero as outside its domain; add a separate zero bucket or validate a nonzero value before using the logarithm.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
Zero has no finite integer logarithm, while returning zero would collide with the valid base-two logarithm of one.
First discriminating check
Test zero separately from one and values around powers of two before defining an application-specific empty bucket.

The bit pattern of zero is perfectly valid, but its integer logarithm is not.

The failing program expects 0_u32.checked_ilog2() to produce a number. checked_ilog2 returns None.

Logarithm asks for a positive magnitude

For a positive integer, ilog2 returns the floor of the base-two logarithm. It identifies the exponent of the greatest power of two not exceeding the value.

There is no finite integer exponent satisfying that relationship for zero. As values approach zero from the positive real side, the logarithm heads toward negative infinity. Returning zero would confuse an undefined input with the valid result for one.

The checked method preserves that distinction through Option.

The unchecked-looking convenience method panics

u32::ilog2 returns a plain u32, so it panics for zero. checked_ilog2 is the right operation when zero can be ordinary input.

This is domain checking rather than overflow checking. The result for every positive u32 fits easily in u32; the failure is that zero has no valid answer.

I read “checked” as “reports this operation's invalid numerical cases,” not only “detects a too-large result.”

Zero buckets are an application policy

Histogram and allocator code often uses ilog2 to select a size class. The product may need a bucket for empty values. I encode that separately:

zero -> empty bucket
positive n -> logarithmic bucket

Mapping zero directly to bucket zero can be correct, but bucket zero then contains both zero and values whose ilog2 is zero, such as one. If those populations need different treatment, the bucket identifier should preserve the distinction.

The repaired program keeps None for zero and shows the floor behavior for one, eight, and fifteen.

ilog2 is floor, not exact-power validation

Eight returns three, and fifteen also returns three. ilog2 does not say the input is a power of two. It says which power-of-two interval contains it.

If exactness matters, I combine the result with is_power_of_two or compare 1 << exponent with the input. Using only the log can accept a rounded-down value as an exact alignment.

This matters when calculating masks, page sizes, or tree heights.

Sometimes the real question is how many bits are needed to represent a value. Zero policies differ: a mathematical bit length may be zero, while a storage field may require at least one bit.

That is not automatically the same as ilog2. For positive values, bit width is generally ilog2 + 1; zero needs its own convention. I name helpers after the question rather than using a log and patching cases until tests pass.

A nonzero type can move validation earlier

When an internal algorithm only accepts positive sizes, NonZero can represent the precondition. Parsing or construction remains fallible, but subsequent logarithm calls no longer repeat a zero check.

This is helpful for capacities, alignment, and fan-out values passed through many functions. It is less appropriate when zero is meaningful data that must be retained.

What I test

My boundary table includes zero, one, values immediately below and above powers of two, and the integer maximum. It checks floor behavior separately from power-of-two classification.

For bucket systems, I assert monotonicity, exact boundary transitions, maximum bucket count, and the explicit zero policy. A correct primitive can still feed an off-by-one array index if bucket allocation uses another convention.

General bases add another invalid input

The related ilog(base) family also requires a base of at least two. A checked variant can therefore fail because the value is zero or because the base is invalid. If errors need different messages, collapsing both into one None is too weak and I validate each operand before calling.

Base one is especially tempting when a configurable grouping factor defaults badly. It cannot produce logarithmic progress: every power of one remains one. Base zero has the same kind of domain problem plus arithmetic hazards.

I log the value and base as separate fields. “Logarithm unavailable” without operands makes production diagnosis slow and can hide that a rollout changed configuration rather than data.

Do not derive allocation shifts without checking

Code often turns a logarithm back into a power using 1 << exponent. That round trip gives the lower power-of-two boundary, not the original input unless it was exact. A later +1 for ceiling buckets must also handle exact powers differently.

I test the complete mapping from requested size to bucket capacity. The individual log call may be correct while the surrounding rounding formula allocates a bucket that is too small.

The core principle is that representable input is not always inside an operation's mathematical domain. Zero fits in u32 but has no finite integer logarithm. checked_ilog2 exposes that absence cleanly; the application can reject zero, represent it separately, or map it to a named bucket without confusing it with the valid logarithm of one.