1ЧТО ЭТО
2=======
3
4Pire (Perl Incompatible Regular Expressions) — это библиотека,
5заточенная на быструю проверку текста большим количеством регулярных
6выражений. Она умеет проверять, матчится ли текст паттерну, но умеет
7делать это действительно быстро (400 Мб/с вне зависимости от сложности
8регулярки). Если же паттернов более одного, их можно объединять,
9выполняя за один проход проверку примерно десятью паттернами (с той же скоростью).
10
11Pire может давать гарантии на скорость работы (на нижнем уровне она
12просматривает входную строку один раз, подряд, без откатов, и тратит
13(на x86 и x86_64) по 5 инструкций на символ), так что в принципе
14может использоваться в realtime-задачах.
15
16Расплачиваться за такую скорость приходится ограниченной функциональностью.
17В pire нету сложных перловых конструкций типа условных регулярок,
18backtracking-а, lookahead-а. Кроме того, pire вообще не занимается
19захватом фрагментов сматчившегося текста (хотя в ограниченном варианте
20такой захват можно реализовать, см.ниже).
21
22Вся теория, которая делает такую скорость возможной, описана в the Dragon Book,
23настоятельно рекомендуемой для изучения.
24
25Pire была разработана в компании Яндекс (изначально как часть робота, но
26в итоге развилась в самостоятельный проект).
27
28
29БЫСТРЫЙ СТАРТ
30=============
31
32#include <stdio.h>
33#include <vector>
34#include <pire/pire.h>
35
36Pire::Scanner CompileRegexp(const char* pattern)
37{
38	// Переводим шаблон в UCS4
39	std::vector<Pire::wchar32> ucs4;
40	Pire::Encodings::Utf8().FromLocal(pattern, pattern + strlen(pattern), std::back_inserter(ucs4));
41	// или другая кодировка
42
43	return Pire::Lexer(ucs4.begin(), ucs4.end())
44		.AddFeature(Pire::Features::CaseInsensitive())	// если хочется нечувствительность к регистру
45		.SetEncoding(Pire::Encodings::Utf8())		// Устанавливаем кодировку, в которой будет приходить проверяемый текст
46		.Parse() 					// Разбираем шаблон
47		.Surround()					// если мы не хотим логику PCRE_ANCHORED
48		.Compile<Pire::Scanner>();			// Компилируем регулярку
49}
50
51bool Matches(const Pire::Scanner& scanner, const char* ptr, size_t len)
52{
53	return Pire::Runner(scanner)
54		.Begin()	// Начало строки
55		.Run(ptr, len)	// Строка
56		.End();		// Конец строки
57		// Оно неявно кастится к bool
58}
59
60int main()
61{
62	char re[] = "hello\\s+w.+d$";
63	char str[] = "Hello world";
64
65	Pire::Scanner sc = CompileRegexp(re);
66
67	bool res = Matches(sc, str, strlen(str));
68
69	printf("String \"%s\" %s \"%s\"\n", str, (res ? "matches" : "doesn't match"), re);
70
71	return 0;
72}
73
74
75КАК ВЫЗЫВАТЬ MATCH
76==================
77
78Скомпилированная регулярка хранится в сканере (т.е. конечном автомате, представленом
79в виде, оптимизированном для прохода по строке). В Pire есть несколько сканеров
80(как минимум Scanner, SlowScanner и SimpleScanner), работа с ними во многом совпадает.
81
82У каждого сканера есть тип State, хранящий состояние автомата, и методы
83Initialize(State&), отдающий начальное состояние автомата, Next(State&, char),
84переводящий автомат в следующее состояние и Final(const State&), сообщающий,
85является ли данное состояние допускающим. Соответственно, чтобы проверить
86строчку на соответствие паттерну, необходимо вызвать Initialize(), Next()
87на каждую букву, и Final(), чтобы проверить, соответствует ли строчка паттерну.
88
89Существует метод Pire::Run(const Scanner&, Scanner::State&, const char* begin,
90const char* end), осуществляющий соптимизированный вызов Scanner::Next() на
91каждый символ диапазона.
92
93В паттерне могут попадаться символы начала и конца строки (^ и $). При компиляции
94они заменяются специальными символьными символами (sentinels) — BeginMark и EndMark.
95
96Если одну и ту же строчку требуется прогнать через два сканера, то можно воспользоваться
97специальной функцией Pire::Run(const Scanner1&, const Scanner2&, Scanner1::State&,
98Scanner2::State&, const char* begin, const char* end), которая работает несколько
99быстрее, чем два последовательных вызова обычной Run().
100
101Наконец, есть специальный класс RunHelper, облегчающий работу с циклом
102Initialize-Step-Run-Step-Final и позволяющий записать его в виде одного выражения (см.пример).
103
104Существуют ещё четыре функции, полезные при организации лексического разбора:
105LongestPrefix(), LongestSuffix(), ShortestPrefix() и ShortestSuffix(). Они возвращают
106наибольший/наименьший префикс/суффикс строки, допускаемый паттерном. Для использования
107в LongestSuffix() и ShortestSuffix() автомат надо предварительно «вывернуть наизнанку»
108(при помощи Fsm::Reverse()).
109
110
111СКЛЕЙКА СКАНЕРОВ
112================
113
114Как уже говорилось, время проверки строки на соответствие паттерну не зависит от размера
115паттерна, таким образом с точки зрения скорости работы лучше иметь несколько сложных
116регулярок, чем много простых. В то же время иногда возникает необходимость проверить
117текст достаточно большим количеством регулярок, многие из которых могут быть очень
118простыми. В таком случае существует возможность объединения нескольких сканеров в один.
119
120Сканеры объединяются функцией Pire::Scanner::Glue(). В результате получается сканер,
121который за один проход проверяет строку на соответствие всем содержащимся в нём
122паттернам. Общее количество паттернов возвращает Scanner::RegexpsCount(). Вместо
123Final() имеет смысл вызвать AcceptedRegexps(const State&), возвращающая пару
124итераторов (с семантикой [begin,end)), указывающую на диапазон номеров регулярок,
125которым соответствует прочитанная строка.
126
127Номера начинаются с нуля, при склейке двух сканеров все номера правого сдвигаются на
128количество регулярок в левом.
129
130Склеенные сканеры в свою очередь можно склеить между собой. В то же время процесс склейки
131нельзя повторять до бесконечности, потому как количество состояний в полученном автомате
132растёт экспоненциально с каждым добавленным паттерном. Иногда имеет смысл ограничить
133размер полученного автомата, указав ненулевой параметр maxSize в Glue(). В таком случае
134при превышении указанного размера возвращается пустой автомат (Size() == 0, Empty() == true).
135
136
137РАЗБОР РЕГУЛЯРНОГО ВЫРАЖЕНИЯ
138============================
139
140До сих пор речь велась о том, как гонять по тексту уже готовый сканер,
141но не говорилось о том, откуда этот сканер взять. Самый простой способ получить
142сканер — это сконструировать его из строки, содержащей регулярное выражение,
143воспользовавшись классом Pire::Lexer.
144
145Строка, задающая регулярное выражение, имеет стандартный синтаксис, похожий
146на POSIX regexps. Поддерживаются стандартные возможности (a|b, a*, a+, ., [a-z],
147a{3}, a{3,5}, a{3,}) и классы символов (\w (буква), \W (не буква), \d (цифра),
148\D (не цифра), \s (whitespace), \S (не whitespace)). Кроме того, могут быть добавлены
149операции a&b (пересечение, см. Fsm::operator &) и ~a (инверсия, см. Fsm::Complement).
150Помимо этого, для Fsm::SlowCapturingScanner есть возможность использовать нежадные аналоги
151повторений - операторы *?, +?, ??. В этом сканере данные операторы будут принимать
152как можно более короткие выражения, в отличие от жадных аналогов (которые, наоборот, принимают
153как можно более длинные.
154
155Lexer принимает регулярку в кодировке UCS-4, как диапазон из wchar32 (или любых других
156типов, неявно преобразовывающихся к wchar32). Таким образом, если регулярное выражение
157задано как const char* regexp и все символы в ней есть в latin-1, можно просто вызвать
158lexer.Assign(regexp, regexp + strlen(regexp)), но если оно задано в UTF-8 или KOI8-R —-
159то его для начала нужно явно перевести в UCS-4.
160
161lexer.SetEncoding() указывает кодировку, в которой будут поступать строки для
162сопоставления с выражением (ещё раз: она не указывает кодировку самого регулярного
163выражения!). Это необходимо, поскольку сканер работает уже не на уровне символов,
164а на уровне байтов, а, скажем, точка (один любой символ) в UTF-8 —- это весьма
165нетривиальная конструкция.
166
167Тонкая настройка лексера возможна посредством добавления туда «фич» (Pire::Feature),
168которые можно добавлять к лексеру вызовами lexer.AddFeature(). Скажем,
169нечувствительность к регистру реализована фичей Pire::CaseInsensitive.
170
171После настройки лексера следует вызвать lexer.Parse(), который вернёт разобранный
172конечный автомат (Pire::Fsm). Его можно скомпилировать в сканер, вызвав
173Fsm::Compile<Pire::Scanner>().
174
175Иногда шаблон регулярки оказывается слишком сложным, чтобы его можно было скомпилировать
176в детерминированный автомат разумного размера. В этом случае Fsm::Compile() раскрутит
177исключение. Если хочется обрабатывать такую ситуацию, может иметь смысл явно
178вызвать Fsm::Determine(size_t maxSize) и, если он возвратил false, не пытаться
179компилировать автомат в Pire::Scanner, а ограничиться Pire::SlowScanner-ом (см. далее).
180
181
182РУЧНОЕ ПОСТРОЕНИЕ АВТОМАТА
183==========================
184
185Кроме разбора шаблона регулярного выражения, автомат можно строить непосредственно,
186для чего служит класс Pire::Fsm. Наиболее полезные его методы:
187
188    * Конструктор по умолчанию — создаёт автомат, допускающий пустую строку.
189    * MakeFalse() — создаёт автомат, не допускающий ни одной строки.
190    * Size() — размер автомата (количество состояний).
191    * Append(unsigned char c) — добавляет в автомат переход для матча символа c.
192    * AppendStrings(const vector<string>&) — добавляет в автомат переходы для
193      матча хотя бы одной из переданных строк.
194    * operator + (const Fsm&) — возвращает конкатенацию автоматов (допускающую
195      строки, делящиеся на две части так, что первая допускается первым автоматом,
196      а вторая — вторым).
197    * operator | (const Fsm&) — возвращает объединение (union) авмоматов (допускающее
198      строки, допускаемые хотя бы одним автоматом).
199    * operator * () — возвращает итерацию автоматов (допускающую строки,
200      представляющие собой повторение строки, допускаемой исходным автоматом,
201      0 или более раз (т.н. звезда Клини)).
202    * operator ~ () — возвращает инверсию автомата (допускающего строки, которые
203      не допускаются исходным автоматом).
204    * operator & (const Fsm&) — возвращает пересечение автоматов (допускающее строки,
205      которые допускаются обоими автоматами).
206    * operator * (size_t n) — возвращает автомат, допускающий строки, представляющие
207      собой повторение строки, допускаемой исходным автоматом, ровно n раз.
208    * Surrounded() — возвращает автомат, допускающий любые строки, содержащие в себе
209      строку, допускаемую исходным автоматом (т.е. добавляет в начало и в конец по /.*/).
210    * operator +=, operator |=, Iterate, Complement, operator &=, operator *=, и
211      Surround, двойственные к семи выше описанным.
212    * Reverse() — возвращает автомат, допускающий строки, являющиеся зеркальным
213      обращением строк, допускаемых исходным автоматом.
214
215Возможно комбинированное построение автомата (например,
216(lexer1.Parse() | lexer2.Parse()).Compile<Scanner>()). Таким образом возможно, например,
217прочитать из файла список паттернов, скомпилировать их в Fsm'ы и объединить их все
218в один большой Fsm, который скомпилировать в сканер.
219
220
221СКАНЕРЫ
222=======
223
224В Pire существует три сканера: Scanner, SimpleScanner и SlowScanner.
225
226    * Scanner — это основная рабочая лошадка всего Pire. До сих пор речь велась именно о нём.
227    * SimpleScanner — это более простая версия сканера, пригодная для проверки
228      текста одной несложной регуляркой. Она не поддерживает склейки автоматов,
229      и её таблица переходов несколько больше, чем в Scanner-е, зато SimpleScanner
230      работает приблизительно на треть быстрее.
231    * SlowScanner — работает крайне медленно, но не требует детерминизации автомата и,
232      таким образом, может использоваться при матче сложных конструкций типа /x.{50}$/,
233      которые не могут быть скомпилированы в Scanner.
234
235Нужный тип сканера может быть получен вызовом нужной специализации метода Fsm::Compile()
236(или явным конструированием нужного сканера из Fsm-а).
237
238
239КОДИРОВКИ
240=========
241
242«Из коробки» Pire поддерживает кодировки Latin-1 и UTF-8. Нужные экземпляры классов
243Pire::Encoding могут быть получены вызовами соотвествующих функций из namespace Pire::Encodings.
244
245При желании можно добавить поддержку других кодировок, понаследовавшись от Pire::Encoding.
246В наследованном классе следует переопределить методы:
247
248    * wchar32 FromLocal(const char*& begin, const char* end) — читает и возвращает очередной
249      символ входного потока, продвигает begin. В случае невалидной последовательности
250      на входе кидает исключение.
251    * std::string ToLocal(wchar32) — возвращает байтовое представление символа в данной
252      кодировке. Если символ в данной кодировке непредставим, возвращает пустую строку.
253    * AppendDot(Fsm&) — добавляет к конечному автомату фрагмент, допускающий один любой
254      символ в данной кодировке.
255
256После этого экземпляр такого класса можно передать в lexer.SetEncoding() наравне со встроенными.
257
258
259СЕРИАЛИЗАЦИЯ, MMAP() И INLINING
260===============================
261
262Построенные сканеры могут быть сохранены в std::ostream или загружены из std::istream
263путем вызова Scanner::Save() и Scanner::Load() (аналогично для других сканеров).
264
265Если сканер был сохранен в файл, а файл потом при-mmap()-лен в память, то вместо вызова
266Scanner::Load() можно воспользоваться Scanner::Mmap(), который не будет выполняеть
267копирования таблицы переходов (которая может быть очень большой). Mmap() возвращает
268указатель на первый байт после сериализованного представления регулярки.
269
270Следует, однако, учесть, что начало сканера должно находиться в памяти по адресу,
271выровненному по границе машинного слова. Если в файл пишется ещё что-то кроме
272сериализованных сканеров (сами представления сканеров всегда занимают целое количество
273машинных слов), следует позаботиться о выравнивании самостоятельно или воспользоваться
274классами AlignedInput и AlignedOutput, предоставляющими нужную функциональность.
275
276Сериализованное представление сканера непереносимо между архитектурами (даже между x86 и x86_64).
277При попытке прочитать/приммапить регулярку, сериализованную на другой архитектуре, будет exception.
278
279Если же в коде есть необходимость использовать одно и то же заранее определённое регулярное
280выражение, то можно, конечно, написать static const Scanner = Pire::Lexer(«...»).Parse().Compile(),
281но в таком варианте компиляция выражения будет происходить при каждом запуске программы,
282что может быть достаточно неприятным, особенно если шаблон достаточно сложный. Чтобы
283избежать таких задержек, выражение можно скомпилировать в сканер, сериализовать его,
284подставить сериализованное представление в код в виде строкового литерала, а затем вызвать
285на нём Scanner::Mmap(). Всё это делает программа pire_inline, которая принимает на вход файл
286с кодом на C++, ищет там все вхождения PIRE_REGEXP("pattern", "flags") вне комментариев и строк
287и заменяет их на выражение, возвращающее автомат с готовой предвычисленной таблицей переходов.
288
289В строчке с флагами могут находиться следующие символы:
290
291    * i — нечувствительность к регистру;
292    * u — скомпилировать регулярку в UTF-8;
293    * s — обрамить регулярку .* с каждой стороны;
294    * a — включить поддержку операторов & и ~ в регулярках (см.выше);
295    * g — выполнить преобразование fsm = ~fsm.Surrounded() + fsm (для нужд Scan()).
296
297
298РАСШИРЕНИЯ PIRE
299===============
300
301Если требуется какая-нибудь функциональность, в Pire отсутствующая, есть вероятность,
302что её можно реализовать, воспользовавшись существующими в Pire точками расширения
303или спустившись на уровень ниже (возможно, до этого стоит прочитать Dragon Book).
304
305
306** ЕЩЕ РАЗ ПРО FSM
307
308Fsm — это конечный автомат с выходом. Соответственно, в нём есть множество состояний
309(пронумерованных целыми числами от 0 до Fsm::Size() – 1), для каждого состояния и
310каждой буквы задано множество состояний, в которые по этой букве из этого состояния
311можно перейти (возможно, это множество пусто), одно состояние объявлено начальным,
312а часть состояний отмечены допускающими.
313
314Для манипуляции этими данными у Fsm есть функции:
315
316    * Size(), Resize();
317    * Destinations(), Connected(), Connect();
318    * Initial(), SetInitial();
319    * Finals(), IsFinal(), SetFinal(), ClearFinal().
320
321Кроме того, с каждым состоянием и каждым переходом автомата может быть ассоциировано
322битовое поле флагов. Флаги переходов называются выходами (outputs, поскольку реализуют
323классический КА с выходом), для работы с ними предназначены функции Output(),
324SetOutput() и ClearOutputs(). Флаги состояний называются тегами и поддерживаются
325функциями Tag() и SetTag().
326
327При детерминизации или ином объединении состояний все флаги объединяются побитовым ИЛИ.
328
329Ещё ряд функций не принадлежит никакой группе:
330
331    * Divert(size_t from, size_t to, size_t dest) — разрывает все переходы из `from' в `to'
332      и перенаправляет их в состояние dest. Все флаги, которыми были помечены переходы,
333      переносятся на новые переходы.
334    * Import(const Fsm&) — копирует внешний автомат внутрь данного. Все его состояния
335      сдвигаются на количество состояний в данном. Сами состояния никак не подсоединяются.
336    * DeadStates() — возвращает набор состояний, из которых недостижимо ни одно из допускающих
337      состояний.
338    * RemoveEpsilons() — ликивдирует в автомате все epsilon-переходы (переходы из одного
339      состояния в другое по пустой строке).
340
341
342** ЕЩЕ РАЗ ПРО LEXER И FEATURE
343
344Основная функция Lexer-а — это разбирать входную строку на последовательность
345термов (Pire::Term).
346
347Feature — это возможность изменить поведение лексера. У фичи есть три основные
348возможности:
349
350    * Обработать фрагмент паттерна каким-либо своим способом, вернув терм
351      (функции Accepts() и Lex());
352    * Изменить уже выделенный терм (Alter());
353    * Изменить выделенный фрагмент конечного автомата, заключенный
354      в круглые скобки (Parenthesized()).
355
356Если фича разбирает фрагмент паттерна, то она должна реализовывать функцию
357Accepts(wchar32), которая возвращает true, если передаваемый символ является
358началом последовательности символов, воспринимаемой фичей, и функцию Lex(),
359которая собственно и осуществляет лексический разбор (если Accepts() вернула
360false, Lex() вызвана не будет). Внутри фичи доступны стандартные функции
361работы с входным потоком: GetChar(), PeekChar() и UngetChar(). Если
362Accepts() вернула true, а уже внутри Lex()-а стало понятно, что последовательность
363не подходит, можно положить её всю обратно посредством UngetChar(), а затем
364вернуть пустой Term(), показав тем самым лексеру, что фича отказалась
365обрабатывать поток.
366
367В лексере может быть несколько фич, они отсортированы по приоритету и объединены
368в цепочку. Lex() и Accepts() вызываются от большего приоритета к меньшему
369до тех пор, пока какая-нибудь фича не обработает входной поток с получением
370терма. Этот терм пропускается в обратном порядке через Feature::Alter().
371
372При чтении закрывающей круглой скобки фрагмент автомата, соовтетствующий
373находящемуся в скобках фрагменту регулярки, прогоняется через
374Feature::Parenthesized(), опять-таки в порядке возрастания приоритетов.
375
376Простой пример использования: нечувствительность к регистру сделана именно фичей,
377у которой метод Alter() изменяет каждый возвращаемый диапазон символов, добавляя
378к каждому символу из диапазона его в верхнем и нижнем регистре.
379
380
381** ПРИМЕР: CapturingScanner
382
383Скажем, нам нужен ограниченный захват одного фрагмента в тексте (например, матчить текст
384шаблону /id\s*=\s*['"]([a-z0–9]+)['"]/ и извлекать собственно ID).
385
386    * Заводим два флага для переходов: BeginCapture и EndCapture.
387    * Пишем фичу, в которой отслеживаем все скобки. Lex() возвращает собственно
388      скобку и при этом отсчитывает нужную скобку по порядку.
389    * После того, как нужная закрывающая скобка найдена, в Parenthesized() фича получает
390      автомат, который матчит то, что в скобках. Расширяем этот автомат на два состояния,
391      переносим делаем новым начальным состоянием предпоследнее, единственным допускающим —-
392      последнее, и соединяем их со старым начальным и допускающими состояниями
393      epsilon-переходами. Эти переходы отмечаем соответственно флагами
394      BeginCapture и EndCapture.
395    * Пишем собственный сканер, который при переходе, отмеченном BeginCapure или EndCapture,
396      отмечает соответствующую позицию во входном потоке.
397
398Пример, где всё это реализовано, находится в extra/capture.h и extra/capture.cpp.
399
400