Раку: эффект маркеров захвата теряется «выше»

Aug 15 2020

Следующий сценарий Раку:

#!/usr/bin/env raku
use v6.d;

grammar MyGrammar
{
    rule TOP { <keyword> '=' <value> }
    token keyword { \w+ }
    token value { <strvalue> | <numvalue> }
    token strvalue { '"' <( <-["]>* )> '"' }
    token numvalue { '-'? \d+ [ '.' \d* ]? }
}

say MyGrammar.parse('foo = 42');
say MyGrammar.parse('bar = "Hello, World!"');

имеет следующий вывод:

「foo = 42」
 keyword => 「foo」
 value => 「42」
  numvalue => 「42」
「bar = "Hello, World!"」
 keyword => 「bar」
 value => 「"Hello, World!"」
  strvalue => 「Hello, World!」

Для второго пункта, заметим , что strvalueсодержит значение строки без кавычек, как и предполагалось с рынками захвата <(... )>. Однако, к моему удивлению, котировки будут включены в value.

Это можно обойти?

Ответы

6 raiph Aug 15 2020 at 22:30

TL; DR Используйте "множественную отправку". [1,2] См. Ответ @ user0721090601 для подробного объяснения того, почему все так, как есть. См. @ P6steve для действительно умного изменения вашей грамматики, если вы хотите, чтобы синтаксис вашего числа соответствовал синтаксису Raku.

Решение для множественной отправки

Это можно обойти?

Один из способов - перейти на явную множественную отправку.

В настоящее время у вас есть valueтокен, который вызывает варианты значений с конкретными именами:

    token value { <strvalue> | <numvalue> }

Замените это на:

    proto token value {*}

а затем переименуйте вызываемые токены в соответствии с правилами таргетинга на множественную отправку грамматики, чтобы грамматика стала такой:

grammar MyGrammar
{
    rule TOP { <keyword> '=' <value> }
    token keyword { \w+ }
    proto token value {*}
    token value:str { '"' <( <-["]>* )> '"' }
    token value:num { '-'? \d+ [ '.' \d* ]? }
}

say MyGrammar.parse('foo = 42');
say MyGrammar.parse('bar = "Hello, World!"');

Это отображает:

「foo = 42」
 keyword => 「foo」
 value => 「42」
「bar = "Hello, World!"」
 keyword => 「bar」
 value => 「Hello, World!」

По умолчанию это не фиксирует отдельные изменения. Мы можем придерживаться принципа «множественная отправка», но вновь ввести именование подзахватов:

grammar MyGrammar
{
    rule TOP { <keyword> '=' <value> }
    token keyword { \w+ }
    proto token value { * }
    token value:str { '"' <( $<strvalue>=(<-["]>*) )> '"' } token value:num { $<numvalue>=('-'? \d+ [ '.' \d* ]?) }
}

say MyGrammar.parse('foo = 42');
say MyGrammar.parse('bar = "Hello, World!"');

отображает:

「foo = 42」
 keyword => 「foo」
 value => 「42」
  numvalue => 「42」
「bar = "Hello, World!"」
 keyword => 「bar」
 value => 「Hello, World!」
  strvalue => 「Hello, World!」

Сюрпризы

к моему удивлению, цитаты включены в value.

Я тоже изначально был удивлен. [3]

Но текущее поведение также имеет для меня смысл по крайней мере в следующих смыслах:

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

  • Было бы неудивительно, если бы я ожидал этого, что, я думаю, вполне мог бы сделать при каких-то других обстоятельствах;

  • Нелегко увидеть, как можно было бы получить текущее поведение, если бы оно было желательным, но вместо этого работало так, как вы (и я) изначально ожидали;

  • Есть решение, как описано выше.

Сноски

[1] Использование множественной диспетчеризации [2] являетсярешением, но кажется слишком сложными имоучетом исходной задачей. Возможно, есть более простое решение. Возможно, кто-то предоставит это в другом ответе на ваш вопрос. Если нет, я надеюсь, что однажды у нас будет хотя бы одно гораздо более простое решение. Однако я не удивлюсь, если мы его не получим в течение многих лет. У нас есть решение, описанное выше, и есть еще много дел.

[2] Хотя вы можете объявить, скажем,method value:foo { ... }и написать метод (при условии, что каждый такой метод возвращает соответствующий объект), я не думаю, что Rakudo использует обычный механизм диспетчеризации нескольких методов для отправки к изменениям правил без методов, а вместо этого использует NFA .

[3] Некоторые могут возразить, что это «должно», «могло» или «было бы» «к лучшему», если бы Раку поступил так, как мы ожидали. Я считаю, что лучше всего буду избегать [sh | c | w] сообщений об ошибках / функциях, если только я не готов принять во внимание любые недостатки, которые другие поднимают, и готов помочь в работе, необходимой для того, чтобы все сделано. Поэтому я просто скажу, что в настоящее время я рассматриваю это как 10% ошибку, 90% функцию, но "могу" перейти на 100% ошибку или 100% функцию в зависимости от того, хочу ли я такое поведение или нет в данном сценарии и в зависимости от того, что думают другие.

6 user0721090601 Aug 15 2020 at 22:46

Эти <(и )>маркеры захвата работают только в пределах заданного данный маркер. По сути, каждый токен возвращает Matchобъект, который говорит: «Я сопоставил исходную строку от индекса X ( .from) до индекса Y ( .to)», что учитывается при преобразовании Matchобъектов в строку . Вот что происходит с вашим токеном strvalue:

my $text = 'bar = "Hello, World!"'; my $m = MyGrammar.parse: $text; my $start = $m<value><strvalue>.from; # 7 my $end   = $m<value><strvalue>.to; # 20 say $text.substr: $start, $end - $start;  # Hello, World!

Вы заметите, что есть только два числа: начальное и конечное значение. Это означает, что когда вы смотрите на valueимеющийся у вас токен, он не может создать несмежное совпадение. Итак, он .fromустановлен на 6, а его .toна 21.

Есть два способа обойти это: используя (а) объект действий или (б) мультитокен. Оба имеют свои преимущества, и в зависимости от того, как вы хотите использовать это в более крупном проекте, вы можете выбрать один или другой.

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

class MyActions { 
  method TOP      ($/) { make $<keyword>.made => $<value>.made }
  method keyword  ($/) { make ~$/ }
  method value    ($/) { make ($<numvalue> // $<strvalue>).made } method numvalue ($/) { make +$/ } method strvalue ($/) { make ~$/ }
}

Каждый уровень makeдля передачи значений до любого токена, который его включает. И включающий токен имеет доступ к своим значениям через .madeметод. Это действительно приятно, когда вместо работы с чистыми строковыми значениями вы хотите сначала каким-то образом обработать их и создать объект или подобное.

Чтобы разобрать, вы просто делаете:

my $m = MyGrammar.parse: $text, :actions(MyActions); say $m.made; # bar => Hello, World!

Что на самом деле является Pairобъектом. Вы можете изменить точный результат, изменив TOPметод.

Второй способ обойти ситуацию - использовать multi token. При разработке грамматики довольно часто используется что-то вроде

token foo { <option-A> | <option-B> }

Но, как вы можете видеть из класса действий, он требует, чтобы мы проверили и увидели, какой из них действительно соответствует. Вместо этого, если чередование приемлемо |, вы можете использовать мультивитокен:

proto token foo { * }
multi token:sym<A> { ... }
multi token:sym<B> { ... }

Когда вы используете его <foo>в своей грамматике, он будет соответствовать любой из двух мультиверсий, как если бы он был в базовой линии <foo>. Еще лучше, если вы используете класс действий, вы можете точно так же просто использовать $<foo>и знать его без каких-либо условий или других проверок.

В вашем случае это будет выглядеть так:

grammar MyGrammar
{
    rule TOP { <keyword> '=' <value> }
    token keyword { \w+ }
    proto token value { * }
    multi token value:sym<str> { '"' <( <-["]>* )> '"' }
    multi token value:sym<num> { '-'? \d+ [ '.' \d* ]? }
}

Теперь мы можем получить доступ к вещам, как вы изначально ожидали, без использования объекта действий:

my $text = 'bar = "Hello, World!"';
my $m = MyGrammar.parse: $text;

say $m; # 「bar = "Hello, World!"」 # keyword => 「bar」 # value => 「Hello, World!」 say $m<value>; # 「Hello, World!」

Для справки, вы можете комбинировать оба метода. Вот как бы я теперь написал объект действий с учетом мульти-токена:

class MyActions { 
  method TOP            ($/) { make $<keyword>.made => $<value>.made } method keyword ($/) { make ~$/ } method value:sym<str> ($/) { make ~$/ } method value:sym<num> ($/) { make +$/ }
}

Что на первый взгляд кажется немного более привлекательным.

2 p6steve Aug 16 2020 at 03:13

Вместо того, чтобы использовать собственное значение токена: str и значение токена: num, вы можете использовать логическую проверку Regex для соответствия Num (+) и Str (~) - как объяснено мне здесь и задокументировано здесь

token number { \S+ <?{ defined +"$/" }> } token string { \S+ <?{ defined ~"$/" }> }