Почему в некоторых случаях я не могу использовать токен в качестве маркера приоритета

Предположим, этот код работает:

left '*'
left '+'

expr: expr '+' expr
    | expr '*' expr
    ;

Я хочу определить другой маркер приоритета, например:

left MULTIPLY
left PLUS

expr: expr '+' expr %prec PLUS
    | expr '*' expr %prec MULTIPLY
    ;

Тем не менее, это на самом деле не эффективно.

Я полагаю, что эти две формы должны быть эквивалентны, однако это не так.

Это не практическая проблема. Я просто хочу знать причину и принцип этого явления.

Благодарю.

3 ответа

Решение

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

Я думаю, вы обнаружите, что вам не нужно часто использовать маркер приоритета. Как правило, читателю проще и понятнее переписать грамматику так, чтобы приоритет был явно учтен. Чтобы дать умножение и деление более высокого приоритета, чем сложение и вычитание, вы можете сделать что-то вроде этого (пример, адаптированный из John Levine, lex & yacc 2 / e, 1992):

%token NAME NUMBER

%%

stmt : NAME '=' expr
     | expr
     ;

expr : expr '+' term
     | expr '-' term
     | term
     ;

term : term '*' factor
     | term '/' factor
     | factor
     ;

factor : '(' expr ')'
       | '-' factor
       | NUMBER
       ;

В вашем примере PLUS а также MULTIPLY не настоящие токены; Вы не можете использовать их взаимозаменяемо с '+' а также '*', Левин называет их псевдо-токенами. Они там, чтобы связать ваши произведения обратно с вашим списком приоритетов, которые вы определили с %left а также %nonassoc деклараций. Он приводит этот пример того, как вы могли бы использовать %prec дать одинарный минус высокий приоритет, даже если токен '-' имеет низкий приоритет:

%token NAME NUMBER
%left '-' '+'
%left '*' '/'
%nonassoc UMINUS

%%

stmt : NAME '=' expr
     | expr
     ;

expr : expr '+' expr
     | expr '-' expr
     | expr '*' expr
     | expr '/' expr
     | '-' expr %prec UMINUS
     | '(' expr ')'
     | NUMBER
     ;

Подводя итог, я бы рекомендовал следовать шаблону моего первого примера кода, а не второго; сделать грамматику явной

Правила приоритета Yacc на самом деле не относятся к приоритету выражений, хотя они могут использоваться для этого. Вместо этого они являются способом разрешения конфликтов смещения / уменьшения (и ТОЛЬКО смещения / уменьшения конфликтов) явно.

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

Основная проблема с вышеизложенным (и тем, что решает весь механизм генератора синтаксического анализатора) состоит в том, чтобы знать, когда сокращать правило по сравнению с тем, когда сдвигать токен, если оба возможны. Генератор синтаксического анализатора (yacc или bison) создает конечный автомат, который отслеживает, какие символы были сдвинуты, и поэтому знает, какие "частично совпадающие" правила в настоящее время возможны, и ограничивает сдвиги только теми токенами, которые могут соответствовать большему количеству такого правила. Это не работает, если рассматриваемая грамматика не является LALR(1), и поэтому в таких случаях отчеты yacc/bsion сдвигают / уменьшают или уменьшают / уменьшают конфликты.

Правила приоритета работают для разрешения конфликтов уменьшения сдвига, назначая приоритет определенным токенам и правилам в грамматике. Всякий раз, когда возникает конфликт сдвига / уменьшения между токеном, подлежащим сдвигу, и правилом, подлежащим сокращению, и ОБА имеют приоритет, он будет делать то, что имеет более высокий приоритет. Если они имеют тот же приоритет, то он смотрит на %left/%right/%nonassoc флаг, связанный с уровнем приоритета - %left значит уменьшить, %right означает сдвиг, и %nonassoc означает, что не делайте ни одного, и рассматривайте это как синтаксическую ошибку.

Единственный хитрый оставшийся бит - как токены и правила получают свой приоритет. Токены получают их от %left/%right/%nonassoc директива, в которой они находятся, которая также устанавливает порядок. Правила получают приоритет от %prec директива ИЛИ от самого правого терминала на их правой стороне. Итак, когда у вас есть:

%left '*'
%left '+'

expr: expr '+' expr
    | expr '*' expr
    ;

Вы устанавливаете приоритет '*' а также '+' с %left директивы, и эти два правила получают их приоритет от этих токенов.

Когда у тебя есть:

%left MULTIPLY
%left PLUS

expr: expr '+' expr %prec PLUS
    | expr '*' expr %prec MULTIPLY
    ;

Вы устанавливаете приоритет токенов MULTIPLY а также PLUS а затем явно установить правила, чтобы иметь эти приоритеты. Однако вы НЕ УСТАНАВЛИВАЕТЕ ЛЮБОЙ ПРЕЦЕДЕНТНОСТИ для токенов '*' а также '+', Таким образом, когда есть сдвиг / уменьшение конфликта между одним из двух правил и либо '*' или же '+', приоритет не разрешает это, потому что токен не имеет приоритета.

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

expr: expr '+' expr
    | expr '*' expr
    | expr '*' '*' expr

Причина, по которой все это сбивает с толку, заключается в том, что способ, которым Bison отдает приоритет правилу "уменьшить", заключается в том, чтобы связать его с токеном (последний терминал в правиле по умолчанию или токен из предварительного объявления), а затем он использует приоритет таблица для сравнения этого токена с токеном, который вы пытаетесь сдвинуть. По сути, предварительные декларации имеют смысл только для "уменьшенной" части конфликта, и они не учитываются для сменной части.

Один из способов увидеть это с помощью следующей грамматики

command: IF '(' expr ')' command               %prec NOELSE
       : IF '(' expr ')' command ELSE command

В этой грамматике вам нужно выбрать между уменьшением первого правила или смещением токена ELSE. Вы делаете это либо путем предоставления приоритетов токену ')' и токену ELSE, либо используя предварительное объявление и давая приоритет для NOELSE вместо ')'. Если вы попытаетесь дать предварительное объявление второму, оно будет проигнорировано, и Бизон продолжит поиск приоритета токена ELSE в таблице приоритетов.

Другие вопросы по тегам