У меня есть библиотека синтаксического анализа Rust для буферов байтов с проводной кодировкой, и я пытаюсь создать структуру данных «представления» ByteSliceReader
для написания субпарсеров, которые обеспечивают проверку границ во время компиляции. Поскольку дженерики const, по-видимому, не обладают достаточной функциональностью для этого (поправьте меня, если я ошибаюсь), я попробовал использовать крейт typenum
. Однако я получаю странные/трудные для понимания ошибки компилятора, поэтому я ищу рекомендации по их устранению.
(Я не думаю, что смогу использовать generic-array
, поскольку субпарсерам необходимо использовать такие функции, как f32::from_le_bytes
, которые принимают [u8; 4]
.)
Структура данных выглядит примерно так (за исключением того, что на самом деле данные хранятся в кольцевом буфере, а не в массиве):
use typenum::*;
struct ByteSliceReader<'a, const LEN: usize, Cursor> {
buffer: &'a [u8; LEN],
cursor: core::marker::PhantomData<Cursor>,
}
Здесь Cursor
представляет собой беззнаковое целое число на основе typenum
, которое отслеживает, сколько байтов было использовано субпарсером. У него есть один метод для «выталкивания» количества байтов, определенного константным обобщением:
impl<'a, const LEN: usize, Cursor: Unsigned> ByteSliceReader<'a, LEN, Cursor>
where
Const<LEN>: ToUInt,
U<LEN>: Unsigned,
{
#[must_use]
fn pop<const NUM: usize>(self) -> (ByteSliceReader<'a, LEN, Sum<Cursor, U<NUM>>>, [u8; NUM])
where
Const<NUM>: ToUInt,
Cursor: core::ops::Add<U<NUM>>,
Sum<Cursor, U<NUM>>: IsLessOrEqual<U<LEN>>,
{
let mut byte_window = [0; NUM];
for (i, byte) in byte_window.iter_mut().enumerate() {
*byte = self.buffer[Cursor::to_usize() + i];
}
(
ByteSliceReader::<LEN, _> {
buffer: self.buffer,
cursor: core::marker::PhantomData,
},
byte_window,
)
}
}
Все это компилируется нормально. Но когда я пытаюсь использовать его в этом примере вспомогательной функции (предназначенной для извлечения и анализа некоторых байтов), она не компилируется:
fn get_serial_and_part_num<const LEN: usize, Cursor>(
reader: ByteSliceReader<'_, LEN, Cursor>,
) -> (ByteSliceReader<'_, LEN, Sum<Sum<Cursor, U1>, U1>>, (u8, u8))
where
Cursor: Unsigned,
Cursor: core::ops::Add<U1>,
Sum<Cursor, U1>: core::ops::Add<U1> + Unsigned,
Const<LEN>: ToUInt,
U<LEN>: Unsigned,
{
let (reader, serial_num) = reader.pop::<1>();
let (reader, part_num) = reader.pop::<1>();
(reader, (serial_num[0], part_num[0]))
}
error[E0599]: the method `pop` exists for struct `ByteSliceReader<'_, LEN, Cursor>`, but its trait bounds were not satisfied
--> fsw/sensors/ring_parser.rs:528:43
|
483 | struct ByteSliceReader<'a, const LEN: usize, Cursor> {
| ---------------------------------------------------- method `pop` not found for this struct
...
528 | let (reader, serial_num) = reader.pop::<1>();
| ^^^ method cannot be called on `ByteSliceReader<'_, LEN, Cursor>` due to unsatisfied trait bounds
|
= note: the following trait bounds were not satisfied:
`Cursor: Add<<typenum::Const<_> as typenum::ToUInt>::Output>`
help: consider restricting the type parameter to satisfy the trait bound
|
526 | U<LEN>: Unsigned, Cursor: Add<<typenum::Const<_> as typenum::ToUInt>::Output>
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Предложение компилятора содержит _
, который я интерпретирую как 1, но его применение не помогает. Я пробовал различные перестановки границ и получал разные похожие ошибки. Есть ли способ добиться того, чего я хочу?
Непосредственная проблема заключается в том, что вам нужно сообщить компилятору, что Output
из Const<1> as ToUInt
будет U1
:
Const<1>: ToUInt<Output = U1>,
Однако это приводит к дальнейшим неудовлетворенным границам признаков (а именно к IsLessOrEqual
):
Sum<Cursor, U1>: IsLessOrEqual<U<LEN>>,
Sum<Sum<Cursor, U1>, U1>: IsLessOrEqual<U<LEN>>,
Большое спасибо. ToUInt
реализован для Const<1>
в typenum
, так зачем же нужна эта первая граница? И знаете ли вы, почему ошибки компилятора оказались такими бесполезными?
@JoshBurkart: Вы полагаетесь на то, что U<1>
есть U1
, но это деталь реализации Const<1>
ToUInt
, которая не гарантируется; поэтому нам необходимо ограничить применимость нашей функции только при выполнении этого условия.
@JoshBurkart: Ошибки бесполезны главным образом потому, что сложное использование системы типов, подобное этому, немного выходит за рамки сценариев использования, для которых компилятор ржавчины и его диагностические выходные данные подвергались тщательному стресс-тестированию. Вообще говоря, я считаю, что лучше избегать слишком сложных проектов на уровне типов в пользу более простых проверок во время выполнения, которые в любом случае часто оптимизируются. Только в случае крайней необходимости я мог искать какое-то сложное решение, связанное с системой типов.
Обязательно откройте диагностический отчет об ошибках в системе отслеживания проблем проекта, если вы обнаружите сообщения об ошибках, которые требуют улучшения.
Спасибо за дополнительный контекст. Реализация ToUInt
для Const<1>
, похоже, напрямую содержит type Output = U1;
, поэтому я все еще не понимаю, зачем нужна эта привязка?
Я не совсем уверен, но требование, чтобы компилятор решал подобные проекции признаков, могло бы сделать решатель признаков трудноразрешимым? В любом случае, философия Rust обычно предпочитает быть явными, а не скрытыми зависимостями, которые могут стать угрозой стабильности: поэтому вы должны заявить, что ваш код зависит от U<1> = U1
, а не продолжать компиляцию (возможно, с неожиданными результатами), если это произойдет. измениться вверх по течению.