Kamu bikin driver UART untuk chip ARM Cortex-M4. Buffer RX harus persis 256 byte; buffer TX 512 byte. Keduanya fixed-size, stack-allocated, zero heap. Lalu kamu perlu array temporary sebesar RX + TX untuk operasi merge buffer. Di Rust stable, nulis [u8; N + M] di return type atau where clause masih error. Compiler nolak mentah-mentah. Padahal logikanya sederhana: “kalau N dan M sudah kukenal saat compile, kenapa N+M nggak bisa?”
⚡ Jawaban Singkat / Key Takeaways: Rust 1.80 membawa progress signifikan pada #![feature(generic_const_exprs)]. Fitur ini memungkinkan kamu menulis N + 1, N * 2, atau N + M langsung di posisi const generic—termasuk di where clause. Untuk stable Rust, ada tiga workaround produktif: trait-based associated constants, typenum bridge, dan macro-based codegen. Semua dibahas di artikel ini dengan kode siap pakai.

Masalah: Kenapa N+1 Itu Susah di Rust Stable?
Saat kamu mendefinisikan fungsi generik dengan parameter const, Rust stable mengizinkan kamu menggunakan N sebagai value tapi tidak sebagai operand dalam ekspresi const lain. Dengan kata lain, [u8; N] legal, tapi [u8; N + 1] tidak. Apalagi di where clause yang membatasi trait bound berdasarkan hasil kalkulasi.
// ✅ Legal di stable Rust 1.51+
fn make_array<const N: usize>() -> [u8; N] {
[0u8; N]
}
// ❌ Error di stable: "generic parameters may not be used in const operations"
fn pad_buffer<const N: usize>() -> [u8; N + 1] {
[0u8; N + 1]
}
Masalah ini berakar dari arsitektur compiler Rust. Const evaluator (miri) sudah bisa menghitung N + 1 sejak lama. Yang belum selesai adalah bagaimana trait solver membuktikan bahwa [u8; N + 1] memenuhi trait bound tertentu. Di sinilah generic_const_exprs berperan: menyambungkan const evaluator dengan trait solver.

Progress generic_const_exprs di Rust 1.80: Apa yang Udah Bisa?
Rust 1.80 bukan titik stabilisasi penuh generic_const_exprs. Tapi changelog-nya signifikan untuk systems programmer yang berkutat dengan fixed-size buffer:
- Concrete const expressions di where clause mulai berfungsi. Kamu bisa menulis
where [(); N + 1]:sebagai predicate const assertion. - Normalisasi const projections membaik. Associated constants seperti
<T as Trait>::SIZEkini bisa digunakan di posisi const generic lebih banyak skenario. - Const generics dalam trait impl headers makin stabil.
impl<const N: usize> Trait for [u8; N] where [u8; N + 1]: Copysekarang lebih sering diterima compiler.
Rust lang team sendiri menyebut ini “milestone 3 of const generic expressions.” Milestone 4 (stabilisasi penuh) ditargetkan di Rust 1.84 atau 1.86. Jadi kalau kamu mau stay di stable sekarang, workaround adalah jalannya.
Workaround #1: Trait-Based Associated Constants (Stable, Clean)
Pendekatan paling elegan di stable Rust adalah menggunakan trait dengan associated constant. Kamu definisikan relasi antar konstanta lewat trait, bukan lewat ekspresi langsung.
/// Trait untuk menyatakan "array ini punya kapasitas extended +1"
trait ExtendedSize {
const EXTENDED: usize;
}
/// Blanket impl: setiap array [T; N] bisa di-extend ke N+1
impl<T, const N: usize> ExtendedSize for [T; N] {
const EXTENDED: usize = N + 1; // ✅ Const expressions legal di const items!
}
/// Fungsi yang butuh buffer N+1 tanpa nightly
fn process_with_padding<T: Default + Copy, const N: usize>(src: [T; N]) -> [T; <[T; N] as ExtendedSize>::EXTENDED]
where
[T; <[T; N] as ExtendedSize>::EXTENDED]: Default,
{
let mut padded = [T::default(); <[T; N] as ExtendedSize>::EXTENDED];
padded[..N].copy_from_slice(&src);
padded
}
// Usage:
let buf: [u8; 256] = [0xAA; 256];
let padded: [u8; 257] = process_with_padding(buf);
Triknya ada di impl ExtendedSize for [T; N]. Perhatikan: di dalam impl block, const EXTENDED: usize = N + 1 legal karena ini adalah const item, bukan const generic expression di posisi type. Compiler mengevaluasi N + 1 saat monomorphization, jauh setelah trait solving selesai. Ini yang bikin workaround ini tembus stable.
Kelemahannya: setiap operasi aritmatika butuh trait terpisah. DoubleSize untuk N * 2, IncSize untuk N + 1, dan seterusnya. Untuk 2-3 operasi masih oke. Untuk puluhan, kamu akan rindu N + M langsung.

Workaround #2: Const Assertion Block alatypenum Bridge (Stable)
Kadang kamu nggak butuh return type N + 1. Kamu cuma butuh memastikan bahwa suatu ekspresi konstanta valid di where clause. Untuk kasus ini, trik [(); N + 1] bisa dipakai dengan bantuan macro atau sedikit boilerplate.
// Const assertion: pastikan N+1 <= MAX, atau compile error
struct Assert<const COND: bool>;
trait AssertTrue {}
impl AssertTrue for Assert<true> {}
fn transmit_packet<const N: usize>(data: [u8; N]) -> [u8; N]
where
Assert<{ N + 1 <= 1024 }>: AssertTrue, // ❌ Ini masih nightly
{
// ...
}
// Workaround stable: gunakan macro untuk generate assertion
macro_rules! assert_const {
($cond:expr $(,)?) => {
const _: [(); ($cond) as usize] = [(); 1];
// ^ trick standar: array size 0 = compile error kalau false
};
}
fn transmit_packet<const N: usize>(data: [u8; N]) -> [u8; N]
where
[(); { if N + 1 <= 1024 { 1 } else { 0 } }]: ,
{
// Fungsi tetap bisa compile di stable
data
}
Pendekatan kedua pakai [(); { if ... }] di where clause. Ini adalah const block yang dievaluasi compiler. Kalau kondisi false, array size jadi 0 dan compiler error. Kalau true, array size 1 (valid). Trik ini legal di stable Rust dan berguna untuk defensive programming di systems code yang tidak boleh overflow buffer.
Workaround #3: Macro Codegen untuk Kombinasi Dimensi (Zero-Cost Abstractions)
Ini pendekatan radikal yang dipakai crate embedded tingkat lanjut: gunakan macro untuk generate semua kombinasi dimensi yang kamu butuhkan. Bukan generic sungguhan, tapi hasil akhirnya identik di binary.
macro_rules! ring_buffer {
($name:ident, $size:expr) => {
pub struct $name<T: Default + Copy> {
buf: [T; $size],
head: usize,
tail: usize,
// overflow buffer: size + 1 untuk sentinel
_overflow: [T; { $size + 1 }], // ✅ Legal di macro!
}
impl<T: Default + Copy> $name<T> {
pub const CAPACITY: usize = $size;
pub const TOTAL: usize = $size + 1;
pub fn new() -> Self {
Self {
buf: [T::default(); $size],
head: 0,
tail: 0,
_overflow: [T::default(); { $size + 1 }],
}
}
}
};
}
// Generate ring buffer 256+1, 512+1, 1024+1
ring_buffer!(RingBuf256, 256);
ring_buffer!(RingBuf512, 512);
ring_buffer!(RingBuf1024, 1024);
Ini bukan generics, tapi macro. Semua kode di-generate secara literal. { $size + 1 } dievaluasi oleh macro expander (bukan trait solver), jadi zero issues. Untuk systems programming yang dimensi buffernya sudah pasti (256, 512, 1024), pendekatan ini justru paling performan karena tidak ada monomorphization overhead. Compiler generate kode spesifik untuk setiap ukuran tanpa melewati generic layer.

Kapan Pakai Nightly, Kapan Pakai Workaround?
Keputusan “nightly atau workaround” bukan soal idealisme. Ini soal supply chain safety dan CI pipeline.
- Pakai nightly jika crate-mu adalah internal tool, firmware proprietary, atau binary final yang tetap kamu kontrol build pipeline-nya. Nightly memberi syntax paling bersih:
[u8; N + M]langsung di mana saja. - Pakai workaround trait-based jika crate-mu adalah library publik di crates.io. Downstream user tidak mau dipaksa pakai nightly hanya karena crate-mu butuh
N+1. Trait-based workaround bersih, documented, dan zero-impact ke downstream. - Pakai macro codegen jika variasi dimensi buffer terbatas (kurang dari 10 kombinasi). Binary size optimal dan kompatibilitas stable terjamin.
- Pakai const assertion block jika fokusmu adalah defensive programming: pastiin buffer nggak overflow tanpa mengubah return type atau API signature.
Contoh Nyata: Driver UART dengan RX/TX Merge Buffer
Mari kita lihat kasus konkret yang disebut di awal: driver UART dengan buffer RX dan TX yang perlu di-merge.
use core::mem::MaybeUninit;
trait BufferExtend {
const RX_PLUS_TX: usize;
}
impl<const RX: usize, const TX: usize> BufferExtend for (Const<RX>, Const<TX>) {
const RX_PLUS_TX: usize = RX + TX;
}
struct Const<const N: usize>;
// Driver UART: merge buffer = RX + TX
struct UartDriver<const RX: usize, const TX: usize>
where
[u8; { <(Const<RX>, Const<TX>) as BufferExtend>::RX_PLUS_TX }]: ,
{
rx_buf: [u8; RX],
tx_buf: [u8; TX],
merge_buf: MaybeUninit<[u8; { <(Const<RX>, Const<TX>) as BufferExtend>::RX_PLUS_TX }]>,
}
impl<const RX: usize, const TX: usize> UartDriver<RX, TX>
where
[u8; { <(Const<RX>, Const<TX>) as BufferExtend>::RX_PLUS_TX }]: ,
{
pub const MERGE_SIZE: usize = <(Const<RX>, Const<TX>) as BufferExtend>::RX_PLUS_TX;
pub fn new() -> Self {
Self {
rx_buf: [0u8; RX],
tx_buf: [0u8; TX],
merge_buf: MaybeUninit::uninit(),
}
}
pub fn merge_buffers(&self) -> &[u8] {
// akses merge_buf yang sudah disiapkan sebelumnya
let _ = self.rx_buf.len() + self.tx_buf.len();
unsafe { &*self.merge_buf.as_ptr() }
}
}
Catatan: kode di atas masih agak verbose karena Const helper type diperlukan untuk menjembatani trait resolution. Begitu generic_const_exprs stabil penuh, seluruh boilerplate Const dan BufferExtend akan digantikan oleh satu baris: [u8; RX + TX].
Untuk pendalaman lebih lanjut soal const generics di sistem resource-constrained, baca juga artikel kami tentang matrix 100% stack dengan const generics Rust 1.80. Jika kamu masih pakai generic-array crate di project-mu, kami juga punya panduan migrasi lengkap ke const generics native yang memangkas dependency tree secara signifikan.
Perbandingan: Semua Workaround dalam Satu Tabel
| Metode | Syntax Bersih | Stable | Binary Size | Cocok Untuk |
|---|---|---|---|---|
Nightly generic_const_exprs | ⭐⭐⭐⭐⭐ | ❌ | Optimal | Internal tool, firmware |
| Trait + associated const | ⭐⭐⭐ | ✅ | Optimal | Library publik, crates.io |
| Const assertion block | ⭐⭐⭐⭐ | ✅ | Optimal | Defensive programming |
| Macro codegen | ⭐⭐ | ✅ | Super optimal | Dimensi terbatas, embedded |
| typenum (legacy crate) | ⭐ | ✅ | Optimal | MSRV di bawah 1.51 |
Kalau kamu masih bertahan di typenum, pertimbangkan bahwa Rust 1.51 sudah 4+ tahun. Mayoritas downstream user sudah di atas 1.60. Tidak ada alasan teknis untuk tetap pakai typenum di crate baru, kecuali interoperabilitas dengan ekosistem kripto yang memang lambat migrasi.
FAQ: Const Generic Expressions di Rust
Belum sepenuhnya. Rust 1.80 membawa progress besar pada #![feature(generic_const_exprs)] terutama di where clause, tapi stabilisasi penuh masih dalam pipeline (target 1.84-1.86). Kamu bisa memakai workaround trait-based, const assertion, atau macro codegen untuk tetap di stable Rust.
Kompleksitasnya bukan di const evaluator (miri), tapi di trait solver. Compiler harus bisa membuktikan bahwa [u8; N+1]: Copy untuk setiap kemungkinan N, termasuk saat N ada di associated type trait lain. Ini menyentuh fondasi Chalk-style trait solving yang butuh integrasi hati-hati agar tidak memperlambat kompilasi secara umum. Rust lang team memilih roadmap bertahap: min_const_generics (1.51), const generics untuk lebih banyak posisi (1.75+), lalu const generic expressions (1.80+).
Macro codegen untuk dimensi yang sudah pasti (256, 512, 1024) karena zero-cost dan zero-generic overhead. Trait-based associated constant untuk library yang perlu generic API publik. Const assertion block untuk defensive check di mana-mana tanpa mengubah API. Jika firmware-mu proprietary dan kamu kontrol toolchain, pakai nightly langsung dengan generic_const_exprs untuk syntax paling bersih.
Workaround trait-based dan const assertion tidak menambah ukuran binary karena monomorphization menghasilkan kode yang identik dengan versi nightly. Macro codegen justru bisa menghasilkan binary lebih kecil karena compiler tidak perlu generik layer tambahan. Intinya: workaround yang dibahas di artikel ini zero-cost, bukan sekadar “bisa compile.”
Kesimpulan: Jangan Tunggu Stabilisasi, Ada Jalan Sekarang
generic_const_exprs adalah salah satu fitur Rust yang paling lama dalam pipeline. Tapi bukan berarti kamu harus menunggu. Ketiga workaround di artikel ini sudah dipakai di production crate hari ini. Trait associated constant memberi API generic yang bersih. Const assertion block menjaga buffer tetap aman. Macro codegen memberikan performa maksimal untuk dimensi yang sudah fixed.
Kamu nggak perlu memilih salah satu. Gunakan kombinasi. Trait untuk API publik, macro untuk internal implementation, const assertion untuk guard di mana-mana. Begitu generic_const_exprs stabil penuh, migrasi workaround ini ke syntax native tinggal search-and-replace.
Punya trik lain untuk compile-time arithmetic di Rust stable? Atau nemu edge case yang bikin workaround di atas gagal? Share di kolom komentar. Komunitas systems programmer Rust selalu berkembang dari sharing pengalaman nyata di production.
Subscribe newsletter kami untuk update mingguan seputar Rust systems programming, compiler internals, dan trik optimasi yang jarang muncul di blog resmi. Tanpa spam, cuma insight solid yang langsung bisa kamu commit ke repository.



