В чем реальная разница между токеном и правилом? - PullRequest
15 голосов
/ 27 мая 2020

Меня привлекла Raku из-за его встроенной грамматики, и я решил поиграться с ним и написать простой парсер адресов электронной почты, единственная проблема: я не мог заставить его работать.

Я пробовал бесчисленное количество итераций, прежде чем остановиться на чем-то, что действительно работает, и я изо всех сил пытаюсь понять, почему.

Все сводилось к замене token на rule.

Вот мой пример кода:

grammar Email {
  token TOP { <name> '@' [<subdomain> '.']* <domain> '.' <tld> }  
  token name { \w+ ['.' \w+]* }
  token domain { \w+ }
  token subdomain { \w+ }
  token tld { \w+ }
}
say Email.parse('foo.bar@baz.example.com');

не работает, он просто печатает Nil, но

grammar Email {
  rule TOP { <name> '@' [<subdomain> '.']* <domain> '.' <tld> }  
  token name { \w+ ['.' \w+]* }
  token domain { \w+ }
  token subdomain { \w+ }
  token tld { \w+ }
}
say Email.parse('foo.bar@baz.example.com');

работает и правильно печатает

「foo.bar@baz.example.com」
 name => 「foo.bar」
 subdomain => 「baz」
 domain => 「example」
 tld => 「com」

И все, что я изменил, было token TOP на rule TOP.

Из того, что я могу понять из документации, единственное различие между этими двумя ключевыми словами состоит в том, что пробелы имеют значение в rule , но его нет в token. Если это правда, первый пример должен работать, так как я хочу игнорировать пробелы между отдельными частями шаблона.

Удаление пробелов между частями

rule TOP { <name>'@'[<subdomain>'.']*<domain>'.'<tld> }

возвращает поведение обратно для печати Nil.

Кто-нибудь может подсказать мне, что здесь происходит?

РЕДАКТИРОВАТЬ : вместо этого измените правило TOP на regex , который позволяет выполнять поиск с возвратом, заставляет это работать. ) нет?

В адресе электронной почты нет пробелов, поэтому для всех намерений и целей он должен сразу выйти из строя

Ответы [ 3 ]

14 голосов
/ 28 мая 2020

Этот ответ объясняет проблему, предлагает простое решение, а затем углубляется.

Проблема с вашей грамматикой

Во-первых, ваш SO демонстрирует то, что кажется либо необычной ошибкой, либо распространенное недоразумение. См. Ответ JJ по проблеме, которую он подал, чтобы исправить ее, и / или мою сноску. [4]

Оставляя ошибку / "ошибку" в стороне, ваша грамматика направляет Raku на соответствие не вашему вводу:

  • Атом [<subdomain> '.']* с нетерпением потребляет строку 'baz.example.' из вашего ввода;

  • Оставшийся вход ('com') не соответствует оставшимся атомам (<domain> '.' <tld>);

  • :ratchet, который находится в эффект для token s означает, что грамматический движок не возвращается к атому [<subdomain> '.']*.

Таким образом, полное совпадение не выполняется.

Простейшее решение

Самый простой способ заставить вашу грамматику работать - это добавить ! к шаблону [<subdomain> '.']* в вашем token.

Это даст следующий эффект:

  • Если какой-либо из остатка из token терпит неудачу (после атома поддомена), грамматический движок вернется к атому поддомена, отбросит последнее совпадение. ns, а затем попробуйте двигаться вперед еще раз;

  • Если сопоставление снова не удается, движок снова вернется к атому поддомена, отбросит еще одно повторение и попытается снова;

  • Механизм грамматики будет повторять указанные выше действия до тех пор, пока не совпадет остальная часть token или не останется совпадений с [<subdomain> '.'] атомом, по которому можно выполнить возврат.

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

Игра с Raku, разработка грамматик и отладка

Nil прекрасен в качестве ответа от грамматики, которая, как известно (или считается), работает нормально, и вам не нужен более полезный ответ в случае сбоя синтаксического анализа.

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

В частности, для игры или разработка грамматики или ее отладка, лучший вариант - установить бесплатную запятую и использовать ее Grammar Live View .

Исправление грамматики; общие стратегии

Ваша грамматика предлагает два три варианта 1 :

  • Анализировать вперед с некоторым возвратом. (Самое простое решение.)

  • Анализировать в обратном направлении. Запишите шаблон в обратном порядке и поменяйте местами ввод и вывод.

  • Выполнить синтаксический анализ после синтаксического анализа.

Разобрать вперед с некоторым обратным отслеживанием

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

Чтобы включить отслеживание с возвратом для всего токена, просто переключите декларатор на regex. A regex аналогичен токену, но, в частности, позволяет выполнять обратное отслеживание, как традиционное регулярное выражение.

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

token TOP { <name> '@' [<subdomain> '.']*! <domain> '.' <tld> }
                                         ?

Альтернативой ! является вставка :!ratchet для отключения "храпового механизма" для части правила, а затем :ratchet для повторного включения храпового механизма, например:

token TOP { <name> '@' :!ratchet [<subdomain> '.']* :ratchet <domain> '.' <tld> }  

(Вы также можете использовать r в качестве сокращения для ratchet, т.е. :!r и :r.)

Обратный синтаксический анализ

Классический трюк с синтаксическим анализом c который работает для некоторых сценариев ios, заключается в обратном синтаксическом анализе, чтобы избежать обратного отслеживания.

grammar Email {
  token TOP { <tld> '.' <domain> ['.' <subdomain> ]* '@' <name> }  
  token name { \w+ ['.' \w+]* }
  token domain { \w+ }
  token subdomain { \w+ }
  token tld { \w+ }
}
say Email.parse(flip 'foo.bar@baz.example.com').hash>>.flip;
#{domain => example, name => foo.bar, subdomain => [baz], tld => com}

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

Опубликовать синтаксический анализ

Выше я представил решение, которое вводит некоторый возврат с возвратом, и другое решение, которое позволяет его избежать, но со значительными затратами с точки зрения уродства, когнитивной нагрузки и c. (анализ в обратном направлении?!?).

Есть еще один очень важный прием, который я упустил, пока не напомнил о нем ответ JJ. 1 Просто проанализируйте результаты синтаксического анализа.

Вот один способ. Я полностью реструктурировал грамматику, частично, чтобы лучше понять этот способ работы, а частично, чтобы продемонстрировать некоторые особенности грамматики Raku:

grammar Email {
  token TOP {
              <dotted-parts(1)> '@'
    $<host> = <dotted-parts(2)>
  }
  token dotted-parts(\min) { <parts> ** {min..*} % '.' }
  token parts { \w+ }
}
say Email.parse('foo.bar@baz.buz.example.com')<host><parts>

отображает:

[「baz」 「buz」 「example」 「com」]

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

  • Грамматика сокращена до трех токенов.

  • Токен TOP делает два вызова универсального c dotted-parts токена с аргументом, указывающим минимальное количество частей.

  • $<host> = ... захватывает следующее атом под именем <host>.

    (Обычно это избыточно, если атом сам является именованным шаблоном, как в данном случае - <dotted-parts>. Но «части с точками» довольно общие; и чтобы сослаться на второе совпадение (первое идет перед @), нам нужно будет написать <dotted-parts>[1]. Итак, я прибрался, назвав это <host>.)

  • Шаблон dotted-parts может показаться немного сложным, но я На самом деле это довольно просто:

    • Он использует предложение квантификатора (** {min..max}) для express любого количества частей при условии, что оно не менее минимального.

    • В нем используется модификатор (% <separator>), в котором говорится, что между каждой частью должна быть точка.

  • <host><parts> извлекает из дерева синтаксического анализа захваченные данные, связанные с токеном parts второго использования в правиле TOP для dotted-parts. Это массив: [「baz」 「buz」 「example」 「com」].

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

JJ показал один способ кодирования того, что называется действия. Это включало:

  • Создание класса «действий», содержащего методы, имена которых соответствуют именованным правилам грамматики;

  • Указание методу синтаксического анализа используйте этот класс действий;

  • Если правило выполняется успешно, вызывается метод действия с соответствующим именем (пока правило остается в стеке вызовов);

  • Соответствующий правилу объект сопоставления передается методу действия;

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

Проще, а иногда и лучше писать действия прямо в строке:

grammar Email {
  token TOP {
              <dotted-parts(1)> '@'
    $<host> = <dotted-parts(2)>

    # The new bit:
    {
      make (subs => .[ 0 .. *-3 ],
            dom  => .[      *-2 ],
            tld  => .[      *-1 ])

      given $<host><parts>
    }

  }
  token dotted-parts(\min) { <parts> ** {min..*} % '.' }
  token parts { \w+ }
}
.say for Email.parse('foo.bar@baz.buz.example.com') .made;

отображает:

subs => (「baz」 「buz」)
dom => 「example」
tld => 「com」

Примечания:

  • Я непосредственно встроил код, выполняющий повторный анализ.

    (Можно вставлять произвольные блоки кода ({...}) везде, где в противном случае можно было бы вставить атом. В те дни, когда у нас были отладчики грамматики, classi c вариант использования был { say $/ } который pri nts $/, объект соответствия, как он есть в точке появления блока кода.)

  • Если блок кода помещен в конец rule, как и я, он почти эквивалентен методу действия.

    (Он будет вызываться, когда правило будет выполнено в противном случае и $/ уже полностью заполнено. В некоторых сценариях ios встраивание анонимного блока действий является способом go. В других случаях лучше разбить его на именованный метод в классе действия, как это сделал JJ.)

  • make - основной вариант использования кода действия.

    (Все, что делает make, - это сохраняет свой аргумент в атрибуте .made элемента $/, который в данном контексте является текущим узлом дерева синтаксического анализа. Результаты, сохраненные make, автоматически отбрасываются, если при обратном отслеживании впоследствии отбрасываются закрывающие узел синтаксического анализа. Часто это именно то, что нужно.)

  • foo => bar образует Pair.

  • Оператор postcircumfix [...] индексирует его invocant :

    • В этом случае есть только префикс . без явного LHS, поэтому инициатор является «он». «Это» было установлено given, т.е. это (извините за каламбур) $<host><parts>.
  • * в индексе *-n - длина инвоканта; поэтому [ 0 .. *-3 ] - это все, кроме двух последних элементов $<host><parts>.

  • Строка .say for ... заканчивается на .made 3 , чтобы подобрать make значение d.

  • Значение make 'd представляет собой список из трех выходящих пар $<host><parts>.

Сноски

1 Я действительно думал, что мои первые два варианта были двумя основными доступными. Прошло около 30 лет с тех пор, как я встретил Тима Тоуди в сети. Можно было подумать, что я уже выучил наизусть его одноименный афоризм - Есть более чем один способ сделать это!

2 Остерегайтесь «патологическое возвращение» . В производственном контексте, если у вас есть подходящий контроль над вводом данных или системой, на которой работает ваша программа, вам, возможно, не придется беспокоиться о преднамеренных или случайных DoS-атаках, потому что они либо не могут произойти, либо бесполезно отключат систему, которая перезагружается в случае недоступности. Но если вы делаете , вам нужно беспокоиться, т. Е. Анализ выполняется на компьютере, который должен быть защищен от DoS-атаки, тогда оценка угрозы будет разумной. (Прочтите подробности сбоя Cloudflare 2 июля 2019 , чтобы понять, что может go ошибаться.) Если вы запускаете код синтаксического анализа Raku в такой требовательной производственной среде, вам может потребоваться начать аудит кода с поиска шаблонов, которые используют regex, /.../ (... являются метасинтаксисом), :!r (включая :!ratchet) или *!.

3 Для .made есть псевдоним; это .ast. Я думаю, это означает A S parse T ree или A nnotated S ubset T ree и есть вопрос cs.stackexchange.com , который со мной согласен.

4 Решите вашу проблему, это кажется неправильным:

say 'a' ~~ rule  { .* a } # 「a」

В более общем плане, я думал , единственная разница между token и rule заключалась в том, что последний вводит <.ws> в каждое значащее пространство . Но это означало бы, что это должно работать:

token TOP { <name> <.ws> '@' <.ws> [<subdomain> <.ws> '.']* <.ws>
            <domain> <.ws> '.' <.ws> <tld> <.ws>
} 

Но это не так!

Сначала это меня напугало. Написав эту сноску два месяца спустя, я чувствую себя несколько менее взволнованным.

Отчасти это мои предположения о причине, по которой я не смог найти никого, кто сообщил бы об этом за 15 лет, прошедших с момента первого Раку. прототип грамматики стал доступен через Pugs. Это предположение включает возможность того, что @Larry намеренно спроектировал их так, чтобы они работали так, как они, и это «ошибка» - это в первую очередь недопонимание среди нынешнего поколения простых смертных, подобных нам, пытающихся объяснить, почему Раку делает то, что он делает, основываясь на наш анализ наших источников - жаркое, исходная проектная документация, исходный код компилятора и т. д. c.

Вдобавок, учитывая, что текущее «глючное» поведение кажется идеальным и интуитивно понятным (за исключением противоречия c), я сосредотачиваюсь на интерпретации моего ощущения сильного дискомфорта - в течение этого промежуточного периода неизвестной продолжительности в что я не понимаю , почему это правильно - как положительный опыт. Я надеюсь, что другие тоже могут - или, намного лучше, выяснить, что на самом деле происходит, и сообщить нам!

8 голосов
/ 28 мая 2020

Изменить : это, вероятно, ошибка , поэтому прямой ответ на вопрос - это интерпретация пробелов (некоторыми ограниченными способами), хотя ответ в этом случае кажется " трещотка ". Однако этого не должно быть, и это случается лишь иногда, поэтому был создан отчет об ошибке. Большое спасибо за вопрос. В любом случае, найдите ниже другой (и, возможно, не содержащий ошибок) способ решения проблемы грамматики. скачай и поставь use Grammar::Tracer вверху. В первом случае: Grammar with token

Токены не возвращаются, поэтому токен <domain> поглощает все, пока не выйдет из строя. Давайте посмотрим, что происходит с rule

Grammar with rule

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

Что вы можете сделать? Вероятно, будет лучше, если вы примете во внимание обратное отслеживание при разделении хоста.

use Grammar::Tracer;

grammar Email {
  token TOP { <name> '@' <host> }  
  token name { \w+ ['.' \w+]* }
    token host { [\w+] ** 2..* % '.' }
}
say Email.parse('foo.bar@baz.example.com');

Здесь мы убеждаемся, что у нас есть как минимум два фрагмента, разделенных точкой.

И затем вы используете действия для разделения между различными частями хоста

grammar Email {
  token TOP { <name> '@' <host> }  
  token name { \w+ ['.' \w+]* }
  token host { [\w+] ** 2..* % '.' }
}

class Email-Action {
    method TOP ($/) {
    my %email;
    %email<name> = $/<name>.made;
    my @fragments = $/<host>.made.split("\.");
    %email<tld> = @fragments.pop;
    %email<domain> = @fragments.pop;
    %email<subdomain> = @fragments.join(".") if @fragments;
    make %email;

    }
    method name ($/) { make $/ }
    method host ($/) { make $/ }
}
say Email.parse('foo.bar@baz.example.com', actions => Email-Action.new).made;

Мы всплываем дважды, поскольку знаем, что, по крайней мере, у нас есть TLD и домен; если что-то осталось, то попадает в поддомены. Это напечатает, для этого

say Email.parse('foo.bar@baz.example.com', actions => Email-Action.new).made;
say Email.parse('foo@example.com', actions => Email-Action.new).made;
say Email.parse('foo.bar.baz@quux.zuuz.example.com', actions => Email-Action.new).made;

Правильный ответ:

{domain => example, name => 「foo.bar」, subdomain => baz, tld => com}
{domain => example, name => 「foo」, tld => com}
{domain => example, name => 「foo.bar.baz」, subdomain => quux.zuuz, tld => com}

Грамматики невероятно эффективны, но также, с его поиском в глубину, довольно сложно отладить и обернуть ваш голова вокруг. Но если есть часть, которую можно отложить до действий, которая, кроме того, дает вам готовую структуру данных, почему бы не использовать ее? токен ведет себя иначе, чем правило, и правило ведет себя так, как если бы оно было регулярным выражением, без использования пробелов, а также с храповым механизмом. Я просто не знаю. Проблема в том, что в том, как вы сформулировали свою грамматику, после того, как она сожрала точку, она не вернет ее. Итак, либо вы каким-то образом включаете субдомен и домен в один токен, чтобы он соответствовал, либо вам понадобится среда без храповика, такая как регулярные выражения (и, ну, очевидно, правила тоже), чтобы заставить его работать. Учтите, что токен и регулярные выражения - это разные вещи. Они используют одни и те же обозначения и все такое, но его поведение совершенно другое. Я рекомендую вам использовать Grammar :: Tracer или среду тестирования грамматики в CommaIDE, чтобы проверить различия.

3 голосов
/ 28 мая 2020

Согласно Raku docs :

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

Не игнорируется означает, что они рассматриваются как синтаксис, а не совпадают буквально. Они фактически вставляют <.ws>. См. sigspace для получения дополнительной информации об этом.

...