Синтаксически-управляемый компилятор

Ранее был приведен пример использования генераторов лексических и синтаксических анализаторов LEX и YACC для реализации компилятора, почти без пояснений. Входной язык даже не предоставлял возможности написания функций, но выражения и условия могли быть сколь угодно сложными.

Здесь приведены несколько вариантов синтаксически-управляемых компиляторов языка Tiny Context, возможно, немного измененного, но никаких сторонних инструменов не используется:

Большая часть пояснений находится на этой странице.

Как и раньше компилируемая программа должна размещаться в файле с именем c.prg, результат компиляции записывается в файл c.com. Описание грамматики языка должно размещаться в файле c.def. Если DOS недоступен, можно использовать эмулятор DOSBox.

Первая компиляции приведенных примеров выполняется почти также. Проще всего использовать Tiny Context 1.18 (c.118), для его сборки необходимо выполнить командный файл c.bat. Компиляция с помощью DOS-версии Context'a тоже возможна, но она потребует нескольких изменений в исходном тесте:

Добавить в начало текста объявление массива Temp (это связано с различиями в распределении памяти):

char Temp [16640]; // Перед char Text [16384];

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

word Digraph(char C1, char C2) is -> word Digraph(char C1; char C2) is

Приведенные компиляторы могут компилировать друг друга (и себя), но нужно использовать соответствующий им файл описания грамматики языка (c.def).

Как и в Tiny Context параметры функций и локальные переменные размещаются в фиксированных областях памяти, но здесь это не создает никаких проблем, поскольку рекурсия не используется.

Если в исходной реализации правила языка лишь подразумевались, в этих они используются явно. Для записи правил используется нормальная форма Бэкуса. Исходный вариант грамматики приведен ниже. В первой части перечислены лексемы (примерный аналог директив YACC %token), во второй - правила. Лексеме ставятся в соответствие код вида лексемы и значение. Ненулевой код используется чтобы отличить имена типов от всех прочих лексем, значение содержит код операции или размер объекта указанного типа в байтах. Привилу ставится в соответствие код действия, которое дожно быть выполнено при его применении (в YACC вместо этого можно написать фрагмент программы на языке Си). Коды действий будут назначены позже.

symb ; numb ; name ; lsb "[" 0 0; rsb "]" 0 0; lcb "(" 0 0; rcb ")" 0 0; assign ":=" 0 0; plus "+" 0 0; minus "-" 0 1; star "*" 0 2; slash "/" 0 3; pct "%" 0 4; lt "<" 0 0; le "<=" 0 1; eq "=" 0 2; ne "!=" 0 3; ge ">=" 0 4; gt ">" 0 5; comma "," 0 0; semi ";" 0 0; is 0 0; begin 0 0; if 0 0; then 0 0; while 0 0; do 0 0; inline 0 0; return 0 0; end 0 0; char 1 1; byte 1 1; word 1 2. Program : 0 Declarations Block | 0 Block ; Declarations : 0 Declarations Declaration | 0 Declaration ; Block : 0 Main Stmts end ; Declaration : 0 TypeName semi | 0 TypeName lsb numb rsb semi | 0 Header Stmts end ; Header : 0 TypeName lcb rcb is | 0 TypeName lcb Args rcb is ; Args : 0 Args comma Arg | 0 Arg ; Arg : 0 TypeName ; TypeName : 0 Type name ; Type : 0 char | 0 byte | 0 word ; Main : 0 begin ; Stmts : 0 Stmts Stmt | 0 Stmt ; Stmt : 0 IfBlk | 0 WhileBlk | 0 Inlines | 0 Ret | 0 Local | 0 Assign | 0 PCall ; IfBlk : 0 IfHdr Stmts end ; IfHdr : 0 if Cond then ; WhileBlk : 0 WhileHdr Stmts end ; WhileHdr : 0 Loop Cond do ; Loop : 0 while ; Cond : 0 Expr RelOp Expr ; Inlines : 0 inline OpCodes semi ; OpCodes : 0 OpCodes comma OpCode | 0 OpCode ; OpCode : 0 numb ; Ret : 0 return Expr semi ; Local : 0 LocalVar assign Expr semi | 0 LocalVar semi ; LocalVar : 0 TypeName ; Assign : 0 Ref assign Expr semi ; PCall : 0 Call semi ; Expr : 0 Expr Op1 Term | 0 Term ; Term : 0 Term Op2 Value | 0 Value ; Value : 0 symb | 0 numb | 0 Ref | 0 Call | 0 Cast | 0 lcb Expr rcb ; Ref : 0 Name | 0 Name lsb Expr rsb ; Call : 0 Name lcb Params rcb | 0 Name lcb rcb ; Name : 0 name ; Params : 0 Params comma Param | 0 Param ; Param : 0 Expr ; Cast : 0 Type lcb Expr rcb ; Op1 : 0 plus | 0 minus ; Op2 : 0 star | 0 slash | 0 pct ; RelOp : 0 lt | 0 le | 0 eq | 0 ne | 0 ge | 0 gt .

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

Main : 0 begin ;

или

Loop : 0 while ;

Они нужны потому, что при распознавании символов begin и while требуется запоминать текущий адрес чтобы позже выполнить безусловный переход к нему.

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

begin word name assign numb semi end

word name semi begin name assign numb semi end

begin name assign numb semi end

С грамматикой связан ряд вопросов, в том числе вопросы об

По-видимому, однозначность является необходимым свойством, но она не обязательно достигается средствами грамматики. Если заменить правило для Expr следующим (Op - любой арифметический оператор)

Expr : 0 Expr Op Expr | 0 Value ;

Будут потеряны не только приоритеты операций, но и однозначность. Чтобы доказать факт неоднозначности достаточно привести один пример. Выражению

3 - 2 + 1

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

(3 - 2) + 1 // (Expr - Expr) + Expr

и

3 - (2 + 1) // Expr - (Expr + Expr)

Будет рассмотрен только синтаксический анализатор (и примитивный генератор кода). Лексический анализатор (сканер) ввиду его простоты будет написан вручную (будет использован ранее написанный с небольшими изменениями).

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

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

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

Каждому встретившемуся в описании символу (терминальному символу, т.е. лексеме, нетерминальному символу, т.е. имени в левой части правила, но не символу алфавита) будет назначен уникальный числовой идентификатор, это позволит заменить все сравнения строк сравнениями чисел. Соответственно сканер должен возвращать идентификатор, он будет определяться поиском в таблице символов.

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

Ref : 0 Name | 0 Name lsb Expr rsb ;

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

Declarations : 0 Declarations Declaration | 0 Declaration ;

Здесь применение второго правила при возможности применения первого приведет к ошибке, поскольку последовательность символов

Declarations Declarations

уже ни во что преобразована быть не может.

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

type // Тип var // Переменная array // Массив fn // Функция

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

Ref : 0 var | 0 array lsb Expr rsb ;

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

Param : 410 symb comma | 410 numb comma | 420 Ref comma | 430 Call comma | 430 Cast comma | 430 Expr comma ;

Это увеличивает объем грамматики и запутывает ее, но все же может быть сделано. Пример приведен здесь.

Исключить совпадения правил можно явно включив в них допустимые предществующие символы. Например, если вместо правила

Value : 0 numb ;

ввести набор правил

Value : 0 if < numb | 0 Loop < numb | 0 assign < numb | 0 return < numb | 0 array lsb < numb | 0 lcb < numb | 0 Op < numb | 0 RelOp < numb | 0 Params < numb ;

можно исключить преобразование numb в Value при разборе определения массива. Пример приведен здесь. Ясно, что многократное повторение одних и тех же правил это плохо, но может быть добавит некоторое понимание вопроса. Ниже будет показано, как анализатор может разобраться с предшествующими символами самостоятельно.

Этот пример был получен из расмотренного ниже SLR(0)-анализатора.

По крайней мере теоретически возможен прямой перебор всех сверток. Практически это осуществимо лишь для очень коротких программ.

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

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

Program : 0 Declarations Block | 0 Block ;

Символы Declarations и Block могут возникнуть только в результате применения одного из следующих правил

Declarations : 0 Declarations Declaration | 0 Declaration ; Block : 0 Main Stmts end ;

Символ Declarations уже принят во внимание и нового ничего не даст, а символы Declaration и Main могут возникнуть в результате применения одного из следующих правил

Declaration : 0 TypeName semi | 0 TypeName lsb numb rsb semi | 0 Header Stmts end ; Main : 0 begin ;

Символы TypeName и Header могут возникнуть в результате применения одного из следующих правил

TypeName : 0 Type name ; Header : 0 TypeName lcb rcb is | 0 TypeName lcb Args rcb is ;

Символ TypeName уже принят во внимание, а вновь встретившийся символ Type может возникнуть в результате применения одного из следующих правил

Type : 0 char | 0 byte | 0 word ;

Более никаких новых нетерминальных символов не встретилось, соответственно добавить новые правила невозможно. Вот полный список найденных правил

Program : 0 Declarations Block | 0 Block ; Declarations : 0 Declarations Declaration | 0 Declaration ; Block : 0 Main Stmts end ; Declaration : 0 TypeName semi | 0 TypeName lsb numb rsb semi | 0 Header Stmts end ; Main : 0 begin ; TypeName : 0 Type name ; Header : 0 TypeName lcb rcb is | 0 TypeName lcb Args rcb is ; Type : 0 char | 0 byte | 0 word ;

Четыре правила начинаются с терминальных символов, соответственно корректная программа может только с одного из них, т.е. с char, byte, word или begin. Допустим, встретился символ char. Он однозначно определяет, что должно быть применено правило

Type : 0 char ;

Аналогично, символ Type однозначно определяет, что должно быть применено правило

TypeName : 0 Type name ;

но его применение возможно только если после Type встретится символ name.

Символ TypeName не определяет однозначно допустимое правило, но определяет множество допустимых правил

Declaration : 0 TypeName semi | 0 TypeName lsb numb rsb semi ; Header : 0 TypeName lcb rcb is | 0 TypeName lcb Args rcb is ;

Чтобы отразить тот факт, что символ TypeName уже просмотрен используется следующая нотация:

Declaration : 0 TypeName . semi | 0 TypeName . lsb numb rsb semi ; Header : 0 TypeName . lcb rcb is | 0 TypeName . lcb Args rcb is ;

Точки указывают позиции в правилах. В правиле из N символов возможна N+1 позиция. Позиции называются пунктами, множество равновозможных пунктов называется состоянием.

Если далее встретится символ semi или символ lsb, допустимым останется лишь одно из правил. Если же встретится символ lcb, допустимых правил останется два, но следующий символ позволит выбрать одно из них:

Header : 0 TypeName lcb . rcb is | 0 TypeName lcb . Args rcb is ;

Для этого придется разобраться с нетерминальным символом Args примерно также как это было сделано выше с символом Declarations.

Здесь нужно заметить, что для заголовка функции без параметров введено отдельное правило. Альтернатива этому - введение пустого правила для Args

Args : 0 Args comma Arg | 0 Arg | 0 ;

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

К сожалению, применение такого подхода также не позволяет работать с исходным вариантом грамматики. Но чтобы сделать его работоспособным изменений потребуется уже немного. Для начала все операторы в выражениях будут иметь одинаковый приоритет, позже это будет исправлено и это не потребует больших усилий. Измененная грамматика и компилятор приведены здесь. Это SLR(0)-анализатор.

Более или менее точный учет символов, могущих встретиться после заданного символа позволяет реализовать анализатор реальных языков программирования не прибегая к существенному усложнению грамматики. Для некоторых грамматик, в том числе и для рассматриваемой, чтобы выбрать между переносом и сверткой достаточно учитывать следующий терминальный символ без учета контекста. Свертка по правилу выполняется если после символа в левой части этого правила может встречаться символ, который будет прочитан следующим. Например в части грамматики, определяющей арифметических выражений с приоритетами

Expr : 0 Expr Op1 Term | 0 Term ; Term : 0 Term Op2 Value | 0 Value ; Op1 : 0 plus | 0 minus ; Op2 : 0 star | 0 slash | 0 pct ;

одно из возможных состояний следующее

Expr : 0 Term . ; Term : 0 Term . Op2 Value ; Op2 : 0 . star | 0 . slash | 0 . pct ;

С точки зрения SLR(0)-анализатора имеет место конфликт между переносом и сверткой. Но в рассматриваемой грамматике после Expr могут встречаться только символы plus, minus, lt, le, eq, ne, ge, gt, rsb, rcb, then, do, comma и semi. Соответственно, если следующий терминальный символ один из перечисленных, перенос приведет к пустому состоянию и следует выполнить свертку Term в Expr. Если же следующий символ star, slash или pct, которые после Expr встречаться не могут, следует выполнить перенос (затем свертку в Op2 и еще один перенос). Если следующий символ отличается от всех перечисленных, анализируемый текст не соответствует грамматике. Это SLR(1)-анализатор. Его реализация требует совсем небольших изменений, они приведены здесь.

Чтобы уяснить принцип его работы имеет смысл рассмотреть грамматику выражений без приоритетов операций и с приоритетами отдельно от всего остального и изобразить на бумаге все возможные состояния анализатора и все символы, которые могут встретиться. Чтобы отделить эту часть грамматики нужно заменить символ Value на терминальный символ numb и добавить в начало еще одно правило (вместо Program):

Result : 0 Expr semi ;

Можно построить грамматики, причем однозначные, для которых такой анализ не устраняет конфликтов. Для них SLR(1)-анализатор непригоден, но для некоторых из них можно постоить анализатор, учитывающий зависимость допустимых последующих символов от предшествующего контекста.

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

Program : 0 . Declarations Block | 0 . Block ;

Символ Declarations может возникнуть только в результате применения одного из следующих правил

Declarations : 0 . Declarations Declaration | 0 . Declaration ;

Кроме этого, и это самое важное, последующая свертка в Program не будет возможна, если после Declarations не встретится начальный символ Block (т.е. begin). Добавим к списку возможных в начале программы правил эти два и в фигурных скобках укажем, что после них ожидается символ begin:

Declarations : 0 . Declarations Declaration { begin } | 0 . Declaration { begin } ;

Вообще-то после Declarations могут встречаться и другие символы, но из одного первого правила грамматики это не следует.

Далее, символ Block может возникнуть только в результате применения правила

Block : 0 . Main Stmts end ;

Что может встетится после Block? Ответ может быть или ничего (достигнут конец файла) или любой, в том числе и отсутствующий в языке, символ. Пока вместо допустимого символа или символов поставим знак вопроса

Block : 0 . Main Stmts end {?} ;

и продолжим поиск возможных правил и следующих за ними символов. Рассмотрим первое из добавленных правил

Declarations : 0 . Declarations Declaration { begin } ;

Применение этого правила не будет возможно, если после Declarations не встретится начальный символ Declaration (т.е. char, byte или word). Добавим в список определения Declarations (с ожидаемыми следующими символами):

Declarations : 0 . Declarations Declaration { char } | 0 . Declarations Declaration { byte } | 0 . Declarations Declaration { word } | 0 . Declaration { char } | 0 . Declaration { byte } | 0 . Declaration { word } ;

Второе определяющее Declarations правило

Declarations : 0 . Declaration { begin } ;

состоит из одного символа Declaration, который определяется тремя правилами:

Declaration : 0 TypeName semi | 0 TypeName lsb numb rsb semi | 0 Header Stmts end ;

После Declaration ожидается символ begin, так что и после любого из этих трех правил ожидается он же:

Declaration : 0 . TypeName semi { begin } | 0 . TypeName lsb numb rsb semi { begin } | 0 . Header Stmts end { begin } ;

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

Program : 0 . Declarations Block {#} | 0 . Block {#} ;

и

Block : 0 . Main Stmts end {#} ;

Здесь символ # использется для обозначения конца файла.

Подолжая этот процесс и исключая повторы получим замыкание множества пунктов начального состояния анализатора:

Program : 0 . Declarations Block { # } | 0 . Block { # } ; Declarations : 0 . Declarations Declaration { begin } | 0 . Declaration { begin } ; Block : 0 . Main Stmts end { # } ; Declarations : 0 . Declarations Declaration { char } | 0 . Declarations Declaration { byte } | 0 . Declarations Declaration { word } | 0 . Declaration { char } | 0 . Declaration { byte } | 0 . Declaration { word } ; Declaration : 0 . TypeName semi { begin } | 0 . TypeName lsb numb rsb semi { begin } | 0 . Header Stmts end { begin } ; Main : 0 . begin { inline } | 0 . begin { return } | 0 . begin { if } | 0 . begin { while } | 0 . begin { name } | 0 . begin { char } | 0 . begin { byte } | 0 . begin { word } ; Declaration : 0 . TypeName semi { char } : 0 . TypeName lsb numb rsb semi { char } : 0 . Header Stmts end { char } : 0 . TypeName semi { byte } : 0 . TypeName lsb numb rsb semi { byte } : 0 . Header Stmts end { byte } : 0 . TypeName semi { word } : 0 . TypeName lsb numb rsb semi { word } : 0 . Header Stmts end { word } ; TypeName : 0 . Type name { semi } | 0 . Type name { lsb } ; Header : 0 . TypeName lcb rcb is { inline } | 0 . TypeName lcb rcb is { return } | 0 . TypeName lcb rcb is { if } | 0 . TypeName lcb rcb is { while } | 0 . TypeName lcb rcb is { name } | 0 . TypeName lcb rcb is { char } | 0 . TypeName lcb rcb is { byte } | 0 . TypeName lcb rcb is { word } | 0 . TypeName lcb Args rcb is { inline } | 0 . TypeName lcb Args rcb is { return } | 0 . TypeName lcb Args rcb is { if } | 0 . TypeName lcb Args rcb is { while } | 0 . TypeName lcb Args rcb is { name } | 0 . TypeName lcb Args rcb is { char } | 0 . TypeName lcb Args rcb is { byte } | 0 . TypeName lcb Args rcb is { word } ; Type : 0 . char { name } | 0 . byte { name } | 0 . word { name } ; TypeName : 0 . Type name { lcb } ;

Четыре правила (одно из которых повторено восемь раз) начинаются с терминальных символов, соответственно корректная программа может начинаться только с одного из них, т.е. с char, byte, word или begin. Допустим, встретился символ char. Он однозначно определяет подходящее правило

Type : 0 . char { name } ;

Перенос символа char в стек приводит к состоянию из одного пункта:

Type : 0 char . { name } ;

Если следующий символ name, то выпоняется свертка char в Type, затем перенос Type в стек, если нет - дальнейшие действия бессмысленны - текст не соответствует грамматике. Таким образом придем к состоянию анализатора из одного повторенного три раза пункта:

TypeName : 0 Type . name { semi } | 0 Type . name { lsb } | 0 Type . name { lcb } ;

Перенос символа name в стек приведет к следующему состянию:

TypeName : 0 Type name . { semi } | 0 Type name . { lsb } | 0 Type name . { lcb } ;

Если следующий символ semi, lsb или lcb, то выпоняется свертка в TypeName и перенос TypeName в стек, иначе фиксируется ошибка - никаких иных символов после Typename быть не должно. При нормальном ходе дела придем к состянию, четыре пункта которого повторены в общей сложности двадцать четыре раза:

Declaration : 0 TypeName . semi { begin } | 0 TypeName . lsb numb rsb semi { begin } | 0 TypeName . semi { char } | 0 TypeName . lsb numb rsb semi { char } | 0 TypeName . semi { byte } | 0 TypeName . lsb numb rsb semi { byte } | 0 TypeName . semi { word } | 0 TypeName . lsb numb rsb semi { word } ; Header : 0 TypeName . lcb rcb is { inline } | 0 TypeName . lcb rcb is { return } | 0 TypeName . lcb rcb is { if } | 0 TypeName . lcb rcb is { while } | 0 TypeName . lcb rcb is { name } | 0 TypeName . lcb rcb is { char } | 0 TypeName . lcb rcb is { byte } | 0 TypeName . lcb rcb is { word } | 0 TypeName . lcb Args rcb is { inline } | 0 TypeName . lcb Args rcb is { return } | 0 TypeName . lcb Args rcb is { if } | 0 TypeName . lcb Args rcb is { while } | 0 TypeName . lcb Args rcb is { name } | 0 TypeName . lcb Args rcb is { char } | 0 TypeName . lcb Args rcb is { byte } | 0 TypeName . lcb Args rcb is { word } ;

Если далее встретится символ semi, перенос его в стек приведет к состоянию из одного пункта, повторенного четыре раза:

Declaration : 0 TypeName semi . { begin } | 0 TypeName semi . { char } | 0 TypeName semi . { byte } | 0 TypeName semi . { word } ;

Если следующий символ begin, char, byte или word, то выпоняется свертка в Declaration, иначе фиксируется ошибка.

Продолжая процесс получим в итоге символ Program или зафиксируем ошибку. Это LR(1)-анализатор. Его реализация требует изменения функций построения замыкания, и анализа возможности свертки (последняя сводится к сравнению реального и ожидаемого символов), они приведены здесь.

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

Также нужно заметить, что в программе выражения могут встечаться в разных контекстах и множества могущих следовать за ними символов в разных контекстах различны. Например, если выражение всречается в левой части сравнения в заголовке цикла, множество допустимых следующих символов состоит из шести символов операций сравнения, если же в правой - из одного символа do.

Для рассматриваемой грамматики эти действия избыточны. Кроме того, для хранения информации о состояниях анализатора требуется довольно много памяти, что может затруднить реализацию на машине с небольшой оперативной памятью (т.е. на восьми- или шестнадцатиразрядной машине с 64 килобайтами памяти). Можно уменьшить избыточность LR(1)-анализатора, несколько снизив при этом его общность (LALR(1)-анализатор). При наличии гигабайтов памяти эта проблема несущественна, но LALR(1)-анализаторы YACC/BISON пока никуда не исчезли.

В принципе возможно включить в грамматику правила образования слов языка и исключить лексический анализатор (вместо него потребуется всего лишь функция Read(), возвращающая при каждом вызове слежующий символ текста программы). Но LR(1)-анализатор для этого не очень приспособлен. Cледующие правила приводят к конфликту между переносом и сверткой:

if : 0 'i' 'f' ; inline : 0 'i' 'n' 'l' 'i' 'n' 'e' ; is : 0 'i' 's' ; name : 0 name 'a'-'z' | 0 'a'-'z' ;

Диапазоны символов ('a'-'z') использованы для сокращения записи, рассмотренные анализаторы не могут их прочесть, так что придется либо повторить каждое такое правило для каждого из символов диапазона, либо изменить загрузчик грамматики.

После считывания символа 'i' и следующего за ним символа 'f', 'n' или 's' невозможно выбрать между переносом и сверткой в name, для принятия решения нужно смотреть более чем один следующий символ. Это содержательный пример не-LR(1)-грамматики.

Конфликт исчезнет если преобразовать грамматику как показано ниже:

i : 0 'i' ; if : 0 i 'f' ; in : 0 i 'n' ; inl : 0 in 'l' ; inli : 0 inl 'i' ; inlin : 0 inli 'n' ; inline : 0 inlin 'e' ; is : 0 i 's' ; name : 0 temp | 0 i | 0 in | 0 inl | 0 inli | 0 inlin ; temp : 0 temp 'a'-'z' | 0 i 'a'-'e' | 0 i 'g'-'m' | 0 i 'o'-'r' | 0 i 't'-'z' | 0 if 'a'-'z' | 0 in 'a'-'k' | 0 in 'm'-'z' | 0 inl 'a'-'h' | 0 inl 'j'-'z' | 0 inli 'a'-'m' | 0 inli 'o'-'z' | 0 inlin 'a'-'d' | 0 inlin 'f'-'z' | 0 inline 'a'-'z' | 0 is 'a'-'z' | 0 'a'-'h' | 0 'j'-'z' ;

Новый символ грамматики temp не может начинаться с символа 'i', равно как и с любого другого символа, с которого начинается слово языка. Это тоже надо явно написать, так что последние два правила должны быть заменены. Также должны быть добавлены правила, содержащие прописные буквы, цифры и, возможно, другие символы.

Имена вроде inli выглядят странно, но формально допустимы. Если есть желание, можно их запретить и этим немного уменьшить число правил.

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

Сайт создан в системе uCoz