Разбор необязательной точки с запятой в конце инструкции

Я писал парсер для синтаксического анализа грамматик типа C.

Во-первых, теперь он может разобрать код, например:

a = 1;
b = 2;

Теперь я хочу сделать точку с запятой в конце строки опциональной.

Исходное правило YACC:

stmt: expr ';' { ... }

Если новая строка обрабатывается лексером, написанным мной (код упрощен):

rule(/\r\n|\r|\n/)          { increase_lineno(); return :PASS }

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

Из-за этого я не могу просто изменить правило YACC на:

stmt: expr end_of_stmt { ... }
;
end_of_stmt: ';'
    | '\n'
;

Итак, я решил динамически изменять состояние лексера с помощью синтаксического анализатора.

Вот так:

stmt: expr { state = :STATEMENT_END } ';' { ... }

И добавьте правило lexer, которое может соответствовать новой строке с новым состоянием:

rule(/\r\n|\r|\n/, :STATEMENT_END) { increase_lineno(); state = nil; return ';' }

Это означает, что когда lexer находится в состоянии: STATEMENT_END. он сначала увеличит номер строки, как обычно, а затем установит состояние в начальное, а затем притворится, что это точка с запятой.

Странно, что на самом деле он не работает со следующим кодом:

a = 1
b = 2

Я отлаживал его и получал, что на самом деле не получается ';' как и ожидалось при сканировании новой строки после номера 1, и указанное правило не выполняется.

И код для установки нового состояния выполняется после того, как он уже отсканировал новую строку и ничего не вернул, это означает, что эти работы выполняются следующим образом:

  • scan a, = и 1
  • сканировать новую строку и пропустить, так что получите следующее значение b
  • выполняется вставленный код ({ state = :STATEMENT_END })
  • ошибка при поднятии - неожиданно b здесь

Это то, что я ожидаю:

  • scan a, = и 1
  • обнаружил, что он соответствует правилу expr, поэтому уменьшите до stmt
  • выполнить вставленный код, чтобы установить новое лексерское состояние
  • сканируйте новую строку и верните ; в соответствии с новым правилом сопоставления состояний
  • продолжить сканирование и анализ следующей строки

После интроспекции я обнаружил, что это может быть вызвано тем, что YACC использует LALR (1), этот синтаксический анализатор будет читать вперед для первого токена. Когда он сканирует туда, состояние еще не установлено, поэтому он не может получить правильный токен.

Мой вопрос: как заставить его работать должным образом? Я понятия не имею об этом.

Спасибо.

Ответ 1

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

a = b
-c;

Вы хотите рассматривать вышеописанное как одно выражение expr или как два отдельных оператора с первой точкой с запятой? Подобная потенциальная двусмысленность возникает с синтаксисом вызова функции на языке C-like:

a = b
(c);

Если вы хотите, чтобы они разрешались как два оператора, вы можете использовать подход, который вы пробовали; вам просто нужно установить состояние одного токена ранее. Это становится сложным, поскольку вы НЕ хотите устанавливать состояние, если у вас есть закрытая скобка, поэтому вам нужно дополнительное состояние var для записи глубины вложенности парсов и установить только состояние insert-semi-before-newline, когда это 0.

Если вы хотите разрешить вышеуказанные случаи как один оператор, все становится сложным, так как вам действительно нужно больше взглядов, чтобы решить, когда новая строка должна заканчивать утверждение - по крайней мере вам нужно посмотреть на токен ПОСЛЕ новой строки (и любые комментарии или другие игнорируемые материалы). В этом случае вы можете использовать lexer для дополнительного просмотра. Если бы вы использовали flex (которого вы, по-видимому, нет?), Я бы предложил либо использовать оператор / (который непосредственно смотрит), либо отложить возврат точки с запятой до тех пор, пока правило lexer не будет соответствовать следующему токену.

В общем, при выполнении такой записи состояния токена я считаю, что проще всего сделать это в лексере, где это возможно, поэтому вам не нужно беспокоиться о дополнительном знаке просмотра иногда (но не всегда) парсером. В этом конкретном случае простым подходом было бы иметь запись лексера в виде скобки (+1 для (, -1 для )) и последний возвращаемый токен. Затем в правиле новой строки, если уровень пэра равен 0, а последний токен - это то, что может привести к выражению (идентификатор или константа или ) или только постфиксный оператор), верните дополнительные ;

Альтернативный подход состоит в том, чтобы вернуть lexer NEWLINE в качестве своего собственного токена. Затем вы измените парсер, чтобы принять stmt: expr NEWLINE, а также дополнительные строки новой строки между большинством других токенов в грамматике. Это предоставляет неопределенность непосредственно парсеру (его теперь не LALR (1)), поэтому вам нужно разрешить его либо с помощью правил приоритета оператора yacc (сложный и подверженный ошибкам), либо используя что-то вроде опции bison %glr-parser или btacc backtracking способность справляться с двусмысленностью напрямую.

Ответ 2

То, что вы пытаетесь, безусловно, возможно.

Ruby, фактически, делает именно это, и у него есть парсер yacc. Операторы soft-terminate Newlines, точки с запятой являются необязательными, и операторы автоматически продолжаются на нескольких строках "если это необходимо".

Общение между анализатором и лексическим анализатором может быть необходимым, и да, устаревший yacc - LALR (1).

Я точно не знаю, как это делает Ruby. Моя догадка всегда заключалась в том, что она фактически не обменивается (а), но, скорее, lexer распознает конструкции, которые явно не закончены и молча обрабатывают новые строки как пробелы, пока баланс парнеров и скобок не будет сбалансирован. Он также должен заметить, когда строки заканчиваются двоичными операторами или запятыми, и они тоже есть эти строки.

Просто догадаться, но я считаю, что эта техника будет работать. И Ruby - это с открытым исходным кодом..., если вы хотите точно увидеть, как Matz сделал это.