hekate_math/fft/reed_solomon.rs
1// SPDX-License-Identifier: Apache-2.0
2// This file is part of the hekate-math project.
3// Copyright (C) 2026 Andrei Kochergin <andrei@oumuamua.dev>
4// Copyright (C) 2026 Oumuamua Labs <info@oumuamua.dev>.
5//
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Systematic Reed–Solomon encoding on the additive FFT.
19
20use super::{AdditiveFft, FftError};
21use crate::{BinaryFieldExtras, Flat, HardwareField, PackedFlat};
22
23/// Error returned by the Reed–Solomon encoder.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum RsError {
27 /// `log_k` is outside `1..log_n`.
28 BadRate {
29 /// The `log_k` passed to `new`.
30 log_k: u32,
31
32 /// The `log_n` passed to `new`.
33 log_n: u32,
34 },
35
36 /// `log_n` exceeds `max_log_n`, the largest size `F` supports.
37 FieldTooSmall {
38 /// The `log_n` passed to `new`.
39 log_n: u32,
40
41 /// The largest `log_n` that `F` supports on this target.
42 max_log_n: u32,
43 },
44
45 /// `msg` or `out` has length `got`; it needs `expected`, k or n.
46 BadLength {
47 /// k for `msg`, n for `out`.
48 expected: usize,
49
50 /// The length of the rejected buffer.
51 got: usize,
52 },
53
54 /// Allocating a 2^log_size-point twiddle table failed.
55 TwiddleAlloc {
56 /// The `log_k` or `log_n` of the failing transform.
57 log_size: u32,
58 },
59}
60
61impl core::fmt::Display for RsError {
62 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
63 match self {
64 RsError::BadRate { log_k, log_n } => {
65 write!(
66 f,
67 "ReedSolomon rate: log_k {log_k} must satisfy 1 <= log_k < log_n {log_n}"
68 )
69 }
70 RsError::FieldTooSmall { log_n, max_log_n } => {
71 write!(
72 f,
73 "ReedSolomon log_n {log_n} exceeds the maximum {max_log_n} \
74 = min(field degree, usize::BITS - 1)"
75 )
76 }
77 RsError::BadLength { expected, got } => {
78 write!(f, "ReedSolomon buffer length {got}, expected {expected}")
79 }
80 RsError::TwiddleAlloc { log_size } => {
81 write!(
82 f,
83 "ReedSolomon 2^{log_size}-point twiddle table allocation failed"
84 )
85 }
86 }
87 }
88}
89
90impl core::error::Error for RsError {}
91
92impl From<FftError> for RsError {
93 fn from(e: FftError) -> Self {
94 match e {
95 FftError::BadLength { expected, got } => RsError::BadLength { expected, got },
96 FftError::BadLogN { log_n, max } => RsError::FieldTooSmall {
97 log_n,
98 max_log_n: max,
99 },
100 FftError::TwiddleAlloc { log_n } => RsError::TwiddleAlloc { log_size: log_n },
101 }
102 }
103}
104
105/// Systematic Reed–Solomon `[n, k, n-k+1]` encoder
106/// over the additive-FFT subspaces `W_k ⊂ W_n`:
107/// `encode(msg)[..k] == msg` (Lin–Chung–Han 2014).
108pub struct ReedSolomon<F> {
109 fft_k: AdditiveFft<F>,
110 fft_n: AdditiveFft<F>,
111 k: usize,
112 n: usize,
113}
114
115impl<F: BinaryFieldExtras + HardwareField> ReedSolomon<F> {
116 /// Builds the encoder for `k = 2^log_k` message and `n = 2^log_n`
117 /// codeword symbols. The only allocation is the two twiddle schedules.
118 ///
119 /// # Errors
120 /// `BadRate` unless `1 <= log_k < log_n`; `FieldTooSmall`
121 /// if `log_n > min(F::BITS, usize::BITS - 1)`;
122 /// `TwiddleAlloc` if a twiddle table cannot be allocated.
123 pub fn new(log_k: u32, log_n: u32) -> Result<Self, RsError> {
124 if log_k < 1 || log_k >= log_n {
125 return Err(RsError::BadRate { log_k, log_n });
126 }
127
128 let max_log_n = F::BITS.min(usize::BITS as usize - 1) as u32;
129
130 if log_n > max_log_n {
131 return Err(RsError::FieldTooSmall { log_n, max_log_n });
132 }
133
134 Ok(Self {
135 fft_k: AdditiveFft::new(log_k)?,
136 fft_n: AdditiveFft::new(log_n)?,
137 k: 1usize << log_k,
138 n: 1usize << log_n,
139 })
140 }
141
142 /// Returns k, the message length per row.
143 pub fn message_len(&self) -> usize {
144 self.k
145 }
146
147 /// Returns n, the codeword length per row.
148 pub fn codeword_len(&self) -> usize {
149 self.n
150 }
151
152 /// Encodes one row: `out[..k] == msg` on return,
153 /// `out[k..]` the n - k parity symbols.
154 ///
155 /// # Errors
156 /// `BadLength` unless `msg.len() == k` and `out.len() == n`.
157 pub fn encode_scalar(&self, msg: &[Flat<F>], out: &mut [Flat<F>]) -> Result<(), RsError> {
158 self.check(msg.len(), out.len())?;
159
160 out[..self.k].copy_from_slice(msg);
161 out[self.k..].fill(Flat::from_raw(F::ZERO));
162
163 self.fft_k.inverse_scalar(&mut out[..self.k])?;
164 self.fft_n.forward_scalar(out)?;
165
166 Ok(())
167 }
168
169 /// Encodes `F::WIDTH` rows in lockstep, one per lane;
170 /// lengths count packed elements.
171 ///
172 /// # Errors
173 /// `BadLength` unless `msg.len() == k` and `out.len() == n`.
174 pub fn encode(&self, msg: &[PackedFlat<F>], out: &mut [PackedFlat<F>]) -> Result<(), RsError> {
175 self.check(msg.len(), out.len())?;
176
177 out[..self.k].copy_from_slice(msg);
178 out[self.k..].fill(PackedFlat::default());
179
180 self.fft_k.inverse(&mut out[..self.k])?;
181 self.fft_n.forward(out)?;
182
183 Ok(())
184 }
185
186 fn check(&self, msg_len: usize, out_len: usize) -> Result<(), RsError> {
187 if msg_len != self.k {
188 return Err(RsError::BadLength {
189 expected: self.k,
190 got: msg_len,
191 });
192 }
193
194 if out_len != self.n {
195 return Err(RsError::BadLength {
196 expected: self.n,
197 got: out_len,
198 });
199 }
200
201 Ok(())
202 }
203}