Compile-time computation com const fn e const generics

1. Fundamentos da Computação em Tempo de Compilação

Em Rust, const fn permite que funções sejam executadas durante a compilação, produzindo valores constantes que podem ser usados em contextos onde o compilador exige valores conhecidos em tempo de compilação. Diferente de funções regulares, const fn são avaliadas pelo compilador quando chamadas em contextos const, como inicialização de constantes, tamanhos de arrays estáticos ou parâmetros de const generics.

Historicamente, const fn era extremamente limitada — apenas expressões simples sem loops ou condicionais. A partir do Rust 1.46, loops for, while e if foram permitidos. Versões posteriores estabilizaram match, operadores booleanos e operações em inteiros. Hoje, const fn pode conter a maior parte da lógica de fluxo, exceto alocação dinâmica, I/O e operações com ponteiros brutos.

Uma função const fn pode ser chamada tanto em contexto de compilação quanto em runtime. Seu comportamento é idêntico, mas quando usada em const ou static, o compilador a avalia durante a compilação.

const fn soma(a: u32, b: u32) -> u32 {
    a + b
}

const RESULTADO: u32 = soma(10, 20); // avaliado em compile-time
let runtime = soma(5, 3);            // avaliado em runtime

2. Const Fn: Sintaxe, Regras e Capacidades

A sintaxe é simples: basta prefixar fn com const. Dentro de const fn, você pode usar:

  • Operadores aritméticos, lógicos e de comparação
  • if, match, loops loop, while, for
  • Chamadas a outras const fn
  • Acesso a campos de structs e enums
  • Operações com slices e arrays de tamanho fixo

Operações proibidas incluem:
- Alocação de heap (Box, Vec, String)
- I/O (println!, leitura de arquivos)
- Ponteiros brutos (desreferenciamento)
- Traits não marcados como const
- Closures

const fn fatorial(n: u32) -> u32 {
    let mut resultado = 1;
    let mut i = 2;
    while i <= n {
        resultado *= i;
        i += 1;
    }
    resultado
}

const FATORIAL_10: u32 = fatorial(10); // 3628800

const fn valida_tamanho(n: usize) -> usize {
    if n < 1 || n > 1024 {
        panic!("Tamanho inválido"); // panic! é permitido em const fn
    }
    n
}

3. Const Generics: Tipos Parametrizados por Valores Constantes

Const generics permitem que tipos sejam parametrizados por valores constantes, não apenas por tipos. A sintaxe usa const N: tipo na lista de parâmetros genéricos.

struct Buffer<T, const N: usize> {
    dados: [T; N],
    posicao: usize,
}

impl<T, const N: usize> Buffer<T, N> {
    const fn new() -> Self {
        Buffer {
            dados: unsafe { std::mem::zeroed() }, // apenas para demonstração
            posicao: 0,
        }
    }
}

let buf: Buffer<u8, 256> = Buffer::new();

Tipos permitidos como parâmetros const: inteiros (usize, u8, i32, etc.), bool, char. Atualmente, floats e strings não são permitidos.

4. Combinando Const Fn e Const Generics

O poder real surge quando const fn calcula valores usados como argumentos para const generics.

const fn tamanho_otimizado(entrada: usize) -> usize {
    let mut t = 1;
    while t < entrada {
        t *= 2;
    }
    t
}

struct Alinhado<const N: usize> {
    dados: [u8; tamanho_otimizado(N)],
}

// Exemplo: lookup table em compile-time
const fn seno_lookup(index: usize) -> f64 {
    // Aproximação simplificada
    match index {
        0 => 0.0,
        1 => 0.5,
        2 => 1.0,
        _ => 0.0,
    }
}

struct LookupTable<const N: usize> {
    valores: [f64; N],
}

impl<const N: usize> LookupTable<N> {
    const fn new() -> Self {
        let mut valores = [0.0; N];
        let mut i = 0;
        while i < N {
            valores[i] = seno_lookup(i);
            i += 1;
        }
        LookupTable { valores }
    }
}

5. Padrões Avançados com Const Generics

Const generics permitem substituir crates como typenum para operações em tempo de compilação.

// Tipo genérico sobre tamanho de matriz
struct Matriz<T, const LINHAS: usize, const COLUNAS: usize> {
    dados: [[T; COLUNAS]; LINHAS],
}

impl<T: Copy + Default, const L: usize, const C: usize> Matriz<T, L, C> {
    const fn identidade() -> Self
    where
        [(); L]: Sized,
        [(); C]: Sized,
    {
        let mut m = Matriz { dados: [[T::default(); C]; L] };
        let mut i = 0;
        while i < L.min(C) {
            m.dados[i][i] = T::default(); // simplificado
            i += 1;
        }
        m
    }
}

Limitações atuais: não é possível fazer N + 1 diretamente como expressão genérica. É necessário usar arrays auxiliares ou traits.

6. Otimizações e Zero-Cost Abstractions na Prática

O compilador Rust elimina completamente o overhead de cálculos em tempo de compilação. Uma const fn chamada em contexto const gera zero instruções em runtime — o resultado é embutido como literal no binário.

const CRC8_TABLE: [u8; 256] = {
    let mut tabela = [0u8; 256];
    let mut i = 0;
    while i < 256 {
        let mut crc = i as u8;
        let mut j = 0;
        while j < 8 {
            if crc & 0x80 != 0 {
                crc = (crc << 1) ^ 0x07;
            } else {
                crc <<= 1;
            }
            j += 1;
        }
        tabela[i] = crc;
        i += 1;
    }
    tabela
};

fn crc8_runtime(dados: &[u8]) -> u8 {
    dados.iter().fold(0u8, |crc, &byte| CRC8_TABLE[(crc ^ byte) as usize])
}

O array CRC8_TABLE é completamente calculado em compile-time. Não há custo de inicialização em runtime.

7. Casos de Uso Reais e Exemplos Completos

Buffer Circular com Tamanho Fixo

use core::fmt;

struct CircularBuffer<T, const N: usize> {
    buffer: [T; N],
    head: usize,
    tail: usize,
    cheio: bool,
}

impl<T: Default + Copy, const N: usize> CircularBuffer<T, N> {
    const fn new() -> Self {
        CircularBuffer {
            buffer: [T::default(); N],
            head: 0,
            tail: 0,
            cheio: false,
        }
    }

    fn push(&mut self, item: T) {
        self.buffer[self.head] = item;
        self.head = (self.head + 1) % N;
        if self.cheio {
            self.tail = (self.tail + 1) % N;
        }
        self.cheio = self.head == self.tail;
    }
}

Validação de Configurações

const fn pagina_alinhada(tamanho: usize, pagina: usize) -> bool {
    tamanho > 0 && tamanho % pagina == 0
}

struct Config<const TAMANHO: usize, const PAGINA: usize>
where
    [(); (pagina_alinhada(TAMANHO, PAGINA) as usize)]: Sized,
{
    dados: [u8; TAMANHO],
}

// Uso: Config<4096, 4096> compila, Config<4095, 4096> não compila

8. Limitações e Futuro das Const Fn e Const Generics

Apesar do progresso, ainda há limitações significativas:

  • Closures em const fn: não são permitidas (RFC em andamento)
  • Alocação dinâmica: impossível em contexto const
  • ?Sized: const generics exigem tipos Sized
  • Expressões aritméticas em const generics: N + 1 não funciona diretamente
  • Const traits: ainda não estabilizados (permitiriam implementações condicionais mais elegantes)

Comparado a C++ constexpr, Rust é mais restrito mas mais seguro. Zig comptime oferece mais flexibilidade, mas Rust está evoluindo rapidamente nessa direção.

RFCs como const_trait_impl e melhorias em const generics (como suporte a floats e expressões) estão em andamento e devem expandir significativamente as capacidades nas próximas versões.

Referências