Какой самый эффективный способ создать лексер? - PullRequest
0 голосов
/ 27 августа 2018

В настоящее время я пытаюсь научиться создавать свой собственный лексический анализатор вручную. Я много использовал Flex (вместе с Bison), чтобы попрактиковаться и изучить, как он работает внутри, но в настоящее время я вижу как минимум 3 различных решения для разработки своего собственного.

  1. Используя список RE, просматривая каждый из них и, когда он совпадает, просто верните связанный токен (см. Документы Python о RE)
  2. Создание DFA из RE (как это делает Flex, например: на основе RE создайте большой конечный автомат)
  3. Создание моего собственного «конечного автомата» с множеством вариантов переключения или операторов if (я думаю, например, Lua делает это)

Я уверен, что могу попробовать каждое решение, но:

  • Есть ли случай, который одно решение не может решить?
  • В каком случае вы бы использовали одно решение, а не другое?
  • И как гласит заголовок: какой из них производит наиболее эффективный код?

Заранее спасибо!

1 Ответ

0 голосов
/ 27 августа 2018

Второй и третий варианты будут эквивалентны тогда и только тогда, когда вам удастся написать свой конечный автомат и свое лексическое описание flex без каких-либо ошибок. YMMV, но мой опыт показывает, что намного проще написать (и прочитать) гибкое лексическое описание.

Первая альтернатива, вероятно, не эквивалентна, и не будет тривиально сделать ее эквивалентной в общем случае.

Проблема заключается в том, что происходит, если регулярному выражению соответствует несколько шаблонов. (Эта проблема также приводит к незначительным ошибкам при написании массивных операторов switch, как указано выше.) Общепринятая лексическая стратегия заключается в использовании правила "maximal munch" в этом случае: выберите шаблон, который приводит к самому длинному соответствию и если существует более одного такого шаблона, выберите тот, который появляется первым в лексическом определении.

В качестве простого примера того, почему это правило важно, рассмотрим язык с ключевыми словами do и double. Обратите внимание, что желаемое поведение:

do {         => First token is keyword do
double d;    => First token is keyword double
doubt = 0.9; => First token is identifier doubt

В стандартном файле (f) lex это будет реализовано следующим образом:

"do"       {  return T_FOR; }
"double"   {  return T_FOREACH; }
[[:alpha:]_][[:alnum:]_]*  { yyval.str = strdup(yytext); return T_ID; }

(F) lex будет производить точно такой же сканер, если первые два правила окажутся в другом порядке, хотя третье правило обязательно должно быть в конце. Но возможность изменить порядок первых двух правил гораздо менее подвержена ошибкам. Конечно, некоторые люди напишут свои лексические правила с ключевыми словами в алфавитном порядке, как указано выше, но другие могут решить упорядочить ключевые слова по синтаксической функции, так что do объединяется с for, while, done и т. д., а также double с int, char и т. д. Для последней организации программисту будет трудно обеспечить наложение ключевых слов в любом конкретном порядке, поэтому полезно, чтобы flex не все равно; в этом случае (как и во многих других случаях) выбор самого длинного совпадения, безусловно, является правильным.

Если вы создаете список регулярных выражений и просто выбираете первое совпадение, вам необходимо убедиться, что регулярные выражения расположены в обратном порядке по длине совпадения, чтобы первое совпадало с тем, которое соответствует самому длинному ключевому слову. (Это ставит double перед do, поэтому ключевые слова в алфавитном порядке потерпят неудачу.)

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

В качестве альтернативы вы можете попробовать все регулярных выражений и отслеживать то, которое имело наибольшее совпадение. Это будет правильно реализовывать максимальный Munch (но см. Ниже), но это еще более неэффективно, потому что каждый шаблон должен быть сопоставлен с каждым токеном.

Фактический код, используемый в документации Python, на которую вы ссылаетесь, фактически создает единственное регулярное выражение из предоставленных шаблонов, интерполируя операторы | между различными регулярными выражениями. (Это делает невозможным использование пронумерованных снимков, но это может и не быть проблемой.)

Если бы регулярные выражения Python имели семантику Posix с самым длинным соответствием, это было бы эквивалентно максимальному munch, но это не так: чередование Python предпочтет первое совпадение, если для продолжения регулярного выражения не требуется более позднее совпадение:

>>> pat = re.compile("(wee|week)(night|knight)")
>>> pat.match("weeknight").group(1)
'wee'
>>> pat.match("weekknight").group(1)
'week'

Чтобы сделать это правильно, вам нужно будет позаботиться о том, чтобы ваши регулярные выражения были правильно упорядочены и не мешали совпадениям друг друга. (Не все библиотеки регулярных выражений работают так же, как Python, но многие работают. Вам нужно будет проверить документацию и, возможно, поэкспериментировать.)

Короче говоря, для отдельного языка, если вы готовы вложить в него какую-то работу, вы сможете создавать лексеры, которые работают «правильно» (при условии, что язык настаивает на максимальном значении, так как большинство стандартизированных языков в основном делать), но это определенно рутина. И не только для вас: это будет дополнительная работа для тех, кто хочет понять или подтвердить ваш код.

Таким образом, с точки зрения эффективности написания кода (включая отладку), я бы сказал, что лексические генераторы, такие как (f) lex, - явный победитель.

Существует давний мем о том, что лексические генераторы ручной сборки (или с открытым кодом) работают быстрее. Если вы хотите поэкспериментировать с этим, вы можете попробовать использовать re2c, который производит высокооптимизированные лексические сканеры с открытым кодом. (Под открытым кодом я имею в виду, что они не используют таблицы переходов.) Эта теория может или не может быть верной для данного набора лексических правил, потому что основанные на таблице лексеры (как произведено (f) lex) как правило, гораздо меньше по размеру кода, и, следовательно, более эффективно использовать процессорные кэши. Если вы выбираете быстрые (но большие) параметры таблицы flex, то внутренний цикл сканера очень короткий и содержит только одну условную ветвь. (Но предсказание ветвления на этой отдельной ветке не будет очень эффективным). Напротив, сканеры с открытым кодом содержат большое количество кода в цикле с множеством условных переходов, большинство из которых достаточно легко предсказать. (Дело не в том, что путь выполнения длиннее; скорее, внутренний цикл недостаточно короткий для кэширования.)

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

...