Реализация статической проверки границ в Rust с использованием typenum

Постановка задачи

У меня есть библиотека синтаксического анализа 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, но его применение не помогает. Я пробовал различные перестановки границ и получал разные похожие ошибки. Есть ли способ добиться того, чего я хочу?

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
1
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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, так зачем же нужна эта первая граница? И знаете ли вы, почему ошибки компилятора оказались такими бесполезными?

Josh Burkart 28.08.2024 17:38

@JoshBurkart: Вы полагаетесь на то, что U<1> есть U1, но это деталь реализации Const<1>ToUInt, которая не гарантируется; поэтому нам необходимо ограничить применимость нашей функции только при выполнении этого условия.

eggyal 28.08.2024 18:06

@JoshBurkart: Ошибки бесполезны главным образом потому, что сложное использование системы типов, подобное этому, немного выходит за рамки сценариев использования, для которых компилятор ржавчины и его диагностические выходные данные подвергались тщательному стресс-тестированию. Вообще говоря, я считаю, что лучше избегать слишком сложных проектов на уровне типов в пользу более простых проверок во время выполнения, которые в любом случае часто оптимизируются. Только в случае крайней необходимости я мог искать какое-то сложное решение, связанное с системой типов.

eggyal 28.08.2024 18:08

Обязательно откройте диагностический отчет об ошибках в системе отслеживания проблем проекта, если вы обнаружите сообщения об ошибках, которые требуют улучшения.

eggyal 28.08.2024 18:10

Спасибо за дополнительный контекст. Реализация ToUInt для Const<1>, похоже, напрямую содержит type Output = U1;, поэтому я все еще не понимаю, зачем нужна эта привязка?

Josh Burkart 28.08.2024 18:23

Я не совсем уверен, но требование, чтобы компилятор решал подобные проекции признаков, могло бы сделать решатель признаков трудноразрешимым? В любом случае, философия Rust обычно предпочитает быть явными, а не скрытыми зависимостями, которые могут стать угрозой стабильности: поэтому вы должны заявить, что ваш код зависит от U<1> = U1, а не продолжать компиляцию (возможно, с неожиданными результатами), если это произойдет. измениться вверх по течению.

eggyal 28.08.2024 18:41

Другие вопросы по теме