Skip to content

← Performance and Data Layout step 20 of 20

Hard End-to-End

const fn and compile-time evaluation

Build a CRC-32 lookup table during compilation, then use it to compute checksums at runtime. Repair the starter's table-building loops while preserving these public items:

pub const fn crc32_table() -> [u32; 256]
pub static TABLE: [u32; 256] = crc32_table();
pub fn crc32(data: &[u8]) -> u32

The checksum algorithm

Use reflected CRC-32 with polynomial 0xEDB8_8320, an initial accumulator of 0xFFFF_FFFF and a final XOR of 0xFFFF_FFFF.

For table entry i, start with c = i as u32. Repeat this step eight times, then store c in entry i:

if c & 1 != 0:
    c = 0xEDB8_8320 ^ (c >> 1)
else:
    c = c >> 1

For each input byte, in order, update the accumulator using the table:

index = ((crc ^ u32::from(byte)) & 0xFF) as usize
crc = TABLE[index] ^ (crc >> 8)

Return the accumulator XOR 0xFFFF_FFFF. The mask keeps each lookup index within 0..256. All checksum state is u32.

Empty input returns zero. The check value for the ASCII bytes 123456789 is 0xCBF4_3926. Process arbitrary bytes unchanged, including zero bytes; do not interpret them as characters. When the harness supplies text, it passes that text's UTF-8 bytes. Other tests generate byte buffers, including one with 8,000,000 bytes.

Keep the starter's compile-time assertion as well:

const _: () = assert!(crc32_table()[1] == 0x7707_3096);

The harness checks checksums and selected table entries. Keeping the const fn, static and assertion is part of the exercise; output checks alone do not establish every source-level requirement.

Repair the const function

The starter uses range-based for loops inside crc32_table. These require iterator operations that are not available in this stable const context, producing E0015. Use two while loops with explicit counters instead: one for the 256 entries, the other for each entry's eight steps.

Const evaluation supports the local arrays, assignments, indexing, integer operations and branches needed here. Which library operations are callable in a const context depends on their const support and the compiler version. Check the specific API rather than assuming every trait operation, floating calculation or static access is forbidden.

Const-callable does not mean always compile-time

const fn permits calls from a const context. The initializer of TABLE is such a context, so the table is built during compilation. Calling the same function elsewhere does not force compile-time evaluation; optimization may still simplify the call.

This complete example builds a small table and also calls a const function with an argument obtained at runtime:

const fn squares() -> [u32; 4] {
    let mut values = [0; 4];
    let mut i = 0;
    while i < values.len() {
        values[i] = (i as u32) * (i as u32);
        i += 1;
    }
    values
}

const fn low_byte(value: u32) -> u32 {
    value & 0xFF
}

static SQUARES: [u32; 4] = squares();
const _: () = assert!(SQUARES[3] == 9);

fn main() {
    let value = std::env::args().nth(1)
        .map(|arg| arg.parse::<u32>().expect("expected u32"))
        .unwrap_or(513);
    assert_eq!(low_byte(value), value % 256);
    assert_eq!(SQUARES, [0, 1, 4, 9]);
}

The assertion deliberately reads an immutable static. That is allowed here on current stable Rust. The CRC exercise calls crc32_table() in its assertion to check the builder directly, not because reading TABLE is prohibited. Static writes, external statics and mutable or interior-mutable statics have additional const-evaluation restrictions.

Why use a static table?

A static provides a shared allocation with a stable address. A const names a value and does not promise a single storage location across uses. That distinction does not imply a fresh 1024-byte copy in the executable for every mention of a const array. Placement, duplication and elimination depend on how the value is used and optimized.

The table initializer does no runtime table-building work, but checksum calls still read input, access table entries and update the accumulator. Moving initialization to compilation does not remove those operations.

Keep compile-time work bounded. Expensive algorithms can slow compilation and may trigger long_running_const_eval; there is no universal input-size threshold or fixed speed ratio to runtime execution. This builder performs 256 sets of eight simple steps.

If a compile-time assertion is false, compilation fails with E0080. Use that feedback to check the polynomial, shift direction and loop bounds. Submissions must compile, pass the checksum tests and pass the runner's Clippy checks.