Давайте поговорим о лексическом анализе. Сначала я собирался назвать этот пост «Реализуем токенайзер», но ветер переменился, времена изменились… и, чтобы не утонуть в потоке комментариев вида «фыр, а где мой BPE-токенизатор LLama, который вы мне обещали», ограничимся пока лексическим анализом. Эта статья ориентирована на читателей, только начинающих пробовать свои силы в лексическом анализе Rust. Поэтому, пожалуйста, помните о целевой аудитории, прежде, чем сетовать: «хм, да я тут на коленке набросал поиск в таблице, и он работает в десять раз лучше, чем это недоразумение» и «с такими временами жизни я сам до завершения программы не доживу».  Но конструктивные комментарии и подсказки, как действительно можно было бы сделать лучше, всегда приветствуются. Длинновато для вводного дисклеймера. Надеюсь, дочитав до этого места, вы уже хотя бы разок вздрогнули. Довольно слов, приступим.  Лексическая токенизация – это преобразование текста в (семантически или синтаксически) значимые лексические единицы (токены), которые распределяются по категориям при помощи программы-«лексера».   Проще говоря, на вход этой программе подаётся набор символов, а на выход она предлагает набор токенов. Обратите внимание: токены должны иметь смысловую нагрузку именно в вашем контексте, и освежевать кота токенизировать строку можно по-разному. Вот, например, как это можно сделать со строкой "hello world": В сущности, так токенизатор превращается в функцию вида , которая принимает список символов из алфавита S и алфавит токенов, после чего производит список токенов из алфавита T. Как минимум, в моём представлении всё именно так. В рамках этой статьи реализуем токенизатор для анализа простого выражения Cron. Постараемся работать с такими выражениями, каждое из которых состоит ровно из 5 полей, и в качестве токенов в этом выражении допускаются только 0-9, *, /, ,, -, а на уровне каждого из полей действуют следующие правила:  Поля Допустимые значения Допустимые специальные символы Минуты 0 – 59 * - ,    Часы 0 – 23 * - ,    День месяца 1 – 31 * - ,    Месяц 1 – 12 * - ,    День недели 0 – 6 * - , Теперь можно запустить новый проект Rust. Для этого выполним cargo new cronlexer и перейдём к делу. Я предпочитаю первым делом определить токены в коде. Поскольку нет двух одинаковых токенов, их максимально удобно выразить в Rust в виде перечисления. Вот пример, демонстрирующий, как можно структурировать токены: Названия вариантов, входящих в перечисление, самоочевидны, и напрямую соответствуют тем токенам, которые нас интересуют. Правда, как вы, возможно, заметили, мы решили реализовать токен-цифру как Number(u8). Так мы принуждаем токенизатор объединить все цифры в число, и только после этого выдавать токен. Но мы могли бы также сделать каждую цифру отдельным токеном, вот так: Можно даже поступить прямо противоположным образом и предусмотреть отдельные токены для дней, недель и года, вот так: В зависимости от того, как будет структурирована эта информация, её синтаксический разбор может пойти проще или сложнее. Пока отвлечёмся от синтаксического разбора и просто примем набор токенов, которые имеют смысл в контексте нашего примера и сочтём, что они (сколько бы их ни было) синтаксически корректны. В результате получим: Когда я проектирую библиотечный интерфейс, я всегда задумываюсь о конечном пользователе. Как он будет обращаться с моей библиотекой? Каковы будут входные и выходные значения предоставляемых в библиотеке публичных функций, структур и перечислений? Затем, нащупывая, таким образом, как лучше работать с библиотекой, я могу сделать её эргономичнее, и пользователю тоже станет удобнее её применять. Для начала попробую использовать мою библиотеку следующим образом: Поскольку мы хотим перебирать токены, можно опереться на следующее соглашение: мы вправе реализовать типаж IntoIterator для нашего Tokenizer, и именно в нём возвращать конкретный итератор. В Rust принято, что всякий раз, когда вы хотите вернуть некоторую сущность, реализующую набор типажей, вы обёртываете их в структуру и заставляете эту структуру их реализовать. Такое встречается повсюду, например, столь разные функции, как map, peek, iter, split_whitespaces, все возвращают разнообразные вспомогательные структуры, реализующие необходимые типажи. Но затем быстро приходит понимание, что при токенизации может быть возвращена ошибка. Например, если попытаться расставить токены в "schedule every day" в нашем Cron-выражении, то грамматика с этим выражением просто не справится. Вместо этого воспользуемся методом tokenize, который будет возвращать результат токенизации. В реальной ситуации здесь уже могут возникнуть другие вещи, которые также потребуется делать токенизатору: подсмотреть следующее значение, затем выполнить поиск с возвращением, причём, сделать это быстро (с оптимизацией). Пока давайте не усложнять: мы хотим просто токенизировать данные и перебрать токены. Наша библиотека будет использоваться примерно так:  Сначала давайте заполним наши базовые структуры. Нам понадобится главная структура Tokenizer, а также TokenizerError Теперь давайте реализуем токенизатор. Он будет просто просматривать токены в направлении слева направо, затем жадно потреблять их и ставить в результирующий вектор токенов. Некоторые интересные замечания и наблюдения: tokenize принимает mut self, так как изменяет внутреннее состояние и не является идемпотентным (если вызвать tokenize дважды, то это приведёт к ошибке). Так очень удобно просигнализировать клиенту, что вызывать tokenize по несколько раз не разрешается и принудительно обеспечить соблюдение этого условия во время компиляции. Наш Tokenizer принимает source по ссылке. Когда требуется прочитать эту информацию в простых случаях, например, таких, как мы разбираем сейчас, нужно продумать, как долго должна оставаться действительной ссылка на Tokenizer? В общем, ссылка source должна пережить Tokenizer (или прожить столько же, сколько и он). В противном случае у нас не будет допустимой точки, из которой мы могли бы считать данные. Во внутреннем состоянии мы ведём актуальный индекс pos, жадно продвигая его по мере синтаксического разбора токенов. Синтаксический разбор токенов – это просто одна гигантская операция match (которую можно было бы вынести в отдельную функцию). В рамках этой операции мы возвращаем сам токен, а также число потреблённых байт. Наша грамматика Cron поддаётся недвусмысленной токенизации. Поэтому весь процесс получается довольно тривиальным упражнением: нужно просто аккуратно наращивать индекс и сопоставлять токены, пока мы не исчерпаем запас байт. Напишем несколько тестов: 100% покрытие тестами. Именно так я и пишу в проде. p/s идет Черная пятница в издательстве «Питер»